Skip to content

Instantly share code, notes, and snippets.

@DJStompZone
Last active August 24, 2025 03:16
Show Gist options
  • Save DJStompZone/c0347b251e74a7753710d9f14cca14ae to your computer and use it in GitHub Desktop.
Save DJStompZone/c0347b251e74a7753710d9f14cca14ae to your computer and use it in GitHub Desktop.
SnekSynth
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SnekSynth
A seemingly simple, self-sufficient serpentine sonic synthesizer script.
Voicing:
• Bass: sine oscillator with a slightly longer ADSR
• Stabs: detuned, band-limited saw pair with a snappier ADSR
Output:
demo.wav
"""
import math
import wave
import numpy as np
SAMPLE_RATE = 44100
BPM = 160
BEATS_PER_BAR = 3
NOTE_DIVISION = 2 # eighth-note grid
SECONDS_PER_BEAT = 60.0 / BPM
SECONDS_PER_STEP = SECONDS_PER_BEAT / NOTE_DIVISION
OUT_PATH = "demo.wav"
NATS = "CDEFGAB";
MASK = f"{110:07b}"
STEPS = zip(NATS,MASK)
TONES = dict(zip([a for n,b in STEPS for a in (n,(n+"#")*int(b)) if a], range(12)))
TONES.update({f"{nx}B": TONES[f"{n}#"] for n,nx,b in zip(NATS,NATS[1:]+NATS[:1],MASK) if b=="1"})
TONES = dict(sorted(list(TONES.items())))
def note_to_freq(name: str) -> float:
"""
Convert conventional pitch notation (C0=0 indexing) to frequency in Hz.
Equal temperament with A4 = 440 Hz.
"""
a4_index = 9 + 12 * 4 # index 57
name = name.strip().upper()
if len(name) < 2:
raise ValueError(f"Bad note name: {name}")
if name[1] in ["#", "B"]:
key = name[:2]
octave = int(name[2:])
else:
key = name[0]
octave = int(name[1:])
if key not in TONES:
raise ValueError(f"Bad note key: {key}")
idx = TONES[key] + 12 * octave
semitones_from_a4 = idx - a4_index
return 440.0 * (2.0 ** (semitones_from_a4 / 12.0))
def adsr_env(length_samples: int, sr: int, a: float = 0.005, d: float = 0.08,
s: float = 0.45, r: float = 0.06) -> np.ndarray:
"""
Generate a linear ADSR envelope of a given sample length.
Args:
length_samples: Total envelope length in samples.
sr: Sample rate.
a: Attack time in seconds.
d: Decay time in seconds.
s: Sustain level (0..1).
r: Release time in seconds.
Returns:
Envelope array shaped to length_samples.
"""
attack = max(1, int(a * sr))
decay = max(1, int(d * sr))
release = max(1, int(r * sr))
sustain_len = max(0, length_samples - (attack + decay + release))
env = np.zeros(length_samples, dtype=np.float64)
env[:attack] = np.linspace(0.0, 1.0, attack, endpoint=False)
env[attack:attack + decay] = np.linspace(1.0, s, decay, endpoint=False)
env[attack + decay:attack + decay + sustain_len] = s
env[attack + decay + sustain_len:] = np.linspace(s, 0.0, release, endpoint=False)
return env
def sine_osc(freq: float, length_s: float, sr: int) -> np.ndarray:
"""
Generate a pure sine oscillator.
Args:
freq: Frequency in Hz.
length_s: Duration in seconds.
sr: Sample rate.
Returns:
Numpy array containing the oscillator signal.
"""
n = int(length_s * sr)
t = np.linspace(0.0, length_s, n, endpoint=False)
return np.sin(2.0 * math.pi * freq * t)
def detuned_saws(freq: float, length_s: float, sr: int, detune_cents: float = 7.0) -> np.ndarray:
"""
Generate two detuned band-limited sawtooth waves and mix them.
Args:
freq: Base frequency in Hz.
length_s: Duration in seconds.
sr: Sample rate.
detune_cents: Detune amount for the pair in cents.
Returns:
Normalized mixed waveform of two detuned saws.
"""
n = int(length_s * sr)
t = np.linspace(0.0, length_s, n, endpoint=False)
cents = detune_cents / 1200.0
f1 = freq * (2.0 ** cents)
f2 = freq * (2.0 ** -cents)
def bandlimited_saw(f: float) -> np.ndarray:
partials = 12
wave = np.zeros_like(t)
for k in range(1, partials + 1):
wave += (1.0 / k) * np.sin(2.0 * math.pi * k * f * t)
peak = np.max(np.abs(wave))
return wave / peak if peak > 0.0 else wave
w1 = bandlimited_saw(f1)
w2 = bandlimited_saw(f2)
mix = 0.5 * (w1 + w2)
peak = np.max(np.abs(mix))
return mix / peak if peak > 0.0 else mix
def place_note(buffer: np.ndarray, start_idx: int, length_samples: int,
signal: np.ndarray, env: np.ndarray, gain: float = 1.0) -> None:
"""
Mix a note into the main buffer with envelope and gain. Safely clamps at buffer end.
Args:
buffer: Destination audio buffer.
start_idx: Start sample index.
length_samples: Intended note length in samples.
signal: Dry oscillator samples.
env: Envelope samples.
gain: Linear gain multiplier.
"""
end_idx = min(start_idx + length_samples, buffer.shape[0])
seg_len = end_idx - start_idx
if seg_len <= 0:
return
sig = signal[:seg_len] * env[:seg_len] * gain
buffer[start_idx:end_idx] += sig
def build_mix() -> np.ndarray:
"""
Build the full 4-bar mix for the specified waltz pattern.
Returns:
Mono audio buffer as a float64 numpy array in range roughly [-1, 1].
"""
steps_per_bar = BEATS_PER_BAR * NOTE_DIVISION
total_bars = 4
total_steps = total_bars * steps_per_bar
total_seconds = total_steps * SECONDS_PER_STEP
total_samples = int(total_seconds * SAMPLE_RATE)
mix = np.zeros(total_samples, dtype=np.float64)
bars_spec = [
("D4", "A4", "A4"),
("A3", "E4", "E4"),
("F3", "C4", "C4"),
("G3", "D4", "D4")
] * 16
bass_gain = 0.45
stab_gain = 0.35
def bass_env(length_samples: int) -> np.ndarray:
return adsr_env(length_samples, SAMPLE_RATE, a=0.003, d=0.08, s=0.55, r=0.10)
def stab_env(length_samples: int) -> np.ndarray:
return adsr_env(length_samples, SAMPLE_RATE, a=0.002, d=0.05, s=0.35, r=0.06)
for bar_index, (bass_note, stab1, stab2) in enumerate(bars_spec):
bar_start_step = bar_index * steps_per_bar
bass_freq = note_to_freq(bass_note)
bass_start_sample = int((bar_start_step + 0) * SECONDS_PER_STEP * SAMPLE_RATE)
bass_len_samples = int(2 * SECONDS_PER_STEP * SAMPLE_RATE)
bass_sig = sine_osc(bass_freq, 2 * SECONDS_PER_STEP, SAMPLE_RATE)
place_note(mix, bass_start_sample, bass_len_samples, bass_sig, bass_env(bass_len_samples), gain=bass_gain)
stab1_freq = note_to_freq(stab1)
stab1_start = int((bar_start_step + 2) * SECONDS_PER_STEP * SAMPLE_RATE)
stab_len = int(SECONDS_PER_STEP * SAMPLE_RATE)
stab1_sig = detuned_saws(stab1_freq, SECONDS_PER_STEP, SAMPLE_RATE, detune_cents=8.0)
place_note(mix, stab1_start, stab_len, stab1_sig, stab_env(stab_len), gain=stab_gain)
stab2_freq = note_to_freq(stab2)
stab2_start = int((bar_start_step + 4) * SECONDS_PER_STEP * SAMPLE_RATE)
stab2_sig = detuned_saws(stab2_freq, SECONDS_PER_STEP, SAMPLE_RATE, detune_cents=8.0)
place_note(mix, stab2_start, stab_len, stab2_sig, stab_env(stab_len), gain=stab_gain)
peak = np.max(np.abs(mix))
if peak > 0.0:
mix = mix / peak * 0.98
return mix
def save_wav(path: str, audio: np.ndarray, sr: int) -> None:
"""
Save a mono float64 buffer to a 16-bit PCM WAV file.
Args:
path: Destination file path.
audio: Mono audio buffer in range [-1, 1].
sr: Sample rate.
"""
with wave.open(path, "w") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(sr)
f.writeframes((np.clip(audio, -1.0, 1.0) * 32767.0).astype(np.int16).tobytes())
if __name__ == "__main__":
mix = build_mix()
save_wav(OUT_PATH, mix, SAMPLE_RATE)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment