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