Initial commit

This commit is contained in:
thib8956 committed 2026-10-27 22:10:43 +01:00
commit 91a1e2e312
22 files changed
+885

No files matched your search

+1
View File
@@ -0,0 +1 @@
main
+15
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
+34
View File
@@ -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
+49
View File
@@ -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()
+53
View File
@@ -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()
+73
View File
@@ -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)
+68
View File
@@ -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("<f", sample) for sample in samples)
def main():
with AplaySink(SAMPLE_RATE) as sink:
data = mix(
sin(440, 0.05, 1),
sin(440*2**(4/12), 0.05, 1),
sin(440*2**(7/12), 0.05, 1))
packed = pack(data)
sink.submit(packed)
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
import os
import sys
from math import pi, sin, tanh
from keyboard import setup_term
from pyalsa import Alsa
SAMPLE_RATE = 44_100
LATENCY = 50_000 # 50ms
INT16_MAX = 2**15 - 1
alsa = Alsa(SAMPLE_RATE, LATENCY)
keys = "azertyuiop"
gain = 0.5
base_freq = 440.0
freqs = [0.0] * len(keys)
phases = [0.0] * len(keys)
mix_mode = "voice_count"
buf_size = 512
def tanh_mix(voices, gain):
sample = tanh(gain * sum(voices)) # apply tanh with gain (soft clipping)
return sample
def voice_count_mix(voices, gain):
sample = sum(voices) / len(voices) # normalize to [0, 1] range
sample = sample * gain
return sample
def clamp(x, a, b):
return max(a, min(b, x))
os.set_blocking(sys.stdin.fileno(), False)
with setup_term():
while True:
buf = [0] * buf_size
for i in range(buf_size):
voices = [0.0] * len(keys)
for j in range(len(keys)):
phase, freq = phases[j], freqs[j]
if freq == 0:
continue
voices[j] = sin(phase)
phases[j] += 2 * pi * freq / SAMPLE_RATE
if voices == []:
continue
# mixing
mixfn = tanh_mix if mix_mode == "tanh" else voice_count_mix
mix = int(mixfn(voices, gain) * INT16_MAX)
buf[i] = clamp(
mix, -INT16_MAX - 1, INT16_MAX
) # final clamp to int16 range, just in case
alsa.write(buf) # this is blocking
# input handling
key = sys.stdin.read(1)
if key == "q":
break
elif key == " ":
mix_mode = "tanh" if mix_mode == "voice_count" else "voice_count"
print(mix_mode)
elif key is not None and key != "" and key in keys:
i = keys.index(key)
if freqs[i] == 0:
freqs[i] = base_freq * (2 ** (i / 12))
else:
freqs[i] = 0
+99
View File
@@ -0,0 +1,99 @@
import importlib
from tkinter import Frame, Tk
import synth
from pyalsa import Alsa
SAMPLE_RATE = 44_100
LATENCY = 50_000 # 50ms
INT16_MAX = 2**15 - 1
alsa = Alsa(SAMPLE_RATE, LATENCY)
keys = "qzsedftgyhujk"
base_freq = 261.6256 # C4, piano middle C
keys_map = [0] * len(keys)
def keydown(e):
if e.keysym in keys:
i = keys.index(e.keysym)
keys_map[i] = 1
def keyup(e):
if e.keysym in keys:
i = keys.index(e.keysym)
keys_map[i] = 0
def key_to_freq(i):
if 0 <= i < len(keys):
return base_freq * (2 ** (i / 12))
return 0
root = Tk()
# note parameters: freq, volume
# note state: phase, volume
class Note:
def __init__(self):
self.freq = 0
self.vol = 0
self.phase = 0
self.gain = 0
self.playing = False
self.waveform = "sine"
self.attack = 0.01
self.release = 0.05
self.waveshaping = False
self.lowpass = False
self.lp = 0
def reload_synth(event):
importlib.reload(synth)
print("reloaded synth module")
def update(root, note):
pressed_key = keys_map.index(1) if 1 in keys_map else -1
note_freq = key_to_freq(pressed_key)
if note_freq != note.freq:
note.freq = note_freq
if not note.playing and note_freq != 0:
note.playing = True
if note.playing and note_freq == 0:
note.playing = False
buf = synth.synth(note)
alsa.write(buf) # send sound to alsa
root.after(0, update, root, note) # schedule next update
def toggle_note_waveform(e):
waves = ["sine", "saw", "square", "triangle"]
current = waves.index(note.waveform)
note.waveform = waves[(current + 1) % len(waves)]
print("waveform:", note.waveform)
def toggle_note_waveshaping(e):
note.waveshaping = not note.waveshaping
print("waveshaping:", note.waveshaping)
def toggle_note_lowpass(e):
note.lowpass = not note.lowpass
print("lowpass:", note.lowpass)
note = Note()
frame = Frame(root, width=100, height=100)
frame.bind("<KeyPress>", keydown)
frame.bind("<KeyRelease>", keyup)
root.bind_all("<Control-r>", 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()
+36
View File
@@ -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
+166
View File
@@ -0,0 +1,166 @@
#include <alsa/asoundlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <math.h>
#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(&note, 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;
}
+34
View File
@@ -0,0 +1,34 @@
#include "synth.h"
#include <math.h>
#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);
}
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef NOTE_H
#define NOTE_H
#include <stdbool.h>
#include <stdint.h>
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
+2
View File
@@ -0,0 +1,2 @@
*.o
*.wasm
+16
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
#include <stdint.h>
#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);
}
}
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #000000;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>
</head>
<body>
</body>
<script src="index.js" type="module"></script>
</html>
+23
View File
@@ -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));
+59
View File
@@ -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);
}
+9
View File
@@ -0,0 +1,9 @@
#ifndef _MATH_H
#define _MATH_H
#include <stdint.h>
#define PI 3.14159265358979323846
float sintau(float x);
#endif
+8
View File
@@ -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