Skip to content

Commit

Permalink
Merge pull request #245 from shorepine/offset
Browse files Browse the repository at this point in the history
Sequencer takes offset ticks for patterns
  • Loading branch information
bwhitman authored Nov 7, 2024
2 parents 96f3f34 + bb1f31b commit e60d41b
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 61 deletions.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ It supports
* Control of overall gain and 3-band EQ
* Built in patches for PCM, DX7, Juno and partials
* A front end for Juno-6 patches and conversion setup commands
* Built-in clock for short term sequencing of events
* Built-in clock and pattern sequencer
* Can use multi-core (including microcontrollers) for rendering if available

The FM synth provides a Python library, [`fm.py`](https://github.com/shorepine/amy/blob/main/fm.py) that can convert any DX7 patch into AMY setup commands, and also a pure-Python implementation of the AMY FM synthesizer in [`dx7_simulator.py`](https://github.com/shorepine/amy/blob/main/dx7_simulator.py).
Expand Down Expand Up @@ -197,7 +197,7 @@ Here's the full list:
| `f` | `freq` | float[,float...] | Frequency of oscillator, set of ControlCoefficients. Default is 0,1,0,0,0,0,1 (from `note` pitch plus `pitch_bend`) |
| `F` | `filter_freq` | float[,float...] | Center/break frequency for variable filter, set of ControlCoefficients |
| `G` | `filter_type` | 0-4 | Filter type: 0 = none (default.) 1 = lowpass, 2 = bandpass, 3 = highpass, 4 = double-order lowpass. |
| `H` | `sequence` | int,int,int | Tick number, divider number, tag number for sequencing |
| `H` | `sequence` | int,int,int | Tick offset, length, tag for sequencing |
| `h` | `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. |
| `I` | `ratio` | float | For ALGO types, ratio of modulator frequency to base note frequency / For the PARTIALS base note, ratio controls the speed of the playback |
| `j` | `tempo` | float | The tempo (BPM, quarter notes) of the sequencer. Defaults to 108.0. |
Expand All @@ -217,7 +217,7 @@ Here's the full list:
| `r` | `voices` | int[,int] | Comma separated list of voices to send message to, or load patch into. |
| `R` | `resonance` | float | Q factor of variable filter, 0.5-16.0. default 0.7 |
| `s` | `pitch_bend` | float | Sets the global pitch bend, by default modifying all note frequencies by (fractional) octaves up or down |
| `S` | `reset_osc` | uint | Resets given oscillator. set to RESET_ALL_OSCS to reset all oscillators, gain and EQ. RESET_TIMEBASE resets the clock. RESET_AMY restarts AMY.|
| `S` | `reset` | uint | Resets given oscillator. set to RESET_ALL_OSCS to reset all oscillators, gain and EQ. RESET_TIMEBASE resets the clock. RESET_AMY restarts AMY. RESET_SEQUENCER clears the sequencer.|
| `t` | `time` | uint | Request playback time relative to some fixed start point on your host, in ms. Allows precise future scheduling. |
| `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 |
Expand Down Expand Up @@ -266,13 +266,15 @@ Both `amy.send()`s will return immediately, but you'll hear the second note play

### The sequencer

On supported platforms (right now any unix device with pthreads, and the ESP32 or related chip), AMY starts a sequencer that works on `ticks` from starting. You can reset the `ticks` to 0 with an `amy.send(reset_osc=amy.RESET_TIMEBASE)`.
On supported platforms (right now any unix device with pthreads, and the ESP32 or related chip), AMY starts a sequencer that works on `ticks` from startup. You can reset the `ticks` to 0 with an `amy.send(reset=amy.RESET_TIMEBASE)`.

Ticks run at 48 PPQ at the set tempo. The tempo defaults to 108 BPM. This means there are 108 quarter notes a minute, and `48 * 108 = 5184` ticks a minute, 86 ticks a second. The tempo can be changed with `amy.send(tempo=120)`.

You can schedule an event to happen at a precise tick with `amy.send(... ,sequence="tick,divider,tag")`. `tick` is an absolute tick number. If given, `divider` is ignored. Once AMY reaches `tick`, the rest of your event will play and the saved event will be removed from memory. If `tick` is in the past, AMY will ignore it.
You can schedule an event to happen at a precise tick with `amy.send(... ,sequence="tick,length,tag")`. `tick` can be an absolute or offset tick number. If `length` is ommited or 0, `tick` is assumed to be absolute and once AMY reaches `tick`, the rest of your event will play and the saved event will be removed from memory. If an absolute `tick` is in the past, AMY will ignore it.

You can schedule repeating events (like a step sequencer or drum machine) with `divider`. For example a `divider` of 48 will trigger once every quarter note. A `divider` of 24 will happen twice every quarter note. A `divider` of 96 will happen every two quarter notes. `divider` can be any number to allow for complex rhythms.
You can schedule repeating events (like a step sequencer or drum machine) with `length`, which is the length of the sequence in ticks. For example a `length` of 48 with `ticks` omitted or 0 will trigger once every quarter note. A `length` of 24 will happen twice every quarter note. A `length` of 96 will happen every two quarter notes. `length` can be any whole number to allow for complex rhythms.

For pattern sequencers like drum machines, you will also want to use `tick` alongisde `length`. If both are given and nonzero, `tick` is assumed to be an offset on the `length`. For example, for a 16-step drum machine pattern running on eighth notes (PPQ/2), you would use a `length` of `16 * 24 = 384`. The first slot of the drum machine would have a `tick` of 0, the 2nd would have a `tick` offset of 24, and so on.

`tag` should be given, and will be `0` if not. You should set `tag` to a random or incrementing number in your code that you can refer to later. `tag` allows you to replace or delete the event once scheduled.

Expand All @@ -281,13 +283,19 @@ If you are including AMY in a program, you can set the hook `void (*amy_external
Sequencer examples:

```python
amy.send(osc=2, vel=1, wave=amy.PCM, patch=2, sequence="1000,,3") # play a PCM drum at absolute tick 1000

amy.send(osc=0, vel=1, wave=amy.PCM, patch=0, sequence=",24,1") # play a PCM drum every eighth note.
amy.send(osc=1, vel=1, wave=amy.PCM, patch=1, sequence=",48,2") # play a PCM drum every quarter note.
amy.send(sequence=",,1") # remove the eighth note sequence
amy.send(osc=1, vel=1, wave=amy.PCM, patch=1, note=70, sequence=",48,2") # change the quarter note event

amy.send(osc=2, vel=1, wave=amy.PCM, patch=2, sequence="1000,,3") # play a PCM drum at absolute tick 1000
amy.send(reset=amy.RESET_SEQUENCER) # clears the sequence

amy.send(osc=0, vel=1, wave=amy.PCM, patch=0, sequence="0,384,1") # first slot of a 16 1/8th note drum machine
amy.send(osc=1, vel=1, wave=amy.PCM, patch=1, sequence="216,384,2") # ninth slot of a 16 1/8th note drum machine


```

## Examples
Expand Down
2 changes: 1 addition & 1 deletion amy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, BYO_PARTIALS, AUDIO_IN0, AUDIO_IN1, CUSTOM, OFF = range(16)
FILTER_NONE, FILTER_LPF, FILTER_BPF, FILTER_HPF, FILTER_LPF24 = range(5)
ENVELOPE_NORMAL, ENVELOPE_LINEAR, ENVELOPE_DX7, ENVELOPE_TRUE_EXPONENTIAL = range(4)
RESET_ALL_OSCS, RESET_TIMEBASE, RESET_AMY = (8192, 16384, 32768)
RESET_SEQUENCER, RESET_ALL_OSCS, RESET_TIMEBASE, RESET_AMY = (4096, 8192, 16384, 32768)
AMY_LATENCY_MS = 0

# If set, inserts func as time for every call to send(). Will not override an explicitly set time
Expand Down
11 changes: 6 additions & 5 deletions src/amy.c
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,9 @@ void play_event(struct delta d) {
if(*(int16_t *)&d.data & RESET_ALL_OSCS) {
amy_reset_oscs();
}
if(*(int16_t *)&d.data & RESET_SEQUENCER) {
sequencer_reset();
}
if(*(int16_t *)&d.data < AMY_OSCS+1) {
reset_osc(*(int16_t *)&d.data);
}
Expand Down Expand Up @@ -1709,9 +1712,7 @@ struct event amy_parse_message(char * message) {
int16_t length = strlen(message);

// default values for sequence message
uint16_t divider = 0;
uint32_t tag = 0;
uint32_t tick = 0;
uint32_t seq_message[3] = {0,0,0};
uint8_t sequence_message = 0;

// Check if we're in a transfer block, if so, parse it and leave this loop
Expand Down Expand Up @@ -1757,7 +1758,7 @@ struct event amy_parse_message(char * message) {
case 'F': parse_coef_message(message + start, e.filter_freq_coefs); break;
case 'G': e.filter_type = atoi(message + start); break;
/* g used for Alles for client # */
case 'H': parse_tick_and_tag(message+start, &tick, &divider, &tag); sequence_message = 1; break;
case 'H': parse_list_uint32_t(message+start, seq_message, 3, 0); sequence_message = 1; break;
case 'h': if (AMY_HAS_REVERB) {
float reverb_params[4] = {AMY_UNSET_VALUE(reverb.liveness), AMY_UNSET_VALUE(reverb.liveness),
AMY_UNSET_VALUE(reverb.liveness), AMY_UNSET_VALUE(reverb.liveness)};
Expand Down Expand Up @@ -1857,7 +1858,7 @@ struct event amy_parse_message(char * message) {

if(length>0) { // only do this if we got some data
if(sequence_message) {
uint8_t added = sequencer_add_event(e, tick, divider, tag);
uint8_t added = sequencer_add_event(e, seq_message[0], seq_message[1], seq_message[2]);
(void)added; // we don't need to do anything with this info at this time
} else {
// if time is set, play then
Expand Down
1 change: 1 addition & 0 deletions src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ enum coefs{
#define ENVELOPE_TRUE_EXPONENTIAL 3

// Reset masks
#define RESET_SEQUENCER 4096
#define RESET_ALL_OSCS 8192
#define RESET_TIMEBASE 16384
#define RESET_AMY 32768
Expand Down
89 changes: 43 additions & 46 deletions src/sequencer.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ typedef struct {
struct event e; // parsed event -- we clear out sequence and time here obv
uint32_t tag;
uint32_t tick; // 0 means not used
uint16_t divider; // 0 means not used
uint32_t length; // 0 means not used
} sequence_entry;

// linked list of sequence_entries
Expand All @@ -30,37 +30,37 @@ esp_timer_handle_t periodic_timer;
#include <pthread.h>
#endif

void sequencer_reset() {
// Remove all events
sequence_entry_ll_t **entry_ll_ptr = &sequence_entry_ll_start; // Start pointing to the root node.
while ((*entry_ll_ptr) != NULL) {
sequence_entry_ll_t *doomed = *entry_ll_ptr;
*entry_ll_ptr = doomed->next; // close up list.
free(doomed->sequence);
free(doomed);
}
sequence_entry_ll_start = NULL;
}

void sequencer_recompute() {
us_per_tick = (uint32_t) (1000000.0 / ((amy_global.tempo/60.0) * (float)AMY_SEQUENCER_PPQ));
next_amy_tick_us = amy_sysclock()*1000 + us_per_tick;
}

extern int parse_list_uint32_t(char *message, uint32_t *vals, int max_num_vals, uint32_t skipped_val);

// Parses the tick, divider and tag out of a sequence message
void parse_tick_and_tag(char * message, uint32_t *tick, uint16_t *divider, uint32_t *tag) {
uint32_t vals[3];
parse_list_uint32_t(message, vals, 3, 0);
*tick = (uint32_t)vals[0]; *divider = (uint16_t)vals[1]; *tag = (uint32_t)vals[2];
}

uint8_t sequencer_add_event(struct event e, uint32_t tick, uint16_t divider, uint32_t tag) {
uint8_t sequencer_add_event(struct event e, uint32_t tick, uint32_t length, uint32_t tag) {
// add this event to the list of sequencer events in the LL
// if the tag already exists - if there's tick/divider, overwrite, if there's no tick / divider, we should remove the entry
if(divider != 0 && tick != 0) { divider = 0; } // if tick is set ignore divider

// Dan's version
// if the tag already exists - if there's tick/length, overwrite, if there's no tick / length, we should remove the entry
sequence_entry_ll_t **entry_ll_ptr = &sequence_entry_ll_start; // Start pointing to the root node.
while ((*entry_ll_ptr) != NULL) {
if ((*entry_ll_ptr)->sequence->tag == tag) {
if (divider == 0 && tick == 0) { // delete
if (length == 0 && tick == 0) { // delete
sequence_entry_ll_t *doomed = *entry_ll_ptr;
*entry_ll_ptr = doomed->next; // close up list.
free(doomed->sequence);
free(doomed);
return 0;
} else { // replace
(*entry_ll_ptr)->sequence->divider = divider;
(*entry_ll_ptr)->sequence->length = length;
(*entry_ll_ptr)->sequence->tick = tick;
(*entry_ll_ptr)->sequence->e = e;
return 0;
Expand All @@ -70,13 +70,13 @@ uint8_t sequencer_add_event(struct event e, uint32_t tick, uint16_t divider, uin
}

// If we got here, we didn't find the tag in the list, so add it at the end.
if(tick == 0 && divider == 0) return 0; // Ignore non-schedulable event.
if(tick != 0 && tick <= sequencer_tick_count) return 0; // don't schedule things in the past.
if(tick == 0 && length == 0) return 0; // Ignore non-schedulable event.
if(tick != 0 && length == 0 && tick <= sequencer_tick_count) return 0; // don't schedule things in the past.
(*entry_ll_ptr) = malloc(sizeof(sequence_entry_ll_t));
(*entry_ll_ptr)->sequence = malloc(sizeof(sequence_entry));
(*entry_ll_ptr)->sequence->e = e;
(*entry_ll_ptr)->sequence->tick = tick;
(*entry_ll_ptr)->sequence->divider = divider;
(*entry_ll_ptr)->sequence->length = length;
(*entry_ll_ptr)->sequence->tag = tag;
(*entry_ll_ptr)->next = NULL;
return 1;
Expand All @@ -88,38 +88,35 @@ static void sequencer_check_and_fill() {
while(amy_sysclock() >= (next_amy_tick_us/1000)) {
sequencer_tick_count++;
// Scan through LL looking for matches
sequence_entry_ll_t *entry_ll = sequence_entry_ll_start;
sequence_entry_ll_t *prev_entry_ll = NULL;
while(entry_ll != NULL) {
uint8_t already_deleted = 0;
if(entry_ll->sequence->tick == sequencer_tick_count || (entry_ll->sequence->divider > 0 && (sequencer_tick_count % entry_ll->sequence->divider == 0))) {
// hit
struct event to_add = entry_ll->sequence->e;
sequence_entry_ll_t **entry_ll_ptr = &sequence_entry_ll_start; // Start pointing to the root node.
while ((*entry_ll_ptr) != NULL) {
uint8_t deleted = 0;
uint8_t hit = 0;
uint8_t delete = 0;
if((*entry_ll_ptr)->sequence->length != 0) { // length set
uint32_t offset = sequencer_tick_count % (*entry_ll_ptr)->sequence->length;
if(offset == (*entry_ll_ptr)->sequence->tick) hit = 1;
} else {
// Test for absolute tick (no length set)
if ((*entry_ll_ptr)->sequence->tick == sequencer_tick_count) { hit = 1; delete = 1; }
}
if(hit) {
struct event to_add = (*entry_ll_ptr)->sequence->e;
to_add.status = EVENT_SCHEDULED;
to_add.time = 0; // play now
amy_add_event(to_add);

// Delete tick addressed sequence entry if sent
if(entry_ll->sequence->tick > 0) {
if(prev_entry_ll == NULL) { // start
sequence_entry_ll_start = entry_ll->next;
} else {
prev_entry_ll->next = entry_ll->next;
}
free(entry_ll->sequence);
free(entry_ll);

if(prev_entry_ll != NULL) {
entry_ll = prev_entry_ll->next;
} else {
entry_ll = NULL;
}
already_deleted = 1;
// Delete absolute tick addressed sequence entry if sent
if(delete) {
sequence_entry_ll_t *doomed = *entry_ll_ptr;
*entry_ll_ptr = doomed->next; // close up list.
free(doomed->sequence);
free(doomed);
deleted = 1;
}
}
if(!already_deleted) {
prev_entry_ll = entry_ll;
entry_ll = entry_ll->next;
if(!deleted) {
entry_ll_ptr = &((*entry_ll_ptr)->next); // Update to point to the next field in the preceding list node.
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/sequencer.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ extern uint32_t us_per_tick ;

void sequencer_init();
void sequencer_recompute();
void parse_tick_and_tag(char * message, uint32_t *tick, uint16_t *divider, uint32_t *tag);
uint8_t sequencer_add_event(struct event e, uint32_t tick, uint16_t divider, uint32_t tag);
uint8_t sequencer_add_event(struct event e, uint32_t tick, uint32_t length, uint32_t tag);
void sequencer_reset();
extern void (*amy_external_sequencer_hook)(uint32_t);

#define AMY_SEQUENCER_PPQ 48
Expand Down

0 comments on commit e60d41b

Please sign in to comment.