diff --git a/CMakeLists.txt b/CMakeLists.txt index 853d078..a1d8d7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,6 +177,7 @@ target_sources(OpenStudio PRIVATE Source/MainComponent.cpp Source/MixerWindowManager.cpp Source/AppUpdater.cpp + Source/CrashDiagnostics.cpp Source/AudioEngine.cpp Source/AudioRecorder.cpp Source/PlaybackEngine.cpp @@ -370,6 +371,7 @@ if(WIN32) if(MSVC) target_link_libraries(OpenStudio PRIVATE "${webview2_SOURCE_DIR}/build/native/x64/WebView2LoaderStatic.lib" + dbghelp dwmapi ) endif() diff --git a/Source/AudioEngine.cpp b/Source/AudioEngine.cpp index 3df4e85..4226206 100644 --- a/Source/AudioEngine.cpp +++ b/Source/AudioEngine.cpp @@ -5,6 +5,7 @@ #include "S13PitchCorrector.h" #include "PitchAnalyzer.h" #include "PitchResynthesizer.h" +#include "CrashDiagnostics.h" namespace { @@ -2535,6 +2536,13 @@ static float peakFromDoubleBuffer(const juce::AudioBuffer& buffer, int n } static std::unique_ptr createBuiltInEffect(const juce::String& name); +static constexpr int kMinimumHostedPluginBlockSize = 512; + +static int getSafeHostedPluginBlockSize(int requestedBlockSize) +{ + return juce::jmax(kMinimumHostedPluginBlockSize, + requestedBlockSize > 0 ? requestedBlockSize : kMinimumHostedPluginBlockSize); +} static void prepareHostedProcessorForPrecision(juce::AudioProcessor* proc, double sampleRate, int blockSize, ProcessingPrecisionMode precisionMode) @@ -2552,7 +2560,7 @@ static void prepareHostedProcessorForPrecision(juce::AudioProcessor* proc, doubl : juce::AudioProcessor::singlePrecision); } - proc->prepareToPlay(sampleRate, blockSize); + proc->prepareToPlay(sampleRate, getSafeHostedPluginBlockSize(blockSize)); } static void prepareHostedProcessorPreservingLayout(juce::AudioProcessor* proc, double sampleRate, @@ -2571,13 +2579,23 @@ static void prepareHostedProcessorPreservingLayout(juce::AudioProcessor* proc, d : juce::AudioProcessor::singlePrecision); } + const int safeBlockSize = getSafeHostedPluginBlockSize(blockSize); auto savedLayout = proc->getBusesLayout(); - proc->prepareToPlay(sampleRate, blockSize); + proc->prepareToPlay(sampleRate, safeBlockSize); if (proc->getBusesLayout() != savedLayout) { - proc->setBusesLayout(savedLayout); - proc->prepareToPlay(sampleRate, blockSize); + if (proc->setBusesLayout(savedLayout)) + { + proc->prepareToPlay(sampleRate, safeBlockSize); + } + else + { + juce::Logger::writeToLog("AudioEngine: Plugin refused saved bus layout during stage prepare: " + + proc->getName() + + " requestedBlock=" + juce::String(blockSize) + + " safeBlock=" + juce::String(safeBlockSize)); + } } } @@ -2728,6 +2746,9 @@ static float findPeakInDoubleBuffer(const juce::AudioBuffer& buffer, int AudioEngine::AudioEngine() { + OpenStudioCrashDiagnostics::installCrashHandlers(); + OpenStudioCrashDiagnostics::recordBreadcrumb("audio_engine_constructed"); + // Initialize graphs and managers BEFORE opening the audio device. // addAudioCallback() (below) triggers audioDeviceAboutToStart() synchronously // on an already-running device. audioDeviceAboutToStart() checks @@ -3003,7 +3024,7 @@ void AudioEngine::rebindStageEditors(const std::shared_ptr& bool AudioEngine::publishMasterStageSpec(const DesiredFXStageSpec& spec) { const double sr = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; - const int preparedBlockSize = currentBlockSize > 0 ? currentBlockSize : 512; + const int preparedBlockSize = getSafeHostedPluginBlockSize(currentBlockSize); const double buildStart = juce::Time::getMillisecondCounterHiRes(); juce::String errorMessage; auto stage = buildActiveFXStage(spec, sr, preparedBlockSize, processingPrecisionMode, false, errorMessage); @@ -3034,7 +3055,7 @@ bool AudioEngine::publishMasterStageSpec(const DesiredFXStageSpec& spec) bool AudioEngine::publishMonitoringStageSpec(const DesiredFXStageSpec& spec) { const double sr = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; - const int preparedBlockSize = currentBlockSize > 0 ? currentBlockSize : 512; + const int preparedBlockSize = getSafeHostedPluginBlockSize(currentBlockSize); const double buildStart = juce::Time::getMillisecondCounterHiRes(); juce::String errorMessage; auto stage = buildActiveFXStage(spec, sr, preparedBlockSize, processingPrecisionMode, true, errorMessage); @@ -3421,6 +3442,9 @@ void AudioEngine::updateSpectrumAnalyzer (const float* const* outputChannelData, if (spectrumWritePos >= FFT_SIZE) { spectrumWritePos = 0; + spectrumFftDecimationCounter = (spectrumFftDecimationCounter + 1) & 3; + if (spectrumFftDecimationCounter != 0) + continue; float fftData[FFT_SIZE * 2] = {}; std::memcpy (fftData, spectrumInputBuffer, sizeof (float) * static_cast (FFT_SIZE)); @@ -3469,6 +3493,13 @@ void AudioEngine::processMasterFXChain (const std::shared_ptrgetCallbackLock()); + if (!pluginProcessLock.isLocked()) + { + masterFXBusySkipCount.fetch_add(1, std::memory_order_relaxed); + continue; + } + const bool canProcessDouble = useHybrid64Summing && !slot.forceFloat && slot.supportsDouble @@ -3557,6 +3588,13 @@ void AudioEngine::processMonitoringFXChain (const std::shared_ptrgetCallbackLock()); + if (!pluginProcessLock.isLocked()) + { + monitoringFXBusySkipCount.fetch_add(1, std::memory_order_relaxed); + continue; + } + const bool canProcessDouble = hybrid64PostChainActive && !slot.forceFloat && slot.supportsDouble @@ -4417,7 +4455,7 @@ juce::String AudioEngine::addTrack(const juce::String& explicitId) trackOrder.push_back(trackId); // Pre-allocate sidechain output buffer for this track (avoids heap alloc on audio thread) - int scBlockSize = currentBlockSize > 0 ? currentBlockSize : 512; + int scBlockSize = getSafeHostedPluginBlockSize(currentBlockSize); auto sidechainBuffer = std::make_shared>(); sidechainBuffer->setSize(2, scBlockSize); sidechainOutputBuffers[trackId] = sidechainBuffer; @@ -4809,10 +4847,13 @@ bool AudioEngine::loadInstrument(const juce::String& trackId, const juce::String // Load the VST instrument using PluginManager with actual device rate double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); + OpenStudioCrashDiagnostics::recordBreadcrumb("load_instrument_begin", + "trackId=" + trackId + " path=" + vstPath + " sr=" + juce::String(sr) + " block=" + juce::String(bs)); auto plugin = pluginManager.loadPluginFromFile(vstPath, sr, bs); if (!plugin) { + OpenStudioCrashDiagnostics::recordBreadcrumb("load_instrument_failed", "trackId=" + trackId + " path=" + vstPath); juce::Logger::writeToLog("AudioEngine: Failed to load instrument: " + vstPath); return false; } @@ -4821,6 +4862,7 @@ bool AudioEngine::loadInstrument(const juce::String& trackId, const juce::String auto* pluginInstance = dynamic_cast(plugin.get()); if (!pluginInstance) { + OpenStudioCrashDiagnostics::recordBreadcrumb("load_instrument_not_audio_plugin", "trackId=" + trackId + " path=" + vstPath); juce::Logger::writeToLog("AudioEngine: Plugin is not an AudioPluginInstance: " + vstPath); return false; } @@ -4836,6 +4878,8 @@ bool AudioEngine::loadInstrument(const juce::String& trackId, const juce::String trackMap[trackId]->setInstrument(std::move(instrumentPtr), sr, bs); trackMap[trackId]->setTrackType(TrackType::Instrument); + OpenStudioCrashDiagnostics::recordBreadcrumb("load_instrument_success", + "trackId=" + trackId + " path=" + vstPath + " block=" + juce::String(bs)); juce::Logger::writeToLog("AudioEngine: Loaded instrument on track " + trackId + ": " + vstPath); return true; } @@ -4874,26 +4918,57 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, // Apply MIDI CC mappings: route incoming CC values to mapped plugin parameters if (message.isController()) { - const juce::ScopedLock sl(midiLearnLock); - int ccNum = message.getControllerNumber(); - float normalizedValue = message.getControllerValue() / 127.0f; + struct PendingMappedParameterUpdate + { + juce::String trackId; + int pluginIndex = -1; + int paramIndex = -1; + float normalizedValue = 0.0f; + }; + + juce::Array updates; + { + const juce::ScopedLock sl(midiLearnLock); + const int ccNum = message.getControllerNumber(); + const float normalizedValue = message.getControllerValue() / 127.0f; + + for (const auto& mapping : midiLearnMappings) + { + if (mapping.ccNumber == ccNum) + { + PendingMappedParameterUpdate update; + update.trackId = mapping.trackId; + update.pluginIndex = mapping.pluginIndex; + update.paramIndex = mapping.paramIndex; + update.normalizedValue = normalizedValue; + updates.add(update); + } + } + } - for (const auto& mapping : midiLearnMappings) + if (!updates.isEmpty()) { - if (mapping.ccNumber == ccNum) + midiMappedParameterUpdateCount.fetch_add(updates.size(), std::memory_order_relaxed); + juce::MessageManager::callAsync([this, updates]() { - auto it = trackMap.find(mapping.trackId); - if (it != trackMap.end() && it->second) + for (const auto& update : updates) { - auto* processor = it->second->getTrackFXProcessor(mapping.pluginIndex); - if (processor) + auto it = trackMap.find(update.trackId); + if (it == trackMap.end() || it->second == nullptr) + continue; + + auto processor = it->second->getTrackFXProcessorShared(update.pluginIndex); + if (!processor) + continue; + + const juce::ScopedLock processorLock(processor->getCallbackLock()); + const auto& params = processor->getParameters(); + if (update.paramIndex >= 0 && params.size() > update.paramIndex) { - const auto& params = processor->getParameters(); - if (params.size() > mapping.paramIndex) - params[mapping.paramIndex]->setValueNotifyingHost(normalizedValue); + params[update.paramIndex]->setValueNotifyingHost(update.normalizedValue); } } - } + }); } } @@ -4922,6 +4997,7 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, } // Route MIDI to appropriate tracks + int fanoutCount = 0; for (auto const& [id, track] : trackMap) { if (!track) continue; @@ -4946,7 +5022,10 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, // Audio/MIDI tracks require explicit input monitoring toggle. if (track->getInputMonitoring() || track->getTrackType() == TrackType::Instrument) - track->enqueueMidiMessage(message, sampleOffset); + { + if (track->enqueueMidiMessage(message, sampleOffset)) + ++fanoutCount; + } // Record MIDI event if armed and in record mode if (isPlaying && isRecordMode && track->getRecordArmed()) @@ -4955,6 +5034,14 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, midiRecorder.recordEvent(id, timestamp, message); } } + + midiLastInputFanoutCount.store(fanoutCount, std::memory_order_relaxed); + int prevFanoutMax = midiMaxInputFanoutCount.load(std::memory_order_relaxed); + while (fanoutCount > prevFanoutMax + && !midiMaxInputFanoutCount.compare_exchange_weak(prevFanoutMax, fanoutCount, + std::memory_order_relaxed)) + { + } } juce::MidiBuffer AudioEngine::buildTrackMidiBlock(const juce::String& trackId, double blockStartTimeSeconds, @@ -5654,7 +5741,7 @@ juce::var AudioEngine::getPluginCapabilities(const juce::String& pluginPath) obj->setProperty("fileOrIdentifier", pluginPath); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); auto plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); if (!plugin) { @@ -6295,10 +6382,15 @@ bool AudioEngine::addTrackInputFX(const juce::String& trackId, const juce::Strin // crackling and distortion. The plugin can still process 32-sample blocks fine when // prepared with a larger maximumExpectedSamplesPerBlock. double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); + OpenStudioCrashDiagnostics::recordBreadcrumb("add_track_input_fx_begin", + "trackId=" + trackId + " path=" + pluginPath + " sr=" + juce::String(sr) + " block=" + juce::String(bs)); auto plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); if (!plugin) + { + OpenStudioCrashDiagnostics::recordBreadcrumb("add_track_input_fx_failed", "trackId=" + trackId + " path=" + pluginPath); return false; + } // Provide tempo/position info to the plugin plugin->setPlayHead (this); @@ -6323,6 +6415,8 @@ bool AudioEngine::addTrackInputFX(const juce::String& trackId, const juce::Strin if (openEditor) openPluginEditor(trackId, fxIndex, true); juce::Logger::writeToLog("AudioEngine: Added input FX" + juce::String(openEditor ? " and opened editor" : "")); + OpenStudioCrashDiagnostics::recordBreadcrumb("add_track_input_fx_success", + "trackId=" + trackId + " fxIndex=" + juce::String(fxIndex) + " block=" + juce::String(bs)); recalculatePDC(); } return success; @@ -6334,16 +6428,21 @@ bool AudioEngine::addTrackFX(const juce::String& trackId, const juce::String& pl // Use actual device sample rate so the plugin initialises at the correct rate. // IMPORTANT: Clamp max-block-size to at least 512 — same rationale as addTrackInputFX. double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); logToDisk("AudioEngine::addTrackFX DIAGNOSTIC"); logToDisk(" currentSampleRate=" + juce::String(currentSampleRate) + " currentBlockSize=" + juce::String(currentBlockSize)); logToDisk(" creating plugin at sr=" + juce::String(sr) + " bs=" + juce::String(bs)); + OpenStudioCrashDiagnostics::recordBreadcrumb("add_track_fx_begin", + "trackId=" + trackId + " path=" + pluginPath + " sr=" + juce::String(sr) + " block=" + juce::String(bs)); auto plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); if (!plugin) + { + OpenStudioCrashDiagnostics::recordBreadcrumb("add_track_fx_failed", "trackId=" + trackId + " path=" + pluginPath); return false; + } logToDisk(" plugin created: " + plugin->getName() + " inCh=" + juce::String(plugin->getTotalNumInputChannels()) + @@ -6390,7 +6489,7 @@ bool AudioEngine::addTrackFX(const juce::String& trackId, const juce::String& pl { logToDisk(" Trying ARA init for plugin at index " + juce::String(fxIndex)); it2->second->initializeARA(fxIndex, currentSampleRate > 0 ? currentSampleRate : 44100.0, - currentBlockSize > 0 ? currentBlockSize : 512, + getSafeHostedPluginBlockSize(currentBlockSize), [this, trackIdCopy, fxIndexCopy, openEditorCopy] (bool araSuccess, bool pluginSupportsARA, const juce::String& errorMessage) { if (araSuccess) { @@ -6425,6 +6524,8 @@ bool AudioEngine::addTrackFX(const juce::String& trackId, const juce::String& pl } logToDisk(" addTrackFX complete (ARA check pending)"); + OpenStudioCrashDiagnostics::recordBreadcrumb("add_track_fx_success", + "trackId=" + trackId + " fxIndex=" + juce::String(fxIndex) + " block=" + juce::String(bs)); } return success; } @@ -6456,7 +6557,7 @@ bool AudioEngine::addTrackBuiltInFX(const juce::String& trackId, const juce::Str } double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); plugin->setPlayHead(this); prepareHostedProcessorForPrecision(plugin.get(), sr, bs, processingPrecisionMode); @@ -6497,7 +6598,7 @@ bool AudioEngine::addMasterBuiltInFX(const juce::String& effectName) return false; double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); plugin->setPlayHead(this); prepareHostedProcessorForPrecision(plugin.get(), sr, bs, processingPrecisionMode); @@ -6555,7 +6656,7 @@ bool AudioEngine::addTrackS13FX(const juce::String& trackId, const juce::String& s13fx->setPlayHead(this); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); bool success = false; { @@ -6591,7 +6692,7 @@ bool AudioEngine::addMasterS13FX(const juce::String& scriptPath) s13fx->setPlayHead(this); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); prepareHostedProcessorForPrecision(s13fx.get(), sr, bs, processingPrecisionMode); @@ -6822,6 +6923,7 @@ void AudioEngine::openPluginEditor(const juce::String& trackId, int fxIndex, boo return; juce::AudioProcessor* processor = nullptr; + std::shared_ptr processorOwner; juce::String windowTitle; // Calculate display index (1-based) @@ -6831,21 +6933,29 @@ void AudioEngine::openPluginEditor(const juce::String& trackId, int fxIndex, boo { if (fxIndex >= 0 && fxIndex < track->getNumInputFX()) { - processor = track->getInputFXProcessor(fxIndex); - windowTitle = "Track " + juce::String(displayIndex) + " - " + processor->getName(); + processorOwner = track->getInputFXProcessorShared(fxIndex); + processor = processorOwner.get(); + if (processor != nullptr) + windowTitle = "Track " + juce::String(displayIndex) + " - " + processor->getName(); } } else { if (fxIndex >= 0 && fxIndex < track->getNumTrackFX()) { - processor = track->getTrackFXProcessor(fxIndex); - windowTitle = "Track " + juce::String(displayIndex) + " - " + processor->getName(); + processorOwner = track->getTrackFXProcessorShared(fxIndex); + processor = processorOwner.get(); + if (processor != nullptr) + windowTitle = "Track " + juce::String(displayIndex) + " - " + processor->getName(); } } if (processor) { + OpenStudioCrashDiagnostics::recordBreadcrumb("open_plugin_editor_requested", + "trackId=" + trackId + " fxIndex=" + juce::String(fxIndex) + + " inputFX=" + juce::String(isInputFX ? "true" : "false") + + " plugin=" + processor->getName()); // Defer window creation to next message-loop cycle — creating a native // DocumentWindow from inside a WebView2 NativeFunction callback can cause // re-entrancy crashes with the Windows message pump. @@ -6855,7 +6965,7 @@ void AudioEngine::openPluginEditor(const juce::String& trackId, int fxIndex, boo : PluginWindowManager::PluginEditorTarget::Scope::TrackFX; target.trackId = trackId; target.fxIndex = fxIndex; - auto* proc = processor; + auto proc = processorOwner; auto title = windowTitle; juce::MessageManager::callAsync([this, proc, title, target]() { @@ -6878,13 +6988,15 @@ void AudioEngine::openInstrumentEditor(const juce::String& trackId) if (!track) return; - auto* instrument = track->getInstrument(); + std::shared_ptr instrument = track->getInstrumentShared(); if (instrument) { int displayIndex = getTrackIndex(trackId) + 1; juce::String windowTitle = "Track " + juce::String(displayIndex) + " - " + instrument->getName(); - auto* proc = instrument; + auto proc = instrument; auto title = windowTitle; + OpenStudioCrashDiagnostics::recordBreadcrumb("open_instrument_editor_requested", + "trackId=" + trackId + " plugin=" + instrument->getName()); PluginWindowManager::PluginEditorTarget target; target.scope = PluginWindowManager::PluginEditorTarget::Scope::Instrument; target.trackId = trackId; @@ -7333,7 +7445,7 @@ bool AudioEngine::addMasterFX(const juce::String& pluginPath) // Load the plugin with actual device sample rate & block size double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); auto plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); if (!plugin) { @@ -7353,7 +7465,7 @@ bool AudioEngine::addMasterFX(const juce::String& pluginPath) prepareHostedProcessorForPrecision( plugin.get(), device->getCurrentSampleRate(), - device->getCurrentBufferSizeSamples(), + getSafeHostedPluginBlockSize(device->getCurrentBufferSizeSamples()), processingPrecisionMode); } @@ -7471,7 +7583,7 @@ void AudioEngine::openMasterFXEditor(int fxIndex) juce::MessageManager::callAsync([this, processor, title, target]() { if (processor) - pluginWindowManager.openEditor(processor.get(), title, target); + pluginWindowManager.openEditor(processor, title, target); }); } } @@ -7519,7 +7631,7 @@ bool AudioEngine::addMonitoringFX(const juce::String& pluginPath) juce::Logger::writeToLog("AudioEngine: addMonitoringFX called with: " + pluginPath); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); auto plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); if (!plugin) { @@ -7535,7 +7647,7 @@ bool AudioEngine::addMonitoringFX(const juce::String& pluginPath) prepareHostedProcessorForPrecision( plugin.get(), device->getCurrentSampleRate(), - device->getCurrentBufferSizeSamples(), + getSafeHostedPluginBlockSize(device->getCurrentBufferSizeSamples()), processingPrecisionMode); } @@ -7654,7 +7766,7 @@ void AudioEngine::openMonitoringFXEditor(int fxIndex) juce::MessageManager::callAsync([this, processor, title, target]() { if (processor) - pluginWindowManager.openEditor(processor.get(), title, target); + pluginWindowManager.openEditor(processor, title, target); }); } } @@ -8370,10 +8482,21 @@ juce::var AudioEngine::getAudioDebugSnapshot() const juce::Array playbackTracks; for (const auto& [trackId, track] : trackMap) { - juce::ignoreUnused(track); auto* trackObj = new juce::DynamicObject(); trackObj->setProperty("trackId", trackId); trackObj->setProperty("clipCount", playbackEngine.getNumClipsForTrack(trackId)); + if (track != nullptr) + { + trackObj->setProperty("pluginBusySkipCount", track->getPluginBusySkipCount()); + trackObj->setProperty("midiOverflowCount", track->getMidiOverflowCount()); + trackObj->setProperty("lastBuiltMidiEventCount", track->getLastBuiltMidiEventCount()); + trackObj->setProperty("maxBuiltMidiEventCount", track->getMaxBuiltMidiEventCount()); + trackObj->setProperty("inputFXCount", track->getNumInputFX()); + trackObj->setProperty("trackFXCount", track->getNumTrackFX()); + trackObj->setProperty("isInstrument", track->getTrackType() == TrackType::Instrument); + trackObj->setProperty("recordArmed", track->getRecordArmed()); + trackObj->setProperty("inputMonitoring", track->getInputMonitoring()); + } playbackTracks.add(juce::var(trackObj)); } @@ -8395,6 +8518,8 @@ juce::var AudioEngine::getAudioDebugSnapshot() const root->setProperty("audioCallbackTrackBufferResizeCount", audioCallbackTrackBufferResizeCount.load(std::memory_order_relaxed)); root->setProperty("audioCallbackPitchScrubBufferResizeCount", audioCallbackPitchScrubBufferResizeCount.load(std::memory_order_relaxed)); root->setProperty("audioCallbackSidechainBufferResizeCount", audioCallbackSidechainBufferResizeCount.load(std::memory_order_relaxed)); + root->setProperty("crashBreadcrumbLogPath", OpenStudioCrashDiagnostics::getBreadcrumbLogFile().getFullPathName()); + root->setProperty("lastCrashDumpPath", OpenStudioCrashDiagnostics::getLastCrashDumpFile().getFullPathName()); root->setProperty("spectrumFftPublishCount", static_cast(spectrumFftPublishCount.load(std::memory_order_relaxed))); root->setProperty("spectrumFftLockMissCount", static_cast(spectrumFftLockMissCount.load(std::memory_order_relaxed))); root->setProperty("postTrackPlaybackPeak", lastPostTrackPlaybackPeak.load(std::memory_order_relaxed)); @@ -8403,6 +8528,9 @@ juce::var AudioEngine::getAudioDebugSnapshot() const root->setProperty("postMonitoringFxPeak", lastPostMonitoringFXPeak.load(std::memory_order_relaxed)); root->setProperty("finalOutputPeak", lastFinalOutputPeak.load(std::memory_order_relaxed)); root->setProperty("lastRecordingClipCountReturned", lastReturnedRecordingClipCount.load(std::memory_order_relaxed)); + root->setProperty("midiLastInputFanoutCount", midiLastInputFanoutCount.load(std::memory_order_relaxed)); + root->setProperty("midiMaxInputFanoutCount", midiMaxInputFanoutCount.load(std::memory_order_relaxed)); + root->setProperty("midiMappedParameterUpdateCount", midiMappedParameterUpdateCount.load(std::memory_order_relaxed)); root->setProperty("playbackTryLockFailureCount", playbackEngine.getTryLockFailureCount()); root->setProperty("playbackMissingReaderCount", playbackEngine.getMissingReaderCount()); root->setProperty("lastOverlappingClipCount", playbackEngine.getLastOverlappingClipCount()); @@ -8412,6 +8540,8 @@ juce::var AudioEngine::getAudioDebugSnapshot() const root->setProperty("playbackPitchShiftWorkBufferResizeCount", playbackEngine.getPitchShiftWorkBufferResizeCount()); root->setProperty("playbackRenderResampleScratchResizeCount", playbackEngine.getRenderResampleScratchResizeCount()); root->setProperty("playbackChunkBoundaryReserveCount", playbackEngine.getChunkBoundaryReserveCount()); + root->setProperty("playbackAudioDataCacheMissCount", playbackEngine.getAudioDataCacheMissCount()); + root->setProperty("playbackAudioDataCachedFileCount", playbackEngine.getAudioDataCachedFileCount()); const auto pitchRouting = playbackEngine.getPitchPreviewRoutingStatus({}); root->setProperty("pitchRouteMonitorMode", pitchRouting.monitorMode); root->setProperty("pitchRouteRenderedSegmentActive", pitchRouting.renderedSegmentActive); @@ -13887,7 +14017,7 @@ juce::var AudioEngine::initializeARAForTrack(const juce::String& trackId, int fx } auto* track = it->second; - bool success = track->initializeARA(fxIndex, currentSampleRate, currentBlockSize); + bool success = track->initializeARA(fxIndex, currentSampleRate, getSafeHostedPluginBlockSize(currentBlockSize)); obj->setProperty("success", success); if (!success) @@ -13958,7 +14088,7 @@ juce::var AudioEngine::addARAClip(const juce::String& trackId, const juce::Strin { const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; - int bs = currentBlockSize > 0 ? currentBlockSize : 512; + int bs = getSafeHostedPluginBlockSize(currentBlockSize); const bool forceFloat = it->second->getTrackFXPrecisionOverride(araFxIdx); prepareHostedProcessorPreservingLayout(araPlugin, sr, bs, forceFloat ? ProcessingPrecisionMode::Float32 : processingPrecisionMode); diff --git a/Source/AudioEngine.h b/Source/AudioEngine.h index 2ef363b..2343c43 100644 --- a/Source/AudioEngine.h +++ b/Source/AudioEngine.h @@ -708,6 +708,9 @@ class AudioEngine : public juce::AudioIODeviceCallback, std::atomic midiLateEventCount { 0 }; std::atomic midiMaxEventsPerBlock { 0 }; std::atomic midiLastComputedSampleOffset { 0 }; + std::atomic midiLastInputFanoutCount { 0 }; + std::atomic midiMaxInputFanoutCount { 0 }; + std::atomic midiMappedParameterUpdateCount { 0 }; ProcessingPrecisionMode processingPrecisionMode { ProcessingPrecisionMode::Float32 }; // Master FX (Phase 5) @@ -840,6 +843,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, float spectrumInputBuffer[FFT_SIZE * 2] = {}; // ring buffer for FFT input float spectrumOutputBuffer[FFT_SIZE] = {}; // magnitude spectrum int spectrumWritePos { 0 }; + int spectrumFftDecimationCounter { 0 }; bool spectrumReady { false }; juce::CriticalSection spectrumLock; std::atomic spectrumFftPublishCount { 0 }; diff --git a/Source/CrashDiagnostics.cpp b/Source/CrashDiagnostics.cpp new file mode 100644 index 0000000..7561d3b --- /dev/null +++ b/Source/CrashDiagnostics.cpp @@ -0,0 +1,109 @@ +#include "CrashDiagnostics.h" + +#include + +#if JUCE_WINDOWS + #include + #include +#endif + +namespace +{ +std::once_flag installOnce; + +juce::File getDiagnosticsDirectory() +{ + auto dir = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory) + .getChildFile("OpenStudio"); + dir.createDirectory(); + return dir; +} + +void rotateIfLarge(const juce::File& file) +{ + constexpr juce::int64 maxBytes = 512 * 1024; + if (!file.existsAsFile() || file.getSize() <= maxBytes) + return; + + const auto archived = file.getSiblingFile(file.getFileNameWithoutExtension() + ".1." + file.getFileExtension()); + archived.deleteFile(); + file.moveFileTo(archived); +} + +#if JUCE_WINDOWS +LONG WINAPI openStudioUnhandledExceptionFilter(EXCEPTION_POINTERS* exceptionInfo) +{ + const auto dumpFile = OpenStudioCrashDiagnostics::getLastCrashDumpFile(); + HANDLE dumpHandle = CreateFileW(dumpFile.getFullPathName().toWideCharPointer(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (dumpHandle != INVALID_HANDLE_VALUE) + { + MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo {}; + dumpExceptionInfo.ThreadId = GetCurrentThreadId(); + dumpExceptionInfo.ExceptionPointers = exceptionInfo; + dumpExceptionInfo.ClientPointers = FALSE; + + MiniDumpWriteDump(GetCurrentProcess(), + GetCurrentProcessId(), + dumpHandle, + MiniDumpNormal, + exceptionInfo != nullptr ? &dumpExceptionInfo : nullptr, + nullptr, + nullptr); + CloseHandle(dumpHandle); + } + + juce::String detail = "dump=" + dumpFile.getFullPathName(); + if (exceptionInfo != nullptr && exceptionInfo->ExceptionRecord != nullptr) + { + detail += " code=0x" + juce::String::toHexString( + static_cast(exceptionInfo->ExceptionRecord->ExceptionCode)); + } + OpenStudioCrashDiagnostics::recordBreadcrumb("unhandled_exception", detail); + return EXCEPTION_CONTINUE_SEARCH; +} +#endif +} + +namespace OpenStudioCrashDiagnostics +{ +juce::File getBreadcrumbLogFile() +{ + return getDiagnosticsDirectory().getChildFile("crash_breadcrumbs.log"); +} + +juce::File getLastCrashDumpFile() +{ + return getDiagnosticsDirectory().getChildFile("last_crash.dmp"); +} + +void recordBreadcrumb(const juce::String& stage, const juce::String& detail) +{ + const auto logFile = getBreadcrumbLogFile(); + rotateIfLarge(logFile); + + juce::String line = juce::Time::getCurrentTime().toString(true, true) + + " | " + stage; + if (detail.isNotEmpty()) + line += " | " + detail; + + logFile.appendText(line + "\n"); +} + +void installCrashHandlers() +{ + std::call_once(installOnce, [] + { +#if JUCE_WINDOWS + SetUnhandledExceptionFilter(openStudioUnhandledExceptionFilter); +#endif + recordBreadcrumb("crash_handlers_installed"); + }); +} +} diff --git a/Source/CrashDiagnostics.h b/Source/CrashDiagnostics.h new file mode 100644 index 0000000..b820361 --- /dev/null +++ b/Source/CrashDiagnostics.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace OpenStudioCrashDiagnostics +{ + void installCrashHandlers(); + void recordBreadcrumb(const juce::String& stage, const juce::String& detail = {}); + juce::File getBreadcrumbLogFile(); + juce::File getLastCrashDumpFile(); +} diff --git a/Source/PeakCache.cpp b/Source/PeakCache.cpp index 0727504..8dabb20 100644 --- a/Source/PeakCache.cpp +++ b/Source/PeakCache.cpp @@ -64,95 +64,83 @@ juce::var PeakCache::getPeaks(const juce::File& audioFile, int startSample, int numPixels) const { - juce::Array peakData; - - CacheEntry entry; - bool loaded = false; - - // Try memory cache - { - const juce::ScopedLock sl(cacheLock); - auto it = memoryCache.find(audioFile.getFullPathName()); - if (it != memoryCache.end()) - { - entry = it->second; - loaded = true; - } - } - - // Try loading from disk - if (!loaded) + auto buildResult = [samplesPerPixel, startSample, numPixels](const CacheEntry& entry) { - auto peakFile = getPeakFilePath(audioFile); - if (!peakFile.existsAsFile()) - peakFile = getLegacyPeakFilePath(audioFile); + juce::Array peakData; + if (entry.levels.empty()) + return juce::var(peakData); - if (peakFile.existsAsFile() && loadFromFile(peakFile, audioFile, entry)) + // Find the best mipmap level (closest stride <= samplesPerPixel) + const MipmapLevel* bestLevel = &entry.levels[0]; + for (const auto& level : entry.levels) { - loaded = true; - // Store in memory cache for next time - const juce::ScopedLock sl(cacheLock); - memoryCache[audioFile.getFullPathName()] = entry; + if (level.stride <= samplesPerPixel) + bestLevel = &level; } - } - - if (!loaded || entry.levels.empty()) - return peakData; - - // Find the best mipmap level (closest stride <= samplesPerPixel) - const MipmapLevel* bestLevel = &entry.levels[0]; - for (const auto& level : entry.levels) - { - if (level.stride <= samplesPerPixel) - bestLevel = &level; - } - // Calculate how many source peaks we need per output pixel - int ratio = samplesPerPixel / bestLevel->stride; - if (ratio < 1) ratio = 1; + int ratio = samplesPerPixel / bestLevel->stride; + if (ratio < 1) ratio = 1; - int numChannels = bestLevel->numChannels; + const int numChannels = bestLevel->numChannels; + const int startPeak = (startSample > 0) ? std::min(startSample / bestLevel->stride, bestLevel->numPeaks - 1) : 0; + const int remainingMipmapPeaks = std::max(0, bestLevel->numPeaks - startPeak); + const int actualPeaks = std::min(numPixels, remainingMipmapPeaks / std::max(1, ratio)); - // Convert startSample to an index in this mipmap level, clamped to valid range - int startPeak = (startSample > 0) ? std::min(startSample / bestLevel->stride, bestLevel->numPeaks - 1) : 0; - // remainingPeaks: mipmap-level peaks left after startPeak. - // Divide by ratio to get output pixels. (startPeak is in mipmap units, NOT output-pixel units.) - int remainingMipmapPeaks = std::max(0, bestLevel->numPeaks - startPeak); - int actualPeaks = std::min(numPixels, remainingMipmapPeaks / std::max(1, ratio)); + peakData.ensureStorageAllocated(1 + actualPeaks * numChannels * 2); + peakData.add(juce::var(numChannels)); - peakData.ensureStorageAllocated(1 + actualPeaks * numChannels * 2); - peakData.add(juce::var(numChannels)); // Header - - int stride2 = numChannels * 2; // Floats per peak entry in the data array - - for (int pixel = 0; pixel < actualPeaks; ++pixel) - { - int srcStart = startPeak + pixel * ratio; - int srcEnd = std::min(srcStart + ratio, bestLevel->numPeaks); - - for (int ch = 0; ch < numChannels; ++ch) + const int stride2 = numChannels * 2; + for (int pixel = 0; pixel < actualPeaks; ++pixel) { - float minVal = 0.0f; - float maxVal = 0.0f; + const int srcStart = startPeak + pixel * ratio; + const int srcEnd = std::min(srcStart + ratio, bestLevel->numPeaks); - for (int s = srcStart; s < srcEnd; ++s) + for (int ch = 0; ch < numChannels; ++ch) { - int dataIdx = s * stride2 + ch * 2; - if (dataIdx + 1 < (int)bestLevel->data.size()) + float minVal = 0.0f; + float maxVal = 0.0f; + + for (int s = srcStart; s < srcEnd; ++s) { - float mn = bestLevel->data[static_cast(dataIdx)]; - float mx = bestLevel->data[static_cast(dataIdx) + 1]; - if (s == srcStart || mn < minVal) minVal = mn; - if (s == srcStart || mx > maxVal) maxVal = mx; + const int dataIdx = s * stride2 + ch * 2; + if (dataIdx + 1 < static_cast(bestLevel->data.size())) + { + const float mn = bestLevel->data[static_cast(dataIdx)]; + const float mx = bestLevel->data[static_cast(dataIdx) + 1]; + if (s == srcStart || mn < minVal) minVal = mn; + if (s == srcStart || mx > maxVal) maxVal = mx; + } } - } - peakData.add(juce::var(minVal)); - peakData.add(juce::var(maxVal)); + peakData.add(juce::var(minVal)); + peakData.add(juce::var(maxVal)); + } } + + return juce::var(peakData); + }; + + { + const juce::ScopedLock sl(cacheLock); + auto it = memoryCache.find(audioFile.getFullPathName()); + if (it != memoryCache.end()) + return buildResult(it->second); } - return peakData; + CacheEntry entry; + auto peakFile = getPeakFilePath(audioFile); + if (!peakFile.existsAsFile()) + peakFile = getLegacyPeakFilePath(audioFile); + + if (!peakFile.existsAsFile() || !loadFromFile(peakFile, audioFile, entry)) + return juce::var(juce::Array()); + + auto result = buildResult(entry); + { + const juce::ScopedLock sl(cacheLock); + memoryCache[audioFile.getFullPathName()] = std::move(entry); + } + return result; } bool PeakCache::loadFromFile(const juce::File& peakFile, const juce::File& audioFile, CacheEntry& entry) const diff --git a/Source/PeakCache.h b/Source/PeakCache.h index 35f0a07..d6b5616 100644 --- a/Source/PeakCache.h +++ b/Source/PeakCache.h @@ -119,8 +119,9 @@ class PeakCache std::set pendingGenerations; juce::CriticalSection pendingLock; - // Background thread pool for concurrent peak generation - juce::ThreadPool backgroundPool { juce::jmax(2, juce::SystemStats::getNumCpus() / 2) }; + // Keep waveform generation serialized and low-impact; concurrent full-file + // peak scans can steal disk/CPU from 32/64-sample monitoring callbacks. + juce::ThreadPool backgroundPool { 1 }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PeakCache) }; diff --git a/Source/PlaybackEngine.cpp b/Source/PlaybackEngine.cpp index cc24f82..10b4252 100644 --- a/Source/PlaybackEngine.cpp +++ b/Source/PlaybackEngine.cpp @@ -4,11 +4,7 @@ namespace { -#if JUCE_DEBUG -constexpr bool kAudioPlaybackDebugLogs = true; -#else constexpr bool kAudioPlaybackDebugLogs = false; -#endif static void logAudioPlayback(const juce::String& message) { @@ -149,6 +145,7 @@ PlaybackEngine::~PlaybackEngine() { juce::ScopedLock sl(lock); readers.clear(); + audioDataCache.clear(); clips.clear(); } @@ -166,6 +163,7 @@ void PlaybackEngine::preloadReader(const juce::File& file) std::unique_ptr newReader(formatManager.createReaderFor(file)); if (newReader) { + preloadAudioData(file, *newReader); readers[filePath] = std::move(newReader); readerAccessTimes[filePath] = juce::Time::currentTimeMillis(); evictOldReaders(); @@ -173,15 +171,42 @@ void PlaybackEngine::preloadReader(const juce::File& file) } } +void PlaybackEngine::preloadAudioData(const juce::File& file, juce::AudioFormatReader& reader) +{ + constexpr juce::int64 maxCachedAudioBytesPerFile = 256LL * 1024LL * 1024LL; + const auto filePath = file.getFullPathName(); + const int channels = static_cast(reader.numChannels); + const auto length = reader.lengthInSamples; + if (channels <= 0 || length <= 0 || length > std::numeric_limits::max()) + { + audioDataCache.erase(filePath); + return; + } + + const juce::int64 requiredBytes = length * static_cast(channels) * static_cast(sizeof(float)); + if (requiredBytes > maxCachedAudioBytesPerFile) + { + audioDataCache.erase(filePath); + return; + } + + auto cached = std::make_shared(); + cached->sampleRate = reader.sampleRate; + cached->lengthInSamples = length; + cached->numChannels = channels; + cached->buffer.setSize(channels, static_cast(length)); + if (reader.read(&cached->buffer, 0, static_cast(length), 0, true, true)) + audioDataCache[filePath] = std::move(cached); + else + audioDataCache.erase(filePath); +} + juce::AudioFormatReader* PlaybackEngine::getCachedReader(const juce::File& file) { // Audio-thread safe: only looks up, never creates readers auto it = readers.find(file.getFullPathName()); if (it != readers.end() && it->second != nullptr) - { - readerAccessTimes[file.getFullPathName()] = juce::Time::currentTimeMillis(); return it->second.get(); - } return nullptr; } @@ -205,6 +230,7 @@ void PlaybackEngine::evictOldReaders() { const auto& path = entries[i].second; readers.erase(path); + audioDataCache.erase(path); readerAccessTimes.erase(path); } @@ -358,6 +384,7 @@ void PlaybackEngine::replaceClipAudioFile(const juce::String& clipId, const juce { // Evict old reader so the audio thread stops reading the old file readers.erase(clip.audioFile.getFullPathName()); + audioDataCache.erase(clip.audioFile.getFullPathName()); readerAccessTimes.erase(clip.audioFile.getFullPathName()); // Swap in the new file. Corrected files start at sample 0, but restoring // the original file should also restore the original trim offset. @@ -689,6 +716,7 @@ void PlaybackEngine::clearAllClips() const int preservedCorrectedCount = static_cast(pitchCorrectedFiles.size()); clips.clear(); readers.clear(); + audioDataCache.clear(); readerAccessTimes.clear(); // NOTE: clipPitchPreviews is NOT cleared here — it must survive sync cycles. // syncClipsWithBackend calls clearAllClips + re-adds clips, and the preview @@ -792,6 +820,7 @@ void PlaybackEngine::setClipPitchPreview (const juce::String& clipId, if (! usingOriginalAlready) { readers.erase (clip.audioFile.getFullPathName()); + audioDataCache.erase (clip.audioFile.getFullPathName()); readerAccessTimes.erase (clip.audioFile.getFullPathName()); clip.audioFile = clip.originalAudioFile; clip.offset = clip.originalOffset; @@ -1222,8 +1251,10 @@ void PlaybackEngine::fillTrackBuffer(const juce::String& trackId, if (requestedOutputSamples <= 0) return false; - auto* reader = getCachedReader (playbackFile); - if (reader == nullptr) + const auto cacheIt = audioDataCache.find(playbackFile.getFullPathName()); + const auto cachedAudio = cacheIt != audioDataCache.end() ? cacheIt->second : nullptr; + auto* reader = cachedAudio ? nullptr : getCachedReader (playbackFile); + if (cachedAudio == nullptr && reader == nullptr) { const int missingReaders = missingReaderCount.fetch_add (1, std::memory_order_relaxed) + 1; logAudioPlayback ("fillTrackBuffer missingReader track=" + trackId @@ -1234,7 +1265,10 @@ void PlaybackEngine::fillTrackBuffer(const juce::String& trackId, return false; } - const double fileSampleRate = reader->sampleRate; + if (cachedAudio == nullptr) + audioDataCacheMissCount.fetch_add(1, std::memory_order_relaxed); + + const double fileSampleRate = cachedAudio ? cachedAudio->sampleRate : reader->sampleRate; const double ratio = fileSampleRate / sampleRate; double exactFileStart = juce::jmax (0.0, playbackOffset) * fileSampleRate; const double roundedFileStart = std::round (exactFileStart); @@ -1248,7 +1282,8 @@ void PlaybackEngine::fillTrackBuffer(const juce::String& trackId, const double bufferStartPosition = static_cast (readStartOffset) + fileStartFraction; int outputSamples = requestedOutputSamples; int fileSamplesToRead = static_cast (std::ceil (bufferStartPosition + outputSamples * ratio)) + 3; - const juce::int64 fileSamplesAvailable = reader->lengthInSamples - readStartSample; + const juce::int64 sourceLengthInSamples = cachedAudio ? cachedAudio->lengthInSamples : reader->lengthInSamples; + const juce::int64 fileSamplesAvailable = sourceLengthInSamples - readStartSample; if (fileSamplesAvailable <= 0) return false; if (fileSamplesAvailable < fileSamplesToRead) @@ -1260,7 +1295,7 @@ void PlaybackEngine::fillTrackBuffer(const juce::String& trackId, if (outputSamples <= 0 || fileSamplesToRead <= 0) return false; - const int readerChannels = static_cast (reader->numChannels); + const int readerChannels = cachedAudio ? cachedAudio->numChannels : static_cast (reader->numChannels); if (reusableFileBuffer.getNumChannels() < readerChannels || reusableFileBuffer.getNumSamples() < fileSamplesToRead) { @@ -1269,7 +1304,16 @@ void PlaybackEngine::fillTrackBuffer(const juce::String& trackId, fileBufferResizeCount.fetch_add (1, std::memory_order_relaxed); } reusableFileBuffer.clear (0, fileSamplesToRead); - reader->read (&reusableFileBuffer, 0, fileSamplesToRead, readStartSample, true, true); + if (cachedAudio) + { + const int sourceStart = static_cast(readStartSample); + for (int ch = 0; ch < readerChannels; ++ch) + reusableFileBuffer.copyFrom(ch, 0, cachedAudio->buffer, ch, sourceStart, fileSamplesToRead); + } + else + { + reader->read (&reusableFileBuffer, 0, fileSamplesToRead, readStartSample, true, true); + } const bool allowLivePitchPreviewForChunk = ! usingRenderedPreviewSegment && ! usingCorrectedSource; const double chunkClipStart = currentTime + (static_cast (outputStart) / sampleRate) - clip.startTime; @@ -1717,10 +1761,16 @@ void PlaybackEngine::fillTrackBuffer(const juce::String& trackId, } // end real-time path } - const float playbackPeak = peakForBuffer(buffer, numSamples); + const bool shouldUpdatePlaybackPeak = mixedClipCount > 0 && ((fillCall & 15) == 0 || shouldLogDetailed); + const float playbackPeak = mixedClipCount == 0 + ? 0.0f + : (shouldUpdatePlaybackPeak + ? peakForBuffer(buffer, numSamples) + : lastTrackPlaybackPeak.load(std::memory_order_relaxed)); lastOverlappingClipCount.store(overlappingClipCount, std::memory_order_relaxed); lastMixedClipCount.store(mixedClipCount, std::memory_order_relaxed); - lastTrackPlaybackPeak.store(playbackPeak, std::memory_order_relaxed); + if (shouldUpdatePlaybackPeak || mixedClipCount == 0) + lastTrackPlaybackPeak.store(playbackPeak, std::memory_order_relaxed); if (shouldLogDetailed || (overlappingClipCount > 0 && mixedClipCount == 0)) { logAudioPlayback("fillTrackBuffer summary track=" + trackId diff --git a/Source/PlaybackEngine.h b/Source/PlaybackEngine.h index d70cb26..1b163c7 100644 --- a/Source/PlaybackEngine.h +++ b/Source/PlaybackEngine.h @@ -234,6 +234,8 @@ class PlaybackEngine int getPitchShiftWorkBufferResizeCount() const { return pitchShiftWorkBufferResizeCount.load(std::memory_order_relaxed); } int getRenderResampleScratchResizeCount() const { return renderResampleScratchResizeCount.load(std::memory_order_relaxed); } int getChunkBoundaryReserveCount() const { return chunkBoundaryReserveCount.load(std::memory_order_relaxed); } + int getAudioDataCacheMissCount() const { return audioDataCacheMissCount.load(std::memory_order_relaxed); } + int getAudioDataCachedFileCount() const { const juce::ScopedLock sl(lock); return static_cast(audioDataCache.size()); } // Thread-safe snapshot of all clips (for offline rendering) std::vector getClipSnapshot() const; @@ -247,6 +249,14 @@ class PlaybackEngine private: std::vector clips; std::map> readers; + struct CachedAudioData + { + juce::AudioBuffer buffer; + double sampleRate = 0.0; + juce::int64 lengthInSamples = 0; + int numChannels = 0; + }; + std::map> audioDataCache; juce::AudioFormatManager formatManager; mutable juce::CriticalSection lock; @@ -266,6 +276,7 @@ class PlaybackEngine // Pre-load reader on message thread so it's ready for audio thread void preloadReader(const juce::File& file); + void preloadAudioData(const juce::File& file, juce::AudioFormatReader& reader); // Legacy: get or create reader (only called from message thread now) juce::AudioFormatReader* getReader(const juce::File& file); @@ -340,6 +351,7 @@ class PlaybackEngine std::atomic pitchShiftWorkBufferResizeCount { 0 }; std::atomic renderResampleScratchResizeCount { 0 }; std::atomic chunkBoundaryReserveCount { 0 }; + std::atomic audioDataCacheMissCount { 0 }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PlaybackEngine) }; diff --git a/Source/PluginWindowManager.cpp b/Source/PluginWindowManager.cpp index c8c4a06..55b10e2 100644 --- a/Source/PluginWindowManager.cpp +++ b/Source/PluginWindowManager.cpp @@ -165,12 +165,14 @@ std::optional PluginWindowManager::Plug PluginWindowManager::PluginWindow::PluginWindow(PluginWindowManager& ownerIn, juce::AudioProcessor& proc, const juce::String& title, - const PluginEditorTarget& targetIn) + const PluginEditorTarget& targetIn, + std::shared_ptr keepAliveIn) : DocumentWindow(title, juce::Colours::darkgrey, DocumentWindow::allButtons), owner(ownerIn), processor(proc), + keepAlive(std::move(keepAliveIn)), target(targetIn) { setUsingNativeTitleBar(true); @@ -290,6 +292,36 @@ void PluginWindowManager::openEditor(juce::AudioProcessor* processor, const juce activeWindows[processor] = std::move(window); } +void PluginWindowManager::openEditor(std::shared_ptr processor, const juce::String& windowTitle, + const PluginEditorTarget& target) +{ + if (!processor) + return; + + auto* rawProcessor = processor.get(); + auto it = activeWindows.find(rawProcessor); + if (it != activeWindows.end()) + { + if (it->second != nullptr) + { + it->second->setVisible(true); + positionWindow(*it->second); + it->second->toFront(true); + logWindowEvent(target, "editor_reopened_to_front"); + } + return; + } + + if (!rawProcessor->hasEditor()) + return; + + auto window = std::make_unique(*this, *rawProcessor, windowTitle, target, std::move(processor)); + if (window->getContentComponent() == nullptr) + return; + + activeWindows[rawProcessor] = std::move(window); +} + void PluginWindowManager::closeEditor(juce::AudioProcessor* processor) { if (processor == nullptr) diff --git a/Source/PluginWindowManager.h b/Source/PluginWindowManager.h index 533d1a8..109c94c 100644 --- a/Source/PluginWindowManager.h +++ b/Source/PluginWindowManager.h @@ -38,6 +38,8 @@ class PluginWindowManager : public juce::Timer // Open plugin editor for a specific processor void openEditor(juce::AudioProcessor* processor, const juce::String& windowTitle, const PluginEditorTarget& target); + void openEditor(std::shared_ptr processor, const juce::String& windowTitle, + const PluginEditorTarget& target); // Close editor for a specific processor (async, safe from any thread) void closeEditor(juce::AudioProcessor* processor); @@ -75,7 +77,8 @@ class PluginWindowManager : public juce::Timer struct PluginWindow : public juce::DocumentWindow { PluginWindow(PluginWindowManager& ownerIn, juce::AudioProcessor& proc, - const juce::String& title, const PluginEditorTarget& targetIn); + const juce::String& title, const PluginEditorTarget& targetIn, + std::shared_ptr keepAliveIn = {}); ~PluginWindow() override; void closeButtonPressed() override; @@ -84,6 +87,7 @@ class PluginWindowManager : public juce::Timer PluginWindowManager& owner; juce::AudioProcessor& processor; + std::shared_ptr keepAlive; PluginEditorTarget target; }; diff --git a/Source/S13PitchCorrector.h b/Source/S13PitchCorrector.h index 2e5faf4..af8ea53 100644 --- a/Source/S13PitchCorrector.h +++ b/Source/S13PitchCorrector.h @@ -3,7 +3,14 @@ #include #include "PitchDetector.h" #include "PitchMapper.h" +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable: 4244 4267 4305 4456) +#endif #include "signalsmith-stretch.h" +#if defined(_MSC_VER) + #pragma warning(pop) +#endif #include #include diff --git a/Source/TrackProcessor.cpp b/Source/TrackProcessor.cpp index bef65d6..f0088e7 100644 --- a/Source/TrackProcessor.cpp +++ b/Source/TrackProcessor.cpp @@ -3,6 +3,7 @@ // Maximum channel count for the pre-allocated FX processing buffer. // Must be large enough for multi-output instruments (e.g. Komplete Kontrol = 32 out). static constexpr int kMaxFXChannels = 64; +static constexpr int kMinimumHostedPluginBlockSize = 512; // Debug logging — always active for FX diagnostics static void logToDisk(const juce::String& msg) @@ -15,6 +16,12 @@ static void logToDisk(const juce::String& msg) f.appendText(juce::Time::getCurrentTime().toString(true, true) + ": " + msg + "\n"); } +static int getSafeHostedPluginBlockSize(int requestedBlockSize) +{ + return juce::jmax(kMinimumHostedPluginBlockSize, + requestedBlockSize > 0 ? requestedBlockSize : kMinimumHostedPluginBlockSize); +} + static void computePanLawGains(PanLaw panLaw, float pan, float volumeGain, float& leftGain, float& rightGain) { @@ -503,7 +510,8 @@ void TrackProcessor::changeProgramName (int index, const juce::String& newName) static void preparePluginPreservingLayout(juce::AudioProcessor* plugin, double sampleRate, int maxBlock, ProcessingPrecisionMode precisionMode) { - const juce::ScopedLock callbackLock(plugin->getCallbackLock()); + const juce::ScopedLock pluginCallbackGuard(plugin->getCallbackLock()); + const int safeMaxBlock = getSafeHostedPluginBlockSize(maxBlock); if (plugin->supportsDoublePrecisionProcessing()) { @@ -514,14 +522,23 @@ static void preparePluginPreservingLayout(juce::AudioProcessor* plugin, double s } auto savedLayout = plugin->getBusesLayout(); - plugin->prepareToPlay(sampleRate, maxBlock); + plugin->prepareToPlay(sampleRate, safeMaxBlock); // If prepareToPlay changed the bus layout, restore and re-prepare so the // plugin operates with its original (createPluginInstance) channel config. if (plugin->getBusesLayout() != savedLayout) { - plugin->setBusesLayout(savedLayout); - plugin->prepareToPlay(sampleRate, maxBlock); + if (plugin->setBusesLayout(savedLayout)) + { + plugin->prepareToPlay(sampleRate, safeMaxBlock); + } + else + { + juce::Logger::writeToLog("TrackProcessor: Plugin refused saved bus layout during prepare: " + + plugin->getName() + + " requestedBlock=" + juce::String(maxBlock) + + " safeBlock=" + juce::String(safeMaxBlock)); + } } } @@ -551,7 +568,7 @@ void TrackProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) // Prepare plugins with the actual device block size so realtime hosting // matches the hardware callback configuration. - int pluginMaxBlock = samplesPerBlock > 0 ? samplesPerBlock : 512; + int pluginMaxBlock = getSafeHostedPluginBlockSize(samplesPerBlock); // Propagate new sample rate and buffer size to all internal FX plugins, // preserving each plugin's bus layout (see preparePluginPreservingLayout). @@ -677,8 +694,9 @@ void TrackProcessor::processBlockInternal (juce::AudioBuffer& buffer, juc // One-time diagnostic log on first processBlock call with FX loaded // (helps diagnose crashes — last log entry before crash shows where it stopped) + static bool enableRealtimeFirstFXLog = false; static bool loggedFirstFXProcess = false; - if (hasAnyFX && !loggedFirstFXProcess) + if (enableRealtimeFirstFXLog && hasAnyFX && !loggedFirstFXProcess) { loggedFirstFXProcess = true; logToDisk("TrackProcessor::processBlock FIRST CALL WITH FX"); @@ -734,6 +752,13 @@ void TrackProcessor::processBlockInternal (juce::AudioBuffer& buffer, juc // Channel-safe FX processing helper auto safeProcessFX = [&](juce::AudioProcessor* proc, bool forceFloat, bool isInputFXChain, int fxIndex) { + juce::ScopedTryLock pluginProcessLock(proc->getCallbackLock()); + if (!pluginProcessLock.isLocked()) + { + pluginBusySkipCount.fetch_add(1, std::memory_order_relaxed); + return; + } + applyPluginAutomationForProcessor(proc, isInputFXChain, fxIndex, blockTimeSeconds); // Compute isARAProcessor first so we can gate expensive QPC calls on it. @@ -1036,6 +1061,13 @@ void TrackProcessor::processBlockInternal (juce::AudioBuffer& buffer, juc if (hasSidechain) { + juce::ScopedTryLock pluginProcessLock(proc->getCallbackLock()); + if (!pluginProcessLock.isLocked()) + { + pluginBusySkipCount.fetch_add(1, std::memory_order_relaxed); + continue; + } + applyPluginAutomationForProcessor(proc, false, fxIdx, blockTimeSeconds); // Sidechain path: expand buffer to include sidechain channels after @@ -1393,7 +1425,7 @@ bool TrackProcessor::addInputFX(std::unique_ptr plugin, do if (!plugin) return false; - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); // Only set stereo layout if the plugin has no channels configured // (some plugins start at 0-in/0-out and need explicit bus setup). @@ -1412,9 +1444,9 @@ bool TrackProcessor::addInputFX(std::unique_ptr plugin, do // Prefer caller-supplied rate (from AudioEngine), fall back to our own, // then to 44100 as last resort. Use the realtime device block size when known. double sr = callerSampleRate > 0 ? callerSampleRate : getSampleRate(); - int bs = callerBlockSize > 0 ? callerBlockSize : getBlockSize(); + int bs = getSafeHostedPluginBlockSize(callerBlockSize > 0 ? callerBlockSize : getBlockSize()); if (sr <= 0) sr = 44100.0; - if (bs <= 0) bs = 512; + if (bs <= 0) bs = kMinimumHostedPluginBlockSize; // Prepare while preserving bus layout (see preparePluginPreservingLayout). preparePluginPreservingLayout(plugin.get(), sr, bs, @@ -1435,7 +1467,7 @@ bool TrackProcessor::addTrackFX(std::unique_ptr plugin, do if (!plugin) return false; - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); // Only set stereo layout if the plugin has no channels configured // (same rationale as addInputFX — preserve the plugin's default layout). @@ -1450,9 +1482,9 @@ bool TrackProcessor::addTrackFX(std::unique_ptr plugin, do // Prefer caller-supplied rate (from AudioEngine), fall back to our own, // then to 44100 as last resort. Use the realtime device block size when known. double sr = callerSampleRate > 0 ? callerSampleRate : getSampleRate(); - int bs = callerBlockSize > 0 ? callerBlockSize : getBlockSize(); + int bs = getSafeHostedPluginBlockSize(callerBlockSize > 0 ? callerBlockSize : getBlockSize()); if (sr <= 0) sr = 44100.0; - if (bs <= 0) bs = 512; + if (bs <= 0) bs = kMinimumHostedPluginBlockSize; // Prepare while preserving bus layout (see preparePluginPreservingLayout). preparePluginPreservingLayout(plugin.get(), sr, bs, @@ -1470,7 +1502,7 @@ bool TrackProcessor::addTrackFX(std::unique_ptr plugin, do void TrackProcessor::removeInputFX(int index) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (index >= 0 && index < (int)inputFXPlugins.size()) { inputFXPlugins.erase(inputFXPlugins.begin() + index); @@ -1497,7 +1529,7 @@ void TrackProcessor::removeInputFX(int index) void TrackProcessor::removeTrackFX(int index) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (index >= 0 && index < (int)trackFXPlugins.size()) { if (index == araFXIndex) @@ -1537,7 +1569,7 @@ void TrackProcessor::removeTrackFX(int index) void TrackProcessor::bypassInputFX(int index, bool bypassed) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (index >= 0 && index < (int)inputFXPlugins.size()) { if (bypassed) @@ -1551,7 +1583,7 @@ void TrackProcessor::bypassInputFX(int index, bool bypassed) void TrackProcessor::bypassTrackFX(int index, bool bypassed) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (index >= 0 && index < (int)trackFXPlugins.size()) { if (bypassed) @@ -1607,6 +1639,20 @@ const juce::AudioProcessor* TrackProcessor::getTrackFXProcessor(int index) const return nullptr; } +std::shared_ptr TrackProcessor::getInputFXProcessorShared(int index) const +{ + if (index >= 0 && index < (int)inputFXPlugins.size()) + return inputFXPlugins[static_cast(index)]; + return {}; +} + +std::shared_ptr TrackProcessor::getTrackFXProcessorShared(int index) const +{ + if (index >= 0 && index < (int)trackFXPlugins.size()) + return trackFXPlugins[static_cast(index)]; + return {}; +} + std::shared_ptr>> TrackProcessor::getInputFXSnapshot() const { return std::atomic_load_explicit(&realtimeInputFXSnapshot, std::memory_order_acquire); @@ -1639,7 +1685,7 @@ std::shared_ptr> TrackProcessor::getTrackFXPrecisionOv bool TrackProcessor::reorderInputFX(int fromIndex, int toIndex) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (fromIndex < 0 || fromIndex >= (int)inputFXPlugins.size() || toIndex < 0 || toIndex >= (int)inputFXPlugins.size() || fromIndex == toIndex) @@ -1683,7 +1729,7 @@ bool TrackProcessor::reorderInputFX(int fromIndex, int toIndex) bool TrackProcessor::reorderTrackFX(int fromIndex, int toIndex) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (fromIndex < 0 || fromIndex >= (int)trackFXPlugins.size() || toIndex < 0 || toIndex >= (int)trackFXPlugins.size() || fromIndex == toIndex) @@ -1918,13 +1964,13 @@ void TrackProcessor::fillSendBuffer(int sendIndex, const juce::AudioBuffer plugin, double callerSampleRate, int callerBlockSize) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (plugin) { double sr = callerSampleRate > 0 ? callerSampleRate : getSampleRate(); - int bs = callerBlockSize > 0 ? callerBlockSize : getBlockSize(); + int bs = getSafeHostedPluginBlockSize(callerBlockSize > 0 ? callerBlockSize : getBlockSize()); if (sr <= 0) sr = 44100.0; - if (bs <= 0) bs = 512; + if (bs <= 0) bs = kMinimumHostedPluginBlockSize; preparePluginPreservingLayout(plugin.get(), sr, bs, resolvePluginPrecisionMode(processingPrecisionMode, @@ -2167,7 +2213,7 @@ void TrackProcessor::setInputFXPrecisionOverride(int index, bool forceFloat) if (auto* plugin = inputFXPlugins[static_cast(index)].get()) { double sr = getSampleRate() > 0 ? getSampleRate() : 44100.0; - int bs = getBlockSize() > 0 ? getBlockSize() : 512; + int bs = getSafeHostedPluginBlockSize(getBlockSize()); preparePluginPreservingLayout(plugin, sr, bs, resolvePluginPrecisionMode(processingPrecisionMode, forceFloat)); } @@ -2184,7 +2230,7 @@ void TrackProcessor::setTrackFXPrecisionOverride(int index, bool forceFloat) if (auto* plugin = trackFXPlugins[static_cast(index)].get()) { double sr = getSampleRate() > 0 ? getSampleRate() : 44100.0; - int bs = getBlockSize() > 0 ? getBlockSize() : 512; + int bs = getSafeHostedPluginBlockSize(getBlockSize()); preparePluginPreservingLayout(plugin, sr, bs, resolvePluginPrecisionMode(processingPrecisionMode, forceFloat)); } @@ -2198,7 +2244,7 @@ void TrackProcessor::setInstrumentPrecisionOverride(bool forceFloat) if (instrumentPlugin) { double sr = getSampleRate() > 0 ? getSampleRate() : 44100.0; - int bs = getBlockSize() > 0 ? getBlockSize() : 512; + int bs = getSafeHostedPluginBlockSize(getBlockSize()); preparePluginPreservingLayout(instrumentPlugin.get(), sr, bs, resolvePluginPrecisionMode(processingPrecisionMode, forceFloat)); } @@ -2230,14 +2276,14 @@ bool TrackProcessor::getTrackFXBypassed(int index) const void TrackProcessor::setProcessingPrecisionMode(ProcessingPrecisionMode mode) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (processingPrecisionMode == mode) return; processingPrecisionMode = mode; double sr = getSampleRate() > 0 ? getSampleRate() : 44100.0; - int bs = getBlockSize() > 0 ? getBlockSize() : 512; + int bs = getSafeHostedPluginBlockSize(getBlockSize()); for (int index = 0; index < static_cast(inputFXPlugins.size()); ++index) if (auto* plugin = inputFXPlugins[static_cast(index)].get()) @@ -2341,7 +2387,7 @@ void TrackProcessor::setOutputChannels(int startChannel, int numChannels) void TrackProcessor::setMIDIOutputDevice(const juce::String& deviceName) { - const juce::ScopedLock callbackLock(getCallbackLock()); + const juce::ScopedLock processorCallbackGuard(getCallbackLock()); if (deviceName == midiOutputDeviceName) return; diff --git a/Source/TrackProcessor.h b/Source/TrackProcessor.h index 9c56aab..a4dab27 100644 --- a/Source/TrackProcessor.h +++ b/Source/TrackProcessor.h @@ -154,6 +154,8 @@ class TrackProcessor : public juce::AudioProcessor const juce::AudioProcessor* getInputFXProcessor(int index) const; juce::AudioProcessor* getTrackFXProcessor(int index); const juce::AudioProcessor* getTrackFXProcessor(int index) const; + std::shared_ptr getInputFXProcessorShared(int index) const; + std::shared_ptr getTrackFXProcessorShared(int index) const; std::shared_ptr>> getInputFXSnapshot() const; std::shared_ptr>> getTrackFXSnapshot() const; std::shared_ptr> getInputFXBypassSnapshot() const; @@ -225,6 +227,7 @@ class TrackProcessor : public juce::AudioProcessor // Instrument plugin (Phase 2) void setInstrument(std::unique_ptr plugin, double callerSampleRate = 0.0, int callerBlockSize = 0); juce::AudioPluginInstance* getInstrument() const { return instrumentPlugin.get(); } + std::shared_ptr getInstrumentShared() const { return instrumentPlugin; } // MIDI intake / scheduling bool enqueueMidiMessage(const juce::MidiMessage& message, int sampleOffset = 0); @@ -239,6 +242,7 @@ class TrackProcessor : public juce::AudioProcessor int getLastBuiltMidiEventCount() const { return lastBuiltMidiEventCount.load(std::memory_order_relaxed); } int getMaxBuiltMidiEventCount() const { return maxBuiltMidiEventCount.load(std::memory_order_relaxed); } int getRealtimeFallbackReuseCount() const { return realtimeFallbackReuseCount.load(std::memory_order_relaxed); } + int getPluginBusySkipCount() const { return pluginBusySkipCount.load(std::memory_order_relaxed); } void setProcessingPrecisionMode(ProcessingPrecisionMode mode); ProcessingPrecisionMode getProcessingPrecisionMode() const { return processingPrecisionMode; } @@ -518,6 +522,7 @@ class TrackProcessor : public juce::AudioProcessor std::unique_ptr midiOutputDevice; juce::AudioBuffer realtimeFallbackBuffer; std::atomic realtimeFallbackReuseCount { 0 }; + std::atomic pluginBusySkipCount { 0 }; struct PendingMIDIEvent { diff --git a/docs/implemented_features.md b/docs/implemented_features.md new file mode 100644 index 0000000..808be4a --- /dev/null +++ b/docs/implemented_features.md @@ -0,0 +1,206 @@ +# Implemented DAW Features + +This audit treats the codebase as the source of truth: `CMakeLists.txt`, `Source/*`, `frontend/src/App.tsx`, `frontend/src/store/useDAWStore.ts`, store action modules, `frontend/src/store/actionRegistry.ts`, and mounted React panels/modals. + +Features are sorted by impact first, then complexity. + +Ratings: + +- `H`: High +- `M`: Medium +- `L`: Low + +## Core Engine / Transport + +| Feature | Impact | Complexity | +|---|---:|---:| +| JUCE audio engine with React/WebView2 frontend bridge | H | H | +| Real-time playback engine with sample-rate-aware clip mixing | H | H | +| Audio device setup: driver, I/O, sample rate, buffer, channels | H | H | +| Multitrack audio recording with armed tracks, monitoring, punch range | H | H | +| Transport: play, stop, pause, record, seek, loop, current time | H | M | +| Tempo, time signature, tap tempo, tempo markers | H | M | +| Metronome with accenting, volume, custom sounds, render-to-track | M | M | +| Background waveform peak cache and recording waveform previews | H | H | +| MIDI recording preview and completed MIDI clip handoff | H | M | + +## Arrangement / Editing + +| Feature | Impact | Complexity | +|---|---:|---:| +| Konva-based timeline with ruler, grid, zoom, scroll, snap | H | H | +| Audio and MIDI clip creation, import, drag/drop, move, trim, resize | H | H | +| Multi-clip and multi-track selection | H | M | +| Split, cut, copy, paste, duplicate, delete, nudge, fine nudge | H | M | +| Time selection editing: cut, copy, delete, insert silence | H | H | +| Razor edit areas and razor content deletion | H | H | +| Slip editing, free item positioning, ripple modes | H | H | +| Clip fades, clip volume, gain envelopes, mute, lock, color | H | M | +| Clip reverse, normalize, time stretch, pitch shift | H | H | +| Auto-crossfade and crossfade editor | H | M | +| Takes: explode, implode, active-take style state | M | M | +| Markers, named markers, regions, region manager | H | M | +| Tempo marker support | H | M | +| Quantize selected clips | M | M | + +## Mixing / Routing / Metering + +| Feature | Impact | Complexity | +|---|---:|---:| +| Mixer panel, channel strips, master strip, detached mixer | H | H | +| Track volume, pan, mute, solo, arm, monitor controls | H | M | +| Master volume, pan, mute, mono, master automation | H | M | +| Peak/RMS metering, master meter, clipping reset | H | H | +| Track sends, send pan/level/phase, pre/post routing | H | H | +| Routing matrix and track routing modal | H | H | +| Bus tracks, folder tracks, create bus from selected tracks | H | M | +| Track groups / linked group params | M | H | +| Sidechain routing into plugins | H | H | +| Phase invert, stereo width, pan law, DC offset handling | M | M | +| Output channel selection, track channel count, playback offset | M | M | +| LUFS measurement, phase correlation, spectrum data | M | H | +| Channel strip EQ modal | M | M | + +## Plugins / FX / Scripting + +| Feature | Impact | Complexity | +|---|---:|---:| +| Plugin scanning/loading for hosted FX formats, primarily VST3 with CLAP/LV2 code paths | H | H | +| Native plugin editor window management | H | H | +| Input FX, track FX, master FX, monitoring FX chains | H | H | +| Add, remove, bypass, reorder FX chains | H | M | +| Plugin parameters, presets, state save/load, A/B compare | H | H | +| Plugin MIDI learn and parameter mapping | H | H | +| Processing precision override / hybrid precision support | M | H | +| Plugin capability matrix, guardrails, release benchmark hooks | M | H | +| Built-in EQ, compressor, gate, limiter, delay, reverb, chorus, saturator | H | H | +| Built-in real-time pitch corrector FX | H | H | +| Built-in FX editors and oversampling controls | M | H | +| S13FX / JSFX-style script effects with sliders and reload | H | H | +| S13FX `@gfx` native editor support | M | H | +| Lua script execution, script listing, script editor UI | M | H | + +## MIDI / Instruments + +| Feature | Impact | Complexity | +|---|---:|---:| +| MIDI device enumeration, input open/close, output routing | H | H | +| MIDI track type, instrument track type, MIDI channel routing | H | M | +| MIDI clips with note storage and playback scheduling | H | H | +| MIDI recording into clips with live preview | H | H | +| Piano roll editor | H | H | +| MIDI note draw/edit/select, velocity, CC editing | H | H | +| Virtual piano keyboard | M | M | +| Step sequencer and step input state/actions | M | M | +| MIDI transforms: transpose, velocity scale, reverse, invert | M | M | +| MIDI import/export and project MIDI export | H | M | +| Load/open virtual instrument on instrument tracks | H | H | + +## Pitch / Audio Analysis + +| Feature | Impact | Complexity | +|---|---:|---:| +| Monophonic pitch analysis with YIN contour and note segmentation | H | H | +| Graphical pitch editor with blobs, contour, piano grid, zoom/scroll | H | H | +| Pitch tools: pitch, drift, vibrato, transition, draw, split | H | H | +| Scale/key snapping, chromatic snap, correct-pitch macro, scale detection | H | H | +| Offline monophonic pitch correction render/apply path | H | H | +| Pitch preview, scrub preview, HQ note/full-clip render states | H | H | +| Real-time auto-tune style pitch corrector plugin | H | H | +| Pitch editor undo/redo and A/B style comparison state | M | M | +| Transient detection and silent-region detection | M | M | +| Polyphonic pitch detection and MIDI extraction via Basic Pitch / ONNX | H | H | +| Stem-aware / AI-adjacent audio analysis plumbing | M | H | + +## Rendering / Export / Interchange + +| Feature | Impact | Complexity | +|---|---:|---:| +| Offline project render through the same playback/FX engine | H | H | +| Render formats: WAV, AIFF, FLAC, MP3, OGG | H | H | +| Render options: sample rate, bit depth/quality, mono/stereo, normalize, tail | H | M | +| Dithered render path | M | H | +| Stem/track render code path and region render matrix UI | H | H | +| Render queue | M | M | +| Add rendered output back into project | M | M | +| Render metadata and filename wildcards | M | M | +| Render in place, consolidate track, freeze/unfreeze | H | H | +| Batch audio converter | M | M | +| DDP export | M | H | +| Session archive/unarchive | M | H | +| RPP import and RPP/EDL export | M | H | + +## Project / Media Management + +| Feature | Impact | Complexity | +|---|---:|---:| +| Project new/open/save/save as/close, unsaved changes flow | H | H | +| Recent projects and startup recovery/diagnostics | M | M | +| Project tabs | M | M | +| Project settings, notes, author/revision metadata | M | M | +| Project templates and save-from-template flow | M | M | +| Safe-mode project open / FX bypass recovery path | H | M | +| Media import and drag/drop handling | H | M | +| Missing media resolver | H | M | +| Media explorer browse/import | M | M | +| Clean project directory tool | M | M | +| Project compare | M | M | +| Preferences, autosave/backup/display/editing settings | M | M | + +## AI / Assisted Audio + +| Feature | Impact | Complexity | +|---|---:|---:| +| AI tools runtime status, install, cancel, reset flow | H | H | +| Stem separation workflow with selectable stems and progress polling | H | H | +| Stem separation result import into new tracks/clips | H | H | +| AI track type and AI track header controls | M | H | +| Text-to-music generation workflow | H | H | +| Lyrics + style music generation workflow | H | H | +| AI generation progress/cancel handling | M | H | + +## Workflow / UI Customization + +| Feature | Impact | Complexity | +|---|---:|---:| +| Central action registry powering menus, shortcuts, command palette | H | H | +| Menu bar, main toolbar, custom toolbar strip/editor | H | M | +| Keyboard shortcuts modal and global shortcut handling | H | M | +| Command palette | H | M | +| Screensets/layout state | M | M | +| Theme editor and custom theme state | M | M | +| Mouse modifier preferences | M | M | +| Big clock and timecode display settings | M | M | +| Help overlay and getting started guide | L | M | +| App updater hooks | M | M | +| Crash diagnostics source/module present | M | M | + +## Sync / Control / Video / Pro Tools + +| Feature | Impact | Complexity | +|---|---:|---:| +| MIDI clock output/input | M | H | +| MTC output/input and sync status/source management | M | H | +| Control surface manager with MIDI learn/mappings | M | H | +| OSC connection support | M | H | +| MCU-style control surface support | M | H | +| Video window, video metadata/frame extraction, audio extraction path | M | H | +| Surround/channel layout and VBAP panner code paths | M | H | +| ARA host controller lifecycle and track ARA status plumbing | H | H | + +## Implemented But Partial / Caveated + +These have real code surfaces, but should not be counted as fully delivered DAW features yet. + +| Feature | Status | +|---|---| +| Polyphonic pitch correction / solo-note resynthesis | Detection and MIDI extraction exist; `PolyResynthesizer` is still stub-like | +| AAF import | Stubbed in session interchange | +| LTC output | Bridge stub exists, not a real implementation | +| Live capture start/stop | Bridge stubs exist | +| Media Explorer audio preview | UI exists; backend preview function appears to only acknowledge/log | +| AI continuation workflow | Present in workflow list but marked unavailable | +| Drum editor / media pool | Store toggles/actions exist, but no mounted full UI components were found | +| Master FX reorder | Track/input reorder exists; master reorder is noted as unsupported in UI | +| Legacy `executeScript/loadScriptFile` bridge names | Stubbed, but newer `runScript/runScriptCode` paths are implemented | + diff --git a/frontend/src/components/ChannelStrip.tsx b/frontend/src/components/ChannelStrip.tsx index 774d68a..d320fd7 100644 --- a/frontend/src/components/ChannelStrip.tsx +++ b/frontend/src/components/ChannelStrip.tsx @@ -128,7 +128,8 @@ export const ChannelStrip = React.memo(function ChannelStrip({ }, [track.id]); const [showFXChain, setShowFXChain] = useState(false); - const hasFx = track.inputFxCount + track.trackFxCount > 0; + const hasBypassableFx = track.inputFxCount + track.trackFxCount > 0; + const hasFx = hasBypassableFx || Boolean(track.instrumentPlugin); // Find the clip under the playhead for gain staging display. // Reads currentTime from a snapshot (not a selector) to avoid 60fps re-renders. @@ -424,7 +425,7 @@ export const ChannelStrip = React.memo(function ChannelStrip({ className={classNames( "h-4 w-4 rounded rounded-r-none text-[7px] flex items-center justify-center cursor-pointer transition-colors p-0", hasFx - ? track.fxBypassed + ? hasBypassableFx && track.fxBypassed ? "bg-neutral-800 border border-red-500 text-red-400 shadow-[0_0_6px_rgba(239,68,68,0.4)]" : "bg-neutral-800 border border-green-500 text-green-400 shadow-[0_0_6px_rgba(34,197,94,0.4)]" : "bg-neutral-800 border border-dashed border-neutral-600 text-neutral-500 hover:border-green-500 hover:text-green-500", @@ -434,7 +435,7 @@ export const ChannelStrip = React.memo(function ChannelStrip({ + + + )} + {fxSlots.map((fx, index) => { const isS13FX = fx.type === "s13fx"; const isBuiltIn = fx.type === "builtin"; return ( @@ -1280,7 +1355,8 @@ export function FXChainPanel({ )} ); - }) + })} + )} diff --git a/frontend/src/components/MainToolbar.tsx b/frontend/src/components/MainToolbar.tsx index 9ff21a8..8170e34 100644 --- a/frontend/src/components/MainToolbar.tsx +++ b/frontend/src/components/MainToolbar.tsx @@ -13,7 +13,6 @@ import { Scissors, VolumeX, Wand2, - Cpu, } from "lucide-react"; import { usePitchEditorStore } from "../store/pitchEditorStore"; import { getDisplayShortcut, getActionShortcutScopeLabel } from "../store/actionRegistry"; @@ -352,7 +351,7 @@ export function MainToolbar({ aria-label="AI Tools" style={aiButtonHaloStyle} > - + AI - -
- - setZoom(Number.parseInt(e.target.value))} - className="zoom-slider" - /> + + setZoom(Number.parseInt(event.target.value, 10))} className="zoom-slider" />
- - {/* Scale Selector */} - - setPianoRollScaleRoot(Number.parseInt(event.target.value, 10))}> + {NOTE_NAMES.map((name, index) => ( + ))} - - setPianoRollScaleType(event.target.value)}> {Object.entries(SCALE_DISPLAY_NAMES).map(([key, displayLabel]) => ( - + ))}
- - {/* CC Lane Selector */} - - setSelectedCC(Number.parseInt(event.target.value, 10))}> {CC_PRESETS.map((preset) => ( - + ))}
- - {/* Transform Dropdown */}
- {showTransformMenu && ( -
- - - - -
- - -
- - +
+ + + + +
+ + +
+ +
)}
- - {/* Step Input Mode */} - {stepInputEnabled && ( <> - - setStepInputSize(Number.parseFloat(event.target.value))}> + {STEP_SIZE_OPTIONS.map((option) => ( + ))} - - Oct: {stepInputOctave} - + Oct: {stepInputOctave} )} - - {/* Multi-clip indicator */} {additionalClips.length > 0 && ( <>
- - Editing {additionalClips.length + 1} clips - + Editing {additionalClips.length + 1} clips )}
@@ -1362,44 +1564,27 @@ export function PianoRoll({ clipId, trackId, additionalClipIds = [] }: PianoRoll - {/* Background */} - - - {/* Grid with beat shading and scale highlighting */} + {renderGrid()} - - {/* Piano keyboard */} {renderPianoKeyboard()} - - {/* Ghost notes from other MIDI clips on this track */} {renderGhostNotes()} - - {/* MIDI notes with velocity coloring (includes multi-clip notes) */} - {renderNotes()} - - {/* Step input cursor line */} + {renderAdditionalClipNotes()} + {renderPrimaryNotes()} + {renderDrawingPreview()} {renderStepInputCursor()} - - {/* Velocity lane */} {renderVelocityLane()} - - {/* CC lane */} {renderCCLane()} + +
+
+
); } diff --git a/frontend/src/components/PluginBrowser.tsx b/frontend/src/components/PluginBrowser.tsx index cd72896..c311fa4 100644 --- a/frontend/src/components/PluginBrowser.tsx +++ b/frontend/src/components/PluginBrowser.tsx @@ -14,6 +14,7 @@ import { Code, Star, FolderOpen, + Search, } from "lucide-react"; import { nativeBridge } from "../services/NativeBridge"; import { useDAWStore } from "../store/useDAWStore"; @@ -396,6 +397,7 @@ export function PluginBrowser({ ); if (success) { store.updateTrack(trackId, { + type: "instrument", instrumentPlugin: plugin.fileOrIdentifier, }); await waitForInstrumentPlugin( @@ -516,26 +518,34 @@ export function PluginBrowser({
- setSearchTerm(e.target.value)} - className="flex-1" - /> +
+ + setSearchTerm(e.target.value)} + fullWidth + className="block w-full" + inputClassName="w-full pl-9 bg-neutral-950 border-neutral-500 text-white placeholder:text-neutral-400 shadow-inner focus:border-blue-400 focus:ring-2 focus:ring-blue-500/30" + /> +