diff --git a/Makefile b/Makefile index 09a37ac..179bc6e 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude LDFLAGS ?= -CORE_SOURCES := src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c +CORE_SOURCES := src/midi/output.c src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes) ifeq ($(JACK_AVAILABLE),yes) JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack) diff --git a/docs/MIDI_WORKFLOWS.md b/docs/MIDI_WORKFLOWS.md index 9fe3841..a4fa2f0 100644 --- a/docs/MIDI_WORKFLOWS.md +++ b/docs/MIDI_WORKFLOWS.md @@ -13,3 +13,22 @@ All persistent preference fields are listed by the shared C schema and available in Preferences. Existing project tempo/controller maps remain project data; Preferences project defaults apply to New. No configuration I/O runs in an audio callback. Tests and screenshot captures use isolated configuration paths. + +## MIDI backend + +xtc exposes `midi_in` and `midi_out` JACK ports. The output sends scheduled MIDI +clip notes from unmuted, solo-eligible tracks, retaining source channels and +velocities, with clipped note boundaries and notes chased after seeks/loops. +Tracks share one output; matching pitches/channels on different tracks can +conflict on receiving devices. Audio faders, effects and bus processing do not +alter external MIDI. MIDI CC automation, program changes, clock and SysEx output +are outside this note-output workflow. + +Input can filter a single channel, and output can preserve or force a channel. +Optional thru forwards supported live notes, controllers and pitch bend while +transport is active. Thru defaults off to avoid accidental feedback. Callback +work and event buffers are bounded; overflow requests all-notes-off and increments +the MIDI-drop counter. Panic and stop send CC123 on all channels. Immediate stop +waits up to 100 ms for callback acknowledgement before deactivation; delivery +cannot be guaranteed after server/device loss or to devices ignoring CC123. +No device is connected unless a user chooses it or an external patchbay routes it. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a3788c2..1e36015 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2241,12 +2241,12 @@ Implemented: ~/.xtc.conf is loaded before project/backend creation and saved ato Expose JACK MIDI source/destination discovery, routing, input filtering and external MIDI output with bounded callback work and panic safety. ### Tasks and acceptance criteria -- [ ] Implement and document the workflow with bounded validation and safe ownership. -- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. -- [ ] Commit this phase separately after validation. +- [x] Implement and document the workflow with bounded validation and safe ownership. +- [x] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [x] Commit this phase separately after validation. ### Implementation status -Planned. +Implemented: public JACK MIDI discovery/naming/routing/options APIs, a prepared bounded note-output schedule, optional filtered live thru and output channel override. Seek/loop discontinuities chase notes; panic/stop/overflow send all-notes-off. Core and allocation-failure suites and real JACK input/thru/receiver checks passed; deterministic backend checks cover event offsets, filtering, routing, panic and seek/note-off. See MIDI_WORKFLOWS.md for shared-output, transport-active and device-loss limits. --- diff --git a/include/xtc/xtc_backend.h b/include/xtc/xtc_backend.h index 1a8f9ba..97761c5 100644 --- a/include/xtc/xtc_backend.h +++ b/include/xtc/xtc_backend.h @@ -21,6 +21,7 @@ typedef struct { } xtc_meter_snapshot; typedef struct xtc_backend xtc_backend; +typedef int (*xtc_backend_port_visitor)(const char *name, void *context); typedef enum { XTC_BACKEND_JACK = 1 @@ -157,6 +158,16 @@ xtc_result xtc_backend_panic(xtc_backend *backend); * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out_name); +/* MIDI counterparts. direction is local for naming/routing, remote for visiting. + * Routing requires an active backend. No server starts or implicit device routes. + * Port names are JACK/PipeWire names; physical ALSA devices need host exposure. */ +xtc_result xtc_backend_midi_name(const xtc_backend *,xtc_port_direction,const char **); +xtc_result xtc_backend_visit_midi_ports(xtc_backend *,xtc_port_direction,xtc_backend_port_visitor,void *); +xtc_result xtc_backend_route_midi(xtc_backend *,xtc_port_direction,const char *,int disconnect); +/* Stopped only. Input channel/output channel -1 preserves all/original channels, + * otherwise 0..15. thru=1 forwards supported live messages to MIDI output. */ +xtc_result xtc_backend_set_midi_options(xtc_backend *,int input_channel,int output_channel,int thru); + /** * @brief Attaches or detaches a borrowed recorder while the backend is stopped. * @@ -210,7 +221,7 @@ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction * @param context Opaque pointer supplied to xtc_backend_visit_ports(). * @return Nonzero to stop enumeration, zero to continue. */ -typedef int (*xtc_backend_port_visitor)(const char *name, void *context); + /** * @brief Visits available audio ports matching the requested direction until the visitor stops. * diff --git a/src/backend/jack_backend.c b/src/backend/jack_backend.c index 84ddc00..41b0c79 100644 --- a/src/backend/jack_backend.c +++ b/src/backend/jack_backend.c @@ -1,7 +1,10 @@ +#define _POSIX_C_SOURCE 200809L #include "xtc/xtc_backend.h" #include "xtc/xtc_realtime.h" #include "xtc/xtc_dsp.h" #include "../mixer/mix_internal.h" +#include "../midi/output.h" +#include #include #include @@ -13,10 +16,13 @@ struct xtc_backend { jack_client_t *client; - jack_port_t *input, *output, *midi_input, *output_right; + jack_port_t *input, *output, *midi_input, *output_right, *midi_output; xtc_realtime *engine; xtc_audio_plan *audio; xtc_midi_plan *midi; + xtc_midi_output_plan *midi_out; + unsigned int output_seen,output_muted; + int input_channel,output_channel,midi_thru; unsigned int panic_requested, panic_seen, midi_dropped, peak_left, peak_right; xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */ float *timeline, *instrument, *monitor; @@ -72,6 +78,7 @@ static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_ if (!buffer || total > XTC_MIDI_MAX_EVENTS) { (void)__atomic_fetch_add(&backend->midi_dropped, total ? total : 1U, __ATOMIC_RELAXED); xtc_midi_plan_panic(backend->midi, position); + (void)__atomic_fetch_add(&backend->panic_requested,1U,__ATOMIC_RELEASE); } else for (i = 0; i < total; ++i) { jack_midi_event_t raw; @@ -79,6 +86,7 @@ static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_ if (jack_midi_event_get(&raw, buffer, i) != 0 || raw.time >= frames) { (void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED); xtc_midi_plan_panic(backend->midi, position); + (void)__atomic_fetch_add(&backend->panic_requested,1U,__ATOMIC_RELEASE); count = 0; break; } @@ -88,14 +96,43 @@ static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_ if (result != XTC_OK || (count && events[count].offset < events[count - 1].offset)) { (void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED); xtc_midi_plan_panic(backend->midi, position); + (void)__atomic_fetch_add(&backend->panic_requested,1U,__ATOMIC_RELEASE); count = 0; break; } - ++count; + if(backend->input_channel<0 || events[count].channel==(unsigned int)backend->input_channel)++count; } return count; } +static int write_midi(xtc_backend *b,void *buffer,xtc_midi_event e,unsigned int offset) +{ + unsigned char bytes[3]; + unsigned int channel=b->output_channel<0 ? e.channel : (unsigned int)b->output_channel; + bytes[0]=(unsigned char)((e.type==XTC_MIDI_NOTE_ON ? 0x90 : e.type==XTC_MIDI_NOTE_OFF ? 0x80 : e.type==XTC_MIDI_BEND ? 0xe0 : 0xb0)|channel); + bytes[1]=(unsigned char)(e.type==XTC_MIDI_ALL_OFF ? 123 : e.pitch);bytes[2]=(unsigned char)e.velocity; + return jack_midi_event_write(buffer,offset,bytes,3); +} +static void output_panic(xtc_backend *b,void *buffer,unsigned int offset) +{ + unsigned int ch; + for(ch=0;ch<16;++ch){unsigned char bytes[3]={(unsigned char)(0xb0|ch),123,0}; + if(jack_midi_event_write(buffer,offset,bytes,3)) (void)__atomic_fetch_add(&b->midi_dropped,1U,__ATOMIC_RELAXED); + } +} +static void output_segment(xtc_backend *b,void *buffer,uint64_t position,size_t frames,unsigned int offset, + const xtc_midi_event *live,size_t live_count) +{ + xtc_midi_event scheduled[XTC_MIDI_MAX_EVENTS];size_t count=0,i=0,j=0; + if(__atomic_load_n(&b->output_muted,__ATOMIC_ACQUIRE))return; + xtc_result r=b->midi_out ? xtc_midi_output_events(b->midi_out,position,frames,scheduled,&count) : XTC_OK; + if(!b->midi_thru)live_count=0; + while(r==XTC_OK && (imidi_dropped,1U,__ATOMIC_RELAXED);} +} /** * @brief Runs the JACK audio callback, rendering sources, processing the graph, and publishing status. * @@ -109,7 +146,15 @@ static int process(jack_nframes_t frames, void *context) float *output = jack_port_get_buffer(backend->output, frames); float *right = backend->output_right ? jack_port_get_buffer(backend->output_right, frames) : NULL; xtc_result result = XTC_OK; + void *midi_buffer=jack_port_get_buffer(backend->midi_output,frames); + unsigned int panic=__atomic_load_n(&backend->panic_requested,__ATOMIC_ACQUIRE); + if(midi_buffer){ + jack_midi_clear_buffer(midi_buffer); + if(panic!=__atomic_load_n(&backend->output_seen,__ATOMIC_RELAXED) || __atomic_load_n(&backend->stop_done,__ATOMIC_ACQUIRE))output_panic(backend,midi_buffer,0); + __atomic_store_n(&backend->output_seen,panic,__ATOMIC_RELEASE); + } if (output == NULL || (backend->output_right && !right)) { + if(midi_buffer){jack_midi_clear_buffer(midi_buffer);output_panic(backend,midi_buffer,0);} if (output) memset(output, 0, (size_t)frames * sizeof(*output)); if (right) @@ -156,6 +201,7 @@ static int process(jack_nframes_t frames, void *context) for (i = 0; i < count; ++i) if (events[i].offset >= offset && events[i].offset < offset+n) { segment_events[queued] = events[i]; segment_events[queued++].offset -= (unsigned int)offset; } + if(midi_buffer)output_segment(backend,midi_buffer,position,n,(unsigned int)offset,segment_events,queued); if (result == XTC_OK && backend->midi) result = xtc_midi_plan_render_stereo(backend->midi,position,segment_events,queued,backend->instrument, right ? backend->instrument+backend->max_frames : NULL,n); @@ -192,6 +238,7 @@ static int process(jack_nframes_t frames, void *context) __atomic_store_n(&backend->stop_done, 1U, __ATOMIC_RELEASE); } if (result != XTC_OK) { + if(midi_buffer){jack_midi_clear_buffer(midi_buffer);output_panic(backend,midi_buffer,0);} (void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE); if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U); @@ -344,12 +391,14 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_ goto fail; backend->input = jack_port_register(backend->client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U); backend->output = jack_port_register(backend->client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U); + backend->input_channel=backend->output_channel=-1; + backend->midi_output=jack_port_register(backend->client,"midi_out",JACK_DEFAULT_MIDI_TYPE,JackPortIsOutput,0U); backend->midi_input = jack_port_register(backend->client, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0U); if (config->channel_count == 2U) backend->output_right = jack_port_register(backend->client, "output_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U); if ((config->channel_count == 2U && !backend->output_right) || backend->input == NULL || backend->output == NULL || - backend->midi_input == NULL) { + backend->midi_input == NULL || !backend->midi_output) { result = XTC_ERR_IO; goto fail; } @@ -374,6 +423,7 @@ fail: } xtc_realtime_destroy(backend->engine); xtc_audio_plan_destroy(backend->audio); + xtc_midi_output_destroy(backend->midi_out); xtc_midi_plan_destroy(backend->midi); free(backend->monitor); free(backend->instrument); @@ -392,11 +442,13 @@ xtc_result xtc_backend_close(xtc_backend *backend) { if (backend == NULL) return XTC_OK; + if(backend->ready && backend->running){xtc_result r=xtc_backend_stop(backend);if(r!=XTC_OK)return r;} /* JACK removes this client and joins callbacks before returning success. */ if (jack_client_close(backend->client) != 0) return XTC_ERR_IO; xtc_realtime_destroy(backend->engine); xtc_audio_plan_destroy(backend->audio); + xtc_midi_output_destroy(backend->midi_out); xtc_midi_plan_destroy(backend->midi); free(backend->monitor); free(backend->instrument); @@ -421,8 +473,11 @@ xtc_result xtc_backend_start(xtc_backend *backend) return XTC_ERR_INVALID_STATE; if (backend->running) return XTC_ERR_INVALID_STATE; + __atomic_store_n(&backend->output_muted,0U,__ATOMIC_RELEASE); + xtc_midi_output_reset(backend->midi_out); xtc_midi_plan_reset(backend->midi); backend->panic_seen = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE); + __atomic_store_n(&backend->output_seen,backend->panic_seen,__ATOMIC_RELEASE); __atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED); __atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED); backend->stop_seen = 0; @@ -451,6 +506,9 @@ xtc_result xtc_backend_stop(xtc_backend *backend) xtc_midi_plan_reset(backend->midi); return XTC_OK; } + __atomic_store_n(&backend->output_muted,1U,__ATOMIC_RELEASE); + {unsigned int request=__atomic_add_fetch(&backend->panic_requested,1U,__ATOMIC_RELEASE);unsigned int tries; + for(tries=0;tries<100 && __atomic_load_n(&backend->output_seen,__ATOMIC_ACQUIRE)!=request;++tries){struct timespec pause={0,1000000};nanosleep(&pause,NULL);}} if (jack_deactivate(backend->client) != 0) return XTC_ERR_IO; backend->running = 0; @@ -530,6 +588,7 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project * { xtc_audio_plan *candidate = NULL; xtc_midi_plan *midi = NULL; + xtc_midi_output_plan *midi_out=NULL; float *monitor = NULL; size_t tracks = 0, clips = 0, i; xtc_result result; @@ -549,10 +608,12 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project * result = xtc_midi_plan_create(project, backend->expected_rate, &midi); if (result == XTC_OK) result = xtc_midi_plan_set_fade_frames(midi, backend->transition_frames); + if(result==XTC_OK)result=xtc_midi_output_create(project,backend->expected_rate,&midi_out); if (result == XTC_OK) result = xtc_backend_submit(backend, xtc_project_graph(project)); if (result != XTC_OK) { xtc_audio_plan_destroy(candidate); + xtc_midi_output_destroy(midi_out); xtc_midi_plan_destroy(midi); free(monitor); return result; @@ -571,7 +632,9 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project * xtc_midi_plan_monitor(midi, monitor, backend->max_frames); xtc_audio_plan_destroy(backend->audio); backend->audio = candidate; + xtc_midi_output_destroy(backend->midi_out); xtc_midi_plan_destroy(backend->midi); + backend->midi_out=midi_out; backend->midi = midi; return XTC_OK; } @@ -845,6 +908,38 @@ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direc return route(backend, direction, remote, 1); } +xtc_result xtc_backend_set_midi_options(xtc_backend *b,int input,int output,int thru) +{ + if(!b || input < -1 || input>15 || output < -1 || output>15 || (thru!=0 && thru!=1))return XTC_ERR_INVALID_ARGUMENT; + if(!b->ready || b->running)return XTC_ERR_INVALID_STATE; + b->input_channel=input;b->output_channel=output;b->midi_thru=thru;return XTC_OK; +} +xtc_result xtc_backend_midi_name(const xtc_backend *b,xtc_port_direction d,const char **out) +{ + if(!b || !out || (d!=XTC_PORT_INPUT && d!=XTC_PORT_OUTPUT))return XTC_ERR_INVALID_ARGUMENT; + if(!b->ready)return XTC_ERR_INVALID_STATE; + *out=jack_port_name(d==XTC_PORT_INPUT ? b->midi_input : b->midi_output);return XTC_OK; +} +xtc_result xtc_backend_visit_midi_ports(xtc_backend *b,xtc_port_direction d,xtc_backend_port_visitor visit,void *context) +{ + const char **ports;size_t i; + if(!b || !visit || (d!=XTC_PORT_INPUT && d!=XTC_PORT_OUTPUT))return XTC_ERR_INVALID_ARGUMENT; + if(!b->ready || __atomic_load_n(&b->lost,__ATOMIC_ACQUIRE))return XTC_ERR_INVALID_STATE; + ports=jack_get_ports(b->client,NULL,JACK_DEFAULT_MIDI_TYPE,d==XTC_PORT_INPUT ? JackPortIsInput : JackPortIsOutput); + if(ports){for(i=0;ports[i];++i)if(strcmp(ports[i],jack_port_name(b->midi_input)) && strcmp(ports[i],jack_port_name(b->midi_output)) && visit(ports[i],context))break;jack_free(ports);} + return XTC_OK; +} +xtc_result xtc_backend_route_midi(xtc_backend *b,xtc_port_direction d,const char *remote,int disconnect) +{ + const char *own;int code;xtc_result r; + if(!remote || !*remote || (disconnect!=0 && disconnect!=1))return XTC_ERR_INVALID_ARGUMENT; + r=xtc_backend_midi_name(b,d,&own);if(r!=XTC_OK)return r; + if(!b->running)return XTC_ERR_INVALID_STATE; + if(!strcmp(remote,jack_port_name(b->midi_input)) || !strcmp(remote,jack_port_name(b->midi_output)))return XTC_ERR_INVALID_ARGUMENT; + code=disconnect ? jack_disconnect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own) : + jack_connect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own); + return code==0 || (!disconnect && code==EEXIST) ? XTC_OK : XTC_ERR_IO; +} xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out) { size_t i; @@ -1135,4 +1230,8 @@ xtc_result xtc_backend_request_stop(xtc_backend *backend) xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out) { (void)backend; (void)out; return XTC_ERR_UNSUPPORTED; } +xtc_result xtc_backend_set_midi_options(xtc_backend *b,int i,int o,int t){(void)b;(void)i;(void)o;(void)t;return XTC_ERR_UNSUPPORTED;} +xtc_result xtc_backend_midi_name(const xtc_backend *b,xtc_port_direction d,const char **o){(void)b;(void)d;(void)o;return XTC_ERR_UNSUPPORTED;} +xtc_result xtc_backend_visit_midi_ports(xtc_backend *b,xtc_port_direction d,xtc_backend_port_visitor v,void *c){(void)b;(void)d;(void)v;(void)c;return XTC_ERR_UNSUPPORTED;} +xtc_result xtc_backend_route_midi(xtc_backend *b,xtc_port_direction d,const char *r,int x){(void)b;(void)d;(void)r;(void)x;return XTC_ERR_UNSUPPORTED;} #endif diff --git a/src/midi/output.c b/src/midi/output.c new file mode 100644 index 0000000..6bf6714 --- /dev/null +++ b/src/midi/output.c @@ -0,0 +1,65 @@ +#include "output.h" +#include "../project/project_internal.h" +#include +#include +typedef struct {uint64_t start,end;unsigned int pitch,velocity,channel;} output_note; +typedef struct {uint64_t sample;xtc_midi_event event;} output_event; +struct xtc_midi_output_plan { + size_t count,event_count;uint64_t next; + output_note notes[XTC_PROJECT_MAX_NOTES]; + output_event events[XTC_PROJECT_MAX_NOTES*2]; +}; +static int compare(const void *a,const void *b) +{ + const output_event *x=a,*y=b; + if(x->sample!=y->sample)return x->samplesample ? -1 : 1; + if(x->event.type!=y->event.type)return x->event.type==XTC_MIDI_NOTE_OFF ? -1 : 1; + if(x->event.channel!=y->event.channel)return x->event.channelevent.channel ? -1 : 1; + return x->event.pitchevent.pitch ? -1 : x->event.pitch!=y->event.pitch; +} +void xtc_midi_output_destroy(xtc_midi_output_plan *p){free(p);} +void xtc_midi_output_reset(xtc_midi_output_plan *p){if(p)p->next=UINT64_MAX;} +xtc_result xtc_midi_output_create(const xtc_project *p,unsigned int rate,xtc_midi_output_plan **out) +{ + xtc_midi_output_plan *plan;size_t i,j;int solo=0;xtc_result r; + if(!p || !out || rate<8000 || rate>192000)return XTC_ERR_INVALID_ARGUMENT; + plan=calloc(1,sizeof(*plan));if(!plan)return XTC_ERR_OUT_OF_MEMORY; + for(i=0;itrack_count;++i)if(!p->tracks[i].is_bus && p->tracks[i].solo)solo=1; + for(i=0;inote_count;++i){ + const xtc_note_info *n=p->notes+i;xtc_clip_info c;uint64_t begin,end,a,b;output_note *note; + r=xtc_project_find_clip(p,n->clip_id,&c);if(r!=XTC_OK){free(plan);return r;} + for(j=0;jtrack_count;++j)if(p->tracks[j].id==c.track_id)break; + if(j==p->track_count || p->tracks[j].mute || (solo && !p->tracks[j].solo))continue; + begin=n->start_tick>c.offset_tick ? n->start_tick : c.offset_tick; + end=n->start_tick+n->length_tick; + if(end>c.offset_tick+c.length_tick)end=c.offset_tick+c.length_tick; + if(begin>=end)continue; + r=xtc_project_tick_to_sample(p,c.start_tick+begin-c.offset_tick,rate,&a); + if(r==XTC_OK)r=xtc_project_tick_to_sample(p,c.start_tick+end-c.offset_tick,rate,&b); + if(r!=XTC_OK){free(plan);return r;} + if(b<=a)b=a+1; + note=plan->notes+plan->count++;*note=(output_note){a,b,n->pitch,n->velocity,n->channel}; + plan->events[plan->event_count++]=(output_event){a,{0,XTC_MIDI_NOTE_ON,n->channel,n->pitch,n->velocity}}; + plan->events[plan->event_count++]=(output_event){b,{0,XTC_MIDI_NOTE_OFF,n->channel,n->pitch,0}}; + } + qsort(plan->events,plan->event_count,sizeof(*plan->events),compare);plan->next=UINT64_MAX;*out=plan;return XTC_OK; +} +xtc_result xtc_midi_output_events(xtc_midi_output_plan *p,uint64_t position,size_t frames,xtc_midi_event *events,size_t *count) +{ + size_t n=0,i,lo=0,hi; + if(!p || !events || !count || !frames || frames>65536 || position>UINT64_MAX-frames)return XTC_ERR_INVALID_ARGUMENT; + if(p->next!=position){ + for(i=0;i<16;++i)events[n++]=(xtc_midi_event){0,XTC_MIDI_ALL_OFF,(unsigned int)i,123,0}; + for(i=0;icount;++i)if(p->notes[i].startnotes[i].end>position){ + output_note *note=p->notes+i;if(n==XTC_MIDI_MAX_EVENTS)return XTC_ERR_CAPACITY; + events[n++]=(xtc_midi_event){0,XTC_MIDI_NOTE_ON,note->channel,note->pitch,note->velocity}; + } + } + hi=p->event_count; + while(loevents[mid].sampleevent_count && p->events[i].sampleevents[i].event;events[n++].offset=(unsigned int)(p->events[i].sample-position); + } + p->next=position+frames;*count=n;return XTC_OK; +} diff --git a/src/midi/output.h b/src/midi/output.h new file mode 100644 index 0000000..4c262e1 --- /dev/null +++ b/src/midi/output.h @@ -0,0 +1,11 @@ +#ifndef XTC_MIDI_OUTPUT_H +#define XTC_MIDI_OUTPUT_H +#include "xtc/xtc_midi.h" +/* Prepared on control thread, single audio owner after publication. */ +typedef struct xtc_midi_output_plan xtc_midi_output_plan; +xtc_result xtc_midi_output_create(const xtc_project *,unsigned int,xtc_midi_output_plan **); +void xtc_midi_output_destroy(xtc_midi_output_plan *); +void xtc_midi_output_reset(xtc_midi_output_plan *); +/* Discontinuities emit all-off plus chased notes. Capacity errors retain count; discard scratch events on error. */ +xtc_result xtc_midi_output_events(xtc_midi_output_plan *,uint64_t,size_t,xtc_midi_event *,size_t *); +#endif diff --git a/tests/integration/test_jack_midi.c b/tests/integration/test_jack_midi.c index ff0424b..b48f1e9 100644 --- a/tests/integration/test_jack_midi.c +++ b/tests/integration/test_jack_midi.c @@ -10,8 +10,8 @@ #include #include typedef struct { - jack_port_t *input, *right, *midi; - unsigned int command, seen, loud, right_loud, heard, bad; + jack_port_t *input, *right, *midi, *receive; + unsigned int command, seen, loud, right_loud, heard, bad, midi_on, midi_off; } probe; static int process(jack_nframes_t frames, void *context) { @@ -23,6 +23,11 @@ static int process(jack_nframes_t frames, void *context) unsigned int command = __atomic_load_n(&p->command, __ATOMIC_ACQUIRE), loud = 0; jack_nframes_t i; if (!input || !right || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; } + {void *received=jack_port_get_buffer(p->receive,frames);uint32_t count=jack_midi_get_event_count(received),j; + for(j=0;jmidi_on,1U,__ATOMIC_RELAXED); + if((e.buffer[0]&0xf0)==0x80 || ((e.buffer[0]&0xf0)==0xb0 && e.buffer[1]==123))__atomic_fetch_add(&p->midi_off,1U,__ATOMIC_RELAXED); + }}} jack_midi_clear_buffer(midi); if (command && command != p->seen) { unsigned char bytes[3] = {0x90, 69, command % 2 ? 127 : 0}; @@ -68,7 +73,9 @@ int main(void) state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); state.right = jack_port_register(client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); state.midi = jack_port_register(client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); - REQUIRE(state.input && state.right && state.midi); + state.receive=jack_port_register(client,"receive",JACK_DEFAULT_MIDI_TYPE,JackPortIsInput,0); + REQUIRE(state.input && state.right && state.midi && state.receive); + REQUIRE(xtc_backend_set_midi_options(backend,-1,-1,1)==XTC_OK); REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0); REQUIRE(xtc_backend_start(backend) == XTC_OK); REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK); @@ -83,10 +90,12 @@ int main(void) int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); } { const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK); int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); } + REQUIRE(xtc_backend_route_midi(backend,XTC_PORT_OUTPUT,jack_port_name(state.receive),0)==XTC_OK); __atomic_store_n(&state.command, 1U, __ATOMIC_RELEASE); wait_blocks(); REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED)); __atomic_store_n(&state.command, 2U, __ATOMIC_RELEASE); wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED)); + REQUIRE(__atomic_load_n(&state.midi_on,__ATOMIC_RELAXED)>0 && __atomic_load_n(&state.midi_off,__ATOMIC_RELAXED)>0); __atomic_store_n(&state.command, 3U, __ATOMIC_RELEASE); wait_blocks(); REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED)); REQUIRE(xtc_backend_panic(backend) == XTC_OK); wait_blocks(); diff --git a/tests/unit/test_backend.c b/tests/unit/test_backend.c index f78f0ad..2351f17 100644 --- a/tests/unit/test_backend.c +++ b/tests/unit/test_backend.c @@ -20,7 +20,8 @@ static JackXRunCallback xrun_cb; static JackShutdownCallback shutdown_cb; static void *context; static float input[128], output[128], output_right[128]; -static int client_token, input_token, output_token, right_token, midi_token; +static int client_token, input_token, output_token, right_token, midi_token, midi_output_token; +static unsigned char sent[512][3];static unsigned int sent_time[512],sent_count; static unsigned int midi_count; static jack_midi_event_t midi_events[256]; static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail; @@ -46,6 +47,7 @@ jack_port_t *jack_port_register(jack_client_t *client, const char *name, const c ++port_calls; if (register_fail == port_calls) return NULL; if (!strcmp(name, "output_right")) return (jack_port_t *)&right_token; + if(!strcmp(name,"midi_out"))return (jack_port_t *)&midi_output_token; if (!strcmp(type, JACK_DEFAULT_MIDI_TYPE)) return (jack_port_t *)&midi_token; return (jack_port_t *)(flags == JackPortIsInput ? &input_token : &output_token); } @@ -68,12 +70,15 @@ int jack_activate(jack_client_t *client) int jack_deactivate(jack_client_t *client) { (void)client; if (deactivate_fail) return -1; active = 0; return 0; } void *jack_port_get_buffer(jack_port_t *port, jack_nframes_t frames) -{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; if (port == (jack_port_t *)&right_token) return output_right; return port == (jack_port_t *)&input_token ? input : output; } +{ (void)frames; if (null_buffer) return NULL; if(port==(jack_port_t *)&midi_output_token)return &midi_output_token; if (port == (jack_port_t *)&midi_token) return &midi_token; if (port == (jack_port_t *)&right_token) return output_right; return port == (jack_port_t *)&input_token ? input : output; } +void jack_midi_clear_buffer(void *buffer){(void)buffer;sent_count=0;} +int jack_midi_event_write(void *buffer,jack_nframes_t time,const jack_midi_data_t *data,size_t size) +{(void)buffer;if(size!=3 || sent_count>=512)return -1;memcpy(sent[sent_count],data,3);sent_time[sent_count++]=time;return 0;} uint32_t jack_midi_get_event_count(void *buffer) { (void)buffer; return midi_count; } int jack_midi_event_get(jack_midi_event_t *event, void *buffer, uint32_t index) { (void)buffer; if (index >= midi_count || index >= 256) return -1; *event = midi_events[index]; return 0; } const char *jack_port_name(const jack_port_t *port) -{ if (port == (jack_port_t *)&right_token) return "test:output_right"; if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; } +{ if(port==(jack_port_t *)&midi_output_token)return "test:midi_out"; if (port == (jack_port_t *)&right_token) return "test:output_right"; if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; } const char **jack_get_ports(jack_client_t *client, const char *pattern, const char *type, unsigned long flags) { const char **list = malloc(2U * sizeof(*list)); @@ -402,6 +407,23 @@ int main(void) xtc_project_destroy(p); } CHECK(closed >= 104); + { + xtc_project *p=NULL;uint64_t t,c,n;const char *midi_name=NULL;unsigned char on[]={0x92,64,100}; + CHECK(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Out",&t)==XTC_OK); + CHECK(xtc_project_add_midi_clip(p,t,0,3840,&c)==XTC_OK && xtc_project_add_note(p,c,0,960,60,90,1,&n)==XTC_OK); + CHECK(xtc_backend_open(&config,&backend)==XTC_OK && xtc_backend_prepare_project(backend,p)==XTC_OK); + CHECK(xtc_backend_midi_name(backend,XTC_PORT_OUTPUT,&midi_name)==XTC_OK && !strcmp(midi_name,"test:midi_out")); + CHECK(xtc_backend_set_midi_options(backend,2,4,1)==XTC_OK && xtc_backend_start(backend)==XTC_OK); + CHECK(xtc_backend_route_midi(backend,XTC_PORT_OUTPUT,"device:in",0)==XTC_OK); + CHECK(xtc_backend_set_midi_options(backend,-1,-1,0)==XTC_ERR_INVALID_STATE); + midi_count=1;midi_events[0]=(jack_midi_event_t){8,3,on};CHECK(process_cb(32,context)==0); + CHECK(sent_count==18 && sent[16][0]==0x94 && sent[16][1]==60 && sent_time[17]==8 && sent[17][1]==64); + on[0]=0x93;CHECK(process_cb(32,context)==0 && sent_count==0); + midi_count=0;CHECK(xtc_backend_panic(backend)==XTC_OK && process_cb(32,context)==0 && sent_count==16); + CHECK(xtc_backend_stop(backend)==XTC_OK && xtc_backend_seek(backend,23984)==XTC_OK && xtc_backend_start(backend)==XTC_OK); + CHECK(process_cb(32,context)==0 && sent_count==18 && sent[16][0]==0x94 && sent_time[16]==0 && sent[17][0]==0x84 && sent_time[17]==16); + CHECK(xtc_backend_stop(backend)==XTC_OK && xtc_backend_close(backend)==XTC_OK);xtc_project_destroy(p); + } CHECK(xtc_backend_close(NULL) == XTC_OK); xtc_graph_destroy(invalid_graph); xtc_graph_destroy(graph);