mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2024-12-16 18:07:13 +03:00
265 lines
7.5 KiB
C
265 lines
7.5 KiB
C
#include "dtmf_dolphin_audio.h"
|
|
|
|
DTMFDolphinAudio* current_player;
|
|
|
|
static void dtmf_dolphin_audio_dma_isr(void* ctx) {
|
|
FuriMessageQueue* event_queue = ctx;
|
|
|
|
if(LL_DMA_IsActiveFlag_HT1(DMA1)) {
|
|
LL_DMA_ClearFlag_HT1(DMA1);
|
|
|
|
DTMFDolphinCustomEvent event = {.type = DTMFDolphinEventDMAHalfTransfer};
|
|
furi_message_queue_put(event_queue, &event, 0);
|
|
}
|
|
|
|
if(LL_DMA_IsActiveFlag_TC1(DMA1)) {
|
|
LL_DMA_ClearFlag_TC1(DMA1);
|
|
|
|
DTMFDolphinCustomEvent event = {.type = DTMFDolphinEventDMAFullTransfer};
|
|
furi_message_queue_put(event_queue, &event, 0);
|
|
}
|
|
}
|
|
|
|
void dtmf_dolphin_audio_clear_samples(DTMFDolphinAudio* player) {
|
|
for(size_t i = 0; i < player->buffer_length; i++) {
|
|
player->sample_buffer[i] = 0;
|
|
}
|
|
}
|
|
|
|
DTMFDolphinOsc* dtmf_dolphin_osc_alloc() {
|
|
DTMFDolphinOsc* osc = malloc(sizeof(DTMFDolphinOsc));
|
|
osc->cached_freq = 0;
|
|
osc->offset = 0;
|
|
osc->period = 0;
|
|
osc->lookup_table = NULL;
|
|
return osc;
|
|
}
|
|
|
|
DTMFDolphinPulseFilter* dtmf_dolphin_pulse_filter_alloc() {
|
|
DTMFDolphinPulseFilter* pf = malloc(sizeof(DTMFDolphinPulseFilter));
|
|
pf->duration = 0;
|
|
pf->period = 0;
|
|
pf->offset = 0;
|
|
pf->lookup_table = NULL;
|
|
return pf;
|
|
}
|
|
|
|
DTMFDolphinAudio* dtmf_dolphin_audio_alloc() {
|
|
DTMFDolphinAudio* player = malloc(sizeof(DTMFDolphinAudio));
|
|
player->buffer_length = SAMPLE_BUFFER_LENGTH;
|
|
player->half_buffer_length = SAMPLE_BUFFER_LENGTH / 2;
|
|
player->sample_buffer = malloc(sizeof(uint16_t) * player->buffer_length);
|
|
player->osc1 = dtmf_dolphin_osc_alloc();
|
|
player->osc2 = dtmf_dolphin_osc_alloc();
|
|
player->volume = 1.0f;
|
|
player->queue = furi_message_queue_alloc(10, sizeof(DTMFDolphinCustomEvent));
|
|
player->filter = dtmf_dolphin_pulse_filter_alloc();
|
|
player->playing = false;
|
|
dtmf_dolphin_audio_clear_samples(player);
|
|
|
|
return player;
|
|
}
|
|
|
|
size_t calc_waveform_period(float freq) {
|
|
if(!freq) {
|
|
return 0;
|
|
}
|
|
// DMA Rate calculation, thanks to Dr_Zlo
|
|
float dma_rate = CPU_CLOCK_FREQ / 2 / DTMF_DOLPHIN_HAL_DMA_PRESCALER /
|
|
(DTMF_DOLPHIN_HAL_DMA_AUTORELOAD + 1);
|
|
|
|
// Using a constant scaling modifier, which likely represents
|
|
// the combined system overhead and isr latency.
|
|
return (uint16_t)dma_rate * 2 / freq * 0.801923;
|
|
}
|
|
|
|
void osc_generate_lookup_table(DTMFDolphinOsc* osc, float freq) {
|
|
if(osc->lookup_table != NULL) {
|
|
free(osc->lookup_table);
|
|
}
|
|
osc->offset = 0;
|
|
osc->cached_freq = freq;
|
|
osc->period = calc_waveform_period(freq);
|
|
if(!osc->period) {
|
|
osc->lookup_table = NULL;
|
|
return;
|
|
}
|
|
osc->lookup_table = malloc(sizeof(float) * osc->period);
|
|
|
|
for(size_t i = 0; i < osc->period; i++) {
|
|
osc->lookup_table[i] = sin(i * PERIOD_2_PI / osc->period) + 1;
|
|
}
|
|
}
|
|
|
|
void filter_generate_lookup_table(
|
|
DTMFDolphinPulseFilter* pf,
|
|
uint16_t pulses,
|
|
uint16_t pulse_ms,
|
|
uint16_t gap_ms) {
|
|
if(pf->lookup_table != NULL) {
|
|
free(pf->lookup_table);
|
|
}
|
|
pf->offset = 0;
|
|
|
|
uint16_t gap_period = calc_waveform_period(1000 / (float)gap_ms);
|
|
uint16_t pulse_period = calc_waveform_period(1000 / (float)pulse_ms);
|
|
pf->period = pulse_period + gap_period;
|
|
|
|
if(!pf->period) {
|
|
pf->lookup_table = NULL;
|
|
return;
|
|
}
|
|
pf->duration = pf->period * pulses;
|
|
pf->lookup_table = malloc(sizeof(bool) * pf->duration);
|
|
|
|
for(size_t i = 0; i < pf->duration; i++) {
|
|
pf->lookup_table[i] = i % pf->period < pulse_period;
|
|
}
|
|
}
|
|
|
|
float sample_frame(DTMFDolphinOsc* osc) {
|
|
float frame = 0.0;
|
|
|
|
if(osc->period) {
|
|
frame = osc->lookup_table[osc->offset];
|
|
osc->offset = (osc->offset + 1) % osc->period;
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
bool sample_filter(DTMFDolphinPulseFilter* pf) {
|
|
bool frame = true;
|
|
|
|
if(pf->duration) {
|
|
if(pf->offset < pf->duration) {
|
|
frame = pf->lookup_table[pf->offset];
|
|
pf->offset = pf->offset + 1;
|
|
} else {
|
|
frame = false;
|
|
}
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
void dtmf_dolphin_osc_free(DTMFDolphinOsc* osc) {
|
|
if(osc->lookup_table != NULL) {
|
|
free(osc->lookup_table);
|
|
}
|
|
free(osc);
|
|
}
|
|
|
|
void dtmf_dolphin_filter_free(DTMFDolphinPulseFilter* pf) {
|
|
if(pf->lookup_table != NULL) {
|
|
free(pf->lookup_table);
|
|
}
|
|
free(pf);
|
|
}
|
|
|
|
void dtmf_dolphin_audio_free(DTMFDolphinAudio* player) {
|
|
furi_message_queue_free(player->queue);
|
|
dtmf_dolphin_osc_free(player->osc1);
|
|
dtmf_dolphin_osc_free(player->osc2);
|
|
dtmf_dolphin_filter_free(player->filter);
|
|
free(player->sample_buffer);
|
|
free(player);
|
|
current_player = NULL;
|
|
}
|
|
|
|
bool generate_waveform(DTMFDolphinAudio* player, uint16_t buffer_index) {
|
|
uint16_t* sample_buffer_start = &player->sample_buffer[buffer_index];
|
|
|
|
for(size_t i = 0; i < player->half_buffer_length; i++) {
|
|
float data = 0;
|
|
if(player->osc2->period) {
|
|
data = (sample_frame(player->osc1) / 2) + (sample_frame(player->osc2) / 2);
|
|
} else {
|
|
data = (sample_frame(player->osc1));
|
|
}
|
|
data *= sample_filter(player->filter) ? player->volume : 0.0;
|
|
data *= UINT8_MAX / 2; // scale -128..127
|
|
data += UINT8_MAX / 2; // to unsigned
|
|
|
|
if(data < 0) {
|
|
data = 0;
|
|
}
|
|
|
|
if(data > 255) {
|
|
data = 255;
|
|
}
|
|
|
|
sample_buffer_start[i] = data;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool dtmf_dolphin_audio_play_tones(
|
|
float freq1,
|
|
float freq2,
|
|
uint16_t pulses,
|
|
uint16_t pulse_ms,
|
|
uint16_t gap_ms) {
|
|
if(current_player != NULL && current_player->playing) {
|
|
// Cannot start playing while still playing something else
|
|
return false;
|
|
}
|
|
current_player = dtmf_dolphin_audio_alloc();
|
|
|
|
osc_generate_lookup_table(current_player->osc1, freq1);
|
|
osc_generate_lookup_table(current_player->osc2, freq2);
|
|
filter_generate_lookup_table(current_player->filter, pulses, pulse_ms, gap_ms);
|
|
|
|
generate_waveform(current_player, 0);
|
|
generate_waveform(current_player, current_player->half_buffer_length);
|
|
|
|
dtmf_dolphin_speaker_init();
|
|
dtmf_dolphin_dma_init((uint32_t)current_player->sample_buffer, current_player->buffer_length);
|
|
|
|
furi_hal_interrupt_set_isr(
|
|
FuriHalInterruptIdDma1Ch1, dtmf_dolphin_audio_dma_isr, current_player->queue);
|
|
|
|
dtmf_dolphin_dma_start();
|
|
dtmf_dolphin_speaker_start();
|
|
current_player->playing = true;
|
|
return true;
|
|
}
|
|
|
|
bool dtmf_dolphin_audio_stop_tones() {
|
|
if(current_player != NULL && !current_player->playing) {
|
|
// Can't stop a player that isn't playing.
|
|
return false;
|
|
}
|
|
while(current_player->filter->offset > 0 &&
|
|
current_player->filter->offset < current_player->filter->duration) {
|
|
// run remaining ticks if needed to complete filter sequence
|
|
dtmf_dolphin_audio_handle_tick();
|
|
}
|
|
dtmf_dolphin_speaker_stop();
|
|
dtmf_dolphin_dma_stop();
|
|
|
|
furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, NULL, NULL);
|
|
|
|
dtmf_dolphin_audio_free(current_player);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool dtmf_dolphin_audio_handle_tick() {
|
|
bool handled = false;
|
|
|
|
if(current_player) {
|
|
DTMFDolphinCustomEvent event;
|
|
if(furi_message_queue_get(current_player->queue, &event, 250) == FuriStatusOk) {
|
|
if(event.type == DTMFDolphinEventDMAHalfTransfer) {
|
|
generate_waveform(current_player, 0);
|
|
handled = true;
|
|
} else if(event.type == DTMFDolphinEventDMAFullTransfer) {
|
|
generate_waveform(current_player, current_player->half_buffer_length);
|
|
handled = true;
|
|
}
|
|
}
|
|
}
|
|
return handled;
|
|
} |