540 lines
18 KiB
C
540 lines
18 KiB
C
|
/* Copyright 2016-2020 Jack Humbert
|
||
|
* Copyright 2020 JohSchneider
|
||
|
|
||
|
* This program is free software: you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU General Public License as published by
|
||
|
* the Free Software Foundation, either version 2 of the License, or
|
||
|
* (at your option) any later version.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
#include "audio.h"
|
||
|
#include "eeconfig.h"
|
||
|
#include "timer.h"
|
||
|
#include "wait.h"
|
||
|
|
||
|
/* audio system:
|
||
|
*
|
||
|
* audio.[ch] takes care of all overall state, tracking the actively playing
|
||
|
* notes/tones; the notes a SONG consists of;
|
||
|
* ...
|
||
|
* = everything audio-related that is platform agnostic
|
||
|
*
|
||
|
* driver_[avr|chibios]_[dac|pwm] take care of the lower hardware dependent parts,
|
||
|
* specific to each platform and the used subsystem/driver to drive
|
||
|
* the output pins/channels with the calculated frequencies for each
|
||
|
* active tone
|
||
|
* as part of this, the driver has to trigger regular state updates by
|
||
|
* calling 'audio_update_state' through some sort of timer - be it a
|
||
|
* dedicated one or piggybacking on for example the timer used to
|
||
|
* generate a pwm signal/clock.
|
||
|
*
|
||
|
*
|
||
|
* A Note on terminology:
|
||
|
* tone, pitch and frequency are used somewhat interchangeably, in a strict Wikipedia-sense:
|
||
|
* "(Musical) tone, a sound characterized by its duration, pitch (=frequency),
|
||
|
* intensity (=volume), and timbre"
|
||
|
* - intensity/volume is currently not handled at all, although the 'dac_additive' driver could do so
|
||
|
* - timbre is handled globally (TODO: only used with the pwm drivers at the moment)
|
||
|
*
|
||
|
* in musical_note.h a 'note' is the combination of a pitch and a duration
|
||
|
* these are used to create SONG arrays; during playback their frequencies
|
||
|
* are handled as single successive tones, while the durations are
|
||
|
* kept track of in 'audio_update_state'
|
||
|
*
|
||
|
* 'voice' as it is used here, equates to a sort of instrument with its own
|
||
|
* characteristics sound and effects
|
||
|
* the audio system as-is deals only with (possibly multiple) tones of one
|
||
|
* instrument/voice at a time (think: chords). since the number of tones that
|
||
|
* can be reproduced depends on the hardware/driver in use: pwm can only
|
||
|
* reproduce one tone per output/speaker; DACs can reproduce/mix multiple
|
||
|
* when doing additive synthesis.
|
||
|
*
|
||
|
* 'duration' can either be in the beats-per-minute related unit found in
|
||
|
* musical_notes.h, OR in ms; keyboards create SONGs with the former, while
|
||
|
* the internal state of the audio system does its calculations with the later - ms
|
||
|
*/
|
||
|
|
||
|
#ifndef AUDIO_TONE_STACKSIZE
|
||
|
# define AUDIO_TONE_STACKSIZE 8
|
||
|
#endif
|
||
|
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
|
||
|
musical_tone_t tones[AUDIO_TONE_STACKSIZE]; // stack of currently active tones
|
||
|
|
||
|
bool playing_melody = false; // playing a SONG?
|
||
|
bool playing_note = false; // or (possibly multiple simultaneous) tones
|
||
|
bool state_changed = false; // global flag, which is set if anything changes with the active_tones
|
||
|
|
||
|
// melody/SONG related state variables
|
||
|
float (*notes_pointer)[][2]; // SONG, an array of MUSICAL_NOTEs
|
||
|
uint16_t notes_count; // length of the notes_pointer array
|
||
|
bool notes_repeat; // PLAY_SONG or PLAY_LOOP?
|
||
|
uint16_t melody_current_note_duration = 0; // duration of the currently playing note from the active melody, in ms
|
||
|
uint8_t note_tempo = TEMPO_DEFAULT; // beats-per-minute
|
||
|
uint16_t current_note = 0; // index into the array at notes_pointer
|
||
|
bool note_resting = false; // if a short pause was introduced between two notes with the same frequency while playing a melody
|
||
|
uint16_t last_timestamp = 0;
|
||
|
|
||
|
#ifdef AUDIO_ENABLE_TONE_MULTIPLEXING
|
||
|
# ifndef AUDIO_MAX_SIMULTANEOUS_TONES
|
||
|
# define AUDIO_MAX_SIMULTANEOUS_TONES 3
|
||
|
# endif
|
||
|
uint16_t tone_multiplexing_rate = AUDIO_TONE_MULTIPLEXING_RATE_DEFAULT;
|
||
|
uint8_t tone_multiplexing_index_shift = 0; // offset used on active-tone array access
|
||
|
#endif
|
||
|
|
||
|
// provided and used by voices.c
|
||
|
extern uint8_t note_timbre;
|
||
|
extern bool glissando;
|
||
|
extern bool vibrato;
|
||
|
extern uint16_t voices_timer;
|
||
|
|
||
|
#ifndef STARTUP_SONG
|
||
|
# define STARTUP_SONG SONG(STARTUP_SOUND)
|
||
|
#endif
|
||
|
#ifndef AUDIO_ON_SONG
|
||
|
# define AUDIO_ON_SONG SONG(AUDIO_ON_SOUND)
|
||
|
#endif
|
||
|
#ifndef AUDIO_OFF_SONG
|
||
|
# define AUDIO_OFF_SONG SONG(AUDIO_OFF_SOUND)
|
||
|
#endif
|
||
|
float startup_song[][2] = STARTUP_SONG;
|
||
|
float audio_on_song[][2] = AUDIO_ON_SONG;
|
||
|
float audio_off_song[][2] = AUDIO_OFF_SONG;
|
||
|
|
||
|
static bool audio_initialized = false;
|
||
|
static bool audio_driver_stopped = true;
|
||
|
audio_config_t audio_config;
|
||
|
|
||
|
void audio_init() {
|
||
|
if (audio_initialized) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Check EEPROM
|
||
|
#ifdef EEPROM_ENABLE
|
||
|
if (!eeconfig_is_enabled()) {
|
||
|
eeconfig_init();
|
||
|
}
|
||
|
audio_config.raw = eeconfig_read_audio();
|
||
|
#else // EEPROM settings
|
||
|
audio_config.enable = true;
|
||
|
# ifdef AUDIO_CLICKY_ON
|
||
|
audio_config.clicky_enable = true;
|
||
|
# endif
|
||
|
#endif // EEPROM settings
|
||
|
|
||
|
for (uint8_t i = 0; i < AUDIO_TONE_STACKSIZE; i++) {
|
||
|
tones[i] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0};
|
||
|
}
|
||
|
|
||
|
if (!audio_initialized) {
|
||
|
audio_driver_initialize();
|
||
|
audio_initialized = true;
|
||
|
}
|
||
|
stop_all_notes();
|
||
|
}
|
||
|
|
||
|
void audio_startup(void) {
|
||
|
if (audio_config.enable) {
|
||
|
PLAY_SONG(startup_song);
|
||
|
}
|
||
|
|
||
|
last_timestamp = timer_read();
|
||
|
}
|
||
|
|
||
|
void audio_toggle(void) {
|
||
|
if (audio_config.enable) {
|
||
|
stop_all_notes();
|
||
|
}
|
||
|
audio_config.enable ^= 1;
|
||
|
eeconfig_update_audio(audio_config.raw);
|
||
|
if (audio_config.enable) {
|
||
|
audio_on_user();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void audio_on(void) {
|
||
|
audio_config.enable = 1;
|
||
|
eeconfig_update_audio(audio_config.raw);
|
||
|
audio_on_user();
|
||
|
PLAY_SONG(audio_on_song);
|
||
|
}
|
||
|
|
||
|
void audio_off(void) {
|
||
|
PLAY_SONG(audio_off_song);
|
||
|
wait_ms(100);
|
||
|
audio_stop_all();
|
||
|
audio_config.enable = 0;
|
||
|
eeconfig_update_audio(audio_config.raw);
|
||
|
}
|
||
|
|
||
|
bool audio_is_on(void) { return (audio_config.enable != 0); }
|
||
|
|
||
|
void audio_stop_all() {
|
||
|
if (audio_driver_stopped) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
active_tones = 0;
|
||
|
|
||
|
audio_driver_stop();
|
||
|
|
||
|
playing_melody = false;
|
||
|
playing_note = false;
|
||
|
|
||
|
melody_current_note_duration = 0;
|
||
|
|
||
|
for (uint8_t i = 0; i < AUDIO_TONE_STACKSIZE; i++) {
|
||
|
tones[i] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0};
|
||
|
}
|
||
|
|
||
|
audio_driver_stopped = true;
|
||
|
}
|
||
|
|
||
|
void audio_stop_tone(float pitch) {
|
||
|
if (pitch < 0.0f) {
|
||
|
pitch = -1 * pitch;
|
||
|
}
|
||
|
|
||
|
if (playing_note) {
|
||
|
if (!audio_initialized) {
|
||
|
audio_init();
|
||
|
}
|
||
|
bool found = false;
|
||
|
for (int i = AUDIO_TONE_STACKSIZE - 1; i >= 0; i--) {
|
||
|
found = (tones[i].pitch == pitch);
|
||
|
if (found) {
|
||
|
tones[i] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0};
|
||
|
for (int j = i; (j < AUDIO_TONE_STACKSIZE - 1); j++) {
|
||
|
tones[j] = tones[j + 1];
|
||
|
tones[j + 1] = (musical_tone_t){.time_started = 0, .pitch = -1.0f, .duration = 0};
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (!found) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
state_changed = true;
|
||
|
active_tones--;
|
||
|
if (active_tones < 0) active_tones = 0;
|
||
|
#ifdef AUDIO_ENABLE_TONE_MULTIPLEXING
|
||
|
if (tone_multiplexing_index_shift >= active_tones) {
|
||
|
tone_multiplexing_index_shift = 0;
|
||
|
}
|
||
|
#endif
|
||
|
if (active_tones == 0) {
|
||
|
audio_driver_stop();
|
||
|
audio_driver_stopped = true;
|
||
|
playing_note = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void audio_play_note(float pitch, uint16_t duration) {
|
||
|
if (!audio_config.enable) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!audio_initialized) {
|
||
|
audio_init();
|
||
|
}
|
||
|
|
||
|
if (pitch < 0.0f) {
|
||
|
pitch = -1 * pitch;
|
||
|
}
|
||
|
|
||
|
// round-robin: shifting out old tones, keeping only unique ones
|
||
|
// if the new frequency is already amongst the active tones, shift it to the top of the stack
|
||
|
bool found = false;
|
||
|
for (int i = active_tones - 1; i >= 0; i--) {
|
||
|
found = (tones[i].pitch == pitch);
|
||
|
if (found) {
|
||
|
for (int j = i; (j < active_tones - 1); j++) {
|
||
|
tones[j] = tones[j + 1];
|
||
|
tones[j + 1] = (musical_tone_t){.time_started = timer_read(), .pitch = pitch, .duration = duration};
|
||
|
}
|
||
|
return; // since this frequency played already, the hardware was already started
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// frequency/tone is actually new, so we put it on the top of the stack
|
||
|
active_tones++;
|
||
|
if (active_tones > AUDIO_TONE_STACKSIZE) {
|
||
|
active_tones = AUDIO_TONE_STACKSIZE;
|
||
|
// shift out the oldest tone to make room
|
||
|
for (int i = 0; i < active_tones - 1; i++) {
|
||
|
tones[i] = tones[i + 1];
|
||
|
}
|
||
|
}
|
||
|
state_changed = true;
|
||
|
playing_note = true;
|
||
|
tones[active_tones - 1] = (musical_tone_t){.time_started = timer_read(), .pitch = pitch, .duration = duration};
|
||
|
|
||
|
// TODO: needs to be handled per note/tone -> use its timestamp instead?
|
||
|
voices_timer = timer_read(); // reset to zero, for the effects added by voices.c
|
||
|
|
||
|
if (audio_driver_stopped) {
|
||
|
audio_driver_start();
|
||
|
audio_driver_stopped = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void audio_play_tone(float pitch) { audio_play_note(pitch, 0xffff); }
|
||
|
|
||
|
void audio_play_melody(float (*np)[][2], uint16_t n_count, bool n_repeat) {
|
||
|
if (!audio_config.enable) {
|
||
|
audio_stop_all();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!audio_initialized) {
|
||
|
audio_init();
|
||
|
}
|
||
|
|
||
|
// Cancel note if a note is playing
|
||
|
if (playing_note) audio_stop_all();
|
||
|
|
||
|
playing_melody = true;
|
||
|
note_resting = false;
|
||
|
|
||
|
notes_pointer = np;
|
||
|
notes_count = n_count;
|
||
|
notes_repeat = n_repeat;
|
||
|
|
||
|
current_note = 0; // note in the melody-array/list at note_pointer
|
||
|
|
||
|
// start first note manually, which also starts the audio_driver
|
||
|
// all following/remaining notes are played by 'audio_update_state'
|
||
|
audio_play_note((*notes_pointer)[current_note][0], audio_duration_to_ms((*notes_pointer)[current_note][1]));
|
||
|
last_timestamp = timer_read();
|
||
|
melody_current_note_duration = audio_duration_to_ms((*notes_pointer)[current_note][1]);
|
||
|
}
|
||
|
|
||
|
float click[2][2];
|
||
|
void audio_play_click(uint16_t delay, float pitch, uint16_t duration) {
|
||
|
uint16_t duration_tone = audio_ms_to_duration(duration);
|
||
|
uint16_t duration_delay = audio_ms_to_duration(delay);
|
||
|
|
||
|
if (delay <= 0.0f) {
|
||
|
click[0][0] = pitch;
|
||
|
click[0][1] = duration_tone;
|
||
|
click[1][0] = 0.0f;
|
||
|
click[1][1] = 0.0f;
|
||
|
audio_play_melody(&click, 1, false);
|
||
|
} else {
|
||
|
// first note is a rest/pause
|
||
|
click[0][0] = 0.0f;
|
||
|
click[0][1] = duration_delay;
|
||
|
// second note is the actual click
|
||
|
click[1][0] = pitch;
|
||
|
click[1][1] = duration_tone;
|
||
|
audio_play_melody(&click, 2, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool audio_is_playing_note(void) { return playing_note; }
|
||
|
|
||
|
bool audio_is_playing_melody(void) { return playing_melody; }
|
||
|
|
||
|
uint8_t audio_get_number_of_active_tones(void) { return active_tones; }
|
||
|
|
||
|
float audio_get_frequency(uint8_t tone_index) {
|
||
|
if (tone_index >= active_tones) {
|
||
|
return 0.0f;
|
||
|
}
|
||
|
return tones[active_tones - tone_index - 1].pitch;
|
||
|
}
|
||
|
|
||
|
float audio_get_processed_frequency(uint8_t tone_index) {
|
||
|
if (tone_index >= active_tones) {
|
||
|
return 0.0f;
|
||
|
}
|
||
|
|
||
|
int8_t index = active_tones - tone_index - 1;
|
||
|
// new tones are stacked on top (= appended at the end), so the most recent/current is MAX-1
|
||
|
|
||
|
#ifdef AUDIO_ENABLE_TONE_MULTIPLEXING
|
||
|
index = index - tone_multiplexing_index_shift;
|
||
|
if (index < 0) // wrap around
|
||
|
index += active_tones;
|
||
|
#endif
|
||
|
|
||
|
if (tones[index].pitch <= 0.0f) {
|
||
|
return 0.0f;
|
||
|
}
|
||
|
|
||
|
return voice_envelope(tones[index].pitch);
|
||
|
}
|
||
|
|
||
|
bool audio_update_state(void) {
|
||
|
if (!playing_note && !playing_melody) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool goto_next_note = false;
|
||
|
uint16_t current_time = timer_read();
|
||
|
|
||
|
if (playing_melody) {
|
||
|
goto_next_note = timer_elapsed(last_timestamp) >= melody_current_note_duration;
|
||
|
if (goto_next_note) {
|
||
|
uint16_t delta = timer_elapsed(last_timestamp) - melody_current_note_duration;
|
||
|
last_timestamp = current_time;
|
||
|
uint16_t previous_note = current_note;
|
||
|
current_note++;
|
||
|
voices_timer = timer_read(); // reset to zero, for the effects added by voices.c
|
||
|
|
||
|
if (current_note >= notes_count) {
|
||
|
if (notes_repeat) {
|
||
|
current_note = 0;
|
||
|
} else {
|
||
|
audio_stop_all();
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!note_resting && (*notes_pointer)[previous_note][0] == (*notes_pointer)[current_note][0]) {
|
||
|
note_resting = true;
|
||
|
|
||
|
// special handling for successive notes of the same frequency:
|
||
|
// insert a short pause to separate them audibly
|
||
|
audio_play_note(0.0f, audio_duration_to_ms(2));
|
||
|
current_note = previous_note;
|
||
|
melody_current_note_duration = audio_duration_to_ms(2);
|
||
|
|
||
|
} else {
|
||
|
note_resting = false;
|
||
|
|
||
|
// TODO: handle glissando here (or remember previous and current tone)
|
||
|
/* there would need to be a freq(here we are) -> freq(next note)
|
||
|
* and do slide/glissando in between problem here is to know which
|
||
|
* frequency on the stack relates to what other? e.g. a melody starts
|
||
|
* tones in a sequence, and stops expiring one, so the most recently
|
||
|
* stopped is the starting point for a glissando to the most recently started?
|
||
|
* how to detect and preserve this relation?
|
||
|
* and what about user input, chords, ...?
|
||
|
*/
|
||
|
|
||
|
// '- delta': Skip forward in the next note's length if we've over shot
|
||
|
// the last, so the overall length of the song is the same
|
||
|
uint16_t duration = audio_duration_to_ms((*notes_pointer)[current_note][1]);
|
||
|
|
||
|
// Skip forward past any completely missed notes
|
||
|
while (delta > duration && current_note < notes_count - 1) {
|
||
|
delta -= duration;
|
||
|
current_note++;
|
||
|
duration = audio_duration_to_ms((*notes_pointer)[current_note][1]);
|
||
|
}
|
||
|
|
||
|
if (delta < duration) {
|
||
|
duration -= delta;
|
||
|
} else {
|
||
|
// Only way to get here is if it is the last note and
|
||
|
// we have completely missed it. Play it for 1ms...
|
||
|
duration = 1;
|
||
|
}
|
||
|
|
||
|
audio_play_note((*notes_pointer)[current_note][0], duration);
|
||
|
melody_current_note_duration = duration;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (playing_note) {
|
||
|
#ifdef AUDIO_ENABLE_TONE_MULTIPLEXING
|
||
|
tone_multiplexing_index_shift = (int)(current_time / tone_multiplexing_rate) % MIN(AUDIO_MAX_SIMULTANEOUS_TONES, active_tones);
|
||
|
goto_next_note = true;
|
||
|
#endif
|
||
|
if (vibrato || glissando) {
|
||
|
// force update on each cycle, since vibrato shifts the frequency slightly
|
||
|
goto_next_note = true;
|
||
|
}
|
||
|
|
||
|
// housekeeping: stop notes that have no playtime left
|
||
|
for (int i = 0; i < active_tones; i++) {
|
||
|
if ((tones[i].duration != 0xffff) // indefinitely playing notes, started by 'audio_play_tone'
|
||
|
&& (tones[i].duration != 0) // 'uninitialized'
|
||
|
) {
|
||
|
if (timer_elapsed(tones[i].time_started) >= tones[i].duration) {
|
||
|
audio_stop_tone(tones[i].pitch); // also sets 'state_changed=true'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// state-changes have a higher priority, always triggering the hardware to update
|
||
|
if (state_changed) {
|
||
|
state_changed = false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return goto_next_note;
|
||
|
}
|
||
|
|
||
|
// Tone-multiplexing functions
|
||
|
#ifdef AUDIO_ENABLE_TONE_MULTIPLEXING
|
||
|
void audio_set_tone_multiplexing_rate(uint16_t rate) { tone_multiplexing_rate = rate; }
|
||
|
void audio_enable_tone_multiplexing(void) { tone_multiplexing_rate = AUDIO_TONE_MULTIPLEXING_RATE_DEFAULT; }
|
||
|
void audio_disable_tone_multiplexing(void) { tone_multiplexing_rate = 0; }
|
||
|
void audio_increase_tone_multiplexing_rate(uint16_t change) {
|
||
|
if ((0xffff - change) > tone_multiplexing_rate) {
|
||
|
tone_multiplexing_rate += change;
|
||
|
}
|
||
|
}
|
||
|
void audio_decrease_tone_multiplexing_rate(uint16_t change) {
|
||
|
if (change <= tone_multiplexing_rate) {
|
||
|
tone_multiplexing_rate -= change;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
// Tempo functions
|
||
|
|
||
|
void audio_set_tempo(uint8_t tempo) {
|
||
|
if (tempo < 10) note_tempo = 10;
|
||
|
// else if (tempo > 250)
|
||
|
// note_tempo = 250;
|
||
|
else
|
||
|
note_tempo = tempo;
|
||
|
}
|
||
|
|
||
|
void audio_increase_tempo(uint8_t tempo_change) {
|
||
|
if (tempo_change > 255 - note_tempo)
|
||
|
note_tempo = 255;
|
||
|
else
|
||
|
note_tempo += tempo_change;
|
||
|
}
|
||
|
|
||
|
void audio_decrease_tempo(uint8_t tempo_change) {
|
||
|
if (tempo_change >= note_tempo - 10)
|
||
|
note_tempo = 10;
|
||
|
else
|
||
|
note_tempo -= tempo_change;
|
||
|
}
|
||
|
|
||
|
// TODO in the int-math version are some bugs; songs sometimes abruptly end - maybe an issue with the timer/system-tick wrapping around?
|
||
|
uint16_t audio_duration_to_ms(uint16_t duration_bpm) {
|
||
|
#if defined(__AVR__)
|
||
|
// 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
|
||
|
return ((uint32_t)duration_bpm * 60 * 1000) / (64 * note_tempo);
|
||
|
// NOTE: beware of uint16_t overflows when note_tempo is low and/or the duration is long
|
||
|
#else
|
||
|
return ((float)duration_bpm * 60) / (64 * note_tempo) * 1000;
|
||
|
#endif
|
||
|
}
|
||
|
uint16_t audio_ms_to_duration(uint16_t duration_ms) {
|
||
|
#if defined(__AVR__)
|
||
|
return ((uint32_t)duration_ms * 64 * note_tempo) / 60 / 1000;
|
||
|
#else
|
||
|
return ((float)duration_ms * 64 * note_tempo) / 60 / 1000;
|
||
|
#endif
|
||
|
}
|