74 lines
1.9 KiB
Python
74 lines
1.9 KiB
Python
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
|