1414
1515#include " AudioFilePlayer.h"
1616
17- #include < filesystem>
1817#include < memory>
1918
2019#include " components/icons/svg/SvgIconLookup.h"
2120#include " components/src/EclipsaColours.h"
2221#include " data_structures/src/FilePlayback.h"
23- #include " player/src/transport/IAMFPlaybackDevice.h"
24- #include " processors/file_output/iamf_export_utils/IAMFFileReader.h"
2522
2623class AudioFilePlayer ::Spinner : public juce::Component, private juce::Timer {
2724 public:
@@ -61,34 +58,34 @@ class AudioFilePlayer::Spinner : public juce::Component, private juce::Timer {
6158};
6259
6360AudioFilePlayer::AudioFilePlayer (FilePlaybackRepository& filePlaybackRepo,
64- FileExportRepository& fileExportRepo )
61+ FilePlaybackProcessorData& fpbData )
6562 : playButton_(" Play" , SvgMap::kPlay ),
6663 pauseButton_(" Pause" , SvgMap::kPause ),
6764 stopButton_(" Stop" , SvgMap::kStop ),
6865 timeLabel_(" timeLabel" , " 00:00 / 00:00" ),
6966 volumeIcon_(SvgMap::kVolume ),
7067 spinner_(std::make_unique<Spinner>()),
7168 fpbr_(filePlaybackRepo),
72- fer_(fileExportRepo ) {
69+ fpbData_(fpbData ) {
7370 playButton_.setColour (juce::TextButton::buttonColourId,
7471 EclipsaColours::rolloverGrey);
7572 pauseButton_.setColour (juce::TextButton::buttonColourId,
7673 EclipsaColours::rolloverGrey);
7774 stopButton_.setColour (juce::TextButton::buttonColourId,
7875 EclipsaColours::rolloverGrey);
7976 playButton_.onClick = [this ]() {
80- auto fpb = fpbr_.get ();
81- fpb.setPlayState (FilePlayback::kPlay );
77+ FilePlayback fpb = fpbr_.get ();
78+ fpb.setPlaybackCommand (FilePlayback::PlaybackCommand ::kPlay );
8279 fpbr_.update (fpb);
8380 };
8481 pauseButton_.onClick = [this ]() {
85- auto fpb = fpbr_.get ();
86- fpb.setPlayState (FilePlayback::kPause );
82+ FilePlayback fpb = fpbr_.get ();
83+ fpb.setPlaybackCommand (FilePlayback::PlaybackCommand ::kPause );
8784 fpbr_.update (fpb);
8885 };
8986 stopButton_.onClick = [this ]() {
90- auto fpb = fpbr_.get ();
91- fpb.setPlayState (FilePlayback::kStop );
87+ FilePlayback fpb = fpbr_.get ();
88+ fpb.setPlaybackCommand (FilePlayback::PlaybackCommand ::kStop );
9289 fpbr_.update (fpb);
9390 };
9491
@@ -104,22 +101,28 @@ AudioFilePlayer::AudioFilePlayer(FilePlaybackRepository& filePlaybackRepo,
104101 fileSelectLabel_.setJustificationType (juce::Justification::centred);
105102
106103 playbackSlider_.setRange (0.0 , 1.0 );
107- playbackSlider_.setValue (0.0 );
104+ playbackSlider_.setValue (0.0 , juce::sendNotification );
108105 playbackSlider_.setSliderStyle (juce::Slider::LinearHorizontal);
109106 playbackSlider_.setTextBoxStyle (juce::Slider::NoTextBox, false , 0 , 0 );
110- playbackSlider_.onValueChange = [this ]() {
111- auto fpb = fpbr_.get ();
112- fpb.setSeekPosition (static_cast < float >( playbackSlider_.getValue () ));
107+ playbackSlider_.onDragEnd = [this ]() {
108+ FilePlayback fpb = fpbr_.get ();
109+ fpb.setSeekPosition (playbackSlider_.getValue ());
113110 fpbr_.update (fpb);
114111 };
115112 addAndMakeVisible (playbackSlider_);
116113
117- volumeSlider_.setRange (0 , 1 );
118- volumeSlider_.setValue (0.5 );
114+ volumeSlider_.setRange (0 , 2 );
115+ volumeSlider_.setValue (1 , juce::sendNotification );
119116 volumeSlider_.setSliderStyle (juce::Slider::LinearHorizontal);
120117 volumeSlider_.setTextBoxStyle (juce::Slider::NoTextBox, false , 0 , 0 );
121- addAndMakeVisible (volumeSlider_);
118+ volumeSlider_.onValueChange = [this ]() {
119+ FilePlayback fpb = fpbr_.get ();
120+ const float kVol = volumeSlider_.getValue ();
121+ fpb.setVolume (kVol );
122+ fpbr_.update (fpb);
123+ };
122124
125+ addAndMakeVisible (volumeSlider_);
123126 addAndMakeVisible (playButton_);
124127 addAndMakeVisible (pauseButton_);
125128 addAndMakeVisible (stopButton_);
@@ -128,28 +131,13 @@ AudioFilePlayer::AudioFilePlayer(FilePlaybackRepository& filePlaybackRepo,
128131 addAndMakeVisible (fileSelectLabel_);
129132 addAndMakeVisible (*spinner_);
130133
131- fpbr_.registerListener (this );
132- fer_.registerListener (this );
133- if (fpbr_.get ().getPlaybackFile ().isNotEmpty ()) {
134- attemptCreatePlaybackEngine ();
135- }
136134 updateComponentVisibility ();
137135 startTimerHz (30 );
138136}
139137
140138AudioFilePlayer::~AudioFilePlayer () {
141- // Signal that we're being destroyed. Join the background thread for safe
142- // cleanup.
143- isBeingDestroyed_ = true ;
144- if (playbackEngineLoaderThread_.joinable ()) {
145- playbackEngineLoaderThread_.join ();
146- }
147-
148- fpbr_.deregisterListener (this );
149- fer_.deregisterListener (this );
150-
151139 FilePlayback fpb = fpbr_.get ();
152- fpb.setPlayState (FilePlayback::kStop );
140+ fpb.setPlaybackCommand (FilePlayback::PlaybackCommand:: kPause );
153141 fpbr_.update (fpb);
154142}
155143
@@ -171,11 +159,10 @@ void AudioFilePlayer::resized() {
171159 const int kButtonSz = 24 ;
172160 const int kGap = 5 ;
173161
174- auto fpb = fpbr_.get ();
175- bool isBuffering = (fpb.getPlayState () == FilePlayback::kBuffering );
176-
177162 // Only render the warning label if we get to a disabled state
178- if (fpb.getPlayState () == FilePlayback::kDisabled ) {
163+ FilePlayback::ProcessorState state;
164+ fpbData_.processorState .read (state);
165+ if (state == FilePlayback::ProcessorState::kError ) {
179166 flexBox.items .add (juce::FlexItem (fileSelectLabel_)
180167 .withFlex (1 )
181168 .withHeight (kButtonSz )
@@ -185,7 +172,7 @@ void AudioFilePlayer::resized() {
185172 }
186173
187174 // Render the spinner when buffering
188- if (isBuffering ) {
175+ if (state == FilePlayback::ProcessorState:: kBuffering ) {
189176 flexBox.items .add (
190177 juce::FlexItem (*spinner_)
191178 .withWidth (kButtonSz )
@@ -232,161 +219,60 @@ void AudioFilePlayer::resized() {
232219}
233220
234221void AudioFilePlayer::update () {
235- std::lock_guard<std::mutex> lock (pbeMutex_);
236- if (playbackEngine_) {
237- const IAMFFileReader::StreamData kData = playbackEngine_->getStreamData ();
238- const float kDuration_s =
239- kData .numFrames * kData .frameSize / (float )kData .sampleRate ;
240- const float kPosition_s =
241- kData .currentFrameIdx * kData .frameSize / (float )kData .sampleRate ;
242-
243- const int durationMins = (int )(kDuration_s / 60 );
244- const int durationSecs = (int )(kDuration_s ) % 60 ;
245- const int currentMins = (int )(kPosition_s / 60 );
246- const int currentSecs = (int )(kPosition_s ) % 60 ;
247- timeLabel_.setText (
248- juce::String::formatted (" %02d:%02d / %02d:%02d" , currentMins,
249- currentSecs, durationMins, durationSecs),
250- juce::dontSendNotification);
251- playbackSlider_.setValue (
252- kDuration_s > 0 .0f ? (kPosition_s / kDuration_s ) : 0 .0f ,
253- juce::dontSendNotification);
254- playbackEngine_->setVolume (volumeSlider_.getValue ());
255- } else {
256- timeLabel_.setText (" 00:00 / 00:00" , juce::dontSendNotification);
257- playbackSlider_.setValue (0 , juce::dontSendNotification);
258- }
259- }
260-
261- void AudioFilePlayer::timerCallback () { update (); }
262-
263- void AudioFilePlayer::valueTreePropertyChanged (
264- juce::ValueTree& tree, const juce::Identifier& property) {
265- if (property == FilePlayback::kPlayState ) {
266- triggerAsyncUpdate ();
267- } else if (property == FilePlayback::kPlaybackFile ) {
268- attemptCreatePlaybackEngine ();
269- } else if (property == FileExport::kExportCompleted ) {
270- // When this property is false a new export is starting, so we want to
271- // destroy the player and wait until export is complete.
272- // When this property is true we want to attempt to create the playback
273- // engine again.
274- if (fer_.get ().getExportCompleted ()) {
275- auto safeThis = juce::Component::SafePointer<AudioFilePlayer>(this );
276- juce::MessageManager::callAsync (
277- [safeThis]() { safeThis->attemptCreatePlaybackEngine (); });
222+ FilePlayback::ProcessorState state;
223+ fpbData_.processorState .read (state);
224+ if (state != FilePlayback::ProcessorState::kError ) {
225+ float currPos = 0 .0f ;
226+ fpbData_.currFilePosition .read (currPos);
227+ unsigned long long duration_s = 0 .0f ;
228+ fpbData_.fileDuration_s .read (duration_s);
229+ const float kCurrentTime = currPos * duration_s;
230+
231+ const int kCurrMinutes = static_cast <int >(kCurrentTime ) / 60 ;
232+ const int kCurrSeconds = static_cast <int >(kCurrentTime ) % 60 ;
233+ const int kTotalMinutes = static_cast <int >(duration_s) / 60 ;
234+ const int kTotalSeconds = static_cast <int >(duration_s) % 60 ;
235+
236+ juce::String timeStr;
237+ // Format long file durations differently
238+ if (duration_s >= 3600 ) {
239+ timeStr =
240+ juce::String::formatted (" %d:%02d / %d:%02d" , kCurrMinutes ,
241+ kCurrSeconds , kTotalMinutes , kTotalSeconds );
278242 } else {
279- cancelCreatePlaybackEngine ();
243+ timeStr =
244+ juce::String::formatted (" %02d:%02d / %02d:%02d" , kCurrMinutes ,
245+ kCurrSeconds , kTotalMinutes , kTotalSeconds );
246+ }
247+ timeLabel_.setText (timeStr, juce::dontSendNotification);
248+
249+ if (!playbackSlider_.isMouseButtonDown ()) {
250+ playbackSlider_.setValue (currPos, juce::dontSendNotification);
280251 }
281252 }
282253}
283254
284- void AudioFilePlayer::handleAsyncUpdate () {
255+ void AudioFilePlayer::timerCallback () {
256+ update ();
285257 updateComponentVisibility ();
286258 resized ();
287259}
288260
289261void AudioFilePlayer::updateComponentVisibility () {
290- auto fpb = fpbr_.get ();
291- auto playState = fpb.getPlayState ();
292- const bool kPlaying = (playState == FilePlayback::kPlay );
293- const bool kBuffering = (playState == FilePlayback::kBuffering );
294- const bool kDisabled = (playState == FilePlayback::kDisabled );
295- fileSelectLabel_.setVisible (kDisabled );
296- playButton_.setVisible (!kPlaying && !kBuffering && !kDisabled );
297- pauseButton_.setVisible (kPlaying && !kDisabled );
298- stopButton_.setVisible (!kBuffering && !kDisabled );
299- timeLabel_.setVisible (!kDisabled );
300- playbackSlider_.setVisible (!kDisabled );
301- volumeIcon_.setVisible (!kDisabled );
302- volumeSlider_.setVisible (!kDisabled );
262+ FilePlayback::ProcessorState playState;
263+ fpbData_.processorState .read (playState);
264+ const bool kPlaying = (playState == FilePlayback::ProcessorState::kPlaying );
265+ const bool kBuffering =
266+ (playState == FilePlayback::ProcessorState::kBuffering );
267+ const bool kError = (playState == FilePlayback::ProcessorState::kError );
268+ fileSelectLabel_.setVisible (kError );
269+ playButton_.setVisible (!kPlaying && !kBuffering && !kError );
270+ pauseButton_.setVisible (kPlaying && !kError );
271+ stopButton_.setVisible (!kBuffering && !kError );
272+ timeLabel_.setVisible (!kError );
273+ playbackSlider_.setVisible (!kError );
274+ playbackSlider_.setEnabled (!kBuffering );
275+ volumeIcon_.setVisible (!kError );
276+ volumeSlider_.setVisible (!kError );
303277 if (spinner_) spinner_->setVisible (kBuffering );
304- }
305-
306- void AudioFilePlayer::cancelCreatePlaybackEngine () {
307- isBeingDestroyed_ = true ;
308- if (playbackEngineLoaderThread_.joinable ()) {
309- playbackEngineLoaderThread_.join ();
310- }
311- // Wait for async callback to complete
312- std::unique_lock<std::mutex> lock (pbeMutex_);
313- pbeCv_.wait (lock, [this ] { return !isLoadingPlaybackEngine_; });
314- isBeingDestroyed_ = false ;
315- }
316-
317- void AudioFilePlayer::attemptCreatePlaybackEngine () {
318- cancelCreatePlaybackEngine ();
319-
320- // If the file doesn't exist or it's a new file, we set the player to a
321- // stopped state
322- auto fe = fer_.get ();
323- const std::filesystem::path kFileToLoad (fe.getExportFile ().toStdString ());
324- if (kFileToLoad .empty () || kFileToLoad .extension () != " .iamf" ||
325- !std::filesystem::exists (kFileToLoad )) {
326- auto playbackState = fpbr_.get ();
327- playbackState.setPlayState (FilePlayback::kStop );
328- fpbr_.update (playbackState);
329- return ;
330- }
331-
332- auto fpb = fpbr_.get ();
333- fpb.setPlayState (FilePlayback::kBuffering );
334- fpbr_.update (fpb);
335-
336- createPlaybackEngine (kFileToLoad );
337- }
338-
339- void AudioFilePlayer::createPlaybackEngine (
340- const std::filesystem::path iamfPath) {
341- const juce::String kDevice = fpbr_.get ().getPlaybackDevice ();
342- playbackEngineLoaderThread_ = std::thread ([this , iamfPath, kDevice ]() {
343- isLoadingPlaybackEngine_ = true ;
344-
345- IAMFPlaybackDevice::Result res = IAMFPlaybackDevice::create (
346- iamfPath, kDevice , isBeingDestroyed_, fpbr_, deviceManager_);
347-
348- auto safeThis = juce::Component::SafePointer<AudioFilePlayer>(this );
349-
350- if (isBeingDestroyed_) {
351- isLoadingPlaybackEngine_ = false ;
352- return ;
353- }
354- juce::MessageManager::callAsync (
355- [safeThis, device = res.device .release (), error = res.error ]() {
356- if (safeThis && !safeThis->isBeingDestroyed_ ) {
357- safeThis->onPlaybackEngineCreated (IAMFPlaybackDevice::Result{
358- std::unique_ptr<IAMFPlaybackDevice>(device), error});
359- } else {
360- delete device;
361- }
362-
363- // Always reset the loading flag and notify waiters
364- if (safeThis) {
365- std::lock_guard<std::mutex> lock (safeThis->pbeMutex_ );
366- safeThis->isLoadingPlaybackEngine_ = false ;
367- safeThis->pbeCv_ .notify_all ();
368- }
369- });
370- });
371- }
372-
373- void AudioFilePlayer::onPlaybackEngineCreated (IAMFPlaybackDevice::Result res) {
374- std::lock_guard<std::mutex> lock (pbeMutex_);
375- if (!res.device && res.error == IAMFPlaybackDevice::Error::kInvalidIAMFFile ) {
376- // Failed to create playback engine - reset state to disabled
377- playbackEngine_ = nullptr ;
378- auto fpb = fpbr_.get ();
379- fpb.setPlayState (FilePlayback::kDisabled );
380- fpbr_.update (fpb);
381- } else if (res.error == IAMFPlaybackDevice::kEarlyAbortRequested ||
382- isBeingDestroyed_) {
383- // Do nothing - destruction was requested
384- playbackEngine_ = nullptr ;
385- } else {
386- playbackEngine_ = std::move (res.device );
387- // Update play state from buffering to ready
388- auto fpb = fpbr_.get ();
389- fpb.setPlayState (FilePlayback::kStop );
390- fpbr_.update (fpb);
391- }
392278}
0 commit comments