From adb7fd834ae6c9a5e8f84e7d492fd9d2d3128256 Mon Sep 17 00:00:00 2001 From: Sourav Das Date: Fri, 11 Sep 2026 00:37:30 +0530 Subject: [PATCH 01/25] chore: Updated NAM Rack authentication, applied for windows partner program, updated policies for that, added MiniMax Music 3 model support --- .github/workflows/release.yml | 212 +- .github/workflows/verify.yml | 13 + .gitignore | 5 + AGENTS.md | 42 +- CLAUDE.md | 26 +- CMakeLists.txt | 98 +- README.md | 41 +- Source/AITrackEngine.cpp | 334 +- Source/AITrackEngine.h | 10 +- Source/ARADebug.h | 4 +- Source/ARAHostController.cpp | 80 +- Source/ARAHostController.h | 36 +- Source/AppPaths.h | 31 + Source/AppUpdater.cpp | 402 +- Source/AppUpdater.h | 36 +- Source/ApplicationLaunchState.cpp | 2 +- Source/AudioEngine.cpp | 4764 ++++++++++++----- Source/AudioEngine.h | 57 +- Source/AudioFileConversion.cpp | 109 + Source/AudioFileConversion.h | 17 + Source/AudioFileConversionRegression.cpp | 190 + Source/AudioFileConversionRegression.h | 4 + Source/AudioInputPolicy.h | 29 + Source/AudioRecorder.cpp | 128 +- Source/AudioRecorder.h | 7 +- Source/BuiltInEffects.cpp | 166 +- Source/BuiltInEffects.h | 68 +- Source/BuiltInEffects2.cpp | 2334 ++++---- Source/BuiltInEffects2.h | 340 +- Source/ControlSurfaceManager.cpp | 22 +- Source/ControlSurfaceManager.h | 5 + Source/ControlSurfaceMidiInbox.h | 56 + Source/CrashDiagnostics.cpp | 330 +- Source/CrashDiagnostics.h | 12 + Source/CrashReportWriter.cpp | 93 + Source/CrashReporterMain.cpp | 96 + Source/CrashReporterProtocol.h | 31 + Source/FieldTestRegression.h | 71 + Source/IsolatedPlugin.cpp | 551 ++ Source/IsolatedPlugin.h | 57 + Source/IsolatedPluginProtocol.h | 85 + Source/IsolatedPluginRegression.cpp | 310 ++ Source/IsolatedPluginWorker.cpp | 454 ++ .../{S13FXGfxEditor.cpp => JSFXGfxEditor.cpp} | 44 +- Source/{S13FXGfxEditor.h => JSFXGfxEditor.h} | 14 +- .../{S13FXProcessor.cpp => JSFXProcessor.cpp} | 56 +- Source/{S13FXProcessor.h => JSFXProcessor.h} | 16 +- Source/JsonEnvelope.h | 35 + Source/MIDIManager.cpp | 4 +- Source/Main.cpp | 306 +- Source/MainComponent.cpp | 693 +-- Source/MainComponent.h | 12 + Source/MessageThreadLifetime.h | 18 + Source/Metronome.cpp | 296 +- Source/Metronome.h | 28 + Source/MetronomeRegression.cpp | 346 ++ Source/MetronomeRegression.h | 6 + Source/MicrophoneAccess.h | 28 + Source/MicrophoneAccess.mm | 46 + Source/MixerWindowManager.cpp | 15 +- Source/NAMDelayRegression.cpp | 186 +- Source/NAMDelayRegression.h | 4 +- Source/NAMDelayRegressionLifecycle.cpp | 48 +- Source/NAMDelayRegressionRackTail.cpp | 54 +- ...ector.cpp => OpenStudioPitchCorrector.cpp} | 34 +- ...Corrector.h => OpenStudioPitchCorrector.h} | 12 +- ...ditors.cpp => OpenStudioPluginEditors.cpp} | 346 +- ...ginEditors.h => OpenStudioPluginEditors.h} | 258 +- ...tWindow.cpp => OpenStudioScriptWindow.cpp} | 58 +- ...criptWindow.h => OpenStudioScriptWindow.h} | 14 +- Source/OwnedBackgroundTasks.h | 36 + Source/OwnedChildProcess.cpp | 279 + Source/OwnedChildProcess.h | 24 + Source/PeakCache.cpp | 88 +- Source/PeakCache.h | 9 +- Source/PlaybackEngine.cpp | 2 +- Source/PluginManager.cpp | 205 +- Source/PluginManager.h | 33 +- Source/PluginSettingsMigration.h | 65 + Source/PluginStateValidation.h | 41 + Source/PluginWindowManager.cpp | 110 +- Source/PluginWindowManager.h | 12 +- Source/PolyPitchDetector.cpp | 8 +- Source/PolyPitchDetector.h | 4 +- Source/ProcessorSafety.h | 117 + Source/ProjectFileStore.cpp | 217 + Source/ProjectFileStore.h | 31 + Source/RecordingDestination.h | 18 + Source/RecordingRecovery.cpp | 159 + Source/RecordingRecovery.h | 10 + Source/RecordingRecoveryRegression.cpp | 144 + Source/RecordingRecoveryRegression.h | 5 + Source/RecordingWriterSafety.h | 116 + Source/RecoveryJournal.cpp | 198 + Source/RecoveryJournal.h | 29 + Source/RuntimeSafetyRegression.cpp | 800 +++ Source/RuntimeSafetyRegression.h | 8 + Source/ScriptEngine.cpp | 124 +- Source/ScriptEngine.h | 16 +- Source/SendGraphSafety.h | 20 + Source/StemSeparator.cpp | 270 +- Source/StemSeparator.h | 8 +- Source/StoreUpdater.cpp | 324 ++ Source/StoreUpdater.h | 38 + Source/StoreUpdaterRegression.h | 91 + Source/TimecodeSync.cpp | 2 +- Source/ToneSearchRequest.h | 27 + Source/TrackProcessor.cpp | 233 +- Source/TrackProcessor.h | 30 +- Source/TunerPitchTracker.cpp | 2 +- Source/VBAPPanner.cpp | 2 +- Source/VideoReader.cpp | 2 +- Source/WindowsPackage.h | 50 + THIRD_PARTY_LICENSES.md | 2 +- WORKFLOWS.md | 4 +- build.py | 15 +- docs/API.md | 142 +- docs/USER_MANUAL.md | 201 +- docs/ara2-hosting-challenges-blog.md | 2 +- docs/code-signing-policy.md | 26 + docs/implemented_features.md | 4 +- docs/input-profiles.md | 84 +- docs/nam-rack.md | 137 +- docs/pitch_renderer_research_notes.md | 2 +- docs/release-runbook.md | 441 +- docs/release-smoke-checklist.md | 8 +- docs/releases/0.1.01.md | 30 + docs/releases/0.1.02.md | 35 + docs/runtime-hardening.md | 240 + docs/testing.md | 68 +- effects/README.txt | 2 +- frontend/e2e/ai-workflow-ui.spec.ts | 212 + frontend/e2e/hotkey-focus-regression.spec.ts | 15 +- .../e2e/nam-rack-approved-surfaces.spec.ts | 185 +- .../e2e/nam-rack-cab-resource-actions.spec.ts | 321 ++ frontend/e2e/plugin-isolation-ui.spec.ts | 63 + frontend/e2e/profile-onboarding.spec.ts | 63 +- frontend/e2e/render-export-flow.spec.ts | 10 +- frontend/e2e/runtime-recovery-ui.spec.ts | 121 + frontend/e2e/work-recovery-ui.spec.ts | 150 + frontend/src/App.tsx | 100 +- frontend/src/MidiEditorWindowApp.tsx | 61 +- frontend/src/MixerWindowApp.tsx | 48 +- frontend/src/PluginEditorWindowApp.tsx | 80 +- frontend/src/__tests__/aiClipJobs.test.ts | 96 + .../src/__tests__/aiGenerationLease.test.ts | 25 + .../src/__tests__/aiGenerationStore.test.ts | 5 +- .../__tests__/aiToolsFeatureInstaller.test.ts | 25 +- frontend/src/__tests__/aiTrackJobs.test.ts | 121 + .../src/__tests__/aiWorkflowParams.test.ts | 23 +- frontend/src/__tests__/appUpdateStore.test.ts | 140 + .../__tests__/atomicShortcutMutations.test.ts | 11 +- .../src/__tests__/browserWheelGuard.test.ts | 4 +- .../src/__tests__/builtInPluginPanel.test.tsx | 18 +- .../builtInPluginParamHistory.test.ts | 162 + .../channelStripEQIntegration.test.ts | 2 +- .../commandSurfaceReachability.test.ts | 18 + .../componentScopedActionExecutors.test.ts | 16 + .../__tests__/detachedWindowAuthority.test.ts | 55 + .../src/__tests__/domShortcutEvent.test.ts | 200 + .../__tests__/editorShortcutRegistry.test.ts | 9 +- .../exhaustiveInputProfileDispatch.test.ts | 2 +- .../src/__tests__/externalURLSafety.test.ts | 14 +- frontend/src/__tests__/fieldTestUX.test.ts | 68 + .../__tests__/hotkeyFocusRegression.test.ts | 31 +- .../src/__tests__/inputProfileHelp.test.ts | 1 + .../__tests__/interactionSafetyGuards.test.ts | 32 +- .../markerRegionActionSemantics.test.ts | 9 +- .../__tests__/masterFXRemovalContract.test.ts | 2 +- .../src/__tests__/metronomePractice.test.ts | 157 + frontend/src/__tests__/midiInputMeter.test.ts | 3 +- .../__tests__/mouseBehaviorProfiles.test.ts | 4 +- ...rovedSurfaceImplementationContract.test.ts | 4 +- .../namCabPresentationFeedback.test.ts | 8 +- .../__tests__/namCabRoomConsoleLayout.test.ts | 73 +- frontend/src/__tests__/namCabV2.test.ts | 219 + .../src/__tests__/namCabinetSpace.test.ts | 32 +- .../__tests__/namCompressorV7Controls.test.ts | 4 +- .../__tests__/namCurrentDspMigration.test.ts | 57 +- .../__tests__/namGraphicEqControls.test.ts | 23 +- .../src/__tests__/namHardwareStandard.test.ts | 6 +- frontend/src/__tests__/namInputMode.test.ts | 2 +- .../__tests__/namInstrumentProfile.test.ts | 28 + .../namNativeCapabilityFallback.test.ts | 7 +- .../src/__tests__/namPortableState.test.ts | 47 +- frontend/src/__tests__/namPreEqV16.test.ts | 4 +- .../namPrecisionDriveVoiceV15.test.ts | 11 +- ...amRackFeedbackPresentationContract.test.ts | 10 + .../__tests__/namRackMutationReadback.test.ts | 16 + .../namRackPresetTransactions.test.ts | 9 +- .../src/__tests__/namReverbV5Controls.test.ts | 4 +- .../src/__tests__/namToneSaveMetadata.test.ts | 4 +- .../nativeShortcutForwarding.test.ts | 52 + frontend/src/__tests__/pluginActivity.test.ts | 32 + .../pluginEditorShortcutFocus.test.ts | 122 + .../profileNavigationRegression.test.ts | 48 + .../src/__tests__/projectNativeQueue.test.ts | 25 + .../src/__tests__/projectSaveSafety.test.ts | 139 + .../src/__tests__/projectValidation.test.ts | 75 + .../shortcutAssignmentConflicts.test.ts | 78 +- .../__tests__/shortcutContextRouting.test.ts | 63 + .../src/__tests__/tone3000LiveSearch.test.ts | 4 +- .../src/__tests__/transportRecording.test.ts | 34 + .../__tests__/undoableDAWMutations.test.ts | 8 +- .../__tests__/wheelGestureResolver.test.ts | 14 +- frontend/src/__tests__/workRecovery.test.ts | 116 + .../src/components/AIClipGenerationModal.tsx | 109 +- frontend/src/components/AITrackHeader.tsx | 456 +- frontend/src/components/AIWorkflowModal.tsx | 8 +- frontend/src/components/AiToolsSetupModal.tsx | 65 +- frontend/src/components/AppDialogHost.tsx | 52 + frontend/src/components/AppUpdatePanel.tsx | 88 + .../src/components/BatchConverterModal.tsx | 82 +- .../src/components/BuiltInPluginPanel.tsx | 258 +- frontend/src/components/ChannelStrip.tsx | 2 +- .../CustomKeyboardProfileManager.tsx | 39 +- frontend/src/components/FXChainPanel.css | 1 - frontend/src/components/FXChainPanel.tsx | 304 +- .../src/components/GettingStartedGuide.tsx | 8 +- .../src/components/InputGestureReference.tsx | 67 + .../src/components/InputProfileSelectors.tsx | 2 +- .../src/components/KeyboardShortcutsModal.tsx | 170 +- frontend/src/components/MainToolbar.tsx | 13 +- frontend/src/components/MenuBar.tsx | 160 +- frontend/src/components/MetronomeControls.tsx | 52 + frontend/src/components/MetronomeSettings.tsx | 85 +- frontend/src/components/MixerPanel.tsx | 143 +- frontend/src/components/NAMCabPresentation.ts | 10 +- frontend/src/components/NAMExplorer.tsx | 66 +- frontend/src/components/NAMRackDesignPort.tsx | 603 ++- .../NAMRackDesignPortSourceFlow.css | 32 +- frontend/src/components/NAMRackHardware.css | 42 - frontend/src/components/NAMRackHeader.css | 8 +- frontend/src/components/NAMRackMixer.tsx | 12 +- frontend/src/components/NAMRackPanel.tsx | 279 +- frontend/src/components/NAMRackStage.css | 599 ++- .../ParametricGraph/ChorusGraph.tsx | 6 +- .../ParametricGraph/CompressorGraph.tsx | 6 +- .../components/ParametricGraph/DelayGraph.tsx | 6 +- .../components/ParametricGraph/EQGraph.tsx | 8 +- .../components/ParametricGraph/GateGraph.tsx | 6 +- .../ParametricGraph/ParametricGraph.tsx | 18 +- .../ParametricGraph/ReverbGraph.tsx | 6 +- .../ParametricGraph/SaturationGraph.tsx | 6 +- frontend/src/components/PianoRoll.tsx | 6 +- .../src/components/PitchCorrectorPanel.tsx | 4 +- .../src/components/PitchEditorLowerZone.tsx | 16 +- frontend/src/components/PluginActivity.tsx | 8 + frontend/src/components/PluginBrowser.tsx | 77 +- .../src/components/ProjectRecoveryDialog.tsx | 81 + .../src/components/RecordingFailureBanner.tsx | 25 + .../src/components/RegionMarkerManager.tsx | 9 +- .../src/components/RegionRenderMatrix.tsx | 3 +- frontend/src/components/RenderModal.tsx | 48 +- frontend/src/components/ScriptEditor.tsx | 7 +- frontend/src/components/SettingsModal.tsx | 8 +- .../src/components/SortableTrackHeader.tsx | 5 +- frontend/src/components/ThemeEditor.tsx | 8 +- frontend/src/components/Timeline.tsx | 11 +- frontend/src/components/TimelineRuler.tsx | 4 +- frontend/src/components/ToolbarEditor.tsx | 5 +- frontend/src/components/TransportBar.tsx | 17 +- frontend/src/components/WorkRecoveryItems.tsx | 80 + frontend/src/components/menus/EditMenu.tsx | 66 +- .../src/components/menus/MenuDropdown.tsx | 269 +- frontend/src/components/ui/Input/Input.tsx | 9 +- frontend/src/components/ui/Modal/Modal.tsx | 2 +- frontend/src/components/ui/Select/Select.tsx | 6 +- .../src/components/ui/Textarea/Textarea.tsx | 7 +- frontend/src/data/aiWorkflows.ts | 139 +- frontend/src/e2e/shortcutHarness.ts | 19 +- frontend/src/index.css | 3 +- frontend/src/main.tsx | 2 + frontend/src/services/NativeBridge.ts | 368 +- frontend/src/services/aiClipJobs.ts | 53 + frontend/src/services/aiGenerationJobs.ts | 165 + frontend/src/services/aiTrackJobs.ts | 24 + frontend/src/services/appDialogs.ts | 35 + frontend/src/services/tone3000Session.ts | 15 + frontend/src/services/workRecovery.ts | 126 + frontend/src/store/actionRegistry.ts | 183 +- frontend/src/store/actions/automation.ts | 8 +- frontend/src/store/actions/macros.ts | 4 +- .../src/store/actions/metronomePractice.ts | 66 + frontend/src/store/actions/midi.ts | 2 + frontend/src/store/actions/project.ts | 100 +- frontend/src/store/actions/rendering.ts | 9 +- frontend/src/store/actions/routing.ts | 2 +- frontend/src/store/actions/screensets.ts | 4 +- frontend/src/store/actions/tracks.ts | 24 +- frontend/src/store/actions/transport.ts | 15 +- frontend/src/store/appUpdateStore.ts | 100 + frontend/src/store/pitchEditorStore.ts | 12 +- frontend/src/store/useDAWStore.ts | 124 +- frontend/src/utils/aiGenerationLease.ts | 14 + frontend/src/utils/browserWheelGuard.ts | 47 +- .../src/utils/builtInPluginParamHistory.ts | 195 + frontend/src/utils/customShortcutProfiles.ts | 2 +- frontend/src/utils/domShortcutEvent.ts | 74 + .../src/utils/globalShortcutDispatcher.ts | 34 +- frontend/src/utils/helpTexts.ts | 24 +- frontend/src/utils/midiEditorWindowSync.ts | 55 + frontend/src/utils/modalEventGuards.ts | 9 +- frontend/src/utils/mouseBehaviorProfiles.ts | 31 +- frontend/src/utils/namDisplayName.ts | 1 - frontend/src/utils/namPortableState.ts | 11 +- frontend/src/utils/namRackMutationReadback.ts | 11 + .../src/utils/namRackPresetTransactions.ts | 67 +- frontend/src/utils/panelLayout.ts | 14 + frontend/src/utils/pluginActivity.ts | 25 + frontend/src/utils/projectLifetime.ts | 21 + frontend/src/utils/projectNativeQueue.ts | 14 + frontend/src/utils/projectValidation.ts | 165 + .../src/utils/shortcutAssignmentConflicts.ts | 260 +- frontend/src/utils/shortcutContext.ts | 59 +- frontend/src/utils/sourceClipIdentity.ts | 8 + frontend/src/utils/tone3000LiveSearch.ts | 2 +- frontend/src/utils/transportAutoStop.ts | 4 +- frontend/src/utils/usePanelLayout.ts | 35 + frontend/src/utils/wheelGestureResolver.ts | 22 + frontend/src/utils/workRecoveryValidation.ts | 27 + packaging/msix/AppxManifest.xml.in | 36 + packaging/msix/webview2-runtime.json | 8 + packaging/release-notes-template.md | 4 +- packaging/signpath/windows-installer.xml | 11 + packaging/signpath/windows-payload.xml | 14 + packaging/windows/CrashReporter.rc.in | 24 + packaging/windows/OpenStudio.iss | 159 +- tests/test_release_notes.py | 149 + tests/test_store_submission.py | 280 + tools/ai-generation-ui-harness.mjs | 4 +- tools/archive-runtime-symbols.ps1 | 20 + tools/check-product-naming.py | 42 + tools/copy-asan-runtime.cmake | 9 + tools/diffusers_audio_pipeline.py | 175 + tools/field-test-browser-regression.js | 211 + tools/fx-menu-browser-regression.js | 142 + tools/generate-release-metadata.ps1 | 8 + tools/input-profile-browser-regression.js | 176 + tools/midi-editor-acceptance-harness.mjs | 260 +- .../mime/application-x-openstudio-project.xml | 1 - tools/nam-rack-visual-harness.mjs | 6 +- tools/package-linux-release.sh | 9 +- tools/package-macos-release.sh | 38 +- tools/package-windows-release.ps1 | 43 +- tools/package-windows-store.ps1 | 160 + tools/prepare-public-release.ps1 | 10 +- tools/prepare_diffusers_audio.py | 59 + tools/run-diffusers-audio-smoke.py | 56 + tools/run-isolated-plugin-regression.ps1 | 36 + tools/run-metronome-regression.ps1 | 41 + tools/run-nam-rack-headless-regression.ps1 | 15 +- tools/run-release-preflight.ps1 | 11 +- .../run-render-export-headless-regression.ps1 | 4 +- tools/run-runtime-capacity-diagnostic.ps1 | 41 + tools/run-runtime-safety-regression.ps1 | 105 + tools/run-window-lifecycle-smoke.ps1 | 22 +- tools/run-windows-rc.ps1 | 7 + tools/stable_audio3_generate.py | 172 +- tools/submit_store_release.py | 325 ++ tools/test-windows-signing.ps1 | 106 + tools/test-windows-store-installed.ps1 | 61 + tools/test-windows-store-package.ps1 | 23 + tools/test_diffusers_audio_pipeline.py | 97 + tools/validate-release-notes.py | 58 + tools/windows-signing.ps1 | 75 + 366 files changed, 27188 insertions(+), 7861 deletions(-) create mode 100644 Source/AppPaths.h create mode 100644 Source/AudioFileConversion.cpp create mode 100644 Source/AudioFileConversion.h create mode 100644 Source/AudioFileConversionRegression.cpp create mode 100644 Source/AudioFileConversionRegression.h create mode 100644 Source/AudioInputPolicy.h create mode 100644 Source/ControlSurfaceMidiInbox.h create mode 100644 Source/CrashReportWriter.cpp create mode 100644 Source/CrashReporterMain.cpp create mode 100644 Source/CrashReporterProtocol.h create mode 100644 Source/FieldTestRegression.h create mode 100644 Source/IsolatedPlugin.cpp create mode 100644 Source/IsolatedPlugin.h create mode 100644 Source/IsolatedPluginProtocol.h create mode 100644 Source/IsolatedPluginRegression.cpp create mode 100644 Source/IsolatedPluginWorker.cpp rename Source/{S13FXGfxEditor.cpp => JSFXGfxEditor.cpp} (88%) rename Source/{S13FXGfxEditor.h => JSFXGfxEditor.h} (84%) rename Source/{S13FXProcessor.cpp => JSFXProcessor.cpp} (86%) rename Source/{S13FXProcessor.h => JSFXProcessor.h} (88%) create mode 100644 Source/JsonEnvelope.h create mode 100644 Source/MessageThreadLifetime.h create mode 100644 Source/MetronomeRegression.cpp create mode 100644 Source/MetronomeRegression.h create mode 100644 Source/MicrophoneAccess.h create mode 100644 Source/MicrophoneAccess.mm rename Source/{S13PitchCorrector.cpp => OpenStudioPitchCorrector.cpp} (90%) rename Source/{S13PitchCorrector.h => OpenStudioPitchCorrector.h} (93%) rename Source/{S13PluginEditors.cpp => OpenStudioPluginEditors.cpp} (87%) rename Source/{S13PluginEditors.h => OpenStudioPluginEditors.h} (57%) rename Source/{S13ScriptWindow.cpp => OpenStudioScriptWindow.cpp} (75%) rename Source/{S13ScriptWindow.h => OpenStudioScriptWindow.h} (86%) create mode 100644 Source/OwnedBackgroundTasks.h create mode 100644 Source/OwnedChildProcess.cpp create mode 100644 Source/OwnedChildProcess.h create mode 100644 Source/PluginSettingsMigration.h create mode 100644 Source/PluginStateValidation.h create mode 100644 Source/ProcessorSafety.h create mode 100644 Source/ProjectFileStore.cpp create mode 100644 Source/ProjectFileStore.h create mode 100644 Source/RecordingDestination.h create mode 100644 Source/RecordingRecovery.cpp create mode 100644 Source/RecordingRecovery.h create mode 100644 Source/RecordingRecoveryRegression.cpp create mode 100644 Source/RecordingRecoveryRegression.h create mode 100644 Source/RecordingWriterSafety.h create mode 100644 Source/RecoveryJournal.cpp create mode 100644 Source/RecoveryJournal.h create mode 100644 Source/RuntimeSafetyRegression.cpp create mode 100644 Source/RuntimeSafetyRegression.h create mode 100644 Source/SendGraphSafety.h create mode 100644 Source/StoreUpdater.cpp create mode 100644 Source/StoreUpdater.h create mode 100644 Source/StoreUpdaterRegression.h create mode 100644 Source/ToneSearchRequest.h create mode 100644 Source/WindowsPackage.h create mode 100644 docs/code-signing-policy.md create mode 100644 docs/releases/0.1.01.md create mode 100644 docs/releases/0.1.02.md create mode 100644 docs/runtime-hardening.md create mode 100644 frontend/e2e/ai-workflow-ui.spec.ts create mode 100644 frontend/e2e/nam-rack-cab-resource-actions.spec.ts create mode 100644 frontend/e2e/plugin-isolation-ui.spec.ts create mode 100644 frontend/e2e/runtime-recovery-ui.spec.ts create mode 100644 frontend/e2e/work-recovery-ui.spec.ts create mode 100644 frontend/src/__tests__/aiClipJobs.test.ts create mode 100644 frontend/src/__tests__/aiGenerationLease.test.ts create mode 100644 frontend/src/__tests__/aiTrackJobs.test.ts create mode 100644 frontend/src/__tests__/appUpdateStore.test.ts create mode 100644 frontend/src/__tests__/builtInPluginParamHistory.test.ts create mode 100644 frontend/src/__tests__/domShortcutEvent.test.ts create mode 100644 frontend/src/__tests__/fieldTestUX.test.ts create mode 100644 frontend/src/__tests__/metronomePractice.test.ts create mode 100644 frontend/src/__tests__/namCabV2.test.ts create mode 100644 frontend/src/__tests__/pluginActivity.test.ts create mode 100644 frontend/src/__tests__/pluginEditorShortcutFocus.test.ts create mode 100644 frontend/src/__tests__/profileNavigationRegression.test.ts create mode 100644 frontend/src/__tests__/projectNativeQueue.test.ts create mode 100644 frontend/src/__tests__/projectSaveSafety.test.ts create mode 100644 frontend/src/__tests__/projectValidation.test.ts create mode 100644 frontend/src/__tests__/workRecovery.test.ts create mode 100644 frontend/src/components/AppDialogHost.tsx create mode 100644 frontend/src/components/AppUpdatePanel.tsx create mode 100644 frontend/src/components/InputGestureReference.tsx create mode 100644 frontend/src/components/MetronomeControls.tsx create mode 100644 frontend/src/components/PluginActivity.tsx create mode 100644 frontend/src/components/ProjectRecoveryDialog.tsx create mode 100644 frontend/src/components/RecordingFailureBanner.tsx create mode 100644 frontend/src/components/WorkRecoveryItems.tsx create mode 100644 frontend/src/services/aiClipJobs.ts create mode 100644 frontend/src/services/aiGenerationJobs.ts create mode 100644 frontend/src/services/aiTrackJobs.ts create mode 100644 frontend/src/services/appDialogs.ts create mode 100644 frontend/src/services/workRecovery.ts create mode 100644 frontend/src/store/actions/metronomePractice.ts create mode 100644 frontend/src/store/appUpdateStore.ts create mode 100644 frontend/src/utils/aiGenerationLease.ts create mode 100644 frontend/src/utils/builtInPluginParamHistory.ts create mode 100644 frontend/src/utils/domShortcutEvent.ts create mode 100644 frontend/src/utils/panelLayout.ts create mode 100644 frontend/src/utils/pluginActivity.ts create mode 100644 frontend/src/utils/projectLifetime.ts create mode 100644 frontend/src/utils/projectNativeQueue.ts create mode 100644 frontend/src/utils/projectValidation.ts create mode 100644 frontend/src/utils/sourceClipIdentity.ts create mode 100644 frontend/src/utils/usePanelLayout.ts create mode 100644 frontend/src/utils/workRecoveryValidation.ts create mode 100644 packaging/msix/AppxManifest.xml.in create mode 100644 packaging/msix/webview2-runtime.json create mode 100644 packaging/signpath/windows-installer.xml create mode 100644 packaging/signpath/windows-payload.xml create mode 100644 packaging/windows/CrashReporter.rc.in create mode 100644 tests/test_release_notes.py create mode 100644 tests/test_store_submission.py create mode 100644 tools/archive-runtime-symbols.ps1 create mode 100644 tools/check-product-naming.py create mode 100644 tools/copy-asan-runtime.cmake create mode 100644 tools/diffusers_audio_pipeline.py create mode 100644 tools/field-test-browser-regression.js create mode 100644 tools/fx-menu-browser-regression.js create mode 100644 tools/input-profile-browser-regression.js create mode 100644 tools/package-windows-store.ps1 create mode 100644 tools/prepare_diffusers_audio.py create mode 100644 tools/run-diffusers-audio-smoke.py create mode 100644 tools/run-isolated-plugin-regression.ps1 create mode 100644 tools/run-metronome-regression.ps1 create mode 100644 tools/run-runtime-capacity-diagnostic.ps1 create mode 100644 tools/run-runtime-safety-regression.ps1 create mode 100644 tools/submit_store_release.py create mode 100644 tools/test-windows-signing.ps1 create mode 100644 tools/test-windows-store-installed.ps1 create mode 100644 tools/test-windows-store-package.ps1 create mode 100644 tools/test_diffusers_audio_pipeline.py create mode 100644 tools/validate-release-notes.py create mode 100644 tools/windows-signing.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfeb616..4ce3e14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,35 @@ on: release_notes_file: description: "Path to a markdown file in the repo for release notes" required: false - default: "packaging/release-notes-template.md" + default: "" permissions: contents: read jobs: + validate-release-notes: + runs-on: ubuntu-24.04 + outputs: + notes_file: ${{ steps.notes.outputs.notes_file }} + env: + VERSION: ${{ github.event.inputs.version || github.ref_name }} + NOTES_FILE: ${{ github.event.inputs.release_notes_file }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Validate exact-version release notes before building + id: notes + run: python3 tools/validate-release-notes.py --version "$VERSION" --notes-file "$NOTES_FILE" --github-output "$GITHUB_OUTPUT" + build-windows: + needs: validate-release-notes runs-on: windows-latest + permissions: + contents: read + actions: read env: + WINDOWS_SIGNING_PROVIDER: ${{ vars.OPENSTUDIO_WINDOWS_SIGNING_PROVIDER || 'certificate' }} VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} BUILD_DIR: build-release-windows ASIO_SDK_DIR: thirdparty/asio ONNXRUNTIME_VERSION: 1.24.4 @@ -34,6 +53,25 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Validate Windows signing configuration + shell: pwsh + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }} + SIGNPATH_SIGNING_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + run: | + if ($env:WINDOWS_SIGNING_PROVIDER -notin @('certificate', 'signpath')) { + throw "OPENSTUDIO_WINDOWS_SIGNING_PROVIDER must be certificate or signpath." + } + if ($env:WINDOWS_SIGNING_PROVIDER -eq 'signpath') { + foreach ($name in @('SIGNPATH_API_TOKEN', 'SIGNPATH_ORGANIZATION_ID', 'SIGNPATH_PROJECT_SLUG', 'SIGNPATH_SIGNING_POLICY_SLUG')) { + if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { + throw "SignPath is enabled but $name is missing. See docs/release-runbook.md#windows-signing-with-signpath." + } + } + } + - name: Normalize version shell: pwsh run: | @@ -165,7 +203,50 @@ jobs: $report = Join-Path $env:RUNNER_TEMP "OpenStudio_WindowLifecycleHarness.json" ./tools/run-window-lifecycle-smoke.ps1 -AppPath $exePath -ReportPath $report -TimeoutSeconds 180 + - name: Stage Windows payload for SignPath + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "$env:BUILD_DIR/OpenStudio_artefacts/Release" ` + -DestinationDirectory "$env:RUNNER_TEMP/signpath-payload-unsigned" -Version $env:VERSION -RequireEmptyDestination + + - name: Upload unsigned Windows payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + id: signpath-payload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: signpath-payload-unsigned-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/signpath-payload-unsigned/* + if-no-files-found: error + retention-days: 7 + + - name: Sign Windows payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: windows-payload + github-artifact-id: ${{ steps.signpath-payload.outputs.artifact-id }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 3600 + output-artifact-directory: ${{ runner.temp }}/signpath-payload-signed + parameters: | + version: ${{ toJSON(env.VERSION) }} + + - name: Verify and restore signed Windows payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "$env:RUNNER_TEMP/signpath-payload-signed" ` + -DestinationDirectory "$env:BUILD_DIR/OpenStudio_artefacts/Release" -Version $env:VERSION -RequireSignature + - name: Prepare Windows signing certificate + if: env.WINDOWS_SIGNING_PROVIDER == 'certificate' shell: pwsh env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} @@ -189,6 +270,7 @@ jobs: "WINDOWS_CODESIGN_CERT_PATH=$certPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Package Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'certificate' shell: pwsh env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} @@ -215,6 +297,7 @@ jobs: $env:WINDOWS_TIMESTAMP_URL = "" $arguments = @{ Version = $env:VERSION + NotesFile = $env:RELEASE_NOTES_FILE SourceDir = "$env:BUILD_DIR/OpenStudio_artefacts/Release" } @@ -236,6 +319,52 @@ jobs: ./tools/package-windows-release.ps1 @arguments + - name: Package Windows installer with signed payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + ./tools/package-windows-release.ps1 -Version $env:VERSION -NotesFile $env:RELEASE_NOTES_FILE ` + -SourceDir "$env:BUILD_DIR/OpenStudio_artefacts/Release" -RequireSignedPayload + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "dist/windows" -Kind installer ` + -DestinationDirectory "$env:RUNNER_TEMP/signpath-installer-unsigned" -Version $env:VERSION -RequireEmptyDestination + + - name: Upload unsigned Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + id: signpath-installer + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: signpath-installer-unsigned-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/signpath-installer-unsigned/* + if-no-files-found: error + retention-days: 7 + + - name: Sign Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: windows-installer + github-artifact-id: ${{ steps.signpath-installer.outputs.artifact-id }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 3600 + output-artifact-directory: ${{ runner.temp }}/signpath-installer-signed + parameters: | + version: ${{ toJSON(env.VERSION) }} + + - name: Verify and restore signed Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "$env:RUNNER_TEMP/signpath-installer-signed" -Kind installer ` + -DestinationDirectory "dist/windows" -Version $env:VERSION -RequireSignature + Assert-OpenStudioSigningFiles -Directory "$env:BUILD_DIR/OpenStudio_artefacts/Release" ` + -Version $env:VERSION -RequireSignature + - name: Verify Windows release outputs shell: pwsh run: | @@ -254,6 +383,33 @@ jobs: shell: pwsh run: ./tools/setup-ffmpeg.ps1 -CorrespondingSourceDestination "dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip" + - name: Build validated Microsoft Store package + if: vars.OPENSTUDIO_STORE_ENABLED == 'true' + shell: pwsh + env: + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} + run: | + $storeVersion = python tools/submit_store_release.py --version $env:VERSION --print-package-version + if ($LASTEXITCODE -ne 0) { throw 'Invalid Store release version.' } + ./tools/package-windows-store.ps1 -Version $storeVersion -NotesFile $env:RELEASE_NOTES_FILE ` + -SourceDir "$env:BUILD_DIR/OpenStudio_artefacts/Release" -OutputDir dist/store + python tools/submit_store_release.py --version $env:VERSION --package-dir dist/store ` + --notes-file $env:RELEASE_NOTES_FILE --report dist/store/validation.json + if ($LASTEXITCODE -ne 0) { throw 'Store submission preflight failed.' } + + - name: Retain Microsoft Store submission artifact + if: vars.OPENSTUDIO_STORE_ENABLED == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: microsoft-store-package + path: | + dist/store/*.msix + dist/store/package-report.json + dist/store/validation.json + compression-level: 0 + retention-days: 30 + if-no-files-found: error + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-release @@ -263,9 +419,11 @@ jobs: if-no-files-found: error build-macos: + needs: validate-release-notes runs-on: macos-14 env: VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} BUILD_DIR: build-release-macos AI_RUNTIME_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_VERSION || github.event.inputs.version || github.ref_name }} AI_RUNTIME_STANDALONE_RELEASE_TAG: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_RELEASE_TAG != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_RELEASE_TAG || '20260325' }} @@ -449,7 +607,7 @@ jobs: echo "OpenStudio.app was not found in the build output." >&2 exit 1 fi - ./tools/package-macos-release.sh "$APP_PATH" "$VERSION" + ./tools/package-macos-release.sh "$APP_PATH" "$VERSION" dist/macos "$RELEASE_NOTES_FILE" if [[ -n "$MACOS_CODESIGN_IDENTITY" ]]; then echo "OPENSTUDIO_MACOS_PACKAGE_SIGNED=true" >> "$GITHUB_ENV" fi @@ -502,9 +660,11 @@ jobs: if-no-files-found: error build-linux: + needs: validate-release-notes runs-on: ubuntu-24.04 env: VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} BUILD_DIR: build-release-linux ONNXRUNTIME_VERSION: 1.24.4 ONNXRUNTIME_LINUX_X64_SHA256: 3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747 @@ -610,7 +770,7 @@ jobs: - name: Package AppImage run: | chmod +x ./tools/package-linux-release.sh - bash ./tools/package-linux-release.sh "$VERSION" "$BUILD_DIR" + bash ./tools/package-linux-release.sh "$VERSION" "$BUILD_DIR" "$RELEASE_NOTES_FILE" - name: Verify Linux release outputs run: test -f "dist/linux/OpenStudio-${VERSION}-linux-x86_64.AppImage" @@ -642,6 +802,7 @@ jobs: publish: runs-on: ubuntu-latest needs: + - validate-release-notes - build-windows - build-macos - build-linux @@ -649,7 +810,7 @@ jobs: contents: write env: VERSION: ${{ github.event.inputs.version || github.ref_name }} - RELEASE_NOTES_FILE: ${{ github.event.inputs.release_notes_file || 'packaging/release-notes-template.md' }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} AI_RUNTIME_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_VERSION || github.event.inputs.version || github.ref_name }} AI_RUNTIME_RELEASE_TAG: ${{ vars.OPENSTUDIO_AI_RUNTIME_RELEASE_TAG }} WEBSITE_REPO: ${{ vars.OPENSTUDIO_WEBSITE_REPO != '' && vars.OPENSTUDIO_WEBSITE_REPO || 'sdevil7th/OpenStudioWebsite' }} @@ -849,3 +1010,46 @@ jobs: event-type: ${{ env.WEBSITE_DISPATCH_EVENT_TYPE }} client-payload: >- {"tag":"${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', github.event.inputs.version) }}","channel":"stable","desktopRepo":"${{ github.repository }}"} + + submit-store: + name: Submit Microsoft Store update + needs: [publish, build-windows, validate-release-notes] + if: vars.OPENSTUDIO_STORE_ENABLED == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 25 + environment: microsoft-store + concurrency: + group: openstudio-microsoft-store-submission + cancel-in-progress: false + permissions: + contents: read + actions: read + env: + VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: microsoft-store-package + path: dist/store + + - name: Submit the exact release package and notes + env: + MS_STORE_TENANT_ID: ${{ secrets.MS_STORE_TENANT_ID }} + MS_STORE_CLIENT_ID: ${{ secrets.MS_STORE_CLIENT_ID }} + MS_STORE_CLIENT_SECRET: ${{ secrets.MS_STORE_CLIENT_SECRET }} + run: >- + python3 tools/submit_store_release.py --version "$VERSION" + --package-dir dist/store --notes-file "$RELEASE_NOTES_FILE" + --report output/store-submission.json --submit + + - name: Retain sanitized submission status + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: microsoft-store-submission-${{ github.run_attempt }} + path: output/store-submission.json + if-no-files-found: warn + retention-days: 30 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 4619afb..5367e83 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -17,9 +17,14 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Check first-party product names + run: python3 tools/check-product-naming.py + - name: Run Python tooling unit tests run: >- python3 -m unittest + tests.test_release_notes + tests.test_store_submission tests.test_ai_runtime_dependency_pins tests.test_install_ai_tools_feature_gating tests.test_install_ai_tools_runtime_repair @@ -66,6 +71,14 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Test Windows signing safeguards + shell: pwsh + run: ./tools/test-windows-signing.ps1 + + - name: Test Microsoft Store packaging safeguards + shell: pwsh + run: ./tools/test-windows-store-package.ps1 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version-file: frontend/.nvmrc diff --git a/.gitignore b/.gitignore index 2e01647..cc0295a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /build-*/ /out /tmp/ +/tmp_* /cmake-build-* CMakeCache.txt CMakeFiles @@ -177,3 +178,7 @@ yarn-error.log* Thumbs.db ehthumbs.db Desktop.ini + +# Local cabinet presentation captures +/tmp_cab_showcase/ +/tmp_cab_showcase_v2/ diff --git a/AGENTS.md b/AGENTS.md index 4f0a046..d6d2dfa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ OpenStudio/ │ ├── MIDIClip.h/cpp # MIDI note event storage and time-range queries │ ├── Metronome.h/cpp # Click track generation (BPM, time sig, accent patterns) │ ├── AudioConverter.h/cpp # Channel/sample-rate conversion utilities -│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.s13peaks sidecar files) +│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.ospeaks sidecar files) │ ├── AudioAnalyzer.h/cpp # Audio analysis utilities │ ├── BuiltInEffects.h/cpp # Built-in audio effects (EQ, compressor, etc.) │ ├── BuiltInEffects2.h/cpp # Additional built-in effects @@ -58,13 +58,13 @@ OpenStudio/ │ ├── HarmonicMaskGenerator.h/cpp # Wiener-filter soft masks at harmonic positions for poly separation │ ├── SpectralPitchShifter.h/cpp # Phase vocoder on masked spectrograms with cepstral formant preservation │ ├── SpectralProcessor.h/cpp # STFT/ISTFT utilities for spectral processing -│ ├── S13PitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) +│ ├── OpenStudioPitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) │ │ │ │ # Plugin System -│ ├── S13FXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) -│ ├── S13FXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps -│ ├── S13PluginEditors.h/cpp # Built-in plugin editor windows -│ ├── S13ScriptWindow.h/cpp # Lua gfx API framebuffer window +│ ├── JSFXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) +│ ├── JSFXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps +│ ├── OpenStudioPluginEditors.h/cpp # Built-in plugin editor windows +│ ├── OpenStudioScriptWindow.h/cpp # Lua gfx API framebuffer window │ ├── ScriptEngine.h/cpp # Lua scripting engine (sol2) │ │ │ │ # Other Features @@ -115,7 +115,7 @@ OpenStudio/ │ │ ├── PitchCorrectorPanel.tsx # Real-time inline corrector (auto-tune style, key/scale/retune) │ │ ├── PitchEditorLowerZone.tsx # Graphical pitch editor: canvas host, tools, controls, interaction handlers │ │ ├── PitchEditorCanvas.ts # Imperative canvas renderer (60fps RAF loop): notes, contour, grid, piano keys -│ │ ├── S13PitchEditor.tsx # Pitch editor wrapper/container +│ │ ├── OpenStudioPitchEditor.tsx # Pitch editor wrapper/container │ │ ├── pitchCorrectorPresets.ts # Preset definitions for real-time pitch corrector │ │ │ │ │ │ # Other @@ -146,6 +146,22 @@ OpenStudio/ ## Build & Dev +### Release notes are a required release gate + +Before preparing or publishing any release, review the exact Git range since the +previous application tag and write `docs/releases/.md`. Describe concrete +user-visible changes, fixes, known limitations and upgrade steps. Distinguish +verified changes in the tagged source from unshipped working-tree changes; never +copy a template, a raw commit list, or claim hardware/platform testing not run. +Include a comparison/commit/PR link, review the prose, then run +`python tools/validate-release-notes.py --version ` before building or +publishing. The same notes feed GitHub and updater metadata. CI and local release +entry points must fail closed when notes are missing, mismatched or unfinished. +Do not bypass the gate or fabricate notes just to make a release pass. +For a cumulative release page, review its main release PR as well as later +hotfixes. Cover the main features and label the hotfix-only comparison separately; +the immediately preceding tag can already contain the main feature release. + ```bash # Full dev (installs deps, builds C++ Debug, starts Vite HMR, launches app) python build.py dev --run @@ -160,7 +176,7 @@ cmake --build build --config Debug cmake --build build --config Release # Production (builds frontend + Release C++, single .exe with embedded frontend) -python build.py prod +python build.py prod --version 0.1.02 ``` **No feature flags** — all features (ASIO, WASAPI, DirectSound, VST3 hosting, WebView2) are always enabled via hardcoded `target_compile_definitions` in CMakeLists.txt. The `build.py dev` mode uses Debug config; `build.py prod` uses Release. @@ -215,7 +231,7 @@ For **continuous edits** (faders, knobs), use the begin/commit pattern: `beginXE ### Timeline Rendering - **Konva** (react-konva) for canvas-based rendering -- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.s13peaks` files), never reads audio files directly +- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.ospeaks` files), never reads audio files directly - `samplesPerPixel` uses the clip's `sampleRate` (not hardcoded) with power-of-2 quantization for cache stability - Zoom: exponential scaling via `Math.exp(-deltaY * sensitivity)`, anchored to cursor position - Zoom debounce: suppresses waveform re-fetches during active zoom (`isZoomingRef`, 200ms timeout) @@ -246,7 +262,7 @@ Polyphonic pipeline: PolyPitchDetector (Basic-Pitch ONNX) -> PolyNotes -> HarmonicMaskGenerator (Wiener) -> SpectralPitchShifter -> PolyResynthesizer Real-time corrector: - S13PitchCorrector (per-block, key/scale aware) -> inserted as FX plugin on track + OpenStudioPitchCorrector (per-block, key/scale aware) -> inserted as FX plugin on track ``` **Key data flow** (graphical editor): @@ -288,11 +304,11 @@ Real-time corrector: - **Cached pan gains**: `TrackProcessor` pre-computes `cos`/`sin` pan gains as `std::atomic` (`cachedPanL`, `cachedPanR`) when `setPan()` or `setVolume()` is called on the message thread. `processBlock()` on the audio thread loads these atomics cheaply — no trig computation per callback. - **AudioRecorder::writeBlock()** also uses `ScopedTryLock` — same pattern. -### PeakCache System (.s13peaks) +### PeakCache System (.ospeaks) -- REAPER-inspired multi-resolution peak cache stored as `.s13peaks` sidecar files alongside audio files +- REAPER-inspired multi-resolution peak cache stored as `.ospeaks` sidecar files alongside audio files - 4 mipmap levels at strides: 64, 256, 1024, 4096 samples per peak -- File format: `PeakFileHeader` (magic `0x53313350` / "S13P", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level +- File format: `PeakFileHeader` (magic `0x4f53504b` / "OSPK", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level - `AudioEngine::getWaveformPeaks()` reads from PeakCache — never reads audio files directly. First call generates the cache synchronously; subsequent calls are instant (memory-cached mipmap lookup) - Peak generation is triggered automatically in the background when recording stops (`peakCache.generateAsync()` for each completed clip) - `PeakCache::buildPeaks()` reads the audio file in a single pass, computing all 4 mipmap levels simultaneously using per-level accumulators diff --git a/CLAUDE.md b/CLAUDE.md index b987957..2096d30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ OpenStudio/ │ ├── MIDIClip.h/cpp # MIDI note event storage and time-range queries │ ├── Metronome.h/cpp # Click track generation (BPM, time sig, accent patterns) │ ├── AudioConverter.h/cpp # Channel/sample-rate conversion utilities -│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.s13peaks sidecar files) +│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.ospeaks sidecar files) │ ├── AudioAnalyzer.h/cpp # Audio analysis utilities │ ├── BuiltInEffects.h/cpp # Built-in audio effects (EQ, compressor, etc.) │ ├── BuiltInEffects2.h/cpp # Additional built-in effects @@ -58,13 +58,13 @@ OpenStudio/ │ ├── HarmonicMaskGenerator.h/cpp # Wiener-filter soft masks at harmonic positions for poly separation │ ├── SpectralPitchShifter.h/cpp # Phase vocoder on masked spectrograms with cepstral formant preservation │ ├── SpectralProcessor.h/cpp # STFT/ISTFT utilities for spectral processing -│ ├── S13PitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) +│ ├── OpenStudioPitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) │ │ │ │ # Plugin System -│ ├── S13FXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) -│ ├── S13FXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps -│ ├── S13PluginEditors.h/cpp # Built-in plugin editor windows -│ ├── S13ScriptWindow.h/cpp # Lua gfx API framebuffer window +│ ├── JSFXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) +│ ├── JSFXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps +│ ├── OpenStudioPluginEditors.h/cpp # Built-in plugin editor windows +│ ├── OpenStudioScriptWindow.h/cpp # Lua gfx API framebuffer window │ ├── ScriptEngine.h/cpp # Lua scripting engine (sol2) │ │ │ │ # Other Features @@ -115,7 +115,7 @@ OpenStudio/ │ │ ├── PitchCorrectorPanel.tsx # Real-time inline corrector (auto-tune style, key/scale/retune) │ │ ├── PitchEditorLowerZone.tsx # Graphical pitch editor: canvas host, tools, controls, interaction handlers │ │ ├── PitchEditorCanvas.ts # Imperative canvas renderer (60fps RAF loop): notes, contour, grid, piano keys -│ │ ├── S13PitchEditor.tsx # Pitch editor wrapper/container +│ │ ├── OpenStudioPitchEditor.tsx # Pitch editor wrapper/container │ │ ├── pitchCorrectorPresets.ts # Preset definitions for real-time pitch corrector │ │ │ │ │ │ # Other @@ -160,7 +160,7 @@ cmake --build build --config Debug cmake --build build --config Release # Production (builds frontend + Release C++, single .exe with embedded frontend) -python build.py prod +python build.py prod --version 0.1.02 ``` **No feature flags** — all features (ASIO, WASAPI, DirectSound, VST3 hosting, WebView2) are always enabled via hardcoded `target_compile_definitions` in CMakeLists.txt. The `build.py dev` mode uses Debug config; `build.py prod` uses Release. @@ -200,7 +200,7 @@ For **continuous edits** (faders, knobs), use the begin/commit pattern: `beginXE ### Timeline Rendering - **Konva** (react-konva) for canvas-based rendering -- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.s13peaks` files), never reads audio files directly +- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.ospeaks` files), never reads audio files directly - `samplesPerPixel` uses the clip's `sampleRate` (not hardcoded) with power-of-2 quantization for cache stability - Zoom: exponential scaling via `Math.exp(-deltaY * sensitivity)`, anchored to cursor position - Zoom debounce: suppresses waveform re-fetches during active zoom (`isZoomingRef`, 200ms timeout) @@ -228,7 +228,7 @@ Polyphonic pipeline: PolyPitchDetector (Basic-Pitch ONNX) → PolyNotes → HarmonicMaskGenerator (Wiener) → SpectralPitchShifter → PolyResynthesizer Real-time corrector: - S13PitchCorrector (per-block, key/scale aware) → inserted as FX plugin on track + OpenStudioPitchCorrector (per-block, key/scale aware) → inserted as FX plugin on track ``` **Key data flow** (graphical editor): @@ -259,11 +259,11 @@ Real-time corrector: - **Cached pan gains**: `TrackProcessor` pre-computes `cos`/`sin` pan gains as `std::atomic` (`cachedPanL`, `cachedPanR`) when `setPan()` or `setVolume()` is called on the message thread. `processBlock()` on the audio thread loads these atomics cheaply — no trig computation per callback. - **AudioRecorder::writeBlock()** also uses `ScopedTryLock` — same pattern. -### PeakCache System (.s13peaks) +### PeakCache System (.ospeaks) -- REAPER-inspired multi-resolution peak cache stored as `.s13peaks` sidecar files alongside audio files +- REAPER-inspired multi-resolution peak cache stored as `.ospeaks` sidecar files alongside audio files - 4 mipmap levels at strides: 64, 256, 1024, 4096 samples per peak -- File format: `PeakFileHeader` (magic `0x53313350` / "S13P", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level +- File format: `PeakFileHeader` (magic `0x4f53504b` / "OSPK", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level - `AudioEngine::getWaveformPeaks()` reads from PeakCache — never reads audio files directly. First call generates the cache synchronously; subsequent calls are instant (memory-cached mipmap lookup) - Peak generation is triggered automatically in the background when recording stops (`peakCache.generateAsync()` for each completed clip) - `PeakCache::buildPeaks()` reads the audio file in a single pass, computing all 4 mipmap levels simultaneously using per-level accumulators diff --git a/CMakeLists.txt b/CMakeLists.txt index e523ad1..3cae93c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,17 @@ if(MSVC) add_compile_definitions( $<$:_ITERATOR_DEBUG_LEVEL=0> ) + # Opt-in diagnostic configuration, not a production feature switch. Keep + # instrumented binaries/objects separate from the user's Debug/Release app. + if(CMAKE_CONFIGURATION_TYPES) + list(APPEND CMAKE_CONFIGURATION_TYPES ASan) + list(REMOVE_DUPLICATES CMAKE_CONFIGURATION_TYPES) + set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES}" CACHE STRING "Build configurations" FORCE) + endif() + set(CMAKE_C_FLAGS_ASAN "/Zi /O1 /DNDEBUG" CACHE STRING "ASan C flags") + set(CMAKE_CXX_FLAGS_ASAN "/Zi /O1 /DNDEBUG" CACHE STRING "ASan C++ flags") + add_compile_options($<$:/fsanitize=address>) + add_link_options($<$:/INCREMENTAL:NO> $<$:/DEBUG>) endif() option(OPENSTUDIO_ENABLE_EXTERNAL_PYTHON_AI_FALLBACK "Allow dev/unbundled builds to bootstrap AI tools from a system Python interpreter" ON) @@ -317,6 +328,17 @@ juce_add_gui_app(OpenStudio MICROPHONE_PERMISSION_ENABLED TRUE MICROPHONE_PERMISSION_TEXT "OpenStudio needs access to your audio input to record from microphones and audio interfaces." ) +if(WIN32) + # JUCE's RC generator depends on the icon but omits Info.txt. Without this + # dependency, reconfiguring an existing build with a new release version + # leaves the executable's Windows ProductVersion at the previous value. + get_target_property(openstudio_generated_sources OpenStudio JUCE_GENERATED_SOURCES_DIRECTORY) + get_target_property(openstudio_resource_info OpenStudio JUCE_INFO_FILE) + add_custom_command( + OUTPUT "${openstudio_generated_sources}/OpenStudio_resources.rc" + APPEND DEPENDS "${openstudio_resource_info}" + ) +endif() if(MSVC) # Realtime audio translation units below are intentionally optimized # in Debug. CMP0184 controls runtime checks at target scope, so the target @@ -337,8 +359,16 @@ target_sources(OpenStudio PRIVATE Source/MainComponent.cpp Source/MixerWindowManager.cpp Source/AppUpdater.cpp + Source/StoreUpdater.cpp Source/CrashDiagnostics.cpp + Source/CrashReportWriter.cpp + Source/RuntimeSafetyRegression.cpp + Source/ProjectFileStore.cpp + Source/RecoveryJournal.cpp + Source/RecordingRecovery.cpp + Source/RecordingRecoveryRegression.cpp Source/AudioEngine.cpp + Source/MetronomeRegression.cpp Source/NAMDelayRegression.h Source/NAMDelayRegression.cpp Source/NAMDelayRegressionLifecycle.cpp @@ -347,16 +377,21 @@ target_sources(OpenStudio PRIVATE Source/PlaybackEngine.cpp Source/PeakCache.cpp Source/AudioConverter.cpp + Source/AudioFileConversion.cpp + Source/AudioFileConversionRegression.cpp Source/MIDIManager.cpp Source/MIDIClip.cpp Source/PluginManager.cpp + Source/IsolatedPlugin.cpp + Source/IsolatedPluginWorker.cpp + Source/IsolatedPluginRegression.cpp Source/PluginWindowManager.cpp Source/TrackProcessor.cpp Source/Metronome.cpp Source/AudioAnalyzer.cpp - Source/S13FXProcessor.cpp - Source/S13FXGfxEditor.cpp - Source/S13ScriptWindow.cpp + Source/JSFXProcessor.cpp + Source/JSFXGfxEditor.cpp + Source/OpenStudioScriptWindow.cpp Source/ScriptEngine.cpp Source/AutomationList.cpp Source/MIDIRecorder.cpp @@ -372,11 +407,11 @@ target_sources(OpenStudio PRIVATE Source/NAMCabPresentation.cpp Source/NAMPolyOctaver.h Source/NAMPolyOctaver.cpp - Source/S13PluginEditors.cpp + Source/OpenStudioPluginEditors.cpp Source/TunerPitchTracker.cpp Source/PitchDetector.cpp Source/PitchMapper.cpp - Source/S13PitchCorrector.cpp + Source/OpenStudioPitchCorrector.cpp Source/PitchAnalyzer.cpp Source/PitchResynthesizer.cpp Source/LpcEnvelopeTransfer.cpp @@ -386,9 +421,41 @@ target_sources(OpenStudio PRIVATE Source/PolyPitchDetector.cpp Source/StemSeparator.cpp Source/AITrackEngine.cpp + Source/OwnedChildProcess.cpp Source/ARAHostController.cpp ) +target_compile_definitions(OpenStudio PRIVATE OPENSTUDIO_BUILD_CONFIGURATION="$") +if(WIN32) + # Tiny hidden helper: no JUCE, WebView, audio device, plug-in or Python startup. + add_executable(OpenStudioCrashReporter Source/CrashReporterMain.cpp Source/CrashReportWriter.cpp) + # SignPath enforces the same product name/version on every first-party binary. + configure_file(packaging/windows/CrashReporter.rc.in + "${CMAKE_CURRENT_BINARY_DIR}/OpenStudioCrashReporter.rc" @ONLY) + target_sources(OpenStudioCrashReporter PRIVATE + "${CMAKE_CURRENT_BINARY_DIR}/OpenStudioCrashReporter.rc") + target_compile_features(OpenStudioCrashReporter PRIVATE cxx_std_17) + target_link_libraries(OpenStudioCrashReporter PRIVATE dbghelp) + if(MSVC) + target_compile_options(OpenStudioCrashReporter PRIVATE /W4) + endif() + add_dependencies(OpenStudio OpenStudioCrashReporter) + foreach(diagnostic_target IN ITEMS OpenStudio OpenStudioCrashReporter) + if(MSVC) + target_compile_options(${diagnostic_target} PRIVATE $<$:/Zi>) + target_link_options(${diagnostic_target} PRIVATE $<$:/DEBUG> $<$:/INCREMENTAL:NO>) + endif() + add_custom_command(TARGET ${diagnostic_target} POST_BUILD + COMMAND ${CMAKE_COMMAND} "-DCONFIG=$" "-DCOMPILER=${CMAKE_CXX_COMPILER}" + "-DDESTINATION=$" + -P "${CMAKE_CURRENT_SOURCE_DIR}/tools/copy-asan-runtime.cmake") + endforeach() + add_custom_command(TARGET OpenStudio POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ $ + COMMENT "Copying out-of-process crash reporter") +endif() + # Keep the complete production audio callback path optimized even in the app's # developer build. `build.py dev --run` uses Debug; leaving JUCE's ASIO # conversion/convolution modules or the surrounding host/track helpers at /Od @@ -407,7 +474,7 @@ set(OPENSTUDIO_REALTIME_AUDIO_SOURCES Source/BuiltInEffects2.cpp Source/NAMCabPresentation.cpp Source/NAMPolyOctaver.cpp - Source/S13PitchCorrector.cpp + Source/OpenStudioPitchCorrector.cpp Source/TunerPitchTracker.cpp "${juce_SOURCE_DIR}/modules/juce_audio_basics/juce_audio_basics.cpp" "${juce_SOURCE_DIR}/modules/juce_audio_devices/juce_audio_devices.cpp" @@ -492,14 +559,14 @@ target_link_libraries(OpenStudio PRIVATE if(WIN32) # AudioEngine registers ASIO callback threads with the Windows Multimedia # Class Scheduler Service's "Pro Audio" task. - target_link_libraries(OpenStudio PRIVATE avrt) + target_link_libraries(OpenStudio PRIVATE avrt windowsapp) endif() # ONNX Runtime link (optional) if(HAS_ONNXRUNTIME) target_link_directories(OpenStudio PRIVATE "${ONNXRUNTIME_ROOT}/lib") target_link_libraries(OpenStudio PRIVATE onnxruntime) - target_compile_definitions(OpenStudio PRIVATE S13_HAS_ONNXRUNTIME=1) + target_compile_definitions(OpenStudio PRIVATE OPENSTUDIO_HAS_ONNXRUNTIME=1) endif() # ============================================================================== @@ -604,11 +671,16 @@ endif() # macOS-only: Link system frameworks # ============================================================================== if(APPLE) + enable_language(OBJCXX) + set_target_properties(OpenStudio PROPERTIES OBJCXX_STANDARD 20 OBJCXX_STANDARD_REQUIRED YES) + target_sources(OpenStudio PRIVATE Source/MicrophoneAccess.mm) + find_library(AVFOUNDATION_FRAMEWORK AVFoundation REQUIRED) find_library(WEBKIT_FRAMEWORK WebKit) find_library(COREAUDIO_FRAMEWORK CoreAudio) find_library(COREMIDI_FRAMEWORK CoreMIDI) find_library(ACCELERATE_FRAMEWORK Accelerate) target_link_libraries(OpenStudio PRIVATE + ${AVFOUNDATION_FRAMEWORK} ${WEBKIT_FRAMEWORK} ${COREAUDIO_FRAMEWORK} ${COREMIDI_FRAMEWORK} @@ -1123,6 +1195,16 @@ if(EXISTS "${AI_MUSIC_GENERATION_SCRIPT}") endif() set(AI_STABLE_AUDIO_GENERATION_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/tools/stable_audio3_generate.py") +openstudio_copy_runtime_file( + "${CMAKE_CURRENT_SOURCE_DIR}/tools/prepare_diffusers_audio.py" + "scripts/prepare_diffusers_audio.py" + "Copying official Diffusers conversion launcher" +) +openstudio_copy_runtime_file( + "${CMAKE_CURRENT_SOURCE_DIR}/tools/diffusers_audio_pipeline.py" + "scripts/diffusers_audio_pipeline.py" + "Copying Diffusers audio pipeline adapter" +) if(EXISTS "${AI_STABLE_AUDIO_GENERATION_SCRIPT}") openstudio_copy_runtime_file( "${AI_STABLE_AUDIO_GENERATION_SCRIPT}" diff --git a/README.md b/README.md index 5dd8eee..f640fa3 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ It combines classic multitrack production with newer tools such as stem separati | Render | WAV, AIFF, FLAC, MP3, and OGG; target sample rate, mono/stereo, normalize, tail, dither, track stems, and secondary output (MP3/OGG and FFmpeg conversions require system FFmpeg on macOS/Linux) | | Delivery | Render queue, region render matrix, DDP export, batch converter, session archive, project compare, and clean-project tools | | Workflow | Command palette, searchable/printable shortcut reference, 19 built-in DAW keyboard profiles, independently selectable mouse/scroll profiles, scoped rebinding, custom keyboard-profile import/export, help overlay, getting started guide, screensets, themes, toolbar editor | -| Extensibility | Lua scripting API, script editor, JSFX/S13FX script effects, project automation helpers | +| Extensibility | Lua scripting API, script editor, JSFX script effects, project automation helpers | | Sync / media | Timecode display/settings, big clock, media explorer browse/import, media-pool plumbing (partial), missing media resolver, video window plumbing | Some advanced features are still evolving. See [Implemented But Partial / Caveated](docs/implemented_features.md#implemented-but-partial--caveated) for the honest edges. @@ -79,7 +79,7 @@ licenses, accounts, hardware requirements, and usage terms. | MIDI note editing | **Included — free** | Piano roll, note/velocity/CC editing, transforms, virtual keyboard, and MIDI import/export | | Pitch and source workflows | **Included — free** | Graphical monophonic pitch workflow, real-time pitch corrector, stem-separation integration, and audio-to-MIDI plumbing; optional models/runtime may be required | | Guitar and bass rig | **Included — free** | NAM A1/A2 capture hosting, pedal/amp/full-rig slots, local cabinet-IR loading, native pedalboard and post effects, Guitar/Bass voicing, presets, A/B, and project recall | -| Plug-in and scripting host | **Included — free** | Primarily VST3 hosting with CLAP/LV2 code paths, built-in effects, JSFX/S13FX-style effects, and Lua scripting | +| Plug-in and scripting host | **Included — free** | Primarily VST3 hosting with CLAP/LV2 code paths, built-in effects, JSFX-style effects, and Lua scripting | | Hotkey and mouse profiles | **Included — free** | 19 built-in DAW-style keyboard maps, independent mouse/scroll maps, scoped multi-binding, conflict checks, and named custom keyboard profiles with JSON import/export | | AI-assisted creation | **Free / optional setup** | Local integration for generation, variation, inpaint, continuation, stems, and audio-to-MIDI; the small Basic Pitch model is included and its inference runtime is enabled in current Windows/Linux releases, while large generation/stem runtimes and models remain optional downloads | | Local and inspectable workflow | **Included — free** | Local project files and source-visible JUCE/React implementation under the GNU AGPLv3 | @@ -159,7 +159,7 @@ Cubase is a mature commercial composition DAW. OpenStudio is the open local-firs | Recording / editing / mixing | ✅ Decades of polished DAW workflows | ✅ Native JUCE engine, clip editing, takes, razor/ripple edits, sends, buses, mixer snapshots | | Pitch / stems / audio-to-MIDI | VariAudio 3 is in Artist/Pro; AI stem separation is Pro-only; Audio-to-MIDI chords are in Elements/Artist/Pro | Graphical pitch editor, real-time pitch corrector, stem separation integration, and Basic Pitch audio-to-MIDI in ONNX-enabled Windows/Linux builds | | AI generation | ⚠️ Assisted tools, but not a local AI music-generation DAW workflow | ✅ Optional local AI Tools runtime for ACE-Step, Stable Audio 3, variation, inpaint, continuation, stems | -| Plugin ecosystem | VST3 hosting; ARA 2 in Artist/Pro. VST2 can be enabled in limited cases but is officially unsupported | ✅ VST3 plus CLAP/LV2 code paths, input/track/master/monitoring FX, JSFX/S13FX, Lua | +| Plugin ecosystem | VST3 hosting; ARA 2 in Artist/Pro. VST2 can be enabled in limited cases but is officially unsupported | ✅ VST3 plus CLAP/LV2 code paths, input/track/master/monitoring FX, JSFX, Lua | | Local / private workflow | Local desktop DAW; proprietary software and licensing | ✅ Local project files, optional local AI runtime, source-visible implementation | | Open / customizable | ❌ Closed source | ✅ Fork it, script it, extend it, inspect the engine | | Where Cubase still wins | ✅ Mature scoring, VariAudio polish, bundled content, commercial support | ⚠️ Some advanced OpenStudio features remain partial or release-hardening dependent | @@ -175,7 +175,7 @@ Pro Tools is the facility standard. OpenStudio is for producers and builders who | Recording / editing / mixing | ✅ Deep tracking, comping, editing, post-production, automation | ✅ Multitrack recording, takes, razor/ripple edits, routing, automation, mixer, render queue | | Pitch / repair | All tiers support ARA 2. Intro requires a separately licensed or trial ARA plug-in; active paid subscriptions and perpetual Upgrade Plans include Melodyne essential, RePitch Elements, and other repair tools | Built-in graphical pitch editor, real-time pitch corrector, ARA host plumbing | | AI generation | ⚠️ Integrations and assisted workflows, but not a local AI music generator inside the DAW core | ✅ Text-to-music, lyrics+style, text-to-audio, variation, inpaint, continuation, stems | -| Plugin ecosystem | AAX Native/AudioSuite across all tiers, plus ARA 2 integration; third-party AAX works in Intro and paid tiers | VST3 plus CLAP/LV2 code paths, built-in FX, JSFX/S13FX, Lua scripting | +| Plugin ecosystem | AAX Native/AudioSuite across all tiers, plus ARA 2 integration; third-party AAX works in Intro and paid tiers | VST3 plus CLAP/LV2 code paths, built-in FX, JSFX, Lua scripting | | Local / private workflow | Local desktop DAW; proprietary software/licensing with optional connected services | ✅ Local files, optional local AI runtime, inspectable source | | Open / customizable | ❌ Closed source | ✅ Source-visible, scriptable, hackable | | Where Pro Tools still wins | ✅ Commercial facilities, HDX/AAX workflows, immersive/post pipelines, support ecosystem | ⚠️ OpenStudio is not claiming facility-standard interchange parity | @@ -255,12 +255,9 @@ The base app does not bundle heavy AI runtimes. Optional AI Tools are installed ## Project And File Formats - Project files: `.osproj` -- Legacy project import/open: `.s13` - Theme exports: `.ostheme` -- Legacy theme import: `.s13theme` - Built-in FX presets: `.ospreset` - Waveform peak cache: `.ospeaks` -- Legacy peak cache support: `.s13peaks` ## Architecture @@ -318,9 +315,34 @@ cmake --build build --config Debug cmake --build build --config Release # Production package path -python build.py prod +python build.py prod --version 0.1.02 ``` +## Installation and first-use access + +On Windows, open Setup normally and approve its UAC elevation request. The VC++ +and WebView2 prerequisites are machine-wide installations; a standard account +may need administrator credentials. Setup skips a sufficient VC++ runtime, +recognizes restart-required results, and offers Retry with the actual exit code +and logs under `%ProgramData%\OpenStudio\InstallerLogs`. If you explicitly +started Setup as another administrator, launch OpenStudio yourself from the Start +menu afterward so it uses your account's preferences and credentials. + +On macOS, microphone authorization is requested when you enable hardware input +monitoring, record audio, or apply an input device. Playback, MIDI editing and +playback effects do not need microphone consent. If access was denied, enable +OpenStudio in **System Settings > Privacy & Security > Microphone**. User media, +effects and scripts can still require Documents access. Internal macOS logs use +`~/Library/Logs/OpenStudio`; plugin settings use +`~/Library/Application Support/OpenStudio/Plugins`. Existing OpenStudio plugin +settings are copied and verified on first plugin use, with the originals kept. + +For online NAM captures, open the Amp or Cab library and use **Connect TONE3000** +next to the results-panel status. Sign in in your browser; release builds provide +the app configuration and users do not enter an API key. Login is remembered for +each OS user/computer, subject to provider revocation and secure-store access. +Local NAM captures and IR files remain usable offline. + ## macOS First Launch Note The v1 macOS community package is unsigned. Verify the published SHA-256 checksum, try to open the app once, then use **System Settings > Privacy & Security > Open Anyway** and confirm **Open**. This is Apple's per-app override and preserves the diagnostic distinction between Gatekeeper and an OpenStudio startup failure. @@ -335,6 +357,7 @@ Removing quarantine is not the preferred installation path because it recursivel ## Documentation +- [Code signing policy (draft)](docs/code-signing-policy.md) and [Windows SignPath setup](docs/release-runbook.md#windows-signing-with-signpath) - [User Manual](docs/USER_MANUAL.md) - [Lua Scripting API](docs/API.md) - [Runtime Dependency Contract](docs/runtime-dependency-contract.md) @@ -359,7 +382,7 @@ available in Git instead of accumulating as dated plan documents. | Frontend | React 18, TypeScript, Vite, Zustand, Tailwind CSS, Konva | | Plugins | JUCE plugin hosting, VST3, CLAP integration, LV2 code paths, ARA SDK | | Analysis / AI | ONNX Runtime, Basic Pitch model flow, ACE-Step, Stable Audio 3 optional runtime | -| Scripting | Lua / sol2, S13FX / JSFX-style script processor | +| Scripting | Lua / sol2, JSFX / JSFX-style script processor | | Packaging | CMake, build.py orchestration, platform release scripts | ## Status diff --git a/Source/AITrackEngine.cpp b/Source/AITrackEngine.cpp index 54b820f..f968f89 100644 --- a/Source/AITrackEngine.cpp +++ b/Source/AITrackEngine.cpp @@ -1,15 +1,31 @@ +#if defined(_WIN32) +#include +#endif #include "AITrackEngine.h" +#include "RecoveryJournal.h" +#include "JsonEnvelope.h" namespace { constexpr auto kPinnedMusicGenerationModelId = "ace-step-v15-xl-turbo"; constexpr auto kPinnedMusicGenerationModelRepoId = "ACE-Step/acestep-v15-xl-turbo-diffusers"; constexpr auto kStableAudioModelId = "stable-audio-3-medium"; +constexpr auto kMiniMaxModelId = "minimax-music-3"; +bool isDiffusersAudioModel(const juce::String& id) +{ + return id == kStableAudioModelId || id == kMiniMaxModelId; +} +juce::String audioModelLabel(const juce::String& id) +{ + return id == kMiniMaxModelId ? "MiniMax Music 3" + : id == kStableAudioModelId ? "Stable Audio 3" : "ACE-Step"; +} constexpr auto kReaderSleepMs = 50; constexpr auto kWorkerStartupTimeoutMs = 45000; constexpr auto kWorkerRequestTimeoutMs = 10000; constexpr auto kWorkerProtocolVersion = 2; constexpr auto kMaxFramedPayloadBytes = 8 * 1024 * 1024; +constexpr size_t kMaxWorkerLineBytes = 64 * 1024; constexpr auto kColdDecodeStallTimeoutMs = 90000; juce::String createSafeMusicGenerationTimestamp() @@ -85,47 +101,28 @@ juce::String computeScriptVersion(const juce::File& script) if (! script.loadFileAsData(scriptBytes)) return {}; - return juce::MD5(scriptBytes.getData(), scriptBytes.getSize()).toHexString().substring(0, 16); -} - -bool killWindowsProcessTree(int pid, juce::String* output = nullptr) -{ - #if JUCE_WINDOWS - if (pid <= 0) - return false; - - juce::StringArray command; - command.add("taskkill"); - command.add("/PID"); - command.add(juce::String(pid)); - command.add("/F"); - command.add("/T"); - - juce::ChildProcess killer; - if (! killer.start(command, juce::ChildProcess::wantStdOut | juce::ChildProcess::wantStdErr)) - return false; + if (script.getFileName() == "stable_audio3_generate.py") + { + juce::MemoryBlock adapterBytes; + if (! script.getSiblingFile("diffusers_audio_pipeline.py").loadFileAsData(adapterBytes)) + return {}; + scriptBytes.append(adapterBytes.getData(), adapterBytes.getSize()); + } - killer.waitForProcessToFinish(5000); - if (output != nullptr) - *output = killer.readAllProcessOutput().trim(); - return true; - #else - juce::ignoreUnused(pid, output); - return false; - #endif + return juce::MD5(scriptBytes.getData(), scriptBytes.getSize()).toHexString().substring(0, 16); } -bool writeSocketFully(juce::StreamingSocket& socket, const char* data, int totalBytes, int timeoutMs) +bool writeSocketFully(juce::StreamingSocket& socket, const char* data, int totalBytes, int timeoutMs, const std::atomic& cancelled) { auto bytesWritten = 0; const auto deadline = juce::Time::currentTimeMillis() + timeoutMs; - while (bytesWritten < totalBytes && juce::Time::currentTimeMillis() < deadline) + while (!cancelled.load() && bytesWritten < totalBytes && juce::Time::currentTimeMillis() < deadline) { if (socket.waitUntilReady(false, 250) <= 0) continue; - const auto chunkBytes = socket.write(data + bytesWritten, totalBytes - bytesWritten); + const auto chunkBytes = socket.write(data + bytesWritten, juce::jmin(4096, totalBytes - bytesWritten)); if (chunkBytes <= 0) return false; @@ -135,13 +132,13 @@ bool writeSocketFully(juce::StreamingSocket& socket, const char* data, int total return bytesWritten == totalBytes; } -bool readSocketFully(juce::StreamingSocket& socket, void* destination, int totalBytes, int timeoutMs) +bool readSocketFully(juce::StreamingSocket& socket, void* destination, int totalBytes, int timeoutMs, const std::atomic& cancelled) { auto* writePtr = static_cast (destination); auto bytesRead = 0; const auto deadline = juce::Time::currentTimeMillis() + timeoutMs; - while (bytesRead < totalBytes && juce::Time::currentTimeMillis() < deadline) + while (!cancelled.load() && bytesRead < totalBytes && juce::Time::currentTimeMillis() < deadline) { if (socket.waitUntilReady(true, 250) <= 0) continue; @@ -156,13 +153,39 @@ bool readSocketFully(juce::StreamingSocket& socket, void* destination, int total return bytesRead == totalBytes; } +struct RequestSerializationAborted {}; + +// JUCE's JSON formatter ignores a false write result. An explicit local abort +// stops formatting as well as allocation when cancellation or the size bound +// is reached. This runs only on the generation thread, never the audio thread. +class RequestJSONStream final : public juce::OutputStream +{ +public: + explicit RequestJSONStream(const std::atomic& stop) : cancelled(stop) {} + bool write(const void* data, size_t bytes) override + { + if (cancelled.load(std::memory_order_relaxed) + || bytes > static_cast(kMaxFramedPayloadBytes) - memory.getDataSize()) + throw RequestSerializationAborted {}; + return memory.write(data, bytes); + } + void flush() override {} + juce::int64 getPosition() override { return memory.getPosition(); } + bool setPosition(juce::int64 position) override { return memory.setPosition(position); } + juce::MemoryOutputStream memory; +private: + const std::atomic& cancelled; +}; + bool writeFramedJson(juce::StreamingSocket& socket, const juce::var& payload, int timeoutMs, - int& payloadBytesWritten) + int& payloadBytesWritten, const std::atomic& cancelled) { - auto json = juce::JSON::toString(payload, false); - payloadBytesWritten = static_cast (json.getNumBytesAsUTF8()); + RequestJSONStream json(cancelled); + try { juce::JSON::writeToStream(json, payload, false); } + catch (const RequestSerializationAborted&) { return false; } + payloadBytesWritten = static_cast (json.memory.getDataSize()); if (payloadBytesWritten <= 0 || payloadBytesWritten > kMaxFramedPayloadBytes) return false; @@ -172,15 +195,15 @@ bool writeFramedJson(juce::StreamingSocket& socket, header[2] = static_cast ((payloadBytesWritten >> 8) & 0xFF); header[3] = static_cast (payloadBytesWritten & 0xFF); - return writeSocketFully(socket, header, 4, timeoutMs) - && writeSocketFully(socket, json.toRawUTF8(), payloadBytesWritten, timeoutMs); + return writeSocketFully(socket, header, 4, timeoutMs, cancelled) + && writeSocketFully(socket, static_cast(json.memory.getData()), payloadBytesWritten, timeoutMs, cancelled); } -juce::var readFramedJson(juce::StreamingSocket& socket, int timeoutMs, int& payloadBytesRead) +juce::var readFramedJson(juce::StreamingSocket& socket, int timeoutMs, int& payloadBytesRead, const std::atomic& cancelled) { char header[4] {}; payloadBytesRead = 0; - if (! readSocketFully(socket, header, 4, timeoutMs)) + if (! readSocketFully(socket, header, 4, timeoutMs, cancelled)) return {}; payloadBytesRead = ((static_cast (header[0]) << 24) @@ -193,7 +216,7 @@ juce::var readFramedJson(juce::StreamingSocket& socket, int timeoutMs, int& payl juce::HeapBlock buffer(static_cast (payloadBytesRead + 1)); zeromem(buffer.get(), static_cast (payloadBytesRead + 1)); - if (! readSocketFully(socket, buffer.get(), payloadBytesRead, timeoutMs)) + if (! readSocketFully(socket, buffer.get(), payloadBytesRead, timeoutMs, cancelled)) return {}; return juce::JSON::parse(juce::String::fromUTF8(buffer.get(), payloadBytesRead)); @@ -250,7 +273,7 @@ juce::File AITrackEngine::getUserRuntimeRoot() const juce::File AITrackEngine::getStableAudioRuntimeRoot() const { - return getUserDataRoot().getChildFile("stable-audio-runtime"); + return getUserDataRoot().getChildFile("diffusers-audio-runtime"); } juce::File AITrackEngine::getMusicGenerationCheckpointRoot() const @@ -261,11 +284,11 @@ juce::File AITrackEngine::getMusicGenerationCheckpointRoot() const .getChildFile("diffusers"); } -juce::File AITrackEngine::getStableAudioModelRoot() const +juce::File AITrackEngine::getStableAudioModelRoot(const juce::String& modelId) const { return getUserDataRoot() .getChildFile("models") - .getChildFile(kStableAudioModelId); + .getChildFile(modelId); } juce::File AITrackEngine::findPython() const @@ -358,42 +381,6 @@ juce::File AITrackEngine::findStableAudioScript() const return {}; } -void AITrackEngine::cleanupLegacyWorkerProcesses(const juce::File& python, const juce::File& script) const -{ - #if JUCE_WINDOWS - auto escapedScriptPath = script.getFullPathName().replace("'", "''"); - auto escapedPythonPath = python.getFullPathName().replace("'", "''"); - juce::StringArray command; - command.add("powershell"); - command.add("-NoProfile"); - command.add("-ExecutionPolicy"); - command.add("Bypass"); - command.add("-Command"); - command.add( - "Get-CimInstance Win32_Process | " - "Where-Object { $_.CommandLine -like '*--worker*' " - " -and $_.CommandLine -like '*" + escapedScriptPath + "*' " - " -and (" - " $_.CommandLine -like '*" + escapedPythonPath + "*' " - " -or $_.CommandLine -like '*stable_audio3_generate.py*' " - " -or $_.CommandLine -like '*generate_music.py*'" - " ) } | " - "ForEach-Object { " - " & taskkill /PID $_.ProcessId /F /T 2>$null | Out-Null; " - " Write-Output ('stopped legacy AI generation worker pid ' + $_.ProcessId) " - "}"); - - juce::ChildProcess cleanup; - if (cleanup.start(command, juce::ChildProcess::wantStdOut | juce::ChildProcess::wantStdErr)) - { - cleanup.waitForProcessToFinish(5000); - auto output = cleanup.readAllProcessOutput().trim(); - if (output.isNotEmpty()) - juce::Logger::writeToLog("AITrackEngine: " + output); - } - #endif -} - juce::String AITrackEngine::appendProcessDetailsLocked(const juce::String& message) const { juce::String decorated = message; @@ -462,9 +449,7 @@ bool AITrackEngine::waitForWorkerReady(int timeoutMs) if (currentProgress_.error.isEmpty()) { - const auto modelLabel = currentProgress_.modelId == kStableAudioModelId - ? juce::String("Stable Audio 3") - : juce::String("ACE-Step"); + const auto modelLabel = audioModelLabel(currentProgress_.modelId); const auto message = workerExitedBeforeReady ? appendProcessDetailsLocked(modelLabel + " worker exited before reporting ready.") : appendProcessDetailsLocked(modelLabel + " worker did not become ready in time."); @@ -492,19 +477,17 @@ void AITrackEngine::stopWorkerSession(bool clearProgress, bool userCancelled, bo { std::thread readerThreadToJoin; bool shouldKillProcess = false; - int workerPidToKill = 0; - juce::ChildProcess* processToKill = nullptr; + OwnedChildProcess* processToKill = nullptr; { const juce::ScopedLock sl(lock_); readerShouldExit_ = true; expectedProcessExit_ = true; - cancelRequested_ = userCancelled; + cancelRequested_ = cancelRequested_ || userCancelled; if (! keepGenerationActive) generationActive_ = false; workerReady_ = false; workerPort_ = 0; - workerPidToKill = workerPid_; processToKill = workerProcess_.get(); if (workerProcess_ != nullptr && workerProcess_->isRunning()) @@ -516,24 +499,8 @@ void AITrackEngine::stopWorkerSession(bool clearProgress, bool userCancelled, bo if (shouldKillProcess && processToKill != nullptr && processToKill->isRunning()) { - auto killedTree = false; - #if JUCE_WINDOWS - if (workerPidToKill > 0) - { - juce::String killOutput; - juce::Logger::writeToLog("AITrackEngine: stopping AI generation child process tree pid=" - + juce::String(workerPidToKill)); - killedTree = killWindowsProcessTree(workerPidToKill, &killOutput); - if (killOutput.isNotEmpty()) - juce::Logger::writeToLog("AITrackEngine: " + truncateForLog(killOutput, 512)); - } - #endif - - if (! killedTree && processToKill->isRunning()) - { - juce::Logger::writeToLog("AITrackEngine: stopping AI generation child process"); - processToKill->kill(); - } + if (!processToKill->kill()) + juce::Logger::writeToLog("AITrackEngine: owned process-tree termination failed."); } if (readerThreadToJoin.joinable()) @@ -553,8 +520,9 @@ void AITrackEngine::stopWorkerSession(bool clearProgress, bool userCancelled, bo bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce::File& script, const juce::String& modelId) { - const auto isStableAudio = modelId == kStableAudioModelId; - const auto modelLabel = isStableAudio ? juce::String("Stable Audio 3") : juce::String("ACE-Step"); + if (stopRequested_.load(std::memory_order_acquire)) return false; + const auto isStableAudio = isDiffusersAudioModel(modelId); + const auto modelLabel = audioModelLabel(modelId); const auto expectedScriptVersion = computeScriptVersion(script); { const juce::ScopedLock sl(lock_); @@ -563,15 +531,17 @@ bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce:: && workerReady_ && workerPort_ > 0 && workerProtocolVersion_ == kWorkerProtocolVersion - && workerScriptVersion_ == expectedScriptVersion) + && workerScriptVersion_ == expectedScriptVersion + && workerModelId_ == modelId) return true; } stopWorkerSession(false, false, true); - cleanupLegacyWorkerProcesses(python, script); for (int launchAttempt = 0; launchAttempt < 2; ++launchAttempt) { + if (stopRequested_.load(std::memory_order_acquire)) return false; + auto nextWorker = std::make_unique(); juce::StringArray command; command.add(python.getFullPathName()); command.add(script.getFullPathName()); @@ -579,7 +549,9 @@ bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce:: if (isStableAudio) { command.add("--model-root"); - command.add(getStableAudioModelRoot().getFullPathName()); + command.add(getStableAudioModelRoot(modelId).getFullPathName()); + command.add("--model-id"); + command.add(modelId); } else { @@ -599,7 +571,6 @@ bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce:: { const juce::ScopedLock sl(lock_); - workerProcess_ = std::make_unique(); resetProcessStateLocked(); readerShouldExit_ = false; expectedProcessExit_ = false; @@ -635,7 +606,7 @@ bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce:: currentProgress_.failureDetail.clear(); } - if (! workerProcess_->start(command, juce::ChildProcess::wantStdOut | juce::ChildProcess::wantStdErr)) + if (! nextWorker->start(command)) { const juce::ScopedLock sl(lock_); workerProcess_.reset(); @@ -645,6 +616,11 @@ bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce:: return false; } + { + const juce::ScopedLock sl(lock_); + if (stopRequested_.load(std::memory_order_acquire)) return false; + workerProcess_ = std::move(nextWorker); + } readerThread_ = std::thread([this]() { readerLoop(); }); if (waitForWorkerReady(kWorkerStartupTimeoutMs)) return true; @@ -659,7 +635,6 @@ bool AITrackEngine::ensureWorkerAvailable(const juce::File& python, const juce:: if (! shouldRetryLaunch) return false; - cleanupLegacyWorkerProcesses(python, script); } return false; @@ -670,16 +645,19 @@ bool AITrackEngine::sendGenerateRequest(const juce::String& modelId, const juce::String& paramsJson, const juce::File& outputFile) { - const auto isStableAudio = modelId == kStableAudioModelId; - const auto modelLabel = isStableAudio ? juce::String("Stable Audio 3") : juce::String("ACE-Step"); + const auto modelLabel = audioModelLabel(modelId); int port = 0; juce::String requestId; + juce::String scriptVersion; + int workerPid = 0; { const juce::ScopedLock sl(lock_); if (cancelRequested_) return false; port = workerPort_; requestId = currentRequestId_; + scriptVersion = expectedScriptVersion_; + workerPid = workerPid_; } if (port <= 0) @@ -701,6 +679,15 @@ bool AITrackEngine::sendGenerateRequest(const juce::String& modelId, return false; } + #if JUCE_WINDOWS + // Readiness is not a promise that a large blocking send will complete. + // Bound individual sends as well as the framing/cancellation loop. + const DWORD sendTimeoutMs = 250; + if (setsockopt(static_cast(socket.getRawSocketHandle()), SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&sendTimeoutMs), sizeof(sendTimeoutMs)) != 0) + return false; + #endif + auto request = std::make_unique(); request->setProperty("command", "generate"); request->setProperty("modelId", modelId); @@ -709,10 +696,10 @@ bool AITrackEngine::sendGenerateRequest(const juce::String& modelId, request->setProperty("output", outputFile.getFullPathName()); request->setProperty("requestId", requestId); request->setProperty("protocolVersion", kWorkerProtocolVersion); - request->setProperty("scriptVersion", expectedScriptVersion_); + request->setProperty("scriptVersion", scriptVersion); int payloadBytesWritten = 0; - if (! writeFramedJson(socket, juce::var(request.release()), kWorkerRequestTimeoutMs, payloadBytesWritten)) + if (! writeFramedJson(socket, juce::var(request.release()), kWorkerRequestTimeoutMs, payloadBytesWritten, stopRequested_)) { const juce::ScopedLock sl(lock_); if (cancelRequested_) @@ -726,11 +713,11 @@ bool AITrackEngine::sendGenerateRequest(const juce::String& modelId, juce::Logger::writeToLog("AITrackEngine: sent framed worker request" " requestId=" + requestId - + " workerPid=" + juce::String(workerPid_) + + " workerPid=" + juce::String(workerPid) + " payloadBytes=" + juce::String(payloadBytesWritten)); int ackPayloadBytes = 0; - auto parsed = readFramedJson(socket, kWorkerRequestTimeoutMs, ackPayloadBytes); + auto parsed = readFramedJson(socket, kWorkerRequestTimeoutMs, ackPayloadBytes, stopRequested_); if (parsed.isVoid()) { const juce::ScopedLock sl(lock_); @@ -770,7 +757,7 @@ bool AITrackEngine::sendGenerateRequest(const juce::String& modelId, const auto ackScriptVersion = object->getProperty("scriptVersion").toString(); if (ackRequestId != requestId || ackProtocolVersion != kWorkerProtocolVersion - || ackScriptVersion != expectedScriptVersion_) + || ackScriptVersion != scriptVersion) { const juce::ScopedLock sl(lock_); setProgressErrorLocked("worker_protocol_failed", @@ -793,8 +780,7 @@ void AITrackEngine::launchGenerationTask(const juce::File& python, const juce::String& paramsJson, const juce::File& outputFile) { - const auto isStableAudio = modelId == kStableAudioModelId; - const auto modelLabel = isStableAudio ? juce::String("Stable Audio 3") : juce::String("ACE-Step"); + const auto modelLabel = audioModelLabel(modelId); if (! ensureWorkerAvailable(python, script, modelId)) { @@ -860,18 +846,28 @@ bool AITrackEngine::startGeneration(const juce::String& modelId, const juce::String& paramsJson, const juce::File& outputDir) { - joinGenerationThread(); - + if (!hasBoundedJsonEnvelope(paramsJson, 8 * 1024 * 1024)) return false; + auto parameters = juce::JSON::parse(paramsJson); + if (!parameters.isObject()) return false; + const auto recoveryId = parameters.getProperty("_openStudioRecoveryId", "").toString(); + parameters.getDynamicObject()->removeProperty("_openStudioRecoveryId"); + const auto workerParameters = juce::JSON::toString(parameters, true); { const juce::ScopedLock sl(lock_); if (generationActive_) return false; } - const auto isStableAudio = modelId == kStableAudioModelId; + joinGenerationThread(); + if (modelId != kPinnedMusicGenerationModelId && ! isDiffusersAudioModel(modelId)) + return false; + if (modelId == kMiniMaxModelId && workflowId != "lyrics-style" && workflowId != "structured-song") + return false; + + const auto isStableAudio = isDiffusersAudioModel(modelId); const auto python = isStableAudio ? findStableAudioPython() : findPython(); const auto script = isStableAudio ? findStableAudioScript() : findScript(); - const auto modelLabel = isStableAudio ? juce::String("Stable Audio 3") : juce::String("ACE-Step"); + const auto modelLabel = audioModelLabel(modelId); if (! python.existsAsFile() || ! script.existsAsFile()) { @@ -900,7 +896,9 @@ bool AITrackEngine::startGeneration(const juce::String& modelId, outputFile = currentOutputFile_; generationActive_ = true; cancelRequested_ = false; + stopRequested_.store(false, std::memory_order_release); currentRequestId_ = juce::Uuid().toString(); + recoveryJournalId_ = recoveryId; expectedScriptVersion_ = computeScriptVersion(script); generationStartedAtMs_ = juce::Time::currentTimeMillis(); lastHeartbeatAtMs_ = generationStartedAtMs_; @@ -937,9 +935,20 @@ bool AITrackEngine::startGeneration(const juce::String& modelId, currentProgress_.lmStage.clear(); } - generationThread_ = std::thread([this, python, script, modelId, workflowId, paramsJson, outputFile]() + generationThread_ = std::thread([this, python, script, modelId, workflowId, workerParameters, outputFile]() { - launchGenerationTask(python, script, modelId, workflowId, paramsJson, outputFile); + if (recoveryJournalId_.isNotEmpty()) { + auto fields = std::make_unique(); + fields->setProperty("requestId", currentRequestId_); + fields->setProperty("expectedOutputFile", outputFile.getFullPathName()); + if (!RecoveryJournal::updateOwnedAI(recoveryJournalId_, juce::var(fields.release()))) { + const juce::ScopedLock sl(lock_); + setProgressErrorLocked("journal_failed", "Could not persist the generation request", "storage"); + generationActive_ = false; + return; + } + } + launchGenerationTask(python, script, modelId, workflowId, workerParameters, outputFile); }); return true; @@ -998,9 +1007,7 @@ void AITrackEngine::parseOutputLine(const juce::String& line) if (workerProtocolVersion_ != kWorkerProtocolVersion || (! expectedScriptVersion_.isEmpty() && workerScriptVersion_ != expectedScriptVersion_)) { - const auto modelLabel = currentProgress_.modelId == kStableAudioModelId - ? juce::String("Stable Audio 3") - : juce::String("ACE-Step"); + const auto modelLabel = audioModelLabel(currentProgress_.modelId); workerProtocolRejected_ = true; workerReady_ = false; workerPort_ = 0; @@ -1016,10 +1023,12 @@ void AITrackEngine::parseOutputLine(const juce::String& line) return; } + workerModelId_ = obj->getProperty("modelId").toString(); + if (workerModelId_.isEmpty()) workerModelId_ = kPinnedMusicGenerationModelId; workerReady_ = true; workerPort_ = static_cast (double (obj->getProperty("port"))); const auto backend = obj->getProperty("backend").toString(); - const auto readyLabel = backend == "stable-audio-3" ? juce::String("Stable Audio 3") : juce::String("ACE-Step"); + const auto readyLabel = audioModelLabel(workerModelId_); currentProgress_.state = "idle"; currentProgress_.progress = 0.0f; currentProgress_.phase = "worker_ready"; @@ -1040,6 +1049,8 @@ void AITrackEngine::parseOutputLine(const juce::String& line) } } + if (obj->getProperty("requestId").toString() != currentRequestId_ || currentRequestId_.isEmpty()) + return; // Never let stale/foreign progress change ownership or release this worker. sawStructuredOutput_ = true; lastStdoutLine_ = trimmed; if (! loggedFirstStructuredOutput_) @@ -1057,10 +1068,7 @@ void AITrackEngine::parseOutputLine(const juce::String& line) const auto currentStateIsTerminal = currentProgress_.state == "done" || currentProgress_.state == "error" || currentProgress_.state == "cancelled"; - const auto incomingStateIsTerminal = incomingState == "done" - || incomingState == "error" - || incomingState == "cancelled"; - if (currentStateIsTerminal && ! incomingStateIsTerminal) + if (currentStateIsTerminal) { juce::Logger::writeToLog("AITrackEngine: ignoring late non-terminal progress after terminal state" " currentState=" + currentProgress_.state @@ -1069,6 +1077,29 @@ void AITrackEngine::parseOutputLine(const juce::String& line) return; } + if (incomingState == "done") + { + const auto output = obj->getProperty("outputFile").toString(); + if (!juce::File::isAbsolutePath(output) || juce::File(output) != currentOutputFile_ + || !currentOutputFile_.existsAsFile()) + { + setProgressErrorLocked("invalid_worker_output", "The generation worker returned an unexpected or missing output file.", "worker_protocol"); + generationActive_ = false; + return; + } + // A crash between native completion and the next frontend poll must not + // discard a finished artifact. This executes on the worker reader thread. + if (recoveryJournalId_.isNotEmpty()) { + auto fields = std::make_unique(); + fields->setProperty("status", "completed"); + fields->setProperty("outputFile", output); + fields->setProperty("outputBytes", currentOutputFile_.getSize()); + fields->setProperty("requestId", currentRequestId_); + if (!RecoveryJournal::updateOwnedAI(recoveryJournalId_, juce::var(fields.release()))) + juce::Logger::writeToLog("AI generation completed, but its recovery journal update failed"); + } + } + if (obj->hasProperty("state")) currentProgress_.state = obj->getProperty("state").toString(); if (obj->hasProperty("progress")) @@ -1209,17 +1240,8 @@ void AITrackEngine::handleWorkerExit() return; } - if (currentOutputFile_.existsAsFile()) - { - currentProgress_.state = "done"; - currentProgress_.progress = 1.0f; - currentProgress_.outputFile = currentOutputFile_.getFullPathName(); - currentProgress_.phase = "done"; - currentProgress_.message = "Music generation completed."; - currentProgress_.failureKind.clear(); - generationActive_ = false; - return; - } + // A partial WAV may exist after a crash. Only a matching terminal + // protocol message proves completion; never infer it from existence. if (! sawStructuredOutput_) finalError = appendProcessDetailsLocked("The ACE-Step process exited before reporting progress."); @@ -1268,6 +1290,13 @@ void AITrackEngine::readerLoop() shouldLogFirstByte = true; } + if (processOutputBuffer_.size() >= kMaxWorkerLineBytes) + { + setProgressErrorLocked("worker_output_overflow", "The worker emitted an oversized diagnostic line.", "worker_protocol"); + generationActive_ = false; + readerShouldExit_ = true; + break; + } processOutputBuffer_.push_back(byte); while (true) @@ -1361,8 +1390,15 @@ void AITrackEngine::resetProcessStateLocked() void AITrackEngine::stopWorker(bool clearProgress, bool userCancelled) { - stopWorkerSession(clearProgress, userCancelled, false); + // Cancellation never resets process ownership while launch/request work is + // still using it. Handshake and socket loops observe this before joining. + stopRequested_.store(true, std::memory_order_release); + { + const juce::ScopedLock sl(lock_); + cancelRequested_ = true; + } joinGenerationThread(); + stopWorkerSession(clearProgress, userCancelled, false); } AIGenerationProgress AITrackEngine::pollProgress() @@ -1442,9 +1478,9 @@ AIGenerationProgress AITrackEngine::pollProgress() } if (shouldStopForDecodeStall) - stopWorkerSession(false, false, false); + stopWorker(false, false); else if (shouldReleaseTerminalWorker) - stopWorkerSession(false, false, false); + stopWorker(false, false); const juce::ScopedLock sl(lock_); return currentProgress_; diff --git a/Source/AITrackEngine.h b/Source/AITrackEngine.h index 6d935a4..ae89ce8 100644 --- a/Source/AITrackEngine.h +++ b/Source/AITrackEngine.h @@ -5,6 +5,7 @@ #include #include #include +#include "OwnedChildProcess.h" struct AIGenerationProgress { @@ -60,16 +61,16 @@ class AITrackEngine bool isRunning() const; private: + friend class RuntimeSafetyRegression; juce::File getUserDataRoot() const; juce::File getUserRuntimeRoot() const; juce::File getStableAudioRuntimeRoot() const; juce::File getMusicGenerationCheckpointRoot() const; - juce::File getStableAudioModelRoot() const; + juce::File getStableAudioModelRoot(const juce::String& modelId) const; juce::File findPython() const; juce::File findStableAudioPython() const; juce::File findScript() const; juce::File findStableAudioScript() const; - void cleanupLegacyWorkerProcesses(const juce::File& python, const juce::File& script) const; bool ensureWorkerAvailable(const juce::File& python, const juce::File& script, const juce::String& modelId); bool sendGenerateRequest(const juce::String& modelId, const juce::String& workflowId, @@ -94,7 +95,8 @@ class AITrackEngine const juce::String& failureKind); void resetProcessStateLocked(); - std::unique_ptr workerProcess_; + std::unique_ptr workerProcess_; + std::atomic stopRequested_ { false }; std::thread readerThread_; std::thread generationThread_; std::atomic readerShouldExit_ { false }; @@ -106,9 +108,11 @@ class AITrackEngine juce::String lastStderrLine_; juce::File currentOutputFile_; juce::String currentRequestId_; + juce::String recoveryJournalId_; juce::String expectedScriptVersion_; juce::String workerScriptVersion_; juce::String workerScriptPath_; + juce::String workerModelId_; bool generationActive_ = false; bool expectedProcessExit_ = false; bool cancelRequested_ = false; diff --git a/Source/ARADebug.h b/Source/ARADebug.h index 3fb97be..45aa412 100644 --- a/Source/ARADebug.h +++ b/Source/ARADebug.h @@ -1,3 +1,4 @@ +#include "AppPaths.h" #pragma once #include @@ -9,8 +10,7 @@ inline void logARADebugLine(const juce::String& msg) if (!kEnableARADebugDiagnostics) return; - auto logFile = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory) - .getChildFile("OpenStudio").getChildFile("debug_log.txt"); + auto logFile = AppPaths::diagnostics().getChildFile("debug_log.txt"); logFile.getParentDirectory().createDirectory(); logFile.appendText(juce::Time::getCurrentTime().toString(true, true) + ": " + msg + "\n"); diff --git a/Source/ARAHostController.cpp b/Source/ARAHostController.cpp index 08beed7..f632c25 100644 --- a/Source/ARAHostController.cpp +++ b/Source/ARAHostController.cpp @@ -7,7 +7,7 @@ ARAHostController::~ARAHostController() shutdown(); } -#if S13_HAS_ARA +#if OpenStudio_HAS_ARA namespace { @@ -31,7 +31,7 @@ juce::String araContentTypeToString(ARA::ARAContentType type) // ============================================================================= ARA::ARAAudioReaderHostRef -ARAHostController::S13AudioAccessController::createAudioReaderForSource ( +ARAHostController::OpenStudioAudioAccessController::createAudioReaderForSource ( ARA::ARAAudioSourceHostRef audioSourceHostRef, bool use64BitSamples) noexcept { @@ -48,7 +48,7 @@ ARAHostController::S13AudioAccessController::createAudioReaderForSource ( return ref; } -bool ARAHostController::S13AudioAccessController::readAudioSamples ( +bool ARAHostController::OpenStudioAudioAccessController::readAudioSamples ( ARA::ARAAudioReaderHostRef readerRef, ARA::ARASamplePosition samplePosition, ARA::ARASampleCount samplesPerChannel, @@ -177,20 +177,20 @@ bool ARAHostController::S13AudioAccessController::readAudioSamples ( return true; } -void ARAHostController::S13AudioAccessController::destroyAudioReader ( +void ARAHostController::OpenStudioAudioAccessController::destroyAudioReader ( ARA::ARAAudioReaderHostRef readerRef) noexcept { audioReaders.erase (ReaderConverter::fromHostRef (readerRef)); } // Archiving Controller -ARA::ARASize ARAHostController::S13ArchivingController::getArchiveSize ( +ARA::ARASize ARAHostController::OpenStudioArchivingController::getArchiveSize ( ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept { return static_cast (ReaderConverter::fromHostRef (archiveReaderHostRef)->getSize()); } -bool ARAHostController::S13ArchivingController::readBytesFromArchive ( +bool ARAHostController::OpenStudioArchivingController::readBytesFromArchive ( ARA::ARAArchiveReaderHostRef archiveReaderHostRef, ARA::ARASize position, ARA::ARASize length, ARA::ARAByte* buffer) noexcept @@ -204,7 +204,7 @@ bool ARAHostController::S13ArchivingController::readBytesFromArchive ( return false; } -bool ARAHostController::S13ArchivingController::writeBytesToArchive ( +bool ARAHostController::OpenStudioArchivingController::writeBytesToArchive ( ARA::ARAArchiveWriterHostRef archiveWriterHostRef, ARA::ARASize position, ARA::ARASize length, const ARA::ARAByte* buffer) noexcept @@ -215,57 +215,57 @@ bool ARAHostController::S13ArchivingController::writeBytesToArchive ( return false; } -void ARAHostController::S13ArchivingController::notifyDocumentArchivingProgress (float) noexcept {} -void ARAHostController::S13ArchivingController::notifyDocumentUnarchivingProgress (float) noexcept {} +void ARAHostController::OpenStudioArchivingController::notifyDocumentArchivingProgress (float) noexcept {} +void ARAHostController::OpenStudioArchivingController::notifyDocumentUnarchivingProgress (float) noexcept {} -ARA::ARAPersistentID ARAHostController::S13ArchivingController::getDocumentArchiveID ( +ARA::ARAPersistentID ARAHostController::OpenStudioArchivingController::getDocumentArchiveID ( ARA::ARAArchiveReaderHostRef) noexcept { - return "studio13-ara-archive-v1"; + return "openstudio-ara-archive-v1"; } // Content Access Controller -bool ARAHostController::S13ContentAccessController::isMusicalContextContentAvailable ( +bool ARAHostController::OpenStudioContentAccessController::isMusicalContextContentAvailable ( ARA::ARAMusicalContextHostRef, ARA::ARAContentType type) noexcept { return type == ARA::kARAContentTypeTempoEntries || type == ARA::kARAContentTypeBarSignatures; } -ARA::ARAContentGrade ARAHostController::S13ContentAccessController::getMusicalContextContentGrade ( +ARA::ARAContentGrade ARAHostController::OpenStudioContentAccessController::getMusicalContextContentGrade ( ARA::ARAMusicalContextHostRef, ARA::ARAContentType) noexcept { return ARA::kARAContentGradeInitial; } ARA::ARAContentReaderHostRef -ARAHostController::S13ContentAccessController::createMusicalContextContentReader ( +ARAHostController::OpenStudioContentAccessController::createMusicalContextContentReader ( ARA::ARAMusicalContextHostRef, ARA::ARAContentType type, const ARA::ARAContentTimeRange*) noexcept { return Converter::toHostRef (type); } -bool ARAHostController::S13ContentAccessController::isAudioSourceContentAvailable ( +bool ARAHostController::OpenStudioContentAccessController::isAudioSourceContentAvailable ( ARA::ARAAudioSourceHostRef, ARA::ARAContentType) noexcept { return false; } -ARA::ARAContentGrade ARAHostController::S13ContentAccessController::getAudioSourceContentGrade ( +ARA::ARAContentGrade ARAHostController::OpenStudioContentAccessController::getAudioSourceContentGrade ( ARA::ARAAudioSourceHostRef, ARA::ARAContentType) noexcept { return ARA::kARAContentGradeInitial; } ARA::ARAContentReaderHostRef -ARAHostController::S13ContentAccessController::createAudioSourceContentReader ( +ARAHostController::OpenStudioContentAccessController::createAudioSourceContentReader ( ARA::ARAAudioSourceHostRef, ARA::ARAContentType, const ARA::ARAContentTimeRange*) noexcept { return nullptr; } -ARA::ARAInt32 ARAHostController::S13ContentAccessController::getContentReaderEventCount ( +ARA::ARAInt32 ARAHostController::OpenStudioContentAccessController::getContentReaderEventCount ( ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept { auto contentType = Converter::fromHostRef (contentReaderHostRef); @@ -274,7 +274,7 @@ ARA::ARAInt32 ARAHostController::S13ContentAccessController::getContentReaderEve return 0; } -const void* ARAHostController::S13ContentAccessController::getContentReaderDataForEvent ( +const void* ARAHostController::OpenStudioContentAccessController::getContentReaderDataForEvent ( ARA::ARAContentReaderHostRef contentReaderHostRef, ARA::ARAInt32 eventIndex) noexcept { @@ -303,11 +303,11 @@ const void* ARAHostController::S13ContentAccessController::getContentReaderDataF return nullptr; } -void ARAHostController::S13ContentAccessController::destroyContentReader ( +void ARAHostController::OpenStudioContentAccessController::destroyContentReader ( ARA::ARAContentReaderHostRef) noexcept {} // Model Update Controller -void ARAHostController::S13ModelUpdateController::notifyAudioSourceAnalysisProgress ( +void ARAHostController::OpenStudioModelUpdateController::notifyAudioSourceAnalysisProgress ( ARA::ARAAudioSourceHostRef, ARA::ARAAnalysisProgressState state, float value) noexcept { analysisProgress.store (value); @@ -328,21 +328,21 @@ void ARAHostController::S13ModelUpdateController::notifyAudioSourceAnalysisProgr analysisComplete.store (true); } -void ARAHostController::S13ModelUpdateController::notifyAudioSourceContentChanged ( +void ARAHostController::OpenStudioModelUpdateController::notifyAudioSourceContentChanged ( ARA::ARAAudioSourceHostRef audioSourceHostRef, const ARA::ARAContentTimeRange*, ARA::ContentUpdateScopes) noexcept { using SrcConverter = ARAHostModel::ConversionFunctions; auto* entry = SrcConverter::fromHostRef(audioSourceHostRef); owner.noteEditDrivenDebugEvent("notifyAudioSourceContentChanged", entry != nullptr ? entry->clipId : juce::String()); } -void ARAHostController::S13ModelUpdateController::notifyAudioModificationContentChanged ( +void ARAHostController::OpenStudioModelUpdateController::notifyAudioModificationContentChanged ( ARA::ARAAudioModificationHostRef audioModificationHostRef, const ARA::ARAContentTimeRange*, ARA::ContentUpdateScopes) noexcept { using ModConverter = ARAHostModel::ConversionFunctions; auto* entry = ModConverter::fromHostRef(audioModificationHostRef); owner.noteEditDrivenDebugEvent("notifyAudioModificationContentChanged", entry != nullptr ? entry->clipId : juce::String()); } -void ARAHostController::S13ModelUpdateController::notifyPlaybackRegionContentChanged ( +void ARAHostController::OpenStudioModelUpdateController::notifyPlaybackRegionContentChanged ( ARA::ARAPlaybackRegionHostRef playbackRegionHostRef, const ARA::ARAContentTimeRange*, ARA::ContentUpdateScopes) noexcept { using RegConverter = ARAHostModel::ConversionFunctions; @@ -351,29 +351,29 @@ void ARAHostController::S13ModelUpdateController::notifyPlaybackRegionContentCha } // Playback Controller -void ARAHostController::S13PlaybackController::requestStartPlayback() noexcept +void ARAHostController::OpenStudioPlaybackController::requestStartPlayback() noexcept { owner.requestStartPlaybackFromPlugin(); } -void ARAHostController::S13PlaybackController::requestStopPlayback() noexcept +void ARAHostController::OpenStudioPlaybackController::requestStopPlayback() noexcept { owner.requestStopPlaybackFromPlugin(); } -void ARAHostController::S13PlaybackController::requestSetPlaybackPosition (ARA::ARATimePosition timePosition) noexcept +void ARAHostController::OpenStudioPlaybackController::requestSetPlaybackPosition (ARA::ARATimePosition timePosition) noexcept { owner.requestSetPlaybackPositionFromPlugin(static_cast (timePosition)); } -void ARAHostController::S13PlaybackController::requestSetCycleRange (ARA::ARATimePosition startTime, +void ARAHostController::OpenStudioPlaybackController::requestSetCycleRange (ARA::ARATimePosition startTime, ARA::ARATimeDuration duration) noexcept { owner.requestSetCycleRangeFromPlugin(static_cast (startTime), static_cast (duration)); } -void ARAHostController::S13PlaybackController::requestEnableCycle (bool enable) noexcept +void ARAHostController::OpenStudioPlaybackController::requestEnableCycle (bool enable) noexcept { owner.requestEnableCycleFromPlugin(enable); } @@ -419,11 +419,11 @@ void ARAHostController::completeInitialization (juce::ARAFactoryWrapper factory, try { // Create the host-side controllers (we keep raw pointers before moving ownership) - auto audioAccess = std::make_unique(); - auto archiving = std::make_unique(); - auto contentAccess = std::make_unique(); - auto modelUpdate = std::make_unique(*this); - auto playbackCtrl = std::make_unique(*this); + auto audioAccess = std::make_unique(); + auto archiving = std::make_unique(); + auto contentAccess = std::make_unique(); + auto modelUpdate = std::make_unique(*this); + auto playbackCtrl = std::make_unique(*this); requestedAnalysisContentTypes.clear(); const auto* araFactory = factory.get(); @@ -810,7 +810,7 @@ juce::MemoryBlock ARAHostController::saveState() const // Use the ARA archiving interface to save document state juce::MemoryOutputStream stream (stateData, false); - auto writerRef = S13ArchivingController::WriterConverter::toHostRef (&stream); + auto writerRef = OpenStudioArchivingController::WriterConverter::toHostRef (&stream); dc.storeDocumentToArchive (writerRef); return stateData; @@ -823,7 +823,7 @@ bool ARAHostController::restoreState (const juce::MemoryBlock& data) auto& dc = araDocController->getDocumentController(); auto dataCopy = data; // Need non-const - auto readerRef = S13ArchivingController::ReaderConverter::toHostRef (&dataCopy); + auto readerRef = OpenStudioArchivingController::ReaderConverter::toHostRef (&dataCopy); bool ok = dc.beginRestoringDocumentFromArchive (readerRef); if (ok) ok = dc.endRestoringDocumentFromArchive (readerRef); @@ -906,7 +906,7 @@ void ARAHostController::setPlaybackRequestHandlers(PlaybackRequestHandlers handl float ARAHostController::getAnalysisProgress() const { -#if S13_HAS_ARA +#if OpenStudio_HAS_ARA return modelUpdateControllerPtr != nullptr ? modelUpdateControllerPtr->analysisProgress.load() : 0.0f; @@ -917,7 +917,7 @@ float ARAHostController::getAnalysisProgress() const bool ARAHostController::isAnalysisComplete() const { -#if S13_HAS_ARA +#if OpenStudio_HAS_ARA return modelUpdateControllerPtr != nullptr ? modelUpdateControllerPtr->analysisComplete.load() : false; @@ -1076,7 +1076,7 @@ void ARAHostController::requestEnableCycleFromPlugin(bool enable) const juce::MessageManager::callAsync([handler, enable]() { handler(enable); }); } -#else // !S13_HAS_ARA +#else // !OpenStudio_HAS_ARA // Stub implementations when ARA is not available @@ -1120,4 +1120,4 @@ void ARAHostController::notePlaybackStart(double) {} void ARAHostController::notePlaybackStop(double) {} -#endif // S13_HAS_ARA +#endif // OpenStudio_HAS_ARA diff --git a/Source/ARAHostController.h b/Source/ARAHostController.h index 28d494b..74281ae 100644 --- a/Source/ARAHostController.h +++ b/Source/ARAHostController.h @@ -4,15 +4,15 @@ // ARA hosting requires the ARA SDK and JUCE_PLUGINHOST_ARA=1 #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) -#define S13_HAS_ARA 1 +#define OpenStudio_HAS_ARA 1 #else -#define S13_HAS_ARA 0 +#define OpenStudio_HAS_ARA 0 #endif #include #include "ARADebug.h" -#if S13_HAS_ARA +#if OpenStudio_HAS_ARA #include #include #endif @@ -125,13 +125,13 @@ class ARAHostController int currentBlockSize = 512; int pluginInputChannelCount = 0; -#if S13_HAS_ARA +#if OpenStudio_HAS_ARA // ========================================================================= // ARA Host Interface Controllers // ========================================================================= - class S13AudioAccessController final : public ARA::Host::AudioAccessControllerInterface + class OpenStudioAudioAccessController final : public ARA::Host::AudioAccessControllerInterface { public: struct AudioReader @@ -156,7 +156,7 @@ class ARAHostController std::map> audioReaders; }; - class S13ArchivingController final : public ARA::Host::ArchivingControllerInterface + class OpenStudioArchivingController final : public ARA::Host::ArchivingControllerInterface { public: using ReaderConverter = ARAHostModel::ConversionFunctions; @@ -174,7 +174,7 @@ class ARAHostController ARA::ARAPersistentID getDocumentArchiveID (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override; }; - class S13ContentAccessController final : public ARA::Host::ContentAccessControllerInterface + class OpenStudioContentAccessController final : public ARA::Host::ContentAccessControllerInterface { public: using Converter = ARAHostModel::ConversionFunctions; @@ -199,10 +199,10 @@ class ARAHostController ARA::ARAContentBarSignature barSignature {}; }; - class S13ModelUpdateController final : public ARA::Host::ModelUpdateControllerInterface + class OpenStudioModelUpdateController final : public ARA::Host::ModelUpdateControllerInterface { public: - explicit S13ModelUpdateController(ARAHostController& ownerIn) : owner(ownerIn) {} + explicit OpenStudioModelUpdateController(ARAHostController& ownerIn) : owner(ownerIn) {} // Keep this adapter aligned with the vendored ARA 2.2.0 SDK. void notifyAudioSourceAnalysisProgress (ARA::ARAAudioSourceHostRef, ARA::ARAAnalysisProgressState, float) noexcept override; @@ -218,10 +218,10 @@ class ARAHostController ARAHostController& owner; }; - class S13PlaybackController final : public ARA::Host::PlaybackControllerInterface + class OpenStudioPlaybackController final : public ARA::Host::PlaybackControllerInterface { public: - explicit S13PlaybackController(ARAHostController& ownerIn) : owner(ownerIn) {} + explicit OpenStudioPlaybackController(ARAHostController& ownerIn) : owner(ownerIn) {} void requestStartPlayback() noexcept override; void requestStopPlayback() noexcept override; @@ -262,11 +262,11 @@ class ARAHostController }; // Raw pointers to our controllers (ownership is transferred to ARAHostDocumentController) - S13AudioAccessController* audioAccessControllerPtr = nullptr; - S13ArchivingController* archivingControllerPtr = nullptr; - S13ContentAccessController* contentAccessControllerPtr = nullptr; - S13ModelUpdateController* modelUpdateControllerPtr = nullptr; - S13PlaybackController* playbackControllerPtr = nullptr; + OpenStudioAudioAccessController* audioAccessControllerPtr = nullptr; + OpenStudioArchivingController* archivingControllerPtr = nullptr; + OpenStudioContentAccessController* contentAccessControllerPtr = nullptr; + OpenStudioModelUpdateController* modelUpdateControllerPtr = nullptr; + OpenStudioPlaybackController* playbackControllerPtr = nullptr; // JUCE high-level ARA document controller (owns the host interface controllers) std::unique_ptr araDocController; @@ -288,9 +288,9 @@ class ARAHostController void completeInitialization (juce::ARAFactoryWrapper factory, std::function onComplete); - friend class S13AudioAccessController; + friend class OpenStudioAudioAccessController; -#endif // S13_HAS_ARA +#endif // OpenStudio_HAS_ARA void noteDebugOperation(const juce::String& operationType, const juce::String& clipId); void noteEditDrivenDebugEvent(const juce::String& eventType, const juce::String& clipId); diff --git a/Source/AppPaths.h b/Source/AppPaths.h new file mode 100644 index 0000000..6e13ae4 --- /dev/null +++ b/Source/AppPaths.h @@ -0,0 +1,31 @@ +#pragma once +#include + +namespace AppPaths +{ +inline juce::File documents() +{ + return juce::File::getSpecialLocation(juce::File::userDocumentsDirectory).getChildFile("OpenStudio"); +} +inline juce::File applicationData() +{ + return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("OpenStudio"); +} +inline juce::File diagnostics() +{ + #if JUCE_MAC + return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("Logs/OpenStudio"); + #else + return documents(); + #endif +} +inline juce::File pluginSettings() +{ + #if JUCE_MAC + return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) + .getChildFile("Application Support/OpenStudio/Plugins"); + #else + return documents(); + #endif +} +} diff --git a/Source/AppUpdater.cpp b/Source/AppUpdater.cpp index 1cc6b59..ce8a667 100644 --- a/Source/AppUpdater.cpp +++ b/Source/AppUpdater.cpp @@ -1,8 +1,14 @@ #include "AppUpdater.h" +#include "WindowsPackage.h" #include namespace { +#if JUCE_DEBUG +constexpr bool isDevelopmentBuild = true; +#else +constexpr bool isDevelopmentBuild = false; +#endif struct ParsedUpdateFeed { juce::String version; @@ -58,21 +64,9 @@ juce::File getOpenStudioAppDataDirectory() .getChildFile("OpenStudio"); } -juce::File getLegacyStudio13AppDataDirectory() -{ - return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) - .getChildFile("Studio13"); -} - juce::File getPreferredUpdaterStateFile() { - auto openStudioDir = getOpenStudioAppDataDirectory(); - auto legacyStateFile = getLegacyStudio13AppDataDirectory().getChildFile("updater-state.json"); - auto preferredDir = (!openStudioDir.exists() && legacyStateFile.existsAsFile()) - ? getLegacyStudio13AppDataDirectory() - : openStudioDir; - - return preferredDir.getChildFile("updater-state.json"); + return getOpenStudioAppDataDirectory().getChildFile("updater-state.json"); } constexpr juce::int64 kAutomaticUpdateCheckIntervalMs = 24LL * 60LL * 60LL * 1000LL; @@ -121,6 +115,11 @@ juce::String getXmlChildText(const juce::XmlElement& parent, std::initializer_li AppUpdater::AppUpdater() { + if (WindowsPackage::isStoreManaged()) + { + storeUpdater = std::make_unique([this](const juce::var& status) { publishStatus(status); }); + return; + } const auto stateFile = getPreferredUpdaterStateFile(); if (stateFile.existsAsFile()) { @@ -129,10 +128,23 @@ AppUpdater::AppUpdater() persistedState = parsed; } - publishStatus(makeStatus("idle", "Updater ready", {}, {}, {}, {}, {}, {}, {}, 0, {}, getCurrentChannel())); + publishStatus(makeStatus(isDevelopmentBuild ? "development" : "idle", + isDevelopmentBuild ? "Development build: update installers are disabled. Build this checkout to update it." : "Updater ready", + {}, {}, {}, {}, {}, {}, {}, 0, {}, getCurrentChannel())); } -AppUpdater::~AppUpdater() = default; +AppUpdater::~AppUpdater() { shutdown(); } +void AppUpdater::shutdown() { if (storeUpdater) storeUpdater->shutdown(); jobs.shutdown(); setStatusCallback({}); } + +bool AppUpdater::rejectDevelopmentUpdate(const Completion& completion) +{ + if (!isDevelopmentBuild) return false; + const auto result = makeStatus("development", + "Development build: update installers are disabled. Build this checkout to update it."); + publishStatus(result); + if (completion) completion(result); + return true; +} juce::String AppUpdater::getCurrentVersion() const { @@ -153,13 +165,20 @@ void AppUpdater::setStatusCallback(StatusCallback callback) void AppUpdater::checkForUpdates(bool manual, Completion completion) { + if (!MessageThreadLifetime::accepts(jobs.token())) return; + if (storeUpdater) { storeUpdater->check(manual, std::move(completion)); return; } + if (rejectDevelopmentUpdate(completion)) return; + if (installInProgress.load() || getStringProperty(getLastStatus(), "status") == "download-ready") + { + if (completion) completion(getLastStatus()); + return; + } if (checkInProgress.exchange(true)) { auto busy = makeStatus("busy", "An update check is already running.", {}, {}, {}, {}, {}, {}, {}, 0, {}, getCurrentChannel()); - publishStatus(busy); if (completion) - juce::MessageManager::callAsync([completion, busy]() { completion(busy); }); + juce::MessageManager::callAsync([completion, busy, alive = jobs.token()]() { if (MessageThreadLifetime::accepts(alive)) completion(busy); }); return; } @@ -170,10 +189,9 @@ void AppUpdater::checkForUpdates(bool manual, Completion completion) auto skipped = makeStatus("skipped", "Automatic update check skipped until the next scheduled window.", {}, {}, {}, {}, {}, {}, {}, 0, {}, getCurrentChannel()); - publishStatus(skipped); if (completion) - juce::MessageManager::callAsync([completion, skipped]() { completion(skipped); }); + juce::MessageManager::callAsync([completion, skipped, alive = jobs.token()]() { if (MessageThreadLifetime::accepts(alive)) completion(skipped); }); return; } @@ -182,57 +200,90 @@ void AppUpdater::checkForUpdates(bool manual, Completion completion) manual ? "Checking for updates..." : "Checking for updates in the background...", {}, {}, {}, {}, {}, {}, {}, 0, {}, getCurrentChannel())); - std::thread([this, completion = std::move(completion)]() mutable + jobs.addJob([this, alive = jobs.token(), completion = std::move(completion)]() mutable { auto result = performUpdateCheck(); - checkInProgress = false; - - juce::MessageManager::callAsync([this, completion = std::move(completion), result]() mutable + juce::MessageManager::callAsync([this, alive, completion = std::move(completion), result]() mutable { + if (!MessageThreadLifetime::accepts(alive)) return; + checkInProgress = false; + { + const juce::ScopedLock lock(stateLock); + if (getStringProperty(result, "status") == "update-available") availableUpdate = result.clone(); + else availableUpdate = juce::var(); + } publishStatus(result); if (completion) completion(result); }); - }).detach(); + }); } -void AppUpdater::downloadAndInstallUpdate(const juce::String& downloadUrl, - const juce::String& version, - const juce::String& expectedSha256, - const juce::String& releasePageUrl, - const juce::String& installerArguments, - juce::int64 expectedSize, - Completion completion) +void AppUpdater::downloadUpdate(Completion completion) { - if (installInProgress.exchange(true)) + if (!MessageThreadLifetime::accepts(jobs.token())) return; + if (storeUpdater) { storeUpdater->download(std::move(completion)); return; } + if (rejectDevelopmentUpdate(completion)) return; + if (checkInProgress.load() || installInProgress.exchange(true)) { - auto busy = makeStatus("busy", "An update download is already running.", version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, {}, getCurrentChannel(), false, installerArguments); - publishStatus(busy); - - if (completion) - juce::MessageManager::callAsync([completion, busy]() { completion(busy); }); - + if (completion) completion(getLastStatus()); return; } + juce::var offer; + { + const juce::ScopedLock lock(stateLock); + offer = availableUpdate.clone(); + } + cancelRequested = false; + auto starting = offer.isObject() ? offer.clone() : makeStatus("downloading", "Downloading update..."); + starting.getDynamicObject()->setProperty("status", "downloading"); + starting.getDynamicObject()->setProperty("message", "Downloading update..."); + publishStatus(starting); + jobs.addJob([this, alive = jobs.token(), offer, completion = std::move(completion)]() mutable + { + auto result = performDownload(offer); + juce::MessageManager::callAsync([this, alive, completion = std::move(completion), result]() mutable + { + if (!MessageThreadLifetime::accepts(alive)) return; + installInProgress = false; + publishStatus(result); + if (completion) completion(result); + }); + }); +} - publishStatus(makeStatus("downloading", "Downloading update...", version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, {}, getCurrentChannel(), false, installerArguments)); +void AppUpdater::cancelDownload() { if (storeUpdater) storeUpdater->cancel(); else cancelRequested = true; } - std::thread([this, completion = std::move(completion), downloadUrl, version, expectedSha256, releasePageUrl, installerArguments, expectedSize]() mutable +void AppUpdater::installDownloadedUpdate(Completion completion) +{ + if (!MessageThreadLifetime::accepts(jobs.token())) return; + if (storeUpdater) { storeUpdater->install(std::move(completion)); return; } + if (rejectDevelopmentUpdate(completion)) return; + if (checkInProgress.load() || installInProgress.exchange(true)) { - auto result = performDownloadAndInstall(downloadUrl, version, expectedSha256, releasePageUrl, installerArguments, expectedSize); - installInProgress = false; - - juce::MessageManager::callAsync([this, completion = std::move(completion), result]() mutable + if (completion) completion(getLastStatus()); + return; + } + auto status = getLastStatus().clone(); + status.getDynamicObject()->setProperty("status", "installing"); + status.getDynamicObject()->setProperty("message", "Verifying update before opening..."); + publishStatus(status); + jobs.addJob([this, alive = jobs.token(), completion = std::move(completion)]() mutable + { + const auto result = performInstall(); + juce::MessageManager::callAsync([this, alive, completion = std::move(completion), result]() mutable { + if (!MessageThreadLifetime::accepts(alive)) return; + installInProgress = false; publishStatus(result); - if (completion) - completion(result); + if (completion) completion(result); }); - }).detach(); + }); } juce::var AppUpdater::performUpdateCheck() { + if (WindowsPackage::isStoreManaged()) return makeStatus("error", "This installation uses Microsoft Store package updates."); const auto manifestUrl = getManifestUrl().trim(); const auto appcastUrl = getAppcastUrl().trim(); const auto currentChannel = getCurrentChannel(); @@ -266,6 +317,9 @@ juce::var AppUpdater::performUpdateCheck() latestVersion, {}, feed.sha256, feed.notes, releasePageUrl, feed.releaseNotesUrl, feed.publishedAt, feed.expectedSize, feed.fileName, feedChannel, isMandatory, feed.installerArguments, feed.source); + juce::String downloadError; + if (!validateUpdateDownload(feed.downloadUrl, feed.sha256, feed.expectedSize, downloadError)) + return makeStatus("error", downloadError, latestVersion, {}, {}, {}, releasePageUrl); recordSuccessfulCheck(latestVersion, feed.publishedAt); if (compareVersions(latestVersion, getCurrentVersion()) <= 0) @@ -381,8 +435,21 @@ juce::var AppUpdater::performUpdateCheck() const auto trimmedUrl = feedUrl.trim(); if (trimmedUrl.isEmpty()) return makeStatus("error", "No update feed URL is configured.", {}, {}, {}, {}, {}, {}, {}, 0, {}, currentChannel); - - auto feedText = juce::URL(trimmedUrl).readEntireTextStream(false).trim(); + if (juce::URL(trimmedUrl).getScheme().toLowerCase() != "https") + return makeStatus("error", "The update feed must use HTTPS."); + + if (!MessageThreadLifetime::accepts(jobs.token())) return makeStatus("cancelled", "Update check cancelled during shutdown"); + auto stream = juce::URL(trimmedUrl).createInputStream(juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress) + .withConnectionTimeoutMs(5000).withNumRedirectsToFollow(5)); + juce::MemoryOutputStream feed; + char chunk[8192]; + while (stream && !stream->isExhausted() && MessageThreadLifetime::accepts(jobs.token()) && feed.getDataSize() < 2 * 1024 * 1024) + { + const auto bytes = stream->read(chunk, sizeof(chunk)); + if (bytes <= 0) break; + feed.write(chunk, static_cast(bytes)); + } + auto feedText = feed.getDataSize() >= 2 * 1024 * 1024 ? juce::String() : feed.toUTF8().trim(); if (feedText.isEmpty()) return makeStatus("error", "Could not reach the update server.", {}, {}, {}, {}, {}, {}, {}, 0, {}, currentChannel); @@ -410,77 +477,126 @@ juce::var AppUpdater::performUpdateCheck() lastFailure = appcastResult; } + // GitHub release metadata remains available if the website deployment is down. + if (currentChannel == "stable") + { + auto fallback = tryFeedUrl("https://github.com/sdevil7th/OpenStudio/releases/latest/download/OpenStudio-release-stable-latest.json", false); + if (getStringProperty(fallback, "status") != "error") return fallback; + } return lastFailure; } -juce::var AppUpdater::performDownloadAndInstall(const juce::String& downloadUrl, - const juce::String& version, - const juce::String& expectedSha256, - const juce::String& releasePageUrl, - const juce::String& installerArguments, - juce::int64 expectedSize) const +bool AppUpdater::validateUpdateDownload(const juce::String& downloadUrl, const juce::String& sha256, + juce::int64 size, juce::String& error) { - const auto currentChannel = getCurrentChannel(); + const juce::URL url(downloadUrl); + if (url.getScheme().toLowerCase() != "https" || url.getDomain().isEmpty()) + error = "The update download must use HTTPS."; + else if (sha256.length() != 64 || !sha256.containsOnly("0123456789abcdefABCDEF")) + error = "The update feed must include a valid SHA-256 checksum."; + else if (size <= 0 || size > 2LL * 1024 * 1024 * 1024) + error = "The update feed must include a valid download size."; + else + return true; + return false; +} - if (downloadUrl.trim().isEmpty()) +juce::var AppUpdater::performDownload(const juce::var& offer) +{ + if (WindowsPackage::isStoreManaged()) return makeStatus("error", "This installation uses Microsoft Store package updates."); + auto result = offer.isObject() ? offer.clone() : makeStatus("error", "Check for updates before downloading."); + auto finish = [&](const juce::String& status, const juce::String& message) + { + result.getDynamicObject()->setProperty("status", status); + result.getDynamicObject()->setProperty("message", message); + return result; + }; + if (!offer.isObject()) return result; + const auto downloadUrl = getStringProperty(offer, "downloadUrl"); + const auto version = getStringProperty(offer, "version"); + if (compareVersions(version, getCurrentVersion()) <= 0) + return finish("up-to-date", "OpenStudio is already up to date."); + const auto sha256 = getStringProperty(offer, "sha256"); + const auto size = getInt64Property(offer, "size"); + juce::String error; + if (!validateUpdateDownload(downloadUrl, sha256, size, error)) return finish("error", error); + const juce::URL url(downloadUrl); + const auto installerFile = getOpenStudioAppDataDirectory().getChildFile("updates") + .getChildFile(juce::Uuid().toString()).getChildFile(getDownloadFileName(url, version)); + if (installerFile.getParentDirectory().createDirectory().failed()) + return finish("error", "Could not create the update download folder."); + auto progress = [this, alive = jobs.token(), offer, size](juce::int64 bytes) { - if (releasePageUrl.isNotEmpty()) + auto status = offer.clone(); + auto* obj = status.getDynamicObject(); + obj->setProperty("status", "downloading"); + obj->setProperty("message", "Downloading update..."); + obj->setProperty("downloadedBytes", bytes); + obj->setProperty("progress", juce::jlimit(0.0, 1.0, static_cast(bytes) / static_cast(size))); + juce::MessageManager::callAsync([this, alive, status]() { - juce::URL(releasePageUrl).launchInDefaultBrowser(); - return makeStatus("release-page-opened", - "Opened the release page in your browser.", - version, {}, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, {}, currentChannel, false, installerArguments); - } - - return makeStatus("error", "No download URL was provided for this update.", version, {}, expectedSha256, {}, {}, {}, {}, expectedSize, {}, currentChannel, false, installerArguments); + if (MessageThreadLifetime::accepts(alive)) publishStatus(status); + }); + }; + if (!downloadToFile(url, installerFile, error, jobs.token(), &cancelRequested, progress) + || !verifyDownloadedFileSize(installerFile, size, error) + || !verifyDownloadedFileSha256(installerFile, sha256, error) + || cancelRequested.load() || !MessageThreadLifetime::accepts(jobs.token())) + { + installerFile.deleteFile(); + installerFile.getParentDirectory().deleteFile(); // Only the empty folder created for this download. + return finish(cancelRequested.load() ? "cancelled" : "error", cancelRequested.load() ? "Download cancelled. You can try again later." : error); } + { + const juce::ScopedLock lock(stateLock); + if (downloadedInstaller.existsAsFile()) downloadedInstaller.deleteFile(); + downloadedInstaller = installerFile; + downloadedUpdate = offer.clone(); + } + return finish("download-ready", "Update downloaded and verified. Install it when you are ready."); +} - const juce::URL url(downloadUrl); - const auto fileName = getDownloadFileName(url, version); - auto updateDir = juce::File::getSpecialLocation(juce::File::tempDirectory) - .getChildFile("OpenStudio") - .getChildFile("updates"); - updateDir.createDirectory(); - - const auto installerFile = updateDir.getChildFile(fileName); +juce::var AppUpdater::performInstall() +{ + if (WindowsPackage::isStoreManaged()) return makeStatus("error", "This installation uses Microsoft Store package updates."); + juce::File installer; + juce::var offer; + { + const juce::ScopedLock lock(stateLock); + installer = downloadedInstaller; + offer = downloadedUpdate.clone(); + } + if (!offer.isObject()) return makeStatus("error", "Download the update before installing it."); + if (compareVersions(getStringProperty(offer, "version"), getCurrentVersion()) <= 0) + return makeStatus("up-to-date", "OpenStudio is already up to date."); + auto result = offer.clone(); juce::String error; - - if (!downloadToFile(url, installerFile, error)) + if (!verifyDownloadedFileSize(installer, getInt64Property(offer, "size"), error) + || !verifyDownloadedFileSha256(installer, getStringProperty(offer, "sha256"), error)) { - if (releasePageUrl.isNotEmpty()) - juce::URL(releasePageUrl).launchInDefaultBrowser(); - - return makeStatus("error", - error.isNotEmpty() ? error : "The update download failed.", - version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, installerFile.getFileName(), currentChannel, false, installerArguments); + result.getDynamicObject()->setProperty("status", "error"); + result.getDynamicObject()->setProperty("message", error); + return result; } - - if (!verifyDownloadedFileSize(installerFile, expectedSize, error)) - return makeStatus("error", - error.isNotEmpty() ? error : "The downloaded update did not match the published size.", - version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, installerFile.getFileName(), currentChannel, false, installerArguments); - - if (!verifyDownloadedFileSha256(installerFile, expectedSha256, error)) - return makeStatus("error", - error.isNotEmpty() ? error : "The downloaded update did not match the published checksum.", - version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, installerFile.getFileName(), currentChannel, false, installerArguments); - - if (!launchDownloadedInstaller(installerFile, installerArguments, error)) - return makeStatus("error", - error.isNotEmpty() ? error : "The update downloaded, but the installer could not be opened.", - version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, installerFile.getFileName(), currentChannel, false, installerArguments); - - #if JUCE_MAC - const auto successMessage = "The update DMG has been opened. Drag OpenStudio to Applications, then use right-click > Open if macOS warns about an unidentified developer."; - #elif JUCE_WINDOWS - const auto successMessage = "The update installer has been opened."; + if (!MessageThreadLifetime::accepts(jobs.token())) return makeStatus("cancelled", "Update cancelled during shutdown."); + #if JUCE_LINUX + // Keep the verified AppImage in a stable user-writable location. The user + // chooses where their portable app lives; never replace a package-manager installation. + if (!installer.setExecutePermission(true)) error = "Could not make the downloaded AppImage executable."; + else installer.revealToUser(); + const auto successMessage = "The verified AppImage is ready in the opened folder. Replace your previous AppImage, then launch it."; #else - const auto successMessage = "The update package has been opened."; + // The Windows installer must not forcibly close a session that changed while + // verification was running. The frontend uses the normal saved-project quit path. + const auto arguments = getPlatformKey() == "windows" ? "/SP- /NOICONS /NOCLOSEAPPLICATIONS /NORESTARTAPPLICATIONS" : ""; + launchDownloadedInstaller(installer, arguments, error); + const auto successMessage = getPlatformKey() == "macos" + ? "The update DMG is open. Quit OpenStudio and drag the new app into Applications, then reopen it." + : "The installer is open. Follow its steps to update and reopen OpenStudio."; #endif - - return makeStatus("install-started", - successMessage, - version, downloadUrl, expectedSha256, {}, releasePageUrl, {}, {}, expectedSize, installerFile.getFileName(), currentChannel, false, installerArguments); + result.getDynamicObject()->setProperty("status", error.isEmpty() ? "install-started" : "error"); + result.getDynamicObject()->setProperty("message", error.isEmpty() ? successMessage : error); + return result; } void AppUpdater::publishStatus(const juce::var& status) @@ -530,6 +646,8 @@ juce::String AppUpdater::getPlatformKey() return "windows"; #elif JUCE_MAC return "macos"; + #elif JUCE_LINUX + return "linux"; #else return "unsupported"; #endif @@ -548,6 +666,8 @@ bool AppUpdater::shouldSkipAutomaticCheck() const { const juce::ScopedLock lock(stateLock); const auto lastSuccessfulCheckAtMs = getInt64Property(persistedState, "lastSuccessfulCheckAtMs", 0); + if (compareVersions(getStringProperty(persistedState, "lastSeenVersion"), getCurrentVersion()) > 0) + return false; // Re-offer an update after relaunch even if it was checked today. if (lastSuccessfulCheckAtMs <= 0) return false; @@ -627,7 +747,7 @@ juce::String AppUpdater::getDownloadFileName(const juce::URL& url, const juce::S candidate = candidate.upToFirstOccurrenceOf("?", false, false); if (candidate.isNotEmpty()) - return candidate; + return juce::File::createLegalFileName(candidate); #if JUCE_WINDOWS return "OpenStudio-" + version + "-Setup.exe"; @@ -638,14 +758,16 @@ juce::String AppUpdater::getDownloadFileName(const juce::URL& url, const juce::S #endif } -bool AppUpdater::downloadToFile(const juce::URL& url, const juce::File& targetFile, juce::String& error) +bool AppUpdater::downloadToFile(const juce::URL& url, const juce::File& targetFile, juce::String& error, + const MessageThreadLifetime::Token& alive, + const std::atomic* cancelled, + std::function progress) { - const auto tempFile = targetFile.getSiblingFile(targetFile.getFileName() + ".download"); - tempFile.deleteFile(); + juce::TemporaryFile temporary(targetFile); auto input = url.createInputStream( juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress) - .withConnectionTimeoutMs(15000) + .withConnectionTimeoutMs(5000) .withNumRedirectsToFollow(5)); if (input == nullptr) @@ -654,33 +776,40 @@ bool AppUpdater::downloadToFile(const juce::URL& url, const juce::File& targetFi return false; } - juce::FileOutputStream output(tempFile); - if (!output.openedOk()) + auto output = temporary.getFile().createOutputStream(); + if (!output) { error = "Could not create a temporary installer file."; return false; } - if (output.writeFromInputStream(*input, -1) <= 0) + const auto started = juce::Time::getMillisecondCounterHiRes(); + auto lastProgressAt = started - 250.0; + char chunk[65536]; + while (!input->isExhausted()) { - error = "The installer download was incomplete."; - tempFile.deleteFile(); - return false; + if (!MessageThreadLifetime::accepts(alive) || (cancelled != nullptr && cancelled->load()) + || juce::Time::getMillisecondCounterHiRes() - started > 600000) + { error = "Update download cancelled or timed out."; return false; } + const auto bytes = input->read(chunk, sizeof(chunk)); + if (bytes <= 0 || output->getPosition() + bytes > 2LL * 1024 * 1024 * 1024 + || !output->write(chunk, static_cast(bytes))) + { error = "Update download was incomplete, too large, or could not be written."; return false; } + const auto now = juce::Time::getMillisecondCounterHiRes(); + if (progress && now - lastProgressAt >= 150.0) + { + progress(output->getPosition()); + lastProgressAt = now; + } } - - output.flush(); - - if (targetFile.existsAsFile() && !targetFile.deleteFile()) - { - error = "Could not replace the previous downloaded installer."; - tempFile.deleteFile(); - return false; - } - - if (!tempFile.moveFileTo(targetFile)) + output->flush(); + if (output->getPosition() <= 0 || output->getStatus().failed() || !MessageThreadLifetime::accepts(alive) + || (cancelled != nullptr && cancelled->load())) + { error = "The installer download was not finalized."; return false; } + output.reset(); + if (!temporary.overwriteTargetFileWithTemporary()) { error = "Could not move the downloaded installer into place."; - tempFile.deleteFile(); return false; } @@ -690,7 +819,10 @@ bool AppUpdater::downloadToFile(const juce::URL& url, const juce::File& targetFi bool AppUpdater::verifyDownloadedFileSize(const juce::File& targetFile, juce::int64 expectedSize, juce::String& error) { if (expectedSize <= 0) - return true; + { + error = "The update is missing its published size."; + return false; + } if (!targetFile.existsAsFile()) { @@ -708,8 +840,11 @@ bool AppUpdater::verifyDownloadedFileSize(const juce::File& targetFile, juce::in bool AppUpdater::verifyDownloadedFileSha256(const juce::File& targetFile, const juce::String& expectedSha256, juce::String& error) { const auto normalizedExpected = expectedSha256.trim().toLowerCase(); - if (normalizedExpected.isEmpty()) - return true; + if (normalizedExpected.length() != 64 || !normalizedExpected.containsOnly("0123456789abcdef")) + { + error = "The update is missing a valid SHA-256 checksum."; + return false; + } if (!targetFile.existsAsFile()) { @@ -745,9 +880,6 @@ bool AppUpdater::launchDownloadedInstaller(const juce::File& installerFile, if (installerFile.startAsProcess(installerArguments.trim())) return true; - if (juce::URL(installerFile).launchInDefaultBrowser()) - return true; - error = "The update was downloaded, but OpenStudio could not open it automatically."; return false; } diff --git a/Source/AppUpdater.h b/Source/AppUpdater.h index e78a85a..c9ca813 100644 --- a/Source/AppUpdater.h +++ b/Source/AppUpdater.h @@ -3,6 +3,8 @@ #include #include #include +#include "OwnedBackgroundTasks.h" +#include "StoreUpdater.h" class AppUpdater { @@ -12,30 +14,27 @@ class AppUpdater AppUpdater(); ~AppUpdater(); + void shutdown(); juce::String getCurrentVersion() const; juce::var getLastStatus() const; void setStatusCallback(StatusCallback callback); void checkForUpdates(bool manual, Completion completion = {}); - void downloadAndInstallUpdate(const juce::String& downloadUrl, - const juce::String& version, - const juce::String& expectedSha256, - const juce::String& releasePageUrl, - const juce::String& installerArguments = {}, - juce::int64 expectedSize = 0, - Completion completion = {}); + void downloadUpdate(Completion completion = {}); + void cancelDownload(); + void installDownloadedUpdate(Completion completion = {}); private: + friend class RuntimeSafetyRegression; juce::var performUpdateCheck(); - juce::var performDownloadAndInstall(const juce::String& downloadUrl, - const juce::String& version, - const juce::String& expectedSha256, - const juce::String& releasePageUrl, - const juce::String& installerArguments, - juce::int64 expectedSize) const; + juce::var performDownload(const juce::var& offer); + juce::var performInstall(); + static bool validateUpdateDownload(const juce::String& url, const juce::String& sha256, + juce::int64 size, juce::String& error); void publishStatus(const juce::var& status); + bool rejectDevelopmentUpdate(const Completion& completion); bool shouldSkipAutomaticCheck() const; void recordSuccessfulCheck(const juce::String& latestVersion, const juce::String& publishedAt); void savePersistedState() const; @@ -48,7 +47,10 @@ class AppUpdater static int compareVersions(const juce::String& lhs, const juce::String& rhs); static juce::StringArray tokenizeVersion(const juce::String& version); static juce::String getDownloadFileName(const juce::URL& url, const juce::String& version); - static bool downloadToFile(const juce::URL& url, const juce::File& targetFile, juce::String& error); + static bool downloadToFile(const juce::URL& url, const juce::File& targetFile, juce::String& error, + const MessageThreadLifetime::Token& alive, + const std::atomic* cancelled = nullptr, + std::function progress = {}); static bool verifyDownloadedFileSize(const juce::File& targetFile, juce::int64 expectedSize, juce::String& error); static bool verifyDownloadedFileSha256(const juce::File& targetFile, const juce::String& expectedSha256, juce::String& error); static bool launchDownloadedInstaller(const juce::File& installerFile, @@ -73,9 +75,15 @@ class AppUpdater mutable juce::CriticalSection stateLock; juce::var lastStatus; juce::var persistedState; + juce::var availableUpdate; + juce::var downloadedUpdate; + juce::File downloadedInstaller; StatusCallback statusCallback; std::atomic checkInProgress { false }; std::atomic installInProgress { false }; + std::atomic cancelRequested { false }; + OwnedBackgroundTasks jobs; + std::unique_ptr storeUpdater; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AppUpdater) }; diff --git a/Source/ApplicationLaunchState.cpp b/Source/ApplicationLaunchState.cpp index 3ea62f0..182d1e1 100644 --- a/Source/ApplicationLaunchState.cpp +++ b/Source/ApplicationLaunchState.cpp @@ -21,7 +21,7 @@ bool isSupportedProjectPath(const juce::String& candidate) return false; const auto lower = trimmed.toLowerCase(); - return lower.endsWith(".osproj") || lower.endsWith(".s13"); + return lower.endsWith(".osproj"); } void addCandidate(juce::StringArray& candidates, const juce::String& candidate) diff --git a/Source/AudioEngine.cpp b/Source/AudioEngine.cpp index 31ba2a5..925dff5 100644 --- a/Source/AudioEngine.cpp +++ b/Source/AudioEngine.cpp @@ -1,11 +1,19 @@ #include "AudioEngine.h" +#include "MicrophoneAccess.h" +#include "AudioInputPolicy.h" +#include "RecordingDestination.h" +#include "AppPaths.h" +#include "OwnedChildProcess.h" +#include "IsolatedPlugin.h" #include "BuiltInParameterSupport.h" -#include "S13FXProcessor.h" +#include "JSFXProcessor.h" #include "BuiltInEffects.h" #include "BuiltInEffects2.h" #include "NAMModelSafety.h" +#include "PluginStateValidation.h" +#include "SendGraphSafety.h" #include "NAMDelayRegression.h" -#include "S13PitchCorrector.h" +#include "OpenStudioPitchCorrector.h" #include "PitchAnalyzer.h" #include "PitchResynthesizer.h" #include "CrashDiagnostics.h" @@ -234,57 +242,13 @@ struct PitchPhraseRenderRegion bool expandedToFullClip = true; }; -juce::File getOpenStudioDocumentsDirectory() -{ - auto documentsDir = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory); - return documentsDir.getChildFile("OpenStudio"); -} - -juce::File getLegacyStudio13DocumentsDirectory() -{ - auto documentsDir = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory); - return documentsDir.getChildFile("Studio13"); -} - -juce::File getOpenStudioApplicationDataDirectory() -{ - auto appDataDir = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory); - return appDataDir.getChildFile("OpenStudio"); -} - -juce::File getLegacyStudio13ApplicationDataDirectory() -{ - auto appDataDir = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory); - return appDataDir.getChildFile("Studio13"); -} - -juce::File getPreferredAppDataDirectory() -{ - auto openStudioDir = getOpenStudioDocumentsDirectory(); - auto legacyDir = getLegacyStudio13DocumentsDirectory(); - - if (!openStudioDir.exists() && legacyDir.exists()) - return legacyDir; - - return openStudioDir; -} - +juce::File getPreferredAppDataDirectory() { return AppPaths::documents(); } +juce::File getPreferredApplicationDataDirectory() { return AppPaths::applicationData(); } juce::File getApplicationRuntimeDirectory() { return juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory(); } -juce::File getPreferredApplicationDataDirectory() -{ - auto openStudioDir = getOpenStudioApplicationDataDirectory(); - auto legacyDir = getLegacyStudio13ApplicationDataDirectory(); - - if (!openStudioDir.exists() && legacyDir.exists()) - return legacyDir; - - return openStudioDir; -} - int midiNoteFromFrequencyHz (float frequencyHz) { if (frequencyHz <= 0.0f) @@ -2669,7 +2633,7 @@ PitchCompositeStats compositeDryProtectedPitchPatch (juce::AudioBuffer& d // Debug logging — always active for FX diagnostics static void logToDisk(const juce::String& msg) { - auto f = getPreferredAppDataDirectory().getChildFile("debug_log.txt"); + auto f = AppPaths::diagnostics().getChildFile("debug_log.txt"); f.getParentDirectory().createDirectory(); f.appendText(juce::Time::getCurrentTime().toString(true, true) + ": " + msg + "\n"); } @@ -3385,6 +3349,17 @@ AudioEngine::AudioEngine() AudioEngine::~AudioEngine() { + // Stop/join external producers while every field they use is still alive. + if (midiManager) + { + midiManager->closeAllDevices(); + midiManager->setMessageCallback({}); + } + controlSurfaceManager.getMIDIControl().disconnect(); + controlSurfaceManager.getMCUControl().disconnect(); + controlSurfaceManager.getOSCControl().disconnect(); + controlSurfaceManager.setCallback(nullptr); + deferredCallbacks.invalidate(); deviceManager.removeAudioCallback (this); realtimeTrackSnapshotForAudio.store( nullptr, std::memory_order_seq_cst); @@ -3400,6 +3375,25 @@ AudioEngine::~AudioEngine() tunerPitchTracker.setEnabled(false); } +void AudioEngine::reportStageProcessorFaults() +{ + const auto report = [](const std::shared_ptr& stage, const char* chain) + { + if (!stage) return; + for (size_t index = 0; index < stage->slots.size(); ++index) + { + const auto& slot = stage->slots[index]; + if (slot.safety->reportPending.exchange(false, std::memory_order_acq_rel)) + OpenStudioCrashDiagnostics::recordBreadcrumb("processor_fault", + juce::String("chain=") + chain + " slot=" + juce::String(static_cast(index)) + + " reason=" + juce::String(static_cast(slot.safety->lastFailure.load())) + + " fault latched; explicit bypass/re-enable or reload resets it"); + } + }; + report(std::atomic_load(&realtimeMasterFXSnapshot), "master"); + report(std::atomic_load(&realtimeMonitoringFXSnapshot), "monitoring"); +} + juce::String AudioEngine::serialiseProcessorStateToBase64(juce::AudioProcessor* processor) const { if (processor == nullptr) @@ -3407,7 +3401,7 @@ juce::String AudioEngine::serialiseProcessorStateToBase64(juce::AudioProcessor* juce::MemoryBlock stateData; if (auto* rack = - dynamic_cast(processor)) + dynamic_cast(processor)) { // NAM Rack state is composed from atomics plus its model/Cab/UI // publication locks, none of which are acquired by processBlock(). @@ -3429,13 +3423,14 @@ bool AudioEngine::applyBase64StateToProcessor(juce::AudioProcessor* processor, c return processor != nullptr; juce::MemoryBlock stateData; - if (!stateData.fromBase64Encoding(base64State)) + if (!PluginStateValidation::decode(base64State, stateData)) return false; - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) return rack->restoreProjectStateInformation(stateData.getData(), static_cast(stateData.getSize())); processor->setStateInformation(stateData.getData(), static_cast(stateData.getSize())); + if (auto* isolated = dynamic_cast(processor)) return isolated->isHealthy(); return true; } @@ -3506,15 +3501,15 @@ std::unique_ptr AudioEngine::createProcessorForStageSlot(c { processor = createBuiltInEffect(slot.name); } - else if (slot.type == "s13fx") + else if (slot.type == "jsfx") { - auto s13fx = std::make_unique(); - if (!s13fx->loadScript(slot.pluginPath)) + auto jsfx = std::make_unique(); + if (!jsfx->loadScript(slot.pluginPath)) { - errorMessage = "Failed to load S13FX script: " + slot.pluginPath; + errorMessage = "Failed to load JSFX script: " + slot.pluginPath; return nullptr; } - processor = std::move(s13fx); + processor = std::move(jsfx); } else { @@ -3528,7 +3523,7 @@ std::unique_ptr AudioEngine::createProcessorForStageSlot(c return nullptr; } - if (auto* rack = dynamic_cast(processor.get())) + if (auto* rack = dynamic_cast(processor.get())) { rack->setEmbeddedDriveOversamplingFactor( getNAMRackOversamplingFactor()); @@ -3578,6 +3573,8 @@ std::shared_ptr AudioEngine::buildActiveFXStage(cons activeSlot.forceFloat = desiredSlot.forceFloat; activeSlot.supportsDouble = processor->supportsDoublePrecisionProcessing(); activeSlot.processor = std::shared_ptr(std::move(processor)); + if (auto* isolated = dynamic_cast(activeSlot.processor.get())) + activeSlot.safety->remoteFailure = isolated->faultFlag(); activeSlot.bypassDelay = std::make_shared(); activeSlot.bypassDelay->processor = @@ -3627,8 +3624,6 @@ void AudioEngine::rebindStageEditors(const std::shared_ptr& struct EditorRebindRequest { juce::AudioProcessor* oldProcessor = nullptr; - juce::AudioProcessor* newProcessor = nullptr; - juce::String title; int slotId = 0; }; @@ -3640,15 +3635,7 @@ void AudioEngine::rebindStageEditors(const std::shared_ptr& EditorRebindRequest request; request.oldProcessor = oldSlot.processor.get(); - if (const auto* newSlot = findActiveStageSlot(newStage, oldSlot.slotId)) - { - if (newSlot->processor) - { - request.newProcessor = newSlot->processor.get(); - request.title = newSlot->name; - request.slotId = newSlot->slotId; - } - } + request.slotId = oldSlot.slotId; requests.push_back(std::move(request)); } @@ -3658,13 +3645,19 @@ void AudioEngine::rebindStageEditors(const std::shared_ptr& // Capture oldStage by value to keep old processors alive until the async lambda runs. // Without this, raw AudioProcessor* pointers in 'requests' could dangle if the old // stage's shared_ptr drops before the lambda executes. - juce::MessageManager::callAsync([this, requests = std::move(requests), monitoringStage, + juce::ignoreUnused(newStage); // Resolve the latest publication when the UI task actually runs. + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), requests = std::move(requests), monitoringStage, keepAlive = oldStage]() mutable { + if (!MessageThreadLifetime::accepts(lifetime)) return; + const auto currentStage = monitoringStage + ? std::atomic_load(&realtimeMonitoringFXSnapshot) + : std::atomic_load(&realtimeMasterFXSnapshot); for (const auto& request : requests) { pluginWindowManager.closeEditor(request.oldProcessor); - if (request.newProcessor != nullptr) + const auto* currentSlot = findActiveStageSlot(currentStage, request.slotId); + if (currentSlot != nullptr && currentSlot->processor != nullptr) { PluginWindowManager::PluginEditorTarget target; target.scope = monitoringStage @@ -3680,7 +3673,7 @@ void AudioEngine::rebindStageEditors(const std::shared_ptr& break; } } - pluginWindowManager.openEditor(request.newProcessor, request.title, target); + pluginWindowManager.openEditor(currentSlot->processor, currentSlot->name, target); } } }); @@ -3883,11 +3876,12 @@ void AudioEngine::rebuildRealtimeProcessingSnapshots() tracks, sidechainOutputBuffers, sendAccumBuffers); + trackSnapshot.processingOrder.resize(tracks.size()); buildSidechainProcessingOrder( tracks, trackSnapshot.processingOrder.data(), trackSnapshot.processingOrderCount, - maxRealtimeTracks); + static_cast(tracks.size())); publishRealtimeTrackSnapshot( std::make_shared( @@ -3904,10 +3898,10 @@ AudioEngine::getPublishedBuiltInProcessor( if (fxIndex < 0) return owner; - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { const auto stage = std::atomic_load_explicit( - &realtimeMasterFXSnapshot, std::memory_order_acquire); + &(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (stage != nullptr && fxIndex < static_cast(stage->slots.size())) { @@ -3970,7 +3964,7 @@ bool AudioEngine::setNAMRackOversamplingFactor(int factor) namRackOversamplingFactor.store(factor, std::memory_order_release); const auto applyToProcessor = [factor] (juce::AudioProcessor* processor) { - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) rack->setEmbeddedDriveOversamplingFactor(factor); }; @@ -4016,6 +4010,8 @@ void AudioEngine::saveDeviceSettings() xml->setAttribute( "namRackOversamplingFactor", getNAMRackOversamplingFactor()); + if (!isMicrophonePermissionGrantedForInput() && inputPermissionSavedSettings != nullptr) + AudioInputPolicy::preserveInputPreference(*xml, *inputPermissionSavedSettings); auto settingsFile = getDeviceSettingsFile(); settingsFile.getParentDirectory().createDirectory(); xml->writeTo(settingsFile); @@ -4025,32 +4021,8 @@ void AudioEngine::saveDeviceSettings() void AudioEngine::loadDeviceSettings() { - #if JUCE_MAC - if (! isMicrophonePermissionGrantedForInput()) - { - juce::Logger::writeToLog("AudioEngine: macOS microphone permission is not granted; opening output-only audio first."); - loadDeviceSettingsWithChannelCounts(0, 2); - requestMicrophonePermissionIfNeeded([this] (bool granted) - { - juce::MessageManager::callAsync([this, granted] - { - if (granted) - { - juce::Logger::writeToLog("AudioEngine: macOS microphone permission granted; reopening audio inputs."); - loadDeviceSettingsWithChannelCounts(2, 2); - saveDeviceSettings(); - } - else - { - juce::Logger::writeToLog("AudioEngine: macOS microphone permission denied; keeping output-only audio configuration."); - } - }); - }); - return; - } - #endif - - loadDeviceSettingsWithChannelCounts(2, 2); + // Checking authorization never prompts. Request only when hardware input is used. + loadDeviceSettingsWithChannelCounts(isMicrophonePermissionGrantedForInput() ? 2 : 0, 2); } void AudioEngine::applyRoutedDeviceChannelPolicy( @@ -4195,6 +4167,15 @@ void AudioEngine::loadDeviceSettingsWithChannelCounts(int inputChannels, int out ? restoredOversamplingFactor : 4, std::memory_order_release); + if (requestedInputChannels == 0) + { + inputPermissionSavedSettings = std::make_unique(*xml); + AudioInputPolicy::suppressInput(*xml); + } + else + { + inputPermissionSavedSettings.reset(); + } auto error = deviceManager.initialise(requestedInputChannels, requestedOutputChannels, xml.get(), true); if (error.isEmpty()) { @@ -4216,27 +4197,44 @@ void AudioEngine::loadDeviceSettingsWithChannelCounts(int inputChannels, int out bool AudioEngine::isMicrophonePermissionGrantedForInput() const { - #if JUCE_MAC - if (juce::RuntimePermissions::isRequired(juce::RuntimePermissions::recordAudio)) - return juce::RuntimePermissions::isGranted(juce::RuntimePermissions::recordAudio); - #endif - - return true; + return MicrophoneAccess::status() == MicrophoneAccess::Status::authorized; } void AudioEngine::requestMicrophonePermissionIfNeeded(std::function completion) { - #if JUCE_MAC - if (juce::RuntimePermissions::isRequired(juce::RuntimePermissions::recordAudio) - && ! juce::RuntimePermissions::isGranted(juce::RuntimePermissions::recordAudio)) - { - juce::Logger::writeToLog("AudioEngine: requesting macOS microphone permission."); - juce::RuntimePermissions::request(juce::RuntimePermissions::recordAudio, std::move(completion)); - return; - } - #endif + MicrophoneAccess::request(std::move(completion)); +} + +bool AudioEngine::trackRequiresAudioInput(const juce::String& trackId) const +{ + const auto found = trackMap.find(trackId); + if (found == trackMap.end() || found->second == nullptr) return false; + const auto type = found->second->getTrackType(); + return type != TrackType::MIDI && type != TrackType::Instrument; +} - completion(true); +bool AudioEngine::recordingRequiresAudioInput() const +{ + for (const auto& [id, track] : trackMap) + if (track != nullptr && track->getRecordArmed() && trackRequiresAudioInput(id)) return true; + return false; +} + +void AudioEngine::requestAudioInputAccess(std::function completion) +{ + requestMicrophonePermissionIfNeeded([this, lifetime = deferredCallbacks.token(), completion = std::move(completion)](bool granted) mutable + { + if (!MessageThreadLifetime::accepts(lifetime)) return; + if (granted) + { + const auto* device = deviceManager.getCurrentAudioDevice(); + if (device == nullptr || device->getActiveInputChannels().isZero()) + loadDeviceSettingsWithChannelCounts(2, 2); + } + refreshAudioDeviceSetupSnapshot(deviceManager.getCurrentAudioDevice()); + const auto* inputDevice = deviceManager.getCurrentAudioDevice(); + if (completion) completion(granted && inputDevice != nullptr && !inputDevice->getActiveInputChannels().isZero()); + }); } void AudioEngine::resetAudioCallbackWindowTelemetry() noexcept @@ -4465,6 +4463,7 @@ void AudioEngine::audioDeviceAboutToStart (juce::AudioIODevice* device) previousAudioCallbackArrivalWallTimeMs.store( 0.0, std::memory_order_relaxed); currentSampleRate = device->getCurrentSampleRate(); + transportSampleRate.store(currentSampleRate, std::memory_order_release); currentBlockSize = device->getCurrentBufferSizeSamples(); inputLatencySamples = device->getInputLatencyInSamples(); // Query driver capabilities once, before streaming begins. UI callers read @@ -4577,7 +4576,9 @@ void AudioEngine::audioDeviceAboutToStart (juce::AudioIODevice* device) 1.0, device->getCurrentSampleRate()) * continuityRampSeconds)); - reusableRealtimeMidiBuffer.ensureSize(8192); + reusableRealtimeMidiBuffer.ensureSize(8192); + reusableMasterMidiBuffer.ensureSize(8192); + reusableMonitoringMidiBuffer.ensureSize(8192); for (auto& continuity : masterFXContinuity) continuity = {}; for (auto& continuity : monitoringFXContinuity) @@ -4651,6 +4652,7 @@ void AudioEngine::audioDeviceAboutToStart (juce::AudioIODevice* device) void AudioEngine::audioDeviceStopped() { + metronome.setPracticePlaybackAvailable(false); audioDeviceStopCount.fetch_add(1, std::memory_order_relaxed); lastAudioDeviceStopWallTimeMs.store( juce::Time::getMillisecondCounterHiRes(), @@ -5150,7 +5152,8 @@ void AudioEngine::processMasterFXChain (const ActiveFXStage* rtMasterFX, masterBuffer.copyFrom (ch, 0, outputChannelData[ch], numSamples); } - juce::MidiBuffer dummyMidi; + auto& dummyMidi = reusableMasterMidiBuffer; + dummyMidi.clear(); for (size_t slotIndex = 0; slotIndex < rtMasterFX->slots.size(); ++slotIndex) @@ -5350,9 +5353,14 @@ void AudioEngine::processMasterFXChain (const ActiveFXStage* rtMasterFX, } juce::ScopedTryLock pluginProcessLock(proc->getCallbackLock()); - if (!pluginProcessLock.isLocked()) - { - masterFXBusySkipCount.fetch_add(1, std::memory_order_relaxed); + if (!pluginProcessLock.isLocked() + || (slot.safety->refreshRemoteFailure(), slot.safety->failure.load(std::memory_order_acquire) != ProcessorSafety::Failure::none) + || proc->getTotalNumInputChannels() > masterChans + || proc->getTotalNumOutputChannels() > masterChans) + { + if (!pluginProcessLock.isLocked()) masterFXBusySkipCount.fetch_add(1, std::memory_order_relaxed); + else if (proc->getTotalNumInputChannels() > masterChans || proc->getTotalNumOutputChannels() > masterChans) + slot.safety->markFailure(ProcessorSafety::Failure::bufferContract); if (! dryInputAvailable && slot.bypassDelay != nullptr) { @@ -5475,7 +5483,8 @@ void AudioEngine::processMasterFXChain (const ActiveFXStage* rtMasterFX, if (canProcessDouble) { - proc->processBlock (masterBufferDouble, dummyMidi); + if (!slot.safety->process(*proc, masterBufferDouble, dummyMidi) && continuity) + continuity->skippedLastBlock = true; } else { @@ -5490,7 +5499,8 @@ void AudioEngine::processMasterFXChain (const ActiveFXStage* rtMasterFX, } } - proc->processBlock (masterBuffer, dummyMidi); + if (!slot.safety->process(*proc, masterBuffer, dummyMidi) && continuity) + continuity->skippedLastBlock = true; if (useHybrid64Summing) { @@ -5631,7 +5641,8 @@ void AudioEngine::processMonitoringFXChain (const ActiveFXStage* rtMonitoringFX, } }; - juce::MidiBuffer dummyMidi; + auto& dummyMidi = reusableMonitoringMidiBuffer; + dummyMidi.clear(); for (size_t slotIndex = 0; slotIndex < rtMonitoringFX->slots.size(); ++slotIndex) @@ -5799,9 +5810,14 @@ void AudioEngine::processMonitoringFXChain (const ActiveFXStage* rtMonitoringFX, } juce::ScopedTryLock pluginProcessLock(proc->getCallbackLock()); - if (!pluginProcessLock.isLocked()) - { - monitoringFXBusySkipCount.fetch_add(1, std::memory_order_relaxed); + if (!pluginProcessLock.isLocked() + || (slot.safety->refreshRemoteFailure(), slot.safety->failure.load(std::memory_order_acquire) != ProcessorSafety::Failure::none) + || proc->getTotalNumInputChannels() > monChans + || proc->getTotalNumOutputChannels() > monChans) + { + if (!pluginProcessLock.isLocked()) monitoringFXBusySkipCount.fetch_add(1, std::memory_order_relaxed); + else if (proc->getTotalNumInputChannels() > monChans || proc->getTotalNumOutputChannels() > monChans) + slot.safety->markFailure(ProcessorSafety::Failure::bufferContract); if (! dryInputAvailable && slot.bypassDelay != nullptr) { @@ -5891,14 +5907,16 @@ void AudioEngine::processMonitoringFXChain (const ActiveFXStage* rtMonitoringFX, if (canProcessDouble) { - proc->processBlock (monBufferDouble, dummyMidi); + if (!slot.safety->process(*proc, monBufferDouble, dummyMidi) && continuity) + continuity->skippedLastBlock = true; } else { if (hybrid64PostChainActive) copyDoubleBufferToFloatBuffer (monBufferDouble, monBuffer, monChans, numSamples); - proc->processBlock (monBuffer, dummyMidi); + if (!slot.safety->process(*proc, monBuffer, dummyMidi) && continuity) + continuity->skippedLastBlock = true; if (hybrid64PostChainActive) copyFloatBufferToDoubleBuffer (monBuffer, monBufferDouble, monChans, numSamples); @@ -6061,10 +6079,11 @@ void AudioEngine::applyMasterGainPanMono (float* const* outputChannelData, void AudioEngine::buildSidechainProcessingOrder (const std::vector& rtTracks, int processedOrder[], int& orderCount, int maxTracks) { - std::array trackProcessed {}; const int numTracks = juce::jmin( static_cast(rtTracks.size()), - juce::jmin(maxTracks, maxRealtimeTracks)); + juce::jmax(0, maxTracks)); + // Called only while constructing a new immutable routing publication. + std::vector trackProcessed(static_cast(numTracks), false); orderCount = 0; // Sidechains and sends both require each source to run before its @@ -6201,6 +6220,7 @@ void AudioEngine::audioDeviceIOCallbackWithContext (const float* const* inputCha lastCallbackInputChannels.store(numInputChannels, std::memory_order_relaxed); lastCallbackOutputChannels.store(numOutputChannels, std::memory_order_relaxed); lastActiveOutputChannels.store(numOutputChannels, std::memory_order_relaxed); + lastAudioBlockSamples.store(numSamples, std::memory_order_relaxed); const double callbackBlockDurationMs = currentSampleRate > 0.0 ? (static_cast(numSamples) @@ -6450,14 +6470,19 @@ void AudioEngine::audioDeviceIOCallbackWithContext (const float* const* inputCha / currentSampleRate); metronome.setBpm(callbackTempo); - // Mix Metronome (if enabled and transport is running) - if (isPlaying || isRecordMode) + // One sample-clock generator: practice never changes track/record/playhead + // state. Transport takes over its phase when Play or Record is triggered. { - juce::AudioBuffer outputBuffer(const_cast(outputChannelData), numOutputChannels, numSamples); - metronome.getNextAudioBlock( + // The click only addresses L/R. A wider AudioBuffer pointer view can + // allocate its channel table on devices exposing 32+ output channels. + juce::AudioBuffer outputBuffer(outputChannelData, juce::jmin(2, numOutputChannels), numSamples); + metronome.getNextTransportBlock( outputBuffer, static_cast( - blockStartSamplePosition)); + blockStartSamplePosition), + // Record mode can be armed before the transport advances. A parked + // record latch must not repeatedly restart the click at one position. + isPlaying.load(std::memory_order_relaxed)); } if (shouldSampleAudioCallbackStages) { @@ -7283,6 +7308,7 @@ juce::String AudioEngine::addTrack(const juce::String& explicitId, const juce::S if (trackNode) { juce::String trackId = explicitId.isNotEmpty() ? explicitId : juce::Uuid().toString(); + rawTrackPtr->setDiagnosticsTrackId(trackId); trackMap[trackId] = rawTrackPtr; trackNodeMap[trackId] = trackNode; @@ -7379,6 +7405,11 @@ bool AudioEngine::removeTrack(const juce::String& trackId) invalidatePluginABStatesForTrack(trackId); } + if (onFXSlotsRemoved) + { + onFXSlotsRemoved(trackId, "input", 0); + onFXSlotsRemoved(trackId, "track", 0); + } recalculatePDC(); return true; @@ -7707,7 +7738,8 @@ bool AudioEngine::sendMidiNote(const juce::String& trackId, int note, int veloci auto* track = it->second; const int channel = track->getMIDIChannel() > 0 ? track->getMIDIChannel() : 1; int sampleOffset = 0; - if (currentSampleRate > 0.0 && currentBlockSize > 0) + const int midiBlockSamples = lastAudioBlockSamples.load(std::memory_order_relaxed); + if (midiBlockSamples > 0) { const double blockStartMs = lastAudioBlockWallTimeMs.load(std::memory_order_acquire); const double blockDurationMs = lastAudioBlockDurationMs.load(std::memory_order_acquire); @@ -7724,8 +7756,8 @@ bool AudioEngine::sendMidiNote(const juce::String& trackId, int note, int veloci sampleOffset = juce::jlimit( 0, - juce::jmax(0, currentBlockSize - 1), - static_cast(std::round((elapsedMs / 1000.0) * currentSampleRate))); + midiBlockSamples - 1, + static_cast(std::round((elapsedMs / blockDurationMs) * midiBlockSamples))); midiLastComputedSampleOffset.store(sampleOffset, std::memory_order_relaxed); } } @@ -7895,7 +7927,7 @@ bool AudioEngine::setInstrumentState(const juce::String& trackId, const juce::St return false; juce::MemoryBlock stateData; - if (!stateData.fromBase64Encoding(base64State)) + if (!PluginStateValidation::decode(base64State, stateData)) return false; { @@ -7904,6 +7936,7 @@ bool AudioEngine::setInstrumentState(const juce::String& trackId, const juce::St } juce::Logger::writeToLog("AudioEngine: Restored instrument state for track " + trackId); + if (auto* isolated = dynamic_cast(processor)) return isolated->isHealthy(); return true; } @@ -7978,8 +8011,9 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, if (!updates.isEmpty()) { midiMappedParameterUpdateCount.fetch_add(updates.size(), std::memory_order_relaxed); - juce::MessageManager::callAsync([this, updates]() + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), updates]() { + if (!MessageThreadLifetime::accepts(lifetime)) return; for (const auto& update : updates) { auto it = trackMap.find(update.trackId); @@ -8010,7 +8044,8 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, } int sampleOffset = 0; - if (currentSampleRate > 0.0 && currentBlockSize > 0) + const int midiBlockSamples = lastAudioBlockSamples.load(std::memory_order_relaxed); + if (midiBlockSamples > 0) { const double blockStartMs = lastAudioBlockWallTimeMs.load(std::memory_order_acquire); const double blockDurationMs = lastAudioBlockDurationMs.load(std::memory_order_acquire); @@ -8027,16 +8062,21 @@ void AudioEngine::handleMIDIMessage(const juce::String& deviceName, int channel, sampleOffset = juce::jlimit( 0, - juce::jmax(0, currentBlockSize - 1), - static_cast(std::round((elapsedMs / 1000.0) * currentSampleRate))); + midiBlockSamples - 1, + static_cast(std::round((elapsedMs / blockDurationMs) * midiBlockSamples))); midiLastComputedSampleOffset.store(sampleOffset, std::memory_order_relaxed); } } - // Route MIDI to appropriate tracks + // MIDI device callbacks must not walk the message-thread-owned trackMap. + // Retain nodes through an immutable snapshot until this producer returns. int fanoutCount = 0; - for (auto const& [id, track] : trackMap) + const auto routing = std::atomic_load_explicit(&realtimeTrackSnapshot, std::memory_order_acquire); + if (!routing) return; + for (const auto& entry : routing->tracks) { + const auto& id = entry.id; + auto* track = entry.processor; if (!track) continue; // Check if track accepts MIDI @@ -8145,19 +8185,27 @@ void AudioEngine::resetMeterClip(const juce::String& trackId) juce::var AudioEngine::getAudioDeviceSetup() { + juce::var snapshot; { const juce::ScopedLock lock(audioDeviceSetupSnapshotLock); - if (! audioDeviceSetupSnapshot.isVoid()) - return audioDeviceSetupSnapshot; + snapshot = audioDeviceSetupSnapshot; } - - // This path is only expected before the first device start. Do not query a - // possibly-streaming device here; aboutToStart publishes the full snapshot. - auto fallbackSnapshot = buildAudioDeviceSetupSnapshot(nullptr); - const juce::ScopedLock lock(audioDeviceSetupSnapshotLock); - if (audioDeviceSetupSnapshot.isVoid()) - audioDeviceSetupSnapshot = fallbackSnapshot; - return audioDeviceSetupSnapshot; + // Do not enumerate or query a streaming device from a UI polling request. + if (snapshot.isVoid()) snapshot = buildAudioDeviceSetupSnapshot(nullptr); + #if JUCE_MAC + // Consent can change in System Settings without a device restart. Overlay + // current authorization on a copy, leaving the shared device snapshot intact. + snapshot = snapshot.clone(); + if (auto* current = snapshot.getProperty("current", {}).getDynamicObject()) + { + const auto status = MicrophoneAccess::status(); + const bool granted = status == MicrophoneAccess::Status::authorized; + current->setProperty("microphonePermissionGranted", granted); + current->setProperty("microphonePermissionRequired", !granted); + current->setProperty("microphonePermissionStatus", MicrophoneAccess::statusName()); + } + #endif + return snapshot; } void AudioEngine::refreshAudioDeviceSetupSnapshot( @@ -8180,15 +8228,18 @@ juce::var AudioEngine::buildAudioDeviceSetupSnapshot( currentSetup->setProperty("audioDeviceType", currentDeviceType); currentSetup->setProperty("microphonePermissionGranted", microphonePermissionGranted); + currentSetup->setProperty("microphonePermissionStatus", MicrophoneAccess::statusName()); #if JUCE_MAC - currentSetup->setProperty("microphonePermissionRequired", juce::RuntimePermissions::isRequired(juce::RuntimePermissions::recordAudio)); + currentSetup->setProperty("microphonePermissionRequired", !microphonePermissionGranted); #else currentSetup->setProperty("microphonePermissionRequired", false); #endif if (device) { - currentSetup->setProperty("inputDevice", device->getName()); - currentSetup->setProperty("outputDevice", device->getName()); // Basic assumption for now + juce::AudioDeviceManager::AudioDeviceSetup actualSetup; + deviceManager.getAudioDeviceSetup(actualSetup); + currentSetup->setProperty("inputDevice", actualSetup.inputDeviceName); + currentSetup->setProperty("outputDevice", actualSetup.outputDeviceName); currentSetup->setProperty("sampleRate", device->getCurrentSampleRate()); currentSetup->setProperty("bufferSize", device->getCurrentBufferSizeSamples()); @@ -8404,11 +8455,12 @@ void AudioEngine::setAudioDeviceSetup( { juce::Logger::writeToLog("AudioEngine: audio input setup requested before macOS microphone permission was granted."); requestMicrophonePermissionIfNeeded( - [this, type, input, output, sampleRate, bufferSize, completion = std::move(completion)] (bool granted) mutable + [this, lifetime = deferredCallbacks.token(), type, input, output, sampleRate, bufferSize, completion = std::move(completion)] (bool granted) mutable { juce::MessageManager::callAsync( - [this, type, input, output, sampleRate, bufferSize, granted, completion = std::move(completion)] () mutable + [this, lifetime, type, input, output, sampleRate, bufferSize, granted, completion = std::move(completion)] () mutable { + if (!MessageThreadLifetime::accepts(lifetime)) return; juce::String errorMessage; bool applied = false; if (granted) @@ -8418,7 +8470,7 @@ void AudioEngine::setAudioDeviceSetup( } else { - errorMessage = "Microphone permission was denied."; + errorMessage = "Audio input access is unavailable. Enable OpenStudio in System Settings > Privacy & Security > Microphone. Playback and MIDI remain available."; juce::Logger::writeToLog("AudioEngine: macOS microphone permission denied; input device setup was not applied."); } @@ -8450,7 +8502,18 @@ bool AudioEngine::applyAudioDeviceSetup( // 1. Change Type if needed if (deviceManager.getCurrentAudioDeviceType() != type) { - deviceManager.setCurrentAudioDeviceType(type, true); + if (!isMicrophonePermissionGrantedForInput()) + { + // Switching type normally restores JUCE's last per-type input mask. + // Install an output-only configuration before that device can open. + juce::XmlElement effective("DEVICESETUP"); + effective.setAttribute("deviceType", type); + effective.setAttribute("audioOutputDeviceName", output); + AudioInputPolicy::suppressInput(effective); + errorMessage = deviceManager.initialise(0, 2, &effective, false); + if (errorMessage.isNotEmpty()) return false; + } + else deviceManager.setCurrentAudioDeviceType(type, true); } // 2. Setup Config @@ -8518,8 +8581,24 @@ void AudioEngine::setTrackRecordArm(const juce::String& trackId, bool armed) } } +void AudioEngine::setTrackInputMonitoringAsync(const juce::String& trackId, bool enabled, std::function completion) +{ + const auto generation = ++monitorRequestGenerations[trackId]; + auto apply = [this, trackId, enabled, generation, completion = std::move(completion)](bool allowed) + { + const bool accepted = allowed && monitorRequestGenerations[trackId] == generation + && trackMap.find(trackId) != trackMap.end(); + if (accepted) setTrackInputMonitoring(trackId, enabled); + if (completion) completion(accepted); + }; + if (enabled && trackRequiresAudioInput(trackId)) requestAudioInputAccess(std::move(apply)); + else apply(true); +} + void AudioEngine::setTrackInputMonitoring(const juce::String& trackId, bool enabled) { + ++monitorRequestGenerations[trackId]; + if (enabled && trackRequiresAudioInput(trackId) && !isMicrophonePermissionGrantedForInput()) return; if (trackMap.find(trackId) != trackMap.end()) { auto* track = trackMap[trackId]; @@ -9029,8 +9108,22 @@ bool AudioEngine::hasAnyActiveARA() const return false; } +void AudioEngine::setTransportRecordingAsync(bool recording, std::function completion) +{ + const auto generation = ++recordingRequestGeneration; + auto apply = [this, recording, generation, completion = std::move(completion)](bool allowed) + { + const bool accepted = allowed && recordingRequestGeneration == generation; + if (accepted) setTransportRecording(recording); + if (completion) completion(accepted && isTransportRecording() == recording); + }; + if (recording && recordingRequiresAudioInput()) requestAudioInputAccess(std::move(apply)); + else apply(true); +} + void AudioEngine::setTransportRecording(bool recording) { + ++recordingRequestGeneration; logToDisk("setTransportRecording(" + juce::String(recording ? "true" : "false") + ") called. Current: " + juce::String(isRecordMode ? "true" : "false")); logAudioRecord("setTransportRecording recording=" + juce::String(recording ? "true" : "false") + " current=" + juce::String(isRecordMode.load() ? "true" : "false")); @@ -9038,24 +9131,28 @@ void AudioEngine::setTransportRecording(bool recording) if (isRecordMode == recording) return; // No change - isRecordMode = recording; - + if (recording && recordingRequiresAudioInput() && !isMicrophonePermissionGrantedForInput()) + { + juce::Logger::writeToLog("Recording requires microphone authorization; no recording state was changed."); + return; + } if (recording) { + const auto destination = getPreferredAppDataDirectory().getChildFile("Audio"); + const auto ready = prepareRecordingDirectory(destination); + if (ready.failed()) + { + juce::Logger::writeToLog("Recording did not start: " + ready.getErrorMessage()); + return; + } loopTakeCounter = 0; // Reset loop take counter for new recording session lastCompletedClips.clear(); lastCompletedMIDIClips.clear(); - projectAudioFolder = getPreferredAppDataDirectory().getChildFile("Audio"); + projectAudioFolder = destination; logToDisk("Target Audio Folder: " + projectAudioFolder.getFullPathName()); - if (!projectAudioFolder.exists()) - { - bool created = projectAudioFolder.createDirectory(); - logToDisk("Folder created status: " + juce::String(created ? "SUCCESS" : "FAIL")); - } - // Start recording for each armed track logToDisk("Checking " + juce::String((int)trackMap.size()) + " tracks for arming..."); juce::StringArray armedTrackIds; @@ -9125,9 +9222,11 @@ void AudioEngine::setTransportRecording(bool recording) } logAudioRecord("record start armedTracks=" + armedTrackIds.joinIntoString(",") + " anyStarted=" + juce::String(anyAudioStarted ? "true" : "false")); + isRecordMode = anyAudioStarted; } else { + isRecordMode = false; // Stop all recordings and collect clip info logToDisk("Record mode OFF - stopping recordings"); auto completedAudioTakes = audioRecorder.stopAllRecordings(currentSampleRate); @@ -9206,6 +9305,10 @@ juce::var AudioEngine::getAvailablePlugins() pluginObj->setProperty("identifier", plugin.createIdentifierString()); pluginObj->setProperty("isInstrument", plugin.isInstrument); pluginObj->setProperty("hasARA", plugin.hasARAExtension); + pluginObj->setProperty("isolatedHosting", pluginManager.usesIsolatedHosting(plugin)); + pluginObj->setProperty("isolationAvailable", JUCE_WINDOWS != 0); + pluginObj->setProperty("isolationLatencySamples", 2 * juce::jmax(512, currentBlockSize)); + pluginObj->setProperty("isolationLatencyMs", 2000.0 * juce::jmax(512, currentBlockSize) / juce::jmax(8000.0, currentSampleRate)); pluginObj->setProperty("pluginFormatName", plugin.pluginFormatName); pluginObj->setProperty("pluginFormat", plugin.pluginFormatName.toLowerCase()); pluginObj->setProperty("numInputChannels", plugin.numInputChannels); @@ -9500,7 +9603,7 @@ juce::var AudioEngine::runEngineBenchmarks() if (backup.processor) { if (auto* rack = - dynamic_cast( + dynamic_cast( backup.processor)) { // Engine benchmarking mutates preparation/runtime history, not @@ -9887,7 +9990,7 @@ juce::var AudioEngine::runCleanGuitarPitchBendRegression() int pitchWheelValue, float stringMode) -> GuitarProbe { - S13CleanGuitarInstrument guitar; + OpenStudioCleanGuitarInstrument guitar; guitar.model.store(0.0f); guitar.tone.store(0.22f); guitar.body.store(0.62f); @@ -10199,7 +10302,7 @@ juce::var AudioEngine::runNAMRackRegression() return writer->writeFromAudioSampleBuffer(ir, 0, ir.getNumSamples()); }; - auto configureRack = [] (S13NAMRack& rack) + auto configureRack = [] (OpenStudioNAMRack& rack) { rack.inputTrimDb.store(0.0f); rack.setRoutedInputChannelCount(1); @@ -10252,10 +10355,6 @@ juce::var AudioEngine::runNAMRackRegression() rack.cabHPFHz.store(75.0f); rack.cabLPFHz.store(6800.0f); rack.cabPhaseInvert.store(0.0f); - rack.cabMicPosition.store(0.5f); - rack.cabMicDistance.store(0.0f); - rack.cabMicBlend.store(0.5f); - rack.cabRoomSend.store(0.0f); rack.cabPan.store(0.0f); rack.eqEnabled.store(1.0f); rack.chorusMix.store(0.18f); @@ -10301,7 +10400,7 @@ juce::var AudioEngine::runNAMRackRegression() } }; - auto renderRack = [&] (S13NAMRack& rack, + auto renderRack = [&] (OpenStudioNAMRack& rack, bool compareDry, float inputScale = 1.0f) { @@ -10369,7 +10468,7 @@ juce::var AudioEngine::runNAMRackRegression() return probe; }; - auto renderRackCapture = [&] (S13NAMRack& rack) + auto renderRackCapture = [&] (OpenStudioNAMRack& rack) { const int totalBlocks = static_cast(std::ceil(totalDurationSec * fixtureSampleRate / static_cast(fixtureBlockSize))); juce::AudioBuffer captured(2, totalBlocks * fixtureBlockSize); @@ -11762,7 +11861,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto irFile = makeTempFile("openstudio_cab_ir_fixture", ".wav"); const auto alternateIRFile = makeTempFile("openstudio_cab_ir_swap_fixture", ".wav"); - auto configureNeutralRack = [] (S13NAMRack& rack) + auto configureNeutralRack = [] (OpenStudioNAMRack& rack) { rack.inputTrimDb.store(0.0f); rack.setRoutedInputChannelCount(1); @@ -11859,7 +11958,7 @@ juce::var AudioEngine::runNAMRackRegression() "A selected mono hardware input must be added to both processing channels after the raw recorder tap without replacing existing stereo clip/send content or modifying the source buffer.", monoHardwareInputCenteringProbe); - S13NAMRack neutralRack; + OpenStudioNAMRack neutralRack; neutralRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(neutralRack); const auto neutralRackProbe = renderRack(neutralRack, true); @@ -11875,7 +11974,7 @@ juce::var AudioEngine::runNAMRackRegression() // Exercise the rack-owned pre-trim and final-output meters with deliberately // unequal channels, then prove a mono source hides and clears its R input // lane even though the host processing buffer remains stereo. - S13NAMRack channelMeterRack; + OpenStudioNAMRack channelMeterRack; channelMeterRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(channelMeterRack); channelMeterRack.setRoutedInputChannelCount(2); @@ -12004,7 +12103,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::var(channelMeterValue)); constexpr float highLevelNeutralInputScale = 16.0f; - S13NAMRack highLevelNeutralRack; + OpenStudioNAMRack highLevelNeutralRack; highLevelNeutralRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(highLevelNeutralRack); const auto highLevelNeutralRackProbe = renderRack( @@ -12022,15 +12121,42 @@ juce::var AudioEngine::runNAMRackRegression() highLevelNeutralRackDiagnostics.getProperty( "rackOutputSafetyGuardHitCount", static_cast(0))); + const auto highLevelEmergencyMuteTrips = + static_cast( + highLevelNeutralRackDiagnostics.getProperty( + "rackOutputEmergencyMuteTripCount", + static_cast(0))); + const bool highLevelEmergencyMuteRecovered = + ! static_cast( + highLevelNeutralRackDiagnostics.getProperty( + "rackOutputEmergencyMuteActive", true)); + const auto postEmergencyRecoveryProbe = renderRack( + highLevelNeutralRack, false, 1.0f); + const auto postEmergencyRecoveryDiagnostics = + highLevelNeutralRack.getDiagnosticState(); + const auto postEmergencyTripCount = + static_cast( + postEmergencyRecoveryDiagnostics.getProperty( + "rackOutputEmergencyMuteTripCount", + static_cast(0))); const bool highLevelNeutralRackProtected = highLevelNeutralRackProbe.nonFiniteCount == 0 && highLevelNeutralRawPeak - > S13NAMRack::namOutputSafetyKnee + > OpenStudioNAMRack::namOutputSafetyKnee && highLevelNeutralGuardHits > 0 + && highLevelEmergencyMuteTrips == 0 + && highLevelEmergencyMuteRecovered + && highLevelNeutralRackProbe.peak > 1.0e-5f && highLevelNeutralRackProbe.peak - < S13NAMRack::namOutputSafetyCeiling + < OpenStudioNAMRack::namOutputSafetyCeiling && highLevelNeutralRackProbe.maxDiffFromDry - > 1.0e-6f; + > 1.0e-6f + && postEmergencyRecoveryProbe.nonFiniteCount == 0 + && postEmergencyRecoveryProbe.peak > 1.0e-5f + && postEmergencyRecoveryProbe.peak + < OpenStudioNAMRack::namOutputSafetyKnee + && postEmergencyTripCount + == highLevelEmergencyMuteTrips; auto* highLevelNeutralValue = new juce::DynamicObject(); highLevelNeutralValue->setProperty( @@ -12042,13 +12168,22 @@ juce::var AudioEngine::runNAMRackRegression() highLevelNeutralValue->setProperty( "rackOutputSafetyGuardHitCount", highLevelNeutralGuardHits); + highLevelNeutralValue->setProperty( + "emergencyMuteTripCount", + highLevelEmergencyMuteTrips); + highLevelNeutralValue->setProperty( + "emergencyMuteRecovered", + highLevelEmergencyMuteRecovered); + highLevelNeutralValue->setProperty( + "postRecoveryProbe", + makeProbeVar(postEmergencyRecoveryProbe)); addCheck(checks, "high_level_neutral_rack_final_output_guard", highLevelNeutralRackProtected ? "pass" : "fail", - "The single final NAM Rack guard must contain an intentionally over-hot rack output while the separate below-knee neutral probe remains sample-transparent.", + "The final NAM Rack guard must contain an intentionally over-hot but finite rack output without misclassifying ordinary gain staging as numerical runaway, entering a timed mute, or silencing the following below-knee signal.", juce::var(highLevelNeutralValue)); - S13NAMRack compressorImpulseRack; + OpenStudioNAMRack compressorImpulseRack; compressorImpulseRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(compressorImpulseRack); compressorImpulseRack.compressorEnabled.store(1.0f); @@ -12128,7 +12263,7 @@ juce::var AudioEngine::runNAMRackRegression() for (const float visibleMix : mixValues) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(2); rack.delayEnabled.store(visibleMix > 0.0f ? 1.0f : 0.0f); @@ -12193,7 +12328,7 @@ juce::var AudioEngine::runNAMRackRegression() cases.add(juce::var(caseValue)); } - S13Delay standaloneDelay; + OpenStudioDelay standaloneDelay; constexpr float standaloneMix = 0.80f; standaloneDelay.mix.store(standaloneMix); standaloneDelay.delayTimeL.store(2000.0f); @@ -12240,7 +12375,7 @@ juce::var AudioEngine::runNAMRackRegression() rackDelayDirectGainContract.getProperty("pass", false) ? "pass" : "fail", - "At several visible Mix positions the post-Cab Rack Delay must apply its explicit equal-power dry gain before any repeat can arrive, while standalone S13Delay retains its historical 1-Mix direct law.", + "At several visible Mix positions the post-Cab Rack Delay must apply its explicit equal-power dry gain before any repeat can arrive, while standalone OpenStudioDelay retains its historical 1-Mix direct law.", rackDelayDirectGainContract); auto runCompressorCurrentOutputAndDrainProbe = [&] () @@ -12260,7 +12395,7 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto configureCompressor = [] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, bool enabled, float volumeDb) { @@ -12313,7 +12448,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto render = [&] (int effectsVersion, float volumeDb) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.namEffectsDspVersion.store(effectsVersion); configureCompressor(rack, true, volumeDb); @@ -12362,13 +12497,13 @@ juce::var AudioEngine::runNAMRackRegression() const auto legacyV1 = render(1, 6.0f); const auto legacyV2 = render(2, 6.0f); const auto currentMinus = render( - S13NAMRack::currentNAMEffectsDspVersion, -6.0f); + OpenStudioNAMRack::currentNAMEffectsDspVersion, -6.0f); const auto currentZero = render( - S13NAMRack::currentNAMEffectsDspVersion, 0.0f); + OpenStudioNAMRack::currentNAMEffectsDspVersion, 0.0f); const auto currentPlus = render( - S13NAMRack::currentNAMEffectsDspVersion, 6.0f); + OpenStudioNAMRack::currentNAMEffectsDspVersion, 6.0f); const auto currentPlusEighteen = render( - S13NAMRack::currentNAMEffectsDspVersion, 18.0f); + OpenStudioNAMRack::currentNAMEffectsDspVersion, 18.0f); float plusScalingError = 0.0f; float minusScalingError = 0.0f; @@ -12415,8 +12550,8 @@ juce::var AudioEngine::runNAMRackRegression() maxDiffBetweenBuffers( legacyV2.capture, currentPlus.capture); - S13NAMRack previouslyExcited; - S13NAMRack cleanReference; + OpenStudioNAMRack previouslyExcited; + OpenStudioNAMRack cleanReference; configureNeutralRack(previouslyExcited); configureNeutralRack(cleanReference); previouslyExcited.namEffectsDspVersion.store(3); @@ -12495,7 +12630,7 @@ juce::var AudioEngine::runNAMRackRegression() lifecycleBlocks * fixtureBlockSize); result.capture.clear(); - S13NAMRack lifecycleRack; + OpenStudioNAMRack lifecycleRack; configureNeutralRack(lifecycleRack); lifecycleRack.namEffectsDspVersion.store( effectsVersion); @@ -12579,9 +12714,9 @@ juce::var AudioEngine::runNAMRackRegression() + legacyLifecycleNonFiniteCount; const bool pass = nonFiniteCount == 0 - && currentPlus.peak < S13NAMRack::namOutputSafetyKnee + && currentPlus.peak < OpenStudioNAMRack::namOutputSafetyKnee && currentPlusEighteen.peak - < S13NAMRack::namOutputSafetyKnee + < OpenStudioNAMRack::namOutputSafetyKnee && plusScalingError <= 2.0e-5f && minusScalingError <= 2.0e-5f && plusEighteenScalingError <= 2.0e-5f @@ -12658,7 +12793,7 @@ juce::var AudioEngine::runNAMRackRegression() bool bassStimulus, bool unevenPartition) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.compressorEnabled.store(1.0f); rack.compressorComp.store(0.86f); @@ -12788,7 +12923,7 @@ juce::var AudioEngine::runNAMRackRegression() float intensity, float sidechainMode) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.compressorEnabled.store(1.0f); rack.compressorComp.store(comp); @@ -12832,7 +12967,7 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto renderDetectorDc = [&] (float sidechainMode) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.compressorEnabled.store(1.0f); rack.compressorComp.store(0.86f); @@ -12886,7 +13021,7 @@ juce::var AudioEngine::runNAMRackRegression() bool unevenPartition, bool silentInput) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.compressorEnabled.store(1.0f); rack.compressorComp.store(0.86f); @@ -13223,7 +13358,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int totalSamples = 8192; const auto configurePedalParameters = [] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, PedalKind kind) { switch (kind) @@ -13251,7 +13386,7 @@ juce::var AudioEngine::runNAMRackRegression() } }; const auto setPedalActive = [] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, PedalKind kind, bool active) { @@ -13317,7 +13452,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto& pattern, int inputLayout) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); configurePedalParameters(rack, kind); rack.prepareToPlay(sampleRate, 1024); @@ -13465,8 +13600,8 @@ juce::var AudioEngine::runNAMRackRegression() constexpr double lifecycleSampleRate = 48000.0; for (const auto& pedalCase : pedalCases) { - S13NAMRack activeRack; - S13NAMRack referenceRack; + OpenStudioNAMRack activeRack; + OpenStudioNAMRack referenceRack; configureNeutralRack(activeRack); configureNeutralRack(referenceRack); activeRack.namEffectsDspVersion.store(3); @@ -13652,8 +13787,8 @@ juce::var AudioEngine::runNAMRackRegression() auto runRackReverbSpilloverProbe = [&] () { - S13NAMRack unityDryRack; - S13NAMRack unityDryReferenceRack; + OpenStudioNAMRack unityDryRack; + OpenStudioNAMRack unityDryReferenceRack; configureNeutralRack(unityDryRack); configureNeutralRack(unityDryReferenceRack); unityDryRack.reverbEnabled.store(1.0f); @@ -13728,8 +13863,8 @@ juce::var AudioEngine::runNAMRackRegression() const bool unityDryMixLawPass = maximumUnityDryError <= 1.0e-6f; - S13NAMRack spillRack; - S13NAMRack referenceRack; + OpenStudioNAMRack spillRack; + OpenStudioNAMRack referenceRack; spillRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); referenceRack.prepareToPlay( @@ -13893,7 +14028,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr int totalSamples = 8192; auto configureOctaver = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, float direct, float down, float up) @@ -13944,7 +14079,7 @@ juce::var AudioEngine::runNAMRackRegression() octaverInputSample(1, absoluteSample)); } }; - auto renderOctaver = [&] (S13NAMRack& rack) + auto renderOctaver = [&] (OpenStudioNAMRack& rack) { juce::AudioBuffer capture(2, totalSamples); capture.clear(); @@ -13966,9 +14101,9 @@ juce::var AudioEngine::runNAMRackRegression() return capture; }; - S13NAMRack directOneRack; - S13NAMRack directZeroRack; - S13NAMRack directBoostRack; + OpenStudioNAMRack directOneRack; + OpenStudioNAMRack directZeroRack; + OpenStudioNAMRack directBoostRack; configureOctaver(directOneRack, 1.0f, 0.52f, 0.31f); configureOctaver(directZeroRack, 0.0f, 0.52f, 0.31f); configureOctaver(directBoostRack, 1.25f, 0.0f, 0.0f); @@ -14045,7 +14180,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr float sourceFrequency = 110.0f; auto renderOctaver = [&] (bool antiPhase) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.octaverEnabled.store(1.0f); rack.octaverDirectMix.store(0.0f); @@ -14170,10 +14305,10 @@ juce::var AudioEngine::runNAMRackRegression() constexpr std::array unevenPattern { 127, 509, 64, 1000, 31, 257, 733 }; - auto configureChorus = [] (S13Chorus& chorus) + auto configureChorus = [] (OpenStudioChorus& chorus) { chorus.mode.store( - static_cast(S13Chorus::Mode::Flanger)); + static_cast(OpenStudioChorus::Mode::Flanger)); chorus.rate.store(9.5f); chorus.depth.store(0.82f); chorus.fbAmount.store(0.63f); @@ -14181,7 +14316,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus.voices.store(3.0f); chorus.lfoShape.store( static_cast( - S13Chorus::LFOShape::SampleAndHold)); + OpenStudioChorus::LFOShape::SampleAndHold)); chorus.spread.store(0.67f); chorus.highCut.store(7200.0f); chorus.lowCut.store(110.0f); @@ -14189,7 +14324,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus.characterMode.store(1.0f); }; auto renderChorus = [&] ( - S13Chorus& chorus, + OpenStudioChorus& chorus, const auto& pattern) { juce::AudioBuffer capture(2, totalSamples); @@ -14239,14 +14374,14 @@ juce::var AudioEngine::runNAMRackRegression() return capture; }; - S13Chorus fixedChorus; + OpenStudioChorus fixedChorus; configureChorus(fixedChorus); fixedChorus.prepareToPlay( fixtureSampleRate, 1024); const auto fixedCapture = renderChorus(fixedChorus, fixedPattern); - S13Chorus unevenChorus; + OpenStudioChorus unevenChorus; configureChorus(unevenChorus); unevenChorus.prepareToPlay( fixtureSampleRate, 1024); @@ -14295,11 +14430,11 @@ juce::var AudioEngine::runNAMRackRegression() 127, 509, 64, 1000, 31, 257, 733 }; auto configureEnsemble = [] ( - S13Chorus& chorus, + OpenStudioChorus& chorus, float stereoSpread) { chorus.mode.store( - static_cast(S13Chorus::Mode::Chorus)); + static_cast(OpenStudioChorus::Mode::Chorus)); chorus.rate.store(0.92f); chorus.depth.store(0.68f); chorus.fbAmount.store(0.18f); @@ -14307,7 +14442,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus.voices.store(5.0f); chorus.lfoShape.store( static_cast( - S13Chorus::LFOShape::Sine)); + OpenStudioChorus::LFOShape::Sine)); chorus.spread.store(stereoSpread); chorus.highCut.store(12000.0f); chorus.lowCut.store(90.0f); @@ -14316,7 +14451,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus.randomBlend.store(0.0f); }; auto renderEnsemble = [&] ( - S13Chorus& chorus, + OpenStudioChorus& chorus, const auto& pattern, int channelCount) { @@ -14372,7 +14507,7 @@ juce::var AudioEngine::runNAMRackRegression() return capture; }; - S13Chorus fixedChorus; + OpenStudioChorus fixedChorus; configureEnsemble(fixedChorus, 0.72f); fixedChorus.prepareToPlay( fixtureSampleRate, 1024); @@ -14380,7 +14515,7 @@ juce::var AudioEngine::runNAMRackRegression() renderEnsemble( fixedChorus, fixedPattern, 2); - S13Chorus unevenChorus; + OpenStudioChorus unevenChorus; configureEnsemble(unevenChorus, 0.72f); unevenChorus.prepareToPlay( fixtureSampleRate, 1024); @@ -14395,7 +14530,7 @@ juce::var AudioEngine::runNAMRackRegression() renderEnsemble( fixedChorus, fixedPattern, 2); - S13Chorus narrowStereoChorus; + OpenStudioChorus narrowStereoChorus; configureEnsemble(narrowStereoChorus, 0.0f); narrowStereoChorus.prepareToPlay( fixtureSampleRate, 1024); @@ -14403,7 +14538,7 @@ juce::var AudioEngine::runNAMRackRegression() renderEnsemble( narrowStereoChorus, fixedPattern, 2); - S13Chorus monoChorus; + OpenStudioChorus monoChorus; configureEnsemble(monoChorus, 0.0f); monoChorus.prepareToPlay( fixtureSampleRate, 1024); @@ -14511,10 +14646,10 @@ juce::var AudioEngine::runNAMRackRegression() float frequency, bool impulse) { - S13Chorus chorus; + OpenStudioChorus chorus; chorus.mode.store( static_cast( - S13Chorus::Mode::Chorus)); + OpenStudioChorus::Mode::Chorus)); chorus.rate.store(0.73f); chorus.depth.store(0.61f); chorus.fbAmount.store(0.12f); @@ -14522,7 +14657,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus.voices.store(5.0f); chorus.lfoShape.store( static_cast( - S13Chorus::LFOShape::Sine)); + OpenStudioChorus::LFOShape::Sine)); chorus.spread.store(0.68f); chorus.highCut.store(12000.0f); chorus.lowCut.store(80.0f); @@ -14724,7 +14859,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto render = [&] (float visibleMix) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(2); rack.modulatorEnabled.store(1.0f); @@ -14961,10 +15096,10 @@ juce::var AudioEngine::runNAMRackRegression() const auto render = [&] (int character) { - S13Chorus chorus; + OpenStudioChorus chorus; chorus.mode.store( static_cast( - S13Chorus::Mode::Chorus)); + OpenStudioChorus::Mode::Chorus)); chorus.rate.store(0.01f); chorus.depth.store(1.0f); chorus.fbAmount.store(0.0f); @@ -14972,7 +15107,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus.voices.store(6.0f); chorus.lfoShape.store( static_cast( - S13Chorus::LFOShape::Sine)); + OpenStudioChorus::LFOShape::Sine)); chorus.spread.store(0.72f); chorus.highCut.store(16000.0f); chorus.lowCut.store(40.0f); @@ -15114,7 +15249,7 @@ juce::var AudioEngine::runNAMRackRegression() int effectsDspVersion, int character) { - S13NAMRack rackProbe; + OpenStudioNAMRack rackProbe; configureNeutralRack(rackProbe); rackProbe.namEffectsDspVersion.store( effectsDspVersion, @@ -15222,7 +15357,7 @@ juce::var AudioEngine::runNAMRackRegression() const int observationSamples = observationBlocks * fixtureBlockSize; auto configureModulator = [&] ( - S13NAMRack& rack) + OpenStudioNAMRack& rack) { configureNeutralRack(rack); rack.modulatorEnabled.store(1.0f); @@ -15237,7 +15372,7 @@ juce::var AudioEngine::runNAMRackRegression() rack.modulatorPedalPosition.store(0.50f); }; auto prepareRack = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, bool withModulator) { if (withModulator) @@ -15248,7 +15383,7 @@ juce::var AudioEngine::runNAMRackRegression() fixtureSampleRate, fixtureBlockSize); }; auto warmSilence = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, juce::MidiBuffer& midi) { for (int blockIndex = 0; @@ -15262,8 +15397,8 @@ juce::var AudioEngine::runNAMRackRegression() } }; - S13NAMRack markerRack; - S13NAMRack silenceRack; + OpenStudioNAMRack markerRack; + OpenStudioNAMRack silenceRack; prepareRack(markerRack, true); prepareRack(silenceRack, true); juce::MidiBuffer midi; @@ -15359,8 +15494,8 @@ juce::var AudioEngine::runNAMRackRegression() } } - S13NAMRack dryRack; - S13NAMRack dryReferenceRack; + OpenStudioNAMRack dryRack; + OpenStudioNAMRack dryReferenceRack; prepareRack(dryRack, true); prepareRack(dryReferenceRack, false); warmSilence(dryRack, midi); @@ -15425,7 +15560,7 @@ juce::var AudioEngine::runNAMRackRegression() absoluteSample += fixtureBlockSize; } - S13NAMRack staleRack; + OpenStudioNAMRack staleRack; prepareRack(staleRack, true); warmSilence(staleRack, midi); juce::AudioBuffer excitation( @@ -15507,7 +15642,7 @@ juce::var AudioEngine::runNAMRackRegression() auto renderRandomAmount = [&] ( float randomAmount) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.modulatorEnabled.store(1.0f); rack.chorusMix.store(0.78f); @@ -15623,7 +15758,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr int totalSamples = 8192; auto configureDrive = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, float drive) { configureNeutralRack(rack); @@ -15634,7 +15769,7 @@ juce::var AudioEngine::runNAMRackRegression() rack.precisionDriveGate.store(0.0f); rack.precisionDriveVolumeDb.store(0.0f); }; - auto renderDrive = [&] (S13NAMRack& rack) + auto renderDrive = [&] (OpenStudioNAMRack& rack) { juce::AudioBuffer capture(2, totalSamples); capture.clear(); @@ -15678,8 +15813,8 @@ juce::var AudioEngine::runNAMRackRegression() return capture; }; - S13NAMRack belowBoundary; - S13NAMRack aboveBoundary; + OpenStudioNAMRack belowBoundary; + OpenStudioNAMRack aboveBoundary; configureDrive(belowBoundary, 0.6799f); configureDrive(aboveBoundary, 0.6801f); belowBoundary.prepareToPlay( @@ -15726,7 +15861,7 @@ juce::var AudioEngine::runNAMRackRegression() auto renderAttack = [&] (float attack) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.precisionDriveEnabled.store(1.0f); rack.precisionDriveDrive.store(1.0f); @@ -15903,7 +16038,7 @@ juce::var AudioEngine::runNAMRackRegression() result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.namEffectsDspVersion.store( recalledVersion, std::memory_order_relaxed); @@ -16083,7 +16218,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool casePass = capture.nonFiniteCount == 0 && capture.outputGuardHits == 0 - && capture.peak < S13NAMRack::namOutputSafetyKnee + && capture.peak < OpenStudioNAMRack::namOutputSafetyKnee && std::abs(capture.mean) < 0.002 && capture.residualToFundamental >= minimumResidualRatios[index] @@ -16281,7 +16416,7 @@ juce::var AudioEngine::runNAMRackRegression() factorValue->setProperty("pass", factorPass); multirateValues.add(juce::var(factorValue)); } - S13NAMRack defaultRack; + OpenStudioNAMRack defaultRack; const bool defaultVolumeIsPlusNine = std::abs( defaultRack.precisionDriveVolumeDb.load( @@ -16301,7 +16436,7 @@ juce::var AudioEngine::runNAMRackRegression() && levelPlusNine.nonFiniteCount == 0 && std::abs(levelPlusNine.mean) < 0.002 && levelPlusNine.outputGuardHits == 0 - && levelPlusNine.peak < S13NAMRack::namOutputSafetyKnee; + && levelPlusNine.peak < OpenStudioNAMRack::namOutputSafetyKnee; const bool realtimeInvariantPass = partitionError <= 2.0e-5f && versionMarkerError <= 1.0e-7f @@ -16414,7 +16549,7 @@ juce::var AudioEngine::runNAMRackRegression() auto renderDrive = [&] (float driveAmount) { GuardedDistortionCapture result; - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.chaosEnabled.store(1.0f); rack.chaosDrive.store(driveAmount); @@ -16530,7 +16665,7 @@ juce::var AudioEngine::runNAMRackRegression() / static_cast(measuredSamples)) : 0.0f; - S13NAMRack stateSource; + OpenStudioNAMRack stateSource; configureNeutralRack(stateSource); stateSource.chaosEnabled.store(1.0f); stateSource.chaosDrive.store(0.73f); @@ -16540,7 +16675,7 @@ juce::var AudioEngine::runNAMRackRegression() stateSource.chaosLevelDb.store(-1.5f); juce::MemoryBlock state; stateSource.getStateInformation(state); - S13NAMRack stateRestored; + OpenStudioNAMRack stateRestored; stateRestored.setStateInformation( state.getData(), static_cast(state.getSize())); @@ -16580,7 +16715,7 @@ juce::var AudioEngine::runNAMRackRegression() && highDriveCapture.outputGuardHits == 0 && differenceRms > 0.005f && highDriveRms > 0.005f - && highDrivePeak < S13NAMRack::namOutputSafetyKnee + && highDrivePeak < OpenStudioNAMRack::namOutputSafetyKnee && std::abs(highDriveMean) < 0.05f && stateRoundTrips); return juce::var(value); @@ -16630,7 +16765,7 @@ juce::var AudioEngine::runNAMRackRegression() Render result; result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.precisionDriveEnabled.store(distortion ? 0.0f : 1.0f); rack.precisionDriveDrive.store(drive); @@ -16878,7 +17013,7 @@ juce::var AudioEngine::runNAMRackRegression() && silentRightPeak <= 1.0e-7f && silencePeak <= 1.0e-7f && std::abs(current.mean) < 0.01f - && current.peak < S13NAMRack::namOutputSafetyKnee + && current.peak < OpenStudioNAMRack::namOutputSafetyKnee && outputGuardHits == 0 && precision.latency == current.latency; auto* value = new juce::DynamicObject(); @@ -16942,7 +17077,7 @@ juce::var AudioEngine::runNAMRackRegression() const int totalSamples = static_cast( std::round(sampleRate * 0.25)); juce::AudioBuffer capture(1, totalSamples); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setEmbeddedDriveOversamplingFactor(factor); rack.chaosEnabled.store(1.0f); @@ -17135,7 +17270,7 @@ juce::var AudioEngine::runNAMRackRegression() const int totalSamples = static_cast( std::round(sampleRate * 0.25)); juce::AudioBuffer capture(1, totalSamples); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setEmbeddedDriveOversamplingFactor(factor); rack.chaosEnabled.store(1.0f); @@ -17315,7 +17450,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::Decibels::decibelsToGain( static_cast(inputPeakDbfs), -300.0f); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setEmbeddedDriveOversamplingFactor(4); rack.chaosEnabled.store(1.0f); @@ -17448,7 +17583,7 @@ juce::var AudioEngine::runNAMRackRegression() const int totalSamples = static_cast( std::round(sampleRate * 0.5)); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.precisionDriveEnabled.store(1.0f); rack.precisionDriveDrive.store( @@ -17893,7 +18028,7 @@ juce::var AudioEngine::runNAMRackRegression() { AliasRender result; result.audio.resize(source.size(), 0.0f); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setEmbeddedDriveOversamplingFactor(oversamplingFactor); rack.precisionDriveEnabled.store(distortion ? 0.0f : 1.0f); @@ -18499,7 +18634,7 @@ juce::var AudioEngine::runNAMRackRegression() { for (int profile = 0; profile < 2; ++profile) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(2); rack.setEmbeddedDriveOversamplingFactor(factor); @@ -18561,7 +18696,7 @@ juce::var AudioEngine::runNAMRackRegression() const float drive = calibrationDrives[driveIndex]; const float scale = - S13NAMRack::resolveChaosSmallSignalScale( + OpenStudioNAMRack::resolveChaosSmallSignalScale( static_cast(mode), drive); const double envelopeBoundDb = scale < 1.0f ? 32.0 @@ -18813,9 +18948,9 @@ juce::var AudioEngine::runNAMRackRegression() } const float heavyDefaultScale = - S13NAMRack::resolveChaosSmallSignalScale(0.0f, 0.62f); + OpenStudioNAMRack::resolveChaosSmallSignalScale(0.0f, 0.62f); const float crunchDefaultScale = - S13NAMRack::resolveChaosSmallSignalScale(2.0f, 0.62f); + OpenStudioNAMRack::resolveChaosSmallSignalScale(2.0f, 0.62f); const double heavyCollapsedDefaultRawGainDb = maximumRawByModeDrive[0][10]; const double crunchCollapsedDefaultRawGainDb = @@ -18840,10 +18975,10 @@ juce::var AudioEngine::runNAMRackRegression() const float input = 0.25f * static_cast(index) / 1024.0f; const float positive = - S13NAMRack::applyChaosSmallSignalNormalizer( + OpenStudioNAMRack::applyChaosSmallSignalNormalizer( input, scale); const float negative = - S13NAMRack::applyChaosSmallSignalNormalizer( + OpenStudioNAMRack::applyChaosSmallSignalNormalizer( -input, scale); maximumOddError = juce::jmax( maximumOddError, @@ -18860,7 +18995,7 @@ juce::var AudioEngine::runNAMRackRegression() { mapperSaturatedIdentityExact = mapperSaturatedIdentityExact - && S13NAMRack::applyChaosSmallSignalNormalizer( + && OpenStudioNAMRack::applyChaosSmallSignalNormalizer( saturated, scale) == saturated; } } @@ -19052,7 +19187,7 @@ juce::var AudioEngine::runNAMRackRegression() result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(2); rack.setEmbeddedDriveOversamplingFactor(factor); @@ -19332,8 +19467,8 @@ juce::var AudioEngine::runNAMRackRegression() // renders do not enter that fallback, so fixed/uneven partition parity // alone cannot prove skip() agrees with one getNext() per sample at // the exact release-hold boundary. - S13NAMRack perSampleNormalizer; - S13NAMRack bulkNormalizer; + OpenStudioNAMRack perSampleNormalizer; + OpenStudioNAMRack bulkNormalizer; constexpr double skipParityEmbeddedRate = 192000.0; perSampleNormalizer.prepareChaosSmallSignalNormalizer( skipParityEmbeddedRate); @@ -19341,16 +19476,16 @@ juce::var AudioEngine::runNAMRackRegression() skipParityEmbeddedRate); perSampleNormalizer.setChaosSmallSignalNormalizerTarget( 1.0f, 1.0f, 1.0f, 1.0f, - S13NAMRack::bassInstrumentProfile, true); + OpenStudioNAMRack::bassInstrumentProfile, true); bulkNormalizer.setChaosSmallSignalNormalizerTarget( 1.0f, 1.0f, 1.0f, 1.0f, - S13NAMRack::bassInstrumentProfile, true); + OpenStudioNAMRack::bassInstrumentProfile, true); perSampleNormalizer.setChaosSmallSignalNormalizerTarget( 2.0f, 0.0f, 0.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile, false); + OpenStudioNAMRack::guitarInstrumentProfile, false); bulkNormalizer.setChaosSmallSignalNormalizerTarget( 2.0f, 0.0f, 0.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile, false); + OpenStudioNAMRack::guitarInstrumentProfile, false); const int holdSamples = perSampleNormalizer.chaosSmallSignalReleaseHoldSamples; const std::array skipChunks { @@ -19405,7 +19540,7 @@ juce::var AudioEngine::runNAMRackRegression() AutomationCapture result; result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(2); rack.setEmbeddedDriveOversamplingFactor(factor); @@ -19894,7 +20029,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr float inputPeak = 1.0e-7f; StackedLevelCapture capture; capture.audio.resize(totalSamples, 0.0f); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(1); rack.setEmbeddedDriveOversamplingFactor(4); @@ -20229,7 +20364,7 @@ juce::var AudioEngine::runNAMRackRegression() result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.namEffectsDspVersion.store( recalledVersionMarker, std::memory_order_relaxed); @@ -20506,7 +20641,7 @@ juce::var AudioEngine::runNAMRackRegression() std::abs(right))); } const float musicalTargetPeak = - S13NAMRack::namOutputSafetyKnee + OpenStudioNAMRack::namOutputSafetyKnee - 0.01f; musicalSource.applyGain( musicalTargetPeak @@ -20519,7 +20654,7 @@ juce::var AudioEngine::runNAMRackRegression() const juce::AudioBuffer& source, juce::AudioBuffer& destination) { - S13NAMRack::NAMOutputSafetyGuardResult + OpenStudioNAMRack::NAMOutputSafetyGuardResult aggregate; std::array previous {}; std::array hasPrevious {}; @@ -20557,7 +20692,7 @@ juce::var AudioEngine::runNAMRackRegression() chunkSamples); } const auto result = - S13NAMRack:: + OpenStudioNAMRack:: applyNAMOutputSafetyGuard( chunk, source @@ -20603,7 +20738,7 @@ juce::var AudioEngine::runNAMRackRegression() std::array musicalHasPrevious {}; const auto musicalWholeResult = - S13NAMRack:: + OpenStudioNAMRack:: applyNAMOutputSafetyGuard( musicalWhole, musicalWhole @@ -20695,21 +20830,21 @@ juce::var AudioEngine::runNAMRackRegression() 1, 0, -6.0f); excursionSource.setSample( 0, 10, - S13NAMRack:: + OpenStudioNAMRack:: namOutputSafetyKnee); excursionSource.setSample( 1, 10, - -S13NAMRack:: + -OpenStudioNAMRack:: namOutputSafetyKnee * 0.5f); excursionSource.setSample( 0, 11, - S13NAMRack:: + OpenStudioNAMRack:: namOutputSafetyKnee + 0.01f); excursionSource.setSample( 1, 11, - -(S13NAMRack:: + -(OpenStudioNAMRack:: namOutputSafetyKnee + 0.01f) * 0.5f); @@ -20735,7 +20870,7 @@ juce::var AudioEngine::runNAMRackRegression() std::array excursionHasPrevious {}; const auto excursionWholeResult = - S13NAMRack:: + OpenStudioNAMRack:: applyNAMOutputSafetyGuard( excursionWhole, excursionWhole @@ -20840,12 +20975,12 @@ juce::var AudioEngine::runNAMRackRegression() + 0.75f) <= 1.0e-6f && std::abs(protectedLeft) - < S13NAMRack:: + < OpenStudioNAMRack:: namOutputSafetyCeiling && std::abs( excursionWhole .getSample(0, 10) - - S13NAMRack:: + - OpenStudioNAMRack:: namOutputSafetyKnee) <= 1.0e-7f && excursionWhole @@ -20869,11 +21004,11 @@ juce::var AudioEngine::runNAMRackRegression() new juce::DynamicObject(); value->setProperty( "knee", - S13NAMRack:: + OpenStudioNAMRack:: namOutputSafetyKnee); value->setProperty( "ceiling", - S13NAMRack:: + OpenStudioNAMRack:: namOutputSafetyCeiling); value->setProperty( "musicalTargetPeak", @@ -20961,9 +21096,9 @@ juce::var AudioEngine::runNAMRackRegression() stereoHost.setSample(1, sample, stereoDryRight); } - S13NAMRack::mixNAMOutputForHost( + OpenStudioNAMRack::mixNAMOutputForHost( monoHost, monoDry, modelOutput, 2, wetMix); - S13NAMRack::mixNAMOutputForHost( + OpenStudioNAMRack::mixNAMOutputForHost( stereoHost, stereoDry, modelOutput, 2, wetMix); float monoMaxError = 0.0f; @@ -21023,9 +21158,9 @@ juce::var AudioEngine::runNAMRackRegression() stageRamp.setSample(0, sample, 1.0f); } - S13NAMRack::mixNAMOutputForHost( + OpenStudioNAMRack::mixNAMOutputForHost( modelRamp, rampDry, rampWet, 1, 0.0f, 1.0f); - S13NAMRack::crossfadeProcessedWithDry( + OpenStudioNAMRack::crossfadeProcessedWithDry( stageRamp, rampDry, 0.0f, 1.0f); float modelRampMaxError = 0.0f; @@ -21044,7 +21179,8 @@ juce::var AudioEngine::runNAMRackRegression() auto renderTrimRamp = [&] (bool inputTrim) { - S13NAMRack rack; + constexpr float sourceLevel = 0.2f; + OpenStudioNAMRack rack; rack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(rack); @@ -21054,8 +21190,8 @@ juce::var AudioEngine::runNAMRackRegression() juce::AudioBuffer warmup(2, fixtureBlockSize); for (int sample = 0; sample < fixtureBlockSize; ++sample) { - warmup.setSample(0, sample, 1.0f); - warmup.setSample(1, sample, -0.5f); + warmup.setSample(0, sample, sourceLevel); + warmup.setSample(1, sample, -0.5f * sourceLevel); } juce::MidiBuffer midi; rack.processBlock(warmup, midi); @@ -21068,8 +21204,8 @@ juce::var AudioEngine::runNAMRackRegression() juce::AudioBuffer block(2, fixtureBlockSize); for (int sample = 0; sample < fixtureBlockSize; ++sample) { - block.setSample(0, sample, 1.0f); - block.setSample(1, sample, -0.5f); + block.setSample(0, sample, sourceLevel); + block.setSample(1, sample, -0.5f * sourceLevel); } rack.processBlock(block, midi); @@ -21093,10 +21229,11 @@ juce::var AudioEngine::runNAMRackRegression() value->setProperty( "pass", monotonic - && block.getSample(0, 0) >= 0.9999f - && block.getSample(0, 0) < 1.01f - && block.getSample(0, fixtureBlockSize - 1) > 1.5f - && block.getSample(0, fixtureBlockSize - 1) < 3.98f + && block.getSample(0, 0) >= sourceLevel - 1.0e-4f + && block.getSample(0, 0) < sourceLevel + 0.002f + && block.getSample(0, fixtureBlockSize - 1) > 0.3f + && block.getSample(0, fixtureBlockSize - 1) + < sourceLevel * 3.98f && maximumStep < 0.01f); return juce::var(value); }; @@ -21126,7 +21263,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runEmptyAmpProbe = [&] () { - S13NAMRack rack; + OpenStudioNAMRack rack; rack.setEmbeddedDriveOversamplingFactor(4); rack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(rack); @@ -21163,7 +21300,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runEmbeddedSaturatorOversamplingPolicyProbe = [&] () { - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureNeutralRack(rack); rack.precisionDriveDrive.store(0.65f); @@ -21235,7 +21372,7 @@ juce::var AudioEngine::runNAMRackRegression() bool factorSelectionPassed = true; for (const int factor : { 2, 4, 8 }) { - S13NAMRack factorRack; + OpenStudioNAMRack factorRack; factorRack.setEmbeddedDriveOversamplingFactor(factor); factorRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); @@ -21331,7 +21468,7 @@ juce::var AudioEngine::runNAMRackRegression() bool distortionOn, bool enabledBeforePrepare) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.precisionDriveDrive.store(0.83f); rack.precisionDriveBright.store(0.71f); @@ -21572,7 +21709,7 @@ juce::var AudioEngine::runNAMRackRegression() result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setEmbeddedDriveOversamplingFactor(factor); rack.precisionDriveEnabled.store( @@ -22095,7 +22232,7 @@ juce::var AudioEngine::runNAMRackRegression() result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.chaosEnabled.store(1.0f, std::memory_order_relaxed); rack.chaosMode.store(0.0f, std::memory_order_relaxed); @@ -22511,7 +22648,7 @@ juce::var AudioEngine::runNAMRackRegression() == gateDisabledFixed.latencySamples; outputBounded = outputBounded && capture->maximumRawOutputPeak - < S13NAMRack::namOutputSafetyKnee; + < OpenStudioNAMRack::namOutputSafetyKnee; dcControlled = dcControlled && std::abs(capture->means[0]) < 0.012 && std::abs(capture->means[1]) < 0.012; @@ -22631,7 +22768,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runSaturatorAlignmentProbe = [&] () { - S13Saturator saturator; + OpenStudioSaturator saturator; saturator.setOversamplingMode(1.0f); saturator.setOversamplingEnabled(true); saturator.mix.store(0.0f); @@ -22731,7 +22868,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runSaturatorStateProbe = [&] () { - S13Saturator source; + OpenStudioSaturator source; source.setOversamplingMode(2.0f); source.setOversamplingEnabled(true); source.prepareToPlay( @@ -22739,7 +22876,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryBlock state; source.getStateInformation(state); - S13Saturator restored; + OpenStudioSaturator restored; restored.setStateInformation( state.getData(), static_cast(state.getSize())); @@ -22777,7 +22914,7 @@ juce::var AudioEngine::runNAMRackRegression() "Standalone Saturator state must restore both its 4x quality selection and the enabled latency-bearing processing path.", saturatorStateProbe); - auto configureDrivenRack = [&] (S13NAMRack& rack) + auto configureDrivenRack = [&] (OpenStudioNAMRack& rack) { configureNeutralRack(rack); rack.precisionDriveEnabled.store(1.0f); @@ -22826,7 +22963,7 @@ juce::var AudioEngine::runNAMRackRegression() }; auto renderPass = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, const auto& pattern) { juce::AudioBuffer capture(2, totalSamples); @@ -22854,13 +22991,13 @@ juce::var AudioEngine::runNAMRackRegression() return capture; }; - S13NAMRack fixedRack; + OpenStudioNAMRack fixedRack; fixedRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureDrivenRack(fixedRack); const auto fixedCapture = renderPass(fixedRack, fixedPattern); - S13NAMRack unevenRack; + OpenStudioNAMRack unevenRack; unevenRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureDrivenRack(unevenRack); const auto unevenCapture = @@ -22868,7 +23005,7 @@ juce::var AudioEngine::runNAMRackRegression() const float partitionError = maxDiffBetweenBuffers(fixedCapture, unevenCapture); - S13NAMRack renderPreparedRack; + OpenStudioNAMRack renderPreparedRack; renderPreparedRack.prepareToPlay( fixtureSampleRate, 2048); configureDrivenRack(renderPreparedRack); @@ -22938,7 +23075,7 @@ juce::var AudioEngine::runNAMRackRegression() { for (int channels = 1; channels <= 2; ++channels) { - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay(sampleRate, blockSize); configureDrivenRack(rack); int nonFiniteCount = 0; @@ -23024,7 +23161,7 @@ juce::var AudioEngine::runNAMRackRegression() TrackProcessor track; track.prepareToPlay( fixtureSampleRate, fixtureBlockSize); - auto rack = std::make_unique(); + auto rack = std::make_unique(); auto* const rackPointer = rack.get(); const bool added = track.addTrackFX( std::move(rack), @@ -23277,7 +23414,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr double benchmarkSampleRate = 48000.0; constexpr int benchmarkBlockSize = 128; constexpr int benchmarkBlocks = 4096; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay( benchmarkSampleRate, benchmarkBlockSize); configureDrivenRack(rack); @@ -23346,7 +23483,7 @@ juce::var AudioEngine::runNAMRackRegression() const juce::String& id, int configuration) { - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay( benchmarkSampleRate, benchmarkBlockSize); configureNeutralRack(rack); @@ -23578,7 +23715,7 @@ juce::var AudioEngine::runNAMRackRegression() }; auto configureReverb = [] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, float engineVersion, bool shimmerEnabled) { @@ -23587,7 +23724,7 @@ juce::var AudioEngine::runNAMRackRegression() std::memory_order_relaxed); reverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), std::memory_order_relaxed); reverb.roomSize.store( 0.55f, std::memory_order_relaxed); @@ -23725,7 +23862,7 @@ juce::var AudioEngine::runNAMRackRegression() repeat < repeatCount; ++repeat) { - S13Reverb reverb; + OpenStudioReverb reverb; configureReverb( reverb, engineVersion, @@ -23937,7 +24074,7 @@ juce::var AudioEngine::runNAMRackRegression() checks, "reverb_v4_huge_endpoint_cpu_comparison", "diagnostic_only", - "Machine/build-specific direct S13Reverb V3-versus-V4 timing at Decay 10 with Shimmer off/on and exact 8/16/128-sample calls. Each result is the median of three equal-duration runs and includes the identical input-copy fixture overhead; no timing value is an objective release gate.", + "Machine/build-specific direct OpenStudioReverb V3-versus-V4 timing at Decay 10 with Shimmer off/on and exact 8/16/128-sample calls. Each result is the median of three equal-duration runs and includes the identical input-copy fixture overhead; no timing value is an objective release gate.", reverbV4HugeCpuDiagnostic); auto runReverbRuntimeCacheProbe = [&] () @@ -23946,7 +24083,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int cacheBlockSize = 128; juce::MidiBuffer midi; - S13Reverb directReverb; + OpenStudioReverb directReverb; directReverb.engineVersion.store( 2.0f, std::memory_order_relaxed); directReverb.lowCut.store( @@ -23993,7 +24130,7 @@ juce::var AudioEngine::runNAMRackRegression() && coefficientRestabilisedCount == 2 && coefficientReprepareCount == 1; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.reverbEnabled.store( 1.0f, std::memory_order_relaxed); rack.reverbMix.store( @@ -24028,12 +24165,12 @@ juce::var AudioEngine::runNAMRackRegression() const auto tailStableBlockCount = processRackBlock(); rack.reverbVoice.store( - static_cast(S13NAMRack::plateReverbVoice), + static_cast(OpenStudioNAMRack::plateReverbVoice), std::memory_order_relaxed); const auto tailVoiceChangedCount = processRackBlock(); rack.reverbVoice.store( - static_cast(S13NAMRack::studioReverbVoice), + static_cast(OpenStudioNAMRack::studioReverbVoice), std::memory_order_relaxed); const auto tailVoiceRestoredCount = processRackBlock(); @@ -24199,7 +24336,7 @@ juce::var AudioEngine::runNAMRackRegression() "Standalone V2 coefficients must remain cached, while the NAM Rack current Reverb tail cache invalidates for Voice and current tail controls. Retired Character, Freeze, Shimmer Regen, and engine selectors must be inert.", reverbRuntimeCacheProbe); - S13NAMRack retiredTransposeRack; + OpenStudioNAMRack retiredTransposeRack; retiredTransposeRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); const int latencyBeforeLegacyTransposeRequest = @@ -24232,70 +24369,70 @@ juce::var AudioEngine::runNAMRackRegression() "Legacy NAM transpose requests must remain neutral: the compatibility setter stores zero and cannot change rack latency.", juce::var(retiredTransposeValue)); - constexpr std::array supportedReverbAlgorithms { - S13Reverb::Algorithm::Room, - S13Reverb::Algorithm::Hall, - S13Reverb::Algorithm::Plate, - S13Reverb::Algorithm::Chamber + constexpr std::array supportedReverbAlgorithms { + OpenStudioReverb::Algorithm::Room, + OpenStudioReverb::Algorithm::Hall, + OpenStudioReverb::Algorithm::Plate, + OpenStudioReverb::Algorithm::Chamber }; std::array supportedReverbTailSeconds {}; bool supportedReverbTailsFinite = true; for (size_t index = 0; index < supportedReverbAlgorithms.size(); ++index) { - supportedReverbTailSeconds[index] = S13Reverb::calculateTailLengthSeconds( + supportedReverbTailSeconds[index] = OpenStudioReverb::calculateTailLengthSeconds( static_cast(supportedReverbAlgorithms[index]), 0.92f, 1.0f, 0.28f, false, 500.0f, 12.0f, fixtureSampleRate); supportedReverbTailsFinite = supportedReverbTailsFinite && std::isfinite(supportedReverbTailSeconds[index]) && supportedReverbTailSeconds[index] > 0.5; } - const double frozenPlateTailSeconds = S13Reverb::calculateTailLengthSeconds( - static_cast(S13Reverb::Algorithm::Plate), + const double frozenPlateTailSeconds = OpenStudioReverb::calculateTailLengthSeconds( + static_cast(OpenStudioReverb::Algorithm::Plate), 0.92f, 1.0f, 0.28f, true, 500.0f, 12.0f, fixtureSampleRate); - const double bypassedReverbTailSeconds = S13Reverb::calculateTailLengthSeconds( - static_cast(S13Reverb::Algorithm::Plate), + const double bypassedReverbTailSeconds = OpenStudioReverb::calculateTailLengthSeconds( + static_cast(OpenStudioReverb::Algorithm::Plate), 0.92f, 0.0f, 0.0f, false, 500.0f, 12.0f, fixtureSampleRate); const double v2BasePlateTailSeconds = - S13Reverb::calculateTailLengthSecondsV2( + OpenStudioReverb::calculateTailLengthSecondsV2( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), 0.92f, 1.0f, 0.28f, false, 500.0f, 12.0f, fixtureSampleRate, 0.0f, 0.55f); const double v2ShimmerPlateTailSeconds = - S13Reverb::calculateTailLengthSecondsV2( + OpenStudioReverb::calculateTailLengthSecondsV2( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), 0.92f, 1.0f, 0.28f, false, 500.0f, 12.0f, fixtureSampleRate, 1.0f, 1.0f); const double v2BypassedShimmerTailSeconds = - S13Reverb::calculateTailLengthSecondsV2( + OpenStudioReverb::calculateTailLengthSecondsV2( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), 0.92f, 0.0f, 0.0f, false, 500.0f, 12.0f, fixtureSampleRate, 1.0f, 1.0f); const double v3BaseTailSeconds = - S13Reverb::calculateTailLengthSecondsV3( + OpenStudioReverb::calculateTailLengthSecondsV3( 1.0f, 500.0f, 12.0f, fixtureSampleRate, 0.0f); const double v3ShimmerTailSeconds = - S13Reverb::calculateTailLengthSecondsV3( + OpenStudioReverb::calculateTailLengthSecondsV3( 1.0f, 500.0f, 12.0f, fixtureSampleRate, 1.0f); const double v3BypassedShimmerTailSeconds = - S13Reverb::calculateTailLengthSecondsV3( + OpenStudioReverb::calculateTailLengthSecondsV3( 0.0f, 500.0f, 12.0f, fixtureSampleRate, 1.0f); - S13NAMRack automatedReverbTailRack; + OpenStudioNAMRack automatedReverbTailRack; automatedReverbTailRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); automatedReverbTailRack.reverbEngineVersion.store( 3.0f, std::memory_order_relaxed); const double ordinaryAutomatedReverbTailSeconds = automatedReverbTailRack.getAutomatedTailLengthSeconds( - S13NAMRack::tailAutomationReverb); + OpenStudioNAMRack::tailAutomationReverb); const double retiredFreezeAutomatedReverbTailSeconds = automatedReverbTailRack.getAutomatedTailLengthSeconds( - S13NAMRack::tailAutomationReservedReverbFreeze); + OpenStudioNAMRack::tailAutomationReservedReverbFreeze); const bool retiredFreezeTailIsIgnored = retiredFreezeAutomatedReverbTailSeconds == 0.0 && ordinaryAutomatedReverbTailSeconds > 0.0 @@ -24306,7 +24443,7 @@ juce::var AudioEngine::runNAMRackRegression() track.prepareToPlay( fixtureSampleRate, fixtureBlockSize); - auto rack = std::make_unique(); + auto rack = std::make_unique(); auto* const rackPointer = rack.get(); rackPointer->reverbEnabled.store( 1.0f, std::memory_order_relaxed); @@ -24426,8 +24563,8 @@ juce::var AudioEngine::runNAMRackRegression() TrackProcessor track; track.prepareToPlay(fixtureSampleRate, fixtureBlockSize); - auto neighbourRack = std::make_unique(); - auto automatedRack = std::make_unique(); + auto neighbourRack = std::make_unique(); + auto automatedRack = std::make_unique(); auto* const neighbourRackIdentity = neighbourRack.get(); auto* const automatedRackIdentity = automatedRack.get(); const bool neighbourRackAdded = track.addTrackFX( @@ -24514,9 +24651,9 @@ juce::var AudioEngine::runNAMRackRegression() const bool staleRackRouteGone = ! track.resolveAutomationTarget( "builtin_track_1_ampGainDb", false).has_value(); - auto* rackAfterReorder = dynamic_cast( + auto* rackAfterReorder = dynamic_cast( track.getTrackFXProcessor(0)); - auto* neighbourAfterReorder = dynamic_cast( + auto* neighbourAfterReorder = dynamic_cast( track.getTrackFXProcessor(1)); if (rackAfterReorder != nullptr) { @@ -24573,8 +24710,8 @@ juce::var AudioEngine::runNAMRackRegression() // race that a purely sequential lifecycle check cannot observe. TrackProcessor stressTrack; stressTrack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); - auto stressFirstRack = std::make_unique(); - auto stressSecondRack = std::make_unique(); + auto stressFirstRack = std::make_unique(); + auto stressSecondRack = std::make_unique(); auto* const stressFirstIdentity = stressFirstRack.get(); auto* const stressSecondIdentity = stressSecondRack.get(); const bool stressRacksAdded = stressTrack.addTrackFX( @@ -24640,9 +24777,9 @@ juce::var AudioEngine::runNAMRackRegression() std::this_thread::yield(); } stressAudioThread.join(); - auto* const stressFirstAfter = dynamic_cast( + auto* const stressFirstAfter = dynamic_cast( stressTrack.getTrackFXProcessor(0)); - auto* const stressSecondAfter = dynamic_cast( + auto* const stressSecondAfter = dynamic_cast( stressTrack.getTrackFXProcessor(1)); const bool concurrentPublicationStressPassed = stressReordersPassed @@ -24712,7 +24849,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int totalSamples = 96000; constexpr int analysisStart = 48000; constexpr float sourceFrequency = 220.0f; - S13OctaveShimmerShifter shifter; + OpenStudioOctaveShimmerShifter shifter; shifter.prepare(fixtureSampleRate); std::vector firstPass( static_cast(totalSamples), 0.0f); @@ -24929,7 +25066,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::roundToInt(sampleRate); const int analysisSamples = totalSamples - analysisStart; - S13OctaveShimmerShifter shifter; + OpenStudioOctaveShimmerShifter shifter; shifter.prepare(sampleRate); std::array real {}; @@ -25274,7 +25411,7 @@ juce::var AudioEngine::runNAMRackRegression() minimumGrainWrapCount, grainWrapCount); - S13OctaveShimmerShifter shifter; + OpenStudioOctaveShimmerShifter shifter; shifter.prepare( sampleRate, grainDurationSeconds, @@ -25602,13 +25739,13 @@ juce::var AudioEngine::runNAMRackRegression() { for (const int blockSize : blockSizes) { - S13Reverb reverb; + OpenStudioReverb reverb; reverb.engineVersion.store( engineVersion, std::memory_order_relaxed); reverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), std::memory_order_relaxed); reverb.roomSize.store( 0.55f, std::memory_order_relaxed); @@ -26169,7 +26306,7 @@ juce::var AudioEngine::runNAMRackRegression() 127, 509, 64, 1000, 31, 257, 733 }; auto configureReverb = [] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, float shimmer, float shimmerRegen = 0.55f, float engineVersion = 3.0f) @@ -26178,7 +26315,7 @@ juce::var AudioEngine::runNAMRackRegression() engineVersion); reverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Plate)); + OpenStudioReverb::Algorithm::Plate)); reverb.roomSize.store(0.72f); reverb.damping.store(0.34f); reverb.wetLevel.store(1.0f); @@ -26200,7 +26337,7 @@ juce::var AudioEngine::runNAMRackRegression() shimmerRegen); }; auto renderReverb = [&] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, const auto& pattern, int inputMode) { @@ -26296,7 +26433,7 @@ juce::var AudioEngine::runNAMRackRegression() return capture; }; - S13Reverb fixedShimmerReverb; + OpenStudioReverb fixedShimmerReverb; configureReverb(fixedShimmerReverb, 0.78f); fixedShimmerReverb.prepareToPlay( fixtureSampleRate, 1024); @@ -26306,7 +26443,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb legacyShimmerReverb; + OpenStudioReverb legacyShimmerReverb; configureReverb( legacyShimmerReverb, 0.78f, @@ -26320,7 +26457,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb unevenShimmerReverb; + OpenStudioReverb unevenShimmerReverb; configureReverb(unevenShimmerReverb, 0.78f); unevenShimmerReverb.prepareToPlay( fixtureSampleRate, 1024); @@ -26339,7 +26476,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb baseReverb; + OpenStudioReverb baseReverb; configureReverb(baseReverb, 0.0f); baseReverb.prepareToPlay( fixtureSampleRate, 1024); @@ -26349,7 +26486,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb fixedVoiceReferenceReverb; + OpenStudioReverb fixedVoiceReferenceReverb; configureReverb( fixedVoiceReferenceReverb, 0.47f); fixedVoiceReferenceReverb.prepareToPlay( @@ -26360,12 +26497,12 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb retiredControlsReverb; + OpenStudioReverb retiredControlsReverb; configureReverb( retiredControlsReverb, 0.47f); retiredControlsReverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Room)); + OpenStudioReverb::Algorithm::Room)); retiredControlsReverb.roomSize.store(0.0f); retiredControlsReverb.width.store(0.0f); retiredControlsReverb.freezeMode.store(1.0f); @@ -26383,7 +26520,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb preDelayOnsetReverb; + OpenStudioReverb preDelayOnsetReverb; configureReverb( preDelayOnsetReverb, 0.0f); constexpr float onsetPreDelayMs = 50.0f; @@ -26423,7 +26560,7 @@ juce::var AudioEngine::runNAMRackRegression() firstWetOutputIndex >= expectedPreDelaySamples - 2; - S13Reverb shimmerZeroMaximumRegenReverb; + OpenStudioReverb shimmerZeroMaximumRegenReverb; configureReverb( shimmerZeroMaximumRegenReverb, 0.0f, @@ -26436,7 +26573,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeStereo); - S13Reverb antiPhaseReverb; + OpenStudioReverb antiPhaseReverb; configureReverb(antiPhaseReverb, 0.0f); antiPhaseReverb.preDelay.store(0.0f); antiPhaseReverb.earlyLevel.store(0.0f); @@ -26448,7 +26585,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeAntiPhase); - S13Reverb leftOnlyReverb; + OpenStudioReverb leftOnlyReverb; configureReverb( leftOnlyReverb, 0.78f, 0.75f); leftOnlyReverb.preDelay.store(0.0f); @@ -26462,7 +26599,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeLeftOnly); - S13Reverb rightOnlyReverb; + OpenStudioReverb rightOnlyReverb; configureReverb( rightOnlyReverb, 0.78f, 0.75f); rightOnlyReverb.preDelay.store(0.0f); @@ -26476,7 +26613,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeRightOnly); - S13Reverb leftOnlyBaseReverb; + OpenStudioReverb leftOnlyBaseReverb; configureReverb( leftOnlyBaseReverb, 0.0f, 0.75f); leftOnlyBaseReverb.preDelay.store(0.0f); @@ -26490,7 +26627,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeLeftOnly); - S13Reverb rightOnlyBaseReverb; + OpenStudioReverb rightOnlyBaseReverb; configureReverb( rightOnlyBaseReverb, 0.0f, 0.75f); rightOnlyBaseReverb.preDelay.store(0.0f); @@ -26504,7 +26641,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeRightOnly); - S13Reverb monoInputReverb; + OpenStudioReverb monoInputReverb; configureReverb( monoInputReverb, 0.0f, 0.75f); monoInputReverb.preDelay.store(0.0f); @@ -26518,7 +26655,7 @@ juce::var AudioEngine::runNAMRackRegression() fixedPattern, reverbProbeMono); - S13Reverb collapsedStereoInputReverb; + OpenStudioReverb collapsedStereoInputReverb; configureReverb( collapsedStereoInputReverb, 0.0f); collapsedStereoInputReverb.prepareToPlay( @@ -26962,7 +27099,7 @@ juce::var AudioEngine::runNAMRackRegression() baseStereoTransferPass && shimmerStereoTransferPass; - S13Reverb dryLatencyReverb; + OpenStudioReverb dryLatencyReverb; configureReverb( dryLatencyReverb, 1.0f, 1.0f); dryLatencyReverb.wetLevel.store(0.0f); @@ -27026,7 +27163,7 @@ juce::var AudioEngine::runNAMRackRegression() && dryImpulseMaximumError <= 1.0e-7f && dryPathLatencySamples == 0; - S13Reverb stabilityReverb; + OpenStudioReverb stabilityReverb; configureReverb( stabilityReverb, 1.0f, 1.0f); stabilityReverb.roomSize.store(0.92f); @@ -27087,7 +27224,7 @@ juce::var AudioEngine::runNAMRackRegression() stabilityReverb .getEmergencyBoundHitCount(); - S13Reverb tailShutdownReverb; + OpenStudioReverb tailShutdownReverb; configureReverb( tailShutdownReverb, 0.0f); tailShutdownReverb.preDelay.store(0.0f); @@ -27124,7 +27261,7 @@ juce::var AudioEngine::runNAMRackRegression() && tailShutdownSamples < tailShutdownLimit; - S13Reverb enabledSleepReverb; + OpenStudioReverb enabledSleepReverb; configureReverb( enabledSleepReverb, 0.0f); enabledSleepReverb.preDelay.store(50.0f); @@ -27425,7 +27562,7 @@ juce::var AudioEngine::runNAMRackRegression() float maximumRawWetOutputPeak = 0.0f; }; - auto configure = [] (S13Reverb& reverb, + auto configure = [] (OpenStudioReverb& reverb, float engineVersion, float decaySeconds, float shimmerAmount) @@ -27434,7 +27571,7 @@ juce::var AudioEngine::runNAMRackRegression() engineVersion, std::memory_order_relaxed); reverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), std::memory_order_relaxed); reverb.roomSize.store( 0.55f, std::memory_order_relaxed); @@ -27482,7 +27619,7 @@ juce::var AudioEngine::runNAMRackRegression() int inputMode, bool automateDecay) { - S13Reverb reverb; + OpenStudioReverb reverb; configure( reverb, engineVersion, @@ -29174,7 +29311,7 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto configure = [] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, float engineVersion, int voice, float decayControl, @@ -29183,7 +29320,7 @@ juce::var AudioEngine::runNAMRackRegression() float padMode = 0.0f) { const auto macro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( static_cast(voice), 0.46f, decayControl, @@ -29191,7 +29328,7 @@ juce::var AudioEngine::runNAMRackRegression() 23.0f, 90.0f, textureControl, - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, padMode); reverb.engineVersion.store( engineVersion, std::memory_order_relaxed); @@ -29251,7 +29388,7 @@ juce::var AudioEngine::runNAMRackRegression() float padMode = 0.0f, bool padToneBurst = false) { - S13Reverb reverb; + OpenStudioReverb reverb; configure( reverb, engineVersion, @@ -29443,56 +29580,56 @@ juce::var AudioEngine::runNAMRackRegression() } const auto studioMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 0.0f, 0.28f, 2.2f, 0.62f, 18.0f, 120.0f, - 0.0f, S13NAMRack::bassInstrumentProfile); + 0.0f, OpenStudioNAMRack::bassInstrumentProfile); const auto plateGuitarMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 1.0f, 0.28f, 2.2f, 0.62f, 18.0f, 120.0f, - 0.0f, S13NAMRack::guitarInstrumentProfile); + 0.0f, OpenStudioNAMRack::guitarInstrumentProfile); const auto plateBassMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 1.0f, 0.28f, 2.2f, 0.62f, 18.0f, 120.0f, - 0.0f, S13NAMRack::bassInstrumentProfile); + 0.0f, OpenStudioNAMRack::bassInstrumentProfile); const auto plateShimmerMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 1.0f, 0.28f, 2.2f, 0.62f, 18.0f, 120.0f, - 0.73f, S13NAMRack::guitarInstrumentProfile); + 0.73f, OpenStudioNAMRack::guitarInstrumentProfile); const auto hallStillMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 2.0f, 0.28f, 2.2f, 0.15f, 18.0f, 120.0f, - 0.0f, S13NAMRack::guitarInstrumentProfile); + 0.0f, OpenStudioNAMRack::guitarInstrumentProfile); const auto hallMotionMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 2.0f, 0.28f, 2.2f, 0.85f, 18.0f, 120.0f, - 1.0f, S13NAMRack::guitarInstrumentProfile); + 1.0f, OpenStudioNAMRack::guitarInstrumentProfile); const auto hallBassMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 2.0f, 0.28f, 2.2f, 0.85f, 18.0f, 120.0f, - 1.0f, S13NAMRack::bassInstrumentProfile); + 1.0f, OpenStudioNAMRack::bassInstrumentProfile); const auto roomSmallMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 3.0f, 0.28f, 0.2f, 0.62f, 18.0f, 120.0f, - 0.0f, S13NAMRack::guitarInstrumentProfile); + 0.0f, OpenStudioNAMRack::guitarInstrumentProfile); const auto roomLargeEarlyMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 3.0f, 0.28f, 12.0f, 0.62f, 18.0f, 120.0f, - 1.0f, S13NAMRack::guitarInstrumentProfile); + 1.0f, OpenStudioNAMRack::guitarInstrumentProfile); const auto roomBassMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 3.0f, 0.28f, 12.0f, 0.62f, 18.0f, 120.0f, - 1.0f, S13NAMRack::bassInstrumentProfile); + 1.0f, OpenStudioNAMRack::bassInstrumentProfile); const auto padOffMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 0.0f, 0.28f, 2.2f, 0.62f, 18.0f, 120.0f, - 0.40f, S13NAMRack::guitarInstrumentProfile, 0.0f); + 0.40f, OpenStudioNAMRack::guitarInstrumentProfile, 0.0f); const auto padMacro = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( 0.0f, 0.28f, 2.2f, 0.62f, 18.0f, 120.0f, - 0.40f, S13NAMRack::guitarInstrumentProfile, 1.0f); + 0.40f, OpenStudioNAMRack::guitarInstrumentProfile, 1.0f); const auto ordinaryFieldsMatch = [] ( - const S13NAMRack::ReverbMacroState& first, - const S13NAMRack::ReverbMacroState& second) noexcept + const OpenStudioNAMRack::ReverbMacroState& first, + const OpenStudioNAMRack::ReverbMacroState& second) noexcept { return first.voice == second.voice && first.algorithmIndex == second.algorithmIndex @@ -29519,9 +29656,9 @@ juce::var AudioEngine::runNAMRackRegression() && padMacro.padMode == 1.0f && ordinaryFieldsMatch(padOffMacro, padMacro); const bool macroContractPass = - studioMacro.voice == S13NAMRack::studioReverbVoice + studioMacro.voice == OpenStudioNAMRack::studioReverbVoice && studioMacro.algorithmIndex - == static_cast(S13Reverb::Algorithm::Plate) + == static_cast(OpenStudioReverb::Algorithm::Plate) && studioMacro.roomSize == 0.55f && studioMacro.damping == 1.0f - 0.62f * 0.82f && studioMacro.wetGain == 0.28f * (2.0f - 0.28f) @@ -29564,28 +29701,28 @@ juce::var AudioEngine::runNAMRackRegression() const auto hallStillCapture = render( 5.0f, - S13NAMRack::hallReverbVoice, + OpenStudioNAMRack::hallReverbVoice, fixedPartitions, 4.4f, 0.63f, 0.0f); const auto hallMotionCapture = render( 5.0f, - S13NAMRack::hallReverbVoice, + OpenStudioNAMRack::hallReverbVoice, fixedPartitions, 4.4f, 0.63f, 1.0f); const auto roomLateCapture = render( 5.0f, - S13NAMRack::roomReverbVoice, + OpenStudioNAMRack::roomReverbVoice, fixedPartitions, 6.1f, 0.63f, 0.0f); const auto roomEarlyCapture = render( 5.0f, - S13NAMRack::roomReverbVoice, + OpenStudioNAMRack::roomReverbVoice, fixedPartitions, 6.1f, 0.63f, @@ -29600,7 +29737,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto ordinaryShimmerCapture = render( 5.0f, - S13NAMRack::studioReverbVoice, + OpenStudioNAMRack::studioReverbVoice, fixedPartitions, 4.4f, 0.63f, @@ -29609,7 +29746,7 @@ juce::var AudioEngine::runNAMRackRegression() true); const auto padFixedCapture = render( 5.0f, - S13NAMRack::studioReverbVoice, + OpenStudioNAMRack::studioReverbVoice, fixedPartitions, 4.4f, 0.63f, @@ -29618,7 +29755,7 @@ juce::var AudioEngine::runNAMRackRegression() true); const auto padUnevenCapture = render( 5.0f, - S13NAMRack::studioReverbVoice, + OpenStudioNAMRack::studioReverbVoice, unevenPartitions, 4.4f, 0.63f, @@ -29627,7 +29764,7 @@ juce::var AudioEngine::runNAMRackRegression() true); const auto padOffZeroShimmerCapture = render( 5.0f, - S13NAMRack::studioReverbVoice, + OpenStudioNAMRack::studioReverbVoice, fixedPartitions, 4.4f, 0.63f, @@ -29636,7 +29773,7 @@ juce::var AudioEngine::runNAMRackRegression() true); const auto padZeroShimmerCapture = render( 5.0f, - S13NAMRack::studioReverbVoice, + OpenStudioNAMRack::studioReverbVoice, fixedPartitions, 4.4f, 0.63f, @@ -29762,12 +29899,12 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr int switchSample = 12000; constexpr int switchRenderSamples = 48000; - S13Reverb reverb; + OpenStudioReverb reverb; configure( reverb, static_cast( - S13NAMRack::currentReverbEngineVersion), - S13NAMRack::studioReverbVoice, + OpenStudioNAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::studioReverbVoice, 4.4f, 0.63f, 0.0f); @@ -29788,8 +29925,8 @@ juce::var AudioEngine::runNAMRackRegression() configure( reverb, static_cast( - S13NAMRack::currentReverbEngineVersion), - S13NAMRack::roomReverbVoice, + OpenStudioNAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::roomReverbVoice, 4.4f, 0.63f, 0.0f); @@ -30051,22 +30188,22 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto configure = [] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, bool padEnabled) { - const auto macro = S13NAMRack::resolveReverbMacroState( - static_cast(S13NAMRack::studioReverbVoice), + const auto macro = OpenStudioNAMRack::resolveReverbMacroState( + static_cast(OpenStudioNAMRack::studioReverbVoice), 0.46f, 2.4f, 0.63f, 23.0f, 90.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, padEnabled ? 1.0f : 0.0f); reverb.engineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::currentReverbEngineVersion), std::memory_order_relaxed); reverb.rackVoice.store( static_cast(macro.voice), @@ -30120,7 +30257,7 @@ juce::var AudioEngine::runNAMRackRegression() const std::array& partitions, double livePadOffSeconds) { - S13Reverb reverb; + OpenStudioReverb reverb; configure(reverb, padEnabled); reverb.prepareToPlay(sampleRate, maximumBlockSize); juce::AudioBuffer storage(2, maximumBlockSize); @@ -30928,7 +31065,7 @@ juce::var AudioEngine::runNAMRackRegression() auto* value = new juce::DynamicObject(); value->setProperty( - "fixture", "full_s13_reverb_pad_on_minus_pad_off"); + "fixture", "full_openstudio_reverb_pad_on_minus_pad_off"); value->setProperty("renderSeconds", renderSeconds); value->setProperty("capturesHealthy", capturesHealthy); value->setProperty("maximumOutputPeak", sparseOn.peak); @@ -31087,7 +31224,7 @@ juce::var AudioEngine::runNAMRackRegression() reverbV5PadV3ReferenceBehaviorProbe.getProperty( "referenceBehaviorPass", false) ? "pass" : "fail", - "Using the complete S13Reverb path with ordinary Shimmer at zero, PAD V3 must keep the whole deterministic fixture within 1 dB and its intentionally more audible isolated wet window within -3 to +4.5 dB of ordinary Reverb, sustain a longer but declining and bounded tail, drain below -90 dBFS after a live Off, keep residual stereo correlation in -0.25 to +0.40 and Side/Mid in -3 to +1.5 dB, and remain callback-partition invariant. This deliberately does not force Neural's phase-cancelling replacement behavior onto the additive NAM PAD contract.", + "Using the complete OpenStudioReverb path with ordinary Shimmer at zero, PAD V3 must keep the whole deterministic fixture within 1 dB and its intentionally more audible isolated wet window within -3 to +4.5 dB of ordinary Reverb, sustain a longer but declining and bounded tail, drain below -90 dBFS after a live Off, keep residual stereo correlation in -0.25 to +0.40 and Side/Mid in -3 to +1.5 dB, and remain callback-partition invariant. This deliberately does not force Neural's phase-cancelling replacement behavior onto the additive NAM PAD contract.", reverbV5PadV3ReferenceBehaviorProbe); addCheck( checks, @@ -31127,8 +31264,8 @@ juce::var AudioEngine::runNAMRackRegression() auto runReverbV5PadMacroIsolationProbe = [] () { const auto ordinaryFieldsMatch = [] ( - const S13NAMRack::ReverbMacroState& first, - const S13NAMRack::ReverbMacroState& second) noexcept + const OpenStudioNAMRack::ReverbMacroState& first, + const OpenStudioNAMRack::ReverbMacroState& second) noexcept { return first.voice == second.voice && first.algorithmIndex == second.algorithmIndex @@ -31153,12 +31290,12 @@ juce::var AudioEngine::runNAMRackRegression() juce::Array cases; bool allCasesPass = true; - for (int voice = S13NAMRack::studioReverbVoice; - voice <= S13NAMRack::roomReverbVoice; + for (int voice = OpenStudioNAMRack::studioReverbVoice; + voice <= OpenStudioNAMRack::roomReverbVoice; ++voice) { - for (int profile = S13NAMRack::guitarInstrumentProfile; - profile <= S13NAMRack::bassInstrumentProfile; + for (int profile = OpenStudioNAMRack::guitarInstrumentProfile; + profile <= OpenStudioNAMRack::bassInstrumentProfile; ++profile) { const float mix = 0.17f @@ -31176,7 +31313,7 @@ juce::var AudioEngine::runNAMRackRegression() : 0.31f + 0.19f * static_cast(profile); const auto padOff = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( static_cast(voice), mix, decay, @@ -31187,7 +31324,7 @@ juce::var AudioEngine::runNAMRackRegression() profile, 0.0f); const auto padOn = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( static_cast(voice), mix, decay, @@ -31198,7 +31335,7 @@ juce::var AudioEngine::runNAMRackRegression() profile, 1.0f); const auto belowThreshold = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( static_cast(voice), mix, decay, @@ -31209,7 +31346,7 @@ juce::var AudioEngine::runNAMRackRegression() profile, 0.499f); const auto atThreshold = - S13NAMRack::resolveReverbMacroState( + OpenStudioNAMRack::resolveReverbMacroState( static_cast(voice), mix, decay, @@ -31293,22 +31430,22 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto applyPadMacro = [] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, float padMode) { - const auto macro = S13NAMRack::resolveReverbMacroState( - static_cast(S13NAMRack::studioReverbVoice), + const auto macro = OpenStudioNAMRack::resolveReverbMacroState( + static_cast(OpenStudioNAMRack::studioReverbVoice), 0.46f, 4.4f, 0.63f, 23.0f, 90.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, padMode); reverb.engineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::currentReverbEngineVersion), std::memory_order_relaxed); reverb.rackVoice.store( static_cast(macro.voice), @@ -31367,7 +31504,7 @@ juce::var AudioEngine::runNAMRackRegression() sampleRate * 1.45); const int totalSamples = juce::roundToInt( sampleRate * 2.55); - S13Reverb reverb; + OpenStudioReverb reverb; applyPadMacro(reverb, 0.0f); reverb.prepareToPlay(sampleRate, blockSize); juce::AudioBuffer storage(2, blockSize); @@ -31985,7 +32122,7 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto configure = [&configureNeutralRack] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, int routedInputChannels, bool padEnabled) { @@ -31995,7 +32132,7 @@ juce::var AudioEngine::runNAMRackRegression() configureNeutralRack(rack); rack.setRoutedInputChannelCount(routedInputChannels); rack.reverbVoice.store( - static_cast(S13NAMRack::studioReverbVoice), + static_cast(OpenStudioNAMRack::studioReverbVoice), std::memory_order_relaxed); rack.reverbMix.store(0.46f, std::memory_order_relaxed); rack.reverbDecaySec.store(4.4f, std::memory_order_relaxed); @@ -32123,9 +32260,9 @@ juce::var AudioEngine::runNAMRackRegression() for (const auto& definition : caseDefinitions) { - S13NAMRack ordinaryRack; - S13NAMRack preconfiguredPadRack; - S13NAMRack liveToggleRack; + OpenStudioNAMRack ordinaryRack; + OpenStudioNAMRack preconfiguredPadRack; + OpenStudioNAMRack liveToggleRack; ordinaryRack.prepareToPlay(sampleRate, maximumBlockSize); preconfiguredPadRack.prepareToPlay( sampleRate, maximumBlockSize); @@ -32377,7 +32514,7 @@ juce::var AudioEngine::runNAMRackRegression() auto* value = new juce::DynamicObject(); value->setProperty( "fixture", - "full_s13_nam_rack_post_prepare_pad_live_toggle"); + "full_openstudio_nam_rack_post_prepare_pad_live_toggle"); value->setProperty("sampleRate", sampleRate); value->setProperty("maximumBlockSize", maximumBlockSize); value->setProperty("switchOnSeconds", 0.25); @@ -32422,7 +32559,7 @@ juce::var AudioEngine::runNAMRackRegression() "nam_rack_reverb_pad_post_prepare_live_toggle_and_quiet_level_contract", namRackReverbPadPostPrepareLevelProbe.getProperty("pass", false) ? "pass" : "fail", - "Through the complete S13NAMRack path, a live PAD parameter write made only after prepare and while guitar audio is already flowing must remain identical to PAD Off before the switch, become an objectively non-zero stereo and mono-compatible additive layer after its bloom, remain within 12 dB of an otherwise identical preconfigured PAD render, preserve a non-zero release after Off, and retain its residual-to-ordinary level within 6 dB when the input falls from 0.20 to 0.02 peak. Mono-hot, mono-medium, mono-quiet, and stereo-quiet routes must each stay finite and bounded. Perceived Neural/whisper similarity and subjective audibility remain not_asserted.", + "Through the complete OpenStudioNAMRack path, a live PAD parameter write made only after prepare and while guitar audio is already flowing must remain identical to PAD Off before the switch, become an objectively non-zero stereo and mono-compatible additive layer after its bloom, remain within 12 dB of an otherwise identical preconfigured PAD render, preserve a non-zero release after Off, and retain its residual-to-ordinary level within 6 dB when the input falls from 0.20 to 0.02 peak. Mono-hot, mono-medium, mono-quiet, and stereo-quiet routes must each stay finite and bounded. Perceived Neural/whisper similarity and subjective audibility remain not_asserted.", namRackReverbPadPostPrepareLevelProbe); auto runReverbV5PadStereoMonoSilenceProbe = [] () @@ -32446,22 +32583,22 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto configure = [] ( - S13Reverb& reverb, + OpenStudioReverb& reverb, bool padEnabled) { - const auto macro = S13NAMRack::resolveReverbMacroState( - static_cast(S13NAMRack::studioReverbVoice), + const auto macro = OpenStudioNAMRack::resolveReverbMacroState( + static_cast(OpenStudioNAMRack::studioReverbVoice), 0.46f, 2.2f, 0.63f, 23.0f, 90.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, padEnabled ? 1.0f : 0.0f); reverb.engineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::currentReverbEngineVersion), std::memory_order_relaxed); reverb.rackVoice.store( static_cast(macro.voice), @@ -32497,7 +32634,7 @@ juce::var AudioEngine::runNAMRackRegression() int stimulus, bool padEnabled) { - S13Reverb reverb; + OpenStudioReverb reverb; configure(reverb, padEnabled); reverb.prepareToPlay(sampleRate, blockSize); if (stimulus == 0) @@ -32851,7 +32988,7 @@ juce::var AudioEngine::runNAMRackRegression() }; auto applyControlState = - [] (S13Reverb& reverb, + [] (OpenStudioReverb& reverb, const ControlState& state) { reverb.decayTime.store( @@ -32914,11 +33051,11 @@ juce::var AudioEngine::runNAMRackRegression() [&] (bool stereoInput, bool movingControls) { - S13Reverb reverb; + OpenStudioReverb reverb; reverb.engineVersion.store(4.0f); reverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Plate)); + OpenStudioReverb::Algorithm::Plate)); reverb.roomSize.store(0.55f); reverb.wetLevel.store(1.0f); reverb.dryLevel.store(0.0f); @@ -33667,7 +33804,7 @@ juce::var AudioEngine::runNAMRackRegression() }; auto configure = - [] (S13Reverb& reverb, + [] (OpenStudioReverb& reverb, float engineVersion, float decaySeconds, float preDelayMs, @@ -33680,7 +33817,7 @@ juce::var AudioEngine::runNAMRackRegression() std::memory_order_relaxed); reverb.algorithm.store( static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), std::memory_order_relaxed); reverb.roomSize.store( 0.55f, std::memory_order_relaxed); @@ -33728,7 +33865,7 @@ juce::var AudioEngine::runNAMRackRegression() 1.0f, std::memory_order_relaxed); }; - S13Reverb representativeReverb; + OpenStudioReverb representativeReverb; configure( representativeReverb, 3.0f, @@ -33743,7 +33880,7 @@ juce::var AudioEngine::runNAMRackRegression() representativeReverb .resetEmergencyBoundHitCount(); - S13Reverb maximumShimmerReverb; + OpenStudioReverb maximumShimmerReverb; configure( maximumShimmerReverb, 4.0f, @@ -33758,7 +33895,7 @@ juce::var AudioEngine::runNAMRackRegression() maximumShimmerReverb .resetEmergencyBoundHitCount(); - std::array reverbs { + std::array reverbs { &representativeReverb, &maximumShimmerReverb }; @@ -34577,10 +34714,10 @@ juce::var AudioEngine::runNAMRackRegression() const int lateSilenceStartSample = totalSamples - juce::roundToInt( stressSampleRate * lateSilenceWindowSeconds); - S13Reverb reverb; + OpenStudioReverb reverb; reverb.engineVersion.store(4.0f, std::memory_order_relaxed); reverb.algorithm.store( - static_cast(S13Reverb::Algorithm::Plate), + static_cast(OpenStudioReverb::Algorithm::Plate), std::memory_order_relaxed); reverb.roomSize.store(0.55f, std::memory_order_relaxed); reverb.damping.store( @@ -34840,7 +34977,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int blockSize = 16; constexpr float finiteFaultSample = 1000.0f; - S13Reverb reverb; + OpenStudioReverb reverb; reverb.engineVersion.store(4.0f, std::memory_order_relaxed); reverb.wetLevel.store(1.0f, std::memory_order_relaxed); reverb.dryLevel.store(1.0f, std::memory_order_relaxed); @@ -34923,7 +35060,7 @@ juce::var AudioEngine::runNAMRackRegression() reverbV4WetStateIntegrityRecovery); auto configureWetOnlyReverb = - [] (S13Reverb& reverb, + [] (OpenStudioReverb& reverb, double sampleRate, int blockSize, float decaySeconds, @@ -34957,8 +35094,8 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int blockSize = 16; constexpr int totalSamples = 16384; constexpr float scale = 2.0f; - S13Reverb lowReverb; - S13Reverb highReverb; + OpenStudioReverb lowReverb; + OpenStudioReverb highReverb; configureWetOnlyReverb( lowReverb, sampleRate, @@ -35115,7 +35252,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int blockSize = 16; constexpr int totalSamples = static_cast(sampleRate * 12.0); - S13Reverb reverb; + OpenStudioReverb reverb; configureWetOnlyReverb( reverb, sampleRate, @@ -35335,7 +35472,7 @@ juce::var AudioEngine::runNAMRackRegression() expectedDefault); }; - S13NAMRack rackSchemaProbe; + OpenStudioNAMRack rackSchemaProbe; const auto rackSchema = describeBuiltInProcessor(&rackSchemaProbe, "track", 0); const auto* currentCompressorOutputParameter = findSchemaParameter(rackSchema, "compressorVolumeDb"); @@ -35349,7 +35486,7 @@ juce::var AudioEngine::runNAMRackRegression() -18.0, 18.0, 0.0); - S13NAMRack retiredMarkerRackSchemaProbe; + OpenStudioNAMRack retiredMarkerRackSchemaProbe; retiredMarkerRackSchemaProbe.namEffectsDspVersion.store(2); const auto retiredMarkerRackSchema = describeBuiltInProcessor( &retiredMarkerRackSchemaProbe, "track", 0); @@ -35373,9 +35510,9 @@ juce::var AudioEngine::runNAMRackRegression() }; const bool schemaEffectsVersionTruthful = schemaEffectsVersion(rackSchema) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && schemaEffectsVersion(retiredMarkerRackSchema) - == S13NAMRack::currentNAMEffectsDspVersion; + == OpenStudioNAMRack::currentNAMEffectsDspVersion; const auto* retiredMarkerCompressorVolumeParameter = findSchemaParameter(retiredMarkerRackSchema, "compressorVolumeDb"); const bool retiredMarkerPreNAMLabelsCanonicalized = @@ -35556,7 +35693,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool chorusCharacterLabelsTruthful = labelsMatch( chorusCharacterLabels, { "Clean", "Ensemble" }); - S13Reverb reverbSchemaProbe; + OpenStudioReverb reverbSchemaProbe; const auto reverbSchema = describeBuiltInProcessor(&reverbSchemaProbe, "track", 0); const auto reverbAlgorithmLabels = enumLabelsFromParameter( findSchemaParameter(reverbSchema, "algorithm")); @@ -35687,7 +35824,7 @@ juce::var AudioEngine::runNAMRackRegression() && std::abs( rackSchemaProbe.reverbEngineVersion.load() - static_cast( - S13NAMRack::currentReverbEngineVersion)) <= 0.0001f; + OpenStudioNAMRack::currentReverbEngineVersion)) <= 0.0001f; auto* defaultEffectStateValue = new juce::DynamicObject(); defaultEffectStateValue->setProperty("allPreAndPostEffectsBypassed", defaultEffectPowerPass); defaultEffectStateValue->setProperty("chorusMix", rackSchemaProbe.chorusMix.load()); @@ -35800,7 +35937,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool irWritten = writeFixtureIR(irFile); const bool alternateIRWritten = writeFixtureIR(alternateIRFile, true); - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureRack(rack); @@ -35819,13 +35956,13 @@ juce::var AudioEngine::runNAMRackRegression() "A rejected direct NAM load must not replace the current slot identity with the failed path.", rack.getAmpModelPath()); - juce::ValueTree missingProjectTree("S13NAMRack"); + juce::ValueTree missingProjectTree("OpenStudioNAMRack"); missingProjectTree.setProperty("ampModelPath", missingNAMPath, nullptr); missingProjectTree.setProperty("inputTrimDb", -4.0, nullptr); juce::MemoryBlock missingProjectState; juce::MemoryOutputStream missingProjectStream(missingProjectState, false); missingProjectTree.writeToStream(missingProjectStream); - S13NAMRack recoveryRack; + OpenStudioNAMRack recoveryRack; recoveryRack.inputTrimDb.store(2.0f); const bool partialProjectRestoreUsable = recoveryRack.restoreProjectStateInformation( missingProjectState.getData(), static_cast(missingProjectState.getSize())) @@ -35839,7 +35976,7 @@ juce::var AudioEngine::runNAMRackRegression() "Project recovery should retain a missing path for relinking, publish other usable state, and report success with degraded detail in lastLoadError.", recoveryRack.getLastLoadError()); - S13NAMRack strictToneRack; + OpenStudioNAMRack strictToneRack; strictToneRack.inputTrimDb.store(3.0f); const bool strictToneFailurePreservesState = ! strictToneRack.restoreTonePresetStateInformation( missingProjectState.getData(), static_cast(missingProjectState.getSize())) @@ -35887,7 +36024,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack fixtureRack; + OpenStudioNAMRack fixtureRack; fixtureRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); fixtureRack.inputTrimDb.store(0.0f); fixtureRack.outputTrimDb.store(0.0f); @@ -35917,7 +36054,7 @@ juce::var AudioEngine::runNAMRackRegression() static_cast(0))); const bool expectedGuardActivity = maximumRawOutputPeak - <= S13NAMRack:: + <= OpenStudioNAMRack:: namOutputSafetyKnee + 1.0e-6f ? guardHitCount == 0 @@ -35925,7 +36062,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool guardContract = expectedGuardActivity && probe.peak - <= S13NAMRack:: + <= OpenStudioNAMRack:: namOutputSafetyCeiling + 1.0e-3f; value->setProperty("probe", makeProbeVar(probe)); @@ -35978,7 +36115,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack swapRack; + OpenStudioNAMRack swapRack; swapRack.prepareToPlay(fixtureSampleRate, stressBlockSize); swapRack.inputTrimDb.store(0.0f); swapRack.outputTrimDb.store(0.0f); @@ -36191,7 +36328,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack handoffRack; + OpenStudioNAMRack handoffRack; handoffRack.prepareToPlay( fixtureSampleRate, handoffBlockSize); handoffRack.inputTrimDb.store(0.0f); @@ -36216,8 +36353,16 @@ juce::var AudioEngine::runNAMRackRegression() handoffRack.setCabRequestedEnabled(false); handoffRack.eqEnabled.store(0.0f); handoffRack.modulatorEnabled.store(0.0f); - handoffRack.delayEnabled.store(0.0f); - handoffRack.reverbEnabled.store(0.0f); + // Keep both post-Amp time effects active and already excited. The + // complete-rack handoff boundary must mute their existing tails while + // the model graph and its latency domain are being replaced; muting + // only the Amp input would let those tails escape the transaction. + handoffRack.delayEnabled.store(1.0f); + handoffRack.delayTimeMs.store(20.0f); + handoffRack.delayFeedback.store(0.48f); + handoffRack.delayMix.store(0.38f); + handoffRack.reverbEnabled.store(1.0f); + handoffRack.reverbMix.store(0.42f); juce::AudioBuffer buffer( 2, handoffBlockSize); @@ -36230,6 +36375,9 @@ juce::var AudioEngine::runNAMRackRegression() float maximumAdjacentOutputDelta = 0.0f; double settledEnergy = 0.0; int settledEnergySamples = 0; + bool observeHandoffMute = false; + int observedHandoffBlocks = 0; + int mutedHandoffBlocks = 0; const auto processBlocks = [&] (int blockCount, @@ -36290,6 +36438,23 @@ juce::var AudioEngine::runNAMRackRegression() handoffRack.processBlock(buffer, midi); + if (observeHandoffMute) + { + ++observedHandoffBlocks; + float blockPeak = 0.0f; + for (int channel = 0; + channel < buffer.getNumChannels(); + ++channel) + { + blockPeak = juce::jmax( + blockPeak, + buffer.getMagnitude( + channel, 0, handoffBlockSize)); + } + if (blockPeak <= 1.0e-9f) + ++mutedHandoffBlocks; + } + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) @@ -36370,14 +36535,23 @@ juce::var AudioEngine::runNAMRackRegression() handoffRack.loadAmpModel( stableModelFile .getFullPathName()); + observeHandoffMute = true; processBlocks( blocksForSeconds(0.140), false); + observeHandoffMute = false; + const int firstHandoffMutedBlocks = + mutedHandoffBlocks; const bool replacementGraphLoaded = handoffRack.loadAmpModel( stableModelFile .getFullPathName()); + observeHandoffMute = true; processBlocks( blocksForSeconds(0.160), true); + observeHandoffMute = false; + const int replacementHandoffMutedBlocks = + mutedHandoffBlocks + - firstHandoffMutedBlocks; const auto diagnostics = handoffRack.getDiagnosticState(); @@ -36414,6 +36588,8 @@ juce::var AudioEngine::runNAMRackRegression() && oversizeBypassCount == 0 && modelProcessFailCount == 0 && realtimeSafetyBypassCount == 0 + && firstHandoffMutedBlocks > 0 + && replacementHandoffMutedBlocks > 0 && maximumOutputPeak > 1.0e-5f && maximumOutputPeak < 4.0f && maximumAdjacentOutputDelta @@ -36453,6 +36629,15 @@ juce::var AudioEngine::runNAMRackRegression() value->setProperty( "realtimeSafetyBypassCount", realtimeSafetyBypassCount); + value->setProperty( + "observedHandoffBlocks", + observedHandoffBlocks); + value->setProperty( + "firstHandoffMutedBlocks", + firstHandoffMutedBlocks); + value->setProperty( + "replacementHandoffMutedBlocks", + replacementHandoffMutedBlocks); value->setProperty( "diagnostics", diagnostics); value->setProperty( @@ -36482,6 +36667,491 @@ juce::var AudioEngine::runNAMRackRegression() const auto slimmableWavenetModelFile = findNAMCoreExample("slimmable_wavenet.nam"); + auto runStableBiquadAutomationProbe = [&] () + { + struct StageCapture + { + juce::AudioBuffer audio; + int nonFiniteSamples = 0; + float maximumPeak = 0.0f; + float maximumAdjacentDelta = 0.0f; + std::uint64_t toneRecoveries = 0; + std::uint64_t preEqRecoveries = 0; + std::uint64_t graphicEqRecoveries = 0; + }; + + constexpr std::array gainSequence { + -12.0f, 12.0f, -9.0f, 9.0f, -6.0f, + 6.0f, -3.0f, 3.0f, 0.0f + }; + const auto setAllGainBands = [&gainSequence] ( + OpenStudioNAMRack& rack, + int eventIndex) + { + const auto gain = [&] (int offset) + { + const auto index = static_cast( + (eventIndex + offset) + % static_cast(gainSequence.size())); + return gainSequence[index]; + }; + rack.bassDb.store(gain(0)); + rack.midDb.store(gain(2)); + rack.trebleDb.store(gain(4)); + rack.presenceDb.store(gain(6)); + rack.preEq120Db.store(gain(1)); + rack.preEq250Db.store(gain(2)); + rack.preEq500Db.store(gain(3)); + rack.preEq1kDb.store(gain(4)); + rack.preEq2k5Db.store(gain(5)); + rack.preEq5kDb.store(gain(6)); + rack.preEq8kDb.store(gain(7)); + rack.preEq12kDb.store(gain(8)); + rack.eq65Db.store(gain(8)); + rack.eq125Db.store(gain(7)); + rack.eq250Db.store(gain(6)); + rack.eq500Db.store(gain(5)); + rack.eq1kDb.store(gain(4)); + rack.eq2kDb.store(gain(3)); + rack.eq4kDb.store(gain(2)); + rack.eq8kDb.store(gain(1)); + rack.eq16kDb.store(gain(0)); + }; + const auto setAllGainBandsNeutral = [] (OpenStudioNAMRack& rack) + { + rack.bassDb.store(0.0f); + rack.midDb.store(0.0f); + rack.trebleDb.store(0.0f); + rack.presenceDb.store(0.0f); + rack.preEq120Db.store(0.0f); + rack.preEq250Db.store(0.0f); + rack.preEq500Db.store(0.0f); + rack.preEq1kDb.store(0.0f); + rack.preEq2k5Db.store(0.0f); + rack.preEq5kDb.store(0.0f); + rack.preEq8kDb.store(0.0f); + rack.preEq12kDb.store(0.0f); + rack.eq65Db.store(0.0f); + rack.eq125Db.store(0.0f); + rack.eq250Db.store(0.0f); + rack.eq500Db.store(0.0f); + rack.eq1kDb.store(0.0f); + rack.eq2kDb.store(0.0f); + rack.eq4kDb.store(0.0f); + rack.eq8kDb.store(0.0f); + rack.eq16kDb.store(0.0f); + }; + const auto renderStages = [ + &configureNeutralRack, + &setAllGainBands, + &setAllGainBandsNeutral] ( + double sampleRate, + const std::vector& partitions, + bool retargetEveryCallback) + { + const int totalSamples = juce::roundToInt(sampleRate * 0.70); + const int automationEndSample = + juce::roundToInt(sampleRate * 0.42); + const int alignedAutomationStep = juce::jmax( + 8, juce::roundToInt(sampleRate * 0.0015)); + OpenStudioNAMRack rack; + configureNeutralRack(rack); + rack.instrumentProfile.store( + static_cast(OpenStudioNAMRack::bassInstrumentProfile)); + rack.preEqEnabled.store(1.0f); + rack.eqEnabled.store(1.0f); + setAllGainBands(rack, 0); + rack.prepareToPlay(sampleRate, 1024); + + StageCapture capture; + capture.audio.setSize(2, totalSamples, false, true, false); + std::array previous {}; + std::array hasPrevious {}; + int cursor = 0; + int eventIndex = 0; + int nextAlignedEvent = 0; + bool neutralTargetApplied = false; + size_t partitionIndex = 0; + while (cursor < totalSamples) + { + if (retargetEveryCallback && cursor < automationEndSample) + { + setAllGainBands(rack, eventIndex++); + } + else if (! retargetEveryCallback + && cursor == nextAlignedEvent + && cursor < automationEndSample) + { + setAllGainBands(rack, eventIndex++); + nextAlignedEvent = juce::jmin( + automationEndSample, + cursor + alignedAutomationStep); + } + if (cursor >= automationEndSample + && ! neutralTargetApplied) + { + setAllGainBandsNeutral(rack); + neutralTargetApplied = true; + nextAlignedEvent = totalSamples; + } + + int blockSamples = juce::jmin( + partitions[partitionIndex % partitions.size()], + totalSamples - cursor); + if (! retargetEveryCallback + && nextAlignedEvent > cursor) + { + blockSamples = juce::jmin( + blockSamples, + nextAlignedEvent - cursor); + } + if (! neutralTargetApplied + && cursor < automationEndSample) + { + blockSamples = juce::jmin( + blockSamples, + automationEndSample - cursor); + } + + juce::AudioBuffer block(2, blockSamples); + for (int sample = 0; sample < blockSamples; ++sample) + { + const int absoluteSample = cursor + sample; + const double time = + static_cast(absoluteSample) / sampleRate; + const float transient = absoluteSample % 4096 < 20 + ? 0.07f * static_cast( + 20 - absoluteSample % 4096) / 20.0f + : 0.0f; + const float value = transient + + 0.060f * static_cast(std::sin( + juce::MathConstants::twoPi + * 41.2034 * time)) + + 0.040f * static_cast(std::sin( + juce::MathConstants::twoPi + * 82.4069 * time)) + + 0.025f * static_cast(std::sin( + juce::MathConstants::twoPi + * 329.6276 * time)); + block.setSample(0, sample, value); + block.setSample(1, sample, value); + } + + rack.latchInstrumentProfileForBlock(); + rack.processPreEQ(block); + rack.processAmpToneStack(block); + rack.processGraphicEQ(block); + rack.releaseInstrumentProfileForBlock(); + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < blockSamples; ++sample) + { + const float output = block.getSample(channel, sample); + if (! std::isfinite(output)) + { + ++capture.nonFiniteSamples; + continue; + } + capture.maximumPeak = juce::jmax( + capture.maximumPeak, std::abs(output)); + if (hasPrevious[static_cast(channel)]) + { + capture.maximumAdjacentDelta = juce::jmax( + capture.maximumAdjacentDelta, + std::abs( + output + - previous[static_cast(channel)])); + } + previous[static_cast(channel)] = output; + hasPrevious[static_cast(channel)] = true; + } + capture.audio.copyFrom( + channel, cursor, block, channel, 0, blockSamples); + } + cursor += blockSamples; + ++partitionIndex; + } + capture.toneRecoveries = + rack.diagnosticAmpToneFilterRecoveryCount.load(); + capture.preEqRecoveries = + rack.diagnosticPreEqFilterRecoveryCount.load(); + capture.graphicEqRecoveries = + rack.diagnosticGraphicEqFilterRecoveryCount.load(); + rack.releaseResources(); + return capture; + }; + const auto captureHealthy = [] (const StageCapture& capture) + { + return capture.nonFiniteSamples == 0 + && capture.maximumPeak < 64.0f + && capture.maximumAdjacentDelta < 16.0f + && capture.toneRecoveries == 0 + && capture.preEqRecoveries == 0 + && capture.graphicEqRecoveries == 0; + }; + const auto maximumDifference = [] ( + const juce::AudioBuffer& first, + const juce::AudioBuffer& second) + { + float difference = 0.0f; + for (int channel = 0; channel < first.getNumChannels(); ++channel) + { + for (int sample = 0; sample < first.getNumSamples(); ++sample) + { + difference = juce::jmax( + difference, + std::abs( + first.getSample(channel, sample) + - second.getSample(channel, sample))); + } + } + return difference; + }; + + constexpr std::array sampleRates { + 44100.0, 48000.0, 96000.0 + }; + constexpr std::array callbackSizes { 8, 64, 512, 1024 }; + juce::Array rateResults; + bool stageMatrixPass = true; + for (const double sampleRate : sampleRates) + { + const auto reference = renderStages( + sampleRate, { 8 }, false); + const auto fixed64 = renderStages( + sampleRate, { 64 }, false); + const auto fixed512 = renderStages( + sampleRate, { 512 }, false); + const auto fixed1024 = renderStages( + sampleRate, { 1024 }, false); + const auto uneven = renderStages( + sampleRate, { 3, 17, 5, 257, 11, 1024, 29, 64 }, false); + const float partitionError = juce::jmax( + maximumDifference(reference.audio, fixed64.audio), + maximumDifference( + reference.audio, + fixed512.audio), + maximumDifference( + reference.audio, + fixed1024.audio), + maximumDifference(reference.audio, uneven.audio)); + + bool callbackStressPass = true; + float callbackStressPeak = 0.0f; + float callbackStressDelta = 0.0f; + for (const int callbackSize : callbackSizes) + { + const auto stressed = renderStages( + sampleRate, { callbackSize }, true); + callbackStressPass = callbackStressPass + && captureHealthy(stressed); + callbackStressPeak = juce::jmax( + callbackStressPeak, stressed.maximumPeak); + callbackStressDelta = juce::jmax( + callbackStressDelta, + stressed.maximumAdjacentDelta); + } + const bool ratePass = captureHealthy(reference) + && captureHealthy(fixed64) + && captureHealthy(fixed512) + && captureHealthy(fixed1024) + && captureHealthy(uneven) + && partitionError <= 2.0e-6f + && callbackStressPass; + stageMatrixPass = stageMatrixPass && ratePass; + auto* rateValue = new juce::DynamicObject(); + rateValue->setProperty("sampleRate", sampleRate); + rateValue->setProperty("partitionMaximumError", partitionError); + rateValue->setProperty( + "sampleAlignedMaximumPeak", reference.maximumPeak); + rateValue->setProperty( + "sampleAlignedMaximumAdjacentDelta", + reference.maximumAdjacentDelta); + rateValue->setProperty( + "callbackStressMaximumPeak", callbackStressPeak); + rateValue->setProperty( + "callbackStressMaximumAdjacentDelta", callbackStressDelta); + rateValue->setProperty("pass", ratePass); + rateResults.add(juce::var(rateValue)); + } + + auto* realModelValue = new juce::DynamicObject(); + const bool realModelAvailable = calibratedModelFile.existsAsFile(); + realModelValue->setProperty("available", realModelAvailable); + realModelValue->setProperty( + "modelPath", calibratedModelFile.getFullPathName()); + bool realModelPass = ! realModelAvailable; + if (realModelAvailable) + { + constexpr double hostSampleRate = 44100.0; + constexpr int blockSize = 8; + const int totalSamples = juce::roundToInt( + hostSampleRate * 0.70); + const int automationStart = juce::roundToInt( + hostSampleRate * 0.10); + const int automationEnd = juce::roundToInt( + hostSampleRate * 0.48); + const int audibleTailStart = juce::roundToInt( + hostSampleRate * 0.60); + OpenStudioNAMRack rack; + configureNeutralRack(rack); + rack.instrumentProfile.store( + static_cast(OpenStudioNAMRack::bassInstrumentProfile)); + rack.ampEnabled.store(1.0f); + rack.ampMix.store(1.0f); + rack.bassDb.store(0.0f); + rack.prepareToPlay(hostSampleRate, blockSize); + const bool modelLoaded = rack.loadAmpModel( + calibratedModelFile.getFullPathName()); + juce::AudioBuffer block(2, blockSize); + juce::MidiBuffer midi; + int nonFiniteSamples = 0; + float maximumPeak = 0.0f; + float maximumAdjacentDelta = 0.0f; + std::array previous {}; + std::array hasPrevious {}; + double audibleTailSquares = 0.0; + int audibleTailSamples = 0; + int automationEvent = 0; + for (int cursor = 0; cursor < totalSamples; cursor += blockSize) + { + if (cursor >= automationStart && cursor < automationEnd) + { + const int phase = automationEvent++ % 97; + const float rapidBassDb = phase <= 48 + ? -12.0f + static_cast(phase) * 0.5f + : 12.0f - static_cast(phase - 48) * 0.5f; + rack.bassDb.store(rapidBassDb); + } + else if (cursor == automationEnd) + { + rack.bassDb.store(12.0f); + } + for (int sample = 0; sample < blockSize; ++sample) + { + const int absoluteSample = cursor + sample; + const double time = + static_cast(absoluteSample) / hostSampleRate; + const float value = + 0.14f * static_cast(std::sin( + juce::MathConstants::twoPi + * 41.2034 * time)) + + 0.08f * static_cast(std::sin( + juce::MathConstants::twoPi + * 82.4069 * time)); + block.setSample(0, sample, value); + block.setSample(1, sample, value); + } + midi.clear(); + rack.processBlock(block, midi); + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < blockSize; ++sample) + { + const float output = block.getSample(channel, sample); + if (! std::isfinite(output)) + { + ++nonFiniteSamples; + continue; + } + maximumPeak = juce::jmax( + maximumPeak, std::abs(output)); + if (hasPrevious[static_cast(channel)]) + { + maximumAdjacentDelta = juce::jmax( + maximumAdjacentDelta, + std::abs( + output + - previous[static_cast(channel)])); + } + previous[static_cast(channel)] = output; + hasPrevious[static_cast(channel)] = true; + if (cursor + sample >= audibleTailStart) + { + audibleTailSquares += + static_cast(output) * output; + ++audibleTailSamples; + } + } + } + } + const float audibleTailRms = static_cast(std::sqrt( + audibleTailSquares + / static_cast(juce::jmax(1, audibleTailSamples)))); + const auto diagnostics = rack.getDiagnosticState(); + const auto emergencyTrips = static_cast( + diagnostics.getProperty( + "rackOutputEmergencyMuteTripCount", + static_cast(-1))); + const auto toneRecoveries = static_cast( + diagnostics.getProperty( + "ampToneFilterRecoveryCount", + static_cast(-1))); + const int modelProcessFailures = static_cast( + diagnostics.getProperty("modelProcessFailCount", -1)); + const bool resampled = static_cast( + diagnostics.getProperty("lastResampled", false)); + const bool modelFaulted = static_cast( + diagnostics.getProperty("ampModelFaulted", true)); + realModelPass = modelLoaded + && resampled + && ! modelFaulted + && modelProcessFailures == 0 + && emergencyTrips == 0 + && toneRecoveries == 0 + && nonFiniteSamples == 0 + && maximumPeak <= OpenStudioNAMRack::namOutputSafetyCeiling + && maximumAdjacentDelta < 1.5f + && audibleTailRms > 1.0e-5f; + realModelValue->setProperty("modelLoaded", modelLoaded); + realModelValue->setProperty("hostSampleRate", hostSampleRate); + realModelValue->setProperty("callbackSize", blockSize); + realModelValue->setProperty("lastResampled", resampled); + realModelValue->setProperty("modelFaulted", modelFaulted); + realModelValue->setProperty( + "modelProcessFailCount", modelProcessFailures); + realModelValue->setProperty( + "emergencyMuteTripCount", emergencyTrips); + realModelValue->setProperty( + "toneFilterRecoveryCount", toneRecoveries); + realModelValue->setProperty( + "nonFiniteSamples", nonFiniteSamples); + realModelValue->setProperty("maximumPeak", maximumPeak); + realModelValue->setProperty( + "maximumAdjacentDelta", maximumAdjacentDelta); + realModelValue->setProperty("audibleTailRms", audibleTailRms); + realModelValue->setProperty("pass", realModelPass); + rack.releaseResources(); + } + + auto* value = new juce::DynamicObject(); + value->setProperty("rates", rateResults); + value->setProperty("stageMatrixPass", stageMatrixPass); + value->setProperty( + "real48kModelAt44100", + juce::var(realModelValue)); + value->setProperty("realModelAvailable", realModelAvailable); + value->setProperty("realModelPass", realModelPass); + value->setProperty( + "pass", stageMatrixPass && realModelPass); + return juce::var(value); + }; + const auto stableBiquadAutomationProbe = + runStableBiquadAutomationProbe(); + const bool stableBiquadRealModelAvailable = static_cast( + stableBiquadAutomationProbe.getProperty( + "realModelAvailable", false)); + addCheck( + checks, + "nam_rack_stable_biquad_rapid_automation_contract", + ! stableBiquadRealModelAvailable + ? "not_asserted" + : (stableBiquadAutomationProbe.getProperty("pass", false) + ? "pass" + : "fail"), + "Amp tone, PRE EQ, and Graphic EQ must switch only between complete stable biquad coefficient sets, coalesce rapid full-range retargets without a queue, remain finite and callback-partition invariant at 44.1/48/96 kHz and 8/64/512/1024 samples, and never invoke local recovery for valid controls. A real 48 kHz bass-amp NAM at a 44.1 kHz host additionally receives a continuous Bass -12 to +12 dB live sweep at exact 8-sample callbacks without a model fault, emergency mute, silent latch, or filter recovery.", + stableBiquadAutomationProbe); + auto runDualMonoNAMGraphContractProbe = [&] () { auto* value = new juce::DynamicObject(); @@ -36500,7 +37170,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int testBlockSize = 8; constexpr int totalSamples = 4096; const auto configureModelRack = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, bool loadPedal) { rack.prepareToPlay( @@ -36532,7 +37202,7 @@ juce::var AudioEngine::runNAMRackRegression() return pedalLoaded && ampLoaded; }; - S13NAMRack ownershipRack; + OpenStudioNAMRack ownershipRack; const bool ownershipLoaded = configureModelRack(ownershipRack, true); const auto* const pedalPrimary = @@ -36570,7 +37240,7 @@ juce::var AudioEngine::runNAMRackRegression() && pedalSecondary != ampSecondary && ampPrimary != ampSecondary; - S13NAMRack emptyRouteRack; + OpenStudioNAMRack emptyRouteRack; emptyRouteRack.prepareToPlay( testSampleRate, testBlockSize); configureNeutralRack(emptyRouteRack); @@ -36603,7 +37273,7 @@ juce::var AudioEngine::runNAMRackRegression() "effectiveInputRoutingMode", -1)) == 0; const auto render = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, bool antiPhase, bool identicalStereo, const std::array& partitions, @@ -36704,8 +37374,8 @@ juce::var AudioEngine::runNAMRackRegression() return measurement; }; - S13NAMRack dualAntiRack; - S13NAMRack singleAntiRack; + OpenStudioNAMRack dualAntiRack; + OpenStudioNAMRack singleAntiRack; const bool antiRacksLoaded = configureModelRack(dualAntiRack, false) && configureModelRack(singleAntiRack, false); @@ -36773,7 +37443,7 @@ juce::var AudioEngine::runNAMRackRegression() leftOnlyPeak > 1.0e-4f && rightLaneIsolationDifference <= 1.0e-7f; - S13NAMRack identicalRack; + OpenStudioNAMRack identicalRack; const bool identicalLoaded = configureModelRack(identicalRack, false); const auto identicalOutput = render( @@ -36789,8 +37459,8 @@ juce::var AudioEngine::runNAMRackRegression() && identicalMeasurement[2] == 0.0 && identicalMeasurement[1] <= 1.0e-6; - S13NAMRack pausedDoublerReferenceRack; - S13NAMRack pausedDoublerStereoRack; + OpenStudioNAMRack pausedDoublerReferenceRack; + OpenStudioNAMRack pausedDoublerStereoRack; const bool pausedDoublerRackLoaded = configureModelRack( pausedDoublerReferenceRack, false) @@ -36877,8 +37547,8 @@ juce::var AudioEngine::runNAMRackRegression() std::memory_order_relaxed) - 5.25f) <= 1.0e-6f; - S13NAMRack fixedRack; - S13NAMRack unevenRack; + OpenStudioNAMRack fixedRack; + OpenStudioNAMRack unevenRack; const bool partitionRacksLoaded = configureModelRack(fixedRack, false) && configureModelRack(unevenRack, false); @@ -36967,9 +37637,9 @@ juce::var AudioEngine::runNAMRackRegression() std::atomic& faultFlag; }; - S13NAMRack runtimeFaultRack; - S13NAMRack runtimeFaultDryReferenceRack; - S13NAMRack runtimeFaultWetControlRack; + OpenStudioNAMRack runtimeFaultRack; + OpenStudioNAMRack runtimeFaultDryReferenceRack; + OpenStudioNAMRack runtimeFaultWetControlRack; const bool runtimeFaultRacksLoaded = configureModelRack(runtimeFaultRack, false) && configureModelRack( @@ -37148,7 +37818,7 @@ juce::var AudioEngine::runNAMRackRegression() > 1.0e-5f; const auto renderModeChange = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, const std::array& partitions) { rack.setRoutedInputChannelCount(1); @@ -37315,7 +37985,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto savedStateTree = juce::ValueTree::readFromData( savedState.getData(), savedState.getSize()); - S13NAMRack restoredRack; + OpenStudioNAMRack restoredRack; restoredRack.prepareToPlay( testSampleRate, testBlockSize); const bool restored = @@ -37353,7 +38023,7 @@ juce::var AudioEngine::runNAMRackRegression() ->dualMonoLane.get() == originalAmpLane; - S13NAMRack singleLaneFallbackRack; + OpenStudioNAMRack singleLaneFallbackRack; const bool singleLaneFallbackLoaded = configureModelRack( singleLaneFallbackRack, false); @@ -37419,7 +38089,7 @@ juce::var AudioEngine::runNAMRackRegression() "ampDualMonoWarning", juce::var()).toString() .isNotEmpty(); - S13NAMRack routedChannelGuardRack; + OpenStudioNAMRack routedChannelGuardRack; const bool routedChannelGuardLoaded = configureModelRack( routedChannelGuardRack, false); @@ -37515,7 +38185,7 @@ juce::var AudioEngine::runNAMRackRegression() std::memory_order_relaxed) - 0.63f) <= 1.0e-6f; - S13NAMRack timingRack; + OpenStudioNAMRack timingRack; const bool timingRackLoaded = configureModelRack(timingRack, false); const auto measureEightSampleProcessing = [&] ( @@ -37752,7 +38422,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::Array cases; bool allCasesPass = true; const auto configure = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, int blockSize, int routedChannels) { @@ -37783,8 +38453,8 @@ juce::var AudioEngine::runNAMRackRegression() { for (const int routedChannels : { 1, 2 }) { - S13NAMRack transitionRack; - S13NAMRack bypassRack; + OpenStudioNAMRack transitionRack; + OpenStudioNAMRack bypassRack; const bool transitionLoaded = configure( transitionRack, blockSize, routedChannels); const bool bypassLoaded = configure( @@ -37917,7 +38587,7 @@ juce::var AudioEngine::runNAMRackRegression() && rackGuardHits == 0 && lateRms > 1.0e-5 && earlyToLate > 0.16 - && maximumPeak < S13NAMRack::namOutputSafetyCeiling + && maximumPeak < OpenStudioNAMRack::namOutputSafetyCeiling && maximumAdjacentDelta < 0.45f; allCasesPass = allCasesPass && casePass; auto* caseValue = new juce::DynamicObject(); @@ -37958,7 +38628,7 @@ juce::var AudioEngine::runNAMRackRegression() for (const bool distortion : { false, true }) { const auto configureCurrentDriveCase = [&] ( - S13NAMRack& target, + OpenStudioNAMRack& target, bool precisionOn, bool distortionOn) { @@ -37974,9 +38644,9 @@ juce::var AudioEngine::runNAMRackRegression() target.chaosDrive.store(0.70f); target.chaosMix.store(1.0f); }; - S13NAMRack preFirstCallbackRack; - S13NAMRack preFirstActiveReferenceRack; - S13NAMRack preFirstBypassReferenceRack; + OpenStudioNAMRack preFirstCallbackRack; + OpenStudioNAMRack preFirstActiveReferenceRack; + OpenStudioNAMRack preFirstBypassReferenceRack; configureCurrentDriveCase( preFirstCallbackRack, ! distortion, distortion); configureCurrentDriveCase( @@ -38066,7 +38736,7 @@ juce::var AudioEngine::runNAMRackRegression() } } } - S13NAMRack rack; + OpenStudioNAMRack rack; configureCurrentDriveCase(rack, ! distortion, distortion); rack.prepareToPlay(48000.0, 8); rack.reset(); @@ -38216,7 +38886,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int bypassSamples = 4096; constexpr int captureSamples = 6144; const auto configureCase = [&] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, bool precisionOn, bool distortionOn) { @@ -38258,8 +38928,8 @@ juce::var AudioEngine::runNAMRackRegression() bool allPass = true; for (const bool oldPedalWasDistortion : { false, true }) { - S13NAMRack sequenced; - S13NAMRack freshOtherOnly; + OpenStudioNAMRack sequenced; + OpenStudioNAMRack freshOtherOnly; configureCase( sequenced, ! oldPedalWasDistortion, @@ -38436,7 +39106,7 @@ juce::var AudioEngine::runNAMRackRegression() float& outputPeak, int& reportedLatency) { - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(1); rack.ampEnabled.store(1.0f); @@ -38745,7 +39415,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runNAMTonePresetContractProbe = [&] () { auto* value = new juce::DynamicObject(); - S13NAMRack source; + OpenStudioNAMRack source; source.prepareToPlay( fixtureSampleRate, fixtureBlockSize); @@ -38830,10 +39500,6 @@ juce::var AudioEngine::runNAMRackRegression() source.cabHPFHz.store(91.0f); source.cabLPFHz.store(11200.0f); source.cabPhaseInvert.store(1.0f); - source.cabMicPosition.store(0.61f); - source.cabMicDistance.store(0.29f); - source.cabMicBlend.store(0.72f); - source.cabRoomSend.store(0.18f); source.cabPan.store(-0.23f); source.cabRoomEnabled.store(1.0f); source.cabRoomAmount.store(0.41f); @@ -38872,7 +39538,7 @@ juce::var AudioEngine::runNAMRackRegression() source.reverbEnabled.store(1.0f); source.reverbEngineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion)); + OpenStudioNAMRack::currentReverbEngineVersion)); source.outputTrimDb.store(-2.25f); source.calibrationReferenceDbu.store(9.25f); source.auditionSource.store(1.0f); @@ -39004,7 +39670,7 @@ juce::var AudioEngine::runNAMRackRegression() && exclusionsExact && rootPropertySetExact; - S13NAMRack restored; + OpenStudioNAMRack restored; restored.prepareToPlay( fixtureSampleRate, fixtureBlockSize); @@ -39049,7 +39715,7 @@ juce::var AudioEngine::runNAMRackRegression() .reverbEngineVersion .load() - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 1.0e-6f && restored.reverbPad.load( std::memory_order_relaxed) >= 0.5f; @@ -39279,15 +39945,12 @@ juce::var AudioEngine::runNAMRackRegression() { "pedalOverrideOutputLevelDbu", "Pedal Calibration Output", ControlFamily::pedalCalibration, 10.0f, 14.0f }, { "pedalCalibrationMode", "Pedal Calibration Mode", ControlFamily::pedalCalibration, 0.0f, 2.0f, true, "selector" }, - // Cabinet IR, shaping, room, and routing. + // Cab V3 literal IR operations, room, and routing. Retired Cab + // filters migrate into Graphic EQ and are not live controls. { "cabLevelDb", "Cab Level", ControlFamily::cab, -3.0f, 3.0f }, - { "cabHPFHz", "Cab High Pass", ControlFamily::cab, 120.0f, 180.0f }, - { "cabLPFHz", "Cab Low Pass", ControlFamily::cab, 8000.0f, 6000.0f }, + { "cabIRStereo", "Cab Stereo IR", ControlFamily::cab, 0.0f, 1.0f, true }, + { "cabDirectMix", "Cab Bass Direct Mix", ControlFamily::cab, 0.20f, 0.55f }, { "cabPhaseInvert", "Cab Phase", ControlFamily::cab, 0.0f, 1.0f, true }, - { "cabMicPosition", "Cab Tone Edge", ControlFamily::cab, 0.30f, 0.70f }, - { "cabMicDistance", "Cab Tone Damp", ControlFamily::cab, 0.20f, 0.60f }, - { "cabMicBlend", "Cab Shaper Blend", ControlFamily::cab, 0.30f, 0.70f }, - { "cabRoomSend", "Cab Low Bloom", ControlFamily::cab, 0.15f, 0.45f }, { "cabPan", "Cab Pan", ControlFamily::cab, -0.25f, 0.25f }, { "cabRoomEnabled", "Room Power", ControlFamily::cabSpace, 1.0f, 0.0f, true }, { "cabRoomAmount", "Room Amount", ControlFamily::cabSpace, 0.25f, 0.70f }, @@ -39368,7 +40031,7 @@ juce::var AudioEngine::runNAMRackRegression() }; auto configureForFamily = - [&] (S13NAMRack& rack, + [&] (OpenStudioNAMRack& rack, ControlFamily family) { configureNeutralRack(rack); @@ -39484,15 +40147,14 @@ juce::var AudioEngine::runNAMRackRegression() .store(12.0f); break; case ControlFamily::cab: + rack.instrumentProfile.store( + static_cast( + OpenStudioNAMRack::bassInstrumentProfile)); rack.setCabRequestedEnabled(true); rack.cabLevelDb.store(0.0f); - rack.cabHPFHz.store(80.0f); - rack.cabLPFHz.store(8500.0f); + rack.cabIRStereo.store(0.0f); + rack.cabDirectMix.store(0.35f); rack.cabPhaseInvert.store(0.0f); - rack.cabMicPosition.store(0.50f); - rack.cabMicDistance.store(0.25f); - rack.cabMicBlend.store(0.50f); - rack.cabRoomSend.store(0.20f); rack.cabPan.store(0.0f); break; case ControlFamily::cabSpace: @@ -39542,7 +40204,7 @@ juce::var AudioEngine::runNAMRackRegression() }; auto prepareCaseRack = - [&] (S13NAMRack& rack, + [&] (OpenStudioNAMRack& rack, const ControlCase& controlCase, bool& resourceLoaded) { @@ -39846,8 +40508,8 @@ juce::var AudioEngine::runNAMRackRegression() } }; - S13NAMRack testRack; - S13NAMRack referenceRack; + OpenStudioNAMRack testRack; + OpenStudioNAMRack referenceRack; bool testResourceLoaded = true; bool referenceResourceLoaded = true; const bool testPrepared = prepareCaseRack( @@ -40328,8 +40990,8 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack testRack; - S13NAMRack referenceRack; + OpenStudioNAMRack testRack; + OpenStudioNAMRack referenceRack; bool testResourceLoaded = true; bool referenceResourceLoaded = true; const bool testPrepared = @@ -40753,7 +41415,7 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto setBandGain = [] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, int band, float gainDb) { @@ -40780,7 +41442,7 @@ juce::var AudioEngine::runNAMRackRegression() double frequencyHz) { constexpr int blockSize = 256; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.eqEnabled.store(1.0f); rack.eqLevelDb.store(levelDb); setBandGain(rack, band, bandGainDb); @@ -40854,7 +41516,7 @@ juce::var AudioEngine::runNAMRackRegression() double frequencyHz) { constexpr int blockSize = 256; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.eqEnabled.store(1.0f); rack.eqHPFHz.store(hpfHz); rack.eqLPFHz.store(lpfHz); @@ -40950,7 +41612,7 @@ juce::var AudioEngine::runNAMRackRegression() } }; - const auto configureCurve = [] (S13NAMRack& rack) + const auto configureCurve = [] (OpenStudioNAMRack& rack) { rack.eqEnabled.store(1.0f); rack.eq65Db.store(-4.0f); @@ -41037,7 +41699,7 @@ juce::var AudioEngine::runNAMRackRegression() source, 0, sampleRate, false, false); juce::AudioBuffer bypassed; bypassed.makeCopyOf(source, true); - S13NAMRack bypassRack; + OpenStudioNAMRack bypassRack; configureCurve(bypassRack); bypassRack.eqEnabled.store(0.0f); bypassRack.eqLevelDb.store(12.0f); @@ -41045,7 +41707,7 @@ juce::var AudioEngine::runNAMRackRegression() bypassRack.processGraphicEQ(bypassed); juce::AudioBuffer flatEnabled; flatEnabled.makeCopyOf(source, true); - S13NAMRack flatRack; + OpenStudioNAMRack flatRack; flatRack.eqEnabled.store(1.0f); flatRack.eqLevelDb.store(0.0f); flatRack.prepareToPlay(sampleRate, nullSamples); @@ -41076,7 +41738,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::AudioBuffer isolated(2, nullSamples); fillDeterministicStereo( isolated, 0, sampleRate, true, false); - S13NAMRack isolationRack; + OpenStudioNAMRack isolationRack; configureCurve(isolationRack); isolationRack.prepareToPlay(sampleRate, nullSamples); isolationRack.processGraphicEQ(isolated); @@ -41090,7 +41752,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::AudioBuffer identical(2, nullSamples); fillDeterministicStereo( identical, 0, sampleRate, false, true); - S13NAMRack parityRack; + OpenStudioNAMRack parityRack; configureCurve(parityRack); parityRack.prepareToPlay(sampleRate, nullSamples); parityRack.processGraphicEQ(identical); @@ -41112,7 +41774,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int totalSamples = 8192; juce::AudioBuffer capture(2, totalSamples); capture.clear(); - S13NAMRack rack; + OpenStudioNAMRack rack; configureCurve(rack); rack.prepareToPlay(sampleRate, 512); int cursor = 0; @@ -41162,7 +41824,7 @@ juce::var AudioEngine::runNAMRackRegression() } const bool partitionPass = partitionError <= 1.0e-7f; - S13NAMRack headroomRack; + OpenStudioNAMRack headroomRack; headroomRack.inputTrimDb.store(0.0f); headroomRack.setRoutedInputChannelCount(1); headroomRack.outputTrimDb.store(0.0f); @@ -41311,7 +41973,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runNAMRackGraphicEqAutomationCurveProbe = [] () { - S13NAMRack rack; + OpenStudioNAMRack rack; OpenStudioBuiltInAutomationDescriptor hpfDescriptor; OpenStudioBuiltInAutomationDescriptor lpfDescriptor; const bool descriptorsPresent = @@ -41463,7 +42125,7 @@ juce::var AudioEngine::runNAMRackRegression() 120.0f, 250.0f, 500.0f, 1000.0f, 2500.0f, 5000.0f, 8000.0f, 12000.0f }; - const auto setBand = [] (S13NAMRack& rack, int band, float value) + const auto setBand = [] (OpenStudioNAMRack& rack, int band, float value) { switch (band) { @@ -41486,7 +42148,7 @@ juce::var AudioEngine::runNAMRackRegression() float lpfHz) { constexpr int blockSize = 256; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); if (activeBand >= 0) setBand(rack, activeBand, 6.0f); @@ -41638,7 +42300,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr double sampleRate = 48000.0; constexpr int blockSize = 257; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(enabled ? 1.0f : 0.0f); rack.preEq120Db.store(shaped ? 6.0f : 0.0f); rack.preEqHPFHz.store(shaped ? 100.0f : 0.0f); @@ -41666,7 +42328,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr double sampleRate = 48000.0; constexpr int blockSize = 128; - S13NAMRack leakageRack; + OpenStudioNAMRack leakageRack; leakageRack.preEqEnabled.store(1.0f); leakageRack.preEq1kDb.store(6.0f); leakageRack.prepareToPlay(sampleRate, blockSize); @@ -41683,7 +42345,7 @@ juce::var AudioEngine::runNAMRackRegression() } leakageRack.releaseResources(); - S13NAMRack parityRack; + OpenStudioNAMRack parityRack; parityRack.preEqEnabled.store(1.0f); parityRack.preEq1kDb.store(6.0f); parityRack.preEqHPFHz.store(70.0f); @@ -41709,7 +42371,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr double sampleRate = 48000.0; constexpr int totalSamples = 8192; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); rack.preEq120Db.store(-3.0f); rack.preEq1kDb.store(4.0f); @@ -41752,7 +42414,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr double sampleRate = 48000.0; constexpr int blockSize = 128; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); rack.prepareToPlay(sampleRate, blockSize); for (int blockIndex = 0; blockIndex < 96; ++blockIndex) @@ -41790,7 +42452,7 @@ juce::var AudioEngine::runNAMRackRegression() rack.releaseResources(); } - S13NAMRack automationRack; + OpenStudioNAMRack automationRack; OpenStudioBuiltInAutomationDescriptor hpfDescriptor; OpenStudioBuiltInAutomationDescriptor lpfDescriptor; const bool descriptorsPresent = @@ -41902,7 +42564,7 @@ juce::var AudioEngine::runNAMRackRegression() 17, 511, 3, 229, 41, 128, 7, 383, 61 }; const auto setBand = [] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, int band, float value) noexcept { @@ -41919,7 +42581,7 @@ juce::var AudioEngine::runNAMRackRegression() default: break; } }; - const auto setTightCurve = [] (S13NAMRack& rack) noexcept + const auto setTightCurve = [] (OpenStudioNAMRack& rack) noexcept { rack.preEqEnabled.store(1.0f); rack.preEq120Db.store(-4.0f); @@ -41945,7 +42607,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int preparedBlockSize = 512; constexpr int totalSamples = 32768; constexpr float impulseAmplitude = 0.25f; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); setBand(rack, activeBand, bandGainDb); rack.prepareToPlay(sampleRate, preparedBlockSize); @@ -42029,7 +42691,7 @@ juce::var AudioEngine::runNAMRackRegression() int silenceNonFiniteSamples = 0; { constexpr int totalSamples = 8192; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); for (int band = 0; band < static_cast(bandFrequencies.size()); @@ -42073,7 +42735,7 @@ juce::var AudioEngine::runNAMRackRegression() float flatNullError = 0.0f; { constexpr int totalSamples = 8192; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); rack.prepareToPlay(sampleRate, 512); int cursor = 0; @@ -42130,7 +42792,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr int blockSize = 257; const int totalSamples = juce::roundToInt(sampleRate * 0.50); - S13NAMRack shapedRack; + OpenStudioNAMRack shapedRack; shapedRack.preEqEnabled.store(1.0f); shapedRack.preEq120Db.store(12.0f); shapedRack.preEq250Db.store(-12.0f); @@ -42142,7 +42804,7 @@ juce::var AudioEngine::runNAMRackRegression() shapedRack.preEq12kDb.store(-3.0f); shapedRack.prepareToPlay(sampleRate, blockSize); - S13NAMRack highPassedRack; + OpenStudioNAMRack highPassedRack; highPassedRack.preEqEnabled.store(1.0f); highPassedRack.preEqHPFHz.store(80.0f); highPassedRack.prepareToPlay(sampleRate, blockSize); @@ -42230,7 +42892,7 @@ juce::var AudioEngine::runNAMRackRegression() int& nonFiniteSamples) { constexpr int totalSamples = 16384; - S13NAMRack rack; + OpenStudioNAMRack rack; setTightCurve(rack); rack.prepareToPlay(sampleRate, 512); capture.setSize(2, totalSamples, false, true, false); @@ -42348,7 +43010,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int sourceSamples = 12288; constexpr int automationSamples = 16384; constexpr int tailMeasureStart = 28672; - S13NAMRack rack; + OpenStudioNAMRack rack; rack.preEqEnabled.store(1.0f); rack.prepareToPlay(sampleRate, 512); int cursor = 0; @@ -42547,7 +43209,7 @@ juce::var AudioEngine::runNAMRackRegression() const std::vector& partitions, juce::AudioBuffer& capture) { - S13NAMRack rack; + OpenStudioNAMRack rack; rack.eqEnabled.store(1.0f); rack.prepareToPlay(sampleRate, preparedBlockSize); capture.setSize(2, totalSamples, false, true, false); @@ -42817,7 +43479,7 @@ juce::var AudioEngine::runNAMRackRegression() // The standalone instance is configured to the same bounded V3 // tank used by the rack so pre-delay, wet filtering, and shimmer - // transition guards are covered independently of S13NAMRack. + // transition guards are covered independently of OpenStudioNAMRack. { "wetLevel", "Reverb Wet", StandaloneFamily::reverb, 0.30f, 0.75f }, { "dryLevel", "Reverb Dry", StandaloneFamily::reverb, 0.75f, 0.35f }, { "preDelay", "Reverb Pre-delay", StandaloneFamily::reverb, 12.0f, 55.0f }, @@ -42847,21 +43509,21 @@ juce::var AudioEngine::runNAMRackRegression() switch (family) { case StandaloneFamily::gate: - return std::make_unique(); + return std::make_unique(); case StandaloneFamily::compressor: - return std::make_unique(); + return std::make_unique(); case StandaloneFamily::limiter: - return std::make_unique(); + return std::make_unique(); case StandaloneFamily::delay: - return std::make_unique(); + return std::make_unique(); case StandaloneFamily::chorus: - return std::make_unique(); + return std::make_unique(); case StandaloneFamily::saturator: - return std::make_unique(true); + return std::make_unique(true); case StandaloneFamily::reverb: - return std::make_unique(); + return std::make_unique(); case StandaloneFamily::eq: - return std::make_unique(); + return std::make_unique(); } return {}; @@ -42871,7 +43533,7 @@ juce::var AudioEngine::runNAMRackRegression() [] (juce::AudioProcessor& processor) { if (auto* gate = - dynamic_cast(&processor)) + dynamic_cast(&processor)) { gate->threshold.store(-17.5f); gate->attackMs.store(2.0f); @@ -42887,7 +43549,7 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* compressor = - dynamic_cast( + dynamic_cast( &processor)) { compressor->threshold.store(-22.0f); @@ -42905,7 +43567,7 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* limiter = - dynamic_cast( + dynamic_cast( &processor)) { limiter->threshold.store(-8.0f); @@ -42916,7 +43578,7 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* delay = - dynamic_cast(&processor)) + dynamic_cast(&processor)) { delay->delayTimeL.store(35.0f); delay->delayTimeR.store(47.0f); @@ -42935,12 +43597,12 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* chorus = - dynamic_cast( + dynamic_cast( &processor)) { chorus->mode.store( static_cast( - S13Chorus::Mode::Chorus)); + OpenStudioChorus::Mode::Chorus)); chorus->rate.store(0.35f); chorus->depth.store(0.35f); chorus->fbAmount.store(0.10f); @@ -42948,7 +43610,7 @@ juce::var AudioEngine::runNAMRackRegression() chorus->voices.store(2.0f); chorus->lfoShape.store( static_cast( - S13Chorus::LFOShape::Sine)); + OpenStudioChorus::LFOShape::Sine)); chorus->spread.store(0.65f); chorus->highCut.store(14000.0f); chorus->lowCut.store(30.0f); @@ -42960,12 +43622,12 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* saturator = - dynamic_cast( + dynamic_cast( &processor)) { saturator->satType.store( static_cast( - S13Saturator::SatType::Tape)); + OpenStudioSaturator::SatType::Tape)); saturator->drive.store(8.0f); saturator->mix.store(0.70f); saturator->toneFreq.store(14000.0f); @@ -42977,13 +43639,13 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* reverb = - dynamic_cast( + dynamic_cast( &processor)) { reverb->engineVersion.store(3.0f); reverb->algorithm.store( static_cast( - S13Reverb::Algorithm::Plate)); + OpenStudioReverb::Algorithm::Plate)); reverb->roomSize.store(0.68f); reverb->damping.store(0.48f); reverb->wetLevel.store(0.58f); @@ -43002,7 +43664,7 @@ juce::var AudioEngine::runNAMRackRegression() } if (auto* eq = - dynamic_cast(&processor)) + dynamic_cast(&processor)) { for (auto& band : eq->bands) band.enabled.store(0.0f); @@ -43010,7 +43672,7 @@ juce::var AudioEngine::runNAMRackRegression() eq->bands[0].enabled.store(1.0f); eq->bands[0].type.store( static_cast( - S13EQ::FilterType::LowCut)); + OpenStudioEQ::FilterType::LowCut)); eq->bands[0].freq.store(55.0f); eq->bands[0].q.store(0.71f); eq->bands[0].slope.store(1.0f); @@ -43018,7 +43680,7 @@ juce::var AudioEngine::runNAMRackRegression() eq->bands[3].enabled.store(1.0f); eq->bands[3].type.store( static_cast( - S13EQ::FilterType::Bell)); + OpenStudioEQ::FilterType::Bell)); eq->bands[3].freq.store(850.0f); eq->bands[3].gain.store(3.0f); eq->bands[3].q.store(0.70f); @@ -43038,7 +43700,7 @@ juce::var AudioEngine::runNAMRackRegression() if (parameterId == "shimmerAmount") { if (auto* reverb = - dynamic_cast( + dynamic_cast( &processor)) { reverb->shimmerAmount.store( @@ -43051,7 +43713,7 @@ juce::var AudioEngine::runNAMRackRegression() if (parameterId == "powerEnabled") { if (auto* eq = - dynamic_cast( + dynamic_cast( &processor)) { eq->setPowerEnabled( @@ -43631,7 +44293,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack slimRack; + OpenStudioNAMRack slimRack; slimRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); configureNeutralRack(slimRack); @@ -43845,7 +44507,7 @@ juce::var AudioEngine::runNAMRackRegression() && ! perSlotTree.hasProperty( "namModelSize"); - S13NAMRack emptyStateSource; + OpenStudioNAMRack emptyStateSource; juce::MemoryBlock currentEmptyState; emptyStateSource.getStateInformation( currentEmptyState); @@ -43867,7 +44529,7 @@ juce::var AudioEngine::runNAMRackRegression() legacyState, false); legacyTree.writeToStream(stream); } - S13NAMRack legacyRestoredRack; + OpenStudioNAMRack legacyRestoredRack; legacyRestoredRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); const bool legacyRestored = @@ -43899,7 +44561,7 @@ juce::var AudioEngine::runNAMRackRegression() preSlimState, false); legacyTree.writeToStream(stream); } - S13NAMRack preSlimRestoredRack; + OpenStudioNAMRack preSlimRestoredRack; preSlimRestoredRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); const bool preSlimRestored = @@ -43973,7 +44635,7 @@ juce::var AudioEngine::runNAMRackRegression() requestedBenchmarkModelPath.isNotEmpty(); const juce::File lowBlockBenchmarkModel = benchmarkModelWasRequested ? juce::File(requestedBenchmarkModelPath) - : a2ModelFile; + : a1ModelFile; const auto requestedBenchmarkSampleRateText = juce::SystemStats::getEnvironmentVariable( "OPENSTUDIO_NAM_BENCHMARK_SAMPLE_RATE", {}).trim(); @@ -44001,7 +44663,7 @@ juce::var AudioEngine::runNAMRackRegression() "modelSource", benchmarkModelWasRequested ? "OPENSTUDIO_NAM_BENCHMARK_MODEL" - : "bundled_A2_fixture"); + : "bundled_stable_A1_fixture"); value->setProperty( "sampleRate", lowBlockBenchmarkSampleRate); value->setProperty( @@ -44017,12 +44679,12 @@ juce::var AudioEngine::runNAMRackRegression() "reason", benchmarkModelWasRequested ? "OPENSTUDIO_NAM_BENCHMARK_MODEL does not point to a file." - : "The bundled A2 fixture was not found."); + : "The bundled stable A1 fixture was not found."); value->setProperty("pass", false); return juce::var(value); } - S13NAMRack benchmarkRack; + OpenStudioNAMRack benchmarkRack; benchmarkRack.prepareToPlay( lowBlockBenchmarkSampleRate, 256); configureNeutralRack(benchmarkRack); @@ -44295,7 +44957,7 @@ juce::var AudioEngine::runNAMRackRegression() : a1ModelFile; const juce::File dualAmpModelFile = dualAmpModelWasRequested ? juce::File(requestedDualAmpModelPath) - : a2ModelFile; + : a1ModelFile; auto runDualNAMSerialDiagnostic = [&] () { constexpr int benchmarkFrames = 32768; @@ -44322,7 +44984,7 @@ juce::var AudioEngine::runNAMRackRegression() "ampModelSource", dualAmpModelWasRequested ? "OPENSTUDIO_NAM_BENCHMARK_AMP_MODEL" - : "bundled_A2_fixture"); + : "bundled_stable_A1_fixture"); value->setProperty("sampleRate", lowBlockBenchmarkSampleRate); const bool modelsAvailable = dualPedalModelFile.existsAsFile() @@ -44337,7 +44999,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack benchmarkRack; + OpenStudioNAMRack benchmarkRack; benchmarkRack.prepareToPlay( lowBlockBenchmarkSampleRate, 256); configureNeutralRack(benchmarkRack); @@ -44722,7 +45384,7 @@ juce::var AudioEngine::runNAMRackRegression() if (! fixturesAvailable) return juce::var(value); - S13NAMRack benchmarkRack; + OpenStudioNAMRack benchmarkRack; benchmarkRack.prepareToPlay( benchmarkSampleRate, benchmarkBlockSize); configureDrivenRack(benchmarkRack); @@ -44752,7 +45414,7 @@ juce::var AudioEngine::runNAMRackRegression() callbackDeadlineMicroseconds); const auto configureCompleteRack = [&] ( - S13NAMRack& target, + OpenStudioNAMRack& target, int routedChannels) { configureDrivenRack(target); @@ -44795,7 +45457,7 @@ juce::var AudioEngine::runNAMRackRegression() target.delayFeedback.store(0.24f); target.reverbEngineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion)); + OpenStudioNAMRack::currentReverbEngineVersion)); target.reverbEnabled.store(1.0f); target.reverbMix.store(0.18f); target.reverbDecaySec.store(2.4f); @@ -45009,7 +45671,7 @@ juce::var AudioEngine::runNAMRackRegression() } const auto settleAmpHandoff = - [&] (S13NAMRack& rack) + [&] (OpenStudioNAMRack& rack) { constexpr double settlingSeconds = 0.06; const int settlingBlocks = @@ -45041,7 +45703,7 @@ juce::var AudioEngine::runNAMRackRegression() 0, R"json("metadata":{"name":"Peavey 5150","gear_make":"Victory","gear_model":"V30 The Countess","gear_type":"amp_cab"},)json")); - S13NAMRack routingRack; + OpenStudioNAMRack routingRack; routingRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); routingRack.setCabRequestedEnabled(true); const bool embeddedLoaded = fixtureWritten && routingRack.loadAmpModel(embeddedCabModel.getFullPathName()); @@ -45074,7 +45736,7 @@ juce::var AudioEngine::runNAMRackRegression() && routingRack.cabEnabled.load() < 0.5f; const auto runRequestedCabRecallRoundTrip = [&] (bool requestedEnabled) { - S13NAMRack recallRack; + OpenStudioNAMRack recallRack; recallRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); if (! recallRack.loadCabIR(irFile.getFullPathName())) return false; @@ -45129,7 +45791,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool requestedOnRecallRoundTrip = runRequestedCabRecallRoundTrip(true); const bool requestedOffRecallRoundTrip = runRequestedCabRecallRoundTrip(false); - S13NAMRack declaredFallbackRack; + OpenStudioNAMRack declaredFallbackRack; declaredFallbackRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); declaredFallbackRack.setCabRequestedEnabled(true); const bool declaredFallbackLoaded = @@ -45153,7 +45815,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryBlock declaredFallbackState; declaredFallbackRack.getTonePresetStateInformation( declaredFallbackState); - S13NAMRack declaredFallbackRestoredRack; + OpenStudioNAMRack declaredFallbackRestoredRack; declaredFallbackRestoredRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); const bool declaredFallbackStateLoaded = @@ -45186,7 +45848,7 @@ juce::var AudioEngine::runNAMRackRegression() && ! declaredFallbackRack.ampModelIncludesCab() && declaredFallbackRack.cabEnabled.load() >= 0.5f; - S13NAMRack samePathFallbackRack; + OpenStudioNAMRack samePathFallbackRack; samePathFallbackRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); samePathFallbackRack.setCabRequestedEnabled(true); @@ -45212,7 +45874,7 @@ juce::var AudioEngine::runNAMRackRegression() && samePathFallbackRack.ampModelIncludesCab() && samePathFallbackRack.cabEnabled.load() < 0.5f; - S13NAMRack metadataPriorityRack; + OpenStudioNAMRack metadataPriorityRack; metadataPriorityRack.prepareToPlay( fixtureSampleRate, fixtureBlockSize); metadataPriorityRack.setCabRequestedEnabled(true); @@ -45280,7 +45942,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(value); } - S13NAMRack calibrationRack; + OpenStudioNAMRack calibrationRack; calibrationRack.prepareToPlay(48000.0, fixtureBlockSize); calibrationRack.inputTrimDb.store(2.5f); calibrationRack.outputTrimDb.store(-1.5f); @@ -45356,7 +46018,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryBlock calibrationState; calibrationRack.getStateInformation(calibrationState); - S13NAMRack restoredCalibrationRack; + OpenStudioNAMRack restoredCalibrationRack; restoredCalibrationRack.prepareToPlay(48000.0, fixtureBlockSize); restoredCalibrationRack.setStateInformation(calibrationState.getData(), static_cast(calibrationState.getSize())); const bool stateRoundTrip = std::abs(restoredCalibrationRack.calibrationReferenceDbu.load() - 10.0f) < 0.0001f @@ -45377,18 +46039,18 @@ juce::var AudioEngine::runNAMRackRegression() && std::abs(calibrationRack.inputTrimDb.load() - 2.5f) < 0.0001f && std::abs(calibrationRack.calibrationReferenceDbu.load() - 7.0f) < 0.0001f; - juce::ValueTree legacyTree("S13NAMRack"); + juce::ValueTree legacyTree("OpenStudioNAMRack"); legacyTree.setProperty("inputTrimDb", 3.0, nullptr); juce::MemoryBlock legacyState; juce::MemoryOutputStream legacyStream(legacyState, false); legacyTree.writeToStream(legacyStream); - S13NAMRack legacyRack; + OpenStudioNAMRack legacyRack; legacyRack.setStateInformation(legacyState.getData(), static_cast(legacyState.getSize())); const bool legacyDefaultsOff = legacyRack.pedalCalibrationMode.load() < 0.5f && legacyRack.ampCalibrationMode.load() < 0.5f && std::abs(legacyRack.inputTrimDb.load() - 3.0f) < 0.0001f; - S13NAMRack missingCalibrationRack; + OpenStudioNAMRack missingCalibrationRack; missingCalibrationRack.prepareToPlay( 48000.0, fixtureBlockSize); const bool missingCalibrationModelLoaded = @@ -45415,7 +46077,7 @@ juce::var AudioEngine::runNAMRackRegression() result.audio.setSize(2, totalSamples); result.audio.clear(); - S13NAMRack dualRack; + OpenStudioNAMRack dualRack; configureNeutralRack(dualRack); dualRack.calibrationReferenceDbu.store(referenceDbu); dualRack.pedalCalibrationMode.store(1.0f); @@ -45650,7 +46312,7 @@ juce::var AudioEngine::runNAMRackRegression() break; const auto file = entry.getFile(); - S13NAMRack localRack; + OpenStudioNAMRack localRack; localRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureRack(localRack); localRack.setCabRequestedEnabled(false); @@ -45679,7 +46341,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool guardTransparent = loaded && maximumRawOutputPeak - < S13NAMRack:: + < OpenStudioNAMRack:: namOutputSafetyKnee && guardHitCount == 0; const bool pass = loaded @@ -45764,11 +46426,11 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr std::size_t directionProbeBlockSize = 16; auto* direction = new juce::DynamicObject(); - S13NAMRack::NAMResamplerKernel kernel; + OpenStudioNAMRack::NAMResamplerKernel kernel; kernel.prepare( static_cast(inputRate), static_cast(outputRate)); - S13NAMRack::NAMResamplerState state; + OpenStudioNAMRack::NAMResamplerState state; state.reset(); const auto denominator = @@ -45803,7 +46465,7 @@ juce::var AudioEngine::runNAMRackRegression() static_cast( baseInputSamples % static_cast( - S13NAMRack:: + OpenStudioNAMRack:: namResamplerHistorySize)); std::array input {}; @@ -45895,7 +46557,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int initialCursor = 3; constexpr int expectedCursor = (initialCursor + activeSamples) % delaySamples; - S13NAMRack::LoadedNAMModel model; + OpenStudioNAMRack::LoadedNAMModel model; model.resampledDryDelaySamples = delaySamples; model.resampledDryDelayWrite = initialCursor; model.ampBypassDryDelayWrite = initialCursor; @@ -45944,12 +46606,12 @@ juce::var AudioEngine::runNAMRackRegression() if (ampBypass) { - S13NAMRack::processAmpBypassDryDelay( + OpenStudioNAMRack::processAmpBypassDryDelay( buffer, model, activeSamples); } else { - S13NAMRack::processModelDryDelay( + OpenStudioNAMRack::processModelDryDelay( buffer, model, activeSamples); } @@ -46038,7 +46700,7 @@ juce::var AudioEngine::runNAMRackRegression() return juce::var(result); } - S13NAMRack probeRack; + OpenStudioNAMRack probeRack; probeRack.prepareToPlay(44100.0, 512); probeRack.gateThresholdDb.store(-100.0f); probeRack.auditionSource.store(1.0f); @@ -46097,9 +46759,9 @@ juce::var AudioEngine::runNAMRackRegression() // independent of how the host partitions it into callbacks. Render // the same internal DI source through fixed and deliberately uneven // block sequences and compare the resulting samples directly. - S13NAMRack fixedBlockRack; - S13NAMRack unevenBlockRack; - const auto configurePartitionProbe = [&] (S13NAMRack& candidate) + OpenStudioNAMRack fixedBlockRack; + OpenStudioNAMRack unevenBlockRack; + const auto configurePartitionProbe = [&] (OpenStudioNAMRack& candidate) { candidate.prepareToPlay(44100.0, 1024); candidate.gateThresholdDb.store(-100.0f); @@ -46132,7 +46794,7 @@ juce::var AudioEngine::runNAMRackRegression() const std::array unevenPattern { 8, 16, 32, 64, 127, 384, 256, 640, 512, 1024 }; - const auto renderPartition = [&] (S13NAMRack& candidate, const auto& pattern) + const auto renderPartition = [&] (OpenStudioNAMRack& candidate, const auto& pattern) { juce::AudioBuffer captured(2, comparisonFrames); captured.clear(); @@ -46192,7 +46854,7 @@ juce::var AudioEngine::runNAMRackRegression() partitionedBlockMaxDiff == 0.0f; const auto checkDryDelayCursor = [&] ( - S13NAMRack& candidate, + OpenStudioNAMRack& candidate, int& delaySampleCount, int& actualCursor, int& expectedCursor) @@ -46295,7 +46957,7 @@ juce::var AudioEngine::runNAMRackRegression() track.prepareToPlay(fixtureSampleRate, fixtureBlockSize); track.setTrackType(TrackType::Audio); - const bool fxAdded = track.addTrackFX(std::make_unique(), + const bool fxAdded = track.addTrackFX(std::make_unique(), fixtureSampleRate, fixtureBlockSize); bool enabled = false; @@ -46303,7 +46965,7 @@ juce::var AudioEngine::runNAMRackRegression() RackProbe probe; if (fxAdded) { - if (auto* rack = dynamic_cast(track.getTrackFXProcessor(0))) + if (auto* rack = dynamic_cast(track.getTrackFXProcessor(0))) { rack->gateThresholdDb.store(-100.0f); rack->auditionSource.store(1.0f); @@ -46361,11 +47023,11 @@ juce::var AudioEngine::runNAMRackRegression() const auto addConfiguredReverbRack = [&] (TrackProcessor& track) { const bool added = track.addTrackFX( - std::make_unique(), + std::make_unique(), fixtureSampleRate, tailBlockSize); auto* rack = added - ? dynamic_cast( + ? dynamic_cast( track.getTrackFXProcessor(0)) : nullptr; if (rack == nullptr) @@ -46375,7 +47037,7 @@ juce::var AudioEngine::runNAMRackRegression() rack->reverbEnabled.store(1.0f); rack->reverbEngineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion)); + OpenStudioNAMRack::currentReverbEngineVersion)); rack->reverbMix.store(0.68f); rack->reverbDecaySec.store(1.6f); rack->reverbPreDelayMs.store(0.0f); @@ -46571,7 +47233,7 @@ juce::var AudioEngine::runNAMRackRegression() "modelSource", benchmarkModelWasRequested ? "OPENSTUDIO_NAM_BENCHMARK_MODEL" - : "bundled_A2_fixture"); + : "bundled_stable_A1_fixture"); if (! modelFile.existsAsFile()) { @@ -46581,11 +47243,11 @@ juce::var AudioEngine::runNAMRackRegression() "reason", benchmarkModelWasRequested ? "OPENSTUDIO_NAM_BENCHMARK_MODEL does not point to a file." - : "The bundled A2 fixture was not found."); + : "The bundled stable A1 fixture was not found."); return juce::var(result); } - S13NAMRack lowBufferRack; + OpenStudioNAMRack lowBufferRack; lowBufferRack.prepareToPlay(fixtureSampleRate, 256); configureNeutralRack(lowBufferRack); lowBufferRack.ampEnabled.store(1.0f); @@ -46754,11 +47416,11 @@ juce::var AudioEngine::runNAMRackRegression() track.setTrackType(TrackType::Audio); const bool fxAdded = track.addTrackFX( - std::make_unique(), + std::make_unique(), fixtureSampleRate, lowBufferBlockSize); auto* rack = fxAdded - ? dynamic_cast( + ? dynamic_cast( track.getTrackFXProcessor(0)) : nullptr; if (rack != nullptr) @@ -47022,6 +47684,7 @@ juce::var AudioEngine::runNAMRackRegression() bool retiredReverbControlsRejected = false; bool ampFaceplateApplied = false; bool cabMicRoomApplied = false; + bool retiredCabFiltersRejected = false; bool cabPresentationApplied = false; bool auditionApplied = false; bool auditionHiddenFromPublicState = false; @@ -47074,10 +47737,13 @@ juce::var AudioEngine::runNAMRackRegression() graphicEqApplied = graphicEqApplied && setBuiltInPluginParam(trackId, "track", 0, "eqLPFHz", 11800.0f); graphicEqApplied = graphicEqApplied && setBuiltInPluginParam(trackId, "track", 0, "eqLevelDb", -1.25f); graphicEqApplied = graphicEqApplied && setBuiltInPluginParam(trackId, "track", 0, "eqEnabled", 1.0f); - cabMicRoomApplied = setBuiltInPluginParam(trackId, "track", 0, "cabMicPosition", 0.72f) - && setBuiltInPluginParam(trackId, "track", 0, "cabMicDistance", 0.38f) - && setBuiltInPluginParam(trackId, "track", 0, "cabMicBlend", 0.61f) - && setBuiltInPluginParam(trackId, "track", 0, "cabRoomSend", 0.29f) + retiredCabFiltersRejected = + ! setBuiltInPluginParam(trackId, "track", 0, "cabHPFEnabled", 1.0f) + && ! setBuiltInPluginParam(trackId, "track", 0, "cabLPFEnabled", 1.0f) + && ! setBuiltInPluginParam(trackId, "track", 0, "cabHPFHz", 91.0f) + && ! setBuiltInPluginParam(trackId, "track", 0, "cabLPFHz", 12700.0f); + cabMicRoomApplied = setBuiltInPluginParam(trackId, "track", 0, "cabIRStereo", 1.0f) + && setBuiltInPluginParam(trackId, "track", 0, "cabDirectMix", 0.37f) && setBuiltInPluginParam(trackId, "track", 0, "cabPan", -0.32f); cabPresentationApplied = setBuiltInPluginParam( @@ -47151,12 +47817,12 @@ juce::var AudioEngine::runNAMRackRegression() && ! setBuiltInPluginParam(trackId, "track", 0, "tapeEchoFeedback", 0.81f) && ! setBuiltInPluginParam(trackId, "track", 0, "tapeEchoMod", 0.79f) && ! setBuiltInPluginParam(trackId, "track", 0, "tapeEchoTone", 0.03f); - S13NAMRack* bridgeProbeRack = nullptr; + OpenStudioNAMRack* bridgeProbeRack = nullptr; const auto bridgeProbeTrack = trackMap.find(trackId); if (bridgeProbeTrack != trackMap.end() && bridgeProbeTrack->second != nullptr) { - bridgeProbeRack = dynamic_cast( + bridgeProbeRack = dynamic_cast( bridgeProbeTrack->second ->getTrackFXProcessor(0)); } @@ -47194,7 +47860,7 @@ juce::var AudioEngine::runNAMRackRegression() bridgeProbeRack->reverbEngineVersion.load( std::memory_order_relaxed) - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 1.0e-6f; retiredReverbControlsRejected = ! setBuiltInPluginParam(trackId, "track", 0, "reverbCharacter", 2.0f) @@ -47263,10 +47929,17 @@ juce::var AudioEngine::runNAMRackRegression() && closeEnough(values->getProperty("eqHPFLastActiveHz"), 95.0) && closeEnough(values->getProperty("eqLPFLastActiveHz"), 11800.0) && closeEnough(values->getProperty("eqLevelDb"), -1.25) - && closeEnough(values->getProperty("cabMicPosition"), 0.72) - && closeEnough(values->getProperty("cabMicDistance"), 0.38) - && closeEnough(values->getProperty("cabMicBlend"), 0.61) - && closeEnough(values->getProperty("cabRoomSend"), 0.29) + && closeEnough(values->getProperty("cabEngineVersion"), 3.0) + && closeEnough(values->getProperty("cabHPFEnabled"), 0.0) + && closeEnough(values->getProperty("cabLPFEnabled"), 0.0) + && closeEnough(values->getProperty("cabHPFHz"), 30.0) + && closeEnough(values->getProperty("cabLPFHz"), 16000.0) + && closeEnough(values->getProperty("cabIRStereo"), 1.0) + && closeEnough(values->getProperty("cabDirectMix"), 0.37) + && ! values->hasProperty("cabMicPosition") + && ! values->hasProperty("cabMicDistance") + && ! values->hasProperty("cabMicBlend") + && ! values->hasProperty("cabRoomSend") && closeEnough(values->getProperty("cabPan"), -0.32) && closeEnough(values->getProperty("cabRoomEnabled"), 1.0) && closeEnough(values->getProperty("cabRoomAmount"), 0.47) @@ -47477,7 +48150,7 @@ juce::var AudioEngine::runNAMRackRegression() dspState->getProperty( "reverbEngineVersion")) - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 1.0e-6; portableNAMEffectsDspStateExported = dspState->hasProperty( @@ -47485,7 +48158,7 @@ juce::var AudioEngine::runNAMRackRegression() && static_cast( dspState->getProperty( "namEffectsDspVersion")) - == S13NAMRack::currentNAMEffectsDspVersion; + == OpenStudioNAMRack::currentNAMEffectsDspVersion; } const auto portableStateSchema = getBuiltInPluginSchema( @@ -47530,7 +48203,7 @@ juce::var AudioEngine::runNAMRackRegression() .load( std::memory_order_relaxed) - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 1.0e-6f && std::abs( static_cast( @@ -47538,7 +48211,7 @@ juce::var AudioEngine::runNAMRackRegression() ->getProperty( "reverbEngineVersion")) - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 1.0e-6; } @@ -47575,7 +48248,7 @@ juce::var AudioEngine::runNAMRackRegression() markerlessApplied && bridgeProbeRack->namEffectsDspVersion.load( std::memory_order_relaxed) - == S13NAMRack::currentNAMEffectsDspVersion; + == OpenStudioNAMRack::currentNAMEffectsDspVersion; bridgeProbeRack->namEffectsDspVersion.store( 3, std::memory_order_relaxed); @@ -47589,7 +48262,7 @@ juce::var AudioEngine::runNAMRackRegression() legacyBundleApplied && bridgeProbeRack->namEffectsDspVersion.load( std::memory_order_relaxed) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && std::abs( bridgeProbeRack ->precisionDriveVolumeDb.load() @@ -47695,7 +48368,7 @@ juce::var AudioEngine::runNAMRackRegression() "{\"schemaVersion\":1,\"modelState\":{}," "\"dspState\":{\"namEffectsDspVersion\":" + juce::String( - S13NAMRack::developmentNAMEffectsDspVersionAlias) + OpenStudioNAMRack::developmentNAMEffectsDspVersionAlias) + "},\"values\":{\"delayMode\":4}}"); const bool developmentAliasDelayPreserved = developmentAliasDelayBundleApplied @@ -47710,7 +48383,7 @@ juce::var AudioEngine::runNAMRackRegression() "{\"schemaVersion\":1,\"modelState\":{}," "\"dspState\":{\"namEffectsDspVersion\":" + juce::String( - S13NAMRack::currentNAMEffectsDspVersion + 1) + OpenStudioNAMRack::currentNAMEffectsDspVersion + 1) + "},\"values\":{\"delayMode\":4}}"); const bool futureDelayDefaulted = futureDelayBundleApplied @@ -47738,7 +48411,7 @@ juce::var AudioEngine::runNAMRackRegression() portableNAMEffectsVersionsRoundTrip = true; for (int version = 1; - version <= S13NAMRack::currentNAMEffectsDspVersion; + version <= OpenStudioNAMRack::currentNAMEffectsDspVersion; ++version) { const juce::String versionState = @@ -47764,12 +48437,12 @@ juce::var AudioEngine::runNAMRackRegression() && versionApplied && bridgeProbeRack->namEffectsDspVersion.load( std::memory_order_relaxed) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && exportedDspState != nullptr && static_cast( exportedDspState->getProperty( "namEffectsDspVersion")) - == S13NAMRack::currentNAMEffectsDspVersion; + == OpenStudioNAMRack::currentNAMEffectsDspVersion; } const bool rollbackSourceApplied = @@ -47801,10 +48474,10 @@ juce::var AudioEngine::runNAMRackRegression() && rollbackApplied && bridgeProbeRack->namEffectsDspVersion.load( std::memory_order_relaxed) - == S13NAMRack::currentNAMEffectsDspVersion; + == OpenStudioNAMRack::currentNAMEffectsDspVersion; bridgeProbeRack->namEffectsDspVersion.store( - S13NAMRack::currentNAMEffectsDspVersion, + OpenStudioNAMRack::currentNAMEffectsDspVersion, std::memory_order_relaxed); bridgeProbeRack->chaosMode.store( 2.0f, std::memory_order_relaxed); @@ -47833,7 +48506,7 @@ juce::var AudioEngine::runNAMRackRegression() portablePreV5ControlsCanonicalized = completeV4Applied && bridgeProbeRack->namEffectsDspVersion.load() - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && std::abs(bridgeProbeRack->chaosMode.load()) <= 1.0e-6f && std::abs( @@ -47952,10 +48625,10 @@ juce::var AudioEngine::runNAMRackRegression() "cabDoublerMix")) - 0.24) <= 1.0e-6 && static_cast(dspState->getProperty( "namEffectsDspVersion")) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && static_cast(dspState->getProperty( "reverbEngineVersion")) - == S13NAMRack::currentReverbEngineVersion; + == OpenStudioNAMRack::currentReverbEngineVersion; }; const auto* migratedUiObject = migratedUiState.getDynamicObject(); @@ -48092,6 +48765,7 @@ juce::var AudioEngine::runNAMRackRegression() && bassApplied && graphicEqApplied && cabMicRoomApplied + && retiredCabFiltersRejected && cabPresentationApplied && compressorApplied && retiredTapeEchoRejected @@ -48186,6 +48860,9 @@ juce::var AudioEngine::runNAMRackRegression() auditionClearedByOrdinaryRestore); result->setProperty("invalidRejected", invalidRejected); result->setProperty("valuesMatched", valuesMatched); + result->setProperty( + "retiredCabFiltersRejected", + retiredCabFiltersRejected); result->setProperty( "graphicEqOffRecallRoundTrip", graphicEqOffRecallRoundTrip); @@ -48265,7 +48942,7 @@ juce::var AudioEngine::runNAMRackRegression() "OpenStudio NAM Rack", false); - S13NAMRack* rack = nullptr; + OpenStudioNAMRack* rack = nullptr; if (fxAdded) { const juce::ScopedLock graphLock( @@ -48276,7 +48953,7 @@ juce::var AudioEngine::runNAMRackRegression() if (trackIt != trackMap.end() && trackIt->second != nullptr) { - rack = dynamic_cast( + rack = dynamic_cast( trackIt->second ->getTrackFXProcessor(0)); } @@ -48566,7 +49243,7 @@ juce::var AudioEngine::runNAMRackRegression() "OpenStudio NAM Rack", false); - S13NAMRack* rack = nullptr; + OpenStudioNAMRack* rack = nullptr; if (fxAdded) { const juce::ScopedLock graphLock( @@ -48575,7 +49252,7 @@ juce::var AudioEngine::runNAMRackRegression() if (trackIt != trackMap.end() && trackIt->second != nullptr) { - rack = dynamic_cast( + rack = dynamic_cast( trackIt->second ->getTrackFXProcessor(0)); } @@ -48656,7 +49333,7 @@ juce::var AudioEngine::runNAMRackRegression() tree.isValid() && tree.getType() == juce::Identifier( - "S13NAMRack"), + "OpenStudioNAMRack"), std::memory_order_release); } } @@ -48759,7 +49436,7 @@ juce::var AudioEngine::runNAMRackRegression() ? getPublishedBuiltInProcessor( trackId, "track", 0) : PublishedBuiltInProcessorOwner {}; - auto* eq = dynamic_cast( + auto* eq = dynamic_cast( publishedOwner.processor.get()); juce::WaitableEvent holderOwnsLocks; @@ -48984,7 +49661,7 @@ juce::var AudioEngine::runNAMRackRegression() auto renderTrim = [&] (float inputTrimDb, float outputTrimDb) { - S13NAMRack trimRack; + OpenStudioNAMRack trimRack; trimRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureRack(trimRack); trimRack.inputTrimDb.store(inputTrimDb); @@ -49144,11 +49821,9 @@ juce::var AudioEngine::runNAMRackRegression() const bool nonFiniteClassified = isDeviceOutputDiscontinuityCandidate( 1, 0.0f, 0.0f); - const bool thresholdsCoverRackGuard = + const bool rackGuardOwnsSubCeilingProtection = kDeviceOutputDiscontinuityPeakThreshold - >= S13NAMRack::namOutputSafetyKnee - && kDeviceOutputDiscontinuityPeakThreshold - < S13NAMRack::namOutputSafetyCeiling; + > OpenStudioNAMRack::namOutputSafetyCeiling; const bool pass = ! normal.candidate && normal.peak <= 1.0f + 1.0e-6f @@ -49159,7 +49834,7 @@ juce::var AudioEngine::runNAMRackRegression() < kDeviceOutputDiscontinuityPeakThreshold && deltaOnly.candidate && nonFiniteClassified - && thresholdsCoverRackGuard; + && rackGuardOwnsSubCeilingProtection; auto* result = new juce::DynamicObject(); result->setProperty("pass", pass); @@ -49200,8 +49875,8 @@ juce::var AudioEngine::runNAMRackRegression() "nonFiniteClassified", nonFiniteClassified); result->setProperty( - "thresholdsCoverRackGuard", - thresholdsCoverRackGuard); + "rackGuardOwnsSubCeilingProtection", + rackGuardOwnsSubCeilingProtection); return juce::var(result); }; const auto outputDiscontinuityClassifierProbe = @@ -49277,8 +49952,8 @@ juce::var AudioEngine::runNAMRackRegression() auto runAuditionSourceIsolationProbe = [&] () { - S13NAMRack noisyRack; - S13NAMRack quietRack; + OpenStudioNAMRack noisyRack; + OpenStudioNAMRack quietRack; noisyRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); quietRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); noisyRack.gateThresholdDb.store(-100.0f); @@ -49444,7 +50119,7 @@ juce::var AudioEngine::runNAMRackRegression() checks, "amp_model_handoff_is_dezippered_at_16_samples", stressStatus(lowBufferAmpHandoffProbe), - "At 44.1 kHz with exact 16-sample callbacks, first publication and replacement with a fresh graph from the stable A1 fixture must use the zero-boundary handoff, remain finite and bounded, introduce no large adjacent-sample discontinuity, and avoid every process-failure or callback-capacity fallback. The synthetic A2-max architecture stress fixture remains covered by finite/bounded checks but is excluded from this audio-discontinuity oracle because its own steady output contains full-scale single-sample jumps.", + "At 44.1 kHz with exact 16-sample callbacks and already-excited Delay/Reverb tails, first publication and replacement with a fresh graph from the stable A1 fixture must cross a complete-rack zero boundary, remain finite and bounded, introduce no large adjacent-sample discontinuity, and avoid every process-failure or callback-capacity fallback. The synthetic A2-max architecture stress fixture remains covered by finite/bounded checks but is excluded from this audio-discontinuity oracle because its own steady output contains full-scale single-sample jumps.", lowBufferAmpHandoffProbe); const bool slimModelQualityAvailable = @@ -49641,7 +50316,7 @@ juce::var AudioEngine::runNAMRackRegression() .getProperty("pass", false) ? "pass" : "fail", - "Diagnostic-only output classification must ignore a full-scale 6 kHz band-limited signal, but latch both a finite impulse inside the NAM guard range and a sub-peak-threshold adjacent-sample jump.", + "Diagnostic-only device output classification must ignore a full-scale 6 kHz band-limited signal, while the rack guard owns sub-ceiling protection and the device diagnostic still latches unguarded finite impulses and adjacent-sample jumps from other host paths.", outputDiscontinuityClassifierProbe); addCheck(checks, @@ -49657,7 +50332,124 @@ juce::var AudioEngine::runNAMRackRegression() "Generated fixture IR should load into the Cab/IR slot.", rack.getCabIRPath()); - S13NAMRack irSwapRack; + const auto runCabV3LiteralIRProbe = [&] () + { + OpenStudioNAMRack neutralCab; + OpenStudioNAMRack retiredCabFilters; + OpenStudioNAMRack graphicEqLowPass; + auto configure = [&] (OpenStudioNAMRack& target) + { + target.prepareToPlay(fixtureSampleRate, fixtureBlockSize); + configureNeutralRack(target); + target.cabIRStereo.store(0.0f); + target.cabDirectMix.store(0.0f); + const bool loaded = target.loadCabIR(irFile.getFullPathName()); + target.setCabRequestedEnabled(true); + return loaded && target.hasCabIR(); + }; + + const bool loaded = irWritten + && configure(neutralCab) + && configure(retiredCabFilters) + && configure(graphicEqLowPass); + retiredCabFilters.cabHPFEnabled.store(1.0f); + retiredCabFilters.cabLPFEnabled.store(1.0f); + retiredCabFilters.cabHPFHz.store(500.0f); + retiredCabFilters.cabLPFHz.store(1000.0f); + graphicEqLowPass.eqEnabled.store(1.0f); + graphicEqLowPass.eqLPFHz.store(3000.0f); + + float retiredFilterMaximumError = 0.0f; + float graphicEqMaximumDifference = 0.0f; + int nonFiniteCount = 0; + constexpr int totalSamples = 8192; + int cursor = 0; + juce::MidiBuffer midi; + while (cursor < totalSamples) + { + const int blockSamples = juce::jmin( + fixtureBlockSize, totalSamples - cursor); + juce::AudioBuffer neutralBlock(2, blockSamples); + juce::AudioBuffer ignoredBlock(2, blockSamples); + juce::AudioBuffer filteredBlock(2, blockSamples); + for (int sample = 0; sample < blockSamples; ++sample) + { + const int absoluteSample = cursor + sample; + const float input = 0.05f * static_cast(std::sin( + juce::MathConstants::twoPi * 173.0 + * static_cast(absoluteSample) + / fixtureSampleRate)) + + 0.035f * static_cast(std::sin( + juce::MathConstants::twoPi * 6000.0 + * static_cast(absoluteSample) + / fixtureSampleRate)); + for (int channel = 0; channel < 2; ++channel) + { + neutralBlock.setSample(channel, sample, input); + ignoredBlock.setSample(channel, sample, input); + filteredBlock.setSample(channel, sample, input); + } + } + if (loaded) + { + neutralCab.processBlock(neutralBlock, midi); + retiredCabFilters.processBlock(ignoredBlock, midi); + graphicEqLowPass.processBlock(filteredBlock, midi); + } + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < blockSamples; ++sample) + { + const float neutral = neutralBlock.getSample(channel, sample); + const float ignored = ignoredBlock.getSample(channel, sample); + const float filtered = filteredBlock.getSample(channel, sample); + if (! std::isfinite(neutral) + || ! std::isfinite(ignored) + || ! std::isfinite(filtered)) + { + ++nonFiniteCount; + continue; + } + retiredFilterMaximumError = juce::jmax( + retiredFilterMaximumError, + std::abs(neutral - ignored)); + if (cursor + sample >= totalSamples / 2) + { + graphicEqMaximumDifference = juce::jmax( + graphicEqMaximumDifference, + std::abs(neutral - filtered)); + } + } + } + cursor += blockSamples; + } + + const bool pass = loaded + && nonFiniteCount == 0 + && retiredFilterMaximumError <= 1.0e-6f + && graphicEqMaximumDifference > 1.0e-4f; + auto* value = new juce::DynamicObject(); + value->setProperty("loaded", loaded); + value->setProperty( + "retiredFilterMaximumError", + retiredFilterMaximumError); + value->setProperty( + "graphicEqMaximumDifference", + graphicEqMaximumDifference); + value->setProperty("nonFiniteCount", nonFiniteCount); + value->setProperty("pass", pass); + return juce::var(value); + }; + const auto cabV3LiteralIRProbe = runCabV3LiteralIRProbe(); + addCheck( + checks, + "cab_v3_literal_ir_and_graphic_eq_filter_owner", + cabV3LiteralIRProbe.getProperty("pass", false) + ? "pass" : "fail", + "Cab V3 must remain sample-identical when retired Cab cutoff fields change, while the visible Graphic EQ LPF measurably alters the deterministic fixture. Perceived cabinet quality remains not_asserted pending user audition.", + cabV3LiteralIRProbe); + + OpenStudioNAMRack irSwapRack; irSwapRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); irSwapRack.gateThresholdDb.store(-100.0f); const bool irSwapLoaded = irWritten @@ -49728,7 +50520,7 @@ juce::var AudioEngine::runNAMRackRegression() "Delay/reverb post-FX should create a deterministic finite tail after the dry signal stops.", processed.tailPeak); - auto configureOrderProbeRack = [&] (S13NAMRack& orderRack, const juce::String& uiState) + auto configureOrderProbeRack = [&] (OpenStudioNAMRack& orderRack, const juce::String& uiState) { orderRack.prepareToPlay(fixtureSampleRate, fixtureBlockSize); configureRack(orderRack); @@ -49756,8 +50548,8 @@ juce::var AudioEngine::runNAMRackRegression() const juce::String defaultOrderUiState = R"json({"namRackSlots":{"schemaVersion":1,"order":["gate","pedal","amp","cab","eq","mod","delay","reverb"]}})json"; const juce::String reorderedUiState = R"json({"namRackSlots":{"schemaVersion":1,"order":["gate","pedal","amp","cab","reverb","delay","mod","eq"]}})json"; - S13NAMRack defaultOrderRack; - S13NAMRack reorderedRack; + OpenStudioNAMRack defaultOrderRack; + OpenStudioNAMRack reorderedRack; configureOrderProbeRack(defaultOrderRack, defaultOrderUiState); configureOrderProbeRack(reorderedRack, reorderedUiState); const auto defaultOrderCapture = renderRackCapture(defaultOrderRack); @@ -49782,15 +50574,15 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr float expectedRackVersion = static_cast( - S13NAMRack::currentReverbEngineVersion); + OpenStudioNAMRack::currentReverbEngineVersion); constexpr int expectedRackDspVersion = - S13NAMRack::currentReverbEngineVersion; + OpenStudioNAMRack::currentReverbEngineVersion; const int expectedDspVersion = expectedVersion >= 4.5f ? 5 : (expectedVersion >= 3.5f ? 4 : 3); - S13NAMRack rackSource; + OpenStudioNAMRack rackSource; rackSource.reverbEngineVersion.store( expectedVersion, std::memory_order_relaxed); @@ -49801,7 +50593,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::ValueTree::readFromData( rackState.getData(), rackState.getSize()); - S13NAMRack rackRestored; + OpenStudioNAMRack rackRestored; const bool rackRestoreAccepted = rackRestored .restoreProjectStateInformation( @@ -49816,7 +50608,7 @@ juce::var AudioEngine::runNAMRackRegression() rackResavedState.getData(), rackResavedState.getSize()); - S13Reverb standaloneSource; + OpenStudioReverb standaloneSource; standaloneSource.engineVersion.store( expectedVersion, std::memory_order_relaxed); @@ -49827,7 +50619,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::ValueTree::readFromData( standaloneState.getData(), standaloneState.getSize()); - S13Reverb standaloneRestored; + OpenStudioReverb standaloneRestored; standaloneRestored.setStateInformation( standaloneState.getData(), static_cast( @@ -49985,19 +50777,19 @@ juce::var AudioEngine::runNAMRackRegression() .getProperty("pass", false) ? "pass" : "fail", - "NAM Rack binary state must canonicalize every recognized old Reverb marker to the current engine, while standalone S13Reverb retains its independent exact round-trip contract.", + "NAM Rack binary state must canonicalize every recognized old Reverb marker to the current engine, while standalone OpenStudioReverb retains its independent exact round-trip contract.", reverbEngineBinaryStateRoundTripProbe); auto runCompressorV6StateMigrationProbe = [&] () { - juce::ValueTree legacy("S13NAMRack"); + juce::ValueTree legacy("OpenStudioNAMRack"); legacy.setProperty("namEffectsDspVersion", 6, nullptr); legacy.setProperty("compressorDetail", 0.37, nullptr); legacy.setProperty("compressorEnabled", 1.0, nullptr); juce::MemoryOutputStream stream; legacy.writeToStream(stream); - S13NAMRack migrated; + OpenStudioNAMRack migrated; migrated.prepareToPlay( fixtureSampleRate, fixtureBlockSize); @@ -50017,7 +50809,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool compressorPass = tree.isValid() && static_cast(tree.getProperty( "namEffectsDspVersion", 0)) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && ! tree.hasProperty("compressorDetail") && std::abs(static_cast(tree.getProperty( "compressorAttackMs", 0.0)) - expectedAttackMs) @@ -50089,14 +50881,17 @@ juce::var AudioEngine::runNAMRackRegression() int version, double hpfHz, double lpfHz, - S13NAMRack& destination, + bool currentCab, + OpenStudioNAMRack& destination, juce::ValueTree& canonicalTree) { - juce::ValueTree source("S13NAMRack"); + juce::ValueTree source("OpenStudioNAMRack"); source.setProperty( "namEffectsDspVersion", version, nullptr); source.setProperty("eqHPFHz", hpfHz, nullptr); source.setProperty("eqLPFHz", lpfHz, nullptr); + if (currentCab) + source.setProperty("cabEngineVersion", 3.0, nullptr); juce::MemoryBlock state; juce::MemoryOutputStream stream(state, false); source.writeToStream(stream); @@ -50111,94 +50906,99 @@ juce::var AudioEngine::runNAMRackRegression() return restored && canonicalTree.isValid(); }; - S13NAMRack fromV12; + OpenStudioNAMRack fromV12; juce::ValueTree v12Tree; const bool v12Restored = restoreCase( - 12, 180.0, 9000.0, fromV12, v12Tree); - S13NAMRack fromV13; + 12, 180.0, 9000.0, false, fromV12, v12Tree); + OpenStudioNAMRack fromV13; juce::ValueTree v13Tree; const bool v13Restored = restoreCase( - S13NAMRack::developmentNAMEffectsDspVersionAlias, + OpenStudioNAMRack::developmentNAMEffectsDspVersionAlias, 180.0, 9000.0, + false, fromV13, v13Tree); - S13NAMRack fromV14; - juce::ValueTree v14Tree; - const bool v14Restored = restoreCase( - S13NAMRack::currentNAMEffectsDspVersion, + OpenStudioNAMRack fromCurrent; + juce::ValueTree currentTree; + const bool currentRestored = restoreCase( + OpenStudioNAMRack::currentNAMEffectsDspVersion, 180.0, 9000.0, - fromV14, - v14Tree); + true, + fromCurrent, + currentTree); - const auto migratedOff = [] ( - const S13NAMRack& rack, + const auto migratedLegacyCabWindow = [] ( + const OpenStudioNAMRack& rack, const juce::ValueTree& tree) { - return std::abs(rack.eqHPFHz.load()) <= 1.0e-7f - && std::abs(rack.eqLPFHz.load() - 24000.0f) + return rack.eqEnabled.load() >= 0.5f + && std::abs(rack.eqHPFHz.load() - 80.0f) <= 1.0e-4f + && std::abs(rack.eqLPFHz.load() - 8500.0f) <= 1.0e-4f && std::abs(rack.eqHPFLastActiveHz.load() - 80.0f) <= 1.0e-4f - && std::abs(rack.eqLPFLastActiveHz.load() - 12000.0f) + && std::abs(rack.eqLPFLastActiveHz.load() - 8500.0f) <= 1.0e-4f && std::abs(static_cast( - tree.getProperty("eqHPFHz", -1.0))) + tree.getProperty("eqHPFHz", -1.0)) - 80.0) <= 1.0e-7 && std::abs(static_cast( - tree.getProperty("eqLPFHz", 0.0)) - 24000.0) + tree.getProperty("eqLPFHz", 0.0)) - 8500.0) <= 1.0e-7 && static_cast(tree.getProperty( "namEffectsDspVersion", 0)) - == S13NAMRack::currentNAMEffectsDspVersion; + == OpenStudioNAMRack::currentNAMEffectsDspVersion; }; - const bool v12DefaultsOff = - v12Restored && migratedOff(fromV12, v12Tree); - const bool v13DefaultsOff = - v13Restored && migratedOff(fromV13, v13Tree); - const bool v14Preserved = v14Restored - && std::abs(fromV14.eqHPFHz.load() - 180.0f) + const bool v12CabWindowMigrated = + v12Restored && migratedLegacyCabWindow(fromV12, v12Tree); + const bool v13CabWindowMigrated = + v13Restored && migratedLegacyCabWindow(fromV13, v13Tree); + const bool currentPreserved = currentRestored + && std::abs(fromCurrent.eqHPFHz.load() - 180.0f) <= 1.0e-4f - && std::abs(fromV14.eqLPFHz.load() - 9000.0f) + && std::abs(fromCurrent.eqLPFHz.load() - 9000.0f) <= 1.0e-4f - && std::abs(fromV14.eqHPFLastActiveHz.load() - 180.0f) + && std::abs(fromCurrent.eqHPFLastActiveHz.load() - 180.0f) <= 1.0e-4f - && std::abs(fromV14.eqLPFLastActiveHz.load() - 9000.0f) + && std::abs(fromCurrent.eqLPFLastActiveHz.load() - 9000.0f) <= 1.0e-4f && std::abs(static_cast( - v14Tree.getProperty("eqHPFHz", 0.0)) - 180.0) + currentTree.getProperty("eqHPFHz", 0.0)) - 180.0) <= 1.0e-7 && std::abs(static_cast( - v14Tree.getProperty("eqLPFHz", 0.0)) - 9000.0) + currentTree.getProperty("eqLPFHz", 0.0)) - 9000.0) <= 1.0e-7; auto* value = new juce::DynamicObject(); - value->setProperty("v12DefaultsOff", v12DefaultsOff); - value->setProperty("v13AliasDefaultsOff", v13DefaultsOff); - value->setProperty("v14Preserved", v14Preserved); + value->setProperty("v12CabWindowMigrated", v12CabWindowMigrated); + value->setProperty("v13AliasCabWindowMigrated", v13CabWindowMigrated); + value->setProperty("currentV3Preserved", currentPreserved); value->setProperty( "pass", - v12DefaultsOff && v13DefaultsOff && v14Preserved); + v12CabWindowMigrated + && v13CabWindowMigrated + && currentPreserved); return juce::var(value); }; const auto graphicEqEdgeFilterStateMigrationProbe = runGraphicEqEdgeFilterStateMigrationProbe(); addCheck( checks, - "graphic_eq_edge_filters_migrate_on_v14_contract", + "graphic_eq_edge_filters_and_legacy_cab_window_migrate_once", graphicEqEdgeFilterStateMigrationProbe.getProperty( "pass", false) ? "pass" : "fail", - "V12 and the development-only V13 alias must migrate Graphic EQ HPF/LPF to exact OFF sentinels, while a V14 state preserves active cutoff values and every canonical re-save stamps V14.", + "Historical states must move the old implicit Cab filter window into Graphic EQ, while an explicit current Cab V3 state preserves its own Graphic EQ cutoffs and every canonical re-save stamps the current DSP version.", graphicEqEdgeFilterStateMigrationProbe); auto runCabPresentationEnableStateMigrationProbe = [] () { const auto restoreTree = [] ( const juce::ValueTree& tree, - S13NAMRack& destination) + OpenStudioNAMRack& destination) { juce::MemoryBlock stateData; juce::MemoryOutputStream stream(stateData, false); @@ -50208,24 +51008,24 @@ juce::var AudioEngine::runNAMRackRegression() static_cast(stateData.getSize())); }; - juce::ValueTree legacyAudible("S13NAMRack"); + juce::ValueTree legacyAudible("OpenStudioNAMRack"); legacyAudible.setProperty("cabRoomAmount", 0.39, nullptr); legacyAudible.setProperty("cabDoublerMix", 0.24, nullptr); - S13NAMRack migratedAudible; + OpenStudioNAMRack migratedAudible; const bool audibleRestored = restoreTree( legacyAudible, migratedAudible); - juce::ValueTree legacyBypassed("S13NAMRack"); - S13NAMRack migratedBypassed; + juce::ValueTree legacyBypassed("OpenStudioNAMRack"); + OpenStudioNAMRack migratedBypassed; const bool bypassedRestored = restoreTree( legacyBypassed, migratedBypassed); - juce::ValueTree explicitBypassed("S13NAMRack"); + juce::ValueTree explicitBypassed("OpenStudioNAMRack"); explicitBypassed.setProperty("cabRoomEnabled", 0.0, nullptr); explicitBypassed.setProperty("cabRoomAmount", 0.73, nullptr); explicitBypassed.setProperty("cabDoublerEnabled", 0.0, nullptr); explicitBypassed.setProperty("cabDoublerMix", 0.61, nullptr); - S13NAMRack restoredExplicitBypass; + OpenStudioNAMRack restoredExplicitBypass; const bool explicitBypassRestored = restoreTree( explicitBypassed, restoredExplicitBypass); @@ -50295,6 +51095,132 @@ juce::var AudioEngine::runNAMRackRegression() "Legacy Room/Doubler Amount and Mix must derive explicit power once, while current bypass preserves useful stored settings and canonical re-save writes both power controls.", cabPresentationEnableStateMigrationProbe); + const auto runCabV3StateMigrationProbe = [] () + { + const auto restoreTree = [] ( + const juce::ValueTree& tree, + OpenStudioNAMRack& target) + { + juce::MemoryBlock stateData; + juce::MemoryOutputStream stream(stateData, false); + tree.writeToStream(stream); + return target.restoreProjectStateInformation( + stateData.getData(), + static_cast(stateData.getSize())); + }; + + juce::ValueTree legacy("OpenStudioNAMRack"); + legacy.setProperty("cabHPFHz", 87.0, nullptr); + legacy.setProperty("cabLPFHz", 8300.0, nullptr); + legacy.setProperty("cabMicPosition", 0.73, nullptr); + legacy.setProperty("cabMicDistance", 0.42, nullptr); + legacy.setProperty("cabMicBlend", 0.19, nullptr); + legacy.setProperty("cabRoomSend", 0.31, nullptr); + OpenStudioNAMRack restoredLegacy; + const bool legacyRestored = restoreTree(legacy, restoredLegacy); + + juce::ValueTree current("OpenStudioNAMRack"); + current.setProperty("cabEngineVersion", 2.0, nullptr); + current.setProperty("cabHPFEnabled", 0.0, nullptr); + current.setProperty("cabLPFEnabled", 1.0, nullptr); + current.setProperty("cabHPFHz", 31.0, nullptr); + current.setProperty("cabLPFHz", 15750.0, nullptr); + current.setProperty("cabIRStereo", 0.0, nullptr); + current.setProperty("cabDirectMix", 0.37, nullptr); + OpenStudioNAMRack restoredCurrent; + const bool currentRestored = restoreTree(current, restoredCurrent); + + juce::MemoryBlock canonicalState; + restoredLegacy.getStateInformation(canonicalState); + const auto canonicalTree = juce::ValueTree::readFromData( + canonicalState.getData(), canonicalState.getSize()); + const bool legacyMigrated = legacyRestored + && restoredLegacy.cabHPFEnabled.load() < 0.5f + && restoredLegacy.cabLPFEnabled.load() < 0.5f + && std::abs(restoredLegacy.cabHPFHz.load() - 30.0f) + <= 1.0e-6f + && std::abs(restoredLegacy.cabLPFHz.load() - 16000.0f) + <= 1.0e-6f + && restoredLegacy.eqEnabled.load() >= 0.5f + && std::abs(restoredLegacy.eqHPFHz.load() - 87.0f) + <= 1.0e-6f + && std::abs(restoredLegacy.eqLPFHz.load() - 8300.0f) + <= 1.0e-6f + && restoredLegacy.cabIRStereo.load() >= 0.5f + && std::abs(restoredLegacy.cabDirectMix.load()) <= 1.0e-6f + && canonicalTree.isValid() + && static_cast(canonicalTree.getProperty( + "cabEngineVersion", 0)) == 3 + && ! canonicalTree.hasProperty("cabMicPosition") + && ! canonicalTree.hasProperty("cabMicDistance") + && ! canonicalTree.hasProperty("cabMicBlend") + && ! canonicalTree.hasProperty("cabRoomSend"); + const bool currentPreserved = currentRestored + && restoredCurrent.cabHPFEnabled.load() < 0.5f + && restoredCurrent.cabLPFEnabled.load() < 0.5f + && std::abs(restoredCurrent.cabHPFHz.load() - 30.0f) + <= 1.0e-6f + && std::abs(restoredCurrent.cabLPFHz.load() - 16000.0f) + <= 1.0e-6f + && restoredCurrent.eqEnabled.load() >= 0.5f + && std::abs(restoredCurrent.eqHPFHz.load()) <= 1.0e-6f + && std::abs(restoredCurrent.eqLPFHz.load() - 15750.0f) + <= 1.0e-6f + && restoredCurrent.cabIRStereo.load() < 0.5f + && std::abs(restoredCurrent.cabDirectMix.load() - 0.37f) + <= 1.0e-6f; + + bool bypassMigration = true; + for (const int version : { 1, 2 }) + for (const bool cabOn : { false, true }) + for (const bool eqOn : { false, true }) + { + auto fixture = legacy.createCopy(); + fixture.setProperty("namEffectsDspVersion", 19, nullptr); + fixture.setProperty("cabEngineVersion", version, nullptr); + fixture.setProperty("cabEnabled", cabOn ? 1.0 : 0.0, nullptr); + fixture.setProperty("cabRequestedEnabled", cabOn ? 1.0 : 0.0, nullptr); + fixture.setProperty("cabHPFEnabled", 1.0, nullptr); + fixture.setProperty("cabLPFEnabled", 1.0, nullptr); + fixture.setProperty("eqEnabled", eqOn ? 1.0 : 0.0, nullptr); + fixture.setProperty("eq1kDb", 6.0, nullptr); + fixture.setProperty("eqLevelDb", 3.0, nullptr); + fixture.setProperty("eqHPFHz", 120.0, nullptr); + fixture.setProperty("eqLPFHz", 4000.0, nullptr); + OpenStudioNAMRack restored; + const bool restores = restoreTree(fixture, restored); + const bool neutralCurve = cabOn && !eqOn; + const auto matches = [&] (const OpenStudioNAMRack& rack) { + return (rack.eqEnabled.load() >= 0.5f) == (cabOn || eqOn) + && rack.eq1kDb.load() == (neutralCurve ? 0.0f : 6.0f) + && rack.eqLevelDb.load() == (neutralCurve ? 0.0f : 3.0f) + && rack.eqHPFHz.load() == (neutralCurve ? 87.0f : 120.0f) + && rack.eqLPFHz.load() == (neutralCurve ? 8300.0f : 4000.0f); + }; + juce::MemoryBlock saved; + restored.getStateInformation(saved); + OpenStudioNAMRack reopened; + const bool reopens = reopened.restoreProjectStateInformation(saved.getData(), static_cast(saved.getSize())); + bypassMigration = restores && reopens && matches(restored) && matches(reopened) && bypassMigration; + } + + auto* value = new juce::DynamicObject(); + value->setProperty("legacyMigrated", legacyMigrated); + value->setProperty("currentPreserved", currentPreserved); + value->setProperty("bypassMigration", bypassMigration); + value->setProperty( + "pass", legacyMigrated && currentPreserved && bypassMigration); + return juce::var(value); + }; + const auto cabV3StateMigrationProbe = runCabV3StateMigrationProbe(); + addCheck( + checks, + "cab_single_engine_state_migration", + cabV3StateMigrationProbe.getProperty("pass", false) + ? "pass" : "fail", + "Every historical Cab state must migrate to the one current IR engine, move old cutoff intent into visible Graphic EQ controls, neutralize retired Cab filters, preserve stereo/direct routing, discard retired shapers, and re-save canonically.", + cabV3StateMigrationProbe); + const juce::String compareUiState = R"json({"namRackCompare":{"schemaVersion":1,"compareSlot":"B","snapshots":{"B":{"values":{"ampMix":1.0,"trebleDb":2.5},"modelState":{"ampModelPath":"C:/OpenStudio/NAM/compare_fixture.nam"},"presetId":"clean-twin","focusedModule":"amp","capturedAt":1}}},"namRackSlots":{"schemaVersion":1,"order":["gate","pedal","amp","cab","reverb","delay","mod","eq"]}})json"; rack.setUiStateJSON(compareUiState); rack.reverbCharacter.store(2.0f); @@ -50318,6 +51244,12 @@ juce::var AudioEngine::runNAMRackRegression() rack.cabDoublerMix.store(0.24f); rack.cabDoublerDelayMs.store(6.5f); rack.cabDoublerSpread.store(0.63f); + rack.cabHPFEnabled.store(0.0f); + rack.cabLPFEnabled.store(1.0f); + rack.cabHPFHz.store(31.0f); + rack.cabLPFHz.store(15750.0f); + rack.cabIRStereo.store(0.0f); + rack.cabDirectMix.store(0.37f); rack.preEqEnabled.store(1.0f); rack.preEq120Db.store(-2.5f); rack.preEq250Db.store(-1.0f); @@ -50334,11 +51266,11 @@ juce::var AudioEngine::runNAMRackRegression() rack.eqHPFHz.store(112.0f); rack.eqLPFHz.store(13400.0f); rack.precisionDriveVoice.store( - static_cast(S13NAMRack::screamerDriveVoice)); + static_cast(OpenStudioNAMRack::screamerDriveVoice)); juce::MemoryBlock state; rack.getStateInformation(state); - S13NAMRack restored; + OpenStudioNAMRack restored; restored.prepareToPlay(fixtureSampleRate, fixtureBlockSize); restored.setStateInformation(state.getData(), static_cast(state.getSize())); juce::MemoryBlock migratedReverbState; @@ -50361,14 +51293,14 @@ juce::var AudioEngine::runNAMRackRegression() && ! migratedReverbTree.hasProperty("inputMode") && ! migratedReverbTree.hasProperty("precisionDriveVoice") && static_cast(migratedReverbTree.getProperty( - "reverbVoice", -1)) == S13NAMRack::roomReverbVoice + "reverbVoice", -1)) == OpenStudioNAMRack::roomReverbVoice && static_cast(migratedReverbTree.getProperty( "reverbPad", 0.0)) >= 0.5; const bool stateRestorePass = restored.hasCabIR() && restored.getCabIRPath() == irFile.getFullPathName() && retiredReverbStatePruned && restored.getInstrumentProfile() - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && restored.namEffectsDspVersion.load( std::memory_order_relaxed) == rack.namEffectsDspVersion.load( @@ -50401,7 +51333,7 @@ juce::var AudioEngine::runNAMRackRegression() && std::abs(restored.preEqLPFLastActiveHz.load() - 12300.0f) <= 0.0001f && std::abs(restored.precisionDriveEnabled.load() - rack.precisionDriveEnabled.load()) <= 0.0001f && juce::roundToInt(restored.precisionDriveVoice.load()) - == S13NAMRack::precisionDrivePrecisionVoice + == OpenStudioNAMRack::precisionDrivePrecisionVoice && std::abs(restored.precisionDriveVolumeDb.load() - rack.precisionDriveVolumeDb.load()) <= 0.0001f && std::abs(restored.precisionDriveBright.load() - rack.precisionDriveBright.load()) <= 0.0001f && std::abs(restored.precisionDriveAttack.load() - rack.precisionDriveAttack.load()) <= 0.0001f @@ -50461,12 +51393,14 @@ juce::var AudioEngine::runNAMRackRegression() && std::abs( restored.reverbEngineVersion.load() - static_cast( - S13NAMRack::currentReverbEngineVersion)) <= 0.0001f + OpenStudioNAMRack::currentReverbEngineVersion)) <= 0.0001f && std::abs(restored.reverbEnabled.load() - rack.reverbEnabled.load()) <= 0.0001f - && std::abs(restored.cabMicPosition.load() - rack.cabMicPosition.load()) <= 0.0001f - && std::abs(restored.cabMicDistance.load() - rack.cabMicDistance.load()) <= 0.0001f - && std::abs(restored.cabMicBlend.load() - rack.cabMicBlend.load()) <= 0.0001f - && std::abs(restored.cabRoomSend.load() - rack.cabRoomSend.load()) <= 0.0001f + && restored.cabHPFEnabled.load() < 0.5f + && restored.cabLPFEnabled.load() < 0.5f + && std::abs(restored.cabHPFHz.load() - 30.0f) <= 0.0001f + && std::abs(restored.cabLPFHz.load() - 16000.0f) <= 0.0001f + && std::abs(restored.cabIRStereo.load() - rack.cabIRStereo.load()) <= 0.0001f + && std::abs(restored.cabDirectMix.load() - rack.cabDirectMix.load()) <= 0.0001f && std::abs(restored.cabPan.load() - rack.cabPan.load()) <= 0.0001f && std::abs(restored.cabRoomEnabled.load() - rack.cabRoomEnabled.load()) <= 0.0001f && std::abs(restored.cabRoomAmount.load() - rack.cabRoomAmount.load()) <= 0.0001f @@ -50485,7 +51419,7 @@ juce::var AudioEngine::runNAMRackRegression() { const auto restoreTree = [] ( const juce::ValueTree& tree, - S13NAMRack& target) + OpenStudioNAMRack& target) { juce::MemoryBlock stateData; juce::MemoryOutputStream stream(stateData, false); @@ -50495,7 +51429,7 @@ juce::var AudioEngine::runNAMRackRegression() static_cast(stateData.getSize())); }; - juce::ValueTree v15("S13NAMRack"); + juce::ValueTree v15("OpenStudioNAMRack"); v15.setProperty("namEffectsDspVersion", 15, nullptr); v15.setProperty("preEqEnabled", 1.0, nullptr); v15.setProperty("preEq100Db", 12.0, nullptr); @@ -50503,7 +51437,7 @@ juce::var AudioEngine::runNAMRackRegression() v15.setProperty("preEqLPFHz", 3100.0, nullptr); v15.setProperty("preEqHPFLastActiveHz", 175.0, nullptr); v15.setProperty("preEqLPFLastActiveHz", 3100.0, nullptr); - S13NAMRack migratedV15; + OpenStudioNAMRack migratedV15; const bool v15Restored = restoreTree(v15, migratedV15); const bool v15CollisionRejected = v15Restored && migratedV15.preEqEnabled.load() < 0.5f @@ -50514,7 +51448,7 @@ juce::var AudioEngine::runNAMRackRegression() && migratedV15.preEqHPFLastActiveHz.load() == 80.0f && migratedV15.preEqLPFLastActiveHz.load() == 12000.0f; - juce::ValueTree v18("S13NAMRack"); + juce::ValueTree v18("OpenStudioNAMRack"); v18.setProperty( "namEffectsDspVersion", 18, @@ -50532,7 +51466,7 @@ juce::var AudioEngine::runNAMRackRegression() v18.setProperty("preEqLPFHz", 24000.0, nullptr); v18.setProperty("preEqHPFLastActiveHz", 67.0, nullptr); v18.setProperty("preEqLPFLastActiveHz", 12700.0, nullptr); - S13NAMRack restoredV18; + OpenStudioNAMRack restoredV18; const bool v18Restored = restoreTree(v18, restoredV18); juce::MemoryBlock roundTripState; restoredV18.getStateInformation(roundTripState); @@ -50555,7 +51489,7 @@ juce::var AudioEngine::runNAMRackRegression() && roundTripTree.isValid() && static_cast(roundTripTree.getProperty( "namEffectsDspVersion", 0)) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && std::abs(static_cast(roundTripTree.getProperty( "preEqHPFLastActiveHz", 0.0)) - 67.0) <= 1.0e-9 && std::abs(static_cast(roundTripTree.getProperty( @@ -50586,14 +51520,14 @@ juce::var AudioEngine::runNAMRackRegression() const auto restoreRetiredPrecisionDriveVoice = [] (int effectsVersion) { - juce::ValueTree voiceTree("S13NAMRack"); + juce::ValueTree voiceTree("OpenStudioNAMRack"); voiceTree.setProperty("namEffectsDspVersion", effectsVersion, nullptr); voiceTree.setProperty("precisionDriveVoice", 1.0, nullptr); juce::MemoryBlock voiceState; juce::MemoryOutputStream voiceStream(voiceState, false); voiceTree.writeToStream(voiceStream); - S13NAMRack voiceRack; + OpenStudioNAMRack voiceRack; const bool restored = voiceRack.restoreProjectStateInformation( voiceState.getData(), static_cast(voiceState.getSize())); juce::MemoryBlock canonicalState; @@ -50603,12 +51537,12 @@ juce::var AudioEngine::runNAMRackRegression() return restored && juce::roundToInt(voiceRack.precisionDriveVoice.load( std::memory_order_relaxed)) - == S13NAMRack::precisionDrivePrecisionVoice + == OpenStudioNAMRack::precisionDrivePrecisionVoice && canonicalTree.isValid() && ! canonicalTree.hasProperty("precisionDriveVoice"); }; - S13NAMRack retiredVoiceProbeRack; + OpenStudioNAMRack retiredVoiceProbeRack; const float retiredVoiceBefore = retiredVoiceProbeRack.precisionDriveVoice.load( std::memory_order_relaxed); const bool retiredVoiceSetterRejected = @@ -50641,7 +51575,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool v15RetiredVoicePruned = restoreRetiredPrecisionDriveVoice(15); const bool v17RetiredVoicePruned = restoreRetiredPrecisionDriveVoice(17); const bool v18RetiredVoicePruned = restoreRetiredPrecisionDriveVoice( - S13NAMRack::currentNAMEffectsDspVersion); + OpenStudioNAMRack::currentNAMEffectsDspVersion); const bool retiredPrecisionDriveVoicePass = v14RetiredVoicePruned && v15RetiredVoicePruned @@ -50679,7 +51613,7 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr double voiceSampleRate = 48000.0; constexpr int voiceRenderSamples = 8192; - S13NAMRack voiceRack; + OpenStudioNAMRack voiceRack; voiceRack.ampEnabled.store(0.0f); voiceRack.cabEnabled.store(0.0f); voiceRack.gateThresholdDb.store(-100.0f); @@ -50707,13 +51641,13 @@ juce::var AudioEngine::runNAMRackRegression() { voiceRack.precisionDriveVoice.store( static_cast( - S13NAMRack::screamerDriveVoice)); + OpenStudioNAMRack::screamerDriveVoice)); } else if (automateVoice && absoluteSample == 5120) { voiceRack.precisionDriveVoice.store( static_cast( - S13NAMRack::precisionDrivePrecisionVoice)); + OpenStudioNAMRack::precisionDrivePrecisionVoice)); } const int requestedBlock = blockPattern.empty() ? 128 @@ -50771,27 +51705,27 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto precisionVoiceRender = renderPrecisionDriveVoice( - S13NAMRack::precisionDrivePrecisionVoice, + OpenStudioNAMRack::precisionDrivePrecisionVoice, { 128 }, false, false); const auto screamerVoiceRender = renderPrecisionDriveVoice( - S13NAMRack::screamerDriveVoice, + OpenStudioNAMRack::screamerDriveVoice, { 128 }, false, false); const auto precisionPartitionRender = renderPrecisionDriveVoice( - S13NAMRack::precisionDrivePrecisionVoice, + OpenStudioNAMRack::precisionDrivePrecisionVoice, { 17, 61, 113, 29 }, false, false); const auto automatedVoiceRender = renderPrecisionDriveVoice( - S13NAMRack::precisionDrivePrecisionVoice, + OpenStudioNAMRack::precisionDrivePrecisionVoice, { 64 }, true, false); const auto screamerSilenceRender = renderPrecisionDriveVoice( - S13NAMRack::screamerDriveVoice, + OpenStudioNAMRack::screamerDriveVoice, { 31, 97 }, false, true); @@ -50918,14 +51852,14 @@ juce::var AudioEngine::runNAMRackRegression() { constexpr int totalSamples = 16384; constexpr int preparedBlockSize = 512; - S13NAMRack probeRack; + OpenStudioNAMRack probeRack; configureNeutralRack(probeRack); probeRack.setRoutedInputChannelCount(2); probeRack.inputTrimDb.store(inputTrimDb); probeRack.calibrationReferenceDbu.store(12.0f); probeRack.precisionDriveEnabled.store(1.0f); probeRack.precisionDriveVoice.store( - static_cast(S13NAMRack::screamerDriveVoice)); + static_cast(OpenStudioNAMRack::screamerDriveVoice)); probeRack.precisionDriveDrive.store(driveAmount); probeRack.precisionDriveBright.store(toneAmount); probeRack.precisionDriveAttack.store(bodyTightAmount); @@ -51326,14 +52260,14 @@ juce::var AudioEngine::runNAMRackRegression() const int lateGapWindowEnd = static_cast( std::round(sampleRate * lateGapWindowEndSeconds)); - S13NAMRack rack; + OpenStudioNAMRack rack; configureNeutralRack(rack); rack.setRoutedInputChannelCount(2); rack.inputTrimDb.store(1.92f); rack.calibrationReferenceDbu.store(12.0f); rack.precisionDriveEnabled.store(1.0f); rack.precisionDriveVoice.store( - static_cast(S13NAMRack::screamerDriveVoice)); + static_cast(OpenStudioNAMRack::screamerDriveVoice)); rack.precisionDriveDrive.store(0.0f); rack.precisionDriveBright.store(0.598f); rack.precisionDriveAttack.store(0.916f); @@ -51784,12 +52718,12 @@ juce::var AudioEngine::runNAMRackRegression() const juce::var& profile, bool includeProperty) { - juce::ValueTree tree("S13NAMRack"); + juce::ValueTree tree("OpenStudioNAMRack"); if (includeProperty) { tree.setProperty( "namEffectsDspVersion", - S13NAMRack::currentNAMEffectsDspVersion, + OpenStudioNAMRack::currentNAMEffectsDspVersion, nullptr); tree.setProperty("instrumentProfile", profile, nullptr); } @@ -51804,7 +52738,7 @@ juce::var AudioEngine::runNAMRackRegression() { auto profileState = makeInstrumentProfileState( profile, includeProperty); - S13NAMRack profileRack; + OpenStudioNAMRack profileRack; const bool restoredState = profileRack.restoreProjectStateInformation( profileState.getData(), @@ -51814,69 +52748,69 @@ juce::var AudioEngine::runNAMRackRegression() : -1; }; const bool instrumentProfileMigrationPassed = - S13NAMRack().getInstrumentProfile() - == S13NAMRack::guitarInstrumentProfile + OpenStudioNAMRack().getInstrumentProfile() + == OpenStudioNAMRack::guitarInstrumentProfile && restoreInstrumentProfile({}, false) - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && restoreInstrumentProfile( std::numeric_limits::quiet_NaN(), true) - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && restoreInstrumentProfile( std::numeric_limits::infinity(), true) - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && restoreInstrumentProfile(-100.0, true) - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && restoreInstrumentProfile(0.49, true) - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && restoreInstrumentProfile(0.5, true) - == S13NAMRack::bassInstrumentProfile + == OpenStudioNAMRack::bassInstrumentProfile && restoreInstrumentProfile(100.0, true) - == S13NAMRack::guitarInstrumentProfile; - juce::ValueTree preV8CollisionTree("S13NAMRack"); + == OpenStudioNAMRack::guitarInstrumentProfile; + juce::ValueTree preV8CollisionTree("OpenStudioNAMRack"); preV8CollisionTree.setProperty( "namEffectsDspVersion", 7, nullptr); preV8CollisionTree.setProperty( "instrumentProfile", - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, nullptr); juce::MemoryBlock preV8CollisionState; juce::MemoryOutputStream preV8CollisionStream( preV8CollisionState, false); preV8CollisionTree.writeToStream(preV8CollisionStream); - S13NAMRack preV8CollisionRack; + OpenStudioNAMRack preV8CollisionRack; const bool preV8CollisionPassed = preV8CollisionRack.restoreProjectStateInformation( preV8CollisionState.getData(), static_cast(preV8CollisionState.getSize())) && preV8CollisionRack.getInstrumentProfile() - == S13NAMRack::guitarInstrumentProfile; - juce::ValueTree v8ProfileTree("S13NAMRack"); + == OpenStudioNAMRack::guitarInstrumentProfile; + juce::ValueTree v8ProfileTree("OpenStudioNAMRack"); v8ProfileTree.setProperty( "namEffectsDspVersion", 8, nullptr); v8ProfileTree.setProperty( "instrumentProfile", - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, nullptr); juce::MemoryBlock v8ProfileState; juce::MemoryOutputStream v8ProfileStream( v8ProfileState, false); v8ProfileTree.writeToStream(v8ProfileStream); - S13NAMRack v8ProfileRack; + OpenStudioNAMRack v8ProfileRack; const bool v8ProfilePreserved = v8ProfileRack.restoreProjectStateInformation( v8ProfileState.getData(), static_cast(v8ProfileState.getSize())) && v8ProfileRack.getInstrumentProfile() - == S13NAMRack::bassInstrumentProfile; - S13NAMRack bassProfileRoundTripSource; + == OpenStudioNAMRack::bassInstrumentProfile; + OpenStudioNAMRack bassProfileRoundTripSource; bassProfileRoundTripSource.instrumentProfile.store( - static_cast(S13NAMRack::bassInstrumentProfile)); + static_cast(OpenStudioNAMRack::bassInstrumentProfile)); juce::MemoryBlock bassProfileRoundTripState; bassProfileRoundTripSource.getStateInformation( bassProfileRoundTripState); - S13NAMRack bassProfileRoundTripDestination; + OpenStudioNAMRack bassProfileRoundTripDestination; const bool bassProfileRoundTripPassed = bassProfileRoundTripDestination .restoreProjectStateInformation( @@ -51884,7 +52818,7 @@ juce::var AudioEngine::runNAMRackRegression() static_cast( bassProfileRoundTripState.getSize())) && bassProfileRoundTripDestination.getInstrumentProfile() - == S13NAMRack::bassInstrumentProfile; + == OpenStudioNAMRack::bassInstrumentProfile; addCheck( checks, "instrument_profile_default_and_migration", @@ -51899,7 +52833,7 @@ juce::var AudioEngine::runNAMRackRegression() && v8ProfilePreserved && bassProfileRoundTripPassed); - S13NAMRack instrumentProfileSetterProbe; + OpenStudioNAMRack instrumentProfileSetterProbe; const auto setProfileForProbe = [&] (float value) { const bool accepted = setBuiltInProcessorParam( @@ -51979,17 +52913,17 @@ juce::var AudioEngine::runNAMRackRegression() [] (bool matched) { return matched; }); const bool instrumentProfileSetterPassed = setterBass.first - && setterBass.second == S13NAMRack::bassInstrumentProfile + && setterBass.second == OpenStudioNAMRack::bassInstrumentProfile && setterNegative.first - && setterNegative.second == S13NAMRack::guitarInstrumentProfile + && setterNegative.second == OpenStudioNAMRack::guitarInstrumentProfile && setterTooHigh.first - && setterTooHigh.second == S13NAMRack::guitarInstrumentProfile + && setterTooHigh.second == OpenStudioNAMRack::guitarInstrumentProfile && setterHalf.first - && setterHalf.second == S13NAMRack::bassInstrumentProfile + && setterHalf.second == OpenStudioNAMRack::bassInstrumentProfile && setterNan.first - && setterNan.second == S13NAMRack::guitarInstrumentProfile + && setterNan.second == OpenStudioNAMRack::guitarInstrumentProfile && setterBassForSchema.first - && setterBassForSchema.second == S13NAMRack::bassInstrumentProfile + && setterBassForSchema.second == OpenStudioNAMRack::bassInstrumentProfile && profileSchemaNonAutomatable && bassPreEqSchemaLabelsPassed; auto* instrumentProfileSetterDiagnostics = @@ -52031,7 +52965,7 @@ juce::var AudioEngine::runNAMRackRegression() int effectsVersion, bool includeProperty) { - juce::ValueTree tree("S13NAMRack"); + juce::ValueTree tree("OpenStudioNAMRack"); tree.setProperty( "namEffectsDspVersion", effectsVersion, nullptr); if (includeProperty) @@ -52039,68 +52973,68 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryBlock state; juce::MemoryOutputStream stream(state, false); tree.writeToStream(stream); - S13NAMRack rackForState; + OpenStudioNAMRack rackForState; const bool restoredState = rackForState.restoreProjectStateInformation( state.getData(), static_cast(state.getSize())); return restoredState - ? S13NAMRack::sanitizeReverbVoice( + ? OpenStudioNAMRack::sanitizeReverbVoice( rackForState.reverbVoice.load( std::memory_order_relaxed)) : -1; }; const bool reverbVoiceStateMigrationPassed = - S13NAMRack::sanitizeReverbVoice( - S13NAMRack().reverbVoice.load( + OpenStudioNAMRack::sanitizeReverbVoice( + OpenStudioNAMRack().reverbVoice.load( std::memory_order_relaxed)) - == S13NAMRack::studioReverbVoice + == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( {}, - S13NAMRack::currentNAMEffectsDspVersion, - false) == S13NAMRack::studioReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + false) == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( 3.0, - S13NAMRack::reverbVoiceIntroducedNAMEffectsDspVersion, - true) == S13NAMRack::roomReverbVoice + OpenStudioNAMRack::reverbVoiceIntroducedNAMEffectsDspVersion, + true) == OpenStudioNAMRack::roomReverbVoice && restoreReverbVoiceForProbe( 3.0, - S13NAMRack::reverbVoiceIntroducedNAMEffectsDspVersion - 1, - true) == S13NAMRack::studioReverbVoice + OpenStudioNAMRack::reverbVoiceIntroducedNAMEffectsDspVersion - 1, + true) == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( std::numeric_limits::quiet_NaN(), - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::studioReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( std::numeric_limits::infinity(), - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::studioReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( -100.0, - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::studioReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( 0.49, - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::studioReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::studioReverbVoice && restoreReverbVoiceForProbe( 0.5, - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::plateReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::plateReverbVoice && restoreReverbVoiceForProbe( 2.49, - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::hallReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::hallReverbVoice && restoreReverbVoiceForProbe( 2.5, - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::roomReverbVoice + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::roomReverbVoice && restoreReverbVoiceForProbe( 100.0, - S13NAMRack::currentNAMEffectsDspVersion, - true) == S13NAMRack::roomReverbVoice; + OpenStudioNAMRack::currentNAMEffectsDspVersion, + true) == OpenStudioNAMRack::roomReverbVoice; - S13NAMRack reverbVoiceSetterProbe; + OpenStudioNAMRack reverbVoiceSetterProbe; const auto setReverbVoiceForProbe = [&] (float value) { const bool accepted = setBuiltInProcessorParam( @@ -52109,7 +53043,7 @@ juce::var AudioEngine::runNAMRackRegression() value); return std::pair { accepted, - S13NAMRack::sanitizeReverbVoice( + OpenStudioNAMRack::sanitizeReverbVoice( reverbVoiceSetterProbe.reverbVoice.load( std::memory_order_relaxed)) }; @@ -52130,22 +53064,22 @@ juce::var AudioEngine::runNAMRackRegression() const bool reverbVoiceSetterPassed = voiceSetterNegative.first && voiceSetterNegative.second - == S13NAMRack::studioReverbVoice + == OpenStudioNAMRack::studioReverbVoice && voiceSetterHalf.first && voiceSetterHalf.second - == S13NAMRack::plateReverbVoice + == OpenStudioNAMRack::plateReverbVoice && voiceSetterTwoFortyNine.first && voiceSetterTwoFortyNine.second - == S13NAMRack::hallReverbVoice + == OpenStudioNAMRack::hallReverbVoice && voiceSetterTwoFive.first && voiceSetterTwoFive.second - == S13NAMRack::roomReverbVoice + == OpenStudioNAMRack::roomReverbVoice && voiceSetterHigh.first && voiceSetterHigh.second - == S13NAMRack::roomReverbVoice + == OpenStudioNAMRack::roomReverbVoice && voiceSetterNan.first && voiceSetterNan.second - == S13NAMRack::studioReverbVoice; + == OpenStudioNAMRack::studioReverbVoice; const auto reverbVoiceSchema = describeBuiltInProcessor( &reverbVoiceSetterProbe, "track", 0); @@ -52203,6 +53137,214 @@ juce::var AudioEngine::runNAMRackRegression() "V9 and newer state must preserve Studio/Plate/Hall/Room through the current V11 migration; pre-V9 collisions force Studio, bridge input uses finite round-then-clamp with non-finite fallback, and the four-option topology enum is explicit and non-automatable.", juce::var(reverbVoiceStateDiagnostics)); + auto runNAMRackGlobalGatePlacementProbe = [&] () + { + constexpr double sampleRate = 48000.0; + constexpr int preparedBlockSize = 64; + constexpr int totalSamples = 32768; + constexpr int analysisSamples = 8192; + + struct Capture + { + juce::AudioBuffer preDrive; + juce::AudioBuffer finalRack; + }; + + const auto render = [&] ( + float thresholdDb, + float inputAmplitude, + const std::vector& partitions) + { + Capture capture; + capture.preDrive.setSize(2, totalSamples); + capture.finalRack.setSize(2, totalSamples); + capture.preDrive.clear(); + capture.finalRack.clear(); + + OpenStudioNAMRack rack; + configureNeutralRack(rack); + rack.gateThresholdDb.store( + thresholdDb, std::memory_order_relaxed); + rack.gateReleaseMs.store(40.0f, std::memory_order_relaxed); + rack.precisionDriveEnabled.store( + 1.0f, std::memory_order_relaxed); + rack.precisionDriveDrive.store( + 0.35f, std::memory_order_relaxed); + rack.precisionDriveAttack.store( + 0.50f, std::memory_order_relaxed); + rack.precisionDriveBright.store( + 0.55f, std::memory_order_relaxed); + rack.precisionDriveGate.store( + 0.0f, std::memory_order_relaxed); + rack.precisionDriveVolumeDb.store( + 9.0f, std::memory_order_relaxed); + rack.prepareToPlay(sampleRate, preparedBlockSize); + + OpenStudioNAMRack::OfflineStageCaptureTarget target; + target.destinations[static_cast( + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)] = + &capture.preDrive; + target.destinations[static_cast( + OpenStudioNAMRack::OfflineStageCapturePoint::finalRack)] = + &capture.finalRack; + target.maximumSamples = totalSamples; + rack.offlineStageCaptureTarget = ⌖ + + const std::vector fallback { preparedBlockSize }; + const auto& pattern = partitions.empty() + ? fallback : partitions; + int cursor = 0; + size_t partitionIndex = 0; + juce::MidiBuffer midi; + while (cursor < totalSamples) + { + const int blockSize = juce::jmin( + preparedBlockSize, + juce::jmax( + 1, + pattern[partitionIndex % pattern.size()]), + totalSamples - cursor); + juce::AudioBuffer block(2, blockSize); + for (int sample = 0; sample < blockSize; ++sample) + { + const int absoluteSample = cursor + sample; + const float value = inputAmplitude + * static_cast(std::sin( + juce::MathConstants::twoPi + * 97.0 + * static_cast(absoluteSample) + / sampleRate)); + block.setSample(0, sample, value); + block.setSample(1, sample, value); + } + rack.processBlock(block, midi); + cursor += blockSize; + ++partitionIndex; + } + rack.offlineStageCaptureTarget = nullptr; + return capture; + }; + + const auto tailRms = [=] ( + const juce::AudioBuffer& audio) + { + const int start = juce::jmax( + 0, audio.getNumSamples() - analysisSamples); + double sumSquares = 0.0; + int count = 0; + for (int channel = 0; + channel < audio.getNumChannels(); + ++channel) + { + for (int sample = start; + sample < audio.getNumSamples(); + ++sample) + { + const double value = audio.getSample(channel, sample); + sumSquares += value * value; + ++count; + } + } + return count > 0 + ? std::sqrt(sumSquares / static_cast(count)) + : 0.0; + }; + const auto maximumError = [] ( + const juce::AudioBuffer& left, + const juce::AudioBuffer& right) + { + float maximum = 0.0f; + const int channels = juce::jmin( + left.getNumChannels(), right.getNumChannels()); + const int samples = juce::jmin( + left.getNumSamples(), right.getNumSamples()); + for (int channel = 0; channel < channels; ++channel) + { + for (int sample = 0; sample < samples; ++sample) + { + maximum = juce::jmax( + maximum, + std::abs( + left.getSample(channel, sample) + - right.getSample(channel, sample))); + } + } + return maximum; + }; + const auto maximumTailError = [=] ( + const juce::AudioBuffer& left, + const juce::AudioBuffer& right) + { + float maximum = 0.0f; + const int channels = juce::jmin( + left.getNumChannels(), right.getNumChannels()); + const int samples = juce::jmin( + left.getNumSamples(), right.getNumSamples()); + const int start = juce::jmax(0, samples - analysisSamples); + for (int channel = 0; channel < channels; ++channel) + { + for (int sample = start; sample < samples; ++sample) + { + maximum = juce::jmax( + maximum, + std::abs( + left.getSample(channel, sample) + - right.getSample(channel, sample))); + } + } + return maximum; + }; + + const auto quietUngated = render(-100.0f, 0.0001f, { 64 }); + const auto quietGated = render(-50.0f, 0.0001f, { 64 }); + const auto quietGatedUneven = render( + -50.0f, 0.0001f, { 7, 31, 5, 64, 11, 47 }); + const auto openUngated = render(-100.0f, 0.05f, { 64 }); + const auto openGated = render(-50.0f, 0.05f, { 64 }); + + const float quietPreDriveError = maximumError( + quietUngated.preDrive, quietGated.preDrive); + const double quietUngatedRms = tailRms(quietUngated.finalRack); + const double quietGatedRms = tailRms(quietGated.finalRack); + const double closedRatio = quietUngatedRms > 1.0e-12 + ? quietGatedRms / quietUngatedRms + : 1.0; + // A gate is expected to open from silence with a short attack. Once + // the detector is held above threshold, however, it must be exactly + // transparent; compare the settled analysis window rather than the + // deliberate onset transition. + const float openOutputError = maximumTailError( + openUngated.finalRack, openGated.finalRack); + const float partitionError = maximumError( + quietGated.finalRack, quietGatedUneven.finalRack); + const bool pass = + quietPreDriveError <= 1.0e-8f + && quietUngatedRms > 1.0e-7 + && closedRatio <= 0.01 + && openOutputError <= 1.0e-6f + && partitionError <= 1.0e-5f + && std::isfinite(closedRatio); + + auto* value = new juce::DynamicObject(); + value->setProperty("quietPreDriveMaximumError", quietPreDriveError); + value->setProperty("quietUngatedTailRms", quietUngatedRms); + value->setProperty("quietGatedTailRms", quietGatedRms); + value->setProperty("closedTailRatio", closedRatio); + value->setProperty("openOutputMaximumError", openOutputError); + value->setProperty("partitionMaximumError", partitionError); + value->setProperty("pass", pass); + return juce::var(value); + }; + const auto namRackGlobalGatePlacement = + runNAMRackGlobalGatePlacementProbe(); + addCheck( + checks, + "nam_rack_global_gate_post_nonlinear_placement", + namRackGlobalGatePlacement.getProperty("pass", false) + ? "pass" : "fail", + "The linked detector must leave the clean signal entering Precision Drive unchanged, apply at least 40 dB of settled attenuation after the nonlinear/model slots, remain transparent while open, and retain fixed-versus-uneven callback equivalence. This deterministic topology check does not assert musical sustain or perceived hiss reduction.", + namRackGlobalGatePlacement); + // Instrument Profile is intentionally a narrow voicing selector rather // than a hidden preset rewrite. Exercise every profile-aware native stage // here, then separately prove that the explicitly invariant components @@ -52302,6 +53444,10 @@ juce::var AudioEngine::runNAMRackRegression() std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN() }; + std::array precisionProfileCleanLowTargets { + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN() + }; const auto renderProfileResponseStage = [&] ( int profile, @@ -52309,7 +53455,7 @@ juce::var AudioEngine::runNAMRackRegression() float frequency, float amplitude) { - S13NAMRack profileRack; + OpenStudioNAMRack profileRack; configureNeutralRack(profileRack); profileRack.instrumentProfile.store( static_cast(profile), std::memory_order_relaxed); @@ -52374,10 +53520,16 @@ juce::var AudioEngine::runNAMRackRegression() profileRack.prepareToPlay( profileMatrixSampleRate, profileMatrixMaximumBlock); - if (stage == ProfileResponseStage::distortion) + const auto profileIndex = static_cast( + profile == OpenStudioNAMRack::bassInstrumentProfile ? 1 : 0); + if (stage == ProfileResponseStage::precision) + { + precisionProfileCleanLowTargets[profileIndex] = + profileRack.smoothedPrecisionDriveBassLowPreservation + .getTargetValue(); + } + else if (stage == ProfileResponseStage::distortion) { - const auto profileIndex = static_cast( - profile == S13NAMRack::bassInstrumentProfile ? 1 : 0); distortionProfileLowPreservationTargets[profileIndex] = profileRack.smoothedChaosLowPreservation .getTargetValue(); @@ -52431,12 +53583,12 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto guitarPrecisionCapture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::precision, 41.2034f, 0.008f); const auto bassPrecisionCapture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::precision, 41.2034f, 0.008f); @@ -52452,12 +53604,12 @@ juce::var AudioEngine::runNAMRackRegression() profileMatrixAnalysisSamples); const auto guitarDistortionCapture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::distortion, 41.2034f, 0.06f); const auto bassDistortionCapture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::distortion, 41.2034f, 0.06f); @@ -52502,12 +53654,12 @@ juce::var AudioEngine::runNAMRackRegression() > distortionProbeInputFundamental * 0.02f; const auto guitarAmpCapture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::ampWrapper, 41.2034f, 0.08f); const auto bassAmpCapture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::ampWrapper, 41.2034f, 0.08f); @@ -52523,22 +53675,22 @@ juce::var AudioEngine::runNAMRackRegression() profileMatrixAnalysisSamples); const auto guitarPreEq50Capture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::preEq, 50.0f, 0.08f); const auto bassPreEq50Capture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::preEq, 50.0f, 0.08f); const auto guitarPreEq120Capture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::preEq, 120.0f, 0.08f); const auto bassPreEq120Capture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::preEq, 120.0f, 0.08f); @@ -52572,32 +53724,32 @@ juce::var AudioEngine::runNAMRackRegression() -160.0f); const auto guitarEq31Capture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::graphicEq, 31.0f, 0.08f); const auto bassEq31Capture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::graphicEq, 31.0f, 0.08f); const auto guitarEq65Capture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::graphicEq, 65.0f, 0.08f); const auto bassEq65Capture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::graphicEq, 65.0f, 0.08f); const auto guitarEq125Capture = renderProfileResponseStage( - S13NAMRack::guitarInstrumentProfile, + OpenStudioNAMRack::guitarInstrumentProfile, ProfileResponseStage::graphicEq, 125.0f, 0.08f); const auto bassEq125Capture = renderProfileResponseStage( - S13NAMRack::bassInstrumentProfile, + OpenStudioNAMRack::bassInstrumentProfile, ProfileResponseStage::graphicEq, 125.0f, 0.08f); @@ -52647,8 +53799,19 @@ juce::var AudioEngine::runNAMRackRegression() const bool profileResponsePassed = std::isfinite(guitarPrecisionFundamental) && std::isfinite(bassPrecisionFundamental) + // Bass selects a strong unity clean-low replacement without using a + // hidden loudness boost as the proof of low-end preservation. Keep + // the 41 Hz fundamental level-matched within 1 dB while asserting the + // actual profile split endpoints independently. + && bassPrecisionFundamental + > guitarPrecisionFundamental + * juce::Decibels::decibelsToGain(-1.0f) && bassPrecisionFundamental - > guitarPrecisionFundamental * 1.08f + < guitarPrecisionFundamental + * juce::Decibels::decibelsToGain(1.0f) + && std::abs(precisionProfileCleanLowTargets[0]) <= 1.0e-6f + && std::abs(precisionProfileCleanLowTargets[1] - 0.72f) + <= 1.0e-6f && std::isfinite(guitarDistortionFundamental) && std::isfinite(bassDistortionFundamental) // Assert the current bounded profile mapping, not the obsolete @@ -52681,6 +53844,12 @@ juce::var AudioEngine::runNAMRackRegression() "precisionGuitarFundamental", guitarPrecisionFundamental); profileResponseValue->setProperty( "precisionBassFundamental", bassPrecisionFundamental); + profileResponseValue->setProperty( + "precisionGuitarCleanLowTarget", + precisionProfileCleanLowTargets[0]); + profileResponseValue->setProperty( + "precisionBassCleanLowTarget", + precisionProfileCleanLowTargets[1]); profileResponseValue->setProperty( "distortionGuitarFundamental", guitarDistortionFundamental); profileResponseValue->setProperty( @@ -52746,7 +53915,7 @@ juce::var AudioEngine::runNAMRackRegression() const auto renderInvariantProfile = [&] (int profile) { - S13NAMRack invariantRack; + OpenStudioNAMRack invariantRack; configureNeutralRack(invariantRack); invariantRack.instrumentProfile.store( static_cast(profile), std::memory_order_relaxed); @@ -52817,9 +53986,9 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto invariantGuitarCapture = renderInvariantProfile( - S13NAMRack::guitarInstrumentProfile); + OpenStudioNAMRack::guitarInstrumentProfile); const auto invariantBassCapture = renderInvariantProfile( - S13NAMRack::bassInstrumentProfile); + OpenStudioNAMRack::bassInstrumentProfile); float invariantMaximumDifference = 0.0f; for (int channel = 0; channel < 2; ++channel) { @@ -52834,7 +54003,7 @@ juce::var AudioEngine::runNAMRackRegression() - invariantBassCapture.getSample(channel, sample))); } } - S13NAMRack invariantControlRack; + OpenStudioNAMRack invariantControlRack; invariantControlRack.cabHPFHz.store(37.0f); invariantControlRack.cabLPFHz.store(9100.0f); invariantControlRack.gateThresholdDb.store(-72.0f); @@ -52880,7 +54049,7 @@ juce::var AudioEngine::runNAMRackRegression() bool chorusEnabled, float frequency) { - S13NAMRack parallelRack; + OpenStudioNAMRack parallelRack; configureNeutralRack(parallelRack); parallelRack.instrumentProfile.store( static_cast(profile), std::memory_order_relaxed); @@ -52941,21 +54110,21 @@ juce::var AudioEngine::runNAMRackRegression() }; const auto guitarChorusLowCapture = renderProfileParallelLow( - S13NAMRack::guitarInstrumentProfile, true, 41.2034f); + OpenStudioNAMRack::guitarInstrumentProfile, true, 41.2034f); const auto bassChorusLowCapture = renderProfileParallelLow( - S13NAMRack::bassInstrumentProfile, true, 41.2034f); + OpenStudioNAMRack::bassInstrumentProfile, true, 41.2034f); const auto guitarDelayLowCapture = renderProfileParallelLow( - S13NAMRack::guitarInstrumentProfile, false, 41.2034f); + OpenStudioNAMRack::guitarInstrumentProfile, false, 41.2034f); const auto bassDelayLowCapture = renderProfileParallelLow( - S13NAMRack::bassInstrumentProfile, false, 41.2034f); + OpenStudioNAMRack::bassInstrumentProfile, false, 41.2034f); const auto guitarChorusE1Capture = renderProfileParallelLow( - S13NAMRack::guitarInstrumentProfile, true, 82.4069f); + OpenStudioNAMRack::guitarInstrumentProfile, true, 82.4069f); const auto bassChorusE1Capture = renderProfileParallelLow( - S13NAMRack::bassInstrumentProfile, true, 82.4069f); + OpenStudioNAMRack::bassInstrumentProfile, true, 82.4069f); const auto guitarDelayE1Capture = renderProfileParallelLow( - S13NAMRack::guitarInstrumentProfile, false, 82.4069f); + OpenStudioNAMRack::guitarInstrumentProfile, false, 82.4069f); const auto bassDelayE1Capture = renderProfileParallelLow( - S13NAMRack::bassInstrumentProfile, false, 82.4069f); + OpenStudioNAMRack::bassInstrumentProfile, false, 82.4069f); const float guitarChorusLowGain = measureProfileMatrixTone( guitarChorusLowCapture, profileMatrixSampleRate, @@ -53039,47 +54208,47 @@ juce::var AudioEngine::runNAMRackRegression() parallelLowValue->setProperty("pass", parallelLowPassed); const auto guitarDelayProfileMacro = - S13NAMRack::resolveDelayMacroState( + OpenStudioNAMRack::resolveDelayMacroState( 420.0f, 0.32f, 0.28f, 0.50f, 0.10f, - static_cast(S13NAMRack::tapeDelayMode), + static_cast(OpenStudioNAMRack::tapeDelayMode), 1.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile); + OpenStudioNAMRack::guitarInstrumentProfile); const auto bassDelayProfileMacro = - S13NAMRack::resolveDelayMacroState( + OpenStudioNAMRack::resolveDelayMacroState( 420.0f, 0.32f, 0.28f, 0.50f, 0.10f, - static_cast(S13NAMRack::tapeDelayMode), + static_cast(OpenStudioNAMRack::tapeDelayMode), 1.0f, 0.0f, - S13NAMRack::bassInstrumentProfile); + OpenStudioNAMRack::bassInstrumentProfile); const auto guitarReverbProfileMacro = - S13NAMRack::resolveReverbMacroState( - static_cast(S13NAMRack::plateReverbVoice), + OpenStudioNAMRack::resolveReverbMacroState( + static_cast(OpenStudioNAMRack::plateReverbVoice), 0.24f, 2.1f, 0.58f, 14.0f, 135.0f, 0.0f, - S13NAMRack::guitarInstrumentProfile); + OpenStudioNAMRack::guitarInstrumentProfile); const auto bassReverbProfileMacro = - S13NAMRack::resolveReverbMacroState( - static_cast(S13NAMRack::plateReverbVoice), + OpenStudioNAMRack::resolveReverbMacroState( + static_cast(OpenStudioNAMRack::plateReverbVoice), 0.24f, 2.1f, 0.58f, 14.0f, 135.0f, 0.0f, - S13NAMRack::bassInstrumentProfile); + OpenStudioNAMRack::bassInstrumentProfile); const bool ambienceProfileMappingPassed = bassDelayProfileMacro.highPassHz > guitarDelayProfileMacro.highPassHz @@ -53126,7 +54295,7 @@ juce::var AudioEngine::runNAMRackRegression() constexpr int totalSamples = 24576; constexpr int bassAtSample = 8192; constexpr int guitarAtSample = 16384; - S13NAMRack liveRack; + OpenStudioNAMRack liveRack; configureNeutralRack(liveRack); liveRack.instrumentProfile.store(0.0f); liveRack.octaverEnabled.store(1.0f); @@ -53180,7 +54349,7 @@ juce::var AudioEngine::runNAMRackRegression() liveRack.delayDucker.store(0.0f); liveRack.reverbEnabled.store(1.0f); liveRack.reverbVoice.store( - static_cast(S13NAMRack::plateReverbVoice)); + static_cast(OpenStudioNAMRack::plateReverbVoice)); liveRack.reverbMix.store(0.12f); liveRack.reverbDecaySec.store(1.4f); liveRack.reverbTone.store(0.55f); @@ -53277,7 +54446,10 @@ juce::var AudioEngine::runNAMRackRegression() && liveProfileAnalysis[0] > 0.001 && liveProfileAnalysis[0] < 4.0 && liveProfileAnalysis[1] < 1.0 - && liveProfilePartitionDifference <= 2.0e-4f; + // This composite continues through nonlinear drive, modulation and + // reverb, which can amplify one-ULP upstream differences. The stable + // biquad matrix separately owns the strict 2e-6 linear-stage bound. + && liveProfilePartitionDifference <= 1.0e-3f; auto* liveProfileValue = new juce::DynamicObject(); liveProfileValue->setProperty( "switchToBassSample", 8192); @@ -53291,17 +54463,19 @@ juce::var AudioEngine::runNAMRackRegression() "nonFiniteSamples", liveProfileAnalysis[2]); liveProfileValue->setProperty( "maximumPartitionDifference", liveProfilePartitionDifference); + liveProfileValue->setProperty( + "acceptedMaximumPartitionDifference", 1.0e-3f); liveProfileValue->setProperty("pass", liveProfileMatrixPassed); const auto runConcurrentInstrumentProfilePublicationProbe = [&] () { constexpr int concurrentSamples = 16384; - const auto configure = [&] (S13NAMRack& rack) + const auto configure = [&] (OpenStudioNAMRack& rack) { configureNeutralRack(rack); rack.instrumentProfile.store( static_cast( - S13NAMRack::guitarInstrumentProfile)); + OpenStudioNAMRack::guitarInstrumentProfile)); rack.octaverEnabled.store(1.0f); rack.octaverDownMix.store(0.16f); rack.octaverUpMix.store(0.06f); @@ -53349,7 +54523,7 @@ juce::var AudioEngine::runNAMRackRegression() rack.delayDucker.store(0.0f); rack.reverbEnabled.store(1.0f); rack.reverbVoice.store( - static_cast(S13NAMRack::plateReverbVoice)); + static_cast(OpenStudioNAMRack::plateReverbVoice)); rack.reverbMix.store(0.06f); rack.reverbDecaySec.store(1.1f); rack.reverbTone.store(0.52f); @@ -53381,7 +54555,7 @@ juce::var AudioEngine::runNAMRackRegression() } }; const auto processProfileConsumers = [] ( - S13NAMRack& rack, + OpenStudioNAMRack& rack, juce::AudioBuffer& block, juce::MidiBuffer& midi) { @@ -53396,8 +54570,8 @@ juce::var AudioEngine::runNAMRackRegression() rack.processReverbStage(block, midi); }; - S13NAMRack publishedRack; - S13NAMRack referenceRack; + OpenStudioNAMRack publishedRack; + OpenStudioNAMRack referenceRack; configure(publishedRack); configure(referenceRack); juce::AudioBuffer publishedBlock(2, concurrentSamples); @@ -53469,9 +54643,9 @@ juce::var AudioEngine::runNAMRackRegression() const bool pass = publicationCount.load(std::memory_order_relaxed) >= 32 && (latchedProfile - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile || latchedProfile - == S13NAMRack::bassInstrumentProfile) + == OpenStudioNAMRack::bassInstrumentProfile) && publishedRack.diagnosticLastBlockInstrumentProfile.load( std::memory_order_relaxed) == latchedProfile @@ -53523,10 +54697,10 @@ juce::var AudioEngine::runNAMRackRegression() checks, "instrument_profile_dsp_response_and_live_switch_matrix", instrumentProfileDspMatrixPassed ? "pass" : "fail", - "At 48 kHz the Bass profile must measurably extend Precision Drive, Distortion, Amp-wrapper, EQ Boost, and 65-Hz EQ low-frequency behavior; invariant Gate/Compressor/Cab controls/Cab Room/Doubler and legacy Studio Reverb must remain identical; Chorus/Delay must retain bounded unity direct low end; every mapped stage, including profile-aware Plate Reverb, must survive exact Guitar-Bass-Guitar switches under fixed 8-sample and uneven callbacks; and concurrent selector publication must still produce one coherent profile for the complete block. Subjective bass tone, feel, and pedal similarity remain not_asserted pending B0/E1 audition.", + "At 48 kHz the Bass profile must measurably extend the Precision Drive clean-low split, Distortion, Amp-wrapper, EQ Boost, and 65-Hz EQ low-frequency behavior while reserving native-drive operating headroom without rewriting interface calibration; invariant Gate/Compressor/Cab controls/Cab Room/Doubler and legacy Studio Reverb must remain identical; Chorus/Delay must retain bounded unity direct low end; every mapped stage, including profile-aware Plate Reverb, must survive exact Guitar-Bass-Guitar switches under fixed 8-sample and uneven callbacks; and concurrent selector publication must still produce one coherent profile for the complete block. Subjective bass tone, feel, hiss, and pedal similarity remain not_asserted pending the user's Bass DI audition.", juce::var(instrumentProfileDspMatrixValue)); - juce::ValueTree retiredRackBbdStateTree("S13NAMRack"); + juce::ValueTree retiredRackBbdStateTree("OpenStudioNAMRack"); retiredRackBbdStateTree.setProperty( "chorusCharacter", 2.0, nullptr); juce::MemoryBlock retiredRackBbdState; @@ -53534,7 +54708,7 @@ juce::var AudioEngine::runNAMRackRegression() retiredRackBbdState, false); retiredRackBbdStateTree.writeToStream( retiredRackBbdStream); - S13NAMRack retiredRackBbdRack; + OpenStudioNAMRack retiredRackBbdRack; const bool retiredRackBbdStateLoaded = retiredRackBbdRack.restoreProjectStateInformation( retiredRackBbdState.getData(), @@ -53564,7 +54738,7 @@ juce::var AudioEngine::runNAMRackRegression() "Legacy NAM Rack BBD character state must migrate to the supported Ensemble character and resave without the retired value.", retiredRackBbdRack.chorusCharacter.load()); - juce::ValueTree retiredLaserStateTree("S13NAMRack"); + juce::ValueTree retiredLaserStateTree("OpenStudioNAMRack"); retiredLaserStateTree.setProperty("ampMix", 0.42, nullptr); retiredLaserStateTree.setProperty("laserEnabled", 1.0, nullptr); retiredLaserStateTree.setProperty("laserMode", 4.0, nullptr); @@ -53581,7 +54755,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryOutputStream retiredLaserStream( retiredLaserState, false); retiredLaserStateTree.writeToStream(retiredLaserStream); - S13NAMRack retiredLaserRack; + OpenStudioNAMRack retiredLaserRack; const bool retiredLaserStateLoaded = retiredLaserRack.restoreProjectStateInformation( retiredLaserState.getData(), @@ -53633,7 +54807,7 @@ juce::var AudioEngine::runNAMRackRegression() "Projects written during NAM Rack development may contain retired Laser fields; project restore and direct UI-state updates must preserve current controls while pruning stale Laser state.", directlyUpdatedRetiredLaserUiState); - juce::ValueTree legacyPseudoShimmerTree("S13NAMRack"); + juce::ValueTree legacyPseudoShimmerTree("OpenStudioNAMRack"); legacyPseudoShimmerTree.setProperty( "reverbShimmer", 1.0, nullptr); juce::MemoryBlock legacyPseudoShimmerState; @@ -53641,7 +54815,7 @@ juce::var AudioEngine::runNAMRackRegression() legacyPseudoShimmerState, false); legacyPseudoShimmerTree.writeToStream( legacyPseudoShimmerStream); - S13NAMRack legacyPseudoShimmerRack; + OpenStudioNAMRack legacyPseudoShimmerRack; legacyPseudoShimmerRack.setStateInformation( legacyPseudoShimmerState.getData(), static_cast( @@ -53660,11 +54834,11 @@ juce::var AudioEngine::runNAMRackRegression() "legacy_pseudo_shimmer_snapshot_migrates_to_off", std::abs(migratedLegacyShimmer) <= 0.0001f && migratedLegacyEffectsVersion - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && std::abs( migratedLegacyReverbEngineVersion - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 0.0001f ? "pass" : "fail", @@ -53684,7 +54858,7 @@ juce::var AudioEngine::runNAMRackRegression() return value; }())); - juce::ValueTree nativeV1ShimmerTree("S13NAMRack"); + juce::ValueTree nativeV1ShimmerTree("OpenStudioNAMRack"); nativeV1ShimmerTree.setProperty( "reverbShimmer", 0.64, nullptr); nativeV1ShimmerTree.setProperty( @@ -53696,7 +54870,7 @@ juce::var AudioEngine::runNAMRackRegression() nativeV1ShimmerState, false); nativeV1ShimmerTree.writeToStream( nativeV1ShimmerStream); - S13NAMRack nativeV1ShimmerRack; + OpenStudioNAMRack nativeV1ShimmerRack; nativeV1ShimmerRack.setStateInformation( nativeV1ShimmerState.getData(), static_cast( @@ -53710,7 +54884,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryBlock resavedNativeV1State; nativeV1ShimmerRack.getStateInformation( resavedNativeV1State); - S13NAMRack reloadedNativeV1ShimmerRack; + OpenStudioNAMRack reloadedNativeV1ShimmerRack; reloadedNativeV1ShimmerRack.setStateInformation( resavedNativeV1State.getData(), static_cast( @@ -53742,7 +54916,7 @@ juce::var AudioEngine::runNAMRackRegression() && std::abs( restoredNativeV1Engine - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 0.0001f && std::abs( reloadedNativeV1Shimmer @@ -53750,7 +54924,7 @@ juce::var AudioEngine::runNAMRackRegression() && std::abs( reloadedNativeV1Engine - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) <= 0.0001f ? "pass" : "fail", @@ -53758,11 +54932,11 @@ juce::var AudioEngine::runNAMRackRegression() juce::var(nativeV1ShimmerValue)); juce::ValueTree standaloneV1ReverbTree( - "S13Reverb"); + "OpenStudioReverb"); standaloneV1ReverbTree.setProperty( "algorithm", static_cast( - S13Reverb::Algorithm::Plate), + OpenStudioReverb::Algorithm::Plate), nullptr); standaloneV1ReverbTree.setProperty( "shimmerAmount", 0.61, nullptr); @@ -53771,7 +54945,7 @@ juce::var AudioEngine::runNAMRackRegression() standaloneV1ReverbState, false); standaloneV1ReverbTree.writeToStream( standaloneV1ReverbStream); - S13Reverb standaloneV1Reverb; + OpenStudioReverb standaloneV1Reverb; standaloneV1Reverb.setStateInformation( standaloneV1ReverbState.getData(), static_cast( @@ -53785,7 +54959,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::MemoryBlock resavedStandaloneV1State; standaloneV1Reverb.getStateInformation( resavedStandaloneV1State); - S13Reverb reloadedStandaloneV1Reverb; + OpenStudioReverb reloadedStandaloneV1Reverb; reloadedStandaloneV1Reverb.setStateInformation( resavedStandaloneV1State.getData(), static_cast( @@ -53826,7 +55000,7 @@ juce::var AudioEngine::runNAMRackRegression() < 1.5f ? "pass" : "fail", - "A markerless standalone S13Reverb V1 state already stored a real shimmerAmount, so it must preserve that amount and the legacy engine through load and save/reload; only the NAM Rack applies pseudo-Shimmer marker gating.", + "A markerless standalone OpenStudioReverb V1 state already stored a real shimmerAmount, so it must preserve that amount and the legacy engine through load and save/reload; only the NAM Rack applies pseudo-Shimmer marker gating.", juce::var(standaloneV1ReverbValue)); const auto restoredUiState = @@ -53869,14 +55043,14 @@ juce::var AudioEngine::runNAMRackRegression() && static_cast( restoredSnapshotValues->getProperty( "instrumentProfile")) - == S13NAMRack::guitarInstrumentProfile + == OpenStudioNAMRack::guitarInstrumentProfile && ! restoredSnapshotValues->hasProperty("precisionDriveMode") && static_cast(restoredSnapshotDsp->getProperty( "namEffectsDspVersion")) - == S13NAMRack::currentNAMEffectsDspVersion + == OpenStudioNAMRack::currentNAMEffectsDspVersion && static_cast(restoredSnapshotDsp->getProperty( "reverbEngineVersion")) - == S13NAMRack::currentReverbEngineVersion; + == OpenStudioNAMRack::currentReverbEngineVersion; addCheck(checks, "state_restore_compare_ui", uiStateRestorePass ? "pass" : "fail", @@ -53888,7 +55062,7 @@ juce::var AudioEngine::runNAMRackRegression() // must write the same float-expanded doubles that getStateInformation() // emits after restore. This also covers presets already stamped V17 by // the earlier, non-idempotent migration. - S13NAMRack legacyFloatCanonicalSource; + OpenStudioNAMRack legacyFloatCanonicalSource; juce::MemoryBlock legacyFloatCanonicalState; legacyFloatCanonicalSource.getTonePresetStateInformation( legacyFloatCanonicalState); @@ -53916,13 +55090,13 @@ juce::var AudioEngine::runNAMRackRegression() legacyFloatCanonicalStream); bool legacyFloatMigrationChanged = false; const bool legacyFloatMigrationSucceeded = - S13NAMRack::migratePresetStateToCurrent( + OpenStudioNAMRack::migratePresetStateToCurrent( legacyFloatCanonicalPayload, legacyFloatMigrationChanged); auto expectedFloatCanonicalTree = juce::ValueTree::readFromData( legacyFloatCanonicalPayload.getData(), legacyFloatCanonicalPayload.getSize()); - S13NAMRack legacyFloatCanonicalRestored; + OpenStudioNAMRack legacyFloatCanonicalRestored; const bool legacyFloatRestoreSucceeded = legacyFloatCanonicalRestored.restoreTonePresetStateInformation( legacyFloatCanonicalPayload.getData(), @@ -53932,7 +55106,7 @@ juce::var AudioEngine::runNAMRackRegression() legacyFloatRoundTripPayload); bool legacyFloatRoundTripMigrationChanged = false; const bool legacyFloatRoundTripMigrationSucceeded = - S13NAMRack::migratePresetStateToCurrent( + OpenStudioNAMRack::migratePresetStateToCurrent( legacyFloatRoundTripPayload, legacyFloatRoundTripMigrationChanged); auto actualFloatCanonicalTree = juce::ValueTree::readFromData( @@ -53999,7 +55173,7 @@ juce::var AudioEngine::runNAMRackRegression() "Restored rack state should process the fixture without NaNs and retain the post-FX tail.", makeProbeVar(restoredProcessed)); - S13NAMRack hostConfigurationRetryRack; + OpenStudioNAMRack hostConfigurationRetryRack; hostConfigurationRetryRack.prepareToPlay(44100.0, 512); juce::MemoryBlock hostConfigurationRetryState; hostConfigurationRetryRack.getStateInformation(hostConfigurationRetryState); @@ -54069,7 +55243,7 @@ juce::var AudioEngine::runNAMRackRegression() auto runNAMHostPublicationRaceProbe = [&] () -> juce::var { auto* value = new juce::DynamicObject(); - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay(44100.0, 128); const auto fixture = juce::File::getSpecialLocation( juce::File::tempDirectory).getChildFile( @@ -54109,11 +55283,11 @@ juce::var AudioEngine::runNAMRackRegression() loader.join(); } - S13NAMRack::ModelHostConfigurationSnapshot hostSnapshot; + OpenStudioNAMRack::ModelHostConfigurationSnapshot hostSnapshot; const bool hostSnapshotRead = rack.readStableModelHostConfiguration(hostSnapshot); bool publishedForCurrentHost = false; - S13NAMRack::LoadedCabIR* cabBeforeFailedRestore = nullptr; + OpenStudioNAMRack::LoadedCabIR* cabBeforeFailedRestore = nullptr; { const juce::ScopedLock lock(rack.cabIRLock); cabBeforeFailedRestore = rack.cabIR.get(); @@ -54145,14 +55319,14 @@ juce::var AudioEngine::runNAMRackRegression() const auto staleRejects = rack.cabPublicationStaleGenerationRejectCount.load( std::memory_order_relaxed); - S13NAMRack snapshotRack; + OpenStudioNAMRack snapshotRack; snapshotRack.prepareToPlay(48000.0, 64); - auto oldPedal = std::make_shared(); - auto oldAmp = std::make_shared(); - auto oldCab = std::make_shared(); - auto newPedal = std::make_shared(); - auto newAmp = std::make_shared(); - auto newCab = std::make_shared(); + auto oldPedal = std::make_shared(); + auto oldAmp = std::make_shared(); + auto oldCab = std::make_shared(); + auto newPedal = std::make_shared(); + auto newAmp = std::make_shared(); + auto newCab = std::make_shared(); snapshotRack.fallbackPedalModel.store( oldPedal.get(), std::memory_order_relaxed); snapshotRack.fallbackAmpModel.store( @@ -54245,20 +55419,20 @@ juce::var AudioEngine::runNAMRackRegression() // of its new tuple. The callback must never wait, must remain on the // complete fallback tuple at zero gain, and must publish the complete // second tuple only after that writer releases the lock. - S13NAMRack overlappingRestoreRack; + OpenStudioNAMRack overlappingRestoreRack; overlappingRestoreRack.prepareToPlay(48000.0, 64); auto overlapOldPedal = - std::make_shared(); + std::make_shared(); auto overlapOldAmp = - std::make_shared(); + std::make_shared(); auto overlapOldCab = - std::make_shared(); + std::make_shared(); auto overlapNewPedal = - std::make_shared(); + std::make_shared(); auto overlapNewAmp = - std::make_shared(); + std::make_shared(); auto overlapNewCab = - std::make_shared(); + std::make_shared(); overlappingRestoreRack.fallbackPedalModel.store( overlapOldPedal.get(), std::memory_order_relaxed); overlappingRestoreRack.fallbackAmpModel.store( @@ -54283,7 +55457,7 @@ juce::var AudioEngine::runNAMRackRegression() 1, std::memory_order_relaxed); overlappingRestoreRack.ampModelHandoff.consumedRequestGeneration = 1; overlappingRestoreRack.ampModelHandoff.phase = - S13NAMRack::AmpModelHandoffPhase::fadeOut; + OpenStudioNAMRack::AmpModelHandoffPhase::fadeOut; overlappingRestoreRack.ampModelHandoff.currentGain = 0.0f; overlappingRestoreRack.ampModelHandoff.phaseStartGain = 0.0f; overlappingRestoreRack.ampModelHandoff.phaseSamplePosition = 1; @@ -54423,6 +55597,51 @@ juce::var AudioEngine::runNAMRackRegression() "Cab publication must reject a graph prepared for a superseded host generation, strict preflight failure must leave live resources untouched, and every callback must observe either the complete old or complete new Pedal/Amp/Cab set.", namHostPublicationRaceProbe); + { + const int monitorIndex = static_cast(desiredMonitoringStageSpec.slots.size()); + const bool inserted = addMonitoringFX("OpenStudio NAM Rack"); + const juce::String presetName("__OpenStudio_monitor_address_regression__"); + const bool edited = inserted && setBuiltInPluginParam({}, "monitor", monitorIndex, "inputTrimDb", 6.0f); + const bool saved = edited && saveBuiltInFXPreset({}, "monitor", monitorIndex, presetName); + const bool recalled = saved && setBuiltInPluginParam({}, "monitor", monitorIndex, "inputTrimDb", -6.0f) + && loadBuiltInFXPreset({}, "monitor", monitorIndex, presetName); + const bool appended = recalled && addMonitoringFX("OpenStudio NAM Rack"); + const auto owner = getPublishedBuiltInProcessor({}, "monitor", monitorIndex); + auto* monitorRack = dynamic_cast(owner.processor.get()); + const bool retained = monitorRack && std::abs(monitorRack->inputTrimDb.load() - 6.0f) < 0.001f; + const auto schema = getBuiltInPluginSchema({}, "monitor", monitorIndex); + addCheck(checks, "monitor_nam_insert_edit_preset_append", inserted && edited && saved && recalled && appended && retained + && schema["name"].toString() == "OpenStudio NAM Rack" ? "pass" : "fail", + "Monitoring NAM must resolve its own editor address, recall its preset and retain live state when another effect is appended."); + if (monitorRack) + { + // Keep the hardware callback paused across both counter reads. + // renderProject resumes its caller's prior pause state; otherwise + // live monitoring can increment this counter immediately after export. + const ScopedAudioCallbackProcessingPause monitoringFixturePause(isRendering, deviceManager); + juce::AudioBuffer block(2, 64); + block.clear(); + juce::MidiBuffer events; + monitorRack->processBlock(block, events); + const auto countBefore = static_cast(monitorRack->getDiagnosticState()["processedBlockCount"]); + juce::TemporaryFile renderFile(".wav"); + const bool rendered = renderProject("master", 0.0, 0.02, renderFile.getFile().getFullPathName(), "wav", 44100.0, 24, 2, false, false, 0.0); + const auto countAfter = static_cast(monitorRack->getDiagnosticState()["processedBlockCount"]); + auto* renderEvidence = new juce::DynamicObject(); + renderEvidence->setProperty("renderSucceeded", rendered); + renderEvidence->setProperty("monitorBlocksBefore", countBefore); + renderEvidence->setProperty("monitorBlocksAfter", countAfter); + addCheck(checks, "monitor_nam_excluded_from_offline_render", rendered && countBefore > 0 && countAfter == countBefore ? "pass" : "fail", + "An active monitoring NAM processor must not receive any offline render blocks.", juce::var(renderEvidence)); + } + else addCheck(checks, "monitor_nam_excluded_from_offline_render", "fail", "Monitoring rack did not instantiate."); + if (appended) removeMonitoringFX(monitorIndex + 1); + if (inserted) removeMonitoringFX(monitorIndex); + (void) deleteBuiltInFXPreset("OpenStudio NAM Rack", presetName); + addCheck(checks, "monitor_nam_remove_restores_chain", static_cast(desiredMonitoringStageSpec.slots.size()) == monitorIndex ? "pass" : "fail", + "Removing the temporary monitoring racks must leave the previous monitoring chain intact."); + } + const bool temporaryMasterRackAdded = addMasterBuiltInFX("OpenStudio NAM Rack"); int masterPublicationLeaseRequestCount = 0; bool masterRestoreCommittedAtomically = false; @@ -54468,6 +55687,18 @@ juce::var AudioEngine::runNAMRackRegression() (void) deleteBuiltInFXPreset("OpenStudio NAM Rack", importedPresetName); const bool masterValuePrepared = setBuiltInPluginParam( {}, "master", temporaryMasterRackIndex, "inputTrimDb", 3.25f); + const auto companionFXIndex = static_cast(desiredMasterStageSpec.slots.size()); + const bool companionAdded = addMasterBuiltInFX("OpenStudio EQ"); + juce::MemoryBlock retainedRackBytes; + const bool retainedRackDecoded = retainedRackBytes.fromBase64Encoding(getMasterPluginState(temporaryMasterRackIndex)); + const auto retainedRack = retainedRackDecoded + ? juce::ValueTree::readFromData(retainedRackBytes.getData(), retainedRackBytes.getSize()) : juce::ValueTree(); + const bool liveStateRetained = masterValuePrepared && companionAdded && retainedRack.isValid() + && std::abs(static_cast(retainedRack.getProperty("inputTrimDb", -100.0)) - 3.25) < 0.001; + const bool companionRemoved = companionAdded && removeMasterFX(companionFXIndex); + addCheck(checks, "field_master_fx_append_preserves_live_nam_state", + liveStateRetained && companionRemoved ? "pass" : "fail", + "Adding another master effect must preserve the current NAM Rack controls, and removal must leave the original rack intact."); masterPresetSaved = masterValuePrepared && saveBuiltInFXPreset({}, "master", temporaryMasterRackIndex, masterPresetName); const bool masterValueChanged = setBuiltInPluginParam( @@ -55118,7 +56349,7 @@ juce::var AudioEngine::runNAMRackRegression() juce::AudioChannelSet::stereo(), juce::AudioChannelSet::mono()); - S13NAMRack layoutContractRack; + OpenStudioNAMRack layoutContractRack; const bool monoToMonoSupported = layoutContractRack.isBusesLayoutSupported(monoToMono); const bool monoToStereoSupported = @@ -55128,7 +56359,7 @@ juce::var AudioEngine::runNAMRackRegression() const bool stereoToMonoRejected = ! layoutContractRack.isBusesLayoutSupported(stereoToMono); - const auto configureNeutralRack = [] (S13NAMRack& rack) + const auto configureNeutralRack = [] (OpenStudioNAMRack& rack) { rack.gateThresholdDb.store(-100.0f); rack.inputTrimDb.store(0.0f); @@ -55141,7 +56372,7 @@ juce::var AudioEngine::runNAMRackRegression() rack.cabDoublerMix.store(0.12f); }; - S13NAMRack neutralMonoStereoRack; + OpenStudioNAMRack neutralMonoStereoRack; configureNeutralRack(neutralMonoStereoRack); neutralMonoStereoRack.setRoutedInputChannelCount(1); const bool neutralMonoStereoLayoutApplied = @@ -55201,7 +56432,7 @@ juce::var AudioEngine::runNAMRackRegression() neutralMonoStereoRack.cabDoublerMix.load() - 0.12f) <= 1.0e-6f; - S13NAMRack activeMonoStereoRack; + OpenStudioNAMRack activeMonoStereoRack; configureNeutralRack(activeMonoStereoRack); activeMonoStereoRack.setRoutedInputChannelCount(1); activeMonoStereoRack.cabRoomEnabled.store(1.0f); @@ -55261,8 +56492,8 @@ juce::var AudioEngine::runNAMRackRegression() // Room may remain armed when no speaker-voiced source exists, but it // must not record raw DI or alter the mono/stereo route. Compare an // enabled Room against its bypassed twin with no Amp NAM and no Cab IR. - S13NAMRack noSourceReferenceRack; - S13NAMRack noSourceRoomRack; + OpenStudioNAMRack noSourceReferenceRack; + OpenStudioNAMRack noSourceRoomRack; configureNeutralRack(noSourceReferenceRack); configureNeutralRack(noSourceRoomRack); noSourceReferenceRack.setRoutedInputChannelCount(1); @@ -55340,7 +56571,7 @@ juce::var AudioEngine::runNAMRackRegression() "cabRoomInputSuppressed", false)) && noSourceRoomRack.cabRoomEnabled.load() >= 0.5f; - S13NAMRack monoRack; + OpenStudioNAMRack monoRack; configureNeutralRack(monoRack); monoRack.setRoutedInputChannelCount(1); const bool monoLayoutApplied = @@ -55378,7 +56609,7 @@ juce::var AudioEngine::runNAMRackRegression() && monoNonFiniteSamples == 0 && monoOutputPeak > 1.0e-3f; - S13NAMRack stereoRack; + OpenStudioNAMRack stereoRack; configureNeutralRack(stereoRack); stereoRack.setRoutedInputChannelCount(2); const bool stereoLayoutApplied = @@ -56197,7 +57428,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, return writeBufferToWavFile(ir, ir.getNumSamples(), sampleRate, irFile); }; - auto configureRack = [] (S13NAMRack& rack) + auto configureRack = [] (OpenStudioNAMRack& rack) { rack.inputTrimDb.store(0.0f); rack.outputTrimDb.store(-6.0f); @@ -56341,7 +57572,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, auto canonicalPayload = rawPayload; bool requiredMigration = false; const bool migratable = readable - && S13NAMRack::migratePresetStateToCurrent( + && OpenStudioNAMRack::migratePresetStateToCurrent( canonicalPayload, requiredMigration); const auto canonicalTree = migratable ? juce::ValueTree::readFromData( @@ -56350,7 +57581,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, : juce::ValueTree(); const bool canonical = canonicalTree.isValid() && canonicalTree.getType() - == juce::Identifier("S13NAMRack"); + == juce::Identifier("OpenStudioNAMRack"); diagnostic->setProperty("migratable", migratable); diagnostic->setProperty("canonical", canonical); diagnostic->setProperty( @@ -56385,7 +57616,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, ? "unreadable_or_empty" : (! migratable ? "migration_failed" - : "not_s13_nam_rack_state"))); + : "not_openstudio_nam_rack_state"))); } presetCandidateDiagnostics.add(juce::var(diagnostic)); return selected; @@ -56437,10 +57668,10 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, ? static_cast(selectedPreset.rawTree.getProperty( "namEffectsDspVersion", 0)) : 0; - S13NAMRack effectsVersionProbe; + OpenStudioNAMRack effectsVersionProbe; const int currentEffectsDspVersion = effectsVersionProbe.getNAMEffectsDspVersion(); - S13NAMRack rack; + OpenStudioNAMRack rack; rack.prepareToPlay(sampleRate, fixtureBlockSize); configureRack(rack); @@ -56576,7 +57807,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, }; const auto setVariantToggle = [] ( - S13NAMRack& auditionRack, + OpenStudioNAMRack& auditionRack, const juce::String& property, float value) { @@ -56594,6 +57825,418 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, auditionRack.precisionDriveEnabled.store(value); }; + const auto runSelectedPresetLiveOnsetProbe = [&] () + { + auto* value = new juce::DynamicObject(); + value->setProperty("classification", "objective"); + value->setProperty("subjectiveQuality", "not_asserted"); + value->setProperty( + "analysisScope", + "first non-zero live-monitoring blocks after Guitar-to-Bass preset recall without reset"); + value->setProperty("available", selectedPreset.available); + if (! selectedPreset.available) + return juce::var(value); + + constexpr std::array inputPeaks {{ + 0.001f, 0.005f, 0.020f, 0.080f, + 0.220f, 0.500f, 0.900f + }}; + constexpr double probeSeconds = 1.0; + const int probeBlocks = juce::jmax( + 1, + juce::roundToInt( + probeSeconds * sampleRate + / static_cast(fixtureBlockSize))); + juce::Array cases; + bool allCasesPass = true; + for (const float inputPeak : inputPeaks) + { + OpenStudioNAMRack liveRack; + liveRack.prepareToPlay(sampleRate, fixtureBlockSize); + liveRack.setRoutedInputChannelCount(1); + // Match the user's sequence: an already-open Guitar rack is + // switched to Bass, then the complete user preset is recalled. + liveRack.instrumentProfile.store( + static_cast(OpenStudioNAMRack::bassInstrumentProfile), + std::memory_order_relaxed); + const bool restored = + liveRack.restoreTonePresetStateInformation( + selectedPreset.canonicalPayload.getData(), + static_cast( + selectedPreset.canonicalPayload.getSize())); + + float maximumOutputPeak = 0.0f; + float maximumAdjacentDelta = 0.0f; + float previousOutput = 0.0f; + int nonFiniteSamples = 0; + int audibleBlocks = 0; + int lastQuarterAudibleBlocks = 0; + juce::AudioBuffer block(2, fixtureBlockSize); + juce::MidiBuffer midi; + for (int blockIndex = 0; + blockIndex < probeBlocks; + ++blockIndex) + { + for (int sample = 0; + sample < fixtureBlockSize; + ++sample) + { + const auto absoluteSample = + blockIndex * fixtureBlockSize + sample; + const float phase = + juce::MathConstants::twoPi + * 82.4069f + * static_cast(absoluteSample) + / static_cast(sampleRate); + const float source = inputPeak * std::sin(phase); + block.setSample(0, sample, source); + block.setSample(1, sample, source); + } + + liveRack.processBlock(block, midi); + float blockPeak = 0.0f; + for (int channel = 0; + channel < block.getNumChannels(); + ++channel) + { + const auto* samples = block.getReadPointer(channel); + for (int sample = 0; + sample < fixtureBlockSize; + ++sample) + { + const float output = samples[sample]; + if (! std::isfinite(output)) + ++nonFiniteSamples; + else + { + blockPeak = juce::jmax( + blockPeak, std::abs(output)); + maximumAdjacentDelta = juce::jmax( + maximumAdjacentDelta, + std::abs(output - previousOutput)); + previousOutput = output; + } + } + } + maximumOutputPeak = juce::jmax( + maximumOutputPeak, blockPeak); + if (blockPeak > 1.0e-6f) + { + ++audibleBlocks; + if (blockIndex >= (probeBlocks * 3) / 4) + ++lastQuarterAudibleBlocks; + } + } + + const auto diagnostics = liveRack.getDiagnosticState(); + const bool emergencyMuteActive = + static_cast(diagnostics.getProperty( + "rackOutputEmergencyMuteActive", false)); + const auto emergencyMuteTripCount = + static_cast(diagnostics.getProperty( + "rackOutputEmergencyMuteTripCount", + static_cast(0))); + const int modelProcessFailCount = + static_cast(diagnostics.getProperty( + "modelProcessFailCount", 0)); + const bool ampModelFaulted = + static_cast(diagnostics.getProperty( + "ampModelFaulted", false)); + const double ampModelSampleRate = + static_cast(diagnostics.getProperty( + "ampModelSampleRate", 0.0)); + const bool crossRateModel = ampModelSampleRate > 0.0 + && std::abs(ampModelSampleRate - sampleRate) > 0.5; + const bool resamplerUsed = + static_cast(diagnostics.getProperty( + "lastResampled", false)); + const bool casePass = restored + && nonFiniteSamples == 0 + && maximumOutputPeak <= 1.0f + && lastQuarterAudibleBlocks > 0 + && ! emergencyMuteActive + && emergencyMuteTripCount == 0 + && modelProcessFailCount == 0 + && ! ampModelFaulted + && (! crossRateModel || resamplerUsed); + allCasesPass = allCasesPass && casePass; + auto* probeCase = new juce::DynamicObject(); + probeCase->setProperty("inputPeak", inputPeak); + probeCase->setProperty("stateRestored", restored); + probeCase->setProperty("pass", casePass); + probeCase->setProperty( + "maximumOutputPeak", maximumOutputPeak); + probeCase->setProperty( + "maximumAdjacentDelta", maximumAdjacentDelta); + probeCase->setProperty( + "nonFiniteSamples", nonFiniteSamples); + probeCase->setProperty("audibleBlocks", audibleBlocks); + probeCase->setProperty( + "lastQuarterAudibleBlocks", + lastQuarterAudibleBlocks); + probeCase->setProperty( + "emergencyMuteActive", + emergencyMuteActive); + probeCase->setProperty( + "emergencyMuteTripCount", + emergencyMuteTripCount); + probeCase->setProperty( + "emergencyLastTripPeak", + diagnostics.getProperty( + "rackOutputEmergencyLastTripPeakLinear", 0.0)); + probeCase->setProperty( + "emergencyLastTripDelta", + diagnostics.getProperty( + "rackOutputEmergencyLastTripDeltaLinear", 0.0)); + probeCase->setProperty( + "modelProcessFailCount", + modelProcessFailCount); + probeCase->setProperty( + "ampModelFaulted", + ampModelFaulted); + probeCase->setProperty( + "precisionDriveOutputPeak", + diagnostics.getProperty( + "pedalInputPeakLinear", 0.0)); + probeCase->setProperty( + "ampNAMMaximumRawOutputPeak", + diagnostics.getProperty( + "ampNAMMaximumRawOutputPeakLinear", 0.0)); + probeCase->setProperty( + "ampNAMMaximumRawOutputDelta", + diagnostics.getProperty( + "ampNAMMaximumRawOutputDeltaLinear", 0.0)); + probeCase->setProperty( + "postCabPeak", + diagnostics.getProperty( + "postCabPeakLinear", 0.0)); + probeCase->setProperty( + "reverbMaximumRawInputSendPeak", + diagnostics.getProperty( + "reverbMaximumRawInputSendPeakLinear", 0.0)); + probeCase->setProperty( + "reverbMaximumRawTankWritePeak", + diagnostics.getProperty( + "reverbMaximumV3RawTankWritePeakLinear", 0.0)); + probeCase->setProperty( + "reverbMaximumRawWetOutputPeak", + diagnostics.getProperty( + "reverbMaximumV3RawWetOutputPeakLinear", 0.0)); + probeCase->setProperty( + "reverbPadReturnPeak", + diagnostics.getProperty( + "reverbPadReturnPeakLinear", 0.0)); + probeCase->setProperty( + "reverbNonFiniteRecoveryCount", + diagnostics.getProperty( + "reverbV3NonFiniteStateHitCount", 0)); + probeCase->setProperty( + "reverbFiniteRunawayRecoveryCount", + diagnostics.getProperty( + "reverbV3FiniteRunawayRecoveryCount", 0)); + probeCase->setProperty( + "reportedLatencySamples", + diagnostics.getProperty( + "reportedLatencySamples", 0)); + probeCase->setProperty( + "hostSampleRate", sampleRate); + probeCase->setProperty( + "ampModelSampleRate", ampModelSampleRate); + probeCase->setProperty( + "crossRateModel", crossRateModel); + probeCase->setProperty( + "resamplerUsed", resamplerUsed); + cases.add(juce::var(probeCase)); + } + value->setProperty("sampleRate", sampleRate); + value->setProperty("blockSize", fixtureBlockSize); + value->setProperty("probeSeconds", probeSeconds); + value->setProperty("cases", juce::var(cases)); + value->setProperty("pass", allCasesPass); + return juce::var(value); + }; + const auto selectedPresetLiveOnsetProbe = + runSelectedPresetLiveOnsetProbe(); + const bool selectedPresetLiveOnsetPass = + selectedPreset.available + && static_cast( + selectedPresetLiveOnsetProbe.getProperty("pass", false)); + addCheck( + checks, + "selected_preset_live_monitoring_first_input_onset", + selectedPresetLiveOnsetPass ? "pass" : "fail", + "After Guitar-to-Bass switching, the exact preset is recalled without reset and deterministic 82.4 Hz live input begins on the first callback from -60 dBFS through -0.9 dBFS. Every case must restore, remain finite and bounded, keep producing output through the final callbacks, avoid the emergency mute, and report no NAM process fault. Subjective tone quality is not asserted.", + selectedPresetLiveOnsetProbe); + + const auto runGraphicEqFilterLiveModulationProbe = [&] () + { + struct HostConfiguration + { + double sampleRate; + int blockSize; + }; + constexpr std::array configurations {{ + { 44100.0, 8 }, + { 44100.0, 64 }, + { 44100.0, 512 }, + { 44100.0, 1024 }, + { 48000.0, 512 }, + { 96000.0, 512 } + }}; + constexpr std::array hpfTargets {{ + 34.56f, 500.0f, 20.0f, 260.0f, 45.0f, 120.0f + }}; + constexpr std::array lpfTargets {{ + 19646.0f, 1000.0f, 20000.0f, 4200.0f, 16000.0f, 7800.0f + }}; + + auto* value = new juce::DynamicObject(); + value->setProperty("classification", "objective"); + value->setProperty("subjectiveQuality", "not_asserted"); + juce::Array cases; + bool allCasesPass = irWritten; + for (const auto& configuration : configurations) + { + OpenStudioNAMRack rack; + rack.prepareToPlay( + configuration.sampleRate, + configuration.blockSize); + rack.setRoutedInputChannelCount(1); + rack.instrumentProfile.store( + static_cast(OpenStudioNAMRack::bassInstrumentProfile), + std::memory_order_relaxed); + rack.eqEnabled.store(1.0f); + rack.cabRoomEnabled.store(0.0f); + rack.cabDoublerEnabled.store(0.0f); + rack.cabLevelDb.store(0.0f); + rack.eqHPFHz.store(30.0f); + rack.eqLPFHz.store(16000.0f); + const bool cabLoaded = irWritten + && rack.loadCabIR(irFile.getFullPathName()); + + const int probeSamples = juce::roundToInt( + configuration.sampleRate * 0.45); + const int probeBlocks = juce::jmax( + 1, + (probeSamples + configuration.blockSize - 1) + / configuration.blockSize); + const int targetSegmentSamples = juce::jmax( + 1, + juce::roundToInt(configuration.sampleRate * 0.06)); + float maximumOutputPeak = 0.0f; + int nonFiniteSamples = 0; + int finalAudibleBlocks = 0; + juce::AudioBuffer block( + 2, configuration.blockSize); + juce::MidiBuffer midi; + for (int blockIndex = 0; + blockIndex < probeBlocks; + ++blockIndex) + { + const int absoluteBlockStart = + blockIndex * configuration.blockSize; + const auto targetIndex = static_cast( + (absoluteBlockStart / targetSegmentSamples) + % static_cast(hpfTargets.size())); + rack.eqHPFHz.store(hpfTargets[targetIndex]); + rack.eqLPFHz.store(lpfTargets[targetIndex]); + for (int sample = 0; + sample < configuration.blockSize; + ++sample) + { + const int absoluteSample = + absoluteBlockStart + sample; + const float time = static_cast(absoluteSample) + / static_cast(configuration.sampleRate); + const float source = 0.02f * std::sin( + juce::MathConstants::twoPi + * 82.4069f * time) + + 0.004f * std::sin( + juce::MathConstants::twoPi + * 997.0f * time); + block.setSample(0, sample, source); + block.setSample(1, sample, source); + } + + rack.processBlock(block, midi); + float blockPeak = 0.0f; + for (int channel = 0; + channel < block.getNumChannels(); + ++channel) + { + const auto* samples = block.getReadPointer(channel); + for (int sample = 0; + sample < configuration.blockSize; + ++sample) + { + const float output = samples[sample]; + if (! std::isfinite(output)) + ++nonFiniteSamples; + else + blockPeak = juce::jmax( + blockPeak, std::abs(output)); + } + } + maximumOutputPeak = juce::jmax( + maximumOutputPeak, blockPeak); + if (blockIndex >= probeBlocks - 3 + && blockPeak > 1.0e-6f) + { + ++finalAudibleBlocks; + } + } + + const auto diagnostics = rack.getDiagnosticState(); + const auto emergencyMuteTripCount = + static_cast(diagnostics.getProperty( + "rackOutputEmergencyMuteTripCount", + static_cast(0))); + const bool emergencyMuteActive = + static_cast(diagnostics.getProperty( + "rackOutputEmergencyMuteActive", false)); + const bool casePass = cabLoaded + && nonFiniteSamples == 0 + && maximumOutputPeak <= 1.0f + && finalAudibleBlocks > 0 + && emergencyMuteTripCount == 0 + && ! emergencyMuteActive; + allCasesPass = allCasesPass && casePass; + + auto* probeCase = new juce::DynamicObject(); + probeCase->setProperty( + "sampleRate", configuration.sampleRate); + probeCase->setProperty( + "blockSize", configuration.blockSize); + probeCase->setProperty("cabLoaded", cabLoaded); + probeCase->setProperty("pass", casePass); + probeCase->setProperty( + "maximumOutputPeak", maximumOutputPeak); + probeCase->setProperty( + "nonFiniteSamples", nonFiniteSamples); + probeCase->setProperty( + "finalAudibleBlocks", finalAudibleBlocks); + probeCase->setProperty( + "emergencyMuteTripCount", emergencyMuteTripCount); + probeCase->setProperty( + "emergencyMuteActive", emergencyMuteActive); + cases.add(juce::var(probeCase)); + } + value->setProperty("cases", juce::var(cases)); + value->setProperty("pass", allCasesPass); + return juce::var(value); + }; + const auto graphicEqFilterLiveModulationProbe = + runGraphicEqFilterLiveModulationProbe(); + const bool graphicEqFilterLiveModulationPass = + static_cast( + graphicEqFilterLiveModulationProbe.getProperty("pass", false)); + addCheck( + checks, + "graphic_eq_filter_live_modulation_sample_rate_block_matrix", + graphicEqFilterLiveModulationPass ? "pass" : "fail", + "From the first synthetic live-input callback, the one visible Graphic EQ HPF/LPF owner sweeps between useful extremes at 44.1, 48, and 96 kHz with 8-to-1024-sample callback sizes. Every case must remain finite, bounded, audible at the end, and avoid the Rack emergency mute. Subjective tone quality is not asserted.", + graphicEqFilterLiveModulationProbe); + const auto renderSelectedPreset = [&] ( const SelectedPresetVariantDefinition& definition) { @@ -56606,7 +58249,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, if (! selectedPreset.available) return result; - S13NAMRack auditionRack; + OpenStudioNAMRack auditionRack; auditionRack.prepareToPlay(sampleRate, fixtureBlockSize); result.stateRestored = auditionRack.restoreTonePresetStateInformation( @@ -56673,7 +58316,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, auditionRack.getTonePresetStateInformation(renderedState); bool renderedStateMigrated = false; const bool renderedStateCanonical = - S13NAMRack::migratePresetStateToCurrent( + OpenStudioNAMRack::migratePresetStateToCurrent( renderedState, renderedStateMigrated); juce::ignoreUnused(renderedStateMigrated); const auto renderedTree = renderedStateCanonical @@ -57288,7 +58931,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, } constexpr size_t internalStageCount = static_cast( - S13NAMRack::OfflineStageCapturePoint::count); + OpenStudioNAMRack::OfflineStageCapturePoint::count); constexpr size_t reportedStageCount = 6; static constexpr std::array reportedStageNames {{ @@ -57308,7 +58951,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, getNAMRackOversamplingFactor(); const auto setPreEqBands = [] ( - S13NAMRack& target, + OpenStudioNAMRack& target, const std::array& bandDb) { target.preEq120Db.store(bandDb[0]); @@ -57320,7 +58963,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, target.preEq8kDb.store(bandDb[6]); target.preEq12kDb.store(bandDb[7]); }; - const auto configureTightPreEq = [&] (S13NAMRack& target) + const auto configureTightPreEq = [&] (OpenStudioNAMRack& target) { target.preEqEnabled.store(1.0f); setPreEqBands(target, tightBandDb); @@ -57349,7 +58992,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, stage.clear(); } - S13NAMRack auditionRack; + OpenStudioNAMRack auditionRack; auditionRack.setEmbeddedDriveOversamplingFactor( selectedOversamplingFactor); auditionRack.prepareToPlay(sampleRate, fixtureBlockSize); @@ -57371,7 +59014,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, auditionRack.precisionDriveEnabled.store(1.0f); auditionRack.reset(); - S13NAMRack::OfflineStageCaptureTarget captureTarget; + OpenStudioNAMRack::OfflineStageCaptureTarget captureTarget; captureTarget.maximumSamples = selectedPresetArtifactSamples; for (size_t stage = 0; stage < internalStageCount; ++stage) captureTarget.destinations[stage] = &result.audio[stage]; @@ -57461,7 +59104,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, { destination.setSize(2, selectedPresetArtifactSamples); destination.clear(); - S13NAMRack stageRack; + OpenStudioNAMRack stageRack; stageRack.setEmbeddedDriveOversamplingFactor( selectedOversamplingFactor); stageRack.prepareToPlay(sampleRate, fixtureBlockSize); @@ -57525,19 +59168,19 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, juce::AudioBuffer silentPostPreEq; const bool preEqBypassRendered = renderPreEqStage( programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::preDriveInput)], + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)], false, false, preEqBypassed); const bool preEqFlatRendered = renderPreEqStage( programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::preDriveInput)], + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)], true, false, preEqFlat); const bool postPreEqRendered = renderPreEqStage( programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::preDriveInput)], + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)], true, true, postPreEq); const bool silentPostPreEqRendered = renderPreEqStage( silenceCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::preDriveInput)], + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)], true, true, silentPostPreEq); const float flatBypassMaximumError = preEqBypassRendered && preEqFlatRendered @@ -57554,30 +59197,30 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, std::array*, reportedStageCount> programmeStages {{ &programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::preDriveInput)], + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)], &postPreEq, &programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::postEmbeddedDrive)], + OpenStudioNAMRack::OfflineStageCapturePoint::postEmbeddedDrive)], &programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::postAmp)], + OpenStudioNAMRack::OfflineStageCapturePoint::postAmp)], &programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::postCab)], + OpenStudioNAMRack::OfflineStageCapturePoint::postCab)], &programmeCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::finalRack)] + OpenStudioNAMRack::OfflineStageCapturePoint::finalRack)] }}; std::array*, reportedStageCount> silenceStages {{ &silenceCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::preDriveInput)], + OpenStudioNAMRack::OfflineStageCapturePoint::preDriveInput)], &silentPostPreEq, &silenceCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::postEmbeddedDrive)], + OpenStudioNAMRack::OfflineStageCapturePoint::postEmbeddedDrive)], &silenceCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::postAmp)], + OpenStudioNAMRack::OfflineStageCapturePoint::postAmp)], &silenceCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::postCab)], + OpenStudioNAMRack::OfflineStageCapturePoint::postCab)], &silenceCapture.audio[static_cast( - S13NAMRack::OfflineStageCapturePoint::finalRack)] + OpenStudioNAMRack::OfflineStageCapturePoint::finalRack)] }}; const auto maximumAbsoluteChannelMean = [] ( @@ -58119,7 +59762,7 @@ juce::var AudioEngine::runNAMRackDIRegression(const juce::File& inputFile, juce::AudioBuffer& scenarioOutput, const juce::File& artifactFile) { - S13NAMRack scenarioRack; + OpenStudioNAMRack scenarioRack; scenarioRack.prepareToPlay(sampleRate, fixtureBlockSize); configureRack(scenarioRack); @@ -59226,10 +60869,8 @@ juce::var AudioEngine::runAutomatedRegressionSuite() trackSnapshot != nullptr && trackSnapshot->processingOrderCount >= 0 && trackSnapshot->processingOrderCount - <= juce::jmin( - static_cast( - trackSnapshot->tracks.size()), - maxRealtimeTracks); + == static_cast(trackSnapshot->tracks.size()) + && trackSnapshot->processingOrder.size() == trackSnapshot->tracks.size(); addSuite( "realtime_startup_snapshot_publication_fixture", @@ -59344,11 +60985,12 @@ juce::var AudioEngine::runAutomatedRegressionSuite() routingTracks, sidechainBuffers, sendBuffers); + routingSnapshot.processingOrder.resize(routingTracks.size()); buildSidechainProcessingOrder( routingTracks, routingSnapshot.processingOrder.data(), routingSnapshot.processingOrderCount, - maxRealtimeTracks); + static_cast(routingTracks.size())); const auto& resolvedDestination = routingTracks[0]; @@ -59398,7 +61040,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() && resolvedSource.sends[3].destinationBuffer == nullptr; - std::array orderSeen {}; + std::vector orderSeen(routingTracks.size(), false); bool orderIndicesValid = routingSnapshot.processingOrderCount == static_cast(routingTracks.size()); @@ -59461,6 +61103,110 @@ juce::var AudioEngine::runAutomatedRegressionSuite() destinationOrderPosition)); } + { + RealtimeTrackSnapshot largeSnapshot; + constexpr int largeTrackCount = 128; + largeSnapshot.tracks.resize(largeTrackCount); + largeSnapshot.processingOrder.resize(largeTrackCount); + TrackProcessor dependencyProbe; + for (int index = 0; index < largeTrackCount; ++index) + { + auto& entry = largeSnapshot.tracks[static_cast(index)]; + entry.id = "capacity-fixture-" + juce::String(index); + entry.processor = &dependencyProbe; + if (index + 1 < largeTrackCount) + entry.sidechainSourceIds.push_back("capacity-fixture-" + juce::String(index + 1)); + } + buildSidechainProcessingOrder(largeSnapshot.tracks, largeSnapshot.processingOrder.data(), + largeSnapshot.processingOrderCount, largeTrackCount); + bool allTracksOrdered = largeSnapshot.processingOrderCount == largeTrackCount; + for (int index = 0; index < largeTrackCount; ++index) + allTracksOrdered = allTracksOrdered && largeSnapshot.processingOrder[static_cast(index)] == largeTrackCount - 1 - index; + addSuite("realtime_routing_more_than_64_tracks", allTracksOrdered, + "128 reverse-dependent tracks must each be processed once, source before destination, without a 64-track truncation."); + } + + if (juce::SystemStats::getEnvironmentVariable("OPENSTUDIO_CAPACITY_BENCHMARK", {}).trim() == "1") + { + // Opt-in diagnostic only. No device output, disk writer or user tracks. + juce::Array measurements; + for (const int benchmarkBlockSize : { 128, 512 }) + { + constexpr double benchmarkRate = 48000.0; + constexpr int benchmarkTrackCount = 100; + constexpr int measuredBlocks = 256; + std::vector> processors; + std::vector> buffers; + juce::AudioBuffer source(2, benchmarkBlockSize), mixed(2, benchmarkBlockSize); + juce::MidiBuffer midi; + bool valid = true; + for (int sample = 0; sample < benchmarkBlockSize; ++sample) + for (int channel = 0; channel < 2; ++channel) + source.setSample(channel, sample, 0.1f * std::sin(static_cast( + 2.0 * juce::MathConstants::pi * sample / benchmarkBlockSize))); + for (int trackIndex = 0; trackIndex < benchmarkTrackCount; ++trackIndex) + { + auto processor = std::make_unique(); + processor->prepareToPlay(benchmarkRate, benchmarkBlockSize); + auto eq = std::make_unique(); + eq->bands[2].gain.store(3.0f); + valid = processor->addTrackFX(std::move(eq), benchmarkRate, benchmarkBlockSize) && valid; + valid = processor->addTrackFX(std::make_unique(), benchmarkRate, benchmarkBlockSize) && valid; + processors.push_back(std::move(processor)); + buffers.emplace_back(2, benchmarkBlockSize); + } + std::vector durations; + durations.reserve(measuredBlocks); + for (int block = -32; block < measuredBlocks; ++block) + { + const auto start = juce::Time::getMillisecondCounterHiRes(); + mixed.clear(); + for (int trackIndex = 0; trackIndex < benchmarkTrackCount; ++trackIndex) + { + auto& buffer = buffers[static_cast(trackIndex)]; + for (int channel = 0; channel < 2; ++channel) + buffer.copyFrom(channel, 0, source, channel, 0, benchmarkBlockSize); + processors[static_cast(trackIndex)]->processBlock(buffer, midi); + for (int channel = 0; channel < 2; ++channel) + mixed.addFrom(channel, 0, buffer, channel, 0, benchmarkBlockSize, 0.01f); + } + const auto elapsed = juce::Time::getMillisecondCounterHiRes() - start; + if (block >= 0) durations.push_back(elapsed); + if (block >= 0) + for (const auto& buffer : buffers) + { + float peak = 0.0f; + for (int channel = 0; channel < 2; ++channel) + for (int sample = 0; sample < benchmarkBlockSize; ++sample) + { + const float value = buffer.getSample(channel, sample); + valid = valid && std::isfinite(value); + peak = juce::jmax(peak, std::abs(value)); + } + valid = valid && peak > 1.0e-7f; + } + } + std::sort(durations.begin(), durations.end()); + const double deadlineMs = 1000.0 * benchmarkBlockSize / benchmarkRate; + auto* measurement = new juce::DynamicObject(); + measurement->setProperty("status", "diagnostic_only"); + measurement->setProperty("trackCount", benchmarkTrackCount); + measurement->setProperty("fxPerTrack", 2); + measurement->setProperty("blockSize", benchmarkBlockSize); + measurement->setProperty("sampleRate", benchmarkRate); + measurement->setProperty("deadlineMs", deadlineMs); + measurement->setProperty("p99Ms", durations[static_cast(measuredBlocks * 99 / 100)]); + measurement->setProperty("maximumMs", durations.back()); + measurement->setProperty("deadlineMisses", static_cast(std::count_if(durations.begin(), durations.end(), + [deadlineMs](double value) { return value > deadlineMs; }))); + measurement->setProperty("allTracksNonzeroAndFinite", valid); + measurements.add(juce::var(measurement)); + addSuite("100_track_eq_compressor_signal_" + juce::String(benchmarkBlockSize), valid, + "100 isolated TrackProcessors with EQ and Compressor produce finite nonzero output. Timing is diagnostic only; disk, device, NAM, UI, routing and master FX are excluded."); + } + root->setProperty("capacityDiagnostics", measurements); + } + if (juce::SystemStats::getEnvironmentVariable( "OPENSTUDIO_RT_SAFETY_FIXTURES_ONLY", {}).trim() == "1") { @@ -59537,54 +61283,54 @@ juce::var AudioEngine::runAutomatedRegressionSuite() processor.releaseResources(); }; - auto eq = std::make_unique(); + auto eq = std::make_unique(); eq->bands[2].dynamicEnabled.store(1.0f); eq->bands[2].dynamicRange.store(-4.0f); eq->bands[2].dynamicThreshold.store(-36.0f); eq->stereoMode.store(1.0f); runEffectSmoke("eq_dynamic_mid", *eq); - auto compressor = std::make_unique(); + auto compressor = std::make_unique(); compressor->threshold.store(-24.0f); compressor->ratio.store(4.0f); compressor->detectorMode.store(2.0f); runEffectSmoke("compressor", *compressor); - auto gate = std::make_unique(); + auto gate = std::make_unique(); gate->threshold.store(-42.0f); runEffectSmoke("gate", *gate); - auto limiter = std::make_unique(); + auto limiter = std::make_unique(); limiter->threshold.store(-6.0f); limiter->ceiling.store(-0.5f); runEffectSmoke("limiter", *limiter); - auto delay = std::make_unique(); + auto delay = std::make_unique(); delay->feedback.store(0.32f); delay->ducking.store(0.25f); runEffectSmoke("delay", *delay); - auto reverb = std::make_unique(); + auto reverb = std::make_unique(); reverb->algorithm.store(2.0f); reverb->decayTime.store(2.4f); runEffectSmoke("reverb", *reverb); - auto chorus = std::make_unique(); + auto chorus = std::make_unique(); chorus->characterMode.store(1.0f); runEffectSmoke("chorus_ensemble", *chorus); - auto saturator = std::make_unique(); + auto saturator = std::make_unique(); saturator->satType.store(6.0f); saturator->oversampleMode.store(2.0f); runEffectSmoke("saturator", *saturator); - auto synth = std::make_unique(); + auto synth = std::make_unique(); runInstrumentSmoke("basic_synth", *synth, 60); - auto piano = std::make_unique(); + auto piano = std::make_unique(); runInstrumentSmoke("piano", *piano, 64); - auto drums = std::make_unique(); + auto drums = std::make_unique(); drums->mapPreset.store(1.0f); juce::AudioBuffer drumBuffer(2, fixtureBlockSize); drumBuffer.clear(); @@ -59608,7 +61354,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() constexpr int fixtureBlockSize = 64; const std::array chordNotes { 36, 43, 48, 52, 55, 60, 64 }; - auto piano = std::make_unique(); + auto piano = std::make_unique(); piano->outputGain.store(-15.0f); piano->prepareToPlay(fixtureSampleRate, fixtureBlockSize); @@ -59646,7 +61392,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() auto track = std::make_unique(); track->prepareToPlay(fixtureSampleRate, fixtureBlockSize); track->setTrackType(TrackType::Instrument); - const bool fxAdded = track->addTrackFX(std::make_unique(), + const bool fxAdded = track->addTrackFX(std::make_unique(), fixtureSampleRate, fixtureBlockSize); const bool livenessPass = fxAdded @@ -59663,7 +61409,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() const int fixtureBlockSize = juce::jmax(512, currentBlockSize); auto runCompressorHPFProbe = [&] (float hpfFreq) { - auto compressor = std::make_unique(); + auto compressor = std::make_unique(); compressor->threshold.store(-28.0f); compressor->ratio.store(8.0f); compressor->attack.store(1.0f); @@ -59704,9 +61450,9 @@ juce::var AudioEngine::runAutomatedRegressionSuite() const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto eq = std::make_unique(); + auto eq = std::make_unique(); eq->bands[1].enabled.store(1.0f); - eq->bands[1].type.store(static_cast(S13EQ::FilterType::Bell)); + eq->bands[1].type.store(static_cast(OpenStudioEQ::FilterType::Bell)); eq->bands[1].freq.store(1000.0f); eq->bands[1].gain.store(6.0f); eq->bands[1].q.store(1.0f); @@ -59725,17 +61471,17 @@ juce::var AudioEngine::runAutomatedRegressionSuite() + ", 10kHz=" + juce::String(response.size() > 2 ? response[2] : -999.0f, 4)); eq->releaseResources(); - auto shelf = std::make_unique(); + auto shelf = std::make_unique(); for (auto& band : shelf->bands) band.enabled.store(0.0f); shelf->bands[0].enabled.store(1.0f); shelf->bands[0].type.store( - static_cast(S13EQ::FilterType::HighShelf)); + static_cast(OpenStudioEQ::FilterType::HighShelf)); shelf->bands[0].freq.store(4000.0f); shelf->bands[0].gain.store(8.0f); shelf->bands[0].q.store(0.70710678f); shelf->bands[0].slope.store( - static_cast(S13EQ::FilterSlope::dB12)); + static_cast(OpenStudioEQ::FilterSlope::dB12)); shelf->prepareToPlay(fixtureSampleRate, fixtureBlockSize); const std::vector shelfProbeFrequencies { 500.0f, 2000.0f, 14000.0f @@ -59743,7 +61489,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() const auto twelveDbShelf = shelf->getMagnitudeResponse(shelfProbeFrequencies); shelf->bands[0].slope.store( - static_cast(S13EQ::FilterSlope::dB48)); + static_cast(OpenStudioEQ::FilterSlope::dB48)); const auto fortyEightDbShelf = shelf->getMagnitudeResponse(shelfProbeFrequencies); const bool shelfSlopePass = @@ -59784,7 +61530,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() } }; - auto compressor = std::make_unique(); + auto compressor = std::make_unique(); compressor->threshold.store(-24.0f); compressor->ratio.store(6.0f); compressor->attack.store(1.0f); @@ -59802,7 +61548,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() const float compressorGR = compressor->getCurrentGainReduction(); compressor->releaseResources(); - auto gate = std::make_unique(); + auto gate = std::make_unique(); gate->threshold.store(-26.0f); gate->range.store(-60.0f); gate->attackMs.store(0.1f); @@ -59815,7 +61561,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() const float gateGR = gate->getGainReductionDB(); gate->releaseResources(); - auto limiter = std::make_unique(); + auto limiter = std::make_unique(); limiter->threshold.store(-6.0f); limiter->ceiling.store(-1.0f); limiter->lookaheadMs.store(5.0f); @@ -59852,7 +61598,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto limiter = std::make_unique(); + auto limiter = std::make_unique(); limiter->threshold.store(-3.0f); limiter->ceiling.store(-1.0f); limiter->lookaheadMs.store(5.0f); @@ -59880,7 +61626,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto delay = std::make_unique(); + auto delay = std::make_unique(); delay->tempoSync.store(1.0f); delay->syncNoteL.store(4.0f); // 1/16 at default 120 BPM = 125 ms target delay->syncNoteR.store(4.0f); @@ -59922,7 +61668,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto reverb = std::make_unique(); + auto reverb = std::make_unique(); reverb->dryLevel.store(0.0f); reverb->wetLevel.store(1.0f); reverb->earlyLevel.store(0.4f); @@ -59971,7 +61717,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto limiter = std::make_unique(); + auto limiter = std::make_unique(); limiter->threshold.store(0.0f); limiter->ceiling.store(0.0f); limiter->lookaheadMs.store(6.0f); @@ -60014,7 +61760,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto delay = std::make_unique(); + auto delay = std::make_unique(); delay->tempoSync.store(0.0f); delay->delayTimeL.store(85.0f); delay->delayTimeR.store(85.0f); @@ -60077,7 +61823,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto piano = std::make_unique(); + auto piano = std::make_unique(); piano->releaseMs.store(160.0f); piano->prepareToPlay(fixtureSampleRate, fixtureBlockSize); juce::MidiBuffer midi; @@ -60119,7 +61865,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() { const double fixtureSampleRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; const int fixtureBlockSize = juce::jmax(512, currentBlockSize); - auto pitch = std::make_unique(); + auto pitch = std::make_unique(); const bool setOk = setBuiltInProcessorParam(pitch.get(), "mix", 0.42f) && setBuiltInProcessorParam(pitch.get(), "key", 7.0f) && setBuiltInProcessorParam(pitch.get(), "scale", 1.0f) @@ -60224,7 +61970,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() auto renderSynthProbe = [&] (int pitchWheelValue, int modWheelValue, bool releaseNote) { - auto synth = std::make_unique(); + auto synth = std::make_unique(); synth->detuneCents.store(0.0f); synth->subLevel.store(0.0f); synth->noiseLevel.store(0.0f); @@ -60543,7 +62289,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() juce::String samplerDetail; const double samplerFixtureRate = currentSampleRate > 0.0 ? currentSampleRate : 44100.0; juce::File samplerFixtureFile = juce::File::getSpecialLocation(juce::File::tempDirectory) - .getChildFile("studio13_sampler_fixture_" + juce::Uuid().toString() + ".wav"); + .getChildFile("openstudio_sampler_fixture_" + juce::Uuid().toString() + ".wav"); { juce::AudioBuffer sampleBuffer(1, static_cast(samplerFixtureRate * 0.1)); for (int sample = 0; sample < sampleBuffer.getNumSamples(); ++sample) @@ -60848,19 +62594,19 @@ juce::var AudioEngine::runAutomatedRegressionSuite() const bool namRackAdded = addTrackBuiltInFX( renderFreezeTrackId, "OpenStudio NAM Rack", false); - S13NAMRack* renderFreezeRack = nullptr; + OpenStudioNAMRack* renderFreezeRack = nullptr; if (namRackAdded) { auto trackIt = trackMap.find(renderFreezeTrackId); if (trackIt != trackMap.end() && trackIt->second != nullptr) { - renderFreezeRack = dynamic_cast( + renderFreezeRack = dynamic_cast( trackIt->second->getTrackFXProcessor(0)); } } const juce::File tempDir = juce::File::getSpecialLocation(juce::File::tempDirectory); - const juce::File renderBaselineFile = tempDir.getChildFile("studio13_midi_render_no_audition_fixture_" + juce::Uuid().toString() + ".wav"); - const juce::File renderFile = tempDir.getChildFile("studio13_midi_render_fixture_" + juce::Uuid().toString() + ".wav"); + const juce::File renderBaselineFile = tempDir.getChildFile("openstudio_midi_render_no_audition_fixture_" + juce::Uuid().toString() + ".wav"); + const juce::File renderFile = tempDir.getChildFile("openstudio_midi_render_fixture_" + juce::Uuid().toString() + ".wav"); constexpr std::uint64_t auditionDisabledSampleSentinel = static_cast(1) << 39U; constexpr std::uint64_t auditionEnabledSampleSentinel = @@ -61081,7 +62827,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() bool midiExportPass = false; juce::String midiExportDetail; const juce::File midiExportFile = juce::File::getSpecialLocation(juce::File::tempDirectory) - .getChildFile("studio13_midi_export_fixture_" + juce::Uuid().toString() + ".mid"); + .getChildFile("openstudio_midi_export_fixture_" + juce::Uuid().toString() + ".mid"); auto midiExportTracks = juce::JSON::parse( R"json([{"name":"Export Fixture","clips":[{"startTime":0.5,"duration":1.0,"events":[{"type":"programChange","timestamp":0.0,"value":7,"channel":3},{"type":"cc","timestamp":0.04,"controller":0,"value":3,"channel":3},{"type":"cc","timestamp":0.05,"controller":32,"value":45,"channel":3},{"type":"noteOn","timestamp":0.1,"note":60,"velocity":101,"channel":3},{"type":"cc","timestamp":0.12,"controller":1,"value":96,"channel":3},{"type":"cc","timestamp":0.12,"controller":33,"value":12,"channel":3},{"type":"pitchBend","timestamp":0.15,"value":12288,"channel":3},{"type":"channelPressure","timestamp":0.18,"value":55,"channel":3},{"type":"polyPressure","timestamp":0.2,"note":60,"value":66,"channel":3},{"type":"noteOn","timestamp":0.25,"note":64,"velocity":0.8,"channel":3},{"type":"noteOff","timestamp":0.35,"note":64,"velocity":0,"channel":3},{"type":"noteOff","timestamp":0.45,"note":60,"releaseVelocity":40,"channel":3}]}]}])json"); const bool midiExportOk = exportProjectMIDI(midiExportFile.getFullPathName(), midiExportTracks, 120.0); @@ -61279,7 +63025,7 @@ juce::var AudioEngine::runAutomatedRegressionSuite() } const juce::File combinedRenderFile = juce::File::getSpecialLocation(juce::File::tempDirectory) - .getChildFile("studio13_combined_midi_render_fixture_" + juce::Uuid().toString() + ".wav"); + .getChildFile("openstudio_combined_midi_render_fixture_" + juce::Uuid().toString() + ".wav"); const bool renderOk = renderProject("master", 0.0, 1.35, combinedRenderFile.getFullPathName(), "wav", currentSampleRate, 24, 2, false, false, 0.0, false); @@ -61486,7 +63232,8 @@ 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, getSafeHostedPluginBlockSize(currentBlockSize), - [this, trackIdCopy, fxIndexCopy, openEditorCopy] (bool araSuccess, bool pluginSupportsARA, const juce::String& errorMessage) { + [this, lifetime = deferredCallbacks.token(), trackIdCopy, fxIndexCopy, openEditorCopy] (bool araSuccess, bool pluginSupportsARA, const juce::String& errorMessage) { + if (!MessageThreadLifetime::accepts(lifetime)) return; if (araSuccess) { logToDisk(" ARA initialized OK for FX " + juce::String(fxIndexCopy) @@ -61505,7 +63252,8 @@ bool AudioEngine::addTrackFX(const juce::String& trackId, const juce::String& pl + " error=" + errorMessage); if (openEditorCopy && !pluginSupportsARA) { - juce::MessageManager::callAsync([this, trackIdCopy, fxIndexCopy] { + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), trackIdCopy, fxIndexCopy] { + if (!MessageThreadLifetime::accepts(lifetime)) return; openPluginEditor(trackIdCopy, fxIndexCopy, false); }); } @@ -61531,20 +63279,20 @@ bool AudioEngine::addTrackFX(const juce::String& trackId, const juce::String& pl static std::unique_ptr createBuiltInEffect(const juce::String& name) { - if (name == "Studio13 Basic Synth" || name == "OpenStudio Basic Synth") return std::make_unique(); - if (name == "Studio13 Piano" || name == "OpenStudio Piano") return std::make_unique(); - if (name == "Studio13 Drums" || name == "OpenStudio Drums") return std::make_unique(); - if (name == "Studio13 Clean Guitar" || name == "OpenStudio Clean Guitar") return std::make_unique(); - if (name == "S13 EQ" || name == "OpenStudio EQ") return std::make_unique(); - if (name == "S13 Compressor" || name == "OpenStudio Compressor") return std::make_unique(); - if (name == "S13 Gate" || name == "OpenStudio Gate") return std::make_unique(); - if (name == "S13 Limiter" || name == "OpenStudio Limiter") return std::make_unique(); - if (name == "S13 NAM Rack" || name == "OpenStudio NAM Rack") return std::make_unique(); - if (name == "S13 Delay" || name == "OpenStudio Delay") return std::make_unique(); - if (name == "S13 Reverb" || name == "OpenStudio Reverb") return std::make_unique(); - if (name == "S13 Chorus" || name == "OpenStudio Chorus") return std::make_unique(); - if (name == "S13 Saturator" || name == "OpenStudio Saturator") return std::make_unique(); - if (name == "S13 Pitch Correct" || name == "OpenStudio Pitch Correct") return std::make_unique(); + if (name == "OpenStudio Basic Synth") return std::make_unique(); + if (name == "OpenStudio Piano") return std::make_unique(); + if (name == "OpenStudio Drums") return std::make_unique(); + if (name == "OpenStudio Clean Guitar") return std::make_unique(); + if (name == "OpenStudio EQ") return std::make_unique(); + if (name == "OpenStudio Compressor") return std::make_unique(); + if (name == "OpenStudio Gate") return std::make_unique(); + if (name == "OpenStudio Limiter") return std::make_unique(); + if (name == "OpenStudio NAM Rack") return std::make_unique(); + if (name == "OpenStudio Delay") return std::make_unique(); + if (name == "OpenStudio Reverb") return std::make_unique(); + if (name == "OpenStudio Chorus") return std::make_unique(); + if (name == "OpenStudio Saturator") return std::make_unique(); + if (name == "OpenStudio Pitch Correct") return std::make_unique(); return nullptr; } @@ -61621,9 +63369,12 @@ static void addToggleParam(juce::Array& params, const juce::String& label, float value, float defaultValue, - const juce::String& graphRole = {}) + const juce::String& graphRole = {}, + bool automatable = true) { - params.add(makeBuiltInParam(id, label, "toggle", value, 0.0f, 1.0f, defaultValue, {}, graphRole)); + params.add(makeBuiltInParam( + id, label, "toggle", value, 0.0f, 1.0f, defaultValue, + {}, graphRole, {}, automatable)); } static void addContinuousParam(juce::Array& params, @@ -61715,7 +63466,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, if (processor == nullptr) return makeBuiltInSchemaObject({}, {}, chainType, fxIndex, params); - if (auto* synth = dynamic_cast(processor)) + if (auto* synth = dynamic_cast(processor)) { addContinuousParam(params, "attackMs", "Attack", synth->attackMs.load(), 0.5f, 2000.0f, 8.0f, "ms", "envelope"); addContinuousParam(params, "releaseMs", "Release", synth->releaseMs.load(), 5.0f, 5000.0f, 180.0f, "ms", "envelope"); @@ -61727,7 +63478,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(synth->getName(), "Instrument", chainType, fxIndex, params); } - if (auto* piano = dynamic_cast(processor)) + if (auto* piano = dynamic_cast(processor)) { addEnumParam(params, "model", "Model", piano->model.load(), 0.0f, makeEnumOptions({ "Studio Grand", "Bright Upright", "Soft Felt" }), "character"); addContinuousParam(params, "tone", "Tone", piano->tone.load(), 0.0f, 1.0f, 0.58f, {}, "tone"); @@ -61740,7 +63491,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(piano->getName(), "Instrument", chainType, fxIndex, params); } - if (auto* guitar = dynamic_cast(processor)) + if (auto* guitar = dynamic_cast(processor)) { addEnumParam(params, "model", "Preset", guitar->model.load(), 0.0f, makeEnumOptions({ "T-Style DI", "S-Style DI", "Clean US Combo", "JC-Style Chorus Clean" }), "character"); @@ -61756,7 +63507,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(guitar->getName(), "Instrument", chainType, fxIndex, params); } - if (auto* drums = dynamic_cast(processor)) + if (auto* drums = dynamic_cast(processor)) { addEnumParam(params, "kit", "Kit", drums->kit.load(), 0.0f, makeEnumOptions({ "Studio", "Rock", "Electronic" }), "drums"); addEnumParam(params, "mapPreset", "MIDI Map", drums->mapPreset.load(), 0.0f, makeEnumOptions({ "GM", "Roland TD" }), "drums"); @@ -61770,15 +63521,15 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(drums->getName(), "Instrument", chainType, fxIndex, params); } - if (auto* eq = dynamic_cast(processor)) + if (auto* eq = dynamic_cast(processor)) { const auto types = makeEnumOptions({ "Bell", "Low Shelf", "High Shelf", "Low Cut", "High Cut", "Notch", "Band Pass" }); const auto slopes = makeEnumOptions({ "6 dB/oct", "12 dB/oct", "24 dB/oct", "48 dB/oct" }); - for (int band = 0; band < S13EQ::numBands; ++band) + for (int band = 0; band < OpenStudioEQ::numBands; ++band) { const auto prefix = "band" + juce::String(band) + "."; const auto bandLabel = "Band " + juce::String(band + 1) + " "; - addToggleParam(params, prefix + "enabled", bandLabel + "On", eq->bands[band].enabled.load(), band == 0 || band == S13EQ::numBands - 1 ? 0.0f : 1.0f, "eqBand"); + addToggleParam(params, prefix + "enabled", bandLabel + "On", eq->bands[band].enabled.load(), band == 0 || band == OpenStudioEQ::numBands - 1 ? 0.0f : 1.0f, "eqBand"); addEnumParam(params, prefix + "type", bandLabel + "Type", eq->bands[band].type.load(), 0.0f, types, "eqBand"); addContinuousParam(params, prefix + "freq", bandLabel + "Freq", eq->bands[band].freq.load(), 20.0f, 20000.0f, 1000.0f, "Hz", "eqBand"); addContinuousParam(params, prefix + "gain", bandLabel + "Gain", eq->bands[band].gain.load(), -30.0f, 30.0f, 0.0f, "dB", "eqBand"); @@ -61810,8 +63561,8 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, auto response = eq->getMagnitudeResponse(frequencies); auto spectrum = eq->getSpectrumData(); std::vector dynamicGains; - dynamicGains.reserve(S13EQ::numBands); - for (int band = 0; band < S13EQ::numBands; ++band) + dynamicGains.reserve(OpenStudioEQ::numBands); + for (int band = 0; band < OpenStudioEQ::numBands; ++band) dynamicGains.push_back(eq->getBandDynamicGainDB(band)); const double sr = eq->getSampleRate() > 0.0 ? eq->getSampleRate() : 44100.0; std::vector preSpectrum; @@ -61820,8 +63571,8 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, postSpectrum.reserve(frequencies.size()); for (float freq : frequencies) { - const int bin = juce::jlimit(0, S13EQ::fftSize / 2 - 1, - static_cast(std::round((static_cast(freq) / (sr * 0.5)) * static_cast(S13EQ::fftSize / 2 - 1)))); + const int bin = juce::jlimit(0, OpenStudioEQ::fftSize / 2 - 1, + static_cast(std::round((static_cast(freq) / (sr * 0.5)) * static_cast(OpenStudioEQ::fftSize / 2 - 1)))); preSpectrum.push_back(spectrum.ready ? spectrum.preEQ[static_cast(bin)] : -100.0f); postSpectrum.push_back(spectrum.ready ? spectrum.postEQ[static_cast(bin)] : -100.0f); } @@ -61838,7 +63589,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return schema; } - if (auto* compressor = dynamic_cast(processor)) + if (auto* compressor = dynamic_cast(processor)) { addContinuousParam(params, "threshold", "Threshold", compressor->threshold.load(), -60.0f, 0.0f, 0.0f, "dB", "dynamics"); addContinuousParam(params, "ratio", "Ratio", compressor->ratio.load(), 1.0f, 20.0f, 1.0f, ":1", "dynamics"); @@ -61866,7 +63617,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return schema; } - if (auto* gate = dynamic_cast(processor)) + if (auto* gate = dynamic_cast(processor)) { addContinuousParam(params, "threshold", "Threshold", gate->threshold.load(), -80.0f, 0.0f, -40.0f, "dB", "dynamics"); addContinuousParam(params, "attackMs", "Attack", gate->attackMs.load(), 0.01f, 50.0f, 1.0f, "ms", "dynamics"); @@ -61889,7 +63640,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return schema; } - if (auto* limiter = dynamic_cast(processor)) + if (auto* limiter = dynamic_cast(processor)) { addContinuousParam(params, "threshold", "Threshold", limiter->threshold.load(), -20.0f, 0.0f, -1.0f, "dB", "dynamics"); addContinuousParam(params, "releaseMs", "Release", limiter->releaseMs.load(), 10.0f, 500.0f, 100.0f, "ms", "dynamics"); @@ -61905,7 +63656,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return schema; } - if (auto* delay = dynamic_cast(processor)) + if (auto* delay = dynamic_cast(processor)) { addContinuousParam(params, "delayTimeL", "Delay L", delay->delayTimeL.load(), 1.0f, 2000.0f, 250.0f, "ms", "time"); addContinuousParam(params, "delayTimeR", "Delay R", delay->delayTimeR.load(), 1.0f, 2000.0f, 250.0f, "ms", "time"); @@ -61925,7 +63676,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(delay->getName(), "Delay", chainType, fxIndex, params); } - if (auto* reverb = dynamic_cast(processor)) + if (auto* reverb = dynamic_cast(processor)) { addEnumParam(params, "algorithm", "Algorithm", reverb->algorithm.load(), 0.0f, makeEnumOptions({ "Room", "Hall", "Plate", "Chamber" }), "space"); addContinuousParam(params, "roomSize", "Size", reverb->roomSize.load(), 0.0f, 1.0f, 0.5f, {}, "space"); @@ -61943,7 +63694,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(reverb->getName(), "Reverb", chainType, fxIndex, params); } - if (auto* chorus = dynamic_cast(processor)) + if (auto* chorus = dynamic_cast(processor)) { addEnumParam(params, "mode", "Mode", chorus->mode.load(), 0.0f, makeEnumOptions({ "Chorus", "Flanger", "Phaser" }), "modulation"); addContinuousParam(params, "rate", "Rate", chorus->rate.load(), 0.01f, 20.0f, 1.0f, "Hz", "modulation"); @@ -61960,7 +63711,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(chorus->getName(), "Modulation", chainType, fxIndex, params); } - if (auto* saturator = dynamic_cast(processor)) + if (auto* saturator = dynamic_cast(processor)) { addEnumParam(params, "satType", "Type", saturator->satType.load(), 0.0f, makeEnumOptions({ "Tape", "Tube", "Transistor", "Clip", "Crush", "Console", "Transformer", "Foldback" }), "character"); addContinuousParam(params, "drive", "Drive", saturator->drive.load(), 0.0f, 30.0f, 6.0f, "dB", "drive"); @@ -61982,7 +63733,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return makeBuiltInSchemaObject(saturator->getName(), "Saturation", chainType, fxIndex, params); } - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { addEnumParam( params, @@ -61990,7 +63741,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, "Instrument", rack->instrumentProfile.load(), static_cast( - S13NAMRack::guitarInstrumentProfile), + OpenStudioNAMRack::guitarInstrumentProfile), makeEnumOptions({ "Guitar", "Bass" }), "instrument", false); @@ -62025,7 +63776,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, addContinuousParam(params, "octaverDirectMix", "Direct", rack->octaverDirectMix.load(), 0.0f, 1.25f, 1.0f, {}, "pitch"); const bool bassPreEqProfile = rack->getInstrumentProfile() - == S13NAMRack::bassInstrumentProfile; + == OpenStudioNAMRack::bassInstrumentProfile; addToggleParam(params, "preEqEnabled", "PRE EQ", rack->preEqEnabled.load(), 0.0f, "preEq"); addContinuousParam(params, "preEq120Db", bassPreEqProfile ? "50 Hz" : "120 Hz", rack->preEq120Db.load(), -12.0f, 12.0f, 0.0f, "dB", "preEq"); addContinuousParam(params, "preEq250Db", bassPreEqProfile ? "120 Hz" : "250 Hz", rack->preEq250Db.load(), -12.0f, 12.0f, 0.0f, "dB", "preEq"); @@ -62093,13 +63844,14 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, addToggleParam(params, "eqEnabled", "EQ Power", rack->eqEnabled.load(), 0.0f, "graphicEq"); addToggleParam(params, "cabEnabled", "Cab/IR", rack->cabEnabled.load(), 0.0f, "cab"); addContinuousParam(params, "cabLevelDb", "Cab Level", rack->cabLevelDb.load(), -24.0f, 12.0f, 0.0f, "dB", "cab"); - addContinuousParam(params, "cabHPFHz", "Cab HPF", rack->cabHPFHz.load(), 20.0f, 500.0f, 80.0f, "Hz", "cab"); - addContinuousParam(params, "cabLPFHz", "Cab LPF", rack->cabLPFHz.load(), 1000.0f, 20000.0f, 8500.0f, "Hz", "cab"); + addContinuousParam(params, "cabEngineVersion", "Cab Engine Version", static_cast(OpenStudioNAMRack::currentCabEngineVersion), 1.0f, 3.0f, 3.0f, {}, "cabInternal", false); + addToggleParam(params, "cabHPFEnabled", "Retired Cab HPF Power", 0.0f, 0.0f, "cabInternal", false); + addToggleParam(params, "cabLPFEnabled", "Retired Cab LPF Power", 0.0f, 0.0f, "cabInternal", false); + addContinuousParam(params, "cabHPFHz", "Retired Cab HPF", 30.0f, 20.0f, 500.0f, 30.0f, "Hz", "cabInternal", false); + addContinuousParam(params, "cabLPFHz", "Retired Cab LPF", 16000.0f, 1000.0f, 20000.0f, 16000.0f, "Hz", "cabInternal", false); + addToggleParam(params, "cabIRStereo", "Stereo IR", rack->cabIRStereo.load(), 0.0f, "cab"); + addContinuousParam(params, "cabDirectMix", "Direct Mix", rack->cabDirectMix.load(), 0.0f, 1.0f, 0.0f, {}, "cab"); addToggleParam(params, "cabPhaseInvert", "Phase", rack->cabPhaseInvert.load(), 0.0f, "cab"); - addContinuousParam(params, "cabMicPosition", "Tone Edge", rack->cabMicPosition.load(), 0.0f, 1.0f, 0.5f, {}, "cab"); - addContinuousParam(params, "cabMicDistance", "Tone Damp", rack->cabMicDistance.load(), 0.0f, 1.0f, 0.0f, {}, "cab"); - addContinuousParam(params, "cabMicBlend", "Shaper Blend", rack->cabMicBlend.load(), 0.0f, 1.0f, 0.5f, {}, "cab"); - addContinuousParam(params, "cabRoomSend", "Low Bloom", rack->cabRoomSend.load(), 0.0f, 1.0f, 0.0f, {}, "cab"); addToggleParam(params, "cabRoomEnabled", "Room", rack->cabRoomEnabled.load(), 0.0f, "cabinetSpace"); addContinuousParam(params, "cabRoomAmount", "Room Amount", rack->cabRoomAmount.load(), 0.0f, 1.0f, 0.22f, {}, "cabinetSpace"); addContinuousParam(params, "cabRoomWidth", "Room Width", rack->cabRoomWidth.load(), 0.0f, 1.0f, 0.65f, {}, "cabinetSpace"); @@ -62212,7 +63964,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, modelInfo->setProperty("cabRequestedEnabled", rack->isCabRequestedEnabled()); modelInfo->setProperty( "namEffectsDspVersion", - S13NAMRack::currentNAMEffectsDspVersion); + OpenStudioNAMRack::currentNAMEffectsDspVersion); modelInfo->setProperty( "inputRoutingMode", rackDiagnostics.getProperty( @@ -62326,7 +64078,7 @@ static juce::var describeBuiltInProcessor(juce::AudioProcessor* processor, return schema; } - if (auto* pitch = dynamic_cast(processor)) + if (auto* pitch = dynamic_cast(processor)) { auto& mapper = pitch->getMapper(); addToggleParam(params, "bypass", "Bypass", pitch->bypass.load(), 0.0f, "mix"); @@ -62391,16 +64143,16 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce // profile latched. Other parameters retain the shared non-finite reject. if (paramId == "instrumentProfile") { - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { const float canonical = std::isfinite(value) && value >= 0.0f && value <= 1.0f && value >= 0.5f ? static_cast( - S13NAMRack::bassInstrumentProfile) + OpenStudioNAMRack::bassInstrumentProfile) : static_cast( - S13NAMRack::guitarInstrumentProfile); + OpenStudioNAMRack::guitarInstrumentProfile); rack->instrumentProfile.store( canonical, std::memory_order_relaxed); return true; @@ -62409,16 +64161,16 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce if (paramId == "reverbVoice") { - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { rack->useCurrentNAMEffectsDspVersion(); rack->reverbEngineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::currentReverbEngineVersion), std::memory_order_relaxed); rack->reverbVoice.store( static_cast( - S13NAMRack::sanitizeReverbVoice(value)), + OpenStudioNAMRack::sanitizeReverbVoice(value)), std::memory_order_relaxed); return true; } @@ -62427,7 +64179,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce if (! std::isfinite(value)) return false; - if (auto* synth = dynamic_cast(processor)) + if (auto* synth = dynamic_cast(processor)) { if (paramId == "attackMs") return storeClamped(synth->attackMs, value, 0.5f, 2000.0f); if (paramId == "releaseMs") return storeClamped(synth->releaseMs, value, 5.0f, 5000.0f); @@ -62439,7 +64191,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* piano = dynamic_cast(processor)) + if (auto* piano = dynamic_cast(processor)) { if (paramId == "model") return storeClamped(piano->model, value, 0.0f, 2.0f); if (paramId == "tone") return storeClamped(piano->tone, value, 0.0f, 1.0f); @@ -62452,7 +64204,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* guitar = dynamic_cast(processor)) + if (auto* guitar = dynamic_cast(processor)) { if (paramId == "model") return storeClamped(guitar->model, value, 0.0f, 3.0f); if (paramId == "tone") return storeClamped(guitar->tone, value, 0.0f, 1.0f); @@ -62466,7 +64218,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* drums = dynamic_cast(processor)) + if (auto* drums = dynamic_cast(processor)) { if (paramId == "kit") return storeClamped(drums->kit, value, 0.0f, 2.0f); if (paramId == "mapPreset") return storeClamped(drums->mapPreset, value, 0.0f, 1.0f); @@ -62480,7 +64232,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* eq = dynamic_cast(processor)) + if (auto* eq = dynamic_cast(processor)) { if (paramId == "outputGain") return storeClamped(eq->outputGain, value, -12.0f, 12.0f); if (paramId == "autoGain") return storeClamped(eq->autoGain, value, 0.0f, 1.0f); @@ -62490,7 +64242,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce { const int dot = paramId.indexOfChar('.'); const int band = paramId.substring(4, dot).getIntValue(); - if (band < 0 || band >= S13EQ::numBands || dot < 0) + if (band < 0 || band >= OpenStudioEQ::numBands || dot < 0) return false; const auto field = paramId.substring(dot + 1); auto& params = eq->bands[band]; @@ -62509,7 +64261,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* compressor = dynamic_cast(processor)) + if (auto* compressor = dynamic_cast(processor)) { if (paramId == "threshold") return storeClamped(compressor->threshold, value, -60.0f, 0.0f); if (paramId == "ratio") return storeClamped(compressor->ratio, value, 1.0f, 20.0f); @@ -62528,7 +64280,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* gate = dynamic_cast(processor)) + if (auto* gate = dynamic_cast(processor)) { if (paramId == "threshold") return storeClamped(gate->threshold, value, -80.0f, 0.0f); if (paramId == "attackMs") return storeClamped(gate->attackMs, value, 0.01f, 50.0f); @@ -62543,7 +64295,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* limiter = dynamic_cast(processor)) + if (auto* limiter = dynamic_cast(processor)) { if (paramId == "threshold") return storeClamped(limiter->threshold, value, -20.0f, 0.0f); if (paramId == "releaseMs") return storeClamped(limiter->releaseMs, value, 10.0f, 500.0f); @@ -62552,7 +64304,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* delay = dynamic_cast(processor)) + if (auto* delay = dynamic_cast(processor)) { if (paramId == "delayTimeL") return storeClamped(delay->delayTimeL, value, 1.0f, 2000.0f); if (paramId == "delayTimeR") return storeClamped(delay->delayTimeR, value, 1.0f, 2000.0f); @@ -62572,7 +64324,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* reverb = dynamic_cast(processor)) + if (auto* reverb = dynamic_cast(processor)) { if (paramId == "algorithm") return storeClamped(reverb->algorithm, value, 0.0f, 3.0f); if (paramId == "roomSize") return storeClamped(reverb->roomSize, value, 0.0f, 1.0f); @@ -62590,7 +64342,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* chorus = dynamic_cast(processor)) + if (auto* chorus = dynamic_cast(processor)) { if (paramId == "mode") return storeClamped(chorus->mode, value, 0.0f, 2.0f); if (paramId == "rate") return storeClamped(chorus->rate, value, 0.01f, 20.0f); @@ -62607,7 +64359,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* saturator = dynamic_cast(processor)) + if (auto* saturator = dynamic_cast(processor)) { if (paramId == "satType") return storeClamped(saturator->satType, value, 0.0f, 7.0f); if (paramId == "drive") return storeClamped(saturator->drive, value, 0.0f, 30.0f); @@ -62626,7 +64378,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { if (paramId == "inputMode") { @@ -62636,7 +64388,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce } rack->useCurrentNAMEffectsDspVersion(); rack->reverbEngineVersion.store( - static_cast(S13NAMRack::currentReverbEngineVersion), + static_cast(OpenStudioNAMRack::currentReverbEngineVersion), std::memory_order_relaxed); if (paramId == "inputTrimDb") return storeClamped(rack->inputTrimDb, value, -24.0f, 24.0f); if (paramId == "calibrationReferenceDbu") return storeClamped(rack->calibrationReferenceDbu, value, -20.0f, 30.0f); @@ -62770,13 +64522,18 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return true; } if (paramId == "cabLevelDb") return storeClamped(rack->cabLevelDb, value, -24.0f, 12.0f); - if (paramId == "cabHPFHz") return storeClamped(rack->cabHPFHz, value, 20.0f, 500.0f); - if (paramId == "cabLPFHz") return storeClamped(rack->cabLPFHz, value, 1000.0f, 20000.0f); + if (paramId == "cabHPFEnabled" + || paramId == "cabLPFEnabled" + || paramId == "cabHPFHz" + || paramId == "cabLPFHz") + { + // These IDs are restore-only compatibility fields. Accepting live + // writes would create state that has no audible or visible owner. + return false; + } + if (paramId == "cabIRStereo") return storeClamped(rack->cabIRStereo, value, 0.0f, 1.0f); + if (paramId == "cabDirectMix") return storeClamped(rack->cabDirectMix, value, 0.0f, 1.0f); if (paramId == "cabPhaseInvert") return storeClamped(rack->cabPhaseInvert, value, 0.0f, 1.0f); - if (paramId == "cabMicPosition") return storeClamped(rack->cabMicPosition, value, 0.0f, 1.0f); - if (paramId == "cabMicDistance") return storeClamped(rack->cabMicDistance, value, 0.0f, 1.0f); - if (paramId == "cabMicBlend") return storeClamped(rack->cabMicBlend, value, 0.0f, 1.0f); - if (paramId == "cabRoomSend") return storeClamped(rack->cabRoomSend, value, 0.0f, 1.0f); if (paramId == "cabRoomEnabled") return storeClamped(rack->cabRoomEnabled, value, 0.0f, 1.0f); if (paramId == "cabRoomAmount") return storeClamped(rack->cabRoomAmount, value, 0.0f, 1.0f); if (paramId == "cabRoomWidth") return storeClamped(rack->cabRoomWidth, value, 0.0f, 1.0f); @@ -62807,7 +64564,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; rack->delayMode.store( static_cast( - S13NAMRack::sanitizeDelayMode(value)), + OpenStudioNAMRack::sanitizeDelayMode(value)), std::memory_order_relaxed); return true; } @@ -62851,7 +64608,7 @@ static bool setBuiltInProcessorParam(juce::AudioProcessor* processor, const juce return false; } - if (auto* pitch = dynamic_cast(processor)) + if (auto* pitch = dynamic_cast(processor)) { auto& mapper = pitch->getMapper(); if (paramId == "bypass") return storeClamped(pitch->bypass, value, 0.0f, 1.0f); @@ -62879,22 +64636,22 @@ static bool supportsLockFreeBuiltInControlPublication( juce::AudioProcessor* processor) noexcept { // These processors publish editor-facing controls and telemetry through - // atomics (S13EQ additionally publishes its analyzer snapshots). Their + // atomics (OpenStudioEQ additionally publishes its analyzer snapshots). Their // schemas and ordinary scalar writes can therefore use the immutable // processor-owner snapshot without making the audio callback skip a block. - return dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr - || dynamic_cast(processor) != nullptr; + return dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr + || dynamic_cast(processor) != nullptr; } static bool requiresLockedBuiltInControlMutation( @@ -62905,7 +64662,7 @@ static bool requiresLockedBuiltInControlMutation( // state, and updates reported latency. Keep it on the explicit locked/PDC // path; every other exposed control in the processors above is a scalar // publication consumed and smoothed by processBlock(). - return dynamic_cast(processor) != nullptr + return dynamic_cast(processor) != nullptr && parameterId == "oversampleMode"; } @@ -62918,7 +64675,7 @@ static OpenStudioBuiltInParameterCurve getOpenStudioBuiltInParameterCurve( juce::AudioProcessor* processor, const juce::String& parameterId) noexcept { - if (dynamic_cast(processor) == nullptr) + if (dynamic_cast(processor) == nullptr) return OpenStudioBuiltInParameterCurve::linear; if (parameterId == "chorusRateHz") @@ -63009,7 +64766,7 @@ bool AudioEngine::addTrackBuiltInFX(const juce::String& trackId, const juce::Str return false; } - if (auto* rack = dynamic_cast(plugin.get())) + if (auto* rack = dynamic_cast(plugin.get())) { rack->setEmbeddedDriveOversamplingFactor( getNAMRackOversamplingFactor()); @@ -63056,7 +64813,7 @@ bool AudioEngine::addMasterBuiltInFX(const juce::String& effectName) if (!plugin) return false; - if (auto* rack = dynamic_cast(plugin.get())) + if (auto* rack = dynamic_cast(plugin.get())) { rack->setEmbeddedDriveOversamplingFactor( getNAMRackOversamplingFactor()); @@ -63072,6 +64829,7 @@ bool AudioEngine::addMasterBuiltInFX(const juce::String& effectName) const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); specCopy = desiredMasterStageSpec; } + syncStageSpecStateFromActive(specCopy, std::atomic_load(&realtimeMasterFXSnapshot)); DesiredFXStageSlot slot; slot.slotId = nextMasterStageSlotId++; @@ -63129,20 +64887,20 @@ juce::var AudioEngine::getAvailableBuiltInFX() } //============================================================================== -// S13FX (JSFX) Management +// JSFX (JSFX) Management -bool AudioEngine::addTrackS13FX(const juce::String& trackId, const juce::String& scriptPath, bool isInputFX) +bool AudioEngine::addTrackJSFX(const juce::String& trackId, const juce::String& scriptPath, bool isInputFX) { - // Create S13FXProcessor and load script BEFORE acquiring the lock - auto s13fx = std::make_unique(); - if (!s13fx->loadScript(scriptPath)) + // Create JSFXProcessor and load script BEFORE acquiring the lock + auto jsfx = std::make_unique(); + if (!jsfx->loadScript(scriptPath)) { - juce::Logger::writeToLog("AudioEngine: Failed to load S13FX script: " + scriptPath); + juce::Logger::writeToLog("AudioEngine: Failed to load JSFX script: " + scriptPath); return false; } // Provide tempo/position info - s13fx->setPlayHead(this); + jsfx->setPlayHead(this); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; int bs = getSafeHostedPluginBlockSize(currentBlockSize); @@ -63154,36 +64912,36 @@ bool AudioEngine::addTrackS13FX(const juce::String& trackId, const juce::String& if (it != trackMap.end() && it->second) { if (isInputFX) - success = it->second->addInputFX(std::move(s13fx), sr, bs); + success = it->second->addInputFX(std::move(jsfx), sr, bs); else - success = it->second->addTrackFX(std::move(s13fx), sr, bs); + success = it->second->addTrackFX(std::move(jsfx), sr, bs); } } if (success) { - juce::Logger::writeToLog("AudioEngine: Added S13FX to " + juce::String(isInputFX ? "input" : "track") + " chain: " + scriptPath); + juce::Logger::writeToLog("AudioEngine: Added JSFX to " + juce::String(isInputFX ? "input" : "track") + " chain: " + scriptPath); recalculatePDC(); } return success; } -bool AudioEngine::addMasterS13FX(const juce::String& scriptPath) +bool AudioEngine::addMasterJSFX(const juce::String& scriptPath) { - auto s13fx = std::make_unique(); - if (!s13fx->loadScript(scriptPath)) + auto jsfx = std::make_unique(); + if (!jsfx->loadScript(scriptPath)) { - juce::Logger::writeToLog("AudioEngine: Failed to load S13FX for master: " + scriptPath); + juce::Logger::writeToLog("AudioEngine: Failed to load JSFX for master: " + scriptPath); return false; } - s13fx->setPlayHead(this); + jsfx->setPlayHead(this); double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; int bs = getSafeHostedPluginBlockSize(currentBlockSize); - prepareHostedProcessorForPrecision(s13fx.get(), sr, bs, processingPrecisionMode); + prepareHostedProcessorForPrecision(jsfx.get(), sr, bs, processingPrecisionMode); DesiredFXStageSpec specCopy; { @@ -63193,21 +64951,21 @@ bool AudioEngine::addMasterS13FX(const juce::String& scriptPath) DesiredFXStageSlot slot; slot.slotId = nextMasterStageSlotId++; - slot.name = s13fx->getName(); - slot.type = "s13fx"; + slot.name = jsfx->getName(); + slot.type = "jsfx"; slot.pluginPath = scriptPath; - slot.pluginFormat = "S13FX"; - slot.serializedState = serialiseProcessorStateToBase64(s13fx.get()); + slot.pluginFormat = "JSFX"; + slot.serializedState = serialiseProcessorStateToBase64(jsfx.get()); specCopy.slots.push_back(std::move(slot)); if (!publishMasterStageSpec(specCopy)) return false; - juce::Logger::writeToLog("AudioEngine: Added S13FX to master chain: " + scriptPath); + juce::Logger::writeToLog("AudioEngine: Added JSFX to master chain: " + scriptPath); return true; } -juce::var AudioEngine::getS13FXSliders(const juce::String& trackId, int fxIndex, bool isInputFX) +juce::var AudioEngine::getJSFXSliders(const juce::String& trackId, int fxIndex, bool isInputFX) { juce::Array sliderList; @@ -63223,11 +64981,11 @@ juce::var AudioEngine::getS13FXSliders(const juce::String& trackId, int fxIndex, if (!proc) return sliderList; - auto* s13fx = dynamic_cast(proc); - if (!s13fx) + auto* jsfx = dynamic_cast(proc); + if (!jsfx) return sliderList; - auto sliders = s13fx->getSliders(); + auto sliders = jsfx->getSliders(); for (const auto& s : sliders) { auto* obj = new juce::DynamicObject(); @@ -63254,7 +65012,7 @@ juce::var AudioEngine::getS13FXSliders(const juce::String& trackId, int fxIndex, return sliderList; } -bool AudioEngine::setS13FXSlider(const juce::String& trackId, int fxIndex, bool isInputFX, int sliderIndex, double value) +bool AudioEngine::setJSFXSlider(const juce::String& trackId, int fxIndex, bool isInputFX, int sliderIndex, double value) { auto it = trackMap.find(trackId); if (it == trackMap.end() || !it->second) @@ -63268,14 +65026,14 @@ bool AudioEngine::setS13FXSlider(const juce::String& trackId, int fxIndex, bool if (!proc) return false; - auto* s13fx = dynamic_cast(proc); - if (!s13fx) + auto* jsfx = dynamic_cast(proc); + if (!jsfx) return false; - return s13fx->setSliderValue(static_cast(sliderIndex), value); + return jsfx->setSliderValue(static_cast(sliderIndex), value); } -bool AudioEngine::reloadS13FX(const juce::String& trackId, int fxIndex, bool isInputFX) +bool AudioEngine::reloadJSFX(const juce::String& trackId, int fxIndex, bool isInputFX) { auto it = trackMap.find(trackId); if (it == trackMap.end() || !it->second) @@ -63289,27 +65047,27 @@ bool AudioEngine::reloadS13FX(const juce::String& trackId, int fxIndex, bool isI if (!proc) return false; - auto* s13fx = dynamic_cast(proc); - if (!s13fx) + auto* jsfx = dynamic_cast(proc); + if (!jsfx) return false; const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); - return s13fx->reloadScript(); + return jsfx->reloadScript(); } -juce::var AudioEngine::getAvailableS13FX() +juce::var AudioEngine::getAvailableJSFX() { - pluginManager.scanForS13FX(); + pluginManager.scanForJSFX(); juce::Array list; - for (const auto& info : pluginManager.getAvailableS13FX()) + for (const auto& info : pluginManager.getAvailableJSFX()) { auto* obj = new juce::DynamicObject(); obj->setProperty("name", info.name); obj->setProperty("filePath", info.filePath); obj->setProperty("author", info.author); obj->setProperty("isStock", info.isStock); - obj->setProperty("type", "s13fx"); + obj->setProperty("type", "jsfx"); juce::Array tags; for (const auto& tag : info.tags) @@ -63323,7 +65081,7 @@ juce::var AudioEngine::getAvailableS13FX() } //============================================================================== -// Lua Scripting (S13Script) +// Lua Scripting (OpenStudioScript) juce::var AudioEngine::runScript(const juce::String& scriptPath) { @@ -63456,8 +65214,15 @@ void AudioEngine::openPluginEditor(const juce::String& trackId, int fxIndex, boo target.fxIndex = fxIndex; auto proc = processorOwner; auto title = windowTitle; - juce::MessageManager::callAsync([this, proc, title, target]() - { + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), proc, title, target]() + { + if (!MessageThreadLifetime::accepts(lifetime)) return; + const auto track = trackMap.find(target.trackId); + if (track == trackMap.end()) return; + const auto current = target.scope == PluginWindowManager::PluginEditorTarget::Scope::TrackInputFX + ? track->second->getInputFXProcessorShared(target.fxIndex) + : track->second->getTrackFXProcessorShared(target.fxIndex); + if (current != proc) return; pluginWindowManager.openEditor(proc, title, target); }); } @@ -63489,8 +65254,11 @@ void AudioEngine::openInstrumentEditor(const juce::String& trackId) PluginWindowManager::PluginEditorTarget target; target.scope = PluginWindowManager::PluginEditorTarget::Scope::Instrument; target.trackId = trackId; - juce::MessageManager::callAsync([this, proc, title, target]() + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), proc, title, target]() { + if (!MessageThreadLifetime::accepts(lifetime)) return; + const auto track = trackMap.find(target.trackId); + if (track == trackMap.end() || track->second->getInstrument() != proc.get()) return; pluginWindowManager.openEditor(proc, title, target); }); } @@ -63545,7 +65313,7 @@ static juce::File getPersistentBuiltInPresetsDir(const juce::String& pluginName) return builtInPresetPersistentRootOverride .getChildFile(getBuiltInPresetDirectoryName(pluginName)); } - return getOpenStudioApplicationDataDirectory() + return AppPaths::applicationData() .getChildFile("Presets") .getChildFile(getBuiltInPresetDirectoryName(pluginName)); } @@ -63578,7 +65346,6 @@ static juce::Array getPresetFilesInDirectoryStableFirst( const juce::File& directory) { juce::Array currentFiles; - juce::Array legacyFiles; if (! directory.isDirectory()) return currentFiles; @@ -63588,12 +65355,8 @@ static juce::Array getPresetFilesInDirectoryStableFirst( const auto extension = file.getFileExtension(); if (extension.equalsIgnoreCase(".ospreset")) currentFiles.add(file); - else if (extension.equalsIgnoreCase(".s13preset")) - legacyFiles.add(file); } currentFiles.sort(); - legacyFiles.sort(); - currentFiles.addArray(legacyFiles); return currentFiles; } @@ -63720,7 +65483,7 @@ static bool migrateNAMRackPresetFileToCurrent( } bool stateChanged = false; - if (! S13NAMRack::migratePresetStateToCurrent( + if (! OpenStudioNAMRack::migratePresetStateToCurrent( migration.payload, stateChanged)) { return false; @@ -63739,7 +65502,7 @@ static bool migrateNAMRackPresetFileToCurrent( if (! migration.targetFile.loadFileAsData(migration.payload)) return false; bool targetChanged = false; - if (! S13NAMRack::migratePresetStateToCurrent( + if (! OpenStudioNAMRack::migratePresetStateToCurrent( migration.payload, targetChanged)) { return false; @@ -63855,24 +65618,24 @@ juce::var AudioEngine::getBuiltInFXPresets(const juce::String& pluginName) bool migrated = false; if (file.loadFileAsData(presetState) && presetState.getSize() > 0 - && S13NAMRack::migratePresetStateToCurrent( + && OpenStudioNAMRack::migratePresetStateToCurrent( presetState, migrated)) { juce::ignoreUnused(migrated); const auto ampModelPath = - S13NAMRack::getTonePresetAmpModelPath( + OpenStudioNAMRack::getTonePresetAmpModelPath( presetState.getData(), static_cast(presetState.getSize())); if (ampModelPath.isNotEmpty()) preset->setProperty("ampModelPath", ampModelPath); const int instrumentProfile = - S13NAMRack::getTonePresetInstrumentProfile( + OpenStudioNAMRack::getTonePresetInstrumentProfile( presetState.getData(), static_cast(presetState.getSize())); preset->setProperty( "instrumentProfile", instrumentProfile - == S13NAMRack::bassInstrumentProfile + == OpenStudioNAMRack::bassInstrumentProfile ? "bass" : "guitar"); } @@ -63894,16 +65657,16 @@ bool AudioEngine::saveBuiltInFXPreset(const juce::String& trackId, const juce::S std::shared_ptr processorOwner; int masterSlotId = 0; - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { { const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); - if (const auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex)) + if (const auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex)) masterSlotId = slot->slotId; } const auto activeStage = std::atomic_load_explicit( - &realtimeMasterFXSnapshot, std::memory_order_acquire); + &(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (const auto* activeSlot = findActiveStageSlot(activeStage, masterSlotId)) processorOwner = activeSlot->processor; } @@ -63925,7 +65688,7 @@ bool AudioEngine::saveBuiltInFXPreset(const juce::String& trackId, const juce::S juce::MemoryBlock stateData; if (auto* rack = - dynamic_cast(processor)) + dynamic_cast(processor)) { rack->getTonePresetStateInformation(stateData); } @@ -63959,16 +65722,16 @@ bool AudioEngine::loadBuiltInFXPreset(const juce::String& trackId, const juce::S std::shared_ptr processorOwner; int masterSlotId = 0; - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { { const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); - if (const auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex)) + if (const auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex)) masterSlotId = slot->slotId; } const auto activeStage = std::atomic_load_explicit( - &realtimeMasterFXSnapshot, std::memory_order_acquire); + &(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (const auto* activeSlot = findActiveStageSlot(activeStage, masterSlotId)) processorOwner = activeSlot->processor; } @@ -63999,10 +65762,10 @@ bool AudioEngine::loadBuiltInFXPreset(const juce::String& trackId, const juce::S int latencyBefore = 0; int latencyAfter = 0; bool restored = false; - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { bool namRackStateMigrated = false; - if (! S13NAMRack::migratePresetStateToCurrent( + if (! OpenStudioNAMRack::migratePresetStateToCurrent( stateData, namRackStateMigrated)) { return false; @@ -64028,13 +65791,14 @@ bool AudioEngine::loadBuiltInFXPreset(const juce::String& trackId, const juce::S latencyBefore = processor->getLatencySamples(); processor->setStateInformation(stateData.getData(), static_cast(stateData.getSize())); latencyAfter = processor->getLatencySamples(); - restored = true; + const auto* isolated = dynamic_cast(processor); + restored = !isolated || isolated->isHealthy(); } if (! restored) return false; - if (dynamic_cast(processor) != nullptr) + if (dynamic_cast(processor) != nullptr) { NAMRackPresetFileMigration migration; if (! migrateNAMRackPresetFileToCurrent( @@ -64049,11 +65813,11 @@ bool AudioEngine::loadBuiltInFXPreset(const juce::String& trackId, const juce::S } } - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { const auto serializedState = serialiseProcessorStateToBase64(processor); const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); - if (auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex); + if (auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex); slot != nullptr && slot->slotId == masterSlotId) { slot->serializedState = serializedState; @@ -64122,7 +65886,7 @@ bool AudioEngine::saveBuiltInFXPresetData(const juce::String& pluginName, return false; juce::MemoryBlock storedPayload; - if (! storedPayload.fromBase64Encoding(base64Data) + if (! PluginStateValidation::decode(base64Data, storedPayload) || storedPayload.getSize() == 0) { return false; @@ -64132,12 +65896,12 @@ bool AudioEngine::saveBuiltInFXPresetData(const juce::String& pluginName, storedPayload.getData(), storedPayload.getSize()); if (! tree.isValid()) return false; - if (safePluginName == "OpenStudio NAM Rack" && tree.getType() != juce::Identifier("S13NAMRack")) + if (safePluginName == "OpenStudio NAM Rack" && tree.getType() != juce::Identifier("OpenStudioNAMRack")) return false; if (safePluginName == "OpenStudio NAM Rack") { bool stateMigrated = false; - if (! S13NAMRack::migratePresetStateToCurrent( + if (! OpenStudioNAMRack::migratePresetStateToCurrent( storedPayload, stateMigrated)) { return false; @@ -64242,8 +66006,7 @@ bool AudioEngine::deleteBuiltInFXPreset(const juce::String& pluginName, const ju juce::File::findFiles, false)) { const auto extension = file.getFileExtension(); - const bool isPreset = extension.equalsIgnoreCase(".ospreset") - || extension.equalsIgnoreCase(".s13preset"); + const bool isPreset = extension.equalsIgnoreCase(".ospreset"); if (isPreset && file.getFileNameWithoutExtension().equalsIgnoreCase( safePresetName)) @@ -64286,39 +66049,40 @@ juce::var AudioEngine::getTrackInputFX(const juce::String& trackId) { juce::DynamicObject::Ptr fxInfo = new juce::DynamicObject(); fxInfo->setProperty("index", i); + fxInfo->setProperty("runtimeFault", track->getProcessorFault(true, i)); fxInfo->setProperty("name", processor->getName()); const bool bypassed = bypassSnapshot != nullptr && bypassSnapshot->count(i) > 0 && bypassSnapshot->at(i); const bool forceFloat = precisionSnapshot != nullptr && precisionSnapshot->count(i) > 0 && precisionSnapshot->at(i); fxInfo->setProperty("bypassed", bypassed); fxInfo->setProperty("precisionOverride", forceFloat ? "float32" : "auto"); - // Check type: built-in > S13FX > VST3 + // Check type: built-in > JSFX > VST3 auto* processorPtr = processor.get(); - if (dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr)) + if (dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr)) { fxInfo->setProperty("type", "builtin"); fxInfo->setProperty("pluginPath", processor->getName()); } - else if (auto* s13fx = dynamic_cast(processorPtr)) + else if (auto* jsfx = dynamic_cast(processorPtr)) { - fxInfo->setProperty("type", "s13fx"); - fxInfo->setProperty("pluginPath", s13fx->getScriptPath()); + fxInfo->setProperty("type", "jsfx"); + fxInfo->setProperty("pluginPath", jsfx->getScriptPath()); } else if (auto* pluginInstance = dynamic_cast(processorPtr)) { auto desc = pluginInstance->getPluginDescription(); fxInfo->setProperty("type", desc.pluginFormatName == "CLAP" ? "clap" : "vst3"); - fxInfo->setProperty("pluginPath", desc.createIdentifierString()); + fxInfo->setProperty("pluginPath", pluginManager.getPluginIdentifier(desc)); } fxList.add(juce::var(fxInfo.get())); } @@ -64351,39 +66115,40 @@ juce::var AudioEngine::getTrackFX(const juce::String& trackId) { juce::DynamicObject::Ptr fxInfo = new juce::DynamicObject(); fxInfo->setProperty("index", i); + fxInfo->setProperty("runtimeFault", track->getProcessorFault(false, i)); fxInfo->setProperty("name", processor->getName()); const bool bypassed = bypassSnapshot != nullptr && bypassSnapshot->count(i) > 0 && bypassSnapshot->at(i); const bool forceFloat = precisionSnapshot != nullptr && precisionSnapshot->count(i) > 0 && precisionSnapshot->at(i); fxInfo->setProperty("bypassed", bypassed); fxInfo->setProperty("precisionOverride", forceFloat ? "float32" : "auto"); - // Check type: built-in > S13FX > VST3 + // Check type: built-in > JSFX > VST3 auto* processorPtr = processor.get(); - if (dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr) - || dynamic_cast(processorPtr)) + if (dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr) + || dynamic_cast(processorPtr)) { fxInfo->setProperty("type", "builtin"); fxInfo->setProperty("pluginPath", processor->getName()); } - else if (auto* s13fx = dynamic_cast(processorPtr)) + else if (auto* jsfx = dynamic_cast(processorPtr)) { - fxInfo->setProperty("type", "s13fx"); - fxInfo->setProperty("pluginPath", s13fx->getScriptPath()); + fxInfo->setProperty("type", "jsfx"); + fxInfo->setProperty("pluginPath", jsfx->getScriptPath()); } else if (auto* pluginInstance = dynamic_cast(processorPtr)) { auto desc = pluginInstance->getPluginDescription(); fxInfo->setProperty("type", desc.pluginFormatName == "CLAP" ? "clap" : "vst3"); - fxInfo->setProperty("pluginPath", desc.createIdentifierString()); + fxInfo->setProperty("pluginPath", pluginManager.getPluginIdentifier(desc)); } fxList.add(juce::var(fxInfo.get())); } @@ -64400,7 +66165,7 @@ static bool isOpenStudioBuiltInParameterEligible( if (processor == nullptr || parameter == nullptr || parameterId.isEmpty()) return false; - if (dynamic_cast(processor) == nullptr) + if (dynamic_cast(processor) == nullptr) return false; static const juce::StringArray excludedNAMParameters { @@ -64619,7 +66384,7 @@ juce::var AudioEngine::getBuiltInPluginSchema(const juce::String& trackId, const // Built-in effects publish ordinary editor controls atomically. Resolve // them from immutable realtime snapshots so opening or refreshing an // editor never owns either callback lock while its schema is assembled. - // S13EQ's analyzer is also a lock-free audio-to-UI publication. + // OpenStudioEQ's analyzer is also a lock-free audio-to-UI publication. const auto publishedOwner = getPublishedBuiltInProcessor(trackId, chainType, fxIndex); if (supportsLockFreeBuiltInControlPublication( @@ -64629,7 +66394,7 @@ juce::var AudioEngine::getBuiltInPluginSchema(const juce::String& trackId, const publishedOwner.processor.get(), chainType, fxIndex); } - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { int slotId = 0; { @@ -64637,11 +66402,11 @@ juce::var AudioEngine::getBuiltInPluginSchema(const juce::String& trackId, const if (! sl.isLocked()) return describeBuiltInProcessor(nullptr, chainType, fxIndex); - if (const auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex)) + if (const auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex)) slotId = slot->slotId; } - auto activeStage = std::atomic_load_explicit(&realtimeMasterFXSnapshot, std::memory_order_acquire); + auto activeStage = std::atomic_load_explicit(&(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (const auto* activeSlot = findActiveStageSlot(activeStage, slotId)) { auto* processor = activeSlot->processor.get(); @@ -64686,7 +66451,7 @@ juce::var AudioEngine::getNAMRackDiagnostics( // diagnostic snapshot may briefly serialize non-realtime model metadata, // but that lock is never acquired by the audio callback; live UI telemetry // therefore cannot make the audio thread skip a NAM block. - if (auto* rack = dynamic_cast( + if (auto* rack = dynamic_cast( processorOwner.processor.get())) return rack->getDiagnosticState(); @@ -64733,7 +66498,7 @@ juce::var AudioEngine::getBuiltInPluginState(const juce::String& trackId, const getPublishedBuiltInProcessor( trackId, chainType, fxIndex); if (const auto* rack = - dynamic_cast( + dynamic_cast( processorOwner.processor.get())) { if (auto* values = root->getProperty( @@ -64794,10 +66559,10 @@ juce::var AudioEngine::getBuiltInPluginState(const juce::String& trackId, const new juce::DynamicObject(); dspState->setProperty( "namEffectsDspVersion", - S13NAMRack::currentNAMEffectsDspVersion); + OpenStudioNAMRack::currentNAMEffectsDspVersion); dspState->setProperty( "reverbEngineVersion", - S13NAMRack::currentReverbEngineVersion); + OpenStudioNAMRack::currentReverbEngineVersion); root->setProperty( "dspState", juce::var(dspState)); @@ -64839,7 +66604,7 @@ bool AudioEngine::setBuiltInPluginParam(const juce::String& trackId, const juce: if (applied && paramId == "auditionSource") { if (auto* rack = - dynamic_cast( + dynamic_cast( publishedProcessor)) { juce::Logger::writeToLog( @@ -64857,12 +66622,12 @@ bool AudioEngine::setBuiltInPluginParam(const juce::String& trackId, const juce: } } - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { int slotId = 0; { const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); - if (const auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex)) + if (const auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex)) slotId = slot->slotId; } @@ -64871,7 +66636,7 @@ bool AudioEngine::setBuiltInPluginParam(const juce::String& trackId, const juce: juce::String serializedState; bool latencyChanged = false; - auto activeStage = std::atomic_load_explicit(&realtimeMasterFXSnapshot, std::memory_order_acquire); + auto activeStage = std::atomic_load_explicit(&(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (const auto* activeSlot = findActiveStageSlot(activeStage, slotId)) { if (!activeSlot->processor) @@ -64926,7 +66691,7 @@ bool AudioEngine::setBuiltInPluginParam(const juce::String& trackId, const juce: { const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); - if (auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex)) + if (auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex)) slot->serializedState = serializedState; } if (latencyChanged) @@ -64974,7 +66739,7 @@ bool AudioEngine::setBuiltInPluginParam(const juce::String& trackId, const juce: it->second->invalidatePluginAutomationCache(); if (applied && paramId == "auditionSource") { - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { juce::Logger::writeToLog("AudioEngine: NAM auditionSource=" + juce::String(value) @@ -65148,15 +66913,15 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: && rawEffectsVersion >= 1.0 && (rawEffectsVersion <= static_cast( - S13NAMRack::currentNAMEffectsDspVersion) + OpenStudioNAMRack::currentNAMEffectsDspVersion) || rawEffectsVersion == static_cast( - S13NAMRack::developmentNAMEffectsDspVersionAlias))) + OpenStudioNAMRack::developmentNAMEffectsDspVersionAlias))) { storedEffectsVersion = rawEffectsVersion == static_cast( - S13NAMRack::developmentNAMEffectsDspVersionAlias) - ? S13NAMRack::developmentNAMEffectsDspVersionAliasSourceVersion + OpenStudioNAMRack::developmentNAMEffectsDspVersionAlias) + ? OpenStudioNAMRack::developmentNAMEffectsDspVersionAliasSourceVersion : juce::roundToInt(rawEffectsVersion); } else @@ -65175,7 +66940,7 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: || rawReverbVersion < 1.0 || rawReverbVersion > static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) { invalidDspStateRequested = true; } @@ -65196,7 +66961,7 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: const bool preV10PortableBundle = completePortableNAMBundle && (! effectsVersionPropertyPresent || storedEffectsVersion - < S13NAMRack::delayV10IntroducedNAMEffectsDspVersion); + < OpenStudioNAMRack::delayV10IntroducedNAMEffectsDspVersion); std::vector> parameterValues; const auto appendParameter = [&] (const juce::String& id, const juce::var& rawValue) @@ -65275,25 +67040,33 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: const bool hasCabDoublerEnabled = findParameter("cabDoublerEnabled") != parameterValues.end(); + const auto cabEngineVersionParameter = findParameter("cabEngineVersion"); + const bool legacyCabState = cabEngineVersionParameter == parameterValues.end() + || ! std::isfinite(cabEngineVersionParameter->second) + || cabEngineVersionParameter->second < 1.5f; + const bool cabFiltersNeedEqMigration = + cabEngineVersionParameter == parameterValues.end() + || ! std::isfinite(cabEngineVersionParameter->second) + || cabEngineVersionParameter->second < 2.5f; const float legacyCabRoomAmount = parameterOrDefault("cabRoomAmount", 0.0f); const float legacyCabDoublerMix = parameterOrDefault("cabDoublerMix", 0.0f); const bool hasCurrentGraphicEqFilters = storedEffectsVersion - >= S13NAMRack::graphicEqFiltersIntroducedNAMEffectsDspVersion + >= OpenStudioNAMRack::graphicEqFiltersIntroducedNAMEffectsDspVersion && storedEffectsVersion - <= S13NAMRack::currentNAMEffectsDspVersion; + <= OpenStudioNAMRack::currentNAMEffectsDspVersion; const bool hasCurrentPreEq = storedEffectsVersion - >= S13NAMRack::preEqIntroducedNAMEffectsDspVersion + >= OpenStudioNAMRack::preEqIntroducedNAMEffectsDspVersion && storedEffectsVersion - <= S13NAMRack::currentNAMEffectsDspVersion; + <= OpenStudioNAMRack::currentNAMEffectsDspVersion; const bool hasEightBandPreEq = storedEffectsVersion - >= S13NAMRack::eightBandPreEqIntroducedNAMEffectsDspVersion + >= OpenStudioNAMRack::eightBandPreEqIntroducedNAMEffectsDspVersion && storedEffectsVersion - <= S13NAMRack::currentNAMEffectsDspVersion; + <= OpenStudioNAMRack::currentNAMEffectsDspVersion; const float requestedPreEqHPFHz = parameterOrDefault("preEqHPFHz", 0.0f); const float requestedPreEqLPFHz = @@ -65429,6 +67202,34 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: appendDefault("chaosGate", 0.22f); appendDefault("chaosMix", 1.0f); appendDefault("chaosLevelDb", 0.0f); + const bool sourceCabHPFEnabled = legacyCabState + || parameterOrDefault("cabHPFEnabled", 0.0f) >= 0.5f; + const bool sourceCabLPFEnabled = legacyCabState + || parameterOrDefault("cabLPFEnabled", 0.0f) >= 0.5f; + const float sourceCabHPFHz = juce::jlimit( + 20.0f, + 500.0f, + parameterOrDefault("cabHPFHz", legacyCabState ? 80.0f : 30.0f)); + const float sourceCabLPFHz = juce::jlimit( + 1000.0f, + 20000.0f, + parameterOrDefault("cabLPFHz", legacyCabState ? 8500.0f : 16000.0f)); + setOrAppendParameter("cabEngineVersion", 3.0f); + setOrAppendParameter("cabHPFEnabled", 0.0f); + setOrAppendParameter("cabLPFEnabled", 0.0f); + setOrAppendParameter("cabHPFHz", 30.0f); + setOrAppendParameter("cabLPFHz", 16000.0f); + setOrAppendParameter( + "cabIRStereo", + legacyCabState + ? 1.0f + : (parameterOrDefault("cabIRStereo", 0.0f) >= 0.5f ? 1.0f : 0.0f)); + setOrAppendParameter( + "cabDirectMix", + juce::jlimit( + 0.0f, + 1.0f, + parameterOrDefault("cabDirectMix", 0.0f))); appendDefault( "cabRoomEnabled", ! hasCabRoomEnabled @@ -65448,20 +67249,39 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: appendDefault("cabDoublerMix", 0.12f); appendDefault("cabDoublerDelayMs", 4.5f); appendDefault("cabDoublerSpread", 0.65f); - const float canonicalEqHPFHz = hasCurrentGraphicEqFilters + float canonicalEqHPFHz = hasCurrentGraphicEqFilters ? (! std::isfinite(requestedEqHPFHz) || requestedEqHPFHz < 20.0f ? 0.0f : juce::jlimit( 20.0f, 500.0f, requestedEqHPFHz)) : 0.0f; - const float canonicalEqLPFHz = hasCurrentGraphicEqFilters + float canonicalEqLPFHz = hasCurrentGraphicEqFilters ? (! std::isfinite(requestedEqLPFHz) || requestedEqLPFHz >= 22000.0f ? 24000.0f : juce::jlimit( 3000.0f, 20000.0f, requestedEqLPFHz)) : 24000.0f; + const bool sourceCabEnabled = parameterOrDefault("cabEnabled", 1.0f) >= 0.5f + && parameterOrDefault("cabRequestedEnabled", 1.0f) >= 0.5f; + if (cabFiltersNeedEqMigration && sourceCabEnabled + && (sourceCabHPFEnabled || sourceCabLPFEnabled)) + { + if (parameterOrDefault("eqEnabled", 0.0f) < 0.5f) + { + for (const auto* id : { "eq65Db", "eq125Db", "eq250Db", "eq500Db", "eq1kDb", + "eq2kDb", "eq4kDb", "eq8kDb", "eq16kDb", "eqLevelDb" }) + setOrAppendParameter(id, 0.0f); + canonicalEqHPFHz = 0.0f; + canonicalEqLPFHz = 24000.0f; + } + setOrAppendParameter("eqEnabled", 1.0f); + if (sourceCabHPFEnabled) + canonicalEqHPFHz = juce::jmax(canonicalEqHPFHz, sourceCabHPFHz); + if (sourceCabLPFEnabled) + canonicalEqLPFHz = juce::jmin(canonicalEqLPFHz, sourceCabLPFHz); + } setOrAppendParameter( "eqHPFHz", canonicalEqHPFHz); @@ -65507,14 +67327,14 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: "delayMode", 1.0f); float canonicalDelayMode = 1.0f; if (storedEffectsVersion - >= S13NAMRack::delayV10IntroducedNAMEffectsDspVersion + >= OpenStudioNAMRack::delayV10IntroducedNAMEffectsDspVersion && storedEffectsVersion - <= S13NAMRack::currentNAMEffectsDspVersion) + <= OpenStudioNAMRack::currentNAMEffectsDspVersion) { canonicalDelayMode = std::floor( juce::jlimit( 0.0f, - static_cast(S13NAMRack::dualDelayMode), + static_cast(OpenStudioNAMRack::dualDelayMode), std::isfinite(requestedDelayMode) ? requestedDelayMode : 1.0f) @@ -65522,12 +67342,12 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: } else if (storedEffectsVersion >= 1 && storedEffectsVersion - < S13NAMRack::currentNAMEffectsDspVersion) + < OpenStudioNAMRack::currentNAMEffectsDspVersion) { canonicalDelayMode = std::floor( juce::jlimit( 0.0f, - static_cast(S13NAMRack::analogDelayMode), + static_cast(OpenStudioNAMRack::analogDelayMode), std::isfinite(requestedDelayMode) ? requestedDelayMode : 1.0f) @@ -65598,6 +67418,11 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: return parameter.first == "compressorDetail" || parameter.first == "precisionDriveMode" || parameter.first == "auditionSource" + || parameter.first == "cabEngineVersion" + || parameter.first == "cabMicPosition" + || parameter.first == "cabMicDistance" + || parameter.first == "cabMicBlend" + || parameter.first == "cabRoomSend" || parameter.first == "preEq100Db" || parameter.first == "preEq200Db" || parameter.first == "preEq400Db" @@ -65623,15 +67448,15 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: std::shared_ptr processorOwner; TrackProcessor* automationTrack = nullptr; int masterSlotId = 0; - if (chainType == "master") + if ((chainType == "master" || chainType == "monitor")) { { const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); - if (const auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex)) + if (const auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex)) masterSlotId = slot->slotId; } - auto activeStage = std::atomic_load_explicit(&realtimeMasterFXSnapshot, std::memory_order_acquire); + auto activeStage = std::atomic_load_explicit(&(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (const auto* activeSlot = findActiveStageSlot(activeStage, masterSlotId)) processorOwner = activeSlot->processor; } @@ -65653,7 +67478,7 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: if (processor == nullptr) return false; - auto* rack = dynamic_cast(processor); + auto* rack = dynamic_cast(processor); const bool modelMetadataRequested = pedalDeclaredCaptureTypeSpecified || ampDeclaredCaptureTypeSpecified; const bool resourcesOrModelMetadataRequested = @@ -65666,7 +67491,7 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: auto uiStateVar = stateObject->getProperty("uiState"); const bool uiStateRequested = ! uiStateVar.isVoid(); if (uiStateRequested && rack != nullptr) - (void) S13NAMRack::migrateUiStateToCurrent( + (void) OpenStudioNAMRack::migrateUiStateToCurrent( uiStateVar, storedEffectsVersion); bool scalarOrUiApplied = false; @@ -65680,19 +67505,19 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: rack->auditionSource.store(0.0f, std::memory_order_relaxed); const bool selectorChanged = rack->getNAMEffectsDspVersion() - != S13NAMRack::currentNAMEffectsDspVersion + != OpenStudioNAMRack::currentNAMEffectsDspVersion || std::abs( rack->reverbEngineVersion.load( std::memory_order_relaxed) - static_cast( - S13NAMRack::currentReverbEngineVersion)) + OpenStudioNAMRack::currentReverbEngineVersion)) > 1.0e-6f; rack->namEffectsDspVersion.store( - S13NAMRack::currentNAMEffectsDspVersion, + OpenStudioNAMRack::currentNAMEffectsDspVersion, std::memory_order_relaxed); rack->reverbEngineVersion.store( static_cast( - S13NAMRack::currentReverbEngineVersion), + OpenStudioNAMRack::currentReverbEngineVersion), std::memory_order_relaxed); scalarOrUiApplied = selectorChanged || dspStateRequested @@ -65770,11 +67595,11 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: if (processor->getLatencySamples() != latencyBefore) recalculatePDC(); - if (chainType == "master" && masterSlotId != 0 && (resourcesApplied || scalarOrUiApplied)) + if ((chainType == "master" || chainType == "monitor") && masterSlotId != 0 && (resourcesApplied || scalarOrUiApplied)) { const auto activeStage = std::atomic_load_explicit( - &realtimeMasterFXSnapshot, + &(chainType == "monitor" ? realtimeMonitoringFXSnapshot : realtimeMasterFXSnapshot), std::memory_order_acquire); if (const auto* activeSlot = findActiveStageSlot( @@ -65796,7 +67621,7 @@ bool AudioEngine::setBuiltInPluginState(const juce::String& trackId, const juce: } const auto serializedState = serialiseProcessorStateToBase64(processor); const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); - if (auto* slot = findDesiredStageSlot(desiredMasterStageSpec, fxIndex); + if (auto* slot = findDesiredStageSlot((chainType == "monitor" ? desiredMonitoringStageSpec : desiredMasterStageSpec), fxIndex); slot != nullptr && slot->slotId == masterSlotId) { slot->serializedState = serializedState; @@ -65886,6 +67711,8 @@ bool AudioEngine::removeTrackInputFX(const juce::String& trackId, int fxIndex) if (removed) { juce::Logger::writeToLog("AudioEngine: Removed input FX " + juce::String(fxIndex) + " from track " + trackId); + if (onFXSlotsRemoved) + onFXSlotsRemoved(trackId, "input", fxIndex); recalculatePDC(); } return removed; @@ -65935,6 +67762,8 @@ bool AudioEngine::removeTrackFX(const juce::String& trackId, int fxIndex) if (removed) { juce::Logger::writeToLog("AudioEngine: Removed track FX " + juce::String(fxIndex) + " from track " + trackId); + if (onFXSlotsRemoved) + onFXSlotsRemoved(trackId, "track", fxIndex); recalculatePDC(); } return removed; @@ -66039,6 +67868,8 @@ bool AudioEngine::addMasterFX(const juce::String& pluginPath) specCopy = desiredMasterStageSpec; } + syncStageSpecStateFromActive(specCopy, std::atomic_load(&realtimeMasterFXSnapshot)); + DesiredFXStageSlot slot; slot.slotId = nextMasterStageSlotId++; slot.name = plugin->getName(); @@ -66046,7 +67877,7 @@ bool AudioEngine::addMasterFX(const juce::String& pluginPath) { auto desc = pluginInstance->getPluginDescription(); slot.type = desc.pluginFormatName == "CLAP" ? "clap" : "vst3"; - slot.pluginPath = desc.createIdentifierString(); + slot.pluginPath = pluginManager.getPluginIdentifier(desc); slot.pluginFormat = desc.pluginFormatName; } else @@ -66071,6 +67902,7 @@ bool AudioEngine::addMasterFX(const juce::String& pluginPath) juce::var AudioEngine::getMasterFX() { const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); + const auto active = std::atomic_load(&realtimeMasterFXSnapshot); juce::Array fxList; for (int i = 0; i < static_cast(desiredMasterStageSpec.slots.size()); ++i) { @@ -66081,6 +67913,8 @@ juce::var AudioEngine::getMasterFX() fxInfo->setProperty("bypassed", slot.bypassed); fxInfo->setProperty("precisionOverride", slot.forceFloat ? "float32" : "auto"); fxInfo->setProperty("type", slot.type); + if (const auto* published = findActiveStageSlot(active, slot.slotId)) + fxInfo->setProperty("runtimeFault", static_cast(published->safety->failure.load())); if (!slot.pluginPath.isEmpty()) fxInfo->setProperty("pluginPath", slot.pluginPath); fxList.add(juce::var(fxInfo.get())); @@ -66126,6 +67960,8 @@ bool AudioEngine::removeMasterFX(int fxIndex) retiringProcessor->releaseResources(); } juce::Logger::writeToLog("AudioEngine: Removed master FX " + juce::String(fxIndex)); + if (onFXSlotsRemoved) + onFXSlotsRemoved({}, "master", fxIndex); } return published; } @@ -66191,8 +68027,12 @@ void AudioEngine::openMasterFXEditor(int fxIndex) PluginWindowManager::PluginEditorTarget target; target.scope = PluginWindowManager::PluginEditorTarget::Scope::MasterFX; target.fxIndex = fxIndex; - juce::MessageManager::callAsync([this, processor, title, target]() + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), processor, title, target]() { + if (!MessageThreadLifetime::accepts(lifetime)) return; + const auto current = std::atomic_load(&realtimeMasterFXSnapshot); + if (!current || target.fxIndex < 0 || target.fxIndex >= static_cast(current->slots.size()) + || current->slots[static_cast(target.fxIndex)].processor != processor) return; if (processor) pluginWindowManager.openEditor(processor, title, target); }); @@ -66244,6 +68084,7 @@ void AudioEngine::bypassMasterFX(int fxIndex, bool bypassed) const juce::ScopedLock processorGuard( currentSlot->processor->getCallbackLock()); currentSlot->processor->reset(); + currentSlot->safety->reset(); } auto nextStage = @@ -66313,7 +68154,10 @@ bool AudioEngine::addMonitoringFX(const juce::String& pluginPath) double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0; int bs = getSafeHostedPluginBlockSize(currentBlockSize); - auto plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); + const bool builtIn = pluginPath == "OpenStudio NAM Rack"; + std::unique_ptr plugin; + if (builtIn) plugin = createBuiltInEffect(pluginPath); + else plugin = pluginManager.loadPluginFromFile(pluginPath, sr, bs); if (!plugin) { juce::Logger::writeToLog("AudioEngine: Failed to load plugin for monitoring FX"); @@ -66337,15 +68181,22 @@ bool AudioEngine::addMonitoringFX(const juce::String& pluginPath) const juce::ScopedLock lockSl(mainProcessorGraph->getCallbackLock()); specCopy = desiredMonitoringStageSpec; } + syncStageSpecStateFromActive(specCopy, std::atomic_load(&realtimeMonitoringFXSnapshot)); DesiredFXStageSlot slot; slot.slotId = nextMonitoringStageSlotId++; slot.name = plugin->getName(); - if (auto* pluginInstance = dynamic_cast(plugin.get())) + if (builtIn) + { + slot.type = "builtin"; + slot.pluginPath = pluginPath; + slot.pluginFormat = "Built-in"; + } + else if (auto* pluginInstance = dynamic_cast(plugin.get())) { auto desc = pluginInstance->getPluginDescription(); slot.type = desc.pluginFormatName == "CLAP" ? "clap" : "vst3"; - slot.pluginPath = desc.createIdentifierString(); + slot.pluginPath = pluginManager.getPluginIdentifier(desc); slot.pluginFormat = desc.pluginFormatName; } else @@ -66371,6 +68222,7 @@ bool AudioEngine::addMonitoringFX(const juce::String& pluginPath) juce::var AudioEngine::getMonitoringFX() { const juce::ScopedLock sl(mainProcessorGraph->getCallbackLock()); + const auto active = std::atomic_load(&realtimeMonitoringFXSnapshot); juce::Array fxList; for (int i = 0; i < static_cast(desiredMonitoringStageSpec.slots.size()); ++i) { @@ -66381,6 +68233,8 @@ juce::var AudioEngine::getMonitoringFX() fxInfo->setProperty("bypassed", slot.bypassed); fxInfo->setProperty("precisionOverride", slot.forceFloat ? "float32" : "auto"); fxInfo->setProperty("type", slot.type); + if (const auto* published = findActiveStageSlot(active, slot.slotId)) + fxInfo->setProperty("runtimeFault", static_cast(published->safety->failure.load())); if (!slot.pluginPath.isEmpty()) fxInfo->setProperty("pluginPath", slot.pluginPath); fxList.add(juce::var(fxInfo.get())); @@ -66422,6 +68276,8 @@ void AudioEngine::removeMonitoringFX(int fxIndex) retiringProcessor->releaseResources(); } juce::Logger::writeToLog("AudioEngine: Removed monitoring FX " + juce::String(fxIndex)); + if (onFXSlotsRemoved) + onFXSlotsRemoved({}, "monitor", fxIndex); } } @@ -66447,8 +68303,12 @@ void AudioEngine::openMonitoringFXEditor(int fxIndex) PluginWindowManager::PluginEditorTarget target; target.scope = PluginWindowManager::PluginEditorTarget::Scope::MonitoringFX; target.fxIndex = fxIndex; - juce::MessageManager::callAsync([this, processor, title, target]() + juce::MessageManager::callAsync([this, lifetime = deferredCallbacks.token(), processor, title, target]() { + if (!MessageThreadLifetime::accepts(lifetime)) return; + const auto current = std::atomic_load(&realtimeMonitoringFXSnapshot); + if (!current || target.fxIndex < 0 || target.fxIndex >= static_cast(current->slots.size()) + || current->slots[static_cast(target.fxIndex)].processor != processor) return; if (processor) pluginWindowManager.openEditor(processor, title, target); }); @@ -66495,6 +68355,7 @@ void AudioEngine::bypassMonitoringFX(int fxIndex, bool bypassed) const juce::ScopedLock processorGuard( currentSlot->processor->getCallbackLock()); currentSlot->processor->reset(); + currentSlot->safety->reset(); } auto nextStage = @@ -66874,6 +68735,13 @@ void AudioEngine::setMetronomeEnabled(bool enabled) metronome.setEnabled(enabled); } +bool AudioEngine::setMetronomePracticeEnabled(bool enabled) +{ + // Availability and the latch share one atomic publication. Device loss + // cannot race a start into reviving practice on a closed device. + return metronome.setPracticeEnabled(enabled); +} + void AudioEngine::setMetronomeVolume(float volume) { metronome.setVolume(volume); @@ -67148,7 +69016,16 @@ juce::String AudioEngine::getSidechainSource(const juce::String& destTrackId, in int AudioEngine::addTrackSend(const juce::String& sourceTrackId, const juce::String& destTrackId) { - if (trackMap.find(sourceTrackId) == trackMap.end()) return -1; + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); + if (trackMap.find(sourceTrackId) == trackMap.end() || trackMap.find(destTrackId) == trackMap.end()) return -1; + if (sendWouldCreateCycle(sourceTrackId, destTrackId, [this](const juce::String& id) { + juce::StringArray destinations; + const auto found = trackMap.find(id); + if (found != trackMap.end() && found->second != nullptr) + for (int index = 0; index < found->second->getNumSends(); ++index) + destinations.add(found->second->getSendDestination(index)); + return destinations; + })) return -1; int sendIndex = trackMap[sourceTrackId]->addSend(destTrackId); rebuildRealtimeProcessingSnapshots(); return sendIndex; @@ -67156,6 +69033,7 @@ int AudioEngine::addTrackSend(const juce::String& sourceTrackId, const juce::Str void AudioEngine::removeTrackSend(const juce::String& sourceTrackId, int sendIndex) { + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); if (trackMap.find(sourceTrackId) != trackMap.end()) { trackMap[sourceTrackId]->removeSend(sendIndex); @@ -67165,6 +69043,8 @@ void AudioEngine::removeTrackSend(const juce::String& sourceTrackId, int sendInd void AudioEngine::setTrackSendLevel(const juce::String& sourceTrackId, int sendIndex, float level) { + if (!std::isfinite(level)) return; + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); if (trackMap.find(sourceTrackId) != trackMap.end()) { trackMap[sourceTrackId]->setSendLevel(sendIndex, level); @@ -67174,6 +69054,8 @@ void AudioEngine::setTrackSendLevel(const juce::String& sourceTrackId, int sendI void AudioEngine::setTrackSendPan(const juce::String& sourceTrackId, int sendIndex, float pan) { + if (!std::isfinite(pan)) return; + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); if (trackMap.find(sourceTrackId) != trackMap.end()) { trackMap[sourceTrackId]->setSendPan(sendIndex, pan); @@ -67183,6 +69065,7 @@ void AudioEngine::setTrackSendPan(const juce::String& sourceTrackId, int sendInd void AudioEngine::setTrackSendEnabled(const juce::String& sourceTrackId, int sendIndex, bool enabled) { + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); if (trackMap.find(sourceTrackId) != trackMap.end()) { trackMap[sourceTrackId]->setSendEnabled(sendIndex, enabled); @@ -67192,6 +69075,7 @@ void AudioEngine::setTrackSendEnabled(const juce::String& sourceTrackId, int sen void AudioEngine::setTrackSendPreFader(const juce::String& sourceTrackId, int sendIndex, bool preFader) { + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); if (trackMap.find(sourceTrackId) != trackMap.end()) { trackMap[sourceTrackId]->setSendPreFader(sendIndex, preFader); @@ -67201,6 +69085,7 @@ void AudioEngine::setTrackSendPreFader(const juce::String& sourceTrackId, int se void AudioEngine::setTrackSendPhaseInvert(const juce::String& sourceTrackId, int sendIndex, bool invert) { + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); if (trackMap.find(sourceTrackId) != trackMap.end()) { trackMap[sourceTrackId]->setSendPhaseInvert(sendIndex, invert); @@ -67210,6 +69095,7 @@ void AudioEngine::setTrackSendPhaseInvert(const juce::String& sourceTrackId, int juce::var AudioEngine::getTrackSends(const juce::String& trackId) { + const juce::ScopedLock graphLock(mainProcessorGraph->getCallbackLock()); juce::Array result; if (trackMap.find(trackId) == trackMap.end()) return result; @@ -68595,8 +70481,7 @@ juce::var AudioEngine::getAudioDebugSnapshot() const juce::String AudioEngine::renderMetronomeToFile(double startTime, double endTime) { - // Create output file in the preferred OpenStudio audio directory, while - // still following the legacy Studio13 location if an existing install uses it. + // Keep user-created audio separate from diagnostic and internal state paths. auto audioFolder = getPreferredAppDataDirectory().getChildFile("Audio"); if (!audioFolder.exists()) @@ -68683,7 +70568,7 @@ bool AudioEngine::setPluginState( { // Decode Base64 to memory block juce::MemoryBlock stateData; - if (!stateData.fromBase64Encoding(base64State)) + if (!PluginStateValidation::decode(base64State, stateData)) return false; std::shared_ptr processorOwner; @@ -68705,7 +70590,7 @@ bool AudioEngine::setPluginState( int latencyBefore = 0; int latencyAfter = 0; bool restored = false; - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { latencyBefore = processor->getLatencySamples(); restored = rack->restoreProjectStateInformation( @@ -68786,7 +70671,7 @@ bool AudioEngine::isMasterNAMRackPlugin(int fxIndex) &realtimeMasterFXSnapshot, std::memory_order_acquire); const auto* activeSlot = findActiveStageSlot(activeStage, slotId); return activeSlot != nullptr - && dynamic_cast(activeSlot->processor.get()) != nullptr; + && dynamic_cast(activeSlot->processor.get()) != nullptr; } bool AudioEngine::setMasterPluginState( @@ -68795,7 +70680,7 @@ bool AudioEngine::setMasterPluginState( const std::function()>& publicationLeaseFactory) { juce::MemoryBlock stateData; - if (!stateData.fromBase64Encoding(base64State)) + if (!PluginStateValidation::decode(base64State, stateData)) return false; int slotId = 0; @@ -68824,7 +70709,7 @@ bool AudioEngine::setMasterPluginState( // matching desired stage state has been persisted, so a newer request cannot // land between audible publication and persistence. std::shared_ptr publicationLease; - if (auto* rack = dynamic_cast(processor)) + if (auto* rack = dynamic_cast(processor)) { restored = rack->restoreProjectStateInformation( stateData.getData(), @@ -68842,7 +70727,8 @@ bool AudioEngine::setMasterPluginState( } const juce::ScopedLock processorLock(processor->getCallbackLock()); processor->setStateInformation(stateData.getData(), static_cast(stateData.getSize())); - restored = true; + const auto* isolated = dynamic_cast(processor); + restored = !isolated || isolated->isHealthy(); } if (! restored) return false; @@ -68874,7 +70760,8 @@ juce::File AudioEngine::findFFmpegExe() const bool AudioEngine::convertWithFFmpeg(const juce::File& inputFile, const juce::File& outputFile, const juce::String& format, double targetSampleRate, int quality, - int bitDepth, int numChannels) const + int bitDepth, int numChannels, + const std::function& keepRunning) const { auto ffmpeg = findFFmpegExe(); if (!ffmpeg.existsAsFile()) @@ -68985,27 +70872,30 @@ bool AudioEngine::convertWithFFmpeg(const juce::File& inputFile, const juce::Fil logToDisk("convertWithFFmpeg: Running: " + args.joinIntoString(" ")); // Run ffmpeg as child process - juce::ChildProcess process; - if (!process.start(args)) + OwnedChildProcess process; + if ((keepRunning && !keepRunning()) || !process.start(args, 0)) { logToDisk("convertWithFFmpeg: FAIL - could not start ffmpeg process"); return false; } // Wait for completion (up to 5 minutes for large files) - if (!process.waitForProcessToFinish(300000)) + const auto deadline = juce::Time::getMillisecondCounterHiRes() + 300000.0; + while (process.isRunning() && (!keepRunning || keepRunning()) + && juce::Time::getMillisecondCounterHiRes() < deadline) + juce::Thread::sleep(10); + if (process.isRunning()) { - logToDisk("convertWithFFmpeg: FAIL - ffmpeg timed out after 5 minutes"); + logToDisk("convertWithFFmpeg: cancelled or exceeded the 5-minute encoding deadline"); process.kill(); outputFile.deleteFile(); return false; } - int exitCode = process.getExitCode(); + const auto exitCode = process.getExitCode(); if (exitCode != 0) { - juce::String errOutput = process.readAllProcessOutput(); - logToDisk("convertWithFFmpeg: FAIL - ffmpeg exit code " + juce::String(exitCode) + " output: " + errOutput); + logToDisk("convertWithFFmpeg: FAIL - ffmpeg exit code " + juce::String(exitCode)); outputFile.deleteFile(); return false; } @@ -69030,9 +70920,12 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do double renderSampleRate, int bitDepth, int numChannels, bool normalize, bool addTail, double tailLengthMs, bool includeMetronome, - const juce::StringArray& includedClipIds) + const juce::StringArray& includedClipIds, + const std::function& keepRunning) { + if (keepRunning && !keepRunning()) return false; const juce::ScopedLock offlineRenderTransaction(offlineRenderTransactionLock); + if (keepRunning && !keepRunning()) return false; // Consume the request at transaction entry so invalid ranges, unsupported // formats, and unwritable paths cannot leak dither into the next render. const int ditherMode = pendingDitherMode_.exchange(0); @@ -69504,6 +71397,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do juce::AudioProcessor* processor; juce::MemoryBlock savedState; bool preparedByTrack = false; + bool wasNonRealtime = false; bool hasNAMTransientState = false; float namAuditionSource = 0.0f; std::uint64_t namAuditionSourceSample = 0; @@ -69525,7 +71419,8 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do PluginStateBackup backup; backup.processor = proc; backup.preparedByTrack = preparedByTrack; - if (auto* rack = dynamic_cast(proc)) + backup.wasNonRealtime = proc->isNonRealtime(); + if (auto* rack = dynamic_cast(proc)) { // auditionSource is deliberately excluded from durable project and // preset state. Offline work still has to restore the live monitor @@ -69536,6 +71431,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do backup.namAuditionSourceSample = rack->auditionSourceSample; } proc->getStateInformation(backup.savedState); + proc->setNonRealtime(true); pluginBackups.push_back(std::move(backup)); }; @@ -69557,7 +71453,8 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do // pre-render state without touching model or IR resources. for (auto& backup : pluginBackups) { - if (auto* rack = dynamic_cast(backup.processor)) + backup.processor->setNonRealtime(backup.wasNonRealtime); + if (auto* rack = dynamic_cast(backup.processor)) { rack->restoreRenderPassStateInformation(backup.savedState.getData(), static_cast(backup.savedState.getSize())); @@ -69580,7 +71477,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do // Rendering does not mutate NAM tone/resource identity. A full // state restore would re-probe model files and re-submit the IR // asynchronously just before live playback resumes. - if (auto* rack = dynamic_cast(backup.processor)) + if (auto* rack = dynamic_cast(backup.processor)) { const bool restored = rack->restoreRenderPassStateInformation( backup.savedState.getData(), static_cast(backup.savedState.getSize())); @@ -69593,7 +71490,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do (int)backup.savedState.getSize()); } backup.processor->reset(); - if (auto* rack = dynamic_cast(backup.processor); + if (auto* rack = dynamic_cast(backup.processor); rack != nullptr && backup.hasNAMTransientState) { rack->auditionSource.store( @@ -70062,7 +71959,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do // each normalization pass can start asynchronous convolution // work and used to arm a model transition. Its reset() now // clears all streaming/effect history without changing state. - if (auto* rack = dynamic_cast(backup.processor)) + if (auto* rack = dynamic_cast(backup.processor)) { const bool restored = rack->restoreRenderPassStateInformation( backup.savedState.getData(), static_cast(backup.savedState.getSize())); @@ -70160,6 +72057,12 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do while (samplesRemaining > 0) { + if (keepRunning && !keepRunning()) + { + writer.reset(); + restorePreparedTracksForRealtime(); + return false; + } int samplesThisBlock = (int)std::min((juce::int64)blockSize, samplesRemaining); const juce::int64 renderBlockStartSample = processingTotalSamples - samplesRemaining; const auto outputWindow = getOfflineRenderBlockWindow(offlineRenderPlan, @@ -70807,7 +72710,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do // Let FFmpeg perform the final output SRC/encode step. bool ffmpegOk = convertWithFFmpeg(renderFile, stagedOutput.getFile(), formatLower, requestedOutputSampleRate, codecQuality, - bitDepth, numChannels); + bitDepth, numChannels, keepRunning); // Clean up temp file renderFile.deleteFile(); @@ -70835,6 +72738,7 @@ bool AudioEngine::renderProject(const juce::String& source, double startTime, do restorePreparedTracksForRealtime(); logToDisk("renderProject: Restored " + juce::String((int)pluginBackups.size()) + " FX plugins for real-time"); + if (keepRunning && !keepRunning()) return false; if (! stagedOutput.overwriteTargetFileWithTemporary()) { logToDisk("renderProject: FAIL - could not atomically publish staged output to " @@ -71551,6 +73455,106 @@ juce::var AudioEngine::runRenderExportRegression(const juce::File& outputDirecto "Selected-item exports must include only the requested clip IDs; empty selections must fail.", juce::var(selectedDiagnostic)); + const auto cancelledOutput = outputDirectory.getChildFile("cancel-preserves-output.wav"); + cancelledOutput.replaceWithText("previous output must survive cancellation"); + int cancellationChecks = 0; + const auto cancelDuringBlocks = [&] { return ++cancellationChecks < 6; }; + const bool cancelled = !renderProject("stem:" + trackId, 0, fixtureDurationSeconds, + cancelledOutput.getFullPathName(), "wav", 48000, 24, 2, true, false, 0, false, {}, cancelDuringBlocks); + addCheck("cancel_during_normalization_preserves_previous_output", cancelled && cancellationChecks == 6 + && cancelledOutput.loadFileAsString() == "previous output must survive cancellation", + "Cancellation between render blocks does not publish a partial output."); + const auto afterCancel = outputDirectory.getChildFile("after-cancellation.wav"); + addCheck("render_succeeds_after_cancelled_transaction", renderProject("stem:" + trackId, 0, fixtureDurationSeconds, + afterCancel.getFullPathName(), "wav", 48000, 24, 2, false, false, 0), + "A cancelled transaction restores processing and leaves the next render usable."); + + if (trackAdded && trackMap.count(trackId)) + { + juce::PluginDescription description; + description.name = "Isolation regression probe"; description.pluginFormatName = "OpenStudioIsolationProbe"; + description.fileOrIdentifier = "openstudio-isolation-regression"; + description.numInputChannels = description.numOutputChannels = 2; + juce::String isolationError; + auto isolated = IsolatedPlugin::create(description, 48000, 512, isolationError); + auto* remote = isolated.get(); + const bool mounted = isolated && trackMap[trackId]->addTrackFX(std::move(isolated), 48000, 512); + const auto isolatedOutput = outputDirectory.getChildFile("isolated-render.wav"); + const bool rendered = mounted && renderProject("stem:" + trackId, 0, fixtureDurationSeconds, + isolatedOutput.getFullPathName(), "wav", 48000, 24, 2, false, false, 0); + juce::AudioBuffer baseline, isolatedAudio; + double baselineRate = 0, isolatedRate = 0; + bool parity = rendered && readAudioFileForParity(afterCancel, baseline, baselineRate) + && readAudioFileForParity(isolatedOutput, isolatedAudio, isolatedRate) + && baselineRate == isolatedRate && baseline.getNumSamples() == isolatedAudio.getNumSamples() + && baseline.getNumChannels() == isolatedAudio.getNumChannels(); + double difference = 0; + if (parity) for (int ch = 0; ch < baseline.getNumChannels(); ++ch) + for (int sample = 0; sample < baseline.getNumSamples(); ++sample) + difference = std::max(difference, static_cast(std::abs(baseline.getSample(ch, sample) - isolatedAudio.getSample(ch, sample)))); + addCheck("isolated_plugin_render_pdc_parity", parity && difference < 0.00001 && remote->isHealthy() && !remote->isNonRealtime(), + "A pass-through isolated child must render aligned with the unhosted baseline and restore live mode.", difference); + if (mounted) trackMap[trackId]->removeTrackFX(0); + } + const auto externalPlugin = juce::SystemStats::getEnvironmentVariable("OPENSTUDIO_FX_REGRESSION_PLUGIN", ""); + if (trackAdded && externalPlugin.isNotEmpty()) + { + // Explicit opt-in only: exercise an installed vendor without opening its editor. + auto loaded = pluginManager.loadPluginFromFile(externalPlugin, 48000, 512); + auto* instance = dynamic_cast(loaded.get()); + juce::String identifier; + if (instance != nullptr) + identifier = pluginManager.getPluginIdentifier(instance->getPluginDescription()); + bool catalogIdentity = false; + for (const auto& candidate : pluginManager.getAvailablePlugins()) + catalogIdentity = catalogIdentity || candidate.createIdentifierString() == identifier; + addCheck("external_fx_catalog_identity", instance != nullptr && catalogIdentity, + "Loaded vendor metadata resolves to its scanned class identifier.", identifier); + loaded.reset(); + const auto hasIdentity = [&] (const juce::var& chain) { + return chain.size() > 0 && chain[0].getProperty("pluginPath", "").toString() == identifier; + }; + if (catalogIdentity) + { + const bool monitorAdded = addMonitoringFX(identifier); + addCheck("external_monitor_fx_insert", monitorAdded && hasIdentity(getMonitoringFX()), + "Monitor insertion retains a catalog-resolvable identity when rebuilding the stage."); + const bool monitorAppended = monitorAdded && addMonitoringFX("OpenStudio NAM Rack"); + addCheck("external_monitor_fx_append_restores_vendor", monitorAppended && hasIdentity(getMonitoringFX()), + "Appending NAM Rack restores the existing vendor state through the same identity."); + if (monitorAppended) removeMonitoringFX(1); + if (monitorAdded) removeMonitoringFX(0); + addCheck("external_monitor_fx_remove", getMonitoringFX().size() == 0, + "Removal leaves the monitoring stage empty."); + + const bool masterAdded = addMasterFX(identifier); + addCheck("external_master_fx_insert", masterAdded && hasIdentity(getMasterFX()), + "Master insertion also uses the scanned class identity."); + const bool masterAppended = masterAdded && addMasterBuiltInFX("OpenStudio EQ"); + addCheck("external_master_fx_append_restores_vendor", masterAppended && hasIdentity(getMasterFX()), + "Appending a built-in restores the existing vendor in the master stage."); + if (masterAppended) removeMasterFX(1); + if (masterAdded) removeMasterFX(0); + + for (const bool input : { false, true }) + { + const auto add = [&] (const juce::String& id) { + return input ? addTrackInputFX(trackId, id, false) : addTrackFX(trackId, id, false); + }; + const auto remove = [&] { return input ? removeTrackInputFX(trackId, 0) : removeTrackFX(trackId, 0); }; + const bool inserted = add(identifier); + const auto chain = input ? getTrackInputFX(trackId) : getTrackFX(trackId); + const auto state = inserted ? getPluginState(trackId, 0, input) : juce::String(); + const auto savedIdentifier = chain.size() > 0 ? chain[0].getProperty("pluginPath", "").toString() : juce::String(); + if (inserted) remove(); + const bool reopened = inserted && hasIdentity(chain) && add(savedIdentifier); + const bool restored = reopened && !state.isEmpty() && setPluginState(trackId, 0, input, state); + addCheck(input ? "external_input_fx_state_round_trip" : "external_track_fx_state_round_trip", + restored, "Serialized FX identity and opaque state can reopen the same vendor class."); + if (reopened) remove(); + } + } + } if (trackAdded) removeTrack(trackId); @@ -72146,6 +74150,7 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) { juce::AudioProcessor* processor = nullptr; juce::MemoryBlock state; + bool wasNonRealtime = false; bool hasNAMTransientState = false; float namAuditionSource = 0.0f; std::uint64_t namAuditionSourceSample = 0; @@ -72157,7 +74162,8 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) return; FreezeProcessorBackup backup; backup.processor = processor; - if (auto* rack = dynamic_cast(processor)) + backup.wasNonRealtime = processor->isNonRealtime(); + if (auto* rack = dynamic_cast(processor)) { backup.hasNAMTransientState = true; backup.namAuditionSource = rack->auditionSource.load( @@ -72165,6 +74171,7 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) backup.namAuditionSourceSample = rack->auditionSourceSample; } processor->getStateInformation(backup.state); + processor->setNonRealtime(true); processorBackups.push_back(std::move(backup)); }; for (int fx = 0; fx < track->getNumInputFX(); ++fx) @@ -72184,9 +74191,12 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) // Seed NAM controls before its embedded processors are prepared, then // restore the remaining hosted state once every processor is live-sized. for (auto& backup : processorBackups) - if (auto* rack = dynamic_cast(backup.processor)) + { + backup.processor->setNonRealtime(backup.wasNonRealtime); + if (auto* rack = dynamic_cast(backup.processor)) rack->restoreRenderPassStateInformation(backup.state.getData(), static_cast(backup.state.getSize())); + } track->setPDCDelay(realtimePdcDelay); track->prepareToPlay(currentSampleRate > 0.0 ? currentSampleRate : renderRate, @@ -72195,7 +74205,7 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) { if (backup.processor == nullptr) continue; - if (auto* rack = dynamic_cast(backup.processor)) + if (auto* rack = dynamic_cast(backup.processor)) { rack->restoreRenderPassStateInformation(backup.state.getData(), static_cast(backup.state.getSize())); @@ -72204,7 +74214,7 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) backup.processor->setStateInformation(backup.state.getData(), static_cast(backup.state.getSize())); backup.processor->reset(); - if (auto* rack = dynamic_cast(backup.processor); + if (auto* rack = dynamic_cast(backup.processor); rack != nullptr && backup.hasNAMTransientState) { rack->auditionSource.store( @@ -72224,7 +74234,7 @@ juce::var AudioEngine::freezeTrack(const juce::String& trackId) track->prepareToPlay(renderRate, renderBlockSize); for (auto& backup : processorBackups) { - if (auto* rack = dynamic_cast(backup.processor)) + if (auto* rack = dynamic_cast(backup.processor)) rack->auditionSource.store(0.0f, std::memory_order_relaxed); if (backup.processor != nullptr) backup.processor->reset(); @@ -72934,7 +74944,7 @@ static bool migrateXMLNAMRackPresetToCurrent( bool persistMigration) { bool stateMigrated = false; - if (! S13NAMRack::migratePresetStateToCurrent( + if (! OpenStudioNAMRack::migratePresetStateToCurrent( stateData, stateMigrated)) { return false; @@ -72962,7 +74972,7 @@ bool AudioEngine::isNAMRackPlugin(const juce::String& trackId, int fxIndex, bool ? trackIt->second->getInputFXProcessorShared(fxIndex) : trackIt->second->getTrackFXProcessorShared(fxIndex); } - return dynamic_cast(processorOwner.get()) != nullptr; + return dynamic_cast(processorOwner.get()) != nullptr; } juce::var AudioEngine::getPluginPresets(const juce::String& trackId, int fxIndex, bool isInputFX) @@ -72977,7 +74987,6 @@ juce::var AudioEngine::getPluginPresets(const juce::String& trackId, int fxIndex if (presetsDir.isDirectory()) { auto files = presetsDir.findChildFiles(juce::File::findFiles, true, "*.ospreset"); - files.addArray(presetsDir.findChildFiles(juce::File::findFiles, true, "*.s13preset")); for (const auto& file : files) { // The legacy generic preset browser can still encounter XML-wrapped @@ -72990,13 +74999,13 @@ juce::var AudioEngine::getPluginPresets(const juce::String& trackId, int fxIndex const auto encodedState = xml->getStringAttribute("state"); if (encodedState.isNotEmpty() - && stateData.fromBase64Encoding(encodedState)) + && PluginStateValidation::decode(encodedState, stateData)) { const auto tree = juce::ValueTree::readFromData( stateData.getData(), stateData.getSize()); if (tree.isValid() && tree.getType() - == juce::Identifier("S13NAMRack")) + == juce::Identifier("OpenStudioNAMRack")) { (void) migrateXMLNAMRackPresetToCurrent( file, *xml, stateData, true); @@ -73041,10 +75050,10 @@ bool AudioEngine::loadPluginPreset(const juce::String& trackId, int fxIndex, boo } } - if (auto* rack = dynamic_cast(processorOwner.get())) + if (auto* rack = dynamic_cast(processorOwner.get())) { juce::MemoryBlock stateData; - if (! stateData.fromBase64Encoding(base64State)) + if (! PluginStateValidation::decode(base64State, stateData)) return false; if (! migrateXMLNAMRackPresetToCurrent( @@ -73090,7 +75099,7 @@ bool AudioEngine::savePluginPreset(const juce::String& trackId, int fxIndex, boo } } - if (auto* rack = dynamic_cast(processorOwner.get())) + if (auto* rack = dynamic_cast(processorOwner.get())) { juce::MemoryBlock stateData; rack->getTonePresetStateInformation(stateData); @@ -73104,7 +75113,7 @@ bool AudioEngine::savePluginPreset(const juce::String& trackId, int fxIndex, boo return false; // Create XML structure - juce::XmlElement xml("S13Preset"); + juce::XmlElement xml("OpenStudioPreset"); xml.setAttribute("name", presetName); xml.setAttribute("state", base64State); xml.setAttribute("version", "1.0"); @@ -73176,7 +75185,7 @@ bool AudioEngine::storePluginABState(const juce::String& trackId, int fxIndex, b } } - if (auto* rack = dynamic_cast(processorOwner.get())) + if (auto* rack = dynamic_cast(processorOwner.get())) { juce::MemoryBlock stateData; rack->getTonePresetStateInformation(stateData); @@ -73239,10 +75248,10 @@ bool AudioEngine::loadPluginABState(const juce::String& trackId, int fxIndex, bo bool ok = false; - if (auto* rack = dynamic_cast(processorOwner.get())) + if (auto* rack = dynamic_cast(processorOwner.get())) { juce::MemoryBlock stateData; - if (! stateData.fromBase64Encoding(storedState)) + if (! PluginStateValidation::decode(storedState, stateData)) return false; const int latencyBefore = rack->getLatencySamples(); @@ -73432,13 +75441,13 @@ bool AudioEngine::setBuiltInFXOversampling(const juce::String& trackId, int fxIn processor->getCallbackLock()); const int latencyBefore = processor->getLatencySamples(); if (auto* builtIn = - dynamic_cast(processor)) + dynamic_cast(processor)) { builtIn->setOversamplingEnabled(enabled); applied = true; } else if (auto* saturator = - dynamic_cast(processor)) + dynamic_cast(processor)) { if (enabled && saturator->oversampleMode.load( @@ -73517,7 +75526,7 @@ juce::var AudioEngine::getPitchCorrectorData(const juce::String& trackId, int fx if (it == trackMap.end() || !it->second) return juce::var(); juce::AudioProcessor* proc = (fxIndex >= 0 && fxIndex < it->second->getNumTrackFX()) ? it->second->getTrackFXProcessor(fxIndex) : nullptr; - auto* pc = dynamic_cast(proc); + auto* pc = dynamic_cast(proc); if (!pc) return juce::var(); auto data = pc->getCurrentPitchData(); @@ -73559,7 +75568,7 @@ void AudioEngine::setPitchCorrectorParam(const juce::String& trackId, int fxInde if (it == trackMap.end() || !it->second) return; juce::AudioProcessor* proc = (fxIndex >= 0 && fxIndex < it->second->getNumTrackFX()) ? it->second->getTrackFXProcessor(fxIndex) : nullptr; - auto* pc = dynamic_cast(proc); + auto* pc = dynamic_cast(proc); if (!pc) return; auto& mapper = pc->getMapper(); @@ -73590,7 +75599,7 @@ juce::var AudioEngine::getPitchHistory(const juce::String& trackId, int fxIndex, if (it == trackMap.end() || !it->second) return juce::Array(); juce::AudioProcessor* proc = (fxIndex >= 0 && fxIndex < it->second->getNumTrackFX()) ? it->second->getTrackFXProcessor(fxIndex) : nullptr; - auto* pc = dynamic_cast(proc); + auto* pc = dynamic_cast(proc); if (!pc) return juce::Array(); auto frames = pc->getPitchHistory(numFrames); @@ -75524,7 +77533,7 @@ juce::var AudioEngine::getPitchPreviewRoutingStatus(const juce::String& clipId) bool AudioEngine::isPolyphonicDetectionAvailable() const { -#if S13_HAS_ONNXRUNTIME +#if OPENSTUDIO_HAS_ONNXRUNTIME return true; #else return false; @@ -76062,13 +78071,13 @@ juce::var AudioEngine::startAIGeneration(const juce::String& trackId, auto aiToolsStatus = stemSeparator.getAiToolsStatus(); if (auto* statusObject = aiToolsStatus.getDynamicObject()) { - if (modelId == "stable-audio-3-medium") + if (modelId == "stable-audio-3-medium" || modelId == "minimax-music-3") { auto stableReady = false; - juce::String stableMessage = "Stable Audio 3 Medium is not set up yet."; + juce::String stableMessage = modelId + " is not set up yet."; if (auto* musicModels = statusObject->getProperty("musicModels").getDynamicObject()) { - if (auto* stableModel = musicModels->getProperty("stable-audio-3-medium").getDynamicObject()) + if (auto* stableModel = musicModels->getProperty(modelId).getDynamicObject()) { stableReady = static_cast(stableModel->getProperty("ready")); stableMessage = stableModel->getProperty("message").toString(); @@ -76080,7 +78089,7 @@ juce::var AudioEngine::startAIGeneration(const juce::String& trackId, result->setProperty("started", false); result->setProperty("error", stableMessage.isNotEmpty() ? stableMessage - : "Import Stable Audio 3 Medium from AI Tools Setup before generating."); + : "Import the selected Diffusers model from AI Tools Setup before generating."); return juce::var(result.release()); } } @@ -76130,9 +78139,9 @@ juce::var AudioEngine::startAIGeneration(const juce::String& trackId, } } - auto outputDir = juce::File::getSpecialLocation(juce::File::tempDirectory) + auto outputDir = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) .getChildFile("OpenStudio") - .getChildFile("generated-music"); + .getChildFile("GeneratedAudio"); if (! aiTrackEngine.startGeneration(modelId, workflowId, paramsJSON, outputDir)) { @@ -76145,6 +78154,7 @@ juce::var AudioEngine::startAIGeneration(const juce::String& trackId, } result->setProperty("started", true); + result->setProperty("requestId", aiTrackEngine.pollProgress().requestId); return juce::var(result.release()); } diff --git a/Source/AudioEngine.h b/Source/AudioEngine.h index 6ee858f..94342a0 100644 --- a/Source/AudioEngine.h +++ b/Source/AudioEngine.h @@ -2,6 +2,7 @@ #include #include "TrackProcessor.h" +#include "MessageThreadLifetime.h" #include "AudioRecorder.h" #include "MIDIRecorder.h" #include "PlaybackEngine.h" @@ -34,13 +35,13 @@ class AudioEngine : public juce::AudioIODeviceCallback, public ControlSurfaceCallback, public juce::Timer { - static constexpr int maxRealtimeTracks = 64; struct RealtimeTrackEntry; struct ActiveFXStage; public: AudioEngine(); ~AudioEngine() override; + void reportStageProcessorFaults(); // Control thread; only called after fault/reset generation changes. void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels, @@ -95,6 +96,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, // Track control (Phase 1) - ID-based void setTrackRecordArm(const juce::String& trackId, bool armed); + void setTrackInputMonitoringAsync(const juce::String& trackId, bool enabled, std::function completion); void setTrackInputMonitoring(const juce::String& trackId, bool enabled); void setTrackInputChannels(const juce::String& trackId, int startChannel, int numChannels); bool setNAMTunerActive(const juce::String& trackId, @@ -109,7 +111,11 @@ class AudioEngine : public juce::AudioIODeviceCallback, // Transport control (Phase 2) void setTransportPlaying(bool playing); + void setTransportRecordingAsync(bool recording, std::function completion); void setTransportRecording(bool recording); + bool recordingRequiresAudioInput() const; + bool trackRequiresAudioInput(const juce::String& trackId) const; + void requestAudioInputAccess(std::function completion); bool isTransportPlaying() const { return isPlaying; } bool isTransportRecording() const { return isRecordMode; } void setLoopMode(bool loop) @@ -122,11 +128,12 @@ class AudioEngine : public juce::AudioIODeviceCallback, } double getTransportPosition() const { - return currentSampleRate > 0.0 + const auto rate = transportSampleRate.load(std::memory_order_acquire); + return rate > 0.0 ? static_cast( currentSamplePosition.load( std::memory_order_acquire)) - / currentSampleRate + / rate : 0.0; } void setTransportPosition(double seconds); @@ -149,6 +156,8 @@ class AudioEngine : public juce::AudioIODeviceCallback, void setMetronomeVolume(float volume); void setMetronomeAccentBeats(const std::vector& accents); bool isMetronomeEnabled() const; + bool setMetronomePracticeEnabled(bool enabled); + bool isMetronomePracticeEnabled() const { return metronome.isPracticeEnabled(); } void setTimeSignature(int numerator, int denominator); void getTimeSignature(int& numerator, int& denominator) const; @@ -162,6 +171,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, // Get clips that were completed in the last recording session std::vector getLastCompletedClips(); + juce::var takeRecordingWriteFailures() { return audioRecorder.takeWriteFailures(); } // Get MIDI clips that were completed in the last recording session std::vector getLastCompletedMIDIClips(); @@ -189,6 +199,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, // FX Management (Phase 3) - ID-based juce::var scanForPlugins(bool forceRescan = false); + bool setIsolatedPluginHosting(const juce::String& identifier, bool enabled) { return pluginManager.setIsolatedHosting(identifier, enabled); } juce::var getPluginScanConfiguration() const; bool addPluginScanPath(const juce::String& path); bool removePluginScanPath(const juce::String& path); @@ -202,13 +213,13 @@ class AudioEngine : public juce::AudioIODeviceCallback, bool addMasterBuiltInFX(const juce::String& effectName); juce::var getAvailableBuiltInFX(); - // S13FX (JSFX) Management - bool addTrackS13FX(const juce::String& trackId, const juce::String& scriptPath, bool isInputFX = false); - bool addMasterS13FX(const juce::String& scriptPath); - juce::var getS13FXSliders(const juce::String& trackId, int fxIndex, bool isInputFX); - bool setS13FXSlider(const juce::String& trackId, int fxIndex, bool isInputFX, int sliderIndex, double value); - bool reloadS13FX(const juce::String& trackId, int fxIndex, bool isInputFX); - juce::var getAvailableS13FX(); + // JSFX (JSFX) Management + bool addTrackJSFX(const juce::String& trackId, const juce::String& scriptPath, bool isInputFX = false); + bool addMasterJSFX(const juce::String& scriptPath); + juce::var getJSFXSliders(const juce::String& trackId, int fxIndex, bool isInputFX); + bool setJSFXSlider(const juce::String& trackId, int fxIndex, bool isInputFX, int sliderIndex, double value); + bool reloadJSFX(const juce::String& trackId, int fxIndex, bool isInputFX); + juce::var getAvailableJSFX(); // Built-in FX Preset System juce::var getBuiltInFXPresets(const juce::String& pluginName); @@ -232,8 +243,16 @@ class AudioEngine : public juce::AudioIODeviceCallback, void openInstrumentEditor(const juce::String& trackId); void closePluginEditor(const juce::String& trackId, int fxIndex, bool isInputFX); void closeAllPluginWindows(); + // Message-thread notification after successful removal, outside audio locks. + // Index-addressed browser editors at/after this slot no longer have a valid target. + std::function + onFXSlotsRemoved; void setPluginWindowOwnerComponent(juce::Component* component); void setPluginWindowShortcutForwardCallback(PluginWindowManager::ShortcutForwardCallback callback); + bool isPluginEditorVisible(const PluginWindowManager::PluginEditorTarget& target) const + { + return pluginWindowManager.isEditorVisible(target); + } // MIDI Device Management (Phase 2) juce::var getMIDIInputDevices(); @@ -361,7 +380,8 @@ class AudioEngine : public juce::AudioIODeviceCallback, double renderSampleRate, int bitDepth, int numChannels, bool normalize, bool addTail, double tailLengthMs, bool includeMetronome = false, - const juce::StringArray& includedClipIds = {}); + const juce::StringArray& includedClipIds = {}, + const std::function& keepRunning = {}); juce::var capturePitchAuditionPlayback(const juce::String& trackId, const juce::String& clipId, double startTime, @@ -430,7 +450,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, juce::String getTrackMIDIOutput(const juce::String& trackId) const; juce::var getTrackRoutingInfo(const juce::String& trackId); - // Lua Scripting (S13Script) + // Lua Scripting (OpenStudioScript) juce::var runScript(const juce::String& scriptPath); juce::var runScriptCode(const juce::String& luaCode); juce::String getScriptDirectory(); @@ -715,6 +735,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, bool forceFloat = false; bool supportsDouble = false; std::shared_ptr processor; + std::shared_ptr safety = std::make_shared(); std::shared_ptr bypassDelay; }; @@ -782,7 +803,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, struct RealtimeTrackSnapshot { std::vector tracks; - std::array processingOrder {}; + std::vector processingOrder; // Sized during control-thread publication, never in the callback. int processingOrderCount = 0; }; struct PublishedBuiltInProcessorOwner @@ -848,8 +869,12 @@ class AudioEngine : public juce::AudioIODeviceCallback, juce::File findFFmpegExe() const; bool convertWithFFmpeg(const juce::File& inputFile, const juce::File& outputFile, const juce::String& format, double targetSampleRate, int quality, - int bitDepth, int numChannels) const; + int bitDepth, int numChannels, + const std::function& keepRunning) const; // Device settings persistence + uint64 recordingRequestGeneration = 0; + std::map monitorRequestGenerations; + std::unique_ptr inputPermissionSavedSettings; void saveDeviceSettings(); void loadDeviceSettings(); void loadDeviceSettingsWithChannelCounts(int inputChannels, int outputChannels); @@ -940,11 +965,13 @@ class AudioEngine : public juce::AudioIODeviceCallback, // seek cannot tear a double or be overwritten by an in-flight callback. std::atomic currentSamplePosition { 0 }; double currentSampleRate = 44100.0; + std::atomic transportSampleRate { 44100.0 }; int currentBlockSize = 512; // Device buffer size for re-preparing plugins after render std::atomic namRackOversamplingFactor { 4 }; int inputLatencySamples = 0; // Device input latency for recording compensation std::atomic lastAudioBlockWallTimeMs { 0.0 }; std::atomic lastAudioBlockDurationMs { 0.0 }; + std::atomic lastAudioBlockSamples { 0 }; std::atomic lastAudioCallbackProcessMs { 0.0 }; std::atomic maxAudioCallbackProcessMs { 0.0 }; std::atomic audioCallbackCounter { 0 }; @@ -1158,6 +1185,7 @@ class AudioEngine : public juce::AudioIODeviceCallback, juce::AudioBuffer reusableStageFXDryBufferDouble; juce::AudioBuffer reusablePitchScrubBuffer; juce::MidiBuffer reusableRealtimeMidiBuffer; + juce::MidiBuffer reusableMasterMidiBuffer, reusableMonitoringMidiBuffer; float stageFXBypassRampStep = 1.0f / 882.0f; int stageFXContinuityRampSamples = 353; std::array stemFileCache; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioEngine) + MessageThreadLifetime deferredCallbacks; }; diff --git a/Source/AudioFileConversion.cpp b/Source/AudioFileConversion.cpp new file mode 100644 index 0000000..f8383d0 --- /dev/null +++ b/Source/AudioFileConversion.cpp @@ -0,0 +1,109 @@ +#include "AudioFileConversion.h" +#include "FFmpegLocator.h" +#include "OwnedChildProcess.h" +#include + +juce::Result AudioFileConversion::convert(const juce::File& source, const juce::File& destination, + const Options& options, const std::function& keepRunning) +{ + const auto cancelled = [&] { return keepRunning && !keepRunning(); }; + if (cancelled()) return juce::Result::fail("Conversion cancelled"); + if (!source.existsAsFile() || !destination.getParentDirectory().isDirectory() || destination.isDirectory()) + return juce::Result::fail("Invalid source or destination"); + const auto executable = OpenStudioFFmpeg::findExecutable(); + if (!executable.existsAsFile()) return juce::Result::fail("The bundled FFmpeg runtime is unavailable"); + + juce::AudioFormatManager formats; + formats.registerBasicFormats(); + std::unique_ptr reader(formats.createReaderFor(source)); + if (!reader || !std::isfinite(reader->sampleRate) || reader->sampleRate < 8000 || reader->sampleRate > 384000 + || reader->numChannels == 0 || reader->numChannels > 64 || reader->lengthInSamples <= 0) + return juce::Result::fail("Unsupported or empty source audio"); + const auto sourceRate = reader->sampleRate; + const auto sourceLength = reader->lengthInSamples; + const int sourceChannels = static_cast(reader->numChannels); + const int rate = options.sampleRate == 0 ? juce::roundToInt(sourceRate) : options.sampleRate; + const int channels = options.channels == 0 ? sourceChannels : options.channels; + const int bits = options.bitDepth == 0 ? static_cast(reader->bitsPerSample) : options.bitDepth; + if (rate < 8000 || rate > 384000 || channels < 1 || channels > 64 + || (bits != 16 && bits != 24 && bits != 32) || options.timeoutMs <= 0 + || (options.format != "wav" && options.format != "aiff" && options.format != "flac") + || (options.format == "flac" && bits == 32)) + return juce::Result::fail("Unsupported output settings (FLAC supports 16 or 24 bit)"); + // Downmixing a surround layout without explicit speaker weights is ambiguous. + // Preserve multichannel files, but limit channel conversion to mono/stereo. + if (channels != sourceChannels && (channels > 2 || sourceChannels > 2)) + return juce::Result::fail("Channel conversion supports mono and stereo only"); + const auto expectedLengthDouble = static_cast(sourceLength) * rate / sourceRate; + if (!std::isfinite(expectedLengthDouble) || expectedLengthDouble > 1.0e12) + return juce::Result::fail("Source duration exceeds the conversion limit"); + const auto expectedLength = static_cast(std::llround(expectedLengthDouble)); + if (expectedLength < 1) return juce::Result::fail("Source is shorter than one output sample"); + reader.reset(); // Allow explicit in-place conversion, but only publish after success. + + juce::TemporaryFile staged(destination, juce::TemporaryFile::useHiddenFile); + juce::String codec; + if (options.format == "flac") codec = "flac"; + else if (options.format == "wav") codec = bits == 32 ? "pcm_f32le" : "pcm_s" + juce::String(bits) + "le"; + else codec = bits == 32 ? "pcm_f32be" : "pcm_s" + juce::String(bits) + "be"; + juce::String filter; + if (sourceChannels == 2 && channels == 1) filter = "pan=mono|c0=0.5*c0+0.5*c1,"; + if (sourceChannels == 1 && channels == 2) filter = "pan=stereo|c0=c0|c1=c0,"; + // Supply the filter's zero tail explicitly, including for files shorter than + // one filter window, then trim to the duration-derived sample count. + filter += "apad=pad_len=4096,aresample=" + juce::String(rate) + + ":resampler=swr:filter_size=64:phase_shift=10:cutoff=0.97,atrim=end_sample=" + juce::String(expectedLength); + juce::StringArray arguments { executable.getFullPathName(), "-hide_banner", "-nostdin", "-v", "error", + "-xerror", "-y", "-threads", "1", "-i", source.getFullPathName(), "-map", "0:a:0", "-vn", "-sn", "-dn", + "-map_metadata", "-1", "-filter_threads", "1", "-af", filter, "-ac", juce::String(channels), + "-ar", juce::String(rate), "-c:a", codec, "-threads", "1" }; + if (options.format == "flac") + { + arguments.addArray({ "-sample_fmt", bits == 16 ? "s16" : "s32", "-bits_per_raw_sample", juce::String(bits) }); + } + if (options.format == "wav") arguments.addArray({ "-rf64", "auto" }); + arguments.addArray({ "-f", options.format, staged.getFile().getFullPathName() }); + OwnedChildProcess process; + if (cancelled() || !process.start(arguments)) return juce::Result::fail("Could not start audio conversion"); + const auto started = juce::Time::getMillisecondCounterHiRes(); + juce::MemoryOutputStream diagnostic; + bool interrupted = false; + while (process.isRunning()) + { + if (cancelled() || juce::Time::getMillisecondCounterHiRes() - started >= options.timeoutMs) + { + interrupted = true; + process.kill(); + break; + } + char bytes[4096]; + for (int block = 0; block < 4; ++block) + { + const auto count = process.readProcessOutput(bytes, sizeof(bytes)); + if (count <= 0) break; + if (diagnostic.getDataSize() < 65536) diagnostic.write(bytes, static_cast(count)); + } + juce::Thread::sleep(10); + } + // A terminated process must relinquish the staged file before cleanup. + const std::atomic waitForExit { true }; + if (!process.waitForProcessToFinish(2000, waitForExit) || interrupted || cancelled()) + return juce::Result::fail("Conversion cancelled or timed out; original files were preserved"); + if (process.getExitCode() != 0) + return juce::Result::fail("Audio conversion failed: " + diagnostic.toString().substring(0, 2048)); + + std::unique_ptr converted(formats.createReaderFor(staged.getFile())); + if (!converted || converted->sampleRate != rate || static_cast(converted->numChannels) != channels + || std::abs(converted->lengthInSamples - expectedLength) > 1) + return juce::Result::fail("Converted audio failed duration/format validation; original files were preserved"); + converted.reset(); + // Observe the OS flush result before publishing. FFmpeg has closed/finalized it. + auto publicationStream = staged.getFile().createOutputStream(); + if (!publicationStream) return juce::Result::fail("Could not verify the converted file"); + publicationStream->flush(); + const bool flushed = publicationStream->getStatus().wasOk(); + publicationStream.reset(); + if (!flushed || cancelled() || !staged.overwriteTargetFileWithTemporary()) + return juce::Result::fail("Could not publish converted audio; the original destination was preserved"); + return juce::Result::ok(); +} diff --git a/Source/AudioFileConversion.h b/Source/AudioFileConversion.h new file mode 100644 index 0000000..97e13dd --- /dev/null +++ b/Source/AudioFileConversion.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include + +// Offline file work only. The shipped FFmpeg worker performs band-limited SRC; +// it never changes a file's rate by merely relabelling its original samples. +namespace AudioFileConversion +{ +struct Options +{ + juce::String format = "wav"; + int sampleRate = 0, bitDepth = 0, channels = 0; // 0 preserves source. + int timeoutMs = 30 * 60 * 1000; +}; +juce::Result convert(const juce::File& source, const juce::File& destination, + const Options&, const std::function& keepRunning); +} diff --git a/Source/AudioFileConversionRegression.cpp b/Source/AudioFileConversionRegression.cpp new file mode 100644 index 0000000..3e79612 --- /dev/null +++ b/Source/AudioFileConversionRegression.cpp @@ -0,0 +1,190 @@ +#include "AudioFileConversionRegression.h" +#include "AudioFileConversion.h" +#include "OwnedChildProcess.h" +#include + +namespace +{ +bool writeFixture(const juce::File& path, int rate, int channels, int length, double frequency) +{ + juce::WavAudioFormat format; + std::unique_ptr stream = path.createOutputStream(); + auto writer = format.createWriterFor(stream, juce::AudioFormatWriterOptions() + .withSampleRate(rate).withNumChannels(channels).withBitsPerSample(24)); + if (!writer) return false; + juce::AudioBuffer samples(channels, length); + for (int channel = 0; channel < channels; ++channel) + for (int sample = 0; sample < length; ++sample) + samples.setSample(channel, sample, static_cast((channel == 0 ? 0.25 : -0.125) + * std::sin(juce::MathConstants::twoPi * frequency * sample / rate))); + return writer->writeFromAudioSampleBuffer(samples, 0, length) && writer->flush(); +} +std::unique_ptr openFixture(const juce::File& file) +{ + juce::AudioFormatManager formats; + formats.registerBasicFormats(); + return std::unique_ptr(formats.createReaderFor(file)); +} +} + +void runAudioFileConversionRegression(const juce::File& root, const std::function& check) +{ + #if !JUCE_WINDOWS + // Exercise the actual POSIX pipe on a quiet long-lived child; FFmpeg normally + // writes no diagnostics on success. Neither the read nor timeout may wait for EOF. + { + OwnedChildProcess quiet; + const bool started = quiet.start({ "/bin/sh", "-c", "sleep 30" }); + char bytes[4096]; + const auto before = juce::Time::getMillisecondCounterHiRes(); + const auto count = quiet.readProcessOutput(bytes, sizeof(bytes)); + check("posix_quiet_child_read_is_nonblocking", started && count == 0 + && juce::Time::getMillisecondCounterHiRes() - before < 100.0); + std::atomic keepRunning { true }; + check("posix_quiet_child_timeout_terminates_group", !quiet.waitForProcessToFinish(50, keepRunning) + && juce::Time::getMillisecondCounterHiRes() - before < 2000.0); + } + { + OwnedChildProcess quiet; + const bool started = quiet.start({ "/bin/sh", "-c", "sleep 30" }); + std::atomic keepRunning { false }; + const auto before = juce::Time::getMillisecondCounterHiRes(); + check("posix_quiet_child_cancellation_is_bounded", started && !quiet.waitForProcessToFinish(30000, keepRunning) + && juce::Time::getMillisecondCounterHiRes() - before < 2000.0); + } + #endif + const auto directory = root.getChildFile("conversion"); + directory.createDirectory(); + const auto alive = [] { return true; }; + AudioFileConversion::Options options; + options.bitDepth = 24; + bool ratesPass = true, pitchPass = true; + for (const int sourceRate : { 44100, 48000, 96000 }) + { + const auto source = directory.getChildFile("source-" + juce::String(sourceRate) + ".wav"); + ratesPass = writeFixture(source, sourceRate, 1, sourceRate / 5, 1000) && ratesPass; + for (const int outputRate : { 44100, 48000, 96000 }) + { + options.sampleRate = outputRate; + const auto output = directory.getChildFile(juce::String(sourceRate) + "-" + juce::String(outputRate) + ".wav"); + const auto result = AudioFileConversion::convert(source, output, options, alive); + auto reader = openFixture(output); + ratesPass = result.wasOk() && reader && reader->sampleRate == outputRate + && reader->lengthInSamples == outputRate / 5 && ratesPass; + if (!reader) { pitchPass = false; continue; } + juce::AudioBuffer samples(1, static_cast(reader->lengthInSamples)); + pitchPass = reader->read(&samples, 0, samples.getNumSamples(), 0, true, false) && pitchPass; + double error = 0; + for (int i = 256; i < samples.getNumSamples() - 256; ++i) + { + const auto expected = 0.25 * std::sin(juce::MathConstants::twoPi * 1000 * i / outputRate); + error = juce::jmax(error, std::abs(expected - samples.getSample(0, i))); + } + pitchPass = error < 0.0001 && pitchPass; + } + } + check("converter_all_44100_48000_96000_rate_pairs_preserve_duration", ratesPass); + check("converter_rate_changes_preserve_1khz_frequency_and_amplitude", pitchPass); + const auto stereo = directory.getChildFile("stereo.wav"); + check("converter_stereo_fixture_written", writeFixture(stereo, 48000, 2, 9600, 1000)); + options.sampleRate = 48000; + const auto same = directory.getChildFile("same.wav"); + bool parity = AudioFileConversion::convert(stereo, same, options, alive).wasOk(); + auto original = openFixture(stereo); + auto copied = openFixture(same); + if (original && copied) + { + juce::AudioBuffer a(2, 9600), b(2, 9600); + parity = original->read(&a, 0, 9600, 0, true, true) && copied->read(&b, 0, 9600, 0, true, true) && parity; + for (int channel = 0; channel < 2; ++channel) + for (int sample = 0; sample < 9600; ++sample) + parity = a.getSample(channel, sample) == b.getSample(channel, sample) && parity; + } + else parity = false; + check("converter_same_rate_same_depth_sample_parity", parity); + options.channels = 1; + const auto mono = directory.getChildFile("mono.wav"); + bool monoPass = AudioFileConversion::convert(stereo, mono, options, alive).wasOk(); + auto downmix = openFixture(mono); + if (downmix) + { + juce::AudioBuffer samples(1, 9600); + monoPass = downmix->numChannels == 1 && downmix->read(&samples, 0, 9600, 0, true, false) && monoPass; + for (int sample = 0; sample < 9600; ++sample) + monoPass = std::abs(samples.getSample(0, sample) - 0.0625 + * std::sin(juce::MathConstants::twoPi * 1000 * sample / 48000)) < 0.000001 && monoPass; + } + else monoPass = false; + check("converter_stereo_to_mono_uses_explicit_average", monoPass); + options.channels = 2; + const auto duplicated = directory.getChildFile("duplicated.wav"); + bool stereoPass = AudioFileConversion::convert(mono, duplicated, options, alive).wasOk(); + auto upmix = openFixture(duplicated); + if (upmix) + { + juce::AudioBuffer samples(2, 9600); + stereoPass = upmix->numChannels == 2 && upmix->read(&samples, 0, 9600, 0, true, true) && stereoPass; + for (int sample = 0; sample < 9600; ++sample) + stereoPass = samples.getSample(0, sample) == samples.getSample(1, sample) && stereoPass; + } + else stereoPass = false; + check("converter_mono_to_stereo_duplicates_without_gain_change", stereoPass); + options.channels = 0; + const auto high = directory.getChildFile("above-nyquist.wav"); + writeFixture(high, 96000, 1, 19200, 30000); + options.sampleRate = 44100; + const auto filtered = directory.getChildFile("filtered.wav"); + bool aliasPass = AudioFileConversion::convert(high, filtered, options, alive).wasOk(); + auto filteredReader = openFixture(filtered); + if (filteredReader) + { + juce::AudioBuffer samples(1, 8820); + aliasPass = filteredReader->read(&samples, 0, 8820, 0, true, false) + && samples.getRMSLevel(0, 256, 8308) < 0.0001f && aliasPass; + } + else aliasPass = false; + check("converter_downsampling_rejects_above_nyquist_signal", aliasPass); + bool shortPass = true; + for (const int length : { 1, 2, 17, 63 }) + { + const auto source = directory.getChildFile("short-" + juce::String(length) + ".wav"); + const auto output = directory.getChildFile("short-out-" + juce::String(length) + ".wav"); + writeFixture(source, 48000, 1, length, 1000); + shortPass = AudioFileConversion::convert(source, output, options, alive).wasOk() && shortPass; + auto reader = openFixture(output); + shortPass = reader && reader->lengthInSamples == std::llround(length * 44100.0 / 48000) && shortPass; + } + check("converter_sub_filter_window_files_have_exact_length", shortPass); + bool formatsPass = true; + for (const auto* format : { "wav", "aiff", "flac" }) + { + options.format = format; + const auto output = directory.getChildFile(juce::String("format.") + format); + formatsPass = AudioFileConversion::convert(stereo, output, options, alive).wasOk() && formatsPass; + } + check("converter_wav_aiff_flac_are_finalized_and_readable", formatsPass); + options.format = "wav"; + const auto protectedOutput = directory.getChildFile("preserve.wav"); + protectedOutput.replaceWithText("existing destination"); + check("converter_pre_cancel_preserves_existing_destination", + AudioFileConversion::convert(stereo, protectedOutput, options, [] { return false; }).failed() + && protectedOutput.loadFileAsString() == "existing destination"); + int cancellationChecks = 0; + check("converter_running_child_cancel_preserves_existing_destination", + AudioFileConversion::convert(stereo, protectedOutput, options, [&] { return ++cancellationChecks < 3; }).failed() + && protectedOutput.loadFileAsString() == "existing destination"); + options.timeoutMs = 1; + check("converter_timeout_preserves_existing_destination", + AudioFileConversion::convert(stereo, protectedOutput, options, alive).failed() + && protectedOutput.loadFileAsString() == "existing destination"); + options.timeoutMs = 30000; + options.sampleRate = -1; + check("converter_invalid_rate_rejected_before_publication", + AudioFileConversion::convert(stereo, protectedOutput, options, alive).failed() + && protectedOutput.loadFileAsString() == "existing destination"); + options.sampleRate = 48000; + check("converter_unwritable_destination_is_reported", + AudioFileConversion::convert(stereo, directory, options, alive).failed()); + check("converter_invalid_input_preserves_destination", + AudioFileConversion::convert(protectedOutput, same, options, alive).failed() && openFixture(same) != nullptr); +} diff --git a/Source/AudioFileConversionRegression.h b/Source/AudioFileConversionRegression.h new file mode 100644 index 0000000..51e1b1a --- /dev/null +++ b/Source/AudioFileConversionRegression.h @@ -0,0 +1,4 @@ +#pragma once +#include +#include +void runAudioFileConversionRegression(const juce::File&, const std::function&); diff --git a/Source/AudioInputPolicy.h b/Source/AudioInputPolicy.h new file mode 100644 index 0000000..61e3839 --- /dev/null +++ b/Source/AudioInputPolicy.h @@ -0,0 +1,29 @@ +#pragma once +#include + +namespace AudioInputPolicy +{ +// JUCE honours explicit XML masks even when initialise() requests zero inputs. +// Sanitize a copy BEFORE opening the device, preserving the original preference. +inline void suppressInput(juce::XmlElement& setup) +{ + if (setup.hasAttribute("audioDeviceName")) + { + setup.setAttribute("audioOutputDeviceName", setup.getStringAttribute("audioDeviceName")); + setup.removeAttribute("audioDeviceName"); + } + setup.setAttribute("audioInputDeviceName", ""); + setup.setAttribute("audioDeviceInChans", "0"); +} + +inline void preserveInputPreference(juce::XmlElement& effective, const juce::XmlElement& desired) +{ + if (effective.getStringAttribute("deviceType") != desired.getStringAttribute("deviceType")) return; + effective.setAttribute("audioInputDeviceName", desired.getStringAttribute("audioInputDeviceName", + desired.getStringAttribute("audioDeviceName"))); + if (desired.hasAttribute("audioDeviceInChans")) + effective.setAttribute("audioDeviceInChans", desired.getStringAttribute("audioDeviceInChans")); + else + effective.removeAttribute("audioDeviceInChans"); +} +} diff --git a/Source/AudioRecorder.cpp b/Source/AudioRecorder.cpp index b0b6ae6..10375ca 100644 --- a/Source/AudioRecorder.cpp +++ b/Source/AudioRecorder.cpp @@ -1,4 +1,5 @@ #include "AudioRecorder.h" +#include "CrashDiagnostics.h" #include @@ -31,7 +32,7 @@ static float peakFromBuffer(const juce::AudioBuffer& buffer, int numSampl #endif } -AudioRecorder::AudioRecorder() +AudioRecorder::AudioRecorder(const juce::File& recoveryRoot) : recoveryJournal(recoveryRoot) { // Start the background I/O thread for ThreadedWriter writerThread.startThread(juce::Thread::Priority::normal); @@ -40,7 +41,8 @@ AudioRecorder::AudioRecorder() AudioRecorder::~AudioRecorder() { stopAllRecordings(44100.0); - writerThread.stopThread(2000); + writerThread.stopThread(-1); // All clients drained above; never forcibly kill an allocator/disk thread. + recoveryJournal.markClean(); } bool AudioRecorder::startRecording(const juce::String& trackId, const juce::File& file, double sampleRate, int numChannels) @@ -71,11 +73,20 @@ bool AudioRecorder::startRecordingInternal(const juce::String& trackId, double initialStartTime, CompletedRecording* replacedTake) { + // Reject before opening a file or touching an existing take. A corrupt + // device/configuration report must not become an enormous peak allocation. + if (trackId.isEmpty() || !std::isfinite(sampleRate) + || sampleRate < 8000.0 || sampleRate > 384000.0 + || numChannels < 1 || numChannels > 64 + || !std::isfinite(initialStartTime)) + return false; OPENSTUDIO_LOG_AUDIO_RECORD("startRecording track=" + trackId + " file=" + file.getFullPathName() + " sampleRate=" + juce::String(sampleRate, 2) + " channels=" + juce::String(numChannels)); std::unique_ptr previousWriter; + std::shared_ptr previousWriteStatus; + double previousSampleRate = sampleRate; // Construct the replacement completely before unpublishing any current // take. During loop preparation the callback can continue writing the old // writer, so file creation cannot open a no-writer gap. @@ -87,6 +98,9 @@ bool AudioRecorder::startRecordingInternal(const juce::String& trackId, parentDir.createDirectory(); } + // Recording destinations are unique takes, never append to or overwrite an + // existing user's file when a caller accidentally reuses a path. + if (file.exists()) return false; // Create WAV file writer std::unique_ptr fileOutputStream = std::make_unique(file); if (static_cast(*fileOutputStream).failedToOpen()) @@ -95,6 +109,11 @@ bool AudioRecorder::startRecordingInternal(const juce::String& trackId, return false; } + auto writeStatus = std::make_shared(); + writeStatus->trackId = trackId; + writeStatus->file = file; + fileOutputStream = std::make_unique(std::move(fileOutputStream), writeStatus); + auto* checkedStream = fileOutputStream.get(); // Create WAV writer (16-bit PCM) auto writer = wavFormat.createWriterFor( fileOutputStream, @@ -103,15 +122,46 @@ bool AudioRecorder::startRecordingInternal(const juce::String& trackId, .withNumChannels(numChannels) .withBitsPerSample(16)); - if (!writer) + if (!writer || writeStatus->fault.load() != RecordingWriteStatus::none) { juce::Logger::writeToLog("AudioRecorder: Failed to create WAV writer"); return false; } + auto journal = std::make_unique(); + journal->setProperty("path", file.getFullPathName()); + journal->setProperty("trackId", trackId); + journal->setProperty("sampleRate", sampleRate); + journal->setProperty("channels", numChannels); + journal->setProperty("bitDepth", 16); + journal->setProperty("startTime", initialStartTime); + journal->setProperty("status", "recording"); + journal->setProperty("writeFault", 0); + journal->setProperty("createdAt", juce::Time::currentTimeMillis()); + journal->setProperty("kind", "recording"); + journal->setProperty("version", 1); + auto metadata = juce::var(journal.release()); + const auto journalId = recoveryJournal.create("recording", metadata); + if (journalId.isEmpty()) + { + juce::Logger::writeToLog("AudioRecorder: Could not create the take recovery journal; recording was not started"); + return false; + } + metadata.getDynamicObject()->setProperty("id", journalId); + writeStatus->journalMetadata = metadata; + writeStatus->journalFile = recoveryJournal.entryFile(journalId); + writeStatus->journalStartTime = initialStartTime; + // Ensure a parseable zero-length header exists before accepting live input. + if (!writer->flush()) return false; + checkedStream->flush(); + if (writeStatus->fault.load() != RecordingWriteStatus::none) return false; + // Wrap in ThreadedWriter - moves disk I/O to background thread + auto checkedWriter = std::make_unique(std::move(writer), writeStatus, checkedStream); auto threadedWriter = std::make_unique( - writer.release(), writerThread, 65536); + checkedWriter.release(), writerThread, 65536); + // Periodic header publication and checked flush, exclusively on disk thread. + threadedWriter->setFlushInterval(static_cast(sampleRate * 5.0)); // Pre-allocate incremental peak table for ~120 seconds of recording. // Entry layout: [min_ch0, max_ch0, min_ch1, max_ch1] per PEAK_STRIDE samples. @@ -138,12 +188,16 @@ bool AudioRecorder::startRecordingInternal(const juce::String& trackId, / juce::jmax(1.0, previous->second.sampleRate); } previousWriter = std::move(previous->second.threadedWriter); + previousWriteStatus = previous->second.writeStatus; + previousSampleRate = previous->second.sampleRate; activeRecordings.erase(previous); } ActiveRecording& state = activeRecordings[trackId]; state.trackId = trackId; state.threadedWriter = std::move(threadedWriter); + state.writeStatus = writeStatus; + pendingWriteStatuses.push_back(writeStatus); state.outputFile = file; state.isActive = true; state.startTime = initialStartTime; @@ -164,6 +218,8 @@ bool AudioRecorder::startRecordingInternal(const juce::String& trackId, state.accumCount = 0; } previousWriter.reset(); + if (replacedTake != nullptr && previousWriteStatus) + replacedTake->duration = previousWriteStatus->writtenSamples.load() / juce::jmax(1.0, previousSampleRate); juce::Logger::writeToLog("AudioRecorder: Started recording track " + trackId + " to " + file.getFullPathName()); @@ -184,6 +240,7 @@ void AudioRecorder::writeBlock(const juce::String& trackId, if (!sl.isLocked()) { writeLockMissCount.fetch_add(1, std::memory_order_relaxed); + OpenStudioCrashDiagnostics::recordRealtimeFault(OpenStudioCrashDiagnostics::RealtimeFault::recordingLockMiss); #if OPENSTUDIO_AUDIO_RECORD_DEBUG static std::atomic globalLockMissLogCounter { 0 }; const int lockMiss = ++globalLockMissLogCounter; @@ -198,6 +255,17 @@ void AudioRecorder::writeBlock(const juce::String& trackId, return; auto& state = it->second; + if (state.writeStatus->fault.load(std::memory_order_relaxed) != RecordingWriteStatus::none) + return; // Quarantine this take; monitoring and other writers continue. + if (numSamples < 0 || numSamples > buffer.getNumSamples() + || buffer.getNumChannels() < state.numChannels) + { + OpenStudioCrashDiagnostics::recordRealtimeFault( + OpenStudioCrashDiagnostics::RealtimeFault::recordingBufferContract); + return; + } + if (numSamples == 0) + return; const double captureToleranceSeconds = juce::jmax( 0.25, 4.0 * static_cast(numSamples) / juce::jmax(1.0, state.sampleRate)); @@ -210,6 +278,7 @@ void AudioRecorder::writeBlock(const juce::String& trackId, && plausibleFirstBlock) { state.startTime = compensatedBlockStartTimeSeconds; + state.writeStatus->journalStartTime.store(state.startTime, std::memory_order_relaxed); state.captureStartOnFirstWrite = false; } #if OPENSTUDIO_AUDIO_RECORD_DEBUG @@ -221,6 +290,8 @@ void AudioRecorder::writeBlock(const juce::String& trackId, if (!state.threadedWriter->write(buffer.getArrayOfReadPointers(), numSamples)) { writerBufferOverflowCount.fetch_add(1, std::memory_order_relaxed); + state.writeStatus->droppedSamples.fetch_add(numSamples, std::memory_order_relaxed); + OpenStudioCrashDiagnostics::recordRealtimeFault(OpenStudioCrashDiagnostics::RealtimeFault::recordingOverflow); return; } state.samplesWritten.fetch_add(numSamples, std::memory_order_relaxed); @@ -319,13 +390,20 @@ bool AudioRecorder::isRecording(const juce::String& trackId) const void AudioRecorder::setRecordingStartTime(const juce::String& trackId, double startTime) { - const juce::ScopedLock lock(writerLock); - auto it = activeRecordings.find(trackId); - if (it != activeRecordings.end()) + if (!std::isfinite(startTime) || startTime < 0) return; + std::shared_ptr status; { - it->second.startTime = startTime; - it->second.hasStartTimeFallback = true; + const juce::ScopedLock lock(writerLock); + auto it = activeRecordings.find(trackId); + if (it != activeRecordings.end()) + { + it->second.startTime = startTime; + it->second.hasStartTimeFallback = true; + status = it->second.writeStatus; + status->journalStartTime = startTime; + } } + if (status) status->persistRecovery(); // Never perform journal IO under writerLock. } std::vector AudioRecorder::stopAllRecordings(double currentSampleRate) @@ -342,9 +420,7 @@ std::vector AudioRecorder::stopAllRecordings( recordingsToFinalize.swap(activeRecordings); } - std::vector> finalizedWriters; std::vector finalizedClips; - finalizedWriters.reserve(recordingsToFinalize.size()); finalizedClips.reserve(recordingsToFinalize.size()); for (auto& [trackId, state] : recordingsToFinalize) @@ -352,11 +428,15 @@ std::vector AudioRecorder::stopAllRecordings( if (! state.threadedWriter) continue; + // Drain outside the publication lock, then use acknowledged writes, + // not the number accepted into the FIFO, for the published clip length. + state.threadedWriter.reset(); + CompletedRecording clip; clip.trackId = trackId; clip.file = state.outputFile; clip.startTime = state.startTime; - clip.duration = state.samplesWritten.load(std::memory_order_relaxed) + clip.duration = state.writeStatus->writtenSamples.load(std::memory_order_relaxed) / juce::jmax(1.0, state.sampleRate); finalizedClips.push_back(clip); OPENSTUDIO_LOG_AUDIO_RECORD("stopAllRecordings pending track=" + trackId @@ -365,10 +445,8 @@ std::vector AudioRecorder::stopAllRecordings( + " samplesWritten=" + juce::String(static_cast(state.samplesWritten.load())) + " duration=" + juce::String(clip.duration, 3) + (clip.duration <= 0.0 ? " WARNING_zero_duration" : "")); - finalizedWriters.push_back(std::move(state.threadedWriter)); } - finalizedWriters.clear(); juce::Logger::writeToLog("AudioRecorder: Stopped all recordings. Completed " + juce::String(finalizedClips.size()) + " clips."); OPENSTUDIO_LOG_AUDIO_RECORD("stopAllRecordings completed clipCount=" @@ -377,6 +455,28 @@ std::vector AudioRecorder::stopAllRecordings( } +juce::var AudioRecorder::takeWriteFailures() +{ + juce::Array failures; + const juce::ScopedLock lock(writerLock); + for (const auto& state : pendingWriteStatuses) + { + const auto fault = state->fault.load(std::memory_order_acquire); + if (fault == RecordingWriteStatus::none || state->reported) continue; + state->reported = true; + auto item = std::make_unique(); + item->setProperty("trackId", state->trackId); + item->setProperty("path", state->file.getFullPathName()); + item->setProperty("reason", fault); + failures.add(juce::var(item.release())); + } + pendingWriteStatuses.erase(std::remove_if(pendingWriteStatuses.begin(), pendingWriteStatuses.end(), [](const auto& state) { + return state->finished.load(std::memory_order_acquire) + && (state->reported || state->fault.load() == RecordingWriteStatus::none); + }), pendingWriteStatuses.end()); + return failures; +} + juce::var AudioRecorder::getRecordingPeaks(const juce::String& trackId, int samplesPerPixel, int numPixels, diff --git a/Source/AudioRecorder.h b/Source/AudioRecorder.h index e006ee3..22c7c24 100644 --- a/Source/AudioRecorder.h +++ b/Source/AudioRecorder.h @@ -4,6 +4,7 @@ #include #include #include +#include "RecordingWriterSafety.h" // Handles recording audio to disk for individual tracks. // Uses JUCE's ThreadedWriter to move disk I/O off the audio thread, @@ -11,7 +12,7 @@ class AudioRecorder { public: - AudioRecorder(); + explicit AudioRecorder(const juce::File& recoveryRoot = RecoveryJournal::defaultRoot()); ~AudioRecorder(); // Start recording for a specific track @@ -33,6 +34,7 @@ class AudioRecorder void setRecordingStartTime(const juce::String& trackId, double timeInSeconds); int getWriteLockMissCount() const { return writeLockMissCount.load(std::memory_order_relaxed); } int getWriterBufferOverflowCount() const { return writerBufferOverflowCount.load(std::memory_order_relaxed); } + juce::var takeWriteFailures(); // Control thread; one notification per failed take. // Stop all active recordings and return info about completed clips struct CompletedRecording { @@ -61,6 +63,7 @@ class AudioRecorder { juce::String trackId; std::unique_ptr threadedWriter; + std::shared_ptr writeStatus; juce::File outputFile; std::atomic isActive { false }; double startTime = 0.0; // Recording start time in seconds @@ -101,6 +104,7 @@ class AudioRecorder }; std::map activeRecordings; + std::vector> pendingWriteStatuses; bool startRecordingInternal(const juce::String& trackId, const juce::File& file, double sampleRate, @@ -108,6 +112,7 @@ class AudioRecorder double initialStartTime, CompletedRecording* replacedTake); juce::WavAudioFormat wavFormat; + RecoveryJournal recoveryJournal; mutable juce::CriticalSection writerLock; // Protects activeRecordings map structure // Background thread for disk I/O (shared by all recordings) diff --git a/Source/BuiltInEffects.cpp b/Source/BuiltInEffects.cpp index 048c10d..aa06019 100644 --- a/Source/BuiltInEffects.cpp +++ b/Source/BuiltInEffects.cpp @@ -1,18 +1,18 @@ #include "BuiltInEffects.h" -#include "S13PluginEditors.h" +#include "OpenStudioPluginEditors.h" //============================================================================== -// S13BuiltInEffect -- shared base class +// OpenStudioBuiltInEffect -- shared base class //============================================================================== -S13BuiltInEffect::S13BuiltInEffect() +OpenStudioBuiltInEffect::OpenStudioBuiltInEffect() : AudioProcessor(BusesProperties() .withInput("Input", juce::AudioChannelSet::stereo(), true) .withOutput("Output", juce::AudioChannelSet::stereo(), true)) { } -bool S13BuiltInEffect::isBusesLayoutSupported(const BusesLayout& layouts) const +bool OpenStudioBuiltInEffect::isBusesLayoutSupported(const BusesLayout& layouts) const { const auto& mainIn = layouts.getMainInputChannelSet(); const auto& mainOut = layouts.getMainOutputChannelSet(); @@ -21,20 +21,20 @@ bool S13BuiltInEffect::isBusesLayoutSupported(const BusesLayout& layouts) const || mainOut == juce::AudioChannelSet::stereo(); } -void S13BuiltInEffect::setOversamplingEnabled(bool enabled) +void OpenStudioBuiltInEffect::setOversamplingEnabled(bool enabled) { oversamplingEnabled = enabled; } -juce::AudioProcessorEditor* S13BuiltInEffect::createEditor() +juce::AudioProcessorEditor* OpenStudioBuiltInEffect::createEditor() { return nullptr; // Derived classes override this } -juce::AudioProcessorEditor* S13EQ::createEditor() { return new S13EQEditor(*this); } -juce::AudioProcessorEditor* S13Compressor::createEditor() { return new S13CompressorEditor(*this); } -juce::AudioProcessorEditor* S13Gate::createEditor() { return new S13GateEditor(*this); } -juce::AudioProcessorEditor* S13Limiter::createEditor() { return new S13LimiterEditor(*this); } +juce::AudioProcessorEditor* OpenStudioEQ::createEditor() { return new OpenStudioEQEditor(*this); } +juce::AudioProcessorEditor* OpenStudioCompressor::createEditor() { return new OpenStudioCompressorEditor(*this); } +juce::AudioProcessorEditor* OpenStudioGate::createEditor() { return new OpenStudioGateEditor(*this); } +juce::AudioProcessorEditor* OpenStudioLimiter::createEditor() { return new OpenStudioLimiterEditor(*this); } static void sanitizeBuiltInBuffer(juce::AudioBuffer& buffer, float limit) { @@ -81,7 +81,7 @@ float safeFilterMaximum(double sampleRate, float nominalMinimum, float nominalMa static_cast(sampleRate * 0.475))); } -void prepareRealtimeFilterLut(std::vector& lut, +void prepareRealtimeFilterLut(std::vector& lut, double sampleRate, float nominalMinimum, float nominalMaximum, @@ -108,8 +108,8 @@ void prepareRealtimeFilterLut(std::vector& lut, } } -const S13IIRCoefficientSet& lookupRealtimeFilterLut( - const std::vector& lut, +const OpenStudioIIRCoefficientSet& lookupRealtimeFilterLut( + const std::vector& lut, double sampleRate, float frequency, float nominalMinimum, @@ -132,7 +132,7 @@ const S13IIRCoefficientSet& lookupRealtimeFilterLut( } void writeRealtimeFilterCoefficients(juce::dsp::IIR::Filter& filter, - const S13IIRCoefficientSet& coefficients) noexcept + const OpenStudioIIRCoefficientSet& coefficients) noexcept { jassert(filter.coefficients != nullptr); if (filter.coefficients == nullptr) @@ -149,7 +149,7 @@ void writeRealtimeFilterCoefficients(juce::dsp::IIR::Filter& filter, void writeRealtimeFilterCoefficients(juce::dsp::IIR::Filter& left, juce::dsp::IIR::Filter& right, - const S13IIRCoefficientSet& coefficients) noexcept + const OpenStudioIIRCoefficientSet& coefficients) noexcept { writeRealtimeFilterCoefficients(left, coefficients); if (right.coefficients != left.coefficients) @@ -158,7 +158,7 @@ void writeRealtimeFilterCoefficients(juce::dsp::IIR::Filter& left, bool advanceRealtimeFilterCoefficients( juce::dsp::IIR::Filter& filter, - const S13IIRCoefficientSet& target, + const OpenStudioIIRCoefficientSet& target, float smoothingProportion) noexcept { if (filter.coefficients == nullptr) @@ -196,7 +196,7 @@ bool advanceRealtimeFilterCoefficients( bool advanceRealtimeFilterCoefficients( juce::dsp::IIR::Filter& left, juce::dsp::IIR::Filter& right, - const S13IIRCoefficientSet& target, + const OpenStudioIIRCoefficientSet& target, float smoothingProportion) noexcept { const bool leftSmoothing = @@ -210,11 +210,11 @@ bool advanceRealtimeFilterCoefficients( || leftSmoothing; } -constexpr S13IIRCoefficientSet kIdentityBiquad { +constexpr OpenStudioIIRCoefficientSet kIdentityBiquad { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f }; -S13IIRCoefficientSet normaliseBiquad( +OpenStudioIIRCoefficientSet normaliseBiquad( const std::array& coefficients) noexcept { const float inverseA0 = std::abs(coefficients[3]) > 1.0e-12f @@ -229,7 +229,7 @@ S13IIRCoefficientSet normaliseBiquad( }; } -S13IIRCoefficientSet normaliseFirstOrderAsBiquad( +OpenStudioIIRCoefficientSet normaliseFirstOrderAsBiquad( const std::array& coefficients) noexcept { const float inverseA0 = std::abs(coefficients[2]) > 1.0e-12f @@ -244,7 +244,7 @@ S13IIRCoefficientSet normaliseFirstOrderAsBiquad( }; } -double getFixedBiquadMagnitude(const S13IIRCoefficientSet& coefficients, +double getFixedBiquadMagnitude(const OpenStudioIIRCoefficientSet& coefficients, double frequency, double sampleRate) noexcept { @@ -272,10 +272,10 @@ double getFixedBiquadMagnitude(const S13IIRCoefficientSet& coefficients, } //============================================================================== -// S13EQ -- 8-band parametric EQ +// OpenStudioEQ -- 8-band parametric EQ //============================================================================== -S13EQ::S13EQ() +OpenStudioEQ::OpenStudioEQ() { const float defaultFreqs[numBands] = { 30.0f, 100.0f, 250.0f, 500.0f, 1000.0f, 2500.0f, 6000.0f, 12000.0f }; for (int i = 0; i < numBands; ++i) @@ -303,7 +303,7 @@ S13EQ::S13EQ() bands[numBands - 1].enabled.store(0.0f); } -int S13EQ::getNumStagesForSlope(FilterSlope slope) const +int OpenStudioEQ::getNumStagesForSlope(FilterSlope slope) const { switch (slope) { @@ -315,7 +315,7 @@ int S13EQ::getNumStagesForSlope(FilterSlope slope) const } } -void S13EQ::prepareToPlay(double sampleRate, int samplesPerBlock) +void OpenStudioEQ::prepareToPlay(double sampleRate, int samplesPerBlock) { cachedSampleRate = sampleRate; publishedSampleRate.store(sampleRate, std::memory_order_release); @@ -427,7 +427,7 @@ static float butterworthCascadeStageQ(int stageIndex, return 1.0f / (2.0f * std::cos(angle)); } -void S13EQ::reset() +void OpenStudioEQ::reset() { resetWetPathState(); if (oversampler) @@ -478,7 +478,7 @@ void S13EQ::reset() } } -void S13EQ::releaseResources() +void OpenStudioEQ::releaseResources() { filtersPrepared = false; spectrumDemandSamplesRemaining.store(0, std::memory_order_release); @@ -528,10 +528,10 @@ void S13EQ::releaseResources() } } -int S13EQ::buildBandTargets( +int OpenStudioEQ::buildBandTargets( int b, bool shouldProcess, - std::array& targets) const noexcept + std::array& targets) const noexcept { targets.fill(kIdentityBiquad); if (!shouldProcess) @@ -672,7 +672,7 @@ int S13EQ::buildBandTargets( return 0; } -void S13EQ::updateBand(int b, int auditionIndex, bool forceImmediate) +void OpenStudioEQ::updateBand(int b, int auditionIndex, bool forceImmediate) { const bool shouldProcess = auditionIndex >= 0 ? b == auditionIndex @@ -710,7 +710,7 @@ void S13EQ::updateBand(int b, int auditionIndex, bool forceImmediate) activeStages[b] = targetStages[b]; } -void S13EQ::updateFilters(bool forceImmediate) +void OpenStudioEQ::updateFilters(bool forceImmediate) { if (!filtersPrepared) return; @@ -760,7 +760,7 @@ void S13EQ::updateFilters(bool forceImmediate) filtersNeedSmoothing = false; } -void S13EQ::updateDynamicBands(const juce::AudioBuffer& buffer, +void OpenStudioEQ::updateDynamicBands(const juce::AudioBuffer& buffer, int detectorChannels) { const int numSamples = buffer.getNumSamples(); @@ -838,7 +838,7 @@ void S13EQ::updateDynamicBands(const juce::AudioBuffer& buffer, } } -bool S13EQ::advanceStageCoefficients(int bandIndex, +bool OpenStudioEQ::advanceStageCoefficients(int bandIndex, int stageIndex) noexcept { auto& coefficientVector = @@ -887,7 +887,7 @@ bool S13EQ::advanceStageCoefficients(int bandIndex, return stillSmoothing; } -void S13EQ::resetWetPathState() noexcept +void OpenStudioEQ::resetWetPathState() noexcept { for (int band = 0; band < numBands; ++band) { @@ -906,7 +906,7 @@ void S13EQ::resetWetPathState() noexcept } } -void S13EQ::advanceAutoGainEstimateProbe() noexcept +void OpenStudioEQ::advanceAutoGainEstimateProbe() noexcept { static constexpr int probeCount = 16; static constexpr std::array probeFrequencies { @@ -936,10 +936,10 @@ void S13EQ::advanceAutoGainEstimateProbe() noexcept const auto& coefficientVector = bandFilters[b][stage].state->coefficients; if (coefficientVector.size() - != static_cast(S13IIRCoefficientSet {}.size())) + != static_cast(OpenStudioIIRCoefficientSet {}.size())) continue; - S13IIRCoefficientSet currentCoefficients {}; + OpenStudioIIRCoefficientSet currentCoefficients {}; for (size_t coefficient = 0; coefficient < currentCoefficients.size(); ++coefficient) @@ -986,7 +986,7 @@ void S13EQ::advanceAutoGainEstimateProbe() noexcept } } -void S13EQ::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midi) +void OpenStudioEQ::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midi) { juce::ignoreUnused(midi); juce::ScopedNoDenormals noDenormals; @@ -1321,7 +1321,7 @@ void S13EQ::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& mid } } -bool S13EQ::isSpectrumCaptureRequested( +bool OpenStudioEQ::isSpectrumCaptureRequested( int numSamples) noexcept { int remaining = @@ -1350,7 +1350,7 @@ bool S13EQ::isSpectrumCaptureRequested( return false; } -int S13EQ::claimSpectrumCaptureSlot() noexcept +int OpenStudioEQ::claimSpectrumCaptureSlot() noexcept { for (const int claimableState : { 0, 2 }) { @@ -1372,7 +1372,7 @@ int S13EQ::claimSpectrumCaptureSlot() noexcept return -1; } -void S13EQ::captureSpectrumBlock( +void OpenStudioEQ::captureSpectrumBlock( const juce::AudioBuffer& preEQ, const juce::AudioBuffer& postEQ) noexcept { @@ -1429,7 +1429,7 @@ void S13EQ::captureSpectrumBlock( } } -void S13EQ::computeSpectrum(const std::array& input, std::array& output) +void OpenStudioEQ::computeSpectrum(const std::array& input, std::array& output) { std::array fftData {}; for (int i = 0; i < fftSize; ++i) @@ -1443,7 +1443,7 @@ void S13EQ::computeSpectrum(const std::array& input, std::array< } } -S13EQ::SpectrumData S13EQ::getSpectrumData() +OpenStudioEQ::SpectrumData OpenStudioEQ::getSpectrumData() { const double sampleRate = publishedSampleRate.load(std::memory_order_acquire); @@ -1499,7 +1499,7 @@ S13EQ::SpectrumData S13EQ::getSpectrumData() return lastSpectrumOutput; } -std::vector S13EQ::getMagnitudeResponse(const std::vector& frequencies) const +std::vector OpenStudioEQ::getMagnitudeResponse(const std::vector& frequencies) const { std::vector response(frequencies.size(), 0.0f); const int auditionIndex = juce::jlimit(-1, numBands - 1, @@ -1512,7 +1512,7 @@ std::vector S13EQ::getMagnitudeResponse(const std::vector& frequen std::memory_order_relaxed) < 0.5f) continue; - std::array targets {}; const int stages = buildBandTargets(b, true, targets); @@ -1542,16 +1542,16 @@ std::vector S13EQ::getMagnitudeResponse(const std::vector& frequen return response; } -float S13EQ::getBandDynamicGainDB(int bandIndex) const +float OpenStudioEQ::getBandDynamicGainDB(int bandIndex) const { if (bandIndex < 0 || bandIndex >= numBands) return 0.0f; return dynamicGainDB[static_cast(bandIndex)].load(std::memory_order_relaxed); } -void S13EQ::getStateInformation(juce::MemoryBlock& destData) +void OpenStudioEQ::getStateInformation(juce::MemoryBlock& destData) { - juce::ValueTree state("S13EQ"); + juce::ValueTree state("OpenStudioEQ"); state.setProperty("outputGain", outputGain.load(), nullptr); state.setProperty("autoGain", autoGain.load(), nullptr); state.setProperty("auditionBand", auditionBand.load(), nullptr); @@ -1575,10 +1575,10 @@ void S13EQ::getStateInformation(juce::MemoryBlock& destData) state.writeToStream(stream); } -void S13EQ::setStateInformation(const void* data, int sizeInBytes) +void OpenStudioEQ::setStateInformation(const void* data, int sizeInBytes) { auto state = juce::ValueTree::readFromData(data, static_cast(sizeInBytes)); - if (!state.isValid() || state.getType().toString() != "S13EQ") return; + if (!state.isValid() || state.getType().toString() != "OpenStudioEQ") return; outputGain.store(static_cast(state.getProperty("outputGain", 0.0f))); autoGain.store(static_cast(state.getProperty("autoGain", 0.0f))); @@ -1604,12 +1604,12 @@ void S13EQ::setStateInformation(const void* data, int sizeInBytes) } //============================================================================== -// S13Compressor -- Multi-style compressor +// OpenStudioCompressor -- Multi-style compressor //============================================================================== -S13Compressor::S13Compressor() {} +OpenStudioCompressor::OpenStudioCompressor() {} -float S13Compressor::computeGain(float inputDB, +float OpenStudioCompressor::computeGain(float inputDB, float thresholdDB, float compressionSlope, float kneeDB) noexcept @@ -1670,7 +1670,7 @@ static float sanitiseCompressorSidechainHPF(float frequency) noexcept return frequency; } -void S13Compressor::getStyleBallistics(float& atkMs, float& relMs) const +void OpenStudioCompressor::getStyleBallistics(float& atkMs, float& relMs) const { const auto styleVal = static_cast