From 159fa939a1672fac243c271e6583fc825c11f88c Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 5 Nov 2026 23:47:54 +0100 Subject: [PATCH] feat: add editable internal instruments, automation and versioned presets --- docs/BUILTIN_DEVICES.md | 39 ++++++++++++-- docs/PROJECT_FORMAT.md | 17 ++++-- docs/ROADMAP.md | 16 +++--- include/xtc/xtc_midi.h | 5 +- include/xtc/xtc_plugin.h | 6 +++ include/xtc/xtc_project.h | 2 +- src/gui/main_window.cpp | 55 +++++++++++++++++++ src/gui/main_window.hpp | 1 + src/midi/instrument.c | 9 ++-- src/native/processor.c | 2 +- src/persistence/project_file.c | 33 ++++++++++++ src/plugin/builtin.c | 57 +++++++++++++------- src/plugin/builtin.h | 2 +- src/plugin/host.c | 35 +++++++++++- src/plugin/modern.c | 8 +-- src/project/project.c | 3 +- tests/integration/test_gui.cpp | 7 +++ tests/integration/test_install.py | 2 +- tests/integration/test_project_inspection.py | 2 +- tests/unit/test_builtin.c | 42 ++++++++++++++- tests/unit/test_project_diagnostics.c | 4 +- 21 files changed, 295 insertions(+), 52 deletions(-) diff --git a/docs/BUILTIN_DEVICES.md b/docs/BUILTIN_DEVICES.md index f97322a..60f93ba 100644 --- a/docs/BUILTIN_DEVICES.md +++ b/docs/BUILTIN_DEVICES.md @@ -5,10 +5,10 @@ built-in sine remains available for existing projects. Choose **Internal: Soft Keys** or **Internal: Sub Bass** in a MIDI track's instrument dropdown. Soft Keys blends a sine with a third harmonic; Sub Bass -uses a pure sine. Both are fixed presets with 32 voices, velocity sensitivity, +uses a pure sine. Both have 32 voices, velocity sensitivity, short attack/release envelopes, sustain pedal, note-off and panic handling. -High harmonics are omitted near Nyquist. These basic instruments have no preset -editor, pitch bend, sample library or oscillator/filter controls. +High harmonics are omitted near Nyquist. The default sounds remain compatible with fixed presets from v13. See the +Phase 41 controls below. A sample library is not provided by these oscillators. In **Modular Graph**, select a wire and choose **Edit → Insert internal delay on wire** or **Insert internal reverb on wire**. This preserves the route and inserts @@ -57,3 +57,36 @@ For repeatable manual listening, run `make builtin-preview` and audition followed by silence for tails. Check onset/release, unwanted discontinuities, finite decay and the documented mono character. Generation is verified; human listening and physical-device qualification remain pending. + +## Phase 41 instrument controls and presets + +**Edit → Selected instrument controls** edits an assigned internal instrument. +The five ports are harmonic amount (0..1), attack (0.1..2000 ms), release +(1..5000 ms), low-pass cutoff (20..24000 Hz; 24000 bypasses the filter) and +pitch-bend range (0..24 semitones, default 2). The harmonic is a third harmonic; +it is suppressed before crossing Nyquist. Attack/release are exponential time +constants, not times to absolute silence. Defaults preserve the original sounds. + +**Selected instrument automation** accepts `port tick mode value`, with absolute +project ticks, linear mode 0, hold mode 1, or -1 to remove a point. Automation +uses absolute sample positions, including seeks and 256-frame instrument chunks. +Static edits stop/reprepare playback and are undoable. Existing v13 assignments +with no parameters upgrade to defaults when opened in the editor. + +**File → Save/Load instrument preset** uses an explicit absolute path. Presets +are versioned single-instrument `.xtc` containers with no clips. Public C APIs +`xtc_plugin_preset_save/load` provide atomic saving and transactional metadata +loading; they never execute plugin code. Loading into a track is an undoable, +prepared edit. Parameter automation is included. Preset paths are ordinary files; +saving uses the existing project replacement semantics. + +Live MIDI pitch bend uses standard 14-bit messages (8192 center) and is applied +at the event's sample offset per channel. Panic clears the affected channel's +bend and sustain. The original unassigned sine ignores bend; CLAP MIDI/VST2 +receive raw bend, while native-note CLAP/VST3 do not yet map it to expressions. +No external-plugin parameter support is implied. + +Format 14 adds instrument controls and automation. Readers still accept v1–v13; +old fixed internal assignments use their original defaults. Older builds reject +v14. Tests cover legacy-default sound equality, v13 migration, parameter/preset +round trips, pitch-bend offsets, automated block partitions, undo and GUI edits. diff --git a/docs/PROJECT_FORMAT.md b/docs/PROJECT_FORMAT.md index c8078c7..de2c558 100644 --- a/docs/PROJECT_FORMAT.md +++ b/docs/PROJECT_FORMAT.md @@ -1,4 +1,4 @@ -# Project format v13 (reads v1–v12) +# Project format v14 (reads v1–v13) Phases 5–31 use a bounded text format implemented in C99 without a parser library. The extension is `.xtc`. File operations run exclusively on the control thread. @@ -104,7 +104,7 @@ node types 1..4, migrating their clips to empty asset paths. Version 2 accepts node types 1..5 and ASSET records. Both migrate to MIDI=0 and no notes. Version 3 accepts instrument nodes and MIDI/NOTES records. Versions 1–3 migrate all tracks to unity gain, centered pan, mute/solo off and no automation. -Saving always writes v12. Unknown versions are rejected without replacing the destination; older +Saving always writes v14. Unknown versions are rejected without replacing the destination; older applications without v12 support cannot read current saves. Migration and maximum-path roundtrips are tested. ## Ownership and failures @@ -274,7 +274,7 @@ must rebuild and initialize the appended `xtc_automation_point.step` field. ## Phase 28 compatibility and diagnostic contract -The current reader accepts versions 1–12 and the writer always emits version 12. +The current reader accepts versions 1–14 and the writer always emits version 14. Phase 28 made no format change; Phase 31 adds plugin format identity in v11. Saving an older document upgrades it; retain an original backup if an older application must still open it. APP is informational; compatibility depends on the format version, not the producer's @@ -396,3 +396,14 @@ Format value 4 identifies dependency-free internal devices at `/xtc/internal`. Indices 0/1 bind fixed Soft Keys/Sub Bass instruments; 2/3 are Delay/Reverb effects with three controls and existing automation records. Identity and control ranges are validated without loading libraries. See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md). + +## Version 14 instrument parameters + +Each track INSTRUMENT identity is followed by `INSTRUMENT_CONTROLS count`, +`CONTROL port value_bits` records, `INSTRUMENT_AUTOMATION count` and +`PARAM tick port mode value_bits` records. Bounds, duplicate points and metadata +are validated before destination replacement, just as for graph effects. +Earlier bindings have no controls/automation; v13 internal instruments retain +fixed default sounds. Modern external instruments still require zero controls. +Instrument tempo follows project tempo. Instrument preset files use this same +schema with exactly one instrument track and no clips; they are metadata only. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b9f95b6..655cb28 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1174,14 +1174,18 @@ manual checks; human listening and physical hardware remain unverified. Extend the fixed Soft Keys/Sub Bass presets into editable instruments. ### Tasks and acceptance criteria -- [ ] Define bounded oscillator, envelope and filter controls with metadata and defaults that preserve existing preset sounds. -- [ ] Provide instrument parameter editing, automation and reusable preset save/load through public C APIs. -- [ ] Support channel pitch bend with a documented range and sustain/note lifecycle behavior. -- [ ] Persist parameters and presets with explicit version migration, undo and failed-preparation rollback. -- [ ] Verify sample timing, block partition consistency, bounds, aliasing behavior and callback safety. +- [x] Define bounded oscillator, envelope and filter controls with metadata and defaults that preserve existing preset sounds. +- [x] Provide instrument parameter editing, automation and reusable preset save/load through public C APIs. +- [x] Support channel pitch bend with a documented range and sustain/note lifecycle behavior. +- [x] Persist parameters and presets with explicit version migration, undo and failed-preparation rollback. +- [x] Verify sample timing, block partition consistency, bounds, aliasing behavior and callback safety. ### Implementation status -Planned. The current instruments expose no editable parameters or pitch bend. +Implemented: five bounded oscillator/envelope/filter/bend controls, absolute-time +automation, instrument editor and reusable preset C APIs/GUI commands. Format 14 +persists bindings and reads v1–v13, preserving legacy default sounds. Core, +reliability and GUI tests pass, including preset/migration, exact partition and +pitch-bend offset regressions. See BUILTIN_DEVICES.md and PROJECT_FORMAT.md. --- diff --git a/include/xtc/xtc_midi.h b/include/xtc/xtc_midi.h index 2c06721..9567457 100644 --- a/include/xtc/xtc_midi.h +++ b/include/xtc/xtc_midi.h @@ -45,7 +45,8 @@ typedef enum { XTC_MIDI_NOTE_ON = 1, XTC_MIDI_NOTE_OFF, XTC_MIDI_ALL_OFF, - XTC_MIDI_CONTROL /* pitch = CC number, velocity = 0..127 value */ + XTC_MIDI_CONTROL, /* pitch = CC number, velocity = 0..127 value */ + XTC_MIDI_BEND /* pitch = LSB, velocity = MSB of 14-bit bend; center 8192 */ } xtc_midi_type; typedef struct { @@ -127,7 +128,7 @@ xtc_result xtc_project_remove_note(xtc_project *project, uint64_t id); /* Complete 3-byte note on/off and CC0..127. Velocity-zero on -> off. * CC120/123 decode to ALL_OFF; other CCs are interpreted by prepared bindings. * Unsupported messages return UNSUPPORTED; malformed supported messages INVALID. - * Output unchanged on error. No running-status/SysEx/program/bend support. */ + * Output unchanged on error. No running-status/SysEx/program support. */ /** * @brief Decodes a complete note, controller or all-notes-off MIDI message into a block event. * diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index ab47d8b..807fcaf 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -73,6 +73,12 @@ void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */ * must obey realtime rules. No MIDI; max one mono input and one mono output. * Transport reset retains LADSPA delay history (no realtime reset contract). */ /* Instruments: sorted bounded MIDI, mono output, single audio owner. */ +/* Absolute sample position supports instrument parameter automation and seek. */ +xtc_result xtc_plugin_process_midi_at(xtc_plugin *plugin, const xtc_midi_event *events, size_t count, float *output, size_t frames, uint64_t position); +/* Control-thread preset files use a versioned single-instrument project container. + * Load is transactional, validates metadata and never loads executable modules. */ +xtc_result xtc_plugin_preset_save(const xtc_plugin_spec *spec, const char *path); +xtc_result xtc_plugin_preset_load(const char *path, xtc_plugin_spec *out); xtc_result xtc_plugin_process_midi(xtc_plugin *plugin, const xtc_midi_event *events, size_t count, float *output, size_t frames); /* Metadata-only track assignment. NULL restores built-in sine. Assignment ensures * an instrument-source graph route transactionally. Missing modules stay saved. */ diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index 43617bc..b95a4c6 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -7,7 +7,7 @@ extern "C" { #endif -#define XTC_PROJECT_FORMAT_VERSION 13U +#define XTC_PROJECT_FORMAT_VERSION 14U #define XTC_PROJECT_MAX_TRACKS 64U #define XTC_PROJECT_MAX_CLIPS 1024U #define XTC_PROJECT_NAME_BYTES 64U diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 7f9ac43..b83374c 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -128,6 +128,10 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) menu->add("Track/Use built-in instrument", 0, instrument_callback, this); menu->add("Track/Live MIDI to selected instrument", 0, instrument_callback, this); menu->add("Track/Live MIDI to built-in sine", 0, instrument_callback, this); + menu->add("Edit/Selected instrument controls",0,instrument_callback,this); + menu->add("Edit/Selected instrument automation",0,instrument_callback,this); + menu->add("File/Save instrument preset",0,instrument_callback,this); + menu->add("File/Load instrument preset",0,instrument_callback,this); menu->add("Edit/Insert internal delay on wire", 0, plugin_callback, this); menu->add("Edit/Insert internal reverb on wire", 0, plugin_callback, this); menu->add("File/Add internal delay", 0, plugin_callback, this); @@ -1850,6 +1854,18 @@ xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsig if (r != XTC_OK) xtc_project_destroy(candidate); else { sequencer_->select(track,clip); mixer_->select_track(track); mark_dirty(); } return r; } +xtc_result MainWindow::set_instrument_spec(uint64_t track,const xtc_plugin_spec &spec) +{ + if(recorder_)return XTC_ERR_INVALID_STATE; + uint64_t clip=sequencer_->selected_clip();xtc_project *candidate=nullptr; + xtc_result r=xtc_project_clone(project_.get(),&candidate); + if(r==XTC_OK)r=xtc_project_set_instrument(candidate,track,&spec); + if(r==XTC_OK)r=xtc_history_reserve(history_.get()); + if(r==XTC_OK)r=install_project(candidate,false); + if(r!=XTC_OK)xtc_project_destroy(candidate); + else {sequencer_->select(track,clip);mixer_->select_track(track);mark_dirty();} + return r; +} xtc_result MainWindow::select_live_instrument(uint64_t track) { if (recorder_) return XTC_ERR_INVALID_STATE; @@ -1869,6 +1885,45 @@ void MainWindow::instrument_callback(Fl_Widget *widget, void *context) const char *label = menu ? menu->mvalue()->label() : "Load instrument plugin"; uint64_t track = w->sequencer_->selected_track(); xtc_result r; + if(std::strcmp(label,"Selected instrument controls")==0 || std::strcmp(label,"Selected instrument automation")==0 || + std::strcmp(label,"Save instrument preset")==0 || std::strcmp(label,"Load instrument preset")==0) { + xtc_plugin_spec spec{}; + if(std::strcmp(label,"Load instrument preset")==0) { + const char *path=fl_input("Instrument preset path (absolute)","");if(!path)return; + r=xtc_plugin_preset_load(path,&spec); + if(r==XTC_OK)r=w->set_instrument_spec(track,spec); + } else { + r=xtc_project_get_instrument(w->project_.get(),track,&spec); + if(r==XTC_OK && spec.format==XTC_PLUGIN_BUILTIN && !spec.control_count) + r=xtc_plugin_probe(XTC_BUILTIN_PATH,spec.index,w->sample_rate(),&spec); + if(r==XTC_OK && std::strcmp(label,"Save instrument preset")==0) { + const char *path=fl_input("Save instrument preset path (absolute)","");if(!path)return; + r=xtc_plugin_preset_save(&spec,path); + } else if(r==XTC_OK && std::strcmp(label,"Selected instrument controls")==0) { + xtc_plugin_details details{};xtc_plugin_spec edited{}; + r=xtc_plugin_describe(&spec,w->sample_rate(),&details); + if(r==XTC_OK && xtc_edit_plugin(spec,details,edited))r=w->set_instrument_spec(track,edited); + } else if(r==XTC_OK) { + xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};r=xtc_plugin_parameters(&spec,w->sample_rate(),info); + std::string prompt="Instrument automation: port tick mode value (mode -1 removes)\n"; + for(unsigned int i=0;i1)r=XTC_ERR_INVALID_ARGUMENT; + else { + unsigned int i=0;for(;i(tick),port,static_cast(mode),value};} + if(r==XTC_OK)r=w->set_instrument_spec(track,spec); + } + } + } + } + if(r!=XTC_OK)w->report_error("Instrument edit failed; check track, preset and control ranges",r); + return; + } if (std::strcmp(label,"Use built-in instrument") == 0) r = w->assign_instrument(track,nullptr,0); else if (std::strcmp(label,"Live MIDI to selected instrument") == 0) r = w->select_live_instrument(track); else if (std::strcmp(label,"Live MIDI to built-in sine") == 0) r = w->select_live_instrument(UINT64_MAX); diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index 82f0e7d..b74a0fd 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -49,6 +49,7 @@ class MainWindow : public Fl_Double_Window { Fl_Widget *velocity_lane() const { return velocity_lane_; } /* Control-thread plugin commands, transactional model adoption. */ xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index); + xtc_result set_instrument_spec(uint64_t track, const xtc_plugin_spec &spec); xtc_result select_live_instrument(uint64_t track); static void instrument_callback(Fl_Widget *, void *); xtc_result set_control_map(unsigned int channel, const unsigned int controllers[3]); diff --git a/src/midi/instrument.c b/src/midi/instrument.c index a36dfdf..3432d13 100644 --- a/src/midi/instrument.c +++ b/src/midi/instrument.c @@ -61,7 +61,7 @@ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int if (!bytes || !size || !out) return XTC_ERR_INVALID_ARGUMENT; command = bytes[0] & 240U; - if (command != 128 && command != 144 && command != 176) + if (command != 128 && command != 144 && command != 176 && command != 224) return XTC_ERR_UNSUPPORTED; if (size != 3 || bytes[1] > 127 || bytes[2] > 127) return XTC_ERR_INVALID_ARGUMENT; @@ -69,7 +69,7 @@ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int e.channel = bytes[0] & 15U; e.pitch = bytes[1]; e.velocity = bytes[2]; - 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; + e.type = command == 224 ? XTC_MIDI_BEND : 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; } @@ -274,6 +274,7 @@ 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_BEND) return; /* Built-in sine retains its established pitch. */ if (e->type == XTC_MIDI_CONTROL) { if (e->pitch == p->control_map[e->channel][0]) (void)xtc_gain_ramp_target(p->volume+e->channel,(float)e->velocity/127.0f,p->controller_frames); else if (e->pitch == p->control_map[e->channel][1]) (void)xtc_gain_ramp_target(p->pan+e->channel, @@ -435,7 +436,7 @@ static xtc_result instruments(xtc_midi_plan *p, uint64_t position, const xtc_mid if (!enqueue(p,&queued,e)) return XTC_ERR_CAPACITY; } } - r = xtc_plugin_process_midi(p->plugins[track],p->queue,queued,p->scratch,n); + r = xtc_plugin_process_midi_at(p->plugins[track],p->queue,queued,p->scratch,n,begin); if (r != XTC_OK) return r; for (j = 0; j < n; ++j) { float sample = p->scratch[j]; @@ -471,7 +472,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_CONTROL || e->pitch > 127 || e->velocity > 127 || e->channel > 15 || + e->type > XTC_MIDI_BEND || e->pitch > 127 || e->velocity > 127 || e->channel > 15 || (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT; } diff --git a/src/native/processor.c b/src/native/processor.c index 3509d1b..353e16c 100644 --- a/src/native/processor.c +++ b/src/native/processor.c @@ -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_CONTROL || e->pitch > 127 || e->velocity > 127 || e->channel > 15 || + e->type > XTC_MIDI_BEND || 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; diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index 5f516ac..3a0ff7a 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -462,6 +462,26 @@ static xtc_result read_project(reader *input, xtc_project *project, const char * READ("MODULE",2U); if (!decode_bytes(input->tokens[1],s->path,sizeof(s->path))) return XTC_ERR_FORMAT; READ("IDENTITY",2U); if (strcmp(input->tokens[1],"-") && !decode_bytes(input->tokens[1],s->identifier,sizeof(s->identifier))) return XTC_ERR_FORMAT; + if (format_version >= 14U) { + size_t k; + READ("INSTRUMENT_CONTROLS",2U);NUM(1U,value); + if(value>XTC_PLUGIN_CONTROLS)return XTC_ERR_FORMAT; + s->control_count=(unsigned int)value; + for(k=0;kcontrol_count;++k) { + READ("CONTROL",3U);NUM(1U,value);if(value>=64)return XTC_ERR_FORMAT; + s->ports[k]=(unsigned int)value; + if(!decode_float(input->tokens[2],s->values+k))return XTC_ERR_FORMAT; + } + READ("INSTRUMENT_AUTOMATION",2U);NUM(1U,value); + if(value>XTC_PLUGIN_POINTS)return XTC_ERR_FORMAT; + s->point_count=(unsigned int)value; + for(k=0;kpoint_count;++k) { + xtc_plugin_point *a=s->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(s) != XTC_OK) return XTC_ERR_FORMAT; } READ("LIVE_INSTRUMENT",2U); NUM(1U,project->live_instrument); @@ -621,6 +641,19 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch if (fprintf(file,"INSTRUMENT %" PRIu64 " %u %u %u ",project->tracks[i].id,(unsigned int)s->format,s->index,s->unique_id) < 0 || !write_name(file,s->label) || fputs("MODULE ",file) == EOF || !write_name(file,s->path) || fputs("IDENTITY ",file) == EOF || (s->identifier[0] ? !write_name(file,s->identifier) : fputs("-\n",file) == EOF)) return XTC_ERR_IO; + { + size_t k; + if(fprintf(file,"INSTRUMENT_CONTROLS %u\n",s->control_count)<0)return XTC_ERR_IO; + for(k=0;kcontrol_count;++k) { + uint32_t bits;memcpy(&bits,s->values+k,4); + if(fprintf(file,"CONTROL %u %08" PRIx32 "\n",s->ports[k],bits)<0)return XTC_ERR_IO; + } + if(fprintf(file,"INSTRUMENT_AUTOMATION %u\n",s->point_count)<0)return XTC_ERR_IO; + for(k=0;kpoint_count;++k) { + uint32_t bits;memcpy(&bits,&s->points[k].value,4); + if(fprintf(file,"PARAM %" PRIu64 " %u %u %08" PRIx32 "\n",s->points[k].tick,s->points[k].port,s->points[k].step,bits)<0)return XTC_ERR_IO; + } + } } if (fprintf(file,"LIVE_INSTRUMENT %" PRIu64 "\n",project->live_instrument) < 0) return XTC_ERR_IO; } diff --git a/src/plugin/builtin.c b/src/plugin/builtin.c index a7c9853..1eaa4c9 100644 --- a/src/plugin/builtin.c +++ b/src/plugin/builtin.c @@ -11,7 +11,12 @@ static const char *controls[2][3] = {{"Time", "Feedback", "Mix"}, {"Decay", "Dam static const float defaults[2][3] = {{350, .35f, .25f}, {.7f, .45f, .25f}}; static const float lows[2][3] = {{1,0,0},{0,0,0}}; static const float highs[2][3] = {{2000,.95f,1},{.95f,.95f,1}}; -typedef struct { double phase, step; float level, velocity; unsigned int note, channel; int active, released, held; } voice; +static const char *synth_names[]={"Harmonic", "Attack", "Release", "Cutoff", "Bend range"}; +static const float synth_low[]={0, .1f, 1, 20, 0}; +static const float synth_high[]={1, 2000, 5000, 24000, 24}; +static float synth_default(unsigned int id,unsigned int port) +{ static const float d[]={.3f,8,80,24000,2};return port==0 && id==1 ? 0 : d[port]; } +typedef struct { double phase, step; float level, velocity, filter; unsigned int note, channel; int active, released, held; } voice; struct xtc_builtin { xtc_plugin_spec spec; double rate; @@ -22,6 +27,7 @@ struct xtc_builtin { voice voices[32]; double frequencies[128]; int sustain[16]; + float bend[16]; }; xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out) { @@ -31,8 +37,8 @@ xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out) strcpy(s.path,XTC_BUILTIN_PATH); strcpy(s.label,names[index]); s.format = XTC_PLUGIN_BUILTIN; s.index = index; s.unique_id = 0x58544300U+index; s.instrument = index < 2; s.inputs = index < 2 ? 0U : 1U; s.tempo = 120000; - s.control_count = index < 2 ? 0U : 3U; - for (i=0;iindex,&a)!=XTC_OK || strcmp(s->path,a.path) || strcmp(s->label,a.label) || s->identifier[0] || s->unique_id!=a.unique_id || s->instrument!=a.instrument || s->inputs!=a.inputs || - s->control_count!=a.control_count) return XTC_ERR_INVALID_ARGUMENT; + (s->control_count!=a.control_count && !(s->index<2 && !s->control_count))) return XTC_ERR_INVALID_ARGUMENT; for(i=0;icontrol_count;++i) if(s->ports[i]!=i || !isfinite(s->values[i]) || - s->values[i]index-2][i] || s->values[i]>highs[s->index-2][i]) return XTC_ERR_INVALID_ARGUMENT; - for(i=0;ipoint_count;++i) if(s->index<2 || s->points[i].port>=3 || !isfinite(s->points[i].value) || - s->points[i].valueindex-2][s->points[i].port] || s->points[i].value>highs[s->index-2][s->points[i].port]) return XTC_ERR_INVALID_ARGUMENT; + s->values[i]<(s->index<2 ? synth_low[i] : lows[s->index-2][i]) || s->values[i]>(s->index<2 ? synth_high[i] : highs[s->index-2][i])) return XTC_ERR_INVALID_ARGUMENT; + for(i=0;ipoint_count;++i) if(s->points[i].port>=s->control_count || !isfinite(s->points[i].value) || + s->points[i].value<(s->index<2 ? synth_low[s->points[i].port] : lows[s->index-2][s->points[i].port]) || + s->points[i].value>(s->index<2 ? synth_high[s->points[i].port] : highs[s->index-2][s->points[i].port])) return XTC_ERR_INVALID_ARGUMENT; return XTC_OK; } xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *out) @@ -55,8 +62,9 @@ xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *ou strcpy(out->maker,"xtc"); strcpy(out->copyright,"See xtc LICENSE"); out->hard_realtime=1; for(i=0;icontrol_count;++i) { xtc_parameter_info *p=out->parameters+i; p->id=i; - strcpy(p->name,controls[s->index-2][i]); if(s->index==2 && i==0) strcpy(p->unit,"ms"); - p->minimum=lows[s->index-2][i]; p->maximum=highs[s->index-2][i]; p->default_value=defaults[s->index-2][i]; + strcpy(p->name,s->index<2 ? synth_names[i] : controls[s->index-2][i]); + if(s->index<2) {strcpy(p->unit,i==1 || i==2 ? "ms" : i==3 ? "Hz" : i==4 ? "st" : "");} if(s->index==2 && i==0) strcpy(p->unit,"ms"); + p->minimum=s->index<2 ? synth_low[i] : lows[s->index-2][i]; p->maximum=s->index<2 ? synth_high[i] : highs[s->index-2][i]; p->default_value=s->index<2 ? synth_default(s->index,i) : defaults[s->index-2][i]; out->flags[i]=XTC_PLUGIN_BOUNDED_BELOW|XTC_PLUGIN_BOUNDED_ABOVE; } return XTC_OK; @@ -82,7 +90,7 @@ static float parameter(const xtc_builtin *p, unsigned int port, uint64_t positio if((double)q->tick<=tick) { if(!a || q->tick>a->tick) a=q; } else if(!b || q->ticktick) b=q; } - if(!a) return b ? b->value : p->spec.values[port]; + if(!a) return b ? b->value : p->spec.control_count ? p->spec.values[port] : synth_default(p->spec.index,port); if(!b || a->step) return a->value; return (float)(a->value+(b->value-a->value)*(tick-(double)a->tick)/(double)(b->tick-a->tick)); } @@ -112,7 +120,8 @@ xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size static void midi_event(xtc_builtin *p, const xtc_midi_event *e) { unsigned int j,slot=0; - if(e->type==XTC_MIDI_ALL_OFF) { memset(p->voices,0,sizeof(p->voices));memset(p->sustain,0,sizeof(p->sustain));return; } + if(e->type==XTC_MIDI_ALL_OFF) { for(j=0;j<32;++j)if(p->voices[j].channel==e->channel)p->voices[j].active=0;p->sustain[e->channel]=0;p->bend[e->channel]=0;return; } + if(e->type==XTC_MIDI_BEND) {p->bend[e->channel]=((float)(e->pitch+128*e->velocity)-8192)/8192;return;} if(e->type==XTC_MIDI_CONTROL) { if(e->pitch==64) {p->sustain[e->channel]=e->velocity>=64; if(e->velocity<64) for(j=0;j<32;++j) if(p->voices[j].channel==e->channel && p->voices[j].held) p->voices[j].released=1;} if(e->pitch==120 || e->pitch==123) for(j=0;j<32;++j) if(p->voices[j].channel==e->channel) p->voices[j].active=0; @@ -127,23 +136,31 @@ static void midi_event(xtc_builtin *p, const xtc_midi_event *e) for(j=0;j<32;++j) { if(!p->voices[j].active) {slot=j;break;} if(p->voices[j].levelvoices[slot].level) slot=j; } {voice *v=p->voices+slot; memset(v,0,sizeof(*v));v->active=1;v->note=e->pitch;v->channel=e->channel;v->velocity=(float)e->velocity/127;v->step=p->frequencies[e->pitch];} } -xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames) +xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames, uint64_t position) { size_t i,k=0; unsigned int j; if(!p || !p->spec.instrument || !out || frames>p->max_frames || count>256 || (count && !events)) return XTC_ERR_INVALID_ARGUMENT; for(i=0;i=frames || (i && events[i].offset15 || events[i].pitch>127 || events[i].velocity>127 || events[i].typeXTC_MIDI_CONTROL) return XTC_ERR_INVALID_ARGUMENT; + events[i].channel>15 || events[i].pitch>127 || events[i].velocity>127 || events[i].typeXTC_MIDI_BEND) return XTC_ERR_INVALID_ARGUMENT; for(i=0;ivoices+j; double wave; + float sum=0; float harmonic=parameter(p,0,position+i), attack=parameter(p,1,position+i), release=parameter(p,2,position+i); + float cutoff=parameter(p,3,position+i), range=parameter(p,4,position+i); + float attack_step=attack==8 ? p->attack : (float)(1-exp(-1/(attack*.001*p->rate))); + float release_step=release==80 ? p->release : (float)exp(-1/(release*.001*p->rate)); + float filter_step=(float)(1-exp(-TAU*cutoff/p->rate)); + while(kvoices+j; double wave,step; if(!v->active) continue; - if(v->released) v->level *= p->release; - else v->level += (1-v->level)*p->attack; + if(v->released) v->level *= release_step; + else v->level += (1-v->level)*attack_step; if(v->released && v->level<.00001f) {v->active=0;continue;} - wave=v->step<.49 ? sin(TAU*v->phase) : 0; - if(p->spec.index==0 && v->step<.16) wave+=.3*sin(3*TAU*v->phase); + step=v->step*pow(2,p->bend[v->channel]*range/12); + wave=step<.49 ? sin(TAU*v->phase) : 0; + if(step<.16) wave+=(harmonic==.3f ? .3 : (double)harmonic)*sin(3*TAU*v->phase); + v->filter += filter_step*((float)wave-v->filter); + if(cutoff<24000) wave=v->filter; sum+=(float)wave*v->level*v->velocity*.12f; - v->phase+=v->step; v->phase-=floor(v->phase); + v->phase+=step; v->phase-=floor(v->phase); } out[i]=sum; } return XTC_OK; } diff --git a/src/plugin/builtin.h b/src/plugin/builtin.h index b2744a9..9680b72 100644 --- a/src/plugin/builtin.h +++ b/src/plugin/builtin.h @@ -8,5 +8,5 @@ xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *ou xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_builtin **out); void xtc_builtin_destroy(xtc_builtin *p); xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size_t frames, uint64_t position); -xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames); +xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames, uint64_t position); #endif diff --git a/src/plugin/host.c b/src/plugin/host.c index 18bba3b..852522e 100644 --- a/src/plugin/host.c +++ b/src/plugin/host.c @@ -81,8 +81,16 @@ xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *in, float *out, siz if (r == XTC_OK) p->next = position <= UINT64_MAX-frames ? position+frames : 0; return r; } +xtc_result xtc_plugin_process_midi_at(xtc_plugin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames, uint64_t position) +{ + xtc_result r; + if (!p || position>UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT; + r=p->builtin ? xtc_builtin_midi(p->builtin,events,count,out,frames,position) : p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames) : XTC_ERR_UNSUPPORTED; + if(r==XTC_OK)p->next=position+frames; + return r; +} xtc_result xtc_plugin_process_midi(xtc_plugin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames) -{ return p && p->builtin ? xtc_builtin_midi(p->builtin,events,count,out,frames) : p && p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames) : XTC_ERR_UNSUPPORTED; } +{ return xtc_plugin_process_midi_at(p,events,count,out,frames,p ? p->next : 0); } xtc_result xtc_plugin_process(xtc_plugin *p, const float *in, float *out, size_t frames) { return xtc_plugin_process_at(p,in,out,frames,p ? p->next : 0); } xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS]) @@ -114,3 +122,28 @@ xtc_result xtc_plugin_reset(xtc_plugin_spec *s, double rate, unsigned int port) *s = candidate; return XTC_OK; } + +xtc_result xtc_plugin_preset_save(const xtc_plugin_spec *spec, const char *path) +{ + xtc_project *project=NULL;uint64_t track;xtc_result r; + if(!path || xtc_plugin_spec_validate(spec)!=XTC_OK || !spec->instrument)return XTC_ERR_INVALID_ARGUMENT; + r=xtc_project_create(&project); + if(r==XTC_OK)r=xtc_project_set_tempo(project,spec->tempo); + if(r==XTC_OK)r=xtc_project_add_track(project,"Instrument preset",&track); + if(r==XTC_OK)r=xtc_project_set_instrument(project,track,spec); + if(r==XTC_OK)r=xtc_project_save(project,path); + xtc_project_destroy(project);return r; +} +xtc_result xtc_plugin_preset_load(const char *path, xtc_plugin_spec *out) +{ + xtc_project *project=NULL;xtc_track_info track;xtc_plugin_spec spec;size_t tracks,clips;xtc_result r; + if(!path || !out)return XTC_ERR_INVALID_ARGUMENT; + r=xtc_project_create(&project); + if(r==XTC_OK)r=xtc_project_load(project,path); + if(r==XTC_OK)r=xtc_project_get_counts(project,&tracks,&clips); + if(r==XTC_OK && (tracks!=1 || clips))r=XTC_ERR_FORMAT; + if(r==XTC_OK)r=xtc_project_get_track(project,0,&track); + if(r==XTC_OK)r=xtc_project_get_instrument(project,track.id,&spec); + if(r==XTC_OK)*out=spec; + xtc_project_destroy(project);return r; +} diff --git a/src/plugin/modern.c b/src/plugin/modern.c index 63e0906..60eeaf6 100644 --- a/src/plugin/modern.c +++ b/src/plugin/modern.c @@ -391,13 +391,13 @@ xtc_result xtc_modern_process_midi(xtc_modern *p, const xtc_midi_event *events, 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->channel > 15 || e->pitch > 127 || e->velocity > 127 || - e->type < XTC_MIDI_NOTE_ON || e->type > XTC_MIDI_CONTROL || (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT; + e->type < XTC_MIDI_NOTE_ON || e->type > XTC_MIDI_BEND || (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT; } p->event_count = count; p->vst_count = 0; p->clap_count = 0; p->event_list.api.lpVtbl = &event_vtable; p->event_list.owner = p; for (i = 0; i < count; ++i) { const xtc_midi_event *e = events+i; - unsigned char status = (unsigned char)((e->type == XTC_MIDI_NOTE_ON ? 144 : e->type == XTC_MIDI_NOTE_OFF ? 128 : 176) | e->channel); + unsigned char status = (unsigned char)((e->type == XTC_MIDI_NOTE_ON ? 144 : e->type == XTC_MIDI_NOTE_OFF ? 128 : e->type == XTC_MIDI_BEND ? 224 : 176) | e->channel); unsigned char key = (unsigned char)(e->type == XTC_MIDI_ALL_OFF ? 120 : e->pitch); unsigned char value = (unsigned char)(e->type == XTC_MIDI_ALL_OFF ? 0 : e->velocity); clap_note *c = p->clap_events+p->clap_count; memset(c,0,sizeof(*c)); @@ -410,9 +410,9 @@ xtc_result xtc_modern_process_midi(xtc_modern *p, const xtc_midi_event *events, c->note.note_id = -1; c->note.port_index = 0; c->note.channel = (int16_t)e->channel; c->note.key = e->type == XTC_MIDI_ALL_OFF ? -1 : (int16_t)e->pitch; c->note.velocity = (double)e->velocity/127; /* No generic controller mapping in native-note CLAP or VST3. */ - if (e->type == XTC_MIDI_CONTROL) c->note.header.type = UINT16_MAX; + if (e->type >= XTC_MIDI_CONTROL) c->note.header.type = UINT16_MAX; } - if (p->clap_midi || e->type != XTC_MIDI_CONTROL) ++p->clap_count; + if (p->clap_midi || e->type < XTC_MIDI_CONTROL) ++p->clap_count; memset(p->v2_events+i,0,sizeof(*p->v2_events)); p->v2_events[i].type = 1; p->v2_events[i].bytes = sizeof(xtc_v2_midi); p->v2_events[i].offset = (int32_t)e->offset; p->v2_events[i].data[0] = status; p->v2_events[i].data[1] = key; p->v2_events[i].data[2] = value; diff --git a/src/project/project.c b/src/project/project.c index fcd7f2c..cd03b56 100644 --- a/src/project/project.c +++ b/src/project/project.c @@ -466,6 +466,7 @@ 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;itrack_count;++i)if(project->instruments[i].instrument)project->instruments[i].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; @@ -745,7 +746,7 @@ xtc_result xtc_project_set_instrument(xtc_project *p, uint64_t track, const xtc_ r = xtc_project_clone(p,&candidate); if (r == XTC_OK && spec) r = xtc_project_ensure_midi_route(candidate); if (r == XTC_OK) { - if (spec) { candidate->instruments[i] = *spec; candidate->live_instrument = track; } + if (spec) { candidate->instruments[i] = *spec; candidate->instruments[i].tempo=p->tempo; candidate->live_instrument = track; } else { memset(candidate->instruments+i,0,sizeof(*candidate->instruments)); if (candidate->live_instrument == track) candidate->live_instrument = UINT64_MAX; } { xtc_graph *old = p->graph; *p = *candidate; candidate->graph = old; } diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index f352a73..8518c06 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -723,6 +723,13 @@ int main(int argc, char **argv) xtc_plugin_spec spec{}; CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==instrument); CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK); + spec.values[1]=25;spec.point_count=1;spec.points[0]={100,0,0,.4f}; + CHECK(window.set_instrument_spec(track,spec)==XTC_OK); + CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK); + CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.values[1]==25 && spec.point_count==1); + auto *bar=dynamic_cast(window.child(0));CHECK(bar); + Fl::add_timeout(.05,[](void *) {press(find(Fl::first_window(),"Apply"));}); + bar->picked(bar->find_item("Edit/Selected instrument controls")); xtc_connection_info wire{}; CHECK(xtc_graph_get_connection(xtc_project_graph(sequence->project()),0,&wire)==XTC_OK); unsigned int effect=instrument+2;size_t id=SIZE_MAX; diff --git a/tests/integration/test_install.py b/tests/integration/test_install.py index 0b0c5e0..7f65f4e 100644 --- a/tests/integration/test_install.py +++ b/tests/integration/test_install.py @@ -56,7 +56,7 @@ def main(): run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')]) assert (docs / 'WORKSPACE.md').is_file() diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True) - assert 'Project formats: 1..13' in diagnose and 'JACK compiled:' in diagnose + assert 'Project formats: 1..14' in diagnose and 'JACK compiled:' in diagnose # Replacing a file symlink must not overwrite an unrelated target. protected = stage / 'unrelated-target' protected.write_text('keep unrelated target\n') diff --git a/tests/integration/test_project_inspection.py b/tests/integration/test_project_inspection.py index 17efa39..6c225ff 100644 --- a/tests/integration/test_project_inspection.py +++ b/tests/integration/test_project_inspection.py @@ -84,7 +84,7 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory: project.write_text(malformed) check("--validate", project, 1) for text, messages in [ - (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..13")), + (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..14")), ("XTC_PROJECT 10\n", ("line 2", "APP")), (base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")), (base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)), diff --git a/tests/unit/test_builtin.c b/tests/unit/test_builtin.c index 480ab5c..0dacb99 100644 --- a/tests/unit/test_builtin.c +++ b/tests/unit/test_builtin.c @@ -147,7 +147,47 @@ static void transport(unsigned int id) assert(!memcmp(x,y,sizeof(x))); xtc_midi_plan_destroy(plan);xtc_midi_plan_destroy(fresh);xtc_project_destroy(project); } +static void editable(void) +{ + xtc_plugin_spec s,legacy,t;xtc_plugin *a=NULL,*b=NULL;float x[256],y[256];size_t k; + xtc_midi_event events[]={{0,XTC_MIDI_NOTE_ON,0,60,100},{64,XTC_MIDI_BEND,0,0,127}}; + char path[]="/tmp/xtc-preset-XXXXXX";int fd=mkstemp(path);assert(fd>=0);close(fd); + assert(xtc_plugin_probe(XTC_BUILTIN_PATH,0,48000,&s)==XTC_OK && s.control_count==5); + legacy=s;legacy.control_count=0; + assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && xtc_plugin_create(&legacy,48000,256,&b)==XTC_OK); + assert(xtc_plugin_process_midi(a,events,1,x,256)==XTC_OK && xtc_plugin_process_midi(b,events,1,y,256)==XTC_OK); + assert(!memcmp(x,y,sizeof(x)));xtc_plugin_destroy(a);xtc_plugin_destroy(b); + assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && xtc_plugin_create(&s,48000,256,&b)==XTC_OK); + assert(xtc_plugin_process_midi(a,events,2,x,256)==XTC_OK && xtc_plugin_process_midi(b,events,1,y,256)==XTC_OK); + assert(!memcmp(x,y,64*sizeof(float)) && memcmp(x+65,y+65,191*sizeof(float))); + xtc_plugin_destroy(a);xtc_plugin_destroy(b); + /* Migrate an actual v13 fixed-preset binding; no new records existed there. */ + assert(xtc_plugin_preset_save(&legacy,path)==XTC_OK); + {FILE *f=fopen(path,"r");char lines[32768],line[2048];size_t used=0;assert(f); + while(fgets(line,sizeof(line),f)) { + if(!strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue; + if(!strncmp(line,"XTC_PROJECT 14",14))strcpy(line,"XTC_PROJECT 13\n"); + assert(used+strlen(line)