[feat](trx-rs): add ft8 decoder
Co-authored-by: Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
#include "audio.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef USE_PORTAUDIO
|
||||
#include <portaudio.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
PaStream* instream;
|
||||
} audio_context_t;
|
||||
|
||||
static audio_context_t audio_context;
|
||||
|
||||
static int audio_cb(void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
|
||||
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData)
|
||||
{
|
||||
audio_context_t* context = (audio_context_t*)userData;
|
||||
float* samples_in = (float*)inputBuffer;
|
||||
|
||||
// PaTime time = data->startTime + timeInfo->inputBufferAdcTime;
|
||||
printf("Callback with %ld samples\n", framesPerBuffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio_list(void)
|
||||
{
|
||||
PaError pa_rc;
|
||||
|
||||
pa_rc = Pa_Initialize(); // Initialize PortAudio
|
||||
if (pa_rc != paNoError)
|
||||
{
|
||||
printf("Error initializing PortAudio.\n");
|
||||
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||
return;
|
||||
}
|
||||
|
||||
int numDevices;
|
||||
numDevices = Pa_GetDeviceCount();
|
||||
if (numDevices < 0)
|
||||
{
|
||||
printf("ERROR: Pa_CountDevices returned 0x%x\n", numDevices);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("%d audio devices found:\n", numDevices);
|
||||
for (int i = 0; i < numDevices; i++)
|
||||
{
|
||||
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
|
||||
|
||||
PaStreamParameters inputParameters = {
|
||||
.device = i,
|
||||
.channelCount = 1, // 1 = mono, 2 = stereo
|
||||
.sampleFormat = paFloat32,
|
||||
.suggestedLatency = 0.2,
|
||||
.hostApiSpecificStreamInfo = NULL
|
||||
};
|
||||
double sample_rate = 12000; // sample rate (frames per second)
|
||||
pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate);
|
||||
|
||||
printf("%d: [%s] [%s]\n", (i + 1), deviceInfo->name, (pa_rc == paNoError) ? "OK" : "NOT SUPPORTED");
|
||||
}
|
||||
}
|
||||
|
||||
int audio_init(void)
|
||||
{
|
||||
PaError pa_rc;
|
||||
|
||||
pa_rc = Pa_Initialize(); // Initialize PortAudio
|
||||
if (pa_rc != paNoError)
|
||||
{
|
||||
printf("Error initializing PortAudio.\n");
|
||||
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||
Pa_Terminate(); // I don't think we need this but...
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_open(const char* name)
|
||||
{
|
||||
PaError pa_rc;
|
||||
audio_context.instream = NULL;
|
||||
|
||||
PaDeviceIndex ndevice_in = -1;
|
||||
int numDevices = Pa_GetDeviceCount();
|
||||
for (int i = 0; i < numDevices; i++)
|
||||
{
|
||||
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
|
||||
if (0 == strcmp(deviceInfo->name, name))
|
||||
{
|
||||
ndevice_in = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ndevice_in < 0)
|
||||
{
|
||||
printf("Could not find device [%s].\n", name);
|
||||
audio_list();
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned long nfpb = 1920 / 4; // frames per buffer
|
||||
double sample_rate = 12000; // sample rate (frames per second)
|
||||
|
||||
PaStreamParameters inputParameters = {
|
||||
.device = ndevice_in,
|
||||
.channelCount = 1, // 1 = mono, 2 = stereo
|
||||
.sampleFormat = paFloat32,
|
||||
.suggestedLatency = 0.2,
|
||||
.hostApiSpecificStreamInfo = NULL
|
||||
};
|
||||
|
||||
// Test if this configuration actually works, so we do not run into an ugly assertion
|
||||
pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate);
|
||||
if (pa_rc != paNoError)
|
||||
{
|
||||
printf("Error opening input audio stream.\n");
|
||||
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||
return -2;
|
||||
}
|
||||
|
||||
PaStream* instream;
|
||||
pa_rc = Pa_OpenStream(
|
||||
&instream, // address of stream
|
||||
&inputParameters,
|
||||
NULL,
|
||||
sample_rate, // Sample rate
|
||||
nfpb, // Frames per buffer
|
||||
paNoFlag,
|
||||
NULL /*(PaStreamCallback*)audio_cb*/, // Callback routine
|
||||
NULL /*(void*)&audio_context*/); // address of data structure
|
||||
if (pa_rc != paNoError)
|
||||
{ // We should have no error here usually
|
||||
printf("Error opening input audio stream:\n");
|
||||
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||
return -3;
|
||||
}
|
||||
// printf("Successfully opened audio input.\n");
|
||||
|
||||
pa_rc = Pa_StartStream(instream); // Start input stream
|
||||
if (pa_rc != paNoError)
|
||||
{
|
||||
printf("Error starting input audio stream!\n");
|
||||
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||
return -4;
|
||||
}
|
||||
|
||||
audio_context.instream = instream;
|
||||
|
||||
// while (Pa_IsStreamActive(instream))
|
||||
// {
|
||||
// Pa_Sleep(100);
|
||||
// }
|
||||
// Pa_AbortStream(instream); // Abort stream
|
||||
// Pa_CloseStream(instream); // Close stream, we're done.
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_read(float* buffer, int num_samples)
|
||||
{
|
||||
PaError pa_rc;
|
||||
pa_rc = Pa_ReadStream(audio_context.instream, (void*)buffer, num_samples);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
int audio_init(void)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
void audio_list(void)
|
||||
{
|
||||
}
|
||||
|
||||
int audio_open(const char* name)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
int audio_read(float* buffer, int num_samples)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endif
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
#ifndef _INCLUDE_AUDIO_H_
|
||||
#define _INCLUDE_AUDIO_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
int audio_init(void);
|
||||
void audio_list(void);
|
||||
int audio_open(const char* name);
|
||||
int audio_read(float* buffer, int num_samples);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // _INCLUDE_AUDIO_H_
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
Vendored
+263
@@ -0,0 +1,263 @@
|
||||
#include "monitor.h"
|
||||
#include <common/common.h>
|
||||
|
||||
#define LOG_LEVEL LOG_INFO
|
||||
#include <ft8/debug.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
static float hann_i(int i, int N)
|
||||
{
|
||||
float x = sinf((float)M_PI * i / N);
|
||||
return x * x;
|
||||
}
|
||||
|
||||
// static float hamming_i(int i, int N)
|
||||
// {
|
||||
// const float a0 = (float)25 / 46;
|
||||
// const float a1 = 1 - a0;
|
||||
|
||||
// float x1 = cosf(2 * (float)M_PI * i / N);
|
||||
// return a0 - a1 * x1;
|
||||
// }
|
||||
|
||||
// static float blackman_i(int i, int N)
|
||||
// {
|
||||
// const float alpha = 0.16f; // or 2860/18608
|
||||
// const float a0 = (1 - alpha) / 2;
|
||||
// const float a1 = 1.0f / 2;
|
||||
// const float a2 = alpha / 2;
|
||||
|
||||
// float x1 = cosf(2 * (float)M_PI * i / N);
|
||||
// float x2 = 2 * x1 * x1 - 1; // Use double angle formula
|
||||
|
||||
// return a0 - a1 * x1 + a2 * x2;
|
||||
// }
|
||||
|
||||
static void waterfall_init(ftx_waterfall_t* me, int max_blocks, int num_bins, int time_osr, int freq_osr)
|
||||
{
|
||||
size_t mag_size = max_blocks * time_osr * freq_osr * num_bins * sizeof(me->mag[0]);
|
||||
me->max_blocks = max_blocks;
|
||||
me->num_blocks = 0;
|
||||
me->num_bins = num_bins;
|
||||
me->time_osr = time_osr;
|
||||
me->freq_osr = freq_osr;
|
||||
me->block_stride = (time_osr * freq_osr * num_bins);
|
||||
me->mag = (WF_ELEM_T*)malloc(mag_size);
|
||||
LOG(LOG_DEBUG, "Waterfall size = %zu\n", mag_size);
|
||||
}
|
||||
|
||||
static void waterfall_free(ftx_waterfall_t* me)
|
||||
{
|
||||
free(me->mag);
|
||||
}
|
||||
|
||||
void monitor_init(monitor_t* me, const monitor_config_t* cfg)
|
||||
{
|
||||
float slot_time = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
|
||||
float symbol_period = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD;
|
||||
// Compute DSP parameters that depend on the sample rate
|
||||
me->block_size = (int)(cfg->sample_rate * symbol_period); // samples corresponding to one FSK symbol
|
||||
me->subblock_size = me->block_size / cfg->time_osr;
|
||||
me->nfft = me->block_size * cfg->freq_osr;
|
||||
me->fft_norm = 2.0f / me->nfft;
|
||||
// const int len_window = 1.8f * me->block_size; // hand-picked and optimized
|
||||
|
||||
me->window = (float*)malloc(me->nfft * sizeof(me->window[0]));
|
||||
for (int i = 0; i < me->nfft; ++i)
|
||||
{
|
||||
// window[i] = 1;
|
||||
me->window[i] = me->fft_norm * hann_i(i, me->nfft);
|
||||
// me->window[i] = blackman_i(i, me->nfft);
|
||||
// me->window[i] = hamming_i(i, me->nfft);
|
||||
// me->window[i] = (i < len_window) ? hann_i(i, len_window) : 0;
|
||||
}
|
||||
me->last_frame = (float*)calloc(me->nfft, sizeof(me->last_frame[0]));
|
||||
|
||||
LOG(LOG_INFO, "Block size = %d\n", me->block_size);
|
||||
LOG(LOG_INFO, "Subblock size = %d\n", me->subblock_size);
|
||||
|
||||
size_t fft_work_size = 0;
|
||||
kiss_fftr_alloc(me->nfft, 0, 0, &fft_work_size);
|
||||
me->fft_work = malloc(fft_work_size);
|
||||
me->fft_cfg = kiss_fftr_alloc(me->nfft, 0, me->fft_work, &fft_work_size);
|
||||
|
||||
LOG(LOG_INFO, "N_FFT = %d\n", me->nfft);
|
||||
LOG(LOG_DEBUG, "FFT work area = %zu\n", fft_work_size);
|
||||
|
||||
#ifdef WATERFALL_USE_PHASE
|
||||
me->nifft = 64; // Gives 200 Hz sample rate for FT8 (160ms symbol period)
|
||||
|
||||
size_t ifft_work_size = 0;
|
||||
kiss_fft_alloc(me->nifft, 1, 0, &ifft_work_size);
|
||||
me->ifft_work = malloc(ifft_work_size);
|
||||
me->ifft_cfg = kiss_fft_alloc(me->nifft, 1, me->ifft_work, &ifft_work_size);
|
||||
|
||||
LOG(LOG_INFO, "N_iFFT = %d\n", me->nifft);
|
||||
LOG(LOG_DEBUG, "iFFT work area = %zu\n", ifft_work_size);
|
||||
#endif
|
||||
|
||||
// Allocate enough blocks to fit the entire FT8/FT4 slot in memory
|
||||
const int max_blocks = (int)(slot_time / symbol_period);
|
||||
// Keep only FFT bins in the specified frequency range (f_min/f_max)
|
||||
me->min_bin = (int)(cfg->f_min * symbol_period);
|
||||
me->max_bin = (int)(cfg->f_max * symbol_period) + 1;
|
||||
const int num_bins = me->max_bin - me->min_bin;
|
||||
|
||||
waterfall_init(&me->wf, max_blocks, num_bins, cfg->time_osr, cfg->freq_osr);
|
||||
me->wf.protocol = cfg->protocol;
|
||||
|
||||
me->symbol_period = symbol_period;
|
||||
|
||||
me->max_mag = -120.0f;
|
||||
}
|
||||
|
||||
void monitor_free(monitor_t* me)
|
||||
{
|
||||
waterfall_free(&me->wf);
|
||||
free(me->fft_work);
|
||||
free(me->last_frame);
|
||||
free(me->window);
|
||||
}
|
||||
|
||||
void monitor_reset(monitor_t* me)
|
||||
{
|
||||
me->wf.num_blocks = 0;
|
||||
me->max_mag = -120.0f;
|
||||
}
|
||||
|
||||
// Compute FFT magnitudes (log wf) for a frame in the signal and update waterfall data
|
||||
void monitor_process(monitor_t* me, const float* frame)
|
||||
{
|
||||
// Check if we can still store more waterfall data
|
||||
if (me->wf.num_blocks >= me->wf.max_blocks)
|
||||
return;
|
||||
|
||||
int offset = me->wf.num_blocks * me->wf.block_stride;
|
||||
int frame_pos = 0;
|
||||
|
||||
// Loop over block subdivisions
|
||||
for (int time_sub = 0; time_sub < me->wf.time_osr; ++time_sub)
|
||||
{
|
||||
kiss_fft_scalar timedata[me->nfft];
|
||||
kiss_fft_cpx freqdata[me->nfft / 2 + 1];
|
||||
|
||||
// Shift the new data into analysis frame
|
||||
for (int pos = 0; pos < me->nfft - me->subblock_size; ++pos)
|
||||
{
|
||||
me->last_frame[pos] = me->last_frame[pos + me->subblock_size];
|
||||
}
|
||||
for (int pos = me->nfft - me->subblock_size; pos < me->nfft; ++pos)
|
||||
{
|
||||
me->last_frame[pos] = frame[frame_pos];
|
||||
++frame_pos;
|
||||
}
|
||||
|
||||
// Do DFT of windowed analysis frame
|
||||
for (int pos = 0; pos < me->nfft; ++pos)
|
||||
{
|
||||
timedata[pos] = me->window[pos] * me->last_frame[pos];
|
||||
}
|
||||
kiss_fftr(me->fft_cfg, timedata, freqdata);
|
||||
|
||||
// Loop over possible frequency OSR offsets
|
||||
for (int freq_sub = 0; freq_sub < me->wf.freq_osr; ++freq_sub)
|
||||
{
|
||||
for (int bin = me->min_bin; bin < me->max_bin; ++bin)
|
||||
{
|
||||
int src_bin = (bin * me->wf.freq_osr) + freq_sub;
|
||||
float mag2 = (freqdata[src_bin].i * freqdata[src_bin].i) + (freqdata[src_bin].r * freqdata[src_bin].r);
|
||||
float db = 10.0f * log10f(1E-12f + mag2);
|
||||
|
||||
#ifdef WATERFALL_USE_PHASE
|
||||
// Save the magnitude in dB and phase in radians
|
||||
float phase = atan2f(freqdata[src_bin].i, freqdata[src_bin].r);
|
||||
me->wf.mag[offset].mag = db;
|
||||
me->wf.mag[offset].phase = phase;
|
||||
#else
|
||||
// Scale decibels to unsigned 8-bit range and clamp the value
|
||||
// Range 0-240 covers -120..0 dB in 0.5 dB steps
|
||||
int scaled = (int)(2 * db + 240);
|
||||
me->wf.mag[offset] = (scaled < 0) ? 0 : ((scaled > 255) ? 255 : scaled);
|
||||
#endif
|
||||
++offset;
|
||||
|
||||
if (db > me->max_mag)
|
||||
me->max_mag = db;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
++me->wf.num_blocks;
|
||||
}
|
||||
|
||||
#ifdef WATERFALL_USE_PHASE
|
||||
void monitor_resynth(const monitor_t* me, const candidate_t* candidate, float* signal)
|
||||
{
|
||||
const int num_ifft = me->nifft;
|
||||
const int num_shift = num_ifft / 2;
|
||||
const int taper_width = 4;
|
||||
const int num_tones = 8;
|
||||
|
||||
// Starting offset is 3 subblocks due to analysis buffer loading
|
||||
int offset = 1; // candidate->time_offset;
|
||||
offset = (offset * me->wf.time_osr) + 1; // + candidate->time_sub;
|
||||
offset = (offset * me->wf.freq_osr); // + candidate->freq_sub;
|
||||
offset = (offset * me->wf.num_bins); // + candidate->freq_offset;
|
||||
|
||||
WF_ELEM_T* el = me->wf.mag + offset;
|
||||
|
||||
// DFT frequency data - initialize to zero
|
||||
kiss_fft_cpx freqdata[num_ifft];
|
||||
for (int i = 0; i < num_ifft; ++i)
|
||||
{
|
||||
freqdata[i].r = 0;
|
||||
freqdata[i].i = 0;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
for (int num_block = 1; num_block < me->wf.num_blocks; ++num_block)
|
||||
{
|
||||
// Extract frequency data around the selected candidate only
|
||||
for (int i = candidate->freq_offset - taper_width - 1; i < candidate->freq_offset + 8 + taper_width - 1; ++i)
|
||||
{
|
||||
if ((i >= 0) && (i < me->wf.num_bins))
|
||||
{
|
||||
int tgt_bin = (me->wf.freq_osr * (i - candidate->freq_offset) + num_ifft) % num_ifft;
|
||||
float weight = 1.0f;
|
||||
if (i < candidate->freq_offset)
|
||||
{
|
||||
weight = ((i - candidate->freq_offset) + taper_width) / (float)taper_width;
|
||||
}
|
||||
else if (i > candidate->freq_offset + 7)
|
||||
{
|
||||
weight = ((candidate->freq_offset + 7 - i) + taper_width) / (float)taper_width;
|
||||
}
|
||||
|
||||
// Convert (dB magnitude, phase) to (real, imaginary)
|
||||
float mag = powf(10.0f, el[i].mag / 20) / 2 * weight;
|
||||
freqdata[tgt_bin].r = mag * cosf(el[i].phase);
|
||||
freqdata[tgt_bin].i = mag * sinf(el[i].phase);
|
||||
|
||||
int i2 = i + me->wf.num_bins;
|
||||
tgt_bin = (tgt_bin + 1) % num_ifft;
|
||||
float mag2 = powf(10.0f, el[i2].mag / 20) / 2 * weight;
|
||||
freqdata[tgt_bin].r = mag2 * cosf(el[i2].phase);
|
||||
freqdata[tgt_bin].i = mag2 * sinf(el[i2].phase);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute inverse DFT and overlap-add the waveform
|
||||
kiss_fft_cpx timedata[num_ifft];
|
||||
kiss_fft(me->ifft_cfg, freqdata, timedata);
|
||||
for (int i = 0; i < num_ifft; ++i)
|
||||
{
|
||||
signal[pos + i] += timedata[i].i;
|
||||
}
|
||||
|
||||
// Move to the next symbol
|
||||
el += me->wf.block_stride;
|
||||
pos += num_shift;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
#ifndef _INCLUDE_MONITOR_H_
|
||||
#define _INCLUDE_MONITOR_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
#include <ft8/decode.h>
|
||||
#include <fft/kiss_fftr.h>
|
||||
|
||||
/// Configuration options for FT4/FT8 monitor
|
||||
typedef struct
|
||||
{
|
||||
float f_min; ///< Lower frequency bound for analysis
|
||||
float f_max; ///< Upper frequency bound for analysis
|
||||
int sample_rate; ///< Sample rate in Hertz
|
||||
int time_osr; ///< Number of time subdivisions
|
||||
int freq_osr; ///< Number of frequency subdivisions
|
||||
ftx_protocol_t protocol; ///< Protocol: FT4 or FT8
|
||||
} monitor_config_t;
|
||||
|
||||
/// FT4/FT8 monitor object that manages DSP processing of incoming audio data
|
||||
/// and prepares a waterfall object
|
||||
typedef struct
|
||||
{
|
||||
float symbol_period; ///< FT4/FT8 symbol period in seconds
|
||||
int min_bin; ///< First FFT bin in the frequency range (begin)
|
||||
int max_bin; ///< First FFT bin outside the frequency range (end)
|
||||
int block_size; ///< Number of samples per symbol (block)
|
||||
int subblock_size; ///< Analysis shift size (number of samples)
|
||||
int nfft; ///< FFT size
|
||||
float fft_norm; ///< FFT normalization factor
|
||||
float* window; ///< Window function for STFT analysis (nfft samples)
|
||||
float* last_frame; ///< Current STFT analysis frame (nfft samples)
|
||||
ftx_waterfall_t wf; ///< Waterfall object
|
||||
float max_mag; ///< Maximum detected magnitude (debug stats)
|
||||
|
||||
// KISS FFT housekeeping variables
|
||||
void* fft_work; ///< Work area required by Kiss FFT
|
||||
kiss_fftr_cfg fft_cfg; ///< Kiss FFT housekeeping object
|
||||
#ifdef WATERFALL_USE_PHASE
|
||||
int nifft; ///< iFFT size
|
||||
void* ifft_work; ///< Work area required by inverse Kiss FFT
|
||||
kiss_fft_cfg ifft_cfg; ///< Inverse Kiss FFT housekeeping object
|
||||
#endif
|
||||
} monitor_t;
|
||||
|
||||
void monitor_init(monitor_t* me, const monitor_config_t* cfg);
|
||||
void monitor_reset(monitor_t* me);
|
||||
void monitor_process(monitor_t* me, const float* frame);
|
||||
void monitor_free(monitor_t* me);
|
||||
|
||||
#ifdef WATERFALL_USE_PHASE
|
||||
void monitor_resynth(const monitor_t* me, const candidate_t* candidate, float* signal);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // _INCLUDE_MONITOR_H_
|
||||
Vendored
+133
@@ -0,0 +1,133 @@
|
||||
#include "wave.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||
int save_wav(const float* signal, int num_samples, int sample_rate, const char* path)
|
||||
{
|
||||
char subChunk1ID[4] = { 'f', 'm', 't', ' ' };
|
||||
uint32_t subChunk1Size = 16; // 16 for PCM
|
||||
uint16_t audioFormat = 1; // PCM = 1
|
||||
uint16_t numChannels = 1;
|
||||
uint16_t bitsPerSample = 16;
|
||||
uint32_t sampleRate = sample_rate;
|
||||
uint16_t blockAlign = numChannels * bitsPerSample / 8;
|
||||
uint32_t byteRate = sampleRate * blockAlign;
|
||||
|
||||
char subChunk2ID[4] = { 'd', 'a', 't', 'a' };
|
||||
uint32_t subChunk2Size = num_samples * blockAlign;
|
||||
|
||||
char chunkID[4] = { 'R', 'I', 'F', 'F' };
|
||||
uint32_t chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
|
||||
char format[4] = { 'W', 'A', 'V', 'E' };
|
||||
|
||||
int16_t* raw_data = (int16_t*)malloc(num_samples * blockAlign);
|
||||
for (int i = 0; i < num_samples; i++)
|
||||
{
|
||||
float x = signal[i];
|
||||
if (x > 1.0)
|
||||
x = 1.0;
|
||||
else if (x < -1.0)
|
||||
x = -1.0;
|
||||
raw_data[i] = (int)(0.5 + (x * 32767.0));
|
||||
}
|
||||
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (f == NULL)
|
||||
return -1;
|
||||
|
||||
// NOTE: works only on little-endian architecture
|
||||
fwrite(chunkID, sizeof(chunkID), 1, f);
|
||||
fwrite(&chunkSize, sizeof(chunkSize), 1, f);
|
||||
fwrite(format, sizeof(format), 1, f);
|
||||
|
||||
fwrite(subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||
fwrite(&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||
fwrite(&audioFormat, sizeof(audioFormat), 1, f);
|
||||
fwrite(&numChannels, sizeof(numChannels), 1, f);
|
||||
fwrite(&sampleRate, sizeof(sampleRate), 1, f);
|
||||
fwrite(&byteRate, sizeof(byteRate), 1, f);
|
||||
fwrite(&blockAlign, sizeof(blockAlign), 1, f);
|
||||
fwrite(&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||
|
||||
fwrite(subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||
fwrite(&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||
|
||||
fwrite(raw_data, blockAlign, num_samples, f);
|
||||
|
||||
fclose(f);
|
||||
|
||||
free(raw_data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||
int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path)
|
||||
{
|
||||
char subChunk1ID[4]; // = {'f', 'm', 't', ' '};
|
||||
uint32_t subChunk1Size; // = 16; // 16 for PCM
|
||||
uint16_t audioFormat; // = 1; // PCM = 1
|
||||
uint16_t numChannels; // = 1;
|
||||
uint16_t bitsPerSample; // = 16;
|
||||
uint32_t sampleRate;
|
||||
uint16_t blockAlign; // = numChannels * bitsPerSample / 8;
|
||||
uint32_t byteRate; // = sampleRate * blockAlign;
|
||||
|
||||
char subChunk2ID[4]; // = {'d', 'a', 't', 'a'};
|
||||
uint32_t subChunk2Size; // = num_samples * blockAlign;
|
||||
|
||||
char chunkID[4]; // = {'R', 'I', 'F', 'F'};
|
||||
uint32_t chunkSize; // = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
|
||||
char format[4]; // = {'W', 'A', 'V', 'E'};
|
||||
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (f == NULL)
|
||||
return -1;
|
||||
|
||||
// NOTE: works only on little-endian architecture
|
||||
fread((void*)chunkID, sizeof(chunkID), 1, f);
|
||||
fread((void*)&chunkSize, sizeof(chunkSize), 1, f);
|
||||
fread((void*)format, sizeof(format), 1, f);
|
||||
|
||||
fread((void*)subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||
fread((void*)&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||
if (subChunk1Size != 16)
|
||||
return -2;
|
||||
|
||||
fread((void*)&audioFormat, sizeof(audioFormat), 1, f);
|
||||
fread((void*)&numChannels, sizeof(numChannels), 1, f);
|
||||
fread((void*)&sampleRate, sizeof(sampleRate), 1, f);
|
||||
fread((void*)&byteRate, sizeof(byteRate), 1, f);
|
||||
fread((void*)&blockAlign, sizeof(blockAlign), 1, f);
|
||||
fread((void*)&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||
|
||||
if (audioFormat != 1 || numChannels != 1 || bitsPerSample != 16)
|
||||
return -3;
|
||||
|
||||
fread((void*)subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||
fread((void*)&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||
|
||||
if (subChunk2Size / blockAlign > *num_samples)
|
||||
return -4;
|
||||
|
||||
*num_samples = subChunk2Size / blockAlign;
|
||||
*sample_rate = sampleRate;
|
||||
|
||||
int16_t* raw_data = (int16_t*)malloc(*num_samples * blockAlign);
|
||||
|
||||
fread((void*)raw_data, blockAlign, *num_samples, f);
|
||||
for (int i = 0; i < *num_samples; i++)
|
||||
{
|
||||
signal[i] = raw_data[i] / 32768.0f;
|
||||
}
|
||||
|
||||
free(raw_data);
|
||||
|
||||
fclose(f);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
#ifndef _INCLUDE_WAVE_H_
|
||||
#define _INCLUDE_WAVE_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||
int save_wav(const float* signal, int num_samples, int sample_rate, const char* path);
|
||||
|
||||
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||
int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // _INCLUDE_WAVE_H_
|
||||
Reference in New Issue
Block a user