feat: add editable internal instruments, automation and versioned presets
This commit is contained in:
parent
e76552e8dd
commit
159fa939a1
21 changed files with 295 additions and 52 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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",)),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue