changeset 637:bb6857da8113

fix: bad audio buffer handling, reduce latency (unbearable on windows)
author Sam <sam@basx.dev>
date Tue, 02 May 2023 02:13:46 +0700
parents 77358570f33b
children bae5b7b884bb
files src/semicongine/audio.nim src/semicongine/audiotypes.nim src/semicongine/platform/linux/audio.nim src/semicongine/platform/windows/audio.nim tests/test_audio.nim
diffstat 5 files changed, 47 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/audio.nim	Tue May 02 01:03:03 2023 +0700
+++ b/src/semicongine/audio.nim	Tue May 02 02:13:46 2023 +0700
@@ -23,6 +23,7 @@
     level: Level
     device: NativeSoundDevice
     lock: Lock
+    buffer: SoundData
 
 proc loadSoundResource(resourcePath: string): Sound =
   assert false, "Not implemented yet"
@@ -31,10 +32,14 @@
   result = Mixer(
     tracks: {"": Track(level: 1'f)}.toTable,
     level: 1'f,
-    device: openSoundDevice(SAMPLERATE, BUFFERSIZE),
   )
   result.lock.initLock()
 
+proc setupDevice(mixer: var Mixer) =
+  # call this inside audio thread
+  mixer.buffer = newSeq[Sample](512)
+  mixer.device = openSoundDevice(44100, addr mixer.buffer)
+
 proc loadSound*(mixer: var Mixer, name: string, resource: string) =
   assert not (name in mixer.sounds)
   mixer.sounds[name] = loadSoundResource(resource)
@@ -128,8 +133,7 @@
 
 proc updateSoundBuffer(mixer: var Mixer) =
   # mix
-  var buffer = newSeq[Sample](BUFFERSIZE)
-  for i in 0 ..< buffer.len:
+  for i in 0 ..< mixer.buffer.len:
     var currentSample = (0'i16, 0'i16)
     mixer.lock.withLock():
       for track in mixer.tracks.mvalues:
@@ -149,9 +153,9 @@
               stoppedSounds.add id
         for id in stoppedSounds:
           track.playing.del(id)
-      buffer[i] = currentSample
+      mixer.buffer[i] = currentSample
   # send data to sound device
-  mixer.device.updateSoundBuffer(buffer)
+  mixer.device.writeSoundData()
 
 
 proc destroy*(mixer: var Mixer) =
@@ -164,6 +168,7 @@
 
 proc audioWorker() {.thread.} =
   onThreadDestruction(proc() = mixer[].lock.withLock(mixer[].destroy()); freeShared(mixer))
+  mixer[].setupDevice()
   while true:
     mixer[].updateSoundBuffer()
 
--- a/src/semicongine/audiotypes.nim	Tue May 02 01:03:03 2023 +0700
+++ b/src/semicongine/audiotypes.nim	Tue May 02 02:13:46 2023 +0700
@@ -5,9 +5,6 @@
 #
 # ffmpeg -i <infile> -f s16le -ac 2 -ar 48000 -acodec pcm_s16le <outfile>
 
-const SAMPLERATE* = 44100
-const BUFFERSIZE* = 512
-
 type
   Level* = 0'f .. 1'f
   Sample* = (int16, int16)
@@ -19,10 +16,10 @@
     sin(x * 2 * Pi * f)
   result = ret
 
-proc sineSoundData*(f: float, len: float): SoundData =
-  let dt = 1'f / float(SAMPLERATE)
+proc sineSoundData*(f: float, len: float, rate: int): SoundData =
+  let dt = 1'f / float(rate)
   var sine = sinewave(f)
-  for i in 0 ..< int(SAMPLERATE * len):
+  for i in 0 ..< int(float(rate) * len):
     let t = dt * float(i)
     let value = int16(sine(t) * float(high(int16)))
     result.add (value, value)
--- a/src/semicongine/platform/linux/audio.nim	Tue May 02 01:03:03 2023 +0700
+++ b/src/semicongine/platform/linux/audio.nim	Tue May 02 02:13:46 2023 +0700
@@ -41,8 +41,9 @@
 type
   NativeSoundDevice* = object
     handle: snd_pcm_p
+    buffer: ptr SoundData
  
-proc openSoundDevice*(sampleRate: uint32, bufferSize: uint32): NativeSoundDevice =
+proc openSoundDevice*(sampleRate: uint32, buffer: ptr SoundData): NativeSoundDevice =
   var hw_params: snd_pcm_hw_params_p = nil
   checkAlsaResult snd_pcm_open(addr result.handle, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_BLOCK)
 
@@ -53,15 +54,15 @@
   checkAlsaResult snd_pcm_hw_params_set_format(result.handle, hw_params, SND_PCM_FORMAT_S16_LE)
   checkAlsaResult snd_pcm_hw_params_set_rate(result.handle, hw_params, sampleRate, 0)
   checkAlsaResult snd_pcm_hw_params_set_channels(result.handle, hw_params, 2)
-  checkAlsaResult snd_pcm_hw_params_set_buffer_size(result.handle, hw_params, snd_pcm_uframes_t(bufferSize))
+  checkAlsaResult snd_pcm_hw_params_set_buffer_size(result.handle, hw_params, snd_pcm_uframes_t(buffer[].len))
   checkAlsaResult snd_pcm_hw_params(result.handle, hw_params)
   snd_pcm_hw_params_free(hw_params)
+  result.buffer = buffer
 
-proc updateSoundBuffer*(soundDevice: NativeSoundDevice, buffer: var SoundData) =
-  var ret = snd_pcm_writei(soundDevice.handle, addr buffer[0], snd_pcm_uframes_t(buffer.len))
+proc writeSoundData*(soundDevice: NativeSoundDevice) =
+  var ret = snd_pcm_writei(soundDevice.handle, addr soundDevice.buffer[][0], snd_pcm_uframes_t(soundDevice.buffer[].len))
   if ret < 0:
     checkAlsaResult snd_pcm_recover(soundDevice.handle, cint(ret), 0)
-    
 
 proc closeSoundDevice*(soundDevice: NativeSoundDevice) =
   discard snd_pcm_close(soundDevice.handle)
--- a/src/semicongine/platform/windows/audio.nim	Tue May 02 01:03:03 2023 +0700
+++ b/src/semicongine/platform/windows/audio.nim	Tue May 02 02:13:46 2023 +0700
@@ -11,8 +11,9 @@
 type
   NativeSoundDevice* = object
     handle: HWAVEOUT
+    buffer: WAVEHDR
  
-proc openSoundDevice*(sampleRate: uint32, bufferSize: uint32): NativeSoundDevice =
+proc openSoundDevice*(sampleRate: uint32, buffer: ptr SoundData): NativeSoundDevice =
   var format = WAVEFORMATEX(
     wFormatTag: WAVE_FORMAT_PCM,
     nChannels: 2,
@@ -24,11 +25,9 @@
   )
 
   checkWinMMResult waveOutOpen(addr result.handle, WAVE_MAPPER, addr format, DWORD_PTR(0), DWORD_PTR(0), CALLBACK_NULL)
-
-proc updateSoundBuffer*(soundDevice: NativeSoundDevice, buffer: var SoundData) =
-  var data = WAVEHDR(
-    lpData: cast[cstring](addr buffer[0]),
-    dwBufferLength: DWORD(buffer.len * sizeof(Sample)),
+  result.buffer = WAVEHDR(
+    lpData: cast[cstring](addr buffer[][0]),
+    dwBufferLength: DWORD(buffer[].len * sizeof(Sample)),
     dwBytesRecorded: 0,
     dwUser: DWORD_PTR(0),
     dwFlags: 0,
@@ -36,11 +35,13 @@
     lpNext: nil,
     reserved: DWORD_PTR(0)
   )
-  checkWinMMResult waveOutPrepareHeader(soundDevice.handle, addr data, UINT(sizeof(WAVEHDR)))
-  checkWinMMResult waveOutWrite(soundDevice.handle, addr data, UINT(sizeof(WAVEHDR)))
-  while (data.dwFlags and WHDR_DONE) != 1:
+  checkWinMMResult waveOutPrepareHeader(result.handle, addr result.buffer, UINT(sizeof(WAVEHDR)))
+
+proc writeSoundData*(soundDevice: var NativeSoundDevice) =
+  checkWinMMResult waveOutWrite(soundDevice.handle, addr soundDevice.buffer, UINT(sizeof(WAVEHDR)))
+  while (soundDevice.buffer.dwFlags and WHDR_DONE) != 1:
     discard
-  checkWinMMResult waveOutUnprepareHeader(soundDevice.handle, addr data, UINT(sizeof(WAVEHDR)))
 
-proc closeSoundDevice*(soundDevice: NativeSoundDevice) =
+proc closeSoundDevice*(soundDevice: var NativeSoundDevice) =
+  checkWinMMResult waveOutUnprepareHeader(soundDevice.handle, addr soundDevice.buffer, UINT(sizeof(WAVEHDR)))
   waveOutClose(soundDevice.handle)
--- a/tests/test_audio.nim	Tue May 02 01:03:03 2023 +0700
+++ b/tests/test_audio.nim	Tue May 02 02:13:46 2023 +0700
@@ -6,8 +6,8 @@
 
 
 proc test1() =
-  mixer[].addSound("test1", newSound(sineSoundData(1000, 2)))
-  mixer[].addSound("test2", newSound(sineSoundData(500, 2)))
+  mixer[].addSound("test1", newSound(sineSoundData(1000, 2, 44100)))
+  mixer[].addSound("test2", newSound(sineSoundData(500, 2, 44100)))
 
 
   let s1 = mixer[].play("test1", loop=true)
@@ -28,19 +28,19 @@
 proc test2() =
   let
     # notes
-    c = sineSoundData(261.6256, 0.5)
-    d = sineSoundData(293.6648, 0.5)
-    e = sineSoundData(329.6276, 0.5)
-    f = sineSoundData(349.2282, 0.5)
-    g = sineSoundData(391.9954, 0.5)
-    a = sineSoundData(440.0000, 0.5)
-    b = sineSoundData(493.8833, 0.5)
-    bb = sineSoundData(466.1638, 0.5)
-    c2 = sineSoundData(523.2511, 0.5)
-    d2 = sineSoundData(587.3295, 0.5)
-    bbShort = sineSoundData(466.1638, 0.25)
-    c2Short = sineSoundData(523.2511, 0.25)
-    d2Short = sineSoundData(587.3295, 0.25)
+    c = sineSoundData(261.6256, 0.5, 44100)
+    d = sineSoundData(293.6648, 0.5, 44100)
+    e = sineSoundData(329.6276, 0.5, 44100)
+    f = sineSoundData(349.2282, 0.5, 44100)
+    g = sineSoundData(391.9954, 0.5, 44100)
+    a = sineSoundData(440.0000, 0.5, 44100)
+    b = sineSoundData(493.8833, 0.5, 44100)
+    bb = sineSoundData(466.1638, 0.5, 44100)
+    c2 = sineSoundData(523.2511, 0.5, 44100)
+    d2 = sineSoundData(587.3295, 0.5, 44100)
+    bbShort = sineSoundData(466.1638, 0.25, 44100)
+    c2Short = sineSoundData(523.2511, 0.25, 44100)
+    d2Short = sineSoundData(587.3295, 0.25, 44100)
 
     # song
     frerejaquesData = concat(
@@ -70,7 +70,7 @@
     song.add sample
 
   mixer[].addSound("pianosong", newSound(song))
-  mixer[].addSound("ping", newSound(sineSoundData(500, 0.05)))
+  mixer[].addSound("ping", newSound(sineSoundData(500, 0.05, 44100)))
   mixer[].addTrack("effects")
   discard mixer[].play("pianosong")