fix: fixes for buffer level handling, player lifecycle handling

This commit is contained in:
2026-03-23 01:07:05 +01:00
parent 3fe10a914a
commit 1a3991db45
6 changed files with 95 additions and 13 deletions

View File

@@ -1,5 +1,7 @@
## TODO ## TODO
* do not open oboe upon app startup, only if we are actually recording
* PlaybackEngine - Buffer overrun on output for channel (1.000000) * PlaybackEngine - Buffer overrun on output for channel (1.000000)
- we are feeding too much data into 'stretcher' - we are feeding too much data into 'stretcher'

View File

@@ -13,7 +13,12 @@
class AudioCallbackProvider { class AudioCallbackProvider {
public: public:
virtual ~AudioCallbackProvider() {} virtual ~AudioCallbackProvider() {}
/** in current impl, this passes a buffer where data may already live. the provider may add to it and re-normalize to [-1.0, 1.0]. */ /**
* in current impl, this passes a buffer where data may already live.
* the provider may add to it and re-normalize to [-1.0, 1.0].
*
* upon is_finished=true it will not do anything - the caller is responsible for 0-ing the data buffer.
* */
virtual void onAudioReady(float *data, int32_t frames) {} virtual void onAudioReady(float *data, int32_t frames) {}
}; };

View File

@@ -22,8 +22,9 @@ protected:
std::vector<int> beatIdx; std::vector<int> beatIdx;
std::atomic<int> startBeat; std::atomic<int> startBeat;
int numBeatsPlaying; int numBeatsPlaying;
bool mIsPlaying;
public: public:
explicit MixingPlayer(std::vector<float> beatSound) : beatSound(beatSound), startBeat(0), numBeatsPlaying(0), mHaveMusic(false) {} explicit MixingPlayer(std::vector<float> beatSound) : beatSound(beatSound), startBeat(0), numBeatsPlaying(0), mIsPlaying(false), mHaveMusic(false) {}
virtual ~MixingPlayer() = default; virtual ~MixingPlayer() = default;
@@ -52,6 +53,7 @@ public:
// Typically, start the stream after querying some stream information, as well as some input from the user // Typically, start the stream after querying some stream information, as well as some input from the user
result = mStream->requestStart(); result = mStream->requestStart();
mIsPlaying = (result == Result::OK);
return (int32_t) result; return (int32_t) result;
} }
@@ -67,8 +69,11 @@ public:
mStream->close(); mStream->close();
mStream.reset(); mStream.reset();
} }
mIsPlaying = false;
} }
bool isPlaying() { return mIsPlaying; }
void setMusic(std::shared_ptr<AudioCallbackProvider> cb) { void setMusic(std::shared_ptr<AudioCallbackProvider> cb) {
std::lock_guard<std::mutex> lock(mLock); std::lock_guard<std::mutex> lock(mLock);
mMusic = std::move(cb); mMusic = std::move(cb);
@@ -115,6 +120,8 @@ public:
} }
if(mHaveMusic.load()) { if(mHaveMusic.load()) {
// note: the contract for onAudioReady() upon is_finished=true implies it will not do anything
// (the buffer must be set to all-0 here in the caller)
mMusic->onAudioReady(floatData, numFrames); mMusic->onAudioReady(floatData, numFrames);
} }

View File

@@ -79,6 +79,7 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
mFilesDir(filesDir), mFilesDir(filesDir),
haveMusicFile(false), haveMusicFile(false),
exitMusicFeedThread(false), exitMusicFeedThread(false),
isSetMusic(false),
android_fd(0), android_fd(0),
haveTimeRatio(false), haveTimeRatio(false),
timeRatio(1.0), timeRatio(1.0),
@@ -86,7 +87,8 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
// these 3 values are preliminary -- will be set from MixingPlayer defaults in the ctor body below // these 3 values are preliminary -- will be set from MixingPlayer defaults in the ctor body below
playbackRate(48000), playbackRate(48000),
numOutChannels(2), numOutChannels(2),
numInChannels(2) numInChannels(2),
back_pressure(0)
{ {
LOGI("PlaybackEngine()"); LOGI("PlaybackEngine()");
LOGI("NDK LOG_LEVEL=%d", LOG_LEVEL); LOGI("NDK LOG_LEVEL=%d", LOG_LEVEL);
@@ -98,10 +100,6 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
LOGI("read_mp3() for bump effect, is_ok=%d", is_ok); LOGI("read_mp3() for bump effect, is_ok=%d", is_ok);
mPlayer = new MixingPlayer(samples); mPlayer = new MixingPlayer(samples);
int32_t res = mPlayer->startAudio();
playbackRate.store(mPlayer->getRate());
numOutChannels.store(mPlayer->getNumChannels());
LOGI("startAudio() = %d rate=%d channels=%d", res, playbackRate.load(), numOutChannels.load());
// configure stretcher and start musicFeedThread() // configure stretcher and start musicFeedThread()
initRubberBand(); initRubberBand();
@@ -109,6 +107,7 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
void PlaybackEngine::initRubberBand() { void PlaybackEngine::initRubberBand() {
// we do not yet have a music file with actual sampling rate, so set the default ratio // we do not yet have a music file with actual sampling rate, so set the default ratio
stretcher.reset();
stretcher.setTimeRatio(1.0); stretcher.setTimeRatio(1.0);
stretcher.setPitchScale(1.0); stretcher.setPitchScale(1.0);
@@ -159,6 +158,8 @@ void PlaybackEngine::closeMusicFile() {
close(android_fd); close(android_fd);
android_fd = 0; android_fd = 0;
} }
isSetMusic.store(false);
mPlayer->setMusic(nullptr);
} }
void PlaybackEngine::mapChannels(int *channel_map, int num_ch_in, int num_ch_out) { void PlaybackEngine::mapChannels(int *channel_map, int num_ch_in, int num_ch_out) {
@@ -217,6 +218,18 @@ void PlaybackEngine::musicFeedThread() {
// thread 2: polling for decoding more mp3 -> process() -- getSamplesRequired() // thread 2: polling for decoding more mp3 -> process() -- getSamplesRequired()
while(!exitMusicFeedThread.load()) { while(!exitMusicFeedThread.load()) {
if(!haveMusicFile.load()) {
// while no MusicProvider is connected, no samples will be read from 'stretcher'
// therefore, we do not write any samples into it!
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
if(!isSetMusic.load()) {
mPlayer->setMusic(std::make_shared<MusicProvider>(&stretcher, buf_size_samples, numOutChannels.load(), &back_pressure));
isSetMusic.store(true);
}
if(haveTimeRatio.load()) { if(haveTimeRatio.load()) {
stretcher.setTimeRatio(timeRatio.load()); stretcher.setTimeRatio(timeRatio.load());
haveTimeRatio.store(false); haveTimeRatio.store(false);
@@ -235,6 +248,22 @@ void PlaybackEngine::musicFeedThread() {
mapChannels(channel_map, num_ch_in, num_ch_out); mapChannels(channel_map, num_ch_in, num_ch_out);
// do work ... // do work ...
// note: getSamplesRequired() itself only gives us how many samples to create another
// output buffer increment, not if the output buffer has been emptied.
// We need to manage the buffer sizes ourselves.
// this draft should always keep the output buffers filled at 50-100 ms
int target_output_buffer_frames = 100 * playbackRate.load() / 1000; // 100 ms worth of audio
if(idebug < 10) {
LOGI("back_pressure available=%d target=%d", back_pressure.load(), target_output_buffer_frames);
}
if(back_pressure.load() >= target_output_buffer_frames) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// at 48000 Hz playbackRate, the 512-1024 frames returned here give us additional (10-21 ms) output buffer
// (this is somewhat approximate, but the above control loop should keep us within a reasonable buffer size)
size_t num_samples = stretcher.getSamplesRequired(); size_t num_samples = stretcher.getSamplesRequired();
// note: how much to sleep until output has played x samples...? // note: how much to sleep until output has played x samples...?
@@ -242,6 +271,7 @@ void PlaybackEngine::musicFeedThread() {
// (is it like double-buffering implemented in 'stretcher'?) // (is it like double-buffering implemented in 'stretcher'?)
if (num_samples == 0) { if (num_samples == 0) {
// this was never the case in actual testing -- see note above.
LOGD("waiting %d us for getSamplesRequired()", loop_delay_us); LOGD("waiting %d us for getSamplesRequired()", loop_delay_us);
std::this_thread::sleep_for(std::chrono::microseconds(loop_delay_us)); std::this_thread::sleep_for(std::chrono::microseconds(loop_delay_us));
continue; continue;
@@ -283,6 +313,8 @@ void PlaybackEngine::musicFeedThread() {
// next iteration will play silence // next iteration will play silence
closeMusicFile(); closeMusicFile();
stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio
stretcher.process(buf_ptr, 0, true); // set end of playback
mPlayer->stopAudio();
continue; continue;
} }
if(err == MPG123_DONE) { if(err == MPG123_DONE) {
@@ -290,6 +322,8 @@ void PlaybackEngine::musicFeedThread() {
LOGI("finished reading mp3 file (MPG123_DONE)"); LOGI("finished reading mp3 file (MPG123_DONE)");
closeMusicFile(); closeMusicFile();
stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio
stretcher.process(buf_ptr, 0, true); // set end of playback
mPlayer->stopAudio();
continue; continue;
} }
@@ -341,14 +375,31 @@ void PlaybackEngine::playMusic(int fd) {
numInChannels.store(musicFile->channels); numInChannels.store(musicFile->channels);
haveMusicFile.store(true); haveMusicFile.store(true);
} }
mPlayer->setMusic(std::make_shared<MusicProvider>(&stretcher, buf_size_samples, numOutChannels.load()));
bool is_finished = (stretcher.available() == -1);
if(is_finished) {
// so that we may play again after "final chunk"
closeRubberBand();
initRubberBand();
} }
MusicProvider::MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out) : if(!mPlayer->isPlaying()) {
int32_t res = mPlayer->startAudio();
playbackRate.store(mPlayer->getRate());
numOutChannels.store(mPlayer->getNumChannels());
LOGI("startAudio() = %d rate=%d channels=%d", res, playbackRate.load(), numOutChannels.load());
}
// to wait the 50 ms that the musicFeedThread() is idling when it first receives a file
// we don't call mPlayer->setMusic() here, but in the musicFeedThread()
}
MusicProvider::MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out, std::atomic<int> *back_pressure) :
stretcher(stretcher), stretcher(stretcher),
idebug(0), idebug(0),
buf_size_samples(buf_size_samples), buf_size_samples(buf_size_samples),
num_ch_out(num_ch_out) num_ch_out(num_ch_out),
back_pressure(back_pressure)
{ {
buf = (float*) malloc(buf_size_samples*num_ch_out*sizeof(float)); buf = (float*) malloc(buf_size_samples*num_ch_out*sizeof(float));
buf_ptr = (float**) malloc(num_ch_out * sizeof(float*)); buf_ptr = (float**) malloc(num_ch_out * sizeof(float*));
@@ -373,8 +424,18 @@ void MusicProvider::onAudioReady(float *data, int32_t frames) {
} }
// 1. read from oboe into our temp de-interleaved buffer 'buf' // 1. read from oboe into our temp de-interleaved buffer 'buf'
size_t num_frames_requested = std::min((size_t) frames, buf_size_samples); int num_frames_requested = std::min((int) frames, (int) buf_size_samples);
size_t num_frames_available = stretcher->available(); int num_frames_available = stretcher->available();
bool is_finished = (num_frames_available == -1);
(*back_pressure).store((int) num_frames_available);
if(is_finished) {
return;
}
if(idebug < 10) {
LOGI("onAudioReady() available=%d", num_frames_available);
}
if(num_frames_available < num_frames_requested) { if(num_frames_available < num_frames_requested) {
// this is an audio glitch // this is an audio glitch
// TODO: bubble info upwards, in a counter (so we can collect device-specific glitch stats) // TODO: bubble info upwards, in a counter (so we can collect device-specific glitch stats)

View File

@@ -18,7 +18,7 @@
/** Provides music through a regular callback to oboe. Called from separate oboe thread. */ /** Provides music through a regular callback to oboe. Called from separate oboe thread. */
class MusicProvider : public AudioCallbackProvider { class MusicProvider : public AudioCallbackProvider {
public: public:
explicit MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out); explicit MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out, std::atomic<int> *back_pressure);
~MusicProvider() override; ~MusicProvider() override;
/** Called from separate oboe thread. */ /** Called from separate oboe thread. */
@@ -30,6 +30,8 @@ private:
int idebug; int idebug;
size_t buf_size_samples; size_t buf_size_samples;
int num_ch_out; int num_ch_out;
/** contains the current available() frames from 'stretcher' in the audio callback thread 2 (oboe) */
std::atomic<int> *back_pressure;
}; };
class PlaybackEngine : public StepListener { class PlaybackEngine : public StepListener {
@@ -47,12 +49,16 @@ private:
std::atomic<bool> haveMusicFile; std::atomic<bool> haveMusicFile;
std::unique_ptr<std::thread> musicFeed; std::unique_ptr<std::thread> musicFeed;
std::atomic<bool> exitMusicFeedThread; std::atomic<bool> exitMusicFeedThread;
/** where musicFeedThread() keeps track of the fact that we have music set -- will start the audio cb */
std::atomic<bool> isSetMusic;
int android_fd; int android_fd;
std::atomic<bool> haveTimeRatio; std::atomic<bool> haveTimeRatio;
std::atomic<double> timeRatio; std::atomic<double> timeRatio;
std::atomic<int> playbackRate; std::atomic<int> playbackRate;
std::atomic<int> numOutChannels; std::atomic<int> numOutChannels;
std::atomic<int> numInChannels; std::atomic<int> numInChannels;
/** contains the current available() frames from 'stretcher' in the audio callback thread 2 (oboe) */
std::atomic<int> back_pressure;
/** this is actually in frames, not samples */ /** this is actually in frames, not samples */
static size_t constexpr buf_size_samples = 1024; static size_t constexpr buf_size_samples = 1024;
void initRubberBand(); void initRubberBand();

View File

@@ -128,6 +128,7 @@ public class MainActivity extends AppCompatActivity implements LstForegroundServ
// TODO: since the Service keeps running, we must signal oboe to stop playing // TODO: since the Service keeps running, we must signal oboe to stop playing
// TODO: signal the pause to the C++ lib // TODO: signal the pause to the C++ lib
startService(LstForegroundService.stopIntent(MainActivity.this));
} }
private boolean isForeground = false; private boolean isForeground = false;