diff options
Diffstat (limited to 'util')
| -rw-r--r-- | util/nvmutil/nvmutil.c | 128 | ||||
| -rw-r--r-- | util/spkmodem_decode/.gitignore | 2 | ||||
| -rw-r--r-- | util/spkmodem_decode/Makefile (renamed from util/spkmodem_recv/Makefile) | 6 | ||||
| -rw-r--r-- | util/spkmodem_decode/spkmodem-decode.c | 725 | ||||
| -rw-r--r-- | util/spkmodem_recv/.gitignore | 1 | ||||
| -rw-r--r-- | util/spkmodem_recv/spkmodem-recv.c | 321 |
6 files changed, 789 insertions, 394 deletions
diff --git a/util/nvmutil/nvmutil.c b/util/nvmutil/nvmutil.c index b718ac6e..811cce31 100644 --- a/util/nvmutil/nvmutil.c +++ b/util/nvmutil/nvmutil.c @@ -47,9 +47,6 @@ * TODO: bound checks for files per-command, e.g. only * first 6 bytes for CMD_SETMAC * - * TODO: clean up the do_rw function: make PSCHREIB and - * so on clearer, probably just define them inline and - * validate them inline (no define). * TODO: in command sanitizer: verify that each given * entry corresponds to the correct function, in the * pointer (this check is currently missing) @@ -85,10 +82,6 @@ * TODO: also document the layout of Intel GbE files, so * that wily individuals can easily expand the * featureset of nvmutil. - * TODO: remove some clever code, e.g.: - * rw_type == PLESEN << 2 - * make stuff like that clearer. - * ditto the invert copy/swap trick * TODO: write a manpage * TODO: simplify the command sanitization, implement more * of it as build time checks, e.g. static asserts. @@ -441,10 +434,10 @@ static const char *argv0; #define ARGC_4 4 enum { - LESEN, - PLESEN, - SCHREIB, - PSCHREIB + IO_READ, + IO_WRITE, + IO_PREAD, + IO_PWRITE }; /* @@ -567,6 +560,10 @@ static size_t cmd_index = CMD_NULL; typedef char assert_argc3[(ARGC_3==3)?1:-1]; typedef char assert_argc4[(ARGC_4==4)?1:-1]; +typedef char assert_read[(IO_READ==0)?1:-1]; +typedef char assert_write[(IO_WRITE==1)?1:-1]; +typedef char assert_pread[(IO_PREAD==2)?1:-1]; +typedef char assert_pwrite[(IO_PWRITE==3)?1:-1]; static int use_prng = 0; @@ -736,9 +733,6 @@ sanitize_command_index(size_t c) if (command[c].flags != O_RDONLY && command[c].flags != O_RDWR) err(EINVAL, "invalid cmd.flags setting"); - - if (!((!LESEN) && (PLESEN == 1) && (SCHREIB == 2) && (PSCHREIB == 3))) - err(EINVAL, "rw type integers are the wrong values"); } static void @@ -906,7 +900,7 @@ read_gbe_file(void) for (p = 0; p < 2; p++) { if (do_read[p]) - rw_gbe_file_part(p, PLESEN, "pread"); + rw_gbe_file_part(p, IO_PREAD, "pread"); } } @@ -1133,20 +1127,12 @@ rhex(void) static size_t n = 0; static uint8_t rnum[12]; - if (use_prng) { - /* - * On very old Unix systems that - * lack /dev/random and /dev/urandom - */ + if (use_prng) return fallback_rand(); - } - - if (urandom_fd < 0) - err(ECANCELED, "Your operating system has no /dev/[u]random"); if (!n) { n = sizeof(rnum); - if (rw_file_exact(urandom_fd, rnum, n, 0, LESEN) == -1) + if (rw_file_exact(urandom_fd, rnum, n, 0, IO_READ) == -1) err(errno, "Randomisation failed"); } @@ -1305,7 +1291,7 @@ gbe_cat_buf(uint8_t *b) while (1) { rval = rw_file_exact(STDOUT_FILENO, b, - GBE_PART_SIZE, 0, SCHREIB); + GBE_PART_SIZE, 0, IO_WRITE); if (rval >= 0) { /* @@ -1346,7 +1332,7 @@ write_gbe_file(void) if (update_checksum) set_checksum(partnum); - rw_gbe_file_part(partnum, PSCHREIB, "pwrite"); + rw_gbe_file_part(partnum, IO_PWRITE, "pwrite"); } } @@ -1470,7 +1456,7 @@ rw_gbe_file_part(size_t p, int rw_type, uint8_t *mem_offset; - if (rw_type == SCHREIB || rw_type == PSCHREIB) + if (rw_type == IO_WRITE || rw_type == IO_PWRITE) invert = 0; /* @@ -1559,8 +1545,8 @@ gbe_x_offset(size_t p, const char *f_op, const char *d_type, * write() to ignore the current file offset and * write at EOF, which means that our use of * lseek in prw() does not guarantee writing at - * a specified offset. So if using PSCHREIB or - * PLESEN, make sure not to pass a file descriptor + * a specified offset. So if using IO_PWRITE or + * IO_PREAD, make sure not to pass a file descriptor * with the O_APPEND flag. Alternatively, modify * do_rw() to directly use pwrite() and pread() * instead of prw(). @@ -1572,19 +1558,26 @@ rw_file_exact(int fd, uint8_t *mem, size_t len, ssize_t rv; size_t rc; - if (fd < 0 || !len || len > (size_t)SSIZE_MAX) { + if (fd < 0 || !len || len > (size_t)SSIZE_MAX + || (unsigned int)rw_type > IO_PWRITE) { errno = EIO; return -1; } - for (rc = 0, rv = 0; rc < len; rc += (size_t)rv) { + for (rc = 0, rv = 0; rc < len; ) { if ((rv = rw_file_once(fd, mem, len, off, rw_type, rc)) == -1) return -1; + + rc += (size_t)rv; } return rc; } +/* + * May not return all requested bytes (len). + * Use rw_file_exact for guaranteed length. + */ static ssize_t rw_file_once(int fd, uint8_t *mem, size_t len, off_t off, int rw_type, size_t rc) @@ -1594,61 +1587,50 @@ rw_file_once(int fd, uint8_t *mem, size_t len, size_t max_retries = 10; read_again: + if ((unsigned int)rw_type > IO_PWRITE) + goto err_rw_file_once; + rv = do_rw(fd, mem + rc, len - rc, off + rc, rw_type); - if (rv < 0 && errno == EINTR) { + if (rv < 0 && errno == EINTR) goto read_again; - } else if (rv < 0) { - errno = EIO; - return -1; - } - /* - * Theoretical bug: if a buggy libc returned - * a size larger than SSIZE_MAX, the cast may - * cause an overflow. Specifications guarantee - * this won't happen, but spec != implementation - */ - if ((size_t)rv > SSIZE_MAX) { - errno = EIO; + if (rv < 0) return -1; - /* we will not tolerate your buggy libc */ - } - if ((size_t)rv > (len - rc) /* Prevent overflow */ - || rv == 0) { /* Prevent infinite 0-byte loop */ - if (rv == 0) { - /* - * Fault tolerance against infinite - * zero-byte loop: re-try a finite - * number of times. This mitigates - * otherwise OK but slow filesystems - * e.g. NFS or slow media. - */ - if (retries_on_zero++ < max_retries) - goto read_again; - } - errno = EIO; - return -1; - } + if ((size_t)rv > SSIZE_MAX /* theoretical buggy libc */ + || (size_t)rv > (len - rc))/* don't overflow */ + goto err_rw_file_once; + + if (rv != 0) + return rv; + + if (retries_on_zero++ < max_retries) + goto read_again; - return rv; +err_rw_file_once: + errno = EIO; + return -1; } static ssize_t do_rw(int fd, uint8_t *mem, size_t len, off_t off, int rw_type) { - if (rw_type == LESEN || rw_type == PLESEN << 2) + if ((unsigned int)rw_type > IO_PWRITE) + goto err_do_rw; + + if (rw_type == IO_READ) return read(fd, mem, len); - if (rw_type == SCHREIB || rw_type == PSCHREIB << 2) + if (rw_type == IO_WRITE) return write(fd, mem, len); - if (rw_type == PLESEN || rw_type == PSCHREIB) + if (rw_type == IO_PREAD || rw_type == IO_PWRITE) return prw(fd, mem, len, off, rw_type); - errno = EINVAL; +err_do_rw: + errno = EIO; return -1; } @@ -1668,6 +1650,14 @@ prw(int fd, void *mem, size_t nrw, off_t off_orig; ssize_t r; int saved_errno; + int prw_type; + + prw_type = rw_type ^ IO_PREAD; + + if (prw_type > IO_WRITE) { + errno = EIO; + return -1; + } if ((off_orig = lseek_eintr(fd, (off_t)0, SEEK_CUR)) == (off_t)-1) return -1; @@ -1675,7 +1665,7 @@ prw(int fd, void *mem, size_t nrw, return -1; do { - r = do_rw(fd, mem, nrw, off, rw_type << 2); + r = do_rw(fd, mem, nrw, off, prw_type); } while (r < 0 && errno == EINTR); saved_errno = errno; diff --git a/util/spkmodem_decode/.gitignore b/util/spkmodem_decode/.gitignore new file mode 100644 index 00000000..42814fe4 --- /dev/null +++ b/util/spkmodem_decode/.gitignore @@ -0,0 +1,2 @@ +spkmodem-recv +spkmodem-decode diff --git a/util/spkmodem_recv/Makefile b/util/spkmodem_decode/Makefile index 293d7475..b00c4f43 100644 --- a/util/spkmodem_recv/Makefile +++ b/util/spkmodem_decode/Makefile @@ -8,12 +8,12 @@ DESTDIR?= PREFIX?=/usr/local INSTALL?=install -PROG=spkmodem-recv +PROG=spkmodem-decode all: $(PROG) -$(PROG): spkmodem-recv.c - $(CC) $(CFLAGS) spkmodem-recv.c -o $(PROG) +$(PROG): spkmodem-decode.c + $(CC) $(CFLAGS) spkmodem-decode.c -o $(PROG) install: $(PROG) mkdir -p $(DESTDIR)$(PREFIX)/bin/ diff --git a/util/spkmodem_decode/spkmodem-decode.c b/util/spkmodem_decode/spkmodem-decode.c new file mode 100644 index 00000000..3b3b33f8 --- /dev/null +++ b/util/spkmodem_decode/spkmodem-decode.c @@ -0,0 +1,725 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * Copyright (c) 2013 Free Software Foundation, Inc. + * Copyright (c) 2023, 2026 Leah Rowe <leah@libreboot.org> + * + * This program receives text encoded as pulses on the PC speaker, + * and decodes them via simple FSK (Frequency Shift Keying) + * demodulation and FIR (Finite Impulse Response) frequency + * discriminator. + * + * It waits for specific tones at specific intervals. + * It detects tones within the audio stream and reconstructs + * characters bit-by-bit as the encoded modem signal is received. + * This is performance-efficient on most CPUs, and has relatively + * high tolerance for noisy signals (similar to techniques used + * for data stored on audio cassette tapes). + * + * This is a special interface provided by coreboot and GNU GRUB, + * for computers that lack serial ports (useful for debugging). + * Note that GRUB and coreboot can both send these signals; this + * tool merely decodes them. This tool does not *encode*, only + * decode. + * + * Usage example (NOTE: little endian!): + * parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-decode + * + * Originally provided by GNU GRUB, this version is a heavily + * modified fork that complies with the OpenBSD Kernel Source + * File Style Guide (KNF) instead of GNU coding standards; it + * emphasises strict error handling, portability and code + * quality, as characterised by OpenBSD projects. Several magic + * numbers have been tidied up, calculated (not hardcoded) and + * thoroughly explained, unlike in the original version. + * + * The original version was essentially a blob, masquerading as + * source code. This forked source code is therefore the result + * of extensive reverse engineering (of the GNU source code)! + * This cleaned up code and extensive commenting will thoroughly + * explain how the decoding works. This was done as an academic + * exercise in 2023, continuing in 2026. + * + * This fork of spkmodem-recv, called spkmodem-decode, is provided + * with Libreboot releases: + * https://libreboot.org/ + * + * The original GNU version is here, if you're morbidly curious: + * https://cgit.git.savannah.gnu.org/cgit/grub.git/plain/util/spkmodem-recv.c?id=3dce38eb196f47bdf86ab028de74be40e13f19fd + * + * Libreboot's version was renamed to spkmodem-decode on 12 March 2026, + * since Libreboot's version no longer closely resembles the GNU + * version at all; ergo, a full rename was in order. GNU's version + * was called spkmodem-recv. + */ + +#define _POSIX_SOURCE + +/* + * For OpenBSD define, to detect version + * for deciding whether to use pledge(2) + */ +#ifdef __OpenBSD__ +#include <sys/param.h> +#endif + +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/* + * spkmodem is essentially using FSK (Frequency Shift Keying) + * with two primary tones representing encoded bits, + * separated by a framing tone. + * Very cheap on CPU cycles and avoids needing something more + * expensive like FFT or Goertzel filters, and tolerates + * weak/noisy signals. + */ + +/* + * Frequency of audio in Hz + * WARNING: if changing, make sure to adjust + * SAMPLES_PER_FRAME accordingly (see maths below) + */ +#define SAMPLE_RATE 48000 + +/* + * One analysis frame spans 5 ms. + * + * frame_time = SAMPLES_PER_FRAME / SAMPLE_RATE + * + * With the default sample rate (48 kHz): + * + * frame_time = N / 48000 + * 0.005 s = N / 48000 + * N = 0.005 × 48000 = 240 samples + */ +#define SAMPLES_PER_FRAME 240 + +/* + * Number of analysis frames per second. + * + * Each increment in the frequency counters corresponds + * roughly to this many Hertz of tone frequency. + * + * With the default values: + * FRAME_RATE = 48000 / 240 = 200 Hz + */ +#define FRAME_RATE ((SAMPLE_RATE) / (SAMPLES_PER_FRAME)) + +/* + * Two FIR windows are maintained; one for data tone, + * and one for the separator tone. They are positioned + * one frame apart in the ring buffer. + */ +#define MAX_SAMPLES (2 * (SAMPLES_PER_FRAME)) + +/* + * Approx byte offset for ring buffer span, just for + * easier debug output correlating to the audio stream. + */ +#define SAMPLE_OFFSET ((MAX_SAMPLES) * (sizeof(short))) + +/* + * Expected tone ranges (approximate, derived from spkmodem). + * These values are intentionally wide because real-world setups + * often involve microphones, room acoustics, and cheap ADCs. + */ +#define SEP_TONE_MIN_HZ 1000 +#define SEP_TONE_MAX_HZ 3000 + +#define SEP_TOLERANCE_PULSES \ + (((SEP_TONE_MAX_HZ) - (SEP_TONE_MIN_HZ)) / (2 * (FRAME_RATE))) + +#define DATA_TONE_MIN_HZ 3000 +#define DATA_TONE_MAX_HZ 12000 + +/* Mid point used to distinguish the two data tones. */ +#define DATA_TONE_THRESHOLD_HZ 5000 + +/* + * Convert tone frequency ranges into pulse counts within the + * sliding analysis window. + * + * pulse_count = tone_frequency / FRAME_RATE + * where FRAME_RATE = SAMPLE_RATE / SAMPLES_PER_FRAME. + */ +#define FREQ_SEP_MIN ((SEP_TONE_MIN_HZ) / (FRAME_RATE)) +#define FREQ_SEP_MAX ((SEP_TONE_MAX_HZ) / (FRAME_RATE)) + +#define FREQ_DATA_MIN ((DATA_TONE_MIN_HZ) / (FRAME_RATE)) +#define FREQ_DATA_MAX ((DATA_TONE_MAX_HZ) / (FRAME_RATE)) + +#define FREQ_DATA_THRESHOLD ((DATA_TONE_THRESHOLD_HZ) / (FRAME_RATE)) + +/* + * These determine how long the program will wait during + * tone auto-detection, before shifting to defaults. + * It is done every LEARN_FRAMES number of frames. + */ +#define LEARN_SECONDS 1 +#define LEARN_FRAMES ((LEARN_SECONDS) * (FRAME_RATE)) + +/* + * Sample amplitude threshold used to convert the waveform + * into a pulse stream. Values near zero are regarded as noise. + */ +#define THRESHOLD 500 + +#define READ_BUF 4096 + +struct decoder_state { + unsigned char pulse[MAX_SAMPLES]; + + signed short inbuf[READ_BUF]; + size_t inpos; + size_t inlen; + + int ringpos; + int sep_pos; + + /* + * Sliding window pulse counters + * used to detect modem tones + */ + int freq_data; + int freq_separator; + int sample_count; + + int ascii_bit; + unsigned char ascii; + + int debug; + int swap_bytes; + + /* dynamic separator calibration */ + int sep_sum; + int sep_samples; + int sep_min; + int sep_max; + + /* for automatic tone detection */ + int freq_min; + int freq_max; + int freq_threshold; + int learn_frames; + + /* previous sample used for edge detection */ + signed short prev_sample; +}; + +static const char *argv0; + +/* + * 16-bit little endian words are read + * continuously. we will swap them, if + * the host cpu is big endian. + */ +static int host_is_big_endian(void); + +/* main loop */ +static void handle_audio(struct decoder_state *st); + +/* separate tone tolerances */ +static void select_separator_tone(struct decoder_state *st); +static int is_valid_signal(struct decoder_state *st); + +/* output to terminal */ +static int set_ascii_bit(struct decoder_state *st); +static void print_char(struct decoder_state *st); +static void reset_char(struct decoder_state *st); + +/* process samples/frames */ +static void decode_pulse(struct decoder_state *st); +static signed short read_sample(struct decoder_state *st); +static void read_words(struct decoder_state *st); + +/* continually adjust tone */ +static void detect_tone(struct decoder_state *st); +static int silent_signal(struct decoder_state *st); +static void select_low_tone(struct decoder_state *st); + +/* debug */ +static void print_stats(struct decoder_state *st); + +/* error handling / usage */ +static void err(int errval, const char *msg, ...); +static void usage(void); +static const char *progname(void); + +/* portability (old systems) */ +int getopt(int, char * const *, const char *); +extern char *optarg; +extern int optind; +extern int opterr; +extern int optopt; + +#ifndef CHAR_BIT +#define CHAR_BIT 8 +#endif + +typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1]; +typedef char static_assert_char_is_1[(sizeof(char) == 1) ? 1 : -1]; +typedef char static_assert_short[(sizeof(short) == 2) ? 1 : -1]; +typedef char static_assert_int_is_4[(sizeof(int) >= 4) ? 1 : -1]; +typedef char static_assert_twos_complement[ + ((-1 & 3) == 3) ? 1 : -1 +]; + +int +main(int argc, char **argv) +{ + struct decoder_state st; + int c; + + argv0 = argv[0]; + +#if defined (__OpenBSD__) && defined(OpenBSD) +#if OpenBSD >= 509 + if (pledge("stdio", NULL) == -1) + err(errno, "pledge"); +#endif +#endif + + memset(&st, 0, sizeof(st)); + + while ((c = getopt(argc, argv, "d")) != -1) { + if (c != 'd') + usage(); + st.debug = 1; + break; + } + + /* fallback in case tone detection fails */ + st.freq_min = 100000; + st.freq_max = 0; + st.freq_threshold = FREQ_DATA_THRESHOLD; + + /* + * Used for separator calibration + */ + st.sep_min = FREQ_SEP_MIN; + st.sep_max = FREQ_SEP_MAX; + + st.ascii_bit = 7; + + st.ringpos = 0; + st.sep_pos = SAMPLES_PER_FRAME; + + if (host_is_big_endian()) + st.swap_bytes = 1; + + setvbuf(stdout, NULL, _IONBF, 0); + + for (;;) + handle_audio(&st); + + return EXIT_SUCCESS; +} + +static int +host_is_big_endian(void) +{ + unsigned int x = 1; + return (*(unsigned char *)&x == 0); +} + +static void +handle_audio(struct decoder_state *st) +{ + int sample; + + /* + * If the modem signal disappears for several (read: 3) + * frames, discard the partially assembled character. + */ + if (st->sample_count >= (3 * SAMPLES_PER_FRAME) || + st->freq_separator <= 0) + reset_char(st); + + st->sample_count = 0; + + /* process exactly one frame */ + for (sample = 0; sample < SAMPLES_PER_FRAME; sample++) + decode_pulse(st); + + select_separator_tone(st); + + if (set_ascii_bit(st) < 0) + print_char(st); + + /* Detect tone per each frame */ + detect_tone(st); +} + +/* + * collect separator tone statistics + * (and auto-adjust tolerances) + */ +static void +select_separator_tone(struct decoder_state *st) +{ + int avg; + + if (!is_valid_signal(st)) + return; + + st->sep_sum += st->freq_separator; + st->sep_samples++; + + if (st->sep_samples != 50) + return; + + avg = st->sep_sum / st->sep_samples; + + st->sep_min = avg - SEP_TOLERANCE_PULSES; + st->sep_max = avg + SEP_TOLERANCE_PULSES; + + /* reset calibration accumulators */ + st->sep_sum = 0; + st->sep_samples = 0; + + if (st->debug) + printf("separator calibrated: %dHz\n", + avg * FRAME_RATE); +} + +/* + * Verify that the observed pulse densities fall within the + * expected ranges for spkmodem tones. This prevents random noise + * from being misinterpreted as data. + */ +static int +is_valid_signal(struct decoder_state *st) +{ + if (st->freq_data <= 0) + return 0; + + if (st->freq_separator < st->sep_min || + st->freq_separator > st->sep_max) + return 0; + + return 1; +} + +/* + * Each validated frame contributes one bit of modem data. + * Bits are accumulated MSB-first into the ASCII byte. + */ +static int +set_ascii_bit(struct decoder_state *st) +{ + if (st->debug) + print_stats(st); + + if (!is_valid_signal(st)) + return st->ascii_bit; + + if (st->freq_data < st->freq_threshold) + st->ascii |= (1 << st->ascii_bit); + + st->ascii_bit--; + + return st->ascii_bit; +} + +static void +print_char(struct decoder_state *st) +{ + if (st->debug) + printf("<%c,%x>", st->ascii, st->ascii); + else + putchar(st->ascii); + + reset_char(st); +} + +static void +reset_char(struct decoder_state *st) +{ + st->ascii = 0; + st->ascii_bit = 7; +} + +/* + * Main demodulation step (moving-sum FIR filter). + */ +static void +decode_pulse(struct decoder_state *st) +{ + unsigned char old_ring, old_sep; + unsigned char new_pulse; + signed short sample; + int ringpos; + int sep_pos; + int diff_edge; + int diff_amp; + + ringpos = st->ringpos; + sep_pos = st->sep_pos; + + /* + * Sliding rectangular FIR (Finite Impulse Response) filter. + * + * After thresholding, the signal becomes a stream of 0/1 pulses. + * The decoder measures pulse density over two windows: + * + * freq_data: pulses in the "data" window + * freq_separator: pulses in the "separator" window + * + * Instead of calculating each window every time (O(N) per frame), we + * update the window sums incrementally: + * + * sum_new = sum_old - pulse_leaving + pulse_entering + * + * This keeps the filter O(1) per sample instead of O(N). + * The technique is equivalent to a rectangular FIR filter + * implemented as a sliding moving sum. + * + * The two windows are exactly SAMPLES_PER_FRAME apart in the ring + * buffer, so the pulse leaving the data window is simultaneously + * entering the separator window. + */ + old_ring = st->pulse[ringpos]; + old_sep = st->pulse[sep_pos]; + st->freq_data -= old_ring; + st->freq_data += old_sep; + st->freq_separator -= old_sep; + + sample = read_sample(st); + + /* + * Avoid startup edge. Since + * it's zero at startup, this + * may wrongly produce a pulse + */ + if (st->sample_count == 0) + st->prev_sample = sample; + + /* + * Detect edges instead of amplitude. + * This is more tolerant of weak microphones + * and speaker distortion.. + * + * However, we check both slope edges and + * amplitude, to mitagate noise. + */ + diff_amp = sample; + diff_edge = sample - st->prev_sample; + if (diff_edge < 0) + diff_edge = -diff_edge; + if (diff_amp < 0) + diff_amp = -diff_amp; + if (diff_edge > THRESHOLD && + diff_amp > THRESHOLD) + new_pulse = 1; + else + new_pulse = 0; + st->prev_sample = sample; + + st->pulse[ringpos] = new_pulse; + st->freq_separator += new_pulse; + + /* + * Advance both FIR windows through the ring buffer. + * The separator window always stays one frame ahead + * of the data window. + */ + if (++ringpos >= MAX_SAMPLES) + ringpos = 0; + if (++sep_pos >= MAX_SAMPLES) + sep_pos = 0; + + st->ringpos = ringpos; + st->sep_pos = sep_pos; + + st->sample_count++; +} + +static signed short +read_sample(struct decoder_state *st) +{ + signed short sample; + unsigned short u; + + while (st->inpos >= st->inlen) + read_words(st); + + sample = st->inbuf[st->inpos++]; + + if (st->swap_bytes) { + u = (unsigned short)sample; + u = (u >> 8) | (u << 8); + + sample = (signed short)u; + } + + return sample; +} + +static void +read_words(struct decoder_state *st) +{ + size_t n; + + n = fread(st->inbuf, sizeof(st->inbuf[0]), + READ_BUF, stdin); + + if (n != 0) { + st->inpos = 0; + st->inlen = n; + + return; + } + + if (ferror(stdin)) + err(errno, "stdin read"); + if (feof(stdin)) + exit(EXIT_SUCCESS); +} + +/* + * Automatically detect spkmodem tone + */ +static void +detect_tone(struct decoder_state *st) +{ + if (st->learn_frames >= LEARN_FRAMES) + return; + + st->learn_frames++; + + if (silent_signal(st)) + return; + + select_low_tone(st); + + if (st->learn_frames != LEARN_FRAMES) + return; + + /* + * If the observed frequencies are too close, + * learning likely failed (only one tone seen). + * Keep the default threshold. + */ + if (st->freq_max - st->freq_min < 2) + return; + + st->freq_threshold = + (st->freq_min + st->freq_max) / 2; + + if (st->debug) + printf("auto threshold: %dHz\n", + st->freq_threshold * FRAME_RATE); +} + +/* + * Ignore silence / near silence. + * Both FIR windows will be near zero when no signal exists. + */ +static int +silent_signal(struct decoder_state *st) +{ + return (st->freq_data <= 2 && + st->freq_separator <= 2); +} + +/* + * Choose the lowest active tone. + * Separator frames carry tone in the separator window, + * data frames carry tone in the data window. + */ +static void +select_low_tone(struct decoder_state *st) +{ + int f; + + f = st->freq_data; + + if (f <= 0 || (st->freq_separator > 0 && + st->freq_separator < f)) + f = st->freq_separator; + + if (f <= 0) + return; + + if (f < st->freq_min) + st->freq_min = f; + + if (f > st->freq_max) + st->freq_max = f; +} + +static void +print_stats(struct decoder_state *st) +{ + long pos; + + int data_hz = st->freq_data * FRAME_RATE; + int sep_hz = st->freq_separator * FRAME_RATE; + int sep_hz_min = st->sep_min * FRAME_RATE; + int sep_hz_max = st->sep_max * FRAME_RATE; + + if ((pos = ftell(stdin)) == -1) { + printf("%d %d %d data=%dHz sep=%dHz(min %dHz %dHz)\n", + st->freq_data, + st->freq_separator, + st->freq_threshold, + data_hz, + sep_hz, + sep_hz_min, + sep_hz_max); + return; + } + + printf("%d %d %d @%ld data=%dHz sep=%dHz(min %dHz %dHz)\n", + st->freq_data, + st->freq_separator, + st->freq_threshold, + pos - SAMPLE_OFFSET, + data_hz, + sep_hz, + sep_hz_min, + sep_hz_max); +} + +static void +err(int errval, const char *msg, ...) +{ + va_list ap; + + fprintf(stderr, "%s: ", progname()); + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + + fprintf(stderr, ": %s\n", strerror(errval)); + exit(EXIT_FAILURE); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-d]\n", progname()); + exit(EXIT_FAILURE); +} + +static const char * +progname(void) +{ + const char *p; + + if (argv0 == NULL || *argv0 == '\0') + return ""; + + p = strrchr(argv0, '/'); + + if (p) + return p + 1; + else + return argv0; +} diff --git a/util/spkmodem_recv/.gitignore b/util/spkmodem_recv/.gitignore deleted file mode 100644 index 2f5c946c..00000000 --- a/util/spkmodem_recv/.gitignore +++ /dev/null @@ -1 +0,0 @@ -spkmodem-recv diff --git a/util/spkmodem_recv/spkmodem-recv.c b/util/spkmodem_recv/spkmodem-recv.c deleted file mode 100644 index f0e8db16..00000000 --- a/util/spkmodem_recv/spkmodem-recv.c +++ /dev/null @@ -1,321 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-2.0-or-later - * Copyright (c) 2013 Free Software Foundation, Inc. - * Copyright (c) 2023, 2026 Leah Rowe <leah@libreboot.org> - * - * This program receives text encoded as pulses on the PC speaker, - * and decodes them. This is a special type of interface provided - * by coreboot and GRUB, for computers that lack serial ports. - * - * Usage example (NOTE: little endian!): - * parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-recv - * - * Originally provided by GNU GRUB, this version is a heavily - * modified fork that complies with the OpenBSD Kernel Source - * File Style Guide (KNF) instead of GNU coding standards; it - * emphasises strict error handling, portability and code - * quality, as characterised by OpenBSD projects. - * - * This fork of spkmodem-recv is provided with Libreboot releases: - * https://libreboot.org/ - */ - -#define _POSIX_SOURCE - -/* - * For OpenBSD define, to detect version - * for deciding whether to use pledge(2) - */ -#ifdef __OpenBSD__ -#include <sys/param.h> -#endif - -#include <errno.h> -#include <stdio.h> -#include <stdarg.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> - -#define SAMPLES_PER_FRAME 240 -#define MAX_SAMPLES (2 * SAMPLES_PER_FRAME) - -#define FREQ_SEP_MIN 5 -#define FREQ_SEP_MAX 15 - -#define FREQ_DATA_MIN 15 -#define FREQ_DATA_THRESHOLD 25 -#define FREQ_DATA_MAX 60 - -#define THRESHOLD 500 - -#define READ_BUF 4096 - -/* TODO: handle this at runtime instead (bswap) */ -#if defined(__BYTE_ORDER__) -#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -#error "spkmodem-recv requires little-endian samples" -#endif -#endif - -struct decoder_state { - signed short frame[MAX_SAMPLES]; - unsigned char pulse[MAX_SAMPLES]; - - signed short inbuf[READ_BUF]; - size_t inpos; - size_t inlen; - - int ringpos; - int sep_pos; - - /* - * Sliding window pulse counters - * used to detect modem tones - */ - int freq_data; - int freq_separator; - int sample_count; - - int ascii_bit; - unsigned char ascii; - - int debug; -}; - -static const char *argv0; - -static void handle_audio(struct decoder_state *st); -static int valid_signal(struct decoder_state *st); -static void decode_pulse(struct decoder_state *st); -static signed short read_sample(struct decoder_state *st); -static int set_ascii_bit(struct decoder_state *st); -static void print_char(struct decoder_state *st); -static void print_stats(struct decoder_state *st); -static void reset_char(struct decoder_state *st); - -static void err(int errval, const char *msg, ...); -static void usage(void); -static const char *progname(void); - -int getopt(int, char * const *, const char *); -extern char *optarg; -extern int optind; -extern int opterr; -extern int optopt; - -#ifndef errno -extern int errno; -#endif - -int -main(int argc, char **argv) -{ - struct decoder_state st; - int c; - -#if defined (__OpenBSD__) && defined(OpenBSD) -#if OpenBSD >= 509 - if (pledge("stdio", NULL) == -1) - err(errno, "pledge"); -#endif -#endif - - memset(&st, 0, sizeof(st)); - st.ascii_bit = 7; - - st.ringpos = 0; - st.sep_pos = SAMPLES_PER_FRAME; - - argv0 = argv[0]; - - while ((c = getopt(argc, argv, "d")) != -1) { - if (c != 'd') - usage(); - st.debug = 1; - break; - } - - setvbuf(stdout, NULL, _IONBF, 0); - - for (;;) - handle_audio(&st); - - return EXIT_SUCCESS; -} - -static void -handle_audio(struct decoder_state *st) -{ - int sample; - - if (st->sample_count > (3 * SAMPLES_PER_FRAME)) - reset_char(st); - if (!valid_signal(st)) { - decode_pulse(st); - return; - } - - if (set_ascii_bit(st) < 0) - print_char(st); - - st->sample_count = 0; - for (sample = 0; sample < SAMPLES_PER_FRAME; sample++) - decode_pulse(st); -} - -static int -valid_signal(struct decoder_state *st) -{ - return (st->freq_separator > FREQ_SEP_MIN && - st->freq_separator < FREQ_SEP_MAX && - st->freq_data > FREQ_DATA_MIN && - st->freq_data < FREQ_DATA_MAX); -} - -static void -decode_pulse(struct decoder_state *st) -{ - unsigned char old_ring, old_sep; - unsigned char new_pulse; - - old_ring = st->pulse[st->ringpos]; - old_sep = st->pulse[st->sep_pos]; - - st->freq_data -= old_ring; - st->freq_data += old_sep; - st->freq_separator -= old_sep; - - st->frame[st->ringpos] = read_sample(st); - - if ((unsigned)(st->frame[st->ringpos] + THRESHOLD) - > (unsigned)(2 * THRESHOLD)) - new_pulse = 1; - else - new_pulse = 0; - - st->pulse[st->ringpos] = new_pulse; - st->freq_separator += new_pulse; - - st->ringpos++; - if (st->ringpos >= MAX_SAMPLES) - st->ringpos = 0; - - st->sep_pos++; - if (st->sep_pos >= MAX_SAMPLES) - st->sep_pos = 0; - - st->sample_count++; -} - -static signed short -read_sample(struct decoder_state *st) -{ - size_t n; - - while (st->inpos >= st->inlen) { - - n = fread(st->inbuf, sizeof(st->inbuf[0]), - READ_BUF, stdin); - - if (n == 0) { - if (ferror(stdin)) - err(errno, "stdin read"); - if (feof(stdin)) - exit(EXIT_SUCCESS); - } - - st->inpos = 0; - st->inlen = n; - } - - return st->inbuf[st->inpos++]; -} - -static int -set_ascii_bit(struct decoder_state *st) -{ - if (st->debug) - print_stats(st); - if (st->freq_data < FREQ_DATA_THRESHOLD) - st->ascii |= (1 << st->ascii_bit); - - st->ascii_bit--; - return st->ascii_bit; -} - -static void -print_char(struct decoder_state *st) -{ - if (st->debug) - printf("<%c,%x>", st->ascii, st->ascii); - else - putchar(st->ascii); - - reset_char(st); -} - -static void -print_stats(struct decoder_state *st) -{ - long pos; - - if ((pos = ftell(stdin)) == -1) { - printf("%d %d %d\n", - st->freq_data, - st->freq_separator, - FREQ_DATA_THRESHOLD); - return; - } - - printf("%d %d %d @%ld\n", - st->freq_data, - st->freq_separator, - FREQ_DATA_THRESHOLD, - pos - sizeof(st->frame)); -} - -static void -reset_char(struct decoder_state *st) -{ - st->ascii = 0; - st->ascii_bit = 7; -} - -static void -err(int errval, const char *msg, ...) -{ - va_list ap; - - fprintf(stderr, "%s: ", progname()); - - va_start(ap, msg); - vfprintf(stderr, msg, ap); - va_end(ap); - - fprintf(stderr, ": %s\n", strerror(errval)); - exit(EXIT_FAILURE); -} - -static void -usage(void) -{ - fprintf(stderr, "usage: %s [-d]\n", progname()); - exit(EXIT_FAILURE); -} - -static const char * -progname(void) -{ - const char *p; - - if (argv0 == NULL || *argv0 == '\0') - return ""; - - p = strrchr(argv0, '/'); - - if (p) - return p + 1; - else - return argv0; -} |
