changeset 167:4d4f7a2eb6ca

add: initial implementation of audio subsystem with mixer, currently missing windows implementation
author Sam <sam@basx.dev>
date Mon, 01 May 2023 01:15:39 +0700
parents 5b0e27e448cb
children 061054515d28
files src/semicongine.nim src/semicongine/audio.nim src/semicongine/audiotypes.nim src/semicongine/engine.nim src/semicongine/platform/linux/audio.nim
diffstat 5 files changed, 238 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine.nim	Sun Apr 30 01:02:33 2023 +0700
+++ b/src/semicongine.nim	Mon May 01 01:15:39 2023 +0700
@@ -1,3 +1,4 @@
+import semicongine/audio
 import semicongine/color
 import semicongine/config
 import semicongine/engine
@@ -10,6 +11,7 @@
 import semicongine/platform/window
 import semicongine/vulkan
 
+export audio
 export color
 export config
 export engine
--- a/src/semicongine/audio.nim	Sun Apr 30 01:02:33 2023 +0700
+++ b/src/semicongine/audio.nim	Mon May 01 01:15:39 2023 +0700
@@ -0,0 +1,141 @@
+import std/tables
+import std/math
+import std/sequtils
+
+import ./audiotypes
+import ./platform/audio
+
+export audiotypes
+
+type
+  Playback = object
+    sound: Sound
+    position: int
+    loop: bool
+    levelLeft: Level
+    levelRight: Level
+  Track = object
+    playing: Table[uint64, Playback]
+    level: Level
+
+  Mixer* = object
+    playbackCounter: uint64
+    tracks: Table[string, Track]
+    sounds*: Table[string, Sound]
+    level: Level
+    device: NativeSoundDevice
+
+
+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))
+
+func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample =
+ (int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight))
+
+func mix(a, b: Sample): Sample =
+  var
+    left = int32(a[0]) + int32(b[0])
+    right = int32(a[1]) + int32(b[1])
+  left = max(min(int32(high(int16)), left), int32(low(int16)))
+  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:
+    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.device.updateSoundBuffer(buffer)
+
+proc destroy*(mixer: Mixer) =
+  mixer.device.closeSoundDevice()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/semicongine/audiotypes.nim	Mon May 01 01:15:39 2023 +0700
@@ -0,0 +1,32 @@
+import std/math
+
+# in order to generate sound files that are directly usable with the engine,
+# convert an audio file to a raw PCM signed 16 bit little endian file with 2 channels and 48kHz:
+#
+# ffmpeg -i <infile> -f s16le -ac 2 -ar 48000 -acodec pcm_s16le <outfile>
+
+const SAMPLERATE* = 48000
+const BUFFERSIZE* = 512
+
+type
+  Level* = 0'f .. 1'f
+  Sample* = (int16, int16)
+  SoundData* = seq[Sample]
+  Sound* = ref SoundData
+
+proc sinewave(f: float): proc(x: float): float =
+  proc ret(x: float): float =
+    sin(x * 2 * Pi * f)
+  result = ret
+
+proc sineSoundData*(f: float, len: float): SoundData =
+  let dt = 1'f / float(SAMPLERATE)
+  var sine = sinewave(f)
+  for i in 0 ..< int(SAMPLERATE * len):
+    let t = dt * float(i)
+    let value = int16(sine(t) * float(high(int16)))
+    result.add (value, value)
+
+proc newSound*(data: SoundData): Sound =
+  result = new Sound
+  result[] = data
--- a/src/semicongine/engine.nim	Sun Apr 30 01:02:33 2023 +0700
+++ b/src/semicongine/engine.nim	Mon May 01 01:15:39 2023 +0700
@@ -10,7 +10,6 @@
 import ./gpu_data
 import ./entity
 import ./renderer
-import ./mesh
 import ./events
 import ./config
 import ./math
--- a/src/semicongine/platform/linux/audio.nim	Sun Apr 30 01:02:33 2023 +0700
+++ b/src/semicongine/platform/linux/audio.nim	Mon May 01 01:15:39 2023 +0700
@@ -0,0 +1,63 @@
+import ../../audiotypes
+
+# alsa API
+type 
+  OpenMode*{.size: sizeof(culong).} = enum
+    SND_PCM_BLOCK = 0x00000000 # added by semicongine, for clarity
+    SND_PCM_NONBLOCK = 0x00000001
+  StreamMode* {.size: sizeof(cint).} = enum
+    SND_PCM_STREAM_PLAYBACK = 0
+  AccessMode*{.size: sizeof(cint).} = enum 
+    SND_PCM_ACCESS_RW_INTERLEAVED = 3
+  PCMFormat*{.size: sizeof(cint).} = enum
+    SND_PCM_FORMAT_S16_LE = 2
+  snd_pcm_p* = ptr object
+  snd_pcm_hw_params_p* = ptr object
+  snd_pcm_uframes_t* = culong
+  snd_pcm_sframes_t* = clong
+{.pragma: alsafunc, importc, cdecl, dynlib: "libasound.so" .}
+proc snd_pcm_open*(pcm_ref: ptr snd_pcm_p, name: cstring, streamMode: StreamMode, openmode: OpenMode): cint {.alsafunc.}
+proc snd_pcm_close*(pcm: snd_pcm_p): cint {.alsafunc.}
+proc snd_pcm_hw_params_malloc*(hw_params_ptr: ptr snd_pcm_hw_params_p): cint {.alsafunc.}
+proc snd_pcm_hw_params_free*(hw_params: snd_pcm_hw_params_p) {.alsafunc.}
+proc snd_pcm_hw_params_any*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p): cint {.alsafunc.}
+proc snd_pcm_hw_params_set_access*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p, mode: AccessMode): cint {.alsafunc.}
+proc snd_pcm_hw_params_set_format*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p, format: PCMFormat): cint {.alsafunc.}
+proc snd_pcm_hw_params_set_channels*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p, val: cuint): cint {.alsafunc.}
+proc snd_pcm_hw_params_set_buffer_size*(pcm: snd_pcm_p, params: snd_pcm_hw_params_p, size: snd_pcm_uframes_t): cint {.alsafunc.}
+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.}
+
+template checkAlsaResult*(call: untyped) =
+  let value = call
+  if value < 0:
+    raise newException(Exception, "Alsa error: " & astToStr(call) &
+      " returned " & $value)
+
+# required for engine:
+
+type
+  NativeSoundDevice* = object
+    handle: snd_pcm_p
+ 
+proc openSoundDevice*(sampleRate: uint32, bufferSize: uint32): 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)
+
+  # hw parameters, quiet a bit of hardcoding here
+  checkAlsaResult snd_pcm_hw_params_malloc(addr hw_params)
+  checkAlsaResult snd_pcm_hw_params_any(result.handle, hw_params)
+  checkAlsaResult snd_pcm_hw_params_set_access(result.handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)
+  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(result.handle, hw_params)
+  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)))
+
+proc closeSoundDevice*(soundDevice: NativeSoundDevice) =
+  checkAlsaResult snd_pcm_close(soundDevice.handle)