From 91a1e2e312749c7ea5748269b27606d293ee0bde Mon Sep 17 00:00:00 2001 From: Thibaud Date: Tue, 27 Oct 2026 22:10:43 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + Makefile | 15 +++ audio_python/.gitignore | 2 + audio_python/keyboard.py | 34 +++++++ audio_python/keyboard_raylib.py | 49 ++++++++++ audio_python/player.py | 53 ++++++++++ audio_python/pyalsa.py | 73 ++++++++++++++ audio_python/sound.py | 68 +++++++++++++ audio_python/sound_alsa.py | 73 ++++++++++++++ audio_python/sound_tkinter.py | 99 +++++++++++++++++++ audio_python/synth.py | 36 +++++++ main.c | 166 ++++++++++++++++++++++++++++++++ synth.c | 34 +++++++ synth.h | 29 ++++++ test_audio_wasm/.gitignore | 2 + test_audio_wasm/Makefile | 16 +++ test_audio_wasm/beep.c | 13 +++ test_audio_wasm/index.html | 23 +++++ test_audio_wasm/index.js | 23 +++++ test_audio_wasm/math.c | 59 ++++++++++++ test_audio_wasm/math.h | 9 ++ todo.txt | 8 ++ 22 files changed, 885 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 audio_python/.gitignore create mode 100644 audio_python/keyboard.py create mode 100644 audio_python/keyboard_raylib.py create mode 100644 audio_python/player.py create mode 100644 audio_python/pyalsa.py create mode 100644 audio_python/sound.py create mode 100644 audio_python/sound_alsa.py create mode 100644 audio_python/sound_tkinter.py create mode 100644 audio_python/synth.py create mode 100644 main.c create mode 100644 synth.c create mode 100644 synth.h create mode 100644 test_audio_wasm/.gitignore create mode 100644 test_audio_wasm/Makefile create mode 100644 test_audio_wasm/beep.c create mode 100644 test_audio_wasm/index.html create mode 100644 test_audio_wasm/index.js create mode 100644 test_audio_wasm/math.c create mode 100644 test_audio_wasm/math.h create mode 100644 todo.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba2906d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +main diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f93feac --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +CC = clang +CFLAGS = -g3 -Wall -Wextra -pedantic -Wconversion -Wdouble-promotion \ + -Wno-unused-parameter -Wno-unused-function -Wno-sign-conversion \ + -fsanitize=undefined -fsanitize-trap -fanalyzer + +main: main.c synth.c + $(CC) $(CFLAGS) -o main main.c synth.c -lasound -lm + +.PHONY: clean run + +run: main + ./main 24:0 + +clean: + rm -f main diff --git a/audio_python/.gitignore b/audio_python/.gitignore new file mode 100644 index 0000000..372c13e --- /dev/null +++ b/audio_python/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ + diff --git a/audio_python/keyboard.py b/audio_python/keyboard.py new file mode 100644 index 0000000..a689acf --- /dev/null +++ b/audio_python/keyboard.py @@ -0,0 +1,34 @@ +import fcntl +import os +import select +import sys +import termios +from contextlib import contextmanager + + +@contextmanager +def setup_term(*args, **kwargs): + fd = sys.stdin.fileno() + oldterm = termios.tcgetattr(fd) + newattr = termios.tcgetattr(fd) + newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, newattr) + oldflags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK) + try: + yield + finally: + # restore terminal settings + termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm) + fcntl.fcntl(fd, fcntl.F_SETFL, oldflags) + + +def getch(timeout): + # sys.stdin.read is blocking, use select instead + # NOTE: this will not work on windows as select only accepts sockets, not all file descriptors + # TODO: use python selectors module? + o, _, _ = select.select([sys.stdin], [], [], timeout) + if o != []: + assert len(o) == 1 + return o[0].read(1) + return None diff --git a/audio_python/keyboard_raylib.py b/audio_python/keyboard_raylib.py new file mode 100644 index 0000000..782880a --- /dev/null +++ b/audio_python/keyboard_raylib.py @@ -0,0 +1,49 @@ +import math +import pyray as ray + +SAMPLE_RATE = 44100 +BUFFER_SIZE = 2048 + +progress = 0.0 # progress within a cycle of a periodic function (0 to 1) + + +def tick(freq): + global progress + progress = (progress + freq / SAMPLE_RATE) % 1.0 + return progress + + +def sine(p): + return math.sin(2 * math.pi * p) + + +def square(p): + return 1.0 if p < 0.5 else -1.0 + + +def saw(p): + return 2.0 * p - 1.0 + + +def tri(p): + return 4 * abs(p - 0.5) - 1.0 + + +ray.init_window(800, 450, "Hello") +ray.init_audio_device() +ray.set_target_fps(60) +ray.set_audio_stream_buffer_size_default(BUFFER_SIZE) +stream = ray.load_audio_stream(SAMPLE_RATE, 32, 1) +ray.play_audio_stream(stream) +buffer = ray.ffi.new("float[]", BUFFER_SIZE) +while not ray.window_should_close(): + if ray.is_key_down(ray.KeyboardKey.KEY_SPACE) and ray.is_audio_stream_processed(stream): + for i in range(BUFFER_SIZE): + buffer[i] = 0.1 * sine(tick(440)) + ray.update_audio_stream(stream, buffer, BUFFER_SIZE) + ray.begin_drawing() + ray.clear_background((0x0B, 0x0B, 0x0B, 0xFF)) + ray.draw_text("Hello world", 190, 200, 20, ray.VIOLET) + ray.end_drawing() +ray.close_window() +ray.close_audio_device() diff --git a/audio_python/player.py b/audio_python/player.py new file mode 100644 index 0000000..2ffe294 --- /dev/null +++ b/audio_python/player.py @@ -0,0 +1,53 @@ +from queue import Empty, Queue +from shlex import split +from subprocess import PIPE, Popen +from sys import stderr +from threading import Event, Thread + +# FFPLAY_CMD = "ffplay -f f32le -ar 44100 -i -" + + +class AplaySink: + def __init__(self, sample_rate: int) -> None: + self.cmd = f"aplay -t raw -f FLOAT_LE -r {sample_rate}" + self.process = Popen(split(self.cmd), stdin=PIPE) + self.queue = Queue() + self.stop_event = Event() + self.feeder_thred = Thread( + target=self._consume, name="aplay-feeder", daemon=False + ) + print(f"Starting {self.feeder_thred.name} thread...", file=stderr) + self.feeder_thred.start() + + def submit(self, samples: bytes): + self.queue.put(samples) + + def close(self): + self.stop_event.set() + self.feeder_thred.join() + if self.process.stdin is not None: + self.process.stdin.close() + self.process.wait() + print(f"Stopped {self.feeder_thred.name} thread...", file=stderr) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def _consume(self, timeout=0.1): + while not self.stop_event.is_set(): + samples = [] + try: + samples = self.queue.get(timeout=timeout) + except Empty: + pass + if not samples: + continue + if self.process.poll() is not None: + raise RuntimeError("aplay process has terminated") + if self.process.stdin is None: + raise RuntimeError("stdin is None") + self.process.stdin.write(samples) + self.process.stdin.flush() diff --git a/audio_python/pyalsa.py b/audio_python/pyalsa.py new file mode 100644 index 0000000..ff79c21 --- /dev/null +++ b/audio_python/pyalsa.py @@ -0,0 +1,73 @@ +import ctypes + +""" +Quick and dirty wrapper for ALSA. +""" + +c_void_pp = ctypes.POINTER(ctypes.c_void_p) +_alsa = ctypes.cdll.LoadLibrary("libasound.so.2") + +_alsa.snd_pcm_open.restype = ctypes.c_int +_alsa.snd_pcm_open.argtypes = [c_void_pp, ctypes.c_char_p, ctypes.c_int, ctypes.c_int] + +_alsa.snd_pcm_set_params.restype = ctypes.c_int +_alsa.snd_pcm_set_params.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_uint, ctypes.c_uint, ctypes.c_int, ctypes.c_uint] +SND_PCM_FORMAT_S16_LE = 2 +SND_PCM_ACCESS_RW_INTERLEAVED = 3 + +# snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size); +_alsa.snd_pcm_writei.restype = ctypes.c_long +_alsa.snd_pcm_writei.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong] + +# int snd_pcm_drain(snd_pcm_t *pcm); +_alsa.snd_pcm_drain.restype = ctypes.c_int +_alsa.snd_pcm_drain.argtypes = [ctypes.c_void_p] + + +class Alsa: + def __init__(self, sample_rate, latency): + self.sample_rate = sample_rate + self.handle = ctypes.c_void_p() + ret = _alsa.snd_pcm_open(ctypes.byref(self.handle), b"default", 0, 0) + assert ret >= 0, "snd_pcm_open failed" + ret = _alsa.snd_pcm_set_params(self.handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, 1, sample_rate, 1, latency) + + def write(self, samples): + BufferType = ctypes.c_int16 * len(samples) + buf = BufferType(*samples) + ret = _alsa.snd_pcm_writei(self.handle, buf, len(samples)) + assert ret == len(samples), "snd_pcm_writei failed" + return ret + + def drain(self): + ret = _alsa.snd_pcm_drain(self.handle) + assert ret >= 0 + + +#_alsa.snd_seq_open.argtypes = [c_void_pp, ctypes.c_char_p, ctypes.c_int, ctypes.c_int] + +class AlsaSeq: + def __init__(self): + self.handle = ctypes.c_void_p() + ret = _alsa.snd_seq_open(ctypes.byref(self.handle), b"default", 2, 0) + if ret < 0: + raise RuntimeError("snd_seq_open failed") + ret = _alsa.snd_seq_set_client_name(self.handle, b"Midi Listener"); + assert ret >= 0, "snd_seq_set_client_name failed" + self.port = _alsa.snd_seq_create_simple_port(self.handle, b"listen:in", 2 | 64, 1 << 20) + assert self.port >= 0, "snd_seq_create_simple_port failed" + print(self.handle, self.port) + err = _alsa.snd_seq_connect_from(self.handle, self.port, 20, 0) + assert err >= 0, "snd_seq_connect_from failed" + + def get_event(self): + evt = ctypes.c_void_p() + err = _alsa.snd_seq_event_input(self.handle, ctypes.byref(evt)) + assert err >= 0, "snd_seq_event_input failed" + return evt + +if __name__ == "__main__": + seq = AlsaSeq() + while True: + evt = seq.get_event() + print(evt) diff --git a/audio_python/sound.py b/audio_python/sound.py new file mode 100644 index 0000000..5e792d3 --- /dev/null +++ b/audio_python/sound.py @@ -0,0 +1,68 @@ +import math +import struct +from itertools import chain +from operator import mul +from random import random + +from player import AplaySink + +SAMPLE_RATE = 44100 + + +def sin(f, amplitude, duration): + samples = (amplitude*math.sin(2*math.pi*f*(t/SAMPLE_RATE)) for t in range(int(duration*SAMPLE_RATE))) + return samples + + +def square(f, amplitude, duration): + period = SAMPLE_RATE / f + half_period = period / 2 + for t in range(int(duration*SAMPLE_RATE)): + if t % period < half_period: + yield amplitude + else: + yield -amplitude + + +def noise(amplitude, duration): + return (amplitude*(2*random()-1) for _ in range(int(duration*SAMPLE_RATE))) + + +def envelope(samples, attack_duration, release_duration): + # TODO: rewrite ADSR envelope + def compose(a, b): + return map(lambda x: mul(*x), zip(a, b)) + + a = int(attack_duration * SAMPLE_RATE) + r = int(release_duration * SAMPLE_RATE) + samples = list(samples) + attack = (x/a for x in range(0, a)) + sustain = len(samples) - a - r + release = (x/r for x in range(r, -1, -1)) + + return chain( + compose(samples[:a], attack), + samples[a:a+sustain], + compose(samples[a+sustain:], release)) + + +def mix(*sources): + return (sum(x) for x in zip(*sources)) + + +def pack(samples) -> bytes: + return b"".join(struct.pack("", keydown) +frame.bind("", keyup) +root.bind_all("", reload_synth) +root.bind_all("w", toggle_note_waveform) +root.bind_all("x", toggle_note_waveshaping) +root.bind_all("c", toggle_note_lowpass) +frame.pack() +frame.focus_set() +root.after(100, update, root, note) +root.mainloop() diff --git a/audio_python/synth.py b/audio_python/synth.py new file mode 100644 index 0000000..5252f04 --- /dev/null +++ b/audio_python/synth.py @@ -0,0 +1,36 @@ +from math import pi, sin + +SAMPLE_RATE = 44_100 +INT16_MAX = 2**15 - 1 + +def synth(note): + buf = [0] * 512 + for i, _ in enumerate(buf): + note.phase = (note.phase + note.freq / SAMPLE_RATE) % 1.0 + # oscillator + match note.waveform: + case "saw": sample = 2 * note.phase - 1.0 + case "square": sample = 1.0 if note.phase < 0.5 else -1.0 + case "sine": sample = sin(2 * pi * note.phase) + case "triangle": sample = 4 * abs(note.phase - 0.5) - 1.0 + case _: sample = 0.0 + # envelope + target = 1 if note.playing else 0 + if target == 1: + note.gain = min(note.gain + 1 / (0.010 * SAMPLE_RATE), 0.2) # attack + else: + note.gain = max(note.gain - 1 / (0.050 * SAMPLE_RATE), 0) # release + + # signal processing + if note.waveshaping: + drive = 2 + sample *= abs(sample) * drive + #sample = tanh(drive * sample) + if note.lowpass: + note.lp += 0.05 * (sample - note.lp) + sample = note.lp * note.gain + else: + sample = sample * note.gain + # convert to 16-bit signed integer and feed to buffer + buf[i] = int(sample * INT16_MAX) + return buf diff --git a/main.c b/main.c new file mode 100644 index 0000000..f66da66 --- /dev/null +++ b/main.c @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include "synth.h" + +#define ALSA_CHK(stmt, msg) do { \ + int alsa_chk_err = (stmt); \ + if (alsa_chk_err < 0) { \ + fprintf(stderr, "%s%s\n", msg, snd_strerror(alsa_chk_err)); \ + return 1; \ + } \ +} while (0) + +#define N_CHANNELS 1 +#define SAMPLE_RATE 44100 +#define PLAYBACK_LATENCY 50000 // 50 ms +#define PLAYBACK_BUFFER_SIZE 512 + +static int write_audio(snd_pcm_t *handle, const int16_t *buffer) { + snd_pcm_uframes_t offset = 0; + + while (offset < PLAYBACK_BUFFER_SIZE) { + snd_pcm_sframes_t written = snd_pcm_writei( + handle, buffer + offset * N_CHANNELS, + PLAYBACK_BUFFER_SIZE - offset); + + if (written < 0) { + int err = snd_pcm_recover(handle, (int)written, 1); + ALSA_CHK(err, "Could not write to alsa device: "); + continue; + } + + if (written == 0) { + int err = snd_pcm_wait(handle, -1); + if (err < 0) { + err = snd_pcm_recover(handle, err, 1); + ALSA_CHK(err, "Could not write to alsa device: "); + } + continue; + } + + offset += (snd_pcm_uframes_t)written; + } + + return 0; +} + +static int midi_open(snd_seq_t **seq_handle, int src_client, int src_port) { + int err = snd_seq_open(seq_handle, "default", SND_SEQ_OPEN_INPUT, SND_SEQ_NONBLOCK); + ALSA_CHK(err, "Error opening ALSA sequencer: "); + + err = snd_seq_set_client_name(*seq_handle, "Midi Listener"); + ALSA_CHK(err, "Error setting sequencer client name: "); + + uint32_t caps = SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE; + int in_port = snd_seq_create_simple_port(*seq_handle, "listen:in", caps, SND_SEQ_PORT_TYPE_APPLICATION); + ALSA_CHK(in_port, "Error creating sequencer port: "); + + err = snd_seq_connect_from(*seq_handle, in_port, src_client, src_port); + ALSA_CHK(err, "Error connecting MIDI source: "); + return 0; +} + +static float midi_note_to_freq(int midi) { + int note_offset = midi - 69; + return 440.0f * exp2f((float) note_offset / 12.0f); +} + +static Waveform cycle_waveform(Waveform form) { + Waveform new_form = form + 1; + if (new_form > WAVE_TRIANGLE) { + new_form = WAVE_SINE; + } + const char *waveform_names[] = {"NONE", "SINE", "SQUARE", "SAW", "TRIANGLE"}; + printf("Waveform changed to: %s\n", waveform_names[new_form]); + return new_form; +} + +static int parse_port(int *src_client, int *src_port, const char *str) { + return sscanf(str, "%d:%d", src_client, src_port) == 2; +} + +int main(int argc, const char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s 20:0\n", argv[0]); + return 1; + } + + int src_client = 0; + int src_port = 0; + if (!parse_port(&src_client, &src_port, argv[1])) { + fprintf(stderr, "Unable to parse midi port %s\n", argv[1]); + return 1; + } + int err = 0; + snd_pcm_t *handle = 0; + + // Init alsa audio + err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0); + ALSA_CHK(err, "Could not open alsa device: "); + err = snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, N_CHANNELS, SAMPLE_RATE, 1, PLAYBACK_LATENCY); + ALSA_CHK(err, "Could not set alsa params: "); + + // Init alsa midi + snd_seq_t *seq_handle = 0; + err = midi_open(&seq_handle, src_client, src_port); + if (err) { + printf("Error initializing MIDI: %d\n", err); + return err; + } + + // Main loop + Note note = { + .form = WAVE_SAW, + .attack_ms = 100.0f, + .release_ms = 50.0f + }; + int16_t buf[PLAYBACK_BUFFER_SIZE] = {0}; + for (;;) { + // Drain queued MIDI events + for (;;) { + snd_seq_event_t *ev = NULL; + int ret = snd_seq_event_input(seq_handle, &ev); + if (ret == -EAGAIN) { + break; + } + ALSA_CHK(ret, "Failed to read midi event: "); + if (!ev) { + continue; + } + + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: { + note.is_playing = ev->data.note.velocity != 0; + note.freq = midi_note_to_freq(ev->data.note.note); + } break; + case SND_SEQ_EVENT_NOTEOFF: { + note.is_playing = false; + } break; + case SND_SEQ_EVENT_CONTROLLER: { + note.filter_cutoff = (float) ev->data.control.value / 64.0f; + printf("filter cutoff: %f\n", note.filter_cutoff); + } break; + case SND_SEQ_EVENT_PGMCHANGE: { + note.form = cycle_waveform(note.form); + } break; + default: + printf("%d\n", ev->type); + break; + } + snd_seq_free_event(ev); + } + + play_note(¬e, buf, PLAYBACK_BUFFER_SIZE); + + if (write_audio(handle, buf) < 0) { + return 1; + } + } + + snd_pcm_drain(handle); // wait for playback to finish (not stricty needed depending on requested latency) + snd_pcm_close(handle); + return 0; +} diff --git a/synth.c b/synth.c new file mode 100644 index 0000000..0cce8b6 --- /dev/null +++ b/synth.c @@ -0,0 +1,34 @@ +#include "synth.h" +#include +#define SAMPLE_RATE 44100.0f + +void play_note(Note *note, int16_t *buffer, int buffer_size) { + for (int i = 0; i < buffer_size; i++) { + // update phase counter + note->phase = fmodf(note->phase + note->freq / SAMPLE_RATE, 1.0f); + + // oscillator + float sample = 0.0f; + switch (note->form) { + case WAVE_SINE: sample = sinf(2.0f * (float) M_PI * note->phase); break; + case WAVE_SAW: sample = 2.0f * note->phase - 1.0f; break; + case WAVE_SQUARE: sample = note->phase < 0.5f ? 1.0f : -1.0f; break; + case WAVE_TRIANGLE: sample = 4.0f * fabsf(note->phase - 0.5f) - 1.0f; break; + default: break; + } + + //note->lp += note->filter_cutoff * (sample - note->lp); + //sample = note->lp; + + // basic attack / release + // convert attack / release time to a number of steps to reach target gain + float attack_step = 0.2f / ((note->attack_ms / 1000.0f) * SAMPLE_RATE); + float release_step = 0.2f / ((note->release_ms / 1000.0f) * SAMPLE_RATE); + note->gain = note->is_playing + ? fminf(note->gain + attack_step, 0.2f) // attack + : fmaxf(note->gain - release_step, 0); // release + + // convert sample to 16-bit signed integer and apply gain + buffer[i] = (int16_t) (note->gain * sample * (float) INT16_MAX); + } +} diff --git a/synth.h b/synth.h new file mode 100644 index 0000000..3f10fdd --- /dev/null +++ b/synth.h @@ -0,0 +1,29 @@ +#ifndef NOTE_H +#define NOTE_H + +#include +#include + +typedef enum { + WAVE_NONE, + WAVE_SINE, + WAVE_SQUARE, + WAVE_SAW, + WAVE_TRIANGLE +} Waveform; + +typedef struct { + float phase; + float freq; + float gain; + float attack_ms; + float release_ms; + float filter_cutoff; + float lp; + bool is_playing; + Waveform form; +} Note; + +void play_note(Note *note, int16_t *buffer, int buffer_size); + +#endif // NOTE_H diff --git a/test_audio_wasm/.gitignore b/test_audio_wasm/.gitignore new file mode 100644 index 0000000..6afa20b --- /dev/null +++ b/test_audio_wasm/.gitignore @@ -0,0 +1,2 @@ +*.o +*.wasm diff --git a/test_audio_wasm/Makefile b/test_audio_wasm/Makefile new file mode 100644 index 0000000..36bc309 --- /dev/null +++ b/test_audio_wasm/Makefile @@ -0,0 +1,16 @@ +CFLAGS = -Wall -Wextra -pedantic -std=c23 \ + -Wconversion -Wdouble-promotion \ + -Wno-unused-parameter -Wno-unused-function -Wno-sign-conversion \ + -fsanitize=undefined \ + --target=wasm32 -O1 -flto -ffreestanding -fno-builtin -nostdlib + +math.o: math.c + clang $(CFLAGS) -c math.c -o math.o + +beep.wasm: beep.c math.o + clang $(CFLAGS) \ + -Wl,--no-entry \ + -Wl,--export=beep \ + -Wl,--export=get_samples \ + beep.c math.o \ + -o beep.wasm diff --git a/test_audio_wasm/beep.c b/test_audio_wasm/beep.c new file mode 100644 index 0000000..ed2cf48 --- /dev/null +++ b/test_audio_wasm/beep.c @@ -0,0 +1,13 @@ +#include +#include "math.h" +#define SAMPLE_COUNT 44100 + +static float SAMPLES[SAMPLE_COUNT]; + +float* get_samples() { return SAMPLES; } + +void beep(uint32_t sample_rate, float freq) { + for (int i = 0; i < SAMPLE_COUNT; ++i) { + SAMPLES[i] = sintau((float)i * freq / (float)sample_rate); + } +} diff --git a/test_audio_wasm/index.html b/test_audio_wasm/index.html new file mode 100644 index 0000000..055f557 --- /dev/null +++ b/test_audio_wasm/index.html @@ -0,0 +1,23 @@ + + + + + + Test + + + + + + diff --git a/test_audio_wasm/index.js b/test_audio_wasm/index.js new file mode 100644 index 0000000..6b5c3aa --- /dev/null +++ b/test_audio_wasm/index.js @@ -0,0 +1,23 @@ +const { instance } = await WebAssembly.instantiateStreaming(fetch("beep.wasm")); + +let ctx = undefined; + +const count = 44100; +const ptr = instance.exports.get_samples(); + +async function playTone(frequency = 440) { + ctx ??= new AudioContext(); + await ctx.resume(); + + instance.exports.beep(ctx.sampleRate, frequency); + const audioBuffer = ctx.createBuffer(1, count, ctx.sampleRate); + const audioChannel = audioBuffer.getChannelData(0); + audioChannel.set(new Float32Array(instance.exports.memory.buffer, ptr, count)); + + const src = ctx.createBufferSource(); + src.buffer = audioBuffer; + src.connect(ctx.destination); + src.start(); +} + +window.addEventListener("click", () => playTone(440)); diff --git a/test_audio_wasm/math.c b/test_audio_wasm/math.c new file mode 100644 index 0000000..9dba30e --- /dev/null +++ b/test_audio_wasm/math.c @@ -0,0 +1,59 @@ +#include "math.h" + +__attribute__((always_inline)) +static float fminf(float a, float b) { + return a < b ? a : b; +} + +/* Approach 1: polynomial; input in radians. */ +static float sin_poly_radians(float x) +{ + x *= 0.159155f; /* approximately 1 / (2*pi) */ + x -= __builtin_floorf(x); /* reduce to [0, 1) */ + float xx = x * x; + float y = -6.87897f; + y = y * xx + 33.7755f; + y = y * xx - 72.5257f; + y = y * xx + 80.5874f; + y = y * xx - 41.2408f; + y = y * xx + 6.28077f; + return x * y; +} + +// Approach 2: rational approximation; input in turns (1.0 = 2*pi radians) +// Calculate sin(tau x) (where x is in the range [0, 1]) using Bhaskara I's approximation +// https://en.wikipedia.org/wiki/Bhaskara_I%27s_sine_approximation_formula +// Accurate to about 0.2% +static float sin_rational_tau(float x) +{ + float frac = x - __builtin_floorf(x); + float sign = 0.5f - frac; + float y = 2.0f * fminf(frac, 1.0f - frac); + float q = y * (1.0f - y); + float approximation = (16.0f * q) / (5.0f - 4.0f * q); + return __builtin_copysignf(approximation, sign); +} + +/* Approach 3: folded polynomial; input in turns (1.0 = 2*pi radians). */ +static float sin_folded_poly_tau(float x) +{ + union { uint32_t i; float f; } coeffs[6] = { + { 0x4261c73fu }, { 0x4113e0d9u }, { 0xc22aaa4bu }, + { 0x3da6b273u }, { 0x40c90140u }, { 0x36d0957eu } + }; + + float frac = x - __builtin_floorf(x); + float x1 = __builtin_fabsf(frac - 0.5f); + float t = fminf(0.5f - x1, x1); /* fold to [0, 0.25] */ + + float y = 0.0f; + for (int i = 0; i < 6; ++i) { + y = y * t + coeffs[i].f; + } + + return __builtin_copysignf(y, 0.5f - frac); +} + +float sintau(float x) { + return sin_rational_tau(x); +} diff --git a/test_audio_wasm/math.h b/test_audio_wasm/math.h new file mode 100644 index 0000000..64dbf52 --- /dev/null +++ b/test_audio_wasm/math.h @@ -0,0 +1,9 @@ +#ifndef _MATH_H +#define _MATH_H +#include + +#define PI 3.14159265358979323846 + +float sintau(float x); + +#endif diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..a06c7ce --- /dev/null +++ b/todo.txt @@ -0,0 +1,8 @@ +- attack & release shaping (exp. decay; smoothstep attack; tanh?) +- ADSR envelope generator +- note quantization / sequencer +- vibrato / tremolo / saturation +- FM synth +- filters +- DSP +- wavetables