SDL Audio - real-time volume/panning

Hi, I’ve been working on getting volume and panning working using SDL audio. This example has it working. I can set the volume/pan and it will work,however I am unable to change it in real-time. Well, any change will only be reflected after SDL_GetAudioStreamAvailable returns 0. Can you give me some insight on how to rework this code to make this happen? Also, I can call SDL_ClearAudioStream after a volume/pan change, it will happen right away, but there are gaps in the audio.

procedure Test05;
const
  CChunkSize = 1024;
var
  LVorbisStream: Pstb_vorbis;
  LInAudioSpec, LOutAudioSpec: SDL_AudioSpec;
  LAudioDevice: SDL_AudioDeviceID;
  LAudioStream: PSDL_AudioStream;
  LInBuffer: array [0..CChunkSize] of Smallint;
  LOutBuffer: array [0..CChunkSize*2] of Smallint;
  BytesToWrite: Integer;
  LInfo: stb_vorbis_info;
  Loop, Done: Boolean;
  LRWops: PSDL_Rwops;
  LError: Integer;
  //LSamples: Cardinal;
  LFilename: string;
  I: Integer;
  LVolume: Single;
  LCalVolume: Integer;
  LPan: Single;
  LLeftVolume: Single;
  LRightVolume: Single;
  LCount: Integer;
begin
  // try to init SDL audio subsystem
  if SDL_Init(SDL_INIT_AUDIO) <> 0 then
    Exit;

  // open OGG from zip file
  LFilename := 'arc/music/song02.ogg';
  //LFilename := 'arc/sfx/samp0.ogg';
  LRWops := SDL_RWFromZipFile(CZipPassword, CZipFilename, LFilename);
  if not Assigned(LRWops) then
  begin
    SDL_Quit;
    Exit;
  end;

  // open OGG stream
  LVorbisStream := stb_vorbis_open_rwops(LRWops, 0, @LError, nil);
  if not Assigned(LVorbisStream) then
  begin
    SDL_RWclose(LRWops);
    SDL_Quit;
    Exit;
  end;

  // get size in samples
  //LSamples := stb_vorbis_stream_length_in_samples(LVorbisStream);

  // get OGG info
  LInfo := stb_vorbis_get_info(LVorbisStream);

  // error if ogg does not have two channels for this example
  if LInfo.channels > 2 then
  begin
    stb_vorbis_close(LVorbisStream);
    SDL_RWclose(LRWops);
    SDL_Quit;
    Exit;
  end;

  // Set input audio specs
  LInAudioSpec.format := SDL_AUDIO_S16;
  //LInAudioSpec.channels := LInfo.channels;
  LInAudioSpec.channels := 2;
  LInAudioSpec.freq := LInfo.sample_rate;

  // set ouput audio specs
  LOutAudioSpec.format := SDL_AUDIO_S16;
  LOutAudioSpec.channels := 2;
  LOutAudioSpec.freq := 44100;

  // open audio device
  LAudioDevice := SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_OUTPUT,
    @LInAudioSpec);

  // check if audio device is open
  if LAudioDevice = 0 then
  begin
    stb_vorbis_close(LVorbisStream);
    SDL_RWclose(LRWops);
    SDL_Quit;
    Exit;
  end;

  // Create audio stream
  LAudioStream := SDL_CreateAudioStream(@LInAudioSpec, @LOutAudioSpec);

  // check if audio stream was create
  if not Assigned(LAudioStream) then
  begin
    SDL_CloseAudioDevice(LAudioDevice);
    stb_vorbis_close(LVorbisStream);
    SDL_RWclose(LRWops);
    SDL_Quit;
    Exit;
  end;

  // bind audio stream to the audio device
  SDL_BindAudioStream(LAudioDevice, LAudioStream);

  // start playing audio
  SDL_ResumeAudioDevice(LAudioDevice);

  Loop := True;
  Done := FAlse;
  LVolume := 1.0;
  LPan := 0.0;
  LCount := 0;

  //PrintLn('Samples: %d', [LSamples]);
  PrintLn('Channels: %d', [LInfo.channels]);
  PrintLn('SampleRate: %d', [LInfo.sample_rate]);
  Print(CRLF+'Press ESC to quit...');

  // update audio
  repeat
    // decode more audio
    BytesToWrite := stb_vorbis_get_samples_short_interleaved(LVorbisStream, LInfo.channels, @LInBuffer[0], CChunkSize);

    if BytesToWrite > 0 then
      begin
        if LInfo.channels = 1 then
          begin
            for i := 0 to CChunkSize - 1 do
            begin
              LOutBuffer[i * 2]     := EnsureRange(Round(LInBuffer[i] * UnitToScalarValue( (1.0 - (Lpan / 2.0 + 0.5)) * LVolume, 1)), -32768, 32767);
              LOutBuffer[i * 2 + 1] :=EnsureRange(Round(LInBuffer[i] * UnitToScalarValue((Lpan / 2.0 + 0.5) * LVolume, 1)), -32768, 32767);
            end;
          end
        else
          begin
            Move(LInBuffer, LOutBuffer, SizeOf(LInBuffer));
            for i := 0 to CChunkSize - 1 do
            begin
              LOutBuffer[i * 2]     := EnsureRange(Round(LOutBuffer[i * 2] * UnitToScalarValue( (1.0 - (Lpan / 2.0 + 0.5)) * LVolume, 1)), -32768, 32767);
              LOutBuffer[i * 2 + 1] := EnsureRange(Round(LOutBuffer[i * 2 + 1] * UnitToScalarValue((Lpan / 2.0 + 0.5) * LVolume, 1)), -32768, 32767);
            end;
          end;

        SDL_PutAudioStreamData(LAudioStream, @LOutBuffer[0], (BytesToWrite*(LOutAudioSpec.channels*SizeOf(SmallInt))));
      end
    else
      begin
        // prevent 100 CPU usage
        Sleep(10);

        if SDL_GetAudioStreamAvailable(LAudioStream) = 0 then
        begin
          if Loop then
            begin
              // seek to start of ogg
              stb_vorbis_seek_start(LVorbisStream);
            end
          else
            Done := True;
        end;

      end;

    if WasKeyPressed(VK_ESCAPE) then
      Done := True;

    if WasKeyPressed(VK_RIGHT) then
    begin
      LPan := LPan + 0.1;
      if LPan > 1.0 then LPan := 1.0;
      writeln(LPan:3:2);
    end
    else
    if WasKeyPressed(VK_LEFT) then
    begin
      LPan := LPan - 0.1;
      if LPan < -1.0 then LPan := -1.0;
      writeln(LPan:3:2);
    end;

    if WasKeyPressed(VK_UP) then
    begin
      LVolume := LVolume + 0.1;
      LVolume := EnsureRange(LVolume, 0, 1);
      writeln(LVolume:3:2);
    end
    else
    if WasKeyPressed(VK_DOWN) then
    begin
      LVolume := LVolume - 0.1;
      LVolume := EnsureRange(LVolume, 0, 1);
      writeln(LVolume:3:2);
    end;

  until Done;

  // clear audio stream
  SDL_ClearAudioStream(LAudioStream);

  // unbind and destroy the audio stream
  SDL_UnbindAudioStream(LAudioStream);
  SDL_DestroyAudioStream(LAudioStream);

  // close audio device
  SDL_CloseAudioDevice(LAudioDevice);

  // clean up OGG stream
  stb_vorbis_close(LVorbisStream);

  // close RWops
  SDL_RWclose(LRWops);

  // quit SDL
  SDL_Quit;
end;

Ok, it turned out to be simple. I just needed to only decode when the sdl audio stream is empty and continue decoding until audio stream has data. I also changed the buffer to a smaller value and now it seems to work.

    if SDL_GetAudioStreamAvailable(LAudioStream) = 0 then
    begin
      repeat
        // decode more audio
        BytesToWrite := stb_vorbis_get_samples_short_interleaved(LVorbisStream, LInfo.channels, @LInBuffer[0], CChunkSize);

        if BytesToWrite > 0 then
          begin
            if LInfo.channels = 1 then
              begin
                for i := 0 to CChunkSize - 1 do
                begin
                  LOutBuffer[i * 2]     := EnsureRange(Round(LInBuffer[i] * UnitToScalarValue( (1.0 - (Lpan / 2.0 + 0.5)), 1) * LVolume*2), -32768, 32767);
                  LOutBuffer[i * 2 + 1] :=EnsureRange(Round(LInBuffer[i] * UnitToScalarValue((Lpan / 2.0 + 0.5), 1) * LVolume*2), -32768, 32767);
                end;
              end
            else
              begin
                Move(LInBuffer, LOutBuffer, SizeOf(LInBuffer));
                for i := 0 to CChunkSize - 1 do
                begin
                  LOutBuffer[i * 2]     := EnsureRange(Round(LOutBuffer[i * 2] * UnitToScalarValue( (1.0 - (Lpan / 2.0 + 0.5)), 1) * LVolume*2), -32768, 32767);
                  LOutBuffer[i * 2 + 1] := EnsureRange(Round(LOutBuffer[i * 2 + 1] * UnitToScalarValue((Lpan / 2.0 + 0.5), 1) * LVolume*2), -32768, 32767);
                end;
              end;

            SDL_PutAudioStreamData(LAudioStream, @LOutBuffer[0], (BytesToWrite*(LOutAudioSpec.channels*SizeOf(SmallInt))));
          end
        else
          begin

            if SDL_GetAudioStreamAvailable(LAudioStream) = 0 then
            begin
              if Loop then
                begin
                  // seek to start of ogg
                  stb_vorbis_seek_start(LVorbisStream);
                end
              else
                begin
                  Done := True;
                  break;
                end;
              end;
            end;
      until SDL_GetAudioStreamAvailable(LAudioStream) <> 0;
    end;

And now I put it in a background thread, working great! :sunglasses:
I’m loving the new audio system in SDL3. :+1:

2 Likes

I have been working on similar sound panning and effects functions, except that I am using callbacks to sync everything. Still not sure how well it is going to work trying to dynamically modify audio data before loading it into a stream like this or if it is just easier to to do all the decoding and mixing then use SDL to pump the final output as a single stream to the audio device.

At first, I was not using any threading. With the later modification, it does work without threads, but if you introduce enough latency in the loop, it will not play correctly. For example, you introduce a 20+ms delay in the loop, it will fail to play which you know will happen at some point during game play. So, I just pushed into a background thread (in between a criticalsection), and now all is good. I can change vol/pan in real-time.

class procedure TAudio.StartThread;
var
  LThread: TThread;
begin
  FThreadTerminated := False;
  LThread := TThread.CreateAnonymousThread(
    procedure
    begin
      while not FThreadTerminated do
      begin
        CriticalSection.Enter;
        try
          UpdateMusic;
          UpdateSound;
        finally
          CriticalSection.Leave;
        end;         
      end;
    end
  );
  LThread.Priority := tpTimeCritical;
  LThread.Start;
end;

The UnitToScalarValue routine smoothly moves the vol/pan between 0 and 1.

function  UnitToScalarValue(const aValue, aMaxValue: Double): Double;
var
  LGain: Double;
  LFactor: Double;
  LVolume: Double;
begin
  LGain := (EnsureRange(aValue, 0.0, 1.0) * 50) - 50;
  LFactor := Power(10, LGain * 0.05);
  LVolume := EnsureRange(aMaxValue * LFactor, 0, aMaxValue);
  Result := LVolume;
end;