Mercurial > games > semicongine
diff semiconginev2/audio/mixer.nim @ 1224:a3fa15c25026 compiletime-tests
did: cleanup, add audio, change platform-dependent structure
author | sam <sam@basx.dev> |
---|---|
date | Wed, 17 Jul 2024 22:02:11 +0700 |
parents | |
children | 841e12f33c47 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/semiconginev2/audio/mixer.nim Wed Jul 17 22:02:11 2024 +0700 @@ -0,0 +1,310 @@ + +const NBUFFERS = 32 +const BUFFERSAMPLECOUNT = 256 +const AUDIO_SAMPLE_RATE* = 44100 + +type + Level* = 0'f .. 1'f + Sample* = array[2, int16] + SoundData* = seq[Sample] + + Playback = object + sound: SoundData + position: int + loop: bool + levelLeft: Level + levelRight: Level + paused: bool + Track = object + playing: Table[uint64, Playback] + level: Level + targetLevel: Level + fadeTime: float + fadeStep: float + +when defined(windows): + include ./platform/windows +when defined(linux): + include ./platform/linux + +type + Mixer* = object + playbackCounter: uint64 + tracks: Table[string, Track] + sounds*: Table[string, SoundData] + level: Level + device: NativeSoundDevice + lock: Lock + buffers: seq[SoundData] + currentBuffer: int + lastUpdate: MonoTime + +proc initMixer(): Mixer = + result = Mixer( + tracks: {"": Track(level: 1'f)}.toTable, + level: 1'f, + ) + result.lock.initLock() + +proc setupDevice(mixer: var Mixer) = + # call this inside audio thread + var bufferaddresses: seq[ptr SoundData] + for i in 0 ..< NBUFFERS: + mixer.buffers.add newSeq[Sample](BUFFERSAMPLECOUNT) + for i in 0 ..< mixer.buffers.len: + bufferaddresses.add (addr mixer.buffers[i]) + mixer.device = OpenSoundDevice(AUDIO_SAMPLE_RATE, bufferaddresses) + +# TODO: this should probably be in the load-code-stuff +# proc LoadSound*(mixer: var Mixer, name: string, resource: string) = + # assert not (name in mixer.sounds) + # mixer.sounds[name] = LoadAudio(resource) + +proc AddSound*(mixer: var Mixer, name: string, sound: SoundData) = + assert not (name in mixer.sounds) + mixer.sounds[name] = sound + +proc ReplaceSound*(mixer: var Mixer, name: string, sound: SoundData) = + assert (name in mixer.sounds) + mixer.sounds[name] = sound + +proc AddTrack*(mixer: var Mixer, name: string, level: Level = 1'f) = + assert not (name in mixer.tracks) + mixer.lock.withLock(): + mixer.tracks[name] = Track(level: level) + +proc Play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, levelLeft, levelRight: Level): uint64 = + assert track in mixer.tracks, &"Track '{track}' does not exists" + assert soundName in mixer.sounds, soundName & " not loaded" + 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, + paused: false, + ) + 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 Pause*(mixer: var Mixer, value: bool) = + mixer.lock.withLock(): + for track in mixer.tracks.mvalues: + for playback in track.playing.mvalues: + playback.paused = value + +proc Pause*(mixer: var Mixer, track: string, value: bool) = + mixer.lock.withLock(): + for playback in mixer.tracks[track].playing.mvalues: + playback.paused = value + +proc Pause*(mixer: var Mixer, playbackId: uint64, value: bool) = + mixer.lock.withLock(): + for track in mixer.tracks.mvalues: + if playbackId in track.playing: + track.playing[playbackId].paused = value + +proc Pause*(mixer: var Mixer) = mixer.Pause(true) +proc Pause*(mixer: var Mixer, track: string) = mixer.Pause(track, true) +proc Pause*(mixer: var Mixer, playbackId: uint64) = mixer.Pause(playbackId, true) +proc Unpause*(mixer: var Mixer) = mixer.Pause(false) +proc Unpause*(mixer: var Mixer, track: string) = mixer.Pause(track, false) +proc Unpause*(mixer: var Mixer, playbackId: uint64) = mixer.Pause(playbackId, false) + +proc FadeTo*(mixer: var Mixer, track: string, level: Level, time: float) = + mixer.tracks[track].targetLevel = level + mixer.tracks[track].fadeTime = time + mixer.tracks[track].fadeStep = level.float - mixer.tracks[track].level.float / time + +proc IsPlaying*(mixer: var Mixer): bool = + mixer.lock.withLock(): + for track in mixer.tracks.mvalues: + for playback in track.playing.values: + if not playback.paused: + return true + return false + +proc IsPlaying*(mixer: var Mixer, track: string): bool = + mixer.lock.withLock(): + if mixer.tracks.contains(track): + for playback in mixer.tracks[track].playing.values: + if not playback.paused: + return true + return false + +func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample = + [int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight)] + +func clip(value: int32): int16 = + int16(max(min(int32(high(int16)), value), int32(low(int16)))) + +# used for combining sounds +func mix(a, b: Sample): Sample = + [ + clip(int32(a[0]) + int32(b[0])), + clip(int32(a[1]) + int32(b[1])), + ] + +proc updateSoundBuffer(mixer: var Mixer) = + let t = getMonoTime() + + let tDebug = getTime() + # echo "" + # echo tDebug + + let dt = (t - mixer.lastUpdate).inNanoseconds.float64 / 1_000_000_000'f64 + mixer.lastUpdate = t + + # update fadings + for track in mixer.tracks.mvalues: + if track.fadeTime > 0: + track.fadeTime -= dt + track.level = (track.level.float64 + track.fadeStep.float64 * dt).clamp(Level.low, Level.high) + if track.fadeTime <= 0: + track.level = track.targetLevel + # mix + for i in 0 ..< mixer.buffers[mixer.currentBuffer].len: + var mixedSample = [0'i16, 0'i16] + mixer.lock.withLock(): + for track in mixer.tracks.mvalues: + var stoppedSounds: seq[uint64] + for (id, playback) in track.playing.mpairs: + if playback.paused: + continue + let sample = applyLevel( + playback.sound[playback.position], + mixer.level * track.level * playback.levelLeft, + mixer.level * track.level * playback.levelRight, + ) + mixedSample = mix(mixedSample, 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) + mixer.buffers[mixer.currentBuffer][i] = mixedSample + # send data to sound device + # echo getTime() - tDebug + mixer.device.WriteSoundData(mixer.currentBuffer) + # echo getTime() - tDebug + mixer.currentBuffer = (mixer.currentBuffer + 1) mod mixer.buffers.len + +# DSP functions +# TODO: finish implementation, one day + +#[ +# +proc lowPassFilter(data: var SoundData, cutoff: int) = + let alpha = float(cutoff) / AUDIO_SAMPLE_RATE + var value = data[0] + for i in 0 ..< data.len: + value[0] += int16(alpha * float(data[i][0] - value[0])) + value[1] += int16(alpha * float(data[i][1] - value[1])) + data[i] = value + + proc downsample(data: var SoundData, n: int) = + let newLen = (data.len - 1) div n + 1 + for i in 0 ..< newLen: + data[i] = data[i * n] + data.setLen(newLen) + + proc upsample(data: var SoundData, m: int) = + data.setLen(data.len * m) + var i = data.len - 1 + while i < 0: + if i mod m == 0: + data[i] = data[i div m] + else: + data[i] = [0, 0] + i.dec + + proc slowdown(data: var SoundData, m, n: int) = + data.upsample(m) + # TODO + # data.lowPassFilter(m) + data.downsample(n) + + ]# + +proc destroy(mixer: var Mixer) = + mixer.lock.deinitLock() + mixer.device.CloseSoundDevice() + +var + mixer* = createShared(Mixer) + audiothread: Thread[void] + +proc audioWorker() {.thread.} = + mixer[].setupDevice() + onThreadDestruction(proc() = mixer[].lock.withLock(mixer[].destroy()); freeShared(mixer)) + while true: + mixer[].updateSoundBuffer() + +# for thread priority (really necessary?) +when defined(windows): + import ./thirdparty/winim/winim/inc/winbase +when defined(linux): + import std/posix + +proc StartMixerThread() = + mixer[] = initMixer() + audiothread.createThread(audioWorker) + debug "Created audio thread" + when defined(windows): + SetThreadPriority(audiothread.handle(), THREAD_PRIORITY_TIME_CRITICAL) + when defined(linux): + discard pthread_setschedprio(Pthread(audiothread.handle()), cint(-20))