Initial commit
This commit is contained in:
commit
91a1e2e312
22 files changed
+885
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
main
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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(¬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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*.o
|
||||||
|
*.wasm
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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));
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef _MATH_H
|
||||||
|
#define _MATH_H
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define PI 3.14159265358979323846
|
||||||
|
|
||||||
|
float sintau(float x);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in new issue
Block a user