Add Voice Clipper class

master
boblark 2 years ago
parent a63ead7a27
commit f5d1c01a58
  1. 1
      OpenAudio_ArduinoLibrary.h
  2. 100
      docs/index.html
  3. 183
      examples/VoiceClipper/VoiceClipper.ino
  4. 184
      examples/VoiceClipper12/VoiceClipper12.ino
  5. BIN
      gui/DesignTool_F32.zip
  6. 184
      radioVoiceClipper_F32.cpp
  7. 343
      radioVoiceClipper_F32.h

@ -63,6 +63,7 @@
#include "AudioFilterFIRGeneral_F32.h"
#include "radioCESSBtransmit_F32.h"
#include "radioCESSB_Z_transmit_F32.h"
#include "radioVoiceClipper_F32.h"
#include "RadioFMDetector_F32.h"
#include "radioBFSKmodulator_F32.h"
#include "radioFT8Modulator_F32.h"

@ -418,10 +418,9 @@ span.mainfunction {color: #993300; font-weight: bolder}
{"type":"radioModulatedGenerator_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"Modulator","inputs":"2","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"2"}},
{"type":"radioNoiseBlanker_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"NoiseBlank","inputs":"2","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"2"}},
{"type":"radioCESSBtransmit_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"CESSB_Mod","inputs":"1","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"2"}},
{"type":"radioCESSB_Z_transmit_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"CESSB_Z_Mod","inputs":"1","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"2"}},
{"type":"radioVoiceClipper_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"Clipper","inputs":"1","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"2"}},
{"type":"RadioFMDiscriminator_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"FMDiscrim","inputs":"1","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"2"}},
{"type":"RadioBFSKModulator_F32","data":{"defaults":{"name":{"value":"new"}},"shortName":"BFSKMod","inputs":"0","output":"0","category":"radio-function","color":"#E6E0F8","icon":"arrow-in.png","outputs":"1"}},
@ -3793,6 +3792,103 @@ look ahead delay, as well.</p>
</div>
</script>
<script type="text/x-red" data-help-name="radioVoiceClipper_F32">
<!-- ============ radioVoiceClipper_F32 ========= -->
<h3>Summary</h3>
<div class=tooltipinfo>
<p>Applies controlled clipping and filtering for uses such as AM, FM and NBFM.
This prevents modulation peaks from exceeding a maximum level without increasing the
spectral width.
This can increase the power density of the modulation by 3 or more dB. The output
is ready to be applied to an AM or FM modulator..
</p>
</div>
<h3>Audio Connections</h3>
<table class=doc align=center cellpadding=3>
<tr class=top><th>Port</th><th>Purpose</th></tr>
<tr class=odd><td align=center>In 0</td><td>Input Audio Signal</td></tr>
<tr class=odd><td align=center>Out 0</td><td>Clipped Audio Signal</td></tr>
</table>
<h3>Functions</h3>
<p class=func><span class=keyword>setSampleRate_Hz</span>
(<strong>float32_t</strong> fs_Hz bitRate);</p>
<p class=desc>Specifically, this sets the sample rate, in samples per second,
that is used by the clipper. It also sets other parameters, such as
decimation ratios and filter cutoff frequencies. Thus this function
is <strong>required.</strong> Two ranges of sampling rate are currently
supported. These are 11 to 12 ksps and 44 to 50 ksps. There is no default value
and the VoiceClipper objects will not run if this function is not called.</p>
<p class=func><span class=keyword>getLevels</span>(<strong>int</strong> what);</p>
<p class=desc>Returns a pointer to a structure of type levels. This allows
knowledge of the average and peak levels at both the input and output sides
of the clipper and overshoot compensator. If what==0 the pointer is returned
but no updating is done. That is used to setup the process before data is
available. If what != 0, the contents of the structure are updated and measuring
is reset. The function levelDataCount() below can be used to set the time
between updates. The stucture is part of the object and is defined as:
<pre>
struct levels {
float32_t pwr0; // Average power at input
float32_t peak0; // Peak voltage at input
float32_t pwr1; // Average power at output
float32_t peak1; // Peak voltage at output
uint32_t countP; // Number of averaged samples for pwr0.
};
</pre></p>
<p class=func><span class=keyword>levelDataCount</span>();</p>
<p class=desc>Returns an uint32_t with the number of averaged samples
of the input power. See getLevels() above. The number of output
samples may differ by an integer factor because of decimation inside
the object.</p>
<p class=func><span class=keyword>setGains</span>(
<strong>float32_t</strong> gainIn,
<strong>float32_t</strong> gainCompensate,
<strong>float32_t</strong> gainOut);</p>
<p class=desc> These are the controls for the ViceCipper class. gainIn sets
the amount of clipping by setting the input level to the clipper.
gainCompensate sets the amount of correction applied to prevent
overshoot. A value of 1.8 is normally used. gainOut is
for convenience and sets the drive level to the next block. </p>
<h3>Examples</h3>
<p class=exam>File &gt; Examples &gt; OpenAudio_ArduinoLibrary &gt; VoiceClipper
</p>
<p class=exam>File &gt; Examples &gt; OpenAudio_ArduinoLibrary &gt; VoiceClipper12
</p>
<h3>Notes</h3>
<p>The technical description is in the include file for this class,
<a href="https://github.com/chipaudette/OpenAudio_ArduinoLibrary/blob/master/radioVoiceClipper_F32.h"
target="_blank">available from Github</a>
These should be used to understand the details of the Voice Clipper.
</p>
<p>The first activity for the Voice Clipper is to limit or clip the amplitude of the input signal.
Internally this always occurs when the magnitude of the input signal exceeds 1.0. This is all done
with floating point arithmetic so values may exceed 1.0. The input level where this occurs
depends on the setting for gainIn, described above. The maximum level seen ahead of the clipper
is measured by getLevels() as described above. One way to control the input to the
Voice Clipper block is with
<a href="http://www.janbob.com/electron/OpenAudio_Design_Tool/index.html?info=AudioEffectCompressor2_F32"
target="_blank">Compressor2 Library block.</a> Note that Compressor2
is not a clipper, but is rather an automatic gain control that uses look-ahead
processing to allow gradual gain changes.
</p>
<p>Voice Clipping as implemented here is intended for voice input, and so filters the voice
to a communications bandwidth of around 3000 Hz.</p>
</script>
<script type="text/x-red" data-template-name="radioCESSBtransmit_F32">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/x-red" data-help-name="RadioBFSKModulator_F32">
<h3>Summary</h3>

@ -0,0 +1,183 @@
// VoiceClipper.ino Bob Larkin W7PUA
// Uses radioVoiceClipper_F32.h and .cpp. See the .h file for
// more information and references. These clippers, when used with
// radio systems, are for AM and FM (or NBFM) modulation, and NOT SSB.
// See the similar class CESSBtransmit_F32 for SSB use.
//
// Tests with voice from SD Card file and a 1 second 750 Hz tone burst.
// Input is from the WAV file on the SD card. Output is to the Teensy
// Audio Adaptor left and right Line Outputs.
//
// The SD card may connect to different pins, depending on the
// hardware you are using. Configure the SD card
// pins to match your hardware. It is set for T4.x Rev D PJRC
// Teensy Audio Adaptor card here.
//
// Your microSD card must have W9GR12.WAV file loaded to it, found at
// https://github.com/chipaudette/OpenAudio_ArduinoLibrary/blob/master/utility/
//
// ******** This example runs at 48 ksps. *********
// As of March 2023, this could be adapted to any rate, 11 to 12 or 44 to 50 ksps.
//
// This example code is in the public domain.
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include "OpenAudio_ArduinoLibrary.h"
//#include "radioVoiceClipper_F32.h" part of "radioVoiceClipper_F32.h"
// T3.x supported sample rates: 2000, 8000, 11025, 16000, 22050, 24000, 32000, 44100, 44117, 48000,
// 88200, 88235 (44117*2), 95680, 96000, 176400, 176470, 192000
// T4.x supports any sample rate the codec will handle.
const float sample_rate_Hz = 48000.0f;
const int audio_block_samples = 128; // Always 128
AudioSettings_F32 audio_settings(sample_rate_Hz, audio_block_samples);
AudioSynthWaveformSine_F32 sine1(audio_settings);
AudioSDPlayer_F32 playWav1(audio_settings);
AudioMixer4_F32 mixer4_0;
radioVoiceClipper_F32 clipper1(audio_settings);
AudioOutputI2S_F32 audioOutput(audio_settings);
AudioAnalyzeFFT1024_F32 fft1;
AudioConnection_F32 patchCord0(playWav1, 0, mixer4_0, 0);
AudioConnection_F32 patchCord1(sine1, 0, mixer4_0, 1);
AudioConnection_F32 patchCord2(mixer4_0, 0, clipper1, 0);
AudioConnection_F32 patchCord3(clipper1, 0, audioOutput, 0);
AudioConnection_F32 patchCord4(clipper1, 0, audioOutput, 1);
AudioConnection_F32 patchCord5(clipper1, 0, fft1, 0);
AudioControlSGTL5000 sgtl5000_1;
// Use these with the Teensy 4.x Rev D Audio Shield (NOT for T3.x)
#define SDCARD_CS_PIN_Z 10
#define SDCARD_MOSI_PIN_Z 11
#define SDCARD_SCK_PIN_Z 13
// wavData is a global struct, definined in AudioSDPlayer_F32.h
// This provides information about the current WAV file to this .INO
struct wavData* pCurrentWavData;
// And data about the clipper
struct levelClipper* pLevelData;
uint32_t writeOne = 0;
uint32_t cntFFT = 0;
uint32_t ttt; // For timing test audio
uint32_t tp;
void setup() { // ********** SETUP **********
Serial.begin(9600); delay(1000);
Serial.println("*** Test Voice Clipper from SD Card Voice Sample ***");
AudioMemory_F32(70, audio_settings);
pCurrentWavData = playWav1.getCurrentWavData();
sgtl5000_1.enable();
delay(500);
SPI.setMOSI(SDCARD_MOSI_PIN_Z);
SPI.setSCK(SDCARD_SCK_PIN_Z);
Serial.print("SD.begin() returns "); Serial.println(SD.begin(SDCARD_CS_PIN_Z));
sine1.frequency(750.0f);
sine1.amplitude(0.707107);
clipper1.setSampleRate_Hz(48000.0f);
// Set input, correction, and output gains
float32_t Pre_Clip_Gain = 1.5f; // Use to set amount of clipping, 1.0 to 2.0f, 3 is excessive
// Correction gain=1.8 leaves max output overshoot at 2.2% for W9GR sample audio
clipper1.setGains(Pre_Clip_Gain, 1.8f, 1.0f);
pLevelData = clipper1.getLevels(0); // Gets pointer to struct
audioOutput.setGain(0.02); // <<< Output volume control
fft1.setOutputType(FFT_DBFS);
fft1.windowFunction(AudioWindowBlackmanHarris1024);
fft1.setNAverage(16);
ttt = millis(); // Time test audio
tp=millis();
}
void playFile(const char *filename) {
if(playWav1.isPlaying())
return;
Serial.println("");
Serial.print("Playing file: ");
Serial.println(filename);
playWav1.play(filename); // Start playing the file.
// A brief delay for the library read WAV info
delay(25);
Serial.print("WAV file format = "); Serial.println(pCurrentWavData->audio_format);
Serial.print("WAV number channels = "); Serial.println(pCurrentWavData->num_channels);
Serial.print("WAV File Sample Rate = "); Serial.println(pCurrentWavData->sample_rate);
Serial.print("Number of bits per Sample = "); Serial.println(pCurrentWavData->bits);
Serial.print("File length, seconds = ");
Serial.println(0.001f*(float32_t)playWav1.lengthMillis(), 3);
}
void loop() {
uint32_t tt=millis() - ttt;
if(tt < 2000)
{
// Thanks to W9GR for the test file, W9GR48.WAV.
playFile("W9GR48.WAV");
mixer4_0.gain(0, 1.41421356f); // Play WAV file on
mixer4_0.gain(1, 0.0f); // Sine Wave 1 off
}
else if(tt > 12300 && tt<13300)
{
mixer4_0.gain(1, 0.0f); // Play WAV file off
// The following puts a 1-sec 750 Hz, full amplitude tone into the input
// .707 on the generator and 1.414 here make the peak sine wave 1.000 at the CESSB input
mixer4_0.gain(1, 1.41421356f); // Sine Wave 1 on, 750 Hz
}
else if(tt >= 13300)
{
mixer4_0.gain(0, 1.41421356f); // Play WAV file on
mixer4_0.gain(1, 0.0f); // Sine Wave 1 off
ttt = millis(); // Start again
}
delay(1);
// Un-comment the following to print out the spectrum
/* if(fft1.available() && ++cntFFT>100 && cntFFT<102)
{
for(int kk=0; kk<512; kk++)
{
Serial.print(46.875f*(float32_t)kk); Serial.print(",");
Serial.println(fft1.read(kk));
}
}
*/
if(clipper1.levelDataCount() > 300) // Typically 300 to 3000
{
clipper1.getLevels(1); // Cause write of data to struct & reset
// Detailed Report
Serial.print(10.0f*log10f(pLevelData->pwr0));
Serial.print(" In Ave Pwr Out ");
Serial.println(10.0f*log10f(pLevelData->pwr1));
Serial.print(20.0f*log10f(pLevelData->peak0));
Serial.print(" In Peak Out ");
Serial.println(20.0f*log10f(pLevelData->peak1));
Serial.print(pLevelData->peak0, 6);
Serial.print(" In Peak Volts Out ");
Serial.println(pLevelData->peak1, 6);
Serial.print("Enhancement = ");
float32_t enhance = (10.0f*log10f(pLevelData->pwr1) - 20.0f*log10f(pLevelData->peak1)) -
(10.0f*log10f(pLevelData->pwr0) - 20.0f*log10f(pLevelData->peak0));
if(enhance<1.0f) enhance = 1.0f;
Serial.print(enhance); Serial.println(" dB");
/*
// CSV Report suitable for entering to spread sheet
// InAve, InPk, OutAve, OutPk, EnhancementdB
Serial.print(pLevelData->pwr0, 5); Serial.print(",");
Serial.print(pLevelData->peak0, 5); Serial.print(",");
Serial.print(pLevelData->pwr1, 5); Serial.print(",");
Serial.print(pLevelData->peak1, 5); Serial.print(",");
float32_t enhance = (10.0f*log10f(pLevelData->pwr1) - 20.0f*log10f(pLevelData->peak1)) -
(10.0f*log10f(pLevelData->pwr0) - 20.0f*log10f(pLevelData->peak0));
if(enhance<1.0f) enhance = 1.0f;
Serial.println(enhance);
*/
}
}

@ -0,0 +1,184 @@
// VoiceClipper12.ino Bob Larkin W7PUA
// Uses radioVoiceClipper_F32.h and .cpp. See the .h file for
// more information and references. These clippers, when used with
// radio systems, are for AM and FM (or NBFM) modulation, and NOT SSB.
// See the similar class CESSBtransmit_F32 for SSB use.
//
// Tests with voice from SD Card file and a 1 second 750 Hz tone burst.
// Input is from the WAV file on the SD card. Output is to the Teensy
// Audio Adaptor left and right Line Outputs.
//
// The SD card may connect to different pins, depending on the
// hardware you are using. Configure the SD card
// pins to match your hardware. It is set for T4.x Rev D PJRC
// Teensy Audio Adaptor card here.
//
// Your microSD card must have W9GR12.WAV file loaded to it, found at
// https://github.com/chipaudette/OpenAudio_ArduinoLibrary/blob/master/utility/
//
// ******** This example runs at 12 ksps. *********
// As of March 2023, this could be adapted to any rate, 11 to 12 or 44 to 50 ksps.
//
// This example code is in the public domain.
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include "OpenAudio_ArduinoLibrary.h"
//#include "radioVoiceClipper_F32.h" // part of OpenAudio_ArduinoLibrary.h
// T3.x supported sample rates: 2000, 8000, 11025, 16000, 22050, 24000, 32000, 44100, 44117, 48000,
// 88200, 88235 (44117*2), 95680, 96000, 176400, 176470, 192000
// T4.x supports any sample rate the codec will handle.
const float sample_rate_Hz = 12000.0f;
const int audio_block_samples = 128; // Always 128
AudioSettings_F32 audio_settings(sample_rate_Hz, audio_block_samples);
AudioSynthWaveformSine_F32 sine1(audio_settings);
AudioSDPlayer_F32 playWav1(audio_settings);
AudioMixer4_F32 mixer4_0;
radioVoiceClipper_F32 clipper1(audio_settings);
AudioOutputI2S_F32 audioOutput(audio_settings);
AudioAnalyzeFFT1024_F32 fft1;
AudioConnection_F32 patchCord0(playWav1, 0, mixer4_0, 0);
AudioConnection_F32 patchCord1(sine1, 0, mixer4_0, 1);
AudioConnection_F32 patchCord2(mixer4_0, 0, clipper1, 0);
AudioConnection_F32 patchCord3(clipper1, 0, audioOutput, 0);
AudioConnection_F32 patchCord4(clipper1, 0, audioOutput, 1);
AudioConnection_F32 patchCord5(clipper1, 0, fft1, 0);
AudioControlSGTL5000 sgtl5000_1;
// Use these with the Teensy 4.x Rev D Audio Shield (NOT for T3.x)
#define SDCARD_CS_PIN_Z 10
#define SDCARD_MOSI_PIN_Z 11
#define SDCARD_SCK_PIN_Z 13
// wavData is a global struct, definined in AudioSDPlayer_F32.h
// This provides information about the current WAV file to this .INO
struct wavData* pCurrentWavData;
// And data about the clipper
struct levelClipper* pLevelData;
uint32_t writeOne = 0;
uint32_t cntFFT = 0;
uint32_t ttt; // For timing test audio
uint32_t tp;
void setup() { // ********** SETUP **********
Serial.begin(9600); delay(1000);
Serial.println("*** Test Voice Clipper from SD Card Voice Sample ***");
AudioMemory_F32(70, audio_settings);
pCurrentWavData = playWav1.getCurrentWavData();
sgtl5000_1.enable();
delay(500);
SPI.setMOSI(SDCARD_MOSI_PIN_Z);
SPI.setSCK(SDCARD_SCK_PIN_Z);
Serial.print("SD.begin() returns "); Serial.println(SD.begin(SDCARD_CS_PIN_Z));
sine1.frequency(750.0f);
sine1.amplitude(0.707107);
clipper1.setSampleRate_Hz(12000.0f);
// Set input, correction, and output gains
float32_t Pre_Clip_Gain = 1.5f; // Use to set amount of clipping, 1.0 to 2.0f, 3 is excessive
// Correction gain=1.8 leaves max output overshoot at 2.2% for W9GR sample audio
clipper1.setGains(Pre_Clip_Gain, 1.8f, 1.0f);
pLevelData = clipper1.getLevels(0); // Gets pointer to struct
audioOutput.setGain(0.02); // <<< Output volume control
fft1.setOutputType(FFT_DBFS);
fft1.windowFunction(AudioWindowBlackmanHarris1024);
fft1.setNAverage(16);
ttt = millis(); // Time test audio
tp=millis();
}
void playFile(const char *filename) {
if(playWav1.isPlaying())
return;
Serial.println("");
Serial.print("Playing file: ");
Serial.println(filename);
playWav1.play(filename); // Start playing the file.
// A brief delay for the library read WAV info
delay(25);
Serial.print("WAV file format = "); Serial.println(pCurrentWavData->audio_format);
Serial.print("WAV number channels = "); Serial.println(pCurrentWavData->num_channels);
Serial.print("WAV File Sample Rate = "); Serial.println(pCurrentWavData->sample_rate);
Serial.print("Number of bits per Sample = "); Serial.println(pCurrentWavData->bits);
Serial.print("File length, seconds = ");
Serial.println(0.001f*(float32_t)playWav1.lengthMillis(), 3);
}
void loop() {
uint32_t tt=millis() - ttt;
if(tt < 2000)
{
// Thanks to W9GR for the test file, W9GR482.WAV from which '12' was derived..
playFile("W9GR12.WAV");
mixer4_0.gain(0, 1.41421356f); // Play WAV file on
mixer4_0.gain(1, 0.0f); // Sine Wave 1 off
}
else if(tt > 12300 && tt<13300)
{
mixer4_0.gain(1, 0.0f); // Play WAV file off
// The following puts a 1-sec 750 Hz, full amplitude tone into the input
// .707 on the generator and 1.414 here make the peak sine wave 1.000 at the CESSB input
mixer4_0.gain(1, 1.41421356f); // Sine Wave 1 on, 750 Hz
}
else if(tt >= 13300)
{
mixer4_0.gain(0, 1.41421356f); // Play WAV file on
mixer4_0.gain(1, 0.0f); // Sine Wave 1 off
ttt = millis(); // Start again
}
delay(1);
// Un-comment the following to print out the spectrum
if(fft1.available() && ++cntFFT>10 && cntFFT<12) // For 12 ksps, about 8 seconds in
{
for(int kk=0; kk<512; kk++)
{
Serial.print(11.71875f*(float32_t)kk); Serial.print(",");
Serial.println(fft1.read(kk));
}
}
if(clipper1.levelDataCount() > 300) // Typically 300 to 3000
{
clipper1.getLevels(1); // Cause write of data to struct & reset
/* // Detailed Report
Serial.print(10.0f*log10f(pLevelData->pwr0));
Serial.print(" In Ave Pwr Out ");
Serial.println(10.0f*log10f(pLevelData->pwr1));
Serial.print(20.0f*log10f(pLevelData->peak0));
Serial.print(" In Peak Out ");
Serial.println(20.0f*log10f(pLevelData->peak1));
Serial.print(pLevelData->peak0, 6);
Serial.print(" In Peak Volts Out ");
Serial.println(pLevelData->peak1, 6);
Serial.print("Enhancement = ");
float32_t enhance = (10.0f*log10f(pLevelData->pwr1) - 20.0f*log10f(pLevelData->peak1)) -
(10.0f*log10f(pLevelData->pwr0) - 20.0f*log10f(pLevelData->peak0));
if(enhance<1.0f) enhance = 1.0f;
Serial.print(enhance); Serial.println(" dB");
*/
/*
// CSV Report suitable for entering to spread sheet
// InAve, InPk, OutAve, OutPk, EnhancementdB
Serial.print(pLevelData->pwr0, 5); Serial.print(",");
Serial.print(pLevelData->peak0, 5); Serial.print(",");
Serial.print(pLevelData->pwr1, 5); Serial.print(",");
Serial.print(pLevelData->peak1, 5); Serial.print(",");
float32_t enhance = (10.0f*log10f(pLevelData->pwr1) - 20.0f*log10f(pLevelData->peak1)) -
(10.0f*log10f(pLevelData->pwr0) - 20.0f*log10f(pLevelData->peak0));
if(enhance<1.0f) enhance = 1.0f;
Serial.println(enhance);
*/
}
}

Binary file not shown.

@ -0,0 +1,184 @@
/*
* radioVoiceClipper_F32.cpp
*
* Bob Larkin, in support of the library:
* Chip Audette, OpenAudio, Dec 2022
* MIT License, Use at your own risk.
*
* See radioVoiceClipper_F32.h for technical info.
*/
// NOTE: 96 ksps sample rate not yet implemented
#include "radioVoiceClipper_F32.h"
void radioVoiceClipper_F32::update(void) {
audio_block_f32_t *blockIn, *blockOut ;
// Temporary storage. Max size for 12 ksps where 128 points at input
// and 256 at interpolated 24ksps
float32_t workingData[256];
float32_t delayedDataI[256]; // Allows batching of 64 data points
float32_t diffI[256];
if(sampleRate!=VC_SAMPLE_RATE_11_12 && sampleRate!=VC_SAMPLE_RATE_44_50 && sampleRate!=VC_SAMPLE_RATE_88_100)
return;
// Get all needed resources, or return if not available.
blockIn = AudioStream_F32::receiveReadOnly_f32();
if (!blockIn)
{ return; }
blockOut = AudioStream_F32::allocate_f32();
if (!blockOut)
{
AudioStream_F32::release(blockIn);
return;
}
// The audio input peak levels for start of clipping are -1.0, 1.0
// when gainIn==1.0.
// uint32_t ttt=micros();
if(sampleRate==VC_SAMPLE_RATE_11_12)
{
// No decimation, 128 samples
for(int k=0; k<128; k++)
workingData[k] = blockIn->data[k];
// We now have nW=128 (for 12 ksps) samples to process
}
else if(sampleRate==VC_SAMPLE_RATE_44_50)
{
// Decimate 48 ksps to 12 ksps, 128 to 32 samples
// or 96 ksps to 12 ksps, 128 to 16 samples
arm_fir_decimate_f32(&decimateInst, &(blockIn->data[0]),
&workingData[0], 128);
// We now have nW=32 (for 48 ksps) or 16 (for 96 ksps) samples to process
}
// Measure input power and peak envelope, before any clipping.
for(int k=0; k<nW; k++)
{
float32_t pwrWorkingData = workingData[k]*workingData[k]; // Replace with absf() <<<<<<<<<<<<<<<<<<<<<<<<
float32_t vWD = sqrtf(pwrWorkingData); // Envelope
powerSum0 += pwrWorkingData;
if(vWD > maxMag0)
maxMag0 = vWD; // Peak envelope
countPower0++;
}
for(int k=0; k<nW; k++)
{
workingData[k] *= gainIn; // Sets the amount of clipping for 1.0 in
//Serial.println(workingData[k]);
}
// Interpolate by 2 up to 24 ksps rate
for(int k=0; k<nW; k++) // 48 ksps: 0 to 31
{
int k2 = 2*(nW - k) - 1; // 48 ksps: 63 to 1
// Zero pack, working from the bottom to not overwrite
workingData[k2] = 0.0f; // 48 ksps: 64 element array
workingData[k2-1] = workingData[nW-k-1];
}
// LPF with gain of 2 built into coefficients, correct for added zeros.
arm_fir_f32(&firInstInterpolate1I, workingData, workingData, nC);
// workingData are now at 24 ksps and ready for clipping
// For input 48 ksps this produces 64 numbers
for(int kk=0; kk<nC; kk++)
{
float32_t power = workingData[kk]*workingData[kk]; // Change to absf()
float32_t mag = sqrtf(power);
if(mag > 1.0f) // This the clipping, scaled to 1.0, desired max
{
workingData[kk] /= mag;
}
}
// clipperIn needs spectrum control, so LP filter it.
// Both BW of the signal and the sample rate have been doubled.
arm_fir_f32(&firInstClipperI, workingData, workingData, nC);
// Ready to compensate for filter overshoots
for (int k=0; k<nC; k++)
{
// Circular delay line for signal to align data with FIR output
// Put I & Q data points into the delay arrays
osDelayI[indexOsDelay & 0X3F] = workingData[k];
// Remove 64 points delayed data from line and save for later
delayedDataI[k] = osDelayI[(indexOsDelay - 63) & 0X3F];
indexOsDelay++;
// Delay line to allow strongest envelope to be used for compensation
// We only need to look ahead 1 or behind 1, so delay line of 4 is OK.
// Enter latest envelope to delay array
osEnv[indexOsEnv & 0X03] = sqrtf(
workingData[k]*workingData[k]); // + workingDataQ[k]*workingDataQ[k]);
// look over the envelope curve to find the max
float32_t eMax = 0.0f;
if(osEnv[(indexOsEnv) & 0X03] > eMax) // Data point just entered
eMax = osEnv[(indexOsEnv) & 0X03];
if(osEnv[(indexOsEnv-1) & 0X03] > eMax) // Entered one before
eMax = osEnv[(indexOsEnv-1) & 0X03];
if(osEnv[(indexOsEnv-2) & 0X03] > eMax) // Entered one before that
eMax = osEnv[(indexOsEnv-2) & 0X03];
if(eMax < 1.0f)
eMax = 1.0f; // Below clipping region
indexOsEnv++;
// Clip the signal to 1.0. -2 allows 1 look ahead on signal.
float32_t eCorrectedI = osDelayI[(indexOsDelay - 2) & 0X3F] / eMax;
// Filtering is linear, so we only need to filter the difference between
// the signal and the clipper output. This needs less filtering, as the
// difference is many dB below the signal to begin with. Hershberger 2014
diffI[k] = osDelayI[(indexOsDelay - 2) & 0X3F] - eCorrectedI;
} // End, for k=0 to 63
// Filter the differences, osFilter has 123 taps and 61 delay
arm_fir_f32(&firInstOShootI, diffI, diffI, nC);
// Do the overshoot compensation
for(int k=0; k<nC; k++)
{
workingData[k] = delayedDataI[k] - gainCompensate*diffI[k];
}
// Measure average output power and peak envelope, after CESSB
// but before gainOut
for(int k=0; k<nC; k++)
{
float32_t pwrOut = workingData[k]*workingData[k];
float32_t vWD = sqrtf(pwrOut); // Envelope
powerSum1 += pwrOut;
if(vWD > maxMag1)
maxMag1 = vWD; // Peak envelope
countPower1++;
}
if(sampleRate==VC_SAMPLE_RATE_11_12)
{
// Decimat24 to 12, 128 samples out. No LPF needed as we just did that
for(int k=0; k<128; k++)
blockOut->data[k] = workingData[2*k];
}
else if(sampleRate==VC_SAMPLE_RATE_44_50)
{
// Finally interpolate to 48 or 96 ksps. Data is in workingData[k]
// and is 64 samples for audio 48 ksps.
for(int k=0; k<nC; k++) // Audio sampling at 48 ksps: 0 to 63
{
int k2 = 2*(nC - k) - 1; // 48 ksps 63 to 1
// Zero pack, working from the bottom to not overwrite
workingData[k2] = 0.0f;
workingData[k2-1] = gainOut*workingData[nC-k-1]; // gainOut does not change CESSB
}
// LPF with gain of 2 built into coefficients, correct for zeros.
arm_fir_f32(&firInstInterpolate2I, workingData, &blockOut->data[0], 128);
// Voltage gain from blockIn->data to here for small sine wave is 1.0
}
AudioStream_F32::transmit(blockOut, 0); // send the outputs
AudioStream_F32::release(blockIn); // Release the blocks
AudioStream_F32::release(blockOut);
jjj++; //For test printing
// Serial.println(micros() - ttt);
} // end update()

@ -0,0 +1,343 @@
/*
* radioVoiceClipper_F32.h
*
* 12 March 2023 (c) copyright Bob Larkin
* But with With much credit to:
* Chip Audette (OpenAudio)
* and of course, to PJRC for the Teensy and Teensy Audio Library
*
* The development of this Voice Clipper was by Bob Larkin, W7PUA, based
* entirely on ideas and suggestions from Dave Hershberger, W9GR.
* Many thanks to Dave. Note that this clipper is is a "real variable"
* version of the Single Sideband CESSB clipper. See the companion
* radioCESSBtransmit_F32.h class which uses all the same principles.
*
* The input signal is a voice (or tones) that will, in general, have
* been compressed in amplitude, keeping the maximum amplitude close to
* 1.0 peak-to-center. For this class, clipping occurs for any input
* greater than 1/gainIn where gainIn comes from the public function
* setGains(). Normally gainIn has a value around 1.5 and so clipping occurs
* for inputs above peak levels of 2/3=0.667. For this level of gaiIn,
* there will be about 3 dB of increase in the average power of the voice
* but still minimal perception of "over-processing."
*
* Internally the audio is clipped at the higher levels and the resulting
* out-of-band distion is low pass filtered. Next, the overshoot that
* occurs with the filter is removed by measuring the overshoot, low-pass
* filtering the overshoot and subtracting it off. All this requires
* care with the timing as all of the filtering steps involve delays.
*
* The compressor2 class in this F32 library is intended to precede this
* class.
*
* NOTE: Do NOT follow this block with any non-linear phase filtering,
* such as IIR. Minimize any linear-phase filtering such as FIR.
* Such activities enhance the overshoots and defeat the purpose of clipping.
*
* An important note: This clipper is suitable for voice modes, such as
* AM or NBFM. Do not use this clipper ahead of a single sideband
* transmitter. That is what the CESSB class is for.
*
* The following reference has information on CESSB, in detail, as well
* as on the use of clippers, similar to this one, in broadcast work:
* Hershberger, D.L. (2014): Controlled Envelope Single Sideband. QEX
* November/December 2014 pp3-13.
* http://www.arrl.org/files/file/QEX_Next_Issue/2014/Nov-Dec_2014/Hershberger_QEX_11_14.pdf
*
* Status: Experimental
*
* Inputs: 0 is voice audio input
* Outputs: 0 is clipped voice.
*
* Functions, available during operation:
* void setSampleRate_Hz(float32_t fs_Hz) Allows dynamic sample rate change.
*
* struct levels* getLevels(int what) {
* what = 0 returns a pointer to struct levels before data is ready
* what = 1 returns a pointer to struct levels
*
* uint32_t levelDataCount() return countPower0
*
* void setGains(float32_t gIn, float32_t gCompensate, float32_t gOut)
*
* Time: T3.6 For an update of a 128 sample block, estimated microseconds
* T4.0 For an update of a 128 sample block, measured microseconds
* These times are for a 48 ksps rate.
*
* NOTE: Do NOT follow this block with any non-linear phase filtering,
* such as IIR. Minimize any linear-phase filtering such as FIR.
* Such activities enhance the overshoots and defeat the purpose of clipping.
*/
#ifndef _radioVoiceClipper_f32_h
#define _radioVoiceClipper_f32_h
#include "Arduino.h"
#include "AudioStream_F32.h"
#include "arm_math.h"
#include "mathDSP_F32.h"
#define VC_SAMPLE_RATE_0 0
#define VC_SAMPLE_RATE_11_12 1
#define VC_SAMPLE_RATE_44_50 2
#define VC_SAMPLE_RATE_88_100 3
#ifndef M_PI
#define M_PI 3.141592653589793f
#endif
#ifndef M_PI_2
#define M_PI_2 1.570796326794897f
#endif
#ifndef M_TWOPI
#define M_TWOPI (M_PI * 2.0f)
#endif
// For the average power and peak voltage readings, global
struct levelClipper {
float32_t pwr0;
float32_t peak0;
float32_t pwr1;
float32_t peak1;
uint32_t countP; // Number of averaged samples for pwr0.
};
class radioVoiceClipper_F32 : public AudioStream_F32 {
//GUI: inputs:1, outputs:2 //this line used for automatic generation of GUI node
//GUI: shortName:CESSBTransmit //this line used for automatic generation of GUI node
public:
radioVoiceClipper_F32(void) :
AudioStream_F32(1, inputQueueArray_f32)
{
setSampleRate_Hz(AUDIO_SAMPLE_RATE);
//uses default AUDIO_SAMPLE_RATE from AudioStream.h
//setBlockLength(128); Always default 128
}
radioVoiceClipper_F32(const AudioSettings_F32 &settings) :
AudioStream_F32(1, inputQueueArray_f32)
{
setSampleRate_Hz(settings.sample_rate_Hz);
//setBlockLength(128); Always default 128
}
// Sample rate starts at default 44.1 ksps. That will work. Filters
// are designed for 48 and 96 ksps, however. This is a *required*
// function at setup().
void setSampleRate_Hz(const float _fs_Hz) {
sample_rate_Hz = _fs_Hz;
if(sample_rate_Hz>10900.0f && sample_rate_Hz<12600.0f)
{
// Design point is 12 ksps. No initial decimation. Interpolate
// to 24 ksps for clipping and then decimate back to 12 at the end.
sampleRate = VC_SAMPLE_RATE_11_12;
nW = 128;
nC = 256;
countLevelMax = 10; // About 0.1 sec for 12 ksps
inverseMaxCount = 1.0f/(float32_t)countLevelMax;
arm_fir_init_f32(&firInstInterpolate1I, 23, (float32_t*)interpolateFilter1,
&pStateInterpolate1I[0], nC);
arm_fir_init_f32(&firInstClipperI, 123, (float32_t*)clipperOut,
&pStateClipperI[0], nC);
arm_fir_init_f32(&firInstOShootI, 123, (float32_t*)clipperOut,
&pStateOShootI[0], nC);
}
else if(sample_rate_Hz>43900.0f && sample_rate_Hz<50100.0f)
{
// Design point is 48 ksps
sampleRate = VC_SAMPLE_RATE_44_50;
nW = 32;
nC = 64;
countLevelMax = 37; // About 0.1 sec for 48 ksps
inverseMaxCount = 1.0f/(float32_t)countLevelMax;
arm_fir_decimate_init_f32(&decimateInst, 65, 4,
(float32_t*)decimateFilter48, &pStateDecimate[0], 128);
arm_fir_init_f32(&firInstInterpolate1I, 23, (float32_t*)interpolateFilter1,
&pStateInterpolate1I[0], nC);
arm_fir_init_f32(&firInstClipperI, 123, (float32_t*)clipperOut,
&pStateClipperI[0], nC);
arm_fir_init_f32(&firInstOShootI, 123, (float32_t*)clipperOut,
&pStateOShootI[0], nC);
arm_fir_init_f32(&firInstInterpolate2I, 23, (float32_t*)interpolateFilter1,
&pStateInterpolate2I[0], nC);
}
else if(sample_rate_Hz>88000.0f && sample_rate_Hz<100100.0f)
{
// GET THINGS WORKING AT VC_SAMPLE_RATE_44_50 FIRST AND THEN FIX UP 96 ksps
// Design point is 96 ksps
/* sampleRate = VC_SAMPLE_RATE_88_100; //<<<<<<<<<<<<<<<<<<<<<<FIXUP
nW = 16;
nC = 32;
countLevelMax = 75; // About 0.1 sec for 96 ksps
inverseMaxCount = 1.0f/(float32_t)countLevelMax;
arm_fir_decimate_init_f32 (&decimateInst, 55, 4,
(float32_t*)decimateFilter48, pStateDecimate, 128);
arm_fir_init_f32(&firInstClipper, 199, basebandFilter,
&StateFirClipperF32[0], 128);
*/
}
else
{
// Unsupported sample rate
sampleRate = VC_SAMPLE_RATE_0;
nW = 1;
nC = 1;
}
newLevelDataReady = false;
}
struct levelClipper* getLevels(int what) {
if(what != 0) // 0 leaves a way to get pointer before data is ready
{
levelData.pwr0 = powerSum0/((float32_t)countPower0);
levelData.peak0 = maxMag0;
levelData.pwr1 = powerSum1/(float32_t)countPower1;
levelData.peak1 = maxMag1;
levelData.countP = countPower0;
// Automatic reset for next set of readings
powerSum0 = 0.0f;
maxMag0 = -1.0f;
powerSum1 = 0.0f;
maxMag1 = -1.0f;
countPower0 = 0;
countPower1 = 0;
}
return &levelData;
}
uint32_t levelDataCount(void) {
return countPower0; // Input count, out may be different
}
void setGains(float32_t gIn, float32_t gCompensate, float32_t gOut)
{
gainIn = gIn;
gainCompensate = gCompensate;
gainOut = gOut;
}
virtual void update(void);
private:
void sincos_Z_(float32_t ph);
struct levelClipper levelData;
audio_block_f32_t *inputQueueArray_f32[1];
uint32_t jjj = 0; // Used for diagnostic printing
// Input/Output is at 12, 48 or 96 ksps.
// Clipping and overshoot processing is at 24 ksps.
// Next line is to indicate that setSampleRateHz() has not executed
int sampleRate = VC_SAMPLE_RATE_0;
float32_t sample_rate_Hz = AUDIO_SAMPLE_RATE; // 44.1 ksps
int16_t nW = 32; // 128, 32 or 16
int16_t nC = 64; // 256, 64 or 32
uint16_t block_length = 128;
float32_t pStateDecimate[128 + 65 - 1]; // Goes with CMSIS decimate function
arm_fir_decimate_instance_f32 decimateInst;
// For 12 ksps case, 24 kHz clipper uses 256 points
float32_t pStateInterpolate1I[256 + 23 - 1]; // For interpolate 12 to 24 ksps
arm_fir_instance_f32 firInstInterpolate1I;
float32_t pStateClipperI[256 + 123 - 1]; // Goes with Clipper filter
arm_fir_instance_f32 firInstClipperI; // at 24 ksps
float32_t pStateOShootI[256+123-1];
arm_fir_instance_f32 firInstOShootI;
float32_t pStateInterpolate2I[256 + 23 - 1]; // For interpolate 12 to 24 ksps
arm_fir_instance_f32 firInstInterpolate2I;
float32_t gainIn = 1.0f;
float32_t gainCompensate = 1.4f;
float32_t gainOut = 1.0f; // Does not change Clipping, here for convenience to set out level
// A tiny delay to allow negative time for the previous path
float32_t osEnv[4];
uint16_t indexOsEnv = 4; // 0 to 3 by using a 2-bit mask
// We need a delay for overshoot remove to account for the FIR
// filter in the correction path. Some where around 128 taps works
// but if we make the delay exactly 2^6=64 the delay line is simple
// resulting in a FIR size of 2*64+1=129 taps.
float32_t osDelayI[64];
uint16_t indexOsDelay = 64;
// RMS and Peak variable for monitoring levels and changes to the
// Peak to RMS ratio. These are temporary storage. Data is
// transferred by global levelData struct at the top of this file.
float32_t powerSum0 = 0.0f;
float32_t maxMag0 = -1.0f;
float32_t powerSum1 = 0.0f;
float32_t maxMag1 = -1.0f;
uint32_t countPower0 = 0;
uint32_t countPower1 = 0;
bool newLevelDataReady = false;
int countLevel = 0;
int countLevelMax = 37; // About 0.1 sec for 48 ksps
float32_t inverseMaxCount = 1.0f/(float32_t)countLevelMax;
/* Input filter for decimate by 4:
* FIR filter designed with http://t-filter.appspot.com
* Sampling frequency: 48000 Hz
* 0 Hz - 3000 Hz ripple = 0.075 dB
* 6000 Hz - 24000 Hz atten = -95.93 dB */
const float32_t decimateFilter48[65] = {
0.00004685f, 0.00016629f, 0.00038974f, 0.00073279f, 0.00113663f, 0.00148721f,
0.00159057f, 0.00125129f, 0.00032821f,-0.00114283f,-0.00289782f,-0.00441933f,
-0.00505118f,-0.00418143f,-0.00151748f, 0.00268876f, 0.00751487f, 0.01147689f,
0.01286243f, 0.01027735f, 0.00323528f,-0.00737003f,-0.01913035f,-0.02842381f,
-0.03117447f,-0.02390063f,-0.00480378f, 0.02544011f, 0.06344286f, 0.10357132f,
0.13904464f, 0.16342506f, 0.17210799f, 0.16342506f, 0.13904464f, 0.10357132f,
0.06344286f, 0.02544011f,-0.00480378f,-0.02390063f,-0.03117447f,-0.02842381f,
-0.01913035f,-0.00737003f, 0.00323528f, 0.01027735f, 0.01286243f, 0.01147689f,
0.00751487f, 0.00268876f,-0.00151748f,-0.00418143f,-0.00505118f,-0.00441933f,
-0.00289782f,-0.00114283f, 0.00032821f, 0.00125129f, 0.00159057f, 0.00148721f,
0.00113663f, 0.00073279f, 0.00038974f, 0.00016629f, 0.00004685};
/* Filter for outputs of clipper
* Use also overshoot corrector, but might be able to use less terms.
* FIR filter designed with http://t-filter.appspot.com
* Sample frequency: 24000 Hz
* 0 Hz - 2800 Hz ripple = 0.14 dB
* 3200 Hz - 12000 Hz atten = 40.51 dB */
const float32_t clipperOut[123] = {
-0.003947255f, 0.001759588f, 0.002221444f, 0.002407244f, 0.001833343f, 0.000524622f,
-0.000946260f,-0.001768428f,-0.001395297f, 0.000055916f, 0.001779024f, 0.002694998f,
0.002099736f, 0.000157764f,-0.002092190f,-0.003282801f,-0.002542927f,-0.000116969f,
0.002694319f, 0.004153363f, 0.003197589f, 0.000143560f,-0.003346600f,-0.005148200f,
-0.003947437f,-0.000152425f, 0.004166345f, 0.006378882f, 0.004871469f, 0.000164557f,
-0.005173898f,-0.007896395f,-0.006014470f,-0.000173552f, 0.006447615f, 0.009828080f,
0.007480359f, 0.000184482f,-0.008116957f,-0.012379161f,-0.009436712f,-0.000194737f,
0.010412610f, 0.015941971f, 0.012213107f, 0.000200845f,-0.013823966f,-0.021360759f,
-0.016552097f,-0.000205707f, 0.019544260f, 0.030836344f, 0.024523278f, 0.000211298f,
-0.031509151f,-0.052450055f,-0.044811840f,-0.000214078f, 0.074661107f, 0.158953216f,
0.225159581f, 0.250214862f, 0.225159581f, 0.158953216f, 0.074661107f,-0.000214078f,
-0.044811840f,-0.052450055f,-0.031509151f, 0.000211298f, 0.024523278f, 0.030836344f,
0.019544260f,-0.000205707f,-0.016552097f,-0.021360759f,-0.013823966f, 0.000200845f,
0.012213107f, 0.015941971f, 0.010412610f,-0.000194737f,-0.009436712f,-0.012379161f,
-0.008116957f, 0.000184482f, 0.007480359f, 0.009828080f, 0.006447615f,-0.000173552f,
-0.006014470f,-0.007896395f,-0.005173898f, 0.000164557f, 0.004871469f, 0.006378882f,
0.004166345f,-0.000152425f,-0.003947437f,-0.005148200f,-0.003346600f, 0.000143560f,
0.003197589f, 0.004153363f, 0.002694319f,-0.000116969f,-0.002542927f,-0.003282801f,
-0.002092190f, 0.000157764f, 0.002099736f, 0.002694998f, 0.001779024f, 0.000055916f,
-0.001395297f,-0.001768428f,-0.000946260f, 0.000524622f, 0.001833343f, 0.002407244f,
0.002221444f, 0.001759588f,-0.003947255f};
/* FIR filter designed with http://t-filter.appspot.com
* Sampling frequency: 24000 sps
* 0 Hz - 3000 Hz gain = 2 ripple = 0.11 dB
* 6000 Hz - 12000 Hz atten = -62.4 dB
* (At Sampling Frequency=48ksps, double all frequency values) */
const float32_t interpolateFilter1[23] = {
-0.00413402f,-0.01306124f,-0.01106321f, 0.01383359f, 0.04386756f, 0.02731837f,
-0.05470066f,-0.12407408f,-0.04389386f, 0.23355907f, 0.56707488f, 0.71763165f,
0.56707488f, 0.23355907f,-0.04389386f,-0.12407408f,-0.05470066f, 0.02731837f,
0.04386756f, 0.01383359f,-0.01106321f,-0.01306124f,-0.00413402};
}; // end Class
#endif
Loading…
Cancel
Save