Resolves the open question in TODO.md about whether WavAudioFormat's
"must be 1 or 2 channels" doc comment is an enforced limit - a
5-channel plugin recorded a valid, non-silent WAV with no clamping.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JAZXxJbHD7PzdKaNE6Nuiv
Drop the -pre suffix and finalize the version. Also corrects
APP_VERSION_HEX, which had drifted out of sync with APP_VERSION since the
1.1.0 bump (stayed at 0x010001 instead of following the major/minor/patch
per-byte encoding established at 1.0.0/0x010000 and 1.0.1/0x010001); now
0x010101 to match 1.1.1. Updates JuceLibraryCode/AppConfig.h's fallback
JucePlugin_Version*/VersionString defines to match (normally overridden by
the Makefile's -D flags, but were stale at 0.5.8).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
The chunk-advertisement misdiagnosis is already fully disproved and
documented in tests/README.md's "corrected finding" section; condense the
TODO.md entry to a one-line resolved note instead of duplicating the full
narrative, keeping only the genuinely still-open question (why specific
old external .fxp sample files fail to apply).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
Add Vst2RawChunk, a raw effGetChunk/effSetChunk bypass around JUCE's
VSTPluginInstance::usesChunks() gate, plus --loadBankChunk/--saveBankChunk/
--loadPatchChunk/--savePatchChunk CLI flags and a new test_chunk_roundtrip
regression test asserting the saved file is genuinely zlib/XML (0x78 header,
>1000 bytes), not a per-parameter fallback.
Building and using this bypass disproved an earlier documented finding: the
compiled VST2 wrapper does set effFlagsProgramChunks and usesChunks() is
genuinely true, so the previous "chunk-advertisement gap" theory in TODO.md
and tests/README.md was wrong. Corrected in both places. The real cause of
old bundled .fxp sample files having no effect remains open and is now
narrowed to a JaySynth-side XML/schema parsing question, not a host/plugin
capability mismatch.
Also adds a PluginHost-method-to-VST2-wiring-to-test coverage table in
tests/README.md, and documents the remaining "exercised but not asserted"
gaps (channel/param counts, DSP correctness) in TODO.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
tools/testhost/tests/run_tests.sh dynamically exercises the tracked
hardening findings using testhost itself - no GUI, sub-second wall
time:
- oversized_blocksize (Critical #5): --blockSize 16384 under a
timeout, catching either the overflow or the deadlock the original
bug could cause.
- nrpn_out_of_range/nrpn_in_range (Critical #1): JSON scenarios
sending the 5-message MIDI NRPN CC sequence, once targeting the
max 14-bit ID (16383) and once a legitimate in-range ID (500), so
a regression that made the bounds check too aggressive would also
show up.
- patch_save_load_roundtrip / bank_save_load_roundtrip: save a
distinctive continuous parameter value, reload in a fresh process,
check it round-trips within tolerance - covers the
setCurrentProgramStateInformation no-op bug and the
patchDecodeXml/patchImportXml fixes.
- stability_sweep: loads every bundled .fxp under extras/sounds/ and
renders a held note through each, as a broad crash/NaN net.
Added --printParams to main.cpp (prints every parameter's index and
value) to make the round-trip tests possible - this was also already
a noted nice-to-have in testhost/TODO.md.
While building the oversized_blocksize test, found and fixed (on the
hardening branch, commit d713ca5) a second, previously-unknown bug:
VCF_CalcCoeff_LPF/_HPF/_BPF advance the coefficient buffer pointer
once per (sample, filter-section) pair, but the buffer was only ever
allocated for bufsize samples, not bufsize*sections - a 4th-order
filter overflowed it for any block between 4097 and 8192 samples.
Found via bisecting the crashing block size then confirming with an
ASan build; see tests/README.md's debugging-notes section for exactly
how, including the ASan symbolizer hang encountered along the way and
the workaround.
Also discovered (documented in tools/testhost/TODO.md's Investigate
section, not fixed - out of scope): JaySynth's compiled VST2 wrapper
never advertises chunk support to the host, so real .fxp/.fxb sample
files have no effect when loaded through the generic AudioProcessor
state API. The round-trip tests work around this by using testhost's
own save output as the fixture rather than an external sample file.
All 10 checks pass against the current hardening-branch build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
Found while building tools/testhost's regression suite for the
Critical #5 block-size fix: VCF_CalcCoeff_LPF/_HPF/_BPF advance the
coefficient pointer once per (sample, filter-section) pair, but a
4th-order filter uses FILTER_MAX_ORDER/2 = 2 cascaded 2nd-order
sections per sample. VCF_SetBufsize only allocated pCoef with bufsize
entries, not bufsize*sections, so any host block between 4097 and
8192 samples with a 4th-order filter selected wrote past the
allocation.
Confirmed with AddressSanitizer before the fix: heap-buffer-overflow,
WRITE of size 8, exactly 8 bytes past a 393216-byte (8192 x
sizeof(vcf_coef_t)) allocation. Fixed by sizing pCoef for the worst
case. Re-verified with ASan across blockSize 4097/8192/16384/20000 -
zero errors after the fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- README.md: what the tool is and isn't (generic, not JaySynth-specific),
build instructions, CLI quick-start, full JSON scenario schema/field
reference, the patch/bank state-format caveat (this tool's load/save
goes through JUCE's host-side AudioPluginInstance state calls, not
JaySynth's own GUI-triggered loadPatchFromFile/patchImportXml - a
different code path worth knowing about), and an architecture summary.
- TODO.md: tracks what's deferred by design (LV2/VST3/LADSPA format
registration, Windows build), test coverage gaps (stereo effects
untested, no regression-baseline tooling, no CI wiring, no JSON-parser
unit tests, no polyphony testing), two things worth investigating
(a non-fatal DPF plugin assertion seen on load, an unconfirmed
WavAudioFormat channel-count limit), and one nice-to-have.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- TestScenario: parses a JSON file describing sample rate/block size/
duration, an input signal config (silence/sine/noise/impulse), and a
timed sequence of events (noteOn/noteOff/controller/param/loadPatch/
loadBank/savePatch/saveBank), sorted by sample position. Relative
file paths inside the JSON resolve against the scenario file's own
directory, not the process cwd, so scenario files are portable.
Nothing here is JaySynth-specific: an event type a given plugin
doesn't care about (e.g. MIDI sent to a plugin with no MIDI input)
is simply inert, since AudioProcessor/MidiBuffer already tolerate
that generically.
- main.cpp: unified the ad-hoc CLI flags (--note/--param/--input/etc.)
and --scenario JSON files through one render loop, by synthesizing
a TestScenario from the CLI flags when no JSON file is given. Added
--bank/--savePatch/--saveBank for the ad-hoc path (loadPatch/wav
already existed). Non-MIDI events (param/patch/bank) scheduled
inside a block apply at the start of that block; MIDI events keep
full per-sample timing via MidiBuffer's own offset parameter.
- PerformanceStats: wraps each processBlock call with a high-resolution
timer, reports min/mean/max render time and a real-time factor
(audio seconds rendered / wall-clock seconds), plus optional
per-block CSV export for tracking regressions over time. Never
paces the loop to real-time - the point is to run faster than that.
Verified end-to-end:
- Ad-hoc CLI path against JaySynth.so still renders correctly and now
reports performance (e.g. ~74x real-time on this machine).
- A JSON scenario driving JaySynth (timed noteOn/param/noteOff/
savePatch) produces correct WAV output and a valid saved patch file.
- That saved patch round-trips back in via --patch and renders
cleanly (proves the save/load path is consistent, not just
one-directional).
- A JSON scenario driving a real third-party effect (ZamComp-vst.so,
2 in/1 out - a genuinely asymmetric channel count, not just "0 in"
or "stereo") with generated pink noise and a param event produces
correct mono output at >1000x real-time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
tools/testhost/ is a standalone CLI executable, not a JaySynth-specific
tool: it drives any VST2 plugin offline (no audio device, no GUI event
loop) via JUCE's own format-agnostic AudioPluginFormatManager /
AudioPluginInstance hosting API, so adding another format later (VST3,
LADSPA, eventually LV2) is just another addFormat() call - none of the
host logic changes.
- PluginHost: format-agnostic load/prepare/process/release, parameter
get/set, and patch/bank state I/O, all delegating straight to
AudioProcessor. Always queries the plugin's real input/output channel
counts rather than assuming "0 in" (instrument) or a fixed channel
layout - the same code path drives JaySynth (0 in/2 out, MIDI-driven)
and a stereo/mono effect identically.
- AudioInputSource: fills the plugin's input channels with silence
(default, correct no-op for instruments), a sine tone, white/pink
noise, or an impulse - what makes the host equally useful for
effect plugins, which need a real input signal to do anything.
- WavRecorder: records the plugin's actual output channel count to a
WAV file via WavAudioFormat/AudioFormatWriter.
- main.cpp: CLI (--plugin/--patch/--param/--note/--input/--duration/
--wav/--sampleRate/--blockSize), with basic NaN/Inf output detection.
Has its own JuceLibraryCode-equivalent package that compiles only the
module amalgams needed for hosting (not juce_audio_plugin_client,
which is the plugin-side VST entry point and would collide with this
executable's own main()), reusing the already-vendored
sdk/juce/JUCE-3.1.1 rather than a second JUCE copy - the host and any
plugin it loads are decoupled at the VST2 ABI, not the JUCE source
level, so this doesn't need to track JaySynth's own future JUCE
version bump.
Verified: builds clean (no new warnings) via
`MAKE_HOME=... make -C tools/testhost`. Smoke-tested against two very
different real plugins:
- build/linux/release/JaySynth.so (0 in/2 out, MIDI-driven): --note
60 100 --duration 2 --wav produces correct-length, non-silent,
NaN-free stereo audio.
- /usr/lib/vst/ZamDelay-vst.so (1 in/1 out, real third-party VST2
effect, no MIDI): --input pink --duration 2 --wav correctly sizes
input/output to mono and produces non-silent, NaN-free audio -
proof the host is generic, not JaySynth-shaped.
- /usr/lib/vst/ZamEQ2-vst.so: the host's own NaN/Inf detection caught
a real NaN in this plugin's default state on first render - exactly
the kind of regression this tool exists to catch without a GUI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- patchImportXml: handle the legacy single-patch format (JSYNTH_PATCH
root) as its own top-level branch instead of incorrectly nested
inside the modern per-child loop, which checked/decoded the outer
xml on every iteration instead of the loop variable xml2. Switched
its CC-table import to import_midi_cc, matching the convention
already used (and exercised) by bankDecodeXml_legacy. loadPatchFromFile's
.xmp branch now also accepts legacy-rooted files.
- setCurrentProgramStateInformation: fix a related bug found in
passing - it was passing patchImportXml an XML node one level too
deep, making the VST2 "copy plugin state to another track" feature
a complete no-op.
- Fix the fundamentally broken fxb/fxp magic-number check: it compared
raw file bytes (as text, e.g. "CcnK") against JUCE's int-to-decimal-
text String constructor (e.g. "1130589771"), which can never match.
This meant the entire opaque .fxb/.fxp load path was dead code.
Fixed using ByteOrder::bigEndianInt, matching the pattern JUCE's own
VST3 wrapper uses for the identical struct. "Regular" (non-chunk)
.fxb/.fxp is still unsupported, but now shows an AlertWindow
explaining why instead of silently doing nothing.
- Implement .fxp/.fxb saving using the same opaque-chunk format the
load path expects, reusing getCurrentProgramStateInformation/
getStateInformation for the payload.
- Deduplicate patchDecodeXml/patchDecodeXml_legacy (the latter now
just delegates to the former).
Verified with clean debug/release builds and a standalone round-trip
test of the binary format logic (magic detection, byte order,
size/payload round-trip, safe rejection of truncated/bogus files).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- JaySynth's destructor now uses delete[] to match every new T[n]
allocation (m_pVoices, pPer_voice_controls, pCurrNoteInfos,
humanize_voice_param[i], ppHumanizedSliders[i], m_ppAudioThread,
m_ppEventAudioThreadRdy), fixing undefined behavior from the
mismatched scalar delete.
- Found and fixed the same new[]/delete mismatch pattern via
ScopedPointer in PluginProcessor.cpp: ScopedPointer always calls
scalar delete (per JUCE's own doc comment "do not give it an array
to hold!"), so ScopedPointer<char> holding a new char[...] in
setStateInformation/setCurrentProgramStateInformation had the same
bug. Replaced both with HeapBlock<char>, JUCE's array-owning,
malloc/free-backed smart pointer.
- Added a shared SynthCheckAlloc() helper (synth_defs.h/synth_debug.c)
that aborts with a diagnostic instead of returning NULL, and wrapped
all 24 malloc call sites across the C DSP core (env.c, lfo.c, vcf.c,
vco.c, blit.c, wavetable.c, voice.c, param_scale.c). All are
one-time init/bufsize-change calls, never in the per-block render
path, so this adds no real-time-thread overhead.
Verified with clean debug and release builds (no new warnings/errors)
and a full link.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- synthChanged() no longer builds JaySynthActionListener Strings and
calls sendActionMessage directly on the audio thread. It now pushes
a small POD event into a lock-free SPSC queue (JUCE's AbstractFifo),
drained by the existing 10ms UI timer on the message thread, which
does the String-building/posting there instead.
- VoiceProcessDataV's ~450KB-900KB of SYNTH_MAX_BUFSIZE-sized local
arrays are now persistent per-voice buffers allocated once in
VoiceSetBufsize (freed in VoiceFree), matching the pattern already
used for pBuf_Q and by vcf.c/env.c/lfo.c, instead of being
reallocated on the stack on every audio block.
- SynthDebug uses vsnprintf instead of vsprintf, bounding writes to
its 1024-byte buffer.
Verified with clean debug and release builds (no new warnings) and a
live playback smoke test in Reaper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- handleController: bounds-check NRPN/CC controller ID before indexing
last_midiCC_info[] (NRPN IDs can reach 16383, array is sized 1024)
- JaySynthMidiCC::add/remove: also reject negative controller IDs coming
from patch/bank file data, not just out-of-range-high ones
- bankImportXml/loadPatchFromFile: null-check XML elements after
getFirstChildElement()/XmlDocument::parse() before dereferencing, so
malformed/truncated patch or bank files no longer crash on load
- loadBankFromFile/loadPatchFromFile: validate the loaded file is large
enough for the fxBank/fxProgram header, and that the embedded chunk
size fits within the file, before reading through the raw pointer
- processBlock/JaySynth::renderNextBlock: host blocks larger than
SYNTH_MAX_BUFSIZE no longer overflow the fixed-size stack work buffers
or deadlock the render worker threads; oversized blocks are now
rendered in multiple SYNTH_MAX_BUFSIZE-sized passes (pending MIDI
events are correctly held over between passes)
- JaySynth constructor: guard against createInputStream() returning null
when the wavetable file can't be opened
- JaySynth::setParameter/setControl/setPerVoiceControl/setParam/
ClearControls: take the synth's lock, matching renderNextBlock and the
note/controller handlers, to close a data race between parameter
changes (UI/host automation) and audio-thread rendering
Verified with a full rebuild (make) producing JaySynth.so with no new
warnings or errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
- added ability to import patch file with midicc defined without patch parameter
- fixed notes cut off by calling getStateInformation()
- disabled limiter
- increased version to v1.1.0-pre