Add multi-track playback with mute/solo support
Replaces the single-root-pattern sequencer with a Track[] array that allows multiple patterns to loop independently. Adds ADD_TRACK (0x0A), REMOVE_TRACK (0x0B), PLAY_TRACKS (0x0C), and SET_TRACK_MUTE (0x0D) protocol records. The C backend gains per-track pending_subs and a tracks_mutex. The Node server gains track-state APIs (/api/tracks/:id/ active, mute, solo) and the frontend shows per-pattern track/mute/solo buttons in the sidebar list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,29 @@ int proto_encode_preview_note(uint8_t *buf, const Msg_Preview_Note *r) {
|
||||
return write_frame(buf, RT_PREVIEW_NOTE, 5);
|
||||
}
|
||||
|
||||
int proto_encode_add_track(uint8_t *buf, const Msg_Add_Track *r) {
|
||||
int off = FRAME_HEADER_SIZE;
|
||||
put_u16(buf, &off, r->pattern_id);
|
||||
return write_frame(buf, RT_ADD_TRACK, 2);
|
||||
}
|
||||
|
||||
int proto_encode_remove_track(uint8_t *buf, const Msg_Remove_Track *r) {
|
||||
int off = FRAME_HEADER_SIZE;
|
||||
put_u16(buf, &off, r->pattern_id);
|
||||
return write_frame(buf, RT_REMOVE_TRACK, 2);
|
||||
}
|
||||
|
||||
int proto_encode_play_tracks(uint8_t *buf) {
|
||||
return write_frame(buf, RT_PLAY_TRACKS, 0);
|
||||
}
|
||||
|
||||
int proto_encode_set_track_mute(uint8_t *buf, const Msg_Set_Track_Mute *r) {
|
||||
int off = FRAME_HEADER_SIZE;
|
||||
put_u16(buf, &off, r->pattern_id);
|
||||
put_u8(buf, &off, r->muted);
|
||||
return write_frame(buf, RT_SET_TRACK_MUTE, 3);
|
||||
}
|
||||
|
||||
int proto_encode_ack(uint8_t *buf, const Msg_Ack *r) {
|
||||
int off = FRAME_HEADER_SIZE;
|
||||
put_u8(buf, &off, r->acked_type);
|
||||
@@ -199,6 +222,36 @@ int proto_decode_preview_note(const uint8_t *p, uint16_t len, Msg_Preview_Note *
|
||||
return 0;
|
||||
}
|
||||
|
||||
int proto_decode_add_track(const uint8_t *p, uint16_t len, Msg_Add_Track *r) {
|
||||
if (len < 2) return -1;
|
||||
int off = 0;
|
||||
r->pattern_id = get_u16(p, &off);
|
||||
(void)off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int proto_decode_remove_track(const uint8_t *p, uint16_t len, Msg_Remove_Track *r) {
|
||||
if (len < 2) return -1;
|
||||
int off = 0;
|
||||
r->pattern_id = get_u16(p, &off);
|
||||
(void)off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int proto_decode_play_tracks(const uint8_t *p, uint16_t len, Msg_Play_Tracks *r) {
|
||||
(void)p; (void)len; (void)r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int proto_decode_set_track_mute(const uint8_t *p, uint16_t len, Msg_Set_Track_Mute *r) {
|
||||
if (len < 3) return -1;
|
||||
int off = 0;
|
||||
r->pattern_id = get_u16(p, &off);
|
||||
r->muted = get_u8(p, &off);
|
||||
(void)off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int proto_decode_ack(const uint8_t *p, uint16_t len, Msg_Ack *r) {
|
||||
if (len < 1) return -1;
|
||||
int off = 0;
|
||||
|
||||
@@ -19,6 +19,10 @@ typedef enum {
|
||||
RT_STOP = 0x07,
|
||||
RT_SET_TEMPO = 0x08,
|
||||
RT_PREVIEW_NOTE = 0x09,
|
||||
RT_ADD_TRACK = 0x0A,
|
||||
RT_REMOVE_TRACK = 0x0B,
|
||||
RT_PLAY_TRACKS = 0x0C,
|
||||
RT_SET_TRACK_MUTE = 0x0D,
|
||||
RT_ACK = 0x81,
|
||||
RT_ERROR = 0x82,
|
||||
RT_BEAT_TICK = 0x83,
|
||||
@@ -35,6 +39,10 @@ typedef enum {
|
||||
#define PAYLOAD_SIZE_STOP 0
|
||||
#define PAYLOAD_SIZE_SET_TEMPO 2
|
||||
#define PAYLOAD_SIZE_PREVIEW_NOTE 5
|
||||
#define PAYLOAD_SIZE_ADD_TRACK 2
|
||||
#define PAYLOAD_SIZE_REMOVE_TRACK 2
|
||||
#define PAYLOAD_SIZE_PLAY_TRACKS 0
|
||||
#define PAYLOAD_SIZE_SET_TRACK_MUTE 3
|
||||
#define PAYLOAD_SIZE_ACK 1
|
||||
#define PAYLOAD_SIZE_ERROR 2
|
||||
#define PAYLOAD_SIZE_BEAT_TICK 4
|
||||
@@ -79,6 +87,19 @@ typedef struct {
|
||||
uint8_t velocity;
|
||||
uint16_t duration_ms; /* Note-off will be sent after this many milliseconds */
|
||||
} Msg_Preview_Note;
|
||||
typedef struct {
|
||||
uint16_t pattern_id;
|
||||
} Msg_Add_Track;
|
||||
typedef struct {
|
||||
uint16_t pattern_id;
|
||||
} Msg_Remove_Track;
|
||||
typedef struct {
|
||||
char _empty;
|
||||
} Msg_Play_Tracks;
|
||||
typedef struct {
|
||||
uint16_t pattern_id;
|
||||
uint8_t muted;
|
||||
} Msg_Set_Track_Mute;
|
||||
typedef struct {
|
||||
uint8_t acked_type; /* Record type being acknowledged */
|
||||
} Msg_Ack;
|
||||
@@ -105,6 +126,10 @@ int proto_encode_play(uint8_t *buf, const Msg_Play *r);
|
||||
int proto_encode_stop(uint8_t *buf);
|
||||
int proto_encode_set_tempo(uint8_t *buf, const Msg_Set_Tempo *r);
|
||||
int proto_encode_preview_note(uint8_t *buf, const Msg_Preview_Note *r);
|
||||
int proto_encode_add_track(uint8_t *buf, const Msg_Add_Track *r);
|
||||
int proto_encode_remove_track(uint8_t *buf, const Msg_Remove_Track *r);
|
||||
int proto_encode_play_tracks(uint8_t *buf);
|
||||
int proto_encode_set_track_mute(uint8_t *buf, const Msg_Set_Track_Mute *r);
|
||||
int proto_encode_ack(uint8_t *buf, const Msg_Ack *r);
|
||||
int proto_encode_error(uint8_t *buf, const Msg_Error *r);
|
||||
int proto_encode_beat_tick(uint8_t *buf, const Msg_Beat_Tick *r);
|
||||
@@ -120,6 +145,10 @@ int proto_decode_play(const uint8_t *payload, uint16_t len, Msg_Play *r);
|
||||
int proto_decode_stop(const uint8_t *payload, uint16_t len, Msg_Stop *r);
|
||||
int proto_decode_set_tempo(const uint8_t *payload, uint16_t len, Msg_Set_Tempo *r);
|
||||
int proto_decode_preview_note(const uint8_t *payload, uint16_t len, Msg_Preview_Note *r);
|
||||
int proto_decode_add_track(const uint8_t *payload, uint16_t len, Msg_Add_Track *r);
|
||||
int proto_decode_remove_track(const uint8_t *payload, uint16_t len, Msg_Remove_Track *r);
|
||||
int proto_decode_play_tracks(const uint8_t *payload, uint16_t len, Msg_Play_Tracks *r);
|
||||
int proto_decode_set_track_mute(const uint8_t *payload, uint16_t len, Msg_Set_Track_Mute *r);
|
||||
int proto_decode_ack(const uint8_t *payload, uint16_t len, Msg_Ack *r);
|
||||
int proto_decode_error(const uint8_t *payload, uint16_t len, Msg_Error *r);
|
||||
int proto_decode_beat_tick(const uint8_t *payload, uint16_t len, Msg_Beat_Tick *r);
|
||||
|
||||
@@ -92,7 +92,9 @@ static void on_frame(uint8_t rt, const uint8_t *payload, uint16_t payload_len, v
|
||||
case RT_PLAY: {
|
||||
Msg_Play msg;
|
||||
if (proto_decode_play(payload, payload_len, &msg) < 0) goto bad_payload;
|
||||
sequencer_play(&app->seq, msg.pattern_id);
|
||||
sequencer_stop(&app->seq);
|
||||
sequencer_add_track(&app->seq, msg.pattern_id);
|
||||
sequencer_play(&app->seq);
|
||||
break;
|
||||
}
|
||||
case RT_STOP: {
|
||||
@@ -111,6 +113,28 @@ static void on_frame(uint8_t rt, const uint8_t *payload, uint16_t payload_len, v
|
||||
sequencer_preview_note(&app->seq, msg.channel, msg.note, msg.velocity, msg.duration_ms);
|
||||
break;
|
||||
}
|
||||
case RT_ADD_TRACK: {
|
||||
Msg_Add_Track msg;
|
||||
if (proto_decode_add_track(payload, payload_len, &msg) < 0) goto bad_payload;
|
||||
sequencer_add_track(&app->seq, msg.pattern_id);
|
||||
break;
|
||||
}
|
||||
case RT_REMOVE_TRACK: {
|
||||
Msg_Remove_Track msg;
|
||||
if (proto_decode_remove_track(payload, payload_len, &msg) < 0) goto bad_payload;
|
||||
sequencer_remove_track(&app->seq, msg.pattern_id);
|
||||
break;
|
||||
}
|
||||
case RT_PLAY_TRACKS: {
|
||||
sequencer_play(&app->seq);
|
||||
break;
|
||||
}
|
||||
case RT_SET_TRACK_MUTE: {
|
||||
Msg_Set_Track_Mute msg;
|
||||
if (proto_decode_set_track_mute(payload, payload_len, &msg) < 0) goto bad_payload;
|
||||
sequencer_set_track_mute(&app->seq, msg.pattern_id, msg.muted);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
fprintf(stderr, "main: unknown record type 0x%02x\n", rt);
|
||||
return;
|
||||
|
||||
@@ -55,7 +55,6 @@ static void enqueue_note_off(Sequencer *seq, uint8_t ch, uint8_t note, uint64_t
|
||||
pno->fire_at.tv_nsec = (long)(fire_ns % 1000000000ULL);
|
||||
}
|
||||
|
||||
/* Fire any due note-offs; compact the queue. */
|
||||
static void drain_note_offs(Sequencer *seq) {
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
@@ -77,11 +76,6 @@ static void drain_note_offs(Sequencer *seq) {
|
||||
|
||||
/* ── Tempo helpers ────────────────────────────────────────────────── */
|
||||
|
||||
/* Returns step duration in nanoseconds.
|
||||
* One step = one 16th note.
|
||||
* step_ns = 60,000,000,000 / (bpm_x10/10) / 4
|
||||
* = 150,000,000,000 / bpm_x10
|
||||
* At 120.0 BPM (bpm_x10=1200): 125,000,000 ns = 125 ms */
|
||||
static uint64_t get_step_ns(Sequencer *seq) {
|
||||
pthread_mutex_lock(&seq->tempo_mutex);
|
||||
uint16_t bpm_x10 = seq->bpm_x10;
|
||||
@@ -91,11 +85,8 @@ static uint64_t get_step_ns(Sequencer *seq) {
|
||||
|
||||
/* ── Step processing ──────────────────────────────────────────────── */
|
||||
|
||||
/* Process notes and sub-refs at a given step within a pattern.
|
||||
* is_sub: non-zero means this is a sub-pattern call (don't recurse sub-refs). */
|
||||
static void process_step(Sequencer *seq, Pattern *pat, uint8_t step,
|
||||
uint64_t step_ns, int is_sub) {
|
||||
/* Fire notes */
|
||||
/* Process notes at a given step within a pattern; sub-ref activation uses track's pending_subs. */
|
||||
static void process_step(Sequencer *seq, Track *t, Pattern *pat, uint8_t step, uint64_t step_ns) {
|
||||
for (int i = 0; i < pat->note_count; i++) {
|
||||
Note_Event *ev = &pat->notes[i];
|
||||
if (ev->step != step) continue;
|
||||
@@ -104,22 +95,16 @@ static void process_step(Sequencer *seq, Pattern *pat, uint8_t step,
|
||||
step_ns * (uint64_t)ev->duration_steps);
|
||||
}
|
||||
|
||||
/* Start sub-patterns (root level only; subs don't nest further in this pass) */
|
||||
if (!is_sub) {
|
||||
for (int i = 0; i < pat->sub_ref_count; i++) {
|
||||
Sub_Ref *ref = &pat->sub_refs[i];
|
||||
if (ref->step != step) continue;
|
||||
/* Find an empty slot */
|
||||
for (int j = 0; j < MAX_PENDING_SUBS; j++) {
|
||||
if (!seq->pending_subs[j].active) {
|
||||
seq->pending_subs[j].pattern_id = ref->sub_pattern_id;
|
||||
seq->pending_subs[j].local_step = 0;
|
||||
seq->pending_subs[j].active = 1;
|
||||
if (j >= seq->pending_sub_count) {
|
||||
seq->pending_sub_count = j + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < pat->sub_ref_count; i++) {
|
||||
Sub_Ref *ref = &pat->sub_refs[i];
|
||||
if (ref->step != step) continue;
|
||||
for (int j = 0; j < MAX_SUBS_PER_TRACK; j++) {
|
||||
if (!t->pending_subs[j].active) {
|
||||
t->pending_subs[j].pattern_id = ref->sub_pattern_id;
|
||||
t->pending_subs[j].local_step = 0;
|
||||
t->pending_subs[j].active = 1;
|
||||
if (j >= t->pending_sub_count) t->pending_sub_count = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,93 +121,98 @@ static void *tick_fn(void *arg) {
|
||||
while (atomic_load(&seq->running)) {
|
||||
uint64_t sn = get_step_ns(seq);
|
||||
|
||||
/* Fire due note-offs */
|
||||
drain_note_offs(seq);
|
||||
|
||||
/* Lock pattern store for the whole tick */
|
||||
pattern_store_lock(seq->ps);
|
||||
pthread_mutex_lock(&seq->tracks_mutex);
|
||||
|
||||
Pattern *root = pattern_store_get(seq->ps, seq->root_pattern_id);
|
||||
if (!root) {
|
||||
pattern_store_unlock(seq->ps);
|
||||
fprintf(stderr, "sequencer: root pattern %u not found, stopping\n",
|
||||
seq->root_pattern_id);
|
||||
atomic_store(&seq->running, 0);
|
||||
break;
|
||||
}
|
||||
for (int ti = 0; ti < MAX_TRACKS; ti++) {
|
||||
Track *t = &seq->tracks[ti];
|
||||
if (!t->active) continue;
|
||||
|
||||
uint8_t step = seq->current_step;
|
||||
|
||||
/* Process root */
|
||||
process_step(seq, root, step, sn, 0);
|
||||
|
||||
/* Process active sub-patterns */
|
||||
for (int i = 0; i < seq->pending_sub_count; i++) {
|
||||
Pending_Sub *ps = &seq->pending_subs[i];
|
||||
if (!ps->active) continue;
|
||||
|
||||
Pattern *subpat = pattern_store_get(seq->ps, ps->pattern_id);
|
||||
if (!subpat) {
|
||||
ps->active = 0;
|
||||
pattern_store_lock(seq->ps);
|
||||
Pattern *pat = pattern_store_get(seq->ps, t->pattern_id);
|
||||
if (!pat) {
|
||||
pattern_store_unlock(seq->ps);
|
||||
t->active = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
process_step(seq, subpat, ps->local_step, sn, 1);
|
||||
ps->local_step++;
|
||||
uint8_t step = t->current_step;
|
||||
|
||||
if (ps->local_step >= subpat->steps) {
|
||||
ps->active = 0;
|
||||
/* Notify sub-pattern end */
|
||||
if (!t->muted) {
|
||||
process_step(seq, t, pat, step, sn);
|
||||
|
||||
/* Process sub-patterns for this track */
|
||||
for (int si = 0; si < t->pending_sub_count; si++) {
|
||||
Pending_Sub *ps = &t->pending_subs[si];
|
||||
if (!ps->active) continue;
|
||||
|
||||
Pattern *subpat = pattern_store_get(seq->ps, ps->pattern_id);
|
||||
if (!subpat) { ps->active = 0; continue; }
|
||||
|
||||
/* Fire sub-step notes (no further recursion) */
|
||||
for (int ni = 0; ni < subpat->note_count; ni++) {
|
||||
Note_Event *ev = &subpat->notes[ni];
|
||||
if (ev->step != ps->local_step) continue;
|
||||
alsa_note_on(seq, subpat->channel, ev->note, ev->velocity);
|
||||
enqueue_note_off(seq, subpat->channel, ev->note,
|
||||
sn * (uint64_t)ev->duration_steps);
|
||||
}
|
||||
|
||||
ps->local_step++;
|
||||
if (ps->local_step >= subpat->steps) {
|
||||
ps->active = 0;
|
||||
uint8_t buf[16];
|
||||
Msg_Pattern_End end_msg = { .pattern_id = ps->pattern_id };
|
||||
int elen = proto_encode_pattern_end(buf, &end_msg);
|
||||
pattern_store_unlock(seq->ps);
|
||||
seq->send_fn(seq->send_ctx, buf, elen);
|
||||
pattern_store_lock(seq->ps);
|
||||
pat = pattern_store_get(seq->ps, t->pattern_id);
|
||||
if (!pat) { t->active = 0; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!t->active) {
|
||||
/* pat is NULL and ps is locked after failed re-fetch */
|
||||
pattern_store_unlock(seq->ps);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Send BEAT_TICK for this track */
|
||||
{
|
||||
uint8_t buf[16];
|
||||
Msg_Pattern_End msg = { .pattern_id = ps->pattern_id };
|
||||
int len = proto_encode_pattern_end(buf, &msg);
|
||||
Msg_Beat_Tick tick = {
|
||||
.pattern_id = t->pattern_id,
|
||||
.step = step,
|
||||
.beat = t->current_beat,
|
||||
};
|
||||
int len = proto_encode_beat_tick(buf, &tick);
|
||||
pattern_store_unlock(seq->ps);
|
||||
seq->send_fn(seq->send_ctx, buf, len);
|
||||
pattern_store_lock(seq->ps);
|
||||
/* Re-fetch root after unlock */
|
||||
root = pattern_store_get(seq->ps, seq->root_pattern_id);
|
||||
if (!root) {
|
||||
pattern_store_unlock(seq->ps);
|
||||
atomic_store(&seq->running, 0);
|
||||
return NULL;
|
||||
}
|
||||
pat = pattern_store_get(seq->ps, t->pattern_id);
|
||||
if (!pat) { t->active = 0; continue; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Send BEAT_TICK */
|
||||
{
|
||||
uint8_t buf[16];
|
||||
Msg_Beat_Tick tick = {
|
||||
.pattern_id = seq->root_pattern_id,
|
||||
.step = step,
|
||||
.beat = seq->current_beat,
|
||||
};
|
||||
int len = proto_encode_beat_tick(buf, &tick);
|
||||
pattern_store_unlock(seq->ps);
|
||||
seq->send_fn(seq->send_ctx, buf, len);
|
||||
pattern_store_lock(seq->ps);
|
||||
root = pattern_store_get(seq->ps, seq->root_pattern_id);
|
||||
if (!root) {
|
||||
/* Advance step, wrap independently per track */
|
||||
t->current_step++;
|
||||
if (t->current_step >= pat->steps) {
|
||||
t->current_step = 0;
|
||||
t->current_beat++;
|
||||
|
||||
uint8_t buf[16];
|
||||
Msg_Pattern_End msg = { .pattern_id = t->pattern_id };
|
||||
int len = proto_encode_pattern_end(buf, &msg);
|
||||
pattern_store_unlock(seq->ps);
|
||||
seq->send_fn(seq->send_ctx, buf, len);
|
||||
} else {
|
||||
pattern_store_unlock(seq->ps);
|
||||
atomic_store(&seq->running, 0);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advance step */
|
||||
seq->current_step++;
|
||||
if (seq->current_step >= root->steps) {
|
||||
seq->current_step = 0;
|
||||
seq->current_beat = (uint8_t)(seq->current_beat + 1);
|
||||
|
||||
uint8_t buf[16];
|
||||
Msg_Pattern_End msg = { .pattern_id = seq->root_pattern_id };
|
||||
int len = proto_encode_pattern_end(buf, &msg);
|
||||
pattern_store_unlock(seq->ps);
|
||||
seq->send_fn(seq->send_ctx, buf, len);
|
||||
} else {
|
||||
pattern_store_unlock(seq->ps);
|
||||
}
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
|
||||
/* Advance absolute deadline — drift-free */
|
||||
uint64_t next_abs = (uint64_t)next.tv_sec * 1000000000ULL
|
||||
@@ -243,9 +233,10 @@ int sequencer_init(Sequencer *seq, Pattern_Store *ps, const char *alsa_name,
|
||||
seq->ps = ps;
|
||||
seq->send_fn = send_fn;
|
||||
seq->send_ctx = send_ctx;
|
||||
seq->bpm_x10 = 1200; /* 120.0 BPM */
|
||||
seq->bpm_x10 = 1200;
|
||||
atomic_store(&seq->running, 0);
|
||||
pthread_mutex_init(&seq->tempo_mutex, NULL);
|
||||
pthread_mutex_init(&seq->tracks_mutex, NULL);
|
||||
|
||||
if (snd_seq_open(&seq->seq, "default", SND_SEQ_OPEN_OUTPUT, 0) < 0) {
|
||||
fprintf(stderr, "sequencer: failed to open ALSA sequencer\n");
|
||||
@@ -271,28 +262,76 @@ void sequencer_destroy(Sequencer *seq) {
|
||||
sequencer_stop(seq);
|
||||
snd_seq_close(seq->seq);
|
||||
pthread_mutex_destroy(&seq->tempo_mutex);
|
||||
pthread_mutex_destroy(&seq->tracks_mutex);
|
||||
}
|
||||
|
||||
void sequencer_play(Sequencer *seq, uint16_t pattern_id) {
|
||||
sequencer_stop(seq);
|
||||
void sequencer_add_track(Sequencer *seq, uint16_t pattern_id) {
|
||||
pthread_mutex_lock(&seq->tracks_mutex);
|
||||
/* Check if already active */
|
||||
for (int i = 0; i < MAX_TRACKS; i++) {
|
||||
if (seq->tracks[i].active && seq->tracks[i].pattern_id == pattern_id) {
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Find first empty slot */
|
||||
for (int i = 0; i < MAX_TRACKS; i++) {
|
||||
if (!seq->tracks[i].active) {
|
||||
memset(&seq->tracks[i], 0, sizeof(Track));
|
||||
seq->tracks[i].pattern_id = pattern_id;
|
||||
seq->tracks[i].active = 1;
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
fprintf(stderr, "sequencer: MAX_TRACKS reached, cannot add pattern %u\n", pattern_id);
|
||||
}
|
||||
|
||||
seq->root_pattern_id = pattern_id;
|
||||
seq->current_step = 0;
|
||||
seq->current_beat = 0;
|
||||
seq->note_off_count = 0;
|
||||
seq->pending_sub_count = 0;
|
||||
memset(seq->pending_subs, 0, sizeof(seq->pending_subs));
|
||||
void sequencer_remove_track(Sequencer *seq, uint16_t pattern_id) {
|
||||
pthread_mutex_lock(&seq->tracks_mutex);
|
||||
for (int i = 0; i < MAX_TRACKS; i++) {
|
||||
if (seq->tracks[i].active && seq->tracks[i].pattern_id == pattern_id) {
|
||||
seq->tracks[i].active = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
}
|
||||
|
||||
void sequencer_set_track_mute(Sequencer *seq, uint16_t pattern_id, uint8_t muted) {
|
||||
pthread_mutex_lock(&seq->tracks_mutex);
|
||||
for (int i = 0; i < MAX_TRACKS; i++) {
|
||||
if (seq->tracks[i].active && seq->tracks[i].pattern_id == pattern_id) {
|
||||
seq->tracks[i].muted = muted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
}
|
||||
|
||||
void sequencer_clear_tracks(Sequencer *seq) {
|
||||
pthread_mutex_lock(&seq->tracks_mutex);
|
||||
memset(seq->tracks, 0, sizeof(seq->tracks));
|
||||
pthread_mutex_unlock(&seq->tracks_mutex);
|
||||
}
|
||||
|
||||
void sequencer_play(Sequencer *seq) {
|
||||
if (atomic_load(&seq->running)) return;
|
||||
atomic_store(&seq->running, 1);
|
||||
pthread_create(&seq->tick_thread, NULL, tick_fn, seq);
|
||||
}
|
||||
|
||||
void sequencer_stop(Sequencer *seq) {
|
||||
if (!atomic_load(&seq->running)) return;
|
||||
if (!atomic_load(&seq->running)) {
|
||||
sequencer_clear_tracks(seq);
|
||||
return;
|
||||
}
|
||||
atomic_store(&seq->running, 0);
|
||||
pthread_join(seq->tick_thread, NULL);
|
||||
alsa_all_notes_off(seq);
|
||||
drain_note_offs(seq);
|
||||
sequencer_clear_tracks(seq);
|
||||
}
|
||||
|
||||
void sequencer_set_tempo(Sequencer *seq, uint16_t bpm_x10) {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
#include "pattern_store.h"
|
||||
|
||||
#define NOTE_OFF_QUEUE_SIZE 256
|
||||
#define MAX_PENDING_SUBS 64
|
||||
#define MAX_SUBS_PER_TRACK 16
|
||||
#define MAX_TRACKS 16
|
||||
|
||||
typedef struct {
|
||||
struct timespec fire_at;
|
||||
@@ -21,6 +22,16 @@ typedef struct {
|
||||
int active;
|
||||
} Pending_Sub;
|
||||
|
||||
typedef struct {
|
||||
uint16_t pattern_id;
|
||||
uint8_t current_step;
|
||||
uint8_t current_beat;
|
||||
uint8_t muted;
|
||||
int active;
|
||||
Pending_Sub pending_subs[MAX_SUBS_PER_TRACK];
|
||||
int pending_sub_count;
|
||||
} Track;
|
||||
|
||||
/* Function pointer used to write frames back to the Node client */
|
||||
typedef void (*Send_Fn)(void *ctx, const uint8_t *buf, int len);
|
||||
|
||||
@@ -37,19 +48,14 @@ typedef struct {
|
||||
Send_Fn send_fn;
|
||||
void *send_ctx;
|
||||
|
||||
/* Playback state — owned exclusively by tick thread when running */
|
||||
uint16_t root_pattern_id;
|
||||
uint8_t current_step;
|
||||
uint8_t current_beat;
|
||||
/* Tracks — protected by tracks_mutex */
|
||||
Track tracks[MAX_TRACKS];
|
||||
pthread_mutex_t tracks_mutex;
|
||||
|
||||
/* Note-off queue — tick thread only, no mutex needed */
|
||||
Pending_Note_Off note_offs[NOTE_OFF_QUEUE_SIZE];
|
||||
int note_off_count;
|
||||
|
||||
/* Pending sub-patterns — tick thread only */
|
||||
Pending_Sub pending_subs[MAX_PENDING_SUBS];
|
||||
int pending_sub_count;
|
||||
|
||||
/* Tempo — protected by tempo_mutex */
|
||||
uint16_t bpm_x10;
|
||||
pthread_mutex_t tempo_mutex;
|
||||
@@ -62,7 +68,11 @@ typedef struct {
|
||||
int sequencer_init(Sequencer *seq, Pattern_Store *ps, const char *alsa_name,
|
||||
Send_Fn send_fn, void *send_ctx);
|
||||
void sequencer_destroy(Sequencer *seq);
|
||||
void sequencer_play(Sequencer *seq, uint16_t pattern_id);
|
||||
void sequencer_add_track(Sequencer *seq, uint16_t pattern_id);
|
||||
void sequencer_remove_track(Sequencer *seq, uint16_t pattern_id);
|
||||
void sequencer_set_track_mute(Sequencer *seq, uint16_t pattern_id, uint8_t muted);
|
||||
void sequencer_clear_tracks(Sequencer *seq);
|
||||
void sequencer_play(Sequencer *seq);
|
||||
void sequencer_stop(Sequencer *seq);
|
||||
void sequencer_set_tempo(Sequencer *seq, uint16_t bpm_x10);
|
||||
void sequencer_preview_note(Sequencer *seq, uint8_t channel, uint8_t note,
|
||||
|
||||
Reference in New Issue
Block a user