0
1
mirror of https://github.com/radio95-rnt/fm95.git synced 2026-02-26 19:23:51 +01:00
This commit is contained in:
2025-01-25 23:26:36 +01:00
parent abaa9764b0
commit b6c7607807
4 changed files with 97 additions and 303 deletions

View File

@@ -1,5 +1,5 @@
{ {
"port": 13452, "port": 13452,
"time": 1737836988947, "time": 1737842835132,
"version": "0.0.3" "version": "0.0.3"
} }

View File

@@ -1,36 +1,28 @@
# FMTools # FMTools
FMTools is a repository of apps you can use to make your FM broadcast better, pirate or not this will help you if you don't have something, maybe you want a better stereo encoder? SCA? We have what you need, for RDS just use MiniRDS FMTools is a repository of apps you can use to make your FM broadcast better, pirate or not this will help you if you don't have something, maybe you want a better stereo encoder? SCA? We have what you need, for RDS just use MiniRDS
# STCode # fm95
STCode is a simple stereo encoder for FM, it uses pasimple and math to: FM95 is a audio processor for FM, it does:
- Calculate mono signal ((L+R)/2) - Pre-Emphasis
- Generate the stereo pilot in phase to the stereo subcarrier - Low Pass Filter
- Generate the stereo diffrence signal using DSB-SC ((L-R)/2) - Stereo
- SSB Stereo
- Polar Stereo
- Polar SSB Stereo (huh)
All that in about 3.5% cpu usage on a RPI-5 (stereo tool has 3 threads which do 100% cpu usage anyway, one 200) Supports 2 inputs:
- Audio (via Pulse)
- MPX (via Pulse)
and one output:
- MPX (via Pulse or ALSA)
Also nearly no latency, not like Stereo Tool (or mpxgen which doesn't even work) Note that i haven't tested it, but i will on monday (29-01-25)
As far as i've tested it (29-31 december) it's been fine but after a fix it was great, so i'd recommend this to you
Also i'd recommend to use the SSB version because it's more spectrum effiecent
but SSB has slightly more cpu usage
This supports alsa output
# PSTCode
This is a yet another version of a Stereo encoder, however for the OIRT band which is in use in Russia, Belarus and other countries
Haven't tested it nor plan to
# SCAMod # SCAMod
SCAMod is a simple FM modulator which can be used to modulate a secondary audio stream, has similiar cpu usage and latency as STCode SCAMod is a simple FM modulator which can be used to modulate a secondary audio stream, has similiar cpu usage and latency as STCode
Has a fine quality, but as it goes for 12 khz fm signals Has a fine quality, but as it goes for 12 khz fm signals
# MonoPass
want to keep mono for a reason but have the lpf and preemphasis, do so
# How to compile? # How to compile?
To compile you need `cmake` and `libpulse-dev`, if you have those then do these commands: To compile you need `cmake` and `libpulse-dev`, if you have those then do these commands:
``` ```

View File

@@ -1,244 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdint.h>
#include <signal.h>
#include <string.h>
#include "../lib/constants.h"
#include "../lib/oscillator.h"
#include "../lib/filters.h"
#include "options.h"
#define SAMPLE_RATE 192000 // Don't go lower than 108 KHz, becuase it (53000*2) and (38000+15000)
#define INPUT_DEVICE "real_real_tx_audio_input.monitor"
#define OUTPUT_DEVICE "alsa_output.platform-soc_sound.stereo-fallback"
// #define ALSA_OUTPUT // Output, not input or both
#define BUFFER_SIZE 512
#define CLIPPER_THRESHOLD 0.525 // Adjust this as needed
#include <pulse/simple.h>
#include <pulse/error.h>
#ifdef ALSA_OUTPUT
#include <alsa/asoundlib.h>
#endif
#define MONO_VOLUME 0.45f // L+R Signal
#define STEREO_VOLUME 0.45f // L-R signal
#ifdef PREEMPHASIS
#define PREEMPHASIS_TAU 0.00005 // 50 microseconds, use 0.000075 if in america
#endif
#ifdef LPF
#define LPF_CUTOFF 15000
#endif
volatile sig_atomic_t to_run = 1;
float hard_clip(float sample) {
if (sample > CLIPPER_THRESHOLD) {
return CLIPPER_THRESHOLD; // Clip to the upper threshold
} else if (sample < -CLIPPER_THRESHOLD) {
return -CLIPPER_THRESHOLD; // Clip to the lower threshold
} else {
return sample; // No clipping
}
}
void uninterleave(const float *input, float *left, float *right, size_t num_samples) {
// For stereo, usually it is like this: LEFT RIGHT LEFT RIGHT LEFT RIGHT so this is used to get LEFT LEFT LEFT and RIGHT RIGHT RIGHT
for (size_t i = 0; i < num_samples/2; i++) {
left[i] = input[i * 2];
right[i] = input[i * 2 + 1];
}
}
static void stop(int signum) {
(void)signum;
printf("\nReceived stop signal. Cleaning up...\n");
to_run = 0;
}
int main() {
printf("PSTCode : (Polar) Stereo encoder made by radio95 (with help of ChatGPT and Claude, thanks!). Note that this version is for the OIRT band which is in use in Russia, Belarus and other CIS countries\n");
// Define formats and buffer atributes
pa_sample_spec stereo_format = {
.format = PA_SAMPLE_FLOAT32NE, //Float32 NE, or Float32 Native Endian, the float in c uses the endianess of your pc, or native endian, and float is float32, and double is float64
.channels = 2,
.rate = SAMPLE_RATE // Same sample rate makes it easy, leave the resampling to pipewire, it should know better
};
pa_sample_spec mono_format = {
.format = PA_SAMPLE_FLOAT32NE,
.channels = 1,
.rate = SAMPLE_RATE
};
pa_buffer_attr input_buffer_atr = {
.maxlength = buffer_maxlength,
.fragsize = buffer_tlength_fragsize
};
#ifndef ALSA_OUTPUT
pa_buffer_attr output_buffer_atr = {
.maxlength = buffer_maxlength,
.tlength = buffer_tlength_fragsize,
.prebuf = buffer_prebuf
};
#endif
printf("Connecting to input device... (%s)\n", INPUT_DEVICE);
pa_simple *input_device = pa_simple_new(
NULL,
"PolarStereoEncoder",
PA_STREAM_RECORD,
INPUT_DEVICE,
"Audio Input",
&stereo_format,
NULL,
&input_buffer_atr,
NULL
);
if (!input_device) {
fprintf(stderr, "Error: cannot open input device.\n");
return 1;
}
printf("Connecting to output device... (%s)\n", OUTPUT_DEVICE);
#ifndef ALSA_OUTPUT
pa_simple *output_device = pa_simple_new(
NULL,
"PolarStereoEncoder",
PA_STREAM_PLAYBACK,
OUTPUT_DEVICE,
"MPX",
&mono_format,
NULL,
&output_buffer_atr,
NULL
);
if (!output_device) {
fprintf(stderr, "Error: cannot open output device.\n");
pa_simple_free(input_device);
return 1;
}
#else
snd_pcm_hw_params_t *output_params;
snd_pcm_t *output_handle;
int output_error = snd_pcm_open(&output_handle, OUTPUT_DEVICE, SND_PCM_STREAM_PLAYBACK, 0);
if(output_error < 0) {
fprintf(stderr, "Error: cannot open output device: %s\n", snd_strerror(output_error));
pa_simple_free(input_device);
return 1;
}
snd_pcm_hw_params_malloc(&output_params);
snd_pcm_hw_params_any(output_handle, output_params);
snd_pcm_hw_params_set_access(output_handle, output_params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(output_handle, output_params, SND_PCM_FORMAT_FLOAT); // Same as pulse's Float32NE
snd_pcm_hw_params_set_channels(output_handle, output_params, 1);
unsigned int rate = SAMPLE_RATE;
int dir;
snd_pcm_hw_params_set_rate_near(output_handle, output_params, &rate, &dir);
snd_pcm_uframes_t frames = BUFFER_SIZE;
snd_pcm_hw_params_set_period_size_near(output_handle, output_params, &frames, &dir); // i don't have a clue why like this
output_error = snd_pcm_hw_params(output_handle, output_params);
if(output_error < 0) {
fprintf(stderr, "Error: cannot open output device: %s\n", snd_strerror(output_error));
snd_pcm_close(output_handle);
pa_simple_free(input_device);
return 1;
}
#endif
Oscillator stereo_osc;
init_oscillator(&stereo_osc, 31250.0, SAMPLE_RATE);
#ifdef PREEMPHASIS
ResistorCapacitor preemp_l, preemp_r;
init_rc_tau(&preemp_l, PREEMPHASIS_TAU, SAMPLE_RATE);
init_rc_tau(&preemp_r, PREEMPHASIS_TAU, SAMPLE_RATE);
#endif
#ifdef LPF
ResistorCapacitor lpf_l, lpf_r;
init_low_pass_filter(&lpf_l, LPF_CUTOFF, SAMPLE_RATE);
init_low_pass_filter(&lpf_r, LPF_CUTOFF, SAMPLE_RATE);
#endif
signal(SIGINT, stop);
signal(SIGTERM, stop);
int pulse_error;
float input[BUFFER_SIZE*2]; // Input from device, interleaved stereo
float left[BUFFER_SIZE+64], right[BUFFER_SIZE+64]; // Audio, same thing as in input but ininterleaved, ai told be there could be a buffer overflow here
float mpx[BUFFER_SIZE]; // MPX, this goes to the output
while (to_run) {
if (pa_simple_read(input_device, input, sizeof(input), &pulse_error) < 0) {
fprintf(stderr, "Error reading from input device: %s\n", pa_strerror(pulse_error));
to_run = 0;
break;
}
uninterleave(input, left, right, BUFFER_SIZE*2);
for (int i = 0; i < BUFFER_SIZE; i++) {
float l_in = left[i];
float r_in = right[i];
#ifdef PREEMPHASIS
#ifdef LPF
float lowpassed_left = apply_low_pass_filter(&lpf_l, l_in);
float lowpassed_right = apply_low_pass_filter(&lpf_r, r_in);
float preemphasized_left = apply_pre_emphasis(&preemp_l, lowpassed_left)*2;
float preemphasized_right = apply_pre_emphasis(&preemp_r, lowpassed_right)*2;
float current_left_input = hard_clip(preemphasized_left);
float current_right_input = hard_clip(preemphasized_right);
#else
float preemphasized_left = apply_pre_emphasis(&preemp_l, l_in)*2;
float preemphasized_right = apply_pre_emphasis(&preemp_r, r_in)*2;
float current_left_input = hard_clip(preemphasized_left);
float current_right_input = hard_clip(preemphasized_right);
#endif
#else
#ifdef LPF
float lowpassed_left = apply_low_pass_filter(&lpf_l, l_in);
float lowpassed_right = apply_low_pass_filter(&lpf_r, r_in);
float current_left_input = hard_clip(lowpassed_left);
float current_right_input = hard_clip(lowpassed_right);
#else
float current_left_input = hard_clip(l_in);
float current_right_input = hard_clip(r_in);
#endif
#endif
float mono = (current_left_input + current_right_input) / 2.0f; // Stereo to Mono
float stereo = (current_left_input - current_right_input) / 2.0f; // Also Stereo to Mono but a bit diffrent
float stereo_carrier = get_oscillator_sin_sample(&stereo_osc);
// -14 db is somewhere around 20% of a 1 volt signal
mpx[i] = mono * MONO_VOLUME +
((stereo+0.2) * stereo_carrier)*STEREO_VOLUME; // the 0.2 add DC, you know what happens then? Carrier wave
}
#ifndef ALSA_OUTPUT
if (pa_simple_write(output_device, mpx, sizeof(mpx), &pulse_error) < 0) {
fprintf(stderr, "Error writing to output device: %s\n", pa_strerror(pulse_error));
to_run = 0;
break;
}
#else
snd_pcm_writei(output_handle, mpx, sizeof(mpx));
#endif
}
printf("Cleaning up...\n");
pa_simple_free(input_device);
#ifndef ALSA_OUTPUT
pa_simple_free(output_device);
#else
snd_pcm_drain(output_handle);
snd_pcm_close(output_handle);
snd_pcm_hw_params_free(&output_params);
#endif
return 0;
}

View File

@@ -10,20 +10,16 @@
#define DEFAULT_STEREO 1 #define DEFAULT_STEREO 1
#define DEFAULT_STEREO_POLAR 0 #define DEFAULT_STEREO_POLAR 0
#define DEFAULT_STEREO_SSB 0
#define DEFAULT_CLIPPER_THRESHOLD 1.0f #define DEFAULT_CLIPPER_THRESHOLD 1.0f
#define DEFAULT_ALSA_OUTPUT 0 #define DEFAULT_ALSA_OUTPUT 0
//#define SSB
#ifdef SSB
//#define USB //#define USB
#endif
#include "../lib/constants.h" #include "../lib/constants.h"
#include "../lib/oscillator.h" #include "../lib/oscillator.h"
#include "../lib/filters.h" #include "../lib/filters.h"
#ifdef SSB
#include "../lib/hilbert.h" #include "../lib/hilbert.h"
#endif
#define SAMPLE_RATE 192000 // Don't go lower than 108 KHz, becuase it (53000*2) and (38000+15000) #define SAMPLE_RATE 192000 // Don't go lower than 108 KHz, becuase it (53000*2) and (38000+15000)
@@ -90,6 +86,10 @@ void show_help(char *name) {
" -o,--output Override output device\n" " -o,--output Override output device\n"
" -M,--mpx Override MPX input device\n" " -M,--mpx Override MPX input device\n"
" -c,--clipper Override the clipper threshold\n" " -c,--clipper Override the clipper threshold\n"
" -P,--polar Force Polar Stereo (does not take effect with -m)\n"
" -g,--ge Force Zenith/GE stereo (does not take effect with -m, default)\n"
" -S,--ssb Force SSB\n"
" -D,--dsb Force DSB\n"
,name ,name
); );
} }
@@ -97,6 +97,8 @@ void show_help(char *name) {
int main(int argc, char **argv) { int main(int argc, char **argv) {
show_version(); show_version();
int stereo = DEFAULT_STEREO; int stereo = DEFAULT_STEREO;
int polar_stereo = DEFAULT_STEREO_POLAR;
int ssb = DEFAULT_STEREO_SSB;
float clipper_threshold = DEFAULT_CLIPPER_THRESHOLD; float clipper_threshold = DEFAULT_CLIPPER_THRESHOLD;
#ifndef MPX_DEVICE #ifndef MPX_DEVICE
char audio_mpx_device[64] = "\0"; char audio_mpx_device[64] = "\0";
@@ -112,17 +114,21 @@ int main(int argc, char **argv) {
int alsa_output = DEFAULT_ALSA_OUTPUT; int alsa_output = DEFAULT_ALSA_OUTPUT;
int opt; int opt;
const char *short_opt = "msi:o:apM:c:hv"; const char *short_opt = "msi:o:apM:c:PgSDhv";
struct option long_opt[] = struct option long_opt[] =
{ {
{"mono", no_argument, NULL, 'm'}, {"mono", no_argument, NULL, 'm'},
{"stereo", no_argument, NULL, 's'}, {"stereo", no_argument, NULL, 's'},
{"input", optional_argument, NULL, 'i'}, {"input", optional_argument, NULL, 'i'},
{"output", optional_argument, NULL, 'o'}, {"output", optional_argument, NULL, 'o'},
{"alsa_out", optional_argument, NULL, 'a'}, {"alsa_out", no_argument, NULL, 'a'},
{"pulse_put", optional_argument, NULL, 'p'}, {"pulse_put", no_argument, NULL, 'p'},
{"mpx", optional_argument, NULL, 'M'}, {"mpx", optional_argument, NULL, 'M'},
{"clipper", optional_argument, NULL, 'c'}, {"clipper", optional_argument, NULL, 'c'},
{"polar", no_argument, NULL, 'P'},
{"ge", no_argument, NULL, 'g'},
{"ssb", no_argument, NULL, 'S'},
{"dsb", no_argument, NULL, 'D'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
@@ -160,6 +166,22 @@ int main(int argc, char **argv) {
clipper_threshold = strtof(optarg, NULL); clipper_threshold = strtof(optarg, NULL);
printf("Running with a clipper threshold of %f\n", clipper_threshold); printf("Running with a clipper threshold of %f\n", clipper_threshold);
break; break;
case 'P': //Polar
polar_stereo = 1;
printf("Using polar stereo\n");
break;
case 'g': //GE
polar_stereo = 0;
printf("Using Zenith/GE stereo\n");
break;
case 'S': //SSB
ssb = 1;
printf("Using SSB\n");
break;
case 'D': //DSB
ssb = 0;
printf("Using DSB\n");
break;
case 'v': // Version case 'v': // Version
show_version(); show_version();
return 0; return 0;
@@ -278,14 +300,15 @@ int main(int argc, char **argv) {
} }
Oscillator pilot_osc; Oscillator pilot_osc;
init_oscillator(&pilot_osc, 19000.0, SAMPLE_RATE); // Pilot, it's there to indicate stereo and as a refrence signal with the stereo carrier if(polar_stereo == 1) {
init_oscillator(&pilot_osc, 31250.0, SAMPLE_RATE); // Pilot, it's there to indicate stereo and as a refrence signal with the stereo carrier
#ifdef SSB } else {
init_oscillator(&pilot_osc, 19000.0, SAMPLE_RATE); // Pilot, it's there to indicate stereo and as a refrence signal with the stereo carrier
}
HilbertTransformer hilbert; // An Hilbert shifts a signal in quadrature, generating the I/Q data HilbertTransformer hilbert; // An Hilbert shifts a signal in quadrature, generating the I/Q data
init_hilbert(&hilbert); init_hilbert(&hilbert);
DelayLine monoDelay; // Hilbert introduces a delay of 99 samples, this should be here to sync the mono with stereo to a sample DelayLine monoDelay; // Hilbert introduces a delay of 99 samples, this should be here to sync the mono with stereo to a sample
init_delay_line(&monoDelay, 99); init_delay_line(&monoDelay, 99);
#endif
#ifdef PREEMPHASIS #ifdef PREEMPHASIS
ResistorCapacitor preemp_l, preemp_r; ResistorCapacitor preemp_l, preemp_r;
init_rc_tau(&preemp_l, PREEMPHASIS_TAU, SAMPLE_RATE); init_rc_tau(&preemp_l, PREEMPHASIS_TAU, SAMPLE_RATE);
@@ -354,29 +377,54 @@ int main(int argc, char **argv) {
float mono = (current_left_input + current_right_input) / 2.0f; // Stereo to Mono float mono = (current_left_input + current_right_input) / 2.0f; // Stereo to Mono
if(stereo == 1) { if(stereo == 1) {
float stereo = (current_left_input - current_right_input) / 2.0f; // Also Stereo to Mono but a bit diffrent float stereo = (current_left_input - current_right_input) / 2.0f; // Also Stereo to Mono but a bit diffrent
float stereo_carrier = get_oscillator_sin_multiplier_ni(&pilot_osc, 2); // Get stereo carrier via multiplication if(polar_stereo == 1) {
#ifdef SSB if(ssb) {
float stereo_carrier_cos = get_oscillator_cos_multiplier_ni(&pilot_osc, 2) // Get Carrier Q of I/Q float stereo_carrier = get_oscillator_sin_multiplier_ni(&pilot_osc, 1); // Get stereo carrier via multiplication
float pilot = get_oscillator_sin_sample(&pilot_osc); float stereo_carrier_cos = get_oscillator_cos_sample(&pilot_osc); // Get Carrier Q of I/Q
float stereo_i, stereo_q; float stereo_i, stereo_q;
apply_hilbert(&hilbert, stereo, &stereo_i, &stereo_q); // Compute I/Q stereo += 0.2;
#ifdef USB apply_hilbert(&hilbert, stereo, &stereo_i, &stereo_q); // Compute I/Q
float signal = (stereo_i*cos38+stereo_q*(sin38*0.775f)); #ifdef USB
#else float signal = (stereo_i*stereo_carrier_cos+stereo_q*(stereo_carrier*0.775f));
float signal = (stereo_i*cos38-stereo_q*(sin38*0.775f)); #else
#endif float signal = (stereo_i*stereo_carrier_cos-stereo_q*(stereo_carrier*0.775f));
output[i] = mono*MONO_VOLUME + #endif
pilot*PILOT_VOLUME + output[i] = delay_line(&monoDelay, mono)*MONO_VOLUME +
signal*STEREO_VOLUME signal*STEREO_VOLUME;
; if(strlen(audio_mpx_device) != 0) output[i] += multiplex_in*MPX_VOLUME;
#else } else {
float pilot = get_oscillator_sin_sample(&pilot_osc); float stereo_carrier = get_oscillator_sin_sample(&pilot_osc);
output[i] = mono*MONO_VOLUME + output[i] = mono*MONO_VOLUME +
pilot*PILOT_VOLUME + ((stereo+0.2)*stereo_carrier)*STEREO_VOLUME;
(stereo*stereo_carrier)*STEREO_VOLUME; if(strlen(audio_mpx_device) != 0) output[i] += multiplex_in*MPX_VOLUME;
if(strlen(audio_mpx_device) != 0) output[i] += multiplex_in*MPX_VOLUME; }
#endif } else {
if(ssb) {
float stereo_carrier = get_oscillator_sin_multiplier_ni(&pilot_osc, 2); // Get stereo carrier via multiplication
float stereo_carrier_cos = get_oscillator_cos_multiplier_ni(&pilot_osc, 2); // Get Carrier Q of I/Q
float pilot = get_oscillator_sin_sample(&pilot_osc);
float stereo_i, stereo_q;
apply_hilbert(&hilbert, stereo, &stereo_i, &stereo_q); // Compute I/Q
#ifdef USB
float signal = (stereo_i*stereo_carrier_cos+stereo_q*(stereo_carrier*0.775f));
#else
float signal = (stereo_i*stereo_carrier_cos-stereo_q*(stereo_carrier*0.775f));
#endif
output[i] = delay_line(&monoDelay, mono)*MONO_VOLUME +
pilot*PILOT_VOLUME +
signal*STEREO_VOLUME;
if(strlen(audio_mpx_device) != 0) output[i] += multiplex_in*MPX_VOLUME;
} else {
float stereo_carrier = get_oscillator_sin_multiplier_ni(&pilot_osc,2);
float pilot = get_oscillator_sin_sample(&pilot_osc);
output[i] = mono*MONO_VOLUME +
pilot*PILOT_VOLUME +
(stereo*stereo_carrier)*STEREO_VOLUME;
if(strlen(audio_mpx_device) != 0) output[i] += multiplex_in*MPX_VOLUME;
}
}
} else { } else {
output[i] = mono*MONO_VOLUME; // Only Mono output[i] = mono*MONO_VOLUME; // Only Mono
} }
@@ -402,9 +450,7 @@ int main(int argc, char **argv) {
snd_pcm_close(output_handle); snd_pcm_close(output_handle);
snd_pcm_hw_params_free(output_params); snd_pcm_hw_params_free(output_params);
} }
#ifdef SSB
exit_hilbert(&hilbert); exit_hilbert(&hilbert);
exit_delay_line(&monoDelay); exit_delay_line(&monoDelay);
#endif
return 0; return 0;
} }