Skip to content

Commit f58bc28

Browse files
authored
Main Bus File Playback (#100)
1 parent 74d330e commit f58bc28

54 files changed

Lines changed: 1977 additions & 1202 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

audioelementplugin/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ target_link_libraries(AudioElementPlugin
7676
substream_rdr
7777
processors
7878
components
79-
player
8079
logger
8180
data_repository
8281
data_structures

common/CMakeLists.txt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ juce_add_modules(
1919
data_repository
2020
data_structures
2121
logger
22-
player
2322
processors
2423
substream_rdr)
2524

@@ -32,9 +31,7 @@ if(CI_TEST OR INTERNAL_TEST)
3231
add_subdirectory(data_repository/tests)
3332
add_subdirectory(logger/tests)
3433
add_subdirectory(data_structures/tests)
35-
add_subdirectory(substream_rdr/tests)
36-
add_subdirectory(player/test)
37-
34+
add_subdirectory(substream_rdr/tests)
3835
endif()
3936

4037
# -------------------------------
@@ -117,13 +114,11 @@ target_link_libraries(processors INTERFACE
117114
target_include_directories(substream_rdr INTERFACE ${OBR_INCLUDE_DIRS})
118115
target_include_directories(processors INTERFACE ${OBR_INCLUDE_DIRS})
119116

120-
target_link_libraries(player INTERFACE processors substream_rdr)
121-
122117
# -------------------------------
123118
# Components and Logging
124119
# -------------------------------
125120

126-
target_link_libraries(components INTERFACE binary_data libear substream_rdr processors data_structures data_repository player)
121+
target_link_libraries(components INTERFACE binary_data libear substream_rdr processors data_structures data_repository)
127122

128123
if(WIN32)
129124
message(STATUS "Linking logger against Boost via CMake targets")

common/components/src/AudioFilePlayer.cpp

Lines changed: 71 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@
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

2623
class 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

6360
AudioFilePlayer::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

140138
AudioFilePlayer::~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

234221
void 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

289261
void 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

Comments
 (0)