diff --git a/.github/workflows/project-pipeline.yml b/.github/workflows/project-pipeline.yml index 86ad88b..f5aa0c7 100644 --- a/.github/workflows/project-pipeline.yml +++ b/.github/workflows/project-pipeline.yml @@ -47,7 +47,7 @@ jobs: run: make build - name: Show Build Output - run: ls -lR ${{github.workspace}}/build + run: ls -lR ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts - name: Upload Artifacts uses: actions/upload-artifact@v4 @@ -55,11 +55,13 @@ jobs: name: linux-bin path: | ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/NeuralDX7PatchGenerator + ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/VST3/ ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/lib/ !**/*.a build-macos: runs-on: macos-latest + if: false steps: - uses: actions/checkout@v4 @@ -96,7 +98,7 @@ jobs: run: make build - name: Show Build Output - run: ls -lR ${{github.workspace}}/build + run: ls -lR ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts - name: Upload Artifacts uses: actions/upload-artifact@v4 @@ -105,6 +107,8 @@ jobs: path: | ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/NeuralDX7PatchGenerator.app ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/NeuralDX7PatchGenerator.component + ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/AU/ + ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/VST3/ build-windows: runs-on: windows-latest @@ -149,7 +153,7 @@ jobs: - name: Show shell: cmd - run: dir /s ${{github.workspace}}\build + run: dir /s ${{github.workspace}}\build\NeuralDX7PatchGenerator_artefacts - name: Upload Artifacts uses: actions/upload-artifact@v4 @@ -157,6 +161,7 @@ jobs: name: windows-bin path: | ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/NeuralDX7PatchGenerator.exe + ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/VST3/ ${{github.workspace}}/build/NeuralDX7PatchGenerator_artefacts/${{env.BUILD_TYPE}}/*.dll release: @@ -185,24 +190,52 @@ jobs: - name: Create release packages run: | - # Create Windows ZIP + # Create Windows Standalone ZIP if [ -d "./artifacts/windows-bin" ]; then cd artifacts/windows-bin - zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-windows.zip . + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-windows-standalone.zip NeuralDX7PatchGenerator.exe *.dll cd ../.. fi - # Create Linux ZIP + # Create Windows VST ZIP + if [ -d "./artifacts/windows-bin" ]; then + cd artifacts/windows-bin + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-windows-vst.zip VST3/ + cd ../.. + fi + + # Create Linux Standalone ZIP + if [ -d "./artifacts/linux-bin" ]; then + cd artifacts/linux-bin + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-linux-standalone.zip NeuralDX7PatchGenerator lib/ + cd ../.. + fi + + # Create Linux VST ZIP if [ -d "./artifacts/linux-bin" ]; then cd artifacts/linux-bin - zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-linux.zip . + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-linux-vst.zip VST3/ + cd ../.. + fi + + # Create macOS Standalone ZIP + if [ -d "./artifacts/macos-bin" ]; then + cd artifacts/macos-bin + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-macos-standalone.zip NeuralDX7PatchGenerator.app + cd ../.. + fi + + # Create macOS VST ZIP + if [ -d "./artifacts/macos-bin" ]; then + cd artifacts/macos-bin + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-macos-vst.zip VST3/ cd ../.. fi - # Create macOS ZIP + # Create macOS AU ZIP if [ -d "./artifacts/macos-bin" ]; then cd artifacts/macos-bin - zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-macos.zip . + zip -r ../../NeuralDX7PatchGenerator-${{ steps.extract_tag.outputs.tag }}-macos-au.zip AU/ cd ../.. fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 156cd12..e738aad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,11 @@ juce_add_plugin(NeuralDX7PatchGenerator COPY_PLUGIN_AFTER_BUILD TRUE PLUGIN_MANUFACTURER_CODE NinA PLUGIN_CODE NeuD - FORMATS Standalone AU + FORMATS Standalone VST3 AU + VST3_CATEGORIES "Generator Tools" + VST3_AUTO_MANIFEST FALSE + LV2_CATEGORIES "GeneratorPlugin" + LV2URI "urn:nintoracaudio:neuraldx7patchgenerator" PRODUCT_NAME "NeuralDX7PatchGenerator") # Override standalone binary location to be in ${CMAKE_BUILD_TYPE}/ instead of ${CMAKE_BUILD_TYPE}/Standalone/ @@ -95,6 +99,7 @@ target_sources(NeuralDX7PatchGenerator Source/DX7BulkPacker.cpp Source/NeuralModelWrapper.cpp Source/EmbeddedModelLoader.cpp + Source/ThreadedInferenceEngine.cpp ${CMAKE_CURRENT_BINARY_DIR}/model_data.h) # Link libraries @@ -197,7 +202,8 @@ endif() # Disable JUCE networking features to avoid curl dependency target_compile_definitions(NeuralDX7PatchGenerator PRIVATE JUCE_WEB_BROWSER=0 - JUCE_USE_CURL=0) + JUCE_USE_CURL=0 + JUCE_VST3_CAN_REPLACE_VST2=0) # Include directories target_include_directories(NeuralDX7PatchGenerator PRIVATE Source ${CMAKE_CURRENT_BINARY_DIR} ${GTK3_INCLUDE_DIRS} ${WEBKIT2GTK_INCLUDE_DIRS}) diff --git a/Source/NeuralModelWrapper.cpp b/Source/NeuralModelWrapper.cpp index 3a83372..87c8e68 100644 --- a/Source/NeuralModelWrapper.cpp +++ b/Source/NeuralModelWrapper.cpp @@ -85,13 +85,13 @@ std::vector NeuralModelWrapper::generateVoices(const std::vector inputs; @@ -99,11 +99,14 @@ std::vector NeuralModelWrapper::generateVoices(const std::vector voices; - voices.reserve(N_VOICES); + voices.reserve(numVoices); - for (int i = 0; i < N_VOICES; ++i) { + for (int i = 0; i < numVoices; ++i) { torch::Tensor voiceLogits = logits[i]; auto parameters = DX7Voice::logitsToParameters(voiceLogits); voices.push_back(DX7Voice::fromParameters(parameters)); @@ -146,26 +149,14 @@ std::vector NeuralModelWrapper::generateMultipleRandomVoices() std::mt19937 gen(rd()); std::normal_distribution dis(0.0f, 1.0f); - // Create tensor with different random latent vectors for each voice - torch::Tensor z = torch::randn({N_VOICES, LATENT_DIM}); - - // Generate parameters using the model - std::vector inputs; - inputs.push_back(z); - - torch::Tensor logits = model.forward(inputs).toTensor(); - - // Convert logits to parameters and then to voices - std::vector voices; - voices.reserve(N_VOICES); + // Create batched latent vectors for multiple voices + std::vector batchedLatent(N_VOICES * LATENT_DIM); - for (int i = 0; i < N_VOICES; ++i) { - torch::Tensor voiceLogits = logits[i]; - auto parameters = DX7Voice::logitsToParameters(voiceLogits); - voices.push_back(DX7Voice::fromParameters(parameters)); + for (int i = 0; i < N_VOICES * LATENT_DIM; ++i) { + batchedLatent[i] = dis(gen); } - return voices; + return generateVoices(batchedLatent); } catch (const std::exception& e) { std::cerr << "Error generating multiple random voices: " << e.what() << "\n"; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 0cb6f3a..c85ed4f 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -3,16 +3,49 @@ #include "DX7BulkPacker.h" #include "DX7VoicePacker.h" +// Simple debounce timer class +class DebounceTimer : public juce::Timer +{ +public: + DebounceTimer(std::function callback) : callback_(callback) {} + + void timerCallback() override + { + stopTimer(); + if (callback_) + callback_(); + } + + void debounce(int milliseconds) + { + startTimer(milliseconds); + } + +private: + std::function callback_; +}; + NeuralDX7PatchGeneratorProcessor::NeuralDX7PatchGeneratorProcessor() : AudioProcessor (BusesProperties()) { - latentVector.resize(NeuralModelWrapper::LATENT_DIM, 0.0f); + inferenceEngine = std::make_unique(); + inferenceEngine->startInferenceThread(); + + // Create debounce timer for slider changes + debounceTimer = std::make_unique([this]() { + // Pre-generate voice for current latent vector + inferenceEngine->preGenerateCustomVoice(latentVector); + }); } NeuralDX7PatchGeneratorProcessor::~NeuralDX7PatchGeneratorProcessor() { + if (inferenceEngine) + { + inferenceEngine->stopInferenceThread(); + } } const juce::String NeuralDX7PatchGeneratorProcessor::getName() const @@ -150,75 +183,79 @@ void NeuralDX7PatchGeneratorProcessor::generateAndSendMidi() { std::cout << "generateAndSendMidi() called" << std::endl; - if (!neuralModel.isModelLoaded()) { - std::cout << "Neural model not loaded, attempting to load..." << std::endl; - if (!neuralModel.loadModelFromFile()) { - std::cout << "Failed to load neural model!" << std::endl; - return; - } - std::cout << "Neural model loaded successfully!" << std::endl; + if (!inferenceEngine->isModelLoaded()) { + std::cout << "Neural model not loaded yet, request ignored" << std::endl; + return; } - std::cout << "Generating voices with latent vector: ["; + std::cout << "Generating voice with latent vector: ["; for (size_t i = 0; i < latentVector.size(); ++i) { std::cout << latentVector[i]; if (i < latentVector.size() - 1) std::cout << ", "; } std::cout << "]" << std::endl; - auto voices = neuralModel.generateVoices(latentVector); - if (voices.empty()) { - std::cout << "No voices generated!" << std::endl; - return; - } - - std::cout << "Generated " << voices.size() << " voices, sending first voice as single voice SysEx" << std::endl; - - // For customise functionality, send just the first voice as a single voice SysEx - auto sysexData = DX7VoicePacker::packSingleVoice(voices[0]); - if (!sysexData.empty()) { - std::cout << "Packed single voice SysEx data: " << sysexData.size() << " bytes" << std::endl; - addMidiSysEx(sysexData); - } else { - std::cout << "Failed to pack single voice SysEx data!" << std::endl; - } + // Use cached request for instant response if available + inferenceEngine->requestCachedCustomVoice(latentVector, [this](std::optional voiceOpt) { + if (!voiceOpt.has_value()) { + std::cout << "Warning: Attempted to send null voice - ignoring request" << std::endl; + return; + } + + std::cout << "Got custom voice, sending as single voice SysEx" << std::endl; + + // For customise functionality, send as single voice SysEx + auto sysexData = DX7VoicePacker::packSingleVoice(voiceOpt.value()); + if (!sysexData.empty()) { + std::cout << "Packed single voice SysEx data: " << sysexData.size() << " bytes" << std::endl; + addMidiSysEx(sysexData); + } else { + std::cout << "Failed to pack single voice SysEx data!" << std::endl; + } + }); } void NeuralDX7PatchGeneratorProcessor::generateRandomVoicesAndSend() { std::cout << "generateRandomVoicesAndSend() called" << std::endl; - if (!neuralModel.isModelLoaded()) { - std::cout << "Neural model not loaded, attempting to load..." << std::endl; - if (!neuralModel.loadModelFromFile()) { - std::cout << "Failed to load neural model!" << std::endl; + // Try to use buffered voices first for instant response + if (inferenceEngine->hasBufferedRandomVoices()) { + std::cout << "Using buffered random voices for instant response" << std::endl; + auto voices = inferenceEngine->getBufferedRandomVoices(); + + if (!voices.empty()) { + std::cout << "Got " << voices.size() << " buffered voices, packing into SysEx..." << std::endl; + auto sysexData = DX7BulkPacker::packBulkDump(voices); + + if (!sysexData.empty()) { + std::cout << "SysEx data packed successfully, sending..." << std::endl; + addMidiSysEx(sysexData); + } else { + std::cout << "Failed to pack SysEx data!" << std::endl; + } return; } } - std::cout << "Generating 32 random voices..." << std::endl; - auto voices = neuralModel.generateMultipleRandomVoices(); - - if (voices.empty()) { - std::cout << "Failed to generate voices!" << std::endl; - return; - } - - std::cout << "Generated " << voices.size() << " voices, packing into SysEx..." << std::endl; - auto sysexData = DX7BulkPacker::packBulkDump(voices); - - if (!sysexData.empty()) { - std::cout << "SysEx data packed successfully, sending..." << std::endl; - addMidiSysEx(sysexData); - } else { - std::cout << "Failed to pack SysEx data!" << std::endl; - } + // If no buffer available, do nothing to prevent weird behavior on rapid clicks + std::cout << "No buffered voices available, ignoring request" << std::endl; } void NeuralDX7PatchGeneratorProcessor::setLatentValues(const std::vector& values) { if (values.size() == NeuralModelWrapper::LATENT_DIM) { latentVector = values; + // Trigger debounced pre-generation + debouncedPreGeneration(); + } +} + +void NeuralDX7PatchGeneratorProcessor::debouncedPreGeneration() +{ + // Debounce slider changes with 150ms delay + if (debounceTimer) { + static_cast(debounceTimer.get())->debounce(150); } } @@ -226,12 +263,39 @@ void NeuralDX7PatchGeneratorProcessor::addMidiSysEx(const std::vector& { std::cout << "addMidiSysEx() called with " << sysexData.size() << " bytes" << std::endl; +#if JucePlugin_Build_LV2 || JucePlugin_Build_VST3 + // For LV2 and VST3, strip SysEx start (0xF0) and end (0xF7) bytes + if (sysexData.size() >= 2 && sysexData[0] == 0xF0 && sysexData[sysexData.size() - 1] == 0xF7) + { + std::cout << "LV2/VST3 plugin detected - stripping SysEx start/end bytes" << std::endl; + std::vector strippedData(sysexData.begin() + 1, sysexData.end() - 1); + + juce::MidiMessage sysexMessage = juce::MidiMessage::createSysExMessage( + strippedData.data(), static_cast(strippedData.size()) + ); + + pendingMidiMessages.addEvent(sysexMessage, 0); + std::cout << "Added stripped SysEx message to pending MIDI buffer" << std::endl; + } + else + { + std::cout << "SysEx data doesn't have expected start/end bytes, sending as-is" << std::endl; + juce::MidiMessage sysexMessage = juce::MidiMessage::createSysExMessage( + sysexData.data(), static_cast(sysexData.size()) + ); + + pendingMidiMessages.addEvent(sysexMessage, 0); + std::cout << "Added SysEx message to pending MIDI buffer" << std::endl; + } +#else + // For other plugin formats, send SysEx data as-is juce::MidiMessage sysexMessage = juce::MidiMessage::createSysExMessage( sysexData.data(), static_cast(sysexData.size()) ); pendingMidiMessages.addEvent(sysexMessage, 0); std::cout << "Added SysEx message to pending MIDI buffer" << std::endl; +#endif } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 15ccb3f..2391ce3 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -4,6 +4,7 @@ #include #include "NeuralModelWrapper.h" #include "DX7VoicePacker.h" +#include "ThreadedInferenceEngine.h" class NeuralDX7PatchGeneratorProcessor : public juce::AudioProcessor { @@ -42,13 +43,17 @@ class NeuralDX7PatchGeneratorProcessor : public juce::AudioProcessor void generateAndSendMidi(); void generateRandomVoicesAndSend(); void setLatentValues(const std::vector& values); + void debouncedPreGeneration(); // For slider changes private: - NeuralModelWrapper neuralModel; + std::unique_ptr inferenceEngine; std::vector latentVector; juce::Random random; juce::MidiBuffer pendingMidiMessages; + // Debouncing for slider changes + std::unique_ptr debounceTimer; + void addMidiSysEx(const std::vector& sysexData); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralDX7PatchGeneratorProcessor) diff --git a/Source/ThreadedInferenceEngine.cpp b/Source/ThreadedInferenceEngine.cpp new file mode 100644 index 0000000..6cb6ecd --- /dev/null +++ b/Source/ThreadedInferenceEngine.cpp @@ -0,0 +1,380 @@ +#include "ThreadedInferenceEngine.h" +#include + +ThreadedInferenceEngine::ThreadedInferenceEngine() + : juce::Thread("InferenceEngine") +{ + neuralModel = std::make_unique(); +} + +ThreadedInferenceEngine::~ThreadedInferenceEngine() +{ + stopInferenceThread(); +} + +void ThreadedInferenceEngine::startInferenceThread() +{ + if (!isThreadRunning()) + { + shouldStop.store(false); + startThread(juce::Thread::Priority::high); + std::cout << "ThreadedInferenceEngine: Started inference thread" << std::endl; + + // Don't pre-generate buffer here - let the thread handle it after model loads + } +} + +void ThreadedInferenceEngine::stopInferenceThread() +{ + if (isThreadRunning()) + { + shouldStop.store(true); + + // Wake up the thread + { + std::unique_lock lock(requestMutex); + requestCondition.notify_all(); + } + + // Wait for thread to finish + stopThread(2000); // 2 second timeout + std::cout << "ThreadedInferenceEngine: Stopped inference thread" << std::endl; + } +} + +void ThreadedInferenceEngine::run() +{ + std::cout << "ThreadedInferenceEngine: Thread started, loading model..." << std::endl; + + // Load model in background thread + if (neuralModel->loadModelFromFile()) + { + modelLoaded.store(true); + std::cout << "ThreadedInferenceEngine: Model loaded successfully" << std::endl; + + // Now that model is loaded, generate initial buffer + preGenerateRandomVoices(); + } + else + { + std::cout << "ThreadedInferenceEngine: Failed to load model" << std::endl; + return; + } + + // Main inference loop + while (!shouldStop.load()) + { + processInferenceRequests(); + + // Wait for new requests or stop signal + std::unique_lock lock(requestMutex); + requestCondition.wait_for(lock, std::chrono::milliseconds(100), [this]() { + return !requestQueue.empty() || shouldStop.load(); + }); + } + + std::cout << "ThreadedInferenceEngine: Thread exiting" << std::endl; +} + +void ThreadedInferenceEngine::processInferenceRequests() +{ + std::queue localQueue; + + // Copy requests to local queue to minimize lock time + { + std::unique_lock lock(requestMutex); + localQueue.swap(requestQueue); + } + + // Process all requests + while (!localQueue.empty() && !shouldStop.load()) + { + processInferenceRequest(localQueue.front()); + localQueue.pop(); + } +} + +void ThreadedInferenceEngine::processInferenceRequest(const InferenceRequest& request) +{ + if (!modelLoaded.load()) + { + std::cout << "ThreadedInferenceEngine: Model not loaded, skipping request" << std::endl; + return; + } + + std::vector voices; + + try + { + switch (request.type) + { + case InferenceRequest::RANDOM_VOICES: + { + std::cout << "ThreadedInferenceEngine: Processing random voices request" << std::endl; + voices = neuralModel->generateMultipleRandomVoices(); + + // Update buffer with new voices for next time + if (!voices.empty()) + { + std::unique_lock lock(bufferMutex); + bufferedRandomVoices = voices; + hasBufferedVoices.store(true); + isGeneratingBuffer.store(false); + } + break; + } + + case InferenceRequest::CUSTOM_VOICES: + { + std::cout << "ThreadedInferenceEngine: Processing custom voices request" << std::endl; + voices = neuralModel->generateVoices(request.latentVector); + break; + } + + case InferenceRequest::SINGLE_CUSTOM_VOICE: + { + std::cout << "ThreadedInferenceEngine: Processing single custom voice request" << std::endl; + voices = neuralModel->generateVoices(request.latentVector); + + // Call single voice callback with first voice (or nullopt if empty) + if (request.singleCallback) + { + std::optional voiceOpt = voices.empty() ? std::nullopt : std::make_optional(voices[0]); + juce::MessageManager::callAsync([callback = request.singleCallback, voiceOpt]() { + callback(voiceOpt); + }); + } + return; + } + } + + // Call multi-voice callback on main thread + if (request.callback && !voices.empty()) + { + juce::MessageManager::callAsync([callback = request.callback, voices]() { + callback(voices); + }); + } + } + catch (const std::exception& e) + { + std::cerr << "ThreadedInferenceEngine: Error processing request: " << e.what() << std::endl; + } +} + +void ThreadedInferenceEngine::requestRandomVoices(std::function)> callback) +{ + std::unique_lock lock(requestMutex); + requestQueue.emplace(InferenceRequest::RANDOM_VOICES, callback); + requestCondition.notify_one(); +} + +void ThreadedInferenceEngine::requestCustomVoices(const std::vector& latentVector, std::function)> callback) +{ + std::unique_lock lock(requestMutex); + requestQueue.emplace(InferenceRequest::CUSTOM_VOICES, latentVector, callback); + requestCondition.notify_one(); +} + +void ThreadedInferenceEngine::requestSingleCustomVoice(const std::vector& latentVector, std::function)> callback) +{ + std::unique_lock lock(requestMutex); + requestQueue.emplace(InferenceRequest::SINGLE_CUSTOM_VOICE, latentVector, callback); + requestCondition.notify_one(); +} + +bool ThreadedInferenceEngine::hasBufferedRandomVoices() const +{ + return hasBufferedVoices.load(); +} + +std::vector ThreadedInferenceEngine::getBufferedRandomVoices() +{ + std::unique_lock lock(bufferMutex); + if (hasBufferedVoices.load()) + { + auto voices = bufferedRandomVoices; + hasBufferedVoices.store(false); + + // Trigger generation of new buffer + preGenerateRandomVoices(); + + return voices; + } + + return {}; +} + +void ThreadedInferenceEngine::preGenerateRandomVoices() +{ + if (!isGeneratingBuffer.load()) + { + isGeneratingBuffer.store(true); + + // Request new random voices for the buffer + requestRandomVoices([this](std::vector voices) { + // Callback is handled in processInferenceRequest + std::cout << "ThreadedInferenceEngine: Buffer regenerated with " << voices.size() << " voices" << std::endl; + }); + } +} + +std::string ThreadedInferenceEngine::latentVectorToKey(const std::vector& latentVector) const +{ + std::string key; + key.reserve(latentVector.size() * 8); // Rough estimate for string size + + for (float value : latentVector) + { + // Round to 3 decimal places to create reasonable cache keys + int rounded = static_cast(value * 1000.0f); + key += std::to_string(rounded) + ","; + } + + return key; +} + +void ThreadedInferenceEngine::addToCache(const std::vector& latentVector, const DX7Voice& voice) +{ + std::unique_lock lock(cacheMutex); + + std::string key = latentVectorToKey(latentVector); + + // If cache is full, evict oldest entry + if (voiceCache.size() >= MAX_CACHE_SIZE) + { + evictOldestFromCache(); + } + + // Add new entry + voiceCache.emplace(key, voice); + cacheOrder.push(key); + + std::cout << "ThreadedInferenceEngine: Added voice to cache (size: " << voiceCache.size() << ")" << std::endl; +} + +void ThreadedInferenceEngine::evictOldestFromCache() +{ + if (!cacheOrder.empty()) + { + std::string oldestKey = cacheOrder.front(); + cacheOrder.pop(); + voiceCache.erase(oldestKey); + std::cout << "ThreadedInferenceEngine: Evicted oldest cache entry" << std::endl; + } +} + +bool ThreadedInferenceEngine::hasCachedVoice(const std::vector& latentVector) const +{ + std::unique_lock lock(cacheMutex); + std::string key = latentVectorToKey(latentVector); + return voiceCache.find(key) != voiceCache.end(); +} + +std::optional ThreadedInferenceEngine::getCachedVoice(const std::vector& latentVector) const +{ + std::unique_lock lock(cacheMutex); + std::string key = latentVectorToKey(latentVector); + auto it = voiceCache.find(key); + + if (it != voiceCache.end()) + { + std::cout << "ThreadedInferenceEngine: Cache hit for custom voice" << std::endl; + return it->second; + } + + std::cout << "ThreadedInferenceEngine: Voice not found in cache, returning null" << std::endl; + return std::nullopt; +} + +void ThreadedInferenceEngine::requestCachedCustomVoice(const std::vector& latentVector, std::function)> callback) +{ + // Check cache first + if (hasCachedVoice(latentVector)) + { + std::optional cachedVoice = getCachedVoice(latentVector); + juce::MessageManager::callAsync([callback, cachedVoice]() { + callback(cachedVoice); + }); + return; + } + + // Not in cache, generate and cache + requestSingleCustomVoice(latentVector, [this, latentVector, callback](std::optional voiceOpt) { + // Add to cache if voice was generated successfully + if (voiceOpt.has_value()) + { + addToCache(latentVector, voiceOpt.value()); + } + + // Call original callback + callback(voiceOpt); + }); +} + +void ThreadedInferenceEngine::preGenerateCustomVoice(const std::vector& latentVector) +{ + // Only generate if not already cached + if (hasCachedVoice(latentVector)) + { + return; + } + + std::unique_lock lock(scheduleMutex); + + // If no request is currently inflight, start one immediately + if (!isPreGenerating.load()) + { + isPreGenerating.store(true); + lock.unlock(); // Release lock before making request + + std::cout << "ThreadedInferenceEngine: Pre-generating custom voice for cache" << std::endl; + requestSingleCustomVoice(latentVector, [this, latentVector](std::optional voiceOpt) { + // Add to cache for future use if voice was generated + if (voiceOpt.has_value()) + { + addToCache(latentVector, voiceOpt.value()); + } + + // Check if there's a scheduled request to process + std::unique_lock scheduleLock(scheduleMutex); + if (scheduledRequest.has_value()) + { + auto nextRequest = std::move(scheduledRequest.value()); + scheduledRequest.reset(); + scheduleLock.unlock(); + + std::cout << "ThreadedInferenceEngine: Processing scheduled request" << std::endl; + // Process the scheduled request + requestSingleCustomVoice(nextRequest.latentVector, [this, nextRequest](std::optional scheduledVoiceOpt) { + if (scheduledVoiceOpt.has_value()) + { + addToCache(nextRequest.latentVector, scheduledVoiceOpt.value()); + } + isPreGenerating.store(false); // Allow new pre-generation requests + std::cout << "ThreadedInferenceEngine: Scheduled request completed" << std::endl; + }); + } + else + { + isPreGenerating.store(false); // Allow new pre-generation requests + std::cout << "ThreadedInferenceEngine: Pre-generated voice cached" << std::endl; + } + }); + } + else + { + // Request is inflight, schedule this one (overwriting any previous scheduled request) + if (scheduledRequest.has_value()) { + std::cout << "ThreadedInferenceEngine: Overwriting previously scheduled request" << std::endl; + } else { + std::cout << "ThreadedInferenceEngine: Scheduling request to run after current completes" << std::endl; + } + scheduledRequest = InferenceRequest(InferenceRequest::SINGLE_CUSTOM_VOICE, latentVector, [](std::optional){}); + } +} + +bool ThreadedInferenceEngine::isModelLoaded() const +{ + return modelLoaded.load(); +} \ No newline at end of file diff --git a/Source/ThreadedInferenceEngine.h b/Source/ThreadedInferenceEngine.h new file mode 100644 index 0000000..7190afb --- /dev/null +++ b/Source/ThreadedInferenceEngine.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "NeuralModelWrapper.h" +#include "DX7Voice.h" + +class ThreadedInferenceEngine : public juce::Thread +{ +public: + struct InferenceRequest + { + enum Type { RANDOM_VOICES, CUSTOM_VOICES, SINGLE_CUSTOM_VOICE }; + Type type; + std::vector latentVector; + std::function)> callback; + std::function)> singleCallback; + + InferenceRequest(Type t, std::function)> cb) + : type(t), callback(cb) {} + + InferenceRequest(Type t, const std::vector& latent, std::function)> cb) + : type(t), latentVector(latent), callback(cb) {} + + InferenceRequest(Type t, const std::vector& latent, std::function)> cb) + : type(t), latentVector(latent), singleCallback(cb) {} + }; + + ThreadedInferenceEngine(); + ~ThreadedInferenceEngine() override; + + // Thread management + void startInferenceThread(); + void stopInferenceThread(); + + // Request inference + void requestRandomVoices(std::function)> callback); + void requestCustomVoices(const std::vector& latentVector, std::function)> callback); + void requestSingleCustomVoice(const std::vector& latentVector, std::function)> callback); + + // Double buffer management + bool hasBufferedRandomVoices() const; + std::vector getBufferedRandomVoices(); + void preGenerateRandomVoices(); + + // Custom voice caching + bool hasCachedVoice(const std::vector& latentVector) const; + std::optional getCachedVoice(const std::vector& latentVector) const; + void requestCachedCustomVoice(const std::vector& latentVector, std::function)> callback); + void preGenerateCustomVoice(const std::vector& latentVector); // For debounced pre-generation + + // Thread safety + bool isModelLoaded() const; + +private: + void run() override; + void processInferenceRequests(); + void processInferenceRequest(const InferenceRequest& request); + + // Neural model wrapper + std::unique_ptr neuralModel; + + // Thread synchronization + std::mutex requestMutex; + std::condition_variable requestCondition; + std::queue requestQueue; + std::atomic shouldStop{false}; + + // Double buffer for random voices + std::mutex bufferMutex; + std::vector bufferedRandomVoices; + std::atomic hasBufferedVoices{false}; + std::atomic isGeneratingBuffer{false}; + + // Custom voice caching + static constexpr size_t MAX_CACHE_SIZE = 1000; + mutable std::mutex cacheMutex; + std::unordered_map voiceCache; + std::queue cacheOrder; // For LRU eviction + std::atomic isPreGenerating{false}; // Prevent multiple inflight cache fills + + // Request scheduling for inflight handling + std::mutex scheduleMutex; + std::optional scheduledRequest; // Next request to run after current completes + + // Helper methods + std::string latentVectorToKey(const std::vector& latentVector) const; + void addToCache(const std::vector& latentVector, const DX7Voice& voice); + void evictOldestFromCache(); + + // Model loading state + std::atomic modelLoaded{false}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ThreadedInferenceEngine) +}; \ No newline at end of file