#include "GrainEngine.h" GrainEngine::GrainEngine() { formatManager.registerBasicFormats(); } void GrainEngine::prepare (double sampleRate, int samplesPerBlock) { currentSampleRate = sampleRate; currentBlockSize = samplesPerBlock; for (auto& voice : voices) voice.prepare (sampleRate); // Filter juce::dsp::ProcessSpec spec; spec.sampleRate = sampleRate; spec.maximumBlockSize = (juce::uint32) samplesPerBlock; spec.numChannels = 2; filter.prepare (spec); filter.setType (juce::dsp::StateVariableTPTFilterType::lowpass); filter.setCutoffFrequency (20000.0f); filter.setResonance (0.707f); // Reverb reverb.setSampleRate (sampleRate); reverbParams.roomSize = 0.0f; reverbParams.damping = 0.5f; reverbParams.wetLevel = 0.0f; reverbParams.dryLevel = 1.0f; reverbParams.width = 1.0f; reverb.setParameters (reverbParams); } void GrainEngine::loadSample (const juce::File& file) { auto* reader = formatManager.createReaderFor (file); if (reader == nullptr) return; juce::AudioBuffer tempBuffer ((int) reader->numChannels, (int) reader->lengthInSamples); reader->read (&tempBuffer, 0, (int) reader->lengthInSamples, 0, true, true); // Convert to mono if stereo if (tempBuffer.getNumChannels() > 1) { juce::AudioBuffer monoBuffer (1, tempBuffer.getNumSamples()); monoBuffer.clear(); for (int ch = 0; ch < tempBuffer.getNumChannels(); ++ch) monoBuffer.addFrom (0, 0, tempBuffer, ch, 0, tempBuffer.getNumSamples(), 1.0f / (float) tempBuffer.getNumChannels()); sampleBuffer = std::move (monoBuffer); } else { sampleBuffer = std::move (tempBuffer); } loadedSamplePath = file.getFullPathName(); sourceSampleRate = reader->sampleRate; // Reset all voices for (auto& voice : voices) voice.getCloud().reset(); delete reader; } void GrainEngine::loadSample (const void* data, size_t dataSize, const juce::String& /*formatName*/) { auto stream = std::make_unique (data, dataSize, false); auto* reader = formatManager.createReaderFor (std::move (stream)); if (reader == nullptr) return; juce::AudioBuffer tempBuffer ((int) reader->numChannels, (int) reader->lengthInSamples); reader->read (&tempBuffer, 0, (int) reader->lengthInSamples, 0, true, true); if (tempBuffer.getNumChannels() > 1) { juce::AudioBuffer monoBuffer (1, tempBuffer.getNumSamples()); monoBuffer.clear(); for (int ch = 0; ch < tempBuffer.getNumChannels(); ++ch) monoBuffer.addFrom (0, 0, tempBuffer, ch, 0, tempBuffer.getNumSamples(), 1.0f / (float) tempBuffer.getNumChannels()); sampleBuffer = std::move (monoBuffer); } else { sampleBuffer = std::move (tempBuffer); } loadedSamplePath = ""; sourceSampleRate = reader->sampleRate; for (auto& voice : voices) voice.getCloud().reset(); delete reader; } void GrainEngine::syncVoiceParameters() { for (auto& voice : voices) { auto& cloud = voice.getCloud(); cloud.position.store (position.load()); cloud.grainSizeMs.store (grainSizeMs.load()); cloud.density.store (density.load()); cloud.pitchSemitones.store (pitchSemitones.load()); cloud.pan.store (pan.load()); cloud.posScatter.store (posScatter.load()); cloud.sizeScatter.store (sizeScatter.load()); cloud.pitchScatter.store (pitchScatter.load()); cloud.panScatter.store (panScatter.load()); cloud.direction.store (direction.load()); cloud.freeze.store (freeze.load()); cloud.sampleRateRatio = (float) (sourceSampleRate / currentSampleRate); voice.rootNote.store (rootNote.load()); voice.attackTime.store (attackTime.load()); voice.decayTime.store (decayTime.load()); voice.sustainLevel.store (sustainLevel.load()); voice.releaseTime.store (releaseTime.load()); } } void GrainEngine::handleNoteOff (int note) { // Release ALL voices playing this note (not just the first) // Prevents stuck notes when the same key is pressed multiple times with sustain pedal for (int i = 0; i < maxVoices; ++i) { if (voices[i].isActive() && voices[i].getCurrentNote() == note) { if (sustainPedalDown) sustainedVoices[i] = true; else voices[i].noteOff(); } } } void GrainEngine::handleMidiEvent (const juce::MidiMessage& msg) { if (msg.isNoteOn()) { // Velocity 0 = note-off (standard MIDI convention) if (msg.getFloatVelocity() == 0.0f) { handleNoteOff (msg.getNoteNumber()); return; } int note = msg.getNoteNumber(); float vel = msg.getFloatVelocity(); // Find free voice or steal oldest int targetIdx = -1; for (int i = 0; i < maxVoices; ++i) { if (! voices[i].isActive()) { targetIdx = i; break; } } // If no free voice, steal first if (targetIdx < 0) targetIdx = 0; // Clear sustained flag when stealing/reusing a voice slot sustainedVoices[targetIdx] = false; voices[targetIdx].noteOn (note, vel); } else if (msg.isNoteOff()) { handleNoteOff (msg.getNoteNumber()); } else if (msg.isSustainPedalOn()) { sustainPedalDown = true; } else if (msg.isSustainPedalOff()) { sustainPedalDown = false; for (int i = 0; i < maxVoices; ++i) { if (sustainedVoices[i]) { voices[i].noteOff(); sustainedVoices[i] = false; } } } else if (msg.isAllNotesOff()) { sustainPedalDown = false; for (int i = 0; i < maxVoices; ++i) { sustainedVoices[i] = false; if (voices[i].isActive()) voices[i].noteOff(); } } else if (msg.isAllSoundOff()) { // Immediate kill — no release tail sustainPedalDown = false; for (int i = 0; i < maxVoices; ++i) { sustainedVoices[i] = false; voices[i].forceStop(); } } } void GrainEngine::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { const int numSamples = buffer.getNumSamples(); // Sync parameters syncVoiceParameters(); // Handle MIDI for (const auto metadata : midiMessages) handleMidiEvent (metadata.getMessage()); // Clear output buffer.clear(); // Process all voices for (auto& voice : voices) { if (voice.isActive()) voice.processBlock (buffer, numSamples, sampleBuffer); } // Global filter { float cutoff = filterCutoff.load(); float reso = filterReso.load(); int fType = filterType.load(); filter.setCutoffFrequency (cutoff); filter.setResonance (reso); switch (fType) { case 0: filter.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; case 1: filter.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; case 2: filter.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; } // Only apply if cutoff < 19999 (otherwise skip for efficiency) if (cutoff < 19999.0f || fType != 0) { juce::dsp::AudioBlock block (buffer); juce::dsp::ProcessContextReplacing context (block); filter.process (context); } } // Global reverb { float size = reverbSize.load(); float decay = reverbDecay.load(); if (size > 0.001f || decay > 0.001f) { reverbParams.roomSize = size; reverbParams.damping = 1.0f - decay; reverbParams.wetLevel = std::max (size, decay) * 0.5f; reverbParams.dryLevel = 1.0f - reverbParams.wetLevel * 0.3f; reverb.setParameters (reverbParams); reverb.processStereo (buffer.getWritePointer (0), buffer.getWritePointer (1), numSamples); } } // Master volume float vol = masterVolume.load(); buffer.applyGain (vol); // VU meter vuLevelL.store (buffer.getMagnitude (0, 0, numSamples)); if (buffer.getNumChannels() > 1) vuLevelR.store (buffer.getMagnitude (1, 0, numSamples)); else vuLevelR.store (vuLevelL.load()); } std::vector GrainEngine::getActiveGrainInfo() const { std::vector result; for (const auto& voice : voices) { if (! voice.isActive()) continue; auto cloudInfo = voice.getCloud().getActiveGrainInfo(); for (const auto& gi : cloudInfo) { if (gi.startSample >= 0) result.push_back ({ gi.startSample, gi.lengthSamples, gi.progress }); } } return result; }