// // Created by david on 01.02.2026. // #ifndef LOCKSTEP_MIXINGPLAYER_H #define LOCKSTEP_MIXINGPLAYER_H #include #include using namespace oboe; /** * Plays event sounds, potentially overlapping in time. * Use setStartBeat() to trigger playing sound for an event. */ class MixingPlayer: public oboe::AudioStreamDataCallback { protected: std::vector beatSound; std::vector beatIdx; std::atomic startBeat; int numBeatsPlaying; public: explicit MixingPlayer(std::vector beatSound) : beatSound(beatSound), startBeat(0), numBeatsPlaying(0) {} virtual ~MixingPlayer() = default; /** * Play a beat sound by setting the startBeat register. * A delay of the audio callback interval will be added. */ void setStartBeat() { startBeat.store(1); } // Call this from Activity onResume() int32_t startAudio() { std::lock_guard lock(mLock); oboe::AudioStreamBuilder builder; // The builder set methods can be chained for convenience. Result result = builder.setSharingMode(oboe::SharingMode::Exclusive) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->setChannelCount(kChannelCount) ->setSampleRate(kSampleRate) ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium) ->setFormat(oboe::AudioFormat::Float) ->setDataCallback(this) ->openStream(mStream); if (result != Result::OK) return (int32_t) result; // Typically, start the stream after querying some stream information, as well as some input from the user result = mStream->requestStart(); return (int32_t) result; } // Call this from Activity onPause() void stopAudio() { // Stop, close and delete in case not already closed. std::lock_guard lock(mLock); if (mStream) { mStream->stop(); mStream->close(); mStream.reset(); } } oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override { // fetch startBeat register int isStartBeat = startBeat.load(); if(isStartBeat) { if(beatIdx.size() < numBeatsPlaying+1) beatIdx.resize(numBeatsPlaying+1); beatIdx[numBeatsPlaying++] = 0; startBeat.store(0); // reset startBeat register } // // mix audio events, each event has a separate idx counter in beatIdx // float *floatData = (float *) audioData; int N = (int) beatSound.size(); for (int i = 0; i < numFrames; i++) { float sample = 0.0; float norm = (float) numBeatsPlaying; for (int k = 0; k < numBeatsPlaying; k++) { sample += beatSound[beatIdx[k]]; beatIdx[k] += 1; } // reduce beatIdx to the events which have not completed playing int l = 0; int m = 0; for (; m < numBeatsPlaying; l++, m++) { while(m < numBeatsPlaying && beatIdx[m] >= N) m++; if(m < numBeatsPlaying) beatIdx[l] = beatIdx[m]; else break; // avoid incrementing l } //beatIdx.resize(l); // we avoid re-allocating numBeatsPlaying = l; // set left and right output channels for (int j = 0; j < kChannelCount; j++) { // normalize sample by numBeatsPlaying (avoids clipping) floatData[i * kChannelCount + j] = sample / norm; } } return oboe::DataCallbackResult::Continue; } private: std::mutex mLock; std::shared_ptr mStream; // Stream params static int constexpr kChannelCount = 2; static int constexpr kSampleRate = 48000; }; #endif //LOCKSTEP_MIXINGPLAYER_H