Fix Memory-management issues from TODO.md

- JaySynth's destructor now uses delete[] to match every new T[n]
  allocation (m_pVoices, pPer_voice_controls, pCurrNoteInfos,
  humanize_voice_param[i], ppHumanizedSliders[i], m_ppAudioThread,
  m_ppEventAudioThreadRdy), fixing undefined behavior from the
  mismatched scalar delete.
- Found and fixed the same new[]/delete mismatch pattern via
  ScopedPointer in PluginProcessor.cpp: ScopedPointer always calls
  scalar delete (per JUCE's own doc comment "do not give it an array
  to hold!"), so ScopedPointer<char> holding a new char[...] in
  setStateInformation/setCurrentProgramStateInformation had the same
  bug. Replaced both with HeapBlock<char>, JUCE's array-owning,
  malloc/free-backed smart pointer.
- Added a shared SynthCheckAlloc() helper (synth_defs.h/synth_debug.c)
  that aborts with a diagnostic instead of returning NULL, and wrapped
  all 24 malloc call sites across the C DSP core (env.c, lfo.c, vcf.c,
  vco.c, blit.c, wavetable.c, voice.c, param_scale.c). All are
  one-time init/bufsize-change calls, never in the per-block render
  path, so this adds no real-time-thread overhead.

Verified with clean debug and release builds (no new warnings/errors)
and a full link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
This commit is contained in:
2026-07-27 18:19:56 +02:00
co-authored by Claude Sonnet 5
parent 608ae003df
commit 54a67562db
13 changed files with 60 additions and 44 deletions
+3 -3
View File
@@ -18,10 +18,10 @@ Findings from a code-review pass over `src/plug` and `src/synth` (2026-07-27, br
- [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
## Memory management (fixed)
- [ ] **`new[]`/`delete` mismatches (UB) in `JaySynth`'s destructor** — `m_pVoices`, `pPer_voice_controls`, `pCurrNoteInfos`, `humanize_voice_param[i]`, `ppHumanizedSliders[i]`, `m_ppAudioThread`, `m_ppEventAudioThreadRdy` all allocated with `new T[n]` but freed with scalar `delete` (`src/plug/JaySynth.cpp:62/186, 77/184, 80/185, 100-101/173-174, 124-125/160,191`). Fix: change to `delete[]` at each site.
- [ ] **`malloc` is never null-checked** across the entire C DSP core (`env.c`, `lfo.c`, `vcf.c`, `vco.c`, `blit.c`, `wavetable.c`, `voice.c`, `param_scale.c`). Fix: check and fail gracefully (or `abort()` with a diagnostic) rather than crashing on first audio callback.
- [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
+9 -9
View File
@@ -158,7 +158,7 @@ JaySynth::~JaySynth()
{
delete(m_ppAudioThread[i]);
}
delete(m_ppAudioThread);
delete[] m_ppAudioThread;
SynthDebug("delete params\n");
paramInfoFree(&pb_range[0]);
@@ -171,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;
}
+2 -3
View File
@@ -972,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);
@@ -1093,8 +1093,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);
+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);
}
+12
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
+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
+4 -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,8 @@ 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)));
pObj->pCoef = (vcf_coef_t*)SynthCheckAlloc(malloc(pObj->bufsize*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)));
}
+12 -12
View File
@@ -306,23 +306,23 @@ void VoiceSetBufsize(voice_t *pObj, UINT32 size)
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*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vco_pitch[i] = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_osc_out_smoothed[i] = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
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*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vco_fm = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vco_am = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vcf_fm = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vcf_qm = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vca_am = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_vca_pan = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_t));
pObj->pBuf_osc_out = (synth_float_t*)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)
+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)));
}
}