diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bab9b30 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +JaySynth is a subtractive/wavetable software synthesizer built as a **VST2 audio plugin** using JUCE (bundled at `sdk/juce/JUCE-3.1.1`) and the Steinberg VST2.4 SDK (`sdk/vst/vstsdk2.4`). It targets Linux (`.so`) and Windows (`.dll`) hosts. Development/testing happens against the Carla plugin host (`~/jaysynth+tal.carxp`) and Reaper. + +## Build + +Building is plain GNU Make, driven by two submodule-provided include files (`submodule/make/defaults.mk`, `compile.mk`, `link.mk`, pulled in via `MAKE_HOME`). There is no CMake/autotools. + +```sh +make # build release .so into build/linux/release/ +make CONFIG=debug # debug build +make install # copies waves.bin + JaySynth.so into ~/.vst/jaysynth/ +make run # install, then launch Carla with the test project (~/jaysynth+tal.carxp) +make bear # distclean + rebuild via `bear` to regenerate compile_commands.json (used by clangd) +``` + +Windows build uses `Makefile.win` with the same `PACKAGES` structure but different tool names (`Source`, `Synth`, `Fir` targets instead of lowercase). + +Submodules must be checked out (`git submodule update --init`) before building: +- `submodule/make` — shared Makefile fragments (compiler flags, link rules) +- `submodule/fir` — FIR/resampling C library used by the synth core + +`sdk/juce` and `sdk/vst` are vendored SDKs, not submodules — they're expected to already exist at those paths. + +There are no automated tests in this repo; verification is manual, by building, installing, and playing the plugin in a host (Carla/Reaper — see `.vscode/launch.json` for debug configs). + +## Architecture + +The codebase has a hard split between a **portable C DSP core** and a **C++/JUCE plugin shell**, built as separate sub-Makefiles and linked together: + +- `src/synth/` — pure C DSP engine (no JUCE, no C++). Oscillators (`vco.c`, BLIT-based band-limited synthesis in `blit.c`), wavetables (`wavetable.c`, `mw_wave_data.c` — Waldorf Microwave/PPG-style wavetables), filter (`vcf.c`), envelopes (`env.c`), LFOs (`lfo.c`), noise (`noise.c`), and parameter scaling (`param_scale.c`). `synth_float_t` is a build-time typedef (`double` on Linux, `float` on Windows — set via `-Dsynth_float_t=...` in the Makefiles). + - `voice.h`/`voice.c` is the center of this layer: a `voice_t` (per-voice state: 2 VCOs, 4 LFOs, 4 envelopes, 1 VCF) driven by a shared `voice_common_t` (global LFO/env/VCF/noise tables and shared per-block buffers, to avoid recomputing identical modulation sources per voice). All per-voice parameters are addressed by the flat `VOICE_PARAM_*` enum in `voice.h`; modulation routing (FM/AM/PWM sources for oscillators, filter cutoff/Q sources) is expressed as source/amount/op triples pointing into `VOICE_MOD_SRC_*`. + - Voice audio rendering can run multi-threaded: `JaySynthThread` (in `src/plug/JaySynth.h`) splits the voice array across CPU cores, each thread calling `VoiceProcessDataV`. +- `src/plug/` — the JUCE `AudioProcessor`/`Synthesiser` plugin implementation, C++11. + - `PluginProcessor.{h,cpp}` (`JaySynthAudioProcessor`) is the VST entry point: parameter get/set, program (patch) management, and all patch/bank XML and VST `.fxp`/`.fxb` import/export. + - `JaySynth.{h,cpp}` (`class JaySynth : public Synthesiser`) owns the array of `voice_t` (via `JaySynthVoice`), MIDI note/CC/NRPN handling, humanize/unison/monophonic modes, and MIDI clock sync (`JK_MidiClock`). + - `JaySynthSound` is a patch: a flat `parameter[SYNTH_NUM_PARAMS]` array plus a `JaySynthMidiCC` (per-parameter MIDI CC/NRPN assignment table). `JaySynthAudioProcessor` holds `NUM_PROGRAMS` (64) of these plus a separate "current patch" edit buffer, and tracks a modified/"[edited]" state independent of the stored program. + - `JaySynthMidiCC`/`MidiNrpn`/`MidiCC_PopUp` implement per-parameter MIDI-learn: any synth parameter can be bound to a CC or NRPN controller, with absolute/relative modes. + - `JaySynthAudioProcessorEditor` + `graph_window`/`ButtonLED`/`VelKey_PopUp` are the custom GUI (no Projucer-managed component files beyond the `.jucer`'s single editor group — most UI is hand-written). + - Notifications from the audio/synth layer to the GUI go through `JaySynthActionListener` (a `JUCE::ActionBroadcaster` wrapping typed events like `JAYSYNTH_CHANGED_PARAM`, `JAYSYNTH_CHANGED_MIDICC`, `JAYSYNTH_CHANGED_PROGRAM` as encoded strings), not direct callbacks — needed because it crosses the audio-thread/message-thread boundary. +- `extras/waves/` — binary wavetable sets (Microwave/PPG format, 8/12-bit) that must be installed as `waves.bin` next to the plugin for the wavetable oscillator feature to activate (see `extras/waves.readme`). +- `extras/sounds/` — example patches/banks in both VST2 native format (`.fxp`/`.fxb`) and a custom XML format (`.xmp`/`.xmb`, patch/bank respectively) — these are the two import/export paths implemented in `PluginProcessor.cpp` (`patchImportXml`/`bankImportXml` vs. `setCurrentProgramStateInformation`/`setStateInformation`). +- `matlab/` — MATLAB/Octave prototyping scripts for the DSP algorithms (oscillator BLEP/BLIT, filter, envelope, LFO, limiter, param curves) — reference models for the C implementation, not part of the build. +- `doc/papers/` — background papers on the DSP techniques used (band-limited synthesis, wavetables). +- `history.txt` — the changelog (German/English mixed); check here before assuming a described bug is still open. +- `todo.txt` — open design/feature notes, also mixed German/English. + +## Conventions to be aware of + +- Per-voice DSP parameters are plain enums (`VOICE_PARAM_*`), not named struct fields — when adding a synth parameter, it must be threaded through the enum in `voice.h`, `param_scale.c`'s scaling table, and the plugin-side parameter name/XML (de)serialization in `PluginProcessor.cpp`. +- Modulation source enums are duplicated per destination (`VOICE_MOD_SRC_0_*` vs `VOICE_MOD_SRC_1_*` vs the general `VOICE_MOD_SRC_*`) because not all destinations can be modulated by all sources (e.g. VCO0 can't modulate itself) — keep this distinction when touching mod routing. +- Comments and log/history text are a German/English mix; don't assume English-only when grepping for TODOs or history entries. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0076495 --- /dev/null +++ b/TODO.md @@ -0,0 +1,37 @@ +# Hardening todo + +Findings from a code-review pass over `src/plug` and `src/synth` (2026-07-27, branch `hardening`). + +## Critical — crashes, memory corruption (fixed, commit `a65b71a`) + +- [x] **NRPN controller ID caused an out-of-bounds write on the audio thread.** `JaySynth::handleController` (`src/plug/JaySynth.cpp:899`) now bounds-checks `midiCC_info.ID` against `NUM_MIDI_CONTROLLERS` before indexing `last_midiCC_info[]` (NRPN IDs can reach 16383, array is sized 1024). +- [x] **Negative controller IDs from patch files could corrupt memory.** `JaySynthMidiCC::add`/`remove` (`src/plug/JaySynthMidiCC.cpp:433-460`) now also reject `< 0`, not just out-of-range-high. +- [x] **Null-pointer dereference on malformed/unparsable patch or bank files.** `bankImportXml` and the `.xmp` branch of `loadPatchFromFile` (`src/plug/PluginProcessor.cpp:860-885, 949-975`) now null-check XML elements after `getFirstChildElement()`/`XmlDocument::parse()` before dereferencing. +- [x] **No size validation before casting raw file bytes to `fxBank`/`fxProgram` structs.** `loadBankFromFile`/`loadPatchFromFile` (`src/plug/PluginProcessor.cpp:834-858, 930-957`) now check the loaded file is at least header-sized and that the embedded chunk size fits within it before reading through the raw pointer. +- [x] **Host block sizes larger than `SYNTH_MAX_BUFSIZE` (8192) caused a stack buffer overflow or a permanent deadlock.** `processBlock` (`src/plug/PluginProcessor.cpp:547`) now chunks rendering into `SYNTH_MAX_BUFSIZE`-sized passes instead of writing the host's full block size into a fixed 8192-sample stack buffer; `JaySynth::renderNextBlock` (`src/plug/JaySynth.cpp:977`) clamps the same way and correctly holds a pending MIDI event over between passes. +- [x] **Unchecked file stream could be null, crashing on plugin construction.** `JaySynth::JaySynth` (`src/plug/JaySynth.cpp:55-57`) now guards the wavetable file read against `createInputStream()` returning null. +- [x] **Parameter-changing calls raced with real-time audio rendering — no lock protected them.** `JaySynth::setParameter`/`setControl`/`setPerVoiceControl`/`setParam`/`ClearControls` (`src/plug/JaySynth.cpp`) now take `ScopedLock sl(lock)`, matching `renderNextBlock` and the note/controller handlers. +- [x] **Found while building `tools/testhost`'s regression suite for this list: the block-size fix above wasn't sufficient on its own.** `VCF_CalcCoeff_LPF`/`_HPF`/`_BPF` (`src/synth/vcf.c`) advance the coefficient pointer once per *(sample, filter-section)* pair - a 4th-order filter uses `FILTER_MAX_ORDER/2` = 2 cascaded sections per sample - but `VCF_SetBufsize` only allocated `pCoef` with `bufsize` entries, not `bufsize * sections`. Any host block between 4097 and 8192 samples (inclusive of the now-correctly-chunked range from the fix above) with a 4th-order filter selected wrote past the end of that buffer, corrupting adjacent heap memory (confirmed with AddressSanitizer: `heap-buffer-overflow`, `WRITE of size 8`, exactly 8 bytes past a 393216-byte/8192-`vcf_coef_t` allocation). Fixed by sizing the allocation for the worst case, `bufsize*(FILTER_MAX_ORDER/2)`. Verified with ASan across the full range (4097, 8192, 16384, 20000 samples) after the fix - zero errors. + +## High — real-time audio-thread safety (fixed) + +- [x] **Every note/CC/voice-count change allocated Strings and posted a message from the audio thread.** `synthChanged()` (`src/plug/PluginProcessor.cpp`) no longer calls `JaySynthActionListener::call*` directly from the audio thread. It now pushes a small POD event into a lock-free single-producer/single-consumer queue (JUCE's `AbstractFifo`, added to `PluginProcessor.h`), and the existing 10ms UI `Timer` (`timerCallback`/`dispatchUiEvents`) drains it on the message thread, where the String-building and `sendActionMessage` now safely happen. +- [x] **~450KB–900KB of stack arrays allocated per voice per audio block.** `VoiceProcessDataV`'s 11 `SYNTH_MAX_BUFSIZE`-sized locals (`src/synth/voice.c`) are now persistent per-voice buffers (`pBuf_VCO_fmout`, `pBuf_vco_pwm`, etc., declared in `voice.h`), allocated once in `VoiceSetBufsize`/freed in `VoiceFree` — same lifecycle/pattern already used by `pBuf_Q` and by `vcf.c`/`env.c`/`lfo.c`. +- [x] **`SynthDebug` used unbounded `vsprintf` into a fixed 1024-byte buffer.** `src/synth/synth_debug.c` now uses `vsnprintf` with the buffer size. (Left the blocking `puts()`/`OutputDebugString` I/O as-is — it's compiled out entirely unless `SYNTH_DEBUG` is defined, so release builds are unaffected, and a lock-free logging redesign felt like overreach for a debug-only path.) + +## Memory management (fixed) + +- [x] **`new[]`/`delete` mismatches (UB) in `JaySynth`'s destructor** — `m_pVoices`, `pPer_voice_controls`, `pCurrNoteInfos`, `humanize_voice_param[i]`, `ppHumanizedSliders[i]`, `m_ppAudioThread`, `m_ppEventAudioThreadRdy` (`src/plug/JaySynth.cpp`) all now freed with `delete[]` to match their `new T[n]` allocations. Also found and fixed the same pattern via `ScopedPointer` in `PluginProcessor.cpp` (`setStateInformation`/`setCurrentProgramStateInformation`) — `ScopedPointer` always calls scalar `delete` (per JUCE's own doc comment), so `ScopedPointer pUncompressedData = new char[...]` was the same bug; replaced with `HeapBlock`, JUCE's array-owning smart pointer. +- [x] **`malloc` was never null-checked** across the C DSP core. Added a shared `SynthCheckAlloc()` helper (`synth_defs.h`/`synth_debug.c`) that aborts with a clear diagnostic instead of returning NULL, and wrapped all 24 `malloc` call sites across `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 overhead. + +## Patch/Bank import-export (fixed) + +- [x] **Legacy patch import decoded the wrong XML node.** `patchImportXml` now handles 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. Also switched its CC-table import to `import_midi_cc` (matching the actually-exercised convention already used by `bankDecodeXml_legacy`, since this branch was previously unreachable dead code). `loadPatchFromFile`'s `.xmp` branch now also accepts legacy-rooted files, so single legacy patches can actually be loaded. Also fixed a related bug found in passing: `setCurrentProgramStateInformation` (VST2 "copy plugin state to another track") was passing `patchImportXml` an XML node one level too deep, making it a complete no-op. +- [x] **Found a much bigger bug while investigating "regular .fxb/.fxp silently ignored": the magic-number check itself was completely broken.** It compared `String((const char*)&bank->chunkMagic)` (raw file bytes as text, e.g. `"CcnK"`) against `String(cMagic)` (JUCE's `int`-to-*decimal-text* constructor, e.g. `"1130589771"`) — these can never be equal, so **the entire opaque `.fxb`/`.fxp` load path was dead code**, regardless of format. Fixed using `ByteOrder::bigEndianInt`, the correct byte-order-aware comparison (matches the pattern JUCE's own VST3 wrapper uses for the identical struct). "Regular" (non-chunk) format is still not supported for load, but now surfaces an `AlertWindow` explaining why instead of silently doing nothing. Verified with a standalone round-trip test of the binary format logic (magic detection, size/payload round-trip, safe rejection of truncated/bogus files). +- [x] **`.fxp`/`.fxb` saving was unimplemented.** Now implemented using the same opaque-chunk format the (now-fixed) load path expects, reusing `getCurrentProgramStateInformation`/`getStateInformation` for the payload. +- [x] **`patchDecodeXml`/`patchDecodeXml_legacy` duplication** — `_legacy` now just delegates to `patchDecodeXml`. + +## Design + +- [ ] **`JaySynth` is a god object** (`src/plug/JaySynth.h`) — owns parameters, MIDI CC/NRPN, MIDI clock, 3 humanize subsystems, unison, monophonic orchestration, voice stealing, and the thread pool, all via direct field access with no internal boundaries. This is very plausibly why the parameter-locking bug (now fixed) was missed in the first place — no seam enforces consistent locking discipline. Consider splitting into a handful of focused collaborators (e.g. MIDI dispatch, voice allocation, humanize/unison modes) that `JaySynth` composes. +- [ ] **`void *the_editor` (`src/plug/PluginProcessor.h:394`) duplicates JUCE's own editor tracking**, is never cleared when the editor is destroyed, and every other call site correctly uses `getEditor(this)` instead — currently harmless but a dangling-pointer trap. Fix: remove the field and route `createEditor()` through `getEditor(this)` like everywhere else. diff --git a/src/plug/JaySynth.cpp b/src/plug/JaySynth.cpp index 94424c7..3263409 100644 --- a/src/plug/JaySynth.cpp +++ b/src/plug/JaySynth.cpp @@ -53,7 +53,8 @@ JaySynth::JaySynth (int num_voices, String pathToWaves) { File wavesFile = File(pathToWaves); ScopedPointerpIn = wavesFile.createInputStream(); - pIn->read(synth_common.vco.wt.wave_rawdata, sizeof(synth_common.vco.wt.wave_rawdata)); + if (pIn != NULL) + pIn->read(synth_common.vco.wt.wave_rawdata, sizeof(synth_common.vco.wt.wave_rawdata)); } max_num_voices = num_voices; @@ -157,7 +158,7 @@ JaySynth::~JaySynth() { delete(m_ppAudioThread[i]); } - delete(m_ppAudioThread); + delete[] m_ppAudioThread; SynthDebug("delete params\n"); paramInfoFree(&pb_range[0]); @@ -170,25 +171,25 @@ JaySynth::~JaySynth() { paramInfoFree(&humanize_voice_param[i][j]); } - delete (humanize_voice_param[i]); - delete (ppHumanizedSliders[i]); + delete[] humanize_voice_param[i]; + delete[] ppHumanizedSliders[i]; } - delete (humanize_voice_param); - delete (ppHumanizedSliders); + delete[] humanize_voice_param; + delete[] ppHumanizedSliders; for (i = max_num_voices; --i >= 0;) { removeVoice(i); } - delete (pPer_voice_controls); - delete (pCurrNoteInfos); - delete (m_pVoices); + delete[] pPer_voice_controls; + delete[] pCurrNoteInfos; + delete[] m_pVoices; for (i=0; i < m_num_audiothreads; i++) { delete (m_ppEventAudioThreadRdy[i]); } - delete(m_ppEventAudioThreadRdy); + delete[] m_ppEventAudioThreadRdy; } @@ -379,6 +380,8 @@ void JaySynth::humanizeVarianceChanged_ENV(void) //============================================================================== void JaySynth::ClearControls(void) { + const ScopedLock sl (lock); + memset(controls, 0, SYNTH_NUM_PARAMS*sizeof(synth_float_t)); } @@ -428,6 +431,8 @@ synth_float_t JaySynth::getParameter(int paramID) void JaySynth::setParameter(int paramID, synth_float_t param) { + const ScopedLock sl (lock); + params[paramID] = param; setParam(paramID, -1); @@ -437,6 +442,8 @@ void JaySynth::setParameter(int paramID, synth_float_t param) void JaySynth::setControl(int paramID, synth_float_t param) { + const ScopedLock sl (lock); + controls[paramID] = param; setParam(paramID, -1); @@ -446,12 +453,16 @@ void JaySynth::setControl(int paramID, synth_float_t param) void JaySynth::setPerVoiceControl(int voice, int channel, int paramID, synth_float_t param) { + const ScopedLock sl (lock); + pPer_voice_controls[voice].ctrl[channel][paramID] = param; setParam(paramID, voice); } void JaySynth::setParam(int paramID, int voice) { + const ScopedLock sl (lock); + int i, j, voice_param_type, voice_min, voice_max; synth_float_t param; @@ -898,6 +909,9 @@ void JaySynth::handlePitchWheel (const int midiChannel, const int wheelValue) void JaySynth::handleController (const midiCC_info_t &midiCC_info) { + if (midiCC_info.ID < 0 || midiCC_info.ID >= NUM_MIDI_CONTROLLERS) + return; + last_midiCC_info[midiCC_info.ID] = midiCC_info; listeners.call (&JaySynthListener::synthChanged, SYNTH_CHANGED_MIDICC, (midiCC_info_t*)&midiCC_info); @@ -990,14 +1004,26 @@ void JaySynth::renderNextBlock (AudioSampleBuffer& outputBuffer, midiIterator.setNextSamplePosition (startSample); MidiMessage m (0xf4, 0.0); + bool havePendingEvent = false; + int midiEventPos = 0; + while (numSamples > 0) { - int midiEventPos; - const bool useEvent = midiIterator.getNextEvent (m, midiEventPos) + if (!havePendingEvent) + havePendingEvent = midiIterator.getNextEvent (m, midiEventPos); + + const bool useEvent = havePendingEvent && midiEventPos < startSample + numSamples; - const int numThisTime = useEvent ? midiEventPos - startSample - : numSamples; + int numThisTime = useEvent ? midiEventPos - startSample + : numSamples; + + // The DSP core's per-block work buffers are fixed at SYNTH_MAX_BUFSIZE + // samples, so render oversized ranges in multiple passes rather than + // overrunning them. The pending midi event (if any) stays queued until + // we actually reach its sample position. + if (numThisTime > SYNTH_MAX_BUFSIZE) + numThisTime = SYNTH_MAX_BUFSIZE; if (numThisTime > 0) { @@ -1035,9 +1061,12 @@ void JaySynth::renderNextBlock (AudioSampleBuffer& outputBuffer, } } } - if (useEvent) + // Only fire the pending event once we've actually rendered up to its + // sample position (numThisTime may have been clamped short of it above). + if (useEvent && numThisTime == midiEventPos - startSample) { handleMidiEvent (m); + havePendingEvent = false; } startSample += numThisTime; diff --git a/src/plug/JaySynthMidiCC.cpp b/src/plug/JaySynthMidiCC.cpp index 15914c9..99c5ad5 100644 --- a/src/plug/JaySynthMidiCC.cpp +++ b/src/plug/JaySynthMidiCC.cpp @@ -432,7 +432,7 @@ int JaySynthMidiCC::import_midi_cc_legacy(XmlElement const *pXML_MidiCc) void JaySynthMidiCC::add(midiCC_container_t *pObj, int controllerID) { - if (controllerID >= NUM_MIDI_CONTROLLERS) + if (controllerID < 0 || controllerID >= NUM_MIDI_CONTROLLERS) return; remove(pObj, pObj->controllerID); @@ -448,7 +448,7 @@ void JaySynthMidiCC::add(midiCC_container_t *pObj, int controllerID) void JaySynthMidiCC::remove(midiCC_container_t *pObj, int controllerID) { - if (controllerID >= NUM_MIDI_CONTROLLERS) + if (controllerID < 0 || controllerID >= NUM_MIDI_CONTROLLERS) return; pObj->isAssigned = 0; diff --git a/src/plug/PluginProcessor.cpp b/src/plug/PluginProcessor.cpp index d8b8eb6..02fcbac 100644 --- a/src/plug/PluginProcessor.cpp +++ b/src/plug/PluginProcessor.cpp @@ -8,6 +8,8 @@ ============================================================================== */ #include +#include +#include #include "PluginProcessor.h" #include "JaySynthAudioProcessorEditor.h" @@ -23,6 +25,7 @@ JaySynthAudioProcessorEditor* getEditor(JaySynthAudioProcessor *pThis) //============================================================================== JaySynthAudioProcessor::JaySynthAudioProcessor() +: m_uiEventFifo(uiEventQueueCapacity) { currProgram = 0; currpatch = &patches[currProgram]; @@ -56,10 +59,77 @@ JaySynthAudioProcessor::~JaySynthAudioProcessor() void JaySynthAudioProcessor::timerCallback() { + dispatchUiEvents(); + if (getEditor(this)) actionListener.callLimiterChanged(m_limiter_active); } +void JaySynthAudioProcessor::pushUiEvent(int type, int i1, int i2, float f1) +{ + int start1, size1, start2, size2; + m_uiEventFifo.prepareToWrite(1, start1, size1, start2, size2); + + if (size1 > 0) + { + SynthUiEvent &e = m_uiEventBuffer[start1]; + e.type = type; + e.i1 = i1; + e.i2 = i2; + e.f1 = f1; + m_uiEventFifo.finishedWrite(1); + } + // else: queue is full, drop the event rather than blocking the audio thread. +} + +void JaySynthAudioProcessor::dispatchUiEvent(const SynthUiEvent &e) +{ + switch (e.type) + { + case JaySynth::SYNTH_CHANGED_PARAM: + actionListener.callParamChanged(e.i1); + break; + + case JaySynth::SYNTH_CHANGED_MIDICC: + actionListener.callMidiControllerChanged(e.i1, e.i2, e.f1); + break; + + case JaySynth::SYNTH_CHANGED_NOTE_PRESSED: + actionListener.callMidiNotePressed(e.i1, e.f1); + break; + + case JaySynth::SYNTH_CHANGED_NOTE_RELEASED: + actionListener.callMidiNoteReleased(e.i1, e.f1); + break; + + case JaySynth::SYNTH_CHANGED_NUM_VOICES_PLAYING: + actionListener.callNumVoicesPlaying(e.i1); + break; + + case JaySynth::SYNTH_CHANGED_MIDICLOCK: + actionListener.callMidiClock((uint32_t)e.i1, (uint32_t)e.i2); + break; + + default: + break; + } +} + +void JaySynthAudioProcessor::dispatchUiEvents(void) +{ + int start1, size1, start2, size2; + m_uiEventFifo.prepareToRead(m_uiEventFifo.getNumReady(), start1, size1, start2, size2); + + int i; + for (i = 0; i < size1; i++) + dispatchUiEvent(m_uiEventBuffer[start1 + i]); + + for (i = 0; i < size2; i++) + dispatchUiEvent(m_uiEventBuffer[start2 + i]); + + m_uiEventFifo.finishedRead(size1 + size2); +} + String JaySynthAudioProcessor::wavesGetPath() { bool isExistent, isValid; @@ -148,37 +218,37 @@ void JaySynthAudioProcessor::synthChanged(int type, void *pData) { case JaySynth::SYNTH_CHANGED_PARAM: if (getEditor(this) != NULL) - actionListener.callParamChanged(*((int*)pData)); + pushUiEvent(type, *((int*)pData), 0, 0.0f); break; case JaySynth::SYNTH_CHANGED_MIDICC: pMidiCC_info = (JaySynth::midiCC_info_t*)pData; applyMidiController(pMidiCC_info, true); if (getEditor(this) != NULL) - actionListener.callMidiControllerChanged(pMidiCC_info->channel, pMidiCC_info->ID, (float)pMidiCC_info->value); + pushUiEvent(type, pMidiCC_info->channel, pMidiCC_info->ID, (float)pMidiCC_info->value); break; case JaySynth::SYNTH_CHANGED_NOTE_PRESSED: pMidi_note_info = (JaySynth::midi_note_info_t*)pData; if (getEditor(this) != NULL) - actionListener.callMidiNotePressed(pMidi_note_info->note, (float)pMidi_note_info->velocity); + pushUiEvent(type, pMidi_note_info->note, 0, (float)pMidi_note_info->velocity); break; case JaySynth::SYNTH_CHANGED_NOTE_RELEASED: pMidi_note_info = (JaySynth::midi_note_info_t*)pData; if (getEditor(this) != NULL) - actionListener.callMidiNoteReleased(pMidi_note_info->note, (float)pMidi_note_info->velocity); + pushUiEvent(type, pMidi_note_info->note, 0, (float)pMidi_note_info->velocity); break; case JaySynth::SYNTH_CHANGED_NUM_VOICES_PLAYING: if (getEditor(this) != NULL) - actionListener.callNumVoicesPlaying(*((int*)pData)); + pushUiEvent(type, *((int*)pData), 0, 0.0f); break; case JaySynth::SYNTH_CHANGED_MIDICLOCK: pMidi_quarter_info = (JaySynth::midi_quarter_clock_info_t*)pData; if (getEditor(this) != NULL) - actionListener.callMidiClock(pMidi_quarter_info->bpm, pMidi_quarter_info->type); + pushUiEvent(type, (int)pMidi_quarter_info->bpm, (int)pMidi_quarter_info->type, 0.0f); break; default: @@ -546,18 +616,16 @@ void JaySynthAudioProcessor::releaseResources() void JaySynthAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) { const int numSamples = buffer.getNumSamples(); - int i; float *buf1 = buffer.getWritePointer (0, 0); float *buf2 = buffer.getWritePointer (1, 0); - synth_float_t buf[2][SYNTH_MAX_BUFSIZE]; float synth_gain; AudioPlayHead::CurrentPositionInfo posInfo; getPlayHead()->getCurrentPosition(posInfo); - + // Parse midi buffer and add midi clock messages m_JK_MidiClock.generateMidiclock(posInfo, &midiMessages, numSamples, getSampleRate()); - + // Now pass any incoming midi messages to our keyboard state object, and let it // add messages to the buffer if the user is clicking on the on-screen keys keyboardState.processNextMidiBuffer (midiMessages, 0, numSamples, true); @@ -566,78 +634,88 @@ void JaySynthAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer buffer.clear (0, 0, numSamples); buffer.clear (1, 0, numSamples); - // and now get the synth to process these midi events and generate its output. - // Render individual voices - pSynth->renderNextBlock (buffer, midiMessages, 0, numSamples); - // apply master volume synth_gain = (float)pSynth->getVolume(); - for (i=0; i m_limiter_env) + const int chunkLen = jsy_min(numSamples - chunkStart, (int)SYNTH_MAX_BUFSIZE); + synth_float_t buf[2][SYNTH_MAX_BUFSIZE]; + int i; + + // and now get the synth to process these midi events and generate its output. + // Render individual voices + pSynth->renderNextBlock (buffer, midiMessages, chunkStart, chunkLen); + + for (i=0; i m_limiter_env) + { + m_limiter_env = (1.0-limiter_ar)*m_limiter_env + limiter_ar*x; + } + else + { + m_limiter_env = (1.0-limiter_af)*m_limiter_env; + } + + limiter_env[i] = m_limiter_env; } - limiter_env[i] = m_limiter_env; - } - - for (i=0; i setName(xml.getStringAttribute("NAME")); - for (int i=0; i < SYNTH_NUM_PARAMS; i++) - { - synth_float_t value = (synth_float_t) xml.getDoubleAttribute (getParameterNameXML(i), patch->getParameter(i)); - if (i==SYNTH_PARAM_TUNE) - { - if (value > 400.0) - { - value = 1200*log(value/440)/log(2.0); - } - } - patch->setParameter(i, value); - } + // Legacy format uses the same per-parameter attribute layout as the + // current format, just addressed via a reference instead of a pointer. + patchDecodeXml(&xml, patch); } void JaySynthAudioProcessor::bankEncodeXml(XmlElement *xml) @@ -838,16 +905,34 @@ void JaySynthAudioProcessor::loadBankFromFile (const File &file) { MemoryBlock mem; file.loadFileAsData(mem); + + const size_t headerSize = offsetof(fxBank, content) + sizeof(VstInt32); + if (mem.getSize() < headerSize) + return; + fxBank *bank = (fxBank*)mem.getData(); - if (String((const char*)&bank->chunkMagic) == String(cMagic)) + // fxb/fxp magic numbers and sizes are stored big-endian on disk (see + // vstfxstore.h); bigEndianInt() reads them correctly regardless of + // host byte order. + if (ByteOrder::bigEndianInt(&bank->chunkMagic) == (uint32_t)cMagic) { - bool is_regular = String((const char*)&bank->fxMagic) == String(bankMagic); - bool is_opaque = String((const char*)&bank->fxMagic) == String(chunkBankMagic); + bool is_regular = ByteOrder::bigEndianInt(&bank->fxMagic) == (uint32_t)bankMagic; + bool is_opaque = ByteOrder::bigEndianInt(&bank->fxMagic) == (uint32_t)chunkBankMagic; if (is_opaque) { - uint32_t size = ByteOrder::swap((uint32_t)bank->content.data.size); + uint32_t size = ByteOrder::bigEndianInt(&bank->content.data.size); + const size_t chunkOffset = offsetof(fxBank, content.data.chunk); + if (chunkOffset > mem.getSize() || (size_t)size > mem.getSize() - chunkOffset) + return; setStateInformation(bank->content.data.chunk, (int)size); } + else if (is_regular) + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "Load bank", + "This .fxb file uses the \"regular\" (non-chunk) VST2 bank format, " + "which JaySynth doesn't support. Only chunk-based .fxb files " + "(as saved by JaySynth or other JUCE-based plugins) can be loaded."); + } } } else if (String(".xmb") == ext) @@ -864,7 +949,7 @@ void JaySynthAudioProcessor::bankImportXml (const XmlElement *xml) if (xml->hasTagName ("JSynth")) { XmlElement *xml2 = xml->getFirstChildElement(); - if (xml2->hasTagName ("Bank")) + if (xml2 != 0 && xml2->hasTagName ("Bank")) { XmlElement *xml3 = xml2->getFirstChildElement(); bankDecodeXml(xml3); @@ -876,8 +961,9 @@ void JaySynthAudioProcessor::bankImportXml (const XmlElement *xml) if (xml->hasTagName ("JSYNTH_BANK")) { XmlElement *xml2 = xml->getFirstChildElement(); - bankDecodeXml_legacy(*xml2); - + if (xml2 != 0) + bankDecodeXml_legacy(*xml2); + } } setCurrentProgram(getCurrentProgram()); @@ -886,7 +972,7 @@ void JaySynthAudioProcessor::bankImportXml (const XmlElement *xml) void JaySynthAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { - ScopedPointer pUncompressedData = new char[8192*1024]; + HeapBlock pUncompressedData (8192*1024); MemoryInputStream inputStream(data, sizeInBytes, false); GZIPDecompressorInputStream GZIPDec(inputStream); @@ -934,22 +1020,40 @@ void JaySynthAudioProcessor::loadPatchFromFile (const File &file, int index) { MemoryBlock mem; file.loadFileAsData(mem); + + const size_t headerSize = offsetof(fxProgram, content) + sizeof(VstInt32); + if (mem.getSize() < headerSize) + return; + fxProgram *program = (fxProgram*)mem.getData(); - if (String((const char*)&program->chunkMagic) == String(cMagic)) + // fxb/fxp magic numbers and sizes are stored big-endian on disk (see + // vstfxstore.h); bigEndianInt() reads them correctly regardless of + // host byte order. + if (ByteOrder::bigEndianInt(&program->chunkMagic) == (uint32_t)cMagic) { - bool is_regular = String((const char*)&program->fxMagic) == String(fMagic); - bool is_opaque = String((const char*)&program->fxMagic) == String(chunkPresetMagic); + bool is_regular = ByteOrder::bigEndianInt(&program->fxMagic) == (uint32_t)fMagic; + bool is_opaque = ByteOrder::bigEndianInt(&program->fxMagic) == (uint32_t)chunkPresetMagic; if (is_opaque) { - uint32_t size = ByteOrder::swap((uint32_t)program->content.data.size); + uint32_t size = ByteOrder::bigEndianInt(&program->content.data.size); + const size_t chunkOffset = offsetof(fxProgram, content.data.chunk); + if (chunkOffset > mem.getSize() || (size_t)size > mem.getSize() - chunkOffset) + return; setCurrentProgramStateInformation(program->content.data.chunk, (int)size); } + else if (is_regular) + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "Load patch", + "This .fxp file uses the \"regular\" (non-chunk) VST2 patch format, " + "which JaySynth doesn't support. Only chunk-based .fxp files " + "(as saved by JaySynth or other JUCE-based plugins) can be loaded."); + } } } else if (String(".xmp") == ext) { ScopedPointer xml = XmlDocument::parse(file); - if (xml->hasTagName ("JSynth")) + if (xml != 0 && (xml->hasTagName ("JSynth") || xml->hasTagName ("JSYNTH_PATCH"))) { patchImportXml(xml, currpatch); } @@ -958,6 +1062,21 @@ void JaySynthAudioProcessor::loadPatchFromFile (const File &file, int index) void JaySynthAudioProcessor::patchImportXml (const XmlElement *xml, JaySynthSound *patch) { + // Legacy single-patch format: parameters are attributes directly on the + // root element, not nested under a "Patch" child - handle it separately + // rather than inside the per-child loop below (xml has no children with + // tag "Patch"/"VelKey"/"MidiCC" in this format, so it doesn't fit that + // loop's shape). Mirrors how bankDecodeXml_legacy decodes each legacy + // bank entry. + if (xml->hasTagName ("JSYNTH_PATCH")) + { + patchDecodeXml_legacy(*xml, patch); + XmlElement *pXML_MidiCc = xml->getChildByName ("JSYNTH_MIDICONTROLLER_TABLE"); + ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc(pXML_MidiCc); + setCurrentProgram(getCurrentProgram()); + return; + } + XmlElement *xml2 = xml->getFirstChildElement(); while(xml2) { @@ -970,20 +1089,13 @@ void JaySynthAudioProcessor::patchImportXml (const XmlElement *xml, JaySynthSoun ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc_legacy(pXML_MidiCc); } } - else if (xml->hasTagName ("JSYNTH_PATCH")) - { - patchDecodeXml_legacy(*xml, patch); - XmlElement *pXML_MidiCc = xml->getChildByName ("JSYNTH_MIDICONTROLLER_TABLE"); - ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc_legacy(pXML_MidiCc); - - } else if (xml2->hasTagName ("VelKey")) { - ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_velkey(xml->getChildByName ("VelKey")); + ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_velkey(xml2); } else if (xml2->hasTagName ("MidiCC")) { - ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc(xml->getChildByName ("MidiCC")); + ((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc(xml2); } xml2 = xml2->getNextElement(); }; @@ -999,8 +1111,7 @@ void JaySynthAudioProcessor::setCurrentProgramStateInformation (const void* data void* pData; int size; - ScopedPointer pUncompressedData; - pUncompressedData = new char[2*65536]; + HeapBlock pUncompressedData (2*65536); MemoryInputStream inputStream(data, sizeInBytes, false); GZIPDecompressorInputStream GZIPDec(inputStream); @@ -1018,8 +1129,10 @@ void JaySynthAudioProcessor::setCurrentProgramStateInformation (const void* data { if (xml->hasTagName ("JSynth")) { - XmlElement *xml2 = xml->getFirstChildElement(); - patchImportXml(xml2, currpatch); + // xml is already the "JSynth" container (Patch/VelKey/MidiCC + // children) - patchImportXml expects that container, not one of + // its children. + patchImportXml(xml, currpatch); } } } @@ -1029,7 +1142,34 @@ void JaySynthAudioProcessor::savePatchToFile (const File &file, int index) String ext = file.getFileExtension(); if (String(".fxp") == ext) { - // ToDo: implement fxp save + commitEditBuffer(); + + // Reuse the same opaque chunk format used for the host's own VST2 + // "current program" state (and expected back by the .fxp opaque-chunk + // branch of loadPatchFromFile). + MemoryBlock chunk; + getCurrentProgramStateInformation(chunk); + + const size_t headerSize = offsetof(fxProgram, content) + sizeof(VstInt32); + MemoryBlock mem(headerSize + chunk.getSize(), true); + fxProgram *program = (fxProgram*)mem.getData(); + + program->chunkMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)cMagic); + program->byteSize = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)(mem.getSize() - 8)); + program->fxMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunkPresetMagic); + program->version = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)1); + program->fxID = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VSTUniqueID); + program->fxVersion = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VersionCode); + program->numParams = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)SYNTH_NUM_PARAMS); + + const String name = currpatch->getName(); + memcpy(program->prgName, name.toRawUTF8(), + jmin(sizeof(program->prgName) - 1, (size_t)name.getNumBytesAsUTF8())); + + program->content.data.size = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunk.getSize()); + memcpy(program->content.data.chunk, chunk.getData(), chunk.getSize()); + + file.replaceWithData(mem.getData(), mem.getSize()); } else if (String(".xmp") == ext) { @@ -1051,7 +1191,30 @@ void JaySynthAudioProcessor::saveBankToFile (const File &file) String ext = file.getFileExtension(); if (String(".fxb") == ext) { - // ToDo: implement fxp save + commitEditBuffer(); + + // Reuse the same opaque chunk format used for the host's own VST2 + // bank state (and expected back by the .fxb opaque-chunk branch of + // loadBankFromFile). + MemoryBlock chunk; + getStateInformation(chunk); + + const size_t headerSize = offsetof(fxBank, content) + sizeof(VstInt32); + MemoryBlock mem(headerSize + chunk.getSize(), true); + fxBank *bank = (fxBank*)mem.getData(); + + bank->chunkMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)cMagic); + bank->byteSize = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)(mem.getSize() - 8)); + bank->fxMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunkBankMagic); + bank->version = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)1); + bank->fxID = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VSTUniqueID); + bank->fxVersion = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VersionCode); + bank->numPrograms = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)NUM_PROGRAMS); + + bank->content.data.size = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunk.getSize()); + memcpy(bank->content.data.chunk, chunk.getData(), chunk.getSize()); + + file.replaceWithData(mem.getData(), mem.getSize()); } else if (String(".xmb") == ext) { diff --git a/src/plug/PluginProcessor.h b/src/plug/PluginProcessor.h index 449d8a6..f2f6234 100644 --- a/src/plug/PluginProcessor.h +++ b/src/plug/PluginProcessor.h @@ -396,7 +396,29 @@ private: int m_limiter_active; synth_float_t m_limiter_env; JK_MidiClock m_JK_MidiClock; - + + // synthChanged() runs on the audio thread. Building the JaySynthActionListener + // Strings and posting them there (as JAYSYNTH_CHANGED_* used to do directly) + // allocates and locks on the audio thread. Instead, synthChanged() pushes a + // small POD event into this lock-free single-producer (audio thread) / + // single-consumer (message-thread timer) queue, and timerCallback() drains it + // and does the actual String-building/sendActionMessage on the message thread. + struct SynthUiEvent + { + int type; + int i1; + int i2; + float f1; + }; + + enum { uiEventQueueCapacity = 256 }; + SynthUiEvent m_uiEventBuffer[uiEventQueueCapacity]; + AbstractFifo m_uiEventFifo; + + void pushUiEvent(int type, int i1, int i2, float f1); + void dispatchUiEvent(const SynthUiEvent &e); + void dispatchUiEvents(void); + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JaySynthAudioProcessor); }; diff --git a/src/synth/blit.c b/src/synth/blit.c index dee60d7..92f9093 100644 --- a/src/synth/blit.c +++ b/src/synth/blit.c @@ -139,7 +139,7 @@ void BLIT_ModInit(blit_common_t *pCom) synth_float_t x, dx, a, b, blit, blep; // Calculate table sizes - pCom->pTableSizes = (UINT32*)malloc(BLIT_NUM_HARM_MAX*sizeof(UINT32)); + pCom->pTableSizes = (UINT32*)SynthCheckAlloc(malloc(BLIT_NUM_HARM_MAX*sizeof(UINT32))); for (m=0; m < BLIT_NUM_HARM_MAX; m++) { pCom->pTableSizes[m] = m*BLIT_TABLE_OVERSAMPLING; @@ -148,18 +148,18 @@ void BLIT_ModInit(blit_common_t *pCom) } // Generate Kaiser window - ppKaiser = (synth_float_t**)malloc(BLIT_NUM_HARM_MAX*sizeof(synth_float_t*)); + ppKaiser = (synth_float_t**)SynthCheckAlloc(malloc(BLIT_NUM_HARM_MAX*sizeof(synth_float_t*))); for (m=0; m < BLIT_NUM_HARM_MAX; m++) { - ppKaiser[m] = (synth_float_t*)malloc(pCom->pTableSizes[m]*sizeof(synth_float_t)); + ppKaiser[m] = (synth_float_t*)SynthCheckAlloc(malloc(pCom->pTableSizes[m]*sizeof(synth_float_t))); CalcKaiser(NULL, ppKaiser[m], 8.f, pCom->pTableSizes[m]); } // Allocate BLIT table - pCom->ppBLEP = (synth_float_t**)malloc(BLIT_NUM_HARM_MAX*sizeof(synth_float_t*)); + pCom->ppBLEP = (synth_float_t**)SynthCheckAlloc(malloc(BLIT_NUM_HARM_MAX*sizeof(synth_float_t*))); for (m=0; m < BLIT_NUM_HARM_MAX; m++) { - pCom->ppBLEP[m] = (synth_float_t*)malloc(pCom->pTableSizes[m]*sizeof(synth_float_t)); + pCom->ppBLEP[m] = (synth_float_t*)SynthCheckAlloc(malloc(pCom->pTableSizes[m]*sizeof(synth_float_t))); } // Generate BLIT diff --git a/src/synth/env.c b/src/synth/env.c index a0f945b..cb56be4 100644 --- a/src/synth/env.c +++ b/src/synth/env.c @@ -43,7 +43,7 @@ void ENV_SetBufsize(envgen_t *pObj, UINT32 size) if (!size) return; - pObj->pOut = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t)); + pObj->pOut = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); } void ENV_SetFS(envgen_t *pObj, synth_float_t fs) diff --git a/src/synth/lfo.c b/src/synth/lfo.c index 64134c1..2197327 100644 --- a/src/synth/lfo.c +++ b/src/synth/lfo.c @@ -276,9 +276,9 @@ void LFO_SetBufsize(lfo_t *pObj, UINT32 size) if (!size) return; - pObj->pOut = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t)); - pObj->pPhase = (sync_result_t*)malloc(pObj->bufsize*sizeof(sync_result_t)); - pObj->pWave = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t)); + pObj->pOut = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pPhase = (sync_result_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(sync_result_t))); + pObj->pWave = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); } diff --git a/src/synth/param_scale.c b/src/synth/param_scale.c index ef5b9c3..9518523 100644 --- a/src/synth/param_scale.c +++ b/src/synth/param_scale.c @@ -986,7 +986,7 @@ void paramInfoInit(param_info_t *pObj, UINT32 id, const char *pName) pObj->pName = NULL; if (pName) { - pObj->pName = (char*)malloc(strlen(pName)+1); + pObj->pName = (char*)SynthCheckAlloc(malloc(strlen(pName)+1)); memcpy(pObj->pName, pName, strlen(pName)+1); } diff --git a/src/synth/synth_debug.c b/src/synth/synth_debug.c index 475eabc..e2f04e7 100644 --- a/src/synth/synth_debug.c +++ b/src/synth/synth_debug.c @@ -6,6 +6,7 @@ #endif #include #include +#include // -------------------------------------------------------------- // internal funcs @@ -14,6 +15,17 @@ // Exported functions // -------------------------------------------------------------- +void *SynthCheckAlloc(void *ptr) +{ + if (!ptr) + { + fprintf(stderr, "JaySynth: out of memory, aborting.\n"); + abort(); + } + + return ptr; +} + void SynthDebug(const char *fmtstr, ...) { #ifndef SYNTH_DEBUG @@ -27,7 +39,7 @@ void SynthDebug(const char *fmtstr, ...) va_list args; va_start(args, fmtstr); - vsprintf(buf, fmtstr, args); + vsnprintf(buf, sizeof(buf), fmtstr, args); va_end(args); #ifdef WIN32 OutputDebugString(buf); diff --git a/src/synth/synth_defs.h b/src/synth/synth_defs.h index 2e39060..c172569 100644 --- a/src/synth/synth_defs.h +++ b/src/synth/synth_defs.h @@ -55,6 +55,11 @@ extern "C" { #endif void SynthDebug(const char *fmtstr, ...); + +// Checks the result of a malloc() call. Aborts with a diagnostic instead of +// returning NULL, so an allocation failure never turns into a silent +// null-pointer dereference deep inside DSP processing. +void *SynthCheckAlloc(void *ptr); #if defined(__cplusplus) } #endif diff --git a/src/synth/vcf.c b/src/synth/vcf.c index 855091c..869f38b 100644 --- a/src/synth/vcf.c +++ b/src/synth/vcf.c @@ -55,8 +55,8 @@ void VCF_ModInit(vcf_t *pObj) { vcf_common_t *pCom = pObj->pCom; - pCom->pLUT_cos = (synth_float_t*)malloc(FILTER_TABLE_SIZE*sizeof(synth_float_t)); - pCom->pLUT_sin = (synth_float_t*)malloc(FILTER_TABLE_SIZE*sizeof(synth_float_t)); + pCom->pLUT_cos = (synth_float_t*)SynthCheckAlloc(malloc(FILTER_TABLE_SIZE*sizeof(synth_float_t))); + pCom->pLUT_sin = (synth_float_t*)SynthCheckAlloc(malloc(FILTER_TABLE_SIZE*sizeof(synth_float_t))); } void VCF_ModFree(vcf_t *pObj) @@ -141,8 +141,12 @@ void VCF_SetBufsize(vcf_t *pObj, UINT32 size) if (!size) return; - pObj->pOut = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t)); - pObj->pCoef = (vcf_coef_t*)malloc(pObj->bufsize*sizeof(vcf_coef_t)); + pObj->pOut = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + // VCF_CalcCoeff_*() advances the coefficient pointer once per + // (sample, filter-section) pair - a 4th-order filter uses + // FILTER_MAX_ORDER/2 cascaded 2nd-order sections per sample, so the + // buffer needs that many times bufsize entries, not just bufsize. + pObj->pCoef = (vcf_coef_t*)SynthCheckAlloc(malloc(pObj->bufsize*(FILTER_MAX_ORDER/2)*sizeof(vcf_coef_t))); pObj->pCoeff_last = pObj->pCoef; VCF_Coeff_update(pObj); diff --git a/src/synth/vco.c b/src/synth/vco.c index 62af8d3..bf17156 100644 --- a/src/synth/vco.c +++ b/src/synth/vco.c @@ -87,7 +87,7 @@ void VCO_SetBufsize(osc_t *pObj, UINT32 size) if (!size) return; - pObj->pOut = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t)); + pObj->pOut = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); } diff --git a/src/synth/voice.c b/src/synth/voice.c index eae76a0..7f63dfb 100644 --- a/src/synth/voice.c +++ b/src/synth/voice.c @@ -121,6 +121,21 @@ void VoiceInit(voice_t *pObj, voice_common_t *pCom, UINT32 id, synth_float_t fs) pObj->pBuf_Q = NULL; + for (i = 0; i < (INT32)VOICE_NUM_OSC; i++) + { + pObj->pBuf_VCO_fmout[i] = NULL; + pObj->pBuf_vco_pitch[i] = NULL; + pObj->pBuf_osc_out_smoothed[i] = NULL; + } + pObj->pBuf_vco_pwm = NULL; + pObj->pBuf_vco_fm = NULL; + pObj->pBuf_vco_am = NULL; + pObj->pBuf_vcf_fm = NULL; + pObj->pBuf_vcf_qm = NULL; + pObj->pBuf_vca_am = NULL; + pObj->pBuf_vca_pan = NULL; + pObj->pBuf_osc_out = NULL; + VoiceSetBufsize(pObj, SYNTH_MAX_BUFSIZE); } @@ -249,11 +264,65 @@ void VoiceSetBufsize(voice_t *pObj, UINT32 size) pObj->pBuf_Q = NULL; + for (i = 0; i < (INT32)VOICE_NUM_OSC; i++) + { + if (pObj->pBuf_VCO_fmout[i]) + free(pObj->pBuf_VCO_fmout[i]); + pObj->pBuf_VCO_fmout[i] = NULL; + + if (pObj->pBuf_vco_pitch[i]) + free(pObj->pBuf_vco_pitch[i]); + pObj->pBuf_vco_pitch[i] = NULL; + + if (pObj->pBuf_osc_out_smoothed[i]) + free(pObj->pBuf_osc_out_smoothed[i]); + pObj->pBuf_osc_out_smoothed[i] = NULL; + } + + if (pObj->pBuf_vco_pwm) free(pObj->pBuf_vco_pwm); + pObj->pBuf_vco_pwm = NULL; + + if (pObj->pBuf_vco_fm) free(pObj->pBuf_vco_fm); + pObj->pBuf_vco_fm = NULL; + + if (pObj->pBuf_vco_am) free(pObj->pBuf_vco_am); + pObj->pBuf_vco_am = NULL; + + if (pObj->pBuf_vcf_fm) free(pObj->pBuf_vcf_fm); + pObj->pBuf_vcf_fm = NULL; + + if (pObj->pBuf_vcf_qm) free(pObj->pBuf_vcf_qm); + pObj->pBuf_vcf_qm = NULL; + + if (pObj->pBuf_vca_am) free(pObj->pBuf_vca_am); + pObj->pBuf_vca_am = NULL; + + if (pObj->pBuf_vca_pan) free(pObj->pBuf_vca_pan); + pObj->pBuf_vca_pan = NULL; + + if (pObj->pBuf_osc_out) free(pObj->pBuf_osc_out); + pObj->pBuf_osc_out = NULL; + if (!size) return; - pObj->pBuf_Q = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t)); + pObj->pBuf_Q = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + for (i = 0; i < (INT32)VOICE_NUM_OSC; i++) + { + pObj->pBuf_VCO_fmout[i] = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vco_pitch[i] = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_osc_out_smoothed[i] = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + } + + pObj->pBuf_vco_pwm = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vco_fm = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vco_am = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vcf_fm = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vcf_qm = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vca_am = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_vca_pan = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); + pObj->pBuf_osc_out = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); } void VoiceEvent(voice_t *pObj, UINT32 type) @@ -844,18 +913,27 @@ void VoiceProcessDataV(voice_t *pObj, float *pOut1, float *pOut2, UINT32 len) synth_float_t *pModSrc[VOICE_NUM_MOD_SOURCES]; voice_common_t *pCom = pObj->pCom; - // Temp Variables - synth_float_t VCO_fmout[VOICE_NUM_OSC][SYNTH_MAX_BUFSIZE]; - synth_float_t vco_pwm[SYNTH_MAX_BUFSIZE]; - synth_float_t vco_fm[SYNTH_MAX_BUFSIZE]; - synth_float_t vco_am[SYNTH_MAX_BUFSIZE]; - synth_float_t vcf_fm[SYNTH_MAX_BUFSIZE]; - synth_float_t vcf_qm[SYNTH_MAX_BUFSIZE]; - synth_float_t vca_am[SYNTH_MAX_BUFSIZE]; - synth_float_t vca_pan[SYNTH_MAX_BUFSIZE]; - synth_float_t vco_pitch[VOICE_NUM_OSC][SYNTH_MAX_BUFSIZE]; - synth_float_t osc_out[SYNTH_MAX_BUFSIZE]; - synth_float_t osc_out_smoothed[VOICE_NUM_OSC][SYNTH_MAX_BUFSIZE]; + // Temp Variables - these point at persistent per-voice buffers (allocated + // in VoiceSetBufsize) rather than being call-local arrays, to avoid + // ~450KB-900KB of stack allocation on every audio block. + synth_float_t *VCO_fmout[VOICE_NUM_OSC]; + synth_float_t *vco_pwm = pObj->pBuf_vco_pwm; + synth_float_t *vco_fm = pObj->pBuf_vco_fm; + synth_float_t *vco_am = pObj->pBuf_vco_am; + synth_float_t *vcf_fm = pObj->pBuf_vcf_fm; + synth_float_t *vcf_qm = pObj->pBuf_vcf_qm; + synth_float_t *vca_am = pObj->pBuf_vca_am; + synth_float_t *vca_pan = pObj->pBuf_vca_pan; + synth_float_t *vco_pitch[VOICE_NUM_OSC]; + synth_float_t *osc_out = pObj->pBuf_osc_out; + synth_float_t *osc_out_smoothed[VOICE_NUM_OSC]; + + for (i = 0; i < VOICE_NUM_OSC; i++) + { + VCO_fmout[i] = pObj->pBuf_VCO_fmout[i]; + vco_pitch[i] = pObj->pBuf_vco_pitch[i]; + osc_out_smoothed[i] = pObj->pBuf_osc_out_smoothed[i]; + } #if 0 // Process parameter smoothing diff --git a/src/synth/voice.h b/src/synth/voice.h index babd62a..d4246dd 100644 --- a/src/synth/voice.h +++ b/src/synth/voice.h @@ -305,6 +305,20 @@ typedef struct _svoice_t smooth_t smooth_vco_enable[VOICE_NUM_OSC]; smooth_t smooth_vco_portamento[VOICE_NUM_OSC]; synth_float_t vco_pitch[VOICE_NUM_OSC]; + // Scratch buffers used only within VoiceProcessDataV (voice.c), allocated + // once per voice (see VoiceSetBufsize) instead of as call-local arrays, to + // avoid ~450KB-900KB of stack allocation on every audio block. + synth_float_t *pBuf_VCO_fmout[VOICE_NUM_OSC]; + synth_float_t *pBuf_vco_pwm; + synth_float_t *pBuf_vco_fm; + synth_float_t *pBuf_vco_am; + synth_float_t *pBuf_vcf_fm; + synth_float_t *pBuf_vcf_qm; + synth_float_t *pBuf_vca_am; + synth_float_t *pBuf_vca_pan; + synth_float_t *pBuf_vco_pitch[VOICE_NUM_OSC]; + synth_float_t *pBuf_osc_out; + synth_float_t *pBuf_osc_out_smoothed[VOICE_NUM_OSC]; // smooth_t *pParam_smoother[VOICE_NUM_PARAMS]; // synth_float_t param_smoothed[VOICE_NUM_PARAMS][SYNTH_MAX_BUFSIZE]; } voice_t; diff --git a/src/synth/wavetable.c b/src/synth/wavetable.c index 28df102..7158202 100644 --- a/src/synth/wavetable.c +++ b/src/synth/wavetable.c @@ -173,10 +173,10 @@ void WT_ModInit(wt_common_t *pCom) // Allocate memory for (i=0; i < WT_NUM_WAVETABLES; i++) { - pCom->ppWavetables[i] = (synth_float_t**)malloc(WT_WAVETABLE_NUM_ENTRIES*sizeof(synth_float_t*)); + pCom->ppWavetables[i] = (synth_float_t**)SynthCheckAlloc(malloc(WT_WAVETABLE_NUM_ENTRIES*sizeof(synth_float_t*))); for (j=0; j < WT_WAVETABLE_NUM_ENTRIES; j++) { - pCom->ppWavetables[i][j] = (synth_float_t*)malloc(WT_WAVE_SIZE*sizeof(synth_float_t)); + pCom->ppWavetables[i][j] = (synth_float_t*)SynthCheckAlloc(malloc(WT_WAVE_SIZE*sizeof(synth_float_t))); } }