diff options
Diffstat (limited to 'src/common/Audio')
-rw-r--r-- | src/common/Audio/audio.c | 50 | ||||
-rw-r--r-- | src/common/Audio/audio.h | 58 | ||||
-rw-r--r-- | src/common/Audio/audio_in.c | 24 | ||||
-rw-r--r-- | src/common/Audio/audio_in.h | 40 | ||||
-rw-r--r-- | src/common/Audio/cw.c | 605 | ||||
-rw-r--r-- | src/common/Audio/cw.h | 53 | ||||
-rw-r--r-- | src/common/Audio/tone.c | 358 | ||||
-rw-r--r-- | src/common/Audio/tone.h | 53 |
8 files changed, 1241 insertions, 0 deletions
diff --git a/src/common/Audio/audio.c b/src/common/Audio/audio.c new file mode 100644 index 0000000..f68f867 --- /dev/null +++ b/src/common/Audio/audio.c @@ -0,0 +1,50 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Matthias P. Braendli, Maximilien Cuony + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + +#include "Audio/audio.h" + +#include <stdlib.h> + +AudioCallbackFunction *callback_function; +void *callback_context; +int16_t * next_buffer_samples; +int next_buffer_length; +int buffer_number; +bool dma_running; + +void audio_initialize(int plln, int pllr, int i2sdiv, int i2sodd, int rate) { + + // Intitialize state. + callback_function = NULL; + callback_context = NULL; + next_buffer_samples = NULL; + next_buffer_length = 0; + buffer_number = 0; + dma_running = false; + + audio_initialize_platform(plln, pllr, i2sdiv, i2sodd, rate); + + audio_set_volume(0xff); + +} diff --git a/src/common/Audio/audio.h b/src/common/Audio/audio.h new file mode 100644 index 0000000..d48cbe5 --- /dev/null +++ b/src/common/Audio/audio.h @@ -0,0 +1,58 @@ +#ifndef __AUDIO_H__ +#define __AUDIO_H__ + +#include <stdint.h> +#include <stdbool.h> + +typedef void AudioCallbackFunction(void *context,int buffer); + +// Variables used by both glutt-o-logique and simulator +extern AudioCallbackFunction *callback_function; +extern void *callback_context; +extern int16_t *next_buffer_samples; +extern int next_buffer_length; +extern int buffer_number; +extern bool dma_running; + +void audio_initialize_platform(int plln, int pllr, int i2sdiv, int i2sodd, int rate); +void audio_start_dma_and_request_buffers(void); +void audio_stop_dma(void); + +#define Audio8000HzSettings 256,5,12,1,8000 +#define Audio16000HzSettings 213,2,13,0,16000 +#define Audio32000HzSettings 213,2,6,1,32000 +#define Audio48000HzSettings 258,3,3,1,48000 +#define Audio96000HzSettings 344,2,3,1,96000 +#define Audio22050HzSettings 429,4,9,1,22050 +#define Audio44100HzSettings 271,2,6,0,44100 +#define AudioVGAHSyncSettings 419,2,13,0,31475 // 31475.3606. Actual VGA timer is 31472.4616. + +#define AUDIO_BUF_LEN 4096 + + +// Initialize and power up audio hardware. Use the above defines for the parameters. +// Can probably only be called once. +void audio_initialize(int plln, int pllr, int i2sdiv, int i2sodd, int rate); + +// Power up and down the audio hardware. +void audio_put_codec_in_reset(void); +void audio_reinit_codec(void); + +// Set audio volume in steps of 0.5 dB. 0xff is +12 dB. +void audio_set_volume(int volume); + +// Start and stop audio playback using DMA. +// Callback is optional, and called whenever a new buffer is needed. +void audio_play_with_callback(AudioCallbackFunction *callback,void *context); + +// Provide a new buffer to the audio DMA. Output is double buffered, so +// at least two buffers must be maintained by the program. It is not allowed +// to overwrite the previously provided buffer until after the next callback +// invocation. +// Buffers must reside in DMA1-accessible memory, that is, the 128k RAM bank, +// or flash. +bool audio_provide_buffer_without_blocking(void *samples,int numsamples); + +void DMA1_Stream7_IRQHandler(void); + +#endif diff --git a/src/common/Audio/audio_in.c b/src/common/Audio/audio_in.c new file mode 100644 index 0000000..dd1994e --- /dev/null +++ b/src/common/Audio/audio_in.c @@ -0,0 +1,24 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Matthias P. Braendli, Maximilien Cuony + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + diff --git a/src/common/Audio/audio_in.h b/src/common/Audio/audio_in.h new file mode 100644 index 0000000..97a0f29 --- /dev/null +++ b/src/common/Audio/audio_in.h @@ -0,0 +1,40 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Matthias P. Braendli, Maximilien Cuony + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + +#pragma once + +#include <stdio.h> +#include "FreeRTOS.h" + +#define AUDIO_IN_RATE 16000 +#define AUDIO_IN_BUF_LEN 1600 + +void audio_in_initialize(void); + +/* Enable or disable the audio input */ +void audio_in_enable(int enable); + +/* The audio input layer must call tone_detect_push_sample() to + * send the samples + */ diff --git a/src/common/Audio/cw.c b/src/common/Audio/cw.c new file mode 100644 index 0000000..667d459 --- /dev/null +++ b/src/common/Audio/cw.c @@ -0,0 +1,605 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Matthias P. Braendli + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + +/* CW and PSK31 generator + * + * Concept: + * + * +-------------------+ +----------------+ + * | cw_push_message() | -> cw_msg_queue -> | cw_psk31task() | -> cw_audio_queue + * +-------------------+ +----------------+ + * + * The cw_psk31_fill_buffer() function can be called to fetch audio from the + * audio_queue + */ + +#include "Audio/cw.h" +#include "Core/common.h" +#include "Audio/audio.h" +#include <string.h> + +#ifdef SIMULATOR +#include <math.h> +#define arm_cos_f32 cosf +#define arm_sin_f32 sinf +#else +#include "arm_math.h" +#endif + +/* Kernel includes. */ +#include "FreeRTOS.h" +#include "task.h" +#include "queue.h" +#include "semphr.h" + +#define MAX_MESSAGE_LEN 1024 +#define MAX_ON_BUFFER_LEN 8192 + +const uint8_t cw_mapping[60] = { // {{{ + // Read bits from right to left + + 0b110101, //+ ASCII 43 + 0b110101, //, ASCII 44 + 0b1011110, //- ASCII 45 + + 0b1010101, //., ASCII 46 + 0b110110, // / ASCII 47 + + 0b100000, // 0, ASCII 48 + 0b100001, // 1 + 0b100011, + 0b100111, + 0b101111, + 0b111111, + 0b111110, + 0b111100, + 0b111000, + 0b110000, // 9, ASCII 57 + + // The following are mostly invalid, but + // required to fill the gap in ASCII between + // numerals and capital letters + 0b10, // : + 0b10, // ; + 0b10, // < + 0b101110, // = + 0b10, // > + 0b1110011, // ? + 0b1101001, //@ + + 0b101, // A ASCII 65 + 0b11110, + 0b11010, + 0b1110, + 0b11, + 0b11011, + 0b1100, + 0b11111, + 0b111, + 0b10001, + 0b1010, + 0b11101, + 0b100, //M + 0b110, + 0b1000, + 0b11001, + 0b10100, + 0b1101, + 0b1111, + 0b10, + 0b1011, + 0b10111, + 0b1001, + 0b10110, + 0b10010, + 0b11100, // Z + + 0b101010, //Start, ASCII [ + 0b1010111, // SK , ASCII '\' +}; //}}} + +#if ENABLE_PSK31 +/* + * PSK31 Varicode + * http://aintel.bi.ehu.es/psk31.html + */ +const char *psk31_varicode[] = { // {{{ + "1010101011", + "1011011011", + "1011101101", + "1101110111", + "1011101011", + "1101011111", + "1011101111", + "1011111101", + "1011111111", + "11101111", + "11101", + "1101101111", + "1011011101", + "11111", + "1101110101", + "1110101011", + "1011110111", + "1011110101", + "1110101101", + "1110101111", + "1101011011", + "1101101011", + "1101101101", + "1101010111", + "1101111011", + "1101111101", + "1110110111", + "1101010101", + "1101011101", + "1110111011", + "1011111011", + "1101111111", + "1", + "111111111", + "101011111", + "111110101", + "111011011", + "1011010101", + "1010111011", + "101111111", + "11111011", + "11110111", + "101101111", + "111011111", + "1110101", + "110101", + "1010111", + "110101111", + "10110111", + "10111101", + "11101101", + "11111111", + "101110111", + "101011011", + "101101011", + "110101101", + "110101011", + "110110111", + "11110101", + "110111101", + "111101101", + "1010101", + "111010111", + "1010101111", + "1010111101", + "1111101", + "11101011", + "10101101", + "10110101", + "1110111", + "11011011", + "11111101", + "101010101", + "1111111", + "111111101", + "101111101", + "11010111", + "10111011", + "11011101", + "10101011", + "11010101", + "111011101", + "10101111", + "1101111", + "1101101", + "101010111", + "110110101", + "101011101", + "101110101", + "101111011", + "1010101101", + "111110111", + "111101111", + "111111011", + "1010111111", + "101101101", + "1011011111", + "1011", + "1011111", + "101111", + "101101", + "11", + "111101", + "1011011", + "101011", + "1101", + "111101011", + "10111111", + "11011", + "111011", + "1111", + "111", + "111111", + "110111111", + "10101", + "10111", + "101", + "110111", + "1111011", + "1101011", + "11011111", + "1011101", + "111010101", + "1010110111", + "110111011", + "1010110101", + "1011010111", + "1110110101", +}; //}}} +#endif + +// Function to display message in GUI +void cw_message_sent(const char* str); + +struct cw_message_s { + char message[MAX_MESSAGE_LEN]; + size_t message_len; + + int freq; + + // If dit_duration is 0, the message is sent in PSK31 + int dit_duration; +}; + +// The queue contains above structs +QueueHandle_t cw_msg_queue; + +// Queue that contains audio data +QueueHandle_t cw_audio_queue; +static int cw_psk31_samplerate; + +static int cw_transmit_ongoing; + +static void cw_psk31_task(void *pvParameters); + +void cw_psk31_init(unsigned int samplerate) +{ + cw_psk31_samplerate = samplerate; + cw_transmit_ongoing = 0; + + cw_msg_queue = xQueueCreate(10, sizeof(struct cw_message_s)); + if (cw_msg_queue == 0) { + while(1); /* fatal error */ + } + + cw_audio_queue = xQueueCreate(1, AUDIO_BUF_LEN * sizeof(int16_t)); + if (cw_audio_queue == 0) { + while(1); /* fatal error */ + } + + xTaskCreate( + cw_psk31_task, + "CWPSKTask", + 8*configMINIMAL_STACK_SIZE, + (void*) NULL, + tskIDLE_PRIORITY + 3UL, + NULL); +} + +/* Parse one CW letter/symbol, and fill in the on_buffer. + * Returns number of uint8_t written. + */ +size_t cw_symbol(uint8_t sym, uint8_t *on_buffer, size_t on_buffer_size) +{ + uint8_t p = 0; + uint8_t val = cw_mapping[sym]; + size_t pos = 0; + + while((val >> p) != 0b1) { + + if (((val >> p) & 0b1) == 0b1) { + if (pos + 2 < on_buffer_size) { + // tone(1) + on_buffer[pos++] = 1; + + // silence(1) + on_buffer[pos++] = 0; + } + } + else { + if (pos + 4 < on_buffer_size) { + // tone(3) + on_buffer[pos++] = 1; + on_buffer[pos++] = 1; + on_buffer[pos++] = 1; + + // silence(1) + on_buffer[pos++] = 0; + } + } + + p++; + } + + // silence(2) + if (pos + 2 < on_buffer_size) { + for (int i = 0; i < 2; i++) { + on_buffer[pos++] = 0; + } + } + + return pos; +} + +// Transmit a string in morse code or PSK31. +// Supported range for CW: +// All ASCII between '+' and '\', which includes +// numerals and capital letters. +// Distinction between CW and PSK31 is done on dit_duration==0 +int cw_psk31_push_message(const char* text, int dit_duration, int frequency) +{ + const int text_len = strlen(text); + + if (strlen(text) > MAX_MESSAGE_LEN) { + return 0; + } + + struct cw_message_s msg; + for (int i = 0; i < MAX_MESSAGE_LEN; i++) { + if (i < text_len) { + msg.message[i] = text[i]; + } + else { + msg.message[i] = '\0'; + } + } + msg.message_len = text_len; + msg.freq = frequency; + msg.dit_duration = dit_duration; + + xQueueSendToBack(cw_msg_queue, &msg, portMAX_DELAY); /* Send Message */ + + cw_message_sent(msg.message); + + return 1; +} + +/* Parse the message and fill the on_buffer with CW on/CW off information. + * Returns the number of on/off bits written. + */ +static size_t cw_text_to_on_buffer(const char *msg, uint8_t *on_buffer, size_t on_buffer_size) +{ + size_t pos = 0; + const char* sym = msg; + do { + if (*sym < '+' || *sym > '\\') { + if (pos + 3 < on_buffer_size) { + for (int i = 0; i < 3; i++) { + on_buffer[pos++] = 0; + } + } + } + else { + pos += cw_symbol(*sym - '+', on_buffer + pos, on_buffer_size - pos); + } + sym++; + } while (*sym != '\0'); + + return pos; +} + + +#if ENABLE_PSK31 +/* + * Turn a null terminated ASCII string into a uint8_t buffer + * of 0 and 1 representing the PSK31 varicode for the input. + * + * outstr must be at least size 20 + strlen(instr)*12 + 20 to accomodate + * the header and tail. + * + * Returns number of bytes written. + */ +static size_t psk31_text_to_phase_buffer(const char* instr, uint8_t* outbits) +{ + int i=0, j, k; + + /* Header of 0s */ + for (j=0; j < 20; j++) { + outbits[i++] = 0; + } + + /* Encode the message, with 00 between letters */ + for (j=0; j < strlen(instr); j++) { + const char* varicode_bits = psk31_varicode[(int)instr[j]]; + for(k=0; k < strlen(varicode_bits); k++) { + outbits[i++] = (varicode_bits[k] == '1') ? 1 : 0; + } + outbits[i++] = 0; + outbits[i++] = 0; + } + + /* Tail of 0s */ + for (j=0; j < 20; j++) { + outbits[i++] = 0; + } + + return i; +} +#endif + + +size_t cw_psk31_fill_buffer(int16_t *buf, size_t bufsize) +{ + if (xQueueReceiveFromISR(cw_audio_queue, buf, NULL)) { + return bufsize; + } + else { + return 0; + } +} + +static int16_t cw_audio_buf[AUDIO_BUF_LEN]; +static uint8_t cw_psk31_buffer[MAX_ON_BUFFER_LEN]; +static struct cw_message_s cw_fill_msg_current; + +// Routine to generate CW audio +static float cw_generate_audio_ampl = 0.0f; +static float cw_generate_audio_nco = 0.0f; +static int16_t cw_generate_audio(float omega, int i, int __attribute__ ((unused))t) +{ + int16_t s = 0; + // Remove clicks from CW + if (cw_psk31_buffer[i]) { + const float remaining = 32768.0f - cw_generate_audio_ampl; + cw_generate_audio_ampl += remaining / 64.0f; + } + else { + cw_generate_audio_ampl -= cw_generate_audio_ampl / 64.0f; + + if (cw_generate_audio_ampl < 0) { + cw_generate_audio_ampl = 0; + } + } + + cw_generate_audio_nco += omega; + if (cw_generate_audio_nco > FLOAT_PI) { + cw_generate_audio_nco -= 2.0f * FLOAT_PI; + } + + s = cw_generate_audio_ampl * arm_sin_f32(cw_generate_audio_nco); + return s; +} + +#if ENABLE_PSK31 +static float psk31_generate_audio_nco = 0.0f; +static int psk31_current_psk_phase = 1; +static int16_t psk31_generate_audio(float omega, int i, int t, int samples_per_symbol) +{ + int16_t s = 0; + const float base_ampl = 20000.0f; + float psk31_generate_audio_ampl = 0.0f; + + if (cw_psk31_buffer[i] == 1) { + psk31_generate_audio_ampl = base_ampl; + } + else { + psk31_generate_audio_ampl = base_ampl * arm_cos_f32( + FLOAT_PI*(float)t/(float)samples_per_symbol); + } + + psk31_generate_audio_nco += omega; + if (psk31_generate_audio_nco > FLOAT_PI) { + psk31_generate_audio_nco -= 2.0f * FLOAT_PI; + } + + s = psk31_generate_audio_ampl * + arm_sin_f32(psk31_generate_audio_nco + + (psk31_current_psk_phase == 1 ? 0.0f : FLOAT_PI)); + + return s; +} +#endif + +static void cw_psk31_task(void __attribute__ ((unused))*pvParameters) +{ + int buf_pos = 0; + + while (1) { + int status = xQueueReceive(cw_msg_queue, &cw_fill_msg_current, portMAX_DELAY); + if (status == pdTRUE) { + + size_t cw_psk31_buffer_len = 0; + + cw_transmit_ongoing = 1; + + if (cw_fill_msg_current.dit_duration) { + cw_psk31_buffer_len = cw_text_to_on_buffer( + cw_fill_msg_current.message, + cw_psk31_buffer, + MAX_ON_BUFFER_LEN); + + } +#if ENABLE_PSK31 + else { + cw_psk31_buffer_len = psk31_text_to_phase_buffer( + cw_fill_msg_current.message, + cw_psk31_buffer); + } +#endif + + // Angular frequency of NCO + const float omega = 2.0f * FLOAT_PI * cw_fill_msg_current.freq / + (float)cw_psk31_samplerate; + + const int samples_per_symbol = (cw_fill_msg_current.dit_duration != 0) ? + (cw_psk31_samplerate * cw_fill_msg_current.dit_duration) / 1000 : + /* BPSK31 is at 31.25 symbols per second. */ + cw_psk31_samplerate * 100 / 3125; + +#if ENABLE_PSK31 + psk31_current_psk_phase = 1; +#endif + + for (int i = 0; i < cw_psk31_buffer_len; i++) { + for (int t = 0; t < samples_per_symbol; t++) { +#if ENABLE_PSK31 + int16_t s = (cw_fill_msg_current.dit_duration != 0) ? + cw_generate_audio(omega, i, t) : + psk31_generate_audio(omega, i, t, samples_per_symbol); +#else + int16_t s = cw_generate_audio(omega, i, t); +#endif + + // Stereo + for (int channel = 0; channel < 2; channel++) { + if (buf_pos == AUDIO_BUF_LEN) { + // It should take AUDIO_BUF_LEN/cw_psk31_samplerate seconds to send one buffer. + // If it takes more than 4 times as long, we think there is a problem. + const TickType_t reasonable_delay = pdMS_TO_TICKS(4000 * AUDIO_BUF_LEN / cw_psk31_samplerate); + if (xQueueSendToBack(cw_audio_queue, &cw_audio_buf, reasonable_delay) != pdTRUE) { + trigger_fault(FAULT_SOURCE_CW_AUDIO_QUEUE); + } + buf_pos = 0; + } + cw_audio_buf[buf_pos++] = s; + } + + } + +#if ENABLE_PSK31 + if (cw_psk31_buffer[i] == 0) { + psk31_current_psk_phase *= -1; + } +#endif + } + + // We have completed this message + + cw_transmit_ongoing = 0; + } + } +} + +int cw_psk31_busy(void) +{ + return cw_transmit_ongoing; +} + diff --git a/src/common/Audio/cw.h b/src/common/Audio/cw.h new file mode 100644 index 0000000..39be9c5 --- /dev/null +++ b/src/common/Audio/cw.h @@ -0,0 +1,53 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Matthias P. Braendli + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + +#ifndef __CW_H_ +#define __CW_H_ + +#include <stdint.h> +#include <stddef.h> + +// Setup the CW generator to create audio samples at the given +// samplerate. +void cw_psk31_init(unsigned int samplerate); + +// Append new CW or PSK31 text to transmit +// CW/PSK31 audio centre frequency in Hz +// if dit_duration == 0, message is sent in PSK31 +// otherwise it is sent in CW, with dit_duration in ms +// returns 0 on failure, 1 on success +int cw_psk31_push_message(const char* text, int frequency, int dit_duration); + +// Write the waveform into the buffer (stereo), both for cw and psk31 +size_t cw_psk31_fill_buffer(int16_t *buf, size_t bufsize); + +// Return 1 if the CW or PSK31 generator is running +int cw_psk31_busy(void); + +void cw_message_sent(const char*); + +size_t cw_symbol(uint8_t, uint8_t *, size_t); + +#endif // __CW_H_ + diff --git a/src/common/Audio/tone.c b/src/common/Audio/tone.c new file mode 100644 index 0000000..90a8394 --- /dev/null +++ b/src/common/Audio/tone.c @@ -0,0 +1,358 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Maximilien Cuony, Matthias P. Braendli + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + +#include "Audio/tone.h" +#include "Core/common.h" +#include "GPIO/usart.h" + +#include <stdlib.h> +#include "queue.h" + +#ifdef SIMULATOR +#include <math.h> +#define arm_cos_f32 cosf +#define arm_sin_f32 sinf +#else +#include "arm_math.h" +#endif + +#define TONE_N 800 + +/* DTMF frequencies + * COL_1 COL_2 COL_3 COL_A + * 1209 1336 1477 1633 + * 697 [1] [2] [3] [A] ROW_1 + * 770 [4] [5] [6] [B] ROW_4 + * 852 [7] [8] [9] [C] ROW_7 + * 941 [*] [0] [#] [D] ROW_STAR + * + * We need 0, 7, and 1750Hz for now, having detectors for + * 1209, 1336, 852, 941 and 1750 is sufficient. */ + +#define DET_COL_1 0 +#define DET_COL_2 1 +#define DET_ROW_7 2 +#define DET_ROW_STAR 3 +#define DET_1750 4 +#define NUM_DETECTORS 5 + +// Incomplete because not all frequencies decoded +enum dtmf_code { + DTMF_NONE = 0, + DTMF_0, + DTMF_7, + DTMF_8, + DTMF_STAR, +}; + +struct tone_detector { + float coef; + float Q1; + float Q2; +}; + +static struct tone_detector detectors[NUM_DETECTORS]; + +static int num_samples_analysed = 0; +static float prev_mean = 0; +static uint32_t accum = 0; +static int lost_results = 0; +static QueueHandle_t m_squared_queue; + +// Apply an IIR filter with alpha = 5/16 to smooth out variations. +// Values are normalised s.t. the mean corresponds to 100 +static int32_t normalised_results[NUM_DETECTORS]; + +static int num_tone_1750_detected = 0; +static uint64_t tone_1750_detected_since = 0; +static int detectors_enabled = 0; + +static enum dtmf_code dtmf_last_seen = 0; +static uint64_t dtmf_last_seen_at = 0; + +// Store the sequence of dtmf codes in this FIFO. If no DTMF code gets +// decoded in the interval, a NONE gets inserted into the sequence +#define NUM_DTMF_SEQ 3 +#define DTMF_MAX_TONE_INTERVAL 2500 +static enum dtmf_code dtmf_sequence[NUM_DTMF_SEQ]; + +static char* dtmf_to_str(enum dtmf_code code) +{ + char *codestr = "?"; + switch (code) { + case DTMF_0: codestr = "0"; break; + case DTMF_7: codestr = "7"; break; + case DTMF_8: codestr = "8"; break; + case DTMF_STAR: codestr = "*"; break; + case DTMF_NONE: codestr = "x"; break; + } + return codestr; +} + +static inline void push_dtmf_code(enum dtmf_code code) +{ + for (int i = 0; i < NUM_DTMF_SEQ-1; i++) { + dtmf_sequence[i] = dtmf_sequence[i+1]; + } + dtmf_sequence[NUM_DTMF_SEQ-1] = code; + + if (code != DTMF_NONE) { + usart_debug("DTMF: [%s, %s, %s]\r\n", + dtmf_to_str(dtmf_sequence[0]), + dtmf_to_str(dtmf_sequence[1]), + dtmf_to_str(dtmf_sequence[2])); + } +} + +// TODO: Does that depend on TONE_N? +const int thresh_dtmf = 200; +const int thresh_1750 = 300; +const int num_1750_required = 5; + +static void analyse_dtmf() +{ + // Bits 0 to 9 are numbers, bit 10 to 13 letters, bit 14 is star, 15 is hash + const uint16_t pattern = + ((normalised_results[DET_COL_1] > thresh_dtmf && + normalised_results[DET_ROW_7] > thresh_dtmf) ? (1 << 7) : 0) + + ((normalised_results[DET_COL_2] > thresh_dtmf && + normalised_results[DET_ROW_7] > thresh_dtmf) ? (1 << 8) : 0) + + ((normalised_results[DET_COL_1] > thresh_dtmf && + normalised_results[DET_ROW_STAR] > thresh_dtmf) ? (1 << 14) : 0) + + ((normalised_results[DET_COL_2] > thresh_dtmf && + normalised_results[DET_ROW_STAR] > thresh_dtmf) ? (1 << 0) : 0); + + // Match patterns exactly to exclude multiple simultaneous DTMF codes. + if (pattern == (1 << 0)) { + if (dtmf_last_seen != DTMF_0) { + push_dtmf_code(DTMF_0); + } + + dtmf_last_seen = DTMF_0; + dtmf_last_seen_at = timestamp_now(); + } + else if (pattern == (1 << 7)) { + if (dtmf_last_seen != DTMF_7) { + push_dtmf_code(DTMF_7); + } + + dtmf_last_seen = DTMF_7; + dtmf_last_seen_at = timestamp_now(); + } + else if (pattern == (1 << 8)) { + if (dtmf_last_seen != DTMF_8) { + push_dtmf_code(DTMF_8); + } + + dtmf_last_seen = DTMF_8; + dtmf_last_seen_at = timestamp_now(); + } + else if (pattern == (1 << 14)) { + if (dtmf_last_seen != DTMF_STAR) { + push_dtmf_code(DTMF_STAR); + } + + dtmf_last_seen = DTMF_STAR; + dtmf_last_seen_at = timestamp_now(); + } + else if (dtmf_last_seen_at + DTMF_MAX_TONE_INTERVAL < timestamp_now()) { + // Flush out all codes + push_dtmf_code(DTMF_NONE); + + dtmf_last_seen = DTMF_NONE; + dtmf_last_seen_at = timestamp_now(); + } +} + + +int tone_1750_status() +{ + return num_tone_1750_detected >= num_1750_required; +} + +int tone_1750_for_5_seconds() +{ + return (num_tone_1750_detected >= num_1750_required) && + tone_1750_detected_since + 5000 < timestamp_now(); +} + +int tone_fax_status() +{ + return dtmf_sequence[0] == DTMF_0 && + dtmf_sequence[1] == DTMF_7 && + dtmf_sequence[2] == DTMF_STAR; +} + +static inline void init_detector(struct tone_detector* detector, int freq) { + detector->coef = 2.0f * arm_cos_f32(2.0f * FLOAT_PI * freq / AUDIO_IN_RATE); + detector->Q1 = 0; + detector->Q2 = 0; +} + +void tone_init() { + m_squared_queue = xQueueCreate(2, NUM_DETECTORS * sizeof(float)); + if (m_squared_queue == 0) { + trigger_fault(FAULT_SOURCE_ADC2_QUEUE); + } + + for (int i = 0; i < NUM_DTMF_SEQ; i++) { + dtmf_sequence[i] = DTMF_NONE; + } + + init_detector(&detectors[DET_COL_1], 1209); + init_detector(&detectors[DET_COL_2], 1336); + init_detector(&detectors[DET_ROW_7], 852); + init_detector(&detectors[DET_ROW_STAR], 941); + init_detector(&detectors[DET_1750], 1750); +} + +void tone_detector_enable(int enable) +{ + if (enable && !detectors_enabled) { + num_samples_analysed = 0; + prev_mean = 0; + accum = 0; + for (int det = 0; det < NUM_DETECTORS; det++) { + detectors[det].Q1 = 0; + detectors[det].Q2 = 0; + } + audio_in_enable(1); + detectors_enabled = 1; + } + else if (!enable && detectors_enabled) { + audio_in_enable(0); + detectors_enabled = 0; + } +} + +void tone_detect_push_sample(const uint16_t sample, int is_irq) +{ + num_samples_analysed++; + + accum += sample; + + // Do not do tone detection before we have calculated a mean + if (prev_mean > 0) { + const float s = sample - prev_mean; + + for (int det = 0; det < NUM_DETECTORS; det++) { + struct tone_detector *detector = &detectors[det]; + + float Q0 = detector->coef * detector->Q1 - detector->Q2 + s; + detector->Q2 = detector->Q1; + detector->Q1 = Q0; + } + } + + if (num_samples_analysed == TONE_N) { + num_samples_analysed = 0; + + if (prev_mean > 0) { + float m_squared[NUM_DETECTORS]; + for (int det = 0; det < NUM_DETECTORS; det++) { + struct tone_detector *detector = &detectors[det]; + m_squared[det] = + detector->Q1 * detector->Q1 + + detector->Q2 * detector->Q2 - + detector->coef * detector->Q1 * detector->Q2; + detector->Q1 = 0; + detector->Q2 = 0; + } + + BaseType_t require_context_switch = 0; + int success; + if (is_irq) { + success = xQueueSendToBackFromISR( + m_squared_queue, + &m_squared, + &require_context_switch); + } + else { + success = xQueueSendToBack( + m_squared_queue, + &m_squared, + 0); + } + + if (success == pdFALSE) { + lost_results++; + } + } + + prev_mean = (float)accum / (float)TONE_N; + accum = 0; + } +} + +void tone_do_analysis() +{ + float m[NUM_DETECTORS]; + while (!xQueueReceive(m_squared_queue, &m, portMAX_DELAY)) {} + + float inv_mean = 0; + for (int det = 0; det < NUM_DETECTORS; det++) { + m[det] = sqrtf(m[det]); + inv_mean += m[det]; + } + inv_mean = NUM_DETECTORS / inv_mean; + + for (int det = 0; det < NUM_DETECTORS; det++) { + normalised_results[det] = + (11 * normalised_results[det] + + (int)(5 * 100 * m[det] * inv_mean)) + >> 4; // divide by 16 + } + + if (num_tone_1750_detected < num_1750_required && + normalised_results[DET_1750] > thresh_1750) { + num_tone_1750_detected++; + + if (num_tone_1750_detected == num_1750_required) { + tone_1750_detected_since = timestamp_now(); + } + } + else if (num_tone_1750_detected > 0 && + normalised_results[DET_1750] <= thresh_1750) { + num_tone_1750_detected--; + } + + analyse_dtmf(); + +#if PRINT_TONES_STATS + static int printcounter = 0; + if (++printcounter == 5) { + usart_debug("Tones: % 3d % 3d % 3d % 3d % 3d since %d\r\n", + normalised_results[0], + normalised_results[1], + normalised_results[2], + normalised_results[3], + normalised_results[4], + (int)(timestamp_now() - tone_1750_detected_since) + ); + + printcounter = 0; + } +#endif // PRINT_TONES_STATS +} + diff --git a/src/common/Audio/tone.h b/src/common/Audio/tone.h new file mode 100644 index 0000000..ba05304 --- /dev/null +++ b/src/common/Audio/tone.h @@ -0,0 +1,53 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Matthias P. Braendli, Maximilien Cuony + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + +#ifndef __TONE_H_ +#define __TONE_H_ + +#include <stdio.h> +#include "Audio/audio_in.h" + +#define TONE_BUFFER_LEN AUDIO_IN_BUF_LEN + +void tone_init(void); + +void tone_detector_enable(int enable); + +/* Return 1 when 1750 detected, 0 otherwise */ +int tone_1750_status(void); + +/* Return 1 if 1750 is currently detected, and has been already for at + * least 5s */ +int tone_1750_for_5_seconds(void); + +/* The FAX status is 1 if the recently decoded DTMF is the 0-7-* sequence. */ +int tone_fax_status(void); + +/* Must be called by task to do the analysis */ +void tone_do_analysis(void); + +/* Push a sample from the ADC ISR or Pulseaudio task */ +void tone_detect_push_sample(const uint16_t sample, int is_irq); + +#endif |