From 4a262baa51539317aff6d2a1ce2fc780e4049530 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:08:36 -0500 Subject: [PATCH 1/2] Live macros: minimoog's held notes follow Volume, Cutoff, Resonance, Env Amount, Noise Mix and Overdrive Each continuous macro is one shared synthio block that every note reads, so a macro change reaches the notes already sounding (#166). _support gains live(), gain() and block_value(); note_is_silent() reads a live amplitude's current value. --- lib/audioinstruments/_support.py | 58 ++++++++++++++++++++++++++--- lib/audioinstruments/minimoog.py | 50 +++++++++++++------------ tests/test_live_macros.py | 63 ++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 tests/test_live_macros.py diff --git a/lib/audioinstruments/_support.py b/lib/audioinstruments/_support.py index 1800138..0270525 100644 --- a/lib/audioinstruments/_support.py +++ b/lib/audioinstruments/_support.py @@ -195,6 +195,53 @@ def env_shape_table(attack, decay, sustain, length=96): return out +_SUM = synthio.MathOperation.SUM +_PRODUCT = synthio.MathOperation.PRODUCT + + +def live(value): + """A continuous macro that notes already sounding follow. + + An instrument that reads a macro as a float when a note starts can only + change the *next* note. A block can be shared instead: every note wires + the same block into its filter frequency, Q, amplitude or LFO, and the + macro handler writes ``block.a = new_value``. Each sounding note picks the + new value up on its next audio block, the way a hardware knob reaches + every voice at once. + + A block holding a constant renders exactly what the plain float renders, + on every interpreter this package runs on, so a macro set before a note + sounds the same as it always did. + """ + return synthio.Math(_SUM, value, 0.0, 0.0) + + +def gain(a, b=1.0, c=1.0): + """``a * b * c`` as one amplitude input, where any of the three may be a + live block. A note's level often depends on more than one macro (volume + times a layer mix); this keeps each of them live.""" + return synthio.Math(_PRODUCT, a, b, c) + + +def block_value(x): + """The value ``x`` holds right now when it is a number, or a SUM/PRODUCT + of numbers and such blocks; ``None`` when it moves on its own (an LFO, an + envelope) and so cannot be known from Python.""" + if isinstance(x, (int, float)): + return x + if isinstance(x, synthio.Math): + op = x.operation + if op != _SUM and op != _PRODUCT: + return None + a = block_value(x.a) + b = block_value(x.b) + c = block_value(x.c) + if a is None or b is None or c is None: + return None + return a + b + c if op == _SUM else a * b * c + return None + + def logmap(v, lo, hi): """Map a normalized 0.0-1.0 value onto ``lo``..``hi`` logarithmically.""" return lo * ((hi / lo) ** v) @@ -264,16 +311,17 @@ def copy_note(source, target): def note_is_silent(note): - """True only when a note is *provably* inaudible - a numeric amplitude of - zero or less. + """True only when a note is *provably* inaudible - an amplitude of zero or + less, read straight off a number or off a live macro's block (see + :func:`block_value`) as it stands now. - An amplitude driven by an LFO or a block input is assumed audible, because + An amplitude driven by an LFO or an envelope is assumed audible, because it may be anywhere in its cycle. Being wrong in that direction costs a thinner steal; being wrong the other way silences a key the player is holding. """ - amplitude = getattr(note, "amplitude", None) - if isinstance(amplitude, (int, float)): + amplitude = block_value(getattr(note, "amplitude", None)) + if amplitude is not None: return amplitude <= 0.0 return False diff --git a/lib/audioinstruments/minimoog.py b/lib/audioinstruments/minimoog.py index dea9b0a..dec0022 100644 --- a/lib/audioinstruments/minimoog.py +++ b/lib/audioinstruments/minimoog.py @@ -47,8 +47,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, noise_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, noise_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -63,15 +63,16 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2000.0 - resonance = 1.0 - env_amount = 3000.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2000.0) + resonance = live(1.0) + env_amount = live(3000.0) glide = 0.05 osc2_detune = 1.01 osc3_detune = 0.99 - noise_mix = 0.0 + noise_mix = live(0.0) amp_a = 0.01 amp_d = 0.3 @@ -81,7 +82,7 @@ def create(sample_rate, channel_count=2, transport=None): filt_a = 0.01 filt_d = 0.3 filt_s = 0.2 - overdrive = 1.0 + overdrive = live(1.0) voices = {} @@ -97,8 +98,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, env_amount, glide, osc2_detune, osc3_detune, noise_mix - nonlocal amp_a, amp_d, amp_s, amp_r, filt_a, filt_d, filt_s, overdrive + nonlocal glide, osc2_detune, osc3_detune + nonlocal amp_a, amp_d, amp_s, amp_r, filt_a, filt_d, filt_s nonlocal serial, last_pitch k = key_of(channel, note_id, data0) @@ -136,33 +137,36 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff_base, f_sweep, 0.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=resonance) - amp = volume * value0 * overdrive + amp = gain(volume, overdrive, value0) + saw_amp = gain(amp, 0.4) - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.4, bend=bend) - o2 = synthio.Note(hz * osc2_detune, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.4, bend=bend) - o3 = synthio.Note(hz * osc3_detune, waveform=SQUARE, envelope=env, filter=lp, amplitude=amp * 0.3, bend=bend) - n = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=lp, amplitude=amp * noise_mix * 0.3) + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=saw_amp, bend=bend) + o2 = synthio.Note(hz * osc2_detune, waveform=SAW, envelope=env, filter=lp, amplitude=saw_amp, bend=bend) + o3 = synthio.Note(hz * osc3_detune, waveform=SQUARE, envelope=env, filter=lp, amplitude=gain(amp, 0.3), bend=bend) + n = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=lp, amplitude=gain(amp, noise_mix, 0.3)) serial += 1 voices[k] = ((o1, o2, o3, n), serial) synth.press(o1) synth.press(o2) synth.press(o3) - if noise_mix > 0.01: + # A layer at zero is not pressed at all, so Noise Mix reaches a + # held note only if the noise was up when the note started. + if noise_mix.a > 0.01: synth.press(n) elif event_type in (EVENT_NOTE_OFF, EVENT_NOTE_ON): release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = logmap(value0, 50.0, 10000.0) - elif data0 == 2: resonance = logmap(value0, 0.5, 4.0) - elif data0 == 3: env_amount = logmap(value0, 100.0, 8000.0) + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = logmap(value0, 50.0, 10000.0) + elif data0 == 2: resonance.a = logmap(value0, 0.5, 4.0) + elif data0 == 3: env_amount.a = logmap(value0, 100.0, 8000.0) elif data0 == 4: glide = logmap(value0, 0.001, 1.0) elif data0 == 5: osc2_detune = 1.0 + (value0 - 0.5) * 0.03 elif data0 == 6: osc3_detune = 1.0 + (value0 - 0.5) * 0.03 - elif data0 == 7: noise_mix = value0 + elif data0 == 7: noise_mix.a = value0 elif data0 == 8: amp_a = logmap(value0, 0.001, 2.0) elif data0 == 9: amp_d = logmap(value0, 0.05, 3.0) elif data0 == 10: amp_s = value0 @@ -170,7 +174,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif data0 == 12: filt_a = logmap(value0, 0.001, 2.0) elif data0 == 13: filt_d = logmap(value0, 0.05, 3.0) elif data0 == 14: filt_s = value0 - elif data0 == 15: overdrive = logmap(value0, 1.0, 3.0) + elif data0 == 15: overdrive.a = logmap(value0, 1.0, 3.0) instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, transport=transport) diff --git a/tests/test_live_macros.py b/tests/test_live_macros.py new file mode 100644 index 0000000..a90a852 --- /dev/null +++ b/tests/test_live_macros.py @@ -0,0 +1,63 @@ +"""Turning a continuous macro changes the notes that are already sounding. + +A player holding a chord and sweeping Cutoff expects to hear the sweep, the +way a hardware knob reaches every voice at once (audiocomponents#166). Each +case here holds a note, moves one macro to an extreme while it sounds, and +asserts the held note comes out differently at 0 than at 127. + +``LIVE`` lists, per instrument, the macros that reach a held note. Macros not +listed take effect on the next note: envelope times, glide, voice modes, +waveform and table choices, pitch, and anything baked into a wavetable when +the note starts. Drum machines are not listed at all: their hits are short, +and every macro reaches the next hit. +""" + +import hashlib +import unittest + +import audiocore +import audioinstruments + +SAMPLE_RATE = 48000 + +#: instrument -> macro indices that a held note follows. +LIVE = { + "minimoog": (0, 1, 2, 3, 7, 15), +} + + +def held(name, index, value): + """Hash of a held note's output after macro ``index`` moves to ``value``. + + The macro starts mid-range so that a layer it would gate off at zero is + sounding when the note starts.""" + instrument = audioinstruments.create(name, SAMPLE_RATE, channel_count=1) + instrument.set_macro(index, 64) + instrument.note_on(57, 110) + for _ in range(20): + audiocore.get_buffer(instrument.output) + instrument.set_macro(index, value) + digest = hashlib.sha256() + for _ in range(40): + digest.update(bytes(audiocore.get_buffer(instrument.output)[1])) + return digest.hexdigest() + + +class LiveMacroTest(unittest.TestCase): + def test_every_listed_instrument_is_melodic(self): + for name in LIVE: + self.assertIn(name, audioinstruments.MELODIC) + + def test_held_notes_follow_live_macros(self): + for name, indices in LIVE.items(): + labels = audioinstruments.load(name).MACRO_LABELS + for index in indices: + with self.subTest(instrument=name, macro=labels[index]): + self.assertNotEqual( + held(name, index, 0), held(name, index, 127), + "%s: turning %s does not reach a held note" + % (name, labels[index])) + + +if __name__ == "__main__": + unittest.main() From 125a336db6fba2d609e3dbe25480cc586b77e3c4 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:28:16 -0500 Subject: [PATCH 2/2] Live macros: held notes follow continuous macros on all 43 melodic instruments Volume, filter cutoff and resonance, layer mixes, envelope amounts and modulation depths and rates are shared blocks every note reads, so a macro change reaches a held chord. Envelope times, glide, pitch and anything baked into a waveform stay next-note. Renders with macros set before the note are byte-identical to before on CPython, MicroPython and CircuitPython. tests/test_live_macros.py holds a note per live macro and asserts it moves. --- CHANGELOG.md | 12 ++++ lib/audioinstruments/andromeda.py | 53 ++++++++-------- lib/audioinstruments/arp2600.py | 50 ++++++++------- lib/audioinstruments/b3.py | 61 +++++++++--------- lib/audioinstruments/clavinet.py | 49 ++++++++------- lib/audioinstruments/cp70.py | 36 +++++------ lib/audioinstruments/cs80.py | 47 +++++++------- lib/audioinstruments/cz101.py | 37 ++++++----- lib/audioinstruments/d50.py | 45 +++++++------- lib/audioinstruments/dx7.py | 54 ++++++++-------- lib/audioinstruments/emulator2.py | 53 ++++++++-------- lib/audioinstruments/fairlight.py | 50 ++++++++------- lib/audioinstruments/farfisa.py | 58 +++++++++-------- lib/audioinstruments/fs1r.py | 32 ++++++---- lib/audioinstruments/jp8000.py | 39 ++++++------ lib/audioinstruments/juno106.py | 60 +++++++++--------- lib/audioinstruments/jupiter8.py | 55 +++++++++-------- lib/audioinstruments/k2600.py | 81 ++++++++++++------------ lib/audioinstruments/karplus.py | 29 +++++---- lib/audioinstruments/mellotron.py | 33 +++++----- lib/audioinstruments/microwave.py | 41 +++++++------ lib/audioinstruments/ms20.py | 53 ++++++++-------- lib/audioinstruments/ms2000.py | 48 ++++++++------- lib/audioinstruments/music_easel.py | 36 ++++++----- lib/audioinstruments/nord_lead.py | 36 ++++++----- lib/audioinstruments/obxa.py | 31 +++++----- lib/audioinstruments/odyssey.py | 39 ++++++------ lib/audioinstruments/pianet.py | 33 +++++----- lib/audioinstruments/polysix.py | 51 +++++++-------- lib/audioinstruments/ppg_wave.py | 35 ++++++----- lib/audioinstruments/prophet5.py | 43 ++++++------- lib/audioinstruments/prophet_vs.py | 76 +++++++++++++---------- lib/audioinstruments/rhodes.py | 37 +++++------ lib/audioinstruments/sh101.py | 35 ++++++----- lib/audioinstruments/solina.py | 40 +++++++----- lib/audioinstruments/taurus.py | 34 +++++----- lib/audioinstruments/tb303.py | 44 +++++++------ lib/audioinstruments/virus.py | 50 ++++++++------- lib/audioinstruments/vl1.py | 52 ++++++++-------- lib/audioinstruments/vox_continental.py | 56 +++++++++-------- lib/audioinstruments/vp330.py | 82 +++++++++++++------------ lib/audioinstruments/wasp.py | 38 +++++++----- lib/audioinstruments/wurlitzer.py | 29 ++++----- tests/test_live_macros.py | 69 ++++++++++++++++++++- 44 files changed, 1121 insertions(+), 901 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 407d1ac..ae773d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ here. The two packages version and release together, from this repository; Releases up to and including audiodsp's v0.1.1 shipped both packages from there, and are recorded in its changelog. +## Unreleased + +### Fixed + +- **Turning a macro now changes the notes already sounding** on all 43 + melodic instruments: cutoff, resonance, volume, layer mixes, envelope + amounts, brightness and modulation depths and rates reach a held chord the + way a hardware knob does. Envelope times, glide, pitch and anything built + into a waveform still take effect on the next note, as do the drum + machines' macros. With a macro set before the note, every instrument + sounds exactly as it did (#166). + ## v0.4.0 (2026-10-15) ### Changed diff --git a/lib/audioinstruments/andromeda.py b/lib/audioinstruments/andromeda.py index f498cd1..3cf83b3 100644 --- a/lib/audioinstruments/andromeda.py +++ b/lib/audioinstruments/andromeda.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,15 +57,17 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - f1_cutoff = 2000.0 - f2_cutoff = 3000.0 - f_mix = 0.5 - res1 = 1.0 - res2 = 1.0 - env_to_f1 = 2000.0 - env_to_f2 = 1000.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + f1_cutoff = live(2000.0) + f2_cutoff = live(3000.0) + f_mix = live(0.5) + f_mix_inv = live(0.5) # 1 - Filter Mix, the Filter 1 side's share + res1 = live(1.0) + res2 = live(1.0) + env_to_f1 = live(2000.0) + env_to_f2 = live(1000.0) unison_detune = 0.0 a_a = 0.01 a_d = 0.3 @@ -87,7 +89,6 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, f1_cutoff, f2_cutoff, f_mix, res1, res2, env_to_f1, env_to_f2 nonlocal unison_detune, a_a, a_d, a_s, a_r, f_a, f_d, master_tune nonlocal serial @@ -99,7 +100,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=a_a, decay_time=a_d, release_time=a_r, attack_level=1.0, sustain_level=a_s) @@ -118,12 +119,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos if unison_detune > 0.01: for i, detune in enumerate([-unison_detune, 0.0, unison_detune]): pan = [-0.5, 0.0, 0.5][i] - n1 = synthio.Note(hz * (1.0 + detune * 0.012), waveform=SAW, envelope=env, filter=lp1, amplitude=amp * (1.0 - f_mix) * 0.3, panning=pan) - n2 = synthio.Note(hz * (1.0 + detune * 0.012), waveform=SQUARE, envelope=env, filter=hp2, amplitude=amp * f_mix * 0.3, panning=pan) + n1 = synthio.Note(hz * (1.0 + detune * 0.012), waveform=SAW, envelope=env, filter=lp1, amplitude=gain(amp, f_mix_inv, 0.3), panning=pan) + n2 = synthio.Note(hz * (1.0 + detune * 0.012), waveform=SQUARE, envelope=env, filter=hp2, amplitude=gain(amp, f_mix, 0.3), panning=pan) notes.extend([n1, n2]) else: - n1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp1, amplitude=amp * (1.0 - f_mix)) - n2 = synthio.Note(hz * 1.002, waveform=SQUARE, envelope=env, filter=hp2, amplitude=amp * f_mix) + n1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp1, amplitude=gain(amp, f_mix_inv)) + n2 = synthio.Note(hz * 1.002, waveform=SQUARE, envelope=env, filter=hp2, amplitude=gain(amp, f_mix)) notes.extend([n1, n2]) serial += 1 @@ -135,14 +136,16 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: f1_cutoff = 50.0 * (100.0 ** value0) - elif data0 == 2: f2_cutoff = 50.0 * (100.0 ** value0) - elif data0 == 3: f_mix = value0 - elif data0 == 4: res1 = 0.5 + value0 * 3.5 - elif data0 == 5: res2 = 0.5 + value0 * 3.5 - elif data0 == 6: env_to_f1 = value0 * 8000.0 - elif data0 == 7: env_to_f2 = value0 * 8000.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: f1_cutoff.a = 50.0 * (100.0 ** value0) + elif data0 == 2: f2_cutoff.a = 50.0 * (100.0 ** value0) + elif data0 == 3: + f_mix.a = value0 + f_mix_inv.a = 1.0 - value0 + elif data0 == 4: res1.a = 0.5 + value0 * 3.5 + elif data0 == 5: res2.a = 0.5 + value0 * 3.5 + elif data0 == 6: env_to_f1.a = value0 * 8000.0 + elif data0 == 7: env_to_f2.a = value0 * 8000.0 elif data0 == 8: unison_detune = value0 elif data0 == 9: a_a = logmap(value0, 0.001, 2.001) elif data0 == 10: a_d = logmap(value0, 0.05, 3.05) diff --git a/lib/audioinstruments/arp2600.py b/lib/audioinstruments/arp2600.py index 0e42c43..1baee75 100644 --- a/lib/audioinstruments/arp2600.py +++ b/lib/audioinstruments/arp2600.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -58,12 +58,13 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2000.0 - resonance = 1.0 - fm_amt = 0.0 - reverb_mix = 0.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2000.0) + resonance = live(1.0) + fm_amt = live(0.0) + reverb_mix = live(0.0) osc2_detune = 1.0 osc3_detune = 0.5 @@ -93,7 +94,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, fm_amt, reverb_mix, osc2_detune, osc3_detune + nonlocal osc2_detune, osc3_detune nonlocal e1_a, e1_r, e2_a, e2_d, e2_s, e2_r, vca_a, vca_r, master_tune nonlocal serial @@ -104,7 +105,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=vca_a, decay_time=e2_d, release_time=vca_r, attack_level=1.0, sustain_level=1.0) @@ -114,25 +115,28 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=resonance) - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.4) - o2 = synthio.Note(hz * osc2_detune, waveform=SQUARE, envelope=env, filter=lp, amplitude=amp * 0.3) + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.4)) + o2 = synthio.Note(hz * osc2_detune, waveform=SQUARE, envelope=env, filter=lp, amplitude=gain(amp, 0.3)) # Env 1 (Attack/Release) patched to osc3's pitch input, scaled by FM Amount: the ARP # 2600 is semi-modular, and "envelope -> VCO frequency" is one of its classic patches # (laser/zap sounds). synthio can't do audio-rate FM, so approximate it as a one-shot # pitch-bend sweep shaped by Env 1. fm_tbl = env_shape_table(e1_a, e1_r, 0.0) - fm_lfo = synthio.LFO(waveform=fm_tbl, once=True, rate=1.0/max(0.01, e1_a + e1_r), scale=fm_amt * 2.0, interpolate=True) if fm_amt > 0.01 else None - o3 = synthio.Note(hz * osc3_detune, waveform=TRIANGLE, envelope=env, filter=lp, amplitude=amp * 0.3, bend=fm_lfo) + # FM Amount reaches a held note only if it was up when the note started. + fm_lfo = synthio.LFO(waveform=fm_tbl, once=True, rate=1.0/max(0.01, e1_a + e1_r), scale=gain(fm_amt, 2.0), interpolate=True) if fm_amt.a > 0.01 else None + o3 = synthio.Note(hz * osc3_detune, waveform=TRIANGLE, envelope=env, filter=lp, amplitude=gain(amp, 0.3), bend=fm_lfo) filtered_notes = [o1, o2, o3] notes = list(filtered_notes) # Reverb Mix: the 2600 itself is dry; approximate a reverb-style tail with a quietly # mixed, softly filtered, longer-release copy of the voice rather than true convolution. - if reverb_mix > 0.01: - wash_env = synthio.Envelope(attack_time=vca_a, decay_time=e2_d, release_time=vca_r + reverb_mix * 2.0, attack_level=1.0, sustain_level=1.0) - wash_filt = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff_base * 0.4, Q=0.7) - wash = synthio.Note(hz * 1.003, waveform=TRIANGLE, envelope=wash_env, filter=wash_filt, amplitude=amp * reverb_mix * 0.25, panning=0.6) + # Likewise Reverb Mix: its level follows the knob while the wash + # sounds, but its tail length is set when the note starts. + if reverb_mix.a > 0.01: + wash_env = synthio.Envelope(attack_time=vca_a, decay_time=e2_d, release_time=vca_r + reverb_mix.a * 2.0, attack_level=1.0, sustain_level=1.0) + wash_filt = synthio.Biquad(synthio.FilterMode.LOW_PASS, gain(cutoff_base, 0.4), Q=0.7) + wash = synthio.Note(hz * 1.003, waveform=TRIANGLE, envelope=wash_env, filter=wash_filt, amplitude=gain(amp, reverb_mix, 0.25), panning=0.6) notes.append(wash) # Env 2 Release: retarget o1/o2/o3's shared filter to a real release @@ -151,11 +155,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 - elif data0 == 3: fm_amt = value0 - elif data0 == 4: reverb_mix = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 + elif data0 == 3: fm_amt.a = value0 + elif data0 == 4: reverb_mix.a = value0 elif data0 == 5: osc2_detune = 0.5 + value0 elif data0 == 6: osc3_detune = 0.25 + value0 elif data0 == 7: e1_a = logmap(value0, 0.001, 2.001) diff --git a/lib/audioinstruments/b3.py b/lib/audioinstruments/b3.py index 3651924..c4dc08a 100644 --- a/lib/audioinstruments/b3.py +++ b/lib/audioinstruments/b3.py @@ -36,8 +36,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - make_table, noise_table, ring_depth_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, noise_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -51,17 +51,18 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - db16 = 1.0 - db8 = 1.0 - db4 = 0.5 - db2 = 0.2 - perc_lvl = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + db16 = live(1.0) + db8 = live(1.0) + db4 = live(0.5) + db2 = live(0.2) + perc_lvl = live(0.5) perc_dec = 0.3 - key_click = 0.3 + key_click = live(0.3) leslie_fast = 0.0 - overdrive = 1.0 + overdrive = live(1.0) master_tune = 1.0 voices = {} @@ -76,7 +77,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, db16, db8, db4, db2, perc_lvl, perc_dec, key_click, leslie_fast, overdrive, master_tune + nonlocal perc_dec, leslie_fast, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -87,7 +88,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 * overdrive + amp = gain(volume, value0, overdrive) env = synthio.Envelope(attack_time=0.01, decay_time=0.1, release_time=0.05, attack_level=1.0, sustain_level=1.0) perc_env = synthio.Envelope(attack_time=0.001, decay_time=perc_dec, release_time=0.05, attack_level=1.0, sustain_level=0.0) @@ -104,19 +105,21 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos notes = [] # Drawbars: 16' (sub), 8' (fund) go through the bass drum rotor; - # 4' (2nd harm), 2' (4th harm) go through the treble horn rotor - if db16 > 0.01: notes.append(synthio.Note(hz * 0.5, waveform=SINE, envelope=env, amplitude=amp * db16 * 0.25, ring_frequency=drum_rate, ring_waveform=drum_wave, bend=drum_vib, panning=-0.2)) - if db8 > 0.01: notes.append(synthio.Note(hz, waveform=SINE, envelope=env, amplitude=amp * db8 * 0.25, ring_frequency=drum_rate, ring_waveform=drum_wave, bend=drum_vib, panning=0.2)) - if db4 > 0.01: notes.append(synthio.Note(hz * 2.0, waveform=SINE, envelope=env, amplitude=amp * db4 * 0.2, ring_frequency=horn_rate, ring_waveform=horn_wave, bend=horn_vib, panning=-0.1)) - if db2 > 0.01: notes.append(synthio.Note(hz * 4.0, waveform=SINE, envelope=env, amplitude=amp * db2 * 0.15, ring_frequency=horn_rate, ring_waveform=horn_wave, bend=horn_vib, panning=0.1)) + # 4' (2nd harm), 2' (4th harm) go through the treble horn rotor. + # A drawbar, Perc Level or Key Click at zero adds no note, so it + # reaches a held note only if it was up when the note started. + if db16.a > 0.01: notes.append(synthio.Note(hz * 0.5, waveform=SINE, envelope=env, amplitude=gain(amp, db16, 0.25), ring_frequency=drum_rate, ring_waveform=drum_wave, bend=drum_vib, panning=-0.2)) + if db8.a > 0.01: notes.append(synthio.Note(hz, waveform=SINE, envelope=env, amplitude=gain(amp, db8, 0.25), ring_frequency=drum_rate, ring_waveform=drum_wave, bend=drum_vib, panning=0.2)) + if db4.a > 0.01: notes.append(synthio.Note(hz * 2.0, waveform=SINE, envelope=env, amplitude=gain(amp, db4, 0.2), ring_frequency=horn_rate, ring_waveform=horn_wave, bend=horn_vib, panning=-0.1)) + if db2.a > 0.01: notes.append(synthio.Note(hz * 4.0, waveform=SINE, envelope=env, amplitude=gain(amp, db2, 0.15), ring_frequency=horn_rate, ring_waveform=horn_wave, bend=horn_vib, panning=0.1)) # 3rd harmonic percussion rides the horn rotor - if perc_lvl > 0.01: notes.append(synthio.Note(hz * 3.0, waveform=SINE, envelope=perc_env, amplitude=amp * perc_lvl * 0.4, ring_frequency=horn_rate, ring_waveform=horn_wave, bend=horn_vib)) + if perc_lvl.a > 0.01: notes.append(synthio.Note(hz * 3.0, waveform=SINE, envelope=perc_env, amplitude=gain(amp, perc_lvl, 0.4), ring_frequency=horn_rate, ring_waveform=horn_wave, bend=horn_vib)) # Key click - if key_click > 0.01: + if key_click.a > 0.01: hp = synthio.Biquad(synthio.FilterMode.HIGH_PASS, 5000.0, Q=1.0) - notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=click_env, filter=hp, amplitude=amp * key_click * 0.3)) + notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=click_env, filter=hp, amplitude=gain(amp, key_click, 0.3))) serial += 1 voices[k] = (tuple(notes), serial) @@ -127,16 +130,16 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: db16 = value0 - elif data0 == 2: db8 = value0 - elif data0 == 3: db4 = value0 - elif data0 == 4: db2 = value0 - elif data0 == 5: perc_lvl = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: db16.a = value0 + elif data0 == 2: db8.a = value0 + elif data0 == 3: db4.a = value0 + elif data0 == 4: db2.a = value0 + elif data0 == 5: perc_lvl.a = value0 elif data0 == 6: perc_dec = logmap(value0, 0.05, 1.05) - elif data0 == 7: key_click = value0 + elif data0 == 7: key_click.a = value0 elif data0 == 8: leslie_fast = value0 - elif data0 == 9: overdrive = 1.0 + value0 * 2.0 + elif data0 == 9: overdrive.a = 1.0 + value0 * 2.0 elif data0 == 10: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/clavinet.py b/lib/audioinstruments/clavinet.py index 1406e37..314bbb5 100644 --- a/lib/audioinstruments/clavinet.py +++ b/lib/audioinstruments/clavinet.py @@ -36,8 +36,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -52,13 +52,15 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - pickup_mix = 0.5 # 0 = DA, 1 = CB + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + pickup_mix = live(0.5) # 0 = DA, 1 = CB + pickup_da = live(0.5) # 1 - Pickup Mix mute_lvl = 0.0 - wah_depth = 0.0 - wah_rate = 2.0 - brilliance = 0.5 + wah_depth = live(0.0) + wah_rate = live(2.0) + cutoff_base = live(1000.0 + 0.5 * 6000.0) # from Brilliance amp_a = 0.01 amp_d = 1.0 amp_s = 0.1 @@ -81,7 +83,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, pickup_mix, mute_lvl, wah_depth, wah_rate, brilliance + nonlocal mute_lvl nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -93,7 +95,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # Mute slider makes decay very short actual_d = amp_d * (1.0 - mute_lvl * 0.9) @@ -101,16 +103,19 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos env = synthio.Envelope(attack_time=amp_a, decay_time=actual_d, release_time=amp_r, attack_level=1.0, sustain_level=actual_s) - cutoff = 1000.0 + (brilliance * 6000.0) + cutoff = cutoff_base - if wah_depth > 0.01: - wah_lfo = synthio.LFO(waveform=SINE, rate=wah_rate, scale=wah_depth * 4000.0) + # The wah switches the filter mode when the note starts, so Wah + # Depth and Wah Rate reach a held note only if the wah was on. + wah_on = wah_depth.a > 0.01 + if wah_on: + wah_lfo = synthio.LFO(waveform=SINE, rate=wah_rate, scale=gain(wah_depth, 4000.0)) cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff, wah_lfo, 0.0) - lp = synthio.Biquad(synthio.FilterMode.BAND_PASS if wah_depth > 0.01 else synthio.FilterMode.LOW_PASS, cutoff, Q=2.0 if wah_depth > 0.01 else 0.8) + lp = synthio.Biquad(synthio.FilterMode.BAND_PASS if wah_on else synthio.FilterMode.LOW_PASS, cutoff, Q=2.0 if wah_on else 0.8) - o_cb = synthio.Note(hz, waveform=WAVE_CB, envelope=env, filter=lp, amplitude=amp * pickup_mix * 0.7) - o_da = synthio.Note(hz, waveform=WAVE_DA, envelope=env, filter=lp, amplitude=amp * (1.0 - pickup_mix) * 0.7) + o_cb = synthio.Note(hz, waveform=WAVE_CB, envelope=env, filter=lp, amplitude=gain(amp, pickup_mix, 0.7)) + o_da = synthio.Note(hz, waveform=WAVE_DA, envelope=env, filter=lp, amplitude=gain(amp, pickup_da, 0.7)) serial += 1 voices[k] = (_support.press_voice(synth, voices, retired, @@ -120,12 +125,14 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: pickup_mix = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: + pickup_mix.a = value0 + pickup_da.a = 1.0 - value0 elif data0 == 2: mute_lvl = value0 - elif data0 == 3: wah_depth = value0 - elif data0 == 4: wah_rate = 0.5 + value0 * 10.0 - elif data0 == 5: brilliance = value0 + elif data0 == 3: wah_depth.a = value0 + elif data0 == 4: wah_rate.a = 0.5 + value0 * 10.0 + elif data0 == 5: cutoff_base.a = 1000.0 + (value0 * 6000.0) elif data0 == 6: amp_a = logmap(value0, 0.001, 0.201) elif data0 == 7: amp_d = logmap(value0, 0.1, 3.1) elif data0 == 8: amp_s = value0 diff --git a/lib/audioinstruments/cp70.py b/lib/audioinstruments/cp70.py index 6cb1bd1..dd1e305 100644 --- a/lib/audioinstruments/cp70.py +++ b/lib/audioinstruments/cp70.py @@ -33,8 +33,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - make_table, ring_depth_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -48,15 +48,16 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - hammer_lvl = 0.8 - string_lvl = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + hammer_lvl = live(0.8) + string_lvl = live(1.0) trem_rate = 5.0 trem_depth = 0.0 chorus = 0.5 decay = 2.0 - brilliance = 0.8 + cutoff = live(500.0 + (0.8 * 8000.0)) # from Brilliance master_tune = 1.0 voices = {} @@ -75,7 +76,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, hammer_lvl, string_lvl, trem_rate, trem_depth, chorus, decay, brilliance, master_tune + nonlocal trem_rate, trem_depth, chorus, decay, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -86,12 +87,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=0.005, decay_time=decay, release_time=0.4, attack_level=1.0, sustain_level=0.0) hammer_env = synthio.Envelope(attack_time=0.001, decay_time=0.1, release_time=0.05, attack_level=1.0, sustain_level=0.0) - cutoff = 500.0 + (brilliance * 8000.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=0.7) trem_wave = ring_depth_table(trem_depth) if trem_depth > 0.01 else None @@ -99,13 +99,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos notes = [] # Chorus simulation if chorus > 0.01: - n1 = synthio.Note(hz, waveform=WAVE_STRING, envelope=env, filter=lp, amplitude=amp * string_lvl * 0.4, ring_frequency=trem_rate, ring_waveform=trem_wave, panning=-0.3) - n2 = synthio.Note(hz * (1.0 + chorus * 0.005), waveform=WAVE_STRING, envelope=env, filter=lp, amplitude=amp * string_lvl * 0.4, ring_frequency=trem_rate, ring_waveform=trem_wave, panning=0.3) + n1 = synthio.Note(hz, waveform=WAVE_STRING, envelope=env, filter=lp, amplitude=gain(amp, string_lvl, 0.4), ring_frequency=trem_rate, ring_waveform=trem_wave, panning=-0.3) + n2 = synthio.Note(hz * (1.0 + chorus * 0.005), waveform=WAVE_STRING, envelope=env, filter=lp, amplitude=gain(amp, string_lvl, 0.4), ring_frequency=trem_rate, ring_waveform=trem_wave, panning=0.3) notes.extend([n1, n2]) else: - notes.append(synthio.Note(hz, waveform=WAVE_STRING, envelope=env, filter=lp, amplitude=amp * string_lvl * 0.8, ring_frequency=trem_rate, ring_waveform=trem_wave)) + notes.append(synthio.Note(hz, waveform=WAVE_STRING, envelope=env, filter=lp, amplitude=gain(amp, string_lvl, 0.8), ring_frequency=trem_rate, ring_waveform=trem_wave)) - notes.append(synthio.Note(hz, waveform=WAVE_HAMMER, envelope=hammer_env, filter=lp, amplitude=amp * hammer_lvl * 0.6, ring_frequency=trem_rate, ring_waveform=trem_wave)) + notes.append(synthio.Note(hz, waveform=WAVE_HAMMER, envelope=hammer_env, filter=lp, amplitude=gain(amp, hammer_lvl, 0.6), ring_frequency=trem_rate, ring_waveform=trem_wave)) serial += 1 voices[k] = (_support.press_voice(synth, voices, retired, @@ -115,14 +115,14 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: hammer_lvl = value0 * 2.0 - elif data0 == 2: string_lvl = value0 * 2.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: hammer_lvl.a = value0 * 2.0 + elif data0 == 2: string_lvl.a = value0 * 2.0 elif data0 == 3: trem_rate = 0.1 + value0 * 10.0 elif data0 == 4: trem_depth = value0 elif data0 == 5: chorus = value0 elif data0 == 6: decay = logmap(value0, 0.5, 4.5) - elif data0 == 7: brilliance = value0 + elif data0 == 7: cutoff.a = 500.0 + (value0 * 8000.0) elif data0 == 8: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/cs80.py b/lib/audioinstruments/cs80.py index 845946f..8b80547 100644 --- a/lib/audioinstruments/cs80.py +++ b/lib/audioinstruments/cs80.py @@ -44,7 +44,8 @@ from audioinstruments._support import ( EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, EVENT_POLY_PRESSURE, - env_shape_table, key_of, logmap, make_table, ring_depth_table, + env_shape_table, gain, key_of, live, logmap, make_table, + ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,14 +58,16 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2500.0 - resonance = 1.0 - hpf_cutoff = 100.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2500.0) + resonance = live(1.0) + hpf_cutoff = live(100.0) ring_speed = 5.0 ring_depth = 0.0 - layer2_mix = 0.5 + layer2_mix = live(0.5) + layer1_mix = live(0.5) # 1 - Layer II Mix poly_at_depth = 0.5 amp_a = 0.05 @@ -74,7 +77,7 @@ def create(sample_rate, channel_count=2, transport=None): filt_a = 0.05 filt_d = 0.5 filt_s = 0.5 - brilliance = 1.0 + brilliance = live(1.0) voices = {} serial = 0 @@ -88,8 +91,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, hpf_cutoff, ring_speed, ring_depth, layer2_mix - nonlocal poly_at_depth, amp_a, amp_d, amp_s, amp_r, filt_a, filt_d, filt_s, brilliance + nonlocal ring_speed, ring_depth + nonlocal poly_at_depth, amp_a, amp_d, amp_s, amp_r, filt_a, filt_d, filt_s nonlocal serial k = key_of(channel, note_id, data0) @@ -100,12 +103,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) env_tbl = env_shape_table(filt_a, filt_d, filt_s) - f_sweep = synthio.LFO(waveform=env_tbl, once=True, rate=1.0/max(0.01, filt_a + filt_d), scale=2000.0 * brilliance, interpolate=True) + f_sweep = synthio.LFO(waveform=env_tbl, once=True, rate=1.0/max(0.01, filt_a + filt_d), scale=gain(2000.0, brilliance), interpolate=True) cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff_base, f_sweep, 0.0) lp1 = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=resonance) @@ -114,10 +117,10 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos ring_wave = ring_depth_table(ring_depth) if ring_depth > 0.01 else None # Layer I: low-pass voice - amp1 = amp * (1.0 - layer2_mix) * 0.8 + amp1 = gain(amp, layer1_mix, 0.8) o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp1, amplitude=amp1, ring_frequency=ring_speed * 10.0, ring_waveform=ring_wave) # Layer II: slightly detuned, high-pass voice (the CS-80's second layer ran through its own filter) - amp2 = amp * layer2_mix * 0.8 + amp2 = gain(amp, layer2_mix, 0.8) o2 = synthio.Note(hz * 1.005, waveform=SAW, envelope=env, filter=hp2, amplitude=amp2, ring_frequency=ring_speed * 10.0, ring_waveform=ring_wave) serial += 1 @@ -134,16 +137,18 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos notes, _, base_amps = voice boost = 1.0 + poly_at_depth * value0 for note, base_amp in zip(notes, base_amps): - note.amplitude = base_amp * boost + note.amplitude = gain(base_amp, boost) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 - elif data0 == 3: hpf_cutoff = logmap(value0, 20.0, 2020.0) + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 + elif data0 == 3: hpf_cutoff.a = logmap(value0, 20.0, 2020.0) elif data0 == 4: ring_speed = 0.1 + value0 * 20.0 elif data0 == 5: ring_depth = value0 - elif data0 == 6: layer2_mix = value0 + elif data0 == 6: + layer2_mix.a = value0 + layer1_mix.a = 1.0 - value0 elif data0 == 7: poly_at_depth = value0 elif data0 == 8: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 9: amp_d = logmap(value0, 0.05, 3.05) @@ -152,7 +157,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif data0 == 12: filt_a = logmap(value0, 0.001, 2.001) elif data0 == 13: filt_d = logmap(value0, 0.05, 3.05) elif data0 == 14: filt_s = value0 - elif data0 == 15: brilliance = value0 * 2.0 + elif data0 == 15: brilliance.a = value0 * 2.0 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, transport=transport) diff --git a/lib/audioinstruments/cz101.py b/lib/audioinstruments/cz101.py index 0894d91..a08e8c3 100644 --- a/lib/audioinstruments/cz101.py +++ b/lib/audioinstruments/cz101.py @@ -37,8 +37,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -55,14 +55,15 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - pd_depth = 4000.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + pd_depth = live(4000.0) pd_a = 0.05 pd_d = 0.5 - res = 1.0 - vib_rate = 5.0 - vib_depth = 0.0 + res = live(1.0) + vib_rate = live(5.0) + vib_depth = live(0.0) amp_a = 0.01 amp_d = 0.5 amp_s = 0.5 @@ -81,7 +82,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, pd_depth, pd_a, pd_d, res, vib_rate, vib_depth + nonlocal pd_a, pd_d nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -93,7 +94,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -104,9 +105,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, synthio.Math(synthio.MathOperation.SUM, 200.0, f_sweep, 0.0), Q=res) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.025) if vib_depth > 0.01 else None + # Vibrato Depth and Rate reach a held note only if the vibrato + # was on when the note started. + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.025)) if vib_depth.a > 0.01 else None - n = synthio.Note(hz, waveform=WAVE_PD, envelope=env, filter=lp, amplitude=amp * 0.8, bend=vib_lfo) + n = synthio.Note(hz, waveform=WAVE_PD, envelope=env, filter=lp, amplitude=gain(amp, 0.8), bend=vib_lfo) serial += 1 voices[k] = ((n,), serial) @@ -116,13 +119,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: pd_depth = value0 * 8000.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: pd_depth.a = value0 * 8000.0 elif data0 == 2: pd_a = logmap(value0, 0.001, 1.001) elif data0 == 3: pd_d = logmap(value0, 0.05, 2.05) - elif data0 == 4: res = 0.5 + value0 * 3.5 - elif data0 == 5: vib_rate = 0.1 + value0 * 10.0 - elif data0 == 6: vib_depth = value0 + elif data0 == 4: res.a = 0.5 + value0 * 3.5 + elif data0 == 5: vib_rate.a = 0.1 + value0 * 10.0 + elif data0 == 6: vib_depth.a = value0 elif data0 == 7: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 8: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 9: amp_s = value0 diff --git a/lib/audioinstruments/d50.py b/lib/audioinstruments/d50.py index 35cbf4c..117f1c4 100644 --- a/lib/audioinstruments/d50.py +++ b/lib/audioinstruments/d50.py @@ -37,8 +37,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -54,13 +54,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - trans_mix = 0.8 - synth_mix = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + trans_mix = live(0.8) + synth_mix = live(0.8) trans_tune = 1.0 - cutoff_val = 3000.0 - res = 1.0 + cutoff_val = live(3000.0) + res = live(1.0) chorus = 0.5 amp_a = 0.01 amp_d = 1.0 @@ -80,7 +81,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, trans_mix, synth_mix, trans_tune, cutoff_val, res, chorus + nonlocal trans_tune, chorus nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -92,7 +93,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # Synth body envelope env_synth = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -101,17 +102,19 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff_val, Q=res) + # A layer at zero adds no note, so its mix reaches a held note + # only if it was up when the note started. notes = [] - if trans_mix > 0.01: - notes.append(synthio.Note(hz * trans_tune, waveform=WAVE_TRANS, envelope=env_trans, filter=lp, amplitude=amp * trans_mix * 0.7)) + if trans_mix.a > 0.01: + notes.append(synthio.Note(hz * trans_tune, waveform=WAVE_TRANS, envelope=env_trans, filter=lp, amplitude=gain(amp, trans_mix, 0.7))) - if synth_mix > 0.01: + if synth_mix.a > 0.01: if chorus > 0.01: # Built in chorus typical of LA synthesis - notes.append(synthio.Note(hz, waveform=WAVE_SYNTH, envelope=env_synth, filter=lp, amplitude=amp * synth_mix * 0.4, panning=-0.4)) - notes.append(synthio.Note(hz * 1.003, waveform=WAVE_SYNTH, envelope=env_synth, filter=lp, amplitude=amp * synth_mix * 0.4, panning=0.4)) + notes.append(synthio.Note(hz, waveform=WAVE_SYNTH, envelope=env_synth, filter=lp, amplitude=gain(amp, synth_mix, 0.4), panning=-0.4)) + notes.append(synthio.Note(hz * 1.003, waveform=WAVE_SYNTH, envelope=env_synth, filter=lp, amplitude=gain(amp, synth_mix, 0.4), panning=0.4)) else: - notes.append(synthio.Note(hz, waveform=WAVE_SYNTH, envelope=env_synth, filter=lp, amplitude=amp * synth_mix * 0.7)) + notes.append(synthio.Note(hz, waveform=WAVE_SYNTH, envelope=env_synth, filter=lp, amplitude=gain(amp, synth_mix, 0.7))) serial += 1 voices[k] = (tuple(notes), serial) @@ -122,12 +125,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: trans_mix = value0 - elif data0 == 2: synth_mix = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: trans_mix.a = value0 + elif data0 == 2: synth_mix.a = value0 elif data0 == 3: trans_tune = 0.5 + value0 * 4.0 - elif data0 == 4: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 5: res = 0.5 + value0 * 3.5 + elif data0 == 4: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 5: res.a = 0.5 + value0 * 3.5 elif data0 == 6: chorus = value0 elif data0 == 7: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 8: amp_d = logmap(value0, 0.05, 4.05) diff --git a/lib/audioinstruments/dx7.py b/lib/audioinstruments/dx7.py index 6dd2038..2da6f0e 100644 --- a/lib/audioinstruments/dx7.py +++ b/lib/audioinstruments/dx7.py @@ -44,8 +44,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - make_table, ring_depth_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -62,22 +62,24 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - fm_amount = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + fm_amount = live(0.5) mod_ratio = 1.0 - feedback = 0.0 + feedback = live(0.0) e1_d = 1.5 e2_d = 0.8 e3_d = 0.2 rel_t = 0.5 - alg_mix = 0.5 + alg_mix = live(0.5) + alg_mellow = live(0.5) # 1 - Alg Mix att_t = 0.01 - brightness = 0.8 + brightness = live(0.8) trem_depth = 0.0 - vib_depth = 0.0 + vib_depth = live(0.0) trem_rate = 2.0 - vib_rate = 5.0 + vib_rate = live(5.0) master_tune = 1.0 voices = {} @@ -92,8 +94,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, fm_amount, mod_ratio, feedback, e1_d, e2_d, e3_d, rel_t - nonlocal alg_mix, att_t, brightness, trem_depth, vib_depth, trem_rate, vib_rate, master_tune + nonlocal mod_ratio, e1_d, e2_d, e3_d, rel_t + nonlocal att_t, trem_depth, trem_rate, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -104,7 +106,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # We simulate the FM modulators by having multiple carriers with different waveforms and decay times. # This gives the dynamic harmonic shift typical of DX7 EPiano. @@ -113,18 +115,20 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos env2 = synthio.Envelope(attack_time=att_t, decay_time=e2_d, release_time=rel_t, attack_level=1.0, sustain_level=0.0) env3 = synthio.Envelope(attack_time=att_t, decay_time=e3_d, release_time=rel_t, attack_level=1.0, sustain_level=0.0) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.02) if vib_depth > 0.01 else None + # Vibrato Depth and Rate reach a held note only if the vibrato + # was on when the note started. + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.02)) if vib_depth.a > 0.01 else None trem_wave = ring_depth_table(trem_depth) if trem_depth > 0.01 else None # Feedback: the DX7's self-modulating operator adds upper-harmonic # bite, so scale it into the metallic tine layer. # Alg mix: crossfades between the mellow (o1) and bright/metallic # (o3) layers, approximating an algorithm change. - o1 = synthio.Note(hz, waveform=EP_HARM_1, envelope=env1, amplitude=amp * 0.6 * (1.0 - alg_mix), + o1 = synthio.Note(hz, waveform=EP_HARM_1, envelope=env1, amplitude=gain(amp, 0.6, alg_mellow), ring_frequency=trem_rate, ring_waveform=trem_wave, bend=vib_lfo) - o2 = synthio.Note(hz * mod_ratio, waveform=EP_HARM_2, envelope=env2, amplitude=amp * 0.3 * fm_amount, + o2 = synthio.Note(hz * mod_ratio, waveform=EP_HARM_2, envelope=env2, amplitude=gain(amp, 0.3, fm_amount), ring_frequency=trem_rate, ring_waveform=trem_wave, bend=vib_lfo) - o3 = synthio.Note(hz, waveform=EP_HARM_3, envelope=env3, amplitude=amp * 0.4 * brightness * (alg_mix + feedback), + o3 = synthio.Note(hz, waveform=EP_HARM_3, envelope=env3, amplitude=gain(gain(amp, 0.4, brightness), synthio.Math(synthio.MathOperation.SUM, alg_mix, feedback, 0.0)), ring_frequency=trem_rate, ring_waveform=trem_wave, bend=vib_lfo) notes = [o1, o2, o3] @@ -137,21 +141,23 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: fm_amount = value0 * 2.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: fm_amount.a = value0 * 2.0 elif data0 == 2: mod_ratio = 1.0 + math.floor(value0 * 10.0) - elif data0 == 3: feedback = value0 + elif data0 == 3: feedback.a = value0 elif data0 == 4: e1_d = logmap(value0, 0.1, 4.1) elif data0 == 5: e2_d = logmap(value0, 0.05, 2.05) elif data0 == 6: e3_d = logmap(value0, 0.01, 1.01) elif data0 == 7: rel_t = logmap(value0, 0.05, 2.05) - elif data0 == 8: alg_mix = value0 + elif data0 == 8: + alg_mix.a = value0 + alg_mellow.a = 1.0 - value0 elif data0 == 9: att_t = logmap(value0, 0.001, 1.001) - elif data0 == 10: brightness = value0 * 2.0 + elif data0 == 10: brightness.a = value0 * 2.0 elif data0 == 11: trem_depth = value0 - elif data0 == 12: vib_depth = value0 + elif data0 == 12: vib_depth.a = value0 elif data0 == 13: trem_rate = 0.1 + value0 * 10.0 - elif data0 == 14: vib_rate = 0.1 + value0 * 15.0 + elif data0 == 14: vib_rate.a = 0.1 + value0 * 15.0 elif data0 == 15: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/emulator2.py b/lib/audioinstruments/emulator2.py index 4564fd7..68fe14b 100644 --- a/lib/audioinstruments/emulator2.py +++ b/lib/audioinstruments/emulator2.py @@ -36,8 +36,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, - logmap, make_table, noise_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, noise_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -53,17 +53,20 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - breath = 0.5 - brilliance = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + breath = live(0.5) + # Tone Brilliance. Emulator II has 27kHz sample rate, creating a + # slightly muffled/aliased top end + cutoff = live(2000.0 + 0.5 * 4000.0) amp_a = 0.1 amp_d = 0.5 amp_s = 0.8 amp_r = 0.5 - vib_rate = 5.0 - vib_depth = 0.0 - f_env = 0.5 + vib_rate = live(5.0) + vib_depth = live(0.0) + f_env = live(0.5) master_tune = 1.0 voices = {} @@ -78,8 +81,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, breath, brilliance, amp_a, amp_d, amp_s, amp_r - nonlocal vib_rate, vib_depth, f_env, master_tune + nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -90,25 +92,24 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) env_breath = synthio.Envelope(attack_time=amp_a*0.5, decay_time=0.2, release_time=amp_r, attack_level=1.0, sustain_level=0.0) - # Emulator II has 27kHz sample rate, creating a slightly muffled/aliased top end - cutoff = 2000.0 + brilliance * 4000.0 - - f_sweep = synthio.LFO(waveform=FALL, once=True, rate=1.0/amp_d, scale=f_env * 4000.0, interpolate=True) + f_sweep = synthio.LFO(waveform=FALL, once=True, rate=1.0/amp_d, scale=gain(f_env, 4000.0), interpolate=True) actual_c = synthio.Math(synthio.MathOperation.SUM, cutoff, f_sweep, 0.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, actual_c, Q=1.0) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.02) if vib_depth > 0.01 else None + # Vibrato Depth and Rate reach a held note only if the vibrato + # was on when the note started; likewise Breath Noise. + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.02)) if vib_depth.a > 0.01 else None notes = [] - notes.append(synthio.Note(hz, waveform=WAVE_FLUTE, envelope=env, filter=lp, amplitude=amp * 0.7, bend=vib_lfo)) - if breath > 0.01: + notes.append(synthio.Note(hz, waveform=WAVE_FLUTE, envelope=env, filter=lp, amplitude=gain(amp, 0.7), bend=vib_lfo)) + if breath.a > 0.01: bp_noise = synthio.Biquad(synthio.FilterMode.BAND_PASS, 3000.0, Q=2.0) - notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env_breath, filter=bp_noise, amplitude=amp * breath * 0.3)) + notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env_breath, filter=bp_noise, amplitude=gain(amp, breath, 0.3))) serial += 1 voices[k] = (tuple(notes), serial) @@ -119,16 +120,16 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: breath = value0 - elif data0 == 2: brilliance = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: breath.a = value0 + elif data0 == 2: cutoff.a = 2000.0 + value0 * 4000.0 elif data0 == 3: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 4: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 5: amp_s = value0 elif data0 == 6: amp_r = logmap(value0, 0.01, 4.01) - elif data0 == 7: vib_rate = 0.1 + value0 * 10.0 - elif data0 == 8: vib_depth = value0 - elif data0 == 9: f_env = value0 + elif data0 == 7: vib_rate.a = 0.1 + value0 * 10.0 + elif data0 == 8: vib_depth.a = value0 + elif data0 == 9: f_env.a = value0 elif data0 == 10: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/fairlight.py b/lib/audioinstruments/fairlight.py index d453e44..620bc89 100644 --- a/lib/audioinstruments/fairlight.py +++ b/lib/audioinstruments/fairlight.py @@ -36,8 +36,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, logmap, - make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -65,16 +65,21 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) patch = 0.0 # < 0.5 = Arr1, > 0.5 = Orch5 - bitcrush = 0.0 + # Bitcrush Approx: the crushed layer's level, the clean layer's level + # and the anti-aliasing cutoff, all set from the one knob. + bitcrush = live(0.0) + clean_lvl = live(1.0 - 0.0 * 0.6) + c_base = live(8000.0 - 0.0 * 5500.0) amp_a = 0.1 amp_d = 0.5 amp_s = 0.8 amp_r = 0.5 pitch_env = 0.0 - f_env = 0.0 + f_env = live(0.0) master_tune = 1.0 voices = {} @@ -89,7 +94,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, patch, bitcrush, amp_a, amp_d, amp_s, amp_r, pitch_env, f_env, master_tune + nonlocal patch, amp_a, amp_d, amp_s, amp_r, pitch_env, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -100,7 +105,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) is_orch = patch > 0.5 @@ -118,20 +123,20 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # Pitch envelope (for Orch5 "whack") bend = synthio.LFO(waveform=FALL, once=True, rate=1.0/0.1, scale=pitch_env) if pitch_env > 0.01 else None - # Fairlight anti-aliasing filter was weak/stepped, so it let the - # 8-bit quantization noise through - continuous with Bitcrush Approx - c_base = 8000.0 - bitcrush * 5500.0 - - # Filter envelope - f_sweep = synthio.LFO(waveform=FALL, once=True, rate=1.0/actual_d, scale=f_env * 5000.0, interpolate=True) + # Filter envelope. The base cutoff is c_base: the Fairlight's + # anti-aliasing filter was weak/stepped, so it let the 8-bit + # quantization noise through - continuous with Bitcrush Approx + f_sweep = synthio.LFO(waveform=FALL, once=True, rate=1.0/actual_d, scale=gain(f_env, 5000.0), interpolate=True) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, synthio.Math(synthio.MathOperation.SUM, c_base, f_sweep, 0.0), Q=1.0) notes = [] # Bitcrush Approx blends in the genuinely 8-bit-quantized table - - # real stair-step distortion, not just a darker filter - notes.append(synthio.Note(hz, waveform=wave, envelope=env, filter=lp, amplitude=amp * 0.7 * (1.0 - bitcrush * 0.6), bend=bend)) - if bitcrush > 0.01: - notes.append(synthio.Note(hz, waveform=wave_crushed, envelope=env, filter=lp, amplitude=amp * 0.7 * bitcrush, bend=bend)) + # real stair-step distortion, not just a darker filter. At zero + # the crushed layer adds no note, so the knob reaches a held note + # only if it was up when the note started. + notes.append(synthio.Note(hz, waveform=wave, envelope=env, filter=lp, amplitude=gain(amp, 0.7, clean_lvl), bend=bend)) + if bitcrush.a > 0.01: + notes.append(synthio.Note(hz, waveform=wave_crushed, envelope=env, filter=lp, amplitude=gain(amp, 0.7, bitcrush), bend=bend)) serial += 1 voices[k] = (tuple(notes), serial) @@ -142,15 +147,18 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: patch = value0 - elif data0 == 2: bitcrush = value0 + elif data0 == 2: + bitcrush.a = value0 + clean_lvl.a = 1.0 - value0 * 0.6 + c_base.a = 8000.0 - value0 * 5500.0 elif data0 == 3: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 4: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 5: amp_s = value0 elif data0 == 6: amp_r = logmap(value0, 0.01, 4.01) elif data0 == 7: pitch_env = value0 - elif data0 == 8: f_env = value0 + elif data0 == 8: f_env.a = value0 elif data0 == 9: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/farfisa.py b/lib/audioinstruments/farfisa.py index 094f5b2..9053620 100644 --- a/lib/audioinstruments/farfisa.py +++ b/lib/audioinstruments/farfisa.py @@ -34,7 +34,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,16 +58,17 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - bass = 1.0 - strings = 1.0 - flute = 1.0 - oboe = 0.5 - trumpet = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + bass = live(1.0) + strings = live(1.0) + flute = live(1.0) + oboe = live(0.5) + trumpet = live(0.5) multi_tone = 0.0 - vib_rate = 6.0 - vib_depth = 0.0 + vib_rate = live(6.0) + vib_depth = live(0.0) master_tune = 1.0 voices = {} @@ -81,7 +83,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, bass, strings, flute, oboe, trumpet, multi_tone, vib_rate, vib_depth, master_tune + nonlocal multi_tone, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -92,7 +94,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=0.01, decay_time=0.1, release_time=0.1, attack_level=1.0, sustain_level=1.0) @@ -103,19 +105,23 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos filter_mode = synthio.FilterMode.HIGH_PASS if boosted else synthio.FilterMode.LOW_PASS lp = synthio.Biquad(filter_mode, cutoff, Q=1.0) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.02) if vib_depth > 0.01 else None + # Vibrato Depth and Rate reach a held note only if the vibrato + # was on when the note started. + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.02)) if vib_depth.a > 0.01 else None w_str = WAVE_STR_BOOST if boosted else WAVE_STR w_flute = WAVE_FLUTE_BOOST if boosted else WAVE_FLUTE w_oboe = WAVE_OBOE_BOOST if boosted else WAVE_OBOE w_trumpet = WAVE_TRUMPET_BOOST if boosted else WAVE_TRUMPET + # A tab at zero adds no note, so it reaches a held note only if + # it was up when the note started. notes = [] - if bass > 0.01: notes.append(synthio.Note(hz * 0.5, waveform=w_str, envelope=env, filter=lp, amplitude=amp * bass * 0.2, bend=vib_lfo)) - if strings > 0.01: notes.append(synthio.Note(hz, waveform=w_str, envelope=env, filter=lp, amplitude=amp * strings * 0.2, bend=vib_lfo)) - if flute > 0.01: notes.append(synthio.Note(hz, waveform=w_flute, envelope=env, filter=lp, amplitude=amp * flute * 0.2, bend=vib_lfo)) - if oboe > 0.01: notes.append(synthio.Note(hz, waveform=w_oboe, envelope=env, filter=lp, amplitude=amp * oboe * 0.2, bend=vib_lfo)) - if trumpet > 0.01: notes.append(synthio.Note(hz, waveform=w_trumpet, envelope=env, filter=lp, amplitude=amp * trumpet * 0.2, bend=vib_lfo)) + if bass.a > 0.01: notes.append(synthio.Note(hz * 0.5, waveform=w_str, envelope=env, filter=lp, amplitude=gain(amp, bass, 0.2), bend=vib_lfo)) + if strings.a > 0.01: notes.append(synthio.Note(hz, waveform=w_str, envelope=env, filter=lp, amplitude=gain(amp, strings, 0.2), bend=vib_lfo)) + if flute.a > 0.01: notes.append(synthio.Note(hz, waveform=w_flute, envelope=env, filter=lp, amplitude=gain(amp, flute, 0.2), bend=vib_lfo)) + if oboe.a > 0.01: notes.append(synthio.Note(hz, waveform=w_oboe, envelope=env, filter=lp, amplitude=gain(amp, oboe, 0.2), bend=vib_lfo)) + if trumpet.a > 0.01: notes.append(synthio.Note(hz, waveform=w_trumpet, envelope=env, filter=lp, amplitude=gain(amp, trumpet, 0.2), bend=vib_lfo)) serial += 1 voices[k] = (tuple(notes), serial) @@ -126,15 +132,15 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: bass = value0 - elif data0 == 2: strings = value0 - elif data0 == 3: flute = value0 - elif data0 == 4: oboe = value0 - elif data0 == 5: trumpet = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: bass.a = value0 + elif data0 == 2: strings.a = value0 + elif data0 == 3: flute.a = value0 + elif data0 == 4: oboe.a = value0 + elif data0 == 5: trumpet.a = value0 elif data0 == 6: multi_tone = value0 - elif data0 == 7: vib_rate = 0.1 + value0 * 10.0 - elif data0 == 8: vib_depth = value0 + elif data0 == 7: vib_rate.a = 0.1 + value0 * 10.0 + elif data0 == 8: vib_depth.a = value0 elif data0 == 9: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/fs1r.py b/lib/audioinstruments/fs1r.py index 584bebc..265b379 100644 --- a/lib/audioinstruments/fs1r.py +++ b/lib/audioinstruments/fs1r.py @@ -35,8 +35,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, logmap, - make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -50,13 +50,16 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) vowel_a = 0.0 # 0=A, 1=E, 2=I, 3=O, 4=U (simplified mapping) vowel_b = 1.0 morph_speed = 0.5 fm_idx = 0.5 brightness = 0.8 + # The modulation depth FM Index and Brightness set together + fm_scale = live(0.5 * (0.04 + 0.8 * 0.18)) amp_a = 0.05 amp_d = 1.0 amp_s = 0.8 @@ -92,7 +95,7 @@ def get_formant_freq2(vowel_idx): else: return 600.0 def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, vowel_a, vowel_b, morph_speed, fm_idx, brightness + nonlocal vowel_a, vowel_b, morph_speed, fm_idx, brightness nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -104,7 +107,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -132,10 +135,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # like real FM) run through formant filters (the real FS1R's actual # party trick). Brightness raises the modulation index rather than # just the volume, since a brighter FM tone is a harder-modulated one. - fm_mod = synthio.LFO(waveform=SINE, rate=hz * 2.0, scale=fm_idx * (0.04 + brightness * 0.18)) if fm_idx > 0.01 else None + # Both reach a held note only if FM Index was up when it started. + fm_mod = synthio.LFO(waveform=SINE, rate=hz * 2.0, scale=fm_scale) if fm_idx > 0.01 else None - n1 = synthio.Note(hz, waveform=WAVE_FM, envelope=env, filter=bp1, amplitude=amp * 0.6, bend=fm_mod, panning=-0.3) - n2 = synthio.Note(hz, waveform=WAVE_FM, envelope=env, filter=bp2, amplitude=amp * 0.6, bend=fm_mod, panning=0.3) + n1 = synthio.Note(hz, waveform=WAVE_FM, envelope=env, filter=bp1, amplitude=gain(amp, 0.6), bend=fm_mod, panning=-0.3) + n2 = synthio.Note(hz, waveform=WAVE_FM, envelope=env, filter=bp2, amplitude=gain(amp, 0.6), bend=fm_mod, panning=0.3) serial += 1 voices[k] = ((n1, n2), serial) @@ -146,12 +150,16 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: vowel_a = value0 elif data0 == 2: vowel_b = value0 elif data0 == 3: morph_speed = value0 - elif data0 == 4: fm_idx = value0 - elif data0 == 5: brightness = value0 * 2.0 + elif data0 == 4: + fm_idx = value0 + fm_scale.a = fm_idx * (0.04 + brightness * 0.18) + elif data0 == 5: + brightness = value0 * 2.0 + fm_scale.a = fm_idx * (0.04 + brightness * 0.18) elif data0 == 6: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 7: amp_d = logmap(value0, 0.05, 4.05) elif data0 == 8: amp_s = value0 diff --git a/lib/audioinstruments/jp8000.py b/lib/audioinstruments/jp8000.py index 10d1a73..980b269 100644 --- a/lib/audioinstruments/jp8000.py +++ b/lib/audioinstruments/jp8000.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -56,13 +56,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2500.0 - resonance = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2500.0) + resonance = live(1.0) supersaw_detune = 0.2 - supersaw_mix = 0.8 - env_depth = 4000.0 + supersaw_mix = live(0.8) + env_depth = live(4000.0) chorus = 0.0 a_a = 0.01 @@ -72,7 +73,7 @@ def create(sample_rate, channel_count=2, transport=None): f_a = 0.01 f_d = 0.3 f_s = 0.5 - hpf = 20.0 + hpf = live(20.0) master_tune = 1.0 voices = {} @@ -87,8 +88,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, supersaw_detune, supersaw_mix, env_depth, chorus - nonlocal a_a, a_d, a_s, a_r, f_a, f_d, f_s, hpf, master_tune + nonlocal supersaw_detune, chorus + nonlocal a_a, a_d, a_s, a_r, f_a, f_d, f_s, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -99,7 +100,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=a_a, decay_time=a_d, release_time=a_r, attack_level=1.0, sustain_level=a_s) @@ -128,7 +129,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos ] pans = [0.0, 0.3, -0.3, 0.6, -0.6, 0.9, -0.9] - base_a = amp * (1.0 / 7.0) * supersaw_mix * 2.0 + base_a = gain(gain(amp, 1.0 / 7.0, supersaw_mix), 2.0) for i in range(7): d = detunes[i] @@ -146,12 +147,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 elif data0 == 3: supersaw_detune = value0 * 0.6 - elif data0 == 4: supersaw_mix = value0 - elif data0 == 5: env_depth = value0 * 8000.0 + elif data0 == 4: supersaw_mix.a = value0 + elif data0 == 5: env_depth.a = value0 * 8000.0 elif data0 == 6: chorus = value0 elif data0 == 7: a_a = logmap(value0, 0.001, 2.001) elif data0 == 8: a_d = logmap(value0, 0.05, 3.05) @@ -160,7 +161,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif data0 == 11: f_a = logmap(value0, 0.001, 2.001) elif data0 == 12: f_d = logmap(value0, 0.05, 3.05) elif data0 == 13: f_s = value0 - elif data0 == 14: hpf = logmap(value0, 20.0, 1020.0) + elif data0 == 14: hpf.a = logmap(value0, 20.0, 1020.0) elif data0 == 15: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/juno106.py b/lib/audioinstruments/juno106.py index 540c8a0..22f0d33 100644 --- a/lib/audioinstruments/juno106.py +++ b/lib/audioinstruments/juno106.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, logmap, - make_table, noise_table, pulse_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, live, + logmap, make_table, noise_table, pulse_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -60,18 +60,19 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2000.0 - resonance = 1.2 - sub_level = 0.5 - noise_level = 0.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2000.0) + resonance = live(1.2) + sub_level = live(0.5) + noise_level = live(0.0) chorus_depth = 0.5 - chorus_rate = 1.0 - hpf_cutoff = 40.0 + chorus_rate = live(1.0) + hpf_cutoff = live(40.0) pwm_amount = 0.5 - lfo_rate = 5.0 - env_depth = 3000.0 + lfo_rate = live(5.0) + env_depth = live(3000.0) amp_a = 0.01 amp_d = 0.3 @@ -91,8 +92,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, sub_level, noise_level, chorus_depth, chorus_rate - nonlocal hpf_cutoff, pwm_amount, lfo_rate, env_depth, amp_a, amp_d, amp_s, amp_r, filt_d + nonlocal chorus_depth, pwm_amount, amp_a, amp_d, amp_s, amp_r, filt_d nonlocal serial k = key_of(channel, note_id, data0) @@ -103,7 +103,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -126,13 +126,15 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos detune = chorus_depth * 0.01 pwm_wave = pulse_table(0.5 - pwm_amount * 0.4) - o_saw = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.4, panning=-chorus_depth, bend=chorus_lfo) - o_sq = synthio.Note(hz * (1.0 + detune), waveform=pwm_wave, envelope=env, filter=lp, amplitude=amp * 0.4, panning=chorus_depth, bend=chorus_lfo) - o_sub = synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=hp, amplitude=amp * sub_level * 0.4, panning=0.0) - n = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=hp, amplitude=amp * noise_level * 0.2) + o_saw = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.4), panning=-chorus_depth, bend=chorus_lfo) + o_sq = synthio.Note(hz * (1.0 + detune), waveform=pwm_wave, envelope=env, filter=lp, amplitude=gain(amp, 0.4), panning=chorus_depth, bend=chorus_lfo) + o_sub = synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=hp, amplitude=gain(amp, sub_level, 0.4), panning=0.0) + n = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=hp, amplitude=gain(amp, noise_level, 0.2)) notes = [o_saw, o_sq, o_sub] - if noise_level > 0.01: + # A layer at zero is not pressed at all, so Noise Level reaches a + # held note only if the noise was up when the note started. + if noise_level.a > 0.01: notes.append(n) serial += 1 @@ -144,17 +146,17 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 - elif data0 == 3: sub_level = value0 - elif data0 == 4: noise_level = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 + elif data0 == 3: sub_level.a = value0 + elif data0 == 4: noise_level.a = value0 elif data0 == 5: chorus_depth = value0 - elif data0 == 6: chorus_rate = 0.1 + value0 * 5.0 - elif data0 == 7: hpf_cutoff = logmap(value0, 20.0, 1020.0) + elif data0 == 6: chorus_rate.a = 0.1 + value0 * 5.0 + elif data0 == 7: hpf_cutoff.a = logmap(value0, 20.0, 1020.0) elif data0 == 8: pwm_amount = value0 - elif data0 == 9: lfo_rate = 0.1 + value0 * 20.0 - elif data0 == 10: env_depth = value0 * 8000.0 + elif data0 == 9: lfo_rate.a = 0.1 + value0 * 20.0 + elif data0 == 10: env_depth.a = value0 * 8000.0 elif data0 == 11: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 12: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 13: amp_s = value0 diff --git a/lib/audioinstruments/jupiter8.py b/lib/audioinstruments/jupiter8.py index f9db664..856e563 100644 --- a/lib/audioinstruments/jupiter8.py +++ b/lib/audioinstruments/jupiter8.py @@ -42,8 +42,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, pulse_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, pulse_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -55,16 +55,17 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2500.0 - resonance = 1.2 - env_depth = 4000.0 - cross_mod = 0.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2500.0) + resonance = live(1.2) + env_depth = live(4000.0) + cross_mod = live(0.0) unison_spread = 0.0 - lfo_rate = 5.0 + lfo_rate = live(5.0) release_time = 0.5 - hpf_cutoff = 40.0 + hpf_cutoff = live(40.0) pw = 0.5 amp_a = 0.01 @@ -86,8 +87,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, env_depth, cross_mod, unison_spread, lfo_rate, release_time - nonlocal amp_a, amp_d, amp_s, filt_a, filt_d, filt_s, hpf_cutoff, pw + nonlocal unison_spread, release_time + nonlocal amp_a, amp_d, amp_s, filt_a, filt_d, filt_s, pw nonlocal serial k = key_of(channel, note_id, data0) @@ -98,7 +99,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=release_time, attack_level=1.0, sustain_level=amp_s) @@ -115,24 +116,24 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # Jupiter 8 Unison creates massive sound if unison_spread > 0.01: - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.3, panning=-0.5) - o2 = synthio.Note(hz * (1.0 + unison_spread * 0.012), waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.3, panning=0.5) - o3 = synthio.Note(hz * (1.0 - unison_spread * 0.012), waveform=pw_wave, envelope=env, filter=lp, amplitude=amp * 0.3, panning=0.0) + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.3), panning=-0.5) + o2 = synthio.Note(hz * (1.0 + unison_spread * 0.012), waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.3), panning=0.5) + o3 = synthio.Note(hz * (1.0 - unison_spread * 0.012), waveform=pw_wave, envelope=env, filter=lp, amplitude=gain(amp, 0.3), panning=0.0) notes = [o1, o2, o3] else: - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5) + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5)) # Cross-modulation: synthio can't FM one oscillator's pitch from another's audio # signal, so approximate the warbling cross-mod character with a fast sub-audio # LFO bending osc2's pitch, scaled by the Cross Mod amount. - cm_lfo = synthio.LFO(rate=max(20.0, hz * 0.25), scale=cross_mod * 0.4) if cross_mod > 0.01 else None - o2 = synthio.Note(hz, waveform=pw_wave, envelope=env, filter=lp, amplitude=amp * 0.5, bend=cm_lfo) + cm_lfo = synthio.LFO(rate=max(20.0, hz * 0.25), scale=gain(cross_mod, 0.4)) if cross_mod.a > 0.01 else None + o2 = synthio.Note(hz, waveform=pw_wave, envelope=env, filter=lp, amplitude=gain(amp, 0.5), bend=cm_lfo) notes = [o1, o2] # The Jupiter-8 has no dedicated per-voice HPF; give the macro a real audible effect # by mixing in a quiet high-passed top-end layer whose brightness tracks HPF Cutoff # (single-filter-per-Note rules out a true series HPF -> VCF chain). hp = synthio.Biquad(synthio.FilterMode.HIGH_PASS, hpf_cutoff, Q=0.7) - notes.append(synthio.Note(hz, waveform=SAW, envelope=env, filter=hp, amplitude=amp * 0.15)) + notes.append(synthio.Note(hz, waveform=SAW, envelope=env, filter=hp, amplitude=gain(amp, 0.15))) serial += 1 voices[k] = (tuple(notes), serial) @@ -143,13 +144,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 - elif data0 == 3: env_depth = value0 * 8000.0 - elif data0 == 4: cross_mod = value0 * 2.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 + elif data0 == 3: env_depth.a = value0 * 8000.0 + elif data0 == 4: cross_mod.a = value0 * 2.0 elif data0 == 5: unison_spread = value0 - elif data0 == 6: lfo_rate = 0.1 + value0 * 20.0 + elif data0 == 6: lfo_rate.a = 0.1 + value0 * 20.0 elif data0 == 7: release_time = logmap(value0, 0.01, 4.01) elif data0 == 8: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 9: amp_d = logmap(value0, 0.05, 3.05) @@ -157,7 +158,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif data0 == 11: filt_a = logmap(value0, 0.001, 2.001) elif data0 == 12: filt_d = logmap(value0, 0.05, 3.05) elif data0 == 13: filt_s = value0 - elif data0 == 14: hpf_cutoff = logmap(value0, 20.0, 2020.0) + elif data0 == 14: hpf_cutoff.a = logmap(value0, 20.0, 2020.0) elif data0 == 15: pw = value0 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/k2600.py b/lib/audioinstruments/k2600.py index ba30cef..2e43b25 100644 --- a/lib/audioinstruments/k2600.py +++ b/lib/audioinstruments/k2600.py @@ -43,7 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, logmap, + make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -61,22 +62,23 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - l1_mix = 0.8 - l2_mix = 0.6 - l3_mix = 0.4 - l4_mix = 0.8 - shimmer = 0.0 - master_cutoff = 4000.0 - master_res = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + l1_mix = live(0.8) + l2_mix = live(0.6) + l3_mix = live(0.4) + l4_mix = live(0.8) + shimmer = live(0.0) + master_cutoff = live(4000.0) + master_res = live(1.0) a_a = 0.5 a_d = 1.0 a_s = 0.8 a_r = 1.5 - mod_wheel = 0.0 - mod_rate = 2.0 - mod_depth = 0.1 + mod_wheel = live(0.0) + mod_rate = live(2.0) + mod_depth = live(0.1) master_tune = 1.0 voices = {} @@ -91,8 +93,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, l1_mix, l2_mix, l3_mix, l4_mix, shimmer, master_cutoff, master_res - nonlocal a_a, a_d, a_s, a_r, mod_wheel, mod_rate, mod_depth, master_tune + nonlocal a_a, a_d, a_s, a_r, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -103,29 +104,31 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=a_a, decay_time=a_d, release_time=a_r, attack_level=1.0, sustain_level=a_s) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, master_cutoff, Q=master_res) # Mod wheel adds vibrato - vib = synthio.LFO(waveform=SINE, rate=mod_rate, scale=mod_depth * mod_wheel * 0.05) if mod_wheel > 0.01 else None + vib = synthio.LFO(waveform=SINE, rate=mod_rate, scale=gain(mod_depth, mod_wheel, 0.05)) if mod_wheel.a > 0.01 else None + # A layer at zero is not pressed at all, so its mix reaches a held + # note only if the layer was up when the note started. notes = [] - if l1_mix > 0.01: - notes.append(synthio.Note(hz, waveform=WAVE_STR, envelope=env, filter=lp, amplitude=amp * l1_mix * 0.3, bend=vib, panning=-0.3)) - if l2_mix > 0.01: - notes.append(synthio.Note(hz * 1.002, waveform=WAVE_CHOIR, envelope=env, filter=lp, amplitude=amp * l2_mix * 0.3, bend=vib, panning=0.3)) - if l3_mix > 0.01: - notes.append(synthio.Note(hz * 2.0, waveform=WAVE_BELL, envelope=env, filter=lp, amplitude=amp * l3_mix * 0.2, bend=vib, panning=0.0)) - if l4_mix > 0.01: - notes.append(synthio.Note(hz * 0.5, waveform=WAVE_BASS, envelope=env, filter=lp, amplitude=amp * l4_mix * 0.4, bend=vib, panning=0.0)) + if l1_mix.a > 0.01: + notes.append(synthio.Note(hz, waveform=WAVE_STR, envelope=env, filter=lp, amplitude=gain(amp, l1_mix, 0.3), bend=vib, panning=-0.3)) + if l2_mix.a > 0.01: + notes.append(synthio.Note(hz * 1.002, waveform=WAVE_CHOIR, envelope=env, filter=lp, amplitude=gain(amp, l2_mix, 0.3), bend=vib, panning=0.3)) + if l3_mix.a > 0.01: + notes.append(synthio.Note(hz * 2.0, waveform=WAVE_BELL, envelope=env, filter=lp, amplitude=gain(amp, l3_mix, 0.2), bend=vib, panning=0.0)) + if l4_mix.a > 0.01: + notes.append(synthio.Note(hz * 0.5, waveform=WAVE_BASS, envelope=env, filter=lp, amplitude=gain(amp, l4_mix, 0.4), bend=vib, panning=0.0)) # Shimmer simulation (high pitched detuned bells) - if shimmer > 0.01: - notes.append(synthio.Note(hz * 4.01, waveform=WAVE_BELL, envelope=env, filter=lp, amplitude=amp * shimmer * 0.1, panning=-0.5)) - notes.append(synthio.Note(hz * 3.99, waveform=WAVE_BELL, envelope=env, filter=lp, amplitude=amp * shimmer * 0.1, panning=0.5)) + if shimmer.a > 0.01: + notes.append(synthio.Note(hz * 4.01, waveform=WAVE_BELL, envelope=env, filter=lp, amplitude=gain(amp, shimmer, 0.1), panning=-0.5)) + notes.append(synthio.Note(hz * 3.99, waveform=WAVE_BELL, envelope=env, filter=lp, amplitude=gain(amp, shimmer, 0.1), panning=0.5)) serial += 1 voices[k] = (tuple(notes), serial) @@ -136,21 +139,21 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: l1_mix = value0 - elif data0 == 2: l2_mix = value0 - elif data0 == 3: l3_mix = value0 - elif data0 == 4: l4_mix = value0 - elif data0 == 5: shimmer = value0 - elif data0 == 6: master_cutoff = 50.0 * (100.0 ** value0) - elif data0 == 7: master_res = 0.5 + value0 * 3.5 + if data0 == 0: volume.a = value0 + elif data0 == 1: l1_mix.a = value0 + elif data0 == 2: l2_mix.a = value0 + elif data0 == 3: l3_mix.a = value0 + elif data0 == 4: l4_mix.a = value0 + elif data0 == 5: shimmer.a = value0 + elif data0 == 6: master_cutoff.a = 50.0 * (100.0 ** value0) + elif data0 == 7: master_res.a = 0.5 + value0 * 3.5 elif data0 == 8: a_a = logmap(value0, 0.001, 4.001) elif data0 == 9: a_d = logmap(value0, 0.05, 5.05) elif data0 == 10: a_s = value0 elif data0 == 11: a_r = logmap(value0, 0.01, 8.01) - elif data0 == 12: mod_wheel = value0 - elif data0 == 13: mod_rate = 0.1 + value0 * 10.0 - elif data0 == 14: mod_depth = value0 + elif data0 == 12: mod_wheel.a = value0 + elif data0 == 13: mod_rate.a = 0.1 + value0 * 10.0 + elif data0 == 14: mod_depth.a = value0 elif data0 == 15: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/karplus.py b/lib/audioinstruments/karplus.py index 0a104bd..aa3bd36 100644 --- a/lib/audioinstruments/karplus.py +++ b/lib/audioinstruments/karplus.py @@ -33,8 +33,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - noise_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, noise_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -129,11 +129,14 @@ def karplus_strong_table(hz, damping, pluck_pos, seed=1234): return out, loop_start, loop_start + delay_len synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. Volume and Body Resonance are blocks every sounding note reads, + # so turning them reaches a ringing string; the rest are read when a note + # starts. + volume = live(0.8) pluck_pos = 0.5 # where along the string it's plucked (comb-filters the burst) damping = 0.5 # how fast the delay loop's feedback dies out - body_res = 0.5 + body_freq = live(400.0 + 0.5 * 3000.0) # Body Resonance moves both + body_q = live(1.0 + 0.5 * 2.0) pick_hard = 0.5 decay_time = 2.0 master_tune = 1.0 @@ -150,7 +153,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, pluck_pos, damping, body_res, pick_hard, decay_time, master_tune + nonlocal pluck_pos, damping, pick_hard, decay_time, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -161,14 +164,14 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # 1. The string itself: a genuine Karplus-Strong delay/feedback # loop rendered into a table, so the ring is real comb-filtered # decay, not a static harmonic wavetable ks_wave, ks_loop_start, ks_loop_end = karplus_strong_table(hz, damping, pluck_pos) env_body = synthio.Envelope(attack_time=0.001, decay_time=decay_time, release_time=0.3, attack_level=1.0, sustain_level=0.0) - lp_body = synthio.Biquad(synthio.FilterMode.LOW_PASS, 400.0 + body_res * 3000.0, Q=1.0 + body_res * 2.0) + lp_body = synthio.Biquad(synthio.FilterMode.LOW_PASS, body_freq, Q=body_q) # 2. The pick/strike transient (short noise burst, harder pick = brighter) env_pick = synthio.Envelope(attack_time=0.001, decay_time=0.02 + pluck_pos * 0.05, release_time=0.01, attack_level=1.0, sustain_level=0.0) @@ -187,11 +190,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # mark one settled lap - by construction one true period of the # string - as the loop; everything before it plays once, as the # decaying attack transient, at the correct real-time rate. - notes.append(synthio.Note(hz, waveform=ks_wave, envelope=env_body, filter=lp_body, amplitude=amp * 0.8, + notes.append(synthio.Note(hz, waveform=ks_wave, envelope=env_body, filter=lp_body, amplitude=gain(amp, 0.8), waveform_loop_start=ks_loop_start, waveform_loop_end=ks_loop_end)) if pick_hard > 0.01: - notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env_pick, filter=hp_pick, amplitude=amp * pick_hard * 0.3)) + notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env_pick, filter=hp_pick, amplitude=gain(amp, pick_hard, 0.3))) serial += 1 voices[k] = (tuple(notes), serial) @@ -202,10 +205,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: pluck_pos = value0 elif data0 == 2: damping = value0 - elif data0 == 3: body_res = value0 + elif data0 == 3: + body_freq.a = 400.0 + value0 * 3000.0 + body_q.a = 1.0 + value0 * 2.0 elif data0 == 4: pick_hard = value0 elif data0 == 5: decay_time = logmap(value0, 0.5, 4.5) elif data0 == 6: master_tune = 0.95 + value0 * 0.1 diff --git a/lib/audioinstruments/mellotron.py b/lib/audioinstruments/mellotron.py index 8a1b43d..26cc19d 100644 --- a/lib/audioinstruments/mellotron.py +++ b/lib/audioinstruments/mellotron.py @@ -32,8 +32,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, - noise_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, logmap, + make_table, noise_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -48,11 +48,12 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - tone = 2000.0 - flutter_rate = 3.0 - flutter_depth = 0.1 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + tone = live(2000.0) + flutter_rate = live(3.0) + flutter_depth = live(0.1) att = 0.1 rel = 0.1 tape_hiss = 0.1 @@ -74,7 +75,7 @@ def update_hiss(): nonlocal hiss_note # Tape hiss continues as long as a note is pressed (tape engaged). if len(voices) > 0 and tape_hiss > 0.01: - target_amp = volume * tape_hiss * 0.1 + target_amp = volume.a * tape_hiss * 0.1 if hiss_note is None: env = synthio.Envelope(attack_time=0.1, decay_time=0.1, release_time=0.1, attack_level=1.0, sustain_level=1.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, 4000.0, Q=0.5) @@ -88,7 +89,7 @@ def update_hiss(): hiss_note = None def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, tone, flutter_rate, flutter_depth, att, rel, tape_hiss, master_tune + nonlocal att, rel, tape_hiss, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -99,17 +100,17 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=att, decay_time=0.1, release_time=rel, attack_level=1.0, sustain_level=1.0) # Tape wow/flutter - flutter_lfo = synthio.LFO(waveform=SINE, rate=flutter_rate, scale=flutter_depth * 0.012) if flutter_depth > 0.01 else None + flutter_lfo = synthio.LFO(waveform=SINE, rate=flutter_rate, scale=gain(flutter_depth, 0.012)) if flutter_depth.a > 0.01 else None # Bandpass filter for lo-fi tape sound bp = synthio.Biquad(synthio.FilterMode.BAND_PASS, tone, Q=0.8) - n = synthio.Note(hz, waveform=WAVE_FLUTE, envelope=env, filter=bp, amplitude=amp * 0.6, bend=flutter_lfo) + n = synthio.Note(hz, waveform=WAVE_FLUTE, envelope=env, filter=bp, amplitude=gain(amp, 0.6), bend=flutter_lfo) serial += 1 voices[k] = ((n,), serial) @@ -122,11 +123,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif event_type == EVENT_PARAMETER: if data0 == 0: - volume = value0 + volume.a = value0 update_hiss() - elif data0 == 1: tone = logmap(value0, 500.0, 4500.0) - elif data0 == 2: flutter_rate = 0.1 + value0 * 10.0 - elif data0 == 3: flutter_depth = value0 + elif data0 == 1: tone.a = logmap(value0, 500.0, 4500.0) + elif data0 == 2: flutter_rate.a = 0.1 + value0 * 10.0 + elif data0 == 3: flutter_depth.a = value0 elif data0 == 4: att = logmap(value0, 0.001, 2.001) elif data0 == 5: rel = logmap(value0, 0.01, 2.01) elif data0 == 6: diff --git a/lib/audioinstruments/microwave.py b/lib/audioinstruments/microwave.py index e092826..13850d2 100644 --- a/lib/audioinstruments/microwave.py +++ b/lib/audioinstruments/microwave.py @@ -38,8 +38,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -53,13 +53,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - wt_pos = 0.0 - cutoff_val = 2000.0 - res = 1.0 - env_wt = 1.0 - env_flt = 4000.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + wt_pos = live(0.0) + cutoff_val = live(2000.0) + res = live(1.0) + env_wt = live(1.0) + env_flt = live(4000.0) f_a = 0.01 amp_a = 0.01 amp_d = 0.5 @@ -79,7 +80,6 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, wt_pos, cutoff_val, res, env_wt, env_flt nonlocal f_a, amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -91,7 +91,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -104,14 +104,15 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # Env to Wavetable: the mix position sweeps from wt_pos to mix_end # over the amp decay time, driven by a live Math crossfade (Note # amplitude accepts a BlockInput, so this actually animates). - mix_end = min(1.0, max(0.0, wt_pos + env_wt)) + mix_end = synthio.Math(synthio.MathOperation.MID, 0.0, 1.0, + synthio.Math(synthio.MathOperation.SUM, wt_pos, env_wt, 0.0)) ramp = synthio.LFO(waveform=array.array("h", (0, 32767)), once=True, rate=1.0 / max(0.01, amp_d), interpolate=True) mix_pos = synthio.Math(synthio.MathOperation.LERP, wt_pos, mix_end, ramp) inv_pos = synthio.Math(synthio.MathOperation.ADD_SUB, 1.0, 0.0, mix_pos) - amp1 = synthio.Math(synthio.MathOperation.PRODUCT, inv_pos, amp * 0.7, 1.0) - amp2 = synthio.Math(synthio.MathOperation.PRODUCT, mix_pos, amp * 0.7, 1.0) + amp1 = synthio.Math(synthio.MathOperation.PRODUCT, inv_pos, gain(amp, 0.7), 1.0) + amp2 = synthio.Math(synthio.MathOperation.PRODUCT, mix_pos, gain(amp, 0.7), 1.0) notes = [ synthio.Note(hz, waveform=WAVE_1, envelope=env, filter=lp, amplitude=amp1), @@ -127,12 +128,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: wt_pos = value0 - elif data0 == 2: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 3.5 - elif data0 == 4: env_wt = -1.0 + value0 * 2.0 - elif data0 == 5: env_flt = value0 * 8000.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: wt_pos.a = value0 + elif data0 == 2: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 3.5 + elif data0 == 4: env_wt.a = -1.0 + value0 * 2.0 + elif data0 == 5: env_flt.a = value0 * 8000.0 elif data0 == 6: f_a = logmap(value0, 0.001, 2.001) elif data0 == 7: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 8: amp_d = logmap(value0, 0.05, 3.05) diff --git a/lib/audioinstruments/ms20.py b/lib/audioinstruments/ms20.py index 05f9414..8063837 100644 --- a/lib/audioinstruments/ms20.py +++ b/lib/audioinstruments/ms20.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, noise_table, ring_depth_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, noise_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -59,20 +59,21 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - hpf_cutoff = 100.0 - hpf_peak = 1.0 - lpf_cutoff = 2000.0 - lpf_peak = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + hpf_cutoff = live(100.0) + hpf_peak = live(1.0) + lpf_cutoff = live(2000.0) + lpf_peak = live(1.0) osc2_pitch = 1.0 - eg2_sweep = 3000.0 + eg2_sweep = live(3000.0) eg2_a = 0.01 eg2_d = 0.3 eg2_s = 0.5 eg2_r = 0.3 ring_mod = 0.0 - noise_lvl = 0.0 + noise_lvl = live(0.0) vca_a = 0.01 vca_r = 0.3 master_tune = 1.0 @@ -93,8 +94,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, hpf_cutoff, hpf_peak, lpf_cutoff, lpf_peak, osc2_pitch, eg2_sweep - nonlocal eg2_a, eg2_d, eg2_s, eg2_r, ring_mod, noise_lvl, vca_a, vca_r, master_tune + nonlocal osc2_pitch + nonlocal eg2_a, eg2_d, eg2_s, eg2_r, ring_mod, vca_a, vca_r, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -104,7 +105,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=vca_a, decay_time=eg2_d, release_time=vca_r, attack_level=1.0, sustain_level=1.0) @@ -121,15 +122,17 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos ring_wave = ring_depth_table(ring_mod) if ring_mod > 0.01 else None - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5, + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5), ring_frequency=hz, ring_waveform=ring_wave) - o2 = synthio.Note(hz * osc2_pitch, waveform=SQUARE, envelope=env, filter=hp, amplitude=amp * 0.5, + o2 = synthio.Note(hz * osc2_pitch, waveform=SQUARE, envelope=env, filter=hp, amplitude=gain(amp, 0.5), ring_frequency=hz, ring_waveform=ring_wave) notes = [o1, o2] lp_notes = [o1] - if noise_lvl > 0.01: + # A layer at zero is not pressed at all, so Noise Level reaches a + # held note only if the noise was up when the note started. + if noise_lvl.a > 0.01: noise_note = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=lp, - amplitude=amp * noise_lvl * 0.4) + amplitude=gain(amp, noise_lvl, 0.4)) notes.append(noise_note) lp_notes.append(noise_note) @@ -139,7 +142,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # post-construction, so this reassigns it rather than trying to # represent an indefinite sustain hold in the one-shot attack/decay # LFO above. - filt_release = (lpf_cutoff, eg2_sweep * eg2_s, eg2_r, lpf_peak) + filt_release = (lpf_cutoff, eg2_sweep.a * eg2_s, eg2_r, lpf_peak) serial += 1 voices[k] = (tuple(notes), serial, filt_release, tuple(lp_notes)) @@ -150,19 +153,19 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: hpf_cutoff = 20.0 * (100.0 ** value0) - elif data0 == 2: hpf_peak = 0.5 + value0 * 5.0 - elif data0 == 3: lpf_cutoff = 50.0 * (100.0 ** value0) - elif data0 == 4: lpf_peak = 0.5 + value0 * 8.0 # MS-20 is very resonant + if data0 == 0: volume.a = value0 + elif data0 == 1: hpf_cutoff.a = 20.0 * (100.0 ** value0) + elif data0 == 2: hpf_peak.a = 0.5 + value0 * 5.0 + elif data0 == 3: lpf_cutoff.a = 50.0 * (100.0 ** value0) + elif data0 == 4: lpf_peak.a = 0.5 + value0 * 8.0 # MS-20 is very resonant elif data0 == 5: osc2_pitch = 1.0 + (value0 - 0.5) - elif data0 == 6: eg2_sweep = value0 * 8000.0 + elif data0 == 6: eg2_sweep.a = value0 * 8000.0 elif data0 == 7: eg2_a = logmap(value0, 0.001, 2.001) elif data0 == 8: eg2_d = logmap(value0, 0.05, 3.05) elif data0 == 9: eg2_s = value0 elif data0 == 10: eg2_r = logmap(value0, 0.01, 4.01) elif data0 == 11: ring_mod = value0 - elif data0 == 12: noise_lvl = value0 + elif data0 == 12: noise_lvl.a = value0 elif data0 == 13: vca_a = logmap(value0, 0.001, 2.001) elif data0 == 14: vca_r = logmap(value0, 0.01, 4.01) elif data0 == 15: master_tune = 0.95 + value0 * 0.1 diff --git a/lib/audioinstruments/ms2000.py b/lib/audioinstruments/ms2000.py index e68d2d1..8a9b302 100644 --- a/lib/audioinstruments/ms2000.py +++ b/lib/audioinstruments/ms2000.py @@ -38,7 +38,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, logmap, + make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -63,14 +64,15 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) osc_wave = 0.0 # 0=Saw, 1=Square, 2=DWGS - cutoff_val = 2000.0 - res = 1.0 - mod_seq_rate = 4.0 - mod_seq_depth = 0.5 - distortion = 0.0 + cutoff_val = live(2000.0) + res = live(1.0) + mod_seq_rate = live(4.0) + mod_seq_depth = live(0.5) + distortion = live(0.0) amp_a = 0.01 amp_d = 0.5 amp_s = 0.8 @@ -89,7 +91,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, osc_wave, cutoff_val, res, mod_seq_rate, mod_seq_depth, distortion + nonlocal osc_wave nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -101,16 +103,18 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) # Mod Sequence: the MS2000's Mod Sequencer is a real 16-step value # sequencer, not an LFO shape - approximated here as an 8-step # stepped table driving the filter cutoff, one step per beat - # division set by Mod Seq Rate, depth set by Mod Seq Depth - mod_lfo = synthio.LFO(waveform=LFO_STEP, rate=mod_seq_rate, scale=mod_seq_depth * 3000.0) if mod_seq_depth > 0.01 else 0.0 - cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff_val, mod_lfo, 0.0) if mod_seq_depth > 0.01 else cutoff_val + # division set by Mod Seq Rate, depth set by Mod Seq Depth. A + # sequencer off at note-on stays off for that note. + mod_on = mod_seq_depth.a > 0.01 + mod_lfo = synthio.LFO(waveform=LFO_STEP, rate=mod_seq_rate, scale=gain(mod_seq_depth, 3000.0)) if mod_on else 0.0 + cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff_val, mod_lfo, 0.0) if mod_on else cutoff_val lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=res) @@ -118,11 +122,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos if osc_wave > 0.6: wave = DWGS elif osc_wave > 0.3: wave = SQUARE - notes = [synthio.Note(hz, waveform=wave, envelope=env, filter=lp, amplitude=amp * 0.7)] + notes = [synthio.Note(hz, waveform=wave, envelope=env, filter=lp, amplitude=gain(amp, 0.7))] # Distortion: fold in an unfiltered octave-up square for extra edge, # the way the MS2000's digital drive stage adds grit above the filter - if distortion > 0.01: - notes.append(synthio.Note(hz * 2.0, waveform=SQUARE, envelope=env, amplitude=amp * distortion * 0.2)) + if distortion.a > 0.01: + notes.append(synthio.Note(hz * 2.0, waveform=SQUARE, envelope=env, amplitude=gain(amp, distortion, 0.2))) serial += 1 voices[k] = (tuple(notes), serial) @@ -133,13 +137,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: osc_wave = value0 - elif data0 == 2: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 3.5 - elif data0 == 4: mod_seq_rate = 0.1 + value0 * 10.0 - elif data0 == 5: mod_seq_depth = value0 - elif data0 == 6: distortion = value0 * 2.0 + elif data0 == 2: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 3.5 + elif data0 == 4: mod_seq_rate.a = 0.1 + value0 * 10.0 + elif data0 == 5: mod_seq_depth.a = value0 + elif data0 == 6: distortion.a = value0 * 2.0 elif data0 == 7: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 8: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 9: amp_s = value0 diff --git a/lib/audioinstruments/music_easel.py b/lib/audioinstruments/music_easel.py index 1239fa3..7989394 100644 --- a/lib/audioinstruments/music_easel.py +++ b/lib/audioinstruments/music_easel.py @@ -34,8 +34,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, logmap, - make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, live, + logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -49,11 +49,13 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - timbre = 0.5 - fm_idx = 0.5 - mod_freq = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + sine_mix = live(1.0 - 0.5) # Timbre sets both halves of the blend + complex_mix = live(0.5) + fm_idx = live(0.5) + mod_rate = live(1.0 + 0.5 * 1000.0) # Mod Osc Freq lpg_strike = 0.8 lpg_decay = 0.5 amp_a = 0.01 @@ -73,7 +75,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, timbre, fm_idx, mod_freq, lpg_strike, lpg_decay + nonlocal lpg_strike, lpg_decay nonlocal amp_a, amp_s, amp_r, master_tune nonlocal serial @@ -84,7 +86,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # A vactrol-driven LPG ties amplitude and filter cutoff to one envelope; more # Strike means a harder snap (shorter decay, more percussive, brighter jolt), @@ -101,11 +103,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=0.5) # LPG has very low Q # FM emulation via vibrato on pitch - fm_lfo = synthio.LFO(waveform=SINE, rate=1.0 + mod_freq * 1000.0, scale=fm_idx * 0.1) if fm_idx > 0.01 else None + fm_lfo = synthio.LFO(waveform=SINE, rate=mod_rate, scale=gain(fm_idx, 0.1)) if fm_idx.a > 0.01 else None # We blend a sine with the complex wave depending on timbre - n1 = synthio.Note(hz, waveform=SINE, envelope=env, filter=lp, amplitude=amp * (1.0 - timbre), bend=fm_lfo) - n2 = synthio.Note(hz, waveform=WAVE_COMPLEX, envelope=env, filter=lp, amplitude=amp * timbre, bend=fm_lfo) + n1 = synthio.Note(hz, waveform=SINE, envelope=env, filter=lp, amplitude=gain(amp, sine_mix), bend=fm_lfo) + n2 = synthio.Note(hz, waveform=WAVE_COMPLEX, envelope=env, filter=lp, amplitude=gain(amp, complex_mix), bend=fm_lfo) serial += 1 voices[k] = ((n1, n2), serial) @@ -116,10 +118,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: timbre = value0 - elif data0 == 2: fm_idx = value0 - elif data0 == 3: mod_freq = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: + sine_mix.a = 1.0 - value0 + complex_mix.a = value0 + elif data0 == 2: fm_idx.a = value0 + elif data0 == 3: mod_rate.a = 1.0 + value0 * 1000.0 elif data0 == 4: lpg_strike = value0 elif data0 == 5: lpg_decay = logmap(value0, 0.05, 2.05) elif data0 == 6: amp_a = logmap(value0, 0.001, 2.001) diff --git a/lib/audioinstruments/nord_lead.py b/lib/audioinstruments/nord_lead.py index 017e1eb..d8946aa 100644 --- a/lib/audioinstruments/nord_lead.py +++ b/lib/audioinstruments/nord_lead.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,8 +57,11 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. Volume is a block every sounding note reads, so turning it + # reaches a held note; so are the cutoff, resonance and sweep depth that + # Cutoff, Resonance and Morph 1-3 combine into. The rest are read when a + # note starts. + volume = live(0.8) cutoff_base = 3000.0 resonance = 1.0 fm_amount = 0.0 @@ -75,6 +78,10 @@ def create(sample_rate, channel_count=2, transport=None): filt_d = 0.3 filt_s = 0.5 + actual_cutoff = live(cutoff_base * (1.0 + morph1 * 2.0)) + actual_res = live(resonance * (1.0 + morph2)) + sweep_depth = live(4000.0 * (1.0 + morph3)) + voices = {} serial = 0 MAX_VOICES = 12 @@ -87,7 +94,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, fm_amount, osc_sync, morph1, morph2, morph3, morph4 + nonlocal cutoff_base, resonance, fm_amount, osc_sync, morph1, morph2, morph3, morph4 nonlocal amp_a, amp_d, amp_s, amp_r, filt_a, filt_d, filt_s nonlocal serial @@ -99,21 +106,17 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) - # Morph parameters dynamically alter the tone - actual_cutoff = cutoff_base * (1.0 + morph1 * 2.0) - actual_res = resonance * (1.0 + morph2) - env_tbl = env_shape_table(filt_a, filt_d, filt_s) - f_sweep = synthio.LFO(waveform=env_tbl, once=True, rate=1.0/max(0.01, filt_a + filt_d), scale=4000.0 * (1.0 + morph3), interpolate=True) + f_sweep = synthio.LFO(waveform=env_tbl, once=True, rate=1.0/max(0.01, filt_a + filt_d), scale=sweep_depth, interpolate=True) cutoff = synthio.Math(synthio.MathOperation.SUM, actual_cutoff, f_sweep, 0.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=actual_res) - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5) + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5)) # FM emulation via detune/amplitude, plus Osc Sync: true audio-rate hard sync isn't # achievable in synthio (no per-sample phase reset), so approximate its buzzy, # harmonically-locked character by snapping osc2 toward an integer multiple of osc1's @@ -128,7 +131,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos else: sync_ratio = base_ratio o2_wave = SQUARE - o2 = synthio.Note(hz * sync_ratio, waveform=o2_wave, envelope=env, filter=lp, amplitude=amp * 0.5 * (0.5 + fm_amount)) + o2 = synthio.Note(hz * sync_ratio, waveform=o2_wave, envelope=env, filter=lp, amplitude=gain(amp, 0.5, 0.5 + fm_amount)) serial += 1 voices[k] = ((o1, o2), serial) @@ -139,7 +142,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) elif data0 == 2: resonance = 0.5 + value0 * 3.5 elif data0 == 3: fm_amount = value0 @@ -155,6 +158,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif data0 == 13: filt_a = logmap(value0, 0.001, 2.001) elif data0 == 14: filt_d = logmap(value0, 0.05, 3.05) elif data0 == 15: filt_s = value0 + # Morph parameters dynamically alter the tone + if data0 in (1, 2, 5, 6, 7): + actual_cutoff.a = cutoff_base * (1.0 + morph1 * 2.0) + actual_res.a = resonance * (1.0 + morph2) + sweep_depth.a = 4000.0 * (1.0 + morph3) instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, transport=transport) diff --git a/lib/audioinstruments/obxa.py b/lib/audioinstruments/obxa.py index 61ac3e4..ea6747b 100644 --- a/lib/audioinstruments/obxa.py +++ b/lib/audioinstruments/obxa.py @@ -36,8 +36,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -49,12 +49,13 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) detune_spread = 0.004 - cutoff_val = 2000.0 - res = 1.0 - env_depth = 4000.0 + cutoff_val = live(2000.0) + res = live(1.0) + env_depth = live(4000.0) brass_a = 0.05 brass_d = 0.5 amp_a = 0.05 @@ -74,7 +75,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, detune_spread, cutoff_val, res, env_depth, brass_a, brass_d + nonlocal detune_spread, brass_a, brass_d nonlocal amp_a, amp_s, amp_r, master_tune nonlocal serial @@ -86,7 +87,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=brass_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -97,8 +98,8 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=res) # Dual oscillator per voice with detune for massive sound - n1 = synthio.Note(hz * (1.0 - detune_spread), waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5, panning=-0.2) - n2 = synthio.Note(hz * (1.0 + detune_spread), waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5, panning=0.2) + n1 = synthio.Note(hz * (1.0 - detune_spread), waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5), panning=-0.2) + n2 = synthio.Note(hz * (1.0 + detune_spread), waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5), panning=0.2) serial += 1 voices[k] = ((n1, n2), serial) @@ -109,11 +110,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: detune_spread = value0 * 0.012 - elif data0 == 2: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 3.5 - elif data0 == 4: env_depth = value0 * 8000.0 + elif data0 == 2: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 3.5 + elif data0 == 4: env_depth.a = value0 * 8000.0 elif data0 == 5: brass_a = logmap(value0, 0.001, 2.001) elif data0 == 6: brass_d = logmap(value0, 0.05, 3.05) elif data0 == 7: amp_a = logmap(value0, 0.001, 2.001) diff --git a/lib/audioinstruments/odyssey.py b/lib/audioinstruments/odyssey.py index 6b17985..2fe1f84 100644 --- a/lib/audioinstruments/odyssey.py +++ b/lib/audioinstruments/odyssey.py @@ -44,7 +44,7 @@ from audioinstruments._support import ( EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, EVENT_CHANNEL_PRESSURE, - FALL, key_of, logmap, make_table, ring_depth_table, + FALL, gain, key_of, live, logmap, make_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,20 +57,21 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2500.0 - resonance = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2500.0) + resonance = live(1.0) ring_mod = 0.0 - lfo_rate = 5.0 - env_sweep = 3000.0 + lfo_rate = live(5.0) + env_sweep = live(3000.0) osc2_detune = 1.01 sync = 0.0 amp_a = 0.01 amp_d = 0.3 amp_s = 0.5 amp_r = 0.3 - hpf_cutoff = 40.0 + hpf_cutoff = live(40.0) ppc = 0.0 master_tune = 1.0 glide = 0.0 @@ -88,8 +89,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, ring_mod, lfo_rate, env_sweep, osc2_detune, sync - nonlocal amp_a, amp_d, amp_s, amp_r, hpf_cutoff, ppc, master_tune, glide + nonlocal ring_mod, osc2_detune, sync + nonlocal amp_a, amp_d, amp_s, amp_r, ppc, master_tune, glide nonlocal serial, last_pitch k = key_of(channel, note_id, data0) @@ -99,7 +100,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # Glide: the Odyssey's portamento is a bend LFO ramping from the last note's pitch # down to 0, same one-shot-LFO trick as the Minimoog's glide. @@ -140,9 +141,9 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # ring_frequency/ring_waveform rather than an amplitude trick. ring_wave = ring_depth_table(ring_mod) if ring_mod > 0.01 else None - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5, bend=bend, + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5), bend=bend, ring_frequency=hz * sync_detune, ring_waveform=ring_wave) - o2 = synthio.Note(hz * sync_detune, waveform=SQUARE, envelope=env, filter=hp, amplitude=amp * 0.5, bend=bend) + o2 = synthio.Note(hz * sync_detune, waveform=SQUARE, envelope=env, filter=hp, amplitude=gain(amp, 0.5), bend=bend) serial += 1 voices[k] = ((o1, o2), serial) @@ -161,19 +162,19 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos note.bend = ppc * value0 * 0.5 elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 elif data0 == 3: ring_mod = value0 - elif data0 == 4: lfo_rate = 0.1 + value0 * 20.0 - elif data0 == 5: env_sweep = value0 * 8000.0 + elif data0 == 4: lfo_rate.a = 0.1 + value0 * 20.0 + elif data0 == 5: env_sweep.a = value0 * 8000.0 elif data0 == 6: osc2_detune = 1.0 + (value0 - 0.5) * 0.03 elif data0 == 7: sync = value0 elif data0 == 8: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 9: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 10: amp_s = value0 elif data0 == 11: amp_r = logmap(value0, 0.01, 4.01) - elif data0 == 12: hpf_cutoff = logmap(value0, 20.0, 2020.0) + elif data0 == 12: hpf_cutoff.a = logmap(value0, 20.0, 2020.0) elif data0 == 13: ppc = value0 elif data0 == 14: master_tune = 0.95 + value0 * 0.1 elif data0 == 15: glide = value0 diff --git a/lib/audioinstruments/pianet.py b/lib/audioinstruments/pianet.py index 0a2e04c..f3404d5 100644 --- a/lib/audioinstruments/pianet.py +++ b/lib/audioinstruments/pianet.py @@ -35,7 +35,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, logmap, + make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -49,13 +50,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) pluck_attack = 0.5 - mellow_tone = 0.5 + cutoff = live(1000.0 + ((1.0 - 0.5) * 4000.0)) # Mellow Tone decay_time = 1.5 - vib_rate = 5.0 - vib_depth = 0.0 + vib_rate = live(5.0) + vib_depth = live(0.0) amp_a = 0.01 amp_s = 0.1 amp_r = 0.4 @@ -77,7 +79,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, pluck_attack, mellow_tone, decay_time, vib_rate, vib_depth + nonlocal pluck_attack, decay_time nonlocal amp_a, amp_s, amp_r, master_tune nonlocal serial @@ -89,18 +91,17 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=decay_time, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) pluck_env = synthio.Envelope(attack_time=0.001, decay_time=0.1, release_time=0.05, attack_level=1.0, sustain_level=0.0) - cutoff = 1000.0 + ((1.0 - mellow_tone) * 4000.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=0.7) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.025) if vib_depth > 0.01 else None + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.025)) if vib_depth.a > 0.01 else None - o_body = synthio.Note(hz, waveform=WAVE_PIANET, envelope=env, filter=lp, amplitude=amp * 0.7, bend=vib_lfo) - o_pluck = synthio.Note(hz, waveform=WAVE_PLUCK, envelope=pluck_env, filter=lp, amplitude=amp * pluck_attack * 0.4, bend=vib_lfo) + o_body = synthio.Note(hz, waveform=WAVE_PIANET, envelope=env, filter=lp, amplitude=gain(amp, 0.7), bend=vib_lfo) + o_pluck = synthio.Note(hz, waveform=WAVE_PLUCK, envelope=pluck_env, filter=lp, amplitude=gain(amp, pluck_attack, 0.4), bend=vib_lfo) serial += 1 voices[k] = (_support.press_voice(synth, voices, retired, @@ -110,12 +111,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: pluck_attack = value0 - elif data0 == 2: mellow_tone = value0 + elif data0 == 2: cutoff.a = 1000.0 + ((1.0 - value0) * 4000.0) elif data0 == 3: decay_time = logmap(value0, 0.5, 3.5) - elif data0 == 4: vib_rate = 0.1 + value0 * 10.0 - elif data0 == 5: vib_depth = value0 + elif data0 == 4: vib_rate.a = 0.1 + value0 * 10.0 + elif data0 == 5: vib_depth.a = value0 elif data0 == 6: amp_a = logmap(value0, 0.001, 0.201) elif data0 == 7: amp_s = value0 elif data0 == 8: amp_r = logmap(value0, 0.05, 2.05) diff --git a/lib/audioinstruments/polysix.py b/lib/audioinstruments/polysix.py index 95a6fec..cee5d09 100644 --- a/lib/audioinstruments/polysix.py +++ b/lib/audioinstruments/polysix.py @@ -37,7 +37,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, logmap, + make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,13 +58,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - sub_osc = 0.5 - cutoff_val = 2000.0 - res = 1.0 - ens_depth = 1.0 - pwm_rate = 2.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + sub_osc = live(0.5) + cutoff_val = live(2000.0) + res = live(1.0) + ens_depth = live(1.0) + pwm_rate = live(2.0) amp_a = 0.01 amp_d = 0.5 amp_s = 0.8 @@ -82,7 +84,6 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, sub_osc, cutoff_val, res, ens_depth, pwm_rate nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -94,34 +95,36 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff_val, Q=res) # Emulate bucket-brigade ensemble with pitch mod - ens_lfo1 = synthio.LFO(waveform=SINE, rate=0.6, scale=ens_depth * 0.01) if ens_depth > 0.01 else None - ens_lfo2 = synthio.LFO(waveform=SINE, rate=6.0, scale=ens_depth * 0.005) if ens_depth > 0.01 else None + # (an ensemble off at note-on stays off for that note) + ens_on = ens_depth.a > 0.01 + ens_lfo1 = synthio.LFO(waveform=SINE, rate=0.6, scale=gain(ens_depth, 0.01)) if ens_on else None + ens_lfo2 = synthio.LFO(waveform=SINE, rate=6.0, scale=gain(ens_depth, 0.005)) if ens_on else None # Real PWM: crossfade the DCO between a narrow and wide pulse table at the # PWM Rate, which sweeps the harmonic balance the way a duty-cycle sweep # would (synthio can't reshape one wavetable's duty cycle in real time). pwm_lfo = synthio.LFO(waveform=SINE, rate=pwm_rate, scale=0.5, offset=0.5) - amp_narrow = synthio.Math(synthio.MathOperation.SCALE_OFFSET, pwm_lfo, -(amp * 0.5), amp * 0.5) - amp_wide = synthio.Math(synthio.MathOperation.SCALE_OFFSET, pwm_lfo, amp * 0.5, 0.0) + amp_narrow = synthio.Math(synthio.MathOperation.SCALE_OFFSET, pwm_lfo, gain(amp, -0.5), gain(amp, 0.5)) + amp_wide = synthio.Math(synthio.MathOperation.SCALE_OFFSET, pwm_lfo, gain(amp, 0.5), 0.0) notes = [] # Main DCO, width-modulated notes.append(synthio.Note(hz, waveform=PULSE_NARROW, envelope=env, filter=lp, amplitude=amp_narrow, bend=ens_lfo1, panning=-0.3)) notes.append(synthio.Note(hz, waveform=PULSE_WIDE, envelope=env, filter=lp, amplitude=amp_wide, bend=ens_lfo1, panning=-0.3)) # Simulated ensemble spread - if ens_depth > 0.01: - notes.append(synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5, bend=ens_lfo2, panning=0.3)) + if ens_on: + notes.append(synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=gain(amp, 0.5), bend=ens_lfo2, panning=0.3)) # Sub osc - if sub_osc > 0.01: - notes.append(synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=lp, amplitude=amp * sub_osc * 0.5)) + if sub_osc.a > 0.01: + notes.append(synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=lp, amplitude=gain(amp, sub_osc, 0.5))) serial += 1 voices[k] = (tuple(notes), serial) @@ -132,12 +135,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: sub_osc = value0 - elif data0 == 2: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 3.5 - elif data0 == 4: ens_depth = value0 * 2.0 - elif data0 == 5: pwm_rate = 0.1 + value0 * 10.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: sub_osc.a = value0 + elif data0 == 2: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 3.5 + elif data0 == 4: ens_depth.a = value0 * 2.0 + elif data0 == 5: pwm_rate.a = 0.1 + value0 * 10.0 elif data0 == 6: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 7: amp_d = logmap(value0, 0.05, 3.05) elif data0 == 8: amp_s = value0 diff --git a/lib/audioinstruments/ppg_wave.py b/lib/audioinstruments/ppg_wave.py index 001b7cd..51a7860 100644 --- a/lib/audioinstruments/ppg_wave.py +++ b/lib/audioinstruments/ppg_wave.py @@ -37,8 +37,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -52,12 +52,13 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - wt_index = 0.5 - cutoff_base = 2000.0 - res = 1.5 - env_amount = 3000.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + wt_index = live(0.5) + cutoff_base = live(2000.0) + res = live(1.5) + env_amount = live(3000.0) f_a = 0.01 f_d = 0.3 amp_a = 0.01 @@ -78,7 +79,6 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, wt_index, cutoff_base, res, env_amount nonlocal f_a, f_d, amp_a, amp_s, amp_r, detune, master_tune nonlocal serial @@ -90,7 +90,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=f_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -106,8 +106,9 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # "wave envelope follows filter envelope" PPG factory-patch behavior. scan_tbl = env_shape_table(f_a, f_d, 1.0) scan_lfo = synthio.LFO(waveform=scan_tbl, once=True, rate=1.0/max(0.01, f_a + f_d), scale=wt_index, interpolate=True) - amp_a_expr = synthio.Math(synthio.MathOperation.SCALE_OFFSET, scan_lfo, -(amp * 0.4), amp * 0.4) - amp_b_expr = synthio.Math(synthio.MathOperation.SCALE_OFFSET, scan_lfo, amp * 0.4, 0.0) + layer_amp = gain(amp, 0.4) + amp_a_expr = synthio.Math(synthio.MathOperation.SCALE_OFFSET, scan_lfo, gain(layer_amp, -1.0), layer_amp) + amp_b_expr = synthio.Math(synthio.MathOperation.SCALE_OFFSET, scan_lfo, layer_amp, 0.0) notes = [ synthio.Note(hz, waveform=WAVE_A, envelope=env, filter=lp, amplitude=amp_a_expr, panning=-0.2), @@ -125,11 +126,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: wt_index = value0 - elif data0 == 2: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 3.5 - elif data0 == 4: env_amount = value0 * 8000.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: wt_index.a = value0 + elif data0 == 2: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 3.5 + elif data0 == 4: env_amount.a = value0 * 8000.0 elif data0 == 5: f_a = logmap(value0, 0.001, 2.001) elif data0 == 6: f_d = logmap(value0, 0.05, 4.05) elif data0 == 7: amp_a = logmap(value0, 0.001, 2.001) diff --git a/lib/audioinstruments/prophet5.py b/lib/audioinstruments/prophet5.py index 4d1b012..872a86a 100644 --- a/lib/audioinstruments/prophet5.py +++ b/lib/audioinstruments/prophet5.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -57,11 +57,12 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - cutoff_base = 2000.0 - resonance = 1.0 - env_amount = 3000.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + cutoff_base = live(2000.0) + resonance = live(1.0) + env_amount = live(3000.0) poly_mod = 0.0 osc2_detune = 1.01 sync = 0.0 @@ -74,8 +75,8 @@ def create(sample_rate, channel_count=2, transport=None): filt_d = 0.3 filt_s = 0.2 - lfo_rate = 5.0 - lfo_depth = 0.0 + lfo_rate = live(5.0) + lfo_scale = live(0.0) # LFO Depth, in Hz of cutoff voices = {} serial = 0 @@ -89,8 +90,8 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, cutoff_base, resonance, env_amount, poly_mod, osc2_detune, sync, release_time - nonlocal amp_a, amp_d, amp_s, filt_a, filt_d, filt_s, lfo_rate, lfo_depth + nonlocal poly_mod, osc2_detune, sync, release_time + nonlocal amp_a, amp_d, amp_s, filt_a, filt_d, filt_s nonlocal serial k = key_of(channel, note_id, data0) @@ -101,19 +102,19 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) - amp = volume * value0 + amp = gain(volume, value0, 0.5) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=release_time, attack_level=1.0, sustain_level=amp_s) env_tbl = env_shape_table(filt_a, filt_d, filt_s) f_sweep = synthio.LFO(waveform=env_tbl, once=True, rate=1.0/max(0.01, filt_a + filt_d), scale=env_amount, interpolate=True) - lfo = synthio.LFO(rate=lfo_rate, scale=lfo_depth * 1000.0) + lfo = synthio.LFO(rate=lfo_rate, scale=lfo_scale) cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff_base, f_sweep, lfo) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=resonance) - o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5) + o1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp) # Poly Mod: filter env routed to osc B frequency, approximated as a static detune offset. # Keep the offset small - at 0.1 a half-open Poly Mod moved osc B by 91 cents, which # swamped the Osc2 Detune control entirely and left the oscillator plainly out of tune. @@ -131,7 +132,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos else: sync_detune = actual_detune o2_wave = SQUARE - o2 = synthio.Note(hz * sync_detune, waveform=o2_wave, envelope=env, filter=lp, amplitude=amp * 0.5) + o2 = synthio.Note(hz * sync_detune, waveform=o2_wave, envelope=env, filter=lp, amplitude=amp) serial += 1 voices[k] = ((o1, o2), serial) @@ -142,10 +143,10 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 2: resonance = 0.5 + value0 * 3.5 - elif data0 == 3: env_amount = value0 * 8000.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 2: resonance.a = 0.5 + value0 * 3.5 + elif data0 == 3: env_amount.a = value0 * 8000.0 elif data0 == 4: poly_mod = value0 elif data0 == 5: osc2_detune = 1.0 + (value0 - 0.5) * 0.03 elif data0 == 6: sync = value0 @@ -156,8 +157,8 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos elif data0 == 11: filt_a = logmap(value0, 0.001, 2.001) elif data0 == 12: filt_d = logmap(value0, 0.05, 3.05) elif data0 == 13: filt_s = value0 - elif data0 == 14: lfo_rate = 0.1 + value0 * 20.0 - elif data0 == 15: lfo_depth = value0 + elif data0 == 14: lfo_rate.a = 0.1 + value0 * 20.0 + elif data0 == 15: lfo_scale.a = value0 * 1000.0 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, transport=transport) diff --git a/lib/audioinstruments/prophet_vs.py b/lib/audioinstruments/prophet_vs.py index 9f7dd18..096de19 100644 --- a/lib/audioinstruments/prophet_vs.py +++ b/lib/audioinstruments/prophet_vs.py @@ -43,8 +43,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, env_shape_table, gain, + key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -61,14 +61,15 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - joy_x = 0.5 - joy_y = 0.5 - cutoff_base = 2500.0 - resonance = 1.0 - env_amount = 3000.0 - chorus = 0.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + joy_x = live(0.5) + joy_y = live(0.5) + cutoff_base = live(2500.0) + resonance = live(1.0) + env_amount = live(3000.0) + chorus = live(0.0) f_a = 0.01 f_d = 0.5 @@ -96,7 +97,6 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, joy_x, joy_y, cutoff_base, resonance, env_amount, chorus nonlocal f_a, f_d, f_s, f_r, a_a, a_d, a_s, a_r, master_tune nonlocal serial @@ -108,7 +108,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=a_a, decay_time=a_d, release_time=a_r, attack_level=1.0, sustain_level=a_s) @@ -120,31 +120,39 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # Vector mixing math # A: top left (1-x, 1-y), B: top right (x, 1-y), C: bottom left (1-x, y), D: bottom right (x, y) - mix_a = (1.0 - joy_x) * (1.0 - joy_y) - mix_b = joy_x * (1.0 - joy_y) - mix_c = (1.0 - joy_x) * joy_y - mix_d = joy_x * joy_y + # The mixes are blocks too, so moving the joystick re-balances a + # held note. A wave whose mix is near zero when the note starts is + # not sounded at all, and stays silent for that note. + x, y = joy_x.a, joy_y.a + inv_x = synthio.Math(synthio.MathOperation.ADD_SUB, 1.0, 0.0, joy_x) + inv_y = synthio.Math(synthio.MathOperation.ADD_SUB, 1.0, 0.0, joy_y) + mix_a = gain(inv_x, inv_y) + mix_b = gain(joy_x, inv_y) + mix_c = gain(inv_x, joy_y) + mix_d = gain(joy_x, joy_y) # Chorus: an animated pitch wobble reads as chorus, not just stereo width, so add a - # slow bend LFO on top of the static spread. - chorus_lfo = synthio.LFO(waveform=SINE, rate=0.6, scale=chorus * 0.006) if chorus > 0.01 else None + # slow bend LFO on top of the static spread. Chorus reaches a held note only if + # it was up when the note started. + chorus_lfo = synthio.LFO(waveform=SINE, rate=0.6, scale=gain(chorus, 0.006)) if chorus.a > 0.01 else None + pan = gain(chorus, -1.0) notes = [] - if mix_a > 0.01: - notes.append(synthio.Note(hz, waveform=WAVE_A, envelope=env, filter=lp, amplitude=amp * mix_a, panning=-chorus, bend=chorus_lfo)) - if mix_b > 0.01: - notes.append(synthio.Note(hz * 1.002, waveform=WAVE_B, envelope=env, filter=lp, amplitude=amp * mix_b, panning=chorus, bend=chorus_lfo)) - if mix_c > 0.01: - notes.append(synthio.Note(hz * 0.998, waveform=WAVE_C, envelope=env, filter=lp, amplitude=amp * mix_c, panning=-chorus*0.5, bend=chorus_lfo)) - if mix_d > 0.01: - notes.append(synthio.Note(hz * 1.001, waveform=WAVE_D, envelope=env, filter=lp, amplitude=amp * mix_d, panning=chorus*0.5, bend=chorus_lfo)) + if (1.0 - x) * (1.0 - y) > 0.01: + notes.append(synthio.Note(hz, waveform=WAVE_A, envelope=env, filter=lp, amplitude=gain(amp, mix_a), panning=pan, bend=chorus_lfo)) + if x * (1.0 - y) > 0.01: + notes.append(synthio.Note(hz * 1.002, waveform=WAVE_B, envelope=env, filter=lp, amplitude=gain(amp, mix_b), panning=chorus, bend=chorus_lfo)) + if (1.0 - x) * y > 0.01: + notes.append(synthio.Note(hz * 0.998, waveform=WAVE_C, envelope=env, filter=lp, amplitude=gain(amp, mix_c), panning=gain(pan, 0.5), bend=chorus_lfo)) + if x * y > 0.01: + notes.append(synthio.Note(hz * 1.001, waveform=WAVE_D, envelope=env, filter=lp, amplitude=gain(amp, mix_d), panning=gain(chorus, 0.5), bend=chorus_lfo)) # Filter Release: retarget the shared filter to a real release sweep # at note-off (Note.filter is mutable post-construction), since the # one-shot attack/decay LFO above can't represent an indefinite # sustain hold followed by a release triggered by an unknown-in- # advance note-off time. - filt_release = (cutoff_base, env_amount * f_s, f_r, resonance) + filt_release = (cutoff_base, gain(env_amount, f_s), f_r, resonance) serial += 1 voices[k] = (tuple(notes), serial, filt_release, @@ -156,13 +164,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: joy_x = value0 - elif data0 == 2: joy_y = value0 - elif data0 == 3: cutoff_base = 50.0 * (100.0 ** value0) - elif data0 == 4: resonance = 0.5 + value0 * 3.5 - elif data0 == 5: env_amount = value0 * 8000.0 - elif data0 == 6: chorus = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: joy_x.a = value0 + elif data0 == 2: joy_y.a = value0 + elif data0 == 3: cutoff_base.a = 50.0 * (100.0 ** value0) + elif data0 == 4: resonance.a = 0.5 + value0 * 3.5 + elif data0 == 5: env_amount.a = value0 * 8000.0 + elif data0 == 6: chorus.a = value0 elif data0 == 7: f_a = logmap(value0, 0.001, 2.001) elif data0 == 8: f_d = logmap(value0, 0.05, 3.05) elif data0 == 9: f_s = value0 diff --git a/lib/audioinstruments/rhodes.py b/lib/audioinstruments/rhodes.py index 260f865..23652fc 100644 --- a/lib/audioinstruments/rhodes.py +++ b/lib/audioinstruments/rhodes.py @@ -38,8 +38,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, - make_table, noise_table, ring_depth_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, noise_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -58,14 +58,15 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - tine_lvl = 0.8 - body_lvl = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + tine_lvl = live(0.8) + body_lvl = live(1.0) trem_rate = 3.0 trem_depth = 0.0 - overdrive = 1.0 - tone = 3000.0 + overdrive = live(1.0) + tone = live(3000.0) amp_a = 0.01 amp_d = 2.0 amp_s = 0.2 @@ -87,7 +88,7 @@ def release_voice(k): if voice is not None and key_off > 0.01: env = synthio.Envelope(attack_time=0.001, decay_time=0.1, release_time=0.1, attack_level=1.0, sustain_level=0.0) bp = synthio.Biquad(synthio.FilterMode.BAND_PASS, 500.0, Q=1.0) - n = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=bp, amplitude=volume * key_off * 0.2) + n = synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=bp, amplitude=gain(volume, key_off, 0.2)) synth.press(n) # Only a noise burst the engine actually took holds a channel, # and only that one is worth offering back to a later key. @@ -99,7 +100,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, tine_lvl, body_lvl, trem_rate, trem_depth, overdrive, tone + nonlocal trem_rate, trem_depth nonlocal amp_a, amp_d, amp_s, amp_r, key_off, master_tune nonlocal serial @@ -111,7 +112,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 * overdrive + amp = gain(volume, value0, overdrive) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) tine_env = synthio.Envelope(attack_time=0.001, decay_time=0.3, release_time=0.1, attack_level=1.0, sustain_level=0.0) @@ -126,8 +127,8 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos pan_l = -0.3 pan_r = 0.3 - o_body = synthio.Note(hz, waveform=WAVE_BODY, envelope=env, filter=lp, amplitude=amp * body_lvl * 0.5, ring_frequency=trem_rate, ring_waveform=trem_wave, panning=pan_l) - o_tine = synthio.Note(hz, waveform=WAVE_TINE, envelope=tine_env, filter=lp, amplitude=amp * tine_lvl * 0.4, ring_frequency=trem_rate, ring_waveform=trem_wave, panning=pan_r) + o_body = synthio.Note(hz, waveform=WAVE_BODY, envelope=env, filter=lp, amplitude=gain(amp, body_lvl, 0.5), ring_frequency=trem_rate, ring_waveform=trem_wave, panning=pan_l) + o_tine = synthio.Note(hz, waveform=WAVE_TINE, envelope=tine_env, filter=lp, amplitude=gain(amp, tine_lvl, 0.4), ring_frequency=trem_rate, ring_waveform=trem_wave, panning=pan_r) serial += 1 voices[k] = (_support.press_voice(synth, voices, retired, @@ -137,13 +138,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: tine_lvl = value0 * 2.0 - elif data0 == 2: body_lvl = value0 * 2.0 + if data0 == 0: volume.a = value0 + elif data0 == 1: tine_lvl.a = value0 * 2.0 + elif data0 == 2: body_lvl.a = value0 * 2.0 elif data0 == 3: trem_rate = 0.1 + value0 * 10.0 elif data0 == 4: trem_depth = value0 - elif data0 == 5: overdrive = 1.0 + value0 * 2.0 - elif data0 == 6: tone = logmap(value0, 500.0, 5500.0) + elif data0 == 5: overdrive.a = 1.0 + value0 * 2.0 + elif data0 == 6: tone.a = logmap(value0, 500.0, 5500.0) elif data0 == 7: amp_a = logmap(value0, 0.001, 0.501) elif data0 == 8: amp_d = logmap(value0, 0.5, 4.5) elif data0 == 9: amp_s = value0 diff --git a/lib/audioinstruments/sh101.py b/lib/audioinstruments/sh101.py index c678625..dd62661 100644 --- a/lib/audioinstruments/sh101.py +++ b/lib/audioinstruments/sh101.py @@ -33,8 +33,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -54,13 +54,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) pw = 0.5 - sub_osc = 0.8 - cutoff_val = 1500.0 - res = 1.0 - env_depth = 3000.0 + sub_osc = live(0.8) + cutoff_val = live(1500.0) + res = live(1.0) + env_depth = live(3000.0) fast_decay = 0.2 master_tune = 1.0 @@ -76,7 +77,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, pw, sub_osc, cutoff_val, res, env_depth, fast_decay, master_tune + nonlocal pw, fast_decay, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -86,7 +87,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=0.01, decay_time=fast_decay, release_time=0.1, attack_level=1.0, sustain_level=0.1) @@ -99,8 +100,8 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # duty-cycle table per note-on from the current Pulse Width macro. duty = 0.05 + pw * 0.9 pulse_wave = make_pulse_table(duty) - n1 = synthio.Note(hz, waveform=pulse_wave, envelope=env, filter=lp, amplitude=amp * 0.5) - n_sub = synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=lp, amplitude=amp * sub_osc * 0.5) + n1 = synthio.Note(hz, waveform=pulse_wave, envelope=env, filter=lp, amplitude=gain(amp, 0.5)) + n_sub = synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=lp, amplitude=gain(amp, sub_osc, 0.5)) serial += 1 voices[k] = ((n1, n_sub), serial) @@ -111,12 +112,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: pw = value0 - elif data0 == 2: sub_osc = value0 - elif data0 == 3: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 4: res = 0.5 + value0 * 3.5 - elif data0 == 5: env_depth = value0 * 6000.0 + elif data0 == 2: sub_osc.a = value0 + elif data0 == 3: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 4: res.a = 0.5 + value0 * 3.5 + elif data0 == 5: env_depth.a = value0 * 6000.0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. diff --git a/lib/audioinstruments/solina.py b/lib/audioinstruments/solina.py index 2d284e3..fc17b00 100644 --- a/lib/audioinstruments/solina.py +++ b/lib/audioinstruments/solina.py @@ -33,7 +33,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -46,12 +47,13 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - violin = 1.0 - viola = 0.5 - cello = 0.5 - chorus_depth = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + violin = live(1.0) + viola = live(0.5) + cello = live(0.5) + chorus_depth = live(1.0) att = 0.1 rel = 0.5 crescendo = 0.0 @@ -69,7 +71,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, violin, viola, cello, chorus_depth, att, rel, crescendo, master_tune + nonlocal att, rel, crescendo, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -80,7 +82,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) # Crescendo makes attack slower based on velocity or macro actual_att = att + (crescendo * 2.0) @@ -95,11 +97,15 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos rates = [0.6, 6.0, 4.0] pans = [-0.5, 0.0, 0.5] + # A register whose mix is near zero when the note starts is not + # sounded at all, so its mix reaches a held note only if it was up. + chorus_scale = gain(chorus_depth, 0.008) for reg_mult, reg_vol in [(1.0, violin), (0.5, viola), (0.25, cello)]: - if reg_vol > 0.01: + if reg_vol.a > 0.01: + reg_amp = gain(amp, reg_vol, 0.15) for i in range(3): - mod_lfo = synthio.LFO(waveform=SINE, rate=rates[i], scale=chorus_depth * 0.008) - n = synthio.Note(hz * reg_mult, waveform=SAW, envelope=env, filter=lp, amplitude=amp * reg_vol * 0.15, bend=mod_lfo, panning=pans[i]) + mod_lfo = synthio.LFO(waveform=SINE, rate=rates[i], scale=chorus_scale) + n = synthio.Note(hz * reg_mult, waveform=SAW, envelope=env, filter=lp, amplitude=reg_amp, bend=mod_lfo, panning=pans[i]) notes.append(n) serial += 1 @@ -111,11 +117,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: violin = value0 - elif data0 == 2: viola = value0 - elif data0 == 3: cello = value0 - elif data0 == 4: chorus_depth = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: violin.a = value0 + elif data0 == 2: viola.a = value0 + elif data0 == 3: cello.a = value0 + elif data0 == 4: chorus_depth.a = value0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. diff --git a/lib/audioinstruments/taurus.py b/lib/audioinstruments/taurus.py index e73a524..05f417e 100644 --- a/lib/audioinstruments/taurus.py +++ b/lib/audioinstruments/taurus.py @@ -38,8 +38,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -52,13 +52,14 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) osc_b_detune = 0.01 glide = 0.0 - cutoff_val = 1000.0 - res = 1.0 - env_mod = 2000.0 + cutoff_val = live(1000.0) + res = live(1.0) + env_mod = live(2000.0) beat_freq = 0.5 amp_a = 0.01 amp_d = 1.0 @@ -79,7 +80,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, osc_b_detune, glide, cutoff_val, res, env_mod, beat_freq + nonlocal osc_b_detune, glide, beat_freq nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial, last_pitch @@ -90,7 +91,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) bend = None if last_pitch is not None and glide > 0.001: @@ -114,11 +115,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos beat_hz = 0.15 + beat_freq * 1.5 actual_detune = osc_b_detune + (beat_hz / hz) - n1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.45, bend=bend) - n2 = synthio.Note(hz * (1.0 + actual_detune), waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.45, bend=bend) + saw_amp = gain(amp, 0.45) + n1 = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=saw_amp, bend=bend) + n2 = synthio.Note(hz * (1.0 + actual_detune), waveform=SAW, envelope=env, filter=lp, amplitude=saw_amp, bend=bend) # Taurus pedals are defined by huge sub content; add a fixed sub-octave sine # under the two detuned saws for the low-end weight the real pedal has. - n3 = synthio.Note(hz * 0.5, waveform=SINE, envelope=env, filter=lp, amplitude=amp * 0.5, bend=bend) + n3 = synthio.Note(hz * 0.5, waveform=SINE, envelope=env, filter=lp, amplitude=gain(amp, 0.5), bend=bend) serial += 1 voices[k] = ((n1, n2, n3), serial) @@ -130,12 +132,12 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: osc_b_detune = value0 * 0.02 elif data0 == 2: glide = value0 - elif data0 == 3: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 4: res = 0.5 + value0 * 5.5 - elif data0 == 5: env_mod = value0 * 5000.0 + elif data0 == 3: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 4: res.a = 0.5 + value0 * 5.5 + elif data0 == 5: env_mod.a = value0 * 5000.0 elif data0 == 6: beat_freq = value0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds diff --git a/lib/audioinstruments/tb303.py b/lib/audioinstruments/tb303.py index 76b2629..3d69fa5 100644 --- a/lib/audioinstruments/tb303.py +++ b/lib/audioinstruments/tb303.py @@ -33,8 +33,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -46,15 +46,16 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) tuning = 1.0 - cutoff_val = 500.0 - res = 2.0 - env_mod = 4000.0 + cutoff_val = live(500.0) + res = live(2.0) + env_mod = live(4000.0) decay_time = 0.5 - accent = 0.0 - overdrive = 1.0 + accent = live(0.0) + overdrive = live(1.0) master_tune = 1.0 voices = {} @@ -69,7 +70,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, tuning, cutoff_val, res, env_mod, decay_time, accent, overdrive, master_tune + nonlocal tuning, decay_time, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -83,10 +84,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # The 303's accent circuit feeds extra voltage into both the VCF envelope # generator and the VCA at once, snapping the filter open harder and louder # together - scale continuously with the accent knob, not a hard switch. - actual_decay = decay_time * (1.0 - 0.7 * accent) - actual_env_mod = env_mod * (1.0 + 1.2 * accent) + # A held note follows Accent's level and envelope depth; its decay + # time is set when the note starts. + actual_decay = decay_time * (1.0 - 0.7 * accent.a) + actual_env_mod = gain(env_mod, synthio.Math(synthio.MathOperation.SUM, 1.0, gain(accent, 1.2), 0.0)) - amp = volume * value0 * overdrive * (1.0 + 0.5 * accent) + accent_amp = synthio.Math(synthio.MathOperation.SUM, 1.0, gain(accent, 0.5), 0.0) + amp = gain(gain(volume, value0, overdrive), accent_amp, 0.5) env = synthio.Envelope(attack_time=0.01, decay_time=actual_decay, release_time=0.1, attack_level=1.0, sustain_level=0.1) @@ -95,7 +99,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=res) - n = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp * 0.5) + n = synthio.Note(hz, waveform=SAW, envelope=env, filter=lp, amplitude=amp) serial += 1 voices[k] = ((n,), serial) @@ -105,17 +109,17 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: tuning = 0.5 + value0 - elif data0 == 2: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 4.5 - elif data0 == 4: env_mod = value0 * 8000.0 + elif data0 == 2: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 4.5 + elif data0 == 4: env_mod.a = value0 * 8000.0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. elif data0 == 5: decay_time = logmap(value0, 0.05, 2.05) - elif data0 == 6: accent = value0 - elif data0 == 7: overdrive = 1.0 + value0 * 3.0 + elif data0 == 6: accent.a = value0 + elif data0 == 7: overdrive.a = 1.0 + value0 * 3.0 elif data0 == 8: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/virus.py b/lib/audioinstruments/virus.py index a7bd4b6..373e0aa 100644 --- a/lib/audioinstruments/virus.py +++ b/lib/audioinstruments/virus.py @@ -37,8 +37,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, key_of, - logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, FALL, gain, key_of, + live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -51,14 +51,15 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) hs_detune = 0.2 - sub_osc = 0.5 - cutoff_val = 2000.0 - res = 1.0 - distortion = 1.0 - env_depth = 4000.0 + sub_osc = live(0.5) + cutoff_val = live(2000.0) + res = live(1.0) + distortion = live(1.0) + env_depth = live(4000.0) amp_a = 0.01 amp_d = 0.5 amp_s = 0.8 @@ -77,7 +78,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, hs_detune, sub_osc, cutoff_val, res, distortion, env_depth + nonlocal hs_detune nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -89,14 +90,15 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) f_sweep = synthio.LFO(waveform=FALL, once=True, rate=1.0/amp_d, scale=env_depth, interpolate=True) # Distortion pushes the filter brighter too, mimicking the extra # harmonic energy an analog-modeled overdrive stage would expose - cutoff = synthio.Math(synthio.MathOperation.SUM, cutoff_val * (1.0 + distortion * 0.6), f_sweep, 0.0) + drive_bright = synthio.Math(synthio.MathOperation.SCALE_OFFSET, distortion, 0.6, 1.0) + cutoff = synthio.Math(synthio.MathOperation.SUM, gain(cutoff_val, drive_bright), f_sweep, 0.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=res) @@ -106,18 +108,20 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos detunes = [0.0, hs_detune * 0.03, -hs_detune * 0.03, hs_detune * 0.06, -hs_detune * 0.06] pans = [0.0, 0.4, -0.4, 0.8, -0.8] - base_a = amp * 0.2 + base_a = gain(amp, 0.2) for i in range(5): notes.append(synthio.Note(hz * (1.0 + detunes[i]), waveform=SAW, envelope=env, filter=lp, amplitude=base_a, panning=pans[i])) - if sub_osc > 0.01: - notes.append(synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=lp, amplitude=amp * sub_osc * 0.4)) + # A layer at zero is not sounded at all, so Sub Osc and Distortion + # reach a held note's layer only if it was up when the note started. + if sub_osc.a > 0.01: + notes.append(synthio.Note(hz * 0.5, waveform=SQUARE, envelope=env, filter=lp, amplitude=gain(amp, sub_osc, 0.4))) # Distortion: an added octave-up harmonic layer through a hotter, # un-lowpassed square wave approximates the extra odd/even overtones # a real overdrive/waveshaper stage would add on top of the saws - if distortion > 0.01: - notes.append(synthio.Note(hz * 2.0, waveform=SQUARE, envelope=env, amplitude=amp * distortion * 0.25)) + if distortion.a > 0.01: + notes.append(synthio.Note(hz * 2.0, waveform=SQUARE, envelope=env, amplitude=gain(amp, distortion, 0.25))) serial += 1 voices[k] = (tuple(notes), serial) @@ -128,13 +132,13 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: hs_detune = value0 * 0.6 - elif data0 == 2: sub_osc = value0 - elif data0 == 3: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 4: res = 0.5 + value0 * 3.5 - elif data0 == 5: distortion = value0 * 2.0 - elif data0 == 6: env_depth = value0 * 8000.0 + elif data0 == 2: sub_osc.a = value0 + elif data0 == 3: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 4: res.a = 0.5 + value0 * 3.5 + elif data0 == 5: distortion.a = value0 * 2.0 + elif data0 == 6: env_depth.a = value0 * 8000.0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. diff --git a/lib/audioinstruments/vl1.py b/lib/audioinstruments/vl1.py index 96bf41c..93e07a9 100644 --- a/lib/audioinstruments/vl1.py +++ b/lib/audioinstruments/vl1.py @@ -35,7 +35,7 @@ from audioinstruments._support import ( EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, EVENT_POLY_PRESSURE, - EVENT_CHANNEL_PRESSURE, key_of, logmap, make_table, + EVENT_CHANNEL_PRESSURE, gain, key_of, live, logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -49,16 +49,17 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - breath = 0.8 - embouchure = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + breath = live(0.8) + embouchure = live(0.5) # Normalised 0..1, not Hz: the macro sets this straight from value0 and the # LFO reads rate = 1.0 + growl_rate * 20.0. Written as 5.0 it meant 5 Hz, # which is both unreachable from the macro and a 101 Hz "growl". 0.2 gives # the 5 Hz that was plainly intended. - growl_rate = 0.2 - growl_depth = 0.0 + growl_rate = live(0.2) + growl_depth = live(0.0) amp_a = 0.1 amp_d = 0.5 amp_s = 0.8 @@ -77,7 +78,6 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, breath, embouchure, growl_rate, growl_depth nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -90,22 +90,28 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos hz = synthio.midi_to_hz(data0 + value1) * master_tune # Breath pressure affects volume and filter cutoff simultaneously - actual_amp = volume * value0 * breath + actual_amp = gain(volume, value0, breath) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) - cutoff = 500.0 + breath * 4000.0 - lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=1.0 + embouchure * 2.0) + # Channel pressure retunes the breath-to-cutoff span (input b). + cutoff = synthio.Math(synthio.MathOperation.SCALE_OFFSET, breath, 4000.0, 500.0) + q = synthio.Math(synthio.MathOperation.SCALE_OFFSET, embouchure, 2.0, 1.0) + lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=q) - # Growl is simulated via fast vibrato/FM - growl_lfo = synthio.LFO(waveform=SINE, rate=1.0 + growl_rate * 20.0, scale=growl_depth * 0.1) if growl_depth > 0.01 else None + # Growl is simulated via fast vibrato/FM. It reaches a held note + # only if Growl Depth was up when the note started. + growl_lfo = None + if growl_depth.a > 0.01: + growl_hz = synthio.Math(synthio.MathOperation.SCALE_OFFSET, growl_rate, 20.0, 1.0) + growl_lfo = synthio.LFO(waveform=SINE, rate=growl_hz, scale=gain(growl_depth, 0.1)) # Embouchure affects pitch slightly and harmonic content (via another layered note) - n1 = synthio.Note(hz, waveform=WAVE_WIND, envelope=env, filter=lp, amplitude=actual_amp * 0.7, bend=growl_lfo) - n2 = synthio.Note(hz * 2.0, waveform=SINE, envelope=env, filter=lp, amplitude=actual_amp * embouchure * 0.3, bend=growl_lfo) + n1 = synthio.Note(hz, waveform=WAVE_WIND, envelope=env, filter=lp, amplitude=gain(actual_amp, 0.7), bend=growl_lfo) + n2 = synthio.Note(hz * 2.0, waveform=SINE, envelope=env, filter=lp, amplitude=gain(actual_amp, embouchure, 0.3), bend=growl_lfo) serial += 1 - voices[k] = ((n1, n2), serial) + voices[k] = ((n1, n2), serial, cutoff) synth.press(n1) synth.press(n2) @@ -118,16 +124,14 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos # the tone of a note already sounding, not just its initial hit pressure = value0 for voice in voices.values(): - for n in voice[0]: - if n.filter is not None: - n.filter.frequency = 500.0 + breath * (2000.0 + pressure * 3000.0) + voice[2].b = 2000.0 + pressure * 3000.0 elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: breath = value0 - elif data0 == 2: embouchure = value0 - elif data0 == 3: growl_rate = value0 - elif data0 == 4: growl_depth = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: breath.a = value0 + elif data0 == 2: embouchure.a = value0 + elif data0 == 3: growl_rate.a = value0 + elif data0 == 4: growl_depth.a = value0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. diff --git a/lib/audioinstruments/vox_continental.py b/lib/audioinstruments/vox_continental.py index f0f4ab2..f8510ab 100644 --- a/lib/audioinstruments/vox_continental.py +++ b/lib/audioinstruments/vox_continental.py @@ -33,7 +33,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -47,15 +48,16 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - db16 = 1.0 - db8 = 1.0 - db4 = 0.8 - db_iv = 0.5 - vib_rate = 6.0 - vib_depth = 0.0 - brilliance = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + db16 = live(1.0) + db8 = live(1.0) + db4 = live(0.8) + db_iv = live(0.5) + vib_rate = live(6.0) + vib_depth = live(0.0) + brilliance = live(0.8) master_tune = 1.0 voices = {} @@ -70,7 +72,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, db16, db8, db4, db_iv, vib_rate, vib_depth, brilliance, master_tune + nonlocal master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -81,20 +83,22 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=0.01, decay_time=0.1, release_time=0.1, attack_level=1.0, sustain_level=1.0) - cutoff = 1000.0 + (brilliance * 6000.0) + cutoff = synthio.Math(synthio.MathOperation.SCALE_OFFSET, brilliance, 6000.0, 1000.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=0.5) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.02) if vib_depth > 0.01 else None + # A drawbar or the vibrato that is off when the note starts is not + # sounded at all, so it reaches a held note only if it was up. + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.02)) if vib_depth.a > 0.01 else None notes = [] - if db16 > 0.01: notes.append(synthio.Note(hz * 0.5, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=amp * db16 * 0.25, bend=vib_lfo)) - if db8 > 0.01: notes.append(synthio.Note(hz, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=amp * db8 * 0.25, bend=vib_lfo)) - if db4 > 0.01: notes.append(synthio.Note(hz * 2.0, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=amp * db4 * 0.25, bend=vib_lfo)) - if db_iv > 0.01: notes.append(synthio.Note(hz * 2.99, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=amp * db_iv * 0.25, bend=vib_lfo)) + if db16.a > 0.01: notes.append(synthio.Note(hz * 0.5, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=gain(amp, db16, 0.25), bend=vib_lfo)) + if db8.a > 0.01: notes.append(synthio.Note(hz, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=gain(amp, db8, 0.25), bend=vib_lfo)) + if db4.a > 0.01: notes.append(synthio.Note(hz * 2.0, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=gain(amp, db4, 0.25), bend=vib_lfo)) + if db_iv.a > 0.01: notes.append(synthio.Note(hz * 2.99, waveform=WAVE_TRANS, envelope=env, filter=lp, amplitude=gain(amp, db_iv, 0.25), bend=vib_lfo)) serial += 1 voices[k] = (tuple(notes), serial) @@ -105,14 +109,14 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: db16 = value0 - elif data0 == 2: db8 = value0 - elif data0 == 3: db4 = value0 - elif data0 == 4: db_iv = value0 - elif data0 == 5: vib_rate = 0.1 + value0 * 10.0 - elif data0 == 6: vib_depth = value0 - elif data0 == 7: brilliance = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: db16.a = value0 + elif data0 == 2: db8.a = value0 + elif data0 == 3: db4.a = value0 + elif data0 == 4: db_iv.a = value0 + elif data0 == 5: vib_rate.a = 0.1 + value0 * 10.0 + elif data0 == 6: vib_depth.a = value0 + elif data0 == 7: brilliance.a = value0 elif data0 == 8: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/vp330.py b/lib/audioinstruments/vp330.py index cda04c1..5cca7d9 100644 --- a/lib/audioinstruments/vp330.py +++ b/lib/audioinstruments/vp330.py @@ -37,7 +37,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -51,18 +52,19 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - male_mix = 0.8 - female_mix = 0.5 - chorus_depth = 1.0 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + male_mix = live(0.8) + female_mix = live(0.5) + chorus_depth = live(1.0) amp_a = 0.1 amp_r = 0.5 - formant_shift = 0.0 - vib_rate = 5.0 - vib_depth = 0.0 - brilliance = 0.5 - bass = 0.5 + formant_shift = live(0.0) + vib_rate = live(5.0) + vib_depth = live(0.0) + brilliance = live(0.5) + bass = live(0.5) master_tune = 1.0 voices = {} @@ -77,8 +79,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, male_mix, female_mix, chorus_depth, amp_a, amp_r - nonlocal formant_shift, vib_rate, vib_depth, brilliance, bass, master_tune + nonlocal amp_a, amp_r, master_tune nonlocal serial k = key_of(channel, note_id, data0) @@ -89,48 +90,51 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=0.1, release_time=amp_r, attack_level=1.0, sustain_level=1.0) - vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=vib_depth * 0.02) if vib_depth > 0.01 else None - ens_lfo = synthio.LFO(waveform=SINE, rate=0.8, scale=chorus_depth * 0.01) if chorus_depth > 0.01 else None + # A section, vibrato or chorus that is off when the note starts is + # not sounded at all, so it reaches a held note only if it was up. + vib_lfo = synthio.LFO(waveform=SINE, rate=vib_rate, scale=gain(vib_depth, 0.02)) if vib_depth.a > 0.01 else None + ens_lfo = synthio.LFO(waveform=SINE, rate=0.8, scale=gain(chorus_depth, 0.01)) if chorus_depth.a > 0.01 else None + shift = synthio.Math(synthio.MathOperation.SUM, 1.0, formant_shift, 0.0) notes = [] # Male formants (approx "Ah" / "Oh") - if male_mix > 0.01: - f1 = 600.0 * (1.0 + formant_shift) - f2 = 1200.0 * (1.0 + formant_shift) + if male_mix.a > 0.01: + f1 = gain(600.0, shift) + f2 = gain(1200.0, shift) bp1 = synthio.Biquad(synthio.FilterMode.BAND_PASS, f1, Q=4.0) bp2 = synthio.Biquad(synthio.FilterMode.BAND_PASS, f2, Q=4.0) - n1 = synthio.Note(hz, waveform=PULSE, envelope=env, filter=bp1, amplitude=amp * male_mix * 0.4, bend=vib_lfo, panning=-0.3) - n2 = synthio.Note(hz * 1.002, waveform=PULSE, envelope=env, filter=bp2, amplitude=amp * male_mix * 0.4, bend=ens_lfo, panning=0.3) + n1 = synthio.Note(hz, waveform=PULSE, envelope=env, filter=bp1, amplitude=gain(amp, male_mix, 0.4), bend=vib_lfo, panning=-0.3) + n2 = synthio.Note(hz * 1.002, waveform=PULSE, envelope=env, filter=bp2, amplitude=gain(amp, male_mix, 0.4), bend=ens_lfo, panning=0.3) notes.extend([n1, n2]) # Female formants (approx "Ee" / high "Ah") - if female_mix > 0.01: - f1 = 900.0 * (1.0 + formant_shift) - f2 = 2500.0 * (1.0 + formant_shift) + if female_mix.a > 0.01: + f1 = gain(900.0, shift) + f2 = gain(2500.0, shift) bp1 = synthio.Biquad(synthio.FilterMode.BAND_PASS, f1, Q=5.0) bp2 = synthio.Biquad(synthio.FilterMode.BAND_PASS, f2, Q=3.0) - n1 = synthio.Note(hz * 2.0, waveform=PULSE, envelope=env, filter=bp1, amplitude=amp * female_mix * 0.3, bend=vib_lfo, panning=-0.5) - n2 = synthio.Note(hz * 1.998, waveform=PULSE, envelope=env, filter=bp2, amplitude=amp * female_mix * 0.3, bend=ens_lfo, panning=0.5) + n1 = synthio.Note(hz * 2.0, waveform=PULSE, envelope=env, filter=bp1, amplitude=gain(amp, female_mix, 0.3), bend=vib_lfo, panning=-0.5) + n2 = synthio.Note(hz * 1.998, waveform=PULSE, envelope=env, filter=bp2, amplitude=gain(amp, female_mix, 0.3), bend=ens_lfo, panning=0.5) notes.extend([n1, n2]) # VP-330's real string/bass section is a separate divide-down organ # register under the choir, an octave down with no formant filtering - if bass > 0.01: + if bass.a > 0.01: bp_bass = synthio.Biquad(synthio.FilterMode.LOW_PASS, 900.0, Q=0.9) - notes.append(synthio.Note(hz * 0.5, waveform=PULSE, envelope=env, filter=bp_bass, amplitude=amp * bass * 0.5)) + notes.append(synthio.Note(hz * 0.5, waveform=PULSE, envelope=env, filter=bp_bass, amplitude=gain(amp, bass, 0.5))) # Brilliance is the VP-330's top-end tone control: a bright unfiltered # doubling that only becomes audible as it's turned up - if brilliance > 0.01: + if brilliance.a > 0.01: hp_bright = synthio.Biquad(synthio.FilterMode.HIGH_PASS, 3000.0, Q=0.7) - notes.append(synthio.Note(hz * 2.0, waveform=PULSE, envelope=env, filter=hp_bright, amplitude=amp * brilliance * 0.25)) + notes.append(synthio.Note(hz * 2.0, waveform=PULSE, envelope=env, filter=hp_bright, amplitude=gain(amp, brilliance, 0.25))) serial += 1 voices[k] = (tuple(notes), serial) @@ -141,20 +145,20 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: male_mix = value0 - elif data0 == 2: female_mix = value0 - elif data0 == 3: chorus_depth = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: male_mix.a = value0 + elif data0 == 2: female_mix.a = value0 + elif data0 == 3: chorus_depth.a = value0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. elif data0 == 4: amp_a = logmap(value0, 0.001, 2.001) elif data0 == 5: amp_r = logmap(value0, 0.01, 4.01) - elif data0 == 6: formant_shift = -0.5 + value0 - elif data0 == 7: vib_rate = 0.1 + value0 * 10.0 - elif data0 == 8: vib_depth = value0 - elif data0 == 9: brilliance = value0 - elif data0 == 10: bass = value0 + elif data0 == 6: formant_shift.a = -0.5 + value0 + elif data0 == 7: vib_rate.a = 0.1 + value0 * 10.0 + elif data0 == 8: vib_depth.a = value0 + elif data0 == 9: brilliance.a = value0 + elif data0 == 10: bass.a = value0 elif data0 == 11: master_tune = 0.95 + value0 * 0.1 instrument = Instrument(synth, handle_event, PATCHES, MACRO_LABELS, diff --git a/lib/audioinstruments/wasp.py b/lib/audioinstruments/wasp.py index 94d038e..9652964 100644 --- a/lib/audioinstruments/wasp.py +++ b/lib/audioinstruments/wasp.py @@ -34,8 +34,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, - noise_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, noise_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -51,12 +51,13 @@ def create(sample_rate, channel_count=2, transport=None): NOISE_HZ = SR / 8192.0 synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) filter_mode = 0.0 # 0=LP, 1=HP - cutoff_val = 2000.0 - res = 1.5 - noise_lvl = 0.0 + cutoff_val = live(2000.0) + res = live(1.5) + noise_lvl = live(0.0) amp_a = 0.01 amp_d = 0.3 amp_s = 0.5 @@ -75,7 +76,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, filter_mode, cutoff_val, res, noise_lvl + nonlocal filter_mode nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -86,7 +87,7 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) @@ -101,11 +102,14 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos notes = [] # Two digital square VCOs, detuned, for the raw doubled-oscillator Wasp tone. - notes.append(synthio.Note(hz, waveform=WAVE_DIGI, envelope=env, filter=flt, amplitude=amp * 0.45)) - notes.append(synthio.Note(hz * 1.007, waveform=WAVE_DIGI, envelope=env, filter=flt, amplitude=amp * 0.45)) + vco_amp = gain(amp, 0.45) + notes.append(synthio.Note(hz, waveform=WAVE_DIGI, envelope=env, filter=flt, amplitude=vco_amp)) + notes.append(synthio.Note(hz * 1.007, waveform=WAVE_DIGI, envelope=env, filter=flt, amplitude=vco_amp)) - if noise_lvl > 0.01: - notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=flt, amplitude=amp * noise_lvl * 0.2)) + # The noise is not sounded at all when it is off at note start, so + # Noise Level reaches a held note only if it was up. + if noise_lvl.a > 0.01: + notes.append(synthio.Note(NOISE_HZ, waveform=NOISE, envelope=env, filter=flt, amplitude=gain(amp, noise_lvl, 0.2))) serial += 1 voices[k] = (tuple(notes), serial) @@ -116,11 +120,11 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 + if data0 == 0: volume.a = value0 elif data0 == 1: filter_mode = value0 - elif data0 == 2: cutoff_val = 50.0 * (100.0 ** value0) - elif data0 == 3: res = 0.5 + value0 * 7.5 # pushes into self-oscillation like the real unstable filter - elif data0 == 4: noise_lvl = value0 + elif data0 == 2: cutoff_val.a = 50.0 * (100.0 ** value0) + elif data0 == 3: res.a = 0.5 + value0 * 7.5 # pushes into self-oscillation like the real unstable filter + elif data0 == 4: noise_lvl.a = value0 # Time is heard as a ratio, so these travel logarithmically: # the steps crowd into the fast end, where a few milliseconds # change the articulation, instead of stepping over it. diff --git a/lib/audioinstruments/wurlitzer.py b/lib/audioinstruments/wurlitzer.py index 28c712a..e70893e 100644 --- a/lib/audioinstruments/wurlitzer.py +++ b/lib/audioinstruments/wurlitzer.py @@ -34,8 +34,8 @@ import synthio from audioinstruments._support import ( - EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, key_of, logmap, make_table, - ring_depth_table, + EVENT_NOTE_ON, EVENT_NOTE_OFF, EVENT_PARAMETER, gain, key_of, live, + logmap, make_table, ring_depth_table, ) from audioinstruments._support import Instrument from audioinstruments import _support @@ -50,10 +50,11 @@ def create(sample_rate, channel_count=2, transport=None): SR = sample_rate synth = _support.synthesizer(SR, channel_count) - # Macros - volume = 0.8 - bite = 0.5 - bark = 0.5 + # Macros. The live ones are blocks every sounding note reads, so turning + # the knob reaches a held note; the rest are read when a note starts. + volume = live(0.8) + bite = live(0.5) + bark = live(0.5) trem_rate = 4.0 trem_depth = 0.0 amp_a = 0.01 @@ -78,7 +79,7 @@ def steal_oldest(): _support.steal_oldest(voices, release_voice) def handle_event(event_type, channel, note_id, data0, value0, value1, sample_position): - nonlocal volume, bite, bark, trem_rate, trem_depth + nonlocal trem_rate, trem_depth nonlocal amp_a, amp_d, amp_s, amp_r, master_tune nonlocal serial @@ -90,19 +91,19 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos steal_oldest() hz = synthio.midi_to_hz(data0 + value1) * master_tune - amp = volume * value0 + amp = gain(volume, value0) env = synthio.Envelope(attack_time=amp_a, decay_time=amp_d, release_time=amp_r, attack_level=1.0, sustain_level=amp_s) bite_env = synthio.Envelope(attack_time=0.001, decay_time=0.2, release_time=0.1, attack_level=1.0, sustain_level=0.0) # Velocity affects filter cutoff heavily for the "bark" - cutoff = 1000.0 + (value0 * bark * 5000.0) + cutoff = synthio.Math(synthio.MathOperation.SUM, 1000.0, gain(value0, bark, 5000.0), 0.0) lp = synthio.Biquad(synthio.FilterMode.LOW_PASS, cutoff, Q=0.8) trem_wave = ring_depth_table(trem_depth) if trem_depth > 0.01 else None - o_reed = synthio.Note(hz, waveform=WAVE_REED, envelope=env, filter=lp, amplitude=amp * 0.6, ring_frequency=trem_rate, ring_waveform=trem_wave) - o_bite = synthio.Note(hz, waveform=WAVE_BITE, envelope=bite_env, filter=lp, amplitude=amp * bite * 0.4, ring_frequency=trem_rate, ring_waveform=trem_wave) + o_reed = synthio.Note(hz, waveform=WAVE_REED, envelope=env, filter=lp, amplitude=gain(amp, 0.6), ring_frequency=trem_rate, ring_waveform=trem_wave) + o_bite = synthio.Note(hz, waveform=WAVE_BITE, envelope=bite_env, filter=lp, amplitude=gain(amp, bite, 0.4), ring_frequency=trem_rate, ring_waveform=trem_wave) serial += 1 voices[k] = (_support.press_voice(synth, voices, retired, @@ -112,9 +113,9 @@ def handle_event(event_type, channel, note_id, data0, value0, value1, sample_pos release_voice(k) elif event_type == EVENT_PARAMETER: - if data0 == 0: volume = value0 - elif data0 == 1: bite = value0 * 2.0 - elif data0 == 2: bark = value0 + if data0 == 0: volume.a = value0 + elif data0 == 1: bite.a = value0 * 2.0 + elif data0 == 2: bark.a = value0 elif data0 == 3: trem_rate = 0.1 + value0 * 10.0 elif data0 == 4: trem_depth = value0 # Time is heard as a ratio, so these travel logarithmically: diff --git a/tests/test_live_macros.py b/tests/test_live_macros.py index a90a852..1aa299a 100644 --- a/tests/test_live_macros.py +++ b/tests/test_live_macros.py @@ -22,9 +22,73 @@ #: instrument -> macro indices that a held note follows. LIVE = { + "andromeda": (0, 1, 2, 3, 4, 5, 6, 7), + "arp2600": (0, 1, 2, 3, 4), + "b3": (0, 1, 2, 3, 4, 5, 7, 9), + "clavinet": (0, 1, 3, 4, 5), + "cp70": (0, 1, 2, 7), + "cs80": (0, 1, 2, 3, 6, 15), + "cz101": (0, 1, 4, 5, 6), + "d50": (0, 1, 2, 4, 5), + "dx7": (0, 1, 3, 8, 10, 12, 14), + "emulator2": (0, 1, 2, 7, 8, 9), + "fairlight": (0, 2, 8), + "farfisa": (0, 1, 2, 3, 4, 5, 7, 8), + "fs1r": (0, 4, 5), + "jp8000": (0, 1, 2, 4, 5, 14), + "juno106": (0, 1, 2, 3, 4, 6, 7, 9, 10), + "jupiter8": (0, 1, 2, 3, 4, 6, 14), + "k2600": (0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 14), + "karplus": (0, 3), + "mellotron": (0, 1, 2, 3, 6), + "microwave": (0, 1, 2, 3, 4, 5), "minimoog": (0, 1, 2, 3, 7, 15), + "ms20": (0, 1, 2, 3, 4, 6, 12), + "ms2000": (0, 2, 3, 4, 5, 6), + "music_easel": (0, 1, 2, 3), + "nord_lead": (0, 1, 2, 5, 6, 7), + "obxa": (0, 2, 3, 4), + "odyssey": (0, 1, 2, 4, 5, 12), + "pianet": (0, 2, 4, 5), + "polysix": (0, 1, 2, 3, 4, 5), + "ppg_wave": (0, 1, 2, 3, 4), + "prophet5": (0, 1, 2, 3, 14, 15), + "prophet_vs": (0, 1, 2, 3, 4, 5, 6), + "rhodes": (0, 1, 2, 5, 6), + "sh101": (0, 2, 3, 4, 5), + "solina": (0, 1, 2, 3, 4), + "taurus": (0, 3, 4, 5), + "tb303": (0, 2, 3, 4, 6, 7), + "virus": (0, 2, 3, 4, 5, 6), + "vl1": (0, 1, 2, 3, 4), + "vox_continental": (0, 1, 2, 3, 4, 5, 6, 7), + "vp330": (0, 1, 2, 3, 6, 7, 8, 9, 10), + "wasp": (0, 2, 3, 4), + "wurlitzer": (0, 1, 2), } +#: (instrument, macro) -> (other macro, value) to set before the note. An LFO +#: rate is only heard while its depth is up, and a modulation LFO whose depth +#: is zero when the note starts is never built at all. +GATES = { + ("clavinet", 4): (3, 100), # Wah Rate needs Wah Depth + ("cz101", 5): (6, 100), # Vibrato Rate needs Vibrato Depth + ("dx7", 14): (12, 100), # Vibrato Rate needs Vibrato Depth + ("emulator2", 7): (8, 100), # Vibrato Rate needs Vibrato Depth + ("farfisa", 7): (8, 100), # Vibrato Rate needs Vibrato Depth + ("k2600", 13): (12, 100), # Mod Rate needs Mod Wheel + ("k2600", 14): (12, 100), # Mod Depth needs Mod Wheel + ("pianet", 4): (5, 100), # Vibrato Rate needs Vibrato Depth + ("prophet5", 14): (15, 100), # LFO Rate needs LFO Depth + ("vl1", 3): (4, 100), # Growl Rate needs Growl Depth + ("vox_continental", 5): (6, 100), # Vibrato Rate needs Vibrato Depth + ("vp330", 7): (8, 100), # Vibrato Rate needs Vibrato Depth +} + +#: Macros on a transient that has died away by the time the other cases move +#: their knob; these move at once. +EARLY = (("b3", 7), ("cp70", 1), ("d50", 1)) # Key Click, Hammer, Transient + def held(name, index, value): """Hash of a held note's output after macro ``index`` moves to ``value``. @@ -32,9 +96,12 @@ def held(name, index, value): The macro starts mid-range so that a layer it would gate off at zero is sounding when the note starts.""" instrument = audioinstruments.create(name, SAMPLE_RATE, channel_count=1) + gate = GATES.get((name, index)) + if gate is not None: + instrument.set_macro(*gate) instrument.set_macro(index, 64) instrument.note_on(57, 110) - for _ in range(20): + for _ in range(0 if (name, index) in EARLY else 20): audiocore.get_buffer(instrument.output) instrument.set_macro(index, value) digest = hashlib.sha256()