feat: add a bounded WAV sampler with looping, relinking and saved presets

This commit is contained in:
Johannes Findeisen 2026-11-05 23:50:14 +01:00
commit 35bf6a00a1
24 changed files with 213 additions and 32 deletions

View file

@ -90,3 +90,34 @@ 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.
## Phase 42 sample instrument
**File → Load sampler WAV** assigns a Sample Player to the selected track or
relinks its existing sample while retaining its controls. Use the instrument
controls editor for root MIDI key (0..127), whole-sample loop (0/1), attack,
release and gain (0..4). One root mapping covers every MIDI key using linear
rate conversion, including source/output sample-rate differences. Loop and root
are integer controls and require hold automation. Live bend spans ±2 semitones.
This initial sampler has no multisample zones, loop-point editor or time stretch.
Extreme transposition can alias; use appropriate source material.
The existing WAV reader supports PCM16/24/32 and float32, mono/stereo downmixed
to mono. Each sample is at most 16M mono frames; the combined instrument plan
budget is 64M frames (256 MiB). Files are at most 256 MiB, with 4096 chunks.
There are 32 preallocated voices per track. Loading and freeing happen only on
the control thread; playback interpolates cached samples without I/O.
Missing/invalid samples retain their references and play silence, reported in
backend asset-issue counts, GUI preparation warnings and `xtc-check --inspect`.
Relink with Load sampler WAV; it is one undoable prepared edit. Allocation and
capacity failures reject preparation. Format 15 adds a SAMPLE reference after
each instrument's controls/automation; in-project samples save relatively and
resolve relative to the project file. Other samples retain absolute paths.
Preset files include the reference but do not embed/copy audio.
`examples/projects/original-pluck.wav` is an original synthesized example with
license/provenance in SAMPLE_LICENSE.md. Both are installed with the examples.
Tests cover source-rate conversion, one-shot endpoint, looping, release,
partitioning, preset references, missing assets, GUI relinking/undo/reload and
preparation-allocation/callback guards.

View file

@ -1,4 +1,4 @@
# Project format v14 (reads v1–v13)
# Project format v15 (reads v1–v14)
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 v14. Unknown versions are rejected without replacing the destination; older
Saving always writes v15. 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–14 and the writer always emits version 14.
The current reader accepts versions 1–15 and the writer always emits version 15.
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
@ -407,3 +407,14 @@ 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.
## Version 15 sample references
Each instrument parameter block ends with `SAMPLE hex_path_or_dash`. Only the
internal Sample Player (index 4) may carry a nonempty sample path. Paths within
the project directory are relative on disk and resolve against the project
file's directory; other paths are absolute. Metadata loading does not read WAVs.
Missing/invalid samples remain references and become reported silence during
preparation. Controls describe root key, whole-file loop, attack, release and
gain. Earlier projects have empty sample paths. Rebuild public API consumers
for the additional copied `sample_path` field in `xtc_plugin_spec`.

View file

@ -1195,14 +1195,19 @@ pitch-bend offset regressions. See BUILTIN_DEVICES.md and PROJECT_FORMAT.md.
Provide sample-based sounds beyond the current oscillator presets.
### Tasks and acceptance criteria
- [ ] Define supported sample formats, mapping, looping, envelopes, voice limits and memory budgets.
- [ ] Load/prepare samples outside callbacks and expose an instrument editor with undo.
- [ ] Persist sample references and mappings with missing-media reporting and relinking.
- [ ] Provide original or appropriately licensed example sounds and document their licenses.
- [ ] Test playback rate conversion, boundaries, voice release, missing assets and project reload.
- [x] Define supported sample formats, mapping, looping, envelopes, voice limits and memory budgets.
- [x] Load/prepare samples outside callbacks and expose an instrument editor with undo.
- [x] Persist sample references and mappings with missing-media reporting and relinking.
- [x] Provide original or appropriately licensed example sounds and document their licenses.
- [x] Test playback rate conversion, boundaries, voice release, missing assets and project reload.
### Implementation status
Planned. Phase 38 supplies no sample instrument or sample library.
Implemented: a 32-voice, single-root WAV sampler with whole-file looping,
envelopes, gain, cached linear rate conversion and an undoable editor/relink
workflow. Format 15 persists relocatable sample references and reads v1–v14.
Missing/invalid media becomes reported silence. Original licensed example and
core, GUI, persistence, release/loop and allocation/callback tests are included.
See BUILTIN_DEVICES.md for memory/format and transposition limits.
---

View file

@ -0,0 +1,7 @@
# Original sampler example
`original-pluck.wav` was synthesized specifically for xtc using an original
exponentially decaying sine/second-harmonic formula. It contains no recordings
or third-party samples. It is distributed under the repository LICENSE.
Load it with File / Load sampler WAV, then play MIDI at root key 60.

Binary file not shown.

View file

@ -156,6 +156,7 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int sample_
*
* @param plan Instrument plan and its single-owner runtime state.
*/
size_t xtc_midi_plan_asset_issues(const xtc_midi_plan *plan);
void xtc_midi_plan_destroy(xtc_midi_plan *plan);
/* Set before publication, while quiescent. Zero preserves raw scheduled notes.
* Linear boundary envelopes do not delay note scheduling; live note-offs and

View file

@ -21,6 +21,7 @@ typedef struct { uint64_t tick; unsigned int port, step; float value; } xtc_plug
typedef struct {
char path[512], label[64];
xtc_plugin_format format;
char sample_path[512]; /* Internal sampler WAV reference; empty for other devices. */
char identifier[128]; /* CLAP stable ID / VST3 class hex; empty for LADSPA/VST2. */
unsigned int index, unique_id, inputs, control_count;
unsigned int instrument; /* Zero-input note instrument; track binding only. */
@ -68,6 +69,8 @@ xtc_result xtc_plugin_parameters(const xtc_plugin_spec *spec, double rate, xtc_p
/* Metadata-only validation: never opens libraries. */
xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *spec);
xtc_result xtc_plugin_create(const xtc_plugin_spec *spec, double rate, size_t frames, xtc_plugin **out);
size_t xtc_plugin_sample_frames(const xtc_plugin *plugin);
size_t xtc_plugin_asset_issues(const xtc_plugin *plugin);
void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */
/* Single audio owner. No loading/allocation/I/O in host process; trusted run()
* must obey realtime rules. No MIDI; max one mono input and one mono output.

View file

@ -7,7 +7,7 @@
extern "C" {
#endif
#define XTC_PROJECT_FORMAT_VERSION 14U
#define XTC_PROJECT_FORMAT_VERSION 15U
#define XTC_PROJECT_MAX_TRACKS 64U
#define XTC_PROJECT_MAX_CLIPS 1024U
#define XTC_PROJECT_NAME_BYTES 64U

View file

@ -584,6 +584,7 @@ const char *xtc_backend_asset_issue(const xtc_backend *backend)
const char *path = "";
if (backend)
xtc_audio_plan_issues(backend->audio, &path);
if(backend && !path[0] && xtc_midi_plan_asset_issues(backend->midi))return "Sampler WAV missing/invalid (File/Load sampler WAV to relink)";
return path;
}
@ -705,7 +706,7 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status
backend->running ? (float)__atomic_load_n(&backend->peak_right, __ATOMIC_RELAXED) / 1000000.0f : 0;
out_status->stopping = (int)__atomic_load_n(&backend->stop_requested, __ATOMIC_ACQUIRE);
out_status->midi_dropped = __atomic_load_n(&backend->midi_dropped, __ATOMIC_RELAXED);
out_status->asset_issues = (unsigned int)xtc_audio_plan_issues(backend->audio, NULL);
out_status->asset_issues = (unsigned int)(xtc_audio_plan_issues(backend->audio, NULL)+xtc_midi_plan_asset_issues(backend->midi));
out_status->sample_rate = __atomic_load_n(&backend->rate, __ATOMIC_RELAXED);
out_status->buffer_frames = __atomic_load_n(&backend->frames, __ATOMIC_RELAXED);
out_status->xruns = __atomic_load_n(&backend->xruns, __ATOMIC_RELAXED);

View file

@ -130,6 +130,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
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/Load sampler WAV",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);
@ -717,7 +718,7 @@ void MainWindow::start_engine()
if (issue[0]) {
char message[640];
std::snprintf(message, sizeof(message),
"Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", issue);
"Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.", issue);
set_label(error_, message);
}
}
@ -1177,6 +1178,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
xtc_midi_plan *offline_midi = nullptr;
if (r == XTC_OK && !ready_)
r = xtc_midi_plan_create(candidate, static_cast<unsigned int>(settings_.offline_rate), &offline_midi);
size_t sample_issues=xtc_midi_plan_asset_issues(offline_midi);
xtc_midi_plan_destroy(offline_midi);
if (r == XTC_OK && !ready_) {
xtc_plan *graph = nullptr;
@ -1195,6 +1197,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
issue = path;
xtc_audio_plan_destroy(offline);
}
if(sample_issues)issue="Sampler WAV missing/invalid; File/Load sampler WAV relinks the selected track";
if (piano_window_)
piano_window_->hide();
if (piano_)
@ -1211,7 +1214,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
dirty_ = false;
if (!issue.empty()) {
char text[640];
std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.",
std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.",
issue.c_str());
set_label(error_, text);
} else
@ -1854,6 +1857,14 @@ 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::load_sampler(uint64_t track,const char *path)
{
if(!path || path[0]!='/' || std::strlen(path)>=sizeof(xtc_plugin_spec{}.sample_path))return XTC_ERR_INVALID_ARGUMENT;
xtc_plugin_spec spec{};xtc_result r=xtc_project_get_instrument(project_.get(),track,&spec);
if(r!=XTC_OK || spec.format!=XTC_PLUGIN_BUILTIN || spec.index!=4)r=xtc_plugin_probe(XTC_BUILTIN_PATH,4,sample_rate(),&spec);
if(r!=XTC_OK)return r;
std::strcpy(spec.sample_path,path);return set_instrument_spec(track,spec);
}
xtc_result MainWindow::set_instrument_spec(uint64_t track,const xtc_plugin_spec &spec)
{
if(recorder_)return XTC_ERR_INVALID_STATE;
@ -1885,6 +1896,11 @@ 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,"Load sampler WAV")==0) {
const char *path=fl_input("Sampler WAV path (absolute; replaces/relinks selected track sample)","");
if(path) {r=w->load_sampler(track,path);if(r!=XTC_OK)w->report_error("Sampler preparation failed",r);}
return;
}
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{};

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 load_sampler(uint64_t track, const char *path);
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 *);

View file

@ -71,6 +71,11 @@ static int inspect(const xtc_project *project, uint64_t version)
quoted(spec.identifier); putchar(' '); quoted(spec.label);
printf(": %s ",available ? "available" : "missing/unreadable"); quoted(spec.path); putchar('\n');
if (!available) ++missing;
if(spec.sample_path[0]) {
int sample_available=readable_file(spec.sample_path);
printf("Sampler WAV track %" PRIu64 ": %s ",track.id,sample_available ? "available" : "missing/unreadable");
quoted(spec.sample_path);putchar('\n');if(!sample_available)++missing;
}
}
}
printf("Dependency references missing/unreadable: %zu.\n",missing);

View file

@ -37,6 +37,7 @@ struct xtc_midi_plan {
xtc_plugin *plugins[XTC_PROJECT_MAX_TRACKS];
size_t live_track;
unsigned int plugin_reset;
size_t sample_frames,asset_issues;
float scratch[256];
xtc_midi_event queue[256];
size_t count;
@ -202,6 +203,9 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
if (xtc_project_get_instrument(project,p->mix.tracks[i].id,&spec) == XTC_OK) {
xtc_result r = xtc_plugin_create(&spec,rate,256,&p->plugins[i]);
if (r != XTC_OK) { xtc_midi_plan_destroy(p); return r; }
p->sample_frames+=xtc_plugin_sample_frames(p->plugins[i]);
p->asset_issues+=xtc_plugin_asset_issues(p->plugins[i]);
if(p->sample_frames>64U*1024U*1024U) {xtc_midi_plan_destroy(p);return XTC_ERR_CAPACITY;}
}
if (project->live_instrument == p->mix.tracks[i].id) p->live_track = i;
}
@ -218,6 +222,7 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
*
* @param p Instrument plan and its single-owner runtime state.
*/
size_t xtc_midi_plan_asset_issues(const xtc_midi_plan *p) {return p ? p->asset_issues : 0;}
void xtc_midi_plan_destroy(xtc_midi_plan *p)
{
size_t i;

View file

@ -482,6 +482,15 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
a->step=(unsigned int)value;if(!decode_float(input->tokens[4],&a->value))return XTC_ERR_FORMAT;
}
}
if(format_version>=15U) {
READ("SAMPLE",2U);
if(strcmp(input->tokens[1],"-") && !decode_bytes(input->tokens[1],s->sample_path,sizeof(s->sample_path)))return XTC_ERR_FORMAT;
if(s->sample_path[0] && s->sample_path[0]!='/') {
size_t root=strlen(directory),len=strlen(s->sample_path);
if(root+len+2>sizeof(s->sample_path))return XTC_ERR_CAPACITY;
memmove(s->sample_path+root+1,s->sample_path,len+1);memcpy(s->sample_path,directory,root);s->sample_path[root]='/';
}
}
if (xtc_plugin_spec_validate(s) != XTC_OK) return XTC_ERR_FORMAT;
}
READ("LIVE_INSTRUMENT",2U); NUM(1U,project->live_instrument);
@ -654,6 +663,12 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch
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(fputs("SAMPLE ",file)==EOF)return XTC_ERR_IO;
if(s->sample_path[0]) {
const char *asset=s->sample_path;size_t root=strlen(directory);
if(!strncmp(asset,directory,root) && asset[root]=='/' && asset[root+1])asset+=root+1;
if(!write_name(file,asset))return XTC_ERR_IO;
} else if(fputs("-\n",file)==EOF)return XTC_ERR_IO;
}
if (fprintf(file,"LIVE_INSTRUMENT %" PRIu64 "\n",project->live_instrument) < 0) return XTC_ERR_IO;
}

View file

@ -1,16 +1,19 @@
/* Original, dependency-free mono devices. All storage is prepared off-thread. */
#include "builtin.h"
#include "../audio/wave_internal.h"
#include "xtc/xtc_midi.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define TAU 6.2831853071795864769
static const char *names[] = {"Soft Keys", "Sub Bass", "Delay", "Reverb"};
static const char *names[] = {"Soft Keys", "Sub Bass", "Delay", "Reverb", "Sample Player"};
static const char *controls[2][3] = {{"Time", "Feedback", "Mix"}, {"Decay", "Damping", "Mix"}};
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}};
static const char *sample_names[]={"Root key", "Loop", "Attack", "Release", "Gain"};
static const float sample_low[]={0,0,.1f,1,0},sample_high[]={127,1,2000,5000,4},sample_defaults[]={60,0,8,80,1};
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};
@ -28,17 +31,19 @@ struct xtc_builtin {
double frequencies[128];
int sustain[16];
float bend[16];
xtc_wave sample;
size_t sample_issue;
};
xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out)
{
unsigned int i; xtc_plugin_spec s = {0};
if (!out) return XTC_ERR_INVALID_ARGUMENT;
if (index >= 4) return XTC_ERR_NOT_FOUND;
if (index >= 5) return XTC_ERR_NOT_FOUND;
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 ? 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]; }
s.instrument = index < 2 || index==4; s.inputs = s.instrument ? 0U : 1U; s.tempo = 120000;
s.control_count = s.instrument ? 5U : 3U;
for (i=0;i<s.control_count;++i) { s.ports[i]=i; s.values[i]=index==4 ? sample_defaults[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)
@ -48,10 +53,12 @@ xtc_result xtc_builtin_validate(const xtc_plugin_spec *s)
s->identifier[0] || s->unique_id!=a.unique_id || s->instrument!=a.instrument || s->inputs!=a.inputs ||
(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]<(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;
s->values[i]<(s->index==4 ? sample_low[i] : s->index<2 ? synth_low[i] : lows[s->index-2][i]) || s->values[i]>(s->index==4 ? sample_high[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;
s->points[i].value<(s->index==4 ? sample_low[s->points[i].port] : s->index<2 ? synth_low[s->points[i].port] : lows[s->index-2][s->points[i].port]) ||
s->points[i].value>(s->index==4 ? sample_high[s->points[i].port] : s->index<2 ? synth_high[s->points[i].port] : highs[s->index-2][s->points[i].port])) return XTC_ERR_INVALID_ARGUMENT;
if(s->index==4) {if(s->values[0]!=floorf(s->values[0]) || s->values[1]!=floorf(s->values[1]))return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->point_count;++i)if(s->points[i].port<2 && (!s->points[i].step || s->points[i].value!=floorf(s->points[i].value)))return XTC_ERR_INVALID_ARGUMENT;}
return XTC_OK;
}
xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *out)
@ -62,10 +69,12 @@ 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,s->index<2 ? synth_names[i] : controls[s->index-2][i]);
strcpy(p->name,s->index==4 ? sample_names[i] : 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];
p->minimum=s->index==4 ? sample_low[i] : s->index<2 ? synth_low[i] : lows[s->index-2][i]; p->maximum=s->index==4 ? sample_high[i] : s->index<2 ? synth_high[i] : highs[s->index-2][i]; p->default_value=s->index==4 ? sample_defaults[i] : s->index<2 ? synth_default(s->index,i) : defaults[s->index-2][i];
out->flags[i]=XTC_PLUGIN_BOUNDED_BELOW|XTC_PLUGIN_BOUNDED_ABOVE;
if(s->index==4 && i<2)out->flags[i]|=XTC_PLUGIN_INTEGER;
if(s->index==4 && (i==2 || i==3))strcpy(p->unit,"ms");
}
return XTC_OK;
}
@ -78,9 +87,16 @@ xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t fram
if(!s->instrument) { p->line=calloc(p->length*4,sizeof(float)); if(!p->line) {free(p);return XTC_ERR_OUT_OF_MEMORY;} }
for(i=0;i<128;++i) p->frequencies[i]=440*pow(2,((double)i-69)/12)/rate;
for(i=0;i<4;++i) { static const double times[]={.0297,.0371,.0411,.0437}; p->comb[i]=(size_t)(rate*times[i])+1; }
if(s->index==4) {
xtc_result r=s->sample_path[0] ? xtc_wave_load(s->sample_path,&p->sample) : XTC_ERR_NOT_FOUND;
if(r==XTC_ERR_OUT_OF_MEMORY || r==XTC_ERR_CAPACITY) {xtc_builtin_destroy(p);return r;}
p->sample_issue=r!=XTC_OK;
}
*out=p; return XTC_OK;
}
void xtc_builtin_destroy(xtc_builtin *p) { if(p) {free(p->line);free(p);} }
size_t xtc_builtin_sample_frames(const xtc_builtin *p) {return p ? p->sample.frames : 0;}
size_t xtc_builtin_asset_issues(const xtc_builtin *p) {return p ? p->sample_issue : 0;}
void xtc_builtin_destroy(xtc_builtin *p) { if(p) {free(p->sample.samples);free(p->line);free(p);} }
static float parameter(const xtc_builtin *p, unsigned int port, uint64_t position)
{
const xtc_plugin_point *a=NULL,*b=NULL; unsigned int i;
@ -117,6 +133,19 @@ xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size
}
return XTC_OK;
}
static float sample_voice(xtc_builtin *p,voice *v,uint64_t position)
{
size_t a,b;float value;double advance;
if(!p->sample.frames)return 0;
if(v->phase>=(double)p->sample.frames) {v->active=0;return 0;}
a=(size_t)v->phase;b=a+1;
if(b>=p->sample.frames)b=parameter(p,1,position)>=.5f ? 0 : a;
value=p->sample.samples[a]+(p->sample.samples[b]-p->sample.samples[a])*(float)(v->phase-(double)a);
advance=(double)p->sample.rate/p->rate*pow(2,((double)v->note-parameter(p,0,position)+p->bend[v->channel]*2)/12);
v->phase+=advance;
if(parameter(p,1,position)>=.5f)v->phase=fmod(v->phase,(double)p->sample.frames);
return value*v->level*v->velocity*parameter(p,4,position);
}
static void midi_event(xtc_builtin *p, const xtc_midi_event *e)
{
unsigned int j,slot=0;
@ -145,6 +174,7 @@ xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t
for(i=0;i<frames;++i) {
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);
if(p->spec.index==4) {attack=parameter(p,2,position+i);release=parameter(p,3,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));
@ -154,6 +184,7 @@ xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t
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;}
if(p->spec.index==4) {sum+=sample_voice(p,v,position+i);continue;}
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);

View file

@ -6,6 +6,8 @@ xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out);
xtc_result xtc_builtin_validate(const xtc_plugin_spec *s);
xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *out);
xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_builtin **out);
size_t xtc_builtin_sample_frames(const xtc_builtin *p);
size_t xtc_builtin_asset_issues(const xtc_builtin *p);
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, uint64_t position);

View file

@ -53,6 +53,8 @@ xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin
snprintf(out->copyright,sizeof(out->copyright),"See plugin license");
return XTC_OK;
}
size_t xtc_plugin_sample_frames(const xtc_plugin *p) {return p ? xtc_builtin_sample_frames(p->builtin) : 0;}
size_t xtc_plugin_asset_issues(const xtc_plugin *p) {return p ? xtc_builtin_asset_issues(p->builtin) : 0;}
void xtc_plugin_destroy(xtc_plugin *p)
{ if (p) { xtc_builtin_destroy(p->builtin); xtc_ladspa_destroy(p->ladspa); xtc_modern_destroy(p->modern); free(p); } }
xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_plugin **out)

View file

@ -27,6 +27,7 @@ xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
s->tempo < 20000 || s->tempo > 300000 || s->point_count > XTC_PLUGIN_POINTS) return XTC_ERR_INVALID_ARGUMENT;
if (s->format < XTC_PLUGIN_LADSPA || s->format > XTC_PLUGIN_BUILTIN ||
!terminated(s->identifier,sizeof(s->identifier))) return XTC_ERR_INVALID_ARGUMENT;
if (!terminated(s->sample_path,sizeof(s->sample_path)) || (s->sample_path[0] && (s->sample_path[0]!='/' || s->format!=XTC_PLUGIN_BUILTIN || s->index!=4)))return XTC_ERR_INVALID_ARGUMENT;
if (s->format == XTC_PLUGIN_BUILTIN) {
if (xtc_builtin_validate(s) != XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
} else if (s->format != XTC_PLUGIN_LADSPA) {

View file

@ -748,6 +748,14 @@ int main(int argc, char **argv)
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-internal-gui.xtc")==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==instrument);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec)==XTC_OK && spec.index==effect && spec.point_count==1);
char sample[1024];CHECK(getcwd(sample,sizeof(sample))!=nullptr);std::strcat(sample,"/examples/projects/original-pluck.wav");
CHECK(window.load_sampler(track,sample)==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==4 && !std::strcmp(spec.sample_path,sample));
CHECK(window.save_project("/tmp/xtc-sampler-gui.xtc")==XTC_OK);
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-sampler-gui.xtc")==XTC_OK);
CHECK(window.load_sampler(track,"/absent/sample.wav")==XTC_OK);
CHECK(window.load_sampler(track,sample)==XTC_OK); // Relink is one prepared history edit.
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);unlink("/tmp/xtc-sampler-gui.xtc");
size_t added=SIZE_MAX;CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect,&added)==XTC_OK);
CHECK(added!=id);CHECK(window.undo()==XTC_OK);
const xtc_project *before=sequence->project();reserve_result=XTC_ERR_OUT_OF_MEMORY;

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..14' in diagnose and 'JACK compiled:' in diagnose
assert 'Project formats: 1..15' 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..14")),
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..15")),
("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

@ -9,6 +9,7 @@
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *__real_malloc(size_t size);
void *__real_calloc(size_t count, size_t size);
@ -156,15 +157,16 @@ static void history_reservations(void)
static void builtin_failures(void)
{
unsigned int id;
for(id=0;id<4;++id) {
for(id=0;id<5;++id) {
int failure, succeeded=0; xtc_plugin_spec spec;
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,id,48000,&spec)==XTC_OK);
if(id==4) {assert(getcwd(spec.sample_path,sizeof(spec.sample_path)));strcat(spec.sample_path,"/examples/projects/original-pluck.wav");}
for(failure=0;failure<16;++failure) {
xtc_plugin *plugin=NULL; xtc_result result; float in[256]={0},out[256];
fail_after=failure;result=xtc_plugin_create(&spec,48000,256,&plugin);fail_after=-1;
if(result!=XTC_OK) {assert(result==XTC_ERR_OUT_OF_MEMORY && !plugin);continue;}
processing=1;
if(id<2) {xtc_midi_event event={0,XTC_MIDI_NOTE_ON,0,60,100};
if(spec.instrument) {xtc_midi_event event={0,XTC_MIDI_NOTE_ON,0,60,100};
assert(xtc_plugin_process_midi(plugin,&event,1,out,256)==XTC_OK);
} else assert(xtc_plugin_process(plugin,in,out,256)==XTC_OK);
processing=0;xtc_plugin_destroy(plugin);succeeded=1;break;

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "../../src/audio/wave_internal.h"
#include <unistd.h>
#include <stdlib.h>
#include "xtc/xtc_midi.h"
@ -165,8 +166,8 @@ static void editable(void)
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");
if(!strncmp(line,"SAMPLE ",7) || !strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue;
if(!strncmp(line,"XTC_PROJECT 15",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);
@ -187,7 +188,40 @@ static void editable(void)
assert(xtc_midi_decode(bend,3,7,&e)==XTC_OK && e.type==XTC_MIDI_BEND && e.channel==3 && e.velocity==64);}
unlink(path);
}
static void sampler(void)
{
xtc_plugin_spec s,t;xtc_plugin *a=NULL,*b=NULL;float x[256],y[256],data[]={0,.25f,.5f,.75f};
xtc_midi_event e={0,XTC_MIDI_NOTE_ON,0,60,127};size_t i;double envelope=0;
char path[]="/tmp/xtc-sample-XXXXXX",preset[]="/tmp/xtc-sample-preset-XXXXXX";
int fd=mkstemp(path),pd=mkstemp(preset);FILE *file;assert(fd>=0 && pd>=0);close(fd);close(pd);
file=fopen(path,"wb");assert(file && xtc_wave_header(file,24000,4) && xtc_wave_write(file,data,4));fclose(file);
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,4,48000,&s)==XTC_OK && s.instrument);
strcpy(s.sample_path,path);s.values[2]=.1f;
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && !xtc_plugin_asset_issues(a) && xtc_plugin_sample_frames(a)==4);
assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK);
for(i=0;i<8;++i) {
double expected=i<6 ? (double)i*.125 : .75;
envelope+=(1-envelope)*(1-exp(-1/(.1*.001*48000)));
assert(fabs(x[i]-expected*envelope)<1e-5);
}
for(i=8;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);s.values[1]=1;
assert(xtc_plugin_preset_save(&s,preset)==XTC_OK && xtc_plugin_preset_load(preset,&t)==XTC_OK);
assert(!strcmp(t.sample_path,path) && t.values[1]==1);
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && xtc_plugin_create(&t,48000,256,&b)==XTC_OK);
assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK && xtc_plugin_process_midi(b,&e,1,y,128)==XTC_OK);
assert(xtc_plugin_process_midi(b,NULL,0,y+128,128)==XTC_OK && !memcmp(x,y,sizeof(x)));
e.type=XTC_MIDI_NOTE_OFF;assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK);
for(i=0;i<200;++i)assert(xtc_plugin_process_midi(a,NULL,0,x,256)==XTC_OK);
for(i=0;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);xtc_plugin_destroy(b);unlink(path);
assert(xtc_plugin_preset_load(preset,&t)==XTC_OK && !strcmp(t.sample_path,path));
assert(xtc_plugin_create(&t,48000,256,&a)==XTC_OK && xtc_plugin_asset_issues(a)==1);
e.type=XTC_MIDI_NOTE_ON;assert(xtc_plugin_process_midi(a,&e,1,x,256)==XTC_OK);
for(i=0;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);unlink(preset);
}
int main(void) {
editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
sampler();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

@ -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..14"));
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..15"));
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"));