Skip to content

Commit

Permalink
Merge pull request #219 from shorepine/byo_partials_syntax
Browse files Browse the repository at this point in the history
Added wave=BYO_PARTIALS for cleaner BYO_PARTIALS syntax.
  • Loading branch information
dpwe authored Sep 20, 2024
2 parents 9c4ef9b + de9647b commit 678639f
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 33 deletions.
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 10 additions & 3 deletions amy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand All @@ -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:
Expand Down
9 changes: 5 additions & 4 deletions src/amy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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):
Expand All @@ -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):
Expand Down

0 comments on commit 678639f

Please sign in to comment.