Skip to content

Commit

Permalink
Merge pull request #261 from shorepine/partials-interp
Browse files Browse the repository at this point in the history
C version of piano partials
  • Loading branch information
bwhitman authored Jan 6, 2025
2 parents 70f122f + 053c248 commit ad79b46
Show file tree
Hide file tree
Showing 12 changed files with 1,282 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ default: $(TARGET)
all: default

SOURCES = src/algorithms.c src/amy.c src/envelope.c src/examples.c \
src/filters.c src/oscillators.c src/pcm.c src/partials.c src/custom.c \
src/filters.c src/oscillators.c src/pcm.c src/partials.c src/interp_partials.c src/custom.c \
src/delay.c src/log2_exp2.c src/patches.c src/transfer.c src/sequencer.c \
src/libminiaudio-audio.c

Expand Down
2 changes: 1 addition & 1 deletion 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, BYO_PARTIALS, AUDIO_IN0, AUDIO_IN1, CUSTOM, OFF = range(16)
SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, BYO_PARTIALS, INTERP_PARTIALS, AUDIO_IN0, AUDIO_IN1, CUSTOM, OFF = range(17)
FILTER_NONE, FILTER_LPF, FILTER_BPF, FILTER_HPF, FILTER_LPF24 = range(5)
ENVELOPE_NORMAL, ENVELOPE_LINEAR, ENVELOPE_DX7, ENVELOPE_TRUE_EXPONENTIAL = range(4)
RESET_SEQUENCER, RESET_ALL_OSCS, RESET_TIMEBASE, RESET_AMY = (4096, 8192, 16384, 32768)
Expand Down
38 changes: 7 additions & 31 deletions amy_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,38 +346,14 @@ def make_clipping_lut(filename):
f.write("#endif\n")
print("wrote", filename)

# Generate the dpwe piano patch

def make_piano_patch():
import json, amy
params_file = 'experiments/Piano.ff.D5.json'

# Read in the params file.
with open(params_file, 'r') as f:
bp_params = json.load(f)

base_osc=0
base_freq=261.63
stretch_coef=0.038

kwargs = {}
num_partials = len(bp_params)
amy.send(osc=base_osc, wave=amy.BYO_PARTIALS, num_partials=num_partials, amp={'eg0': 0}, **kwargs)

harm_nums = range(1, num_partials + 1)

# Quadratic partial stretching
#harm_freqs = [n * (261.8 + stretch_coef * n * n) for n in harm_nums]

for i in harm_nums:
# 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
f0 = bp_params[i - 1][0]
bp_vals = bp_params[i - 1][1]
bp_string = ','.join("%d,%.3f" % (n, max(0, 100 * val - 0.001)) for n, val in bp_vals)
bp_string += ',200,0'
#print(bp_string)
amy.send(osc=base_osc + i, wave=amy.PARTIAL, freq=f0, bp0=bp_string, eg0_type=amy.ENVELOPE_TRUE_EXPONENTIAL, **kwargs)

return len(harm_nums)+1
import amy
# This just allocates the 20 oscs needed for a INTERP_PARTIALS patch
# dpwe wants to add a `num_suboscs` field to fix this behavior soon
amy.send(osc=0, wave=amy.INTERP_PARTIALS, patch=0)
amy.send(osc=20, wave=amy.PARTIAL)
return 21

def make_patches(filename):
def nothing(message):
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ if (TARGET tinyusb_device)
examples.c
oscillators.c
partials.c
interp_partials.c
pcm.c
)

Expand Down
9 changes: 6 additions & 3 deletions src/amy.c
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ void osc_note_on(uint16_t osc, float initial_freq) {
if(AMY_HAS_PARTIALS == 1) {
//if(synth[osc].wave==PARTIAL) partial_note_on(osc);
if(synth[osc].wave==PARTIALS || synth[osc].wave==BYO_PARTIALS) partials_note_on(osc);
if(synth[osc].wave==INTERP_PARTIALS) interp_partials_note_on(osc);
}
if(AMY_HAS_CUSTOM == 1) {
if(synth[osc].wave==CUSTOM) custom_note_on(osc, initial_freq);
Expand Down Expand Up @@ -1091,11 +1092,12 @@ 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 || synth[d.osc].wave==BYO_PARTIALS) {
} else if(synth[d.osc].wave==PARTIALS || synth[d.osc].wave==BYO_PARTIALS || synth[d.osc].wave==INTERP_PARTIALS) {
#if AMY_HAS_PARTIALS == 1
AMY_UNSET(synth[d.osc].note_on_clock);
synth[d.osc].note_off_clock = total_samples;
partials_note_off(d.osc);
if(synth[d.osc].wave==INTERP_PARTIALS) interp_partials_note_off(d.osc);
else partials_note_off(d.osc);
#endif
} else if(synth[d.osc].wave==PCM) {
pcm_note_off(d.osc);
Expand Down Expand Up @@ -1278,7 +1280,8 @@ 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 || synth[osc].wave == BYO_PARTIALS) max_val = render_partials(buf, osc);
if(synth[osc].wave == PARTIALS || synth[osc].wave == BYO_PARTIALS || synth[osc].wave == INTERP_PARTIALS)
max_val = render_partials(buf, osc);
}
}
if(AMY_HAS_CUSTOM == 1) {
Expand Down
12 changes: 8 additions & 4 deletions src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ enum coefs{
#define PARTIAL 9
#define PARTIALS 10
#define BYO_PARTIALS 11
#define AUDIO_IN0 12
#define AUDIO_IN1 13
#define CUSTOM 14
#define WAVE_OFF 15
#define INTERP_PARTIALS 12
#define AUDIO_IN0 13
#define AUDIO_IN1 14
#define CUSTOM 15
#define WAVE_OFF 16
/// !!!!!!! IF YOU CHANGE THE WAV VALUES, YOU HAVE TO KEEP amy.py:8 IN SYNC BY HAND !!!!!!!!

// synth[].status values
#define SYNTH_OFF 0
Expand Down Expand Up @@ -511,6 +513,8 @@ extern SAMPLE render_algo(SAMPLE * buf, uint16_t osc, uint8_t core) ;
extern SAMPLE render_partial(SAMPLE *buf, uint16_t osc) ;
extern void partials_note_on(uint16_t osc);
extern void partials_note_off(uint16_t osc);
extern void interp_partials_note_on(uint16_t osc);
extern void interp_partials_note_off(uint16_t osc);
extern void patches_load_patch(struct event e);
extern void patches_event_has_voices(struct event e);
extern void patches_reset();
Expand Down
149 changes: 149 additions & 0 deletions src/interp_partials.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// interp_partials - AMY kernel-side implementation of the interpolated partials-based synthesis originally implemented in tulip_piano.py.

#include "amy.h"

typedef struct {
// How many sample_times_ms are there?
uint16_t num_sample_times_ms;
// Pointer to an array of the sample_times_ms
const uint16_t *sample_times_ms;
// How many velocities are defined for this voice (same for all notes)
uint16_t num_velocities;
// Pointer to a array of the MIDI velocities.
const uint8_t *velocities;
// How many different pitches do we define? (All velocities are provided for each)
uint16_t num_pitches;
// Pointer to array of structures defining each note (pitch + velocity) entry.
const uint8_t *pitches;
// How many harmonics are allocated for each of the num_velocities * num_pitches notes.
const uint8_t *num_harmonics;
// MIDI Cents freqs for each harmonic.
const uint16_t *harmonics_freq;
// num_sample_times_ms uint8_t dB envelope values for each harmonic.
const uint8_t *harmonics_mags;
} interp_partials_voice_t;

#include "interp_partials.h"

#define MAX_NUM_MAGNITUDES 24

void _cumulate_scaled_harmonic_params(float *harm_param, int harmonic_index, float alpha, const interp_partials_voice_t *partials_voice) {
int num_bps = partials_voice->num_sample_times_ms;
// Pitch
harm_param[0] += alpha * partials_voice->harmonics_freq[harmonic_index];
// Envelope magnitudes
for (int i = 0; i < num_bps; ++i)
harm_param[1 + i] += alpha * partials_voice->harmonics_mags[harmonic_index * num_bps + i];
}

int _harmonic_base_index_for_pitch_vel(int pitch_index, int vel_index, const interp_partials_voice_t *partials_voice) {
int note_number = partials_voice->num_velocities * pitch_index + vel_index;
int harmonic_index = 0;
for (int i = 0; i < note_number; ++i)
harmonic_index += partials_voice->num_harmonics[i];
return harmonic_index;
}

float _logfreq_of_midi_cents(float midi_cents) {
// Frequency is already log scaled, but need to re-center and change from 1200/oct to 1.0/oct.
return (midi_cents - (100 * ZERO_MIDI_NOTE)) / 1200.f;
}

float _env_lin_of_db(float db) {
float lin = powf(10.f, (db - 100) / 20.f) - 0.001;
if (lin < 0) return 0;
return lin;
}

void _osc_on_with_harm_param(uint16_t o, float *harm_param, const interp_partials_voice_t *partials_voice) {
// We coerce this voice into being a partial, regardless of user wishes.
synth[o].wave = PARTIAL;
synth[o].patch = -1; // Flag that this is an envelope-based partial
// Setup the specified frequency.
synth[o].logfreq_coefs[COEF_CONST] = _logfreq_of_midi_cents(harm_param[0]);
// Setup envelope.
synth[o].breakpoint_times[0][0] = 0;
synth[o].breakpoint_values[0][0] = 0;
int last_time = 0;
for (int bp = 0; bp < partials_voice->num_sample_times_ms; ++bp) {
synth[o].breakpoint_times[0][bp + 1] = (partials_voice->sample_times_ms[bp] - last_time) * AMY_SAMPLE_RATE / 1000;
synth[o].breakpoint_values[0][bp + 1] = _env_lin_of_db(harm_param[bp + 1]);
last_time = partials_voice->sample_times_ms[bp];
}
// Final release
synth[o].breakpoint_times[0][partials_voice->num_sample_times_ms + 1] = 200 * AMY_SAMPLE_RATE / 1000;
synth[o].breakpoint_values[0][partials_voice->num_sample_times_ms + 1] = 0;
// Decouple osc freq and amp from note and amp.
synth[o].logfreq_coefs[COEF_NOTE] = 0;
synth[o].amp_coefs[COEF_VEL] = 0;
// Other osc params.
synth[o].status = SYNTH_IS_ALGO_SOURCE;
synth[o].note_on_clock = total_samples;
AMY_UNSET(synth[o].note_off_clock);
partial_note_on(o);
}

void interp_partials_note_on(uint16_t osc) {
// Choose the interp_partials patch.
const interp_partials_voice_t *partials_voice = &interp_partials_map[synth[osc].patch % NUM_INTERP_PARTIALS_PATCHES];
float midi_note = synth[osc].midi_note;
float midi_vel = (int)roundf(synth[osc].velocity * 127.f);
// Clip
if (midi_vel < partials_voice->velocities[0]) midi_vel = partials_voice->velocities[0];
if (midi_vel > partials_voice->velocities[partials_voice->num_velocities - 1]) midi_vel = partials_voice->velocities[partials_voice->num_velocities - 1];
// Find the lower bound pitch/velocity indices.
uint8_t pitch_index = 0, vel_index = 0;
while(pitch_index < partials_voice->num_pitches - 1
&& partials_voice->pitches[pitch_index + 1] < midi_note)
++pitch_index;
while(vel_index < partials_voice->num_velocities - 1
&& partials_voice->velocities[vel_index + 1] < midi_vel)
++vel_index;
// Interp weights
float pitch_alpha = (midi_note - partials_voice->pitches[pitch_index])
/ (float)(partials_voice->pitches[pitch_index + 1] - partials_voice->pitches[pitch_index]);
float vel_alpha = (midi_vel - partials_voice->velocities[vel_index])
/ (float)(partials_voice->velocities[vel_index + 1] - partials_voice->velocities[vel_index]);
float harm_param[MAX_NUM_MAGNITUDES + 1]; // frequency + harmonic magnitudes.
int note_number = partials_voice->num_velocities * pitch_index + vel_index;
int num_harmonics = partials_voice->num_harmonics[note_number];
// Interpolate the 4 notes.
int harmonic_base_index_pl_vl =
_harmonic_base_index_for_pitch_vel(pitch_index, vel_index, partials_voice);
float alpha_pl_vl = (1.f - pitch_alpha) * (1.f - vel_alpha);
int harmonic_base_index_pl_vh =
_harmonic_base_index_for_pitch_vel(pitch_index, vel_index + 1, partials_voice);
float alpha_pl_vh = (1.f - pitch_alpha) * (vel_alpha);
int harmonic_base_index_ph_vl =
_harmonic_base_index_for_pitch_vel(pitch_index + 1, vel_index, partials_voice);
float alpha_ph_vl = (pitch_alpha) * (1.f - vel_alpha);
int harmonic_base_index_ph_vh =
_harmonic_base_index_for_pitch_vel(pitch_index + 1, vel_index + 1, partials_voice);
float alpha_ph_vh = (pitch_alpha) * (vel_alpha);
//fprintf(stderr, "interp_partials@%u: osc %d note %d vel %d pitch_x %d vel_x %d numh %d harm_bi_ll %d pitch_a %.3f vel_a %.3f alphas %.2f %.2f %.2f %.2f\n",
// total_samples, osc, midi_note, midi_vel, pitch_index, vel_index, num_harmonics, harmonic_base_index_pl_vl, pitch_alpha, vel_alpha,
// alpha_pl_vl, alpha_pl_vh, alpha_ph_vl, alpha_ph_vh);
for (int h = 0; h < num_harmonics; ++h) {
for (int i = 0; i < MAX_NUM_MAGNITUDES + 1; ++i) harm_param[i] = 0;
_cumulate_scaled_harmonic_params(harm_param, harmonic_base_index_pl_vl + h,
alpha_pl_vl, partials_voice);
_cumulate_scaled_harmonic_params(harm_param, harmonic_base_index_pl_vh + h,
alpha_pl_vh, partials_voice);
_cumulate_scaled_harmonic_params(harm_param, harmonic_base_index_ph_vl + h,
alpha_ph_vl, partials_voice);
_cumulate_scaled_harmonic_params(harm_param, harmonic_base_index_ph_vh + h,
alpha_ph_vh, partials_voice);
//fprintf(stderr, "harm %d freq %.2f bps %.3f %.3f %.3f %.3f\n", h, harm_param[0], harm_param[1], harm_param[2], harm_param[3], harm_param[4]);
_osc_on_with_harm_param(osc + 1 + h, harm_param, partials_voice);
}
}

void interp_partials_note_off(uint16_t osc) {
const interp_partials_voice_t *partials_voice = &interp_partials_map[synth[osc].patch % NUM_INTERP_PARTIALS_PATCHES];
int num_oscs = partials_voice->num_harmonics[0]; // Assume first patch has the max #harmonics.
for(uint16_t i = osc + 1; i < osc + 1 + num_oscs; i++) {
uint16_t o = i % AMY_OSCS;
AMY_UNSET(synth[o].note_on_clock);
synth[o].note_off_clock = total_samples;
}
}
Loading

0 comments on commit ad79b46

Please sign in to comment.