Added class radioCWModulator_F32 for Morse code

master
boblark 1 year ago
parent 2118306ed9
commit c8291c2d32
  1. 207
      radioCWModulator_F32.cpp
  2. 356
      radioCWModulator_F32.h

@ -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<nSamplesPerUpdate; i++)
{
timeMsF += timeSamplesMs;
timeMsI = (uint32_t)(0.5 + timeMsF);
if(!enableXmit) // Just leave the key up and no new characters
{
levelCW = 0.0f;
goto noXmit;
}
switch(stateCW)
{
case IDLE_CW:
timeMsF = 0.0f;
timeMsI = 0;
if(((indexW-indexR)&0X1FF) > 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);
}

@ -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
Loading…
Cancel
Save