feat(midi): add device routing and bounded external note output (phase 101)
This commit is contained in:
parent
2a86d766ca
commit
75dee16b6a
9 changed files with 251 additions and 15 deletions
2
Makefile
2
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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -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 && (i<count || j<live_count)){
|
||||
xtc_midi_event e=j==live_count || (i<count && scheduled[i].offset<=live[j].offset) ? scheduled[i++] : live[j++];
|
||||
if(write_midi(b,buffer,e,offset+e.offset))r=XTC_ERR_CAPACITY;
|
||||
}
|
||||
if(r!=XTC_OK){jack_midi_clear_buffer(buffer);output_panic(b,buffer,0);(void)__atomic_fetch_add(&b->midi_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
|
||||
|
|
|
|||
65
src/midi/output.c
Normal file
65
src/midi/output.c
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#include "output.h"
|
||||
#include "../project/project_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
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->sample<y->sample ? -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.channel<y->event.channel ? -1 : 1;
|
||||
return x->event.pitch<y->event.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;i<p->track_count;++i)if(!p->tracks[i].is_bus && p->tracks[i].solo)solo=1;
|
||||
for(i=0;i<p->note_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;j<p->track_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;i<p->count;++i)if(p->notes[i].start<position && p->notes[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(lo<hi){size_t mid=lo+(hi-lo)/2;if(p->events[mid].sample<position)lo=mid+1;else hi=mid;}
|
||||
for(i=lo;i<p->event_count && p->events[i].sample<position+frames;++i){
|
||||
if(n==XTC_MIDI_MAX_EVENTS)return XTC_ERR_CAPACITY;
|
||||
events[n]=p->events[i].event;events[n++].offset=(unsigned int)(p->events[i].sample-position);
|
||||
}
|
||||
p->next=position+frames;*count=n;return XTC_OK;
|
||||
}
|
||||
11
src/midi/output.h
Normal file
11
src/midi/output.h
Normal file
|
|
@ -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
|
||||
|
|
@ -10,8 +10,8 @@
|
|||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
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;j<count;++j){jack_midi_event_t e;if(jack_midi_event_get(&e,received,j)==0 && e.size==3){
|
||||
if((e.buffer[0]&0xf0)==0x90 && e.buffer[2])__atomic_fetch_add(&p->midi_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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue