diff --git a/radioCWModulator_F32.cpp b/radioCWModulator_F32.cpp new file mode 100644 index 0000000..d96552c --- /dev/null +++ b/radioCWModulator_F32.cpp @@ -0,0 +1,207 @@ +/* +* radioCWModulator_F32.cpp +* +* Created: Bob larkin W7PUA March 2023 +* +* License: MIT License. Use at your own risk. +*/ + +#include "radioCWModulator_F32.h" +#include "sinTable512_f32.h" + +void radioCWModulator_F32::update(void) { + float32_t keyData[128]; // CW key down and up, 0.0f and 1.0f + float32_t modulateCW[128]; // Storage for data to modulate sine wave + uint16_t index, i; + float32_t a, b; + audio_block_f32_t *blockOut; + + blockOut = AudioStream_F32::allocate_f32(); // Output block + if (!blockOut) return; + + // A new character cannot enter sendBuffer during an interrupt, but + // the state IDLE_CW can be created by some other state ending. + // So it needs to be in the audio sample loop. + + // We always generate CW at 12 ksps. The number of data points in this + // generation varies to provide 128 output output points after + // interpolation to 48 or 96 ksps. + for(i=0; i 0) // Get next char, if available + { + c = sendBuffer[indexR]; + if (c>95) + c -= 64; // Convert lc to caps + else + c -= 32; // Move subscript to (0, 63) + ic = mc[(int)c]; // Ch to morse code lookup + if (c==27) // Long Dash from ';' + { + stateCW = LONG_DASH; + levelCW = 1.0f; + timeMsF = 0.0f; + timeMsI = 0; + } + else if (ic==0X17) // A space character + { + stateCW = WORD_SPACE; + levelCW = 0.0f; + timeMsF = 0.0f; + timeMsI = 0; + } + else if(ic>1 && (ic & 1)==0x01) + { + stateCW = DASH_CW; + levelCW = 1.0f; + timeMsF = 0.0f; + timeMsI = 0; + } + else if(ic>1 && (ic & 1)==0X00) + { + stateCW = DOT_CW; + levelCW = 1.0f; + timeMsF = 0.0f; + timeMsI = 0; + } + else if(ic==0X01) + { + stateCW = IDLE_CW; + levelCW = 0.0f; + } + } // end, if new character + break; + case DASH_CW: + if(timeMsI > dashCW) // Finished dash + { + levelCW = 0.0f; + if(ic>1) ic >>= 1; // Shift right 1 + if(ic==1) + stateCW = CHAR_SPACE; + else + stateCW = DOT_DASH_SPACE; + timeMsF = 0.0f; + timeMsI = 0; + } + break; + case DOT_CW: + if(timeMsI > dotCW) + { + levelCW = 0.0f; + if(ic>1) ic >>= 1; // Shift right 1 + if(ic==1) + stateCW = CHAR_SPACE; + else + stateCW = DOT_DASH_SPACE; + timeMsF = 0.0f; + timeMsI = 0; + } + break; + case DOT_DASH_SPACE: + if(timeMsI > ddCW) // Just finished + { + timeMsF = 0.0f; + timeMsI = 0; + if(ic>1 && (ic & 1)==0x01) + { + stateCW = DASH_CW; + levelCW = 1.0f; + } + else if(ic>1 && (ic & 1)==0X00) + { + stateCW = DOT_CW; + levelCW = 1.0f; + } + else + { + stateCW = IDLE_CW; + levelCW = 0.0; + } + } + break; + case CHAR_SPACE: + if(timeMsI > chCW+ddCW) // Just finished sending a character + // chCW+ddCW sounds better than chCW to me. + { + indexR++; // Sending is ended, bump the read index + indexR = indexR & 0X1FF; // Confine to (0, 511) + timeMsF = 0.0f; + timeMsI = 0; + stateCW = IDLE_CW; + break; + } + case WORD_SPACE: + if(timeMsI > spCW) // Just finished sending a space + { + indexR++; // Sending is ended, bump the read index + indexR = indexR & 0X1FF; // Confine to (0, 511) + timeMsF = 0.0f; + timeMsI = 0; + stateCW = IDLE_CW; + break; + } + case LONG_DASH: + if(timeMsI > longDashCW) // Just finished sending a long dash + { + levelCW = 0.0f; + stateCW = CHAR_SPACE; + timeMsF = 0.0f; + timeMsI = 0; + break; + } + } // end switch + noXmit: + keyData[i] = levelCW; + } // end, over all 128 times + + arm_fir_f32(&GaussLPFInst, keyData, keyData, nSamplesPerUpdate); + + // INTERPOLATE - Interpolate here to support higher sample rates, + // while using the same spectral LPF. To this point we have 128, 32 + // or 16 "active" data points for 12, 48, or 96ksps. + // + // 0 1 2 3 4 5 6 7 8 9 i + // 0 0 0 0 1 1 1 1 2 2 i/4 + // t 0 0 0 t 0 0 0 t 0 i==4*(i/4) + // + if(nSample > 1) // Only needs interpolation if >1 + { + for(i=0; i<128; i++) + { + if( i==(nSample*(1/nSample)) ) + modulateCW[i]= keyData[i/nSample]; + else + modulateCW[i] = 0.0f; + } + arm_fir_f32(&interpolateLPFInst, modulateCW, keyData, 128); + } + + // Interpolation is done, now amplitude modulate CW onto a sine wave. + for (i=0; i < 128; i++) + { + phaseS += phaseIncrement; + if (phaseS > 512.0f) phaseS -= 512.0f; + index = (uint16_t) phaseS; + float32_t deltaPhase = phaseS - (float32_t)index; + // Read two nearest values of input value from the sine table + a = sinTable512_f32[index]; + b = sinTable512_f32[index+1]; + blockOut->data[i] = magnitude*keyData[i]*(a+(b-a)*deltaPhase); + } + AudioStream_F32::transmit(blockOut); + AudioStream_F32::release (blockOut); + } diff --git a/radioCWModulator_F32.h b/radioCWModulator_F32.h new file mode 100644 index 0000000..1aa2a8e --- /dev/null +++ b/radioCWModulator_F32.h @@ -0,0 +1,356 @@ +/* + * radioCWModulator_F32.h For the OpenAudio_TeensyArduino library + * (floating point audio). + * MIT License - Copyright March 2023 Bob Larkin, original write + * + * Parts of this are based on Audio Library for Teensy 3.x and 4.x that is + * Copyright (c) 2023, Paul Stoffregen, paul@pjrc.com + * + * Development of the Teensy audio library was funded by PJRC.COM, LLC by sales of + * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop + * open source software by purchasing Teensy or other PJRC products. + * + * 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, development funding 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. + */ +/* The method comes directly from the UHFA program for the DSP-10 project of 1999 + * by Bob Larkin. Much of the code comes from there, as well. + * + * Sample rates: The CW is generated at a sample rate of 12 ksps. This + * supports a good Gaussian filter to limit the spectral width which is + * equivalent to removing "key clicks." To use higher sample rates, the + * generation is only done for every 2nd, 4th or 8th sample. The output + * for sample rates above 12 ksps are interpolated. The supported sample + * rates become 11.025, 12, 44.1, 48, 50, 88, 96 and 100 ksps or others + * in those general ranges. + */ + + +#ifndef radioCWModulator_F32_h_ +#define radioCWModulator_F32_h_ + +#include "Arduino.h" +#include "AudioStream_F32.h" + +class radioCWModulator_F32 : public AudioStream_F32 +{ +//GUI: inputs:1, outputs:1 //this line used for automatic generation of GUI node +//GUI: shortName:DetCTCSS +public: + radioCWModulator_F32(void) : AudioStream_F32(0, NULL) { + sample_rate_Hz = AUDIO_SAMPLE_RATE; + block_size = AUDIO_BLOCK_SAMPLES; + initCW(); + } + // Option of AudioSettings_F32 change sample rate (always 128 block size): + radioCWModulator_F32(const AudioSettings_F32 &settings) : AudioStream_F32(0, NULL) { + sample_rate_Hz = settings.sample_rate_Hz; + block_size = AUDIO_BLOCK_SAMPLES; + initCW(); + } + +// Values for stateCW +#define IDLE_CW 0 +#define DASH_CW 1 +#define DOT_CW 2 +#define DOT_DASH_SPACE 3 +#define CHAR_SPACE 4 +#define WORD_SPACE 5 +#define LONG_DASH 6 + + void initCW(void) { // Public, but primarily for building objects. Not required in INO. + for(int kk=0; kk<512; kk++) + sendBuffer[kk] = 0; + indexW = 0; + indexR = 0; + setCWSpeedWPM(speedWPM); + arm_fir_init_f32(&interpolateLPFInst, 59, &interpolateLPF[0], &interpolateLPFState[0], 128); + setSampleRate_Hz(sample_rate_Hz); + setFrequency(600.0f); + } + + // Enables the operation of the update(). This is needed when there + // are separate transmit and receive times. + void enableTransmit(bool _transmit) { + enableXmit = _transmit; + } + + bool getEnableTransmit(void) { + return enableXmit; + } + + uint16_t getBufferSpace(void) { + return 512 - ((indexW - indexR) & 0X1FF); + } + + // Place character into queue for sending + bool sendCW(uint16_t _ch) { + if(getBufferSpace() > 0) + { + sendBuffer[indexW++] = _ch; + indexW &= 0X1FF; + return true; + } + else + return false; + } + + bool sendStringCW(char* _pStr) + { + uint16_t space = getBufferSpace(); + uint16_t size = strlen(_pStr); + // Serial.print(space); Serial.print(" space size "); Serial.println(size); + if(space < size) return false; + for(int kk=0; kk<(int)strlen(_pStr); kk++) + sendCW( (uint16_t)*(_pStr+kk) ); + return true; + } + + void setCWSpeedWPM(uint16_t _speed) { + uint16_t kk = 0; + while(tCW[kk].speedWPM < _speed) + kk++; + speedIndex = kk; + dotCW = (uint32_t)tCW[kk].dot; /* msec per dot */ + dashCW = (uint32_t)tCW[kk].dash; /* msec per dash */ + ddCW = (uint32_t)tCW[kk].dd; /* msec between dot or dash */ + chCW = (uint32_t)tCW[kk].ch; /* msec at end of each char */ + spCW = (uint32_t)tCW[kk].sp; /* msec for space char */ + } + + void setFrequency(float32_t _freq) { // Defaults to 600 Hz + frequencyHz = _freq; + if (frequencyHz < 0.0f) + frequencyHz = 0.0f; + if (frequencyHz > sample_rate_Hz/2.0f) + frequencyHz = sample_rate_Hz/2.0f; + phaseIncrement = 512.0f * frequencyHz / sample_rate_Hz; + } + + + void setLongDashMs(uint32_t _longDashMs) { // Defaults to 1000 mSec + longDashCW = _longDashMs; + } + + // See note above on sample rates. + void setSampleRate_Hz (float32_t fs_Hz) { // (const float32_t &fs_Hz) { + sample_rate_Hz = fs_Hz; + timeSamplesMs = 1000.0f/sample_rate_Hz; // In millisec + if(sample_rate_Hz>11000.0f && sample_rate_Hz<12100.0f) + { + nSample = 1; + nSamplesPerUpdate = 128; + } + else if(sample_rate_Hz>44000.0f && sample_rate_Hz<50100.0f) + { + nSample = 4; + nSamplesPerUpdate = 32; + } + else if(sample_rate_Hz>88000.0f && sample_rate_Hz<100100.0f) + { + nSample = 8; + nSamplesPerUpdate = 16; + } + else + { + Serial.print(sample_rate_Hz); + Serial.println(" sample rate not supported."); + nSample = 4; + nSamplesPerUpdate = 32; + } + arm_fir_init_f32(&GaussLPFInst, 145, &firGaussCoeffs[0], &GaussState[0], nSamplesPerUpdate); + setFrequency(frequencyHz); + } + + // The amplitude, a, is the peak, as in zero-to-peak. This produces outputs + // ranging from -a to +a. + void amplitude(float32_t _a) { + if (_a < 0.0f) + _a = 0.0f; + magnitude = _a; + } + + virtual void update(void); + +private: + uint8_t sendBuffer[512]; // Circular send buffer + uint16_t indexW = 0; // Write index in to buffer + uint16_t indexR = 0; // Read index into buffer + float32_t sample_rate_Hz = AUDIO_SAMPLE_RATE; + int16_t block_size = 128; + int16_t nSample = 4; // + int16_t nSamplesPerUpdate = 32; + uint16_t stateCW = IDLE_CW; + uint8_t c = 0; + uint16_t ic = 0; // Current character as integer + bool enableXmit = true; + float32_t levelCW = 0.0f; // sample by sample + float32_t frequencyHz = 600.0f; + float32_t phaseIncrement = 512.0f*frequencyHz/sample_rate_Hz; + float32_t phaseS = 0.0f; + float32_t magnitude = 1.0f; + + float32_t timeSamplesMs = 1000.0f/sample_rate_Hz; // In millisec + float32_t timeMsF = 0.0f; // Update the event clock + uint32_t timeMsI = 0; // This is easier to use + uint16_t speedWPM = 13; + uint16_t speedIndex = 8; + + uint32_t dotCW; /* msec per dot */ + uint32_t dashCW; /* msec per dash */ + uint32_t ddCW; /* msec between dot or dash */ + uint32_t chCW; /* msec at end of each char */ + uint32_t spCW; /* msec for space char */ + uint32_t longDashCW = 1000; + + float32_t* firGaussCoeffs = coeffLPFGaussCW70; + arm_fir_instance_f32 GaussLPFInst; + float32_t GaussState[128 + 145]; + + arm_fir_instance_f32 interpolateLPFInst; + float32_t interpolateLPFState[128 + 59]; + + /* The following mc[] table defines the morse code sequences + * in terms of bits. Starting from lsb and continuing until only one + * "1" remains, send a dot for a "0" and a dash for a "1", with + * a right shift after each dot or dash. */ + + /* sp " $ ' */ +const uint16_t mc[64] = {0x17,0x01,0x52,0x01,0xC8,0x01,0x01,0x5E, + + /* ( ) , - . / */ + 0x2D,0x6D,0x01,0x01,0x73,0x61,0x6A,0x29, + + /* 0 1 2 3 4 5 6 7 */ + 0x3f,0x3e,0x3c,0x38,0x30,0x20,0x21,0x23, + + /* ; = long dash 8 9 : ; = KN ? */ + 0x27,0x2f,0x47,0x01,0x01,0x31,0x2D,0x4C, + + /* @ A B C D E F G */ + 0x5a,0x06,0x11,0x15,0x09,0x02,0x14,0x0b, + + /* H I J K L M N O */ + 0x10,0x04,0x1e,0x0d,0x12,0x07,0x05,0x0f, + + /* P Q R S T U V W */ + 0x16,0x1b,0x0a,0x08,0x03,0x0c,0x18,0x0e, + + /* X Y Z AR AS SK Err _ */ + 0x19,0x1d,0x13,0x2A,0x22,0x68,0x80,0x6C}; + +/* The following set the weightings vs. code speed in wpm + * by selecting the delays for + * .dot length of a dot in ms + * .dash length of a dash in ms + * .dd time between dots and dashes, ms + * .ch time between characters of the same word, ms + * .sp time duration between words, ms */ +struct cw_data { + uint16_t speedWPM; + uint16_t dot; /* msec per dot */ + uint16_t dash; /* msec per dash */ + uint16_t dd; /* msec between dot or dash */ + uint16_t ch; /* msec at end of each char */ + uint16_t sp; /* msec for space char */ + }; +/* Note: Code speed can be any. There are 5 times in milliseconds that + * set the actual speed and weighting. The index (0, 28) allows 29 of these + * that are initialized to 5 WPM to 50 WPM with nominal weightings. */ +struct cw_data tCW[29] = { + { 5, 150, 530, 150, 900,1200}, + { 6, 140, 480, 140, 750,1000}, + { 7, 125, 430, 125, 550, 800}, + { 8, 120, 400, 120, 400, 725}, + { 9, 120, 380, 120, 300, 560}, + { 10, 120, 360, 120, 240, 480}, + { 11, 110, 330, 110, 220, 440}, + { 12, 100, 300, 100, 200, 400}, + { 13, 92, 276, 92, 184, 368}, + { 14, 86, 258, 86, 172, 344}, + { 15, 80, 240, 80, 160, 320}, + { 16, 75, 225, 75, 150, 300}, + { 17, 70, 212, 70, 142, 283}, + { 18, 66, 200, 66, 134, 267}, + { 19, 63, 189, 63, 126, 252}, + { 20, 60, 180, 60, 120, 240}, + { 21, 57, 171, 57, 114, 230}, + { 22, 55, 164, 55, 109, 220}, + { 23, 52, 157, 52, 105, 210}, + { 24, 50, 150, 50, 100, 200}, + { 25, 48, 144, 48, 96, 192}, + { 26, 47, 138, 47, 91, 185}, + { 27, 45, 133, 45, 88, 179}, + { 28, 43, 129, 43, 86, 172}, + { 29, 41, 124, 41, 83, 166}, + { 30, 40, 120, 40, 80, 160}, + { 35, 34, 103, 34, 69, 138}, + { 40, 30, 90, 30, 60, 120}, + { 50, 24, 72, 24, 48, 96}}; + +// CW LPF Gaussian 12 ksps, 70 Hz at 3 dB +float32_t coeffLPFGaussCW70[145] = { + 0.0000000994f, 0.0000002465f, 0.0000005437f, 0.0000010924f, 0.0000020348f, + 0.0000035609f, 0.0000059142f, 0.0000093971f, 0.0000143746f, 0.0000212771f, + 0.0000306017f, 0.0000429118f, 0.0000588363f, 0.0000790660f, 0.0001043500f, + 0.0001354903f, 0.0001733351f, 0.0002187713f, 0.0002727164f, 0.0003361089f, + 0.0004098986f, 0.0004950364f, 0.0005924635f, 0.0007031003f, 0.0008278357f, + 0.0009675161f, 0.0011229345f, 0.0012948202f, 0.0014838290f, 0.0016905333f, + 0.0019154139f, 0.0021588516f, 0.0024211200f, 0.0027023793f, 0.0030026708f, + 0.0033219125f, 0.0036598959f, 0.0040162835f, 0.0043906075f, 0.0047822700f, + 0.0051905436f, 0.0056145733f, 0.0060533799f, 0.0065058635f, 0.0069708087f, + 0.0074468905f, 0.0079326809f, 0.0084266565f, 0.0089272066f, 0.0094326423f, + 0.0099412055f, 0.0104510798f, 0.0109603999f, 0.0114672630f, 0.0119697396f, + 0.0124658847f, 0.0129537493f, 0.0134313915f, 0.0138968878f, 0.0143483444f, + 0.0147839080f, 0.0152017763f, 0.0156002085f, 0.0159775353f, 0.0163321678f, + 0.0166626067f, 0.0169674505f, 0.0172454031f, 0.0174952809f, 0.0177160188f, + 0.0179066758f, 0.0180664400f, 0.0181946320f, 0.0182907087f, 0.0183542653f, + 0.0183850369f, 0.0183828991f, 0.0183478684f, 0.0182801008f, 0.0181798905f, + 0.0180476675f, 0.0178839942f, 0.0176895623f, 0.0174651874f, 0.0172118049f, + 0.0169304634f, 0.0166223192f, 0.0162886292f, 0.0159307439f, 0.0155500995f, + 0.0151482106f, 0.0147266612f, 0.0142870968f, 0.0138312154f, 0.0133607587f, + 0.0128775032f, 0.0123832512f, 0.0118798218f, 0.0113690419f, 0.0108527375f, + 0.0103327249f, 0.0098108024f, 0.0092887417f, 0.0087682803f, 0.0082511135f, + 0.0077388875f, 0.0072331919f, 0.0067355538f, 0.0062474314f, 0.0057702081f, + 0.0053051882f, 0.0048535913f, 0.0044165491f, 0.0039951010f, 0.0035901916f, + 0.0032026682f, 0.0028332785f, 0.0024826695f, 0.0021513865f, 0.0018398728f, + 0.0015484699f, 0.0012774182f, 0.0010268582f, 0.0007968321f, 0.0005872861f, + 0.0003980729f, 0.0002289548f, 0.0000796069f,-0.0000503789f,-0.0001614900f, +-0.0002542886f,-0.0003294068f,-0.0003875421f,-0.0004294517f,-0.0004559477f, +-0.0004678910f,-0.0004661861f,-0.0004517752f,-0.0004256325f,-0.0003887583f, +-0.0003421730f,-0.0002869118f,-0.0002240186f,-0.0001545404f,-0.0000795219f}; + +/* FIR filter designed with http://t-filter.appspot.com +Fs = 96000 Hz [48000] + 0 Hz to 4000 Hz [0 to 2000] actual ripple = 0.05 dB +11000 Hz to 48000 Hz [5500 to 24000] actual atten = -93.1 dB */ +float32_t interpolateLPF[59] = { +-0.000052355f,-0.000146720f,-0.000307316f,-0.000517033f,-0.000721194f,-0.000819999f, +-0.000680277f,-0.000169005f, 0.000793689f, 0.002172809f, 0.003767656f, 0.005194993f, + 0.005927352f, 0.005395822f, 0.003149023f,-0.000960853f,-0.006611837f,-0.012919698f, +-0.018466308f,-0.021473003f,-0.020108212f,-0.012880252f, 0.000965492f, 0.021140621f, + 0.046185831f, 0.073574790f, 0.100053429f, 0.122161788f, 0.136838160f, 0.141979757f, + 0.136838160f, 0.122161788f, 0.100053429f, 0.073574790f, 0.046185831f, 0.021140621f, + +0.000965492f,-0.012880252f,-0.020108212f,-0.021473003f,-0.018466308f,-0.012919698f, +-0.006611837f,-0.000960853f, 0.003149023f, 0.005395822f, 0.005927352f, 0.005194993f, + 0.003767656f, 0.002172809f, 0.000793689f,-0.000169005f,-0.000680277f,-0.000819999f, +-0.000721194f,-0.000517033f,-0.000307316f,-0.000146720f,-0.000052355f}; + +}; +#endif