Mercurial > games > semicongine
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 1223:55896320c8bf | 1224:a3fa15c25026 |
|---|---|
| 1 | |
| 2 const NBUFFERS = 32 | |
| 3 const BUFFERSAMPLECOUNT = 256 | |
| 4 const AUDIO_SAMPLE_RATE* = 44100 | |
| 5 | |
| 6 type | |
| 7 Level* = 0'f .. 1'f | |
| 8 Sample* = array[2, int16] | |
| 9 SoundData* = seq[Sample] | |
| 10 | |
| 11 Playback = object | |
| 12 sound: SoundData | |
| 13 position: int | |
| 14 loop: bool | |
| 15 levelLeft: Level | |
| 16 levelRight: Level | |
| 17 paused: bool | |
| 18 Track = object | |
| 19 playing: Table[uint64, Playback] | |
| 20 level: Level | |
| 21 targetLevel: Level | |
| 22 fadeTime: float | |
| 23 fadeStep: float | |
| 24 | |
| 25 when defined(windows): | |
| 26 include ./platform/windows | |
| 27 when defined(linux): | |
| 28 include ./platform/linux | |
| 29 | |
| 30 type | |
| 31 Mixer* = object | |
| 32 playbackCounter: uint64 | |
| 33 tracks: Table[string, Track] | |
| 34 sounds*: Table[string, SoundData] | |
| 35 level: Level | |
| 36 device: NativeSoundDevice | |
| 37 lock: Lock | |
| 38 buffers: seq[SoundData] | |
| 39 currentBuffer: int | |
| 40 lastUpdate: MonoTime | |
| 41 | |
| 42 proc initMixer(): Mixer = | |
| 43 result = Mixer( | |
| 44 tracks: {"": Track(level: 1'f)}.toTable, | |
| 45 level: 1'f, | |
| 46 ) | |
| 47 result.lock.initLock() | |
| 48 | |
| 49 proc setupDevice(mixer: var Mixer) = | |
| 50 # call this inside audio thread | |
| 51 var bufferaddresses: seq[ptr SoundData] | |
| 52 for i in 0 ..< NBUFFERS: | |
| 53 mixer.buffers.add newSeq[Sample](BUFFERSAMPLECOUNT) | |
| 54 for i in 0 ..< mixer.buffers.len: | |
| 55 bufferaddresses.add (addr mixer.buffers[i]) | |
| 56 mixer.device = OpenSoundDevice(AUDIO_SAMPLE_RATE, bufferaddresses) | |
| 57 | |
| 58 # TODO: this should probably be in the load-code-stuff | |
| 59 # proc LoadSound*(mixer: var Mixer, name: string, resource: string) = | |
| 60 # assert not (name in mixer.sounds) | |
| 61 # mixer.sounds[name] = LoadAudio(resource) | |
| 62 | |
| 63 proc AddSound*(mixer: var Mixer, name: string, sound: SoundData) = | |
| 64 assert not (name in mixer.sounds) | |
| 65 mixer.sounds[name] = sound | |
| 66 | |
| 67 proc ReplaceSound*(mixer: var Mixer, name: string, sound: SoundData) = | |
| 68 assert (name in mixer.sounds) | |
| 69 mixer.sounds[name] = sound | |
| 70 | |
| 71 proc AddTrack*(mixer: var Mixer, name: string, level: Level = 1'f) = | |
| 72 assert not (name in mixer.tracks) | |
| 73 mixer.lock.withLock(): | |
| 74 mixer.tracks[name] = Track(level: level) | |
| 75 | |
| 76 proc Play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, levelLeft, levelRight: Level): uint64 = | |
| 77 assert track in mixer.tracks, &"Track '{track}' does not exists" | |
| 78 assert soundName in mixer.sounds, soundName & " not loaded" | |
| 79 mixer.lock.withLock(): | |
| 80 if stopOtherSounds: | |
| 81 mixer.tracks[track].playing.clear() | |
| 82 mixer.tracks[track].playing[mixer.playbackCounter] = Playback( | |
| 83 sound: mixer.sounds[soundName], | |
| 84 position: 0, | |
| 85 loop: loop, | |
| 86 levelLeft: levelLeft, | |
| 87 levelRight: levelRight, | |
| 88 paused: false, | |
| 89 ) | |
| 90 result = mixer.playbackCounter | |
| 91 inc mixer.playbackCounter | |
| 92 | |
| 93 proc Play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, level: Level = 1'f): uint64 = | |
| 94 Play( | |
| 95 mixer = mixer, | |
| 96 soundName = soundName, | |
| 97 track = track, | |
| 98 stopOtherSounds = stopOtherSounds, | |
| 99 loop = loop, | |
| 100 levelLeft = level, | |
| 101 levelRight = level | |
| 102 ) | |
| 103 | |
| 104 proc Stop*(mixer: var Mixer) = | |
| 105 mixer.lock.withLock(): | |
| 106 for track in mixer.tracks.mvalues: | |
| 107 track.playing.clear() | |
| 108 | |
| 109 proc GetLevel*(mixer: var Mixer): Level = mixer.level | |
| 110 proc GetLevel*(mixer: var Mixer, track: string): Level = mixer.tracks[track].level | |
| 111 proc GetLevel*(mixer: var Mixer, playbackId: uint64): (Level, Level) = | |
| 112 for track in mixer.tracks.mvalues: | |
| 113 if playbackId in track.playing: | |
| 114 return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight) | |
| 115 | |
| 116 proc SetLevel*(mixer: var Mixer, level: Level) = mixer.level = level | |
| 117 proc SetLevel*(mixer: var Mixer, track: string, level: Level) = | |
| 118 mixer.lock.withLock(): | |
| 119 mixer.tracks[track].level = level | |
| 120 proc SetLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) = | |
| 121 mixer.lock.withLock(): | |
| 122 for track in mixer.tracks.mvalues: | |
| 123 if playbackId in track.playing: | |
| 124 track.playing[playbackId].levelLeft = levelLeft | |
| 125 track.playing[playbackId].levelRight = levelRight | |
| 126 proc SetLevel*(mixer: var Mixer, playbackId: uint64, level: Level) = | |
| 127 SetLevel(mixer, playbackId, level, level) | |
| 128 | |
| 129 proc Stop*(mixer: var Mixer, track: string) = | |
| 130 assert track in mixer.tracks | |
| 131 mixer.lock.withLock(): | |
| 132 mixer.tracks[track].playing.clear() | |
| 133 | |
| 134 proc Stop*(mixer: var Mixer, playbackId: uint64) = | |
| 135 mixer.lock.withLock(): | |
| 136 for track in mixer.tracks.mvalues: | |
| 137 if playbackId in track.playing: | |
| 138 track.playing.del(playbackId) | |
| 139 break | |
| 140 | |
| 141 proc Pause*(mixer: var Mixer, value: bool) = | |
| 142 mixer.lock.withLock(): | |
| 143 for track in mixer.tracks.mvalues: | |
| 144 for playback in track.playing.mvalues: | |
| 145 playback.paused = value | |
| 146 | |
| 147 proc Pause*(mixer: var Mixer, track: string, value: bool) = | |
| 148 mixer.lock.withLock(): | |
| 149 for playback in mixer.tracks[track].playing.mvalues: | |
| 150 playback.paused = value | |
| 151 | |
| 152 proc Pause*(mixer: var Mixer, playbackId: uint64, value: bool) = | |
| 153 mixer.lock.withLock(): | |
| 154 for track in mixer.tracks.mvalues: | |
| 155 if playbackId in track.playing: | |
| 156 track.playing[playbackId].paused = value | |
| 157 | |
| 158 proc Pause*(mixer: var Mixer) = mixer.Pause(true) | |
| 159 proc Pause*(mixer: var Mixer, track: string) = mixer.Pause(track, true) | |
| 160 proc Pause*(mixer: var Mixer, playbackId: uint64) = mixer.Pause(playbackId, true) | |
| 161 proc Unpause*(mixer: var Mixer) = mixer.Pause(false) | |
| 162 proc Unpause*(mixer: var Mixer, track: string) = mixer.Pause(track, false) | |
| 163 proc Unpause*(mixer: var Mixer, playbackId: uint64) = mixer.Pause(playbackId, false) | |
| 164 | |
| 165 proc FadeTo*(mixer: var Mixer, track: string, level: Level, time: float) = | |
| 166 mixer.tracks[track].targetLevel = level | |
| 167 mixer.tracks[track].fadeTime = time | |
| 168 mixer.tracks[track].fadeStep = level.float - mixer.tracks[track].level.float / time | |
| 169 | |
| 170 proc IsPlaying*(mixer: var Mixer): bool = | |
| 171 mixer.lock.withLock(): | |
| 172 for track in mixer.tracks.mvalues: | |
| 173 for playback in track.playing.values: | |
| 174 if not playback.paused: | |
| 175 return true | |
| 176 return false | |
| 177 | |
| 178 proc IsPlaying*(mixer: var Mixer, track: string): bool = | |
| 179 mixer.lock.withLock(): | |
| 180 if mixer.tracks.contains(track): | |
| 181 for playback in mixer.tracks[track].playing.values: | |
| 182 if not playback.paused: | |
| 183 return true | |
| 184 return false | |
| 185 | |
| 186 func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample = | |
| 187 [int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight)] | |
| 188 | |
| 189 func clip(value: int32): int16 = | |
| 190 int16(max(min(int32(high(int16)), value), int32(low(int16)))) | |
| 191 | |
| 192 # used for combining sounds | |
| 193 func mix(a, b: Sample): Sample = | |
| 194 [ | |
| 195 clip(int32(a[0]) + int32(b[0])), | |
| 196 clip(int32(a[1]) + int32(b[1])), | |
| 197 ] | |
| 198 | |
| 199 proc updateSoundBuffer(mixer: var Mixer) = | |
| 200 let t = getMonoTime() | |
| 201 | |
| 202 let tDebug = getTime() | |
| 203 # echo "" | |
| 204 # echo tDebug | |
| 205 | |
| 206 let dt = (t - mixer.lastUpdate).inNanoseconds.float64 / 1_000_000_000'f64 | |
| 207 mixer.lastUpdate = t | |
| 208 | |
| 209 # update fadings | |
| 210 for track in mixer.tracks.mvalues: | |
| 211 if track.fadeTime > 0: | |
| 212 track.fadeTime -= dt | |
| 213 track.level = (track.level.float64 + track.fadeStep.float64 * dt).clamp(Level.low, Level.high) | |
| 214 if track.fadeTime <= 0: | |
| 215 track.level = track.targetLevel | |
| 216 # mix | |
| 217 for i in 0 ..< mixer.buffers[mixer.currentBuffer].len: | |
| 218 var mixedSample = [0'i16, 0'i16] | |
| 219 mixer.lock.withLock(): | |
| 220 for track in mixer.tracks.mvalues: | |
| 221 var stoppedSounds: seq[uint64] | |
| 222 for (id, playback) in track.playing.mpairs: | |
| 223 if playback.paused: | |
| 224 continue | |
| 225 let sample = applyLevel( | |
| 226 playback.sound[playback.position], | |
| 227 mixer.level * track.level * playback.levelLeft, | |
| 228 mixer.level * track.level * playback.levelRight, | |
| 229 ) | |
| 230 mixedSample = mix(mixedSample, sample) | |
| 231 inc playback.position | |
| 232 if playback.position >= playback.sound.len: | |
| 233 if playback.loop: | |
| 234 playback.position = 0 | |
| 235 else: | |
| 236 stoppedSounds.add id | |
| 237 for id in stoppedSounds: | |
| 238 track.playing.del(id) | |
| 239 mixer.buffers[mixer.currentBuffer][i] = mixedSample | |
| 240 # send data to sound device | |
| 241 # echo getTime() - tDebug | |
| 242 mixer.device.WriteSoundData(mixer.currentBuffer) | |
| 243 # echo getTime() - tDebug | |
| 244 mixer.currentBuffer = (mixer.currentBuffer + 1) mod mixer.buffers.len | |
| 245 | |
| 246 # DSP functions | |
| 247 # TODO: finish implementation, one day | |
| 248 | |
| 249 #[ | |
| 250 # | |
| 251 proc lowPassFilter(data: var SoundData, cutoff: int) = | |
| 252 let alpha = float(cutoff) / AUDIO_SAMPLE_RATE | |
| 253 var value = data[0] | |
| 254 for i in 0 ..< data.len: | |
| 255 value[0] += int16(alpha * float(data[i][0] - value[0])) | |
| 256 value[1] += int16(alpha * float(data[i][1] - value[1])) | |
| 257 data[i] = value | |
| 258 | |
| 259 proc downsample(data: var SoundData, n: int) = | |
| 260 let newLen = (data.len - 1) div n + 1 | |
| 261 for i in 0 ..< newLen: | |
| 262 data[i] = data[i * n] | |
| 263 data.setLen(newLen) | |
| 264 | |
| 265 proc upsample(data: var SoundData, m: int) = | |
| 266 data.setLen(data.len * m) | |
| 267 var i = data.len - 1 | |
| 268 while i < 0: | |
| 269 if i mod m == 0: | |
| 270 data[i] = data[i div m] | |
| 271 else: | |
| 272 data[i] = [0, 0] | |
| 273 i.dec | |
| 274 | |
| 275 proc slowdown(data: var SoundData, m, n: int) = | |
| 276 data.upsample(m) | |
| 277 # TODO | |
| 278 # data.lowPassFilter(m) | |
| 279 data.downsample(n) | |
| 280 | |
| 281 ]# | |
| 282 | |
| 283 proc destroy(mixer: var Mixer) = | |
| 284 mixer.lock.deinitLock() | |
| 285 mixer.device.CloseSoundDevice() | |
| 286 | |
| 287 var | |
| 288 mixer* = createShared(Mixer) | |
| 289 audiothread: Thread[void] | |
| 290 | |
| 291 proc audioWorker() {.thread.} = | |
| 292 mixer[].setupDevice() | |
| 293 onThreadDestruction(proc() = mixer[].lock.withLock(mixer[].destroy()); freeShared(mixer)) | |
| 294 while true: | |
| 295 mixer[].updateSoundBuffer() | |
| 296 | |
| 297 # for thread priority (really necessary?) | |
| 298 when defined(windows): | |
| 299 import ./thirdparty/winim/winim/inc/winbase | |
| 300 when defined(linux): | |
| 301 import std/posix | |
| 302 | |
| 303 proc StartMixerThread() = | |
| 304 mixer[] = initMixer() | |
| 305 audiothread.createThread(audioWorker) | |
| 306 debug "Created audio thread" | |
| 307 when defined(windows): | |
| 308 SetThreadPriority(audiothread.handle(), THREAD_PRIORITY_TIME_CRITICAL) | |
| 309 when defined(linux): | |
| 310 discard pthread_setschedprio(Pthread(audiothread.handle()), cint(-20)) |
