# HG changeset patch # User Sam # Date 1682878539 -25200 # Node ID c2f04f016104767e39572f579bbcff9c878157be # Parent 34e536f7001f60089f31de8bba8edcd2322fc9b9 add: initial implementation of audio subsystem with mixer, currently missing windows implementation diff -r 34e536f7001f -r c2f04f016104 src/semicongine.nim --- 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 diff -r 34e536f7001f -r c2f04f016104 src/semicongine/audio.nim --- 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() diff -r 34e536f7001f -r c2f04f016104 src/semicongine/audiotypes.nim --- /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 -f s16le -ac 2 -ar 48000 -acodec pcm_s16le + +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 diff -r 34e536f7001f -r c2f04f016104 src/semicongine/engine.nim --- 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 diff -r 34e536f7001f -r c2f04f016104 src/semicongine/platform/linux/audio.nim --- 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)