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