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] **~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.) - [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. - [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.
- [ ] **`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] **`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 ## Patch/Bank import-export
+9 -9
View File
@@ -158,7 +158,7 @@ JaySynth::~JaySynth()
{ {
delete(m_ppAudioThread[i]); delete(m_ppAudioThread[i]);
} }
delete(m_ppAudioThread); delete[] m_ppAudioThread;
SynthDebug("delete params\n"); SynthDebug("delete params\n");
paramInfoFree(&pb_range[0]); paramInfoFree(&pb_range[0]);
@@ -171,25 +171,25 @@ JaySynth::~JaySynth()
{ {
paramInfoFree(&humanize_voice_param[i][j]); paramInfoFree(&humanize_voice_param[i][j]);
} }
delete (humanize_voice_param[i]); delete[] humanize_voice_param[i];
delete (ppHumanizedSliders[i]); delete[] ppHumanizedSliders[i];
} }
delete (humanize_voice_param); delete[] humanize_voice_param;
delete (ppHumanizedSliders); delete[] ppHumanizedSliders;
for (i = max_num_voices; --i >= 0;) for (i = max_num_voices; --i >= 0;)
{ {
removeVoice(i); removeVoice(i);
} }
delete (pPer_voice_controls); delete[] pPer_voice_controls;
delete (pCurrNoteInfos); delete[] pCurrNoteInfos;
delete (m_pVoices); delete[] m_pVoices;
for (i=0; i < m_num_audiothreads; i++) for (i=0; i < m_num_audiothreads; i++)
{ {
delete (m_ppEventAudioThreadRdy[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) 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); MemoryInputStream inputStream(data, sizeInBytes, false);
GZIPDecompressorInputStream GZIPDec(inputStream); GZIPDecompressorInputStream GZIPDec(inputStream);
@@ -1093,8 +1093,7 @@ void JaySynthAudioProcessor::setCurrentProgramStateInformation (const void* data
void* pData; void* pData;
int size; int size;
ScopedPointer<char> pUncompressedData; HeapBlock<char> pUncompressedData (2*65536);
pUncompressedData = new char[2*65536];
MemoryInputStream inputStream(data, sizeInBytes, false); MemoryInputStream inputStream(data, sizeInBytes, false);
GZIPDecompressorInputStream GZIPDec(inputStream); 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; synth_float_t x, dx, a, b, blit, blep;
// Calculate table sizes // 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++) for (m=0; m < BLIT_NUM_HARM_MAX; m++)
{ {
pCom->pTableSizes[m] = m*BLIT_TABLE_OVERSAMPLING; pCom->pTableSizes[m] = m*BLIT_TABLE_OVERSAMPLING;
@@ -148,18 +148,18 @@ void BLIT_ModInit(blit_common_t *pCom)
} }
// Generate Kaiser window // 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++) 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]); CalcKaiser(NULL, ppKaiser[m], 8.f, pCom->pTableSizes[m]);
} }
// Allocate BLIT table // 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++) 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 // Generate BLIT
+1 -1
View File
@@ -43,7 +43,7 @@ void ENV_SetBufsize(envgen_t *pObj, UINT32 size)
if (!size) if (!size)
return; 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) 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) if (!size)
return; 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)));
pObj->pPhase = (sync_result_t*)malloc(pObj->bufsize*sizeof(sync_result_t)); pObj->pPhase = (sync_result_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(sync_result_t)));
pObj->pWave = (synth_float_t*)malloc(pObj->bufsize*sizeof(synth_float_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; pObj->pName = NULL;
if (pName) if (pName)
{ {
pObj->pName = (char*)malloc(strlen(pName)+1); pObj->pName = (char*)SynthCheckAlloc(malloc(strlen(pName)+1));
memcpy(pObj->pName, pName, strlen(pName)+1); memcpy(pObj->pName, pName, strlen(pName)+1);
} }
+12
View File
@@ -6,6 +6,7 @@
#endif #endif
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
// -------------------------------------------------------------- // --------------------------------------------------------------
// internal funcs // internal funcs
@@ -14,6 +15,17 @@
// Exported functions // Exported functions
// -------------------------------------------------------------- // --------------------------------------------------------------
void *SynthCheckAlloc(void *ptr)
{
if (!ptr)
{
fprintf(stderr, "JaySynth: out of memory, aborting.\n");
abort();
}
return ptr;
}
void SynthDebug(const char *fmtstr, ...) void SynthDebug(const char *fmtstr, ...)
{ {
#ifndef SYNTH_DEBUG #ifndef SYNTH_DEBUG
+5
View File
@@ -55,6 +55,11 @@
extern "C" { extern "C" {
#endif #endif
void SynthDebug(const char *fmtstr, ...); 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) #if defined(__cplusplus)
} }
#endif #endif
+4 -4
View File
@@ -55,8 +55,8 @@ void VCF_ModInit(vcf_t *pObj)
{ {
vcf_common_t *pCom = pObj->pCom; vcf_common_t *pCom = pObj->pCom;
pCom->pLUT_cos = (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*)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) void VCF_ModFree(vcf_t *pObj)
@@ -141,8 +141,8 @@ void VCF_SetBufsize(vcf_t *pObj, UINT32 size)
if (!size) if (!size)
return; 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)));
pObj->pCoef = (vcf_coef_t*)malloc(pObj->bufsize*sizeof(vcf_coef_t)); pObj->pCoef = (vcf_coef_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(vcf_coef_t)));
pObj->pCoeff_last = pObj->pCoef; pObj->pCoeff_last = pObj->pCoef;
VCF_Coeff_update(pObj); VCF_Coeff_update(pObj);
+1 -1
View File
@@ -87,7 +87,7 @@ void VCO_SetBufsize(osc_t *pObj, UINT32 size)
if (!size) if (!size)
return; 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) if (!size)
return; 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++) 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_fmout[i] = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t)));
pObj->pBuf_vco_pitch[i] = (synth_float_t*)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*)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_pwm = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t)));
pObj->pBuf_vco_fm = (synth_float_t*)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*)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*)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*)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*)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*)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*)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) void VoiceEvent(voice_t *pObj, UINT32 type)
+2 -2
View File
@@ -173,10 +173,10 @@ void WT_ModInit(wt_common_t *pCom)
// Allocate memory // Allocate memory
for (i=0; i < WT_NUM_WAVETABLES; i++) 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++) 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)));
} }
} }