Skip to content

Instantly share code, notes, and snippets.

@twobob
Created July 3, 2026 10:07
Show Gist options
  • Select an option

  • Save twobob/ca41bfefa77a496a9879e8a06f1cc491 to your computer and use it in GitHub Desktop.

Select an option

Save twobob/ca41bfefa77a496a9879e8a06f1cc491 to your computer and use it in GitHub Desktop.
import numpy as np
import mido
from scipy.io import wavfile
import os
class RissetExpertSynth:
def __init__(self, sample_rate=44100):
self.fs = sample_rate
# Risset's preferred partials (from Csound GEN10 demo)
# Omit 2, 3, 4 to get that 'hollow/metallic' shimmer
self.partial_weights = {1: 1.0, 5: 0.7, 6: 0.7, 7: 0.7, 8: 0.7, 9: 0.7, 10: 0.7}
self.detune_hz = 0.05 # The 'magic' beating frequency (Delta f)
def generate_risset_expert(self, freq, duration, scan_speed=-1.0):
t = np.linspace(0, duration, int(self.fs * duration), endpoint=False)
output = np.zeros_like(t)
# Logarithmic/Exponential scan trajectory
max_p = max(self.partial_weights.keys())
start_sigma = np.log(max_p) if scan_speed < 0 else 0
end_sigma = 0 if scan_speed < 0 else np.log(max_p)
sigmas = np.linspace(start_sigma, end_sigma, len(t))
width = 0.6 # Narrower width for more distinct 'arpeggio' feel
for n, weight in self.partial_weights.items():
# 1. Calculate the 'Spectral Envelope' for this partial
# Using a tighter Gaussian to make the 'cascade' pop
amp = weight * np.exp(-((np.log(n) - sigmas)**2) / (2 * width**2))
# 2. The Shimmer: Three oscillators per partial (Center, +Delta, -Delta)
# This creates the 'phasing' effect heard in the CSound demo
f_n = freq * n
# Fundamental + two beating sidebands
osc_bank = (
np.sin(2 * np.pi * f_n * t) +
0.5 * np.sin(2 * np.pi * (f_n + self.detune_hz) * t) +
0.5 * np.sin(2 * np.pi * (f_n - self.detune_hz) * t)
)
# 3. Apply ADSR-style smoothing
fade = np.clip(t / 0.02, 0, 1) * np.clip((duration - t) / 0.1, 0, 1)
output += (amp * fade) * osc_bank
return output
def process_midi_expert(midi_path, output_path):
if not os.path.exists(midi_path):
print(f"MIDI file {midi_path} not found.")
return
synth = RissetExpertSynth()
mid = mido.MidiFile(midi_path)
full_buffer = np.zeros(int(44100 * (mid.length + 2.0)))
active_notes = {}
current_time = 0
print(f"Synthesizing '{midi_path}' with Risset Detuned Banks...")
for msg in mid:
current_time += msg.time
if msg.type == 'note_on' and msg.velocity > 0:
active_notes[msg.note] = (current_time, msg.velocity)
elif (msg.type == 'note_off') or (msg.type == 'note_on' and msg.velocity == 0):
if msg.note in active_notes:
start_t, vel = active_notes.pop(msg.note)
dur = current_time - start_t
if dur > 0.01:
f = 440.0 * (2.0**((msg.note - 69) / 12.0))
# Pass velocity to amp scaling
wave = synth.generate_risset_expert(f, dur) * (vel / 127.0)
start_idx = int(start_t * 44100)
end_idx = start_idx + len(wave)
if end_idx < len(full_buffer):
full_buffer[start_idx:end_idx] += wave
# Final Soft-Clipping Normalisation
if np.max(np.abs(full_buffer)) > 0:
full_buffer = np.tanh(full_buffer / np.max(np.abs(full_buffer)))
wavfile.write(output_path, 44100, (full_buffer * 32767).astype(np.int16))
print(f"Complete. Expert Cascade saved to {output_path}")
if __name__ == "__main__":
process_midi_expert('input.mid', 'risset_expert.wav')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment