Phase 16: persist sample-timed effect automation and add expressive MIDI controls
This commit is contained in:
parent
f72314b5a3
commit
7fac0a4f6a
23 changed files with 374 additions and 39 deletions
8
Makefile
8
Makefile
|
|
@ -320,3 +320,11 @@ test-plugins-real: $(BUILD_DIR)/test_plugins
|
|||
.PHONY: test-gui-plugins
|
||||
test-gui-plugins: $(BUILD_DIR)/test_gui $(BUILD_DIR)/plugin-fixture.so
|
||||
XTC_TEST_PLUGIN="$(abspath $(BUILD_DIR))/plugin-fixture.so" xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
||||
|
||||
$(BUILD_DIR)/test_control: $(BUILD_DIR)/tests/unit/test_control.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
|
||||
.PHONY: test-control
|
||||
test-control: $(BUILD_DIR)/test_control
|
||||
./$(BUILD_DIR)/test_control
|
||||
test: test-control
|
||||
-include $(BUILD_DIR)/tests/unit/test_control.d
|
||||
|
|
|
|||
49
docs/CONTROL.md
Normal file
49
docs/CONTROL.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Automation and expressive control (Phase 16)
|
||||
|
||||
Track Gain/Pan lanes in the Mixer apply to audio and scheduled instrument voices.
|
||||
They use absolute project ticks, linear interpolation, hold-first/last outside
|
||||
points and the constant project tempo snapshot. A clip trim/move does not move
|
||||
track or effect automation: lanes belong to the project timeline. Instrument
|
||||
volume is controlled by its track Gain lane; the current sine has no timbre
|
||||
parameter. Track gain ramps and boundary fades are described in AUDIO_QUALITY.md.
|
||||
|
||||
Select a LADSPA node in Modular Graph and use Edit/Selected plugin automation.
|
||||
Enter `port tick mode value`: mode 0 interpolates linearly, 1 holds until the next
|
||||
point, and -1 removes the point at that port/tick (value ignored). Port names,
|
||||
ranges and current tick are displayed. There are at most 32 points per plugin;
|
||||
setting an existing port/tick replaces it. Integer/toggle controls require step
|
||||
mode. Invalid values, ticks, ports or duplicate persisted points reject the edit
|
||||
or file. Edits are undoable and rebuild on the control thread while stopped.
|
||||
|
||||
Effect lanes use the project tempo, hold first/last outside their range and
|
||||
apply before run at each exact sample. Distinct ticks rounding to one sample
|
||||
resolve to the later tick. Linear curves avoid stepping continuous controls;
|
||||
intentional step curves may click. The initial implementation calls run(1) while
|
||||
any lane exists, with connected preallocated buffers. CPU cost can be significant;
|
||||
only the documented LADSPA set is qualified. No worker, allocator, loader or
|
||||
clock is called from the host audio path. Native ABI 2 callers continue to use
|
||||
sample-offset parameter events, applied before equal-offset MIDI events.
|
||||
|
||||
Live MIDI input maps CC7 to per-channel volume (0..1), CC10 to pan (64 center),
|
||||
and CC64 to sustain (>=64 down). Volume/pan ramp over 5 ms. These fixed mappings
|
||||
apply to live sine voices, not track faders or prerecorded clips; controller
|
||||
capture, configurable learn and plugin MIDI are not supported. CC120/123 and
|
||||
Panic release all live voices and sustain and suppress already-started scheduled
|
||||
notes. Note-on velocity zero decodes to note-off. Repeated live note-on on one
|
||||
channel/pitch retriggers one voice; note-off releases it or marks it pending while
|
||||
sustain is held. Sustain-up releases pending voices, never newer retriggers.
|
||||
Seek, stop, teardown and disconnect clear live voices; plan reset returns controller
|
||||
values to unity/center. Equal-offset live events execute in input order before
|
||||
the sample; arrange note ends precede starts, and scheduled mixer values are
|
||||
evaluated at that sample. Overlapping arranged notes are independent.
|
||||
|
||||
Format v6 preserves v5 data plus effect lanes. A v5 plugin migrates with no lane
|
||||
and the project tempo. Repeated render/save/reopen, odd block partition, exact
|
||||
endpoints, seek/plan replacement, sustain/retrigger and controller ramp tests:
|
||||
|
||||
```sh
|
||||
make test test-control test-plugins test-gui-plugins test-plugins-real
|
||||
```
|
||||
|
||||
Physical controller devices and listening remain unqualified; no tempo map,
|
||||
latency compensation, MIDI capture, pitch bend or plugin crash isolation is added.
|
||||
|
|
@ -138,3 +138,13 @@ the module. Unknown nodes/versions fail without mutating the current project.
|
|||
Missing modules remain preserved by metadata load/save; GUI installation rejects
|
||||
them with a repair message. No transient DSP state, pointer or plugin binary is
|
||||
stored. See PLUGIN_HOSTING.md and decision 0009 for supported processors/limits.
|
||||
|
||||
## Phase 16: version 6
|
||||
|
||||
Writer emits v6 and readers accept v1–v6. After each plugin's CONTROL records,
|
||||
`PLUGIN_AUTOMATION count` is followed by exactly count (<=32) records:
|
||||
`PARAM tick port mode float32_hex_bits`. Mode is 0 linear or 1 step. Duplicate
|
||||
port/tick pairs, nonfinite values, unknown control ports and ticks beyond the
|
||||
project limit are rejected. Loaded plugin tempo comes from TEMPO. Range and
|
||||
discrete-mode compatibility are checked on control-thread compilation. Versions
|
||||
1–5 migrate with empty plugin lanes. See CONTROL.md for timing/ordering.
|
||||
|
|
|
|||
|
|
@ -324,6 +324,14 @@ unsupported; plugin realtime behavior and physical listening are unqualified.
|
|||
- Held notes are released safely on stop, panic, seek, and relevant failure paths.
|
||||
- Event ordering and interpolation behavior are documented and tested.
|
||||
|
||||
Implemented: persisted sample-evaluated LADSPA parameter lanes with linear/step
|
||||
modes, range/discrete validation and public GUI editing; absolute automation
|
||||
position through seek and plan handoff; fixed live CC7/10/64 mappings with volume/
|
||||
pan ramps, sustain/retrigger safety, and exact block-partition regressions.
|
||||
Existing track Gain/Pan lanes cover audio and scheduled instrument volume.
|
||||
Format v6 migrates v1–v5. See CONTROL.md. MIDI capture/learn, pitch bend, new
|
||||
instrument timbres and physical listening remain outside the supported scope.
|
||||
|
||||
## Phase 17 — Editing Workflow, Undo/Redo, and Usability
|
||||
|
||||
**Goal:** Make common editing tasks efficient and protect users from accidental changes.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ static xtc_result instrument(void *state, const xtc_native_block *b)
|
|||
if (m->type == XTC_MIDI_NOTE_ON) {
|
||||
v->pitch = m->pitch; v->velocity = (float)m->velocity / 127.0f;
|
||||
v->frequency = 440.0 * pow(2.0, ((double)m->pitch - 69.0) / 12.0);
|
||||
} else if (m->type == XTC_MIDI_ALL_OFF || m->pitch == v->pitch) v->velocity = 0;
|
||||
} else if (m->type == XTC_MIDI_ALL_OFF || (m->type == XTC_MIDI_NOTE_OFF && m->pitch == v->pitch)) v->velocity = 0;
|
||||
}
|
||||
b->output[i] = (float)sin(v->phase) * v->velocity * v->gain;
|
||||
v->phase = fmod(v->phase + 6.283185307179586 * v->frequency / v->rate, 6.283185307179586);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ typedef struct {
|
|||
} xtc_audio_buffer;
|
||||
|
||||
typedef struct xtc_plan xtc_plan;
|
||||
/* Single owner: set absolute automation/native-event position, no allocation. */
|
||||
xtc_result xtc_plan_position(xtc_plan *plan, uint64_t position);
|
||||
|
||||
typedef enum {
|
||||
XTC_NODE_GAIN = 1,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ typedef struct {
|
|||
typedef enum {
|
||||
XTC_MIDI_NOTE_ON = 1,
|
||||
XTC_MIDI_NOTE_OFF,
|
||||
XTC_MIDI_ALL_OFF
|
||||
XTC_MIDI_ALL_OFF,
|
||||
XTC_MIDI_CONTROL /* pitch = CC number, velocity = 0..127 value */
|
||||
} xtc_midi_type;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
#define XTC_PLUGIN_CONTROLS 16U
|
||||
#define XTC_PLUGIN_POINTS 32U
|
||||
/* Absolute project ticks. Mode belongs to segment starting at this point:
|
||||
* 0 linear, 1 hold/step. Outside a lane hold first/last; later ticks win when
|
||||
* sample rounding collides. Integer/toggle controls require mode 1. */
|
||||
typedef struct { uint64_t tick; unsigned int port, step; float value; } xtc_plugin_point;
|
||||
/* Plain copied model: no library handles or callback pointers. Absolute trusted
|
||||
* module path, descriptor index + unique ID + label checked again on compile.
|
||||
* Controls are actual LADSPA port indices, in ascending order. */
|
||||
|
|
@ -13,6 +18,8 @@ typedef struct {
|
|||
unsigned int index, unique_id, inputs, control_count;
|
||||
unsigned int ports[XTC_PLUGIN_CONTROLS];
|
||||
float values[XTC_PLUGIN_CONTROLS];
|
||||
unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */
|
||||
xtc_plugin_point points[XTC_PLUGIN_POINTS];
|
||||
} xtc_plugin_spec;
|
||||
typedef struct xtc_plugin xtc_plugin;
|
||||
/* Control thread only. Probe explicitly enumerates a file by descriptor index;
|
||||
|
|
@ -29,6 +36,7 @@ void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */
|
|||
/* Single audio owner. No loading/allocation/I/O in host process; trusted run()
|
||||
* must obey realtime rules. No MIDI; max one mono input and one mono output.
|
||||
* Transport reset retains LADSPA delay history (no realtime reset contract). */
|
||||
xtc_result xtc_plugin_process_at(xtc_plugin *plugin, const float *input, float *output, size_t frames, uint64_t position);
|
||||
xtc_result xtc_plugin_process(xtc_plugin *plugin, const float *input, float *output, size_t frames);
|
||||
xtc_result xtc_graph_add_plugin(xtc_graph *graph, const xtc_plugin_spec *spec, size_t *id);
|
||||
xtc_result xtc_graph_get_plugin(const xtc_graph *graph, size_t id, xtc_plugin_spec *out);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ extern "C" {
|
|||
#endif
|
||||
|
||||
typedef struct xtc_realtime xtc_realtime;
|
||||
/* Single audio owner, or stopped/joined control owner: position current/future plans. */
|
||||
xtc_result xtc_realtime_position(xtc_realtime *engine, uint64_t position);
|
||||
|
||||
typedef struct {
|
||||
unsigned int processed_blocks; /* Wraps modulo UINT_MAX + 1. */
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ static int process(jack_nframes_t frames, void *context)
|
|||
} else {
|
||||
const float *input = jack_port_get_buffer(backend->input, frames);
|
||||
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
||||
(void)xtc_realtime_position(backend->engine,position);
|
||||
if (backend->recorder)
|
||||
xtc_recorder_push(backend->recorder, input, (size_t)frames);
|
||||
if (backend->audio)
|
||||
|
|
@ -434,6 +435,7 @@ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample)
|
|||
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE))
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
xtc_midi_plan_reset(backend->midi);
|
||||
(void)xtc_realtime_position(backend->engine,sample);
|
||||
__atomic_store_n(&backend->sample_position, sample, __ATOMIC_RELAXED);
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -467,6 +469,7 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|||
* Notifications never access engine. Preserve old engine until ready. */
|
||||
xtc_realtime_destroy(backend->engine);
|
||||
backend->engine = replacement;
|
||||
(void)xtc_realtime_position(backend->engine,__atomic_load_n(&backend->sample_position,__ATOMIC_RELAXED));
|
||||
__atomic_store_n(&backend->process_result, (unsigned int)XTC_OK, __ATOMIC_RELAXED);
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,8 +271,8 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
|
|||
}
|
||||
}
|
||||
if (spec->type == XTC_NODE_PLUGIN) {
|
||||
xtc_result result = xtc_plugin_process(plan->plugins[channel * plan->node_count + node],
|
||||
spec->plugin.inputs ? buffer : NULL,buffer,frame_count);
|
||||
xtc_result result = xtc_plugin_process_at(plan->plugins[channel * plan->node_count + node],
|
||||
spec->plugin.inputs ? buffer : NULL,buffer,frame_count,plan->position);
|
||||
if (result != XTC_OK) {
|
||||
memset(output,0,frame_count*sizeof(*output));
|
||||
if (right) memset(right,0,frame_count*sizeof(*right));
|
||||
|
|
@ -372,3 +372,6 @@ xtc_result xtc_plan_process_sources(xtc_plan *p, const float *in, const float *a
|
|||
{
|
||||
return xtc_plan_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames);
|
||||
}
|
||||
|
||||
xtc_result xtc_plan_position(xtc_plan *p, uint64_t position)
|
||||
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; p->position = position; return XTC_OK; }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
struct xtc_realtime {
|
||||
double sample_rate;
|
||||
uint64_t position;
|
||||
size_t max_frames;
|
||||
xtc_plan *pending;
|
||||
xtc_plan *retired;
|
||||
|
|
@ -166,15 +167,20 @@ xtc_result xtc_realtime_process_stereo(xtc_realtime *engine, const float *input,
|
|||
__atomic_store_n(&engine->retired, old, __ATOMIC_RELEASE);
|
||||
}
|
||||
}
|
||||
if (engine->active != NULL)
|
||||
result = xtc_plan_process_stereo(engine->active, input, timeline, timeline_right, instrument,
|
||||
if (engine->active != NULL) {
|
||||
result = xtc_plan_position(engine->active,engine->position);
|
||||
if (result == XTC_OK) result = xtc_plan_process_stereo(engine->active, input, timeline, timeline_right, instrument,
|
||||
instrument_right, output, right, frame_count);
|
||||
else {
|
||||
} else {
|
||||
memset(output, 0, frame_count * sizeof(*output));
|
||||
if (right)
|
||||
memset(right, 0, frame_count * sizeof(*right));
|
||||
}
|
||||
}
|
||||
if (result == XTC_OK) {
|
||||
if (engine->position <= UINT64_MAX-frame_count) engine->position += frame_count;
|
||||
else result = XTC_ERR_CAPACITY;
|
||||
}
|
||||
if (result != XTC_OK) {
|
||||
memset(output, 0, frame_count * sizeof(*output));
|
||||
if (right)
|
||||
|
|
@ -231,3 +237,6 @@ xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const
|
|||
{
|
||||
return xtc_realtime_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames);
|
||||
}
|
||||
|
||||
xtc_result xtc_realtime_position(xtc_realtime *p, uint64_t position)
|
||||
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; p->position = position; return XTC_OK; }
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
|
||||
menu->add("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this);
|
||||
menu->add("File/Load LADSPA plugin", 0, plugin_callback, this);
|
||||
menu->add("Edit/Selected plugin automation", 0, plugin_callback, this);
|
||||
menu->add("Edit/Selected plugin control", 0, plugin_callback, this);
|
||||
menu->add("File/Quit", FL_CTRL + 'q', close_callback, this);
|
||||
menu->add("Audio/Start engine", FL_F + 5, play_callback, this);
|
||||
|
|
@ -1372,8 +1373,31 @@ xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float va
|
|||
for (i = 0; i < spec.control_count && spec.ports[i] != port; ++i) {}
|
||||
if (i == spec.control_count) return XTC_ERR_NOT_FOUND;
|
||||
spec.values[i] = value;
|
||||
return apply_plugin_spec(id,spec);
|
||||
}
|
||||
xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove)
|
||||
{
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
xtc_plugin_spec spec{};
|
||||
xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec);
|
||||
if (r != XTC_OK) return r;
|
||||
unsigned i;
|
||||
for (i = 0; i < spec.point_count; ++i) if (spec.points[i].port == point.port && spec.points[i].tick == point.tick) break;
|
||||
if (remove) {
|
||||
if (i == spec.point_count) return XTC_ERR_NOT_FOUND;
|
||||
for (unsigned j = i+1; j < spec.point_count; ++j) spec.points[j-1] = spec.points[j];
|
||||
--spec.point_count;
|
||||
} else {
|
||||
if (i == XTC_PLUGIN_POINTS) return XTC_ERR_CAPACITY;
|
||||
spec.points[i] = point;
|
||||
if (i == spec.point_count) ++spec.point_count;
|
||||
}
|
||||
return apply_plugin_spec(id,spec);
|
||||
}
|
||||
xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec)
|
||||
{
|
||||
xtc_graph *graph = nullptr;
|
||||
r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
|
||||
xtc_result r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
|
||||
if (r == XTC_OK) r = xtc_graph_set_plugin(graph,id,&spec);
|
||||
/* Compile before adoption even with a mocked/disconnected backend. */
|
||||
xtc_plan *check = nullptr;
|
||||
|
|
@ -1423,6 +1447,21 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
|
|||
for (unsigned i = 0; i < spec.control_count; ++i)
|
||||
prompt += std::to_string(spec.ports[i]) + ": " + info[i].name + " = " + std::to_string(spec.values[i]) +
|
||||
" [" + std::to_string(info[i].minimum) + ", " + std::to_string(info[i].maximum) + "]\n";
|
||||
if (std::strcmp(label,"Selected plugin automation") == 0) {
|
||||
prompt += "Enter port tick mode value; mode 0 linear, 1 step, -1 remove.\nCurrent tick: " +
|
||||
std::to_string(w->sequencer_->position());
|
||||
const char *answer = fl_input("%s","",prompt.c_str());
|
||||
if (!answer) return;
|
||||
unsigned port; unsigned long long tick; int mode; float value; char extra;
|
||||
if (std::sscanf(answer,"%u %llu %d %f %c",&port,&tick,&mode,&value,&extra) != 4 || mode < -1 || mode > 1)
|
||||
r = XTC_ERR_INVALID_ARGUMENT;
|
||||
else {
|
||||
xtc_plugin_point point{static_cast<uint64_t>(tick),port,mode == 1 ? 1U : 0U,value};
|
||||
r = w->set_plugin_automation(id,point,mode == -1);
|
||||
}
|
||||
if (r != XTC_OK) w->report_error("Plugin automation rejected: check tick, port, range and discrete mode",r);
|
||||
return;
|
||||
}
|
||||
prompt += "Enter port and value (for example: 2 0.5):";
|
||||
const char *answer = fl_input("%s","",prompt.c_str());
|
||||
if (!answer) return;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
public:
|
||||
/* Control-thread plugin commands, transactional model adoption. */
|
||||
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
|
||||
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);
|
||||
xtc_result set_plugin_control(size_t id, unsigned int port, float value);
|
||||
/**
|
||||
* @brief Constructs the window, its widgets, and the initial project.
|
||||
|
|
@ -289,6 +290,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
* @param context MainWindow * owning the project and file path.
|
||||
*/
|
||||
static void plugin_callback(Fl_Widget *widget, void *context);
|
||||
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec);
|
||||
xtc_result install_plugin_graph(xtc_graph *graph);
|
||||
static void file_callback(Fl_Widget * widget, void *context);
|
||||
GraphWidget *graph_widget_ = nullptr;
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ typedef struct {
|
|||
} endpoint;
|
||||
|
||||
typedef struct {
|
||||
unsigned int active, channel, pitch, velocity;
|
||||
unsigned int active, channel, pitch, velocity, released;
|
||||
uint64_t age;
|
||||
double phase;
|
||||
} voice;
|
||||
|
||||
struct xtc_midi_plan {
|
||||
size_t fade_frames;
|
||||
size_t fade_frames, controller_frames;
|
||||
xtc_gain_ramp volume[16], pan[16];
|
||||
unsigned int sustain[16];
|
||||
xtc_mix_snapshot mix;
|
||||
span notes[XTC_PROJECT_MAX_NOTES];
|
||||
size_t count;
|
||||
|
|
@ -54,13 +56,13 @@ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int
|
|||
return XTC_ERR_UNSUPPORTED;
|
||||
if (size != 3 || bytes[1] > 127 || bytes[2] > 127)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (command == 176 && bytes[1] != 120 && bytes[1] != 123)
|
||||
if (command == 176 && bytes[1] != 120 && bytes[1] != 123 && bytes[1] != 7 && bytes[1] != 10 && bytes[1] != 64)
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
e.offset = offset;
|
||||
e.channel = bytes[0] & 15U;
|
||||
e.pitch = bytes[1];
|
||||
e.velocity = bytes[2];
|
||||
e.type = command == 176 ? XTC_MIDI_ALL_OFF : command == 128 || !e.velocity ? XTC_MIDI_NOTE_OFF : XTC_MIDI_NOTE_ON;
|
||||
e.type = command == 176 ? (bytes[1] == 120 || bytes[1] == 123 ? XTC_MIDI_ALL_OFF : XTC_MIDI_CONTROL) : command == 128 || !e.velocity ? XTC_MIDI_NOTE_OFF : XTC_MIDI_NOTE_ON;
|
||||
*out = e;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -93,6 +95,11 @@ void xtc_midi_plan_reset(xtc_midi_plan *p)
|
|||
memset(p->cutoff, 0, sizeof(p->cutoff));
|
||||
p->next_position = UINT64_MAX;
|
||||
p->age = 0;
|
||||
{ unsigned int c; for (c = 0; c < 16; ++c) {
|
||||
p->sustain[c] = 0;
|
||||
(void)xtc_gain_ramp_reset(p->volume+c,1);
|
||||
(void)xtc_gain_ramp_reset(p->pan+c,0);
|
||||
} }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -181,6 +188,8 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
|
|||
}
|
||||
}
|
||||
free(points);
|
||||
p->controller_frames = rate / 200U;
|
||||
if (!p->controller_frames) p->controller_frames = 1;
|
||||
xtc_midi_plan_reset(p);
|
||||
*out = p;
|
||||
return XTC_OK;
|
||||
|
|
@ -209,6 +218,7 @@ static void channel_off(xtc_midi_plan *p, unsigned int channel, uint64_t positio
|
|||
for (i = 0; i < XTC_MIDI_VOICES; ++i)
|
||||
if (p->voices[i].channel == channel)
|
||||
p->voices[i].active = 0;
|
||||
p->sustain[channel] = 0;
|
||||
p->cutoff[channel] = position == UINT64_MAX ? position : position + 1;
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +252,17 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position)
|
|||
channel_off(p, e->channel, position);
|
||||
return;
|
||||
}
|
||||
if (e->type == XTC_MIDI_CONTROL) {
|
||||
if (e->pitch == 7) (void)xtc_gain_ramp_target(p->volume+e->channel,(float)e->velocity/127.0f,p->controller_frames);
|
||||
else if (e->pitch == 10) (void)xtc_gain_ramp_target(p->pan+e->channel,
|
||||
e->velocity >= 64 ? (float)(e->velocity-64)/63.0f : ((float)e->velocity-64)/64.0f,p->controller_frames);
|
||||
else if (e->pitch == 64) {
|
||||
p->sustain[e->channel] = e->velocity >= 64;
|
||||
if (!p->sustain[e->channel]) for (i = 0; i < XTC_MIDI_VOICES; ++i)
|
||||
if (p->voices[i].channel == e->channel && p->voices[i].released) p->voices[i].active = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < XTC_MIDI_VOICES; ++i) {
|
||||
if (p->voices[i].active && p->voices[i].channel == e->channel && p->voices[i].pitch == e->pitch) {
|
||||
slot = i;
|
||||
|
|
@ -249,8 +270,10 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position)
|
|||
}
|
||||
}
|
||||
if (e->type == XTC_MIDI_NOTE_OFF) {
|
||||
if (slot != SIZE_MAX)
|
||||
p->voices[slot].active = 0;
|
||||
if (slot != SIZE_MAX) {
|
||||
if (p->sustain[e->channel]) p->voices[slot].released = 1;
|
||||
else p->voices[slot].active = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot == SIZE_MAX) {
|
||||
|
|
@ -267,6 +290,7 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position)
|
|||
slot = i;
|
||||
}
|
||||
p->voices[slot].active = 1;
|
||||
p->voices[slot].released = 0;
|
||||
p->voices[slot].channel = e->channel;
|
||||
p->voices[slot].pitch = e->pitch;
|
||||
p->voices[slot].velocity = e->velocity;
|
||||
|
|
@ -315,19 +339,24 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ
|
|||
right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1);
|
||||
}
|
||||
}
|
||||
for (i = 0; i < XTC_MIDI_VOICES; ++i) {
|
||||
voice *v = p->voices + i;
|
||||
double step = p->step[v->pitch];
|
||||
if (!v->active || step == 0.0)
|
||||
continue;
|
||||
for (j = 0; j < frames; ++j) {
|
||||
float sample = (float)(sin(TAU * v->phase) * 0.1 * v->velocity / 127.0);
|
||||
out[j] += sample;
|
||||
if (right)
|
||||
right[j] += sample;
|
||||
for (j = 0; j < frames; ++j) {
|
||||
float levels[16], pans[16]; unsigned int c;
|
||||
for (c = 0; c < 16; ++c) {
|
||||
levels[c] = pans[c] = 1;
|
||||
(void)xtc_gain_ramp_process(p->volume+c,levels+c,NULL,1);
|
||||
(void)xtc_gain_ramp_process(p->pan+c,pans+c,NULL,1);
|
||||
}
|
||||
for (i = 0; i < XTC_MIDI_VOICES; ++i) {
|
||||
voice *v = p->voices+i;
|
||||
double step = p->step[v->pitch];
|
||||
float sample, pan;
|
||||
if (!v->active || step == 0) continue;
|
||||
sample = (float)(sin(TAU*v->phase)*.1*v->velocity/127.0) * levels[v->channel];
|
||||
pan = pans[v->channel];
|
||||
out[j] += sample * (right && pan > 0 ? 1-pan : 1);
|
||||
if (right) right[j] += sample * (pan < 0 ? 1+pan : 1);
|
||||
v->phase += step;
|
||||
if (v->phase >= 1)
|
||||
v->phase -= 1;
|
||||
if (v->phase >= 1) v->phase -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -354,7 +383,7 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons
|
|||
for (i = 0; i < count; ++i) {
|
||||
const xtc_midi_event *e = events + i;
|
||||
if (e->offset >= frames || (i && e->offset < events[i - 1].offset) || e->type < XTC_MIDI_NOTE_ON ||
|
||||
e->type > XTC_MIDI_ALL_OFF || e->pitch > 127 || e->velocity > 127 || e->channel > 15 ||
|
||||
e->type > XTC_MIDI_CONTROL || e->pitch > 127 || e->velocity > 127 || e->channel > 15 ||
|
||||
(e->type == XTC_MIDI_NOTE_ON && !e->velocity))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ xtc_result xtc_native_process(xtc_native *p, const xtc_native_block *b)
|
|||
for (i = 0; i < b->event_count; ++i) {
|
||||
const xtc_midi_event *e = b->events + i;
|
||||
if (e->offset >= b->frames || (i && e->offset < b->events[i - 1].offset) || e->type < XTC_MIDI_NOTE_ON ||
|
||||
e->type > XTC_MIDI_ALL_OFF || e->channel > 15 || e->pitch > 127 || e->velocity > 127 ||
|
||||
e->type > XTC_MIDI_CONTROL || e->channel > 15 || e->pitch > 127 || e->velocity > 127 ||
|
||||
(e->type == XTC_MIDI_NOTE_ON && !e->velocity))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ xtc_result xtc_processor_process(xtc_processor *p, const xtc_native_block *b, co
|
|||
for (i = 0; i < b->event_count; ++i) {
|
||||
const xtc_midi_event *e = b->events + i;
|
||||
if (e->offset >= b->frames || (i && e->offset < b->events[i-1].offset) || e->type < XTC_MIDI_NOTE_ON ||
|
||||
e->type > XTC_MIDI_ALL_OFF || e->pitch > 127 || e->velocity > 127 || e->channel > 15 ||
|
||||
e->type > XTC_MIDI_CONTROL || e->pitch > 127 || e->velocity > 127 || e->channel > 15 ||
|
||||
(e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!b->frames) return XTC_OK;
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ static xtc_result read_project(reader *input, xtc_project *project)
|
|||
xtc_graph *graph = project->graph;
|
||||
READ("XTC_PROJECT", 2U);
|
||||
NUM(1U, version);
|
||||
if (version < 1U || version > 5U)
|
||||
if (version < 1U || version > 6U)
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
format_version = version;
|
||||
READ("APP", 4U);
|
||||
|
|
@ -255,6 +255,20 @@ static xtc_result read_project(reader *input, xtc_project *project)
|
|||
if (value >= 64 || !decode_float(input->tokens[2],p->values+k)) return XTC_ERR_FORMAT;
|
||||
p->ports[k] = (unsigned int)value;
|
||||
}
|
||||
p->tempo = project->tempo;
|
||||
if (format_version >= 6) {
|
||||
READ("PLUGIN_AUTOMATION",2U); NUM(1U,value);
|
||||
if (value > XTC_PLUGIN_POINTS) return XTC_ERR_FORMAT;
|
||||
p->point_count = (unsigned int)value;
|
||||
for (k = 0; k < p->point_count; ++k) {
|
||||
xtc_plugin_point *a = p->points+k;
|
||||
READ("PARAM",5U); NUM(1U,a->tick); NUM(2U,value);
|
||||
if (value >= 64) return XTC_ERR_FORMAT;
|
||||
a->port = (unsigned int)value;
|
||||
NUM(3U,value); if (value > 1) return XTC_ERR_FORMAT; a->step = (unsigned int)value;
|
||||
if (!decode_float(input->tokens[4],&a->value)) return XTC_ERR_FORMAT;
|
||||
}
|
||||
}
|
||||
if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT;
|
||||
}
|
||||
for (j = 0U; j < i; ++j)
|
||||
|
|
@ -423,7 +437,7 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
|
|||
return XTC_ERR_UNSUPPORTED;
|
||||
if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
if (fprintf(file, "XTC_PROJECT 5\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
|
||||
if (fprintf(file, "XTC_PROJECT 6\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
|
||||
XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id,
|
||||
project->next_clip_id, graph->require_output_nodes, graph->next_id, graph->node_count,
|
||||
graph->edge_count) < 0)
|
||||
|
|
@ -444,6 +458,12 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
|
|||
uint32_t value; memcpy(&value,p->values+k,4);
|
||||
if (fprintf(file,"CONTROL %u %08" PRIx32 "\n",p->ports[k],value) < 0) return XTC_ERR_IO;
|
||||
}
|
||||
if (fprintf(file,"PLUGIN_AUTOMATION %u\n",p->point_count) < 0) return XTC_ERR_IO;
|
||||
for (k = 0; k < p->point_count; ++k) {
|
||||
uint32_t value; memcpy(&value,&p->points[k].value,4);
|
||||
if (fprintf(file,"PARAM %" PRIu64 " %u %u %08" PRIx32 "\n",p->points[k].tick,p->points[k].port,
|
||||
p->points[k].step,value) < 0) return XTC_ERR_IO;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i = 0U; i < graph->edge_count; ++i)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "xtc/xtc_plugin.h"
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include "xtc/xtc_project.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -18,10 +19,20 @@ xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
|
|||
size_t i;
|
||||
if (!s || !terminated(s->path,sizeof(s->path)) || s->path[0] != '/' ||
|
||||
!terminated(s->label,sizeof(s->label)) || !s->label[0] || s->index >= 1024 ||
|
||||
s->inputs > 1 || s->control_count > XTC_PLUGIN_CONTROLS) return XTC_ERR_INVALID_ARGUMENT;
|
||||
s->inputs > 1 || s->control_count > XTC_PLUGIN_CONTROLS ||
|
||||
s->tempo < 20000 || s->tempo > 300000 || s->point_count > XTC_PLUGIN_POINTS) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < s->control_count; ++i)
|
||||
if (s->ports[i] >= 64 || (i && s->ports[i] <= s->ports[i-1]) || !isfinite(s->values[i]))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < s->point_count; ++i) {
|
||||
size_t j;
|
||||
const xtc_plugin_point *a = s->points+i;
|
||||
if (a->tick > XTC_PROJECT_MAX_TICK || a->step > 1 || !isfinite(a->value)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (j = 0; j < s->control_count && s->ports[j] != a->port; ++j) {}
|
||||
if (j == s->control_count) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (j = 0; j < i; ++j) if (s->points[j].port == a->port && s->points[j].tick == a->tick)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
#ifdef XTC_HAVE_LADSPA
|
||||
|
|
@ -36,6 +47,9 @@ struct xtc_plugin {
|
|||
float controls[64];
|
||||
size_t frames;
|
||||
int activated;
|
||||
xtc_plugin_point points[XTC_PLUGIN_POINTS];
|
||||
uint64_t samples[XTC_PLUGIN_POINTS], next;
|
||||
unsigned int point_count;
|
||||
};
|
||||
static xtc_result load(const char *path, unsigned int index, void **module, const LADSPA_Descriptor **descriptor)
|
||||
{
|
||||
|
|
@ -91,6 +105,7 @@ static xtc_result inspect(const LADSPA_Descriptor *d, double rate, xtc_plugin_sp
|
|||
if (!d->Label || !d->Label[0] || strlen(d->Label) >= sizeof(s->label) || d->UniqueID > UINT32_MAX ||
|
||||
!d->PortDescriptors || !d->PortRangeHints || !d->PortCount || d->PortCount > 64 ||
|
||||
!d->instantiate || !d->connect_port || !d->run || !d->cleanup) return XTC_ERR_UNSUPPORTED;
|
||||
s->tempo = 120000;
|
||||
s->unique_id = (unsigned int)d->UniqueID; strcpy(s->label,d->Label);
|
||||
for (i = 0; i < d->PortCount; ++i) {
|
||||
LADSPA_PortDescriptor p = d->PortDescriptors[i];
|
||||
|
|
@ -173,6 +188,15 @@ xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frame
|
|||
if (s->ports[c] != actual.ports[c] || !bounds(p->descriptor->PortRangeHints+s->ports[c],rate,s->values[c])) goto fail;
|
||||
p->controls[s->ports[c]] = s->values[c];
|
||||
}
|
||||
for (c = 0; c < s->point_count; ++c) {
|
||||
const xtc_plugin_point *a = s->points+c;
|
||||
const LADSPA_PortRangeHint *h = p->descriptor->PortRangeHints+a->port;
|
||||
if (!bounds(h,rate,a->value) || (!a->step &&
|
||||
(LADSPA_IS_HINT_INTEGER(h->HintDescriptor) || LADSPA_IS_HINT_TOGGLED(h->HintDescriptor)))) goto fail;
|
||||
p->points[c] = *a;
|
||||
p->samples[c] = a->tick * (uint64_t)rate * UINT64_C(60000) / ((uint64_t)s->tempo * XTC_TICKS_PER_QUARTER);
|
||||
}
|
||||
p->point_count = s->point_count;
|
||||
p->frames = frames;
|
||||
p->input = calloc(frames,sizeof(float)); p->output = calloc(frames,sizeof(float));
|
||||
r = XTC_ERR_OUT_OF_MEMORY; if (!p->input || !p->output) goto fail;
|
||||
|
|
@ -188,17 +212,51 @@ xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frame
|
|||
fail:
|
||||
xtc_plugin_destroy(p); return r;
|
||||
}
|
||||
xtc_result xtc_plugin_process(xtc_plugin *p, const float *input, float *output, size_t frames)
|
||||
static void automate(xtc_plugin *p, uint64_t position)
|
||||
{
|
||||
unsigned long port;
|
||||
for (port = 0; port < p->descriptor->PortCount; ++port) {
|
||||
size_t i, before = SIZE_MAX, after = SIZE_MAX;
|
||||
for (i = 0; i < p->point_count; ++i) if (p->points[i].port == port) {
|
||||
if (p->samples[i] <= position) {
|
||||
if (before == SIZE_MAX || p->points[i].tick > p->points[before].tick) before = i;
|
||||
} else if (after == SIZE_MAX || p->points[i].tick < p->points[after].tick) after = i;
|
||||
}
|
||||
if (before == SIZE_MAX) { if (after != SIZE_MAX) p->controls[port] = p->points[after].value; }
|
||||
else {
|
||||
double value = p->points[before].value;
|
||||
if (after != SIZE_MAX && !p->points[before].step)
|
||||
value += (double)(position-p->samples[before]) / (double)(p->samples[after]-p->samples[before]) *
|
||||
((double)p->points[after].value-p->points[before].value);
|
||||
p->controls[port] = (float)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *input, float *output, size_t frames, uint64_t position)
|
||||
{
|
||||
size_t i;
|
||||
if (!p || !output || frames > p->frames) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!p || !output || frames > p->frames || position > UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!frames) return XTC_OK;
|
||||
for (i = 0; i < frames; ++i) p->input[i] = input ? xtc_dsp_finite(input[i]) : 0;
|
||||
memset(p->output,0,frames*sizeof(float));
|
||||
p->descriptor->run(p->handle,(unsigned long)frames);
|
||||
for (i = 0; i < frames; ++i) output[i] = xtc_dsp_finite(p->output[i]);
|
||||
return XTC_OK;
|
||||
if (!p->point_count) {
|
||||
for (i = 0; i < frames; ++i) p->input[i] = input ? xtc_dsp_finite(input[i]) : 0;
|
||||
memset(p->output,0,frames*sizeof(float));
|
||||
p->descriptor->run(p->handle,(unsigned long)frames);
|
||||
for (i = 0; i < frames; ++i) output[i] = xtc_dsp_finite(p->output[i]);
|
||||
} else {
|
||||
/* Buffers stay connected: run(1) reads/writes slot zero, one control
|
||||
* evaluation per absolute sample. Exact aliasing is safe. */
|
||||
for (i = 0; i < frames; ++i) {
|
||||
automate(p,position+i);
|
||||
p->input[0] = input ? xtc_dsp_finite(input[i]) : 0; p->output[0] = 0;
|
||||
p->descriptor->run(p->handle,1);
|
||||
output[i] = xtc_dsp_finite(p->output[0]);
|
||||
}
|
||||
}
|
||||
p->next = position+frames; return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_plugin_process(xtc_plugin *p, const float *input, float *output, size_t frames)
|
||||
{ return xtc_plugin_process_at(p,input,output,frames,p ? p->next : 0); }
|
||||
|
||||
#else
|
||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double r, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
||||
{ (void)s; (void)r; (void)out; return XTC_ERR_UNSUPPORTED; }
|
||||
|
|
@ -207,6 +265,8 @@ xtc_result xtc_plugin_probe(const char *p, unsigned int i, double r, xtc_plugin_
|
|||
xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double r, size_t f, xtc_plugin **out)
|
||||
{ (void)s; (void)r; (void)f; if (!out) return XTC_ERR_INVALID_ARGUMENT; *out = NULL; return XTC_ERR_UNSUPPORTED; }
|
||||
void xtc_plugin_destroy(xtc_plugin *p) { (void)p; }
|
||||
xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *i, float *o, size_t f, uint64_t pos)
|
||||
{ (void)p; (void)i; (void)o; (void)f; (void)pos; return XTC_ERR_UNSUPPORTED; }
|
||||
xtc_result xtc_plugin_process(xtc_plugin *p, const float *i, float *o, size_t f)
|
||||
{ (void)p; (void)i; (void)o; (void)f; return XTC_ERR_UNSUPPORTED; }
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *source)
|
|||
copy->require_output_nodes = source->require_output_nodes;
|
||||
memcpy(copy->nodes, source->nodes, source->node_count * sizeof(*copy->nodes));
|
||||
memcpy(copy->edges, source->edges, source->edge_count * sizeof(*copy->edges));
|
||||
{ size_t i; for (i = 0; i < copy->node_count; ++i)
|
||||
if (copy->nodes[i].type == XTC_NODE_PLUGIN) copy->nodes[i].plugin.tempo = project->tempo; }
|
||||
xtc_graph_destroy(project->graph);
|
||||
project->graph = copy;
|
||||
return XTC_OK;
|
||||
|
|
@ -446,6 +448,8 @@ xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo)
|
|||
if (project == NULL || tempo < 20000U || tempo > 300000U)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
project->tempo = tempo;
|
||||
{ size_t i; for (i = 0; i < project->graph->node_count; ++i)
|
||||
if (project->graph->nodes[i].type == XTC_NODE_PLUGIN) project->graph->nodes[i].plugin.tempo = tempo; }
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -476,10 +476,15 @@ int main(int argc, char **argv)
|
|||
const xtc_project *before = sequence->project();
|
||||
CHECK(window.add_plugin("/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
||||
CHECK(window.add_plugin(module,2) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
|
||||
xtc_plugin_point point{960,spec.ports[0],0,.25f};
|
||||
CHECK(window.set_plugin_automation(id,point) == XTC_OK);
|
||||
CHECK(window.set_plugin_automation(id,point,true) == XTC_OK);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(window.save_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
|
||||
CHECK(window.new_project() == XTC_OK);
|
||||
CHECK(window.open_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
||||
CHECK(spec.point_count == 1 && spec.points[0].tick == 960 && spec.points[0].value == .25f);
|
||||
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
|
||||
CHECK(xtc_project_create(&missing) == XTC_OK);
|
||||
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
|
||||
|
|
|
|||
|
|
@ -133,6 +133,41 @@ int main(int argc, char **argv)
|
|||
for (i = 0; i < 17; ++i) assert(data[i] == 1);
|
||||
xtc_plugin_destroy(p); p = NULL;
|
||||
graph_roundtrip(&s);
|
||||
s.point_count = 2;
|
||||
s.points[0] = (xtc_plugin_point){0,s.ports[0],0,.5f};
|
||||
s.points[1] = (xtc_plugin_point){1,s.ports[0],0,2};
|
||||
graph_roundtrip(&s);
|
||||
{
|
||||
xtc_graph *g = NULL; xtc_realtime *rt = NULL; size_t id;
|
||||
float in[1] = {1}, out[1];
|
||||
assert(xtc_graph_create(8,&g) == XTC_OK && xtc_graph_add_plugin(g,&s,&id) == XTC_OK);
|
||||
assert(xtc_realtime_create(48000,31,&rt) == XTC_OK);
|
||||
assert(xtc_realtime_position(rt,25) == XTC_OK && xtc_realtime_submit(rt,g) == XTC_OK);
|
||||
audio = 1; assert(xtc_realtime_process(rt,in,out,1) == XTC_OK && out[0] == 2); audio = 0;
|
||||
xtc_realtime_collect(rt);
|
||||
assert(xtc_realtime_submit(rt,g) == XTC_OK);
|
||||
audio = 1; assert(xtc_realtime_process(rt,in,out,1) == XTC_OK && out[0] == 2); audio = 0;
|
||||
xtc_realtime_collect(rt);
|
||||
xtc_realtime_destroy(rt); xtc_graph_destroy(g);
|
||||
}
|
||||
{
|
||||
float whole[31], split[31], in[31]; xtc_plugin *other = NULL;
|
||||
for (i = 0; i < 31; ++i) in[i] = 1;
|
||||
assert(xtc_plugin_create(&s,48000,31,&p) == XTC_OK);
|
||||
assert(xtc_plugin_create(&s,48000,31,&other) == XTC_OK);
|
||||
audio = 1;
|
||||
assert(xtc_plugin_process_at(p,in,whole,31,0) == XTC_OK);
|
||||
assert(xtc_plugin_process_at(other,in,split,7,0) == XTC_OK);
|
||||
assert(xtc_plugin_process_at(other,in+7,split+7,24,7) == XTC_OK);
|
||||
audio = 0;
|
||||
for (i = 0; i < 31; ++i) {
|
||||
assert(whole[i] == split[i]);
|
||||
assert(fabsf(whole[i]-(i < 25 ? .5f+1.5f*(float)i/25 : 2)) < .000001f);
|
||||
}
|
||||
assert(xtc_plugin_process_at(other,in,split,1,25) == XTC_OK && split[0] == 2);
|
||||
xtc_plugin_destroy(p); xtc_plugin_destroy(other); p = NULL;
|
||||
}
|
||||
s.point_count = 0;
|
||||
s.values[0] = 3; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
|
||||
assert(xtc_plugin_probe(argv[1],1,48000,&s) == XTC_OK && !s.inputs);
|
||||
graph_roundtrip(&s);
|
||||
|
|
|
|||
38
tests/unit/test_control.c
Normal file
38
tests/unit/test_control.c
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#include "xtc/xtc_midi.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
int main(void)
|
||||
{
|
||||
xtc_project *project = NULL; xtc_midi_plan *a = NULL, *b = NULL;
|
||||
float whole[400], split[400]; size_t i, j, cursor = 0;
|
||||
xtc_midi_event decoded;
|
||||
const unsigned char cc[] = {0xb2,7,0};
|
||||
xtc_midi_event events[] = {
|
||||
{0,XTC_MIDI_NOTE_ON,2,69,127}, {3,XTC_MIDI_CONTROL,2,64,127},
|
||||
{5,XTC_MIDI_NOTE_OFF,2,69,0}, {9,XTC_MIDI_NOTE_ON,2,69,127},
|
||||
{10,XTC_MIDI_CONTROL,2,7,0}, {11,XTC_MIDI_CONTROL,2,10,127},
|
||||
{260,XTC_MIDI_NOTE_OFF,2,69,0}, {270,XTC_MIDI_CONTROL,2,64,0}};
|
||||
assert(xtc_midi_decode(cc,3,10,&decoded) == XTC_OK && decoded.type == XTC_MIDI_CONTROL && decoded.pitch == 7);
|
||||
assert(xtc_project_create(&project) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(project,48000,&a) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(project,48000,&b) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(a,0,events,8,whole,400) == XTC_OK);
|
||||
while (cursor < 400) {
|
||||
xtc_midi_event local[8]; size_t count = 0, n = 13;
|
||||
if (n > 400-cursor) n = 400-cursor;
|
||||
for (j = 0; j < 8; ++j) if (events[j].offset >= cursor && events[j].offset < cursor+n) {
|
||||
local[count] = events[j]; local[count++].offset -= (unsigned int)cursor;
|
||||
}
|
||||
assert(xtc_midi_plan_render(b,cursor,local,count,split+cursor,n) == XTC_OK); cursor += n;
|
||||
}
|
||||
for (i = 0; i < 400; ++i) assert(whole[i] == split[i]);
|
||||
assert(whole[6] != 0); /* Sustain retains the released note. */
|
||||
for (i = 250; i < 400; ++i) assert(whole[i] == 0); /* CC7 ramp reaches zero in 240 samples. */
|
||||
xtc_midi_plan_reset(a);
|
||||
assert(xtc_midi_plan_render(a,400,NULL,0,whole,400) == XTC_OK);
|
||||
for (i = 0; i < 400; ++i) assert(whole[i] == 0);
|
||||
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b); xtc_project_destroy(project);
|
||||
puts("Sample-timed CC7/CC10/sustain, retrigger, note-off and block partition checks passed.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue