Let me see if I can do a decent job explaining it. I’ve written a module called “Grain Engine MK2”, which does granular synthesis. You use it by loading up to 5 .wav files from disk. I’m building an expansion module that can record incoming audio from other modules and, after recording has completed, pass that audio into Grain Engine MK2 for processing. Grain Engine MK2 can’t record audio, so this is a cool new feature.
My initial implementation was a little sloppy. The expansion module would record the audio, write it to a .wav file, then pass the filename of the saved data to Grain Engine MK2, which would load it into one of the 5 sample slots. One nice thing about this approach is that restarting VCV Rack wouldn’t cause the recorded audio to be lost since Grain Engine MK2 treated it like any other .wav file.
I’ll get back to that. Here’s a bit more background on the code…
Grain Engine MK2 contains an array of 5 Samples that load .wav file data into vectors for playback. Here’s some code to illustrate:
// Sample.hpp : Part of the definition that shows the audio buffers
struct Sample
{
std::vector<float> leftPlayBuffer;
std::vector<float> rightPlayBuffer;
... etc
// GrainEngineMK2 : Showing the array of 5 Sample structures
Sample *samples[NUMBER_OF_SAMPLES];
// In the constructor:
for(unsigned int i=0; i<NUMBER_OF_SAMPLES; i++)
{
samples[i] = new Sample();
}
Everything was working great, but I knew that saving and loading a .wav file is really time consuming, and I felt guilty about that solution! I figured it might cause trouble. Then it dawned on me… the expansion module also used a Sample to store the information it was recording:
// Expansion Module : Uses a single Sample for storing audio information
Sample *sample = new Sample();
I won’t go deeply into how the two modules communicate unless asked. On the surface level, here’s the structure for messages sent from the expander to the main module.
struct GrainEngineExpanderMessage
{
unsigned int sample_slot = 0;
bool message_received = false;
Sample *sample = NULL;
};
// In the expander module:
// GrainEngineExpanderMessage *message_bus = (GrainEngineExpanderMessage *) rightExpander.module->leftExpander.producerMessage;
So my thought was: Can I pass in a pointer to the expansion module’s Sample, which contains the audio data, into the main module? Or, put another way, instead of forcing Grain Engine MK2 to load the saved audio data, how about just sending over a pointer to it?
GrainEngineMK2 has 5 sample slots. So I would be essentially saying, Hey Grain Engine MK2, you know sample slot #4? I have a new Sample for you to use for that slot. So ditch the old one and here’s a pointer to the new one for you.
Juggling the pointers got a bit tricky. Here’s how I envisioned that it would work.
-
You record some audio in the expansion module, which pushes the audio information into the Sample’s vectors (leftPlayBuffer & rightPlayBuffer)
-
The expansion module sends a pointer to the Sample to the main module along with which sample slot to overwrite.
-
The expansion module then saves the recorded audio to disk.
-
After saving the audio, the expansion module “abandons” that pointer and creates a new empty sample for it to use.
// In the expansion module
message_bus->sample = sample; // pass the Sample to the main module
sample->save_recorded_audio(sample->path + sample->filename); // save audio to a file
sample = new Sample(); // Get a fresh new sample to record more audio into
On the Grain Engine MK2 side of things, it sees an incoming message and does the following:
-
Receives the sample slot (0-4) of where to store the incoming sample information
-
Deletes the old Sample structure at that slot
-
Assigns the sample pointer at that slot to the incoming sample pointer
Maybe it’s easier to understand the code:
// In Grain Engine MK2
unsigned int sample_slot = expander_message->sample_slot;
Sample *old_sample = this->samples[sample_slot]; // Me being paranoid
this->samples[sample_slot] = expander_message->sample; // load in new sample coming from expansion module
delete old_sample; // Delete old sample being overwritten by new sample. HERE IS WHERE IT IS CRASHING
If I remove delete old_sample
; it stops crashing, but I suspect that would cause a memory leak.
I haven’t figured out why it crashes when I attempt to delete the old sample. I’m probably overlooking something. Any help would be much appreciated.
Cheers,
Bret