From 35bf6a00a13c0cde5bb662c1518c6378bc6cd836 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 5 Nov 2026 23:50:14 +0100 Subject: [PATCH] feat: add a bounded WAV sampler with looping, relinking and saved presets --- docs/BUILTIN_DEVICES.md | 31 +++++++++++ docs/PROJECT_FORMAT.md | 17 ++++-- docs/ROADMAP.md | 17 +++--- examples/projects/SAMPLE_LICENSE.md | 7 +++ examples/projects/original-pluck.wav | Bin 0 -> 22094 bytes include/xtc/xtc_midi.h | 1 + include/xtc/xtc_plugin.h | 3 ++ include/xtc/xtc_project.h | 2 +- src/backend/jack_backend.c | 3 +- src/gui/main_window.cpp | 20 ++++++- src/gui/main_window.hpp | 1 + src/main.c | 5 ++ src/midi/instrument.c | 5 ++ src/persistence/project_file.c | 15 ++++++ src/plugin/builtin.c | 53 +++++++++++++++---- src/plugin/builtin.h | 2 + src/plugin/host.c | 2 + src/plugin/ladspa.c | 1 + tests/integration/test_gui.cpp | 8 +++ tests/integration/test_install.py | 2 +- tests/integration/test_project_inspection.py | 2 +- tests/integration/test_reliability.c | 6 ++- tests/unit/test_builtin.c | 40 ++++++++++++-- tests/unit/test_project_diagnostics.c | 2 +- 24 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 examples/projects/SAMPLE_LICENSE.md create mode 100644 examples/projects/original-pluck.wav diff --git a/docs/BUILTIN_DEVICES.md b/docs/BUILTIN_DEVICES.md index 60f93ba..80df35d 100644 --- a/docs/BUILTIN_DEVICES.md +++ b/docs/BUILTIN_DEVICES.md @@ -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. diff --git a/docs/PROJECT_FORMAT.md b/docs/PROJECT_FORMAT.md index de2c558..52c5cdd 100644 --- a/docs/PROJECT_FORMAT.md +++ b/docs/PROJECT_FORMAT.md @@ -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`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 655cb28..f39a007 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/examples/projects/SAMPLE_LICENSE.md b/examples/projects/SAMPLE_LICENSE.md new file mode 100644 index 0000000..087b454 --- /dev/null +++ b/examples/projects/SAMPLE_LICENSE.md @@ -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. diff --git a/examples/projects/original-pluck.wav b/examples/projects/original-pluck.wav new file mode 100644 index 0000000000000000000000000000000000000000..331ff9ead5706b9cfa32dfbc3d20699be9b6799d GIT binary patch literal 22094 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 z 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%0DST>`6z!0Rp?QQ-1~cp0_mlb9x&L>+K;gM)5E zr}#rMV8`47ec5XG%L`2P7F@3lhnIE-cjJL|7G}#?_)N$|An%7Mvk4i2sLRkJXT#-R zVD2~uUs_5`#njn_Xo~s66aAeXk^h5j>oYJKjc#H&=H+3CePd*)GhDv{oW{YW9-%wE zfU9}O;hD$5?`2?rg$&LSZK4zMvn!^;jnLyqsYucw^8JW;L=+hP0+vKn(Q4qd3o6tw zbnfHuS+!9>-4{P;N!B3);YkE~kaW1`Q}i)M|Kgkm-{?j(Cu+d!HSnei)a7`%Qx&5J z@Lmg1n+BrGuZKQghfcpj%t9@VLHr*e(pS-^Tm|NP$lOn2l30X(-W7S$6|>e_=@c}X zCE?%FV>dVn?sSTHhMX%WXfRd{%(VxreSvlWnCuBITS4jJn2S^-i|kB+D?KJI;a}Wu z0iRVkyF-(;(APVnvo}fk(r@V#{Qep$(=PbaY{b3~>V0kWP!2#^3D%OJ?DxR>1krwo z`idjr_yg}Wi*j@Y^?-9cx`e}sO1z|(ba?e%m~J;B zM;}2=x$t}?I1M3d0b3K~KvTFtU7!p^y;PxMR=^wLFt6Ul7X1MFr7D~w@EUDU<3rGm zCpkWvb3pG4Jfq8QxkkL3mtzVSd7Ch5h!!Sa*-1?aIpr^ z(Ku-tlz0=lUIcFn#e6dw(c6oBdkvo#PppaegDho1?(TTquS5L`}W7=)ELV5M)WE4rWN2X zTTF+>)1afK-m1u_q!O z03QAEsUejE|kDU6QI>6*yo&r z<8Q)y&cL<1KG0J`AoaoLie$LrXad$^)YL-Umm}r^YNiLSnKqSr0_PGq=tZ1uDmJ&y zsPkPhjYUHB7x4bykfo)Bji5+1u(|@FEAmnUKN66=MfhzzYSAOGAC0S=>wt4A;@$^Q zX^h>SAK{D)8^kCp_bs^a~S+3XyMvo_r2scnseC3fY$dPAzaL8ns+SD$&1F$V@Zv=McYf zCa=*eUBt6@VJ2AkUz{BZ^yJ>2BoKh%@}ytE#e$pX$NY=0@TeaK06?PYC-Y%To*FbiVimmJbVG8 z_tD`WNB^`5x|oG`AB^0qLRv!#LN#>(HZ_=4;$8>je&D(xvZFtIX%Wuu1pfXjqQgtB zc>RueuQ`~?4#5>4p%+R(eDi@!4}2!XyAoXH1N|?s`4-Q(4iz0l)m()t@gJNW(Sz28 z;ynomR8JO>%0pd>1EWvTkDkM3d>3@T7+E?NuHOwv8^g^*!KxR~x!`aGmwxz3ZFE?j z@tjF`_Eyxi+i;F_^mkfBz6I*iWMUZ@It~m^!C5>Ml}!}D)k|5RI{0huRli{CzaHh@SaiQq$9I?L>5cwi#DG`jmM)!0TYO0F;YBC0!+^Oa`u}E_t9nCmQ=#`Qh{HAHP9idt zKqp=Uv+Y1ca49PGVW7B!>lq&q<9I~+H;yFYCvbm3?mh>bR}uBYP|Qs9Vc@e9 zVp0#z)@yh)Xk6) zU67rlP{Wo2=MhXFFOWZZ5{p?W1S;qOzn=|d>;hMpfbd#! zt)D!=1N;S0M_!B)VPreoe5^8eusLhW?= zo1u2}4@H<8euL4Eh~0f;+F9gV6j)jfjm$#y$DlVIfNboJzM~6{Zs4*n*c^@D&4c4^ z1I~-M>Jx*wl%RS#;xnBM!O38#Vga^rJAmXA;(G(g9su7X#QQ$DypD`G4L0{9>g%DR zd8nFW@mIb6%V!O|jyIH~LiOY%6I?VGRqrPp@GYWu2QfT_cis&zT?KcX4XupBK4LJi z_66GBsH^?)-3Yks3^>Co`21l+;XdYrU&wYls**37*yfxh`2XIMG8TUcZJt8@FolDXTg6Nje%+({*C={ zWa&=y&nv;{9Jtg3us0laYB2J3FusmJZcYK)i&1NLAV1He_P#~8kc;zX5czOazAkvr ziSWebi0W=2Jr0+;fDE{T;}Ra7LFOKTckMtHuS9mvLPn25UibVzoS`^lH+a1q-eg1M zi{VcHVe}pR=^kQ#4&Cp5Aovf`xnOQG_!|RVjl?k;3{S$(=0Qd4aXwWz?;^kBaK4rB zCPyfs9^9!n_C?cBwO64B-h-O+FIYVT7SH1T6mTC!?(Rkgufw@40QVE|{DJ?6vlg7* z59h4IEKmH6d>Oj?jK4Yh36B35vA=?Pe++us4v$(1Ma_ek**bgWTK*tuF=d z)1mg^=nH$mEvn*P3r_D3#XJ7Z(5lE+LQC1Gpg*y5`~Wpo#r_hq^axzP3h65Fwh-~3 z2@a>>m@fSTl=I50jTlL@Op-j@VW+QDeo_%aoCH${Tuu1 z(8ft9qzdUq;E6;oE`a;b0m9k%x&SP%fX{9N>;K}pw{d=75R-glXx0A}EdW|-0Y(R6 z2R98`UIwI_z}_y@$h|nC!0!&QydGJx6g!`pIG-_a&%T&9+y575I9|mIbCMQ)D1m;& z1Q#v*%V;8E_Yo1Siv3k&>2V;9LY{7dURHwD#fbGn9E-r^a(L_}{BA!Sw+iQT%!R*@ z>3Zz!HE@&&^my%|-l2%+4CG%V;=2LJwgKM`Al?QpH{fT>!R8!9eIise7*(??;_x3n z!;$CSP`m><fjs;I&Ks%V<1UdH*-|w~?i1|03NBG@GHA)xf$OXqSQA75HvFJbpKv z;RO8iHlh%N%*nzzbErygaKF09vo46_NI2r-(y zBaqL%@fYoo+g17bKb$&vJ&melL;Wm+J7q)lzc7V<`WyQP$kNNuQ}o~H?*yieK(+?> zs*bgIycziSB8yKVo9_VUM>tak&e(>}H#tG8H84N4gMJ4iqEmon0V2Et*w&z$uf_c; zaJdwIFb8~3`kS2tz<*~r#s6>yL6L5MmBjpwycsT94EM`GO-X>7s$%~H4s`k>GM6ls*%`n}Dbf1@}EsjoP43ZU8L2Hjw^U(70y^FJ|C|oN=on$h?>#_jCP0H zj6htc0?}Nsx)}JXjzxGp59p^uL*wv_!8oJtaQXiqr!Snz2{lQM3>8uBOklL=FQX~Q zvhROm{~Qi=2d@7gq(_m7`+#*f&{iFLfbt-`^8{S!+FzXSkuxcHjS6I_46p7BM(Tsn zPKf3pWN8)Bf8ap#!0Q6s<{{#<5c4Tub2K;~2o-gME44;vSpUC#R^fC;bQH)?34Nm( zTKW&8Y1k9S!wWyc^Pi#5zx`jNClKMoK(-(Fs*e45d<1NshT5+qN1j1{-{DNz$aNdM zNek}_#=A8^zI6ptLlD{Vz%(7K&H~!mxSxsdrb6jsaTY@m^pd+cqR{C_w-;Y4n2*3YPa^7 z1yUnF9VDL@Xf@Fp#CD-eBoefUf}M6~x`q$A-{qAI2bylAevx$kb?eehFPX33MvkQEvF6=K~rc;I61h5gOH!+d6kxU@i@gh zK7M~Z?_r5m#PK|yXHheyu1kxq*dYw3=xO20F-?+$it}sjl2P=F`DDBw@fyCDI6gty!1WI4 z7%JxnS1!2A54-0PI#^^(6b+(NbVr&&KN~(-Hs?Z_M9mw%5vE0#>_%t*a7seOdFj$h zGx|-+K6lB3-6lDt{#1RWkO7uX*2ftCEjd!V)mfLsVaIw>cXTn zj&8S1pbWZZdEMc78qX7|W`uM~ivr5&*B%)?z1pNhW|MLLPtm!0|IUOO^Dn|sHSOYX K3o-}3`tKIS58#Xd literal 0 HcmV?d00001 diff --git a/include/xtc/xtc_midi.h b/include/xtc/xtc_midi.h index 9567457..3aea461 100644 --- a/include/xtc/xtc_midi.h +++ b/include/xtc/xtc_midi.h @@ -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 diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index 807fcaf..ad55868 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -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. diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index b95a4c6..83989cf 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -7,7 +7,7 @@ extern "C" { #endif -#define XTC_PROJECT_FORMAT_VERSION 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 diff --git a/src/backend/jack_backend.c b/src/backend/jack_backend.c index d155f1c..1fab1d3 100644 --- a/src/backend/jack_backend.c +++ b/src/backend/jack_backend.c @@ -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); diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index b83374c..7f70fc5 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -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(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{}; diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index b74a0fd..e4cda43 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -49,6 +49,7 @@ class MainWindow : public Fl_Double_Window { Fl_Widget *velocity_lane() const { return velocity_lane_; } /* Control-thread plugin commands, transactional model adoption. */ xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index); + xtc_result 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 *); diff --git a/src/main.c b/src/main.c index 1493326..74eaaa9 100644 --- a/src/main.c +++ b/src/main.c @@ -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); diff --git a/src/midi/instrument.c b/src/midi/instrument.c index 3432d13..20b1e92 100644 --- a/src/midi/instrument.c +++ b/src/midi/instrument.c @@ -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; diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index 3a0ff7a..adbbbdc 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -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; } diff --git a/src/plugin/builtin.c b/src/plugin/builtin.c index 1eaa4c9..13b6306 100644 --- a/src/plugin/builtin.c +++ b/src/plugin/builtin.c @@ -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 #include #include #include #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;iidentifier[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;icontrol_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;ipoint_count;++i) if(s->points[i].port>=s->control_count || !isfinite(s->points[i].value) || - s->points[i].value<(s->index<2 ? synth_low[s->points[i].port] : lows[s->index-2][s->points[i].port]) || - s->points[i].value>(s->index<2 ? synth_high[s->points[i].port] : highs[s->index-2][s->points[i].port])) return XTC_ERR_INVALID_ARGUMENT; + 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;ipoint_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;icontrol_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;ispec.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); diff --git a/src/plugin/builtin.h b/src/plugin/builtin.h index 9680b72..2315822 100644 --- a/src/plugin/builtin.h +++ b/src/plugin/builtin.h @@ -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); diff --git a/src/plugin/host.c b/src/plugin/host.c index 852522e..4400ec5 100644 --- a/src/plugin/host.c +++ b/src/plugin/host.c @@ -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) diff --git a/src/plugin/ladspa.c b/src/plugin/ladspa.c index 1905df1..2363d69 100644 --- a/src/plugin/ladspa.c +++ b/src/plugin/ladspa.c @@ -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) { diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 8518c06..05aa1f3 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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; diff --git a/tests/integration/test_install.py b/tests/integration/test_install.py index 7f65f4e..2637611 100644 --- a/tests/integration/test_install.py +++ b/tests/integration/test_install.py @@ -56,7 +56,7 @@ def main(): run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')]) assert (docs / 'WORKSPACE.md').is_file() diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True) - assert 'Project formats: 1..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') diff --git a/tests/integration/test_project_inspection.py b/tests/integration/test_project_inspection.py index 6c225ff..a508608 100644 --- a/tests/integration/test_project_inspection.py +++ b/tests/integration/test_project_inspection.py @@ -84,7 +84,7 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory: project.write_text(malformed) check("--validate", project, 1) for text, messages in [ - (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..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",)), diff --git a/tests/integration/test_reliability.c b/tests/integration/test_reliability.c index ebe1523..b85c41a 100644 --- a/tests/integration/test_reliability.c +++ b/tests/integration/test_reliability.c @@ -9,6 +9,7 @@ #include #include #include +#include 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; diff --git a/tests/unit/test_builtin.c b/tests/unit/test_builtin.c index 0dacb99..f137d5d 100644 --- a/tests/unit/test_builtin.c +++ b/tests/unit/test_builtin.c @@ -1,5 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include "xtc/xtc_plugin.h" +#include "../../src/audio/wave_internal.h" #include #include #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)=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; } diff --git a/tests/unit/test_project_diagnostics.c b/tests/unit/test_project_diagnostics.c index 266a995..b12416b 100644 --- a/tests/unit/test_project_diagnostics.c +++ b/tests/unit/test_project_diagnostics.c @@ -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"));