Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get partials.py and partials.c up to date with latest AMY, including Loris decomposition playback #151

Merged
merged 12 commits into from
Aug 14, 2024
Merged
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
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.
Loading