diff --git a/pytheory/play.py b/pytheory/play.py index f60207c..80b2052 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -2660,7 +2660,7 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples, filter_sustain=0.0, filter_amount=0.0, vel_to_filter=0.0, filter_q=0.707, synth_kwargs=None, temperament="equal", - reference_pitch=440.0): + reference_pitch=440.0, analog=0.0): """Render a list of Notes into an existing buffer at the correct positions.""" import random as _rnd @@ -2692,6 +2692,12 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples, pitches = [t.pitch(temperament=temperament, reference_pitch=reference_pitch) for t in note.tone.tones] else: pitches = [note.tone.pitch(temperament=temperament, reference_pitch=reference_pitch)] + # Analog drift: slight random pitch offset per note, + # simulating analog oscillator instability. Each note + # gets a unique drift amount (±cents scaled by analog). + if analog > 0: + pitches = [hz * (2 ** (_rnd.gauss(0, analog * 5) / 1200)) + for hz in pitches] # Render oscillators (pass synth_kwargs for FM etc.) waves = [synth_fn(hz, n_samples=n_samples, **_skw) for hz in pitches] @@ -2956,7 +2962,8 @@ def render_score(score): filter_q=part.lowpass_q, synth_kwargs=synth_kwargs, temperament=_temperament, - reference_pitch=_ref_pitch) + reference_pitch=_ref_pitch, + analog=part.analog) # Apply effects — segmented if automation exists auto_points = part._get_automation_points() diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py index b55ceb7..6e3321e 100644 --- a/pytheory/rhythm.py +++ b/pytheory/rhythm.py @@ -26,12 +26,14 @@ INSTRUMENTS = { "detune": 6, "chorus": 0.2, "chorus_rate": 1.0, "lowpass": 4000, "saturation": 0.15, "tremolo_depth": 0.15, "tremolo_rate": 4.5, + "analog": 0.2, }, "organ": { "synth": "organ_synth", "envelope": "organ", "chorus": 0.2, "chorus_rate": 5.5, "lowpass": 5000, "phaser": 0.15, "phaser_rate": 0.4, + "analog": 0.15, }, "harpsichord": { "synth": "pluck_synth", "envelope": "none", @@ -227,6 +229,7 @@ INSTRUMENTS = { "delay": 0.2, "delay_time": 0.25, "delay_feedback": 0.3, "filter_attack": 0.01, "filter_decay": 0.3, "filter_sustain": 0.2, "filter_amount": 3000, + "analog": 0.3, }, "synth_pad": { "synth": "supersaw", "envelope": "pad", @@ -234,6 +237,7 @@ INSTRUMENTS = { "chorus": 0.2, "phaser": 0.3, "phaser_rate": 0.3, "sub_osc": 0.2, + "analog": 0.4, }, "synth_bass": { "synth": "saw", "envelope": "pluck", @@ -241,6 +245,7 @@ INSTRUMENTS = { "filter_attack": 0.005, "filter_decay": 0.2, "filter_sustain": 0.0, "filter_amount": 2000, "sub_osc": 0.4, + "analog": 0.2, }, "acid_bass": { "synth": "saw", "envelope": "pad", @@ -250,6 +255,7 @@ INSTRUMENTS = { "filter_attack": 0.005, "filter_decay": 0.15, "filter_sustain": 0.0, "filter_amount": 4000, "vel_to_filter": 3000, + "analog": 0.3, }, "808_bass": { "synth": "sine", "envelope": "pluck", @@ -2006,6 +2012,7 @@ class Part: phaser_rate: float = 0.5, cabinet: float = 0.0, cabinet_brightness: float = 0.5, + analog: float = 0.0, fm_ratio: float = 2.0, fm_index: float = 3.0): self.name = name @@ -2051,6 +2058,7 @@ class Part: self.phaser_rate = phaser_rate self.cabinet = cabinet self.cabinet_brightness = cabinet_brightness + self.analog = analog self.fm_ratio = fm_ratio self.fm_index = fm_index self._system = "western" # default, overridden by Score.part() @@ -2648,6 +2656,7 @@ class Score: phaser_rate: float = None, cabinet: float = None, cabinet_brightness: float = None, + analog: float = None, fm_ratio: float = None, fm_index: float = None, fretboard=None) -> Part: @@ -2760,6 +2769,7 @@ class Score: "tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate, "phaser": phaser, "phaser_rate": phaser_rate, "cabinet": cabinet, "cabinet_brightness": cabinet_brightness, + "analog": analog, "fm_ratio": fm_ratio, "fm_index": fm_index, } for k, v in _locals.items():