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