Skip to content

Commit

Permalink
Merge pull request #151 from shorepine/partials
Browse files Browse the repository at this point in the history
Get `partials.py` and `partials.c` up to date with latest AMY, including Loris decomposition playback
  • Loading branch information
bwhitman authored Aug 14, 2024
2 parents 0fe08d9 + a1cb534 commit 113dfac
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 46 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ LIBS = -lpthread -lm
# on macOS, need to link to AudioUnit, CoreAudio, and CoreFoundation
ifeq ($(shell uname -s), Darwin)
LIBS += -framework AudioUnit -framework CoreAudio -framework CoreFoundation

# Needed for brew's python3.12+ on MacOS
EXTRA_PIP_ENV = PIP_BREAK_SYSTEM_PACKAGES=1
endif

# on Raspberry Pi, at least under 32-bit mode, libatomic and libdl are needed.
Expand Down Expand Up @@ -66,7 +69,7 @@ amy-message: $(OBJECTS) src/amy-message.o
$(CC) $(OBJECTS) src/amy-message.o -Wall $(LIBS) -o $@

amy-module: amy-example
${PYTHON} -m pip install -r requirements.txt; touch src/amy.c; cd src; ${PYTHON} -m pip install . --force-reinstall; cd ..
${EXTRA_PIP_ENV} ${PYTHON} -m pip install -r requirements.txt; touch src/amy.c; cd src; ${EXTRA_PIP_ENV} ${PYTHON} -m pip install . --force-reinstall; cd ..

test: amy-module
${PYTHON} test.py
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,13 @@ Here's the full list:
| S | reset | uint | resets given oscillator. set to > OSCS to reset all oscillators, gain and EQ |
| s | pitch_bend | float | Sets the global pitch bend, by default modifying all note frequencies by (fractional) octaves up or down |
| t | time | uint | ms of expected playback since some fixed start point on your host. you should always give this if you can. |
| T | eg0_type | uint 0-3 | Type for Envelope Generator 0 - 0: Normal (RC-like) / 1: Linear / 2: DX7-style / 3: True exponential. |
| 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 |
| x | eq_l | float | in dB, fc=800Hz amount, -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. |
| y | eq_m | float | in dB, fc=2500Hz amount, -15 to 15. 0 is off. default 0. |
| z | eq_h | float | in dB, fc=7500Hz amount, -15 to 15. 0 is off. default 0. |

Expand Down
2 changes: 1 addition & 1 deletion amy.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def message(osc=0, wave=None, patch=None, note=None, vel=None, amp=None, freq=No
if(bp0 is not None): m = m +"A%s" % (bp0)
if(bp1 is not None): m = m +"B%s" % (bp1)
if(eg0_type is not None): m = m + "T" + str(eg0_type)
if(eg1_type is not None): m = m + "W" + str(eg1_type)
if(eg1_type is not None): m = m + "X" + str(eg1_type)
if(algo_source is not None): m = m +"O%s" % (algo_source)
if(chained_osc is not None): m = m + "c" + str(chained_osc)
if(clone_osc is not None): m = m + "C" + str(clone_osc)
Expand Down
72 changes: 52 additions & 20 deletions partials.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import amy, sys
import pydub
# We keep this path hack around for dan, whose system needs it to get Loris to work. You shouldn't need it.
sys.path.append('/usr/local/lib/python' + '.'.join(sys.version.split('.', 2)[:2]) + '/site-packages')
import loris
import time
import numpy as np
from math import pi, log2
from collections import deque

from collections import deque, defaultdict
from copy import copy

def list_from_py2_iterator(obj, how_many):
# Oof, the loris object uses some form of iteration that py3 doesn't like.
Expand Down Expand Up @@ -96,6 +98,7 @@ def sequence(filename, max_len_s = 10, amp_floor=-30, hop_time=0.04, max_oscs=am
min_q_len = max_oscs
# Now add in a voice / osc #
osc_map = {}
osc_free_at = {} # map of osc# -> time to safely free at (6ms after you want)
osc_q = deque(range(max_oscs))
for i,s in enumerate(time_ordered):
next_idx = -1
Expand All @@ -116,6 +119,14 @@ def sequence(filename, max_len_s = 10, amp_floor=-30, hop_time=0.04, max_oscs=am
# Start the partials at 0
s[0] = s[0] - first_time

# Free oscs if it's time to
oscs_to_consider_freeing = copy(list(osc_free_at.keys()))
for osc in oscs_to_consider_freeing:
if(s[0]>=osc_free_at[osc]):
#print("freeing osc %d, it's %d and it waited until %d" % (osc, s[0], osc_free_at[osc]))
osc_q.appendleft(osc)
del osc_free_at[osc]

if(s[4]>=0): #new partial
if(len(osc_q)):
osc_map[s[1]] = osc_q.popleft()
Expand All @@ -129,7 +140,11 @@ def sequence(filename, max_len_s = 10, amp_floor=-30, hop_time=0.04, max_oscs=am
sequence.append(s)
if(s[4] == -2): # last bp
# Put the oscillator back
osc_q.appendleft(osc)
# per dan, ONLY put this osc back once at least 12ms (i.e., def 1 AMY frame) has gone by after we get a -2
#print("not letting osc %d die until 6ms from now %d = %d" % (s[1], s[0], s[0]+6))
osc_free_at[s[1]] = s[0] + 12
#osc_q.appendleft(osc)

if(len(osc_q) < min_q_len): min_q_len = len(osc_q)
print("%d partials and %d breakpoints, max oscs used at once was %d" % (partial_count, len(sequence), max_oscs - min_q_len))
# Fix sustain_ms
Expand Down Expand Up @@ -160,37 +175,54 @@ def play(sequence, osc_offset=0, sustain_ms = -1, sustain_len_ms = 0, time_ratio
print("Moving sustain_ms from %d to %d" % (sustain_ms, sequence[-1][0]-100))
sustain_ms = sequence[-1][0] - 100

# Use a default dict so that values we haven't written yet appear as zeros.
time_since_osc_onset = defaultdict(int)

for i,s in enumerate(sequence):
# Wait for the item in the sequence to be close, so I don't overflow the synthesizers' state
while(my_start_time + (s[0] / time_ratio) > (amy.millis() - 500)):
time.sleep(0.01)

# Make envelope strings
bp0 = "0,1.0,%d,%s,0,0" % (s[5] / time_ratio, amy.trunc(s[6]))
bp1 = "0,0.0,%d,%s,0,0" % (s[5] / time_ratio, amy.trunc(log2_or_0(s[7])))
# Make envelope strings. This is weird because we rewrite the envelopes while the oscillator
# is running (and the envelopes are part-evaluated) to allow an unlimited number of segments.
# To make it work, each time we change it we have to make a placeholder first interval to get
# us to the right time offset after the start of the partial (the time frame used by the
# envelope generator).
osc = s[1] + osc_offset
delta_time = int(round(s[5] / time_ratio))
bp0 = "%d,1.0,%d,%s,0,0" % (time_since_osc_onset[osc], delta_time, amy.trunc(s[6]))
bp1 = "%d,0.0,%d,%s,0,0" % (time_since_osc_onset[osc], delta_time, amy.trunc(log2_or_0(s[7])))
# Update the base time for the next segment.
time_since_osc_onset[osc] += delta_time

if(sustain_ms > 0 and sustain_offset == 0):
if(s[0]/time_ratio > sustain_ms/time_ratio):
sustain_offset = sustain_len_ms/time_ratio

osc = s[1]+osc_offset

partial_args = {}

partial_args.update({"time":my_start_time + (s[0]/time_ratio + sustain_offset),
"osc":s[1]+osc_offset,
"wave":amy.PARTIAL,
partial_args = {
"time": my_start_time + (s[0]/time_ratio + sustain_offset),
"osc": s[1]+osc_offset,
"wave": amy.PARTIAL,
"amp": "%s,0,0,1,0" % amy.trunc(s[3]*amp_ratio),
"freq":"%s,0,0,0,1" % amy.trunc(s[2]*pitch_ratio),
"bp0":bp0, "bp1":bp1, "eg0_type": amy.ENVELOPE_LINEAR, "eg1_type":amy.ENVELOPE_LINEAR
})
"freq": "%s,0,0,0,1" % amy.trunc(s[2]*pitch_ratio),
"bp0": bp0,
"bp1": bp1,
"eg0_type": amy.ENVELOPE_LINEAR,
"eg1_type": amy.ENVELOPE_LINEAR
}

if(s[4]==-2): #end, add note off
partial_args['amp'] = "0,0,0,1,0"
amy.send(**partial_args, vel=0)
#partial_args['amp'] = "0,0,0,1,0"
#partial_args['vel'] = 0
# Reset the incremental envelope segment start time.
time_since_osc_onset[osc] = 0
elif(s[4]==-1): # continue
amy.send(**partial_args)
pass
else: #start, add phase and note on
amy.send(**partial_args, vel=s[3]*amp_ratio, phase=s[4])
partial_args['vel'] = 1.0 # Velocity value is ignored?
partial_args['phase'] = s[4]

amy.send(**partial_args)

return sequence[-1][0]/time_ratio

Expand Down
36 changes: 24 additions & 12 deletions src/amy.c
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ void amy_add_event_internal(struct event e, uint16_t base_osc) {
if(AMY_IS_SET(e.eq_m)) { d.param=EQ_M; d.data = *(uint32_t *)&e.eq_m; add_delta_to_queue(d); }
if(AMY_IS_SET(e.eq_h)) { d.param=EQ_H; d.data = *(uint32_t *)&e.eq_h; add_delta_to_queue(d); }

if(AMY_IS_SET(e.eg_type[0])) { d.param=EG0_TYPE; d.data = e.eg_type[0]; add_delta_to_queue(d); }
if(AMY_IS_SET(e.eg_type[1])) { d.param=EG1_TYPE; d.data = e.eg_type[1]; add_delta_to_queue(d); }

if(e.algo_source[0] != 0) {
struct synthinfo t;
parse_algorithm_source(&t, e.algo_source);
Expand Down Expand Up @@ -575,7 +578,7 @@ void reset_osc(uint16_t i ) {
synth[i].amp_coefs[j] = 0;
synth[i].amp_coefs[COEF_VEL] = 1.0f;
synth[i].amp_coefs[COEF_EG0] = 1.0f;
msynth[i].amp = 1.0f;
msynth[i].amp = 0; // This matters for wave=PARTIAL, where msynth amp is effectively 1-frame delayed.
msynth[i].last_amp = 0;
for (int j = 0; j < NUM_COMBO_COEFS; ++j)
synth[i].logfreq_coefs[j] = 0;
Expand Down Expand Up @@ -921,6 +924,9 @@ void play_event(struct delta d) {
synth[synth[d.osc].algo_source[which_source]].status = IS_ALGO_SOURCE;
}

if (d.param == EG0_TYPE) synth[d.osc].eg_type[0] = d.data;
if (d.param == EG1_TYPE) synth[d.osc].eg_type[1] = d.data;

// for global changes, just make the change, no need to update the per-osc synth
if(d.param == VOLUME) amy_global.volume = *(float *)&d.data;
if(d.param == PITCH_BEND) amy_global.pitch_bend = *(float *)&d.data;
Expand Down Expand Up @@ -1068,16 +1074,22 @@ void hold_and_modify(uint16_t osc) {
msynth[osc].pan = combine_controls(ctrl_inputs, synth[osc].pan_coefs);
// amp is a special case - coeffs apply in log domain.
// Also, we advance one frame by writing both last_amp and amp (=next amp)
float new_last_amp = combine_controls_mult(ctrl_inputs, synth[osc].amp_coefs);
// Prevent hard-off on transition to release by updating last_amp only for nonzero new_last_amp.
if (new_last_amp > 0) {
msynth[osc].last_amp = new_last_amp;
// *Except* for partials, where we allow one frame of ramp-on.
float new_amp = combine_controls_mult(ctrl_inputs, synth[osc].amp_coefs);
if (synth[osc].wave == PARTIAL) {
msynth[osc].last_amp = msynth[osc].amp;
msynth[osc].amp = new_amp;
} else {
// Prevent hard-off on transition to release by updating last_amp only for nonzero new_last_amp.
if (new_amp > 0) {
msynth[osc].last_amp = new_amp;
}
// Advance the envelopes to the beginning of the next frame.
ctrl_inputs[COEF_EG0] = S2F(compute_breakpoint_scale(osc, 0, AMY_BLOCK_SIZE));
ctrl_inputs[COEF_EG1] = S2F(compute_breakpoint_scale(osc, 1, AMY_BLOCK_SIZE));
msynth[osc].amp = combine_controls_mult(ctrl_inputs, synth[osc].amp_coefs);
if (msynth[osc].amp <= 0.001) msynth[osc].amp = 0;
}
ctrl_inputs[COEF_EG0] = S2F(compute_breakpoint_scale(osc, 0, AMY_BLOCK_SIZE));
ctrl_inputs[COEF_EG1] = S2F(compute_breakpoint_scale(osc, 1, AMY_BLOCK_SIZE));
msynth[osc].amp = combine_controls_mult(ctrl_inputs, synth[osc].amp_coefs);
if (msynth[osc].amp <= 0.001) msynth[osc].amp = 0;

// synth[osc].feedback is copied to msynth in pcm_note_on, then used to track note-off for looping PCM.
// For PCM, don't re-copy it every loop, or we'd lose track of that flag. (This means you can't change feedback mid-playback for PCM).
if (synth[osc].wave != PCM) msynth[osc].feedback = synth[osc].feedback;
Expand Down Expand Up @@ -1623,14 +1635,14 @@ struct event amy_parse_message(char * message) {
case 'S': e.reset_osc = atoi(message + start); break;
case 's': e.pitch_bend = atoff(message + start); break;
/* t used for time */
/* T unused */
case 'T': e.eg_type[0] = atoi(message + start); break;
/* U used by Alles for sync */
case 'u': patches_store_patch(message+start); AMY_PROFILE_STOP(AMY_PARSE_MESSAGE) return amy_default_event();
case 'v': e.osc=((atoi(message + start)) % (AMY_OSCS+1)); break; // allow osc wraparound
case 'V': e.volume = atoff(message + start); break;
case 'w': e.wave=atoi(message + start); break;
/* W used by Tulip for CV, external_channel */
/* X available */
case 'X': e.eg_type[1] = atoi(message + start); break;
case 'x': e.eq_l = atoff(message+start); break;
/* Y available */
case 'y': e.eq_m = atoff(message+start); break;
Expand Down
7 changes: 4 additions & 3 deletions src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,10 @@ enum params{
ALGO_SOURCE_END=100+MAX_ALGO_OPS, // 106
BP_START=ALGO_SOURCE_END + 1, // 107..138
BP_END=BP_START + (MAX_BREAKPOINT_SETS * MAX_BREAKPOINTS * 2), // 139
CLONE_OSC, // 140
RESET_OSC, // 141
NO_PARAM // 142
EG0_TYPE, EG1_TYPE, // 140, 141
CLONE_OSC, // 142
RESET_OSC, // 143
NO_PARAM // 144
};

#ifdef AMY_DEBUG
Expand Down
4 changes: 3 additions & 1 deletion src/oscillators.c
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ SAMPLE render_partial(SAMPLE * buf, uint16_t osc) {
PHASOR step = F2P(freq / (float)AMY_SAMPLE_RATE); // cycles per sec / samples per sec -> cycles per sample
SAMPLE amp = F2S(msynth[osc].amp);
SAMPLE last_amp = F2S(msynth[osc].last_amp);
//printf("render_partial: time %lld logfreq %f freq %f amp %f step %f\n", total_samples, msynth[osc].logfreq, freq, S2F(amp), P2F(step) * synth[osc].lut->table_size);
//printf("render_partial: time %.3f logfreq %f freq %f last_amp %f amp %f step %f\n", (float)total_samples/(float)AMY_SAMPLE_RATE, msynth[osc].logfreq, freq, S2F(last_amp), S2F(amp), P2F(step) * synth[osc].lut->table_size);
SAMPLE max_value;
synth[osc].phase = render_lut(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value);
msynth[osc].last_amp = msynth[osc].amp;
Expand All @@ -533,6 +533,8 @@ void partial_note_on(uint16_t osc) {
float freq = freq_of_logfreq(msynth[osc].logfreq);
float period_samples = (float)AMY_SAMPLE_RATE / freq;
synth[osc].lut = choose_from_lutset(period_samples, sine_fxpt_lutset);
// Partials ramp up from zero.
msynth[osc].amp = 0;
}

void partial_note_off(uint16_t osc) {
Expand Down
13 changes: 5 additions & 8 deletions src/partials.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ SAMPLE render_partials(SAMPLE *buf, uint16_t osc) {
// Find our ratio using the midi note of the analyzed partial
float freq_logratio = msynth[osc].logfreq - logfreq_for_midi_note(patch.midi_note);

//printf("time %f rel %f: freqlogratio %f new pb: osc %d t_ms %d amp %f freq %f phase %f logfreq %f\n", total_samples / (float)AMY_SAMPLE_RATE, ms_since_started / (float)AMY_SAMPLE_RATE, freq_logratio, pb.osc, pb.ms_offset, pb.amp, pb.freq, pb.phase, logfreq_of_freq(pb.freq));
//printf("time %.3f rel %f: freqlogratio %f new pb: osc %d t_ms %d amp %f freq %f phase %f logfreq %f\n", total_samples / (float)AMY_SAMPLE_RATE, ms_since_started / (float)AMY_SAMPLE_RATE, freq_logratio, pb.osc, pb.ms_offset, pb.amp, pb.freq, pb.phase, logfreq_of_freq(pb.freq));

// All the types share these params or are overwritten
synth[o].wave = PARTIAL;
Expand Down Expand Up @@ -116,11 +116,10 @@ SAMPLE render_partials(SAMPLE *buf, uint16_t osc) {
synth[o].logfreq_coefs[0] = logfreq_of_freq(pb.freq) + freq_logratio;
//printf("[%d %d] o %d continue partial\n", total_samples, ms_since_started, o);
} else if(partial_code==2) { // partial is done, give it one buffer to ramp to zero.
synth[o].amp_coefs[0] = 0;
synth[o].amp_coefs[0] = 0.0001;
//partial_note_off(o);
} else { // start of a partial,
//printf("[%d %d] o %d start partial\n", total_samples,ms_since_started, o);
msynth[o].last_amp = 0;
partial_note_on(o);
}
synth[osc].step++;
Expand All @@ -137,12 +136,10 @@ SAMPLE render_partials(SAMPLE *buf, uint16_t osc) {
for(uint16_t i=osc+1;i<osc+1+oscs;i++) {
uint16_t o = i % AMY_OSCS;
if(synth[o].status ==IS_ALGO_SOURCE) {
// msynth amp used to lag one frame behind, but we advanced it one frame.
// For partials, retard it again to preserve the old behavior.
float last_amp_on_entry = msynth[o].last_amp;
// hold_and_modify contains a special case for wave == PARTIAL so that
// envelope value are delayed by 1 frame compared to other oscs
// so that partials fade in over one frame from zero amp.
hold_and_modify(o);
msynth[o].amp = msynth[o].last_amp;
msynth[o].last_amp = last_amp_on_entry;
//printf("[%d %d] %d amp %f (%f) freq %f (%f) on %d off %d bp0 %d %f bp1 %d %f wave %d\n", total_samples, ms_since_started, o, synth[o].amp, msynth[o].amp, synth[o].freq, msynth[o].freq, synth[o].note_on_clock, synth[o].note_off_clock, synth[o].breakpoint_times[0][0],
// synth[o].breakpoint_values[0][0], synth[o].breakpoint_times[1][0], synth[o].breakpoint_values[1][0], synth[o].wave);
//for(uint16_t j=0;j<AMY_BLOCK_SIZE;j++) pbuf[j] = 0;
Expand Down
Binary file modified tests/ref/TestPartial.wav
Binary file not shown.

0 comments on commit 113dfac

Please sign in to comment.