Hardening pass: critical/high/memory/patch-bank fixes + VCF buffer overflow #2

Merged
jens merged 7 commits from hardening into master 2026-07-27 20:44:34 +02:00
17 changed files with 577 additions and 157 deletions
+56
View File
@@ -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.
+37
View File
@@ -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] **~450KB900KB 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<char> pUncompressedData = new char[...]` was the same bug; replaced with `HeapBlock<char>`, 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.
+44 -15
View File
@@ -53,7 +53,8 @@ JaySynth::JaySynth (int num_voices, String pathToWaves)
{
File wavesFile = File(pathToWaves);
ScopedPointer<FileInputStream>pIn = 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;
+2 -2
View File
@@ -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;
+271 -108
View File
@@ -8,6 +8,8 @@
==============================================================================
*/
#include <math.h>
#include <cstddef>
#include <cstring>
#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 <numSamples; i++)
buf[0][i] = (synth_float_t)buf1[i] * synth_gain;
for (i=0; i <numSamples; i++)
buf[1][i] = (synth_float_t)buf2[i] * synth_gain;
m_limiter_active = 0;
synth_float_t master_gain = 0.5;
#ifdef WITH_LIMITER
synth_float_t limiter_env[SYNTH_MAX_BUFSIZE];
synth_float_t limiter_gain[SYNTH_MAX_BUFSIZE];
synth_float_t x, limiter_ar, limiter_af, limiter_threshold;
// Limiter
limiter_ar = 5.0/(SYNTH_LIMITER_RISE_TIME*m_fs);
limiter_af = 5.0/(SYNTH_LIMITER_FALL_TIME*m_fs);
limiter_threshold = pow((synth_float_t)10.0, (synth_float_t)SYNTH_LIMITER_THRESHOLD/20);
for (i=0; i <numSamples; i++)
// The DSP core (and this function's own work buffers) can only render up to
// SYNTH_MAX_BUFSIZE samples per call, so process oversized host blocks in
// multiple chunks instead of overflowing the fixed-size stack buffers below.
for (int chunkStart = 0; chunkStart < numSamples; chunkStart += SYNTH_MAX_BUFSIZE)
{
x = jsy_max(fabs(buf[0][i]), fabs(buf[1][i]));
if (x > 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 <chunkLen; i++)
buf[0][i] = (synth_float_t)buf1[chunkStart + i] * synth_gain;
for (i=0; i <chunkLen; i++)
buf[1][i] = (synth_float_t)buf2[chunkStart + i] * synth_gain;
m_limiter_active = 0;
#ifdef WITH_LIMITER
synth_float_t limiter_env[SYNTH_MAX_BUFSIZE];
synth_float_t limiter_gain[SYNTH_MAX_BUFSIZE];
synth_float_t x, limiter_ar, limiter_af, limiter_threshold;
// Limiter
limiter_ar = 5.0/(SYNTH_LIMITER_RISE_TIME*m_fs);
limiter_af = 5.0/(SYNTH_LIMITER_FALL_TIME*m_fs);
limiter_threshold = pow((synth_float_t)10.0, (synth_float_t)SYNTH_LIMITER_THRESHOLD/20);
for (i=0; i <chunkLen; i++)
{
m_limiter_env = (1.0-limiter_ar)*m_limiter_env + limiter_ar*x;
}
else
{
m_limiter_env = (1.0-limiter_af)*m_limiter_env;
x = jsy_max(fabs(buf[0][i]), fabs(buf[1][i]));
if (x > 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 <numSamples; i++)
{
limiter_gain[i] = jsy_min(1.0, 1.0/(limiter_env[i] + 1.0e-6));
if (limiter_gain[i] < 1.0)
for (i=0; i <chunkLen; i++)
{
m_limiter_active = 1;
break;
limiter_gain[i] = jsy_min(1.0, 1.0/(limiter_env[i] + 1.0e-6));
if (limiter_gain[i] < 1.0)
{
m_limiter_active = 1;
break;
}
}
}
for (i=0; i <numSamples; i++)
{
buf1[i] = (float)(master_gain*buf[0][i]*limiter_gain[i]);
}
for (i=0; i <chunkLen; i++)
{
buf1[chunkStart + i] = (float)(master_gain*buf[0][i]*limiter_gain[i]);
}
for (i=0; i <numSamples; i++)
{
buf2[i] = (float)(master_gain*buf[1][i]*limiter_gain[i]);
}
for (i=0; i <chunkLen; i++)
{
buf2[chunkStart + i] = (float)(master_gain*buf[1][i]*limiter_gain[i]);
}
#else
for (i=0; i <numSamples; i++)
{
buf1[i] = (float)(master_gain*buf[0][i]);
}
for (i=0; i <chunkLen; i++)
{
buf1[chunkStart + i] = (float)(master_gain*buf[0][i]);
}
for (i=0; i <numSamples; i++)
{
buf2[i] = (float)(master_gain*buf[1][i]);
}
for (i=0; i <chunkLen; i++)
{
buf2[chunkStart + i] = (float)(master_gain*buf[1][i]);
}
#endif
}
// In case we have more outputs than inputs, we'll clear any output
// channels that didn't contain input data, (because these aren't
@@ -766,20 +844,9 @@ void JaySynthAudioProcessor::patchDecodeXml(const XmlElement *xml, JaySynthSound
void JaySynthAudioProcessor::patchDecodeXml_legacy(const XmlElement& xml, JaySynthSound *patch)
{
// ok, now pull out our parameters..
patch->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<char> pUncompressedData = new char[8192*1024];
HeapBlock<char> 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<XmlElement> 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<char> pUncompressedData;
pUncompressedData = new char[2*65536];
HeapBlock<char> 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)
{
+23 -1
View File
@@ -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);
};
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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)));
}
+1 -1
View File
@@ -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);
}
+13 -1
View File
@@ -6,6 +6,7 @@
#endif
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
// --------------------------------------------------------------
// 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);
+5
View File
@@ -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
+8 -4
View File
@@ -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);
+1 -1
View File
@@ -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)));
}
+91 -13
View File
@@ -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
+14
View File
@@ -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;
+2 -2
View File
@@ -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)));
}
}