feat: add editable internal instruments, automation and versioned presets

This commit is contained in:
Johannes Findeisen 2026-11-05 23:47:54 +01:00
commit 159fa939a1
21 changed files with 295 additions and 52 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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.
---

View file

@ -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.
*

View file

@ -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. */

View file

@ -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

View file

@ -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;i<spec.control_count;++i)prompt+=std::to_string(i)+": "+info[i].name+"\n";
if(r==XTC_OK) {
const char *answer=fl_input("%s","",prompt.c_str());if(!answer)return;
unsigned int 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 {
unsigned int i=0;for(;i<spec.point_count;++i)if(spec.points[i].port==port && spec.points[i].tick==tick)break;
if(mode== -1) {if(i==spec.point_count)r=XTC_ERR_NOT_FOUND;else {for(unsigned int j=i+1;j<spec.point_count;++j)spec.points[j-1]=spec.points[j];--spec.point_count;}}
else if(i==XTC_PLUGIN_POINTS)r=XTC_ERR_CAPACITY;
else {if(i==spec.point_count)++spec.point_count;spec.points[i]={static_cast<uint64_t>(tick),port,static_cast<unsigned int>(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);

View file

@ -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]);

View file

@ -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;
}

View file

@ -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;

View file

@ -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;k<s->control_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;k<s->point_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;k<s->control_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;k<s->point_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;
}

View file

@ -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;i<s.control_count;++i) { s.ports[i]=i; s.values[i]=defaults[index-2][i]; }
s.control_count = index < 2 ? 5U : 3U;
for (i=0;i<s.control_count;++i) { s.ports[i]=i; s.values[i]=index<2 ? synth_default(index,i) : defaults[index-2][i]; }
*out=s; return XTC_OK;
}
xtc_result xtc_builtin_validate(const xtc_plugin_spec *s)
@ -40,11 +46,12 @@ xtc_result xtc_builtin_validate(const xtc_plugin_spec *s)
xtc_plugin_spec a; unsigned int i;
if (!s || xtc_builtin_probe(s->index,&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;i<s->control_count;++i) if(s->ports[i]!=i || !isfinite(s->values[i]) ||
s->values[i]<lows[s->index-2][i] || s->values[i]>highs[s->index-2][i]) return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->point_count;++i) if(s->index<2 || s->points[i].port>=3 || !isfinite(s->points[i].value) ||
s->points[i].value<lows[s->index-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;i<s->point_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;i<s->control_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->tick<b->tick) 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].level<p->voices[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<count;++i) if(events[i].offset>=frames || (i && events[i].offset<events[i-1].offset) ||
events[i].channel>15 || events[i].pitch>127 || events[i].velocity>127 || events[i].type<XTC_MIDI_NOTE_ON || events[i].type>XTC_MIDI_CONTROL) return XTC_ERR_INVALID_ARGUMENT;
events[i].channel>15 || events[i].pitch>127 || events[i].velocity>127 || events[i].type<XTC_MIDI_NOTE_ON || events[i].type>XTC_MIDI_BEND) return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<frames;++i) {
float sum=0; while(k<count && events[k].offset==i) midi_event(p,events+k++);
for(j=0;j<32;++j) { voice *v=p->voices+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(k<count && events[k].offset==i) midi_event(p,events+k++);
for(j=0;j<32;++j) { voice *v=p->voices+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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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;i<project->track_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; }

View file

@ -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<Fl_Menu_Bar *>(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;

View file

@ -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')

View file

@ -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",)),

View file

@ -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)<sizeof(lines));strcpy(lines+used,line);used+=strlen(line);
}fclose(f);f=fopen(path,"w");assert(f && fwrite(lines,1,used,f)==used);fclose(f);}
assert(xtc_plugin_preset_load(path,&t)==XTC_OK && !t.control_count && t.index==0);
s.values[3]=700;s.values[1]=100;s.values[2]=400;
s.point_count=2;s.points[0]=(xtc_plugin_point){0,0,0,0};s.points[1]=(xtc_plugin_point){100,0,0,1};
assert(xtc_plugin_preset_save(&s,path)==XTC_OK && xtc_plugin_preset_load(path,&t)==XTC_OK);
assert(!memcmp(s.values,t.values,sizeof(s.values)) && t.point_count==2);
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && xtc_plugin_create(&t,48000,256,&b)==XTC_OK);
for(k=0;k<200;++k) {
assert(xtc_plugin_process_midi(a,events,k==0 ? 2 : 0,x,256)==XTC_OK);
assert(xtc_plugin_process_midi(b,events,k==0 ? 2 : 0,y,128)==XTC_OK);
assert(xtc_plugin_process_midi(b,NULL,0,y+128,128)==XTC_OK);
assert(!memcmp(x,y,sizeof(x)));
}
xtc_plugin_destroy(a);xtc_plugin_destroy(b);
s.values[3]=NAN;assert(xtc_plugin_spec_validate(&s)!=XTC_OK);
{unsigned char bend[]={0xe3,0,64};xtc_midi_event e;
assert(xtc_midi_decode(bend,3,7,&e)==XTC_OK && e.type==XTC_MIDI_BEND && e.channel==3 && e.velocity==64);}
unlink(path);
}
int main(void) {
transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
synth_partition(0);synth_partition(1);puts("Built-in devices passed.");return 0;
}

View file

@ -68,7 +68,7 @@ int main(void)
assert(xtc_project_add_track(old,"Old",&id) == XTC_OK && xtc_project_add_track(next,"New",&id) == XTC_OK);
assert(xtc_project_save(old,song) == XTC_OK); size = bytes(song,before);
assert(xtc_project_load_diagnostic(loaded,song,&diagnostic) == XTC_OK && diagnostic.result == XTC_OK &&
diagnostic.format_version == 13 && !diagnostic.line);
diagnostic.format_version == XTC_PROJECT_FORMAT_VERSION && !diagnostic.line);
for (mode = 1; mode <= 5; ++mode) {
fault = mode;
assert(xtc_project_save(next,song) == XTC_ERR_IO);
@ -81,7 +81,7 @@ int main(void)
}
write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..13"));
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..14"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO"));