Last active
July 22, 2022 09:08
-
-
Save vk2gpu/e2107abf62325b5d4b7afc6a575b76aa to your computer and use it in GitHub Desktop.
mySSTV, simple SSTV encoder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, &l, 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