@@ -1509,11 +1509,40 @@ void GenericProcessor::addSpike (const Spike* spike)
15091509 + spike->spikeChannel ->getTotalEventMetadataSize ()
15101510 + spike->spikeChannel ->getNumChannels () * sizeof (float );
15111511
1512- HeapBlock<char > buffer (size);
1512+ // Reuse a function-local persistent buffer, growing only when needed.
1513+ // This avoids a per-spike HeapBlock allocation.
1514+ // Safe because addSpike is only called from the real-time audio thread.
1515+ static HeapBlock<char > serializeBuffer;
1516+ static size_t serializeBufferSize = 0 ;
15131517
1514- spike->serialize (buffer, size);
1518+ if (size > serializeBufferSize)
1519+ {
1520+ serializeBuffer.realloc (size);
1521+ serializeBufferSize = size;
1522+ }
15151523
1516- m_currentMidiBuffer->addEvent (buffer, int (size), 0 );
1524+ spike->serialize (serializeBuffer, size);
1525+
1526+ // Directly append to MidiBuffer's data array, bypassing addEvent()'s
1527+ // internal findEventAfter() which performs an O(n) scan of existing events
1528+ // for each insertion — resulting in O(n²) total cost for n spikes per callback.
1529+ //
1530+ // This is safe because downstream event consumers (processEventBuffer,
1531+ // checkForEvents) dispatch events by their raw serialized type byte,
1532+ // not by the MidiBuffer sample position.
1533+ auto & bufferData = m_currentMidiBuffer->data ;
1534+ int offset = bufferData.size ();
1535+ int newItemSize = (int ) (size + sizeof (int32) + sizeof (uint16));
1536+ bufferData.insertMultiple (offset, 0 , newItemSize);
1537+
1538+ uint8* d = bufferData.begin () + offset;
1539+ int32 samplePos = 0 ;
1540+ memcpy (d, &samplePos, sizeof (int32));
1541+ d += sizeof (int32);
1542+ uint16 dataLen = static_cast <uint16> (size);
1543+ memcpy (d, &dataLen, sizeof (uint16));
1544+ d += sizeof (uint16);
1545+ memcpy (d, static_cast <const char *> (serializeBuffer), size);
15171546}
15181547
15191548void GenericProcessor::processBlock (AudioBuffer<float >& buffer, MidiBuffer& eventBuffer)
0 commit comments