From 889129cacfa01a087ec4218d1259607acf482e56 Mon Sep 17 00:00:00 2001 From: madeline Date: Fri, 3 Apr 2026 21:51:04 -0700 Subject: [PATCH] do resampling manually --- src/dusk/audio/DuskDsp.cpp | 176 ++++++++++++++++------------------ src/dusk/audio/DuskDsp.hpp | 13 ++- src/dusk/imgui/ImGuiAudio.cpp | 7 +- 3 files changed, 98 insertions(+), 98 deletions(-) diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 7b9401d394..7cf1c44cd8 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -74,19 +74,6 @@ constexpr static int PitchToSampleRate(u16 value) { return static_cast(static_cast(SampleRate) * value / 4096); } -static void UpdateSampleRate(const JASDsp::TChannel& channel, ChannelAuxData& aux) { - auto sampleRate = PitchToSampleRate(channel.mPitch); - - const SDL_AudioSpec spec = { - SDL_AUDIO_S16, - 1, - sampleRate - }; - - SDL_SetAudioStreamFormat(aux.resampleStream, &spec, nullptr); - aux.prevPitch = channel.mPitch; -} - /** * Reset state for a DSP channel between independent playbacks. */ @@ -98,8 +85,9 @@ static void ResetChannel(JASDsp::TChannel& channel, ChannelAuxData& aux) { aux.hist0 = 0; aux.hist1 = 0; - SDL_ClearAudioStream(aux.resampleStream); - UpdateSampleRate(channel, aux); + aux.decodeBufCount = 0; + aux.resamplePos = 0.0; + aux.resamplePrev = 0; for (auto& volume : aux.prevVolume) { volume = NAN; @@ -196,15 +184,16 @@ static void ReadSampleData( } /** - * Read a single *contiguous* chunk of sample data from a channel, - * writes the samples to the channel's resampler stream. + * Read a single *contiguous* chunk of sample data from a channel into outBuf * - * @returns Amount of samples actually read. Can be greater than the amount requested. + * @returns Amount of samples written to outBuf. May be less than desiredSamples */ static int ReadChannelSamplesChunk( JASDsp::TChannel& channel, ChannelAuxData& aux, - int desiredSamples) { + int desiredSamples, + s16* outBuf, + int outBufSize) { assert(desiredSamples >= 0); @@ -249,61 +238,49 @@ static int ReadChannelSamplesChunk( channel.mSamplesLeft -= renderSamples; channel.mSamplePosition += renderSamples; - SDL_PutAudioStreamData( - aux.resampleStream, - renderData + skipSamples, - static_cast(renderSize - skipSamples * sizeof(u16))); + int outputCount = static_cast(renderSamples - skipSamples); + + // this should never be hit with the limits on pitch shift (i think) but just in case!! + outputCount = std::min(outputCount, outBufSize); + if (outputCount > 0) { + memcpy(outBuf, renderData + skipSamples, outputCount * sizeof(s16)); + } assert(curSamplePosition % channel.mSamplesPerBlock == 0 || channel.mSamplesLeft == 0); - return static_cast(renderSamples - skipSamples); + return outputCount; } /** - * Reads new audio channels from a DSP channel and writes them to the resampler stream. + * Fill decodeBuf with at least `needed` samples, fewer may be written if the channel has no loop and its data ends */ -static void SDLCALL ReadChannelSamples( - void *userdata, - SDL_AudioStream*, - int additional_amount, - int) { - - if (additional_amount == 0) { - return; - } - - const auto index = static_cast(reinterpret_cast(userdata)); - auto& channel = JASDsp::CH_BUF[index]; - auto& aux = ChannelAux[index]; - - if (channel.mSamplesLeft == 0 && !channel.mLoopFlag) { - // May get called when we're out of data to read. - // This is expected, as we need to drain the resampler channel before we mark the channel as finished. - return; - } - - auto samplesRead = ReadChannelSamplesChunk(channel, aux, additional_amount); - additional_amount -= samplesRead; - - if (channel.mSamplesLeft == 0) { - // Reached end of buffer. - if (!channel.mLoopFlag) { - return; +static void FillDecodeBuf(JASDsp::TChannel& channel, ChannelAuxData& aux, int needed) { + while (aux.decodeBufCount < needed) { + if (channel.mSamplesLeft == 0) { + if (!channel.mLoopFlag) { + // we aren't a looping channel and there's no samples left, we out of this fuckin loop + break; + } else { + // we are looping, handle loop logic + channel.mSamplesLeft = channel.mEndSample - channel.mLoopStartSample; + channel.mSamplePosition = channel.mLoopStartSample; + aux.hist1 = channel.mpPenult; + aux.hist0 = channel.mpLast; + } } - channel.mSamplesLeft = channel.mEndSample - channel.mLoopStartSample; - channel.mSamplePosition = channel.mLoopStartSample; + int remainingDecodeSpace = ChannelAuxData::DECODE_BUF_SIZE - aux.decodeBufCount; + if (remainingDecodeSpace == 0) { + break; + } - aux.hist1 = channel.mpPenult; - aux.hist0 = channel.mpLast; + aux.decodeBufCount += ReadChannelSamplesChunk( + channel, aux, std::min(remainingDecodeSpace, needed - aux.decodeBufCount), + aux.decodeBuf + aux.decodeBufCount, remainingDecodeSpace + ); } - if (additional_amount >= 0) { - ReadChannelSamplesChunk(channel, aux, additional_amount); - } - - channel.mAramStreamPosition = channel.mWaveAramAddress - + ConvertSamplesToDataLength(channel, channel.mSamplePosition); + channel.mAramStreamPosition = channel.mWaveAramAddress + ConvertSamplesToDataLength(channel, channel.mSamplePosition); } /** @@ -422,57 +399,70 @@ static void RenderOutputChannel( prevVolume = targetVolume; } +/** + * Fetch, decode, resample, output + */ static void RenderChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, OutputSubframe& subframe) { + if (channel.mResetFlag) { ResetChannel(channel, channelAux); - } else if (channelAux.prevPitch != channel.mPitch) { - UpdateSampleRate(channel, channelAux); } - DspSubframe audioLoadBuffer = {}; + // how many input samples we step per output sample, aka the resampling ratio + f32 step = (f32)PitchToSampleRate(channel.mPitch) / SampleRate; - int wantRead = sizeof(audioLoadBuffer); - auto read = SDL_GetAudioStreamData( - channelAux.resampleStream, - &audioLoadBuffer, - wantRead); + // how many input samples to resample to DSP_SUBFRAME_SIZE output samples + int needed = static_cast(channelAux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2; - if (read < wantRead) { + FillDecodeBuf(channel, channelAux, needed); + + // source ran dry, channel is finished + if(channelAux.decodeBufCount < needed) { channel.mIsFinished = true; } - auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, wantRead / sizeof(f32)); + DspSubframe audioLoadBuffer = {}; + f64 pos = channelAux.resamplePos; + s16 prev = channelAux.resamplePrev; + s16 next = channelAux.decodeBufCount > 0 ? channelAux.decodeBuf[0] : prev; + int srcIdx = 0; + + // linear resampling and f32 conversion + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + audioLoadBuffer[i] = static_cast(prev + pos * (next - prev)) / 32768.0f; + pos += step; + while (pos >= 1.0) { + pos -= 1.0; + prev = next; + srcIdx++; + next = srcIdx < channelAux.decodeBufCount ? channelAux.decodeBuf[srcIdx] : prev; + } + } + + // save resampler state for the next subframe, prevents popping on pitch change + channelAux.resamplePos = pos; + channelAux.resamplePrev = prev; + + // move any remaining samples in the decode buf to the beginning + int remainingDecodeBuf = channelAux.decodeBufCount - srcIdx; + if (remainingDecodeBuf > 0) { + memmove(channelAux.decodeBuf, channelAux.decodeBuf + srcIdx, remainingDecodeBuf * sizeof(s16)); + } + + channelAux.decodeBufCount = std::max(0, remainingDecodeBuf); + + auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, DSP_SUBFRAME_SIZE); static_assert(OutputSubframe::NUM_CHANNELS == 2, "Keep RenderChannel in sync!"); RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, hasReadSamples, subframe); - RenderOutputChannel(channel, channelAux,OutputChannel::RIGHT, hasReadSamples, subframe); + RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, hasReadSamples, subframe); } void dusk::audio::DspInit() { - constexpr SDL_AudioSpec srcSpec = { - SDL_AUDIO_S16, - 1, - SampleRate - }; - constexpr SDL_AudioSpec dstSpec = { - SDL_AUDIO_F32, - 1, - SampleRate - }; - - for (u32 i = 0; i < DSP_CHANNELS; i++) { - auto& aux = ChannelAux[i]; - aux.resampleStream = SDL_CreateAudioStream(&srcSpec, &dstSpec); - - SDL_SetAudioStreamGetCallback( - aux.resampleStream, - ReadChannelSamples, - reinterpret_cast(static_cast(i))); - } } void dusk::audio::ApplyVolume( diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index c84bb338db..35b6ccabc9 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -26,8 +26,6 @@ namespace dusk::audio { struct ChannelAuxData { s16 hist1; s16 hist0; - SDL_AudioStream* resampleStream; - u16 prevPitch; // Used for debugging tools. u32 resetCount; @@ -43,6 +41,17 @@ namespace dusk::audio { assert(channel < OutputChannel::OutputChannel_MAX); return prevVolume[static_cast(channel)]; } + + // buffer for decoding before resampling, size is chosen based on how many input samples we would need to fetch for the highest possible pitch + // to fill one subframe of output samples after resampling + static constexpr int DECODE_BUF_SIZE = 2048; + s16 decodeBuf[DECODE_BUF_SIZE]; + int decodeBufCount; + + // basically stores our position between resamplePrev and decodeBuf[0] so we don't lose that fractional resampler position next subframe + f32 resamplePos; + // last consumed sample from decodeBuf + s16 resamplePrev; }; extern ChannelAuxData ChannelAux[DSP_CHANNELS]; diff --git a/src/dusk/imgui/ImGuiAudio.cpp b/src/dusk/imgui/ImGuiAudio.cpp index 707091b158..ae01f119f4 100644 --- a/src/dusk/imgui/ImGuiAudio.cpp +++ b/src/dusk/imgui/ImGuiAudio.cpp @@ -50,9 +50,10 @@ static void DisplayDspChannel(int i) { auto dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.5f; auto fxMix = (channel.mAutoMixerFxMix >> 8) / 127.5f; auto volume = VolumeFromU16(channel.mAutoMixerVolume); + auto pitch = channel.mPitch / 4096.0f; ImGui::Text( - "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f)", - pan, dolby, fxMix, volume); + "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f, pitch %f)", + pan, dolby, fxMix, volume, pitch); } else { ImGui::Text( "Bus connect: %04X(%.2f),%04X(%.2f),%04X(%.2f),%04X(%.2f),%04X(%.2f),%04X(%.2f)", @@ -249,4 +250,4 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() { } ImGui::End(); -} \ No newline at end of file +}