diff options
Diffstat (limited to 'quantum/audio/audio.c')
-rw-r--r-- | quantum/audio/audio.c | 539 |
1 files changed, 539 insertions, 0 deletions
diff --git a/quantum/audio/audio.c b/quantum/audio/audio.c new file mode 100644 index 000000000..46277dd70 --- /dev/null +++ b/quantum/audio/audio.c | |||
@@ -0,0 +1,539 @@ | |||
1 | /* Copyright 2016-2020 Jack Humbert | ||
2 | * Copyright 2020 JohSchneider | ||
3 | |||
4 | * This program is free software: you can redistribute it and/or modify | ||
5 | * it under the terms of the GNU General Public License as published by | ||
6 | * the Free Software Foundation, either version 2 of the License, or | ||
7 | * (at your option) any later version. | ||
8 | * | ||
9 | * This program is distributed in the hope that it will be useful, | ||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | * GNU General Public License for more details. | ||
13 | * | ||
14 | * You should have received a copy of the GNU General Public License | ||
15 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
16 | */ | ||
17 | #include "audio.h" | ||
18 | #include "eeconfig.h" | ||
19 | #include "timer.h" | ||
20 | #include "wait.h" | ||
21 | |||
22 | /* audio system: | ||
23 | * | ||
24 | * audio.[ch] takes care of all overall state, tracking the actively playing | ||
25 | * notes/tones; the notes a SONG consists of; | ||
26 | * ... | ||
27 | * = everything audio-related that is platform agnostic | ||
28 | * | ||
29 | * driver_[avr|chibios]_[dac|pwm] take care of the lower hardware dependent parts, | ||
30 | * specific to each platform and the used subsystem/driver to drive | ||
31 | * the output pins/channels with the calculated frequencies for each | ||
32 | * active tone | ||
33 | * as part of this, the driver has to trigger regular state updates by | ||
34 | * calling 'audio_update_state' through some sort of timer - be it a | ||
35 | * dedicated one or piggybacking on for example the timer used to | ||
36 | * generate a pwm signal/clock. | ||
37 | * | ||
38 | * | ||
39 | * A Note on terminology: | ||
40 | * tone, pitch and frequency are used somewhat interchangeably, in a strict Wikipedia-sense: | ||
41 | * "(Musical) tone, a sound characterized by its duration, pitch (=frequency), | ||
42 | * intensity (=volume), and timbre" | ||
43 | * - intensity/volume is currently not handled at all, although the 'dac_additive' driver could do so | ||
44 | * - timbre is handled globally (TODO: only used with the pwm drivers at the moment) | ||
45 | * | ||
46 | * in musical_note.h a 'note' is the combination of a pitch and a duration | ||
47 | * these are used to create SONG arrays; during playback their frequencies | ||
48 | * are handled as single successive tones, while the durations are | ||
49 | * kept track of in 'audio_update_state' | ||
50 | * | ||
51 | * 'voice' as it is used here, equates to a sort of instrument with its own | ||
52 | * characteristics sound and effects | ||
53 | * the audio system as-is deals only with (possibly multiple) tones of one | ||
54 | * instrument/voice at a time (think: chords). since the number of tones that | ||
55 | * can be reproduced depends on the hardware/driver in use: pwm can only | ||
56 | * reproduce one tone per output/speaker; DACs can reproduce/mix multiple | ||
57 | * when doing additive synthesis. | ||
58 | * | ||
59 | * 'duration' can either be in the beats-per-minute related unit found in | ||
60 | * musical_notes.h, OR in ms; keyboards create SONGs with the former, while | ||
61 | * the internal state of the audio system does its calculations with the later - ms | ||
62 | */ | ||
63 | |||
64 | #ifndef AUDIO_TONE_STACKSIZE | ||
65 | # define AUDIO_TONE_STACKSIZE 8 | ||
66 | #endif | ||
67 | uint8_t active_tones = 0; // number of tones pushed onto the stack by audio_play_tone - might be more than the hardware is able to reproduce at any single time | ||
68 | musical_tone_t tones[AUDIO_TONE_STACKSIZE]; // stack of currently active tones | ||
69 | |||
70 | bool playing_melody = false; // playing a SONG? | ||
71 | bool playing_note = false; // or (possibly multiple simultaneous) tones | ||
72 | bool state_changed = false; // global flag, which is set if anything changes with the active_tones | ||
73 | |||
74 | // melody/SONG related state variables | ||
75 | float (*notes_pointer)[][2]; // SONG, an array of MUSICAL_NOTEs | ||
76 | uint16_t notes_count; // length of the notes_pointer array | ||
77 | bool notes_repeat; // PLAY_SONG or PLAY_LOOP? | ||
78 | uint16_t melody_current_note_duration = 0; // duration of the currently playing note from the active melody, in ms | ||
79 | uint8_t note_tempo = TEMPO_DEFAULT; // beats-per-minute | ||
80 | uint16_t current_note = 0; // index into the array at notes_pointer | ||
81 | bool note_resting = false; // if a short pause was introduced between two notes with the same frequency while playing a melody | ||
82 | uint16_t last_timestamp = 0; | ||
83 | |||
84 | #ifdef AUDIO_ENABLE_TONE_MULTIPLEXING | ||
85 | # ifndef AUDIO_MAX_SIMULTANEOUS_TONES | ||
86 | # define AUDIO_MAX_SIMULTANEOUS_TONES 3 | ||
87 | # endif | ||
88 | uint16_t tone_multiplexing_rate = AUDIO_TONE_MULTIPLEXING_RATE_DEFAULT; | ||
89 | uint8_t tone_multiplexing_index_shift = 0; // offset used on active-tone array access | ||
90 | #endif | ||
91 | |||
92 | // provided and used by voices.c | ||
93 | extern uint8_t note_timbre; | ||
94 | extern bool glissando; | ||
95 | extern bool vibrato; | ||
96 | extern uint16_t voices_timer; | ||
97 | |||
98 | #ifndef STARTUP_SONG | ||
99 | # define STARTUP_SONG SONG(STARTUP_SOUND) | ||
100 | #endif | ||
101 | #ifndef AUDIO_ON_SONG | ||
102 | # define AUDIO_ON_SONG SONG(AUDIO_ON_SOUND) | ||
103 | #endif | ||
104 | #ifndef AUDIO_OFF_SONG | ||
105 | # define AUDIO_OFF_SONG SONG(AUDIO_OFF_SOUND) | ||
106 | #endif | ||
107 | float startup_song[][2] = STARTUP_SONG; | ||
108 | float audio_on_song[][2] = AUDIO_ON_SONG; | ||
109 | float audio_off_song[][2] = AUDIO_OFF_SONG; | ||
110 | |||
111 | static bool audio_initialized = false; | ||
112 | static bool audio_driver_stopped = true; | ||
113 | audio_config_t audio_config; | ||
114 | |||
115 | void audio_init() { | ||
116 | if (audio_initialized) { | ||
117 | return; | ||
118 | } | ||
119 | |||
120 | // Check EEPROM | ||
121 | #ifdef EEPROM_ENABLE | ||
122 | if (!eeconfig_is_enabled()) { | ||
123 | eeconfig_init(); | ||
124 | } | ||
125 | audio_config.raw = eeconfig_read_audio(); | ||
126 | #else // EEPROM settings | ||
127 | audio_config.enable = true; | ||
128 | # ifdef AUDIO_CLICKY_ON | ||
129 | audio_config.clicky_enable = true; | ||
130 | # endif | ||
131 | #endif // EEPROM settings | ||
132 | |||
133 | for (uint8_t i = 0; i < AUDIO_TONE_STACKSIZE; i++) { | ||
134 | tones[i] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0}; | ||
135 | } | ||
136 | |||
137 | if (!audio_initialized) { | ||
138 | audio_driver_initialize(); | ||
139 | audio_initialized = true; | ||
140 | } | ||
141 | stop_all_notes(); | ||
142 | } | ||
143 | |||
144 | void audio_startup(void) { | ||
145 | if (audio_config.enable) { | ||
146 | PLAY_SONG(startup_song); | ||
147 | } | ||
148 | |||
149 | last_timestamp = timer_read(); | ||
150 | } | ||
151 | |||
152 | void audio_toggle(void) { | ||
153 | if (audio_config.enable) { | ||
154 | stop_all_notes(); | ||
155 | } | ||
156 | audio_config.enable ^= 1; | ||
157 | eeconfig_update_audio(audio_config.raw); | ||
158 | if (audio_config.enable) { | ||
159 | audio_on_user(); | ||
160 | } | ||
161 | } | ||
162 | |||
163 | void audio_on(void) { | ||
164 | audio_config.enable = 1; | ||
165 | eeconfig_update_audio(audio_config.raw); | ||
166 | audio_on_user(); | ||
167 | PLAY_SONG(audio_on_song); | ||
168 | } | ||
169 | |||
170 | void audio_off(void) { | ||
171 | PLAY_SONG(audio_off_song); | ||
172 | wait_ms(100); | ||
173 | audio_stop_all(); | ||
174 | audio_config.enable = 0; | ||
175 | eeconfig_update_audio(audio_config.raw); | ||
176 | } | ||
177 | |||
178 | bool audio_is_on(void) { return (audio_config.enable != 0); } | ||
179 | |||
180 | void audio_stop_all() { | ||
181 | if (audio_driver_stopped) { | ||
182 | return; | ||
183 | } | ||
184 | |||
185 | active_tones = 0; | ||
186 | |||
187 | audio_driver_stop(); | ||
188 | |||
189 | playing_melody = false; | ||
190 | playing_note = false; | ||
191 | |||
192 | melody_current_note_duration = 0; | ||
193 | |||
194 | for (uint8_t i = 0; i < AUDIO_TONE_STACKSIZE; i++) { | ||
195 | tones[i] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0}; | ||
196 | } | ||
197 | |||
198 | audio_driver_stopped = true; | ||
199 | } | ||
200 | |||
201 | void audio_stop_tone(float pitch) { | ||
202 | if (pitch < 0.0f) { | ||
203 | pitch = -1 * pitch; | ||
204 | } | ||
205 | |||
206 | if (playing_note) { | ||
207 | if (!audio_initialized) { | ||
208 | audio_init(); | ||
209 | } | ||
210 | bool found = false; | ||
211 | for (int i = AUDIO_TONE_STACKSIZE - 1; i >= 0; i--) { | ||
212 | found = (tones[i].pitch == pitch); | ||
213 | if (found) { | ||
214 | tones[i] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0}; | ||
215 | for (int j = i; (j < AUDIO_TONE_STACKSIZE - 1); j++) { | ||
216 | tones[j] = tones[j + 1]; | ||
217 | tones[j + 1] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0}; | ||
218 | } | ||
219 | break; | ||
220 | } | ||
221 | } | ||
222 | if (!found) { | ||
223 | return; | ||
224 | } | ||
225 | |||
226 | state_changed = true; | ||
227 | active_tones--; | ||
228 | if (active_tones < 0) active_tones = 0; | ||
229 | #ifdef AUDIO_ENABLE_TONE_MULTIPLEXING | ||
230 | if (tone_multiplexing_index_shift >= active_tones) { | ||
231 | tone_multiplexing_index_shift = 0; | ||
232 | } | ||
233 | #endif | ||
234 | if (active_tones == 0) { | ||
235 | audio_driver_stop(); | ||
236 | audio_driver_stopped = true; | ||
237 | playing_note = false; | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | void audio_play_note(float pitch, uint16_t duration) { | ||
243 | if (!audio_config.enable) { | ||
244 | return; | ||
245 | } | ||
246 | |||
247 | if (!audio_initialized) { | ||
248 | audio_init(); | ||
249 | } | ||
250 | |||
251 | if (pitch < 0.0f) { | ||
252 | pitch = -1 * pitch; | ||
253 | } | ||
254 | |||
255 | // round-robin: shifting out old tones, keeping only unique ones | ||
256 | // if the new frequency is already amongst the active tones, shift it to the top of the stack | ||
257 | bool found = false; | ||
258 | for (int i = active_tones - 1; i >= 0; i--) { | ||
259 | found = (tones[i].pitch == pitch); | ||
260 | if (found) { | ||
261 | for (int j = i; (j < active_tones - 1); j++) { | ||
262 | tones[j] = tones[j + 1]; | ||
263 | tones[j + 1] = (musical_tone_t){.time_started = timer_read(), .pitch = pitch, .duration = duration}; | ||
264 | } | ||
265 | return; // since this frequency played already, the hardware was already started | ||
266 | } | ||
267 | } | ||
268 | |||
269 | // frequency/tone is actually new, so we put it on the top of the stack | ||
270 | active_tones++; | ||
271 | if (active_tones > AUDIO_TONE_STACKSIZE) { | ||
272 | active_tones = AUDIO_TONE_STACKSIZE; | ||
273 | // shift out the oldest tone to make room | ||
274 | for (int i = 0; i < active_tones - 1; i++) { | ||
275 | tones[i] = tones[i + 1]; | ||
276 | } | ||
277 | } | ||
278 | state_changed = true; | ||
279 | playing_note = true; | ||
280 | tones[active_tones - 1] = (musical_tone_t){.time_started = timer_read(), .pitch = pitch, .duration = duration}; | ||
281 | |||
282 | // TODO: needs to be handled per note/tone -> use its timestamp instead? | ||
283 | voices_timer = timer_read(); // reset to zero, for the effects added by voices.c | ||
284 | |||
285 | if (audio_driver_stopped) { | ||
286 | audio_driver_start(); | ||
287 | audio_driver_stopped = false; | ||
288 | } | ||
289 | } | ||
290 | |||
291 | void audio_play_tone(float pitch) { audio_play_note(pitch, 0xffff); } | ||
292 | |||
293 | void audio_play_melody(float (*np)[][2], uint16_t n_count, bool n_repeat) { | ||
294 | if (!audio_config.enable) { | ||
295 | audio_stop_all(); | ||
296 | return; | ||
297 | } | ||
298 | |||
299 | if (!audio_initialized) { | ||
300 | audio_init(); | ||
301 | } | ||
302 | |||
303 | // Cancel note if a note is playing | ||
304 | if (playing_note) audio_stop_all(); | ||
305 | |||
306 | playing_melody = true; | ||
307 | note_resting = false; | ||
308 | |||
309 | notes_pointer = np; | ||
310 | notes_count = n_count; | ||
311 | notes_repeat = n_repeat; | ||
312 | |||
313 | current_note = 0; // note in the melody-array/list at note_pointer | ||
314 | |||
315 | // start first note manually, which also starts the audio_driver | ||
316 | // all following/remaining notes are played by 'audio_update_state' | ||
317 | audio_play_note((*notes_pointer)[current_note][0], audio_duration_to_ms((*notes_pointer)[current_note][1])); | ||
318 | last_timestamp = timer_read(); | ||
319 | melody_current_note_duration = audio_duration_to_ms((*notes_pointer)[current_note][1]); | ||
320 | } | ||
321 | |||
322 | float click[2][2]; | ||
323 | void audio_play_click(uint16_t delay, float pitch, uint16_t duration) { | ||
324 | uint16_t duration_tone = audio_ms_to_duration(duration); | ||
325 | uint16_t duration_delay = audio_ms_to_duration(delay); | ||
326 | |||
327 | if (delay <= 0.0f) { | ||
328 | click[0][0] = pitch; | ||
329 | click[0][1] = duration_tone; | ||
330 | click[1][0] = 0.0f; | ||
331 | click[1][1] = 0.0f; | ||
332 | audio_play_melody(&click, 1, false); | ||
333 | } else { | ||
334 | // first note is a rest/pause | ||
335 | click[0][0] = 0.0f; | ||
336 | click[0][1] = duration_delay; | ||
337 | // second note is the actual click | ||
338 | click[1][0] = pitch; | ||
339 | click[1][1] = duration_tone; | ||
340 | audio_play_melody(&click, 2, false); | ||
341 | } | ||
342 | } | ||
343 | |||
344 | bool audio_is_playing_note(void) { return playing_note; } | ||
345 | |||
346 | bool audio_is_playing_melody(void) { return playing_melody; } | ||
347 | |||
348 | uint8_t audio_get_number_of_active_tones(void) { return active_tones; } | ||
349 | |||
350 | float audio_get_frequency(uint8_t tone_index) { | ||
351 | if (tone_index >= active_tones) { | ||
352 | return 0.0f; | ||
353 | } | ||
354 | return tones[active_tones - tone_index - 1].pitch; | ||
355 | } | ||
356 | |||
357 | float audio_get_processed_frequency(uint8_t tone_index) { | ||
358 | if (tone_index >= active_tones) { | ||
359 | return 0.0f; | ||
360 | } | ||
361 | |||
362 | int8_t index = active_tones - tone_index - 1; | ||
363 | // new tones are stacked on top (= appended at the end), so the most recent/current is MAX-1 | ||
364 | |||
365 | #ifdef AUDIO_ENABLE_TONE_MULTIPLEXING | ||
366 | index = index - tone_multiplexing_index_shift; | ||
367 | if (index < 0) // wrap around | ||
368 | index += active_tones; | ||
369 | #endif | ||
370 | |||
371 | if (tones[index].pitch <= 0.0f) { | ||
372 | return 0.0f; | ||
373 | } | ||
374 | |||
375 | return voice_envelope(tones[index].pitch); | ||
376 | } | ||
377 | |||
378 | bool audio_update_state(void) { | ||
379 | if (!playing_note && !playing_melody) { | ||
380 | return false; | ||
381 | } | ||
382 | |||
383 | bool goto_next_note = false; | ||
384 | uint16_t current_time = timer_read(); | ||
385 | |||
386 | if (playing_melody) { | ||
387 | goto_next_note = timer_elapsed(last_timestamp) >= melody_current_note_duration; | ||
388 | if (goto_next_note) { | ||
389 | uint16_t delta = timer_elapsed(last_timestamp) - melody_current_note_duration; | ||
390 | last_timestamp = current_time; | ||
391 | uint16_t previous_note = current_note; | ||
392 | current_note++; | ||
393 | voices_timer = timer_read(); // reset to zero, for the effects added by voices.c | ||
394 | |||
395 | if (current_note >= notes_count) { | ||
396 | if (notes_repeat) { | ||
397 | current_note = 0; | ||
398 | } else { | ||
399 | audio_stop_all(); | ||
400 | return false; | ||
401 | } | ||
402 | } | ||
403 | |||
404 | if (!note_resting && (*notes_pointer)[previous_note][0] == (*notes_pointer)[current_note][0]) { | ||
405 | note_resting = true; | ||
406 | |||
407 | // special handling for successive notes of the same frequency: | ||
408 | // insert a short pause to separate them audibly | ||
409 | audio_play_note(0.0f, audio_duration_to_ms(2)); | ||
410 | current_note = previous_note; | ||
411 | melody_current_note_duration = audio_duration_to_ms(2); | ||
412 | |||
413 | } else { | ||
414 | note_resting = false; | ||
415 | |||
416 | // TODO: handle glissando here (or remember previous and current tone) | ||
417 | /* there would need to be a freq(here we are) -> freq(next note) | ||
418 | * and do slide/glissando in between problem here is to know which | ||
419 | * frequency on the stack relates to what other? e.g. a melody starts | ||
420 | * tones in a sequence, and stops expiring one, so the most recently | ||
421 | * stopped is the starting point for a glissando to the most recently started? | ||
422 | * how to detect and preserve this relation? | ||
423 | * and what about user input, chords, ...? | ||
424 | */ | ||
425 | |||
426 | // '- delta': Skip forward in the next note's length if we've over shot | ||
427 | // the last, so the overall length of the song is the same | ||
428 | uint16_t duration = audio_duration_to_ms((*notes_pointer)[current_note][1]); | ||
429 | |||
430 | // Skip forward past any completely missed notes | ||
431 | while (delta > duration && current_note < notes_count - 1) { | ||
432 | delta -= duration; | ||
433 | current_note++; | ||
434 | duration = audio_duration_to_ms((*notes_pointer)[current_note][1]); | ||
435 | } | ||
436 | |||
437 | if (delta < duration) { | ||
438 | duration -= delta; | ||
439 | } else { | ||
440 | // Only way to get here is if it is the last note and | ||
441 | // we have completely missed it. Play it for 1ms... | ||
442 | duration = 1; | ||
443 | } | ||
444 | |||
445 | audio_play_note((*notes_pointer)[current_note][0], duration); | ||
446 | melody_current_note_duration = duration; | ||
447 | } | ||
448 | } | ||
449 | } | ||
450 | |||
451 | if (playing_note) { | ||
452 | #ifdef AUDIO_ENABLE_TONE_MULTIPLEXING | ||
453 | tone_multiplexing_index_shift = (int)(current_time / tone_multiplexing_rate) % MIN(AUDIO_MAX_SIMULTANEOUS_TONES, active_tones); | ||
454 | goto_next_note = true; | ||
455 | #endif | ||
456 | if (vibrato || glissando) { | ||
457 | // force update on each cycle, since vibrato shifts the frequency slightly | ||
458 | goto_next_note = true; | ||
459 | } | ||
460 | |||
461 | // housekeeping: stop notes that have no playtime left | ||
462 | for (int i = 0; i < active_tones; i++) { | ||
463 | if ((tones[i].duration != 0xffff) // indefinitely playing notes, started by 'audio_play_tone' | ||
464 | && (tones[i].duration != 0) // 'uninitialized' | ||
465 | ) { | ||
466 | if (timer_elapsed(tones[i].time_started) >= tones[i].duration) { | ||
467 | audio_stop_tone(tones[i].pitch); // also sets 'state_changed=true' | ||
468 | } | ||
469 | } | ||
470 | } | ||
471 | } | ||
472 | |||
473 | // state-changes have a higher priority, always triggering the hardware to update | ||
474 | if (state_changed) { | ||
475 | state_changed = false; | ||
476 | return true; | ||
477 | } | ||
478 | |||
479 | return goto_next_note; | ||
480 | } | ||
481 | |||
482 | // Tone-multiplexing functions | ||
483 | #ifdef AUDIO_ENABLE_TONE_MULTIPLEXING | ||
484 | void audio_set_tone_multiplexing_rate(uint16_t rate) { tone_multiplexing_rate = rate; } | ||
485 | void audio_enable_tone_multiplexing(void) { tone_multiplexing_rate = AUDIO_TONE_MULTIPLEXING_RATE_DEFAULT; } | ||
486 | void audio_disable_tone_multiplexing(void) { tone_multiplexing_rate = 0; } | ||
487 | void audio_increase_tone_multiplexing_rate(uint16_t change) { | ||
488 | if ((0xffff - change) > tone_multiplexing_rate) { | ||
489 | tone_multiplexing_rate += change; | ||
490 | } | ||
491 | } | ||
492 | void audio_decrease_tone_multiplexing_rate(uint16_t change) { | ||
493 | if (change <= tone_multiplexing_rate) { | ||
494 | tone_multiplexing_rate -= change; | ||
495 | } | ||
496 | } | ||
497 | #endif | ||
498 | |||
499 | // Tempo functions | ||
500 | |||
501 | void audio_set_tempo(uint8_t tempo) { | ||
502 | if (tempo < 10) note_tempo = 10; | ||
503 | // else if (tempo > 250) | ||
504 | // note_tempo = 250; | ||
505 | else | ||
506 | note_tempo = tempo; | ||
507 | } | ||
508 | |||
509 | void audio_increase_tempo(uint8_t tempo_change) { | ||
510 | if (tempo_change > 255 - note_tempo) | ||
511 | note_tempo = 255; | ||
512 | else | ||
513 | note_tempo += tempo_change; | ||
514 | } | ||
515 | |||
516 | void audio_decrease_tempo(uint8_t tempo_change) { | ||
517 | if (tempo_change >= note_tempo - 10) | ||
518 | note_tempo = 10; | ||
519 | else | ||
520 | note_tempo -= tempo_change; | ||
521 | } | ||
522 | |||
523 | // TODO in the int-math version are some bugs; songs sometimes abruptly end - maybe an issue with the timer/system-tick wrapping around? | ||
524 | uint16_t audio_duration_to_ms(uint16_t duration_bpm) { | ||
525 | #if defined(__AVR__) | ||
526 | // doing int-math saves us some bytes in the overall firmware size, but the intermediate result is less accurate before being cast to/returned as uint | ||
527 | return ((uint32_t)duration_bpm * 60 * 1000) / (64 * note_tempo); | ||
528 | // NOTE: beware of uint16_t overflows when note_tempo is low and/or the duration is long | ||
529 | #else | ||
530 | return ((float)duration_bpm * 60) / (64 * note_tempo) * 1000; | ||
531 | #endif | ||
532 | } | ||
533 | uint16_t audio_ms_to_duration(uint16_t duration_ms) { | ||
534 | #if defined(__AVR__) | ||
535 | return ((uint32_t)duration_ms * 64 * note_tempo) / 60 / 1000; | ||
536 | #else | ||
537 | return ((float)duration_ms * 64 * note_tempo) / 60 / 1000; | ||
538 | #endif | ||
539 | } | ||