Mercurial > games > semicongine
changeset 632:42ad7e6208e9
add: audio subsystem, windows backend still missing
author | Sam <sam@basx.dev> |
---|---|
date | Mon, 01 May 2023 23:55:07 +0700 |
parents | 2c106a77ada3 |
children | 1f2cc5837dff |
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()