Skip to content
Open

Buses #686

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions amy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def str_of_int(arg):
_KW_MAP_LIST = [ # Order matters because patch_string must come last.
('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', 'gI'),
('volume', 'VF'), ('pitch_bend', 'sF'), ('filter_freq', 'FC'), ('resonance', 'RF'),
('volume', 'VL'), ('pitch_bend', 'sF'), ('filter_freq', 'FC'), ('resonance', 'RF'),
('bp0', 'AL'), ('bp1', 'BL'),
('eg0', 'AL'), ('eg1', 'BL'), # Aliases for bp0 and bp1
('eg0_type', 'TI'), ('eg1_type', 'XI'), ('debug', 'DI'), ('chained_osc', 'cI'),
Expand All @@ -150,7 +150,7 @@ def str_of_int(arg):
('to_synth', 'itI'), ('grab_midi_notes', 'imI'), ('synth_delay', 'idI'),
('preset', 'pI'), ('num_partials', 'pI'), # note aliasing
('start_sample', 'zSL'), ('stop_sample', 'zOI'),
('midi_cc', 'icL'),
('midi_cc', 'icL'), ('bus', 'yI'),
('patch_string', 'uS'), # patch_string MUST be last because we can't identify when it ends except by end-of-message.
]
_KW_PRIORITY = {k: i for i, (k, _) in enumerate(_KW_MAP_LIST)} # Maps each key to its index within _KW_MAP_LIST.
Expand Down Expand Up @@ -356,8 +356,8 @@ def b64(b):
def b64(b):
return ubinascii.b2a_base64(b)[:-1]

def start_sample(preset=0, bus=1, max_frames=0, midinote=60, loopstart=0, loopend=0):
s = "%d,%d,%d,%d,%d,%d" % (preset, bus, max_frames, midinote, loopstart, loopend)
def start_sample(preset=0, source=SAMPLE_FROM_OUTPUT, max_frames=0, midinote=60, loopstart=0, loopend=0):
s = "%d,%d,%d,%d,%d,%d" % (preset, source, max_frames, midinote, loopstart, loopend)
send(start_sample=s)

def stop_sample():
Expand Down
8 changes: 5 additions & 3 deletions amy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
AMY_PCM_TYPE_FILE=1
AMY_PCM_TYPE_MEMORY=2
PCM_FILE_BUFFER_MULT=8
AMY_BUS_OUTPUT=1
AMY_BUS_AUDIO_IN=2
SAMPLE_FROM_OUTPUT=1
SAMPLE_FROM_AUDIO_IN=2
AMY_NUM_BUSES=4
AMY_DEFAULT_BUS=0
AMY_MAX_CORES=2
AMY_MAX_CHANNELS=2
AMY_NCHANS=2
Expand All @@ -37,7 +39,7 @@
REVERB_DEFAULT_XOVER_HZ=3000.0
ECHO_DEFAULT_LEVEL=0
ECHO_DEFAULT_DELAY_MS=500.
ECHO_DEFAULT_MAX_DELAY_MS=743.
ECHO_DEFAULT_MAX_DELAY_MS=743.039
ECHO_DEFAULT_FEEDBACK=0
ECHO_DEFAULT_FILTER_COEF=0
AMY_SEQUENCER_PPQ=48
Expand Down
14 changes: 7 additions & 7 deletions amy/fm.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def send_to_AMY(self, reset=True):
# Set up each operator.
last_release_time = 0
last_release_value = 0
# Oscs: 0 is algo, 1 is pitch LFO, 2 is amp LFO, 3-8 are ops 1-6
# Oscs: 0 is algo, 1 is LFO, 2-7 are ops 1-6
main_osc = 0
lfo_osc = 1
# The osc of op0 (they go up from here)
Expand All @@ -251,14 +251,14 @@ def send_to_AMY(self, reset=True):
amp_times[2], t(amp_levels[2]), amp_times[3], t(amp_levels[3]),
amp_times[4], t(amp_levels[4])
)
oscbpfmt = "%d,%s/%d,%s/%d,%s/%d,%s/%d,%s" % (
amp_times[0], t(amp_levels[0]), amp_times[1], t(amp_levels[1]),
amp_times[2], t(amp_levels[2]), amp_times[3], t(amp_levels[3]),
amp_times[4], t(amp_levels[4])
)
if(amp_times[4] > last_release_time):
last_release_time = amp_times[4]
last_release_value = amp_levels[4]
#oscbpfmt = "%d,%s/%d,%s/%d,%s/%d,%s/%d,%s" % (
# amp_times[0], t(amp_levels[0]), amp_times[1], t(amp_levels[1]),
# amp_times[2], t(amp_levels[2]), amp_times[3], t(amp_levels[3]),
# amp_times[4], t(amp_levels[4])
#)
#print("osc %d (op %d) freq %.6f ratio %d env %s amp %.6f amp_mod %d" % \
# (osc_num, osc.op_num_from_0, osc.frequency, osc.freq_is_ratio, oscbpfmt,
# osc.op_amp, osc.ampmodsens))
Expand All @@ -270,7 +270,7 @@ def send_to_AMY(self, reset=True):
args["ratio"] = t(osc.frequency)
else:
args["freq"] = t(osc.frequency)
# TODO: we xignore intensity of amp mod sens, just on/off
# TODO: we ignore intensity of amp mod sens, just on/off
args.update({"mod_source": lfo_osc, "amp": "%s,0,0,1,0,%s" % (t(osc.op_amp), t(self.amp_lfo_amp * (osc.ampmodsens > 0)))})

# We are _NOT_ updating operators with pitch bp, per dan tuesday 7/5 morning (but not monday 7/4 morning)
Expand Down
18 changes: 17 additions & 1 deletion amy/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,7 @@ def run(self):
class TestSample(AmyTest):

def run(self):
amy.start_sample(preset=1024,bus=1,max_frames=22050, midinote=60)
amy.start_sample(preset=1024, source=amy.SAMPLE_FROM_OUTPUT, max_frames=22050, midinote=60)
amy.send(time=0, synth=1, num_voices=4, patch=20)
amy.send(time=50, synth=1, note=48, vel=1)
amy.send(time=150, synth=1, note=60, vel=1)
Expand Down Expand Up @@ -1265,6 +1265,22 @@ def run(self):
amy.send(time=750, synth=1, note=48, vel=1)
amy.send(time=950, synth=1, vel=0)


class TestBuses(AmyTest):
"""You can assign synths to different buses to get independent FX."""

def run(self):
amy.send(time=0, synth=1, num_voices=4, patch=22, bus=0, pan=0.2) # A37 Pizzicato
amy.send(time=0, bus=0, reverb=1, echo=0)
amy.send(time=0, synth=2, num_voices=4, patch=22, bus=1, pan=0.8)
amy.send(time=0, bus=1, reverb=0, echo='1,100,,0.5,0.5')
amy.send(time=0, volume='2,0.5') # Mixdown for buses 0 and 1.
amy.send(time=100, synth=1, note=60, vel=5)
amy.send(time=300, synth=2, note=63, vel=5)
amy.send(time=500, synth=1, note=67, vel=5)
amy.send(time=700, synth=2, note=70, vel=5)


def main(argv):
if len(argv) > 1 and argv[1] == 'quiet':
quiet = True
Expand Down
25 changes: 19 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ A note on list parameters: When an argument is a list of parameters, you can in
| `in` | `oscs_per_voice` | `oscs_per_voice` | >0 | Reserve this many oscs for each voice. Needed when initializing a synth (or voice) withouth an initial patch. Setting `oscs_per_voice` on an existing synth resets all oscs to their default state. |
| `im` | `grab_midi_notes` | `grab_midi_notes` | 0/1 | Use `amy.send(synth=CHANNEL, grab_midi_notes=0)` to prevent the default direct forwarding of MIDI note-on/offs to synth CHANNEL. |
| `ip` | `pedal` | `pedal` | int | Non-zero means pedal is down (i.e., sustain). Must be used with `synth`. |
| `iy` | `bus` | `bus` | int | Bus onto which the synth outputs are added (synonym for `y`). |
| `K` | `patch_number` | `patch` | uint 0-X | Apply a saved or user patch to a specified synth or voice. |
| `r` | `voices[]` | `voices` | int[,int] | Comma separated list of voices to send message to, or load patch into. |
| `u` | **TODO**| `patch_string` | string | Provide AMY message to define up to 32 patches in RAM with ID numbers (1024-1055) provided via `patch_number`, or directly configure a `synth`. |
Expand Down Expand Up @@ -249,6 +250,7 @@ A note on list parameters: When an argument is a list of parameters, you can in
| `R` | `resonance` | `resonance` | float | Q factor of variable filter, 0.5-16.0. default 0.7 |
| `T` | `eg_type[0]` | `eg0_type` | uint 0-3 | Type for Envelope Generator 0 - 0: Normal (RC-like) / 1: Linear / 2: DX7-style / 3: True exponential. |
| `X` | `eg_type[1]` | `eg1_type` | uint 0-3 | Type for Envelope Generator 1 - 0: Normal (RC-like) / 1: Linear / 2: DX7-style / 3: True exponential. |
| `y` | bus | `bus` | int | Bus that this osc gets added onto (default 0) |
| `l` | `velocity` | `vel` | float | Note on velocity. Use to start an envelope or set amplitude |

### CtrlCoefs
Expand All @@ -269,7 +271,7 @@ These per-oscillator parameters use [CtrlCoefs](synth.md) notation
| ------ | -------- | ---------- | ---------- | ------------------------------------- |
| `z` | **TODO**| `load_sample` | uint x 6 | Signal to start loading sample. preset number, length(frames), samplerate, channels, midinote, loopstart, loopend. All subsequent messages are base64 encoded WAVE-style frames of audio until `length` is reached. Set `preset` and `length=0` to unload a sample from RAM. |
| `zF` | **TODO**| `disk_sample` | uint,string,uint | Set a PCM preset to play live from a WAV filename on AMY host disk. Params: preset number, filename, midinote. See `hooks` for reading files on host disk. **Only one file sample can be played at once per preset number. Use multiple presets if you want polyphony from a single sample.** |
| `zS` | **TODO**| `start_sample` | uint x 6 | Start sampling to a stereo PCM preset from bus. Params: preset number, bus, max length in frames, midinote, loopstart, loopend. bus = 1 is AMY mixed output. bus = 2 is AUDIO_IN0 + 1. Will sample until max length is reached, `stop_sample` is issued, or a new `start_sample` is issued. |
| `zS` | **TODO**| `start_sample` | uint x 6 | Start sampling to a stereo PCM preset from source. Params: preset number, source, max length in frames, midinote, loopstart, loopend. source = 1 is AMY mixed output. source = 2 is AUDIO_IN0 + 1. Will sample until max length is reached, `stop_sample` is issued, or a new `start_sample` is issued. |
| `zO` | **TODO**| `stop_sample` | uint | Stop sampling. Does nothing if no sampling active. param ignored. |

### WAVETABLE wave type
Expand All @@ -284,20 +286,31 @@ These per-oscillator parameters use [CtrlCoefs](synth.md) notation
- Each wavetable preset is expected to be one 64-cycle table (normally `16384` samples total, `256` samples per cycle).
- `duty` crossfades across the 64 cycles inside the selected wavetable preset.

### Per-bus Effects

Each of the `y` buses has separate effects units. You set their parameters with commands such as `amy.send(bus=0, reverb=1)` (or `y0h1`).

The final mixdown of the buses onto the AMY output is controlled by one value per bus passed to the `volume` (`V`) command.

Default AMY has 4 buses, 0..3. If the bus (`y`) is not specified for one of these commands, it defaults to 0.

| Wire code | C `amy_event` | Python / JS | Type-range | Notes |
| ------ | -------- | ---------- | ---------- | ------------------------------------- |
| `h` | `reverb_level, reverb_liveness, reverb_damping, reverb_xover_hz` | `reverb` | float[,float,float,float] | Reverb parameters -- level, liveness, damping, xover: Level is for output mix;
| `k` | `chorus_level, chorus_max_delay, chorus_lfo_freq, chorus_depth` | `chorus` | float[,float,float,float] | Chorus parameters -- level, delay, freq, depth: Level is for output mix (0 to turn off); delay is max in samples (320); freq is LFO rate in Hz (0.5); depth is proportion of max delay (0.5). |
| `M` | `echo_level, echo_delay_ms, echo_max_delay_ms, echo_feedback, echo_filter_coef` | `echo` | float[,int,int,float,float] | Echo parameters -- level, delay_ms, max_delay_ms, feedback, filter_coef (-1 is HPF, 0 is flat, +1 is LPF). |
| `x` | `eq_l, eq_m, eq_h` |`eq` | float,float,float | Equalization in dB low (~800Hz) / med (~2500Hz) / high (~7500Gz) -15 to 15. 0 is off. default 0. |

### Other

| Wire code | C `amy_event` | Python / JS | Type-range | Notes |
| ------ | -------- | ---------- | ---------- | ------------------------------------- |
| `H` | `sequence[3]` | `sequence` | int,int,int | Tick offset, period, tag for sequencing |
| `h` | `reverb_level, reverb_liveness, reverb_damping, reverb_xover_hz` | `reverb` | float[,float,float,float] | Reverb parameters -- level, liveness, damping, xover: Level is for output mix; liveness controls decay time, 1 = longest, default 0.85; damping is extra decay of high frequencies, default 0.5; xover is damping crossover frequency, default 3000 Hz. |
| `j` | `tempo` | `tempo` | float | The tempo (BPM, quarter notes) of the sequencer. Defaults to 108.0. |
| `k` | `chorus_level, chorus_max_delay, chorus_lfo_freq, chorus_depth` | `chorus` | float[,float,float,float] | Chorus parameters -- level, delay, freq, depth: Level is for output mix (0 to turn off); delay is max in samples (320); freq is LFO rate in Hz (0.5); depth is proportion of max delay (0.5). |
| `M` | `echo_level, echo_delay_ms, echo_max_delay_ms, echo_feedback, echo_filter_coef` | `echo` | float[,int,int,float,float] | Echo parameters -- level, delay_ms, max_delay_ms, feedback, filter_coef (-1 is HPF, 0 is flat, +1 is LPF). |
| `N` | `latency_ms`| `latency_ms` | uint | Sets latency in ms. default 0 (see LATENCY) |
| `s` | `pitch_bend` | `pitch_bend` | float | Sets the global pitch bend, by default modifying all note frequencies by (fractional) octaves up or down |
| `t` | `time` | `time` | uint | Request playback time relative to some fixed start point on your host, in ms. Allows precise future scheduling. |
| `V` | `volume`| `volume` | float 0-10 | Volume knob for entire synth, default 1.0 |
| `x` | `eq_l, eq_m, eq_h` |`eq` | float,float,float | Equalization in dB low (~800Hz) / med (~2500Hz) / high (~7500Gz) -15 to 15. 0 is off. default 0. |
| `V` | `volume`| `volume` | float, float, ... | Volume knob for each bus in the final mixdown, default 1.0 |
| `g` | `client` | `client` | uint | Client number for Alles distributed synthesis. |
| `W` | `external_channel` | `external_channel` | uint | External channel routing (used by Tulip for CV output). |
| `D` | **TODO** | `debug` | uint, 2-4 | 2 shows queue sample, 3 shows oscillator data, 4 shows modified oscillator. Will interrupt audio! |
Expand Down
6 changes: 3 additions & 3 deletions docs/synth.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,12 @@ amy.send(osc=1, wave=amy.PCM_RIGHT, preset=1025, pan=1, note=60, vel=1)

### Sampling

AMY can also sample directly into a PCM memory buffer from a `bus`. [A bus in AMY is a work in progress](https://github.com/shorepine/amy/issues/114) but for now we support two stereo buses: `bus=1` is the final AMY output and `bus=2` is just `AUDIO_IN0` and `AUDIO_IN1`. To start sampling to a PCM preset, use `start_sample`:
AMY can also sample directly into a PCM memory buffer. We support two stereo sampling sources: `source=amy.SAMPLE_FROM_OUTPUT` is the final AMY output and `source=amy.SAMPLE_FROM_AUDIO_IN` is just `AUDIO_IN0` and `AUDIO_IN1`. To start sampling to a PCM preset, use `start_sample`:

```python
amy.start_sample(preset=1024, bus=0, max_frames=44100) # sample for one second
amy.start_sample(preset=1024, source=amy.SAMPLE_FROM_AUDIO_IN, max_frames=44100) # sample for one second
amy.stop_sample() # stop all sampling, not needed if using max_frames
amy.start_sample(preset=1024, bus=1, max_frames=11025, midinote=60) # set base midi note, looping, too
amy.start_sample(preset=1024, source=amy.SAMPLE_FROM_OUTPUT, max_frames=11025, midinote=60) # set base midi note, looping, too
amy.send(osc=0, wave=amy.PCM_LEFT, preset=1024, pan=0, note=72, vel=1) # play back AUDIO_IN sample an octave higher
amy.send(osc=1, wave=amy.PCM_RIGHT, preset=1024, pan=1, note=72, vel=1)
```
Expand Down
5 changes: 3 additions & 2 deletions src/amy-example.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ void test_patch_set() {
e.wave = SINE;
amy_add_event(&e);

// Change the global volume.
// Change the global volume for bus 0.
e = amy_default_event();
e.volume = 2.0f;
int bus = 0;
e.volume[bus] = 2.0f;
amy_add_event(&e);
}

Expand Down
Loading
Loading