From de9647bbfe7ac5b0507df74078ec4e930026ccbc Mon Sep 17 00:00:00 2001 From: Dan Ellis Date: Fri, 20 Sep 2024 19:39:43 -0400 Subject: [PATCH] Added wave=BYO_PARTIALS for cleaner BYO_PARTIALS syntax. --- README.md | 42 +++++++++++++++++++++--------------------- amy.py | 13 ++++++++++--- src/amy.c | 9 +++++---- src/amy.h | 5 +++-- test.py | 6 +++--- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index bc94f58..d1b50ec 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ Here's the full list: | `u` | `store_patch` | number,string | Store up to 32 patches in RAM with ID number (1024-1055) and AMY message after a comma. Must be sent alone | | `v` | `osc` | uint 0 to OSCS-1 | Which oscillator to control | | `V` | `volume` | float 0-10 | Volume knob for entire synth, default 1.0 | -| `w` | `wave` | uint 0-11 | Waveform: [0=SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, OFF]. default: 0/SINE | +| `w` | `wave` | uint 0-11 | Waveform: [0=SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, BYO_PARTIALS, OFF]. default: 0/SINE | | `x` | `eq` | float,float,float | Equalization in dB low (~800Hz) / med (~2500Hz) / high (~7500Gz) -15 to 15. 0 is off. default 0. | | `X` | `eg1_type` | uint 0-3 | Type for Envelope Generator 1 - 0: Normal (RC-like) / 1: Linear / 2: DX7-style / 3: True exponential. | @@ -486,9 +486,9 @@ Additive synthesis is simply adding together oscillators to make more complex to We have analyzed the partials of a group of instruments and stored them as presets baked into the synth. Each of these patches are comprised of multiple sine wave oscillators, changing over time. The `PARTIALS` type has the presets: ```python -amy.send(osc=0,vel=1,note=50,wave=amy.PARTIALS,patch=5) # a nice organ tone -amy.send(osc=0,vel=1,note=55,wave=amy.PARTIALS,patch=5) # change the frequency -amy.send(osc=0,vel=1,note=50,wave=amy.PARTIALS,patch=6,ratio=0.2) # ratio slows down the partial playback +amy.send(osc=0, vel=1, note=50, wave=amy.PARTIALS, patch=5) # a nice organ tone +amy.send(osc=0, vel=1, note=55, wave=amy.PARTIALS, patch=5) # change the frequency +amy.send(osc=0, vel=1, note=50, wave=amy.PARTIALS, patch=6, ratio=0.2) # ratio slows down the partial playback ``` The presets are just the start of what you can do with partials in AMY. You can analyze any piece of audio and decompose it into sine waves and play it back on the synthesizer in real time. It requires a little setup on the client end, here on macOS: @@ -524,32 +524,32 @@ There's a lot of parameters you can (and should!) play with in Loris. `partials. ```python def sequence(filename, # any audio filename - max_len_s = 10, # analyze first N seconds - amp_floor=-30, # only accept partials at this amplitude in dB, lower #s == more partials - hop_time=0.04, # time between analysis windows, impacts distance between breakpoints - max_oscs=amy.OSCS, # max AMY oscs to take up - freq_res = 10, # freq resolution of analyzer, higher # -- less partials & breakpoints - freq_drift=20, # max difference in Hz within a single partial - analysis_window = 100 # analysis window size - ) # returns (metadata, sequence) + max_len_s = 10, # analyze first N seconds + amp_floor=-30, # only accept partials at this amplitude in dB, lower #s == more partials + hop_time=0.04, # time between analysis windows, impacts distance between breakpoints + max_oscs=amy.OSCS, # max AMY oscs to take up + freq_res = 10, # freq resolution of analyzer, higher # -- less partials & breakpoints + freq_drift=20, # max difference in Hz within a single partial + analysis_window = 100 # analysis window size + ) # returns (metadata, sequence) def play(sequence, # from partials.sequence - osc_offset=0, # start at this oscillator # - sustain_ms = -1, # if the instrument should sustain, here's where (in ms) - sustain_len_ms = 0, # how long to sustain for - time_ratio = 1, # playback speed -- 0.5 , half speed - pitch_ratio = 1, # frequency scale, 0.5 , half freq - amp_ratio = 1, # amplitude scale - ) + osc_offset=0, # start at this oscillator # + sustain_ms = -1, # if the instrument should sustain, here's where (in ms) + sustain_len_ms = 0, # how long to sustain for + time_ratio = 1, # playback speed -- 0.5 , half speed + pitch_ratio = 1, # frequency scale, 0.5 , half freq + amp_ratio = 1, # amplitude scale + ) ``` ## Build-your-own Partials -You can also explicitly control partials in "build-your-own partials" mode. Specifying a negative value for `patch` instructs AMY to leave the amplitude and frequency control of the partials to you, and it decides how many partials to expect with the negative of the patch value you give it. You can then individually set up the amplitude `bp0` envelopes of the next `num_partials` oscs for arbitrary control, subject to the limit of 7 breakpoints plus release for each envelope. For instance, to get an 8-harmonic pluck tone with a 50 ms attack, and harmonic weights and decay times inversely proportional to to the harmonic number: +You can also explicitly control partials in "build-your-own partials" mode, accessed via `wave=amy.BYO_PARTIALS`. This sets up a string of oscs as individual sinusoids, just like `PARTIALS` mode, but it's up to you to control the details of each partial via its parameters, envelopes, etc. You just have to say how many partials you want with `num_partials`. You can then individually set up the amplitude `bp0` envelopes of the next `num_partials` oscs for arbitrary control, subject to the limit of 7 breakpoints plus release for each envelope. For instance, to get an 8-harmonic pluck tone with a 50 ms attack, and harmonic weights and decay times inversely proportional to to the harmonic number: ``` num_partials = 8 -amy.send(osc=0, wave=amy.PARTIALS, patch=-num_partials) +amy.send(osc=0, wave=amy.BYO_PARTIALS, num_partials=num_partials) for i in range(1, num_partials + 1): # Set up each partial as the corresponding harmonic of 261.63 # with an amplitude of 1/N, 50ms attack, and a decay of 1 sec / N. diff --git a/amy.py b/amy.py index 9fda9b4..381ec58 100644 --- a/amy.py +++ b/amy.py @@ -5,7 +5,7 @@ AMY_NCHANS = 2 AMY_OSCS = 120 MAX_QUEUE = 400 -SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, CUSTOM, OFF = range(13) +SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, BYO_PARTIALS, CUSTOM, OFF = range(14) FILTER_NONE, FILTER_LPF, FILTER_BPF, FILTER_HPF, FILTER_LPF24 = range(5) ENVELOPE_NORMAL, ENVELOPE_LINEAR, ENVELOPE_DX7, ENVELOPE_TRUE_EXPONENTIAL = range(4) AMY_LATENCY_MS = 0 @@ -155,12 +155,14 @@ def to_str(x): def message(**kwargs): # Each keyword maps to two chars, first is the wire protocol prefix, second is an arg type code # I=int, F=float, S=str, L=list, C=ctrl_coefs - kw_map = {'osc': 'vI', 'wave': 'wI', 'patch': 'pI', 'note': 'nF', 'vel': 'lF', 'amp': 'aC', 'freq': 'fC', 'duty': 'dC', 'feedback': 'bF', 'time': 'tI', + kw_map = {'osc': 'vI', 'wave': 'wI', 'note': 'nF', 'vel': 'lF', 'amp': 'aC', 'freq': 'fC', 'duty': 'dC', 'feedback': 'bF', 'time': 'tI', 'reset': 'SI', 'phase': 'PF', 'pan': 'QC', 'client': 'cI', 'volume': 'vF', 'pitch_bend': 'sF', 'filter_freq': 'FC', 'resonance': 'RF', 'bp0': 'AL', 'bp1': 'BL', 'eg0_type': 'TI', 'eg1_type': 'XI', 'debug': 'DI', 'chained_osc': 'cI', 'mod_source': 'LI', 'clone_osc': 'CI', 'eq': 'xL', 'filter_type': 'GI', 'algorithm': 'oI', 'ratio': 'IF', 'latency_ms': 'NI', 'algo_source': 'OL', 'chorus': 'kL', 'reverb': 'hL', 'echo': 'ML', 'load_patch': 'KI', 'store_patch': 'uS', 'voices': 'rL', - 'external_channel': 'WI', 'portamento': 'mI'} + 'external_channel': 'WI', 'portamento': 'mI', + 'patch': 'pI', 'num_partials': 'pI', # Note alaising. + } arg_handlers = { 'I': str, 'F': trunc, 'S': str, 'L': str, 'C': parse_ctrl_coefs, } @@ -174,6 +176,11 @@ def message(**kwargs): if 'store_patch' in kwargs and len(kwargs) > 1: print('\'store_patch\' should be the only arg in a message.') # And yet we plow ahead... + if 'num_partials' in kwargs: + if 'patch' in kwargs: + raise ValueError('You cannot use \'num_partials\' and \'patch\' in the same message.') + if 'wave' not in kwargs or kwargs['wave'] != BYO_PARTIALS: + raise ValueError('\'num_partials\' must be used with \'wave\'=BYO_PARTIALS.') m = "" for key, arg in kwargs.items(): if arg is None: diff --git a/src/amy.c b/src/amy.c index bbbd4f3..e27cb83 100644 --- a/src/amy.c +++ b/src/amy.c @@ -903,7 +903,7 @@ void osc_note_on(uint16_t osc, float initial_freq) { if(synth[osc].wave==NOISE) noise_note_on(osc); if(AMY_HAS_PARTIALS == 1) { //if(synth[osc].wave==PARTIAL) partial_note_on(osc); - if(synth[osc].wave==PARTIALS) partials_note_on(osc); + if(synth[osc].wave==PARTIALS || synth[osc].wave==BYO_PARTIALS) partials_note_on(osc); } if(AMY_HAS_CUSTOM == 1) { if(synth[osc].wave==CUSTOM) custom_note_on(osc, initial_freq); @@ -957,7 +957,8 @@ void play_event(struct delta d) { } } if(d.param == PHASE) { synth[d.osc].phase = *(PHASOR *)&d.data; synth[d.osc].trigger_phase = *(PHASOR*)&d.data; } // PHASOR - if(d.param == PATCH) synth[d.osc].patch = *(uint16_t *)&d.data; + // For now, if the wave type is BYO_PARTIALS, negate the patch number (which is also num_partials) and treat like regular PARTIALS - partials_note_on knows what to do. + if(d.param == PATCH) synth[d.osc].patch = ((synth[d.osc].wave == BYO_PARTIALS) ? -1 : 1) * *(uint16_t *)&d.data; if(d.param == FEEDBACK) synth[d.osc].feedback = *(float *)&d.data; if(PARAM_IS_COMBO_COEF(d.param, AMP)) { @@ -1106,7 +1107,7 @@ void play_event(struct delta d) { //synth[d.osc].note_off_clock = total_samples; //partial_note_off(d.osc); #endif - } else if(synth[d.osc].wave==PARTIALS) { + } else if(synth[d.osc].wave==PARTIALS || synth[d.osc].wave==BYO_PARTIALS) { #if AMY_HAS_PARTIALS == 1 AMY_UNSET(synth[d.osc].note_on_clock); synth[d.osc].note_off_clock = total_samples; @@ -1291,7 +1292,7 @@ SAMPLE render_osc_wave(uint16_t osc, uint8_t core, SAMPLE* buf) { if(synth[osc].wave == ALGO) max_val = render_algo(buf, osc, core); if(AMY_HAS_PARTIALS == 1) { //if(synth[osc].wave == PARTIAL) max_val = render_partial(buf, osc); - if(synth[osc].wave == PARTIALS) max_val = render_partials(buf, osc); + if(synth[osc].wave == PARTIALS || synth[osc].wave == BYO_PARTIALS) max_val = render_partials(buf, osc); } } if(AMY_HAS_CUSTOM == 1) { diff --git a/src/amy.h b/src/amy.h index 4423280..5dd470c 100644 --- a/src/amy.h +++ b/src/amy.h @@ -86,8 +86,9 @@ enum coefs{ #define ALGO 8 #define PARTIAL 9 #define PARTIALS 10 -#define CUSTOM 11 -#define WAVE_OFF 12 +#define BYO_PARTIALS 11 +#define CUSTOM 12 +#define WAVE_OFF 13 // synth[].status values #define EMPTY 0 diff --git a/test.py b/test.py index f0bb5c7..a636751 100644 --- a/test.py +++ b/test.py @@ -179,7 +179,7 @@ def run(self): num_partials = 16 base_freq = 261.63 base_osc = 0 - amy.send(time=0, osc=base_osc, wave=amy.PARTIALS, patch=-num_partials) + amy.send(time=0, osc=base_osc, wave=amy.BYO_PARTIALS, num_partials=num_partials) for i in range(1, num_partials + 1): # Set up each partial as the corresponding harmonic of the base_freq, with an amplitude of 1/N, 50ms attack, and a decay of 1 sec / N amy.send(osc=base_osc + i, wave=amy.PARTIAL, freq=base_freq * i, bp0='50,%.2f,%d,0,0,0' % ((1.0 / i), 1000 // i)) @@ -191,7 +191,7 @@ class TestBYOPVoices(AmyTest): def run(self): # Does build-your-own-partials work with the voices mechanism? num_partials = 4 - s = '1024,v0w%dp%dZ' % (amy.PARTIALS, -num_partials) + ''.join(['v%dw%dZ' % (i + 1, amy.PARTIAL) for i in range(num_partials)]) + s = '1024,v0w%dp%dZ' % (amy.BYO_PARTIALS, num_partials) + ''.join(['v%dw%dZ' % (i + 1, amy.PARTIAL) for i in range(num_partials)]) amy.send(store_patch=s) amy.send(time=0, voices='0,1,2,3', load_patch=1024) for i in range(num_partials): @@ -206,7 +206,7 @@ class TestBYOPNoteOff(AmyTest): def run(self): # Partials were not seeing note-offs. num_partials = 8 - s = '1024,v0w%dp%dZ' % (amy.PARTIALS, -num_partials) + ''.join(['v%dw%dZ' % (i + 1, amy.PARTIAL) for i in range(num_partials)]) + s = '1024,v0w%dp%dZ' % (amy.BYO_PARTIALS, num_partials) + ''.join(['v%dw%dZ' % (i + 1, amy.PARTIAL) for i in range(num_partials)]) amy.send(store_patch=s) amy.send(time=0, voices='0,1', load_patch=1024) for i in range(num_partials):