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/_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/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/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/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 new file mode 100644 index 0000000..1aa299a --- /dev/null +++ b/tests/test_live_macros.py @@ -0,0 +1,130 @@ +"""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 = { + "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``. + + 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(0 if (name, index) in EARLY else 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()