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)) |