changeset 171:ceba165f07d8

add: audio subsystem, windows backend still missing
author Sam <sam@basx.dev>
date Mon, 01 May 2023 23:55:07 +0700
parents c1a9a988b9e3
children 062b03d5d094
files README.md src/semicongine/audio.nim src/semicongine/engine.nim src/semicongine/platform/linux/audio.nim tests/test_audio.nim
diffstat 5 files changed, 163 insertions(+), 128 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Mon May 01 01:21:17 2023 +0700
+++ b/README.md	Mon May 01 23:55:07 2023 +0700
@@ -53,7 +53,7 @@
       - [ ] Window
 - [ ] Input-mapping configuration
 - [ ] Audio playing
-      - [ ] Linux (Alsa)
+      - [x] Linux (Alsa)
       - [ ] Windows Waveform API?
 - [ ] Telemetry
 - [ ]  Documentation?
--- a/src/semicongine/audio.nim	Mon May 01 01:21:17 2023 +0700
+++ b/src/semicongine/audio.nim	Mon May 01 23:55:07 2023 +0700
@@ -1,5 +1,6 @@
 import std/tables
 import std/math
+import std/locks
 import std/sequtils
 
 import ./audiotypes
@@ -17,20 +18,104 @@
   Track = object
     playing: Table[uint64, Playback]
     level: Level
-
   Mixer* = object
     playbackCounter: uint64
     tracks: Table[string, Track]
     sounds*: Table[string, Sound]
     level: Level
     device: NativeSoundDevice
-
+    lock: Lock
 
 proc loadSoundResource(resourcePath: string): Sound =
   assert false, "Not implemented yet"
 
-func applyLevel(sample: Sample, level: Level): Sample =
- (int16(float(sample[0]) * level), int16(float(sample[1]) * level))
+proc initMixer*(): Mixer =
+  result = Mixer(
+    tracks: {"": Track(level: 1'f)}.toTable,
+    level: 1'f,
+    device: openSoundDevice(SAMPLERATE, BUFFERSIZE),
+  )
+  result.lock.initLock()
+
+proc loadSound*(mixer: var Mixer, name: string, resource: string) =
+  assert not (name in mixer.sounds)
+  mixer.sounds[name] = loadSoundResource(resource)
+
+proc addSound*(mixer: var Mixer, name: string, sound: Sound) =
+  assert not (name in mixer.sounds)
+  mixer.sounds[name] = sound
+
+proc replaceSound*(mixer: var Mixer, name: string, sound: Sound) =
+  assert (name in mixer.sounds)
+  mixer.sounds[name] = sound
+
+proc addTrack*(mixer: var Mixer, name: string) =
+  assert not (name in mixer.tracks)
+  mixer.lock.withLock():
+    mixer.tracks[name] = Track(level: 1'f)
+
+proc play*(mixer: var Mixer, soundName: string, track="", stopOtherSounds=false, loop=false, levelLeft, levelRight: Level): uint64 =
+  assert track in mixer.tracks
+  assert soundName in mixer.sounds
+  mixer.lock.withLock():
+    if stopOtherSounds:
+      mixer.tracks[track].playing.clear()
+    mixer.tracks[track].playing[mixer.playbackCounter] = Playback(
+      sound: mixer.sounds[soundName],
+      position: 0,
+      loop: loop,
+      levelLeft: levelLeft,
+      levelRight: levelRight
+    )
+  result = mixer.playbackCounter
+  inc mixer.playbackCounter
+
+proc play*(mixer: var Mixer, soundName: string, track="", stopOtherSounds=false, loop=false, level: Level=1'f): uint64 =
+  play(mixer=mixer, soundName=soundName, track=track, stopOtherSounds=stopOtherSounds, loop=loop, levelLeft=level, levelRight=level)
+
+proc stop*(mixer: var Mixer) =
+  mixer.lock.withLock():
+    for track in mixer.tracks.mvalues:
+      track.playing.clear()
+
+proc getLevel*(mixer: var Mixer): Level = mixer.level
+proc getLevel*(mixer: var Mixer, track: string): Level = mixer.tracks[track].level
+proc getLevel*(mixer: var Mixer, playbackId : uint64): (Level, Level) =
+  for track in mixer.tracks.mvalues:
+    if playbackId in track.playing:
+      return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight)
+
+proc setLevel*(mixer: var Mixer, level: Level) = mixer.level = level
+proc setLevel*(mixer: var Mixer, track: string, level: Level) =
+  mixer.lock.withLock():
+    mixer.tracks[track].level = level
+proc setLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) =
+  mixer.lock.withLock():
+    for track in mixer.tracks.mvalues:
+      if playbackId in track.playing:
+        track.playing[playbackId].levelLeft = levelLeft
+        track.playing[playbackId].levelRight = levelRight
+proc setLevel*(mixer: var Mixer, playbackId : uint64, level: Level) =
+  setLevel(mixer, playbackId, level, level)
+
+proc stop*(mixer: var Mixer, track: string) =
+  assert track in mixer.tracks
+  mixer.lock.withLock():
+    mixer.tracks[track].playing.clear()
+
+proc stop*(mixer: var Mixer, playbackId: uint64) =
+  mixer.lock.withLock():
+    for track in mixer.tracks.mvalues:
+      if playbackId in track.playing:
+        track.playing.del(playbackId)
+        break
+
+proc isPlaying*(mixer: var Mixer): bool =
+  mixer.lock.withLock():
+    for track in mixer.tracks.mvalues:
+      if track.playing.len > 0:
+        return true
+  return false
 
 func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample =
  (int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight))
@@ -43,99 +128,48 @@
   right = max(min(int32(high(int16)), right), int32(low(int16)))
   (int16(left), int16(right))
 
-proc initMixer*(): Mixer =
-  Mixer(
-    tracks: {"": Track(level: 1'f)}.toTable,
-    level: 1'f,
-    device: openSoundDevice(SAMPLERATE, BUFFERSIZE),
-  )
-
-proc loadSound*(mixer: var Mixer, name: string, resource: string) =
-  assert not (name in mixer.sounds)
-  mixer.sounds[name] = loadSoundResource(resource)
-
-proc addSound*(mixer: var Mixer, name: string, sound: Sound) =
-  assert not (name in mixer.sounds)
-  mixer.sounds[name] = sound
-
-proc addTrack*(mixer: var Mixer, name: string) =
-  assert not (name in mixer.tracks)
-  mixer.tracks[name] = Track(level: 1'f)
-
-proc play*(mixer: var Mixer, soundName: string, track="", stopOtherSounds=false, loop=false, levelLeft, levelRight: Level): uint64 =
-  assert track in mixer.tracks
-  assert soundName in mixer.sounds
-  if stopOtherSounds:
-    mixer.tracks[track].playing.clear()
-  mixer.tracks[track].playing[mixer.playbackCounter] = Playback(
-    sound: mixer.sounds[soundName],
-    position: 0,
-    loop: loop,
-    levelLeft: levelLeft,
-    levelRight: levelRight
-  )
-  result = mixer.playbackCounter
-  inc mixer.playbackCounter
-proc play*(mixer: var Mixer, soundName: string, track="", stopOtherSounds=false, loop=false, level: Level=1'f): uint64 =
-  play(mixer=mixer, soundName=soundName, track=track, stopOtherSounds=stopOtherSounds, loop=loop, levelLeft=level, levelRight=level)
-
-proc stop*(mixer: var Mixer) =
-  for track in mixer.tracks.mvalues:
-    track.playing.clear()
-
-proc getLevel*(mixer: var Mixer): Level = mixer.level
-proc getLevel*(mixer: var Mixer, track: string): Level = mixer.tracks[track].level
-proc getLevel*(mixer: var Mixer, playbackId : uint64): (Level, Level) =
-  for track in mixer.tracks.mvalues:
-    if playbackId in track.playing:
-      return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight)
-
-proc setLevel*(mixer: var Mixer, level: Level) = mixer.level = level
-proc setLevel*(mixer: var Mixer, track: string, level: Level) = mixer.tracks[track].level = level
-proc setLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) =
-  for track in mixer.tracks.mvalues:
-    if playbackId in track.playing:
-      track.playing[playbackId].levelLeft = levelLeft
-      track.playing[playbackId].levelRight = levelRight
-proc setLevel*(mixer: var Mixer, playbackId : uint64, level: Level) =
-  setLevel(mixer, playbackId, level, level)
-
-proc stop*(mixer: var Mixer, track: string) =
-  assert track in mixer.tracks
-  mixer.tracks[track].playing.clear()
-
-proc stop*(mixer: var Mixer, playbackId: uint64) =
-  for track in mixer.tracks.mvalues:
-    if playbackId in track.playing:
-      track.playing.del(playbackId)
-      break
-
-proc nextBufferData(mixer: var Mixer, nSamples: uint64): seq[Sample] =
-  result = newSeq[Sample](nSamples)
-  for i in 0 ..< nSamples:
+proc updateSoundBuffer(mixer: var Mixer) =
+  # mix
+  var buffer = newSeq[Sample](BUFFERSIZE)
+  for i in 0 ..< buffer.len:
     var currentSample = (0'i16, 0'i16)
-    for track in mixer.tracks.mvalues:
-      var stoppedSounds: seq[uint64]
-      for (id, playback) in track.playing.mpairs:
-        let sample = applyLevel(
-          playback.sound[][playback.position],
-          mixer.level * track.level * playback.levelLeft,
-          mixer.level * track.level * playback.levelRight,
-        )
-        currentSample = mix(currentSample, sample)
-        inc playback.position
-        if playback.position >= playback.sound[].len:
-          if playback.loop:
-            playback.position = 0
-          else:
-            stoppedSounds.add id
-      for id in stoppedSounds:
-        track.playing.del(id)
-    result[i] = currentSample
-
-proc updateSoundBuffer*(mixer: var Mixer) =
-  var buffer = mixer.nextBufferData(BUFFERSIZE)
+    mixer.lock.withLock():
+      for track in mixer.tracks.mvalues:
+        var stoppedSounds: seq[uint64]
+        for (id, playback) in track.playing.mpairs:
+          let sample = applyLevel(
+            playback.sound[][playback.position],
+            mixer.level * track.level * playback.levelLeft,
+            mixer.level * track.level * playback.levelRight,
+          )
+          currentSample = mix(currentSample, sample)
+          inc playback.position
+          if playback.position >= playback.sound[].len:
+            if playback.loop:
+              playback.position = 0
+            else:
+              stoppedSounds.add id
+        for id in stoppedSounds:
+          track.playing.del(id)
+      buffer[i] = currentSample
+  # send data to sound device
   mixer.device.updateSoundBuffer(buffer)
 
-proc destroy*(mixer: Mixer) =
+
+proc destroy*(mixer: var Mixer) =
+  mixer.lock.deinitLock()
   mixer.device.closeSoundDevice()
+
+# Threaded implementation, usually used for audio
+
+var mixer* = createShared(Mixer)
+
+proc audioWorker() {.thread.} =
+  onThreadDestruction(proc() = mixer[].lock.withLock(mixer[].destroy()); freeShared(mixer))
+  while true:
+    mixer[].updateSoundBuffer()
+
+proc startMixerThread*() =
+  mixer[] = initMixer()
+  var audiothread: Thread[void]
+  audiothread.createThread(audioWorker)
--- a/src/semicongine/engine.nim	Mon May 01 01:21:17 2023 +0700
+++ b/src/semicongine/engine.nim	Mon May 01 23:55:07 2023 +0700
@@ -13,6 +13,7 @@
 import ./events
 import ./config
 import ./math
+import ./audio
 
 type
   EngineState* = enum
@@ -91,6 +92,7 @@
     enabledExtensions = @[],
     selectedPhysicalDevice.filterForGraphicsPresentationQueues()
   )
+  startMixerThread()
 
 proc setRenderer*(engine: var Engine, renderPass: RenderPass) =
   assert engine.state != Destroyed
--- a/src/semicongine/platform/linux/audio.nim	Mon May 01 01:21:17 2023 +0700
+++ b/src/semicongine/platform/linux/audio.nim	Mon May 01 23:55:07 2023 +0700
@@ -28,6 +28,7 @@
 proc snd_pcm_hw_params_set_rate*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p, val: cuint, dir: cint): cint {.alsafunc.}
 proc snd_pcm_hw_params*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p): cint {.alsafunc.}
 proc snd_pcm_writei*(pcm: snd_pcm_p, buffer: pointer, size: snd_pcm_uframes_t): snd_pcm_sframes_t {.alsafunc.}
+proc snd_pcm_recover*(pcm: snd_pcm_p, err: cint, silent: cint): cint {.alsafunc.}
 
 template checkAlsaResult*(call: untyped) =
   let value = call
@@ -57,7 +58,10 @@
   snd_pcm_hw_params_free(hw_params)
 
 proc updateSoundBuffer*(soundDevice: NativeSoundDevice, buffer: var SoundData) =
-  discard snd_pcm_writei(soundDevice.handle, addr buffer[0], snd_pcm_uframes_t(len(buffer)))
+  var ret = snd_pcm_writei(soundDevice.handle, addr buffer[0], snd_pcm_uframes_t(buffer.len))
+  if ret < 0:
+    checkAlsaResult snd_pcm_recover(soundDevice.handle, cint(ret), 0)
+    
 
 proc closeSoundDevice*(soundDevice: NativeSoundDevice) =
-  checkAlsaResult snd_pcm_close(soundDevice.handle)
+  discard snd_pcm_close(soundDevice.handle)
--- a/tests/test_audio.nim	Mon May 01 01:21:17 2023 +0700
+++ b/tests/test_audio.nim	Mon May 01 23:55:07 2023 +0700
@@ -1,31 +1,29 @@
+import std/os
 import std/sequtils
 import std/times
 
 import semicongine
 
+
 proc test1() =
-  var mixer = initMixer()
-  mixer.addSound("test1", newSound(sineSoundData(1000, 2)))
-  mixer.addSound("test2", newSound(sineSoundData(500, 2)))
+  mixer[].addSound("test1", newSound(sineSoundData(1000, 2)))
+  mixer[].addSound("test2", newSound(sineSoundData(500, 2)))
 
 
-  let s1 = mixer.play("test1", loop=true)
-  let s2 = mixer.play("test2", loop=true)
+  let s1 = mixer[].play("test1", loop=true)
+  let s2 = mixer[].play("test2", loop=true)
 
   let t0 = now()
   while true:
-    mixer.updateSoundBuffer()
     let runtime = (now() - t0).inMilliseconds()
     if runtime > 1500:
-      mixer.setLevel(0.1)
+      mixer[].setLevel(0.1)
     if runtime > 3000:
-      mixer.stop(s2)
+      mixer[].stop(s2)
     if runtime > 6000:
-      mixer.stop("")
+      mixer[].stop("")
     if runtime > 8000:
-      mixer.stop()
       break
-  mixer.destroy()
 
 proc test2() =
   let
@@ -56,19 +54,13 @@
       f, c, f, f,
     )
 
-  var mixer = initMixer()
-  mixer.addSound("frerejaques", newSound(frerejaquesData))
-  discard mixer.play("frerejaques", loop=true)
+  mixer[].addSound("frerejaques", newSound(frerejaquesData))
+  discard mixer[].play("frerejaques")
 
-  let t0 = now()
-  while true:
-    mixer.updateSoundBuffer()
-    if (now() - t0).inMilliseconds() > 20000:
-      break
-  mixer.destroy()
+  while mixer[].isPlaying():
+    sleep(1)
 
 proc test3() =
-
   var song: SoundData
   var f = open("tests/audiotest.PCM.s16le.48000.2")
   var readLen = 999
@@ -77,18 +69,21 @@
     readLen = f.readBuffer(addr sample, sizeof(Sample))
     song.add sample
 
-  var mixer = initMixer()
-  mixer.addSound("pianosong", newSound(song))
-  discard mixer.play("pianosong", loop=true)
+  mixer[].addSound("pianosong", newSound(song))
+  mixer[].addSound("ping", newSound(sineSoundData(500, 0.05)))
+  mixer[].addTrack("effects")
+  discard mixer[].play("pianosong")
 
   let t0 = now()
-  while true:
-    mixer.updateSoundBuffer()
-    if (now() - t0).inMilliseconds() > 190_000:
-      break
-  mixer.destroy()
+  while mixer[].isPlaying():
+    discard mixer[].play("ping", track="effects", stopOtherSounds=true, level=0.5)
+    var input = stdin.readLine()
 
 when isMainModule:
+  startMixerThread()
   test1()
+  mixer[].stop()
   test2()
+  mixer[].stop()
   test3()
+  mixer[].stop()