|
- /*
- ==============================================================================
-
- This file is part of the JUCE examples.
- Copyright (c) 2022 - Raw Material Software Limited
-
- The code included in this file is provided under the terms of the ISC license
- http://www.isc.org/downloads/software-support-policy/isc-license. Permission
- To use, copy, modify, and/or distribute this software for any purpose with or
- without fee is hereby granted provided that the above copyright notice and
- this permission notice appear in all copies.
-
- THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
- WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
- PURPOSE, ARE DISCLAIMED.
-
- ==============================================================================
- */
-
- /*******************************************************************************
- The block below describes the properties of this PIP. A PIP is a short snippet
- of code that can be read by the Projucer and used to generate a JUCE project.
-
- BEGIN_JUCE_PIP_METADATA
-
- name: AudioPlaybackDemo
- version: 1.0.0
- vendor: JUCE
- website: http://juce.com
- description: Plays an audio file.
-
- dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
- juce_audio_processors, juce_audio_utils, juce_core,
- juce_data_structures, juce_events, juce_graphics,
- juce_gui_basics, juce_gui_extra
- exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone
-
- type: Component
- mainClass: AudioPlaybackDemo
-
- useLocalCopy: 1
-
- END_JUCE_PIP_METADATA
-
- *******************************************************************************/
-
- #pragma once
-
- #include "../Assets/DemoUtilities.h"
-
- class DemoThumbnailComp final : public Component,
- public ChangeListener,
- public FileDragAndDropTarget,
- public ChangeBroadcaster,
- private ScrollBar::Listener,
- private Timer
- {
- public:
- DemoThumbnailComp (AudioFormatManager& formatManager,
- AudioTransportSource& source,
- Slider& slider)
- : transportSource (source),
- zoomSlider (slider),
- thumbnail (512, formatManager, thumbnailCache)
- {
- thumbnail.addChangeListener (this);
-
- addAndMakeVisible (scrollbar);
- scrollbar.setRangeLimits (visibleRange);
- scrollbar.setAutoHide (false);
- scrollbar.addListener (this);
-
- currentPositionMarker.setFill (Colours::white.withAlpha (0.85f));
- addAndMakeVisible (currentPositionMarker);
- }
-
- ~DemoThumbnailComp() override
- {
- scrollbar.removeListener (this);
- thumbnail.removeChangeListener (this);
- }
-
- void setURL (const URL& url)
- {
- if (auto inputSource = makeInputSource (url))
- {
- thumbnail.setSource (inputSource.release());
-
- Range<double> newRange (0.0, thumbnail.getTotalLength());
- scrollbar.setRangeLimits (newRange);
- setRange (newRange);
-
- startTimerHz (40);
- }
- }
-
- URL getLastDroppedFile() const noexcept { return lastFileDropped; }
-
- void setZoomFactor (double amount)
- {
- if (thumbnail.getTotalLength() > 0)
- {
- auto newScale = jmax (0.001, thumbnail.getTotalLength() * (1.0 - jlimit (0.0, 0.99, amount)));
- auto timeAtCentre = xToTime ((float) getWidth() / 2.0f);
-
- setRange ({ timeAtCentre - newScale * 0.5, timeAtCentre + newScale * 0.5 });
- }
- }
-
- void setRange (Range<double> newRange)
- {
- visibleRange = newRange;
- scrollbar.setCurrentRange (visibleRange);
- updateCursorPosition();
- repaint();
- }
-
- void setFollowsTransport (bool shouldFollow)
- {
- isFollowingTransport = shouldFollow;
- }
-
- void paint (Graphics& g) override
- {
- g.fillAll (Colours::darkgrey);
- g.setColour (Colours::lightblue);
-
- if (thumbnail.getTotalLength() > 0.0)
- {
- auto thumbArea = getLocalBounds();
-
- thumbArea.removeFromBottom (scrollbar.getHeight() + 4);
- thumbnail.drawChannels (g, thumbArea.reduced (2),
- visibleRange.getStart(), visibleRange.getEnd(), 1.0f);
- }
- else
- {
- g.setFont (14.0f);
- g.drawFittedText ("(No audio file selected)", getLocalBounds(), Justification::centred, 2);
- }
- }
-
- void resized() override
- {
- scrollbar.setBounds (getLocalBounds().removeFromBottom (14).reduced (2));
- }
-
- void changeListenerCallback (ChangeBroadcaster*) override
- {
- // this method is called by the thumbnail when it has changed, so we should repaint it..
- repaint();
- }
-
- bool isInterestedInFileDrag (const StringArray& /*files*/) override
- {
- return true;
- }
-
- void filesDropped (const StringArray& files, int /*x*/, int /*y*/) override
- {
- lastFileDropped = URL (File (files[0]));
- sendChangeMessage();
- }
-
- void mouseDown (const MouseEvent& e) override
- {
- mouseDrag (e);
- }
-
- void mouseDrag (const MouseEvent& e) override
- {
- if (canMoveTransport())
- transportSource.setPosition (jmax (0.0, xToTime ((float) e.x)));
- }
-
- void mouseUp (const MouseEvent&) override
- {
- transportSource.start();
- }
-
- void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
- {
- if (thumbnail.getTotalLength() > 0.0)
- {
- auto newStart = visibleRange.getStart() - wheel.deltaX * (visibleRange.getLength()) / 10.0;
- newStart = jlimit (0.0, jmax (0.0, thumbnail.getTotalLength() - (visibleRange.getLength())), newStart);
-
- if (canMoveTransport())
- setRange ({ newStart, newStart + visibleRange.getLength() });
-
- if (! approximatelyEqual (wheel.deltaY, 0.0f))
- zoomSlider.setValue (zoomSlider.getValue() - wheel.deltaY);
-
- repaint();
- }
- }
-
- private:
- AudioTransportSource& transportSource;
- Slider& zoomSlider;
- ScrollBar scrollbar { false };
-
- AudioThumbnailCache thumbnailCache { 5 };
- AudioThumbnail thumbnail;
- Range<double> visibleRange;
- bool isFollowingTransport = false;
- URL lastFileDropped;
-
- DrawableRectangle currentPositionMarker;
-
- float timeToX (const double time) const
- {
- if (visibleRange.getLength() <= 0)
- return 0;
-
- return (float) getWidth() * (float) ((time - visibleRange.getStart()) / visibleRange.getLength());
- }
-
- double xToTime (const float x) const
- {
- return (x / (float) getWidth()) * (visibleRange.getLength()) + visibleRange.getStart();
- }
-
- bool canMoveTransport() const noexcept
- {
- return ! (isFollowingTransport && transportSource.isPlaying());
- }
-
- void scrollBarMoved (ScrollBar* scrollBarThatHasMoved, double newRangeStart) override
- {
- if (scrollBarThatHasMoved == &scrollbar)
- if (! (isFollowingTransport && transportSource.isPlaying()))
- setRange (visibleRange.movedToStartAt (newRangeStart));
- }
-
- void timerCallback() override
- {
- if (canMoveTransport())
- updateCursorPosition();
- else
- setRange (visibleRange.movedToStartAt (transportSource.getCurrentPosition() - (visibleRange.getLength() / 2.0)));
- }
-
- void updateCursorPosition()
- {
- currentPositionMarker.setVisible (transportSource.isPlaying() || isMouseButtonDown());
-
- currentPositionMarker.setRectangle (Rectangle<float> (timeToX (transportSource.getCurrentPosition()) - 0.75f, 0,
- 1.5f, (float) (getHeight() - scrollbar.getHeight())));
- }
- };
-
- //==============================================================================
- class AudioPlaybackDemo final : public Component,
- #if (JUCE_ANDROID || JUCE_IOS)
- private Button::Listener,
- #else
- private FileBrowserListener,
- #endif
- private ChangeListener
- {
- public:
- AudioPlaybackDemo()
- {
- addAndMakeVisible (zoomLabel);
- zoomLabel.setFont (Font (15.00f, Font::plain));
- zoomLabel.setJustificationType (Justification::centredRight);
- zoomLabel.setEditable (false, false, false);
- zoomLabel.setColour (TextEditor::textColourId, Colours::black);
- zoomLabel.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
-
- addAndMakeVisible (followTransportButton);
- followTransportButton.onClick = [this] { updateFollowTransportState(); };
-
- #if (JUCE_ANDROID || JUCE_IOS)
- addAndMakeVisible (chooseFileButton);
- chooseFileButton.addListener (this);
- #else
- addAndMakeVisible (fileTreeComp);
-
- directoryList.setDirectory (File::getSpecialLocation (File::userHomeDirectory), true, true);
-
- fileTreeComp.setTitle ("Files");
- fileTreeComp.setColour (FileTreeComponent::backgroundColourId, Colours::lightgrey.withAlpha (0.6f));
- fileTreeComp.addListener (this);
-
- addAndMakeVisible (explanation);
- explanation.setFont (Font (14.00f, Font::plain));
- explanation.setJustificationType (Justification::bottomRight);
- explanation.setEditable (false, false, false);
- explanation.setColour (TextEditor::textColourId, Colours::black);
- explanation.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
- #endif
-
- addAndMakeVisible (zoomSlider);
- zoomSlider.setRange (0, 1, 0);
- zoomSlider.onValueChange = [this] { thumbnail->setZoomFactor (zoomSlider.getValue()); };
- zoomSlider.setSkewFactor (2);
-
- thumbnail = std::make_unique<DemoThumbnailComp> (formatManager, transportSource, zoomSlider);
- addAndMakeVisible (thumbnail.get());
- thumbnail->addChangeListener (this);
-
- addAndMakeVisible (startStopButton);
- startStopButton.setColour (TextButton::buttonColourId, Colour (0xff79ed7f));
- startStopButton.setColour (TextButton::textColourOffId, Colours::black);
- startStopButton.onClick = [this] { startOrStop(); };
-
- // audio setup
- formatManager.registerBasicFormats();
-
- thread.startThread (Thread::Priority::normal);
-
- #ifndef JUCE_DEMO_RUNNER
- audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
- #endif
-
- audioDeviceManager.addAudioCallback (&audioSourcePlayer);
- audioSourcePlayer.setSource (&transportSource);
-
- setOpaque (true);
- setSize (500, 500);
- }
-
- ~AudioPlaybackDemo() override
- {
- transportSource .setSource (nullptr);
- audioSourcePlayer.setSource (nullptr);
-
- audioDeviceManager.removeAudioCallback (&audioSourcePlayer);
-
- #if (JUCE_ANDROID || JUCE_IOS)
- chooseFileButton.removeListener (this);
- #else
- fileTreeComp.removeListener (this);
- #endif
-
- thumbnail->removeChangeListener (this);
- }
-
- void paint (Graphics& g) override
- {
- g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
- }
-
- void resized() override
- {
- auto r = getLocalBounds().reduced (4);
-
- auto controls = r.removeFromBottom (90);
-
- auto controlRightBounds = controls.removeFromRight (controls.getWidth() / 3);
-
- #if (JUCE_ANDROID || JUCE_IOS)
- chooseFileButton.setBounds (controlRightBounds.reduced (10));
- #else
- explanation.setBounds (controlRightBounds);
- #endif
-
- auto zoom = controls.removeFromTop (25);
- zoomLabel .setBounds (zoom.removeFromLeft (50));
- zoomSlider.setBounds (zoom);
-
- followTransportButton.setBounds (controls.removeFromTop (25));
- startStopButton .setBounds (controls);
-
- r.removeFromBottom (6);
-
- #if JUCE_ANDROID || JUCE_IOS
- thumbnail->setBounds (r);
- #else
- thumbnail->setBounds (r.removeFromBottom (140));
- r.removeFromBottom (6);
-
- fileTreeComp.setBounds (r);
- #endif
- }
-
- private:
- // if this PIP is running inside the demo runner, we'll use the shared device manager instead
- #ifndef JUCE_DEMO_RUNNER
- AudioDeviceManager audioDeviceManager;
- #else
- AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
- #endif
-
- AudioFormatManager formatManager;
- TimeSliceThread thread { "audio file preview" };
-
- #if (JUCE_ANDROID || JUCE_IOS)
- std::unique_ptr<FileChooser> fileChooser;
- TextButton chooseFileButton {"Choose Audio File...", "Choose an audio file for playback"};
- #else
- DirectoryContentsList directoryList {nullptr, thread};
- FileTreeComponent fileTreeComp {directoryList};
- Label explanation { {}, "Select an audio file in the treeview above, and this page will display its waveform, and let you play it.." };
- #endif
-
- URL currentAudioFile;
- AudioSourcePlayer audioSourcePlayer;
- AudioTransportSource transportSource;
- std::unique_ptr<AudioFormatReaderSource> currentAudioFileSource;
-
- std::unique_ptr<DemoThumbnailComp> thumbnail;
- Label zoomLabel { {}, "zoom:" };
- Slider zoomSlider { Slider::LinearHorizontal, Slider::NoTextBox };
- ToggleButton followTransportButton { "Follow Transport" };
- TextButton startStopButton { "Play/Stop" };
-
- //==============================================================================
- void showAudioResource (URL resource)
- {
- if (! loadURLIntoTransport (resource))
- {
- // Failed to load the audio file!
- jassertfalse;
- return;
- }
-
- currentAudioFile = std::move (resource);
- zoomSlider.setValue (0, dontSendNotification);
- thumbnail->setURL (currentAudioFile);
- }
-
- bool loadURLIntoTransport (const URL& audioURL)
- {
- // unload the previous file source and delete it..
- transportSource.stop();
- transportSource.setSource (nullptr);
- currentAudioFileSource.reset();
-
- const auto source = makeInputSource (audioURL);
-
- if (source == nullptr)
- return false;
-
- auto stream = rawToUniquePtr (source->createInputStream());
-
- if (stream == nullptr)
- return false;
-
- auto reader = rawToUniquePtr (formatManager.createReaderFor (std::move (stream)));
-
- if (reader == nullptr)
- return false;
-
- currentAudioFileSource = std::make_unique<AudioFormatReaderSource> (reader.release(), true);
-
- // ..and plug it into our transport source
- transportSource.setSource (currentAudioFileSource.get(),
- 32768, // tells it to buffer this many samples ahead
- &thread, // this is the background thread to use for reading-ahead
- currentAudioFileSource->getAudioFormatReader()->sampleRate); // allows for sample rate correction
-
- return true;
- }
-
- void startOrStop()
- {
- if (transportSource.isPlaying())
- {
- transportSource.stop();
- }
- else
- {
- transportSource.setPosition (0);
- transportSource.start();
- }
- }
-
- void updateFollowTransportState()
- {
- thumbnail->setFollowsTransport (followTransportButton.getToggleState());
- }
-
- #if (JUCE_ANDROID || JUCE_IOS)
- void buttonClicked (Button* btn) override
- {
- if (btn == &chooseFileButton && fileChooser.get() == nullptr)
- {
- if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage))
- {
- SafePointer<AudioPlaybackDemo> safeThis (this);
- RuntimePermissions::request (RuntimePermissions::readExternalStorage,
- [safeThis] (bool granted) mutable
- {
- if (safeThis != nullptr && granted)
- safeThis->buttonClicked (&safeThis->chooseFileButton);
- });
- return;
- }
-
- if (FileChooser::isPlatformDialogAvailable())
- {
- fileChooser = std::make_unique<FileChooser> ("Select an audio file...", File(), "*.wav;*.flac;*.aif");
-
- fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles,
- [this] (const FileChooser& fc) mutable
- {
- if (fc.getURLResults().size() > 0)
- {
- auto u = fc.getURLResult();
-
- showAudioResource (std::move (u));
- }
-
- fileChooser = nullptr;
- }, nullptr);
- }
- else
- {
- NativeMessageBox::showAsync (MessageBoxOptions()
- .withIconType (MessageBoxIconType::WarningIcon)
- .withTitle ("Enable Code Signing")
- .withMessage ("You need to enable code-signing for your iOS project and enable \"iCloud Documents\" "
- "permissions to be able to open audio files on your iDevice. See: "
- "https://forum.juce.com/t/native-ios-android-file-choosers"),
- nullptr);
- }
- }
- }
- #else
- void selectionChanged() override
- {
- showAudioResource (URL (fileTreeComp.getSelectedFile()));
- }
-
- void fileClicked (const File&, const MouseEvent&) override {}
- void fileDoubleClicked (const File&) override {}
- void browserRootChanged (const File&) override {}
- #endif
-
- void changeListenerCallback (ChangeBroadcaster* source) override
- {
- if (source == thumbnail.get())
- showAudioResource (URL (thumbnail->getLastDroppedFile()));
- }
-
- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPlaybackDemo)
- };
|