Skip to content

Instantly share code, notes, and snippets.

@vk2gpu
Last active July 22, 2022 09:08
Show Gist options
  • Save vk2gpu/e2107abf62325b5d4b7afc6a575b76aa to your computer and use it in GitHub Desktop.
Save vk2gpu/e2107abf62325b5d4b7afc6a575b76aa to your computer and use it in GitHub Desktop.
mySSTV, simple SSTV encoder
// clang main.c -std=c11 -O3 -lsndfile -lc -lm -o mySSTV
// reference: https://www.classicsstv.com/daytonpaper.pdf
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <memory.h>
#include <sndfile.h>
typedef union
{
struct {
float r, g, b;
};
float ch[3];
} color_t;
static const int WIDTH = 320;
static const int HEIGHT = 256;
#define VIS_CODE_SCOTTIE_1 60
#define VIS_CODE_SCOTTIE_2 56
#define VIS_CODE_SCOTTIE_DX 76
#define SAMPLE_RATE 44100.0f
#define MS_RATE ( 1000.0f / SAMPLE_RATE )
#define TAU 6.28318f
typedef struct context_s context_t;
typedef struct state_s state_t;
typedef struct context_s {
uint32_t x, y;
color_t color;
float alpha;
float curr_hz;
float curr_ampl;
float time_ms;
float oscil_time;
uint8_t data[1024*4]; // 4kb for modes to use how they want.
} context_t;
typedef void(*update_fn)(context_t* ctx, state_t* state);
typedef struct state_s {
update_fn fn;
float ms;
float hz;
uint32_t userData;
state_t* nextState;
} state_t;
void update_silent(context_t* ctx, state_t* state) {
ctx->curr_hz = 1;
ctx->curr_ampl = 0.0f;
ctx->oscil_time = 0.0f;
}
void update_tone(context_t* ctx, state_t* state) {
ctx->curr_hz = state->hz;
ctx->curr_ampl = 1.0f;
}
void state_silence(state_t* thisState, float ms, state_t* nextState) {
state_t state = {
.fn = &update_silent,
.ms = ms,
.hz = 0,
.userData = 0,
.nextState = nextState
};
*thisState = state;
}
void state_tone(state_t* thisState, float ms, float hz, state_t* nextState) {
state_t state = {
.fn = &update_tone,
.ms = ms,
.hz = hz,
.userData = 0,
.nextState = nextState
};
*thisState = state;
}
void update_scottie_scan(context_t* ctx, state_t* state) {
uint32_t ch = state->userData;
ctx->x = ctx->time_ms / (state->ms / (float)WIDTH);
ctx->curr_hz = 1500.0f + (2300.0f - 1500.0f) * ctx->color.ch[ch];
ctx->curr_ampl = 1.0f;
if(ctx->alpha < 0.5f) {
ctx->curr_ampl = 0.0f;
}
if(ctx->time_ms >= state->ms) {
if(ch == 0) {
++ctx->y;
if(ctx->y >= HEIGHT)
state->nextState = state + 1;
}
}
}
void state_scottie_scan(state_t* thisState, float ms, uint32_t channel, state_t* nextState) {
state_t state = {
.fn = &update_scottie_scan,
.ms = ms,
.hz = 0,
.userData = channel,
.nextState = nextState
};
*thisState = state;
}
state_t* mode_scottie(context_t* ctx, state_t* states, uint32_t vis)
{
float scan_ms = 0.0f;
switch(vis) {
case VIS_CODE_SCOTTIE_1: scan_ms = 138.240f; break;
case VIS_CODE_SCOTTIE_2: scan_ms = 88.064f; break;
case VIS_CODE_SCOTTIE_DX: scan_ms = 345.6f; break;
default: return 0;
}
state_tone( &states[0], 9.0f, 1200.0f, &states[1] ); // “Starting” sync pulse (first line only!) 9.0ms 1200hz
state_tone( &states[1], 1.5f, 1500.0f, &states[2] ); // Separator pulse 1.5ms 1500hz
state_scottie_scan( &states[2], scan_ms, 1, &states[3] ); // Green scan
state_tone( &states[3], 1.5f, 1500.0f, &states[4] ); // Separator pulse 1.5ms 1500hz
state_scottie_scan( &states[4], scan_ms, 2, &states[5] ); // Blue scan
state_tone( &states[5], 9.0f, 1200.0f, &states[6] ); // Sync pulse 9.0ms 1200hz
state_tone( &states[6], 1.5f, 1500.0f, &states[7] ); // Sync porch 1.5ms 1500hz
state_scottie_scan( &states[7], scan_ms, 0, &states[1] ); // Red scan
return &states[8];
}
uint32_t count_bits_set(uint32_t value)
{
value = (value & 0x55555555U) + ((value & 0xAAAAAAAAU) >> 1);
value = (value & 0x33333333U) + ((value & 0xCCCCCCCCU) >> 2);
value = (value & 0x0F0F0F0FU) + ((value & 0xF0F0F0F0U) >> 4);
value = (value & 0x00FF00FFU) + ((value & 0xFF00FF00U) >> 8);
return (uint32_t)(value & 0x0000FFFFU) + ((value & 0xFFFF0000U) >> 16);
}
state_t* preamble_stock(context_t* ctx, state_t* states) {
state_tone( &states[0], 100.0f, 1900.0f, &states[1] );
state_tone( &states[1], 100.0f, 1500.0f, &states[2] );
state_tone( &states[2], 100.0f, 1900.0f, &states[3] );
state_tone( &states[3], 100.0f, 1500.0f, &states[4] );
state_tone( &states[4], 100.0f, 2300.0f, &states[5] );
state_tone( &states[5], 100.0f, 1500.0f, &states[6] );
state_tone( &states[6], 100.0f, 2300.0f, &states[7] );
state_tone( &states[7], 100.0f, 1500.0f, &states[8] );
return &states[8];
}
state_t* vis_code(context_t* ctx, state_t* states, uint32_t vis) {
float bits[8] = {
vis & 0b00000001 ? 1100.0f : 1300.0f,
vis & 0b00000010 ? 1100.0f : 1300.0f,
vis & 0b00000100 ? 1100.0f : 1300.0f,
vis & 0b00001000 ? 1100.0f : 1300.0f,
vis & 0b00010000 ? 1100.0f : 1300.0f,
vis & 0b00100000 ? 1100.0f : 1300.0f,
vis & 0b01000000 ? 1100.0f : 1300.0f,
count_bits_set(vis) & 1 ? 1100.0f : 1300.0f,
};
state_tone( &states[0], 300.0f, 1900.0f, &states[1] ); // Leader tone
state_tone( &states[1], 10.0f, 1200.0f, &states[2] ); // Break
state_tone( &states[2], 300.0f, 1900.0f, &states[3] ); // Leader tone
state_tone( &states[3], 30.0f, 1200.0f, &states[4] ); // start bit
state_tone( &states[4], 30.0f, bits[0], &states[5] ); // bit 0
state_tone( &states[5], 30.0f, bits[1], &states[6] ); // bit 1
state_tone( &states[6], 30.0f, bits[2], &states[7] ); // bit 2
state_tone( &states[7], 30.0f, bits[3], &states[8] ); // bit 3
state_tone( &states[8], 30.0f, bits[4], &states[9] ); // bit 4
state_tone( &states[9], 30.0f, bits[5], &states[10] ); // bit 5
state_tone( &states[10], 30.0f, bits[6], &states[11] ); // bit 6
state_tone( &states[11], 30.0f, bits[7], &states[12] ); // parity
state_tone( &states[12], 30.0f, 1200.0f, &states[13] ); // stop bit
return &states[13];
}
state_t* fsk_ch(context_t* ctx, state_t* states, char id_ch) {
state_t* state = states;
for(int i = 0; i < 6; ++i ) {
state_tone( state, 22.0f, (id_ch >> i) & 0x1 ? 1900.0f : 2100.0f, state + 1 );
++state;
}
return state;
}
state_t* fsk_id(context_t* ctx, state_t* states, const char* id_str) {
char id_ch = 0;
char checksum = 0;
state_t* state = states;
state_tone( &states[0], 300.0f, 1500.0f, &states[1] );
state_tone( &states[1], 100.0f, 2100.0f, &states[2] );
state_tone( &states[2], 22.0f, 1900.0f, &states[3] );
state = &states[3];
state = fsk_ch(ctx, state, 0x2A);
while((id_ch = *id_str++)) {
char send_ch = id_ch - 0x20;
state = fsk_ch(ctx, state, send_ch);
checksum ^= send_ch;
}
state = fsk_ch(ctx, state, 0x01);
state = fsk_ch(ctx, state, checksum & 0x3f);
state_tone( state, 100.0f, 1900.0f, state + 1 );
return state + 1;
}
color_t get_pixel(int x, int y);
float get_alpha(int x, int y);
color_t get_hsv(float h, float s, float v);
float get_color_hz(float c);
void generate(SNDFILE* f) {
context_t ctx;
state_t states[1024];
state_t* state = states;
memset(&ctx, 0, sizeof(context_t));
memset(&states, 0, sizeof(states));
uint32_t vis = VIS_CODE_SCOTTIE_2;
state = preamble_stock(&ctx, state);
state = vis_code(&ctx, state, vis);
state = mode_scottie(&ctx, state, vis);
state = fsk_id(&ctx, state, "VK2GPU");
state_t* currState = &states[0];
float curr_ampl = 1.0f;
while(currState->fn != NULL && currState->nextState != NULL) {
ctx.color = get_hsv( ctx.x / (float)WIDTH, ctx.y / (float)HEIGHT, 1.0 );
ctx.alpha = get_alpha(ctx.x ,ctx.y);
if(currState->fn)
currState->fn(&ctx, currState);
if(ctx.time_ms >= currState->ms) {
ctx.time_ms -= currState->ms;
currState = currState->nextState;
}
float oscil_adv = ctx.curr_hz / SAMPLE_RATE;
ctx.oscil_time = fmodf(ctx.oscil_time + oscil_adv, 1.0f);
curr_ampl = curr_ampl * 0.9f + ctx.curr_ampl * 0.1f;
int16_t ampl = (int16_t)(sin(ctx.oscil_time * TAU) * 15000 * curr_ampl);
#if 1 // add some artificial noise for testing
ampl += ((int)rand() % 5) - 2;
#endif
sf_writef_short(f, &ampl, 1);
ctx.time_ms += MS_RATE;
}
}
int main(int argc, const char** argv) {
SF_INFO info = {
.frames = 0,
.samplerate = (int)SAMPLE_RATE,
.channels = 1,
.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16,
.sections = 1,
.seekable = 1,
};
SNDFILE* f = sf_open("test_out.wav", SFM_WRITE, &info);
generate(f);
sf_write_sync(f);
sf_close(f);
return 0;
}
color_t get_pixel(int x, int y) {
color_t col =
{
.r = (x / (float)WIDTH),
.g = (y / (float)HEIGHT),
.b = 0,
};
return col;
}
float get_alpha(int x, int y) {
x = x / (WIDTH / 4);
y = y / (WIDTH / 4);
return ( x + y ) % 2 ? 0.0f : 1.0f;
}
float clamp(float v, float minv, float maxv) {
return v > maxv ? maxv : v < minv ? minv : v;
}
color_t get_hsv(float h, float s, float v) {
color_t hue =
{
.r = clamp(fabsf(h * 6.0f - 3.0f) - 1.0f, 0.0f, 1.0f),
.g = clamp(2.0f - fabsf(h * 6.0f - 2.0f), 0.0f, 1.0f),
.b = clamp(2.0f - fabsf(h * 6.0f - 4.0f), 0.0f, 1.0f),
};
color_t col =
{
.r = ((hue.r - 1.0f) * s + 1.0f) * v,
.g = ((hue.g - 1.0f) * s + 1.0f) * v,
.b = ((hue.b - 1.0f) * s + 1.0f) * v,
};
return col;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment