diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f04272a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +resources/installer/macos-scripts/* text eol=lf diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fb3928c..ce6362d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,7 +55,9 @@ jobs: ninja-build \ pkg-config \ python3 \ - libpulse-dev + libpulse-dev \ + pulseaudio \ + pulseaudio-utils - name: Install dependencies env: @@ -80,6 +82,9 @@ jobs: - name: Test run: npm test + - name: Test isolated Linux multichannel capture + run: npm run test:linux-capture + - name: Build and test Prism TUI run: npm run test:tui @@ -97,7 +102,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-2022] + # Keep the Linux plugin ABI baseline stable for older DAW runtimes. + os: [ubuntu-24.04, macos-latest, windows-2022] steps: - uses: actions/checkout@v6 @@ -169,6 +175,8 @@ jobs: libxrandr-dev \ libxrender-dev \ libwebkit2gtk-4.1-dev \ + xvfb \ + xauth \ libglu1-mesa-dev \ mesa-common-dev @@ -187,6 +195,9 @@ jobs: - name: Test native spectrum and capture exports run: npm run test:spectrum-native + - name: Test native channel routing and PCM formats + run: npm run test:capture-channel-selection-native + - name: Test native spectrogram accuracy run: npm run test:spectrogram-native @@ -232,10 +243,11 @@ jobs: cmake -B plugin/build -S plugin -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DJUCE_PATH="$GITHUB_WORKSPACE/.ci-cache/juce/JUCE-${PRISM_JUCE_VERSION}" \ - -DPRISM_PLUGIN_FORMATS="AU;VST3" \ + -DPRISM_PLUGIN_FORMATS="AU;VST3;CLAP" \ -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF \ -DCMAKE_C_COMPILER_LAUNCHER=sccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + node scripts/check-plugin-formats.mjs - name: Configure JUCE plugins (Linux) if: runner.os == 'Linux' @@ -243,8 +255,9 @@ jobs: cmake -B plugin/build -S plugin -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DJUCE_PATH="$GITHUB_WORKSPACE/.ci-cache/juce/JUCE-${PRISM_JUCE_VERSION}" \ - -DPRISM_PLUGIN_FORMATS=VST3 \ + -DPRISM_PLUGIN_FORMATS="VST3;CLAP" \ -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF + node scripts/check-plugin-formats.mjs - name: Configure JUCE plugins (Windows) if: runner.os == 'Windows' @@ -252,7 +265,8 @@ jobs: run: | for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set VSINSTALL=%%i call "%VSINSTALL%\VC\Auxiliary\Build\vcvars64.bat" - cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release -DJUCE_PATH="%GITHUB_WORKSPACE%\.ci-cache\juce\JUCE-%PRISM_JUCE_VERSION%" -DPRISM_PLUGIN_FORMATS=VST3 -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF -DJUCE_WEBVIEW2_PACKAGE_LOCATION="%JUCE_WEBVIEW2_PACKAGE_LOCATION%" -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release -DJUCE_PATH="%GITHUB_WORKSPACE%\.ci-cache\juce\JUCE-%PRISM_JUCE_VERSION%" -DPRISM_PLUGIN_FORMATS="VST3;CLAP" -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF -DJUCE_WEBVIEW2_PACKAGE_LOCATION="%JUCE_WEBVIEW2_PACKAGE_LOCATION%" -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + node scripts/check-plugin-formats.mjs - name: Build JUCE plugins (macOS) if: runner.os == 'macOS' @@ -262,6 +276,12 @@ jobs: if: runner.os == 'Linux' run: cmake --build plugin/build --target PrismInstallerPlugins --parallel 4 + - name: Test Linux browser helper isolation + if: runner.os == 'Linux' + run: | + cmake --build plugin/build --target PrismLinuxWebViewHelperProbe --parallel 4 + ctest --test-dir plugin/build --output-on-failure -R '^PrismLinuxWebViewHelperTests$' + - name: Build JUCE plugins (Windows) if: runner.os == 'Windows' shell: cmd @@ -269,23 +289,60 @@ jobs: for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set VSINSTALL=%%i call "%VSINSTALL%\VC\Auxiliary\Build\vcvars64.bat" cmake --build plugin/build --target PrismInstallerPlugins --config Release --parallel 4 + cmake --build plugin/build --target PrismReferenceTests PrismBridgeTests PrismWaterfallTests --config Release --parallel 4 + ctest --test-dir plugin/build -C Release --output-on-failure -R "Prism(Reference|BridgeProcessor|Waterfall)Tests" - - name: Stage plugins for Windows installer - if: runner.os == 'Windows' + - name: Test Prism Reference, Bridge, and Waterfall plugins + if: runner.os != 'Windows' + run: | + cmake --build plugin/build --target PrismReferenceTests PrismBridgeTests PrismWaterfallTests --config Release --parallel 4 + ctest --test-dir plugin/build -C Release --output-on-failure -R "Prism(Reference|BridgeProcessor|Waterfall)Tests" + + - name: Stage plugins for installer + run: node scripts/build/stage-plugins.mjs + + - name: Setup Rust for CLAP validation + uses: dtolnay/rust-toolchain@stable + with: + toolchain: '1.95.0' + + - name: Cache CLAP validator + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + .ci-cache/clap-validator/target + key: ${{ runner.os }}-clap-validator-b2f1d9b-rust-1.95.0 + + - name: Build pinned CLAP validator shell: bash run: | - rm -rf plugin/dist-installer - mkdir -p plugin/dist-installer/VST3 - for art in PrismSpectrum PrismOscilloscope PrismVUMeter PrismLUFSMeter PrismVectorscope PrismSpectrogram PrismWaveform; do - vst3_dir="plugin/build/${art}_artefacts/Release/VST3" - [ -d "$vst3_dir" ] && find "$vst3_dir" -maxdepth 1 -name "*.vst3" -exec cp -R {} plugin/dist-installer/VST3/ \; - done - vst3_count="$(find plugin/dist-installer/VST3 -maxdepth 1 -name "*.vst3" | wc -l | tr -d ' ')" - if [ "$vst3_count" -ne 7 ]; then - echo "ERROR: expected 7 VST3 plugins, found $vst3_count" - exit 1 + git clone https://github.com/free-audio/clap-validator.git .ci-cache/clap-validator-source + git -C .ci-cache/clap-validator-source checkout b2f1d9b79b1d264a5747f46707d72b1aa40a02ef + cargo build --release --locked --package clap-validator --manifest-path .ci-cache/clap-validator-source/Cargo.toml --target-dir .ci-cache/clap-validator/target + echo "$GITHUB_WORKSPACE/.ci-cache/clap-validator/target/release" >> "$GITHUB_PATH" + + - name: Validate all CLAP plugins + shell: bash + run: | + if [ "$RUNNER_OS" = "Linux" ]; then + xvfb-run -a node scripts/validate-clap.mjs + else + node scripts/validate-clap.mjs fi + - name: Retain CLAP validation output + if: always() + uses: actions/upload-artifact@v4 + with: + name: clap-validation-${{ runner.os }} + path: plugin/build/clap-validation/ + if-no-files-found: ignore + + - name: Test plugin staging and Unix installer hooks + run: node --test test/plugin-packaging.test.mjs + - name: Stage Prism TUI for Windows installer if: runner.os == 'Windows' run: npm run stage:tui @@ -296,7 +353,7 @@ jobs: env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' - - name: Smoke-test Windows installer PATH integration + - name: Smoke-test Windows installer plugins and PATH integration if: runner.os == 'Windows' shell: pwsh run: ./scripts/test-windows-installer.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cf3513..674b6c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,8 @@ jobs: include: - os: macos-latest dist_cmd: dist:mac - - os: ubuntu-latest + # Keep the Linux plugin ABI baseline stable for older DAW runtimes. + - os: ubuntu-24.04 dist_cmd: dist:linux:packages # Pinned to windows-2022 because windows-latest now ships VS 2026 # (internal version 18), which the @electron/node-gyp version in our @@ -99,6 +100,8 @@ jobs: libxrandr-dev \ libxrender-dev \ libwebkit2gtk-4.1-dev \ + xvfb \ + xauth \ libglu1-mesa-dev \ mesa-common-dev \ rpm @@ -121,6 +124,9 @@ jobs: - name: Test native spectrum and capture exports run: npm run test:spectrum-native + - name: Test native channel routing and PCM formats + run: npm run test:capture-channel-selection-native + - name: Test native spectrogram accuracy run: npm run test:spectrogram-native @@ -153,10 +159,11 @@ jobs: cmake -B plugin/build -S plugin -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DJUCE_PATH="$GITHUB_WORKSPACE/.ci-cache/juce/JUCE-${PRISM_JUCE_VERSION}" \ - -DPRISM_PLUGIN_FORMATS="AU;VST3" \ + -DPRISM_PLUGIN_FORMATS="AU;VST3;CLAP" \ -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF \ -DCMAKE_C_COMPILER_LAUNCHER=sccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + node scripts/check-plugin-formats.mjs - name: Configure JUCE plugins (Linux) if: runner.os == 'Linux' @@ -164,8 +171,9 @@ jobs: cmake -B plugin/build -S plugin -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DJUCE_PATH="$GITHUB_WORKSPACE/.ci-cache/juce/JUCE-${PRISM_JUCE_VERSION}" \ - -DPRISM_PLUGIN_FORMATS=VST3 \ + -DPRISM_PLUGIN_FORMATS="VST3;CLAP" \ -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF + node scripts/check-plugin-formats.mjs # JUCE's NEEDS_WEBVIEW2 expects the Microsoft.Web.WebView2 NuGet package # to already be installed locally; it doesn't fetch it itself. Install it @@ -197,7 +205,8 @@ jobs: run: | for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set VSINSTALL=%%i call "%VSINSTALL%\VC\Auxiliary\Build\vcvars64.bat" - cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release -DJUCE_PATH="%GITHUB_WORKSPACE%\.ci-cache\juce\JUCE-%PRISM_JUCE_VERSION%" -DPRISM_PLUGIN_FORMATS=VST3 -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF -DJUCE_WEBVIEW2_PACKAGE_LOCATION="%JUCE_WEBVIEW2_PACKAGE_LOCATION%" -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release -DJUCE_PATH="%GITHUB_WORKSPACE%\.ci-cache\juce\JUCE-%PRISM_JUCE_VERSION%" -DPRISM_PLUGIN_FORMATS="VST3;CLAP" -DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF -DJUCE_WEBVIEW2_PACKAGE_LOCATION="%JUCE_WEBVIEW2_PACKAGE_LOCATION%" -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + node scripts/check-plugin-formats.mjs - name: Build JUCE plugins (macOS) if: runner.os == 'macOS' @@ -207,6 +216,12 @@ jobs: if: runner.os == 'Linux' run: cmake --build plugin/build --target PrismInstallerPlugins --parallel 4 + - name: Test Linux browser helper isolation + if: runner.os == 'Linux' + run: | + cmake --build plugin/build --target PrismLinuxWebViewHelperProbe --parallel 4 + ctest --test-dir plugin/build --output-on-failure -R '^PrismLinuxWebViewHelperTests$' + - name: Build JUCE plugins (Windows) if: runner.os == 'Windows' shell: cmd @@ -214,53 +229,64 @@ jobs: for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set VSINSTALL=%%i call "%VSINSTALL%\VC\Auxiliary\Build\vcvars64.bat" cmake --build plugin/build --target PrismInstallerPlugins --config Release --parallel 4 + cmake --build plugin/build --target PrismReferenceTests PrismBridgeTests PrismWaterfallTests --config Release --parallel 4 + ctest --test-dir plugin/build -C Release --output-on-failure -R "Prism(Reference|BridgeProcessor|Waterfall)Tests" + + - name: Test Prism Reference, Bridge, and Waterfall plugins + if: runner.os != 'Windows' + run: | + cmake --build plugin/build --target PrismReferenceTests PrismBridgeTests PrismWaterfallTests --config Release --parallel 4 + ctest --test-dir plugin/build -C Release --output-on-failure -R "Prism(Reference|BridgeProcessor|Waterfall)Tests" - name: Show sccache stats if: runner.os != 'Linux' run: sccache --show-stats - name: Stage plugins for installer + run: node scripts/build/stage-plugins.mjs + + - name: Setup Rust for CLAP validation + uses: dtolnay/rust-toolchain@stable + with: + toolchain: '1.95.0' + + - name: Cache CLAP validator + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + .ci-cache/clap-validator/target + key: ${{ runner.os }}-clap-validator-b2f1d9b-rust-1.95.0 + + - name: Build pinned CLAP validator + shell: bash + run: | + git clone https://github.com/free-audio/clap-validator.git .ci-cache/clap-validator-source + git -C .ci-cache/clap-validator-source checkout b2f1d9b79b1d264a5747f46707d72b1aa40a02ef + cargo build --release --locked --package clap-validator --manifest-path .ci-cache/clap-validator-source/Cargo.toml --target-dir .ci-cache/clap-validator/target + echo "$GITHUB_WORKSPACE/.ci-cache/clap-validator/target/release" >> "$GITHUB_PATH" + + - name: Validate all CLAP plugins shell: bash run: | - rm -rf plugin/dist-installer - mkdir -p plugin/dist-installer - mkdir -p plugin/dist-installer/VST3 - [ "$RUNNER_OS" = "macOS" ] && mkdir -p plugin/dist-installer/AU - for art in PrismSpectrum PrismOscilloscope PrismVUMeter PrismLUFSMeter PrismVectorscope PrismSpectrogram PrismWaveform; do - vst3_dir="plugin/build/${art}_artefacts/Release/VST3" - [ -d "$vst3_dir" ] && find "$vst3_dir" -maxdepth 1 -name "*.vst3" -exec cp -R {} plugin/dist-installer/VST3/ \; - if [ "$RUNNER_OS" = "macOS" ]; then - au_dir="plugin/build/${art}_artefacts/Release/AU" - [ -d "$au_dir" ] && find "$au_dir" -maxdepth 1 -name "*.component" -exec cp -R {} plugin/dist-installer/AU/ \; - fi - done - echo "--- staged VST3 ---" - ls -la plugin/dist-installer/VST3 - vst3_count="$(find plugin/dist-installer/VST3 -maxdepth 1 -name "*.vst3" | wc -l | tr -d ' ')" - if [ "$vst3_count" -ne 7 ]; then - echo "ERROR: expected 7 VST3 plugins, found $vst3_count" - exit 1 - fi if [ "$RUNNER_OS" = "Linux" ]; then - for vst3 in plugin/dist-installer/VST3/*.vst3; do - if [ ! -d "$vst3/Contents/x86_64-linux" ]; then - echo "ERROR: Linux VST3 bundle is missing Contents/x86_64-linux: $vst3" - exit 1 - fi - done - cp resources/installer/linux/install-vst3.sh plugin/dist-installer/install-vst3.sh - chmod +x plugin/dist-installer/install-vst3.sh - fi - if [ "$RUNNER_OS" = "macOS" ]; then - echo "--- staged AU ---" - ls -la plugin/dist-installer/AU - au_count="$(find plugin/dist-installer/AU -maxdepth 1 -name "*.component" | wc -l | tr -d ' ')" - if [ "$au_count" -ne 7 ]; then - echo "ERROR: expected 7 AU plugins, found $au_count" - exit 1 - fi + xvfb-run -a node scripts/validate-clap.mjs + else + node scripts/validate-clap.mjs fi + - name: Retain CLAP validation output + if: always() + uses: actions/upload-artifact@v4 + with: + name: clap-validation-${{ runner.os }} + path: plugin/build/clap-validation/ + if-no-files-found: ignore + + - name: Test plugin staging and Unix installer hooks + run: node --test test/plugin-packaging.test.mjs + - name: Stage Prism TUI for installer run: npm run stage:tui @@ -282,20 +308,22 @@ jobs: if: runner.os != 'Linux' run: npm run ${{ matrix.dist_cmd }} env: + PRISM_PLUGINS_PREBUILT: '1' CSC_IDENTITY_AUTO_DISCOVERY: 'false' - - name: Smoke-test Windows installer PATH integration + - name: Smoke-test Windows installer plugins and PATH integration if: runner.os == 'Windows' shell: pwsh run: ./scripts/test-windows-installer.ps1 - - name: Build Linux packages with VST3 plugins + - name: Build Linux packages with VST3 and CLAP plugins if: runner.os == 'Linux' shell: bash run: | rm -rf dist/linux-unpacked npm run ${{ matrix.dist_cmd }} env: + PRISM_PLUGINS_PREBUILT: '1' CSC_IDENTITY_AUTO_DISCOVERY: 'false' - name: Upload artifacts diff --git a/.gitignore b/.gitignore index 0f62405..b30e83c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ # Build outputs +artifacts/linux-tests/ out/ dist/ native/build/ @@ -36,3 +37,6 @@ yarn-error.log* # notes untitled-1 + +# Local latency benchmark recordings, traces, and isolated settings +benchmark-results/ diff --git a/README.md b/README.md index c961a37..6205334 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,22 @@ Prism is a free, open-source audio analyzer and meter rack for Windows, macOS, and Linux. -Monitor system audio or an input with a configurable set of real-time scopes and meters, arrange them however you like, and save the setup as a profile. The same analyzers are also available as VST3/AU plugins, and Prism includes a native terminal interface for lightweight monitoring. +Monitor system audio or an input with a configurable set of real-time scopes and meters, arrange them however you like, and save the setup as a profile. The same analyzers are also available as VST3/AU/CLAP plugins, and Prism includes a native terminal interface for lightweight monitoring. + +### Multichannel input routing + +The desktop app's Channel Routing matrix works with system audio and device inputs on macOS, Windows, and Linux. Select any source channel independently for Prism's Left and Right channels, including the same channel for both. Activity fills show the incoming signal on every available channel, before routing and input trim; routes are saved per source. ![Prism scopes](assets/prism-demo-readme-optimized.gif) ## Scopes -Prism includes seven real-time scopes and meters: +Prism includes eight real-time scopes and meters: * **Spectrum Analyzer** — FFT spectrum with calibrated dBFS levels, configurable FFT size, spectral tilt, Log/Mel/Linear scales, heatmap and fill modes, and peak/pitch readouts * **Oscilloscope** — Time-domain waveform with fundamental-frequency pitch locking and sub-sample triggering for a stable display * **Vectorscope** — Full-band stereo phase analysis with XY, Polar, and M/S Linear views, calibrated references, adjustable zoom, and optional multiband RGB +* **Waterfall** — Layered spectrum history with 1–30 seconds of audio, adaptive ridge density, theme or heat colors, and frequency/time guides in the desktop app, TUI, and DAW plugin * **Spectrogram** — Scrolling frequency-over-time display with Log/Mel/Linear scales, stereo-energy analysis, and frequency reassignment in Sharp and Sharper modes * **VU Meter** — 300 ms metering with adjustable 0 VU reference, stereo correlation, and needle or bar displays * **Loudness Meter** — ITU-R BS.1770 momentary, short-term, and integrated LUFS metering with BS.1770 true-peak activity @@ -32,9 +37,11 @@ Prism includes seven real-time scopes and meters: Every scope can be configured independently. Resize and rearrange them into a rack, rotate supported scopes, pop them into separate windows, or pin them on top of other applications. +On Windows, enable Reserve screen space in the rack's Top/Bottom reposition menu or its tray equivalent to dock the main rack across one monitor. Maximized applications use the remaining workspace. Resize the edge facing the workspace, drag the grab handle to return to a floating rack, or disable the option to restore its previous position and size. Settings open in a separate panel beside the dock. Docking is remembered on this computer independently of profiles; minimizing or hiding Prism releases the reserved space. + ![Prism customize](assets/prism-showcase-layout-readme.gif) -Spectrum, Spectrogram, Oscilloscope, and Waveform also include interactive measurement overlays for inspecting frequency, level, pitch, amplitude, or time directly from the display. +Spectrum, Spectrogram, Oscilloscope, and Waveform also include interactive measurement overlays for inspecting frequency, level, pitch, amplitude, or time directly from the display. Optional Linked Analysis mirrors compatible frequency, history-time, and amplitude guides across docked and detached scopes.
Stats for nerds @@ -51,6 +58,25 @@ Most of Prism's analysis runs in native C++, with the same DSP implementations r * Independent spectral and heatmap tilt around a 1 kHz reference * Peak analysis can report dBFS, frequency, musical note, octave, and cents offset * Interactive measurement overlay exposes frequency, level, and pitch directly from the graph +* One whole-track reference: drop a mono/stereo WAV, AIFF, FLAC, or MP3 onto the spectrum, or use Spectrum settings → Reference → Load track +* Dashed Overlay and a centered ±24 dB Difference view, with ±24 dB reference trim and one-shot Match level using the latest three seconds of live Mid audio +* Cancellable background analysis with an evolving preview; analyzed curves are saved with profiles and Spectrum plugin state, so recall does not need the original file + +Spectrum settings have General and Reference tabs. Matching requires at least one second of current, non-silent audio. During import, the spectrum temporarily shows Overlay; the previous reference remains available if loading fails or is canceled. Reference analysis uses a shared 48 kHz path with all five FFT sizes cached, while respecting the original file's bandwidth. This affects analysis only. + +### Waterfall + +* Independent native FFT analyzer with the same calibrated channel-max stereo spectrum as Spectrum +* Stores 60 spectrum slices per second on the audio clock, independent of terminal or desktop refresh rate +* Bounded 1–30 second history; defaults to 5 seconds +* Sparse, Balanced, and Dense ridge settings adapt to panel size while retaining the selected history duration +* Desktop shows a lightly softened live spectrum followed by its history, retaining distinct peaks and a common frequency axis +* Frequency scale, FFT size, smoothing, range, and spectral tilt controls +* Theme-colored ridges fading with age, or level-based heat colors +* Foreground ridges hide covered sections of older lines +* Available through the desktop add-scope menu and TUI layout editor (shortcut **8**) +* Desktop popouts and saved profiles are supported; existing layouts stay unchanged +* Dedicated Prism Waterfall VST3/AU/CLAP plugin shares the desktop renderer and native history, with settings saved in DAW projects ### Spectrogram @@ -134,16 +160,17 @@ Most of Prism's analysis runs in native C++, with the same DSP implementations r ### Rolling capture * Keeps the previous 5, 10, 30, or 60 seconds of audio available without starting a recording beforehand -* Buffer is stored as PCM and exported as a standard RIFF/WAV file +* Buffer retains 32-bit floating-point samples and exports standard RIFF/WAV files * Mono and stereo capture are supported -* Exported clips use 16-bit PCM +* Choose 16-bit PCM (default) or 32-bit float WAV export in Rolling Capture settings; the choice is remembered across launches * The export path supports source sample rates up to 384 kHz * Captured audio can be dragged directly out of Prism as a file ### DAW plugins -* All seven Prism analyzers are built from the same native DSP source used by the desktop application -* VST3 on Windows, macOS, and Linux +* All eight Prism analyzers are built from the same native DSP source used by the desktop application +* Prism Bridge streams one DAW track or bus into the standalone app without changing the track audio +* VST3 and CLAP on Windows, macOS, and Linux * AU on macOS * Mono and stereo host layouts are supported * Analyzer plugins are pure pass-through: Prism does not modify the host's audio buffer @@ -154,7 +181,7 @@ Most of Prism's analysis runs in native C++, with the same DSP implementations r ### Terminal UI -* Native C++ frontend using the same Spectrum, Oscilloscope, Vectorscope, VU, Loudness, Spectrogram, Waveform, and system-capture implementations +* Native C++ frontend using the same Spectrum, Waterfall, Oscilloscope, Vectorscope, VU, Loudness, Spectrogram, Waveform, and system-capture implementations * 60 FPS default rendering * Experimental 120 FPS mode * Compatibility mode uses 256-color output at up to 60 FPS @@ -170,6 +197,7 @@ Most of Prism's analysis runs in native C++, with the same DSP implementations r * Desktop visualizers can target 10, 30, 60, 120, or 144 FPS, or synchronize to the display refresh rate * Spectrum, Spectrogram, Oscilloscope, and Waveform support interactive measurement overlays * Measurement coordinates account for scope rotation and mirroring, so displayed readouts continue to correspond to the underlying signal after transforming a scope +* Linked Analysis maps only compatible semantic axes between scopes and hides guides when an exact value is outside the target's visible range
@@ -180,31 +208,52 @@ Prism can capture system output directly through CoreAudio on macOS, WASAPI on W No virtual audio cable is required for normal system capture. +The default rack measured [around 7 ms median capture-to-render latency](docs/latency.md) on an M5 Pro at 120 FPS. + The main rack and scope popouts can use solid, blurred, or clear backgrounds, making Prism usable as a normal desktop application or as a set of unobtrusive overlays. Prism can also live in the system tray, start automatically with your computer, and launch either normally or out of the way when you want it running all the time. ## Rolling Capture -Prism can continuously keep the last **5, 10, 30, or 60 seconds** of audio in memory. +Prism can continuously keep the last 5, 10, 30, or 60 seconds of audio in memory. When you hear something you want to keep, drag the buffered audio out of Prism as a WAV file. There is no need to start recording beforehand. +Choose 16-bit PCM for smaller files or 32-bit float to preserve captured floating-point precision and samples above full scale for DAW editing. Changing the format applies to the next export without clearing buffered audio. Exports retain the source sample rate and do not normalize or dither audio. + +The float buffer uses approximately 23 MB for 60 seconds of stereo at 48 kHz. Rolling Capture allocates no recorder buffer while off. + ## DAW Plugins Every Prism scope is also available as a DAW plugin. -Drop a **Spectrum**, **Oscilloscope**, **Vectorscope**, **Spectrogram**, **VU Meter**, **Loudness Meter**, or **Waveform** onto a track and analyze it using the same interface and analysis engine as the desktop app. +Drop a Spectrum, Oscilloscope, Vectorscope, Spectrogram, VU Meter, Loudness Meter, or Waveform onto a track and analyze it using the same interface and analysis engine as the desktop app. -* **VST3** on Windows, macOS, and Linux -* **AU** on macOS +* VST3 and CLAP on Windows, macOS, and Linux +* AU on macOS * Settings are stored with your project * Plugins follow your Prism themes and profiles * Audio passes through untouched -Tested in Ableton Live, FL Studio, and Reaper. +Prism also installs Prism Bridge, a lightweight pass-through plugin for sending +one DAW track or bus to the standalone application. Keep Prism open, insert Bridge +on the source you want to inspect, then choose it under DAW Bridges in Prism's +audio selector. Multiple Bridge instances can be available at once; only the one +you select streams audio. Waveform and Spectrogram can optionally show host-derived +Bars + Beats or Seconds rulers, including loop, seek, and packet-gap seams. + +Bridge is available as VST3 and CLAP on Windows, macOS, and Linux and as AU on macOS. The AU +is intentionally not marked sandbox-safe because it requires localhost access. +Protocol details and host-validation notes are in [docs/daw-bridge.md](docs/daw-bridge.md). + +Existing VST3/AU versions have been tested in Ableton Live, FL Studio, and Reaper. +CLAP requires a DAW that supports CLAP; see [plugin build and validation notes](plugin/README.md). -The plugins install alongside Prism, so there is no separate download. +The plugins ship alongside Prism. The Windows installer offers separate VST3 and +CLAP checkboxes, both enabled by default. The macOS package installs VST3, AU, and +CLAP; Linux deb/rpm packages install VST3 and CLAP. Archives support manual plugin +installation; the Linux AppImage remains app-only. See [installation instructions](plugin/README.md). ## Terminal UI @@ -258,7 +307,7 @@ On Windows, the NSIS installer adds `prism-tui` to the machine `PATH`. Open a ne ## Building from Source -**Prerequisites:** Node.js 18+, npm, CMake 3.22+, and a C++ compiler toolchain. +**Prerequisites:** Node.js 20.19+ or 22.12+ (Node 22.x is used in CI), npm, CMake 3.22+, and a C++ compiler toolchain. | Platform | Toolchain | | -------- | ------------------------------------------------------------------------------------------------------- | @@ -266,6 +315,10 @@ On Windows, the NSIS installer adds `prism-tui` to the machine `PATH`. Open a ne | Windows | Visual Studio Build Tools | | Linux | `build-essential`, `python3`, `libasound2-dev`, `libpulse-dev`, `libgtk-3-dev`, `libwebkit2gtk-4.1-dev` | +To build and test Linux from Windows using WSL2 or a Fedora VM over SSH, see +[Linux testing](docs/linux-testing.md). After setup, run `npm run test:linux`; +add `-- --target fedora --suite full` for Fedora plugin and package checks. + ```bash git clone https://github.com/Boof2015/prism.git cd prism @@ -274,12 +327,21 @@ npm install The `postinstall` script compiles the native C++ module for your platform. +Use `npm run rebuild:native` to rebuild it for the installed Electron version +before running the desktop app. `npm run build:native` builds for your current +Node.js version, for example when running native tests directly with Node. +These commands select the project's `node-gyp` dependency explicitly to avoid +conflicts with the older copy bundled by Electron's build tools on Windows. + ```bash npm run dev # Development npm run build # Build application assets npm run configure:tui # Configure the standalone CMake project npm run build:tui # Build prism-tui npm run test:tui # Build and run native TUI tests +npm run build:plugins # Build bundled UI and native DAW plugins +npm run test:plugins # Build plugins and run native plugin tests +npm run install:plugins # Install built plugins (Windows prompts for UAC) npm run test:lufsmeter-native # Run generated BS.1770/EBU true-peak vectors npm run dist # Package for current platform npm run dist:mac # macOS @@ -295,7 +357,7 @@ npm run test:lufsmeter-ebu -- /path/to/extracted-ebu-loudness-test-set The TUI build downloads the pinned FTXUI source through CMake. Linux also requires the PulseAudio development package. -The DAW plugins build with CMake from the [`plugin/`](plugin/) directory. See [`plugin/README.md`](plugin/README.md) for per-platform build and installation details. +`npm run build:plugins` compiles the DAW plugins locally. `npm run install:plugins` copies the built files, requesting Windows UAC only for the protected copy; macOS/Linux default to user plugin folders. `npm run dist` builds and stages the complete plugin set before packaging on the current OS (the Linux AppImage remains app-only). See [`plugin/README.md`](plugin/README.md) for prerequisites and options. ## Astra Integration diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index f27793c..3fae947 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -26,3 +26,124 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Reference audio decoding and resampling + +Prism includes dr_libs (WAV/AIFF, FLAC, MP3 decoding) and r8brain-free-src. Pinned revisions and license texts are provided in `native/vendor/REFERENCE_DEPENDENCIES.md` and each vendored library directory. + +Sample rate converter designed by Aleksey Vaneev of Voxengo. + +### dr_libs license + +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2020 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +### r8brain license + +MIT License + +Copyright (c) 2013-2026 Aleksey Vaneev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## CLAP plugin support + +Prism plugins link clap-juce-extensions at `9fbefae3d9c3d130aafb558c1ec15427a4bd24be`, +CLAP at `29ffcc273be7c7c651f6c9953b99e69700e2387a`, and clap-helpers at +`a61bcdf0ecc2c8db1e80bfe8bf9cb7e8d9fd2bbc`. + +### clap-juce-extensions + +Copyright 2019-2020, Paul Walker + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### CLAP and clap-helpers + +MIT License + +Copyright (c) 2021 Alexandre BIQUE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/benchmarks/2026-09-16/instrumentation.patch b/docs/benchmarks/2026-09-16/instrumentation.patch new file mode 100644 index 0000000..0a47322 --- /dev/null +++ b/docs/benchmarks/2026-09-16/instrumentation.patch @@ -0,0 +1,1610 @@ +diff --git a/.gitignore b/.gitignore +index 02869b1..40f1ec5 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -37,3 +37,6 @@ yarn-error.log* + + # notes + untitled-1 ++ ++# Local latency benchmark artifacts (raw records, traces, isolated settings) ++benchmark-results/ +diff --git a/package.json b/package.json +index 827d674..20ce6c5 100644 +--- a/package.json ++++ b/package.json +@@ -4,6 +4,9 @@ + "description": "Open-source audio metering and visualization tool", + "main": "./out/main/index.js", + "scripts": { ++ "bench:latency": "node scripts/benchmark/run.mjs", ++ "bench:latency:report": "node scripts/benchmark/report.mjs", ++ "test:latency": "node scripts/benchmark/test.mjs", + "dev": "node ./scripts/build/writeAppBuildMetadata.cjs && electron-vite dev", + "dev:mac": "node ./scripts/build/writeAppBuildMetadata.cjs && env -u ELECTRON_RUN_AS_NODE electron-vite dev", + "build": "electron-vite build && node ./scripts/build/writeAppBuildMetadata.cjs", +diff --git a/scripts/benchmark/README.md b/scripts/benchmark/README.md +new file mode 100644 +index 0000000..81f74ac +--- /dev/null ++++ b/scripts/benchmark/README.md +@@ -0,0 +1,155 @@ ++# Local capture-to-render submission benchmark ++ ++This branch observes Prism's native macOS system-audio capture and its existing ++Spectrum, Oscilloscope, Vectorscope, and VU render paths. It does not change capture ++buffers, polling delays, frame scheduling, DSP settings, or analysis algorithms. ++ ++This revision measures upstream `0.3.1` at ++`2a2d01d32da824d03e60440acd505f3d890a4e6f` (vectorscope optimization). ++The original `latency-benchmark` branch and its results measure `166e0f0`. ++The protocol, analysis settings, timing probes, cooldown, and thermal observer are ++unchanged, so the new run can be compared with that baseline on the same setup. ++Each report records its own exact base commit and instrumentation patch; the ++previous result directory is never overwritten. The current upstream revision ++has no frame-scheduler changes relative to the original baseline. ++ ++## Run ++ ++```sh ++npm run test:latency ++npm run typecheck ++npm run bench:latency -- --quick ++npm run bench:latency ++``` ++ ++The full run takes approximately 29 minutes. Keep the benchmark window visible ++and avoid changing display settings or audio devices during measurement. A quiet, ++changing stereo tone plays through the current default output using `afplay`. ++Other system audio is captured too; stop unrelated playback for a controlled run. ++No physical measurement equipment or virtual audio driver is needed. ++ ++The runner builds production renderer/main/preload bundles and rebuilds the native ++addon for the installed Electron version. It verifies native exports in that ++Electron runtime and refuses to benchmark a missing native capture or DSP module. ++After compilation, the full runner waits at least two minutes, including one ++continuous minute at nominal macOS thermal state (ten-minute timeout). This uses ++a separate Electron process without a rendering window. Quick diagnostics skip ++this wait. The measured app records thermal state and power source at 1 Hz and ++on OS notifications into preallocated main-process memory, plus advertised CPU ++speed-limit changes. A missing speed-limit notification means unknown, not 100%. ++The same observer is active for baselines and probes. Nothing is subtracted from ++latency based on thermals; warm runs remain part of the report. ++The benchmark preload uses the checkout's native addon path; it does not set ++`NODE_ENV=development`. macOS may request audio-capture permission for Electron. ++ ++Use `--output benchmark-results/my-run` to choose a new artifact directory. Existing ++run directories are never overwritten. Each run uses its own Electron user-data ++directory, leaving regular Prism preferences untouched. Nothing is pushed, merged, ++or installed. `out/` and the native build output are regenerated as usual. ++ ++`--quick` exercises all configurations with 2-second warmups and 3-second samples. ++Quick results are always marked diagnostic and cannot produce publication wording. ++ ++The full matrix uses Spectrum alone, Oscilloscope alone, and the default four-scope ++rack. Each configuration runs at display-sync and the 60 FPS target. Each cell has ++one 60-second frame-only baseline and three 60-second probe runs, each preceded by ++10 seconds of warmup. The fixed window is 1200×360 logical pixels. Default scope ++settings are applied without reducing FFT size, smoothing, persistence, or pitch ++locking to obtain a faster result. ++ ++## What is measured ++ ++Each native chunk retains its existing monotonic capture timestamp and sequence. ++The observer records renderer receipt, scope consumption, and the end of the ++successful draw invocation that consumed it. All chunks in a batch are counted, ++once per scope; multiple chunks can share one render frame. Repainting old state ++without consuming new chunks adds no latency samples. ++ ++The endpoint is completion of canvas drawing commands on the renderer thread. ++This is **not** GPU completion, compositor presentation, pixel illumination, or ++the first visible response to a specific sound. The start is inside the native ++capture callback, **not** the original sample's hardware time. Audio buffering ++before that timestamp is excluded. A scope can render selected history or ++smoothed values after consuming newer data; FFT windows, pitch locking, and meter ++ballistics require a separate signal-response experiment. ++ ++64 native-clock reads, bracketed by `performance.now()`, are taken before and after ++each run. The narrowest bracket at each endpoint is selected and the union bounds ++the clock offset. Raw clock samples, drift, uncertainty, and each stage's timing ++are retained. The method assumes no unobserved clock excursion between endpoints. ++Renderer timer granularity is measured outside each run. Calibration intervals are ++widened by that quantum (at least 0.1 ms), with another quantum allowed at the draw ++endpoint. Impossible ordering is rejected; receipt durations below resolution are ++reported as unresolved instead of clamped to zero. ++ ++Native queue overwrites, sequence gaps, trimmed chunks, router overwrites, missing ++receipt metadata, observer-buffer exhaustion, skipped draws, and session changes ++are reported. Chunks queued before observation began and chunks still queued at ++stop are reported as boundaries, not successful latency samples. Invalid runs ++cannot produce claim wording. ++ ++Observation uses preallocated typed arrays. Capture and draw hot paths do not ++write files, log events, or allocate record objects. Export and percentile sorting ++happen after stopping observation. The frame-only baseline retains the same small ++frame recorder but disables chunk probes. Its difference from probe runs is an ++observed workload difference, not an exact correction; nothing is subtracted. ++ ++## Artifacts and reporting ++ ++Each output directory contains: ++ ++- `REPORT.md` and `summary.json`: results, losses, clock bounds, overhead comparison, ++ stage distributions, and supported wording. ++- Per-run JSON: every chunk, chunk/scope/frame association, frame interval, clock ++ calibration sample, router counters, scope settings, and canvas dimensions. ++- `source-manifest.json` and `benchmark.patch`: exact base commit, source hashes, ++ instrumentation patch, and version. New untracked source files are included. ++- `native-build.json`: native addon hash and Electron version. ++- `machine.json`, `stimulus.json`, and `stimulus.wav`: machine/display/runtime and ++ deterministic external test signal. ++- `cooldown.json`, `thermal.json`, and per-run `environment`: cooldown outcome, ++ thermal/power observations, and reported CPU speed limits. `machine.json` also ++ records macOS power settings. Nominal thermal state does not prove an absence ++ of all frequency changes; these records do not establish causal slowdown. ++- `chromium-trace.json` and `trace-measurement.json`: a separate presentation ++ investigation, excluded from latency claims. ++ ++Regenerate a report with: ++ ++```sh ++npm run bench:latency:report -- benchmark-results/my-run ++``` ++ ++Rows are compact arrays with explicit `chunkColumns`, `recordColumns`, and ++`frameColumns` in the same JSON. Scope indices refer to `scopes`. Record outcomes ++are 1 (successful draw), 0 (skipped draw), or -1 (missing receipt metadata). JSON ++uses null for unavailable numeric timestamps; the analyzer rejects those records. ++ ++A claim names the hardware, capture path, configuration, version/base commit, and ++measurement endpoint. Median/p95 wording uses the largest per-run statistic across ++all three repeats rather than selecting the best run. An under-8-ms p95 statement ++requires every valid repeat's uncertainty-adjusted p95 to be below 8 ms. Observed ++maxima are not universal bounds. An instrumented-build result must not be presented ++as an uninstrumented physical-display measurement. ++ ++The separate trace contains `prism.frame...start/end` marks. Do not infer ++presentation from the nearest unrelated compositor event or add a nominal refresh ++interval. If chunk→frame→presentation cannot be established unambiguously, ++presentation remains unavailable. The report attempts a strict association through ++the enclosing RAF task and AnimationFrame interval, exact presentation ID and ++begin-frame source/sequence, and a fully presented compositor frame. Links and ++rejections are exported in `presentation-investigation.json`. Any resulting ++estimate is kept separate from the main measurements and publication wording. ++Chromium also documents estimated presentation ++feedback on some platforms, including macOS: ++https://chromium.googlesource.com/chromium/src.git/+/main/docs/life_of_a_frame.md ++ ++## Build isolation ++ ++`scripts/benchmark/electron.config.mjs` adds only the benchmark compile-time flag ++to the effective defaults used by the existing build command. The repository's ++`electron-vite.config.ts` is not auto-discovered by Electron Vite, which expects ++`electron.vite.config.*`; the benchmark does not change normal config discovery. ++Normal builds never install the benchmark renderer controller or allocate probe ++buffers. The branch-only main-process runner requires the explicit benchmark ++environment and output directory. +diff --git a/scripts/benchmark/check-native.cjs b/scripts/benchmark/check-native.cjs +new file mode 100644 +index 0000000..e18c7f9 +--- /dev/null ++++ b/scripts/benchmark/check-native.cjs +@@ -0,0 +1,8 @@ ++const assert = require('node:assert/strict') ++const addon = require(process.argv[2]) ++for (const key of ['spectrum', 'oscilloscope', 'vectorscope', 'vumeter', 'macosCapture']) { ++ assert.ok(addon[key], `Missing native export: ${key}`) ++} ++assert.equal(addon.macosCapture.getSupport().available, true, 'Native system capture unavailable') ++assert.equal(typeof addon.macosCapture.nowMilliseconds(), 'number') ++console.log('[latency] Native addon preflight passed in Electron', process.versions.electron, 'ABI', process.versions.modules) +diff --git a/scripts/benchmark/cooldown.cjs b/scripts/benchmark/cooldown.cjs +new file mode 100644 +index 0000000..e07a1f6 +--- /dev/null ++++ b/scripts/benchmark/cooldown.cjs +@@ -0,0 +1,32 @@ ++const { app, powerMonitor, powerSaveBlocker } = require('electron') ++const { writeFile } = require('node:fs/promises') ++const { mkdirSync } = require('node:fs') ++const { join } = require('node:path') ++const { setTimeout: delay } = require('node:timers/promises') ++ ++const userData = join(process.env.PRISM_LATENCY_BENCHMARK_DIR, 'cooldown-user-data') ++mkdirSync(userData, { recursive: true }) ++app.setPath('userData', userData) ++app.whenReady().then(async () => { ++ app.dock?.hide() ++ const blocker = powerSaveBlocker.start('prevent-display-sleep') ++ const quick = process.env.PRISM_BENCH_DURATION === '3' ++ const minimumSeconds = quick ? 0 : 120 ++ const requiredNominalSeconds = quick ? 0 : 60 ++ const start = performance.now(), samples = [] ++ let nominalSince = null, success = false ++ console.log(`[latency] Post-build cooldown: at least ${minimumSeconds}s, including ${requiredNominalSeconds}s continuously nominal; maximum 10 minutes.`) ++ while (true) { ++ const now = performance.now(), elapsedSeconds = (now - start) / 1000 ++ const thermalState = powerMonitor.getCurrentThermalState() ++ samples.push({ elapsedSeconds, thermalState, onBattery: powerMonitor.isOnBatteryPower() }) ++ nominalSince = thermalState === 'nominal' ? nominalSince ?? now : null ++ if (quick || (elapsedSeconds >= minimumSeconds && nominalSince !== null && now - nominalSince >= requiredNominalSeconds * 1000)) { success = true; break } ++ if (elapsedSeconds >= 600) break ++ await delay(1000) ++ } ++ await writeFile(join(process.env.PRISM_LATENCY_BENCHMARK_DIR, 'cooldown.json'), JSON.stringify({ success, minimumSeconds, requiredNominalSeconds, samples }, null, 2)) ++ powerSaveBlocker.stop(blocker) ++ console.log(success ? '[latency] Cooldown complete.' : '[latency] Nominal thermal state was not sustained; measurement was not started.') ++ app.exit(success ? 0 : 1) ++}).catch(error => { console.error(error); app.exit(1) }) +diff --git a/scripts/benchmark/electron.config.mjs b/scripts/benchmark/electron.config.mjs +new file mode 100644 +index 0000000..f737207 +--- /dev/null ++++ b/scripts/benchmark/electron.config.mjs +@@ -0,0 +1,8 @@ ++// The existing `npm run build` uses electron-vite's defaults: its auto-discovery ++// expects electron.vite.config.*, whereas this repo has electron-vite.config.ts. ++// Preserve those effective defaults; add only the benchmark compile-time flag. ++export default { ++ main: {}, ++ preload: {}, ++ renderer: { define: { __PRISM_LATENCY_BENCHMARK__: 'true' } }, ++} +diff --git a/scripts/benchmark/presentation.mjs b/scripts/benchmark/presentation.mjs +new file mode 100644 +index 0000000..311b927 +--- /dev/null ++++ b/scripts/benchmark/presentation.mjs +@@ -0,0 +1,64 @@ ++/** Conservative association using frame IDs and enclosing RAF tasks, never nearest timestamps. */ ++export function linkPresentation(events, raw, clock) { ++ const key = e => `${e.pid}:${e.tid}:${e.id2?.local ?? e.id2?.global ?? e.id}` ++ const intervals = [], stacks = new Map(), presentations = new Map(), reporters = new Map(), marks = new Map() ++ const append = (map, id, value) => map.set(id, [...(map.get(id) ?? []), value]) ++ const rafTasks = events.filter(e => e.name === 'FireAnimationFrame' && e.ph === 'X' && e.dur > 0) ++ for (const e of [...events].sort((a, b) => a.ts - b.ts)) { ++ if (e.name === 'AnimationFrame') { ++ if (e.ph === 'b') append(stacks, key(e), e) ++ if (e.ph === 'e') { ++ const start = stacks.get(key(e))?.pop() ++ if (start?.args?.id) intervals.push({ start, end: e }) ++ } ++ } ++ if (e.name === 'AnimationFrame::Presentation') append(presentations, `${e.pid}:${e.args?.id}`, e) ++ if (e.name === 'PipelineReporter' && e.ph === 'b') { ++ const info = e.args?.frame_reporter ++ if (info) append(reporters, `${e.pid}:${info.frame_source}:${info.frame_sequence}`, e) ++ } ++ const match = /^prism\.frame\.(\d+)\.([a-z]+)\.(start|end)$/.exec(e.name ?? '') ++ if (match) append(marks, `${match[1]}:${match[2]}:${match[3]}`, e) ++ } ++ const frameLinks = [], rejected = [] ++ for (const frame of raw.frames) { ++ const id = frame[0], scope = raw.scopes[frame[1]] ++ if (!frame[4]) continue ++ const starts = marks.get(`${id}:${scope}:start`) ?? [], ends = marks.get(`${id}:${scope}:end`) ?? [] ++ const fail = reason => rejected.push({ frame: id, scope, reason }) ++ if (starts.length !== 1 || ends.length !== 1) { fail('missing or duplicate scope markers'); continue } ++ const start = starts[0], end = ends[0] ++ const startCall = start.args?.data?.callTime, endCall = end.args?.data?.callTime ++ const endPerformance = end.args?.data?.startTime ++ if (![startCall, endCall, endPerformance].every(Number.isFinite) || start.pid !== end.pid || start.tid !== end.tid) { fail('invalid marker clock/thread'); continue } ++ const parents = intervals.filter(x => x.start.pid === start.pid && x.start.tid === start.tid && x.start.ts <= startCall && x.end.ts >= endCall) ++ const tasks = rafTasks.filter(x => x.pid === start.pid && x.tid === start.tid && x.ts <= startCall && x.ts + x.dur >= endCall) ++ if (parents.length !== 1 || tasks.length !== 1) { fail('ambiguous enclosing animation frame or RAF task'); continue } ++ const parent = parents[0].start, beginId = parent.args?.animation_frame_timing_info?.begin_frame_id ++ const present = presentations.get(`${parent.pid}:${parent.args.id}`) ?? [] ++ const pipeline = reporters.get(`${parent.pid}:${beginId?.source_id}:${beginId?.sequence_number}`) ?? [] ++ if (present.length !== 1 || pipeline.length !== 1 || pipeline[0].args.frame_reporter.state !== 'STATE_PRESENTED_ALL') { fail('missing/ambiguous presentation or incomplete compositor frame'); continue } ++ const p = present[0], presentId = p.args?.begin_frame_id ++ if (!beginId || beginId.source_id !== presentId?.source_id || beginId.sequence_number !== presentId?.sequence_number ++ || p.ts < endCall || endPerformance + clock.timerQuantumMs < frame[3]) { fail('frame identity or time ordering mismatch'); continue } ++ const rendererPresentationMs = endPerformance + (p.ts - endCall) / 1000 ++ frameLinks.push({ frame: id, scope, rendererPid: parent.pid, animationId: parent.args.id, ++ beginFrameSource: beginId.source_id, beginFrameSequence: beginId.sequence_number, ++ presentationTraceUs: p.ts, markerCallTraceUs: endCall, markerPerformanceMs: endPerformance, ++ rendererPresentationMs, drawToPresentationEstimateMs: rendererPresentationMs - frame[3] }) ++ } ++ const byFrame = new Map(frameLinks.map(link => [link.frame, link])) ++ const samples = [] ++ for (const r of raw.records) { ++ const link = byFrame.get(r[3]) ++ if (!link || r[8] !== 1 || !Number.isFinite(r[4])) continue ++ const latencyMs = link.rendererPresentationMs - (r[4] + clock.offset) ++ if (latencyMs >= 0) samples.push({ session: r[0], sequence: r[1], frame: r[3], scope: link.scope, latencyMs }) ++ } ++ return { available: samples.length > 0, source: 'Chromium AnimationFrame::Presentation; macOS presentation feedback is an estimate', ++ caveat: 'Separate tracing workload. Software clock bounds do not bound OS presentation-estimation error or physical pixel response.', ++ matchedFrames: frameLinks.length, rejectedFrames: rejected.length, matchedChunkScopeRecords: samples.length, ++ successfulChunkScopeRecords: raw.records.filter(r => r[8] === 1).length, ++ softwareTimingUncertaintyMs: clock.uncertaintyMs + clock.timerQuantumMs, ++ frameLinks, rejected, samples } ++} +diff --git a/scripts/benchmark/report.mjs b/scripts/benchmark/report.mjs +new file mode 100644 +index 0000000..8fd4cf5 +--- /dev/null ++++ b/scripts/benchmark/report.mjs +@@ -0,0 +1,120 @@ ++import { build } from 'esbuild' ++import { readdir, readFile, writeFile } from 'node:fs/promises' ++import { resolve, join } from 'node:path' ++import { pathToFileURL } from 'node:url' ++import { linkPresentation } from './presentation.mjs' ++ ++const fmt = value => value == null ? '—' : Number(value).toFixed(3) ++export async function generateReport(directory) { ++ const output = resolve(directory) ++ const bundled = join(output, 'analysis.mjs') ++ await build({ entryPoints: [resolve(import.meta.dirname, '../../src/renderer/benchmark/analyze.ts')], outfile: bundled, ++ bundle: true, platform: 'node', format: 'esm', define: { __PRISM_LATENCY_BENCHMARK__: 'false' } }) ++ const { analyzeMeasurement, statistics } = await import(`${pathToFileURL(bundled).href}?t=${Date.now()}`) ++ const read = async name => JSON.parse(await readFile(join(output, name), 'utf8')) ++ const manifest = await read('source-manifest.json') ++ const machine = await read('machine.json').catch(() => null) ++ const completed = await read('complete.json').catch(() => null) ++ const names = (await readdir(output)).filter(name => /^\d+-.*\.json$/.test(name)).sort() ++ const results = [] ++ for (const name of names) { ++ const raw = await read(name) ++ results.push({ id: raw.id, config: raw.config, target: raw.target, mode: raw.mode, repeat: raw.repeat, ++ metadata: raw.metadata, environment: raw.environment, ...analyzeMeasurement(raw) }) ++ } ++ await writeFile(join(output, 'summary.json'), JSON.stringify(results, null, 2)) ++ const lines = ['# Prism capture-to-render submission benchmark', '', ++ `Base: \`${manifest.baseCommit}\` (${manifest.version}); branch: \`${manifest.branch}\`.`, ++ `Instrumentation patch SHA-256: \`${manifest.patchSha256}\`.`, '', ++ 'Endpoint: the existing native timestamp inside the CoreAudio capture callback → return from a successful scope draw invocation that consumed that chunk. Every consumed chunk is counted once per scope. This measures processing and draw-command submission, not physical presentation or the time for a signal change to become visible.', '', ++ 'Excluded: audio buffering before the native timestamp, GPU execution/compositing, display scanout/pixel response, and the perceptual response of FFT windows, pitch locking, persistence, and meter ballistics. A scope may display selected history after consuming newer audio.', '', ++ `Status: ${completed ? 'suite completed' : 'incomplete suite'}${manifest.quick ? '; diagnostic quick run — no publication claim' : ''}.`, ++ ...(machine ? [`Machine: ${machine.cpu}, ${machine.model}, macOS ${machine.systemVersion}, ${machine.arch}.`, ++ `Window: ${machine.bounds.width}×${machine.bounds.height}; display scale: ${machine.display.scaleFactor}; reported display frequency: ${machine.display.displayFrequency ?? 'unavailable'} Hz.`, ++ `Electron ${machine.versions.electron}; Chromium ${machine.versions.chrome}. ${machine.warmupSeconds}s warmup; ${machine.durationSeconds}s measurement; ${machine.repeats} probe repeats.`] : []), '', ++ 'Complete scope settings, capture device/rate, canvas dimensions, and start/end metadata are in each raw JSON. See machine.json, stimulus.json, source-manifest.json, and benchmark.patch for reproduction.', '', ++ '## Per-run results', '', ++ '| Run | Scope | Median ms | p95 ms | p99 ms | Max ms | N | Timing ±ms | Observed FPS | Valid |', ++ '|---|---|---:|---:|---:|---:|---:|---:|---:|---|'] ++ for (const r of results.filter(r => r.mode === 'probe')) for (const s of r.scopes) { ++ lines.push(`| ${r.id} | ${s.scope} | ${fmt(s.latencyMs.median)} | ${fmt(s.latencyMs.p95)} | ${fmt(s.latencyMs.p99)} | ${fmt(s.latencyMs.max)} | ${s.latencyMs.count} | ${fmt(r.latencyUncertaintyMs)} | ${fmt(s.observedFps)} | ${r.valid ? 'yes' : 'no'} |`) ++ } ++ lines.push('', '## Stage timings', '', ++ '| Run | Scope | Capture → receipt p95 ms | Receipt → consumption p95 ms | Consumption → draw end p95 ms | Unresolved receipt durations |', ++ '|---|---|---:|---:|---:|---:|') ++ for (const r of results.filter(r => r.mode === 'probe')) for (const s of r.scopes) { ++ lines.push(`| ${r.id} | ${s.scope} | ${fmt(s.captureToReceiptMs.p95)} | ${fmt(s.receiptToConsumptionMs.p95)} | ${fmt(s.consumptionToDrawEndMs.p95)} | ${s.unresolvedDelivery} |`) ++ } ++ lines.push('', 'Stage percentiles are separate distributions and must not be added together. Receipt intervals that overlap clock uncertainty are unresolved and excluded only from the receipt-duration distribution; their capture-to-draw measurements remain subject to the full uncertainty bounds.') ++ lines.push('', '## Losses and exclusions', '') ++ for (const r of results) lines.push(`- ${r.id}: ${r.problems.join('; ') || 'no validity failures'}. ${r.mode === 'baseline' ? 'Chunk probes disabled; chunk-loss counters unavailable.' : `Counters: ${JSON.stringify(r.counters)}. Pending at stop: ${r.scopes.map(s => `${s.scope}=${s.unconsumedAtStop}`).join(', ')}.`}`) ++ lines.push('', 'Boundary chunks were queued before observation began and are reported separately; chunks still queued at stop are right-censored. Neither is treated as a successful low-latency sample. Skipped draws, unexplained losses, duplicate consumption, and invalid clock ordering invalidate that run for a claim.', '', ++ '## Probe overhead comparison', '', ++ 'Baseline disables capture/chunk probes but retains the same minimal frame-duration recorder. Differences below are observed workload differences, not a precise causal overhead correction; run order and system load can also affect them. No overhead is subtracted from latency.', '', ++ '| Configuration / target | Scope | Baseline draw p95 ms | Probe draw p95 range ms | Baseline FPS | Probe FPS range |', ++ '|---|---|---:|---|---:|---|') ++ for (const b of results.filter(r => r.mode === 'baseline')) for (const bs of b.scopes) { ++ const probes = results.filter(r => r.mode === 'probe' && r.config === b.config && r.target === b.target).map(r => r.scopes.find(s => s.scope === bs.scope)).filter(Boolean) ++ if (!probes.length) continue ++ const range = values => `${fmt(Math.min(...values))}–${fmt(Math.max(...values))}` ++ lines.push(`| ${b.config} / ${b.target} | ${bs.scope} | ${fmt(bs.frameWorkMs.p95)} | ${range(probes.map(s => s.frameWorkMs.p95))} | ${fmt(bs.observedFps)} | ${range(probes.map(s => s.observedFps))} |`) ++ } ++ lines.push('', '## Thermal and power observations', '', ++ 'The full runner waits at least two minutes after compilation, including one continuous minute at nominal thermal state. Thermal state and AC/battery power are then sampled at 1 Hz in the main process and on OS notifications, using preallocated memory. CPU speed limits are only known after an OS notification; unavailable values do not mean 100%. Both baseline and probe runs include this observer. See cooldown.json, thermal.json, per-run environment records, and machine.json power settings.', '', ++ '| Run | Observed thermal states | Battery observed | Lowest reported CPU speed limit % |', ++ '|---|---|---|---:|') ++ for (const r of results) { ++ const e = r.environment ++ lines.push(`| ${r.id} | ${e?.thermalStates.join(', ') ?? 'unavailable'} | ${e ? e.batteryObserved ? 'yes' : 'no' : 'unavailable'} | ${fmt(e?.minimumReportedCpuSpeedLimitPercent)} |`) ++ } ++ lines.push('', 'These observations can identify reported thermal pressure, but cannot quantify how many milliseconds it caused. Nominal state does not rule out all frequency changes. Run order/configuration and background load confound a causal cool-versus-hot comparison; no thermal correction is subtracted.', '', '## Supported wording', '') ++ let claims = 0 ++ if (!manifest.quick && completed) { ++ for (const config of ['spectrum', 'oscilloscope', 'default-rack']) for (const target of ['display-sync', 60]) { ++ const runs = results.filter(r => r.config === config && r.target === target && r.mode === 'probe') ++ if (runs.length !== 3 || runs.some(r => !r.valid || r.durationMs < 59900)) continue ++ for (const scope of runs[0].scopes.map(s => s.scope)) { ++ const samples = runs.map(r => r.scopes.find(s => s.scope === scope)) ++ const p95 = Math.max(...samples.map(s => s.latencyMs.p95)) ++ const p95Upper = Math.max(...samples.map(s => s.upperLatencyMs.p95)) ++ const median = Math.max(...samples.map(s => s.latencyMs.median)) ++ const uncertainty = Math.max(...runs.map(r => r.latencyUncertaintyMs)) ++ lines.push(`- On ${machine.cpu} / macOS ${machine.systemVersion}, an instrumented build of Prism ${manifest.version} based on ${manifest.baseCommit.slice(0, 7)} measured at most ${fmt(median)} ms median and ${fmt(p95)} ms p95 across three 60-second runs for **${scope}**, using **${config}, ${target} target**, native system capture at ${runs[0].metadata.capture.sampleRate} Hz. Timing uncertainty was at most ±${fmt(uncertainty)} ms. ${p95Upper < 8 ? 'All three observed p95 upper bounds were below 8 ms.' : 'These results do not support an under-8-ms p95 claim.'}`) ++ claims++ ++ } ++ } ++ } ++ if (!claims) lines.push('No publication-ready latency claim is supported by this dataset. See validity failures and suite status above.') ++ lines.push('', 'These statements apply only to the tested configuration and instrumentation patch. Observed maxima are not guaranteed bounds. No claim of capture-to-physical-display latency is made.', '', ++ '## Presentation investigation', '', ++ 'Presentation latency: **unavailable unless separately established from an unambiguous chunk/frame/presentation association**. The separate chromium-trace.json and trace-measurement.json contain scope frame markers and Chromium events; their mere proximity is not a valid association. Presentation feedback may itself be estimated on macOS. No nominal refresh interval is added to manufacture an end-to-end result.', '', ++ '## Clock method', '', ++ '64 synchronous native-clock calls are bracketed by performance.now() before and after each run. The narrowest round-trip bracket at each endpoint is selected, then their union bounds the offset used for the run. This assumes no unobserved clock excursion between endpoints. Calibration bounds include the measured renderer timer quantum (at least 0.1 ms), and draw-end uncertainty includes an additional quantum. The midpoint supplies reported estimates; conservative p95 checks use the older possible capture time and later possible draw time. Receipt durations below timer resolution are counted as unresolved, never clamped or presented as zero latency. Complete calibration samples and endpoint drift are exported.', '') ++ try { ++ const traceRaw = await read('trace-measurement.json') ++ const trace = await read('chromium-trace.json') ++ const traceAnalysis = analyzeMeasurement(traceRaw) ++ const presentation = linkPresentation(trace.traceEvents, traceRaw, traceAnalysis.clock) ++ await writeFile(join(output, 'presentation-investigation.json'), JSON.stringify({ ...presentation, traceValidity: traceAnalysis.problems }, null, 2)) ++ lines.push('## Separate trace findings', '', ++ `Matched ${presentation.matchedFrames} scope frames and ${presentation.matchedChunkScopeRecords}/${presentation.successfulChunkScopeRecords} successful chunk/scope records; ${presentation.rejectedFrames} frame associations were rejected.`, ++ 'Associations require unique scope markers inside one RAF task and one AnimationFrame interval, exact animation/presentation IDs, matching begin-frame source/sequence IDs, and a STATE_PRESENTED_ALL compositor reporter.', '') ++ if (presentation.available && traceAnalysis.valid) { ++ lines.push('| Scope | Estimated capture → presentation median ms | Estimated p95 ms | Matched records |', '|---|---:|---:|---:|') ++ for (const scope of traceRaw.metadata.visibleScopes) { ++ const stats = statistics(presentation.samples.filter(s => s.scope === scope).map(s => s.latencyMs)) ++ lines.push(`| ${scope} | ${fmt(stats.median)} | ${fmt(stats.p95)} | ${stats.count} |`) ++ } ++ lines.push('', `Software timestamp uncertainty is approximately ±${fmt(presentation.softwareTimingUncertaintyMs)} ms, **plus unquantified macOS presentation-estimation error**. These are diagnostic estimates from the separate tracing workload, not physical display measurements or publication claims.`) ++ } else lines.push(`Presentation estimate unavailable: ${traceAnalysis.problems.join('; ') || 'no unambiguous associations'}.`) ++ } catch (error) { ++ lines.push('## Separate trace findings', '', `Presentation estimate unavailable: ${String(error)}.`) ++ } ++ await writeFile(join(output, 'REPORT.md'), lines.join('\n')) ++ console.log(`Report: ${join(output, 'REPORT.md')}`) ++ return results ++} ++if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) { ++ if (!process.argv[2]) throw new Error('Usage: npm run bench:latency:report -- benchmark-results/') ++ await generateReport(process.argv[2]) ++} +diff --git a/scripts/benchmark/run.mjs b/scripts/benchmark/run.mjs +new file mode 100644 +index 0000000..8e6a893 +--- /dev/null ++++ b/scripts/benchmark/run.mjs +@@ -0,0 +1,98 @@ ++import { spawn, execFileSync } from 'node:child_process' ++import { mkdir, readFile, writeFile, open } from 'node:fs/promises' ++import { resolve, join } from 'node:path' ++import { createHash } from 'node:crypto' ++import { createRequire } from 'node:module' ++import { generateReport } from './report.mjs' ++ ++const root = resolve(import.meta.dirname, '../..') ++const args = process.argv.slice(2) ++const quick = args.includes('--quick') ++const outputIndex = args.indexOf('--output') ++if (outputIndex >= 0 && !args[outputIndex + 1]) throw new Error('--output requires a directory') ++const output = resolve(outputIndex >= 0 ? args[outputIndex + 1] : join(root, 'benchmark-results', new Date().toISOString().replaceAll(':', '-'))) ++if (process.platform !== 'darwin') throw new Error('This first benchmark supports native macOS system capture only') ++const run = (command, argv, options = {}) => new Promise((resolveRun, reject) => { ++ const child = spawn(command, argv, { cwd: root, stdio: 'inherit', ...options }) ++ child.on('error', reject) ++ child.on('exit', (code, signal) => code === 0 ? resolveRun() : reject(new Error(`${command} exited ${code ?? signal}`))) ++}) ++await mkdir(output, { recursive: true }) ++// Refuse accidental overwrite of a previous experiment. ++await writeFile(join(output, 'run.lock'), `${process.pid}\n`, { flag: 'wx' }) ++await mkdir(join(output, 'user-data'), { recursive: true }) ++const git = (...argv) => execFileSync('git', argv, { cwd: root, encoding: 'utf8' }) ++const baseCommit = '2a2d01d32da824d03e60440acd505f3d890a4e6f' ++const measurementCommit = git('rev-parse', 'HEAD').trim() ++let patch = git('diff', '--binary', baseCommit) ++const untracked = git('ls-files', '--others', '--exclude-standard', '-z').split('\0').filter(Boolean) ++for (const file of untracked) { ++ try { patch += git('diff', '--no-index', '--binary', '--', '/dev/null', file) } ++ catch (error) { if (error.status !== 1) throw error; patch += error.stdout } ++} ++await writeFile(join(output, 'benchmark.patch'), patch) ++const files = [...new Set([...git('ls-files', '-z').split('\0').filter(Boolean), ...untracked])] ++const hashes = {} ++for (const file of files) { ++ try { hashes[file] = createHash('sha256').update(await readFile(join(root, file))).digest('hex') } ++ catch (error) { if (error.code !== 'ENOENT') throw error; hashes[file] = null } ++} ++const env = { ...process.env, PRISM_LATENCY_BENCHMARK: '1', PRISM_LATENCY_BENCHMARK_DIR: output, ++ PRISM_BENCH_DURATION: quick ? '3' : '60', PRISM_BENCH_WARMUP: quick ? '2' : '10', PRISM_BENCH_REPEATS: quick ? '1' : '3' } ++delete env.ELECTRON_RUN_AS_NODE ++delete env.ELECTRON_RENDERER_URL ++await writeFile(join(output, 'source-manifest.json'), JSON.stringify({ ++ baseCommit, measurementCommit, branch: git('branch', '--show-current').trim(), version: JSON.parse(await readFile(join(root, 'package.json'), 'utf8')).version, ++ patchSha256: createHash('sha256').update(patch).digest('hex'), files: hashes, ++ quick, createdAt: new Date().toISOString(), command: process.argv, nodeVersion: process.version, ++}, null, 2)) ++ ++// A file-backed external player keeps synthesis work off Prism's rendering thread. ++const sampleRate = 48000, seconds = Number(env.PRISM_BENCH_DURATION) + Number(env.PRISM_BENCH_WARMUP) + 15 ++const totalFrames = Math.ceil(sampleRate * seconds), dataSize = totalFrames * 4 ++const header = Buffer.alloc(44) ++header.write('RIFF'); header.writeUInt32LE(36 + dataSize, 4); header.write('WAVEfmt ', 8) ++header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(2, 22) ++header.writeUInt32LE(sampleRate, 24); header.writeUInt32LE(sampleRate * 4, 28) ++header.writeUInt16LE(4, 32); header.writeUInt16LE(16, 34); header.write('data', 36); header.writeUInt32LE(dataSize, 40) ++const wav = await open(join(output, 'stimulus.wav'), 'w') ++await wav.write(header) ++const block = Buffer.alloc(sampleRate * 4) ++let phaseL = 0, phaseR = 0 ++for (let first = 0; first < totalFrames; first += sampleRate) { ++ const frames = Math.min(sampleRate, totalFrames - first) ++ for (let i = 0; i < frames; i++) { ++ const t = (first + i) / sampleRate ++ const step = Math.floor(t / 0.73) % 4 ++ phaseL += 2 * Math.PI * [220, 440, 880, 330][step] / sampleRate ++ phaseR += 2 * Math.PI * [330, 550, 660, 440][step] / sampleRate ++ const amplitude = 0.025 * (0.7 + 0.3 * Math.sin(2 * Math.PI * 1.37 * t)) ++ block.writeInt16LE(Math.round(32767 * amplitude * Math.sin(phaseL)), i * 4) ++ block.writeInt16LE(Math.round(32767 * amplitude * Math.sin(phaseR)), i * 4 + 2) ++ } ++ await wav.write(block.subarray(0, frames * 4)) ++} ++await wav.close() ++await writeFile(join(output, 'stimulus.json'), JSON.stringify({ sampleRate, channels: 2, format: 'PCM16', seconds, ++ peakAmplitude: 0.025, amplitudeModulationHz: 1.37, frequencyStepSeconds: 0.73, ++ leftHz: [220, 440, 880, 330], rightHz: [330, 550, 660, 440], ++ sha256: createHash('sha256').update(await readFile(join(output, 'stimulus.wav'))).digest('hex'), ++}, null, 2)) ++await run(process.execPath, [join(root, 'node_modules/electron-vite/bin/electron-vite.js'), 'build', '--config', 'scripts/benchmark/electron.config.mjs'], { env }) ++await run(process.execPath, ['scripts/build/writeAppBuildMetadata.cjs'], { env }) ++const require = createRequire(import.meta.url) ++const electron = require('electron') ++await run('npm', ['run', 'rebuild:native'], { env }) ++const nativePath = join(root, 'native/build/Release/visualizer_dsp.node') ++await run(electron, [join(root, 'scripts/benchmark/check-native.cjs'), nativePath], { env: { ...env, ELECTRON_RUN_AS_NODE: '1' } }) ++await writeFile(join(output, 'native-build.json'), JSON.stringify({ path: nativePath, ++ sha256: createHash('sha256').update(await readFile(nativePath)).digest('hex'), ++ electronVersion: require('electron/package.json').version, rebuiltFromSource: true, ++}, null, 2)) ++console.log(`Benchmark artifacts: ${output}\n${quick ? 'Diagnostic quick run; not suitable for published claims.' : 'Full suite: approximately 29 minutes. Keep Prism visible; a quiet test tone will play.'}`) ++await run(electron, [join(root, 'scripts/benchmark/cooldown.cjs')], { env }) ++try { ++ await run(electron, [root], { env }) ++} finally { ++ await generateReport(output) ++} +diff --git a/scripts/benchmark/test.mjs b/scripts/benchmark/test.mjs +new file mode 100644 +index 0000000..e94bad0 +--- /dev/null ++++ b/scripts/benchmark/test.mjs +@@ -0,0 +1,24 @@ ++import { build } from 'esbuild' ++import { mkdtemp, rm } from 'node:fs/promises' ++import { tmpdir } from 'node:os' ++import { join, resolve } from 'node:path' ++import { spawn } from 'node:child_process' ++const directory = await mkdtemp(join(tmpdir(), 'prism-latency-tests-')) ++try { ++ for (const [name, enabled] of [['latency-benchmark', false], ['latency-benchmark-integration', true]]) { ++ const file = join(directory, name + '.mjs') ++ await build({ entryPoints: [resolve(import.meta.dirname, `../../test/${name}.test.ts`)], outfile: file, ++ bundle: true, platform: 'node', format: 'esm', define: { __PRISM_LATENCY_BENCHMARK__: String(enabled) } }) ++ process.exitCode = await new Promise((resolveRun, reject) => { ++ const child = spawn(process.execPath, ['--test', file], { stdio: 'inherit' }) ++ child.on('error', reject); child.on('exit', code => resolveRun(code ?? 1)) ++ }) ++ if (process.exitCode) break ++ } ++ if (!process.exitCode) { ++ process.exitCode = await new Promise((resolveRun, reject) => { ++ const child = spawn(process.execPath, ['--test', resolve(import.meta.dirname, '../../test/presentation-benchmark.test.mjs')], { stdio: 'inherit' }) ++ child.on('error', reject); child.on('exit', code => resolveRun(code ?? 1)) ++ }) ++ } ++} finally { await rm(directory, { recursive: true, force: true }) } +diff --git a/src/main/benchmarkEnvironment.ts b/src/main/benchmarkEnvironment.ts +new file mode 100644 +index 0000000..be55c80 +--- /dev/null ++++ b/src/main/benchmarkEnvironment.ts +@@ -0,0 +1,52 @@ ++import { powerMonitor } from 'electron' ++ ++/** Main-process-only observer. No per-frame polling, logging, or file writes. */ ++export function recordBenchmarkEnvironment() { ++ const states = ['unknown', 'nominal', 'fair', 'serious', 'critical'] ++ const columns = ['mainMonotonicMs', 'unixMs', 'thermalState', 'onBattery', 'cpuSpeedLimitPercent', 'reason'] ++ const reasons = ['sample', 'thermal-change', 'speed-limit-change', 'power-change', 'boundary'] ++ const width = columns.length ++ const data = new Float64Array(8192 * width) ++ let count = 0, overflow = 0, speedLimit = Number.NaN ++ const sample = (reason = 0) => { ++ if (count >= data.length / width) { overflow++; return } ++ const offset = count++ * width ++ data[offset] = performance.now() ++ data[offset + 1] = Date.now() ++ data[offset + 2] = states.indexOf(powerMonitor.getCurrentThermalState()) ++ data[offset + 3] = Number(powerMonitor.isOnBatteryPower()) ++ data[offset + 4] = speedLimit ++ data[offset + 5] = reason ++ } ++ const thermal = () => sample(1) ++ const speed = ({ limit }: { limit: number }) => { speedLimit = limit; sample(2) } ++ const power = () => sample(3) ++ powerMonitor.on('thermal-state-change', thermal) ++ powerMonitor.on('speed-limit-change', speed) ++ powerMonitor.on('on-ac', power) ++ powerMonitor.on('on-battery', power) ++ const timer = setInterval(sample, 1000) ++ sample() ++ const exportSince = (first = 0) => { ++ const samples = Array.from({ length: count - first }, (_, i) => Array.from(data.subarray((first + i) * width, (first + i + 1) * width))) ++ const limits = samples.map(row => row[4]).filter(Number.isFinite) ++ return { columns, states, reasons, samples, overflow, ++ thermalStates: [...new Set(samples.map(row => states[row[2]] ?? 'unknown'))], ++ batteryObserved: samples.some(row => row[3] === 1), ++ minimumReportedCpuSpeedLimitPercent: limits.length ? Math.min(...limits) : null, ++ note: '1 Hz snapshots plus OS notifications; CPU speed limit is unknown until a notification arrives. Nominal thermal state does not prove an absence of all clock-frequency changes. No temperature or causal latency correction is inferred.' } ++ } ++ return { ++ mark: () => { const first = count; sample(4); return first }, ++ finishRun: (first: number) => { sample(4); return exportSince(first) }, ++ stop: () => { ++ clearInterval(timer) ++ powerMonitor.removeListener('thermal-state-change', thermal) ++ powerMonitor.removeListener('speed-limit-change', speed) ++ powerMonitor.removeListener('on-ac', power) ++ powerMonitor.removeListener('on-battery', power) ++ sample(4) ++ return exportSince() ++ }, ++ } ++} +diff --git a/src/main/index.ts b/src/main/index.ts +index 105db5b..311f0b9 100644 +--- a/src/main/index.ts ++++ b/src/main/index.ts +@@ -92,6 +92,12 @@ import { resolveTrayAssetPath } from './services/trayAssets' + import { DawBridgeService } from './services/dawBridgeService' + import type { DawBridgeAudioBatch, DawBridgeSnapshot } from '../types/dawBridge' + ++const latencyBenchmarkDirectory = process.env.PRISM_LATENCY_BENCHMARK === '1' ++ ? process.env.PRISM_LATENCY_BENCHMARK_DIR : undefined ++if (latencyBenchmarkDirectory) { ++ app.setPath('userData', join(resolve(latencyBenchmarkDirectory), 'user-data')) ++} ++let latencyBenchmarkStarted = false + let mainWindow: BrowserWindow | null = null + let windowDocking: WindowDockingService | null = null + const dockingGeometryGuard = new DockingGeometryGuard() +@@ -2839,6 +2845,11 @@ function setupIPC(): void { + if (!isMainRendererWindow(targetWindow)) return + + mainRendererReady = true ++ if (latencyBenchmarkDirectory && !latencyBenchmarkStarted && mainWindow) { ++ latencyBenchmarkStarted = true ++ const benchmarkWindow = mainWindow ++ void import('./latencyBenchmark').then(({ runLatencyBenchmark }) => runLatencyBenchmark(benchmarkWindow)) ++ } + windowDocking?.ready() + void processPendingProfileOpenPaths() + }) +diff --git a/src/main/latencyBenchmark.ts b/src/main/latencyBenchmark.ts +new file mode 100644 +index 0000000..87f2946 +--- /dev/null ++++ b/src/main/latencyBenchmark.ts +@@ -0,0 +1,111 @@ ++import { app, contentTracing, powerSaveBlocker, screen, type BrowserWindow } from 'electron' ++import { spawn, execFileSync, type ChildProcess } from 'node:child_process' ++import { writeFile } from 'node:fs/promises' ++import { join, resolve } from 'node:path' ++import os from 'node:os' ++import { setTimeout as delay } from 'node:timers/promises' ++import { recordBenchmarkEnvironment } from './benchmarkEnvironment' ++ ++/** Runs only with the explicit benchmark environment, in an isolated userData directory. */ ++export async function runLatencyBenchmark(window: BrowserWindow): Promise { ++ const directory = process.env.PRISM_LATENCY_BENCHMARK_DIR ++ if (!directory || process.platform !== 'darwin') throw new Error('Benchmark requires macOS and an output directory') ++ const output = resolve(directory) ++ const duration = Number(process.env.PRISM_BENCH_DURATION ?? 60) ++ const warmup = Number(process.env.PRISM_BENCH_WARMUP ?? 10) ++ const repeats = Number(process.env.PRISM_BENCH_REPEATS ?? 3) ++ if (![duration, warmup, repeats].every(n => Number.isFinite(n) && n > 0) || repeats > 10 || duration > 120) throw new Error('Invalid benchmark durations') ++ const invoke = async (method: string, ...args: unknown[]) => window.webContents.executeJavaScript( ++ `window.prismLatencyBenchmark.${method}(...${JSON.stringify(args)})`, ++ ) ++ let player: ChildProcess | null = null ++ let tracing = false ++ const sleepBlocker = powerSaveBlocker.start('prevent-display-sleep') ++ const environment = recordBenchmarkEnvironment() ++ const stopPlayer = () => { player?.kill('SIGTERM'); player = null } ++ const assertWindow = () => { ++ if (window.isDestroyed() || window.isMinimized() || !window.isVisible()) throw new Error('Benchmark window was closed, minimized, or hidden') ++ } ++ try { ++ for (let i = 0; i < 100; i++) { ++ if (await window.webContents.executeJavaScript('Boolean(window.prismLatencyBenchmark)')) break ++ if (i === 99) throw new Error('Benchmark renderer interface unavailable; rebuild with PRISM_LATENCY_BENCHMARK=1') ++ await delay(100) ++ } ++ window.setBounds({ width: 1200, height: 360 }) ++ window.center(); window.show(); window.focus() ++ const display = screen.getDisplayMatching(window.getBounds()) ++ await writeFile(join(output, 'machine.json'), JSON.stringify({ ++ platform: process.platform, arch: process.arch, os: os.release(), systemVersion: process.getSystemVersion(), ++ model: execFileSync('/usr/sbin/sysctl', ['-n', 'hw.model'], { encoding: 'utf8' }).trim(), ++ cpu: os.cpus()[0]?.model, memoryBytes: os.totalmem(), versions: process.versions, ++ display, bounds: window.getBounds(), durationSeconds: duration, warmupSeconds: warmup, repeats, ++ audioPlayer: '/usr/bin/afplay', signalFile: 'stimulus.wav', ++ powerSettings: execFileSync('/usr/bin/pmset', ['-g', 'custom'], { encoding: 'utf8' }), ++ }, null, 2)) ++ const cases = [ ++ { name: 'spectrum', scopes: ['spectrum'] }, ++ { name: 'oscilloscope', scopes: ['oscilloscope'] }, ++ { name: 'default-rack', scopes: ['spectrum', 'oscilloscope', 'vectorscope', 'vumeter'] }, ++ ] ++ const modes = ['baseline', ...Array.from({ length: repeats }, () => 'probe')] ++ let run = 0 ++ for (const target of ['display-sync', 60] as const) { ++ for (const config of cases) { ++ await invoke('configure', config.scopes, target) ++ for (let repeat = 0; repeat < modes.length; repeat++) { ++ const mode = modes[repeat] ++ const id = `${String(++run).padStart(2, '0')}-${config.name}-${target}-${mode}-${repeat}` ++ console.log(`[latency] ${id}: warming up ${warmup}s`) ++ let playerError = '' ++ player = spawn('/usr/bin/afplay', [join(output, 'stimulus.wav')], { stdio: ['ignore', 'ignore', 'pipe'] }) ++ player.on('error', error => { playerError = error.message }) ++ player.stderr?.on('data', data => { playerError += data.toString() }) ++ await delay(warmup * 1000) ++ assertWindow() ++ if (playerError || player.exitCode !== null) throw new Error(`Stimulus player failed: ${playerError}`) ++ const environmentStart = environment.mark() ++ const startMetadata = await invoke('start', mode) ++ console.log(`[latency] ${id}: measuring ${duration}s`) ++ await delay(duration * 1000) ++ assertWindow() ++ const result = await invoke('stop') ++ const runEnvironment = environment.finishRun(environmentStart) ++ if (playerError || player.exitCode !== null) throw new Error(`Stimulus ended during measurement: ${playerError}`) ++ stopPlayer() ++ await writeFile(join(output, `${id}.json`), JSON.stringify({ id, config: config.name, target, repeat, startMetadata, environment: runEnvironment, ...result })) ++ console.log(`[latency] ${id}: saved ${result.records.length} chunk/scope records, ${result.frames.length} frames`) ++ } ++ } ++ } ++ // Diagnostic trace is a separate workload and is never pooled into latency claims. ++ await invoke('configure', cases[2].scopes, 'display-sync') ++ player = spawn('/usr/bin/afplay', [join(output, 'stimulus.wav')], { stdio: 'ignore' }) ++ await delay(warmup * 1000) ++ const categories = await contentTracing.getCategories() ++ await writeFile(join(output, 'trace-categories.json'), JSON.stringify(categories, null, 2)) ++ await contentTracing.startRecording({ included_categories: ['blink.user_timing', 'devtools.timeline', 'cc', 'viz', 'gpu', 'toplevel', 'benchmark'] }) ++ tracing = true ++ const traceEnvironmentStart = environment.mark() ++ await invoke('start', 'trace') ++ await delay(Math.min(duration, 10) * 1000) ++ const traceResult = await invoke('stop') ++ await contentTracing.stopRecording(join(output, 'chromium-trace.json')) ++ tracing = false ++ await writeFile(join(output, 'trace-measurement.json'), JSON.stringify({ ...traceResult, environment: environment.finishRun(traceEnvironmentStart) })) ++ stopPlayer() ++ await writeFile(join(output, 'thermal.json'), JSON.stringify(environment.stop())) ++ await writeFile(join(output, 'complete.json'), JSON.stringify({ completedAt: new Date().toISOString(), runs: run })) ++ console.log(`[latency] Complete: ${output}`) ++ app.exit(0) ++ } catch (error) { ++ stopPlayer() ++ await writeFile(join(output, 'thermal.json'), JSON.stringify(environment.stop())) ++ if (tracing) await contentTracing.stopRecording(join(output, 'interrupted-trace.json')).catch(() => {}) ++ await writeFile(join(output, 'failure.json'), JSON.stringify({ error: String(error), at: new Date().toISOString() }, null, 2)) ++ console.error('[latency]', error) ++ app.exit(1) ++ } finally { ++ if (powerSaveBlocker.isStarted(sleepBlocker)) powerSaveBlocker.stop(sleepBlocker) ++ } ++} +diff --git a/src/preload/index.ts b/src/preload/index.ts +index 4d2ee44..72e44f8 100644 +--- a/src/preload/index.ts ++++ b/src/preload/index.ts +@@ -361,11 +361,13 @@ contextBridge.exposeInMainWorld('electronAPI', { + let nativeAddonModule: NativeAddonModule | null = null + try { + const isDev = process.env.NODE_ENV === 'development' +- const modulePath = isDev ++ const isLatencyBenchmark = process.env.PRISM_LATENCY_BENCHMARK === '1' ++ const modulePath = isDev || isLatencyBenchmark + ? require('path').join(__dirname, '../../native/build/Release/visualizer_dsp.node') + : require('path').join(process.resourcesPath!, 'native/visualizer_dsp.node') + nativeAddonModule = require(modulePath) as NativeAddonModule +-} catch { ++} catch (error) { ++ if (process.env.PRISM_LATENCY_BENCHMARK === '1') console.error('Benchmark native addon load failed:', error) + console.warn('Native DSP module not available — using JS fallback') + } + +diff --git a/src/renderer/audio/AudioCapture.ts b/src/renderer/audio/AudioCapture.ts +index fc414a2..8f3ece8 100644 +--- a/src/renderer/audio/AudioCapture.ts ++++ b/src/renderer/audio/AudioCapture.ts +@@ -6,6 +6,7 @@ + */ + + import { audioRouter } from './AudioRouter' ++import { latencyProbe } from '../benchmark/latencyProbe' + import { nativeVisualizerTransport } from './NativeVisualizerTransport' + import { RollingAudioBuffer } from './RollingAudioBuffer' + import { ChannelActivity, type ChannelActivitySnapshot } from './ChannelActivity' +@@ -596,7 +597,9 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { + result, + this.shouldTrimBacklogForLiveCapture(), + ) ++ latencyProbe?.nativeDrain(result.overwriteCount, result.queueDepth, result.chunks.length - deliveredChunks.length) + for (const chunk of deliveredChunks) { ++ latencyProbe?.receipt(chunk.sequence, chunk.capturedAtMilliseconds, chunk.left.length, chunk.sourceChannelPeaks?.[0] ?? 0) + const routedChunk: CaptureChunk = { + left: chunk.left, + right: chunk.right, +diff --git a/src/renderer/audio/AudioRouter.ts b/src/renderer/audio/AudioRouter.ts +index 9850e4e..0187a2f 100644 +--- a/src/renderer/audio/AudioRouter.ts ++++ b/src/renderer/audio/AudioRouter.ts +@@ -5,6 +5,7 @@ + */ + + import { AUDIO_SCOPE_KINDS, type AudioScopeKind } from '../../types/scope' ++import { latencyProbe } from '../benchmark/latencyProbe' + import type { CaptureBackendKind } from '../../types/capture' + import type { DawTransportSnapshot } from '../../types/dawBridge' + +@@ -259,6 +260,7 @@ export class AudioRouter { + private demandListeners = new Set<(demand: NormalizedVisualizerConsumerDemand) => void>() + + private emitSessionState(): void { ++ latencyProbe?.sessionChanged(this._sessionId) + const state = this.getSessionState() + for (const listener of this.sessionListeners) { + listener(state) +@@ -618,6 +620,7 @@ export class AudioRouter { + const tracker = this.scopeLatency[scope] + const now = performance.now() + for (const record of records) { ++ latencyProbe?.consume(scope, record.sequence) + const captureToScopeMs = Math.max(0, now - record.capturedAt) + tracker.lastCaptureToScopeMs = captureToScopeMs + tracker.latencyWindow.push(captureToScopeMs) +diff --git a/src/renderer/benchmark/analyze.ts b/src/renderer/benchmark/analyze.ts +new file mode 100644 +index 0000000..0469ecd +--- /dev/null ++++ b/src/renderer/benchmark/analyze.ts +@@ -0,0 +1,71 @@ ++import { clockBounds, percentile, type ClockSample, type LatencyProbe } from './latencyProbe' ++import type { AudioRouterDiagnostics } from '../audio/AudioRouter' ++ ++export type RawMeasurement = ReturnType & { ++ clockStart: ClockSample[]; clockEnd: ClockSample[] ++ timerQuantumMs?: number ++ routerStart: AudioRouterDiagnostics; routerEnd: AudioRouterDiagnostics ++ hiddenEvents: number ++ startMetadata?: unknown ++ metadata: { hidden: boolean; visibleScopes: string[]; capture: { activeBackendKind: string | null; isCapturing: boolean } } ++} ++export function statistics(values: number[]) { ++ return { count: values.length, median: percentile(values, 0.5), p95: percentile(values, 0.95), p99: percentile(values, 0.99), max: percentile(values, 1) } ++} ++ ++export function analyzeMeasurement(raw: RawMeasurement) { ++ const timerQuantumMs = raw.timerQuantumMs ?? 0 ++ const clock = { ...clockBounds(raw.clockStart, raw.clockEnd, timerQuantumMs), timerQuantumMs } ++ const latencyUncertaintyMs = clock.uncertaintyMs + timerQuantumMs ++ const problems: string[] = [] ++ if (raw.startMetadata && JSON.stringify(raw.startMetadata) !== JSON.stringify(raw.metadata)) problems.push('Capture, display geometry, or scope settings changed during measurement') ++ if (raw.hiddenEvents || raw.metadata.hidden) problems.push('Window visibility changed or window was hidden') ++ if (!raw.metadata.capture.isCapturing || raw.metadata.capture.activeBackendKind !== 'native-macos') problems.push('Native macOS system capture was not active') ++ for (const key of ['nativeOverwrites', 'sequenceGaps', 'sessionChanges', 'bufferOverflow', 'missingReceipts', 'trimmedChunks', 'consumedOutsideFrame'] as const) { ++ if (raw.counters[key]) problems.push(`${key}: ${raw.counters[key]}`) ++ } ++ if (raw.routerEnd.totalOverwriteCount !== raw.routerStart.totalOverwriteCount) problems.push('Router overwrites occurred') ++ if (raw.mode !== 'baseline' && !raw.chunks.some(c => c[5] > 0.00001)) problems.push('No non-silent captured signal verified') ++ if (raw.mode !== 'baseline' && !raw.chunks.length) problems.push('No captured chunks') ++ const scopes = raw.scopes.filter(s => raw.metadata.visibleScopes.includes(s)).map(scope => { ++ const id = raw.scopes.indexOf(scope) ++ const rows = raw.records.filter(r => r[2] === id) ++ const frames = raw.frames.filter(f => f[1] === id) ++ const frameIntervals = frames.slice(1).map((f, i) => f[2] - frames[i][2]) ++ const drawWork = frames.map(f => f[3] - f[2]) ++ const total: number[] = [], upper: number[] = [], delivery: number[] = [], queue: number[] = [], render: number[] = [] ++ const seen = new Set() ++ let invalid = 0, skipped = 0, duplicate = 0, unresolvedDelivery = 0 ++ for (const r of rows) { ++ const key = `${r[0]}:${r[1]}` ++ if (seen.has(key)) { duplicate++; continue } ++ seen.add(key) ++ if (r[8] === 0) { skipped++; continue } ++ if (r[8] !== 1 || !r.slice(4, 8).every(Number.isFinite)) { invalid++; continue } ++ const capture = r[4] + clock.offset ++ // Reject impossible ordering. Overlapping receipt/capture uncertainty intervals ++ // leave delivery duration unresolved, but need not invalidate a later draw endpoint. ++ if (r[5] + timerQuantumMs < r[4] + clock.low || r[6] < r[5] || r[7] < r[6] ++ || r[7] - capture < 0) { invalid++; continue } ++ total.push(r[7] - capture) ++ upper.push(r[7] + timerQuantumMs - (r[4] + clock.low)) ++ if (r[5] - timerQuantumMs >= r[4] + clock.high) delivery.push(r[5] - capture) ++ else unresolvedDelivery++ ++ queue.push(r[6] - r[5]); render.push(r[7] - r[6]) ++ } ++ if (raw.mode !== 'baseline' && (!total.length || invalid || skipped || duplicate)) problems.push(`${scope}: ${invalid} invalid, ${skipped} skipped, ${duplicate} duplicate, ${total.length} valid`) ++ // End-of-window arrivals still in the normal scope queue are right-censored, not successful draws. ++ const unconsumedAtStop = Math.max(0, raw.chunks.length - seen.size) ++ const pendingAtStop = raw.routerEnd.scopes[scope].queuedChunks ++ if (raw.mode !== 'baseline' && unconsumedAtStop > pendingAtStop) problems.push(`${scope}: unexplained unconsumed chunks`) ++ return { scope, latencyMs: statistics(total), upperLatencyMs: statistics(upper), captureToReceiptMs: statistics(delivery), ++ receiptToConsumptionMs: statistics(queue), consumptionToDrawEndMs: statistics(render), ++ frameWorkMs: statistics(drawWork), frameIntervalMs: statistics(frameIntervals), ++ observedFps: frames.length > 1 ? (frames.length - 1) * 1000 / (frames[frames.length - 1][2] - frames[0][2]) : 0, ++ frameCount: frames.length, skippedDrawFrames: frames.filter(f => !f[4]).length, ++ invalid, skippedChunks: skipped, duplicate, unresolvedDelivery, unconsumedAtStop, pendingAtStop } ++ }) ++ if (!scopes.length || scopes.some(s => s.frameCount < 2)) problems.push('Insufficient scope frames') ++ return { durationMs: raw.stoppedAt - raw.startedAt, clock, latencyUncertaintyMs, problems, valid: problems.length === 0, ++ counters: raw.counters, chunkSizes: [...new Set(raw.chunks.map(c => c[4]))], scopes } ++} +diff --git a/src/renderer/benchmark/controller.ts b/src/renderer/benchmark/controller.ts +new file mode 100644 +index 0000000..754c7e4 +--- /dev/null ++++ b/src/renderer/benchmark/controller.ts +@@ -0,0 +1,85 @@ ++import { audioCapture } from '../audio/AudioCapture' ++import { audioRouter } from '../audio/AudioRouter' ++import { isNativeAvailable } from '../audio/native' ++import { useAudioStore } from '../stores/audioStore' ++import { useSettingsStore } from '../stores/settingsStore' ++import { usePerformanceStore } from '../stores/performanceStore' ++import { createDefaultProfile } from '../../shared/profileState' ++import { SCOPE_KINDS } from '../../types/scope' ++import { BENCHMARK_BUILD, calibrateClock, measureTimerQuantum, latencyProbe, type BenchScope, type ClockSample, type ProbeMode } from './latencyProbe' ++ ++export function installLatencyBenchmark(): void { ++ if (!BENCHMARK_BUILD || !latencyProbe) return ++ const probe = latencyProbe ++ let clockStart: ClockSample[] = [] ++ let timerQuantumStart = 0 ++ let routerStart = audioRouter.getDiagnosticsSnapshot() ++ let hiddenEvents = 0 ++ document.addEventListener('visibilitychange', () => { if (probe.active) hiddenEvents++ }) ++ const metadata = () => ({ ++ capture: audioCapture.getStatus(), session: audioRouter.getSessionState(), ++ frameTarget: usePerformanceStore.getState().frameTarget, ++ scopeSettings: useSettingsStore.getState().scopeSettings, ++ visibleScopes: useSettingsStore.getState().visibleScopes(), ++ pixelRatio: devicePixelRatio, innerWidth, innerHeight, hidden: document.hidden, ++ canvases: [...document.querySelectorAll('canvas')].map(canvas => ({ ++ width: canvas.width, height: canvas.height, cssWidth: canvas.clientWidth, cssHeight: canvas.clientHeight, ++ })), ++ }) ++ const readNative = () => { ++ const api = window.nativeCaptureAPI?.macosCapture ++ if (!api) throw new Error('Native macOS capture API unavailable') ++ return api.nowMilliseconds() ++ } ++ window.prismLatencyBenchmark = { ++ async configure(scopes: BenchScope[], frameTarget: 60 | 'display-sync') { ++ if (probe.active) throw new Error('Cannot configure during a measurement') ++ if (!isNativeAvailable() || !window.nativeCaptureAPI?.macosCapture?.getSupport().available ++ || scopes.some(scope => !window.visualizerAPI?.[scope])) { ++ throw new Error('Benchmark requires the native capture and every selected native DSP module; fallback measurements are forbidden') ++ } ++ const profile = createDefaultProfile() ++ useSettingsStore.setState({ ...profile, hiddenScopes: new Set(SCOPE_KINDS.filter(s => !scopes.includes(s as BenchScope))) }) ++ usePerformanceStore.getState().setFrameTarget(frameTarget) ++ useAudioStore.getState().setRollingCaptureSeconds(null) ++ useAudioStore.getState().setInputGain(0) ++ if (!audioCapture.getStatus().isCapturing) { ++ useAudioStore.getState().setCaptureMode('system') ++ await useAudioStore.getState().startCapture() ++ } ++ const status = audioCapture.getStatus() ++ if (!status.isCapturing || status.activeBackendKind !== 'native-macos') { ++ throw new Error(`Benchmark requires native macOS system capture: ${JSON.stringify(status)}`) ++ } ++ return metadata() ++ }, ++ start(mode: ProbeMode) { ++ if (document.hidden) throw new Error('Benchmark window must be visible') ++ timerQuantumStart = measureTimerQuantum() ++ clockStart = calibrateClock(readNative) ++ routerStart = audioRouter.getDiagnosticsSnapshot() ++ hiddenEvents = 0 ++ probe.start(mode, audioRouter.getSessionState().sessionId) ++ return metadata() ++ }, ++ stop() { ++ probe.stop() ++ const clockEnd = calibrateClock(readNative) ++ const timerQuantumMs = Math.max(timerQuantumStart, measureTimerQuantum()) ++ const routerEnd = audioRouter.getDiagnosticsSnapshot() ++ return { ...probe.export(), clockStart, clockEnd, timerQuantumMs, routerStart, routerEnd, hiddenEvents, metadata: metadata() } ++ }, ++ metadata, ++ } ++} ++ ++declare global { ++ interface Window { ++ prismLatencyBenchmark?: { ++ configure: (scopes: BenchScope[], frameTarget: 60 | 'display-sync') => Promise ++ start: (mode: ProbeMode) => unknown ++ stop: () => unknown ++ metadata: () => unknown ++ } ++ } ++} +diff --git a/src/renderer/benchmark/latencyProbe.ts b/src/renderer/benchmark/latencyProbe.ts +new file mode 100644 +index 0000000..47f3dfe +--- /dev/null ++++ b/src/renderer/benchmark/latencyProbe.ts +@@ -0,0 +1,184 @@ ++/** Benchmark-only observation. This module never changes capture or DSP state. */ ++export const BENCHMARK_BUILD = typeof __PRISM_LATENCY_BENCHMARK__ !== 'undefined' && __PRISM_LATENCY_BENCHMARK__ ++export const BENCH_SCOPES = ['spectrum', 'oscilloscope', 'vectorscope', 'vumeter'] as const ++export type BenchScope = typeof BENCH_SCOPES[number] ++export type ProbeMode = 'baseline' | 'probe' | 'trace' ++export const CHUNK_COLUMNS = ['session', 'sequence', 'nativeAt', 'receivedAt', 'sampleFrames', 'peak'] as const ++export const RECORD_COLUMNS = ['session', 'sequence', 'scope', 'frame', 'nativeAt', 'receivedAt', 'consumedAt', 'drawEnd', 'outcome', 'sampleFrames'] as const ++export const FRAME_COLUMNS = ['frame', 'scope', 'start', 'end', 'drew'] as const ++// outcome: 1 = successful draw invocation, 0 = skipped; -1 = missing receipt metadata. ++const C = CHUNK_COLUMNS.length, R = RECORD_COLUMNS.length, F = FRAME_COLUMNS.length ++ ++export interface ClockSample { before: number; native: number; after: number } ++export function measureTimerQuantum(now = () => performance.now(), minimumMs = 0.1): number { ++ const deltas: number[] = [] ++ let previous = now() ++ for (let i = 0; i < 1000000 && deltas.length < 32; i++) { ++ const current = now() ++ if (current > previous) { deltas.push(current - previous); previous = current } ++ } ++ if (!deltas.length) throw new Error('Renderer clock did not advance') ++ return Math.max(minimumMs, Math.min(...deltas) * 1.05) ++} ++export function calibrateClock(readNative: () => number, now = () => performance.now(), count = 64): ClockSample[] { ++ return Array.from({ length: count }, () => { ++ const before = now(), native = readNative(), after = now() ++ return { before, native, after } ++ }) ++} ++ ++/** Lowest round-trip bracket, widened to cover both endpoint calibrations. */ ++export function clockBounds(start: ClockSample[], end: ClockSample[], timerQuantumMs = 0) { ++ const best = (samples: ClockSample[]) => samples.filter(s => ++ Number.isFinite(s.native) && Number.isFinite(s.before) && Number.isFinite(s.after) && s.after >= s.before, ++ ).sort((a, b) => (a.after - a.before) - (b.after - b.before))[0] ++ const a = best(start), b = best(end) ++ if (!a || !b) throw new Error('Missing valid clock calibration') ++ const low = Math.min(a.before - a.native, b.before - b.native) - timerQuantumMs ++ const high = Math.max(a.after - a.native, b.after - b.native) + timerQuantumMs ++ return { low, high, offset: (low + high) / 2, uncertaintyMs: (high - low) / 2, ++ endpointDriftMs: Math.abs((a.before + a.after) / 2 - a.native - ((b.before + b.after) / 2 - b.native)) } ++} ++ ++export function percentile(values: number[], fraction: number): number | null { ++ if (!values.length) return null ++ const sorted = [...values].sort((a, b) => a - b) ++ return sorted[Math.max(0, Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1))] ++} ++ ++export class LatencyProbe { ++ active = false ++ mode: ProbeMode = 'probe' ++ private chunks: Float64Array ++ private records: Float64Array ++ private frames: Float64Array ++ private receiptSlots: Int32Array ++ private boundaryChunks = 0 ++ private latestNativeOverwrites = 0 ++ private chunkCount = 0 ++ private recordCount = 0 ++ private frameCount = 0 ++ private frameRecordStart = 0 ++ private currentFrame = -1 ++ private currentScope = -1 ++ private didDraw = false ++ private session = 0 ++ private lastSequence = 0 ++ private nativeOverwrites = 0 ++ private sequenceGaps = 0 ++ private sessionChanges = 0 ++ private overflow = 0 ++ private missingReceipts = 0 ++ private trimmedChunks = 0 ++ private outsideFrame = 0 ++ private nativeQueueMax = 0 ++ private lastNativeOverwrites: number | null = null ++ private startedAt = 0 ++ private stoppedAt = 0 ++ constructor(private capacity = 262144, private now = () => performance.now()) { ++ this.chunks = new Float64Array(capacity * C) ++ this.records = new Float64Array(capacity * R) ++ this.frames = new Float64Array(65536 * F) ++ this.receiptSlots = new Int32Array(8192).fill(-1) ++ } ++ start(mode: ProbeMode, session: number): void { ++ if (this.active) throw new Error('Benchmark already running') ++ this.mode = mode ++ this.session = session ++ this.chunkCount = this.recordCount = this.frameCount = this.lastSequence = 0 ++ this.nativeOverwrites = this.sequenceGaps = this.sessionChanges = this.overflow = 0 ++ this.missingReceipts = this.trimmedChunks = this.outsideFrame = this.nativeQueueMax = 0 ++ this.currentFrame = -1 ++ this.lastNativeOverwrites = this.latestNativeOverwrites ++ this.boundaryChunks = 0 ++ this.receiptSlots.fill(-1) ++ this.startedAt = this.now() ++ this.active = true ++ } ++ sessionChanged(session: number): void { ++ if (!this.active || session === this.session) return ++ this.sessionChanges++ ++ this.session = session ++ this.lastSequence = 0 ++ this.lastNativeOverwrites = null ++ this.receiptSlots.fill(-1) ++ } ++ nativeDrain(overwrites: number, queueDepth: number, trimmed: number): void { ++ this.latestNativeOverwrites = overwrites ++ if (!this.active || this.mode === 'baseline') return ++ if (this.lastNativeOverwrites !== null) this.nativeOverwrites += Math.max(0, overwrites - this.lastNativeOverwrites) ++ this.lastNativeOverwrites = overwrites ++ this.nativeQueueMax = Math.max(this.nativeQueueMax, queueDepth) ++ this.trimmedChunks += trimmed ++ } ++ receipt(sequence: number, nativeAt: number, sampleFrames: number, peak: number): void { ++ if (!this.active || this.mode === 'baseline') return ++ const receivedAt = this.now() ++ if (this.lastSequence && sequence !== this.lastSequence + 1) this.sequenceGaps += Math.max(1, sequence - this.lastSequence - 1) ++ this.lastSequence = sequence ++ if (this.chunkCount >= this.capacity) { this.overflow++; return } ++ const row = this.chunkCount++, i = row * C ++ this.chunks[i] = this.session; this.chunks[i + 1] = sequence ++ this.chunks[i + 2] = nativeAt; this.chunks[i + 3] = receivedAt ++ this.chunks[i + 4] = sampleFrames; this.chunks[i + 5] = peak ++ this.receiptSlots[sequence % this.receiptSlots.length] = row ++ } ++ beginFrame(scope: BenchScope): void { ++ if (!this.active) return ++ if (this.currentFrame !== -1) throw new Error('Nested benchmark frames') ++ if (this.frameCount >= this.frames.length / F) { this.overflow++; return } ++ this.currentScope = BENCH_SCOPES.indexOf(scope) ++ this.currentFrame = this.frameCount++ ++ this.frameRecordStart = this.recordCount ++ this.didDraw = false ++ const i = this.currentFrame * F ++ this.frames[i] = this.currentFrame; this.frames[i + 1] = this.currentScope ++ this.frames[i + 2] = this.now() ++ if (this.mode === 'trace') performance.mark(`prism.frame.${this.currentFrame}.${scope}.start`) ++ } ++ consume(scope: string, sequence: number): void { ++ if (!this.active || this.mode === 'baseline' || !BENCH_SCOPES.includes(scope as BenchScope)) return ++ if (this.currentFrame < 0 || BENCH_SCOPES[this.currentScope] !== scope) { this.outsideFrame++; return } ++ if (this.chunkCount === 0 || (this.chunks[0] === this.session && sequence < this.chunks[1])) { this.boundaryChunks++; return } ++ if (this.recordCount >= this.capacity) { this.overflow++; return } ++ const i = this.recordCount++ * R ++ const slot = this.receiptSlots[sequence % this.receiptSlots.length], c = slot * C ++ const found = slot >= 0 && this.chunks[c] === this.session && this.chunks[c + 1] === sequence ++ if (!found) this.missingReceipts++ ++ this.records[i] = this.session; this.records[i + 1] = sequence ++ this.records[i + 2] = this.currentScope; this.records[i + 3] = this.currentFrame ++ this.records[i + 4] = found ? this.chunks[c + 2] : NaN ++ this.records[i + 5] = found ? this.chunks[c + 3] : NaN ++ this.records[i + 6] = this.now(); this.records[i + 7] = NaN ++ this.records[i + 8] = found ? 0 : -1 ++ this.records[i + 9] = found ? this.chunks[c + 4] : 0 ++ } ++ drawn(): void { if (this.active) this.didDraw = true } ++ endFrame(): void { ++ if (!this.active || this.currentFrame < 0) return ++ // Capture endpoint before observer bookkeeping and trace emission. ++ const end = this.now(), i = this.currentFrame * F ++ this.frames[i + 3] = end; this.frames[i + 4] = Number(this.didDraw) ++ for (let row = this.frameRecordStart; row < this.recordCount; row++) { ++ const r = row * R ++ this.records[r + 7] = end ++ if (this.records[r + 8] !== -1) this.records[r + 8] = Number(this.didDraw) ++ } ++ if (this.mode === 'trace') performance.mark(`prism.frame.${this.currentFrame}.${BENCH_SCOPES[this.currentScope]}.end`) ++ this.currentFrame = -1 ++ } ++ stop(): void { this.stoppedAt = this.now(); this.active = false } ++ export() { ++ if (this.active) throw new Error('Stop before export') ++ const rows = (data: Float64Array, count: number, width: number) => Array.from({ length: count }, (_, i) => Array.from(data.subarray(i * width, (i + 1) * width))) ++ return { mode: this.mode, startedAt: this.startedAt, stoppedAt: this.stoppedAt, ++ scopes: BENCH_SCOPES, chunkColumns: CHUNK_COLUMNS, recordColumns: RECORD_COLUMNS, frameColumns: FRAME_COLUMNS, ++ chunks: rows(this.chunks, this.chunkCount, C), records: rows(this.records, this.recordCount, R), frames: rows(this.frames, this.frameCount, F), ++ counters: { boundaryChunks: this.boundaryChunks, nativeOverwrites: this.nativeOverwrites, sequenceGaps: this.sequenceGaps, sessionChanges: this.sessionChanges, ++ bufferOverflow: this.overflow, missingReceipts: this.missingReceipts, trimmedChunks: this.trimmedChunks, ++ consumedOutsideFrame: this.outsideFrame, nativeQueueMax: this.nativeQueueMax } } ++ } ++} ++ ++// Allocate only in the explicit benchmark build, never in a normal build. ++export const latencyProbe = BENCHMARK_BUILD ? new LatencyProbe() : null +diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx +index 74d8f82..25a7bf6 100644 +--- a/src/renderer/main.tsx ++++ b/src/renderer/main.tsx +@@ -24,6 +24,10 @@ const windowMode = params.get('mode') + const windowRole = params.get('window') + const scopeKind = params.get('scope') + ++if (typeof __PRISM_LATENCY_BENCHMARK__ !== 'undefined' && __PRISM_LATENCY_BENCHMARK__ && windowRole === 'main') { ++ void import('./benchmark/controller').then(({ installLatencyBenchmark }) => installLatencyBenchmark()) ++} ++ + if (windowMode !== 'dialog' && windowRole !== 'now-playing-config') { + bootstrapWindowBackgroundFromQuery() + } +diff --git a/src/renderer/visualizers/Oscilloscope.ts b/src/renderer/visualizers/Oscilloscope.ts +index eb0bea1..afcd685 100644 +--- a/src/renderer/visualizers/Oscilloscope.ts ++++ b/src/renderer/visualizers/Oscilloscope.ts +@@ -1,3 +1,4 @@ ++import { latencyProbe } from '../benchmark/latencyProbe' + import { audioRouter } from '../audio/AudioRouter' + import { + oscilloscope as defaultNativeOscilloscope, +@@ -109,6 +110,7 @@ export class Oscilloscope { + this.dataSource = dataSource ?? defaultOscilloscopeDataSource + this.nativeAnalyzer = nativeAnalyzer === undefined ? defaultNativeOscilloscope : (nativeAnalyzer ?? defaultNativeOscilloscope) + this.frameLoop = new VisualizerFrameLoop({ ++ benchmarkScope: 'oscilloscope', + frameScheduler, + shouldRun: () => this.dataSource.isPlaying(), + onFrame: this.drawFrame, +@@ -335,6 +337,7 @@ export class Oscilloscope { + ctx.lineTo(x, y) + } + ctx.stroke() ++ latencyProbe?.drawn() + } + + private renderStaticLayer(): void { +diff --git a/src/renderer/visualizers/SpectrumAnalyzer.ts b/src/renderer/visualizers/SpectrumAnalyzer.ts +index 5ca6e88..9f64ec2 100644 +--- a/src/renderer/visualizers/SpectrumAnalyzer.ts ++++ b/src/renderer/visualizers/SpectrumAnalyzer.ts +@@ -1,3 +1,4 @@ ++import { latencyProbe } from '../benchmark/latencyProbe' + import { REFERENCE_SAMPLE_RATE, type SpectrumReferenceAsset, type SpectrumReferenceSettings, type SpectrumReferenceLevel } from '../../types/spectrumReference' + import { referenceBins, referenceDbAt, differenceReferenceBins, ReferenceCurveTransition } from './referenceCurve' + import { audioRouter } from '../audio/AudioRouter' +@@ -346,6 +347,7 @@ export class SpectrumAnalyzer { + this.nativeAnalyzer = nativeAnalyzer === undefined ? defaultNativeSpectrum : nativeAnalyzer + this.heatLut = buildHeatLUT(this.options.heatColors) + this.frameLoop = new VisualizerFrameLoop({ ++ benchmarkScope: 'spectrum', + frameScheduler, + shouldRun: () => this.dataSource.isPlaying() || this.options.referenceImporting || performance.now() < this.referenceSettleUntil, + onFrame: this.drawFrame, +@@ -1337,6 +1339,7 @@ export class SpectrumAnalyzer { + } + + this.emitPeakInfo(primaryRender.peakInfo) ++ latencyProbe?.drawn() + } + + private renderStaticLayer(minFrequency: number, maxFrequency: number): void { +diff --git a/src/renderer/visualizers/VUMeter.ts b/src/renderer/visualizers/VUMeter.ts +index 70ab830..0bb23a6 100644 +--- a/src/renderer/visualizers/VUMeter.ts ++++ b/src/renderer/visualizers/VUMeter.ts +@@ -1,3 +1,4 @@ ++import { latencyProbe } from '../benchmark/latencyProbe' + import { audioRouter } from '../audio/AudioRouter' + import { + vumeter as nativeVUMeter, +@@ -334,6 +335,7 @@ export class VUMeter { + this.nativeAnalyzer = nativeAnalyzer === undefined ? nativeVUMeter : nativeAnalyzer + this.meterBallistics = new VUMeterBallistics(this.dataSource.getSampleRate()) + this.frameLoop = new VisualizerFrameLoop({ ++ benchmarkScope: 'vumeter', + frameScheduler, + shouldRun: () => this.dataSource.isPlaying(), + onFrame: this.drawFrame, +@@ -1385,6 +1387,7 @@ export class VUMeter { + } else { + this.drawBarMode(width, height) + } ++ latencyProbe?.drawn() + } + + dispose(): void { +diff --git a/src/renderer/visualizers/Vectorscope.ts b/src/renderer/visualizers/Vectorscope.ts +index 67b27de..f0583f6 100644 +--- a/src/renderer/visualizers/Vectorscope.ts ++++ b/src/renderer/visualizers/Vectorscope.ts +@@ -1,3 +1,4 @@ ++import { latencyProbe } from '../benchmark/latencyProbe' + import { audioRouter } from '../audio/AudioRouter' + import { vectorscope as nativeVectorscope, type VectorscopeNativeAnalyzer } from '../audio/native' + import { +@@ -119,6 +120,7 @@ export class Vectorscope { + this.dataSource = dataSource ?? defaultVectorscopeDataSource + this.nativeAnalyzer = nativeAnalyzer === undefined ? nativeVectorscope : nativeAnalyzer + this.frameLoop = new VisualizerFrameLoop({ ++ benchmarkScope: 'vectorscope', + frameScheduler, + shouldRun: () => this.dataSource.isPlaying(), + onFrame: this.drawFrame, +@@ -288,6 +290,7 @@ export class Vectorscope { + + this.renderStaticLayer() + ctx.drawImage(offscreenCanvas, 0, 0) ++ latencyProbe?.drawn() + } + + private isNativeAvailable(): boolean { +diff --git a/src/renderer/visualizers/visualizerFrameLoop.ts b/src/renderer/visualizers/visualizerFrameLoop.ts +index aa7f1a1..5433f9f 100644 +--- a/src/renderer/visualizers/visualizerFrameLoop.ts ++++ b/src/renderer/visualizers/visualizerFrameLoop.ts +@@ -1,12 +1,15 @@ + import { FrameScheduler } from './frameScheduler' ++import { latencyProbe, type BenchScope } from '../benchmark/latencyProbe' + + interface VisualizerFrameLoopOptions { ++ benchmarkScope?: BenchScope + frameScheduler?: FrameScheduler + shouldRun: () => boolean + onFrame: () => void + } + + export class VisualizerFrameLoop { ++ private readonly benchmarkScope?: BenchScope + private readonly frameScheduler: FrameScheduler + private readonly shouldRun: () => boolean + private readonly onFrame: () => void +@@ -14,7 +17,8 @@ export class VisualizerFrameLoop { + private isStarted = false + private isInvalidated = false + +- constructor({ frameScheduler, shouldRun, onFrame }: VisualizerFrameLoopOptions) { ++ constructor({ frameScheduler, shouldRun, onFrame, benchmarkScope }: VisualizerFrameLoopOptions) { ++ this.benchmarkScope = benchmarkScope + this.frameScheduler = frameScheduler ?? new FrameScheduler() + this.shouldRun = shouldRun + this.onFrame = onFrame +@@ -67,7 +71,12 @@ export class VisualizerFrameLoop { + private tick = (): void => { + if (!this.isStarted) return + this.isInvalidated = false +- this.onFrame() ++ if (latencyProbe?.active && this.benchmarkScope) { ++ latencyProbe.beginFrame(this.benchmarkScope) ++ try { this.onFrame() } finally { latencyProbe.endFrame() } ++ } else { ++ this.onFrame() ++ } + this.sync() + } + } +diff --git a/src/types/latencyBenchmark.d.ts b/src/types/latencyBenchmark.d.ts +new file mode 100644 +index 0000000..4743f7d +--- /dev/null ++++ b/src/types/latencyBenchmark.d.ts +@@ -0,0 +1 @@ ++declare const __PRISM_LATENCY_BENCHMARK__: boolean +diff --git a/test/latency-benchmark-integration.test.ts b/test/latency-benchmark-integration.test.ts +new file mode 100644 +index 0000000..60a86b1 +--- /dev/null ++++ b/test/latency-benchmark-integration.test.ts +@@ -0,0 +1,34 @@ ++import test from 'node:test' ++import assert from 'node:assert/strict' ++import { AudioRouter } from '../src/renderer/audio/AudioRouter' ++import { VisualizerFrameLoop } from '../src/renderer/visualizers/visualizerFrameLoop' ++import type { FrameScheduler } from '../src/renderer/visualizers/frameScheduler' ++import { latencyProbe } from '../src/renderer/benchmark/latencyProbe' ++ ++test('real router drains bind to the active visualizer frame without modifying samples', () => { ++ const probe = latencyProbe! ++ const router = new AudioRouter() ++ const session = router.beginSession(48000, 2, 'native-macos') ++ router.setVisualizerConsumerDemand('bench-test', { spectrum: true }) ++ const callbacks = new Set<() => void>() ++ const scheduler = { subscribe(callback: () => void) { callbacks.add(callback); return () => callbacks.delete(callback) } } as FrameScheduler ++ const left = Float32Array.of(0.5, 0.25), right = Float32Array.of(-0.5, -0.25) ++ let observed: ReturnType = [] ++ const loop = new VisualizerFrameLoop({ frameScheduler: scheduler, benchmarkScope: 'spectrum', shouldRun: () => true, ++ onFrame: () => { observed = router.flushPendingSpectrumStereoSamples(); probe.drawn() } }) ++ probe.start('probe', session) ++ probe.receipt(4, 100, 2, 0.5) ++ router.ingestChunk(left, right, { sessionId: session, sequence: 4, capturedAt: 100 }) ++ loop.start() ++ for (const cb of callbacks) cb() ++ assert.equal(observed[0].left, left) ++ assert.equal(observed[0].right, right) ++ for (const cb of callbacks) cb() ++ loop.dispose(); probe.stop() ++ const raw = probe.export() ++ assert.equal(raw.records.length, 1) ++ assert.equal(raw.records[0][1], 4) ++ assert.equal(raw.records[0][8], 1) ++ assert.equal(raw.frames.length, 2) ++ assert.equal(callbacks.size, 0) ++}) +diff --git a/test/latency-benchmark.test.ts b/test/latency-benchmark.test.ts +new file mode 100644 +index 0000000..5d75f48 +--- /dev/null ++++ b/test/latency-benchmark.test.ts +@@ -0,0 +1,139 @@ ++import test from 'node:test' ++import assert from 'node:assert/strict' ++import { LatencyProbe, clockBounds, calibrateClock, percentile } from '../src/renderer/benchmark/latencyProbe' ++import { analyzeMeasurement, type RawMeasurement } from '../src/renderer/benchmark/analyze' ++ ++const calibration = [{ before: 0, native: 1000, after: 0.2 }] ++function fixture(capacity = 100) { ++ let time = 0 ++ const probe = new LatencyProbe(capacity, () => time) ++ probe.start('probe', 7) ++ return { probe, at: (t: number) => { time = t } } ++} ++function raw(probe: LatencyProbe): RawMeasurement { ++ const router = { totalOverwriteCount: 0, scopes: { spectrum: { queuedChunks: 0 } } } as RawMeasurement['routerStart'] ++ return { ...probe.export(), clockStart: calibration, clockEnd: calibration, hiddenEvents: 0, ++ routerStart: router, routerEnd: router, metadata: { hidden: false, visibleScopes: ['spectrum'], ++ capture: { activeBackendKind: 'native-macos', isCapturing: true } } } ++} ++test('clock calibration brackets calls and retains drift as uncertainty', () => { ++ let clock = 0 ++ const samples = calibrateClock(() => 1000, () => clock++, 2) ++ assert.deepEqual(samples, [{ before: 0, native: 1000, after: 1 }, { before: 2, native: 1000, after: 3 }]) ++ const bounds = clockBounds([{ before: 1, native: 1000, after: 9 }, ...calibration], [{ before: 100, native: 1099, after: 100.2 }]) ++ assert.equal(bounds.low, -1000) ++ assert.ok(Math.abs(bounds.high + 998.8) < 1e-9) ++ assert.ok(Math.abs(bounds.uncertaintyMs - 0.6) < 1e-9) ++ assert.throws(() => clockBounds([], calibration)) ++}) ++test('multiple chunks retain their own capture ages and exact frame association', () => { ++ const { probe, at } = fixture() ++ at(3); probe.receipt(1, 1001, 128, 0.02) ++ at(4); probe.receipt(2, 1002, 256, 0.03) ++ at(8); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.consume('spectrum', 2) ++ probe.drawn(); at(10); probe.endFrame() ++ at(20); probe.beginFrame('spectrum'); probe.drawn(); at(21); probe.endFrame() ++ probe.stop() ++ const data = raw(probe), summary = analyzeMeasurement(data) ++ assert.equal(data.records.length, 2, 'redrawing without new audio must not add samples') ++ assert.equal(data.records[0][3], data.records[1][3]) ++ assert.equal(data.records[0][9], 128) ++ assert.equal(summary.scopes[0].latencyMs.count, 2) ++ assert.ok(Math.abs(summary.scopes[0].latencyMs.p95! - 8.9) < 1e-8) ++ assert.equal(summary.scopes[0].upperLatencyMs.p95, 9) ++ assert.equal(summary.valid, true) ++}) ++test('skipped frames are not reported as successful low latency', () => { ++ const { probe, at } = fixture() ++ at(3); probe.receipt(1, 1001, 128, 0.02) ++ at(4); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.endFrame(); probe.stop() ++ const result = analyzeMeasurement(raw(probe)) ++ assert.equal(result.scopes[0].skippedChunks, 1) ++ assert.equal(result.scopes[0].latencyMs.count, 0) ++ assert.equal(result.valid, false) ++}) ++test('negative clock ordering is invalid, never clamped into a fast result', () => { ++ const { probe, at } = fixture() ++ at(3); probe.receipt(1, 1100, 128, 0.02) ++ at(4); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.drawn(); probe.endFrame(); probe.stop() ++ const result = analyzeMeasurement(raw(probe)) ++ assert.equal(result.scopes[0].invalid, 1) ++ assert.equal(result.scopes[0].latencyMs.count, 0) ++}) ++test('same sequence from a new session cannot use old receipt metadata', () => { ++ const { probe, at } = fixture() ++ at(3); probe.receipt(1, 1001, 128, 0.02) ++ probe.sessionChanged(8) ++ at(4); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.drawn(); probe.endFrame(); probe.stop() ++ assert.equal(probe.export().counters.sessionChanges, 1) ++ assert.equal(probe.export().records[0][8], -1) ++ assert.equal(analyzeMeasurement(raw(probe)).valid, false) ++}) ++test('native drops, trimming, sequence gaps and metadata loss are explicit', () => { ++ const { probe, at } = fixture() ++ probe.nativeDrain(2, 3, 1) ++ at(3); probe.receipt(1, 1001, 128, 0.02); probe.receipt(4, 1002, 128, 0.02) ++ probe.beginFrame('spectrum'); probe.consume('spectrum', 3); probe.endFrame(); probe.stop() ++ const counters = probe.export().counters ++ assert.equal(counters.nativeOverwrites, 2) ++ assert.equal(counters.sequenceGaps, 2) ++ assert.equal(counters.trimmedChunks, 1) ++ assert.equal(counters.missingReceipts, 1) ++}) ++test('pre-existing boundary chunks are excluded explicitly, pending tail remains censored', () => { ++ const { probe, at } = fixture() ++ at(3); probe.receipt(10, 1001, 128, 0.02) ++ probe.beginFrame('spectrum'); probe.consume('spectrum', 9); probe.consume('spectrum', 10); probe.drawn(); probe.endFrame() ++ at(5); probe.receipt(11, 1004, 128, 0.02) ++ at(6); probe.beginFrame('spectrum'); probe.drawn(); probe.endFrame(); probe.stop() ++ const data = raw(probe) ++ data.routerEnd = { ...data.routerEnd, scopes: { ...data.routerEnd.scopes, spectrum: { ...data.routerEnd.scopes.spectrum, queuedChunks: 1 } } } ++ const result = analyzeMeasurement(data) ++ assert.equal(result.counters.boundaryChunks, 1) ++ assert.equal(result.scopes[0].latencyMs.count, 1) ++ assert.equal(result.scopes[0].unconsumedAtStop, 1) ++ assert.equal(result.valid, true) ++}) ++test('baseline observes frames but records no chunk probes; overflow never silently overwrites', () => { ++ const { probe, at } = fixture(1) ++ at(3); probe.receipt(1, 1001, 128, 0.02); probe.receipt(2, 1002, 128, 0.02) ++ probe.stop() ++ assert.equal(probe.export().chunks.length, 1) ++ assert.equal(probe.export().counters.bufferOverflow, 1) ++ probe.start('baseline', 7); probe.receipt(1, 1001, 128, 0.02) ++ probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.drawn(); probe.endFrame(); probe.stop() ++ assert.equal(probe.export().records.length, 0) ++ assert.equal(probe.export().frames.length, 1) ++}) ++test('quantiles use nearest rank and preserve worst-case tails', () => { ++ assert.equal(percentile([], 0.95), null) ++ assert.equal(percentile([2, 100, 1, 3], 0.5), 2) ++ assert.equal(percentile([2, 100, 1, 3], 0.95), 100) ++}) ++ ++test('timer-quantized delivery is unresolved while later draw latency remains measurable', () => { ++ const { probe, at } = fixture() ++ at(1); probe.receipt(1, 1001.05, 128, 0.02) ++ at(4); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.drawn(); at(5); probe.endFrame() ++ at(10); probe.beginFrame('spectrum'); probe.drawn(); probe.endFrame(); probe.stop() ++ const data = raw(probe); data.timerQuantumMs = 0.1 ++ const result = analyzeMeasurement(data) ++ assert.equal(result.valid, true) ++ assert.equal(result.scopes[0].unresolvedDelivery, 1) ++ assert.equal(result.scopes[0].captureToReceiptMs.count, 0) ++ assert.equal(result.scopes[0].latencyMs.count, 1) ++ assert.ok(Math.abs(result.latencyUncertaintyMs - 0.3) < 1e-8) ++ assert.ok(result.scopes[0].upperLatencyMs.p95! > result.scopes[0].latencyMs.p95!) ++}) ++ ++test('duplicate consumption and changed configuration cannot support a claim', () => { ++ const { probe, at } = fixture() ++ at(3); probe.receipt(1, 1001, 128, 0.02) ++ at(4); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.drawn(); probe.endFrame() ++ at(8); probe.beginFrame('spectrum'); probe.consume('spectrum', 1); probe.drawn(); probe.endFrame(); probe.stop() ++ const data = raw(probe); data.startMetadata = { changed: true } ++ const result = analyzeMeasurement(data) ++ assert.equal(result.scopes[0].duplicate, 1) ++ assert.equal(result.valid, false) ++ assert.ok(result.problems.some(p => p.includes('settings changed'))) ++}) +diff --git a/test/presentation-benchmark.test.mjs b/test/presentation-benchmark.test.mjs +new file mode 100644 +index 0000000..13313cc +--- /dev/null ++++ b/test/presentation-benchmark.test.mjs +@@ -0,0 +1,39 @@ ++import test from 'node:test' ++import assert from 'node:assert/strict' ++import { linkPresentation } from '../scripts/benchmark/presentation.mjs' ++const fixture = () => { ++ const identity = { sequence_number: 2, source_id: 1 } ++ const event = (name, ph, ts, args = {}, extra = {}) => ({ name, ph, ts, args, pid: 10, tid: 20, id2: { local: '0x3' }, ...extra }) ++ const events = [ ++ event('AnimationFrame', 'b', 100000, { id: 'exact-id', animation_frame_timing_info: { begin_frame_id: identity } }), ++ event('AnimationFrame', 'e', 104000), ++ event('FireAnimationFrame', 'X', 101000, {}, { dur: 2000 }), ++ event('prism.frame.0.spectrum.start', 'I', 101100, { data: { callTime: 101100, startTime: 1.1 } }), ++ event('prism.frame.0.spectrum.end', 'I', 102100, { data: { callTime: 102100, startTime: 2.1 } }), ++ event('AnimationFrame::Presentation', 'n', 110000, { id: 'exact-id', begin_frame_id: identity }), ++ event('PipelineReporter', 'b', 99000, { frame_reporter: { frame_source: 1, frame_sequence: 2, state: 'STATE_PRESENTED_ALL' } }, { tid: 30 }), ++ ] ++ const raw = { scopes: ['spectrum'], frames: [[0, 0, 1, 2, 1]], records: [[1, 4, 0, 0, 1001, 1, 1.5, 2, 1, 128]] } ++ const clock = { offset: -1000, uncertaintyMs: 0.1, timerQuantumMs: 0.1 } ++ return { events, raw, clock } ++} ++test('presentation estimate requires complete identity chain and calibrated clock bridge', () => { ++ const { events, raw, clock } = fixture(), result = linkPresentation(events, raw, clock) ++ assert.equal(result.available, true) ++ assert.equal(result.samples[0].latencyMs, 9) ++ assert.equal(result.frameLinks[0].drawToPresentationEstimateMs, 8) ++}) ++test('duplicate or wrong-frame presentation is rejected, regardless of proximity', () => { ++ const { events, raw, clock } = fixture() ++ assert.equal(linkPresentation([...events, events[5]], raw, clock).available, false) ++ events[5].args.begin_frame_id = { source_id: 1, sequence_number: 3 } ++ assert.equal(linkPresentation(events, raw, clock).available, false) ++}) ++test('a partial compositor frame or marker outside its RAF task cannot establish presentation', () => { ++ const { events, raw, clock } = fixture() ++ events[6].args.frame_reporter.state = 'STATE_PRESENTED_PARTIAL' ++ assert.equal(linkPresentation(events, raw, clock).available, false) ++ events[6].args.frame_reporter.state = 'STATE_PRESENTED_ALL' ++ events[2].dur = 10 ++ assert.equal(linkPresentation(events, raw, clock).available, false) ++}) diff --git a/docs/benchmarks/2026-09-16/results.json b/docs/benchmarks/2026-09-16/results.json new file mode 100644 index 0000000..749d5a1 --- /dev/null +++ b/docs/benchmarks/2026-09-16/results.json @@ -0,0 +1,7783 @@ +{ + "schemaVersion": 1, + "description": "Per-run summary statistics and recorded settings; raw per-chunk timestamps are not included.", + "source": { + "baseCommit": "2a2d01d32da824d03e60440acd505f3d890a4e6f", + "measurementCommit": "ce54ece6deceb9771d2d277d2be089a9f4b68205", + "packageVersion": "0.3.0-beta", + "patchSha256": "f4a582b71c39165193bcd2d5ea50561aea2aa9eed74d1fb2b68c5531184ea0ef", + "startedAt": "2026-09-16T23:48:56.144Z", + "completedAt": "2026-09-17T00:19:36.722Z", + "runs": 24, + "quick": false + }, + "machine": { + "platform": "darwin", + "arch": "arm64", + "os": "25.5.0", + "systemVersion": "26.5.1", + "model": "Mac17,9", + "cpu": "Apple M5 Pro", + "memoryBytes": 51539607552, + "versions": { + "node": "24.14.0", + "acorn": "8.15.0", + "ada": "3.4.2", + "amaro": "1.1.7", + "ares": "1.34.6", + "brotli": "1.0.9", + "cldr": "47.0", + "icu": "77.1", + "llhttp": "9.3.0", + "merve": "1.0.0", + "modules": "143", + "napi": "10", + "nbytes": "0.1.1", + "ncrypto": "0.0.1", + "nghttp2": "1.68.0", + "openssl": "0.0.0", + "simdjson": "4.2.4", + "simdutf": "7.7.0", + "sqlite": "3.51.2", + "tz": "2025b", + "undici": "7.21.0", + "unicode": "16.0", + "uv": "1.51.0", + "uvwasi": "0.0.23", + "v8": "14.4.258.32-electron.0", + "zlib": "1.3.1", + "zstd": "1.5.8", + "electron": "40.8.3", + "chrome": "144.0.7559.236" + }, + "display": { + "accelerometerSupport": "unknown", + "bounds": { + "x": 0, + "y": 0, + "width": 1512, + "height": 982 + }, + "colorDepth": 30, + "colorSpace": "{primaries:P3, transfer:{0.0777*x + 0.0000 if abs(x) < 0.0450 else sign(x)*((0.9495*abs(x) + 0.0495)**2.3955 + 0.0003)}, matrix:RGB, range:FULL}", + "depthPerComponent": 10, + "detected": true, + "displayFrequency": 120.0006103515625, + "id": 1, + "internal": true, + "label": "Built-in Retina Display", + "maximumCursorSize": { + "width": 0, + "height": 0 + }, + "monochrome": false, + "nativeOrigin": { + "x": 0, + "y": 0 + }, + "rotation": 0, + "scaleFactor": 2, + "size": { + "width": 1512, + "height": 982 + }, + "workArea": { + "x": 0, + "y": 33, + "width": 1512, + "height": 949 + }, + "workAreaSize": { + "width": 1512, + "height": 949 + }, + "touchSupport": "unknown" + }, + "bounds": { + "x": 156, + "y": 181, + "width": 1200, + "height": 360 + }, + "durationSeconds": 60, + "warmupSeconds": 10, + "repeats": 3, + "audioPlayer": "/usr/bin/afplay", + "signalFile": "stimulus.wav", + "powerSettings": "Battery Power:\n Sleep On Power Button 1\n powermode 0\n standby 1\n ttyskeepawake 1\n hibernatemode 3\n powernap 1\n hibernatefile /var/vm/sleepimage\n displaysleep 2\n womp 0\n networkoversleep 0\n sleep 1\n lessbright 0\n tcpkeepalive 1\n disksleep 10\nAC Power:\n Sleep On Power Button 1\n powermode 0\n standby 1\n ttyskeepawake 1\n hibernatemode 3\n powernap 1\n hibernatefile /var/vm/sleepimage\n displaysleep 10\n womp 1\n networkoversleep 0\n sleep 1\n tcpkeepalive 1\n disksleep 10\n" + }, + "stimulus": { + "sampleRate": 48000, + "channels": 2, + "format": "PCM16", + "seconds": 85, + "peakAmplitude": 0.025, + "amplitudeModulationHz": 1.37, + "frequencyStepSeconds": 0.73, + "leftHz": [ + 220, + 440, + 880, + 330 + ], + "rightHz": [ + 330, + 550, + 660, + 440 + ], + "sha256": "dc1679f2aa2c49758aab60bcbcb509bfbcf691667c38293f7a4ea9af7b7e3322" + }, + "nativeBuild": { + "sha256": "fcbe0711e820082797ffbdf36e13950381d1c057aa58fffb585f06853f84549d", + "electronVersion": "40.8.3", + "rebuiltFromSource": true + }, + "cooldown": { + "success": true, + "minimumSeconds": 120, + "requiredNominalSeconds": 60, + "elapsedSeconds": 120.46282191700001, + "thermalStates": [ + "nominal" + ] + }, + "thermal": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "sampleCount": 1744, + "overflow": 0, + "note": "1 Hz snapshots plus OS notifications; CPU speed limit is unknown until a notification arrives. Nominal thermal state does not prove an absence of all clock-frequency changes. No temperature or causal latency correction is inferred." + }, + "totalCpuUtilizationMeasured": false, + "runs": [ + { + "id": "01-spectrum-display-sync-baseline-0", + "config": "spectrum", + "target": "display-sync", + "mode": "baseline", + "repeat": 0, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60002.5, + "latencyUncertaintyMs": 0.229394793510437, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 7201, + "median": 1.1999998092651367, + "p95": 1.6999998092651367, + "p99": 1.9000000953674316, + "max": 2 + }, + "frameIntervalMs": { + "count": 7200, + "median": 8.300000190734863, + "p95": 9.300000190734863, + "p99": 9.400000095367432, + "max": 9.699999809265137 + }, + "observedFps": 120.00120001180939, + "frameCount": 7201, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.12439489364624023, + "endpointDriftMs": 0.03879070281982422, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "02-spectrum-display-sync-probe-1", + "config": "spectrum", + "target": "display-sync", + "mode": "probe", + "repeat": 1, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60005.40000009537, + "latencyUncertaintyMs": 0.21310365200042725, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5169, + "median": 7.477687358856201, + "p95": 12.2352294921875, + "p99": 13.566480159759521, + "max": 15.187979698181152 + }, + "upperLatencyMs": { + "count": 5169, + "median": 7.690791010856628, + "p95": 12.448333144187927, + "p99": 13.779583811759949, + "max": 15.40108335018158 + }, + "captureToReceiptMs": { + "count": 5065, + "median": 2.6026878356933594, + "p95": 4.7777299880981445, + "p99": 5.594770431518555, + "max": 6.610645294189453 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 4, + "p95": 7.100000381469727, + "p99": 7.599999904632568, + "max": 9 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 1.299999713897705, + "p95": 1.8000001907348633, + "p99": 2, + "max": 2.3000001907348633 + }, + "frameWorkMs": { + "count": 7200, + "median": 1.200000286102295, + "p95": 1.799999713897705, + "p99": 2, + "max": 2.3000001907348633 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 9.300000190734863, + "p99": 9.400000095367432, + "max": 11.5 + }, + "observedFps": 119.9881328588384, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 104, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.10810375213623047, + "endpointDriftMs": 0.0062084197998046875, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "03-spectrum-display-sync-probe-2", + "config": "spectrum", + "target": "display-sync", + "mode": "probe", + "repeat": 2, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60007.800000190735, + "latencyUncertaintyMs": 0.25085341930389404, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5169, + "median": 7.749477863311768, + "p95": 12.831936359405518, + "p99": 14.215853691101074, + "max": 16.641478061676025 + }, + "upperLatencyMs": { + "count": 5169, + "median": 8.00033175945282, + "p95": 13.08279025554657, + "p99": 14.466707587242126, + "max": 16.892331957817078 + }, + "captureToReceiptMs": { + "count": 4981, + "median": 2.690436363220215, + "p95": 5.056145191192627, + "p99": 5.757186412811279, + "max": 8.120478630065918 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 4.099999904632568, + "p95": 7.5, + "p99": 8.5, + "max": 9.599999904632568 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 1.299999713897705, + "p95": 1.8999996185302734, + "p99": 2, + "max": 2.1000003814697266 + }, + "frameWorkMs": { + "count": 7202, + "median": 1.200000286102295, + "p95": 1.8000001907348633, + "p99": 2, + "max": 2.1000003814697266 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10.199999809265137, + "p99": 10.399999618530273, + "max": 11.300000190734863 + }, + "observedFps": 120.00626612398408, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 188, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.14585351943969727, + "endpointDriftMs": 0.08170795440673828, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "04-spectrum-display-sync-probe-3", + "config": "spectrum", + "target": "display-sync", + "mode": "probe", + "repeat": 3, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60009.40000009537, + "latencyUncertaintyMs": 0.21766650676727295, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 1, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5169, + "median": 8.039499759674072, + "p95": 13.061541557312012, + "p99": 14.383831977844238, + "max": 16.698166847229004 + }, + "upperLatencyMs": { + "count": 5169, + "median": 8.257166743278503, + "p95": 13.279208540916443, + "p99": 14.60149896144867, + "max": 16.915833830833435 + }, + "captureToReceiptMs": { + "count": 5101, + "median": 2.801166534423828, + "p95": 5.261499881744385, + "p99": 6.096499443054199, + "max": 7.179290771484375 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 4.299999713897705, + "p95": 7.900000095367432, + "p99": 8.5, + "max": 9.200000286102295 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 1.200000286102295, + "p95": 1.9000000953674316, + "p99": 2, + "max": 2.1000003814697266 + }, + "frameWorkMs": { + "count": 7202, + "median": 1.200000286102295, + "p95": 1.8000001907348633, + "p99": 2, + "max": 2.1000003814697266 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10.200000286102295, + "p99": 10.400000095367432, + "max": 11.600000381469727 + }, + "observedFps": 120.0076660917098, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 68, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.11266660690307617, + "endpointDriftMs": 0.015334129333496094, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "05-oscilloscope-display-sync-baseline-0", + "config": "oscilloscope", + "target": "display-sync", + "mode": "baseline", + "repeat": 0, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60011.90000009537, + "latencyUncertaintyMs": 0.22754085063934326, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 7202, + "median": 0.5999999046325684, + "p95": 0.9000000953674316, + "p99": 0.9000000953674316, + "max": 1.200000286102295 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10.200000286102295, + "p99": 10.400000095367432, + "max": 11.399999618530273 + }, + "observedFps": 119.99766703605262, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.12254095077514648, + "endpointDriftMs": 0.03508281707763672, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "06-oscilloscope-display-sync-probe-1", + "config": "oscilloscope", + "target": "display-sync", + "mode": "probe", + "repeat": 1, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60007.2999997139, + "latencyUncertaintyMs": 0.238791823387146, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 1, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5169, + "median": 7.345249652862549, + "p95": 12.382124900817871, + "p99": 13.621125221252441, + "max": 15.643291473388672 + }, + "upperLatencyMs": { + "count": 5169, + "median": 7.584041476249695, + "p95": 12.620916724205017, + "p99": 13.859917044639587, + "max": 15.882083296775818 + }, + "captureToReceiptMs": { + "count": 5049, + "median": 2.8019165992736816, + "p95": 5.054374694824219, + "p99": 5.49970817565918, + "max": 6.5341668128967285 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 4, + "p95": 8, + "p99": 9.099999904632568, + "max": 9.800000190734863 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.6999998092651367, + "p95": 0.8000001907348633, + "p99": 0.9000000953674316, + "max": 1.200000286102295 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.5999999046325684, + "p95": 0.9000000953674316, + "p99": 0.9000000953674316, + "max": 1.4000000953674316 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10.200000286102295, + "p99": 10.400000095367432, + "max": 11.899999618530273 + }, + "observedFps": 120.0066661111574, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 120, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.13379192352294922, + "endpointDriftMs": 0.05758476257324219, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "07-oscilloscope-display-sync-probe-2", + "config": "oscilloscope", + "target": "display-sync", + "mode": "probe", + "repeat": 2, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60002.09999990463, + "latencyUncertaintyMs": 0.21385324001312256, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5168, + "median": 7.3501458168029785, + "p95": 12.303604125976562, + "p99": 13.47222900390625, + "max": 15.835021495819092 + }, + "upperLatencyMs": { + "count": 5168, + "median": 7.563999056816101, + "p95": 12.517457365989685, + "p99": 13.686082243919373, + "max": 16.048874735832214 + }, + "captureToReceiptMs": { + "count": 5038, + "median": 2.7769365310668945, + "p95": 5.029728889465332, + "p99": 5.519270896911621, + "max": 6.290228843688965 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 4.099999904632568, + "p95": 8, + "p99": 9.100000381469727, + "max": 9.900000095367432 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.6999998092651367, + "p95": 0.8000001907348633, + "p99": 0.9000000953674316, + "max": 1.299999713897705 + }, + "frameWorkMs": { + "count": 7200, + "median": 0.5999999046325684, + "p95": 0.9000000953674316, + "p99": 1, + "max": 1.3999996185302734 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 10.199999809265137, + "p99": 10.400000095367432, + "max": 12.800000190734863 + }, + "observedFps": 120.0043340917994, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 130, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.10885334014892578, + "endpointDriftMs": 0.0077075958251953125, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "08-oscilloscope-display-sync-probe-3", + "config": "oscilloscope", + "target": "display-sync", + "mode": "probe", + "repeat": 3, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60011.300000190735, + "latencyUncertaintyMs": 0.24416673183441162, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5170, + "median": 7.3863749504089355, + "p95": 12.353624820709229, + "p99": 13.42812442779541, + "max": 16.133207321166992 + }, + "upperLatencyMs": { + "count": 5170, + "median": 7.630541682243347, + "p95": 12.59779155254364, + "p99": 13.672291159629822, + "max": 16.377374053001404 + }, + "captureToReceiptMs": { + "count": 5020, + "median": 2.766416549682617, + "p95": 5.0372490882873535, + "p99": 5.4561662673950195, + "max": 8.925291061401367 + }, + "receiptToConsumptionMs": { + "count": 5170, + "median": 4.199999809265137, + "p95": 8, + "p99": 9.199999809265137, + "max": 9.900000095367432 + }, + "consumptionToDrawEndMs": { + "count": 5170, + "median": 0.6999998092651367, + "p95": 0.8000001907348633, + "p99": 0.9000000953674316, + "max": 1.200000286102295 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.5999999046325684, + "p95": 0.9000000953674316, + "p99": 0.9000000953674316, + "max": 1.3000001907348633 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10.200000286102295, + "p99": 10.400000095367432, + "max": 11.299999713897705 + }, + "observedFps": 120.00426622620478, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 150, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.13916683197021484, + "endpointDriftMs": 0.06833457946777344, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "09-default-rack-display-sync-baseline-0", + "config": "default-rack", + "target": "display-sync", + "mode": "baseline", + "repeat": 0, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60000.59999990463, + "latencyUncertaintyMs": 0.2610825300216675, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 7200, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 10.199999809265137, + "p99": 10.5, + "max": 10.700000286102295 + }, + "observedFps": 120.00133353336334, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 1 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 7200, + "median": 0.3000001907348633, + "p95": 0.40000009536743164, + "p99": 0.5, + "max": 0.6000003814697266 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 10.199999809265137, + "p99": 10.300000190734863, + "max": 10.600000381469727 + }, + "observedFps": 120.00093347062383, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 1 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 7200, + "median": 0.5, + "p95": 0.6000003814697266, + "p99": 0.7000002861022949, + "max": 0.8000001907348633 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 10.199999809265137, + "p99": 10.599999904632568, + "max": 10.900000095367432 + }, + "observedFps": 120.00093346967, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 1 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 7200, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.20000028610229492, + "max": 0.3000001907348633 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 10.199999809265137, + "p99": 10.599999904632568, + "max": 11.099999904632568 + }, + "observedFps": 120.00093346967, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.1560826301574707, + "endpointDriftMs": 0.10216617584228516, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "10-default-rack-display-sync-probe-1", + "config": "default-rack", + "target": "display-sync", + "mode": "probe", + "repeat": 1, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60000.800000190735, + "latencyUncertaintyMs": 0.26506173610687256, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5168, + "median": 6.313645362854004, + "p95": 11.039021492004395, + "p99": 12.349645614624023, + "max": 15.220146179199219 + }, + "upperLatencyMs": { + "count": 5168, + "median": 6.5787070989608765, + "p95": 11.304083228111267, + "p99": 12.614707350730896, + "max": 15.485207915306091 + }, + "captureToReceiptMs": { + "count": 4892, + "median": 2.584770679473877, + "p95": 4.885395526885986, + "p99": 5.463270664215088, + "max": 5.981645584106445 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 3.6999998092651367, + "p95": 7.799999713897705, + "p99": 8.599999904632568, + "max": 10.899999618530273 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.19999980926513672, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.39999961853027344 + }, + "frameWorkMs": { + "count": 7200, + "median": 0.19999980926513672, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.39999961853027344 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.299999713897705, + "p95": 10, + "p99": 10.400000095367432, + "max": 11.5 + }, + "observedFps": 120.00093347062383, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 276, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5168, + "median": 6.150813102722168, + "p95": 10.863229751586914, + "p99": 12.186604499816895, + "max": 15.020146369934082 + }, + "upperLatencyMs": { + "count": 5168, + "median": 6.4158748388290405, + "p95": 11.128291487693787, + "p99": 12.451666235923767, + "max": 15.285208106040955 + }, + "captureToReceiptMs": { + "count": 4892, + "median": 2.584770679473877, + "p95": 4.885395526885986, + "p99": 5.463270664215088, + "max": 5.981645584106445 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 3.5, + "p95": 7.599999904632568, + "p99": 8.5, + "max": 10.599999904632568 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 7200, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.300000190734863, + "p95": 10, + "p99": 10.300000190734863, + "max": 11.300000190734863 + }, + "observedFps": 120.00093347062383, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 276, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 5168, + "median": 6.572645664215088, + "p95": 11.316104412078857, + "p99": 12.629895687103271, + "max": 15.520146369934082 + }, + "upperLatencyMs": { + "count": 5168, + "median": 6.8377074003219604, + "p95": 11.58116614818573, + "p99": 12.894957423210144, + "max": 15.785208106040955 + }, + "captureToReceiptMs": { + "count": 4892, + "median": 2.584770679473877, + "p95": 4.885395526885986, + "p99": 5.463270664215088, + "max": 5.981645584106445 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 3.8999996185302734, + "p95": 8, + "p99": 8.800000190734863, + "max": 11.099999904632568 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 7200, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.299999713897705, + "p95": 10, + "p99": 10.400000095367432, + "max": 11.700000286102295 + }, + "observedFps": 120.00093346967, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 276, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 5168, + "median": 6.6382293701171875, + "p95": 11.354687690734863, + "p99": 12.685896396636963, + "max": 15.62014627456665 + }, + "upperLatencyMs": { + "count": 5168, + "median": 6.90329110622406, + "p95": 11.619749426841736, + "p99": 12.950958132743835, + "max": 15.885208010673523 + }, + "captureToReceiptMs": { + "count": 4892, + "median": 2.584770679473877, + "p95": 4.885395526885986, + "p99": 5.463270664215088, + "max": 5.981645584106445 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 4.199999809265137, + "p95": 8.200000286102295, + "p99": 9.099999904632568, + "max": 11.399999618530273 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameWorkMs": { + "count": 7200, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 7199, + "median": 8.299999713897705, + "p95": 10, + "p99": 10.400000095367432, + "max": 11.599999904632568 + }, + "observedFps": 120.00093347062383, + "frameCount": 7200, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 276, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.16006183624267578, + "endpointDriftMs": 0.11012458801269531, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "11-default-rack-display-sync-probe-2", + "config": "default-rack", + "target": "display-sync", + "mode": "probe", + "repeat": 2, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60009.5, + "latencyUncertaintyMs": 0.23379123210906982, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 4, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5169, + "median": 6.390501022338867, + "p95": 11.09675121307373, + "p99": 12.603333950042725, + "max": 15.534208297729492 + }, + "upperLatencyMs": { + "count": 5169, + "median": 6.624291777610779, + "p95": 11.330541968345642, + "p99": 12.837124705314636, + "max": 15.767999053001404 + }, + "captureToReceiptMs": { + "count": 4961, + "median": 2.6115832328796387, + "p95": 4.93583345413208, + "p99": 5.509792327880859, + "max": 6.00604248046875 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 3.8999996185302734, + "p95": 7.800000190734863, + "p99": 9.099999904632568, + "max": 9.900000095367432 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.19999980926513672, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.10000038146972656, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10.099999904632568, + "p99": 10.400000095367432, + "max": 10.599999904632568 + }, + "observedFps": 120.00186643628211, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 208, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5169, + "median": 6.233917713165283, + "p95": 10.919708251953125, + "p99": 12.410542488098145, + "max": 15.334208488464355 + }, + "upperLatencyMs": { + "count": 5169, + "median": 6.467708468437195, + "p95": 11.153499007225037, + "p99": 12.644333243370056, + "max": 15.567999243736267 + }, + "captureToReceiptMs": { + "count": 4961, + "median": 2.6115832328796387, + "p95": 4.93583345413208, + "p99": 5.509792327880859, + "max": 6.00604248046875 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 3.6999998092651367, + "p95": 7.599999904632568, + "p99": 8.900000095367432, + "max": 9.700000286102295 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10, + "p99": 10.300000190734863, + "max": 10.400000095367432 + }, + "observedFps": 120.00166645835937, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 208, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 5169, + "median": 6.678333282470703, + "p95": 11.37604284286499, + "p99": 12.891375541687012, + "max": 15.834208488464355 + }, + "upperLatencyMs": { + "count": 5169, + "median": 6.912124037742615, + "p95": 11.609833598136902, + "p99": 13.125166296958923, + "max": 16.067999243736267 + }, + "captureToReceiptMs": { + "count": 4961, + "median": 2.6115832328796387, + "p95": 4.93583345413208, + "p99": 5.509792327880859, + "max": 6.00604248046875 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 4, + "p95": 8, + "p99": 9.299999713897705, + "max": 10.099999904632568 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.299999713897705, + "p95": 10.099999904632568, + "p99": 10.400000095367432, + "max": 10.600000381469727 + }, + "observedFps": 120.00166645835937, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 208, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 5169, + "median": 6.727917671203613, + "p95": 11.419167041778564, + "p99": 12.944208145141602, + "max": 15.834208488464355 + }, + "upperLatencyMs": { + "count": 5169, + "median": 6.961708426475525, + "p95": 11.652957797050476, + "p99": 13.177998900413513, + "max": 16.067999243736267 + }, + "captureToReceiptMs": { + "count": 4961, + "median": 2.6115832328796387, + "p95": 4.93583345413208, + "p99": 5.509792327880859, + "max": 6.00604248046875 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 4.299999713897705, + "p95": 8.299999713897705, + "p99": 9.599999904632568, + "max": 10.400000095367432 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameWorkMs": { + "count": 7202, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.299999713897705, + "p95": 10.099999904632568, + "p99": 10.400000095367432, + "max": 10.600000381469727 + }, + "observedFps": 120.00186643628211, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 208, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.12879133224487305, + "endpointDriftMs": 0.047583580017089844, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "12-default-rack-display-sync-probe-3", + "config": "default-rack", + "target": "display-sync", + "mode": "probe", + "repeat": 3, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": "display-sync", + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60010.40000009537, + "latencyUncertaintyMs": 0.21001994609832764, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5170, + "median": 6.381020545959473, + "p95": 10.987812995910645, + "p99": 12.41881275177002, + "max": 15.535937786102295 + }, + "upperLatencyMs": { + "count": 5170, + "median": 6.591040015220642, + "p95": 11.197832465171814, + "p99": 12.628832221031189, + "max": 15.745957255363464 + }, + "captureToReceiptMs": { + "count": 4990, + "median": 2.6012706756591797, + "p95": 4.905938148498535, + "p99": 5.4233551025390625, + "max": 6.014895915985107 + }, + "receiptToConsumptionMs": { + "count": 5170, + "median": 3.700000286102295, + "p95": 7.799999713897705, + "p99": 8.699999809265137, + "max": 10 + }, + "consumptionToDrawEndMs": { + "count": 5170, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.19999980926513672, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10, + "p99": 10.400000095367432, + "max": 10.699999809265137 + }, + "observedFps": 120.0012665045134, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 180, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5170, + "median": 6.19868803024292, + "p95": 10.856687545776367, + "p99": 12.257229804992676, + "max": 15.3359375 + }, + "upperLatencyMs": { + "count": 5170, + "median": 6.408707499504089, + "p95": 11.066707015037537, + "p99": 12.467249274253845, + "max": 15.54595696926117 + }, + "captureToReceiptMs": { + "count": 4990, + "median": 2.6012706756591797, + "p95": 4.905938148498535, + "p99": 5.4233551025390625, + "max": 6.014895915985107 + }, + "receiptToConsumptionMs": { + "count": 5170, + "median": 3.5999999046325684, + "p95": 7.599999904632568, + "p99": 8.5, + "max": 9.799999713897705 + }, + "consumptionToDrawEndMs": { + "count": 5170, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.300000190734863, + "p95": 10, + "p99": 10.300000190734863, + "max": 10.600000381469727 + }, + "observedFps": 120.0010665276366, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 180, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 5170, + "median": 6.669188022613525, + "p95": 11.278979778289795, + "p99": 12.721396446228027, + "max": 15.8359375 + }, + "upperLatencyMs": { + "count": 5170, + "median": 6.879207491874695, + "p95": 11.488999247550964, + "p99": 12.931415915489197, + "max": 16.04595696926117 + }, + "captureToReceiptMs": { + "count": 4990, + "median": 2.6012706756591797, + "p95": 4.905938148498535, + "p99": 5.4233551025390625, + "max": 6.014895915985107 + }, + "receiptToConsumptionMs": { + "count": 5170, + "median": 3.9000000953674316, + "p95": 8, + "p99": 8.799999713897705, + "max": 10.199999809265137 + }, + "consumptionToDrawEndMs": { + "count": 5170, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5999999046325684 + }, + "frameWorkMs": { + "count": 7202, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5999999046325684 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.299999713897705, + "p95": 10, + "p99": 10.400000095367432, + "max": 10.699999809265137 + }, + "observedFps": 120.0012665045134, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 180, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 5170, + "median": 6.700145244598389, + "p95": 11.337437629699707, + "p99": 12.721396446228027, + "max": 15.935937881469727 + }, + "upperLatencyMs": { + "count": 5170, + "median": 6.910164713859558, + "p95": 11.547457098960876, + "p99": 12.931415915489197, + "max": 16.145957350730896 + }, + "captureToReceiptMs": { + "count": 4990, + "median": 2.6012706756591797, + "p95": 4.905938148498535, + "p99": 5.4233551025390625, + "max": 6.014895915985107 + }, + "receiptToConsumptionMs": { + "count": 5170, + "median": 4.199999809265137, + "p95": 8.200000286102295, + "p99": 9.099999904632568, + "max": 10.5 + }, + "consumptionToDrawEndMs": { + "count": 5170, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameWorkMs": { + "count": 7202, + "median": 0, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 7201, + "median": 8.299999713897705, + "p95": 10, + "p99": 10.400000095367432, + "max": 10.699999809265137 + }, + "observedFps": 120.0012665045134, + "frameCount": 7202, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 180, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.10502004623413086, + "endpointDriftMs": 0.00004100799560546875, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "13-spectrum-60-baseline-0", + "config": "spectrum", + "target": 60, + "mode": "baseline", + "repeat": 0, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60000.59999990463, + "latencyUncertaintyMs": 0.2347491979598999, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 3228, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3227, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26.399999618530273 + }, + "observedFps": 53.79059506358142, + "frameCount": 3228, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.12974929809570312, + "endpointDriftMs": 0.04949951171875, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "14-spectrum-60-probe-1", + "config": "spectrum", + "target": 60, + "mode": "probe", + "repeat": 1, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60002.300000190735, + "latencyUncertaintyMs": 0.21668708324432373, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 1, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5168, + "median": 11.899187088012695, + "p95": 24.29002094268799, + "p99": 27.971854209899902, + "max": 29.992562294006348 + }, + "upperLatencyMs": { + "count": 5168, + "median": 12.115874648094177, + "p95": 24.50670850276947, + "p99": 28.188541769981384, + "max": 30.20924985408783 + }, + "captureToReceiptMs": { + "count": 4919, + "median": 2.587228775024414, + "p95": 4.805229187011719, + "p99": 5.159811496734619, + "max": 5.92172908782959 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 9.200000286102295, + "p95": 19.700000286102295, + "p99": 24.5, + "max": 25.200000286102295 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.6000003814697266 + }, + "frameWorkMs": { + "count": 3211, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.6000003814697266 + }, + "frameIntervalMs": { + "count": 3210, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26 + }, + "observedFps": 53.51489497893235, + "frameCount": 3211, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 249, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.11168718338012695, + "endpointDriftMs": 0.013375282287597656, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "15-spectrum-60-probe-2", + "config": "spectrum", + "target": 60, + "mode": "probe", + "repeat": 2, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60006.5, + "latencyUncertaintyMs": 0.2282494306564331, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 2, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5168, + "median": 11.81491756439209, + "p95": 23.561209678649902, + "p99": 28.00254249572754, + "max": 29.594709396362305 + }, + "upperLatencyMs": { + "count": 5168, + "median": 12.043166518211365, + "p95": 23.789458632469177, + "p99": 28.230791449546814, + "max": 29.82295835018158 + }, + "captureToReceiptMs": { + "count": 4948, + "median": 2.5679593086242676, + "p95": 4.828917980194092, + "p99": 5.1947503089904785, + "max": 5.744501113891602 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 9.099999904632568, + "p95": 19.5, + "p99": 24.5, + "max": 25.299999713897705 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.6000003814697266 + }, + "frameWorkMs": { + "count": 3241, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.6000003814697266 + }, + "frameIntervalMs": { + "count": 3240, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26.09999990463257 + }, + "observedFps": 54.001800060002, + "frameCount": 3241, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 220, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.12324953079223633, + "endpointDriftMs": 0.036499977111816406, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "16-spectrum-60-probe-3", + "config": "spectrum", + "target": 60, + "mode": "probe", + "repeat": 3, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60010.89999961853, + "latencyUncertaintyMs": 0.22658288478851318, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5168, + "median": 12.02066707611084, + "p95": 24.281042098999023, + "p99": 28.084333419799805, + "max": 29.948291778564453 + }, + "upperLatencyMs": { + "count": 5168, + "median": 12.247249960899353, + "p95": 24.507624983787537, + "p99": 28.310916304588318, + "max": 30.174874663352966 + }, + "captureToReceiptMs": { + "count": 4910, + "median": 2.565751075744629, + "p95": 4.781625747680664, + "p99": 5.162417411804199, + "max": 5.7882914543151855 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 9.200000286102295, + "p95": 19.90000009536743, + "p99": 24.59999990463257, + "max": 25.800000190734863 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.5999999046325684 + }, + "frameWorkMs": { + "count": 3218, + "median": 0.40000009536743164, + "p95": 0.5, + "p99": 0.5, + "max": 0.5999999046325684 + }, + "frameIntervalMs": { + "count": 3217, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.5, + "max": 26.40000009536743 + }, + "observedFps": 53.63230942358188, + "frameCount": 3218, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 258, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.1215829849243164, + "endpointDriftMs": 0.03316688537597656, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "17-oscilloscope-60-baseline-0", + "config": "oscilloscope", + "target": 60, + "mode": "baseline", + "repeat": 0, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60004.60000038147, + "latencyUncertaintyMs": 0.2321246862411499, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 3258, + "median": 0.20000028610229492, + "p95": 0.39999961853027344, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3257, + "median": 16.699999809265137, + "p95": 25.199999809265137, + "p99": 25.300000190734863, + "max": 26.100000381469727 + }, + "observedFps": 54.27953376614225, + "frameCount": 3258, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.12712478637695312, + "endpointDriftMs": 0.04425048828125, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "18-oscilloscope-60-probe-1", + "config": "oscilloscope", + "target": 60, + "mode": "probe", + "repeat": 1, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60003.90000009537, + "latencyUncertaintyMs": 0.21399915218353271, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5169, + "median": 11.792458057403564, + "p95": 23.46583366394043, + "p99": 27.893625736236572, + "max": 29.731791973114014 + }, + "upperLatencyMs": { + "count": 5169, + "median": 12.006457209587097, + "p95": 23.679832816123962, + "p99": 28.107624888420105, + "max": 29.945791125297546 + }, + "captureToReceiptMs": { + "count": 4970, + "median": 2.6244163513183594, + "p95": 4.864790916442871, + "p99": 5.2530412673950195, + "max": 5.891958236694336 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 9.200000286102295, + "p95": 19.59999990463257, + "p99": 24.59999990463257, + "max": 25.699999809265137 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3246, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3245, + "median": 16.699999809265137, + "p95": 25.199999809265137, + "p99": 25.40000009536743, + "max": 26.59999990463257 + }, + "observedFps": 54.09162738312334, + "frameCount": 3246, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 199, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.10899925231933594, + "endpointDriftMs": 0.007999420166015625, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "19-oscilloscope-60-probe-2", + "config": "oscilloscope", + "target": 60, + "mode": "probe", + "repeat": 2, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60002.7000002861, + "latencyUncertaintyMs": 0.21645820140838623, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 1, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5169, + "median": 11.664832592010498, + "p95": 23.237083435058594, + "p99": 27.62833309173584, + "max": 29.96595811843872 + }, + "upperLatencyMs": { + "count": 5169, + "median": 11.881291270256042, + "p95": 23.453542113304138, + "p99": 27.844791769981384, + "max": 30.182416796684265 + }, + "captureToReceiptMs": { + "count": 4942, + "median": 2.5999159812927246, + "p95": 4.871999740600586, + "p99": 5.280165672302246, + "max": 5.994291305541992 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 9.199999809265137, + "p95": 19.5, + "p99": 24.699999809265137, + "max": 25.40000009536743 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3255, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3254, + "median": 16.699999809265137, + "p95": 25.199999809265137, + "p99": 25.40000009536743, + "max": 26.199999809265137 + }, + "observedFps": 54.23387567243487, + "frameCount": 3255, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 227, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.11145830154418945, + "endpointDriftMs": 0.012917518615722656, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "20-oscilloscope-60-probe-3", + "config": "oscilloscope", + "target": 60, + "mode": "probe", + "repeat": 3, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "oscilloscope" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 2400, + "height": 718, + "cssWidth": 1200, + "cssHeight": 359 + } + ] + }, + "durationMs": 60000.2999997139, + "latencyUncertaintyMs": 0.2264789342880249, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5168, + "median": 11.733896255493164, + "p95": 23.7511043548584, + "p99": 28.14960527420044, + "max": 30.1286883354187 + }, + "upperLatencyMs": { + "count": 5168, + "median": 11.960375189781189, + "p95": 23.977583289146423, + "p99": 28.376084208488464, + "max": 30.355167269706726 + }, + "captureToReceiptMs": { + "count": 4915, + "median": 2.5867300033569336, + "p95": 4.7827301025390625, + "p99": 5.155062675476074, + "max": 5.74360466003418 + }, + "receiptToConsumptionMs": { + "count": 5168, + "median": 9.299999713897705, + "p95": 19.600000381469727, + "p99": 24.700000286102295, + "max": 25.59999990463257 + }, + "consumptionToDrawEndMs": { + "count": 5168, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 3216, + "median": 0.20000028610229492, + "p95": 0.39999961853027344, + "p99": 0.40000009536743164, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 3215, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26.40000009536743 + }, + "observedFps": 53.59905572292686, + "frameCount": 3216, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 253, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.12147903442382812, + "endpointDriftMs": 0.032958984375, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "21-default-rack-60-baseline-0", + "config": "default-rack", + "target": 60, + "mode": "baseline", + "repeat": 0, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60007.09999990463, + "latencyUncertaintyMs": 0.2488325834274292, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 3256, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3255, + "median": 16.700000286102295, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.5 + }, + "observedFps": 54.258410053299535, + "frameCount": 3256, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 3256, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3255, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26.59999990463257 + }, + "observedFps": 54.25831960926548, + "frameCount": 3256, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 3256, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 3255, + "median": 16.700000286102295, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.59999990463257 + }, + "observedFps": 54.25841005373081, + "frameCount": 3256, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "upperLatencyMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "captureToReceiptMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "receiptToConsumptionMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "consumptionToDrawEndMs": { + "count": 0, + "median": null, + "p95": null, + "p99": null, + "max": null + }, + "frameWorkMs": { + "count": 3256, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.20000028610229492, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 3255, + "median": 16.700000286102295, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.40000009536743 + }, + "observedFps": 54.258410053299535, + "frameCount": 3256, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 0, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.14383268356323242, + "endpointDriftMs": 0.0776662826538086, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "22-default-rack-60-probe-1", + "config": "default-rack", + "target": 60, + "mode": "probe", + "repeat": 1, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60006.300000190735, + "latencyUncertaintyMs": 0.27337491512298584, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 4, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5167, + "median": 11.838956832885742, + "p95": 23.937374591827393, + "p99": 28.06879186630249, + "max": 30.498416900634766 + }, + "upperLatencyMs": { + "count": 5167, + "median": 12.112331748008728, + "p95": 24.21074950695038, + "p99": 28.342166781425476, + "max": 30.77179181575775 + }, + "captureToReceiptMs": { + "count": 4906, + "median": 2.6354165077209473, + "p95": 4.831916809082031, + "p99": 5.317707061767578, + "max": 6.037332534790039 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.5, + "p95": 19.90000009536743, + "p99": 24.59999990463257, + "max": 25.799999713897705 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.3000001907348633 + }, + "frameWorkMs": { + "count": 3249, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.3000001907348633 + }, + "frameIntervalMs": { + "count": 3248, + "median": 16.699999809265137, + "p95": 25.300000190734863, + "p99": 25.5, + "max": 26.5 + }, + "observedFps": 54.14930737926842, + "frameCount": 3249, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 261, + "unconsumedAtStop": 2, + "pendingAtStop": 2 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5167, + "median": 11.678291320800781, + "p95": 23.748541355133057, + "p99": 27.93358278274536, + "max": 30.29841709136963 + }, + "upperLatencyMs": { + "count": 5167, + "median": 11.951666235923767, + "p95": 24.021916270256042, + "p99": 28.206957697868347, + "max": 30.571792006492615 + }, + "captureToReceiptMs": { + "count": 4906, + "median": 2.6354165077209473, + "p95": 4.831916809082031, + "p99": 5.317707061767578, + "max": 6.037332534790039 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.299999713897705, + "p95": 19.59999990463257, + "p99": 24.300000190734863, + "max": 25.399999618530273 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3249, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3248, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26.199999809265137 + }, + "observedFps": 54.14912682865836, + "frameCount": 3249, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 261, + "unconsumedAtStop": 2, + "pendingAtStop": 2 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 5167, + "median": 12.13249921798706, + "p95": 24.237374305725098, + "p99": 28.378957271575928, + "max": 30.79841709136963 + }, + "upperLatencyMs": { + "count": 5167, + "median": 12.405874133110046, + "p95": 24.510749220848083, + "p99": 28.652332186698914, + "max": 31.071792006492615 + }, + "captureToReceiptMs": { + "count": 4906, + "median": 2.6354165077209473, + "p95": 4.831916809082031, + "p99": 5.317707061767578, + "max": 6.037332534790039 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.699999809265137, + "p95": 20, + "p99": 24.799999713897705, + "max": 25.899999618530273 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3249, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3248, + "median": 16.699999809265137, + "p95": 25.300000190734863, + "p99": 25.5, + "max": 26.399999618530273 + }, + "observedFps": 54.14930737883795, + "frameCount": 3249, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 261, + "unconsumedAtStop": 2, + "pendingAtStop": 2 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 5167, + "median": 12.1589994430542, + "p95": 24.267791748046875, + "p99": 28.4442081451416, + "max": 30.898416996002197 + }, + "upperLatencyMs": { + "count": 5167, + "median": 12.432374358177185, + "p95": 24.54116666316986, + "p99": 28.717583060264587, + "max": 31.171791911125183 + }, + "captureToReceiptMs": { + "count": 4906, + "median": 2.6354165077209473, + "p95": 4.831916809082031, + "p99": 5.317707061767578, + "max": 6.037332534790039 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 10, + "p95": 20.300000190734863, + "p99": 25.09999990463257, + "max": 26.199999809265137 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameWorkMs": { + "count": 3249, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 3248, + "median": 16.699999809265137, + "p95": 25.300000190734863, + "p99": 25.5, + "max": 26.40000009536743 + }, + "observedFps": 54.14930737926842, + "frameCount": 3249, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 261, + "unconsumedAtStop": 2, + "pendingAtStop": 2 + } + ], + "clock": { + "uncertaintyMs": 0.16837501525878906, + "endpointDriftMs": 0.12675094604492188, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "23-default-rack-60-probe-2", + "config": "default-rack", + "target": 60, + "mode": "probe", + "repeat": 2, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60006, + "latencyUncertaintyMs": 0.25222909450531006, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 4, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5167, + "median": 11.70135498046875, + "p95": 23.293687343597412, + "p99": 27.861562728881836, + "max": 30.002562522888184 + }, + "upperLatencyMs": { + "count": 5167, + "median": 11.95358407497406, + "p95": 23.545916438102722, + "p99": 28.113791823387146, + "max": 30.254791617393494 + }, + "captureToReceiptMs": { + "count": 4894, + "median": 2.589229106903076, + "p95": 4.787895202636719, + "p99": 5.201145172119141, + "max": 5.875354290008545 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.5, + "p95": 19.799999713897705, + "p99": 24.59999990463257, + "max": 25.700000286102295 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.19999980926513672, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.3000001907348633 + }, + "frameWorkMs": { + "count": 3270, + "median": 0.19999980926513672, + "p95": 0.20000028610229492, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 3269, + "median": 16.699999809265137, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.600000381469727 + }, + "observedFps": 54.491688725431345, + "frameCount": 3270, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 273, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5167, + "median": 11.537562370300293, + "p95": 23.107354164123535, + "p99": 27.626479625701904, + "max": 29.902562618255615 + }, + "upperLatencyMs": { + "count": 5167, + "median": 11.789791464805603, + "p95": 23.359583258628845, + "p99": 27.878708720207214, + "max": 30.154791712760925 + }, + "captureToReceiptMs": { + "count": 4894, + "median": 2.589229106903076, + "p95": 4.787895202636719, + "p99": 5.201145172119141, + "max": 5.875354290008545 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.199999809265137, + "p95": 19.5, + "p99": 24.299999713897705, + "max": 25.40000009536743 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3270, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3269, + "median": 16.699999809265137, + "p95": 25.199999809265137, + "p99": 25.300000190734863, + "max": 26.40000009536743 + }, + "observedFps": 54.49168872586447, + "frameCount": 3270, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 273, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 5167, + "median": 11.984437942504883, + "p95": 23.607354164123535, + "p99": 28.126479625701904, + "max": 30.302562713623047 + }, + "upperLatencyMs": { + "count": 5167, + "median": 12.236667037010193, + "p95": 23.859583258628845, + "p99": 28.378708720207214, + "max": 30.554791808128357 + }, + "captureToReceiptMs": { + "count": 4894, + "median": 2.589229106903076, + "p95": 4.787895202636719, + "p99": 5.201145172119141, + "max": 5.875354290008545 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.599999904632568, + "p95": 19.90000009536743, + "p99": 24.700000286102295, + "max": 25.90000009536743 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3270, + "median": 0.3000001907348633, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3269, + "median": 16.699999809265137, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.59999990463257 + }, + "observedFps": 54.49168872586447, + "frameCount": 3270, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 273, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 5167, + "median": 12.047228813171387, + "p95": 23.641811847686768, + "p99": 28.16156244277954, + "max": 30.402562618255615 + }, + "upperLatencyMs": { + "count": 5167, + "median": 12.299457907676697, + "p95": 23.894040942192078, + "p99": 28.41379153728485, + "max": 30.654791712760925 + }, + "captureToReceiptMs": { + "count": 4894, + "median": 2.589229106903076, + "p95": 4.787895202636719, + "p99": 5.201145172119141, + "max": 5.875354290008545 + }, + "receiptToConsumptionMs": { + "count": 5167, + "median": 9.900000095367432, + "p95": 20.199999809265137, + "p99": 25, + "max": 26.200000286102295 + }, + "consumptionToDrawEndMs": { + "count": 5167, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameWorkMs": { + "count": 3270, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.19999980926513672, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 3269, + "median": 16.700000286102295, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.5 + }, + "observedFps": 54.491688725431345, + "frameCount": 3270, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 273, + "unconsumedAtStop": 1, + "pendingAtStop": 1 + } + ], + "clock": { + "uncertaintyMs": 0.14722919464111328, + "endpointDriftMs": 0.08445930480957031, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + }, + { + "id": "24-default-rack-60-probe-3", + "config": "default-rack", + "target": 60, + "mode": "probe", + "repeat": 3, + "metadata": { + "capture": { + "captureMode": "system", + "activeBackendKind": "native-macos", + "backendSupport": { + "nativeBackend": { + "kind": "native-macos", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "deviceInput": { + "kind": "device-input", + "available": true, + "reason": null, + "channelRoutingAvailable": true + }, + "dawBridge": { + "kind": "daw-bridge", + "available": true, + "reason": null + } + }, + "sampleRate": 44100, + "channelCount": 2, + "sourceChannelCount": 2, + "channelRoutingAvailable": true, + "channelRouting": { + "left": 0, + "right": 1 + }, + "isCapturing": true, + "activeSourceId": "BuiltInSpeakerDevice", + "activeSourceLabel": "MacBook Pro Speakers", + "waiting": false + }, + "session": { + "sessionId": 1, + "sampleRate": 44100, + "channelCount": 2, + "capturing": true, + "suspended": false, + "backendKind": "native-macos" + }, + "frameTarget": 60, + "scopeSettings": { + "waterfall": { + "historySeconds": 5, + "density": "balanced", + "colorMode": "theme", + "showGrid": true, + "fftSize": 2048, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "smoothing": 0.9, + "tiltDbPerOctave": 2 + }, + "spectrum": { + "reference": null, + "rotation": 0, + "mirrorHorizontal": false, + "scaleMode": "log", + "frequencyRangeMode": "extended", + "fftSize": 2048, + "tiltDbPerOctave": 2, + "heatmap": false, + "heatmapTiltDbPerOctave": 2, + "heatmapSmoothing": 0.5, + "showGrid": true, + "smoothing": 0.9, + "fillGradient": true, + "showSideLine": false, + "peakInfoMode": "off" + }, + "oscilloscope": { + "rotation": 0, + "mirrorHorizontal": false, + "pitchLock": true, + "underfillEnabled": false, + "showGrid": true, + "lineWidth": 2 + }, + "vectorscope": { + "mode": "lissajous", + "zoomDb": 0, + "multiband": false, + "showGrid": true, + "persistence": 0.1, + "lineWidth": 1.5 + }, + "spectrogram": { + "rotation": 0, + "mirrorHorizontal": false, + "fftSize": 4096, + "tiltDbPerOctave": 4, + "scrollSpeed": 2, + "contrast": 1, + "clarityMode": "sharper", + "scaleMode": "log", + "frequencyRangeMode": "extended", + "showGrid": true, + "colorScheme": "heat", + "timelineUnit": "bars-beats" + }, + "vumeter": { + "mode": "bar", + "orientation": "horizontal", + "needleChannels": "stereo", + "referenceDb": -14 + }, + "lufsmeter": { + "mode": "bar", + "readout": "shortTerm" + }, + "waveform": { + "rotation": 0, + "mirrorHorizontal": false, + "mode": "mono", + "scrollSpeed": 1, + "multiband": false, + "timelineUnit": "bars-beats" + }, + "nowPlaying": { + "showCoverArt": true, + "showTitle": true, + "showArtist": true, + "showProgress": true, + "showTime": true, + "showControls": true + } + }, + "visibleScopes": [ + "spectrum", + "oscilloscope", + "vectorscope", + "vumeter" + ], + "pixelRatio": 2, + "innerWidth": 1200, + "innerHeight": 360, + "hidden": false, + "canvases": [ + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 684, + "height": 718, + "cssWidth": 342, + "cssHeight": 359 + }, + { + "width": 342, + "height": 718, + "cssWidth": 171, + "cssHeight": 359 + } + ] + }, + "durationMs": 60009.5, + "latencyUncertaintyMs": 0.21195733547210693, + "problems": [], + "valid": true, + "counters": { + "boundaryChunks": 0, + "nativeOverwrites": 0, + "sequenceGaps": 0, + "sessionChanges": 0, + "bufferOverflow": 0, + "missingReceipts": 0, + "trimmedChunks": 0, + "consumedOutsideFrame": 0, + "nativeQueueMax": 0 + }, + "chunkSizes": [ + 512 + ], + "scopes": [ + { + "scope": "spectrum", + "latencyMs": { + "count": 5169, + "median": 11.647250175476074, + "p95": 23.729416370391846, + "p99": 27.86920738220215, + "max": 30.419166564941406 + }, + "upperLatencyMs": { + "count": 5169, + "median": 11.859207510948181, + "p95": 23.941373705863953, + "p99": 28.081164717674255, + "max": 30.631123900413513 + }, + "captureToReceiptMs": { + "count": 4951, + "median": 2.5874576568603516, + "p95": 4.795250415802002, + "p99": 5.260249614715576, + "max": 5.884374618530273 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 9.400000095367432, + "p95": 19.800000190734863, + "p99": 24.59999990463257, + "max": 25.5 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameWorkMs": { + "count": 3268, + "median": 0.19999980926513672, + "p95": 0.2999997138977051, + "p99": 0.3000001907348633, + "max": 0.40000009536743164 + }, + "frameIntervalMs": { + "count": 3267, + "median": 16.699999809265137, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.5 + }, + "observedFps": 54.45835028020318, + "frameCount": 3268, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 218, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "oscilloscope", + "latencyMs": { + "count": 5169, + "median": 11.487542152404785, + "p95": 23.57479190826416, + "p99": 27.67212438583374, + "max": 30.1191668510437 + }, + "upperLatencyMs": { + "count": 5169, + "median": 11.699499487876892, + "p95": 23.786749243736267, + "p99": 27.884081721305847, + "max": 30.331124186515808 + }, + "captureToReceiptMs": { + "count": 4951, + "median": 2.5874576568603516, + "p95": 4.795250415802002, + "p99": 5.260249614715576, + "max": 5.884374618530273 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 9.199999809265137, + "p95": 19.59999990463257, + "p99": 24.200000286102295, + "max": 25.199999809265137 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.20000028610229492, + "p95": 0.39999961853027344, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3268, + "median": 0.20000028610229492, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3267, + "median": 16.699999809265137, + "p95": 25.200000286102295, + "p99": 25.40000009536743, + "max": 26.40000009536743 + }, + "observedFps": 54.45835028020318, + "frameCount": 3268, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 218, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vectorscope", + "latencyMs": { + "count": 5169, + "median": 11.947832584381104, + "p95": 23.99345827102661, + "p99": 28.12649965286255, + "max": 30.6191668510437 + }, + "upperLatencyMs": { + "count": 5169, + "median": 12.15978991985321, + "p95": 24.20541560649872, + "p99": 28.338456988334656, + "max": 30.831124186515808 + }, + "captureToReceiptMs": { + "count": 4951, + "median": 2.5874576568603516, + "p95": 4.795250415802002, + "p99": 5.260249614715576, + "max": 5.884374618530273 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 9.599999904632568, + "p95": 20, + "p99": 24.700000286102295, + "max": 25.699999809265137 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameWorkMs": { + "count": 3268, + "median": 0.2999997138977051, + "p95": 0.40000009536743164, + "p99": 0.40000009536743164, + "max": 0.5 + }, + "frameIntervalMs": { + "count": 3267, + "median": 16.699999809265137, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.5 + }, + "observedFps": 54.45844105853719, + "frameCount": 3268, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 218, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + }, + { + "scope": "vumeter", + "latencyMs": { + "count": 5169, + "median": 11.989542007446289, + "p95": 24.041874885559082, + "p99": 28.16920757293701, + "max": 30.71916675567627 + }, + "upperLatencyMs": { + "count": 5169, + "median": 12.201499342918396, + "p95": 24.25383222103119, + "p99": 28.38116490840912, + "max": 30.931124091148376 + }, + "captureToReceiptMs": { + "count": 4951, + "median": 2.5874576568603516, + "p95": 4.795250415802002, + "p99": 5.260249614715576, + "max": 5.884374618530273 + }, + "receiptToConsumptionMs": { + "count": 5169, + "median": 9.900000095367432, + "p95": 20.299999713897705, + "p99": 25, + "max": 25.90000009536743 + }, + "consumptionToDrawEndMs": { + "count": 5169, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.20000028610229492, + "max": 0.20000028610229492 + }, + "frameWorkMs": { + "count": 3268, + "median": 0.09999990463256836, + "p95": 0.10000038146972656, + "p99": 0.20000028610229492, + "max": 0.20000028610229492 + }, + "frameIntervalMs": { + "count": 3267, + "median": 16.699999809265137, + "p95": 25.299999713897705, + "p99": 25.5, + "max": 26.5 + }, + "observedFps": 54.45844105853719, + "frameCount": 3268, + "skippedDrawFrames": 0, + "invalid": 0, + "skippedChunks": 0, + "duplicate": 0, + "unresolvedDelivery": 218, + "unconsumedAtStop": 0, + "pendingAtStop": 0 + } + ], + "clock": { + "uncertaintyMs": 0.10695743560791016, + "endpointDriftMs": 0.0039157867431640625, + "timerQuantumMs": 0.10499989986419678 + }, + "environment": { + "thermalStates": [ + "nominal" + ], + "batteryObserved": false, + "minimumReportedCpuSpeedLimitPercent": null, + "overflow": 0 + } + } + ], + "presentation": { + "available": true, + "source": "Chromium AnimationFrame::Presentation; macOS presentation feedback is an estimate", + "caveat": "Separate tracing workload. Software clock bounds do not bound OS presentation-estimation error or physical pixel response.", + "matchedFrames": 4804, + "rejectedFrames": 0, + "matchedChunkScopeRecords": 3448, + "successfulChunkScopeRecords": 3448, + "softwareTimingUncertaintyMs": 0.24504077434539795, + "traceValidity": [], + "scopes": [ + { + "scope": "spectrum", + "estimatedLatencyMs": { + "count": 862, + "median": 25.212917305063456, + "p95": 30.112042407970876, + "p99": 31.347167072352022, + "max": 32.14487523259595 + } + }, + { + "scope": "oscilloscope", + "estimatedLatencyMs": { + "count": 862, + "median": 25.223374881781638, + "p95": 30.15829247282818, + "p99": 31.303667060798034, + "max": 32.12187494663522 + } + }, + { + "scope": "vectorscope", + "estimatedLatencyMs": { + "count": 862, + "median": 25.202917449874803, + "p95": 30.134834590833634, + "p99": 31.273625614121556, + "max": 32.145874946611 + } + }, + { + "scope": "vumeter", + "estimatedLatencyMs": { + "count": 862, + "median": 25.2163344649598, + "p95": 30.103792789392173, + "p99": 31.29616726306267, + "max": 32.18487532809377 + } + } + ] + } +} diff --git a/docs/channel-routing-testing.md b/docs/channel-routing-testing.md new file mode 100644 index 0000000..473c9ea --- /dev/null +++ b/docs/channel-routing-testing.md @@ -0,0 +1,81 @@ +# Multichannel routing validation + +Routing supports the OS-exposed channels of one selected source. Windows uses +WASAPI shared-mode capture; Linux uses libpulse with PulseAudio or pipewire-pulse. +The source's full channel count and activity are separate from the mono/stereo +audio delivered to analyzers and rolling clips. + +## Automated checks + +Build the native addon before running native tests: + +```sh +npm run build:native +npm run typecheck +npm run test:capture-support +npm run test:capture-channel-selection-native +npm run test:spectrum-native +npm run test:audio-store +npm run test:audio-router +npm run test:renderer-helpers +npm run test:tui +npm run build +``` + +For an Electron development build, use `npm run rebuild:native` afterward to +restore the addon build against Electron's headers. + +On Linux, install the PulseAudio server and command-line tools, then run: + +```sh +npm run test:linux-capture +``` + +The test launches a private server/socket, creates a six-channel null sink and +non-monitor input, and feeds independent tones into each channel. It verifies +enumeration, defaults, full channel metadata, startup/live/duplicate routing, +independent input/output state, activity on unselected channels, silence, +restart, removal, mono reconnect, and server failure. It does not connect to +the desktop's audio server. Temporary processes and files are removed afterward. +CI's Linux quality gate runs this test separately from `npm test`. + +To run the same integration test against PipeWire, install `pipewire`, +`pipewire-pulse`, WirePlumber 0.5+, `dbus-daemon`, and the PulseAudio CLI tools: + +```sh +PRISM_CAPTURE_TEST_SERVER=pipewire npm run test:linux-capture +``` + +This starts a private PipeWire server and a policy-only WirePlumber instance; +physical-device discovery is disabled and configuration/state paths are private. + +## Hardware acceptance + +For Windows and Linux, use a multichannel output endpoint plus an input interface +whose current OS configuration exposes more than two channels: + +1. Confirm every exposed channel appears with a driver/channel-map label or a + numbered fallback. Route a channel above 2 to each analyzer side, then route + one channel to both sides. Confirm signal identity and activity on unselected + channels; changing input trim must not change the activity fills. +2. Change routes during capture. Confirm no restart, dropout caused by reopening + the device, or changes to the other capture mode's saved route. +3. Switch between mono, stereo, and multichannel devices, including different + sample rates. Change the selected device's OS channel layout while retaining + its ID; confirm capture reopens and invalid routes become the default pair. +4. Stop playback, unplug/reconnect the device, change the default device, and + restart Prism. Check silence, existing retry/default notices, and saved routes. +5. Upgrade from a saved browser input ID. Confirm Default Input is selected with + a notice and unrelated saved routes are retained. + +## Validation recorded for this change + +- macOS arm64: native addon, desktop, and TUI builds; native PCM/export tests, + capability tests, renderer/store/router tests, and typecheck passed. +- Linux arm64 container: full native addon and TUI builds/tests; six-channel + integration passed with PulseAudio and PipeWire compatibility. +- Windows x64: capture core and complete TUI cross-built with MinGW, including + audio GUID linkage. The full Electron addon still needs a Windows/MSVC build. +- Physical multichannel hardware, Windows runtime audio, and live desktop + hardware acceptance above were unavailable in this environment and remain + unverified. Virtual audio testing does not establish driver-specific behavior. diff --git a/docs/daw-bridge.md b/docs/daw-bridge.md new file mode 100644 index 0000000..1bc07f7 --- /dev/null +++ b/docs/daw-bridge.md @@ -0,0 +1,92 @@ +# Prism Bridge protocol and host notes + +Prism Bridge is a mono/stereo pass-through effect that sends one selected DAW +track or bus to the standalone Prism application. Prism owns the server and +binds only `127.0.0.1:51789`; Bridge instances reconnect with exponential +backoff from 250 ms to 5 seconds. + +## Using Bridge + +1. Open the standalone Prism application. +2. Insert **Prism Bridge** on each track or bus you may want to inspect. +3. Bridge uses the DAW track name automatically when supplied. To override it, + open its compact nameplate and click the name. Enter or clicking away saves; + Escape cancels. Clearing the name restores automatic naming. +4. In Prism's audio selector, choose an instance under **DAW Bridges**. + +Only the selected instance transmits audio. If it disconnects, Prism stays in +DAW mode and displays **Waiting** without falling back to another input. A +same-format reconnect preserves scope history; a sample-rate or channel-layout +change begins a new analysis session. + +The nameplate is optional: Bridge runs with its editor closed. Its status dot is +gray while waiting for Prism, blue when available, and green when selected; +hover for the status text. Custom names save with the DAW project. Unnamed +sources use `Bridge` plus the nameplate's eight-character live instance tag. +Prism includes the host in source labels and adds the matching tag when names +would otherwise be identical. Tags identify live instances and can change when +a project is reopened; saved source UUIDs and custom names remain unchanged. +Renaming or opening the nameplate does not select a source in Prism. + +Waveform and Spectrogram have a **Timeline** setting with Off, Bars + Beats, and +Seconds choices. The ruler is drawn only for DAW Bridge input. Bars + Beats uses +the host PPQ, tempo, meter, and bar anchor; when beat metadata is missing it +shows a labeled seconds fallback. Loop, seek, and packet discontinuities are +kept in history as dashed Loop, Jump, and Gap seams. + +## Wire format (version 1) + +Every frame starts with a 12-byte little-endian header: + +| Offset | Type | Meaning | +| ---: | --- | --- | +| 0 | `uint32` | Magic `0x4d535250` | +| 4 | `uint16` | Protocol version (`1`) | +| 6 | `uint16` | Message type | +| 8 | `uint32` | Payload byte length | + +Hello (`1`) and source update (`3`) payloads are UTF-8 JSON. Heartbeat (`2`) +has no payload. Prism sends a JSON subscription (`10`) containing `selected`. +Audio (`20`) uses a 76-byte binary metadata header followed by little-endian +planar Float32 samples (all left samples, then all right samples). Metadata +includes sequence, frame count, channels, sample rate, sample/seconds/PPQ +position, tempo, last-bar PPQ, loop points, time signature, and play, record, +and loop flags. + +Payloads larger than 256 KiB and unknown or mismatched protocol versions are +rejected. Prism caps live connections, batches renderer IPC, and expires clients +after three seconds without traffic. Bridge packetizes into preallocated +512-frame records on the audio thread and keeps only the newest approximately +50 ms before transmission. Socket work, JSON, allocation, and locking stay on +the background or UI threads. + +The stable source UUID and custom name are stored in plug-in state. If a DAW +duplicates that state, Prism assigns distinct live keys to all duplicates and +requires an explicit choice instead of guessing which instance was intended. + +## Formats and validation + +Run `npm run test:daw-bridge` for source naming and connection tests. Build +`PrismBridgeTests`, then run `ctest --test-dir plugin/build -R PrismBridgeProcessorTests +--output-on-failure` for state and pass-through checks. `PrismBridgeTests --ui-test` +exercises the native name editor and writes PNGs to the temporary +`prism-bridge-nameplate-qa` directory; `--ui` leaves the nameplate open for manual +inspection. + +Bridge ships as VST3 and CLAP on macOS, Windows, and Linux, and as AU on macOS. It has no +Standalone target. `AU_SANDBOX_SAFE FALSE` is intentional because the AU needs +loopback networking. Logic may load Audio Units in `AUHostingServiceXPC`, so a +successful build or `auval` scan is not enough: live Logic-to-Prism loopback is +a release blocker. + +See Apple's [out-of-process Audio Unit debugging documentation](https://developer.apple.com/documentation/audiotoolbox/debugging-out-of-process-audio-units-on-apple-silicon?language=objc) +and [Audio Unit sandboxing guide](https://developer.apple.com/library/archive/technotes/tn2312/_index.html). + +### CLAP validation + +CLAP uses the same pass-through processor, state, and Bridge protocol as VST3/AU. +Its stable ID is `com.astra.prism.PrismBridge`. Verify discovery, mono/stereo audio, +custom-name recall, duplicate-instance selection, transport/loop/seek updates, +and reconnecting after the Prism app restarts. Host track names are provided when +the DAW supports CLAP track-info; custom names and the instance tag remain available. +See [plugin build and validation instructions](../plugin/README.md). diff --git a/docs/latency.md b/docs/latency.md new file mode 100644 index 0000000..5e12768 --- /dev/null +++ b/docs/latency.md @@ -0,0 +1,229 @@ +# Capture-to-render latency + +The default Spectrum/Oscilloscope/Vectorscope/VU rack measured about **7 ms median +capture-to-render latency** on an M5 Pro at an observed 120 FPS. Each scope's p95 +was below **12 ms** in all three measured repeats, including the software timing +uncertainty. This is a result for the tested macOS desktop setup, not a guarantee +for every machine, capture mode, scope configuration, or platform. + +Here, **capture-to-render** means the time from receipt inside Prism's native +CoreAudio capture callback to completion of the scope's canvas drawing commands. +It does not include audio buffering before that callback, GPU/compositor work, +display scanout, or pixel response. It is not a capture-to-physical-display or +sound-to-visible-response measurement. + +## Default rack at display-sync + +These are the largest per-scope, per-run statistics across three 60-second runs, +not pooled percentiles or a selection of the fastest run. + +| Statistic | Time | +| --- | ---: | +| Median | 6.73 ms | +| p95 | 11.42 ms | +| p99 | 12.94 ms | +| Largest individual observation | 15.94 ms | +| Highest p95 after allowing for timing uncertainty | 11.65 ms | + +Observed frame cadence was approximately 120 FPS. The largest software timing +uncertainty in these rack runs was ±0.265 ms. The observed maximum is not a +guaranteed upper bound. These measurements do not support an under-8-ms p95 claim. + +## Tested setup + +The suite ran on September 16, 2026 (America/New_York). Its instrumented source was +based on [`2a2d01d`](https://github.com/Boof2015/prism/commit/2a2d01d32da824d03e60440acd505f3d890a4e6f) +from the `0.3.1` branch. The package version at that commit was `0.3.0-beta`. + +| Setting | Value | +| --- | --- | +| Computer | Apple M5 Pro, Mac17,9, 48 GiB memory | +| OS and power | macOS 26.5.1, AC power | +| Runtime | Electron 40.8.3, Chromium 144.0.7559.236 | +| Display | Built-in Retina Display, reported 120.0006 Hz, scale factor 2 | +| Window | 1200 × 360 logical pixels, visible during measurement | +| Capture | Native CoreAudio system-output capture, MacBook Pro Speakers | +| Audio format | 44.1 kHz stereo, 512 sample frames per captured chunk | +| Spectrum | 2048-point FFT, smoothing 0.9, logarithmic scale, default extended range | +| Oscilloscope | Pitch lock enabled | +| Vectorscope | XY/Lissajous, 0 dB zoom, persistence 0.1, multiband off | +| VU | Horizontal bar display, −14 dBFS reference | +| Rolling capture / input gain | Off / 0 dB | + +The [result data](benchmarks/2026-09-16/results.json) records the scope settings, +canvas dimensions, runtime versions, stage timings, and per-run statistics. Solo +scopes occupy a larger canvas than each individual scope in the four-scope rack; +these configurations are not identical drawing workloads. + +## Procedure and timing + +The runner built production bundles and the native addon, verified that native +capture and the selected DSP modules were available, and used an isolated +settings directory. It waited at least two minutes after compilation, including +one continuous minute of nominal macOS thermal state, before launching Prism. + +A deterministic, changing stereo signal played through the normal system output +using `afplay`, outside the renderer. The source WAV was 48 kHz PCM16 with a peak +amplitude of 0.025; the capture device delivered 44.1 kHz audio. Frequency steps +and amplitude modulation kept the signal changing throughout each run. + +Spectrum alone, Oscilloscope alone, and the default four-scope rack were each +tested at display-sync and the 60 FPS target. Every configuration/target pair had +one frame-only baseline and three instrumented repeats. Each run used 10 seconds +of warmup followed by 60 seconds of measurement: 24 runs in total. A separate +10-second Chromium trace followed the suite. + +The instrumentation retained the existing capture, buffering, polling, +scheduling, and analysis behavior. Preallocated buffers recorded capture receipt, +scope consumption, and draw completion, associated by capture session, chunk +sequence, scope, and render frame. Every consumed chunk was counted for each +scope, including multiple chunks consumed in one frame. Frames repainting old +audio added no new latency samples. Recording did not log or write files per +frame; export happened after each measurement. + +Before and after each run, 64 native clock reads were bracketed by renderer clock +reads. The narrowest bracket from each endpoint and the measured renderer timer +granularity supplied an offset interval and timing uncertainty. This assumes no +unobserved clock excursion between the endpoints. Impossible timestamp ordering +was rejected, not clamped to zero. Capture-to-receipt durations below resolution +were marked unresolved; those chunks could still have valid capture-to-draw +measurements. Percentiles use the nearest-rank method. + +The initial FFT window, pitch-lock acquisition, vectorscope persistence, and meter +smoothing have their own response characteristics. A scope can consume a new +chunk while drawing selected history or smoothed values, so this benchmark does +not establish when a particular sound change becomes visible. + +## Results for every repeat + +The 60 FPS setting is a requested target. Its observed cadence in this suite was +approximately 53.5–54.5 FPS; display-sync achieved approximately 120 FPS. +In the tables, milliseconds refer to capture-to-draw-command completion. Timing +uncertainty is shown separately; N counts successful chunk/scope associations. + +| Configuration | Target | Repeat | Scope | Median ms | p95 ms | p99 ms | Max ms | N | Timing ±ms | Observed FPS | +| --- | --- | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| spectrum | display-sync | 1 | spectrum | 7.478 | 12.235 | 13.566 | 15.188 | 5169 | 0.213 | 119.988 | +| spectrum | display-sync | 2 | spectrum | 7.749 | 12.832 | 14.216 | 16.641 | 5169 | 0.251 | 120.006 | +| spectrum | display-sync | 3 | spectrum | 8.039 | 13.062 | 14.384 | 16.698 | 5169 | 0.218 | 120.008 | +| oscilloscope | display-sync | 1 | oscilloscope | 7.345 | 12.382 | 13.621 | 15.643 | 5169 | 0.239 | 120.007 | +| oscilloscope | display-sync | 2 | oscilloscope | 7.350 | 12.304 | 13.472 | 15.835 | 5168 | 0.214 | 120.004 | +| oscilloscope | display-sync | 3 | oscilloscope | 7.386 | 12.354 | 13.428 | 16.133 | 5170 | 0.244 | 120.004 | +| default-rack | display-sync | 1 | spectrum | 6.314 | 11.039 | 12.350 | 15.220 | 5168 | 0.265 | 120.001 | +| default-rack | display-sync | 1 | oscilloscope | 6.151 | 10.863 | 12.187 | 15.020 | 5168 | 0.265 | 120.001 | +| default-rack | display-sync | 1 | vectorscope | 6.573 | 11.316 | 12.630 | 15.520 | 5168 | 0.265 | 120.001 | +| default-rack | display-sync | 1 | vumeter | 6.638 | 11.355 | 12.686 | 15.620 | 5168 | 0.265 | 120.001 | +| default-rack | display-sync | 2 | spectrum | 6.391 | 11.097 | 12.603 | 15.534 | 5169 | 0.234 | 120.002 | +| default-rack | display-sync | 2 | oscilloscope | 6.234 | 10.920 | 12.411 | 15.334 | 5169 | 0.234 | 120.002 | +| default-rack | display-sync | 2 | vectorscope | 6.678 | 11.376 | 12.891 | 15.834 | 5169 | 0.234 | 120.002 | +| default-rack | display-sync | 2 | vumeter | 6.728 | 11.419 | 12.944 | 15.834 | 5169 | 0.234 | 120.002 | +| default-rack | display-sync | 3 | spectrum | 6.381 | 10.988 | 12.419 | 15.536 | 5170 | 0.210 | 120.001 | +| default-rack | display-sync | 3 | oscilloscope | 6.199 | 10.857 | 12.257 | 15.336 | 5170 | 0.210 | 120.001 | +| default-rack | display-sync | 3 | vectorscope | 6.669 | 11.279 | 12.721 | 15.836 | 5170 | 0.210 | 120.001 | +| default-rack | display-sync | 3 | vumeter | 6.700 | 11.337 | 12.721 | 15.936 | 5170 | 0.210 | 120.001 | +| spectrum | 60 | 1 | spectrum | 11.899 | 24.290 | 27.972 | 29.993 | 5168 | 0.217 | 53.515 | +| spectrum | 60 | 2 | spectrum | 11.815 | 23.561 | 28.003 | 29.595 | 5168 | 0.228 | 54.002 | +| spectrum | 60 | 3 | spectrum | 12.021 | 24.281 | 28.084 | 29.948 | 5168 | 0.227 | 53.632 | +| oscilloscope | 60 | 1 | oscilloscope | 11.792 | 23.466 | 27.894 | 29.732 | 5169 | 0.214 | 54.092 | +| oscilloscope | 60 | 2 | oscilloscope | 11.665 | 23.237 | 27.628 | 29.966 | 5169 | 0.216 | 54.234 | +| oscilloscope | 60 | 3 | oscilloscope | 11.734 | 23.751 | 28.150 | 30.129 | 5168 | 0.226 | 53.599 | +| default-rack | 60 | 1 | spectrum | 11.839 | 23.937 | 28.069 | 30.498 | 5167 | 0.273 | 54.149 | +| default-rack | 60 | 1 | oscilloscope | 11.678 | 23.749 | 27.934 | 30.298 | 5167 | 0.273 | 54.149 | +| default-rack | 60 | 1 | vectorscope | 12.132 | 24.237 | 28.379 | 30.798 | 5167 | 0.273 | 54.149 | +| default-rack | 60 | 1 | vumeter | 12.159 | 24.268 | 28.444 | 30.898 | 5167 | 0.273 | 54.149 | +| default-rack | 60 | 2 | spectrum | 11.701 | 23.294 | 27.862 | 30.003 | 5167 | 0.252 | 54.492 | +| default-rack | 60 | 2 | oscilloscope | 11.538 | 23.107 | 27.626 | 29.903 | 5167 | 0.252 | 54.492 | +| default-rack | 60 | 2 | vectorscope | 11.984 | 23.607 | 28.126 | 30.303 | 5167 | 0.252 | 54.492 | +| default-rack | 60 | 2 | vumeter | 12.047 | 23.642 | 28.162 | 30.403 | 5167 | 0.252 | 54.492 | +| default-rack | 60 | 3 | spectrum | 11.647 | 23.729 | 27.869 | 30.419 | 5169 | 0.212 | 54.458 | +| default-rack | 60 | 3 | oscilloscope | 11.488 | 23.575 | 27.672 | 30.119 | 5169 | 0.212 | 54.458 | +| default-rack | 60 | 3 | vectorscope | 11.948 | 23.993 | 28.126 | 30.619 | 5169 | 0.212 | 54.458 | +| default-rack | 60 | 3 | vumeter | 11.990 | 24.042 | 28.169 | 30.719 | 5169 | 0.212 | 54.458 | + +All 24 runs passed the benchmark's validity checks. The 18 instrumented runs +recorded 93,043 captured chunks and 186,064 successful chunk/scope associations. +There were no recorded native queue overflows, router overwrites, sequence gaps, +missing receipts, invalid timing records, or skipped consumed-chunk draws. + +Chunks already queued when observation began and chunks still pending at its end +were reported separately. They were not silently discarded into the successful +sample population. Their counts are included in the result data. + +## Probe overhead and thermals + +The frame-only baselines disabled chunk probes but retained the frame-duration +recorder and thermal observer. Frame work below is the scope callback duration, +not total process CPU utilization. Differences between sequential runs include +system-load and run-order effects; they are not an exact causal probe cost, and +no correction was subtracted from latency. + +| Configuration / target | Scope | Baseline work p95 ms | Probe work p95 range ms | Baseline FPS | Probe FPS range | +| --- | --- | ---: | ---: | ---: | ---: | +| spectrum / display-sync | spectrum | 1.700 | 1.800–1.800 | 120.001 | 119.988–120.008 | +| oscilloscope / display-sync | oscilloscope | 0.900 | 0.900–0.900 | 119.998 | 120.004–120.007 | +| default-rack / display-sync | spectrum | 0.400 | 0.200–0.200 | 120.001 | 120.001–120.002 | +| default-rack / display-sync | oscilloscope | 0.400 | 0.300–0.300 | 120.001 | 120.001–120.002 | +| default-rack / display-sync | vectorscope | 0.600 | 0.400–0.400 | 120.001 | 120.001–120.002 | +| default-rack / display-sync | vumeter | 0.100 | 0.100–0.100 | 120.001 | 120.001–120.002 | +| spectrum / 60 | spectrum | 0.500 | 0.500–0.500 | 53.791 | 53.515–54.002 | +| oscilloscope / 60 | oscilloscope | 0.400 | 0.400–0.400 | 54.280 | 53.599–54.234 | +| default-rack / 60 | spectrum | 0.300 | 0.200–0.300 | 54.258 | 54.149–54.492 | +| default-rack / 60 | oscilloscope | 0.400 | 0.400–0.400 | 54.258 | 54.149–54.492 | +| default-rack / 60 | vectorscope | 0.400 | 0.400–0.400 | 54.258 | 54.149–54.492 | +| default-rack / 60 | vumeter | 0.100 | 0.100–0.100 | 54.258 | 54.149–54.492 | + +macOS thermal state remained nominal across all 1,744 recorded observations, and +the machine stayed on AC power. The observer sampled at 1 Hz and recorded OS +notifications into preallocated main-process memory. No CPU speed-limit +notification arrived, so the speed limit was unknown rather than assumed to be +100%. Nominal thermal state does not establish an absence of every frequency +change. Total CPU utilization was not measured. + +## Separate presentation estimate + +The separate Chromium trace estimated approximately **25 ms median / 30 ms p95** +from native capture receipt to reported presentation for the default rack at +display-sync. Across scopes, medians were 25.20–25.23 ms and p95 values were +30.10–30.16 ms. + +All 3,448 successful chunk/scope records were associated with presentation events, +covering 4,804 marked scope frames with no rejected frame associations. Matching +required unique scope markers inside one animation callback and AnimationFrame +interval, exact presentation IDs, matching begin-frame source/sequence IDs, and +a fully presented compositor frame. Temporal proximity alone was not accepted. + +These diagnostic estimates include approximately ±0.245 ms of software timing +uncertainty **plus unquantified macOS presentation-estimation error** and the +separate tracing workload. They are not physical display measurements and were +not pooled into the main latency results. Chromium documents the platform limits +of presentation timestamps in [Life of a frame](https://chromium.googlesource.com/chromium/src.git/+/main/docs/life_of_a_frame.md). + +## Data and reproduction + +- [Per-run statistics and recorded settings](benchmarks/2026-09-16/results.json) +- [Exact benchmark instrumentation patch](benchmarks/2026-09-16/instrumentation.patch) + +The statistics file is a summary, not the raw per-chunk timestamps. Raw records, +isolated settings, audio stimulus, and the large Chromium trace are retained +locally and are not included in this repository. The patch contains the runner, +probes, analysis, tests, and methodology needed to make a new measurement. + +To reproduce from a checkout containing these documents, create a separate +worktree at the tested source revision and apply the archived patch: + +```sh +git worktree add --detach ../prism-latency-repro 2a2d01d32da824d03e60440acd505f3d890a4e6f +git -C ../prism-latency-repro apply "$PWD/docs/benchmarks/2026-09-16/instrumentation.patch" +cd ../prism-latency-repro +npm ci +npm run test:latency +npm run bench:latency +``` + +Use a Mac with native system-capture support, allow audio-capture permission if +requested, keep Prism visible and connected to AC power, and stop unrelated +playback. The full run takes about 32 minutes including build and cooldown. It +creates a fresh `benchmark-results/` directory with the report and raw records. +`npm run bench:latency -- --quick` is a short diagnostic check, not a replacement +for the full suite. Comparisons need the same machine, display, audio device, +window dimensions, and scope settings. diff --git a/docs/linux-testing.md b/docs/linux-testing.md new file mode 100644 index 0000000..912f464 --- /dev/null +++ b/docs/linux-testing.md @@ -0,0 +1,167 @@ +# Linux testing from Windows + +Run `npm run test:linux` from the Windows checkout to test in Ubuntu WSL2. Use +`npm run test:linux -- --target fedora` for an SSH-connected Fedora VM. The +Windows checkout remains the source of truth; each invocation creates a fresh +Linux copy under `~/.cache/prism-linux-tests/runs//source`. +If PowerShell blocks `npm.ps1`, use `npm.cmd` in these commands. + +## One-time setup + +### Ubuntu WSL2 + +From PowerShell in the repository (substitute your Windows repository path): + +```powershell +wsl -d Ubuntu -u root --exec bash /mnt/c/Users/boofi/Documents/astra/prism/scripts/linux/setup.sh system +wsl -d Ubuntu --exec bash /mnt/c/Users/boofi/Documents/astra/prism/scripts/linux/setup.sh node +``` + +The system step installs the Ubuntu CI build dependencies, Electron runtime +libraries, Xvfb, and RPM packaging tools. The user step downloads official Node +22, verifies its SHA-256 checksum, and installs it in +`~/.local/share/prism-linux/node`. It neither replaces Windows Node nor changes +your shell configuration. The runner sets a Linux-only PATH and checks Node's +platform and version. Builds live on the Linux filesystem; Windows +`node_modules`, native binaries, and build directories are never reused. + +### Fedora VM + +Start the VM and log into its desktop. In a Fedora terminal: + +```bash +sudo dnf install -y openssh-server +sudo systemctl enable --now sshd +sudo firewall-cmd --permanent --add-service=ssh +sudo firewall-cmd --reload +whoami +hostname -I +``` + +Add the Windows user's SSH **public** key to Fedora's `~/.ssh/authorized_keys`; +keep `~/.ssh` mode 700 and `authorized_keys` mode 600, then run +`restorecon -RF ~/.ssh`. Keep private keys on Windows. Establish a normal SSH +connection once to verify the host and authentication. The launcher uses batch +mode and fails rather than prompting for a password. + +Add an alias to Windows `%USERPROFILE%\.ssh\config`: + +```sshconfig +Host prism-fedora + HostName + User + IdentityFile ~/.ssh/id_ed25519 + IdentitiesOnly yes +``` + +`ssh prism-fedora id` should work. Update `HostName` if the VM's address changes. +Alternatively pass `--host user@IP` to the launcher. The VM must be running; +the launcher does not change VMware power or network settings. + +Before installing packages or changing the original checkout, record its Git +revision, Node/Electron versions, and desktop session and reproduce any existing +failure. Copy `scripts/linux/setup.sh` to the VM, then run `sudo bash setup.sh +system` and `bash setup.sh node` when ready to prepare the separate test build. +An existing Linux Node 22.12+ on the system PATH also works. Do not run npm or +the application as root. + +## Commands and results + +```powershell +npm run test:linux +npm run test:linux -- --target wsl --distro Ubuntu --suite full +npm run test:linux -- --target fedora +npm run test:linux -- --target fedora --host user@IP --suite full +npm run test:linux -- --dry-run +``` + +- `routine`: install locked dependencies, explicitly rebuild/load the Linux + native addon, typecheck, run `npm test`, build/test the TUI, and build the + desktop and plugin UI. +- `full`: routine checks, then VST3/CLAP build/tests under Xvfb and + `npm run dist:linux` (AppImage, deb, rpm, and tar.gz). Packages remain in the + Linux source copy's `dist/`; they are not installed or published. +- `--dry-run`: list selected files without creating or transferring a run. + +Logs are saved locally in `artifacts/linux-tests//`, which Git ignores: + +- `source-manifest.json`: revision, dirty state, selected files, executable modes, and + SHA-256 hashes. Selection includes staged/unstaged edits and untracked, + non-ignored files; tracked deletions stay deleted. Inspect the dry run if you + have local files you do not want transferred. +- `runner.log` and `result.json`: transport output, overall status, exit codes, + source fingerprint, and Linux run directory. +- `logs/`: Linux environment, command output, individual step statuses, and + shell-ending conversions. Partial logs are collected on failure too. + +The transfer verifies every source hash before running tests. It restores Git +executable modes and converts CRLF to LF in shell scripts in the Linux copy, +including extensionless installer hooks. It records every conversion. Source +symlinks are rejected explicitly. A file changed during archiving causes a +failure; rerun once editing has stopped. + +Native build failure is fatal even though Prism's normal postinstall allows a +JavaScript fallback. The runner stops at the first failed step and returns its +exit code. Logs and source copies remain available for investigation; old runs +are not automatically deleted. npm/Electron download caches are reused, but +compiled outputs are isolated per run. WSL/SSH commands may need approval to +cross the Windows agent sandbox boundary. + +For a VM with unreliable GitHub DNS, an existing clean FTXUI checkout can seed +`~/.cache/prism-linux-tests/dependencies/ftxui-` using `git clone +--no-hardlinks /path/to/existing/ftxui-src ` followed by +`git -C checkout --detach `. Use the immutable `GIT_TAG` +from `tui/CMakeLists.txt`. The runner verifies the cache's revision and clean +status before configuring CMake to use it. It never copies dependencies from +Windows or changes the VM's DNS settings. + +## Fedora desktop and profile diagnosis + +Build tests do not click native menus or prove desktop audio capture works. +Use the original logged-in KDE/GNOME session for the first reproduction. A +plain SSH shell lacks some display and session variables; launching from a +terminal inside the VM is the simplest faithful reproduction. Do not substitute +Xvfb for this check. + +Back up `Documents/Prism Profiles` and the app's user-data directory before +testing; record its real location rather than assuming every installation uses +`~/.config/prism`. Preserve the original checkout. Keep diagnostics and backups +in a private directory outside it. + +The opt-in `scripts/linux/profile-diagnostics.cjs` hook can be copied outside +the original checkout and loaded without editing its application source: + +```bash +cd /path/to/original/prism +set -o pipefail +ELECTRON_ENABLE_LOGGING=1 \ +NODE_OPTIONS='--require=/absolute/path/profile-diagnostics.cjs' \ +npm run dev 2>&1 | tee /absolute/path/prism-dev.log +``` + +This hook is for development Electron only. Confirm `[profile-trace]` appears +with `installed` before testing. It logs native menu creation/clicks, menu events +received by the renderer, profile IPC requests/results/errors, renderer console +errors, and Electron's actual Documents/user-data paths with access checks. +It does not retain native Menu objects or change profile payloads. Restart +without `NODE_OPTIONS` to remove it. Because instrumentation can affect timing, +confirm a diagnosed behavior again without the hook. + +Run this checklist and record pass/fail separately from automated results: + +1. Show Profiles Folder opens the resolved directory in the file manager. +2. Save a uniquely named test profile, change a scope setting, then load it and + confirm restoration. Overwrite, rename, and import a copied `.prsm` file. +3. Quit and relaunch; confirm the profile and active settings persist. Delete + only the uniquely named test profiles when finished. +4. Enumerate output/input devices; play audio in the guest and verify capture + and scope movement. Exercise a scope popout and relaunch. + +For an inert menu, compare `menu-built`, `menu-click`, `renderer-received`, +`invoke`, and `resolved/rejected` to locate the break. Record Fedora release, +desktop, Wayland/X11, Electron version, source revision, and any displayed errors. +Investigate filesystem paths when the request reaches profile storage; do not +infer a permissions failure solely from a menu that closes without action. + +VM audio represents virtual devices, not every physical Linux interface. Report +unexercised hardware and desktop checks as untested. diff --git a/docs/window-docking-validation.md b/docs/window-docking-validation.md new file mode 100644 index 0000000..4606acd --- /dev/null +++ b/docs/window-docking-validation.md @@ -0,0 +1,28 @@ +# Windows docking validation + +Build the Electron native addon with `npm run rebuild:native`, then launch with `npm run dev`. A renderer-only preview cannot exercise AppBar reservations. Older or unavailable native addons hide the docking option. + +Automated checks: `npm run typecheck`, `npm run test:window-state`, `npm run test:desktop-integration`, `npm run test:profiles`, `npm run test:renderer-helpers`, and `npm run build`. The window-state suite covers docking normalization, persistence, monitor fallback, reservation lifecycle, requested versus active state, resize limits, restore geometry, and shell-induced popout geometry suppression. + +## Desktop checks + +- Enable **Reserve screen space** without first choosing an edge: it uses Bottom. Choose Top or Bottom while docked and maximize an ordinary application on that monitor. Confirm taskbars and other AppBars retain their space and other monitors retain their work areas. +- Grow and shrink the inward edge, including the 100-pixel minimum. Outer edges and native move/maximize/resize commands must not change the dock. Repeat with solid, blurred, and clear backgrounds. +- Click the grab handle without moving: remain docked. Drag: restore floating dimensions under the pointer. Disable through either menu: restore the original floating position and dimensions. Check both floating pin preferences. +- Open settings at both edges. Confirm width matches the rack, small workspaces scroll, controls update live meters, theme changes propagate, and application switching leaves settings open. Test toggle, Close and focused Escape. None of these operations should resize the reservation. +- Switch and save profiles while docked. The rack must stay docked, floating restore bounds must remain unchanged, and docking alone must not mark a profile modified. Deliberate popout moves should still persist. +- Minimize/restore and hide/show from the tray. Hidden or minimized Prism must reserve nothing. Restart with docking enabled, then with startup hidden. The remembered monitor, edge, height and floating bounds must survive. +- Recreate the rack through background changes, reload or crash its renderer, and quit. Settings close and reservations follow the visible, ready rack. Force-terminate a development instance and verify Windows releases its work area. +- Cover the dock with a fullscreen application, including borderless fullscreen, then exit fullscreen. Prism yields while fullscreen is foreground and returns above ordinary windows afterward. +- Restart Explorer. Prism must register with the new shell and reserve exactly one strip. Disconnect the selected monitor: docking turns off and the floating rack appears on the primary monitor. Reconnection must not redock it. +- Repeat on monitors with different scaling, including changing DPI while docked and monitors at negative desktop coordinates. Check physical strip height, settings placement and pointer alignment. + +The shell API uses physical pixels; Electron and persisted window geometry use device-independent pixels. The native addon negotiates the rectangle against full monitor bounds and the shell. Its window subclass blocks native move/resize commands and releases registration on HWND destruction. Floating restore runs after the shell work-area message has completed, and uses current native work-area bounds to avoid Electron's stale display cache. + +Settings use a same-origin owned child window and a React portal from the main renderer. The child has no independent settings store. Docking preferences live in the local window-state file, never in profiles. + +## Validation performed + +The Windows development run passed typecheck, native and production builds, and 253 window-state, profile, desktop-integration and renderer checks. Live checks covered top/bottom reservations on a 1920×480 monitor and a 3440×1440 monitor with a taskbar; settings edits, theme synchronization and scrolling; profile switching/saving; solid/clear/blurred recreation; inward resizing and drag/menu restore; hide/minimize; foreground borderless fullscreen; simulated shell registration loss followed by TaskbarCreated; forced process termination; and restart persistence including the floating pin preference. + +Physical monitor removal, mixed-DPI changes, an actual Explorer restart, and packaged startup-hidden behavior still need manual verification. The available monitors all used 100% scaling. Missing-monitor fallback and initially hidden reservation behavior are covered by controller tests. diff --git a/native/binding.gyp b/native/binding.gyp index 123c3a7..c287629 100644 --- a/native/binding.gyp +++ b/native/binding.gyp @@ -8,8 +8,16 @@ "sources": [ "src/main.cpp", "src/system_audio_capture_napi.cpp", + "src/device_input_capture_napi.cpp", + "src/capture_channel_selection.cpp", + "src/capture_channel_selection_napi.cpp", "src/oscilloscope.cpp", "src/spectrum.cpp", + "src/reference_analysis.cpp", + "src/reference_decode.cpp", + "src/reference_napi.cpp", + "src/waterfall.cpp", + "src/waterfall_napi.cpp", "src/spectrogram.cpp", "src/vectorscope.cpp", "src/multiband.cpp", @@ -17,6 +25,7 @@ "src/vumeter.cpp", "src/lufsmeter.cpp", "src/window_chrome.cpp", + "src/window_docking.cpp", "src/dsp_utils.cpp" ], "include_dirs": [ @@ -28,6 +37,7 @@ ["OS=='mac'", { "sources": [ "src/macos_capture.mm", + "src/macos_input_capture.mm", "src/windows_capture_stub.cpp" ], "xcode_settings": { @@ -55,7 +65,10 @@ "avrt.lib", "runtimeobject.lib", "uuid.lib", - "dwmapi.lib" + "ksuser.lib", + "dwmapi.lib", + "shell32.lib", + "comctl32.lib" ], "msvs_settings": { "VCCLCompilerTool": { diff --git a/native/src/capture_channel_selection.cpp b/native/src/capture_channel_selection.cpp new file mode 100644 index 0000000..95adb91 --- /dev/null +++ b/native/src/capture_channel_selection.cpp @@ -0,0 +1,170 @@ +#include "capture_channel_selection.h" + +#include +#include + +namespace Prism::Capture { +namespace { + +uint64_t readUnsignedSample(const uint8_t* data, size_t byteCount, bool bigEndian) { + uint64_t value = 0; + if (bigEndian) { + for (size_t index = 0; index < byteCount; ++index) { + value = (value << 8) | data[index]; + } + } else { + for (size_t index = 0; index < byteCount; ++index) { + value |= static_cast(data[index]) << (index * 8); + } + } + return value; +} + +float decodeSample(const uint8_t* data, const PCMFormat& format) { + const size_t byteCount = format.bitsPerChannel / 8; + if (data == nullptr || byteCount == 0 || byteCount > sizeof(uint64_t)) return 0.0f; + + if (format.encoding == SampleEncoding::Float && format.bitsPerChannel == 32) { + uint32_t bits = static_cast(readUnsignedSample(data, byteCount, format.bigEndian)); + float value = 0.0f; + std::memcpy(&value, &bits, sizeof(value)); + return value; + } + if (format.encoding == SampleEncoding::Float && format.bitsPerChannel == 64) { + const uint64_t bits = readUnsignedSample(data, byteCount, format.bigEndian); + double value = 0.0; + std::memcpy(&value, &bits, sizeof(value)); + return static_cast(value); + } + if (format.encoding != SampleEncoding::SignedInteger + && format.encoding != SampleEncoding::UnsignedInteger) { + return 0.0f; + } + + const uint32_t validBits = format.validBitsPerChannel == 0 + ? format.bitsPerChannel : format.validBitsPerChannel; + uint64_t raw = readUnsignedSample(data, byteCount, format.bigEndian); + if (format.highAligned) raw >>= format.bitsPerChannel - validBits; + raw &= (uint64_t{1} << validBits) - 1; + const uint64_t signBit = uint64_t{1} << (validBits - 1); + const int64_t signedValue = format.encoding == SampleEncoding::UnsignedInteger + ? static_cast(raw) - static_cast(signBit) + : (raw & signBit) != 0 + ? static_cast(raw) - static_cast(uint64_t{1} << validBits) + : static_cast(raw); + const double maximum = static_cast( + format.normalizeByPowerOfTwo || format.encoding == SampleEncoding::UnsignedInteger + ? signBit : signBit - 1); + return maximum > 0.0 ? static_cast(static_cast(signedValue) / maximum) : 0.0f; +} + +float readChannelFrame(const PCMBufferView* buffers, + size_t bufferCount, + const PCMFormat& format, + uint32_t channel, + size_t frame) { + const size_t bytesPerSample = format.bitsPerChannel / 8; + uint32_t channelBase = 0; + for (size_t bufferIndex = 0; bufferIndex < bufferCount; ++bufferIndex) { + const PCMBufferView& buffer = buffers[bufferIndex]; + const uint32_t channelsInBuffer = std::max(1, buffer.channelCount); + if (channel >= channelBase + channelsInBuffer) { + channelBase += channelsInBuffer; + continue; + } + + const size_t localChannel = channel - channelBase; + const size_t sampleIndex = frame * channelsInBuffer + localChannel; + const size_t byteOffset = sampleIndex * bytesPerSample; + if (buffer.data == nullptr || byteOffset + bytesPerSample > buffer.byteLength) return 0.0f; + return decodeSample(buffer.data + byteOffset, format); + } + return 0.0f; +} + +} // namespace + +bool isSupportedPCMFormat(const PCMFormat& format) { + const uint32_t validBits = format.validBitsPerChannel == 0 + ? format.bitsPerChannel : format.validBitsPerChannel; + if (format.encoding == SampleEncoding::Float) { + return (format.bitsPerChannel == 32 || format.bitsPerChannel == 64) + && validBits == format.bitsPerChannel; + } + return (format.encoding == SampleEncoding::SignedInteger + || format.encoding == SampleEncoding::UnsignedInteger) + && format.bitsPerChannel >= 8 && format.bitsPerChannel <= 32 + && format.bitsPerChannel % 8 == 0 + && validBits > 0 && validBits <= format.bitsPerChannel; +} + +void measureSourceChannelPeaks(const PCMBufferView* buffers, + size_t bufferCount, + const PCMFormat& format, + size_t frameCount, + uint32_t sourceChannelCount, + float* peaksOutput) { + if (peaksOutput == nullptr) return; + std::fill_n(peaksOutput, sourceChannelCount, 0.0f); + if (buffers == nullptr || !isSupportedPCMFormat(format)) return; + + const size_t bytesPerSample = format.bitsPerChannel / 8; + uint32_t channelBase = 0; + for (size_t bufferIndex = 0; bufferIndex < bufferCount && channelBase < sourceChannelCount; ++bufferIndex) { + const auto& buffer = buffers[bufferIndex]; + const uint32_t channelsInBuffer = std::max(1, buffer.channelCount); + const uint32_t channelsToRead = std::min(channelsInBuffer, sourceChannelCount - channelBase); + if (buffer.data != nullptr) { + const size_t sampleCount = buffer.byteLength / bytesPerSample; + for (uint32_t channel = 0; channel < channelsToRead; ++channel) { + float peak = 0.0f; + for (size_t frame = 0; frame < frameCount; ++frame) { + const size_t sampleIndex = frame * channelsInBuffer + channel; + if (sampleIndex >= sampleCount) break; + const float sample = decodeSample(buffer.data + sampleIndex * bytesPerSample, format); + // Inspect IEEE bits: std::isfinite can be optimized away by -ffast-math. + uint32_t magnitudeBits = 0; + std::memcpy(&magnitudeBits, &sample, sizeof(sample)); + magnitudeBits &= 0x7fffffffu; + if (magnitudeBits >= 0x7f800000u) continue; + float magnitude = 0.0f; + std::memcpy(&magnitude, &magnitudeBits, sizeof(magnitude)); + peak = std::max(peak, magnitude); + } + peaksOutput[channelBase + channel] = peak; + } + } + channelBase += channelsToRead; + } +} + +bool selectStereoChannels(const PCMBufferView* buffers, + size_t bufferCount, + const PCMFormat& format, + size_t frameCount, + uint32_t sourceChannelCount, + uint32_t leftChannel, + uint32_t rightChannel, + float* leftOutput, + float* rightOutput) { + if (leftOutput == nullptr || rightOutput == nullptr) return false; + const bool valid = buffers != nullptr + && bufferCount > 0 + && isSupportedPCMFormat(format) + && sourceChannelCount > 0 + && leftChannel < sourceChannelCount + && rightChannel < sourceChannelCount; + if (!valid) { + std::fill_n(leftOutput, frameCount, 0.0f); + std::fill_n(rightOutput, frameCount, 0.0f); + return false; + } + + for (size_t frame = 0; frame < frameCount; ++frame) { + leftOutput[frame] = readChannelFrame(buffers, bufferCount, format, leftChannel, frame); + rightOutput[frame] = readChannelFrame(buffers, bufferCount, format, rightChannel, frame); + } + return true; +} + +} // namespace Prism::Capture diff --git a/native/src/capture_channel_selection.h b/native/src/capture_channel_selection.h new file mode 100644 index 0000000..1be6310 --- /dev/null +++ b/native/src/capture_channel_selection.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +namespace Prism::Capture { + +enum class SampleEncoding { + Float, + SignedInteger, + UnsignedInteger, + Unsupported, +}; + +struct PCMFormat { + SampleEncoding encoding = SampleEncoding::Unsupported; + uint32_t bitsPerChannel = 0; + bool bigEndian = false; + // Zero uses bitsPerChannel (the byte-aligned storage/container width). + uint32_t validBitsPerChannel = 0; + bool highAligned = false; + // Preserve the original macOS signed-PCM scale by default. PulseAudio uses + // the negative full-scale magnitude (e.g. 32768 for signed 16-bit PCM). + bool normalizeByPowerOfTwo = false; +}; + +bool isSupportedPCMFormat(const PCMFormat& format); + +struct PCMBufferView { + const uint8_t* data = nullptr; + size_t byteLength = 0; + uint32_t channelCount = 0; +}; + +/** Writes one finite absolute peak per source channel, before stereo routing. */ +void measureSourceChannelPeaks(const PCMBufferView* buffers, + size_t bufferCount, + const PCMFormat& format, + size_t frameCount, + uint32_t sourceChannelCount, + float* peaksOutput); + +/** + * Selects a stereo pair from one or more interleaved or planar PCM buffers. + * The output arrays must each have room for frameCount samples. Invalid routes + * are rendered as silence and reported by the return value. + */ +bool selectStereoChannels(const PCMBufferView* buffers, + size_t bufferCount, + const PCMFormat& format, + size_t frameCount, + uint32_t sourceChannelCount, + uint32_t leftChannel, + uint32_t rightChannel, + float* leftOutput, + float* rightOutput); + +} // namespace Prism::Capture diff --git a/native/src/capture_channel_selection_napi.cpp b/native/src/capture_channel_selection_napi.cpp new file mode 100644 index 0000000..c5a46fa --- /dev/null +++ b/native/src/capture_channel_selection_napi.cpp @@ -0,0 +1,118 @@ +#include "capture_channel_selection_napi.h" + +#include "capture_channel_selection.h" + +#include +#include + +namespace { + +Napi::Value Select(const Napi::CallbackInfo& info, bool rawPCM) { + Napi::Env env = info.Env(); + if (info.Length() < 5 || !info[0].IsArray() || !info[1].IsNumber() + || !info[2].IsNumber() || !info[3].IsNumber() || !info[4].IsNumber()) { + Napi::TypeError::New( + env, + "Expected buffers, frame count, source channel count, left channel, and right channel.") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + Prism::Capture::PCMFormat format{Prism::Capture::SampleEncoding::Float, 32, false}; + if (rawPCM) { + if (info.Length() < 6 || !info[5].IsObject()) { + Napi::TypeError::New(env, "Expected a PCM format descriptor.").ThrowAsJavaScriptException(); + return env.Undefined(); + } + const auto descriptor = info[5].As(); + const auto encoding = descriptor.Get("encoding").ToString().Utf8Value(); + format.encoding = encoding == "float" ? Prism::Capture::SampleEncoding::Float + : encoding == "signed" ? Prism::Capture::SampleEncoding::SignedInteger + : encoding == "unsigned" ? Prism::Capture::SampleEncoding::UnsignedInteger + : Prism::Capture::SampleEncoding::Unsupported; + format.bitsPerChannel = descriptor.Get("bitsPerChannel").ToNumber().Uint32Value(); + if (descriptor.Has("validBitsPerChannel")) { + format.validBitsPerChannel = descriptor.Get("validBitsPerChannel").ToNumber().Uint32Value(); + } + format.bigEndian = descriptor.Get("bigEndian").ToBoolean().Value(); + format.highAligned = descriptor.Get("highAligned").ToBoolean().Value(); + format.normalizeByPowerOfTwo = descriptor.Get("normalizeByPowerOfTwo").ToBoolean().Value(); + } + + const Napi::Array sourceBuffers = info[0].As(); + std::vector retainedArrays; + std::vector buffers; + retainedArrays.reserve(sourceBuffers.Length()); + buffers.reserve(sourceBuffers.Length()); + for (uint32_t index = 0; index < sourceBuffers.Length(); ++index) { + const Napi::Value value = sourceBuffers.Get(index); + if (value.IsNull() || value.IsUndefined()) { + buffers.push_back({nullptr, 0, 1}); + continue; + } + if (!value.IsObject()) { + Napi::TypeError::New(env, "Each buffer must be an object or null.") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + const Napi::Object entry = value.As(); + const Napi::Value dataValue = entry.Get("data"); + const Napi::Value channelCountValue = entry.Get("channelCount"); + if (!dataValue.IsTypedArray() || !channelCountValue.IsNumber()) { + Napi::TypeError::New(env, "Each buffer requires typed-array data and channelCount.") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + const Napi::TypedArray typedArray = dataValue.As(); + if (typedArray.TypedArrayType() != (rawPCM ? napi_uint8_array : napi_float32_array)) { + Napi::TypeError::New(env, rawPCM ? "Buffer data must be a Uint8Array." : "Buffer data must be a Float32Array.") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + retainedArrays.push_back(typedArray); + const auto& retained = retainedArrays.back(); + buffers.push_back({ + static_cast(retained.ArrayBuffer().Data()) + retained.ByteOffset(), + retained.ByteLength(), + channelCountValue.As().Uint32Value(), + }); + } + + const size_t frameCount = info[1].As().Uint32Value(); + Napi::Float32Array left = Napi::Float32Array::New(env, frameCount); + Napi::Float32Array right = Napi::Float32Array::New(env, frameCount); + const bool valid = Prism::Capture::selectStereoChannels( + buffers.data(), + buffers.size(), + format, + frameCount, + info[2].As().Uint32Value(), + info[3].As().Uint32Value(), + info[4].As().Uint32Value(), + left.Data(), + right.Data()); + + Napi::Object result = Napi::Object::New(env); + const uint32_t sourceChannelCount = info[2].As().Uint32Value(); + auto peaks = Napi::Float32Array::New(env, sourceChannelCount); + Prism::Capture::measureSourceChannelPeaks( + buffers.data(), buffers.size(), format, + frameCount, sourceChannelCount, peaks.Data()); + result.Set("sourceChannelPeaks", peaks); + result.Set("valid", Napi::Boolean::New(env, valid)); + result.Set("left", left); + result.Set("right", right); + return result; +} + +Napi::Value SelectFloat32(const Napi::CallbackInfo& info) { return Select(info, false); } +Napi::Value SelectPCM(const Napi::CallbackInfo& info) { return Select(info, true); } + +} // namespace + +void RegisterCaptureChannelSelection(Napi::Env env, Napi::Object exports) { + Napi::Object helper = Napi::Object::New(env); + helper.Set("selectFloat32", Napi::Function::New(env, SelectFloat32)); + helper.Set("selectPCM", Napi::Function::New(env, SelectPCM)); + exports.Set("captureChannelSelection", helper); +} diff --git a/native/src/capture_channel_selection_napi.h b/native/src/capture_channel_selection_napi.h new file mode 100644 index 0000000..83f3a95 --- /dev/null +++ b/native/src/capture_channel_selection_napi.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +void RegisterCaptureChannelSelection(Napi::Env env, Napi::Object exports); diff --git a/native/src/device_input_capture.h b/native/src/device_input_capture.h new file mode 100644 index 0000000..95605ff --- /dev/null +++ b/native/src/device_input_capture.h @@ -0,0 +1,26 @@ +#pragma once + +#include "system_audio_capture.h" + +#include + +namespace Prism::Capture { + +class DeviceInputCapture { +public: + virtual ~DeviceInputCapture() = default; + + virtual Support getSupport() const = 0; + virtual std::vector listInputDevices() = 0; + virtual bool start(const std::string& requestedDeviceId, + StartResult* result, + std::string* errorMessage) = 0; + virtual ChannelRouting setChannelRouting(uint32_t left, uint32_t right) = 0; + virtual void stop() = 0; + virtual DrainResult drain(size_t maxChunks = 64) = 0; + virtual double nowMilliseconds() const = 0; +}; + +std::unique_ptr createDeviceInputCapture(); + +} // namespace Prism::Capture diff --git a/native/src/device_input_capture_adapter.h b/native/src/device_input_capture_adapter.h new file mode 100644 index 0000000..7a3977d --- /dev/null +++ b/native/src/device_input_capture_adapter.h @@ -0,0 +1,39 @@ +#pragma once + +#include "device_input_capture.h" + +#include + +namespace Prism::Capture { + +// A platform engine configured for input endpoints shares its buffering, +// routing, and lifecycle with the system-audio engine, but has its own instance. +class DeviceInputCaptureAdapter final : public DeviceInputCapture { +public: + explicit DeviceInputCaptureAdapter(std::unique_ptr engine) + : engine_(std::move(engine)) {} + + Support getSupport() const override { return engine_->getSupport(); } + std::vector listInputDevices() override { return engine_->listOutputDevices(); } + bool start(const std::string& id, StartResult* result, std::string* error) override { + return engine_->start(id, result, error); + } + ChannelRouting setChannelRouting(uint32_t left, uint32_t right) override { + return engine_->setChannelRouting(left, right); + } + void stop() override { engine_->stop(); } + DrainResult drain(size_t maxChunks) override { return engine_->drain(maxChunks); } + double nowMilliseconds() const override { return engine_->nowMilliseconds(); } + +private: + std::unique_ptr engine_; +}; + +inline ChannelRouting normalizeChannelRouting(ChannelRouting routing, uint32_t count) { + if (count == 0 || routing.left >= count || routing.right >= count) { + return {0, count > 1 ? 1u : 0u}; + } + return routing; +} + +} // namespace Prism::Capture diff --git a/native/src/device_input_capture_napi.cpp b/native/src/device_input_capture_napi.cpp new file mode 100644 index 0000000..9888458 --- /dev/null +++ b/native/src/device_input_capture_napi.cpp @@ -0,0 +1,189 @@ +#include "device_input_capture_napi.h" + +#include "device_input_capture.h" + +#include +#include +#include +#include + +namespace { + +constexpr size_t kDefaultDrainChunkLimit = 64; +constexpr size_t kMaxDrainChunkLimit = 256; + +std::unique_ptr& activeInputCapture() { + static std::unique_ptr capture = + Prism::Capture::createDeviceInputCapture(); + return capture; +} + +Napi::Object routingToNapi(Napi::Env env, const Prism::Capture::ChannelRouting& routing) { + Napi::Object result = Napi::Object::New(env); + result.Set("left", Napi::Number::New(env, routing.left)); + result.Set("right", Napi::Number::New(env, routing.right)); + return result; +} + +Napi::Value GetSupport(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + const auto support = activeInputCapture()->getSupport(); + Napi::Object result = Napi::Object::New(env); + result.Set("available", Napi::Boolean::New(env, support.available)); + result.Set("reason", support.reason.empty() + ? static_cast(env.Null()) + : static_cast(Napi::String::New(env, support.reason))); + return result; +} + +Napi::Value ListInputDevices(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + const auto devices = activeInputCapture()->listInputDevices(); + Napi::Array result = Napi::Array::New(env, devices.size()); + for (size_t index = 0; index < devices.size(); ++index) { + const auto& device = devices[index]; + Napi::Object entry = Napi::Object::New(env); + entry.Set("id", Napi::String::New(env, device.id)); + entry.Set("label", Napi::String::New(env, device.label)); + entry.Set("kind", Napi::String::New(env, "device")); + entry.Set("isDefault", Napi::Boolean::New(env, device.isDefault)); + entry.Set("sampleRate", Napi::Number::New(env, device.sampleRate)); + entry.Set("channelCount", Napi::Number::New(env, device.channelCount)); + entry.Set("channelRoutingAvailable", Napi::Boolean::New(env, true)); + + Napi::Array channels = Napi::Array::New(env, device.channelCount); + for (uint32_t channelIndex = 0; channelIndex < device.channelCount; ++channelIndex) { + Napi::Object channel = Napi::Object::New(env); + const std::string label = channelIndex < device.channels.size() + && !device.channels[channelIndex].label.empty() + ? device.channels[channelIndex].label + : "Channel " + std::to_string(channelIndex + 1); + channel.Set("index", Napi::Number::New(env, channelIndex)); + channel.Set("label", Napi::String::New(env, label)); + channels.Set(channelIndex, channel); + } + entry.Set("channels", channels); + result.Set(static_cast(index), entry); + } + return result; +} + +Napi::Value Start(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + std::string requestedDeviceId; + if (info.Length() >= 1 && info[0].IsString()) { + requestedDeviceId = info[0].As().Utf8Value(); + } + // Stop before setting startup routing: the previous device's channel count + // must not clamp a route intended for the new device. + activeInputCapture()->stop(); + activeInputCapture()->setChannelRouting(0, 1); + + if (info.Length() >= 2 && info[1].IsObject()) { + const Napi::Object routing = info[1].As(); + if (routing.Has("left") && routing.Has("right") + && routing.Get("left").IsNumber() && routing.Get("right").IsNumber()) { + activeInputCapture()->setChannelRouting( + routing.Get("left").As().Uint32Value(), + routing.Get("right").As().Uint32Value()); + } + } + + Prism::Capture::StartResult started; + std::string errorMessage; + if (!activeInputCapture()->start(requestedDeviceId, &started, &errorMessage)) { + Napi::Error::New( + env, + errorMessage.empty() ? "Device input capture failed to start." : errorMessage) + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + Napi::Object result = Napi::Object::New(env); + result.Set("sampleRate", Napi::Number::New(env, started.sampleRate)); + result.Set("channelCount", Napi::Number::New(env, started.channelCount)); + result.Set("sourceChannelCount", Napi::Number::New(env, started.sourceChannelCount)); + result.Set("deviceId", Napi::String::New(env, started.deviceId)); + result.Set("deviceLabel", Napi::String::New(env, started.deviceLabel)); + return result; +} + +Napi::Value SetChannelRouting(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) { + Napi::TypeError::New(env, "Expected left and right channel indices.") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + return routingToNapi( + env, + activeInputCapture()->setChannelRouting( + info[0].As().Uint32Value(), + info[1].As().Uint32Value())); +} + +Napi::Value Stop(const Napi::CallbackInfo& info) { + activeInputCapture()->stop(); + return info.Env().Undefined(); +} + +Napi::Value Drain(const Napi::CallbackInfo& info) { + size_t maxChunks = kDefaultDrainChunkLimit; + if (info.Length() >= 1 && info[0].IsNumber()) { + const int64_t requested = info[0].As().Int64Value(); + if (requested > 0) { + maxChunks = std::min(static_cast(requested), kMaxDrainChunkLimit); + } + } + + Napi::Env env = info.Env(); + auto drained = activeInputCapture()->drain(maxChunks); + Napi::Array chunks = Napi::Array::New(env, drained.chunks.size()); + for (size_t index = 0; index < drained.chunks.size(); ++index) { + auto& chunk = drained.chunks[index]; + Napi::Object entry = Napi::Object::New(env); + Napi::Float32Array left = Napi::Float32Array::New(env, chunk.left.size()); + Napi::Float32Array right = Napi::Float32Array::New(env, chunk.right.size()); + if (!chunk.left.empty()) { + std::memcpy(left.Data(), chunk.left.data(), chunk.left.size() * sizeof(float)); + } + if (!chunk.right.empty()) { + std::memcpy(right.Data(), chunk.right.data(), chunk.right.size() * sizeof(float)); + } + entry.Set("left", left); + entry.Set("right", right); + entry.Set("channelCount", Napi::Number::New(env, chunk.channelCount)); + entry.Set("capturedAtMilliseconds", Napi::Number::New(env, chunk.capturedAtMilliseconds)); + entry.Set("sequence", Napi::Number::New(env, static_cast(chunk.sequence))); + if (!chunk.sourceChannelPeaks.empty()) { + auto peaks = Napi::Float32Array::New(env, chunk.sourceChannelPeaks.size()); + std::memcpy(peaks.Data(), chunk.sourceChannelPeaks.data(), chunk.sourceChannelPeaks.size() * sizeof(float)); + entry.Set("sourceChannelPeaks", peaks); + } + chunks.Set(static_cast(index), entry); + } + + Napi::Object result = Napi::Object::New(env); + result.Set("chunks", chunks); + result.Set("overwriteCount", Napi::Number::New(env, static_cast(drained.overwriteCount))); + result.Set("queueDepth", Napi::Number::New(env, drained.queueDepth)); + return result; +} + +Napi::Value NowMilliseconds(const Napi::CallbackInfo& info) { + return Napi::Number::New(info.Env(), activeInputCapture()->nowMilliseconds()); +} + +} // namespace + +void RegisterDeviceInputCapture(Napi::Env env, Napi::Object exports) { + Napi::Object capture = Napi::Object::New(env); + capture.Set("getSupport", Napi::Function::New(env, GetSupport)); + capture.Set("listInputDevices", Napi::Function::New(env, ListInputDevices)); + capture.Set("start", Napi::Function::New(env, Start)); + capture.Set("setChannelRouting", Napi::Function::New(env, SetChannelRouting)); + capture.Set("stop", Napi::Function::New(env, Stop)); + capture.Set("drain", Napi::Function::New(env, Drain)); + capture.Set("nowMilliseconds", Napi::Function::New(env, NowMilliseconds)); + exports.Set("deviceInputCapture", capture); +} diff --git a/native/src/device_input_capture_napi.h b/native/src/device_input_capture_napi.h new file mode 100644 index 0000000..eda1801 --- /dev/null +++ b/native/src/device_input_capture_napi.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +void RegisterDeviceInputCapture(Napi::Env env, Napi::Object exports); diff --git a/native/src/linux_capture.cpp b/native/src/linux_capture.cpp index c265082..bb1cd33 100644 --- a/native/src/linux_capture.cpp +++ b/native/src/linux_capture.cpp @@ -1,4 +1,6 @@ #include "system_audio_capture.h" +#include "device_input_capture_adapter.h" +#include "capture_channel_selection.h" #if defined(__linux__) @@ -19,150 +21,78 @@ constexpr size_t kMaxQueuedChunks = 256; constexpr size_t kDefaultDrainChunkLimit = 64; constexpr pa_usec_t kTargetRecordFragmentMicroseconds = 10000; -struct OutputDeviceInfo { +struct DeviceInfo { std::string id; std::string label; - std::string monitorSourceName; + std::string recordSourceName; pa_sample_spec sampleSpec{}; pa_channel_map channelMap{}; bool hasChannelMap = false; bool isDefault = false; }; -struct CapturedChunk { - std::vector left; - std::vector right; - uint32_t channelCount = 2; - double capturedAtMilliseconds = 0.0; - uint64_t sequence = 0; -}; +using CapturedChunk = Prism::Capture::AudioChunk; double monotonicMilliseconds() { const auto now = std::chrono::steady_clock::now().time_since_epoch(); return std::chrono::duration(now).count(); } -uint32_t readUint24LE(const uint8_t* data) { - return static_cast(data[0]) | - (static_cast(data[1]) << 8) | - (static_cast(data[2]) << 16); -} - -uint32_t readUint24BE(const uint8_t* data) { - return static_cast(data[2]) | - (static_cast(data[1]) << 8) | - (static_cast(data[0]) << 16); -} - -uint32_t readUint32LE(const uint8_t* data) { - return static_cast(data[0]) | - (static_cast(data[1]) << 8) | - (static_cast(data[2]) << 16) | - (static_cast(data[3]) << 24); -} - -uint32_t readUint32BE(const uint8_t* data) { - return static_cast(data[3]) | - (static_cast(data[2]) << 8) | - (static_cast(data[1]) << 16) | - (static_cast(data[0]) << 24); -} - -int32_t signExtend24(uint32_t value) { - if ((value & 0x00800000U) != 0) { - value |= 0xFF000000U; - } - return static_cast(value); -} - -bool isSupportedSampleFormat(pa_sample_format_t format) { +Prism::Capture::PCMFormat getPCMFormat(pa_sample_format_t format) { + using namespace Prism::Capture; + PCMFormat pcm; + pcm.normalizeByPowerOfTwo = true; switch (format) { - case PA_SAMPLE_U8: - case PA_SAMPLE_S16LE: - case PA_SAMPLE_S16BE: - case PA_SAMPLE_S24LE: - case PA_SAMPLE_S24BE: - case PA_SAMPLE_S24_32LE: - case PA_SAMPLE_S24_32BE: - case PA_SAMPLE_S32LE: - case PA_SAMPLE_S32BE: - case PA_SAMPLE_FLOAT32LE: - case PA_SAMPLE_FLOAT32BE: - return true; - default: - return false; - } + case PA_SAMPLE_U8: pcm.encoding = SampleEncoding::UnsignedInteger; pcm.bitsPerChannel = 8; break; + case PA_SAMPLE_S16LE: case PA_SAMPLE_S16BE: + pcm.encoding = SampleEncoding::SignedInteger; pcm.bitsPerChannel = 16; break; + case PA_SAMPLE_S24LE: case PA_SAMPLE_S24BE: + pcm.encoding = SampleEncoding::SignedInteger; pcm.bitsPerChannel = 24; break; + case PA_SAMPLE_S24_32LE: case PA_SAMPLE_S24_32BE: + pcm.encoding = SampleEncoding::SignedInteger; pcm.bitsPerChannel = 32; + pcm.validBitsPerChannel = 24; break; + case PA_SAMPLE_S32LE: case PA_SAMPLE_S32BE: + pcm.encoding = SampleEncoding::SignedInteger; pcm.bitsPerChannel = 32; break; + case PA_SAMPLE_FLOAT32LE: case PA_SAMPLE_FLOAT32BE: + pcm.encoding = SampleEncoding::Float; pcm.bitsPerChannel = 32; break; + default: break; + } + pcm.bigEndian = pa_sample_format_is_be(format) > 0; + return pcm; } -float readNormalizedSample(const uint8_t* data, pa_sample_format_t format) { - if (data == nullptr) { - return 0.0f; - } - - switch (format) { - case PA_SAMPLE_U8: - return (static_cast(data[0]) - 128) / 128.0f; - case PA_SAMPLE_S16LE: - return static_cast( - static_cast(data[0]) | - (static_cast(data[1]) << 8)) / - 32768.0f; - case PA_SAMPLE_S16BE: - return static_cast( - static_cast(data[1]) | - (static_cast(data[0]) << 8)) / - 32768.0f; - case PA_SAMPLE_S24LE: - return signExtend24(readUint24LE(data)) / 8388608.0f; - case PA_SAMPLE_S24BE: - return signExtend24(readUint24BE(data)) / 8388608.0f; - case PA_SAMPLE_S24_32LE: { - uint32_t raw = readUint32LE(data) & 0x00FFFFFFU; - return signExtend24(raw) / 8388608.0f; - } - case PA_SAMPLE_S24_32BE: { - uint32_t raw = readUint32BE(data) & 0x00FFFFFFU; - return signExtend24(raw) / 8388608.0f; - } - case PA_SAMPLE_S32LE: - return static_cast(readUint32LE(data)) / 2147483648.0f; - case PA_SAMPLE_S32BE: - return static_cast(readUint32BE(data)) / 2147483648.0f; - case PA_SAMPLE_FLOAT32LE: { - const uint32_t raw = readUint32LE(data); - float value = 0.0f; - std::memcpy(&value, &raw, sizeof(value)); - return value; - } - case PA_SAMPLE_FLOAT32BE: { - const uint32_t raw = readUint32BE(data); - float value = 0.0f; - std::memcpy(&value, &raw, sizeof(value)); - return value; - } - default: - return 0.0f; +std::vector channelDescriptors(const DeviceInfo& device) { + std::vector result; + for (uint32_t index = 0; index < device.sampleSpec.channels; ++index) { + const char* label = device.hasChannelMap && index < device.channelMap.channels + ? pa_channel_position_to_pretty_string(device.channelMap.map[index]) : nullptr; + result.push_back({index, label != nullptr ? label : "Channel " + std::to_string(index + 1)}); } + return result; } -struct SinkEnumerationState { +struct DeviceEnumerationState { pa_threaded_mainloop* mainloop = nullptr; std::string defaultSinkName; - std::vector devices; + std::string defaultSourceName; + std::vector devices; }; void HandleServerInfo(pa_context*, const pa_server_info* info, void* userdata) { - auto* state = static_cast(userdata); + auto* state = static_cast(userdata); if (state != nullptr && info != nullptr && info->default_sink_name != nullptr) { state->defaultSinkName = info->default_sink_name; } + if (state != nullptr && info != nullptr && info->default_source_name != nullptr) { + state->defaultSourceName = info->default_source_name; + } if (state != nullptr && state->mainloop != nullptr) { pa_threaded_mainloop_signal(state->mainloop, 0); } } void HandleSinkInfo(pa_context*, const pa_sink_info* info, int eol, void* userdata) { - auto* state = static_cast(userdata); + auto* state = static_cast(userdata); if (state == nullptr || state->mainloop == nullptr) { return; } @@ -173,10 +103,10 @@ void HandleSinkInfo(pa_context*, const pa_sink_info* info, int eol, void* userda } if (info != nullptr && info->name != nullptr && info->monitor_source_name != nullptr) { - OutputDeviceInfo device; + DeviceInfo device; device.id = info->name; device.label = info->description != nullptr ? info->description : info->name; - device.monitorSourceName = info->monitor_source_name; + device.recordSourceName = info->monitor_source_name; device.sampleSpec = info->sample_spec; device.channelMap = info->channel_map; device.hasChannelMap = info->channel_map.channels > 0; @@ -186,6 +116,23 @@ void HandleSinkInfo(pa_context*, const pa_sink_info* info, int eol, void* userda pa_threaded_mainloop_signal(state->mainloop, 0); } +void HandleSourceInfo(pa_context*, const pa_source_info* info, int eol, void* userdata) { + auto* state = static_cast(userdata); + if (state == nullptr || state->mainloop == nullptr) return; + if (eol == 0 && info != nullptr && info->name != nullptr + && info->monitor_of_sink == PA_INVALID_INDEX) { + DeviceInfo device; + device.id = info->name; + device.label = info->description != nullptr ? info->description : info->name; + device.recordSourceName = info->name; + device.sampleSpec = info->sample_spec; + device.channelMap = info->channel_map; + device.hasChannelMap = info->channel_map.channels > 0; + state->devices.push_back(std::move(device)); + } + pa_threaded_mainloop_signal(state->mainloop, 0); +} + class PulseContextConnection { public: PulseContextConnection() = default; @@ -273,11 +220,11 @@ class PulseContextConnection { started_ = false; } - bool enumerateOutputDevices(std::vector* outDevices, + bool enumerateDevices(bool input, std::vector* outDevices, std::string* outErrorMessage) { if (outDevices == nullptr) { if (outErrorMessage != nullptr) { - *outErrorMessage = "Could not store PulseAudio output devices."; + *outErrorMessage = "Could not store PulseAudio devices."; } return false; } @@ -291,7 +238,7 @@ class PulseContextConnection { pa_threaded_mainloop_lock(mainloop_); - SinkEnumerationState state; + DeviceEnumerationState state; state.mainloop = mainloop_; pa_operation* serverOperation = @@ -302,7 +249,8 @@ class PulseContextConnection { } pa_operation* sinkOperation = - pa_context_get_sink_info_list(context_, &HandleSinkInfo, &state); + input ? pa_context_get_source_info_list(context_, &HandleSourceInfo, &state) + : pa_context_get_sink_info_list(context_, &HandleSinkInfo, &state); if (!waitForOperationLocked(sinkOperation, outErrorMessage)) { pa_threaded_mainloop_unlock(mainloop_); return false; @@ -311,14 +259,7 @@ class PulseContextConnection { pa_threaded_mainloop_unlock(mainloop_); for (auto& device : state.devices) { - device.isDefault = device.id == state.defaultSinkName; - } - - if (state.devices.empty()) { - if (outErrorMessage != nullptr) { - *outErrorMessage = "No Linux output devices are available."; - } - return false; + device.isDefault = device.id == (input ? state.defaultSourceName : state.defaultSinkName); } *outDevices = std::move(state.devices); @@ -408,6 +349,8 @@ class PulseContextConnection { class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture { public: + explicit LinuxNativeCaptureEngine(bool input = false) : input_(input) {} + ~LinuxNativeCaptureEngine() override { stop(); } @@ -415,10 +358,10 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture Prism::Capture::Support getSupport() const override { PulseContextConnection connection; std::string errorMessage; - std::vector devices; + std::vector devices; const bool available = connection.connect("Prism Linux Capture Probe", &errorMessage) && - connection.enumerateOutputDevices(&devices, &errorMessage); + connection.enumerateDevices(input_, &devices, &errorMessage); return { available, available ? std::string() : (errorMessage.empty() @@ -430,9 +373,9 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture std::vector listOutputDevices() override { PulseContextConnection connection; std::string errorMessage; - std::vector devices; + std::vector devices; if (!connection.connect("Prism Linux Capture Devices", &errorMessage) || - !connection.enumerateOutputDevices(&devices, &errorMessage)) { + !connection.enumerateDevices(input_, &devices, &errorMessage)) { return {}; } @@ -445,6 +388,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture static_cast(device.sampleSpec.rate), static_cast(device.sampleSpec.channels), device.isDefault, + channelDescriptors(device), }); } return result; @@ -455,20 +399,28 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture std::string* errorMessage) override { if (!startInternal(requestedDeviceId, errorMessage)) { if (errorMessage != nullptr && errorMessage->empty()) { - *errorMessage = "Native Linux monitor capture failed to start."; + *errorMessage = "Native Linux audio capture failed to start."; } return false; } if (result != nullptr) { std::lock_guard lock(stateMutex_); result->sampleRate = sampleRate_; - result->channelCount = channelCount_; + result->channelCount = channelCount_ > 1 ? 2u : 1u; + result->sourceChannelCount = channelCount_; result->deviceId = activeDeviceId_; result->deviceLabel = activeDeviceLabel_; } return true; } + Prism::Capture::ChannelRouting setChannelRouting(uint32_t left, uint32_t right) override { + std::lock_guard lock(stateMutex_); + routing_ = active_ ? Prism::Capture::normalizeChannelRouting({left, right}, channelCount_) + : Prism::Capture::ChannelRouting{left, right}; + return routing_; + } + void stop() override { stopInternal(); } @@ -494,13 +446,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture while (!drained.empty()) { auto chunk = std::move(drained.front()); drained.pop_front(); - result.chunks.push_back({ - std::move(chunk.left), - std::move(chunk.right), - chunk.channelCount, - chunk.capturedAtMilliseconds, - chunk.sequence, - }); + result.chunks.push_back(std::move(chunk)); } return result; } @@ -558,13 +504,22 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture return false; } - std::vector devices; - if (!connection_.enumerateOutputDevices(&devices, outErrorMessage)) { + std::vector devices; + if (!connection_.enumerateDevices(input_, &devices, outErrorMessage)) { connection_.disconnect(); return false; } - const OutputDeviceInfo* selected = nullptr; + if (devices.empty()) { + if (outErrorMessage != nullptr) { + *outErrorMessage = input_ ? "No Linux input devices are available." + : "No Linux output devices are available."; + } + connection_.disconnect(); + return false; + } + + const DeviceInfo* selected = nullptr; if (!requestedDeviceId.empty()) { for (const auto& device : devices) { if (device.id == requestedDeviceId) { @@ -575,7 +530,8 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture if (selected == nullptr) { if (outErrorMessage != nullptr) { *outErrorMessage = - "The selected Linux output device is no longer available."; + input_ ? "The selected Linux input device is no longer available." + : "The selected Linux output device is no longer available."; } connection_.disconnect(); return false; @@ -592,10 +548,10 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture } } - if (!isSupportedSampleFormat(selected->sampleSpec.format)) { + if (!Prism::Capture::isSupportedPCMFormat(getPCMFormat(selected->sampleSpec.format))) { if (outErrorMessage != nullptr) { *outErrorMessage = - "Unsupported PulseAudio sample format for Linux monitor capture."; + "Unsupported PulseAudio sample format for Linux audio capture."; } connection_.disconnect(); return false; @@ -605,7 +561,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture stream_ = pa_stream_new( connection_.context(), - "Prism Output Monitor", + input_ ? "Prism Device Input" : "Prism Output Monitor", &selected->sampleSpec, selected->hasChannelMap ? &selected->channelMap : nullptr); if (stream_ == nullptr) { @@ -629,10 +585,10 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_DONT_MOVE); const int connectResult = pa_stream_connect_record( - stream_, selected->monitorSourceName.c_str(), &requestedBufferAttr, flags); + stream_, selected->recordSourceName.c_str(), &requestedBufferAttr, flags); if (connectResult < 0) { const std::string errorMessage = buildContextErrorMessage( - connection_.context(), "Could not start Linux monitor capture."); + connection_.context(), "Could not start Linux audio capture."); pa_stream_set_read_callback(stream_, nullptr, nullptr); pa_stream_set_state_callback(stream_, nullptr, nullptr); pa_stream_unref(stream_); @@ -678,6 +634,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture activeDeviceLabel_ = selected->label; sampleRate_ = static_cast(sampleSpec_.rate); channelCount_ = std::max(1, sampleSpec_.channels); + routing_ = Prism::Capture::normalizeChannelRouting(routing_, channelCount_); sequence_ = 0; } @@ -696,7 +653,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture if (outErrorMessage != nullptr) { *outErrorMessage = buildContextErrorMessage( connection_.context(), - "PulseAudio monitor stream failed to initialize."); + "PulseAudio recording stream failed to initialize."); } return false; default: @@ -706,7 +663,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture } if (outErrorMessage != nullptr) { - *outErrorMessage = "PulseAudio monitor stream is unavailable."; + *outErrorMessage = "PulseAudio recording stream is unavailable."; } return false; } @@ -765,6 +722,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture pa_sample_spec sampleSpec{}; uint32_t channelCount = 2; uint64_t sequence = 0; + Prism::Capture::ChannelRouting routing; { std::lock_guard lock(stateMutex_); if (!active_) { @@ -773,6 +731,7 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture } sampleSpec = sampleSpec_; channelCount = channelCount_; + routing = routing_; sequence = ++sequence_; } @@ -782,7 +741,6 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture break; } - const size_t bytesPerSample = pa_sample_size_of_format(sampleSpec.format); const size_t frames = length / bytesPerFrame; if (frames == 0) { pa_stream_drop(stream_); @@ -790,27 +748,20 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture } CapturedChunk chunk; - chunk.channelCount = channelCount; + chunk.channelCount = channelCount > 1 ? 2u : 1u; chunk.capturedAtMilliseconds = monotonicMilliseconds(); chunk.sequence = sequence; chunk.left.resize(frames); chunk.right.resize(frames); - if (data != nullptr) { - const auto* rawData = static_cast(data); - for (size_t frameIndex = 0; frameIndex < frames; ++frameIndex) { - const uint8_t* frameData = rawData + (frameIndex * bytesPerFrame); - const float left = readNormalizedSample(frameData, sampleSpec.format); - const float right = channelCount > 1 - ? readNormalizedSample(frameData + bytesPerSample, sampleSpec.format) - : left; - chunk.left[frameIndex] = left; - chunk.right[frameIndex] = right; - } - } else { - std::fill(chunk.left.begin(), chunk.left.end(), 0.0f); - std::fill(chunk.right.begin(), chunk.right.end(), 0.0f); - } + chunk.sourceChannelPeaks.resize(channelCount); + const Prism::Capture::PCMBufferView buffer{ + static_cast(data), length, channelCount}; + const auto format = getPCMFormat(sampleSpec.format); + Prism::Capture::selectStereoChannels(&buffer, 1, format, frames, channelCount, + routing.left, routing.right, chunk.left.data(), chunk.right.data()); + Prism::Capture::measureSourceChannelPeaks(&buffer, 1, format, frames, channelCount, + chunk.sourceChannelPeaks.data()); pa_stream_drop(stream_); pushChunk(std::move(chunk)); @@ -830,6 +781,8 @@ class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture chunkQueue_.push_back(std::move(chunk)); } + const bool input_; + Prism::Capture::ChannelRouting routing_; PulseContextConnection connection_; pa_stream* stream_ = nullptr; mutable std::mutex stateMutex_; @@ -854,6 +807,11 @@ std::unique_ptr createSystemAudioCapture() { return std::make_unique(); } +std::unique_ptr createDeviceInputCapture() { + return std::make_unique( + std::make_unique(true)); +} + } // namespace Prism::Capture #endif diff --git a/native/src/macos_capture.mm b/native/src/macos_capture.mm index f5c6403..ea3bd35 100644 --- a/native/src/macos_capture.mm +++ b/native/src/macos_capture.mm @@ -1,4 +1,5 @@ #include "system_audio_capture.h" +#include "capture_channel_selection.h" #if defined(__APPLE__) @@ -8,9 +9,10 @@ #import #include +#include +#include #include #include -#include #include #include #include @@ -29,14 +31,44 @@ double sampleRate; UInt32 channelCount; bool isDefault; + std::vector channels; }; +std::string cfStringToStdString(CFStringRef value); + +std::vector getChannelDescriptors( + AudioDeviceID deviceId, + AudioObjectPropertyScope scope, + UInt32 channelCount) { + std::vector channels; + channels.reserve(channelCount); + for (UInt32 index = 0; index < channelCount; ++index) { + std::string label = "Channel " + std::to_string(index + 1); + AudioObjectPropertyAddress address{ + kAudioObjectPropertyElementName, + scope, + index + 1, + }; + CFStringRef value = nullptr; + UInt32 size = sizeof(value); + if (AudioObjectGetPropertyData(deviceId, &address, 0, nullptr, &size, &value) == noErr + && value != nullptr) { + const std::string resolved = cfStringToStdString(value); + CFRelease(value); + if (!resolved.empty()) label = resolved; + } + channels.push_back({static_cast(index), label}); + } + return channels; +} + struct CapturedChunk { std::vector left; std::vector right; UInt32 channelCount = 2; double capturedAtMilliseconds = 0.0; uint64_t sequence = 0; + std::vector sourceChannelPeaks; }; double monotonicMilliseconds() { @@ -225,6 +257,10 @@ UInt32 getOutputChannelCount(AudioDeviceID deviceId) { sampleRate, channelCount, deviceId == defaultDeviceId, + getChannelDescriptors( + deviceId, + kAudioDevicePropertyScopeOutput, + channelCount), }); } @@ -246,75 +282,6 @@ bool getTapFormat(AudioObjectID tapId, AudioStreamBasicDescription* outFormat) { return AudioObjectGetPropertyData(tapId, &address, 0, nullptr, &size, outFormat) == noErr; } -float decodeSignedIntegerSample(const uint8_t* data, UInt32 bytesPerSample, bool isBigEndian) { - if (data == nullptr || bytesPerSample == 0 || bytesPerSample > 4) { - return 0.0f; - } - - int32_t rawValue = 0; - if (isBigEndian) { - for (UInt32 byteIndex = 0; byteIndex < bytesPerSample; ++byteIndex) { - rawValue = (rawValue << 8) | data[byteIndex]; - } - } else { - for (UInt32 byteIndex = 0; byteIndex < bytesPerSample; ++byteIndex) { - rawValue |= static_cast(data[byteIndex]) << (byteIndex * 8); - } - } - - const UInt32 totalBits = bytesPerSample * 8; - const int32_t signMask = 1 << (totalBits - 1); - if ((rawValue & signMask) != 0) { - rawValue |= ~((1 << totalBits) - 1); - } - - const double maxMagnitude = static_cast((1u << (totalBits - 1)) - 1u); - if (maxMagnitude <= 0.0) { - return 0.0f; - } - - return static_cast(static_cast(rawValue) / maxMagnitude); -} - -float readSampleFromFormat(const uint8_t* data, - const AudioStreamBasicDescription& format, - UInt32 sampleIndex) { - if (data == nullptr) { - return 0.0f; - } - - const UInt32 bytesPerChannel = format.mBitsPerChannel / 8; - if (bytesPerChannel == 0) { - return 0.0f; - } - - const uint8_t* samplePtr = data + static_cast(sampleIndex) * bytesPerChannel; - const bool isFloat = (format.mFormatFlags & kAudioFormatFlagIsFloat) != 0; - const bool isBigEndian = (format.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0; - - if (isFloat && format.mBitsPerChannel == 32) { - Float32 value = 0.0f; - std::memcpy(&value, samplePtr, sizeof(Float32)); - return value; - } - - if (isFloat && format.mBitsPerChannel == 64) { - Float64 value = 0.0; - std::memcpy(&value, samplePtr, sizeof(Float64)); - return static_cast(value); - } - - if ((format.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0) { - return decodeSignedIntegerSample(samplePtr, bytesPerChannel, isBigEndian); - } - - return 0.0f; -} - -bool isFormatInterleaved(const AudioStreamBasicDescription& format) { - return (format.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0; -} - std::string formatStatusMessage(const char* operation, OSStatus status) { return std::string(operation) + " failed (" + std::to_string(static_cast(status)) + ")"; } @@ -346,6 +313,7 @@ bool isFormatInterleaved(const AudioStreamBasicDescription& format) { device.sampleRate, static_cast(device.channelCount), device.isDefault, + device.channels, }); } return result; @@ -367,13 +335,28 @@ bool start(const std::string& requestedDeviceId, if (result != nullptr) { std::lock_guard lock(stateMutex_); result->sampleRate = sampleRate_; - result->channelCount = static_cast(channelCount_); + const UInt32 sourceChannelCount = channelCount_.load(std::memory_order_relaxed); + result->channelCount = sourceChannelCount > 1 ? 2u : 1u; + result->sourceChannelCount = static_cast(sourceChannelCount); result->deviceId = activeDeviceUid_; result->deviceLabel = activeDeviceLabel_; } return true; } + Prism::Capture::ChannelRouting setChannelRouting( + uint32_t left, + uint32_t right) override { + const UInt32 count = channelCount_.load(std::memory_order_relaxed); + if (active_.load(std::memory_order_acquire) && count > 0) { + left = std::min(left, count - 1); + right = std::min(right, count - 1); + } + routeLeft_.store(left, std::memory_order_release); + routeRight_.store(right, std::memory_order_release); + return {left, right}; + } + void stop() override { std::lock_guard lock(stateMutex_); stopLocked(); @@ -404,6 +387,7 @@ void stop() override { static_cast(chunk.channelCount), chunk.capturedAtMilliseconds, chunk.sequence, + std::move(chunk.sourceChannelPeaks), }); } return result; @@ -439,68 +423,74 @@ OSStatus handleIO(AudioObjectID, const AudioTimeStamp*, AudioBufferList*, const AudioTimeStamp*) { - AudioStreamBasicDescription format{}; - UInt32 channelCount = 0; - - { - std::lock_guard stateLock(stateMutex_); - if (!active_ || inputData == nullptr) { - return noErr; - } - format = tapFormat_; - channelCount = channelCount_; - } + if (!active_.load(std::memory_order_acquire) || inputData == nullptr) return noErr; + // tapFormat_ is configured before active_ is published and is not changed until + // AudioDeviceStop has joined the callback, so the realtime path needs no state lock. + const AudioStreamBasicDescription format = tapFormat_; + const UInt32 channelCount = channelCount_.load(std::memory_order_relaxed); if (inputData->mNumberBuffers == 0 || format.mBytesPerFrame == 0) { return noErr; } - const bool interleaved = isFormatInterleaved(format); const AudioBuffer& firstBuffer = inputData->mBuffers[0]; - const UInt32 frames = - format.mBytesPerFrame == 0 ? 0 : firstBuffer.mDataByteSize / format.mBytesPerFrame; + const UInt32 bytesPerChannel = std::max(1, format.mBitsPerChannel / 8); + const UInt32 firstBufferChannels = std::max(1, firstBuffer.mNumberChannels); + const UInt32 frames = firstBuffer.mDataByteSize / + std::max(1, bytesPerChannel * firstBufferChannels); if (frames == 0) { return noErr; } CapturedChunk chunk; - chunk.channelCount = channelCount; + chunk.channelCount = channelCount > 1 ? 2 : 1; chunk.capturedAtMilliseconds = monotonicMilliseconds(); - { - std::lock_guard stateLock(stateMutex_); - chunk.sequence = ++sequence_; - } + chunk.sequence = sequence_.fetch_add(1, std::memory_order_relaxed) + 1; chunk.left.resize(frames); chunk.right.resize(frames); - if (interleaved) { - const uint8_t* rawData = static_cast(firstBuffer.mData); - for (UInt32 frameIndex = 0; frameIndex < frames; ++frameIndex) { - const UInt32 sampleBaseIndex = frameIndex * std::max(1, channelCount); - const float leftSample = - readSampleFromFormat(rawData, format, sampleBaseIndex); - const float rightSample = channelCount > 1 - ? readSampleFromFormat(rawData, format, sampleBaseIndex + 1) - : leftSample; - chunk.left[frameIndex] = leftSample; - chunk.right[frameIndex] = rightSample; - } - } else { - const uint8_t* leftData = static_cast(inputData->mBuffers[0].mData); - const uint8_t* rightData = static_cast( - inputData->mNumberBuffers > 1 ? inputData->mBuffers[1].mData - : inputData->mBuffers[0].mData); - - for (UInt32 frameIndex = 0; frameIndex < frames; ++frameIndex) { - chunk.left[frameIndex] = - readSampleFromFormat(leftData, format, frameIndex); - chunk.right[frameIndex] = inputData->mNumberBuffers > 1 - ? readSampleFromFormat(rightData, format, frameIndex) - : chunk.left[frameIndex]; - } - } + const UInt32 leftChannel = std::min( + routeLeft_.load(std::memory_order_acquire), channelCount - 1); + const UInt32 rightChannel = std::min( + routeRight_.load(std::memory_order_acquire), channelCount - 1); + constexpr size_t kMaximumAudioBuffers = 128; + if (inputData->mNumberBuffers > kMaximumAudioBuffers) return noErr; + std::array buffers{}; + for (UInt32 bufferIndex = 0; bufferIndex < inputData->mNumberBuffers; ++bufferIndex) { + const AudioBuffer& buffer = inputData->mBuffers[bufferIndex]; + buffers[bufferIndex] = { + static_cast(buffer.mData), + buffer.mDataByteSize, + buffer.mNumberChannels, + }; + } + const Prism::Capture::SampleEncoding encoding = + (format.mFormatFlags & kAudioFormatFlagIsFloat) != 0 + ? Prism::Capture::SampleEncoding::Float + : (format.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0 + ? Prism::Capture::SampleEncoding::SignedInteger + : Prism::Capture::SampleEncoding::Unsupported; + const Prism::Capture::PCMFormat pcmFormat{ + encoding, + format.mBitsPerChannel, + (format.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0, + }; + chunk.sourceChannelPeaks.resize(channelCount); + Prism::Capture::measureSourceChannelPeaks( + buffers.data(), inputData->mNumberBuffers, pcmFormat, + frames, channelCount, chunk.sourceChannelPeaks.data()); + Prism::Capture::selectStereoChannels( + buffers.data(), + inputData->mNumberBuffers, + pcmFormat, + frames, + channelCount, + leftChannel, + rightChannel, + chunk.left.data(), + chunk.right.data()); { std::lock_guard queueLock(chunkMutex_); @@ -675,19 +665,28 @@ bool startInternal(const std::string& requestedDeviceUid, std::string* outErrorM overwriteCount_ = 0; } - active_ = true; + channelCount_.store( + std::max(1, tapFormat_.mChannelsPerFrame > 0 + ? tapFormat_.mChannelsPerFrame + : selected->channelCount), + std::memory_order_relaxed); + const UInt32 resolvedChannelCount = channelCount_.load(std::memory_order_relaxed); + routeLeft_.store( + std::min(routeLeft_.load(std::memory_order_relaxed), resolvedChannelCount - 1), + std::memory_order_relaxed); + routeRight_.store( + std::min(routeRight_.load(std::memory_order_relaxed), resolvedChannelCount - 1), + std::memory_order_relaxed); + active_.store(true, std::memory_order_release); activeDeviceUid_ = selected->uid; activeDeviceLabel_ = selected->label; sampleRate_ = tapFormat_.mSampleRate > 0 ? tapFormat_.mSampleRate : selected->sampleRate; - channelCount_ = - std::max(1, tapFormat_.mChannelsPerFrame > 0 ? tapFormat_.mChannelsPerFrame - : selected->channelCount); - sequence_ = 0; + sequence_.store(0, std::memory_order_relaxed); return true; } void stopLocked() { - active_ = false; + active_.store(false, std::memory_order_release); if (aggregateDeviceId_ != kUnknownObject && ioProcId_ != nullptr) { AudioDeviceStop(aggregateDeviceId_, ioProcId_); @@ -711,8 +710,8 @@ void stopLocked() { activeDeviceUid_.clear(); activeDeviceLabel_.clear(); sampleRate_ = 48000.0; - channelCount_ = 2; - sequence_ = 0; + channelCount_.store(2, std::memory_order_relaxed); + sequence_.store(0, std::memory_order_relaxed); std::lock_guard queueLock(chunkMutex_); chunkQueue_.clear(); @@ -730,18 +729,20 @@ bool isSupported() const { std::mutex chunkMutex_; std::deque chunkQueue_; uint64_t overwriteCount_ = 0; - uint64_t sequence_ = 0; + std::atomic sequence_{0}; AudioObjectID tapId_ = kUnknownObject; AudioObjectID aggregateDeviceId_ = kUnknownObject; AudioDeviceIOProcID ioProcId_ = nullptr; AudioStreamBasicDescription tapFormat_{}; - bool active_ = false; + std::atomic active_{false}; std::string activeDeviceUid_; std::string activeDeviceLabel_; double sampleRate_ = 48000.0; - UInt32 channelCount_ = 2; + std::atomic channelCount_{2}; + std::atomic routeLeft_{0}; + std::atomic routeRight_{1}; }; } // namespace diff --git a/native/src/macos_input_capture.mm b/native/src/macos_input_capture.mm new file mode 100644 index 0000000..6f89823 --- /dev/null +++ b/native/src/macos_input_capture.mm @@ -0,0 +1,558 @@ +#include "device_input_capture.h" +#include "capture_channel_selection.h" + +#if defined(__APPLE__) + +#import +#import +#import + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr size_t kMaxQueuedChunks = 256; +constexpr size_t kDefaultDrainChunkLimit = 64; +constexpr UInt32 kFallbackMaximumFramesPerSlice = 4096; +constexpr AudioObjectID kUnknownObject = kAudioObjectUnknown; + +double monotonicMilliseconds() { + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + return std::chrono::duration(now).count(); +} + +std::string cfStringToStdString(CFStringRef value) { + if (value == nullptr) return {}; + const CFIndex length = CFStringGetLength(value); + const CFIndex maxBytes = + CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + std::vector buffer(static_cast(std::max(1, maxBytes)), '\0'); + if (!CFStringGetCString(value, buffer.data(), maxBytes, kCFStringEncodingUTF8)) return {}; + return std::string(buffer.data()); +} + +template +bool getPropertyData(AudioObjectID objectId, + AudioObjectPropertySelector selector, + AudioObjectPropertyScope scope, + AudioObjectPropertyElement element, + T* outValue) { + if (outValue == nullptr) return false; + AudioObjectPropertyAddress address{selector, scope, element}; + UInt32 size = sizeof(T); + return AudioObjectGetPropertyData(objectId, &address, 0, nullptr, &size, outValue) == noErr; +} + +bool getDeviceStringProperty(AudioDeviceID deviceId, + AudioObjectPropertySelector selector, + std::string* outValue) { + if (outValue == nullptr) return false; + AudioObjectPropertyAddress address{ + selector, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain, + }; + CFStringRef value = nullptr; + UInt32 size = sizeof(value); + if (AudioObjectGetPropertyData(deviceId, &address, 0, nullptr, &size, &value) != noErr + || value == nullptr) { + return false; + } + *outValue = cfStringToStdString(value); + CFRelease(value); + return !outValue->empty(); +} + +AudioDeviceID getDefaultInputDeviceId() { + AudioDeviceID deviceId = kUnknownObject; + getPropertyData( + kAudioObjectSystemObject, + kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain, + &deviceId); + return deviceId; +} + +UInt32 getInputChannelCount(AudioDeviceID deviceId) { + AudioObjectPropertyAddress address{ + kAudioDevicePropertyStreamConfiguration, + kAudioDevicePropertyScopeInput, + kAudioObjectPropertyElementMain, + }; + UInt32 size = 0; + if (AudioObjectGetPropertyDataSize(deviceId, &address, 0, nullptr, &size) != noErr + || size == 0) { + return 0; + } + std::vector storage(size); + auto* bufferList = reinterpret_cast(storage.data()); + if (AudioObjectGetPropertyData(deviceId, &address, 0, nullptr, &size, bufferList) != noErr) { + return 0; + } + UInt32 count = 0; + for (UInt32 index = 0; index < bufferList->mNumberBuffers; ++index) { + count += bufferList->mBuffers[index].mNumberChannels; + } + return count; +} + +double getDeviceSampleRate(AudioDeviceID deviceId) { + Float64 sampleRate = 48000.0; + if (!getPropertyData( + deviceId, + kAudioDevicePropertyNominalSampleRate, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain, + &sampleRate) + || sampleRate <= 0.0) { + return 48000.0; + } + return static_cast(sampleRate); +} + +std::vector getInputChannelDescriptors( + AudioDeviceID deviceId, + UInt32 channelCount) { + std::vector channels; + channels.reserve(channelCount); + for (UInt32 index = 0; index < channelCount; ++index) { + std::string label = "Channel " + std::to_string(index + 1); + AudioObjectPropertyAddress address{ + kAudioObjectPropertyElementName, + kAudioDevicePropertyScopeInput, + index + 1, + }; + CFStringRef value = nullptr; + UInt32 size = sizeof(value); + if (AudioObjectGetPropertyData(deviceId, &address, 0, nullptr, &size, &value) == noErr + && value != nullptr) { + const std::string resolved = cfStringToStdString(value); + CFRelease(value); + if (!resolved.empty()) label = resolved; + } + channels.push_back({static_cast(index), label}); + } + return channels; +} + +struct InputDeviceInfo { + AudioDeviceID objectId = kUnknownObject; + Prism::Capture::OutputDevice descriptor; +}; + +std::vector enumerateInputDevices() { + AudioObjectPropertyAddress address{ + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain, + }; + UInt32 size = 0; + if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address, 0, nullptr, &size) != noErr + || size == 0) { + return {}; + } + std::vector ids(size / sizeof(AudioDeviceID), kUnknownObject); + if (AudioObjectGetPropertyData( + kAudioObjectSystemObject, &address, 0, nullptr, &size, ids.data()) != noErr) { + return {}; + } + + const AudioDeviceID defaultId = getDefaultInputDeviceId(); + std::vector devices; + for (const AudioDeviceID id : ids) { + const UInt32 channelCount = getInputChannelCount(id); + if (channelCount == 0) continue; + + std::string uid; + if (!getDeviceStringProperty(id, kAudioDevicePropertyDeviceUID, &uid)) continue; + std::string label; + if (!getDeviceStringProperty(id, kAudioObjectPropertyName, &label)) label = uid; + + devices.push_back({ + id, + { + uid, + label, + getDeviceSampleRate(id), + static_cast(channelCount), + id == defaultId, + getInputChannelDescriptors(id, channelCount), + }, + }); + } + return devices; +} + +std::string formatStatusMessage(const char* operation, OSStatus status) { + return std::string(operation) + " failed (" + std::to_string(static_cast(status)) + ")"; +} + +class MacOSDeviceInputCapture final : public Prism::Capture::DeviceInputCapture { +public: + ~MacOSDeviceInputCapture() override { stop(); } + + Prism::Capture::Support getSupport() const override { return {true, {}}; } + + std::vector listInputDevices() override { + const auto devices = enumerateInputDevices(); + std::vector result; + result.reserve(devices.size()); + for (const auto& device : devices) result.push_back(device.descriptor); + return result; + } + + bool start(const std::string& requestedDeviceId, + Prism::Capture::StartResult* result, + std::string* errorMessage) override { + std::lock_guard lock(stateMutex_); + stopLocked(); + + const auto devices = enumerateInputDevices(); + const InputDeviceInfo* selected = nullptr; + if (!requestedDeviceId.empty()) { + for (const auto& device : devices) { + if (device.descriptor.id == requestedDeviceId) { + selected = &device; + break; + } + } + if (selected == nullptr) { + if (errorMessage != nullptr) { + *errorMessage = "The selected macOS input device is no longer available."; + } + return false; + } + } else { + for (const auto& device : devices) { + if (device.descriptor.isDefault) { + selected = &device; + break; + } + } + if (selected == nullptr && !devices.empty()) selected = &devices.front(); + } + if (selected == nullptr) { + if (errorMessage != nullptr) *errorMessage = "No macOS input devices are available."; + return false; + } + + AudioComponentDescription description{}; + description.componentType = kAudioUnitType_Output; + description.componentSubType = kAudioUnitSubType_HALOutput; + description.componentManufacturer = kAudioUnitManufacturer_Apple; + const AudioComponent component = AudioComponentFindNext(nullptr, &description); + if (component == nullptr + || AudioComponentInstanceNew(component, &audioUnit_) != noErr + || audioUnit_ == nullptr) { + if (errorMessage != nullptr) *errorMessage = "Could not create the Core Audio input unit."; + stopLocked(); + return false; + } + + UInt32 enabled = 1; + OSStatus status = AudioUnitSetProperty( + audioUnit_, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + 1, + &enabled, + sizeof(enabled)); + if (status == noErr) { + enabled = 0; + status = AudioUnitSetProperty( + audioUnit_, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + 0, + &enabled, + sizeof(enabled)); + } + if (status == noErr) { + AudioDeviceID deviceId = selected->objectId; + status = AudioUnitSetProperty( + audioUnit_, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &deviceId, + sizeof(deviceId)); + } + + sourceChannelCount_ = std::max(1, selected->descriptor.channelCount); + sampleRate_ = selected->descriptor.sampleRate; + clientFormat_ = AudioStreamBasicDescription{}; + clientFormat_.mSampleRate = sampleRate_; + clientFormat_.mFormatID = kAudioFormatLinearPCM; + clientFormat_.mFormatFlags = kAudioFormatFlagIsFloat + | kAudioFormatFlagIsPacked + | kAudioFormatFlagIsNonInterleaved + | kAudioFormatFlagsNativeEndian; + clientFormat_.mBytesPerPacket = sizeof(Float32); + clientFormat_.mFramesPerPacket = 1; + clientFormat_.mBytesPerFrame = sizeof(Float32); + clientFormat_.mChannelsPerFrame = sourceChannelCount_; + clientFormat_.mBitsPerChannel = 8 * sizeof(Float32); + + if (status == noErr) { + status = AudioUnitSetProperty( + audioUnit_, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 1, + &clientFormat_, + sizeof(clientFormat_)); + } + + AURenderCallbackStruct callback{}; + callback.inputProc = StaticInputCallback; + callback.inputProcRefCon = this; + if (status == noErr) { + status = AudioUnitSetProperty( + audioUnit_, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + 0, + &callback, + sizeof(callback)); + } + if (status == noErr) status = AudioUnitInitialize(audioUnit_); + if (status != noErr) { + if (errorMessage != nullptr) { + *errorMessage = formatStatusMessage("Configuring Core Audio input", status); + } + stopLocked(); + return false; + } + + maximumFramesPerSlice_ = kFallbackMaximumFramesPerSlice; + UInt32 propertySize = sizeof(maximumFramesPerSlice_); + AudioUnitGetProperty( + audioUnit_, + kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, + 0, + &maximumFramesPerSlice_, + &propertySize); + maximumFramesPerSlice_ = std::max(1, maximumFramesPerSlice_); + prepareRenderBuffers(); + + activeDeviceId_ = selected->descriptor.id; + activeDeviceLabel_ = selected->descriptor.label; + routeLeft_.store( + std::min(routeLeft_.load(std::memory_order_relaxed), sourceChannelCount_ - 1), + std::memory_order_relaxed); + routeRight_.store( + std::min(routeRight_.load(std::memory_order_relaxed), sourceChannelCount_ - 1), + std::memory_order_relaxed); + sequence_.store(0, std::memory_order_relaxed); + { + std::lock_guard queueLock(queueMutex_); + queue_.clear(); + overwriteCount_ = 0; + } + + active_.store(true, std::memory_order_release); + status = AudioOutputUnitStart(audioUnit_); + if (status != noErr) { + active_.store(false, std::memory_order_release); + if (errorMessage != nullptr) { + *errorMessage = formatStatusMessage("AudioOutputUnitStart", status); + } + stopLocked(); + return false; + } + + if (result != nullptr) { + result->sampleRate = sampleRate_; + result->channelCount = sourceChannelCount_ > 1 ? 2u : 1u; + result->sourceChannelCount = sourceChannelCount_; + result->deviceId = activeDeviceId_; + result->deviceLabel = activeDeviceLabel_; + } + return true; + } + + Prism::Capture::ChannelRouting setChannelRouting( + uint32_t left, + uint32_t right) override { + if (active_.load(std::memory_order_acquire) && sourceChannelCount_ > 0) { + left = std::min(left, sourceChannelCount_ - 1); + right = std::min(right, sourceChannelCount_ - 1); + } + routeLeft_.store(left, std::memory_order_release); + routeRight_.store(right, std::memory_order_release); + return {left, right}; + } + + void stop() override { + std::lock_guard lock(stateMutex_); + stopLocked(); + } + + Prism::Capture::DrainResult drain(size_t maxChunks) override { + const size_t limit = maxChunks == 0 + ? kDefaultDrainChunkLimit + : std::min(maxChunks, kMaxQueuedChunks); + Prism::Capture::DrainResult result; + std::lock_guard lock(queueMutex_); + result.overwriteCount = overwriteCount_; + const size_t count = std::min(limit, queue_.size()); + result.chunks.reserve(count); + for (size_t index = 0; index < count; ++index) { + result.chunks.push_back(std::move(queue_.front())); + queue_.pop_front(); + } + result.queueDepth = queue_.size(); + return result; + } + + double nowMilliseconds() const override { return monotonicMilliseconds(); } + +private: + static OSStatus StaticInputCallback(void* refCon, + AudioUnitRenderActionFlags* actionFlags, + const AudioTimeStamp* timestamp, + UInt32, + UInt32 frameCount, + AudioBufferList*) { + auto* self = static_cast(refCon); + return self == nullptr + ? noErr + : self->captureInput(actionFlags, timestamp, frameCount); + } + + OSStatus captureInput(AudioUnitRenderActionFlags* actionFlags, + const AudioTimeStamp* timestamp, + UInt32 frameCount) { + if (!active_.load(std::memory_order_acquire) || audioUnit_ == nullptr) return noErr; + if (frameCount == 0 || frameCount > maximumFramesPerSlice_ || renderBufferList_ == nullptr) { + return noErr; + } + + for (UInt32 index = 0; index < sourceChannelCount_; ++index) { + renderBufferList_->mBuffers[index].mDataByteSize = frameCount * sizeof(Float32); + } + const OSStatus status = AudioUnitRender( + audioUnit_, actionFlags, timestamp, 1, frameCount, renderBufferList_); + if (status != noErr) return status; + + for (UInt32 index = 0; index < sourceChannelCount_; ++index) { + activityBufferViews_[index].byteLength = renderBufferList_->mBuffers[index].mDataByteSize; + } + + const uint32_t leftChannel = std::min( + routeLeft_.load(std::memory_order_acquire), sourceChannelCount_ - 1); + const uint32_t rightChannel = std::min( + routeRight_.load(std::memory_order_acquire), sourceChannelCount_ - 1); + const auto* left = static_cast(renderBufferList_->mBuffers[leftChannel].mData); + const auto* right = static_cast(renderBufferList_->mBuffers[rightChannel].mData); + + Prism::Capture::AudioChunk chunk; + chunk.sourceChannelPeaks.resize(sourceChannelCount_); + Prism::Capture::measureSourceChannelPeaks( + activityBufferViews_.data(), activityBufferViews_.size(), + {Prism::Capture::SampleEncoding::Float, 32, false}, + frameCount, sourceChannelCount_, chunk.sourceChannelPeaks.data()); + chunk.left.assign(left, left + frameCount); + chunk.right.assign(right, right + frameCount); + chunk.channelCount = sourceChannelCount_ > 1 ? 2u : 1u; + chunk.capturedAtMilliseconds = monotonicMilliseconds(); + chunk.sequence = sequence_.fetch_add(1, std::memory_order_relaxed) + 1; + + std::lock_guard lock(queueMutex_); + if (queue_.size() >= kMaxQueuedChunks) { + queue_.pop_front(); + ++overwriteCount_; + } + queue_.push_back(std::move(chunk)); + return noErr; + } + + void prepareRenderBuffers() { + const size_t listBytes = offsetof(AudioBufferList, mBuffers) + + sizeof(AudioBuffer) * sourceChannelCount_; + renderBufferListStorage_.assign(listBytes, 0); + renderBufferList_ = reinterpret_cast(renderBufferListStorage_.data()); + renderBufferList_->mNumberBuffers = sourceChannelCount_; + renderSampleStorage_.assign( + static_cast(sourceChannelCount_) * maximumFramesPerSlice_, + 0.0f); + activityBufferViews_.resize(sourceChannelCount_); + for (UInt32 index = 0; index < sourceChannelCount_; ++index) { + auto& buffer = renderBufferList_->mBuffers[index]; + buffer.mNumberChannels = 1; + buffer.mDataByteSize = maximumFramesPerSlice_ * sizeof(Float32); + buffer.mData = renderSampleStorage_.data() + + static_cast(index) * maximumFramesPerSlice_; + activityBufferViews_[index] = { + static_cast(buffer.mData), buffer.mDataByteSize, 1, + }; + } + } + + void stopLocked() { + active_.store(false, std::memory_order_release); + if (audioUnit_ != nullptr) { + AudioOutputUnitStop(audioUnit_); + AudioUnitUninitialize(audioUnit_); + AudioComponentInstanceDispose(audioUnit_); + audioUnit_ = nullptr; + } + renderBufferList_ = nullptr; + renderBufferListStorage_.clear(); + renderSampleStorage_.clear(); + activityBufferViews_.clear(); + activeDeviceId_.clear(); + activeDeviceLabel_.clear(); + sampleRate_ = 48000.0; + sourceChannelCount_ = 2; + maximumFramesPerSlice_ = kFallbackMaximumFramesPerSlice; + sequence_.store(0, std::memory_order_relaxed); + std::lock_guard queueLock(queueMutex_); + queue_.clear(); + overwriteCount_ = 0; + } + + mutable std::mutex stateMutex_; + std::mutex queueMutex_; + std::deque queue_; + uint64_t overwriteCount_ = 0; + std::atomic sequence_{0}; + std::atomic active_{false}; + std::atomic routeLeft_{0}; + std::atomic routeRight_{1}; + + AudioUnit audioUnit_ = nullptr; + AudioStreamBasicDescription clientFormat_{}; + std::vector renderBufferListStorage_; + std::vector renderSampleStorage_; + std::vector activityBufferViews_; + AudioBufferList* renderBufferList_ = nullptr; + UInt32 maximumFramesPerSlice_ = kFallbackMaximumFramesPerSlice; + uint32_t sourceChannelCount_ = 2; + double sampleRate_ = 48000.0; + std::string activeDeviceId_; + std::string activeDeviceLabel_; +}; + +} // namespace + +namespace Prism::Capture { + +std::unique_ptr createDeviceInputCapture() { + return std::make_unique(); +} + +} // namespace Prism::Capture + +#endif diff --git a/native/src/main.cpp b/native/src/main.cpp index ad21f93..24428c8 100644 --- a/native/src/main.cpp +++ b/native/src/main.cpp @@ -3,15 +3,20 @@ #include #include #include "system_audio_capture_napi.h" +#include "device_input_capture_napi.h" +#include "capture_channel_selection_napi.h" #include "windows_capture.h" #include "oscilloscope.h" #include "spectrum.h" +#include "reference_napi.h" +#include "waterfall_napi.h" #include "spectrogram.h" #include "vectorscope.h" #include "waveform.h" #include "vumeter.h" #include "lufsmeter.h" #include "window_chrome.h" +#include "window_docking.h" // Global instances static Visualizer::Oscilloscope oscilloscope; @@ -705,6 +710,9 @@ Napi::Value LUFSMeterReset(const Napi::CallbackInfo& info) { // ============== Module Init ============== Napi::Object Init(Napi::Env env, Napi::Object exports) { + // Release the live resampler while r8brain's shared caches are still alive. + // Its process-static cache teardown order differs from this global analyzer. + env.AddCleanupHook([] { spectrum.setReferenceEnabled(false); }); // Oscilloscope Napi::Object oscExports = Napi::Object::New(env); oscExports.Set("setSampleRate", Napi::Function::New(env, OscilloscopeSetSampleRate)); @@ -727,7 +735,21 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { specExports.Set("setSmoothing", Napi::Function::New(env, SpectrumSetSmoothing)); specExports.Set("pushSamples", Napi::Function::New(env, SpectrumPushSamples)); specExports.Set("pushStereoSamples", Napi::Function::New(env, SpectrumPushStereoSamples)); + specExports.Set("hasSpectrumData", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), spectrum.hasSpectrumData()); + })); specExports.Set("fillRawMagnitudes", Napi::Function::New(env, SpectrumFillRawMagnitudes)); + specExports.Set("setReferenceEnabled", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + spectrum.setReferenceEnabled(info.Length() && info[0].IsBoolean() && info[0].As().Value()); + return info.Env().Undefined(); + })); + specExports.Set("getReferenceLevel", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + auto result = Napi::Object::New(info.Env()); + result.Set("meanSquare", spectrum.getReferenceMeanSquare()); + result.Set("seconds", spectrum.getReferenceSeconds()); + return result; + })); + initReferenceAnalysis(env, exports); specExports.Set("fillMagnitudes", Napi::Function::New(env, SpectrumFillMagnitudes)); specExports.Set("fillSideMagnitudes", Napi::Function::New(env, SpectrumFillSideMagnitudes)); specExports.Set("fillChannelMaxMagnitudes", Napi::Function::New(env, SpectrumFillChannelMaxMagnitudes)); @@ -786,9 +808,13 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { lufsExports.Set("reset", Napi::Function::New(env, LUFSMeterReset)); exports.Set("lufsmeter", lufsExports); + RegisterWaterfall(env, exports); RegisterSystemAudioCapture(env, exports); + RegisterDeviceInputCapture(env, exports); + RegisterCaptureChannelSelection(env, exports); RegisterWindowsMedia(env, exports); RegisterWindowChrome(env, exports); + RegisterWindowDocking(env, exports); return exports; } diff --git a/native/src/reference_analysis.cpp b/native/src/reference_analysis.cpp new file mode 100644 index 0000000..ffa940f --- /dev/null +++ b/native/src/reference_analysis.cpp @@ -0,0 +1,175 @@ +#include "reference_analysis.h" +#include "dsp_utils.h" +#include "../vendor/r8brain/CDSPResampler.h" +#include +#include +#include + +namespace Visualizer { +struct ReferenceResampler::Impl { + double sourceRate; + uint64_t inputCount = 0, outputCount = 0; + std::unique_ptr converter; + std::array input {}; + std::vector converted; + explicit Impl(double rate) : sourceRate(rate) { + if (rate < 8000 || rate > 384000 || !std::isfinite(rate)) throw std::runtime_error("Unsupported audio sample rate."); + if (rate != referenceSampleRate) converter = std::make_unique(rate, referenceSampleRate, 4096); + } + void convert(size_t count, const std::function& output) { + double* samples = nullptr; + const int available = converter->process(input.data(), static_cast(count), samples); + const auto expected = static_cast(std::llround(inputCount * referenceSampleRate / sourceRate)); + const size_t n = std::min(static_cast(available), expected - std::min(expected, outputCount)); + if (!n) return; + converted.resize(n); + for (size_t i = 0; i < n; ++i) converted[i] = static_cast(samples[i]); + output(converted.data(), n); + outputCount += n; + } +}; +ReferenceResampler::ReferenceResampler(double rate) : impl(std::make_unique(rate)) {} +ReferenceResampler::~ReferenceResampler() = default; +void ReferenceResampler::reset() { impl = std::make_unique(impl->sourceRate); } +void ReferenceResampler::push(const float* samples, size_t count, const std::function& output) { + if (!impl->converter) { impl->inputCount += count; impl->outputCount += count; output(samples, count); return; } + while (count) { + const size_t n = std::min(count, impl->input.size()); + for (size_t i = 0; i < n; ++i) impl->input[i] = samples[i]; + impl->inputCount += n; + impl->convert(n, output); + count -= n; samples += n; + } +} +void ReferenceResampler::finish(const std::function& output) { + if (!impl->converter) return; + const auto expected = static_cast(std::llround(impl->inputCount * referenceSampleRate / impl->sourceRate)); + impl->input.fill(0); + for (int i = 0; impl->outputCount < expected && i < 128; ++i) impl->convert(impl->input.size(), output); + if (impl->outputCount != expected) throw std::runtime_error("Could not finish audio resampling."); +} + +struct ReferenceAccumulator::Impl { + struct Band { + size_t size, hop; + DSP::FFT fft; + std::vector pending, window, input; + std::vector> output; + std::vector sum; + double weight = 0, fullEnergy = 0; + explicit Band(size_t n) : size(n), hop(n / 4), fft(n), window(n), input(n), output(n), sum(n / 2, 0) { + pending.reserve(n + 4096); + for (size_t i = 0; i < n; ++i) { + window[i] = static_cast(0.5 * (1 - std::cos(2 * M_PI * i / (n - 1)))); + fullEnergy += static_cast(window[i]) * window[i]; + } + } + void frame(size_t valid, size_t offset = 0) { + double windowSum = 0, energy = 0; + std::fill(input.begin(), input.end(), 0); + const size_t padding = (size - valid) / 2; + for (size_t i = 0; i < valid; ++i) { + const auto w = window[padding + i]; + input[padding + i] = pending[offset + i] * w; + windowSum += w; energy += static_cast(w) * w; + } + if (windowSum <= 0 || energy <= 0) return; + fft.forward(input.data(), output.data()); + const double w = energy / fullEnergy; + const double scale = 4 / (windowSum * windowSum); + for (size_t i = 0; i < sum.size(); ++i) sum[i] += std::norm(output[i]) * scale * w; + weight += w; + } + void push(const float* samples, size_t count) { + pending.insert(pending.end(), samples, samples + count); + size_t consumed = 0; + while (pending.size() - consumed >= size) { + frame(size, consumed); + consumed += hop; + } + pending.erase(pending.begin(), pending.begin() + static_cast(consumed)); + } + std::vector curve() const { + std::vector result(sum.size(), 0); + if (weight > 0) for (size_t i = 0; i < sum.size(); ++i) result[i] = static_cast(sum[i] / weight); + return result; + } + }; + double sourceRate; + ReferenceResampler resampler; + std::array, 5> bands; + uint64_t samples = 0; + double power = 0; + bool finished = false; + explicit Impl(double rate) : sourceRate(rate), resampler(rate) { + for (size_t i = 0; i < bands.size(); ++i) bands[i] = std::make_unique(referenceFFTSizes[i]); + } + void accumulate(const float* data, size_t count) { + for (size_t i = 0; i < count; ++i) power += static_cast(data[i]) * data[i]; + samples += count; + for (auto& band : bands) band->push(data, count); + } +}; +ReferenceAccumulator::ReferenceAccumulator(double rate) : impl(std::make_unique(rate)) {} +ReferenceAccumulator::~ReferenceAccumulator() = default; +void ReferenceAccumulator::push(const float* data, size_t count) { + if (impl->finished) throw std::runtime_error("Reference analysis is already complete."); + impl->resampler.push(data, count, [this](const float* p, size_t n) { impl->accumulate(p, n); }); +} +ReferenceCurve ReferenceAccumulator::snapshot(bool finish) { + if (finish && !impl->finished) { + impl->resampler.finish([this](const float* p, size_t n) { impl->accumulate(p, n); }); + for (auto& band : impl->bands) if (!band->pending.empty()) band->frame(band->pending.size()); + impl->finished = true; + } + ReferenceCurve result; + result.sourceNyquistHz = impl->sourceRate / 2; + result.durationSeconds = static_cast(impl->samples) / referenceSampleRate; + result.meanSquare = impl->samples ? impl->power / impl->samples : 0; + for (size_t i = 0; i < impl->bands.size(); ++i) result.powers[i] = impl->bands[i]->curve(); + return result; +} + +ReferenceLiveState::ReferenceLiveState(double rate) + : leftResampler(rate), rightResampler(rate), powers(referenceSampleRate * 3, 0), recentPowers(static_cast(std::ceil(rate / 10)), 0) {} +ReferenceLiveState::~ReferenceLiveState() = default; +void ReferenceLiveState::push(const float* left, const float* right, size_t count, + const std::function& output) { + if (!count) return; + const auto now = std::chrono::steady_clock::now(); + if (lastInput != std::chrono::steady_clock::time_point() && now - lastInput > std::chrono::milliseconds(300)) { + std::fill(powers.begin(), powers.end(), 0); + std::fill(recentPowers.begin(), recentPowers.end(), 0); + position = filled = recentPosition = 0; sum = recentSum = 0; + leftResampler.reset(); rightResampler.reset(); leftPending.clear(); rightPending.clear(); + } + lastInput = now; + for (size_t i = 0; i < count; ++i) { + const double mid = (static_cast(left[i]) + (right ? right[i] : left[i])) * 0.5; + recentSum -= recentPowers[recentPosition]; recentPowers[recentPosition] = mid * mid; recentSum += recentPowers[recentPosition]; + recentPosition = (recentPosition + 1) % recentPowers.size(); + } + leftResampler.push(left, count, [this](const float* p, size_t n) { leftPending.insert(leftPending.end(), p, p + n); }); + rightResampler.push(right ? right : left, count, [this](const float* p, size_t n) { rightPending.insert(rightPending.end(), p, p + n); }); + const size_t n = std::min(leftPending.size(), rightPending.size()); + if (!n) return; + for (size_t i = 0; i < n; ++i) { + const double mid = (static_cast(leftPending[i]) + rightPending[i]) * 0.5; + sum -= powers[position]; powers[position] = mid * mid; sum += powers[position]; + position = (position + 1) % powers.size(); filled = std::min(filled + 1, powers.size()); + } + output(leftPending.data(), rightPending.data(), n); + leftPending.erase(leftPending.begin(), leftPending.begin() + static_cast(n)); + rightPending.erase(rightPending.begin(), rightPending.begin() + static_cast(n)); +} +double ReferenceLiveState::meanSquare() const { + // Matching uses the three-second average only while the most recent 100 ms + // contain signal. Old power must not enable Match on silence/stopped capture. + return seconds() > 0 && recentSum / recentPowers.size() > 1e-9 + ? std::max(0.0, sum / filled) : 0; +} +double ReferenceLiveState::seconds() const { + return std::chrono::steady_clock::now() - lastInput < std::chrono::milliseconds(300) + ? static_cast(filled) / referenceSampleRate : 0; +} +} diff --git a/native/src/reference_analysis.h b/native/src/reference_analysis.h new file mode 100644 index 0000000..8152ddc --- /dev/null +++ b/native/src/reference_analysis.h @@ -0,0 +1,68 @@ +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Visualizer { +constexpr int referenceSampleRate = 48000; +constexpr std::array referenceFFTSizes { 1024, 2048, 4096, 8192, 16384 }; + +class ReferenceResampler { +public: + explicit ReferenceResampler(double sourceRate); + ~ReferenceResampler(); + void push(const float* samples, size_t count, const std::function& output); + void finish(const std::function& output); + void reset(); +private: + struct Impl; + std::unique_ptr impl; +}; + +struct ReferenceCurve { + double sourceNyquistHz = 24000; + double durationSeconds = 0; + double meanSquare = 0; + std::array, 5> powers; +}; + +/** Every object owns its FFTs and accumulation buffers; no live-DSP singleton access. */ +class ReferenceAccumulator { +public: + explicit ReferenceAccumulator(double sourceRate); + ~ReferenceAccumulator(); + void push(const float* mid, size_t count); + ReferenceCurve snapshot(bool finish = false); +private: + struct Impl; + std::unique_ptr impl; +}; + +using ReferenceProgress = std::function; +ReferenceCurve analyzeReferenceFile(const std::string& path, std::atomic& cancelled, + const ReferenceProgress& progress, size_t chunkFrames = 4096); + +class ReferenceLiveState { +public: + explicit ReferenceLiveState(double sampleRate); + ~ReferenceLiveState(); + void push(const float* left, const float* right, size_t count, + const std::function& output); + double meanSquare() const; + double seconds() const; +private: + ReferenceResampler leftResampler, rightResampler; + std::vector leftPending, rightPending; + std::vector powers; + std::vector recentPowers; + size_t position = 0, filled = 0, recentPosition = 0; + double sum = 0, recentSum = 0; + std::chrono::steady_clock::time_point lastInput {}; +}; +} diff --git a/native/src/reference_decode.cpp b/native/src/reference_decode.cpp new file mode 100644 index 0000000..d32d924 --- /dev/null +++ b/native/src/reference_decode.cpp @@ -0,0 +1,110 @@ +#include "reference_analysis.h" +#define DR_WAV_IMPLEMENTATION +#define DR_FLAC_IMPLEMENTATION +#define DR_MP3_IMPLEMENTATION +#include "../vendor/dr_libs/dr_wav.h" +#include "../vendor/dr_libs/dr_flac.h" +#include "../vendor/dr_libs/dr_mp3.h" +#include +#include +#include +#include +#include +#include + +namespace Visualizer { +namespace { +struct Decoder { + drwav wav {}; + drmp3 mp3 {}; + drflac* flac = nullptr; + int kind = 0, channels = 0, rate = 0; + uint64_t frames = 0; + explicit Decoder(const std::string& path) { + const auto nativePath = std::filesystem::u8path(path); + // dr_wav deliberately tolerates truncated containers. Reject a declared + // RIFF/AIFF size beyond EOF rather than silently saving a partial track. + std::ifstream headerFile(nativePath, std::ios::binary); + unsigned char header[12] {}; + if (headerFile.read(reinterpret_cast(header), sizeof(header))) { + const bool little = std::memcmp(header, "RIFF", 4) == 0; + const bool big = std::memcmp(header, "FORM", 4) == 0 || std::memcmp(header, "RIFX", 4) == 0; + if (little || big) { + uint64_t declared = 0; + for (int i = 0; i < 4; ++i) declared |= static_cast(header[4 + i]) << (8 * (little ? i : 3 - i)); + std::error_code error; + const auto bytes = std::filesystem::file_size(nativePath, error); + if (!error && declared != 0xffffffff && declared + 8 > bytes) + throw std::runtime_error("This audio file is incomplete or damaged."); + } + } +#ifdef _WIN32 + const auto* file = nativePath.c_str(); + if (drwav_init_file_w(&wav, file, nullptr)) kind = 1; + else if ((flac = drflac_open_file_w(file, nullptr))) kind = 2; + else if (drmp3_init_file_w(&mp3, file, nullptr)) kind = 3; +#else + const auto* file = nativePath.c_str(); + if (drwav_init_file(&wav, file, nullptr)) kind = 1; + else if ((flac = drflac_open_file(file, nullptr))) kind = 2; + else if (drmp3_init_file(&mp3, file, nullptr)) kind = 3; +#endif + if (kind == 1) { channels = wav.channels; rate = wav.sampleRate; frames = wav.totalPCMFrameCount; } + if (kind == 2) { channels = flac->channels; rate = flac->sampleRate; frames = flac->totalPCMFrameCount; } + if (kind == 3) { channels = mp3.channels; rate = mp3.sampleRate; frames = mp3.totalPCMFrameCount == DRMP3_UINT64_MAX ? 0 : mp3.totalPCMFrameCount; } + if (!kind) throw std::runtime_error("Could not open this audio file. Choose a WAV, AIFF, FLAC, or MP3 file."); + } + ~Decoder() { + if (kind == 1) drwav_uninit(&wav); + if (kind == 2) drflac_close(flac); + if (kind == 3) drmp3_uninit(&mp3); + } + size_t read(float* data, size_t n) { + if (kind == 1) return static_cast(drwav_read_pcm_frames_f32(&wav, n, data)); + if (kind == 2) return static_cast(drflac_read_pcm_frames_f32(flac, n, data)); + return static_cast(drmp3_read_pcm_frames_f32(&mp3, n, data)); + } +}; +bool finiteSample(float value) { + // Keep validation effective even in the existing -ffast-math native build. + uint32_t bits; std::memcpy(&bits, &value, sizeof(bits)); + return (bits & 0x7f800000u) != 0x7f800000u; +} +} +ReferenceCurve analyzeReferenceFile(const std::string& path, std::atomic& cancelled, + const ReferenceProgress& progress, size_t chunkFrames) { + Decoder decoder(path); + if (decoder.channels != 1 && decoder.channels != 2) throw std::runtime_error("Choose a mono or stereo reference track."); + ReferenceAccumulator accumulator(decoder.rate); + chunkFrames = std::clamp(chunkFrames, 1, 16384); + std::vector interleaved(chunkFrames * decoder.channels), mid(chunkFrames); + uint64_t frames = 0; + auto lastProgress = std::chrono::steady_clock::now() - std::chrono::seconds(1); + while (!cancelled.load()) { + const auto count = decoder.read(interleaved.data(), chunkFrames); + if (!count) break; + for (size_t i = 0; i < count; ++i) { + const float left = interleaved[i * decoder.channels]; + const float right = decoder.channels == 2 ? interleaved[i * 2 + 1] : left; + if (!finiteSample(left) || !finiteSample(right) || std::abs(left) > 1e6f || std::abs(right) > 1e6f) + throw std::runtime_error("The reference contains invalid audio samples."); + mid[i] = left * 0.5f + right * 0.5f; + } + accumulator.push(mid.data(), count); + frames += count; + const auto now = std::chrono::steady_clock::now(); + if (progress && now - lastProgress >= std::chrono::milliseconds(100)) { + progress(decoder.frames ? std::min(0.999, static_cast(frames) / decoder.frames) : -1, accumulator.snapshot()); + lastProgress = now; + } + } + if (cancelled.load()) throw std::runtime_error("Reference analysis cancelled."); + if (frames == 0) throw std::runtime_error("This audio file is empty."); + // MP3 totals may include codec delay/padding, unlike decoded PCM counts. + if (decoder.kind != 3 && decoder.frames && frames < decoder.frames) + throw std::runtime_error("This audio file is incomplete or damaged."); + auto result = accumulator.snapshot(true); + if (result.meanSquare <= 1e-12) throw std::runtime_error("This track has no usable signal in the spectrum's Mid channel."); + return result; +} +} diff --git a/native/src/reference_napi.cpp b/native/src/reference_napi.cpp new file mode 100644 index 0000000..69ba73c --- /dev/null +++ b/native/src/reference_napi.cpp @@ -0,0 +1,82 @@ +#include "reference_napi.h" +#include "reference_analysis.h" +#include +#include +#include + +namespace { +std::atomic nextJob { 1 }; +std::mutex jobsMutex; +std::map>> jobs; + +Napi::Object toJS(Napi::Env env, const Visualizer::ReferenceCurve& curve) { + auto result = Napi::Object::New(env); + result.Set("sourceNyquistHz", curve.sourceNyquistHz); + result.Set("durationSeconds", curve.durationSeconds); + result.Set("meanSquare", curve.meanSquare); + auto curves = Napi::Object::New(env); + for (size_t i = 0; i < curve.powers.size(); ++i) { + auto array = Napi::Float32Array::New(env, curve.powers[i].size()); + std::memcpy(array.Data(), curve.powers[i].data(), curve.powers[i].size() * sizeof(float)); + curves.Set(std::to_string(Visualizer::referenceFFTSizes[i]), array); + } + result.Set("curves", curves); + return result; +} +struct Progress { double fraction; Visualizer::ReferenceCurve curve; }; +class ReferenceWorker : public Napi::AsyncWorker { +public: + ReferenceWorker(uint32_t id, std::string file, const Napi::Function& onProgress, const Napi::Function& done, size_t chunkSize) + : Napi::AsyncWorker(done), jobId(id), chunkFrames(chunkSize), path(std::move(file)), cancelled(std::make_shared>(false)), + progress(Napi::ThreadSafeFunction::New(done.Env(), onProgress, "Reference analysis", 1, 1)) { + std::lock_guard lock(jobsMutex); jobs[jobId] = cancelled; + } + ~ReferenceWorker() override { std::lock_guard lock(jobsMutex); jobs.erase(jobId); } + void Execute() override { + try { + result = Visualizer::analyzeReferenceFile(path, *cancelled, [this](double fraction, const Visualizer::ReferenceCurve& curve) { + auto* update = new Progress { fraction, curve }; + const auto status = progress.NonBlockingCall(update, [](Napi::Env env, Napi::Function callback, Progress* value) { + std::unique_ptr owned(value); + if (env && callback) callback.Call({Napi::Number::New(env, value->fraction), toJS(env, value->curve)}); + }); + if (status != napi_ok) delete update; + }, chunkFrames); + } catch (const std::exception& error) { SetError(error.what()); } + progress.Release(); + } + void OnOK() override { + if (cancelled->load()) { Callback().Call({Napi::String::New(Env(), "Reference analysis cancelled."), Env().Null()}); return; } + Callback().Call({Env().Null(), toJS(Env(), result)}); + } + void OnError(const Napi::Error& error) override { Callback().Call({error.Value(), Env().Null()}); } +private: + uint32_t jobId; + size_t chunkFrames; + std::string path; + std::shared_ptr> cancelled; + Napi::ThreadSafeFunction progress; + Visualizer::ReferenceCurve result; +}; +} +void initReferenceAnalysis(Napi::Env env, Napi::Object exports) { + auto api = Napi::Object::New(env); + api.Set("start", Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + if ((info.Length() < 3 || info.Length() > 4) || !info[0].IsString() || !info[1].IsFunction() || !info[2].IsFunction()) { + Napi::TypeError::New(info.Env(), "Expected a file path, progress callback, and completion callback.").ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + const uint32_t id = nextJob.fetch_add(1); + (new ReferenceWorker(id, info[0].As().Utf8Value(), info[1].As(), info[2].As(), info.Length() > 3 && info[3].IsNumber() ? info[3].As().Uint32Value() : 4096))->Queue(); + return Napi::Number::New(info.Env(), id); + })); + api.Set("cancel", Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() && info[0].IsNumber()) { + std::lock_guard lock(jobsMutex); + const auto found = jobs.find(info[0].As().Uint32Value()); + if (found != jobs.end()) found->second->store(true); + } + return info.Env().Undefined(); + })); + exports.Set("referenceAnalysis", api); +} diff --git a/native/src/reference_napi.h b/native/src/reference_napi.h new file mode 100644 index 0000000..277df23 --- /dev/null +++ b/native/src/reference_napi.h @@ -0,0 +1,3 @@ +#pragma once +#include +void initReferenceAnalysis(Napi::Env env, Napi::Object exports); diff --git a/native/src/spectrum.cpp b/native/src/spectrum.cpp index df7f000..198f04f 100644 --- a/native/src/spectrum.cpp +++ b/native/src/spectrum.cpp @@ -29,6 +29,7 @@ Spectrum::Spectrum(size_t fftSize) void Spectrum::setFFTSize(size_t size) { if (size != fftSize_) { + const float silenceDb = referenceEnabled_ ? -120.0f : -100.0f; fftSize_ = size; fft_ = std::make_unique(size); historyBuffer_.assign(size, 0.0f); @@ -36,20 +37,30 @@ void Spectrum::setFFTSize(size_t size) { windowedInput_.resize(size); midSpectrum_.resize(size); sideSpectrum_.resize(size); - rawMagnitudes_.assign(size / 2, -100.0f); + rawMagnitudes_.assign(size / 2, silenceDb); // Initialize to silence (-100.0f dB) - smoothedMagnitudes_.assign(size / 2, -100.0f); - sideRawMagnitudes_.assign(size / 2, -100.0f); - sideSmoothedMagnitudes_.assign(size / 2, -100.0f); - leftSmoothedMagnitudes_.assign(size / 2, -100.0f); - rightSmoothedMagnitudes_.assign(size / 2, -100.0f); - channelMaxMagnitudes_.assign(size / 2, -100.0f); + smoothedMagnitudes_.assign(size / 2, silenceDb); + sideRawMagnitudes_.assign(size / 2, silenceDb); + sideSmoothedMagnitudes_.assign(size / 2, silenceDb); + leftSmoothedMagnitudes_.assign(size / 2, silenceDb); + rightSmoothedMagnitudes_.assign(size / 2, silenceDb); + channelMaxMagnitudes_.assign(size / 2, silenceDb); bufferedSamples_ = 0; + referenceSmoothingPrimed_ = false; + referenceSignalSamples_ = 0; } } void Spectrum::setSampleRate(float sampleRate) { + if (sampleRate_ == sampleRate) return; sampleRate_ = sampleRate; + if (referenceEnabled_) reset(); +} + +void Spectrum::setReferenceEnabled(bool enabled) { + if (referenceEnabled_ == enabled) return; + referenceEnabled_ = enabled; + reset(); } void Spectrum::setSmoothing(float smoothing) { @@ -108,8 +119,14 @@ float Spectrum::magnitudeToDb(float magnitude, float correctionDb) const { } void Spectrum::updateSmoothedMagnitude(float db, float& smoothedMagnitude) { - if (bufferedSamples_ < fftSize_) { + if (bufferedSamples_ < fftSize_ || (referenceEnabled_ && !referenceSmoothingPrimed_)) { smoothedMagnitude = db; + } else if (referenceEnabled_) { + // The imported curve averages linear power. Averaging live dB instead + // heavily weights quiet frames and biases comparisons below zero. + const float power = smoothing_ * std::pow(10.0f, smoothedMagnitude / 10.0f) + + (1.0f - smoothing_) * std::pow(10.0f, db / 10.0f); + smoothedMagnitude = 10.0f * std::log10(std::max(1e-12f, power)); } else { smoothedMagnitude = smoothing_ * smoothedMagnitude + (1.0f - smoothing_) * db; } @@ -123,6 +140,26 @@ void Spectrum::updateMagnitudes() { return; } + if (referenceEnabled_) { + // Do not compare a zero-padded startup window with a recorded track. + // Prime from the first complete signal window, including after silence. + if (referenceSignalSamples_ < fftSize_) { + std::fill(rawMagnitudes_.begin(), rawMagnitudes_.end(), -120.0f); + std::fill(smoothedMagnitudes_.begin(), smoothedMagnitudes_.end(), -120.0f); + std::fill(sideSmoothedMagnitudes_.begin(), sideSmoothedMagnitudes_.end(), -120.0f); + std::fill(channelMaxMagnitudes_.begin(), channelMaxMagnitudes_.end(), -120.0f); + return; + } + double energy = 0; + for (size_t i = 0; i < fftSize_; ++i) + energy += static_cast(historyBuffer_[i]) * historyBuffer_[i] + + static_cast(sideHistoryBuffer_[i]) * sideHistoryBuffer_[i]; + if (energy / fftSize_ <= 1e-16) { + referenceSignalSamples_ = 0; + referenceSmoothingPrimed_ = false; + } + } + applyWindow(historyBuffer_.data(), windowedInput_.data(), fftSize_); fft_->forward(windowedInput_.data(), midSpectrum_.data()); applyWindow(sideHistoryBuffer_.data(), windowedInput_.data(), fftSize_); @@ -149,9 +186,14 @@ void Spectrum::updateMagnitudes() { updateSmoothedMagnitude(rightDb, rightSmoothedMagnitudes_[i]); channelMaxMagnitudes_[i] = std::max(leftSmoothedMagnitudes_[i], rightSmoothedMagnitudes_[i]); } + if (referenceEnabled_) referenceSmoothingPrimed_ = referenceSignalSamples_ >= fftSize_; } void Spectrum::pushSamples(const float* input, size_t length) { + if (referenceEnabled_ && !processingReference_ && input && length) { + pushStereoSamples(input, input, length); + return; + } if (input != nullptr && length > 0) { pushHistory(historyBuffer_, input, length); pushZeroHistory(sideHistoryBuffer_, length); @@ -163,7 +205,27 @@ void Spectrum::pushSamples(const float* input, size_t length) { } void Spectrum::pushStereoSamples(const float* left, const float* right, size_t length) { + if (referenceEnabled_ && !processingReference_ && left && length) { + if (!referenceLive_) referenceLive_ = std::make_unique(sampleRate_); + referenceLive_->push(left, right, length, [this](const float* l, const float* r, size_t n) { + processingReference_ = true; + const size_t hop = std::max(1, fftSize_ / 4); + while (n) { + const auto count = std::min(n, hop); + pushStereoSamples(l, r, count); + l += count; r += count; n -= count; + } + processingReference_ = false; + }); + return; + } if (left != nullptr && right != nullptr && length > 0 && fftSize_ > 0) { + if (referenceEnabled_) { + const bool hasSignal = referenceSignalSamples_ > 0 + || std::any_of(left, left + length, [](float value) { return std::abs(value) > 1e-8f; }) + || std::any_of(right, right + length, [](float value) { return std::abs(value) > 1e-8f; }); + if (hasSignal) referenceSignalSamples_ = std::min(fftSize_, referenceSignalSamples_ + length); + } if (length >= fftSize_) { const size_t start = length - fftSize_; for (size_t i = 0; i < fftSize_; i++) { @@ -195,19 +257,23 @@ const std::vector& Spectrum::process(const float* audioData, size_t lengt } float Spectrum::binToFrequency(int bin) const { - return bin * sampleRate_ / fftSize_; + return bin * (referenceEnabled_ ? referenceSampleRate : sampleRate_) / fftSize_; } void Spectrum::reset() { + referenceLive_.reset(); + referenceSmoothingPrimed_ = false; + referenceSignalSamples_ = 0; std::fill(historyBuffer_.begin(), historyBuffer_.end(), 0.0f); std::fill(sideHistoryBuffer_.begin(), sideHistoryBuffer_.end(), 0.0f); - std::fill(rawMagnitudes_.begin(), rawMagnitudes_.end(), -100.0f); - std::fill(smoothedMagnitudes_.begin(), smoothedMagnitudes_.end(), -100.0f); - std::fill(sideRawMagnitudes_.begin(), sideRawMagnitudes_.end(), -100.0f); - std::fill(sideSmoothedMagnitudes_.begin(), sideSmoothedMagnitudes_.end(), -100.0f); - std::fill(leftSmoothedMagnitudes_.begin(), leftSmoothedMagnitudes_.end(), -100.0f); - std::fill(rightSmoothedMagnitudes_.begin(), rightSmoothedMagnitudes_.end(), -100.0f); - std::fill(channelMaxMagnitudes_.begin(), channelMaxMagnitudes_.end(), -100.0f); + const float silenceDb = referenceEnabled_ ? -120.0f : -100.0f; + std::fill(rawMagnitudes_.begin(), rawMagnitudes_.end(), silenceDb); + std::fill(smoothedMagnitudes_.begin(), smoothedMagnitudes_.end(), silenceDb); + std::fill(sideRawMagnitudes_.begin(), sideRawMagnitudes_.end(), silenceDb); + std::fill(sideSmoothedMagnitudes_.begin(), sideSmoothedMagnitudes_.end(), silenceDb); + std::fill(leftSmoothedMagnitudes_.begin(), leftSmoothedMagnitudes_.end(), silenceDb); + std::fill(rightSmoothedMagnitudes_.begin(), rightSmoothedMagnitudes_.end(), silenceDb); + std::fill(channelMaxMagnitudes_.begin(), channelMaxMagnitudes_.end(), silenceDb); bufferedSamples_ = 0; } diff --git a/native/src/spectrum.h b/native/src/spectrum.h index ca1ffca..994a365 100644 --- a/native/src/spectrum.h +++ b/native/src/spectrum.h @@ -1,6 +1,7 @@ #pragma once #include "dsp_utils.h" +#include "reference_analysis.h" #include #include @@ -14,12 +15,19 @@ class Spectrum { void setFFTSize(size_t size); size_t getFFTSize() const { return fftSize_; } void setSampleRate(float sampleRate); + void setReferenceEnabled(bool enabled); + bool isReferenceEnabled() const { return referenceEnabled_; } + double getReferenceMeanSquare() const { return referenceLive_ ? referenceLive_->meanSquare() : 0; } + double getReferenceSeconds() const { return referenceLive_ ? referenceLive_->seconds() : 0; } void setSmoothing(float smoothing); // 0.0 - 1.0 // Feed new samples into the rolling history and update the latest magnitudes. void pushSamples(const float* input, size_t length); void pushStereoSamples(const float* left, const float* right, size_t length); + // Initial/reset magnitudes are placeholders until audio has been processed. + bool hasSpectrumData() const { return bufferedSamples_ > 0; } + // Read the latest raw clamped dB magnitudes without mutating analyzer state. const std::vector& getRawMagnitudes() const { return rawMagnitudes_; } @@ -39,6 +47,11 @@ class Spectrum { void reset(); private: + bool referenceEnabled_ = false; + bool processingReference_ = false; + bool referenceSmoothingPrimed_ = false; + size_t referenceSignalSamples_ = 0; + std::unique_ptr referenceLive_; size_t fftSize_; float sampleRate_; float smoothing_; diff --git a/native/src/system_audio_capture.h b/native/src/system_audio_capture.h index a1bd75c..dd9dff3 100644 --- a/native/src/system_audio_capture.h +++ b/native/src/system_audio_capture.h @@ -13,17 +13,29 @@ struct Support { std::string reason; }; +struct ChannelDescriptor { + uint32_t index = 0; + std::string label; +}; + +struct ChannelRouting { + uint32_t left = 0; + uint32_t right = 1; +}; + struct OutputDevice { std::string id; std::string label; double sampleRate = 48000.0; uint32_t channelCount = 2; bool isDefault = false; + std::vector channels; }; struct StartResult { double sampleRate = 48000.0; uint32_t channelCount = 2; + uint32_t sourceChannelCount = 2; std::string deviceId; std::string deviceLabel; }; @@ -34,6 +46,8 @@ struct AudioChunk { uint32_t channelCount = 2; double capturedAtMilliseconds = 0.0; uint64_t sequence = 0; + // Empty when the backend does not provide activity for every source channel. + std::vector sourceChannelPeaks; }; struct DrainResult { @@ -51,6 +65,9 @@ class SystemAudioCapture { virtual bool start(const std::string& requestedDeviceId, StartResult* result, std::string* errorMessage) = 0; + virtual ChannelRouting setChannelRouting(uint32_t left, uint32_t right) { + return {left, right}; + } virtual void stop() = 0; virtual DrainResult drain(size_t maxChunks = 64) = 0; virtual double nowMilliseconds() const = 0; diff --git a/native/src/system_audio_capture_napi.cpp b/native/src/system_audio_capture_napi.cpp index c3bcb95..4a7c538 100644 --- a/native/src/system_audio_capture_napi.cpp +++ b/native/src/system_audio_capture_napi.cpp @@ -39,6 +39,33 @@ Napi::Object supportToNapi(Napi::Env env, const Prism::Capture::Support& support return result; } +Napi::Array channelsToNapi( + Napi::Env env, + const std::vector& channels, + uint32_t fallbackCount) { + const uint32_t count = channels.empty() + ? fallbackCount + : static_cast(channels.size()); + Napi::Array result = Napi::Array::New(env, count); + for (uint32_t index = 0; index < count; ++index) { + Napi::Object channel = Napi::Object::New(env); + const std::string label = index < channels.size() && !channels[index].label.empty() + ? channels[index].label + : "Channel " + std::to_string(index + 1); + channel.Set("index", Napi::Number::New(env, index)); + channel.Set("label", Napi::String::New(env, label)); + result.Set(index, channel); + } + return result; +} + +Napi::Object routingToNapi(Napi::Env env, const Prism::Capture::ChannelRouting& routing) { + Napi::Object result = Napi::Object::New(env); + result.Set("left", Napi::Number::New(env, routing.left)); + result.Set("right", Napi::Number::New(env, routing.right)); + return result; +} + Napi::Object drainToNapi(Napi::Env env, Prism::Capture::DrainResult&& drained) { Napi::Array chunks = Napi::Array::New(env, drained.chunks.size()); for (size_t index = 0; index < drained.chunks.size(); ++index) { @@ -57,6 +84,11 @@ Napi::Object drainToNapi(Napi::Env env, Prism::Capture::DrainResult&& drained) { entry.Set("channelCount", Napi::Number::New(env, chunk.channelCount)); entry.Set("capturedAtMilliseconds", Napi::Number::New(env, chunk.capturedAtMilliseconds)); entry.Set("sequence", Napi::Number::New(env, static_cast(chunk.sequence))); + if (!chunk.sourceChannelPeaks.empty()) { + auto peaks = Napi::Float32Array::New(env, chunk.sourceChannelPeaks.size()); + std::memcpy(peaks.Data(), chunk.sourceChannelPeaks.data(), chunk.sourceChannelPeaks.size() * sizeof(float)); + entry.Set("sourceChannelPeaks", peaks); + } chunks.Set(static_cast(index), entry); } @@ -84,6 +116,8 @@ Napi::Value ListOutputDevices(const Napi::CallbackInfo& info) { entry.Set("isDefault", Napi::Boolean::New(env, device.isDefault)); entry.Set("sampleRate", Napi::Number::New(env, device.sampleRate)); entry.Set("channelCount", Napi::Number::New(env, device.channelCount)); + entry.Set("channels", channelsToNapi(env, device.channels, device.channelCount)); + entry.Set("channelRoutingAvailable", Napi::Boolean::New(env, true)); result.Set(static_cast(index), entry); } return result; @@ -96,6 +130,21 @@ Napi::Value Start(const Napi::CallbackInfo& info) { requestedDeviceId = info[0].As().Utf8Value(); } + // Stop before setting startup routing: the previous device's channel count + // must not clamp a route intended for the new device. + activeCapture()->stop(); + activeCapture()->setChannelRouting(0, 1); + + if (info.Length() >= 2 && info[1].IsObject()) { + const Napi::Object routing = info[1].As(); + if (routing.Has("left") && routing.Has("right") + && routing.Get("left").IsNumber() && routing.Get("right").IsNumber()) { + activeCapture()->setChannelRouting( + routing.Get("left").As().Uint32Value(), + routing.Get("right").As().Uint32Value()); + } + } + Prism::Capture::StartResult started; std::string errorMessage; if (!activeCapture()->start(requestedDeviceId, &started, &errorMessage)) { @@ -107,11 +156,26 @@ Napi::Value Start(const Napi::CallbackInfo& info) { Napi::Object result = Napi::Object::New(env); result.Set("sampleRate", Napi::Number::New(env, started.sampleRate)); result.Set("channelCount", Napi::Number::New(env, started.channelCount)); + result.Set("sourceChannelCount", Napi::Number::New(env, started.sourceChannelCount)); result.Set("deviceId", Napi::String::New(env, started.deviceId)); result.Set("deviceLabel", Napi::String::New(env, started.deviceLabel)); return result; } +Napi::Value SetChannelRouting(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) { + Napi::TypeError::New(env, "Expected left and right channel indices.") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + return routingToNapi( + env, + activeCapture()->setChannelRouting( + info[0].As().Uint32Value(), + info[1].As().Uint32Value())); +} + Napi::Value Stop(const Napi::CallbackInfo& info) { activeCapture()->stop(); return info.Env().Undefined(); @@ -140,6 +204,7 @@ void RegisterCaptureObject(Napi::Env env, Napi::Object exports, const char* expo capture.Set("stop", Napi::Function::New(env, Stop)); capture.Set("drain", Napi::Function::New(env, Drain)); capture.Set("nowMilliseconds", Napi::Function::New(env, NowMilliseconds)); + capture.Set("setChannelRouting", Napi::Function::New(env, SetChannelRouting)); exports.Set(exportName, capture); } diff --git a/native/src/waterfall.cpp b/native/src/waterfall.cpp new file mode 100644 index 0000000..ac68d04 --- /dev/null +++ b/native/src/waterfall.cpp @@ -0,0 +1,156 @@ +#include "waterfall.h" +#include +#include +#include + +namespace Visualizer { +namespace { +float finiteOr(float value, float fallback) { + // The addon is built with fast-math on some platforms; inspect IEEE bits so + // NaN/Infinity sanitization cannot be optimized away under those flags. + uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & 0x7f800000U) == 0x7f800000U ? fallback : value; +} +float mel(float hz) { + return hz < 1000 ? hz / (200.0f / 3) : 15 + std::log(hz / 1000) / (std::log(6.4f) / 27); +} +float inverseMel(float value) { + return value < 15 ? value * (200.0f / 3) : 1000 * std::exp((value - 15) * (std::log(6.4f) / 27)); +} +float frequencyAt(float x, const WaterfallConfig& config) { + const float high = std::min(config.maxFrequency, config.sampleRate * 0.5f); + const float low = std::min(config.minFrequency, high); + if (config.scaleMode == "linear") return low + x * (high - low); + if (config.scaleMode == "mel") return inverseMel(mel(low) + x * (mel(high) - mel(low))); + return low * std::pow(high / low, x); +} +} + +WaterfallAnalyzer::WaterfallAnalyzer() { configure(config_); } + +void WaterfallAnalyzer::resizeHistory(size_t capacity) { + const size_t bins = config_.fftSize / 2; + std::vector values(capacity * bins, -100); + std::vector times(capacity); + const size_t keep = std::min(count_, capacity); + for (size_t i = 0; i < keep; ++i) { + const size_t source = (write_ + capacity_ - keep + i) % capacity_; + std::copy_n(history_.data() + source * bins, bins, values.data() + i * bins); + times[i] = times_[source]; + } + history_.swap(values); + times_.swap(times); + capacity_ = capacity; + count_ = keep; + write_ = keep % capacity; +} + +void WaterfallAnalyzer::configure(const WaterfallConfig& value) { + WaterfallConfig next = value; + next.sampleRate = std::clamp(finiteOr(next.sampleRate, 48000), 8000.0f, 384000.0f); + if (next.fftSize < 1024 || next.fftSize > 16384 || (next.fftSize & (next.fftSize - 1))) next.fftSize = 2048; + next.historySeconds = std::clamp(finiteOr(next.historySeconds, 5), 1.0f, 30.0f); + next.smoothing = std::clamp(finiteOr(next.smoothing, 0.9f), 0.0f, 0.99f); + next.tiltDbPerOctave = std::clamp(finiteOr(next.tiltDbPerOctave, 2), -2.0f, 8.0f); + next.minFrequency = std::clamp(finiteOr(next.minFrequency, 10), 1.0f, next.sampleRate * 0.5f); + next.maxFrequency = std::clamp(finiteOr(next.maxFrequency, 24000), next.minFrequency, next.sampleRate * 0.5f); + if (next.scaleMode != "mel" && next.scaleMode != "linear") next.scaleMode = "log"; + const bool restart = next.sampleRate != config_.sampleRate || next.fftSize != config_.fftSize; + if (restart) reset(); + config_ = next; + spectrum_.setFFTSize(config_.fftSize); + spectrum_.setSampleRate(config_.sampleRate); + spectrum_.setSmoothing(config_.smoothing); + const size_t capacity = static_cast(std::ceil(config_.historySeconds * 60)) + 2; + if (restart || capacity != capacity_) resizeHistory(capacity); + pendingLeft_.reserve(static_cast(std::ceil(config_.sampleRate / 60))); + pendingRight_.reserve(pendingLeft_.capacity()); +} + +void WaterfallAnalyzer::append() { + const auto& levels = spectrum_.getChannelMaxMagnitudes(); + std::copy(levels.begin(), levels.end(), history_.begin() + write_ * levels.size()); + times_[write_] = static_cast(samples_) / config_.sampleRate; + write_ = (write_ + 1) % capacity_; + count_ = std::min(count_ + 1, capacity_); +} + +void WaterfallAnalyzer::processStereo(const float* left, const float* right, size_t length) { + if (!left || !right) return; + size_t offset = 0; + while (offset < length) { + const uint64_t boundary = static_cast(std::ceil(tick_ * static_cast(config_.sampleRate) / 60)); + const size_t take = std::min(length - offset, static_cast(boundary - samples_)); + for (size_t i = 0; i < take; ++i) { + pendingLeft_.push_back(finiteOr(left[offset + i], 0)); + pendingRight_.push_back(finiteOr(right[offset + i], 0)); + } + offset += take; + samples_ += take; + if (samples_ == boundary) { + spectrum_.pushStereoSamples(pendingLeft_.data(), pendingRight_.data(), pendingLeft_.size()); + pendingLeft_.clear(); + pendingRight_.clear(); + if (samples_ >= config_.fftSize) append(); + ++tick_; + } + } +} + +WaterfallFrame WaterfallAnalyzer::getFrame(size_t ridges, size_t columns) const { + WaterfallFrame frame; + frame.columns = std::clamp(columns, size_t{2}, size_t{512}); + frame.audioSeconds = static_cast(samples_) / config_.sampleRate; + ridges = std::clamp(ridges, size_t{2}, size_t{64}); + frame.frequencies.resize(frame.columns); + const size_t bins = config_.fftSize / 2; + std::vector positions(frame.columns), tilts(frame.columns); + std::vector starts(frame.columns), ends(frame.columns); + for (size_t x = 0; x < frame.columns; ++x) { + const float t = static_cast(x) / (frame.columns - 1); + const float half = 0.5f / (frame.columns - 1); + const float hz = frequencyAt(t, config_); + frame.frequencies[x] = hz; + positions[x] = std::clamp(hz * config_.fftSize / config_.sampleRate, 0.0f, static_cast(bins - 1)); + starts[x] = std::min(bins - 1, static_cast(std::ceil(frequencyAt(std::max(0.0f, t - half), config_) * config_.fftSize / config_.sampleRate))); + ends[x] = std::min(bins - 1, static_cast(std::floor(frequencyAt(std::min(1.0f, t + half), config_) * config_.fftSize / config_.sampleRate))); + tilts[x] = config_.tiltDbPerOctave * std::log2(hz / 1000); + } + // Anchor historical ridges to the audio timeline so they move without morphing. + const double spacing = config_.historySeconds / static_cast(ridges - 1); + const double anchor = std::floor(frame.audioSeconds / spacing) * spacing; + size_t cursor = 0; + for (size_t ridge = 0; ridge < ridges; ++ridge) { + const double target = ridge == 0 ? frame.audioSeconds : anchor - (ridge - 1) * spacing; + if (ridge == 1 && frame.audioSeconds - anchor < 1.0 / 60) continue; + while (cursor < count_) { + const size_t index = (write_ + capacity_ - 1 - cursor) % capacity_; + if (times_[index] <= target + 1e-8) break; + ++cursor; + } + if (cursor >= count_) break; + const size_t index = (write_ + capacity_ - 1 - cursor) % capacity_; + const float age = static_cast(frame.audioSeconds - times_[index]); + if (age > config_.historySeconds) break; + frame.ages.push_back(age); + const float* values = history_.data() + index * bins; + for (size_t x = 0; x < frame.columns; ++x) { + const size_t lo = static_cast(positions[x]); + const size_t hi = std::min(lo + 1, bins - 1); + float db = values[lo] + (values[hi] - values[lo]) * (positions[x] - lo); + for (size_t bin = starts[x]; bin <= ends[x]; ++bin) db = std::max(db, values[bin]); + frame.levels.push_back(db + tilts[x]); + } + } + return frame; +} + +void WaterfallAnalyzer::reset() { + spectrum_.reset(); + pendingLeft_.clear(); + pendingRight_.clear(); + count_ = write_ = samples_ = 0; + tick_ = 1; +} +} // namespace Visualizer diff --git a/native/src/waterfall.h b/native/src/waterfall.h new file mode 100644 index 0000000..258dae8 --- /dev/null +++ b/native/src/waterfall.h @@ -0,0 +1,52 @@ +#pragma once + +#include "spectrum.h" +#include +#include +#include + +namespace Visualizer { + +struct WaterfallConfig { + float sampleRate = 48000; + size_t fftSize = 2048; + float historySeconds = 5; + float smoothing = 0.9f; + float tiltDbPerOctave = 2; + float minFrequency = 10; + float maxFrequency = 24000; + std::string scaleMode = "log"; +}; + +// Newest ridge first. Levels retain dB units; projection and painting are separate. +struct WaterfallFrame { + std::vector levels; + std::vector ages; + std::vector frequencies; + size_t columns = 0; + double audioSeconds = 0; +}; + +class WaterfallAnalyzer { +public: + WaterfallAnalyzer(); + void configure(const WaterfallConfig& config); + void processStereo(const float* left, const float* right, size_t length); + WaterfallFrame getFrame(size_t ridges, size_t columns) const; + void reset(); + size_t storedSlices() const { return count_; } + size_t capacitySlices() const { return capacity_; } + +private: + WaterfallConfig config_; + Spectrum spectrum_{2048}; + std::vector history_; + std::vector times_; + std::vector pendingLeft_, pendingRight_; + size_t capacity_ = 0, count_ = 0, write_ = 0; + uint64_t samples_ = 0, tick_ = 1; + void resizeHistory(size_t capacity); + void append(); +}; + +} // namespace Visualizer diff --git a/native/src/waterfall_napi.cpp b/native/src/waterfall_napi.cpp new file mode 100644 index 0000000..30f6609 --- /dev/null +++ b/native/src/waterfall_napi.cpp @@ -0,0 +1,76 @@ +#include "waterfall_napi.h" +#include "waterfall.h" +#include +#include + +namespace { +Visualizer::WaterfallAnalyzer analyzer; +Napi::Value configure(const Napi::CallbackInfo& info) { + const auto env = info.Env(); + if (info.Length() < 1 || !info[0].IsObject()) { + Napi::TypeError::New(env, "Expected waterfall configuration").ThrowAsJavaScriptException(); + return env.Undefined(); + } + const auto options = info[0].As(); + Visualizer::WaterfallConfig config; + const auto number = [&](const char* name, float fallback) { + auto value = options.Get(name); + return value.IsNumber() ? value.As().FloatValue() : fallback; + }; + config.sampleRate = number("sampleRate", config.sampleRate); + auto fft = options.Get("fftSize"); + if (fft.IsNumber()) config.fftSize = fft.As().Uint32Value(); + config.historySeconds = number("historySeconds", config.historySeconds); + config.smoothing = number("smoothing", config.smoothing); + config.tiltDbPerOctave = number("tiltDbPerOctave", config.tiltDbPerOctave); + config.minFrequency = number("minFrequency", config.minFrequency); + config.maxFrequency = number("maxFrequency", config.maxFrequency); + auto scale = options.Get("scaleMode"); + if (scale.IsString()) config.scaleMode = scale.As().Utf8Value(); + analyzer.configure(config); + return env.Undefined(); +} +Napi::Value processStereo(const Napi::CallbackInfo& info) { + const auto env = info.Env(); + if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray() + || info[0].As().TypedArrayType() != napi_float32_array + || info[1].As().TypedArrayType() != napi_float32_array) { + Napi::TypeError::New(env, "Expected left and right Float32Array audio").ThrowAsJavaScriptException(); + return env.Undefined(); + } + const auto left = info[0].As(); + const auto right = info[1].As(); + analyzer.processStereo(left.Data(), right.Data(), std::min(left.ElementLength(), right.ElementLength())); + return env.Undefined(); +} +Napi::Value getFrame(const Napi::CallbackInfo& info) { + const auto env = info.Env(); + const size_t ridges = info.Length() > 0 && info[0].IsNumber() ? info[0].As().Uint32Value() : 32; + const size_t columns = info.Length() > 1 && info[1].IsNumber() ? info[1].As().Uint32Value() : 256; + const auto frame = analyzer.getFrame(ridges, columns); + auto output = Napi::Object::New(env); + const auto floats = [&](const char* name, const std::vector& values) { + auto array = Napi::Float32Array::New(env, values.size()); + if (!values.empty()) std::memcpy(array.Data(), values.data(), values.size() * sizeof(float)); + output.Set(name, array); + }; + floats("levels", frame.levels); + floats("ages", frame.ages); + floats("frequencies", frame.frequencies); + output.Set("columns", Napi::Number::New(env, frame.columns)); + output.Set("audioSeconds", Napi::Number::New(env, frame.audioSeconds)); + return output; +} +} + +void RegisterWaterfall(Napi::Env env, Napi::Object exports) { + auto api = Napi::Object::New(env); + api.Set("configure", Napi::Function::New(env, configure)); + api.Set("processStereo", Napi::Function::New(env, processStereo)); + api.Set("getFrame", Napi::Function::New(env, getFrame)); + api.Set("reset", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + analyzer.reset(); + return info.Env().Undefined(); + })); + exports.Set("waterfall", api); +} diff --git a/native/src/waterfall_napi.h b/native/src/waterfall_napi.h new file mode 100644 index 0000000..f1dda9c --- /dev/null +++ b/native/src/waterfall_napi.h @@ -0,0 +1,3 @@ +#pragma once +#include +void RegisterWaterfall(Napi::Env env, Napi::Object exports); diff --git a/native/src/window_docking.cpp b/native/src/window_docking.cpp new file mode 100644 index 0000000..afa8e53 --- /dev/null +++ b/native/src/window_docking.cpp @@ -0,0 +1,200 @@ +#include "window_docking.h" +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr UINT_PTR kSubclass = 0x5052444b; +struct Registration { RECT rectangle{}; bool positioned = false; }; +std::unordered_map registrations; +UINT CallbackMessage() { static UINT id = RegisterWindowMessageW(L"Prism.Docking.AppBar.v1"); return id; } + +HWND ReadHandle(const Napi::CallbackInfo& info) { + if (info.Length() < 1 || !info[0].IsBuffer()) return nullptr; + auto bytes = info[0].As>(); + if (bytes.Length() != sizeof(HWND)) return nullptr; + HWND hwnd = nullptr; + std::memcpy(&hwnd, bytes.Data(), sizeof(hwnd)); + return IsWindow(hwnd) ? hwnd : nullptr; +} + +void Remove(HWND hwnd) { + if (registrations.erase(hwnd) == 0) return; + APPBARDATA data{}; + data.cbSize = sizeof(data); + data.hWnd = hwnd; + SHAppBarMessage(ABM_REMOVE, &data); +} + +LRESULT CALLBACK DockSubclass(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR, DWORD_PTR) { + if (message == WM_NCDESTROY) { + Remove(hwnd); + RemoveWindowSubclass(hwnd, DockSubclass, kSubclass); + } else if (registrations.count(hwnd)) { + // Block native resize/move without changing Electron's size constraints. + // setResizable(false) also freezes programmatic restore/resize bounds. + if (message == WM_SYSCOMMAND) { + const WPARAM command = wParam & 0xfff0; + if (command == SC_SIZE || command == SC_MOVE || command == SC_MAXIMIZE) return 0; + } + if (message == WM_NCHITTEST) { + const LRESULT hit = DefSubclassProc(hwnd, message, wParam, lParam); + return (hit >= HTLEFT && hit <= HTBOTTOMRIGHT) || hit == HTCAPTION ? HTCLIENT : hit; + } + APPBARDATA data{}; + data.cbSize = sizeof(data); + data.hWnd = hwnd; + if (message == WM_ACTIVATE) SHAppBarMessage(ABM_ACTIVATE, &data); + if (message == WM_WINDOWPOSCHANGED) SHAppBarMessage(ABM_WINDOWPOSCHANGED, &data); + } + return DefSubclassProc(hwnd, message, wParam, lParam); +} + +Napi::Value Register(const Napi::CallbackInfo& info) { + HWND hwnd = ReadHandle(info); + if (!hwnd) return Napi::Boolean::New(info.Env(), false); + if (registrations.count(hwnd)) return Napi::Boolean::New(info.Env(), true); + if (!SetWindowSubclass(hwnd, DockSubclass, kSubclass, 0)) return Napi::Boolean::New(info.Env(), false); + APPBARDATA data{}; + data.cbSize = sizeof(data); + data.hWnd = hwnd; + data.uCallbackMessage = CallbackMessage(); + if (!data.uCallbackMessage || !SHAppBarMessage(ABM_NEW, &data)) { + RemoveWindowSubclass(hwnd, DockSubclass, kSubclass); + return Napi::Boolean::New(info.Env(), false); + } + registrations.emplace(hwnd, Registration{}); + return Napi::Boolean::New(info.Env(), true); +} + +Napi::Value Unregister(const Napi::CallbackInfo& info) { + HWND hwnd = ReadHandle(info); + if (hwnd) { + Remove(hwnd); + RemoveWindowSubclass(hwnd, DockSubclass, kSubclass); + } + return info.Env().Undefined(); +} + +bool ReadCoordinate(Napi::Object object, const char* key, LONG& value) { + auto raw = object.Get(key); + if (!raw.IsNumber()) return false; + double number = raw.As().DoubleValue(); + if (!std::isfinite(number) || number < -1000000 || number > 1000000) return false; + value = static_cast(std::round(number)); + return true; +} + +Napi::Value Position(const Napi::CallbackInfo& info) { + auto env = info.Env(); + HWND hwnd = ReadHandle(info); + if (!hwnd || !registrations.count(hwnd) || info.Length() != 4 || !info[1].IsString() + || !info[2].IsObject() || !info[3].IsNumber()) return env.Null(); + const auto edge = info[1].As().Utf8Value(); + if (edge != "top" && edge != "bottom") return env.Null(); + auto monitor = info[2].As(); + LONG x, y, width, height; + if (!ReadCoordinate(monitor, "x", x) || !ReadCoordinate(monitor, "y", y) + || !ReadCoordinate(monitor, "width", width) || !ReadCoordinate(monitor, "height", height) + || width <= 0 || height <= 0) return env.Null(); + const double requested = info[3].As().DoubleValue(); + if (!std::isfinite(requested) || requested <= 0) return env.Null(); + const LONG thickness = static_cast(std::min(requested, static_cast(height))); + APPBARDATA data{}; + data.cbSize = sizeof(data); + data.hWnd = hwnd; + data.uEdge = edge == "top" ? ABE_TOP : ABE_BOTTOM; + data.rc = { x, y, x + width, y + height }; + SHAppBarMessage(ABM_QUERYPOS, &data); + const LONG available = data.rc.bottom - data.rc.top; + if (available <= 0 || data.rc.right <= data.rc.left) return env.Null(); + const LONG actual = std::min(thickness, available); + if (data.uEdge == ABE_TOP) data.rc.bottom = data.rc.top + actual; + else data.rc.top = data.rc.bottom - actual; + // Do not re-submit an unchanged reservation: ABM_SETPOS itself broadcasts + // ABN_POSCHANGED, so doing so would create a shell notification loop. + auto& registration = registrations.at(hwnd); + if (!registration.positioned || !EqualRect(®istration.rectangle, &data.rc)) { + if (!SHAppBarMessage(ABM_SETPOS, &data)) return env.Null(); + registration.rectangle = data.rc; + registration.positioned = true; + } + // BrowserWindow.setBounds applies the negotiated physical rectangle after + // converting it to Electron's display-independent coordinates. + auto result = Napi::Object::New(env); + result.Set("x", data.rc.left); + result.Set("y", data.rc.top); + result.Set("width", data.rc.right - data.rc.left); + result.Set("height", data.rc.bottom - data.rc.top); + return result; +} +} +#endif + +void RegisterWindowDocking(Napi::Env env, Napi::Object exports) { +#ifdef _WIN32 + auto api = Napi::Object::New(env); + api.Set("register", Napi::Function::New(env, Register)); + api.Set("remove", Napi::Function::New(env, Unregister)); + api.Set("lower", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + HWND hwnd = ReadHandle(info); + if (hwnd) SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + return info.Env().Undefined(); + })); + api.Set("position", Napi::Function::New(env, Position)); + api.Set("fullscreen", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + HWND hwnd = ReadHandle(info); + HWND foreground = GetForegroundWindow(); + bool fullscreen = false; + QUERY_USER_NOTIFICATION_STATE notificationState; + if (hwnd && SUCCEEDED(SHQueryUserNotificationState(¬ificationState)) + && notificationState == QUNS_RUNNING_D3D_FULL_SCREEN) fullscreen = true; + // Borderless fullscreen does not consistently produce ABN_FULLSCREENAPP. + // Ignore ordinary maximized windows, our owned panels and the desktop. + if (!fullscreen && hwnd && foreground && foreground != hwnd && GetAncestor(foreground, GA_ROOTOWNER) != hwnd + && foreground != GetShellWindow() && foreground != GetDesktopWindow() + && IsWindowVisible(foreground) && !IsIconic(foreground) && !IsZoomed(foreground) + && !(GetWindowLongPtrW(foreground, GWL_STYLE) & WS_CAPTION)) { + MONITORINFO monitor{}; + monitor.cbSize = sizeof(monitor); + RECT rectangle{}; + if (GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &monitor) + && GetWindowRect(foreground, &rectangle)) { + fullscreen = rectangle.left <= monitor.rcMonitor.left && rectangle.top <= monitor.rcMonitor.top + && rectangle.right >= monitor.rcMonitor.right && rectangle.bottom >= monitor.rcMonitor.bottom; + } + } + return Napi::Boolean::New(info.Env(), fullscreen); + })); + api.Set("workArea", Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() != 1 || !info[0].IsObject()) return info.Env().Null(); + auto monitor = info[0].As(); + LONG x, y, width, height; + if (!ReadCoordinate(monitor, "x", x) || !ReadCoordinate(monitor, "y", y) + || !ReadCoordinate(monitor, "width", width) || !ReadCoordinate(monitor, "height", height) + || width <= 0 || height <= 0) return info.Env().Null(); + RECT rectangle{ x, y, x + width, y + height }; + MONITORINFO details{}; + details.cbSize = sizeof(details); + if (!GetMonitorInfoW(MonitorFromRect(&rectangle, MONITOR_DEFAULTTONULL), &details)) return info.Env().Null(); + auto result = Napi::Object::New(info.Env()); + result.Set("x", details.rcWork.left); + result.Set("y", details.rcWork.top); + result.Set("width", details.rcWork.right - details.rcWork.left); + result.Set("height", details.rcWork.bottom - details.rcWork.top); + return result; + })); + api.Set("messages", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + auto result = Napi::Object::New(info.Env()); + result.Set("callback", CallbackMessage()); + result.Set("taskbarCreated", RegisterWindowMessageW(L"TaskbarCreated")); + return result; + })); + exports.Set("windowDocking", api); +#endif +} diff --git a/native/src/window_docking.h b/native/src/window_docking.h new file mode 100644 index 0000000..482c62b --- /dev/null +++ b/native/src/window_docking.h @@ -0,0 +1,3 @@ +#pragma once +#include +void RegisterWindowDocking(Napi::Env env, Napi::Object exports); diff --git a/native/src/windows_capture.cpp b/native/src/windows_capture.cpp index e30cd34..4573bd3 100644 --- a/native/src/windows_capture.cpp +++ b/native/src/windows_capture.cpp @@ -2,13 +2,19 @@ #include "windows_capture.h" #endif #include "system_audio_capture.h" +#include "device_input_capture_adapter.h" +#include "capture_channel_selection.h" #if defined(_WIN32) -#include +#if defined(__MINGW32__) +// MinGW's import libraries do not supply the audio/property-key GUID objects. +#include +#endif +#include #include -#include -#include +#include +#include #include #include #include @@ -34,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -53,31 +60,24 @@ using winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionP constexpr size_t kMaxQueuedChunks = 256; constexpr size_t kDefaultDrainChunkLimit = 64; -struct OutputDeviceInfo { +struct DeviceInfo { std::string id; std::string label; double sampleRate; UINT32 channelCount; bool isDefault; + std::vector channels; }; -struct CapturedChunk { - std::vector left; - std::vector right; - UINT32 channelCount = 2; - double capturedAtMilliseconds = 0.0; - uint64_t sequence = 0; -}; +using CapturedChunk = Prism::Capture::AudioChunk; struct AudioFormatInfo { bool valid = false; - bool isFloat = false; + Prism::Capture::PCMFormat pcm; + DWORD channelMask = 0; WORD channels = 0; DWORD sampleRate = 48000; - WORD bitsPerSample = 0; - WORD validBitsPerSample = 0; WORD bytesPerFrame = 0; - WORD bytesPerSample = 0; }; double monotonicMilliseconds() { @@ -226,24 +226,38 @@ class ScopedRoInit { bool usable_; }; -bool isSpotifySession(const GlobalSystemMediaTransportControlsSession& session) { +template +auto awaitMediaOperation(const Operation& operation) { + if (operation.wait_for(std::chrono::seconds(5)) == winrt::Windows::Foundation::AsyncStatus::Started) { + operation.Cancel(); + throw std::runtime_error("Windows media request timed out. Retry after checking the player."); + } + return operation.GetResults(); +} + +bool isProviderSession(const GlobalSystemMediaTransportControlsSession& session, const std::string& provider) { if (!session) { return false; } const std::string sourceId = toLowerAscii(winrt::to_string(session.SourceAppUserModelId())); - return sourceId.find("spotify") != std::string::npos; + if (provider == "spotify") return sourceId.find("spotify") != std::string::npos; + if (provider != "tidal") return false; + return sourceId == "com.squirrel.tidal.tidal" || sourceId == "tidal" || + sourceId == "tidal.exe" || sourceId == "com.tidal.desktop" || + (sourceId.rfind("tidalmusicas.tidal_", 0) == 0 && + sourceId.size() > 6 && sourceId.compare(sourceId.size() - 6, 6, "!tidal") == 0); } -std::optional findSpotifySession( - const GlobalSystemMediaTransportControlsSessionManager& manager) { +std::optional findProviderSession( + const GlobalSystemMediaTransportControlsSessionManager& manager, const std::string& provider) { const auto currentSession = manager.GetCurrentSession(); - if (isSpotifySession(currentSession)) { + if (isProviderSession(currentSession, provider)) { return currentSession; } for (const auto& session : manager.GetSessions()) { - if (isSpotifySession(session)) { + if (isProviderSession(session, provider)) { return session; } } @@ -284,30 +298,35 @@ std::string base64Encode(const std::vector& data) { // Thumbnail is fetched on a background thread to avoid blocking the NAPI call // thread with cross-process WinRT async I/O. std::mutex s_thumbMutex; -std::string s_thumbTrackKey; -std::string s_thumbDataUrl; -bool s_thumbFetching = false; +struct ThumbnailCache { + std::string trackKey; + std::string dataUrl; + bool fetching = false; + std::chrono::steady_clock::time_point retryAfter{}; +}; +// One slot per provider; Spotify and TIDAL must not evict each other's artwork. +std::map s_thumbnailCaches; void launchThumbnailFetch( - const std::string& trackKey, + const std::string& provider, const std::string& trackKey, winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionMediaProperties props) { const auto thumbnailRef = props.Thumbnail(); if (!thumbnailRef) { std::lock_guard lock(s_thumbMutex); - s_thumbFetching = false; + s_thumbnailCaches[provider].fetching = false; return; } - std::thread([trackKey, thumbnailRef]() { + std::thread([provider, trackKey, thumbnailRef]() { std::string result; try { using winrt::Windows::Storage::Streams::DataReader; - const HRESULT hr = RoInitialize(RO_INIT_MULTITHREADED); - if (SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE) { - const auto stream = thumbnailRef.OpenReadAsync().get(); + ScopedRoInit init; + if (init.usable()) { + const auto stream = awaitMediaOperation(thumbnailRef.OpenReadAsync()); const uint64_t size = stream.Size(); if (size > 0 && size <= 4u * 1024u * 1024u) { const auto reader = DataReader(stream); - const uint32_t loaded = reader.LoadAsync(static_cast(size)).get(); + const uint32_t loaded = awaitMediaOperation(reader.LoadAsync(static_cast(size))); if (loaded > 0) { std::vector bytes(loaded); reader.ReadBytes(bytes); @@ -318,39 +337,40 @@ void launchThumbnailFetch( result = "data:" + mimeType + ";base64," + base64Encode(bytes); } } - if (SUCCEEDED(hr)) { - RoUninitialize(); - } } } catch (...) {} std::lock_guard lock(s_thumbMutex); - if (s_thumbTrackKey == trackKey) { - s_thumbDataUrl = std::move(result); + auto& cache = s_thumbnailCaches[provider]; + if (cache.trackKey == trackKey) { + cache.dataUrl = std::move(result); + cache.retryAfter = std::chrono::steady_clock::now() + std::chrono::seconds(30); } - s_thumbFetching = false; + cache.fetching = false; }).detach(); } std::string getOrFetchThumbnail( - const std::string& trackKey, + const std::string& provider, const std::string& trackKey, winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionMediaProperties props) { bool shouldFetch = false; std::string result; { std::lock_guard lock(s_thumbMutex); - if (s_thumbTrackKey == trackKey) { - result = s_thumbDataUrl; - } else { - s_thumbTrackKey = trackKey; - s_thumbDataUrl.clear(); - if (!s_thumbFetching) { - s_thumbFetching = true; - shouldFetch = true; - } + auto& cache = s_thumbnailCaches[provider]; + if (cache.trackKey != trackKey) { + cache.trackKey = trackKey; + cache.dataUrl.clear(); + cache.retryAfter = {}; + } + result = cache.dataUrl; + if (result.empty() && !cache.fetching && std::chrono::steady_clock::now() >= cache.retryAfter) { + cache.fetching = true; + cache.retryAfter = std::chrono::steady_clock::now() + std::chrono::seconds(30); + shouldFetch = true; } } if (shouldFetch) { - launchThumbnailFetch(trackKey, std::move(props)); + launchThumbnailFetch(provider, trackKey, std::move(props)); } return result; } @@ -400,97 +420,59 @@ std::string getDeviceFriendlyName(IMMDevice* device) { } AudioFormatInfo getFormatInfo(const WAVEFORMATEX* format) { + using namespace Prism::Capture; AudioFormatInfo info; - if (format == nullptr || format->nChannels == 0 || format->nBlockAlign == 0) { - return info; - } - - info.valid = true; + if (format == nullptr || format->nChannels == 0 || format->nSamplesPerSec == 0) return info; info.channels = format->nChannels; info.sampleRate = format->nSamplesPerSec; - info.bitsPerSample = format->wBitsPerSample; - info.validBitsPerSample = format->wBitsPerSample; info.bytesPerFrame = format->nBlockAlign; - info.bytesPerSample = static_cast(format->nBlockAlign / format->nChannels); - info.isFloat = format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT; - - if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE && - format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { + info.pcm.bitsPerChannel = format->wBitsPerSample; + if (format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { + info.pcm.encoding = SampleEncoding::Float; + } else if (format->wFormatTag == WAVE_FORMAT_PCM) { + info.pcm.encoding = format->wBitsPerSample == 8 + ? SampleEncoding::UnsignedInteger : SampleEncoding::SignedInteger; + } else if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE + && format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { const auto* extensible = reinterpret_cast(format); - info.isFloat = IsEqualGUID(extensible->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT); - if (extensible->Samples.wValidBitsPerSample != 0) { - info.validBitsPerSample = extensible->Samples.wValidBitsPerSample; + info.channelMask = extensible->dwChannelMask; + info.pcm.validBitsPerChannel = extensible->Samples.wValidBitsPerSample; + info.pcm.highAligned = true; + if (IsEqualGUID(extensible->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) { + info.pcm.encoding = SampleEncoding::Float; + } else if (IsEqualGUID(extensible->SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) { + info.pcm.encoding = format->wBitsPerSample == 8 + ? SampleEncoding::UnsignedInteger : SampleEncoding::SignedInteger; } } - + info.valid = isSupportedPCMFormat(info.pcm) + && info.bytesPerFrame == info.channels * (info.pcm.bitsPerChannel / 8); return info; } -float decodeSignedIntegerSample(const BYTE* data, WORD bytesPerSample, WORD validBitsPerSample) { - if (data == nullptr || bytesPerSample == 0) { - return 0.0f; - } - - const WORD totalBits = static_cast(bytesPerSample * 8); - const WORD validBits = static_cast( - std::max(1, std::min(validBitsPerSample == 0 ? totalBits : validBitsPerSample, 32))); - - uint32_t rawBits = 0; - for (WORD byteIndex = 0; byteIndex < bytesPerSample && byteIndex < 4; ++byteIndex) { - rawBits |= static_cast(data[byteIndex]) << (byteIndex * 8); - } - - const uint32_t validMask = - validBits >= 32 ? std::numeric_limits::max() : ((1u << validBits) - 1u); - rawBits &= validMask; - - int32_t signedValue = 0; - if (validBits == 32) { - signedValue = static_cast(rawBits); - } else { - const uint32_t signBit = 1u << (validBits - 1); - if ((rawBits & signBit) != 0) { - rawBits |= ~validMask; - } - signedValue = static_cast(rawBits); - } - - const double maxMagnitude = validBits == 32 - ? static_cast(std::numeric_limits::max()) - : static_cast((1ULL << (validBits - 1)) - 1ULL); - if (maxMagnitude <= 0.0) { - return 0.0f; - } - - return static_cast(static_cast(signedValue) / maxMagnitude); -} - -float readFrameSample(const BYTE* frameData, UINT32 channelIndex, const AudioFormatInfo& format) { - if (frameData == nullptr || !format.valid || format.bytesPerSample == 0 || - channelIndex >= format.channels) { - return 0.0f; +std::vector channelDescriptors(const AudioFormatInfo& format) { + // WAVEFORMATEXTENSIBLE interleaves speakers in ascending mask-bit order. + static constexpr const char* speakerLabels[] = { + "Front Left", "Front Right", "Front Center", "LFE", "Back Left", "Back Right", + "Front Left of Center", "Front Right of Center", "Back Center", "Side Left", "Side Right", + "Top Center", "Top Front Left", "Top Front Center", "Top Front Right", + "Top Back Left", "Top Back Center", "Top Back Right", + }; + std::vector result; + for (uint32_t bit = 0; bit < 32 && result.size() < format.channels; ++bit) { + if ((format.channelMask & (DWORD{1} << bit)) == 0) continue; + const auto index = static_cast(result.size()); + result.push_back({index, bit < sizeof(speakerLabels) / sizeof(speakerLabels[0]) + ? speakerLabels[bit] : "Channel " + std::to_string(index + 1)}); + } + while (result.size() < format.channels) { + const auto index = static_cast(result.size()); + result.push_back({index, "Channel " + std::to_string(index + 1)}); } - - const BYTE* samplePtr = frameData + static_cast(channelIndex) * format.bytesPerSample; - if (format.isFloat) { - if (format.bitsPerSample == 32 && format.bytesPerSample >= sizeof(float)) { - float value = 0.0f; - std::memcpy(&value, samplePtr, sizeof(float)); - return value; - } - - if (format.bitsPerSample == 64 && format.bytesPerSample >= sizeof(double)) { - double value = 0.0; - std::memcpy(&value, samplePtr, sizeof(double)); - return static_cast(value); - } - } - - return decodeSignedIntegerSample( - samplePtr, format.bytesPerSample, format.validBitsPerSample); + return result; } -bool getDeviceMixFormat(IMMDevice* device, double* outSampleRate, UINT32* outChannelCount) { +bool getDeviceMixFormat(IMMDevice* device, AudioFormatInfo* outFormat) { if (device == nullptr) { return false; } @@ -517,17 +499,12 @@ bool getDeviceMixFormat(IMMDevice* device, double* outSampleRate, UINT32* outCha return false; } - if (outSampleRate != nullptr) { - *outSampleRate = static_cast(info.sampleRate); - } - if (outChannelCount != nullptr) { - *outChannelCount = info.channels; - } + *outFormat = info; return true; } -std::vector enumerateOutputDevices() { +std::vector enumerateDevices(bool input) { ScopedCoInit coInit; if (!coInit.usable()) { return {}; @@ -542,13 +519,13 @@ std::vector enumerateOutputDevices() { std::string defaultDeviceId; ComPtr defaultDevice; - hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &defaultDevice); + hr = enumerator->GetDefaultAudioEndpoint(input ? eCapture : eRender, eConsole, &defaultDevice); if (SUCCEEDED(hr) && defaultDevice) { defaultDeviceId = getDeviceId(defaultDevice.Get()); } ComPtr collection; - hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); + hr = enumerator->EnumAudioEndpoints(input ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection); if (FAILED(hr) || !collection) { return {}; } @@ -559,7 +536,7 @@ std::vector enumerateOutputDevices() { return {}; } - std::vector devices; + std::vector devices; devices.reserve(deviceCount); for (UINT index = 0; index < deviceCount; ++index) { @@ -579,16 +556,16 @@ std::vector enumerateOutputDevices() { label = deviceId; } - double sampleRate = 48000.0; - UINT32 channelCount = 2; - getDeviceMixFormat(device.Get(), &sampleRate, &channelCount); + AudioFormatInfo format; + if (!getDeviceMixFormat(device.Get(), &format)) continue; - devices.push_back(OutputDeviceInfo{ + devices.push_back(DeviceInfo{ deviceId, label, - sampleRate, - channelCount, + static_cast(format.sampleRate), + static_cast(format.channels), deviceId == defaultDeviceId, + channelDescriptors(format), }); } @@ -597,6 +574,8 @@ std::vector enumerateOutputDevices() { class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture { public: + explicit WindowsNativeCaptureEngine(bool input = false) : input_(input) {} + ~WindowsNativeCaptureEngine() override { stop(); } @@ -606,7 +585,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu } std::vector listOutputDevices() override { - const auto devices = enumerateOutputDevices(); + const auto devices = enumerateDevices(input_); std::vector result; result.reserve(devices.size()); for (const auto& device : devices) { @@ -616,6 +595,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu device.sampleRate, static_cast(device.channelCount), device.isDefault, + device.channels, }); } return result; @@ -630,13 +610,21 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu if (result != nullptr) { std::lock_guard lock(stateMutex_); result->sampleRate = sampleRate_; - result->channelCount = static_cast(channelCount_); + result->channelCount = channelCount_ > 1 ? 2u : 1u; + result->sourceChannelCount = channelCount_; result->deviceId = activeDeviceId_; result->deviceLabel = activeDeviceLabel_; } return true; } + Prism::Capture::ChannelRouting setChannelRouting(uint32_t left, uint32_t right) override { + std::lock_guard lock(stateMutex_); + routing_ = active_ ? Prism::Capture::normalizeChannelRouting({left, right}, channelCount_) + : Prism::Capture::ChannelRouting{left, right}; + return routing_; + } + void stop() override { stopInternal(); } @@ -660,13 +648,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu while (!drained.empty()) { auto chunk = std::move(drained.front()); drained.pop_front(); - result.chunks.push_back({ - std::move(chunk.left), - std::move(chunk.right), - static_cast(chunk.channelCount), - chunk.capturedAtMilliseconds, - chunk.sequence, - }); + result.chunks.push_back(std::move(chunk)); } return result; } @@ -696,7 +678,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu stopEvent_ = CreateEventW(nullptr, TRUE, FALSE, nullptr); if (stopEvent_ == nullptr) { startPending_ = false; - startError_ = "CreateEventW failed for Windows loopback capture."; + startError_ = "CreateEventW failed for Windows audio capture."; if (outErrorMessage != nullptr) { *outErrorMessage = startError_; } @@ -712,7 +694,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu if (!startSucceeded_) { const std::string errorMessage = startError_.empty() - ? "Native Windows loopback capture failed to start." + ? "Native Windows audio capture failed to start." : startError_; lock.unlock(); stopInternal(); @@ -786,7 +768,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu const std::wstring requestedWide = utf8ToWide(requestedDeviceId); hr = enumerator->GetDevice(requestedWide.c_str(), &device); } else { - hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); + hr = enumerator->GetDefaultAudioEndpoint(input_ ? eCapture : eRender, eConsole, &device); } if (FAILED(hr) || !device) { @@ -794,10 +776,19 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu return; } + ComPtr endpoint; + EDataFlow flow = eAll; + hr = device.As(&endpoint); + if (FAILED(hr) || FAILED(endpoint->GetDataFlow(&flow)) + || flow != (input_ ? eCapture : eRender)) { + notifyStartFailure("The selected Windows device has the wrong capture direction."); + return; + } + const std::string deviceId = getDeviceId(device.Get()); std::string deviceLabel = getDeviceFriendlyName(device.Get()); if (deviceLabel.empty()) { - deviceLabel = deviceId.empty() ? "Windows Output Device" : deviceId; + deviceLabel = deviceId.empty() ? (input_ ? "Windows Input Device" : "Windows Output Device") : deviceId; } ComPtr audioClient; @@ -821,7 +812,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu const AudioFormatInfo format = getFormatInfo(mixFormat); if (!format.valid) { CoTaskMemFree(mixFormat); - notifyStartFailure("Unsupported WASAPI mix format for Windows loopback capture."); + notifyStartFailure("Unsupported WASAPI mix format for Windows audio capture."); return; } @@ -853,7 +844,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu double lastRealPacketAtMs = startMs; hr = audioClient->Initialize( - AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, 0, 0, mixFormat, nullptr); + AUDCLNT_SHAREMODE_SHARED, input_ ? 0 : AUDCLNT_STREAMFLAGS_LOOPBACK, 0, 0, mixFormat, nullptr); if (FAILED(hr)) { CoTaskMemFree(mixFormat); notifyStartFailure(hresultMessage("IAudioClient::Initialize", hr)); @@ -903,26 +894,24 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu if (framesToRead > 0) { CapturedChunk chunk; - chunk.channelCount = std::max(1, format.channels); + chunk.channelCount = format.channels > 1 ? 2u : 1u; chunk.capturedAtMilliseconds = monotonicMilliseconds(); chunk.left.resize(framesToRead); chunk.right.resize(framesToRead); - if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0 && data != nullptr) { - for (UINT32 frameIndex = 0; frameIndex < framesToRead; ++frameIndex) { - const BYTE* frameData = - data + static_cast(frameIndex) * format.bytesPerFrame; - const float left = readFrameSample(frameData, 0, format); - const float right = format.channels > 1 - ? readFrameSample(frameData, 1, format) - : left; - chunk.left[frameIndex] = left; - chunk.right[frameIndex] = right; - } - } else { - std::fill(chunk.left.begin(), chunk.left.end(), 0.0f); - std::fill(chunk.right.begin(), chunk.right.end(), 0.0f); + chunk.sourceChannelPeaks.resize(format.channels); + Prism::Capture::ChannelRouting routing; + { + std::lock_guard lock(stateMutex_); + routing = routing_; } + const Prism::Capture::PCMBufferView buffer{ + (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0 ? data : nullptr, + static_cast(framesToRead) * format.bytesPerFrame, format.channels}; + Prism::Capture::selectStereoChannels(&buffer, 1, format.pcm, framesToRead, + format.channels, routing.left, routing.right, chunk.left.data(), chunk.right.data()); + Prism::Capture::measureSourceChannelPeaks(&buffer, 1, format.pcm, framesToRead, + format.channels, chunk.sourceChannelPeaks.data()); lastChunkPushedAtMs = chunk.capturedAtMilliseconds; lastRealPacketAtMs = chunk.capturedAtMilliseconds; @@ -941,7 +930,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu break; } - if (!pushedThisIteration) { + if (!input_ && !pushedThisIteration) { const double nowMs = monotonicMilliseconds(); const double stallMs = nowMs - lastRealPacketAtMs; const double sinceLastChunkMs = nowMs - lastChunkPushedAtMs; @@ -954,7 +943,8 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu sinceLastChunkMs * static_cast(format.sampleRate) / 1000.0))); CapturedChunk silentChunk; - silentChunk.channelCount = std::max(1, format.channels); + silentChunk.channelCount = format.channels > 1 ? 2u : 1u; + silentChunk.sourceChannelPeaks.assign(format.channels, 0.0f); silentChunk.capturedAtMilliseconds = nowMs; silentChunk.left.assign(silenceFrames, 0.0f); silentChunk.right.assign(silenceFrames, 0.0f); @@ -990,6 +980,7 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu activeDeviceLabel_ = deviceLabel; sampleRate_ = sampleRate; channelCount_ = channelCount; + routing_ = Prism::Capture::normalizeChannelRouting(routing_, channelCount_); sequence_ = 0; startSucceeded_ = true; startPending_ = false; @@ -1030,6 +1021,8 @@ class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCaptu chunkQueue_.push_back(std::move(chunk)); } + const bool input_; + Prism::Capture::ChannelRouting routing_; std::mutex stateMutex_; std::condition_variable startCondition_; std::mutex chunkMutex_; @@ -1061,7 +1054,7 @@ Napi::Value WindowsMediaGetSupport(const Napi::CallbackInfo& info) { hresultMessage("RoInitialize(RO_INIT_MULTITHREADED)", init.result())); } - auto manager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); + auto manager = awaitMediaOperation(GlobalSystemMediaTransportControlsSessionManager::RequestAsync()); (void)manager; return createWindowsMediaSupport(env, true); } catch (const winrt::hresult_error& error) { @@ -1076,7 +1069,7 @@ Napi::Value WindowsMediaGetSupport(const Napi::CallbackInfo& info) { } } -Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info) { +Napi::Value WindowsMediaGetPlaybackState(const Napi::CallbackInfo& info, const std::string& provider) { Napi::Env env = info.Env(); try { @@ -1086,15 +1079,15 @@ Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info) hresultMessage("RoInitialize(RO_INIT_MULTITHREADED)", init.result())); } - const auto manager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); - const auto session = findSpotifySession(manager); + const auto manager = awaitMediaOperation(GlobalSystemMediaTransportControlsSessionManager::RequestAsync()); + const auto session = findProviderSession(manager, provider); if (!session.has_value()) { return env.Null(); } const auto playbackInfo = session->GetPlaybackInfo(); const auto timeline = session->GetTimelineProperties(); - const auto mediaProperties = session->TryGetMediaPropertiesAsync().get(); + const auto mediaProperties = awaitMediaOperation(session->TryGetMediaPropertiesAsync()); const auto durationMs = std::max( 0, @@ -1102,23 +1095,17 @@ Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info) timeline.EndTime() - timeline.StartTime()) .count()); - // Position() is stamped at LastUpdatedTime(); extrapolate forward when playing. - int64_t positionMs; - if (playbackInfo.PlaybackStatus() == - GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing) { - const auto elapsed = winrt::clock::now() - timeline.LastUpdatedTime(); - const auto extrapolated = timeline.Position() + elapsed; - positionMs = std::min( - durationMs, - std::max( - 0, - std::chrono::duration_cast(extrapolated).count())); - } else { - positionMs = std::max( - 0, - std::chrono::duration_cast(timeline.Position()) - .count()); + // Position() is stamped at LastUpdatedTime(). Some players omit the timeline; + // never extrapolate from the default Windows epoch or clamp unknown duration to zero. + int64_t positionMs = std::max(0, + std::chrono::duration_cast(timeline.Position()).count()); + if (playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing && + timeline.LastUpdatedTime().time_since_epoch().count() > 0) { + positionMs += std::max(0, + std::chrono::duration_cast( + winrt::clock::now() - timeline.LastUpdatedTime()).count()); } + if (durationMs > 0) positionMs = std::min(durationMs, positionMs); Napi::Object payload = Napi::Object::New(env); payload.Set( @@ -1136,9 +1123,10 @@ Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info) Napi::String::New(env, winrt::to_string(session->SourceAppUserModelId()))); const std::string trackKey = + provider + "\n" + winrt::to_string(session->SourceAppUserModelId()) + "\n" + winrt::to_string(mediaProperties.Title()) + "\n" + - winrt::to_string(mediaProperties.Artist()); - const std::string artworkDataUrl = getOrFetchThumbnail(trackKey, mediaProperties); + winrt::to_string(mediaProperties.Artist()) + "\n" + winrt::to_string(mediaProperties.AlbumTitle()); + const std::string artworkDataUrl = getOrFetchThumbnail(provider, trackKey, mediaProperties); payload.Set( "artworkDataUrl", artworkDataUrl.empty() ? env.Null() : Napi::String::New(env, artworkDataUrl)); @@ -1158,10 +1146,10 @@ Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info) } } -Napi::Value WindowsMediaSendSpotifyControl(const Napi::CallbackInfo& info) { +Napi::Value WindowsMediaSendControl(const Napi::CallbackInfo& info, const std::string& provider) { Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsString()) { - Napi::TypeError::New(env, "Expected a Spotify control command.").ThrowAsJavaScriptException(); + Napi::TypeError::New(env, "Expected a media control command.").ThrowAsJavaScriptException(); return env.Null(); } @@ -1174,27 +1162,27 @@ Napi::Value WindowsMediaSendSpotifyControl(const Napi::CallbackInfo& info) { hresultMessage("RoInitialize(RO_INIT_MULTITHREADED)", init.result())); } - const auto manager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); - const auto session = findSpotifySession(manager); + const auto manager = awaitMediaOperation(GlobalSystemMediaTransportControlsSessionManager::RequestAsync()); + const auto session = findProviderSession(manager, provider); if (!session.has_value()) { - throw std::runtime_error("Spotify is not running."); + throw std::runtime_error(provider + " is not running."); } bool accepted = false; if (command == "play") { - accepted = session->TryPlayAsync().get(); + accepted = awaitMediaOperation(session->TryPlayAsync()); } else if (command == "pause") { - accepted = session->TryPauseAsync().get(); + accepted = awaitMediaOperation(session->TryPauseAsync()); } else if (command == "next") { - accepted = session->TrySkipNextAsync().get(); + accepted = awaitMediaOperation(session->TrySkipNextAsync()); } else if (command == "previous") { - accepted = session->TrySkipPreviousAsync().get(); + accepted = awaitMediaOperation(session->TrySkipPreviousAsync()); } else { - throw std::runtime_error("Unsupported Spotify control command."); + throw std::runtime_error("Unsupported media control command."); } if (!accepted) { - throw std::runtime_error("Spotify did not allow Prism to complete that request."); + throw std::runtime_error(provider + " did not allow Prism to complete that request."); } return Napi::Boolean::New(env, true); @@ -1211,6 +1199,19 @@ Napi::Value WindowsMediaSendSpotifyControl(const Napi::CallbackInfo& info) { return env.Null(); } } +Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info) { + return WindowsMediaGetPlaybackState(info, "spotify"); +} +Napi::Value WindowsMediaGetTidalPlaybackState(const Napi::CallbackInfo& info) { + return WindowsMediaGetPlaybackState(info, "tidal"); +} +Napi::Value WindowsMediaSendSpotifyControl(const Napi::CallbackInfo& info) { + return WindowsMediaSendControl(info, "spotify"); +} +Napi::Value WindowsMediaSendTidalControl(const Napi::CallbackInfo& info) { + return WindowsMediaSendControl(info, "tidal"); +} + #endif } // namespace @@ -1225,6 +1226,8 @@ void RegisterWindowsMedia(Napi::Env env, Napi::Object exports) { mediaExports.Set( "sendSpotifyControl", Napi::Function::New(env, WindowsMediaSendSpotifyControl)); + mediaExports.Set("getTidalPlaybackState", Napi::Function::New(env, WindowsMediaGetTidalPlaybackState)); + mediaExports.Set("sendTidalControl", Napi::Function::New(env, WindowsMediaSendTidalControl)); exports.Set("windowsMedia", mediaExports); } #endif @@ -1235,6 +1238,11 @@ std::unique_ptr createSystemAudioCapture() { return std::make_unique(); } +std::unique_ptr createDeviceInputCapture() { + return std::make_unique( + std::make_unique(true)); +} + } // namespace Prism::Capture #endif // defined(_WIN32) diff --git a/native/src/windows_capture_stub.cpp b/native/src/windows_capture_stub.cpp index de22197..227851a 100644 --- a/native/src/windows_capture_stub.cpp +++ b/native/src/windows_capture_stub.cpp @@ -12,11 +12,11 @@ Napi::Value MediaGetSupport(const Napi::CallbackInfo& info) { return support; } -Napi::Value GetSpotifyPlaybackState(const Napi::CallbackInfo& info) { +Napi::Value GetPlaybackState(const Napi::CallbackInfo& info) { return info.Env().Null(); } -Napi::Value SendSpotifyControl(const Napi::CallbackInfo& info) { +Napi::Value SendControl(const Napi::CallbackInfo& info) { Napi::Error::New( info.Env(), "Native Windows media-session integration is unavailable on this platform.") .ThrowAsJavaScriptException(); @@ -30,7 +30,9 @@ void RegisterWindowsMedia(Napi::Env env, Napi::Object exports) { mediaExports.Set("getSupport", Napi::Function::New(env, MediaGetSupport)); mediaExports.Set( "getSpotifyPlaybackState", - Napi::Function::New(env, GetSpotifyPlaybackState)); - mediaExports.Set("sendSpotifyControl", Napi::Function::New(env, SendSpotifyControl)); + Napi::Function::New(env, GetPlaybackState)); + mediaExports.Set("sendSpotifyControl", Napi::Function::New(env, SendControl)); + mediaExports.Set("getTidalPlaybackState", Napi::Function::New(env, GetPlaybackState)); + mediaExports.Set("sendTidalControl", Napi::Function::New(env, SendControl)); exports.Set("windowsMedia", mediaExports); } diff --git a/native/vendor/REFERENCE_DEPENDENCIES.md b/native/vendor/REFERENCE_DEPENDENCIES.md new file mode 100644 index 0000000..cac228e --- /dev/null +++ b/native/vendor/REFERENCE_DEPENDENCIES.md @@ -0,0 +1,6 @@ +# Reference analysis dependencies + +- dr_libs: https://github.com/mackron/dr_libs — commit `dfe8377631000664666519fdb83da193fd8037f4`. Vendored `dr_wav.h`, `dr_flac.h`, `dr_mp3.h` and LICENSE. Decoding only; WAV/AIFF, FLAC and MP3. +- r8brain-free-src: https://github.com/avaneev/r8brain-free-src — commit `9e73d2dd59fd5b95108fdb4f590083e35758b45f`. Vendored portable headers and FFT sources, with LICENSE. Uses the default Ooura backend. + +Sample rate converter designed by Aleksey Vaneev of Voxengo. diff --git a/native/vendor/dr_libs/LICENSE b/native/vendor/dr_libs/LICENSE new file mode 100644 index 0000000..16dfbdf --- /dev/null +++ b/native/vendor/dr_libs/LICENSE @@ -0,0 +1,47 @@ +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2020 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native/vendor/dr_libs/dr_flac.h b/native/vendor/dr_libs/dr_flac.h new file mode 100644 index 0000000..051f37c --- /dev/null +++ b/native/vendor/dr_libs/dr_flac.h @@ -0,0 +1,12760 @@ +/* +FLAC audio decoder. Choice of public domain or MIT-0. See license statements at the end of this file. +dr_flac - v0.13.4 - TBD + +David Reid - mackron@gmail.com + +GitHub: https://github.com/mackron/dr_libs +*/ + +/* +Introduction +============ +dr_flac is a single file library. To use it, do something like the following in one .c file. + + ```c + #define DR_FLAC_IMPLEMENTATION + #include "dr_flac.h" + ``` + +You can then #include this file in other parts of the program as you would with any other header file. To decode audio data, do something like the following: + + ```c + drflac* pFlac = drflac_open_file("MySong.flac", NULL); + if (pFlac == NULL) { + // Failed to open FLAC file + } + + drflac_int32* pSamples = malloc(pFlac->totalPCMFrameCount * pFlac->channels * sizeof(drflac_int32)); + drflac_uint64 numberOfInterleavedSamplesActuallyRead = drflac_read_pcm_frames_s32(pFlac, pFlac->totalPCMFrameCount, pSamples); + ``` + +The drflac object represents the decoder. It is a transparent type so all the information you need, such as the number of channels and the bits per sample, +should be directly accessible - just make sure you don't change their values. Samples are always output as interleaved signed 32-bit PCM. In the example above +a native FLAC stream was opened, however dr_flac has seamless support for Ogg encapsulated FLAC streams as well. + +You do not need to decode the entire stream in one go - you just specify how many samples you'd like at any given time and the decoder will give you as many +samples as it can, up to the amount requested. Later on when you need the next batch of samples, just call it again. Example: + + ```c + while (drflac_read_pcm_frames_s32(pFlac, chunkSizeInPCMFrames, pChunkSamples) > 0) { + do_something(); + } + ``` + +You can seek to a specific PCM frame with `drflac_seek_to_pcm_frame()`. + +If you just want to quickly decode an entire FLAC file in one go you can do something like this: + + ```c + unsigned int channels; + unsigned int sampleRate; + drflac_uint64 totalPCMFrameCount; + drflac_int32* pSampleData = drflac_open_file_and_read_pcm_frames_s32("MySong.flac", &channels, &sampleRate, &totalPCMFrameCount, NULL); + if (pSampleData == NULL) { + // Failed to open and decode FLAC file. + } + + ... + + drflac_free(pSampleData, NULL); + ``` + +You can read samples as signed 16-bit integer and 32-bit floating-point PCM with the *_s16() and *_f32() family of APIs respectively, but note that these +should be considered lossy. + + +If you need access to metadata (album art, etc.), use `drflac_open_with_metadata()`, `drflac_open_file_with_metdata()` or `drflac_open_memory_with_metadata()`. +The rationale for keeping these APIs separate is that they're slightly slower than the normal versions and also just a little bit harder to use. dr_flac +reports metadata to the application through the use of a callback, and every metadata block is reported before `drflac_open_with_metdata()` returns. + +The main opening APIs (`drflac_open()`, etc.) will fail if the header is not present. The presents a problem in certain scenarios such as broadcast style +streams or internet radio where the header may not be present because the user has started playback mid-stream. To handle this, use the relaxed APIs: + + `drflac_open_relaxed()` + `drflac_open_with_metadata_relaxed()` + +It is not recommended to use these APIs for file based streams because a missing header would usually indicate a corrupt or perverse file. In addition, these +APIs can take a long time to initialize because they may need to spend a lot of time finding the first frame. + + + +Build Options +============= +#define these options before including this file. + +#define DR_FLAC_NO_STDIO + Disable `drflac_open_file()` and family. + +#define DR_FLAC_NO_OGG + Disables support for Ogg/FLAC streams. + +#define DR_FLAC_BUFFER_SIZE + Defines the size of the internal buffer to store data from onRead(). This buffer is used to reduce the number of calls back to the client for more data. + Larger values means more memory, but better performance. My tests show diminishing returns after about 4KB (which is the default). Consider reducing this if + you have a very efficient implementation of onRead(), or increase it if it's very inefficient. Must be a multiple of 8. + +#define DR_FLAC_NO_CRC + Disables CRC checks. This will offer a performance boost when CRC is unnecessary. This will disable binary search seeking. When seeking, the seek table will + be used if available. Otherwise the seek will be performed using brute force. + +#define DR_FLAC_NO_SIMD + Disables SIMD optimizations (SSE on x86/x64 architectures, NEON on ARM architectures). Use this if you are having compatibility issues with your compiler. + +#define DR_FLAC_NO_WCHAR + Disables all functions ending with `_w`. Use this if your compiler does not provide wchar.h. Not required if DR_FLAC_NO_STDIO is also defined. + + + +Notes +===== +- dr_flac does not support changing the sample rate nor channel count mid stream. +- dr_flac is not thread-safe, but its APIs can be called from any thread so long as you do your own synchronization. +- When using Ogg encapsulation, a corrupted metadata block will result in `drflac_open_with_metadata()` and `drflac_open()` returning inconsistent samples due + to differences in corrupted stream recorvery logic between the two APIs. +*/ + +#ifndef dr_flac_h +#define dr_flac_h + +#ifdef __cplusplus +extern "C" { +#endif + +#define DRFLAC_STRINGIFY(x) #x +#define DRFLAC_XSTRINGIFY(x) DRFLAC_STRINGIFY(x) + +#define DRFLAC_VERSION_MAJOR 0 +#define DRFLAC_VERSION_MINOR 13 +#define DRFLAC_VERSION_REVISION 4 +#define DRFLAC_VERSION_STRING DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MAJOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MINOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_REVISION) + +#include /* For size_t. */ + +/* Sized Types */ +typedef signed char drflac_int8; +typedef unsigned char drflac_uint8; +typedef signed short drflac_int16; +typedef unsigned short drflac_uint16; +typedef signed int drflac_int32; +typedef unsigned int drflac_uint32; +#if defined(_MSC_VER) && !defined(__clang__) + typedef signed __int64 drflac_int64; + typedef unsigned __int64 drflac_uint64; +#else + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wlong-long" + #if defined(__clang__) + #pragma GCC diagnostic ignored "-Wc++11-long-long" + #endif + #endif + typedef signed long long drflac_int64; + typedef unsigned long long drflac_uint64; + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop + #endif +#endif +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__) + typedef drflac_uint64 drflac_uintptr; +#else + typedef drflac_uint32 drflac_uintptr; +#endif +typedef drflac_uint8 drflac_bool8; +typedef drflac_uint32 drflac_bool32; +#define DRFLAC_TRUE 1 +#define DRFLAC_FALSE 0 +/* End Sized Types */ + +/* Decorations */ +#if !defined(DRFLAC_API) + #if defined(DRFLAC_DLL) + #if defined(_WIN32) + #define DRFLAC_DLL_IMPORT __declspec(dllimport) + #define DRFLAC_DLL_EXPORT __declspec(dllexport) + #define DRFLAC_DLL_PRIVATE static + #else + #if defined(__GNUC__) && __GNUC__ >= 4 + #define DRFLAC_DLL_IMPORT __attribute__((visibility("default"))) + #define DRFLAC_DLL_EXPORT __attribute__((visibility("default"))) + #define DRFLAC_DLL_PRIVATE __attribute__((visibility("hidden"))) + #else + #define DRFLAC_DLL_IMPORT + #define DRFLAC_DLL_EXPORT + #define DRFLAC_DLL_PRIVATE static + #endif + #endif + + #if defined(DR_FLAC_IMPLEMENTATION) || defined(DRFLAC_IMPLEMENTATION) + #define DRFLAC_API DRFLAC_DLL_EXPORT + #else + #define DRFLAC_API DRFLAC_DLL_IMPORT + #endif + #define DRFLAC_PRIVATE DRFLAC_DLL_PRIVATE + #else + #define DRFLAC_API extern + #define DRFLAC_PRIVATE static + #endif +#endif +/* End Decorations */ + +#if defined(_MSC_VER) && _MSC_VER >= 1700 /* Visual Studio 2012 */ + #define DRFLAC_DEPRECATED __declspec(deprecated) +#elif (defined(__GNUC__) && __GNUC__ >= 4) /* GCC 4 */ + #define DRFLAC_DEPRECATED __attribute__((deprecated)) +#elif defined(__has_feature) /* Clang */ + #if __has_feature(attribute_deprecated) + #define DRFLAC_DEPRECATED __attribute__((deprecated)) + #else + #define DRFLAC_DEPRECATED + #endif +#else + #define DRFLAC_DEPRECATED +#endif + +DRFLAC_API void drflac_version(drflac_uint32* pMajor, drflac_uint32* pMinor, drflac_uint32* pRevision); +DRFLAC_API const char* drflac_version_string(void); + +/* Allocation Callbacks */ +typedef struct +{ + void* pUserData; + void* (* onMalloc)(size_t sz, void* pUserData); + void* (* onRealloc)(void* p, size_t sz, void* pUserData); + void (* onFree)(void* p, void* pUserData); +} drflac_allocation_callbacks; +/* End Allocation Callbacks */ + +/* +As data is read from the client it is placed into an internal buffer for fast access. This controls the size of that buffer. Larger values means more speed, +but also more memory. In my testing there is diminishing returns after about 4KB, but you can fiddle with this to suit your own needs. Must be a multiple of 8. +*/ +#ifndef DR_FLAC_BUFFER_SIZE +#define DR_FLAC_BUFFER_SIZE 4096 +#endif + + +/* Architecture Detection */ +#if defined(_WIN64) || defined(_LP64) || defined(__LP64__) +#define DRFLAC_64BIT +#endif + +#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)) + #define DRFLAC_X64 +#elif defined(__i386) || defined(_M_IX86) + #define DRFLAC_X86 +#elif defined(__arm__) || defined(_M_ARM) || defined(__arm64) || defined(__arm64__) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) + #define DRFLAC_ARM +#endif +/* End Architecture Detection */ + + +#ifdef DRFLAC_64BIT +typedef drflac_uint64 drflac_cache_t; +#else +typedef drflac_uint32 drflac_cache_t; +#endif + +/* The various metadata block types. */ +#define DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO 0 +#define DRFLAC_METADATA_BLOCK_TYPE_PADDING 1 +#define DRFLAC_METADATA_BLOCK_TYPE_APPLICATION 2 +#define DRFLAC_METADATA_BLOCK_TYPE_SEEKTABLE 3 +#define DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT 4 +#define DRFLAC_METADATA_BLOCK_TYPE_CUESHEET 5 +#define DRFLAC_METADATA_BLOCK_TYPE_PICTURE 6 +#define DRFLAC_METADATA_BLOCK_TYPE_INVALID 127 + +/* The various picture types specified in the PICTURE block. */ +#define DRFLAC_PICTURE_TYPE_OTHER 0 +#define DRFLAC_PICTURE_TYPE_FILE_ICON 1 +#define DRFLAC_PICTURE_TYPE_OTHER_FILE_ICON 2 +#define DRFLAC_PICTURE_TYPE_COVER_FRONT 3 +#define DRFLAC_PICTURE_TYPE_COVER_BACK 4 +#define DRFLAC_PICTURE_TYPE_LEAFLET_PAGE 5 +#define DRFLAC_PICTURE_TYPE_MEDIA 6 +#define DRFLAC_PICTURE_TYPE_LEAD_ARTIST 7 +#define DRFLAC_PICTURE_TYPE_ARTIST 8 +#define DRFLAC_PICTURE_TYPE_CONDUCTOR 9 +#define DRFLAC_PICTURE_TYPE_BAND 10 +#define DRFLAC_PICTURE_TYPE_COMPOSER 11 +#define DRFLAC_PICTURE_TYPE_LYRICIST 12 +#define DRFLAC_PICTURE_TYPE_RECORDING_LOCATION 13 +#define DRFLAC_PICTURE_TYPE_DURING_RECORDING 14 +#define DRFLAC_PICTURE_TYPE_DURING_PERFORMANCE 15 +#define DRFLAC_PICTURE_TYPE_SCREEN_CAPTURE 16 +#define DRFLAC_PICTURE_TYPE_BRIGHT_COLORED_FISH 17 +#define DRFLAC_PICTURE_TYPE_ILLUSTRATION 18 +#define DRFLAC_PICTURE_TYPE_BAND_LOGOTYPE 19 +#define DRFLAC_PICTURE_TYPE_PUBLISHER_LOGOTYPE 20 + +typedef enum +{ + drflac_container_native, + drflac_container_ogg, + drflac_container_unknown +} drflac_container; + +typedef enum +{ + DRFLAC_SEEK_SET, + DRFLAC_SEEK_CUR, + DRFLAC_SEEK_END +} drflac_seek_origin; + +/* The order of members in this structure is important because we map this directly to the raw data within the SEEKTABLE metadata block. */ +typedef struct +{ + drflac_uint64 firstPCMFrame; + drflac_uint64 flacFrameOffset; /* The offset from the first byte of the header of the first frame. */ + drflac_uint16 pcmFrameCount; +} drflac_seekpoint; + +typedef struct +{ + drflac_uint16 minBlockSizeInPCMFrames; + drflac_uint16 maxBlockSizeInPCMFrames; + drflac_uint32 minFrameSizeInPCMFrames; + drflac_uint32 maxFrameSizeInPCMFrames; + drflac_uint32 sampleRate; + drflac_uint8 channels; + drflac_uint8 bitsPerSample; + drflac_uint64 totalPCMFrameCount; + drflac_uint8 md5[16]; +} drflac_streaminfo; + +typedef struct +{ + /* + The metadata type. Use this to know how to interpret the data below. Will be set to one of the + DRFLAC_METADATA_BLOCK_TYPE_* tokens. + */ + drflac_uint32 type; + + /* The size in bytes of the block and the buffer pointed to by pRawData if it's non-NULL. */ + drflac_uint32 rawDataSize; + + /* The offset in the stream of the raw data. */ + drflac_uint64 rawDataOffset; + + /* + A pointer to the raw data. This points to a temporary buffer so don't hold on to it. It's best to + not modify the contents of this buffer. Use the structures below for more meaningful and structured + information about the metadata. It's possible for this to be null. + */ + const void* pRawData; + + union + { + drflac_streaminfo streaminfo; + + struct + { + int unused; + } padding; + + struct + { + drflac_uint32 id; + const void* pData; + drflac_uint32 dataSize; + } application; + + struct + { + drflac_uint32 seekpointCount; + const drflac_seekpoint* pSeekpoints; + } seektable; + + struct + { + drflac_uint32 vendorLength; + const char* vendor; + drflac_uint32 commentCount; + const void* pComments; + } vorbis_comment; + + struct + { + char catalog[128]; + drflac_uint64 leadInSampleCount; + drflac_bool32 isCD; + drflac_uint8 trackCount; + const void* pTrackData; + } cuesheet; + + struct + { + drflac_uint32 type; + drflac_uint32 mimeLength; + const char* mime; + drflac_uint32 descriptionLength; + const char* description; + drflac_uint32 width; + drflac_uint32 height; + drflac_uint32 colorDepth; + drflac_uint32 indexColorCount; + drflac_uint32 pictureDataSize; + drflac_uint64 pictureDataOffset; /* Offset from the start of the stream. */ + const drflac_uint8* pPictureData; + } picture; + } data; +} drflac_metadata; + + +/* +Callback for when data needs to be read from the client. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +pBufferOut (out) + The output buffer. + +bytesToRead (in) + The number of bytes to read. + + +Return Value +------------ +The number of bytes actually read. + + +Remarks +------- +A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback until either the entire bytesToRead is filled or +you have reached the end of the stream. +*/ +typedef size_t (* drflac_read_proc)(void* pUserData, void* pBufferOut, size_t bytesToRead); + +/* +Callback for when data needs to be seeked. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +offset (in) + The number of bytes to move, relative to the origin. Will never be negative. + +origin (in) + The origin of the seek - the current position, the start of the stream, or the end of the stream. + + +Return Value +------------ +Whether or not the seek was successful. + + +Remarks +------- +Seeking relative to the start and the current position must always be supported. If seeking from the end of the stream is not supported, return DRFLAC_FALSE. + +When seeking to a PCM frame using drflac_seek_to_pcm_frame(), dr_flac may call this with an offset beyond the end of the FLAC stream. This needs to be detected +and handled by returning DRFLAC_FALSE. +*/ +typedef drflac_bool32 (* drflac_seek_proc)(void* pUserData, int offset, drflac_seek_origin origin); + +/* +Callback for when the current position in the stream needs to be retrieved. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +pCursor (out) + A pointer to a variable to receive the current position in the stream. + + +Return Value +------------ +Whether or not the operation was successful. +*/ +typedef drflac_bool32 (* drflac_tell_proc)(void* pUserData, drflac_int64* pCursor); + +/* +Callback for when a metadata block is read. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +pMetadata (in) + A pointer to a structure containing the data of the metadata block. + + +Remarks +------- +Use pMetadata->type to determine which metadata block is being handled and how to read the data. This +will be set to one of the DRFLAC_METADATA_BLOCK_TYPE_* tokens. +*/ +typedef void (* drflac_meta_proc)(void* pUserData, drflac_metadata* pMetadata); + + +/* Structure for internal use. Only used for decoders opened with drflac_open_memory. */ +typedef struct +{ + const drflac_uint8* data; + size_t dataSize; + size_t currentReadPos; +} drflac__memory_stream; + +/* Structure for internal use. Used for bit streaming. */ +typedef struct +{ + /* The function to call when more data needs to be read. */ + drflac_read_proc onRead; + + /* The function to call when the current read position needs to be moved. */ + drflac_seek_proc onSeek; + + /* The function to call when the current read position needs to be retrieved. */ + drflac_tell_proc onTell; + + /* The user data to pass around to onRead and onSeek. */ + void* pUserData; + + + /* + The number of unaligned bytes in the L2 cache. This will always be 0 until the end of the stream is hit. At the end of the + stream there will be a number of bytes that don't cleanly fit in an L1 cache line, so we use this variable to know whether + or not the bistreamer needs to run on a slower path to read those last bytes. This will never be more than sizeof(drflac_cache_t). + */ + size_t unalignedByteCount; + + /* The content of the unaligned bytes. */ + drflac_cache_t unalignedCache; + + /* The index of the next valid cache line in the "L2" cache. */ + drflac_uint32 nextL2Line; + + /* The number of bits that have been consumed by the cache. This is used to determine how many valid bits are remaining. */ + drflac_uint32 consumedBits; + + /* + The cached data which was most recently read from the client. There are two levels of cache. Data flows as such: + Client -> L2 -> L1. The L2 -> L1 movement is aligned and runs on a fast path in just a few instructions. + */ + drflac_cache_t cacheL2[DR_FLAC_BUFFER_SIZE/sizeof(drflac_cache_t)]; + drflac_cache_t cache; + + /* + CRC-16. This is updated whenever bits are read from the bit stream. Manually set this to 0 to reset the CRC. For FLAC, this + is reset to 0 at the beginning of each frame. + */ + drflac_uint16 crc16; + drflac_cache_t crc16Cache; /* A cache for optimizing CRC calculations. This is filled when when the L1 cache is reloaded. */ + drflac_uint32 crc16CacheIgnoredBytes; /* The number of bytes to ignore when updating the CRC-16 from the CRC-16 cache. */ +} drflac_bs; + +typedef struct +{ + /* The type of the subframe: SUBFRAME_CONSTANT, SUBFRAME_VERBATIM, SUBFRAME_FIXED or SUBFRAME_LPC. */ + drflac_uint8 subframeType; + + /* The number of wasted bits per sample as specified by the sub-frame header. */ + drflac_uint8 wastedBitsPerSample; + + /* The order to use for the prediction stage for SUBFRAME_FIXED and SUBFRAME_LPC. */ + drflac_uint8 lpcOrder; + + /* A pointer to the buffer containing the decoded samples in the subframe. This pointer is an offset from drflac::pExtraData. */ + drflac_int32* pSamplesS32; +} drflac_subframe; + +typedef struct +{ + /* + If the stream uses variable block sizes, this will be set to the index of the first PCM frame. If fixed block sizes are used, this will + always be set to 0. This is 64-bit because the decoded PCM frame number will be 36 bits. + */ + drflac_uint64 pcmFrameNumber; + + /* + If the stream uses fixed block sizes, this will be set to the frame number. If variable block sizes are used, this will always be 0. This + is 32-bit because in fixed block sizes, the maximum frame number will be 31 bits. + */ + drflac_uint32 flacFrameNumber; + + /* The sample rate of this frame. */ + drflac_uint32 sampleRate; + + /* The number of PCM frames in each sub-frame within this frame. */ + drflac_uint16 blockSizeInPCMFrames; + + /* + The channel assignment of this frame. This is not always set to the channel count. If interchannel decorrelation is being used this + will be set to DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE, DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE or DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE. + */ + drflac_uint8 channelAssignment; + + /* The number of bits per sample within this frame. */ + drflac_uint8 bitsPerSample; + + /* The frame's CRC. */ + drflac_uint8 crc8; +} drflac_frame_header; + +typedef struct +{ + /* The header. */ + drflac_frame_header header; + + /* + The number of PCM frames left to be read in this FLAC frame. This is initially set to the block size. As PCM frames are read, + this will be decremented. When it reaches 0, the decoder will see this frame as fully consumed and load the next frame. + */ + drflac_uint32 pcmFramesRemaining; + + /* The list of sub-frames within the frame. There is one sub-frame for each channel, and there's a maximum of 8 channels. */ + drflac_subframe subframes[8]; +} drflac_frame; + +typedef struct +{ + /* The function to call when a metadata block is read. */ + drflac_meta_proc onMeta; + + /* The user data posted to the metadata callback function. */ + void* pUserDataMD; + + /* Memory allocation callbacks. */ + drflac_allocation_callbacks allocationCallbacks; + + + /* The sample rate. Will be set to something like 44100. */ + drflac_uint32 sampleRate; + + /* + The number of channels. This will be set to 1 for monaural streams, 2 for stereo, etc. Maximum 8. This is set based on the + value specified in the STREAMINFO block. + */ + drflac_uint8 channels; + + /* The bits per sample. Will be set to something like 16, 24, etc. */ + drflac_uint8 bitsPerSample; + + /* The maximum block size, in samples. This number represents the number of samples in each channel (not combined). */ + drflac_uint16 maxBlockSizeInPCMFrames; + + /* + The total number of PCM Frames making up the stream. Can be 0 in which case it's still a valid stream, but just means + the total PCM frame count is unknown. Likely the case with streams like internet radio. + */ + drflac_uint64 totalPCMFrameCount; + + + /* The container type. This is set based on whether or not the decoder was opened from a native or Ogg stream. */ + drflac_container container; + + /* The number of seekpoints in the seektable. */ + drflac_uint32 seekpointCount; + + + /* Information about the frame the decoder is currently sitting on. */ + drflac_frame currentFLACFrame; + + + /* The index of the PCM frame the decoder is currently sitting on. This is only used for seeking. */ + drflac_uint64 currentPCMFrame; + + /* The position of the first FLAC frame in the stream. This is only ever used for seeking. */ + drflac_uint64 firstFLACFramePosInBytes; + + + /* A hack to avoid a malloc() when opening a decoder with drflac_open_memory(). */ + drflac__memory_stream memoryStream; + + + /* A pointer to the decoded sample data. This is an offset of pExtraData. */ + drflac_int32* pDecodedSamples; + + /* A pointer to the seek table. This is an offset of pExtraData, or NULL if there is no seek table. */ + drflac_seekpoint* pSeekpoints; + + /* Internal use only. Only used with Ogg containers. Points to a drflac_oggbs object. This is an offset of pExtraData. */ + void* _oggbs; + + /* Internal use only. Used for profiling and testing different seeking modes. */ + drflac_bool32 _noSeekTableSeek : 1; + drflac_bool32 _noBinarySearchSeek : 1; + drflac_bool32 _noBruteForceSeek : 1; + + /* The bit streamer. The raw FLAC data is fed through this object. */ + drflac_bs bs; + + /* Variable length extra data. We attach this to the end of the object so we can avoid unnecessary mallocs. */ + drflac_uint8 pExtraData[1]; +} drflac; + + +/* +Opens a FLAC decoder. + + +Parameters +---------- +onRead (in) + The function to call when data needs to be read from the client. + +onSeek (in) + The function to call when the read position of the client data needs to move. + +onTell (in) + The function to call when the read position of the client needs to be queried. + +pUserData (in, optional) + A pointer to application defined data that will be passed to onRead and onSeek. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +Returns a pointer to an object representing the decoder. + + +Remarks +------- +Close the decoder with `drflac_close()`. + +`pAllocationCallbacks` can be NULL in which case it will use `DRFLAC_MALLOC`, `DRFLAC_REALLOC` and `DRFLAC_FREE`. + +This function will automatically detect whether or not you are attempting to open a native or Ogg encapsulated FLAC, both of which should work seamlessly +without any manual intervention. Ogg encapsulation also works with multiplexed streams which basically means it can play FLAC encoded audio tracks in videos. + +This is the lowest level function for opening a FLAC stream. You can also use `drflac_open_file()` and `drflac_open_memory()` to open the stream from a file or +from a block of memory respectively. + +The STREAMINFO block must be present for this to succeed. Use `drflac_open_relaxed()` to open a FLAC stream where the header may not be present. + +Use `drflac_open_with_metadata()` if you need access to metadata. + + +Seek Also +--------- +drflac_open_file() +drflac_open_memory() +drflac_open_with_metadata() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC stream with relaxed validation of the header block. + + +Parameters +---------- +onRead (in) + The function to call when data needs to be read from the client. + +onSeek (in) + The function to call when the read position of the client data needs to move. + +onTell (in) + The function to call when the read position of the client needs to be queried. + +container (in) + Whether or not the FLAC stream is encapsulated using standard FLAC encapsulation or Ogg encapsulation. + +pUserData (in, optional) + A pointer to application defined data that will be passed to onRead and onSeek. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +The same as drflac_open(), except attempts to open the stream even when a header block is not present. + +Because the header is not necessarily available, the caller must explicitly define the container (Native or Ogg). Do not set this to `drflac_container_unknown` +as that is for internal use only. + +Opening in relaxed mode will continue reading data from onRead until it finds a valid frame. If a frame is never found it will continue forever. To abort, +force your `onRead` callback to return 0, which dr_flac will use as an indicator that the end of the stream was found. + +Use `drflac_open_with_metadata_relaxed()` if you need access to metadata. +*/ +DRFLAC_API drflac* drflac_open_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC decoder and notifies the caller of the metadata chunks (album art, etc.). + + +Parameters +---------- +onRead (in) + The function to call when data needs to be read from the client. + +onSeek (in) + The function to call when the read position of the client data needs to move. + +onTell (in) + The function to call when the read position of the client needs to be queried. + +onMeta (in) + The function to call for every metadata block. + +pUserData (in, optional) + A pointer to application defined data that will be passed to onRead, onSeek and onMeta. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +Close the decoder with `drflac_close()`. + +`pAllocationCallbacks` can be NULL in which case it will use `DRFLAC_MALLOC`, `DRFLAC_REALLOC` and `DRFLAC_FREE`. + +This is slower than `drflac_open()`, so avoid this one if you don't need metadata. Internally, this will allocate and free memory on the heap for every +metadata block except for STREAMINFO and PADDING blocks. + +The caller is notified of the metadata via the `onMeta` callback. All metadata blocks will be handled before the function returns. This callback takes a +pointer to a `drflac_metadata` object which is a union containing the data of all relevant metadata blocks. Use the `type` member to discriminate against +the different metadata types. + +The STREAMINFO block must be present for this to succeed. Use `drflac_open_with_metadata_relaxed()` to open a FLAC stream where the header may not be present. + +Note that this will behave inconsistently with `drflac_open()` if the stream is an Ogg encapsulated stream and a metadata block is corrupted. This is due to +the way the Ogg stream recovers from corrupted pages. When `drflac_open_with_metadata()` is being used, the open routine will try to read the contents of the +metadata block, whereas `drflac_open()` will simply seek past it (for the sake of efficiency). This inconsistency can result in different samples being +returned depending on whether or not the stream is being opened with metadata. + + +Seek Also +--------- +drflac_open_file_with_metadata() +drflac_open_memory_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_with_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +The same as drflac_open_with_metadata(), except attempts to open the stream even when a header block is not present. + +See Also +-------- +drflac_open_with_metadata() +drflac_open_relaxed() +*/ +DRFLAC_API drflac* drflac_open_with_metadata_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Closes the given FLAC decoder. + + +Parameters +---------- +pFlac (in) + The decoder to close. + + +Remarks +------- +This will destroy the decoder object. + + +See Also +-------- +drflac_open() +drflac_open_with_metadata() +drflac_open_file() +drflac_open_file_w() +drflac_open_file_with_metadata() +drflac_open_file_with_metadata_w() +drflac_open_memory() +drflac_open_memory_with_metadata() +*/ +DRFLAC_API void drflac_close(drflac* pFlac); + + +/* +Reads sample data from the given FLAC decoder, output as interleaved signed 32-bit PCM. + + +Parameters +---------- +pFlac (in) + The decoder. + +framesToRead (in) + The number of PCM frames to read. + +pBufferOut (out, optional) + A pointer to the buffer that will receive the decoded samples. + + +Return Value +------------ +Returns the number of PCM frames actually read. If the return value is less than `framesToRead` it has reached the end. + + +Remarks +------- +pBufferOut can be null, in which case the call will act as a seek, and the return value will be the number of frames seeked. +*/ +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s32(drflac* pFlac, drflac_uint64 framesToRead, drflac_int32* pBufferOut); + + +/* +Reads sample data from the given FLAC decoder, output as interleaved signed 16-bit PCM. + + +Parameters +---------- +pFlac (in) + The decoder. + +framesToRead (in) + The number of PCM frames to read. + +pBufferOut (out, optional) + A pointer to the buffer that will receive the decoded samples. + + +Return Value +------------ +Returns the number of PCM frames actually read. If the return value is less than `framesToRead` it has reached the end. + + +Remarks +------- +pBufferOut can be null, in which case the call will act as a seek, and the return value will be the number of frames seeked. + +Note that this is lossy for streams where the bits per sample is larger than 16. +*/ +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s16(drflac* pFlac, drflac_uint64 framesToRead, drflac_int16* pBufferOut); + +/* +Reads sample data from the given FLAC decoder, output as interleaved 32-bit floating point PCM. + + +Parameters +---------- +pFlac (in) + The decoder. + +framesToRead (in) + The number of PCM frames to read. + +pBufferOut (out, optional) + A pointer to the buffer that will receive the decoded samples. + + +Return Value +------------ +Returns the number of PCM frames actually read. If the return value is less than `framesToRead` it has reached the end. + + +Remarks +------- +pBufferOut can be null, in which case the call will act as a seek, and the return value will be the number of frames seeked. + +Note that this should be considered lossy due to the nature of floating point numbers not being able to exactly represent every possible number. +*/ +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_f32(drflac* pFlac, drflac_uint64 framesToRead, float* pBufferOut); + +/* +Seeks to the PCM frame at the given index. + + +Parameters +---------- +pFlac (in) + The decoder. + +pcmFrameIndex (in) + The index of the PCM frame to seek to. See notes below. + + +Return Value +------------- +`DRFLAC_TRUE` if successful; `DRFLAC_FALSE` otherwise. +*/ +DRFLAC_API drflac_bool32 drflac_seek_to_pcm_frame(drflac* pFlac, drflac_uint64 pcmFrameIndex); + + + +#ifndef DR_FLAC_NO_STDIO +/* +Opens a FLAC decoder from the file at the given path. + + +Parameters +---------- +pFileName (in) + The path of the file to open, either absolute or relative to the current directory. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +Close the decoder with drflac_close(). + + +Remarks +------- +This will hold a handle to the file until the decoder is closed with drflac_close(). Some platforms will restrict the number of files a process can have open +at any given time, so keep this mind if you have many decoders open at the same time. + + +See Also +-------- +drflac_open_file_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_file(const char* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks); +DRFLAC_API drflac* drflac_open_file_w(const wchar_t* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC decoder from the file at the given path and notifies the caller of the metadata chunks (album art, etc.) + + +Parameters +---------- +pFileName (in) + The path of the file to open, either absolute or relative to the current directory. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + +onMeta (in) + The callback to fire for each metadata block. + +pUserData (in) + A pointer to the user data to pass to the metadata callback. + +pAllocationCallbacks (in) + A pointer to application defined callbacks for managing memory allocations. + + +Remarks +------- +Look at the documentation for drflac_open_with_metadata() for more information on how metadata is handled. + + +See Also +-------- +drflac_open_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_file_with_metadata(const char* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); +DRFLAC_API drflac* drflac_open_file_with_metadata_w(const wchar_t* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); +#endif + +/* +Opens a FLAC decoder from a pre-allocated block of memory + + +Parameters +---------- +pData (in) + A pointer to the raw encoded FLAC data. + +dataSize (in) + The size in bytes of `data`. + +pAllocationCallbacks (in) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +This does not create a copy of the data. It is up to the application to ensure the buffer remains valid for the lifetime of the decoder. + + +See Also +-------- +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_memory(const void* pData, size_t dataSize, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC decoder from a pre-allocated block of memory and notifies the caller of the metadata chunks (album art, etc.) + + +Parameters +---------- +pData (in) + A pointer to the raw encoded FLAC data. + +dataSize (in) + The size in bytes of `data`. + +onMeta (in) + The callback to fire for each metadata block. + +pUserData (in) + A pointer to the user data to pass to the metadata callback. + +pAllocationCallbacks (in) + A pointer to application defined callbacks for managing memory allocations. + + +Remarks +------- +Look at the documentation for drflac_open_with_metadata() for more information on how metadata is handled. + + +See Also +------- +drflac_open_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_memory_with_metadata(const void* pData, size_t dataSize, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + + + +/* High Level APIs */ + +/* +Opens a FLAC stream from the given callbacks and fully decodes it in a single operation. The return value is a +pointer to the sample data as interleaved signed 32-bit PCM. The returned data must be freed with drflac_free(). + +You can pass in custom memory allocation callbacks via the pAllocationCallbacks parameter. This can be NULL in which +case it will use DRFLAC_MALLOC, DRFLAC_REALLOC and DRFLAC_FREE. + +Sometimes a FLAC file won't keep track of the total sample count. In this situation the function will continuously +read samples into a dynamically sized buffer on the heap until no samples are left. + +Do not call this function on a broadcast type of stream (like internet radio streams and whatnot). +*/ +DRFLAC_API drflac_int32* drflac_open_and_read_pcm_frames_s32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_and_read_pcm_frames_s32(), except returns signed 16-bit integer samples. */ +DRFLAC_API drflac_int16* drflac_open_and_read_pcm_frames_s16(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_and_read_pcm_frames_s32(), except returns 32-bit floating-point samples. */ +DRFLAC_API float* drflac_open_and_read_pcm_frames_f32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +#ifndef DR_FLAC_NO_STDIO +/* Same as drflac_open_and_read_pcm_frames_s32() except opens the decoder from a file. */ +DRFLAC_API drflac_int32* drflac_open_file_and_read_pcm_frames_s32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_file_and_read_pcm_frames_s32(), except returns signed 16-bit integer samples. */ +DRFLAC_API drflac_int16* drflac_open_file_and_read_pcm_frames_s16(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_file_and_read_pcm_frames_s32(), except returns 32-bit floating-point samples. */ +DRFLAC_API float* drflac_open_file_and_read_pcm_frames_f32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); +#endif + +/* Same as drflac_open_and_read_pcm_frames_s32() except opens the decoder from a block of memory. */ +DRFLAC_API drflac_int32* drflac_open_memory_and_read_pcm_frames_s32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_memory_and_read_pcm_frames_s32(), except returns signed 16-bit integer samples. */ +DRFLAC_API drflac_int16* drflac_open_memory_and_read_pcm_frames_s16(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_memory_and_read_pcm_frames_s32(), except returns 32-bit floating-point samples. */ +DRFLAC_API float* drflac_open_memory_and_read_pcm_frames_f32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Frees memory that was allocated internally by dr_flac. + +Set pAllocationCallbacks to the same object that was passed to drflac_open_*_and_read_pcm_frames_*(). If you originally passed in NULL, pass in NULL for this. +*/ +DRFLAC_API void drflac_free(void* p, const drflac_allocation_callbacks* pAllocationCallbacks); + + +/* Structure representing an iterator for vorbis comments in a VORBIS_COMMENT metadata block. */ +typedef struct +{ + drflac_uint32 countRemaining; + const char* pRunningData; +} drflac_vorbis_comment_iterator; + +/* +Initializes a vorbis comment iterator. This can be used for iterating over the vorbis comments in a VORBIS_COMMENT +metadata block. +*/ +DRFLAC_API void drflac_init_vorbis_comment_iterator(drflac_vorbis_comment_iterator* pIter, drflac_uint32 commentCount, const void* pComments); + +/* +Goes to the next vorbis comment in the given iterator. If null is returned it means there are no more comments. The +returned string is NOT null terminated. +*/ +DRFLAC_API const char* drflac_next_vorbis_comment(drflac_vorbis_comment_iterator* pIter, drflac_uint32* pCommentLengthOut); + + +/* Structure representing an iterator for cuesheet tracks in a CUESHEET metadata block. */ +typedef struct +{ + drflac_uint32 countRemaining; + const char* pRunningData; +} drflac_cuesheet_track_iterator; + +/* The order of members here is important because we map this directly to the raw data within the CUESHEET metadata block. */ +typedef struct +{ + drflac_uint64 offset; + drflac_uint8 index; + drflac_uint8 reserved[3]; +} drflac_cuesheet_track_index; + +typedef struct +{ + drflac_uint64 offset; + drflac_uint8 trackNumber; + char ISRC[12]; + drflac_bool8 isAudio; + drflac_bool8 preEmphasis; + drflac_uint8 indexCount; + const drflac_cuesheet_track_index* pIndexPoints; +} drflac_cuesheet_track; + +/* +Initializes a cuesheet track iterator. This can be used for iterating over the cuesheet tracks in a CUESHEET metadata +block. +*/ +DRFLAC_API void drflac_init_cuesheet_track_iterator(drflac_cuesheet_track_iterator* pIter, drflac_uint32 trackCount, const void* pTrackData); + +/* Goes to the next cuesheet track in the given iterator. If DRFLAC_FALSE is returned it means there are no more comments. */ +DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterator* pIter, drflac_cuesheet_track* pCuesheetTrack); + + +#ifdef __cplusplus +} +#endif +#endif /* dr_flac_h */ + + +/************************************************************************************************************************************************************ + ************************************************************************************************************************************************************ + + IMPLEMENTATION + + ************************************************************************************************************************************************************ + ************************************************************************************************************************************************************/ +#if defined(DR_FLAC_IMPLEMENTATION) || defined(DRFLAC_IMPLEMENTATION) +#ifndef dr_flac_c +#define dr_flac_c + +/* Disable some annoying warnings. */ +#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #if __GNUC__ >= 7 + #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" + #endif +#endif + +#ifdef __linux__ + #ifndef _BSD_SOURCE + #define _BSD_SOURCE + #endif + #ifndef _DEFAULT_SOURCE + #define _DEFAULT_SOURCE + #endif + #ifndef __USE_BSD + #define __USE_BSD + #endif + #include +#endif + +#include +#include + +/* Inline */ +#ifdef _MSC_VER + #define DRFLAC_INLINE __forceinline +#elif defined(__GNUC__) + /* + I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when + the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some + case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the + command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue + I am using "__inline__" only when we're compiling in strict ANSI mode. + */ + #if defined(__STRICT_ANSI__) + #define DRFLAC_GNUC_INLINE_HINT __inline__ + #else + #define DRFLAC_GNUC_INLINE_HINT inline + #endif + + #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) + #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT __attribute__((always_inline)) + #else + #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT + #endif +#elif defined(__WATCOMC__) + #define DRFLAC_INLINE __inline +#else + #define DRFLAC_INLINE +#endif +/* End Inline */ + +/* +Intrinsics Support + +There's a bug in GCC 4.2.x which results in an incorrect compilation error when using _mm_slli_epi32() where it complains with + + "error: shift must be an immediate" + +Unfortuantely dr_flac depends on this for a few things so we're just going to disable SSE on GCC 4.2 and below. +*/ +#if !defined(DR_FLAC_NO_SIMD) + #if defined(DRFLAC_X64) || defined(DRFLAC_X86) + #if defined(_MSC_VER) && !defined(__clang__) + /* MSVC. */ + #if _MSC_VER >= 1400 && !defined(DRFLAC_NO_SSE2) /* 2005 */ + #define DRFLAC_SUPPORT_SSE2 + #endif + #if _MSC_VER >= 1600 && !defined(DRFLAC_NO_SSE41) /* 2010 */ + #define DRFLAC_SUPPORT_SSE41 + #endif + #elif defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))) + /* Assume GNUC-style. */ + #if defined(__SSE2__) && !defined(DRFLAC_NO_SSE2) + #define DRFLAC_SUPPORT_SSE2 + #endif + #if defined(__SSE4_1__) && !defined(DRFLAC_NO_SSE41) + #define DRFLAC_SUPPORT_SSE41 + #endif + #endif + + /* If at this point we still haven't determined compiler support for the intrinsics just fall back to __has_include. */ + #if !defined(__GNUC__) && !defined(__clang__) && defined(__has_include) + #if !defined(DRFLAC_SUPPORT_SSE2) && !defined(DRFLAC_NO_SSE2) && __has_include() + #define DRFLAC_SUPPORT_SSE2 + #endif + #if !defined(DRFLAC_SUPPORT_SSE41) && !defined(DRFLAC_NO_SSE41) && __has_include() + #define DRFLAC_SUPPORT_SSE41 + #endif + #endif + + #if defined(DRFLAC_SUPPORT_SSE41) + #include + #elif defined(DRFLAC_SUPPORT_SSE2) + #include + #endif + #endif + + #if defined(DRFLAC_ARM) + #if !defined(DRFLAC_NO_NEON) && (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64)) + #define DRFLAC_SUPPORT_NEON + #include + #endif + #endif +#endif + +/* Compile-time CPU feature support. */ +#if !defined(DR_FLAC_NO_SIMD) && (defined(DRFLAC_X86) || defined(DRFLAC_X64)) + #if defined(_MSC_VER) && !defined(__clang__) + #if _MSC_VER >= 1400 + #include + static void drflac__cpuid(int info[4], int fid) + { + __cpuid(info, fid); + } + #else + #define DRFLAC_NO_CPUID + #endif + #else + #if defined(__GNUC__) || defined(__clang__) + static void drflac__cpuid(int info[4], int fid) + { + /* + It looks like the -fPIC option uses the ebx register which GCC complains about. We can work around this by just using a different register, the + specific register of which I'm letting the compiler decide on. The "k" prefix is used to specify a 32-bit register. The {...} syntax is for + supporting different assembly dialects. + + What's basically happening is that we're saving and restoring the ebx register manually. + */ + #if defined(DRFLAC_X86) && defined(__PIC__) + __asm__ __volatile__ ( + "xchg{l} {%%}ebx, %k1;" + "cpuid;" + "xchg{l} {%%}ebx, %k1;" + : "=a"(info[0]), "=&r"(info[1]), "=c"(info[2]), "=d"(info[3]) : "a"(fid), "c"(0) + ); + #else + __asm__ __volatile__ ( + "cpuid" : "=a"(info[0]), "=b"(info[1]), "=c"(info[2]), "=d"(info[3]) : "a"(fid), "c"(0) + ); + #endif + } + #else + #define DRFLAC_NO_CPUID + #endif + #endif +#else + #define DRFLAC_NO_CPUID +#endif + +static DRFLAC_INLINE drflac_bool32 drflac_has_sse2(void) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + #if (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(DRFLAC_NO_SSE2) + #if defined(DRFLAC_X64) + return DRFLAC_TRUE; /* 64-bit targets always support SSE2. */ + #elif (defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__) + return DRFLAC_TRUE; /* If the compiler is allowed to freely generate SSE2 code we can assume support. */ + #else + #if defined(DRFLAC_NO_CPUID) + return DRFLAC_FALSE; + #else + int info[4]; + drflac__cpuid(info, 1); + return (info[3] & (1 << 26)) != 0; + #endif + #endif + #else + return DRFLAC_FALSE; /* SSE2 is only supported on x86 and x64 architectures. */ + #endif +#else + return DRFLAC_FALSE; /* No compiler support. */ +#endif +} + +static DRFLAC_INLINE drflac_bool32 drflac_has_sse41(void) +{ +#if defined(DRFLAC_SUPPORT_SSE41) + #if (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(DRFLAC_NO_SSE41) + #if defined(__SSE4_1__) || defined(__AVX__) + return DRFLAC_TRUE; /* If the compiler is allowed to freely generate SSE41 code we can assume support. */ + #else + #if defined(DRFLAC_NO_CPUID) + return DRFLAC_FALSE; + #else + int info[4]; + drflac__cpuid(info, 1); + return (info[2] & (1 << 19)) != 0; + #endif + #endif + #else + return DRFLAC_FALSE; /* SSE41 is only supported on x86 and x64 architectures. */ + #endif +#else + return DRFLAC_FALSE; /* No compiler support. */ +#endif +} + + +#if defined(_MSC_VER) && _MSC_VER >= 1500 && (defined(DRFLAC_X86) || defined(DRFLAC_X64)) && !defined(__clang__) + #define DRFLAC_HAS_LZCNT_INTRINSIC +#elif (defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) + #define DRFLAC_HAS_LZCNT_INTRINSIC +#elif defined(__clang__) + #if defined(__has_builtin) + #if __has_builtin(__builtin_clzll) || __has_builtin(__builtin_clzl) + #define DRFLAC_HAS_LZCNT_INTRINSIC + #endif + #endif +#endif + +#if defined(_MSC_VER) && _MSC_VER >= 1400 && !defined(__clang__) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC +#elif defined(__clang__) + #if defined(__has_builtin) + #if __has_builtin(__builtin_bswap16) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #endif + #if __has_builtin(__builtin_bswap32) + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #endif + #if __has_builtin(__builtin_bswap64) + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC + #endif + #endif +#elif defined(__GNUC__) + #if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC + #endif + #if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #endif +#elif defined(__WATCOMC__) && defined(__386__) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC + extern __inline drflac_uint16 _watcom_bswap16(drflac_uint16); + extern __inline drflac_uint32 _watcom_bswap32(drflac_uint32); + extern __inline drflac_uint64 _watcom_bswap64(drflac_uint64); +#pragma aux _watcom_bswap16 = \ + "xchg al, ah" \ + parm [ax] \ + value [ax] \ + modify nomemory; +#pragma aux _watcom_bswap32 = \ + "bswap eax" \ + parm [eax] \ + value [eax] \ + modify nomemory; +#pragma aux _watcom_bswap64 = \ + "bswap eax" \ + "bswap edx" \ + "xchg eax,edx" \ + parm [eax edx] \ + value [eax edx] \ + modify nomemory; +#endif + + +/* Standard library stuff. */ +#ifndef DRFLAC_ASSERT +#include +#define DRFLAC_ASSERT(expression) assert(expression) +#endif +#ifndef DRFLAC_MALLOC +#define DRFLAC_MALLOC(sz) malloc((sz)) +#endif +#ifndef DRFLAC_REALLOC +#define DRFLAC_REALLOC(p, sz) realloc((p), (sz)) +#endif +#ifndef DRFLAC_FREE +#define DRFLAC_FREE(p) free((p)) +#endif +#ifndef DRFLAC_COPY_MEMORY +#define DRFLAC_COPY_MEMORY(dst, src, sz) memcpy((dst), (src), (sz)) +#endif +#ifndef DRFLAC_ZERO_MEMORY +#define DRFLAC_ZERO_MEMORY(p, sz) memset((p), 0, (sz)) +#endif +#ifndef DRFLAC_ZERO_OBJECT +#define DRFLAC_ZERO_OBJECT(p) DRFLAC_ZERO_MEMORY((p), sizeof(*(p))) +#endif + +#define DRFLAC_MIN(a, b) (((a) < (b)) ? (a) : (b)) + +#define DRFLAC_MAX_SIMD_VECTOR_SIZE 64 /* 64 for AVX-512 in the future. */ + +/* Result Codes */ +typedef drflac_int32 drflac_result; +#define DRFLAC_SUCCESS 0 +#define DRFLAC_ERROR -1 /* A generic error. */ +#define DRFLAC_INVALID_ARGS -2 +#define DRFLAC_INVALID_OPERATION -3 +#define DRFLAC_OUT_OF_MEMORY -4 +#define DRFLAC_OUT_OF_RANGE -5 +#define DRFLAC_ACCESS_DENIED -6 +#define DRFLAC_DOES_NOT_EXIST -7 +#define DRFLAC_ALREADY_EXISTS -8 +#define DRFLAC_TOO_MANY_OPEN_FILES -9 +#define DRFLAC_INVALID_FILE -10 +#define DRFLAC_TOO_BIG -11 +#define DRFLAC_PATH_TOO_LONG -12 +#define DRFLAC_NAME_TOO_LONG -13 +#define DRFLAC_NOT_DIRECTORY -14 +#define DRFLAC_IS_DIRECTORY -15 +#define DRFLAC_DIRECTORY_NOT_EMPTY -16 +#define DRFLAC_END_OF_FILE -17 +#define DRFLAC_NO_SPACE -18 +#define DRFLAC_BUSY -19 +#define DRFLAC_IO_ERROR -20 +#define DRFLAC_INTERRUPT -21 +#define DRFLAC_UNAVAILABLE -22 +#define DRFLAC_ALREADY_IN_USE -23 +#define DRFLAC_BAD_ADDRESS -24 +#define DRFLAC_BAD_SEEK -25 +#define DRFLAC_BAD_PIPE -26 +#define DRFLAC_DEADLOCK -27 +#define DRFLAC_TOO_MANY_LINKS -28 +#define DRFLAC_NOT_IMPLEMENTED -29 +#define DRFLAC_NO_MESSAGE -30 +#define DRFLAC_BAD_MESSAGE -31 +#define DRFLAC_NO_DATA_AVAILABLE -32 +#define DRFLAC_INVALID_DATA -33 +#define DRFLAC_TIMEOUT -34 +#define DRFLAC_NO_NETWORK -35 +#define DRFLAC_NOT_UNIQUE -36 +#define DRFLAC_NOT_SOCKET -37 +#define DRFLAC_NO_ADDRESS -38 +#define DRFLAC_BAD_PROTOCOL -39 +#define DRFLAC_PROTOCOL_UNAVAILABLE -40 +#define DRFLAC_PROTOCOL_NOT_SUPPORTED -41 +#define DRFLAC_PROTOCOL_FAMILY_NOT_SUPPORTED -42 +#define DRFLAC_ADDRESS_FAMILY_NOT_SUPPORTED -43 +#define DRFLAC_SOCKET_NOT_SUPPORTED -44 +#define DRFLAC_CONNECTION_RESET -45 +#define DRFLAC_ALREADY_CONNECTED -46 +#define DRFLAC_NOT_CONNECTED -47 +#define DRFLAC_CONNECTION_REFUSED -48 +#define DRFLAC_NO_HOST -49 +#define DRFLAC_IN_PROGRESS -50 +#define DRFLAC_CANCELLED -51 +#define DRFLAC_MEMORY_ALREADY_MAPPED -52 +#define DRFLAC_AT_END -53 + +#define DRFLAC_CRC_MISMATCH -100 +/* End Result Codes */ + + +#define DRFLAC_SUBFRAME_CONSTANT 0 +#define DRFLAC_SUBFRAME_VERBATIM 1 +#define DRFLAC_SUBFRAME_FIXED 8 +#define DRFLAC_SUBFRAME_LPC 32 +#define DRFLAC_SUBFRAME_RESERVED 255 + +#define DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE 0 +#define DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2 1 + +#define DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT 0 +#define DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE 8 +#define DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE 9 +#define DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE 10 + +#define DRFLAC_SEEKPOINT_SIZE_IN_BYTES 18 +#define DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES 36 +#define DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES 12 + +#define drflac_align(x, a) ((((x) + (a) - 1) / (a)) * (a)) + + +DRFLAC_API void drflac_version(drflac_uint32* pMajor, drflac_uint32* pMinor, drflac_uint32* pRevision) +{ + if (pMajor) { + *pMajor = DRFLAC_VERSION_MAJOR; + } + + if (pMinor) { + *pMinor = DRFLAC_VERSION_MINOR; + } + + if (pRevision) { + *pRevision = DRFLAC_VERSION_REVISION; + } +} + +DRFLAC_API const char* drflac_version_string(void) +{ + return DRFLAC_VERSION_STRING; +} + + +/* CPU caps. */ +#if defined(__has_feature) + #if __has_feature(thread_sanitizer) + #define DRFLAC_NO_THREAD_SANITIZE __attribute__((no_sanitize("thread"))) + #else + #define DRFLAC_NO_THREAD_SANITIZE + #endif +#else + #define DRFLAC_NO_THREAD_SANITIZE +#endif + +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) +static drflac_bool32 drflac__gIsLZCNTSupported = DRFLAC_FALSE; +#endif + +#ifndef DRFLAC_NO_CPUID +static drflac_bool32 drflac__gIsSSE2Supported = DRFLAC_FALSE; +static drflac_bool32 drflac__gIsSSE41Supported = DRFLAC_FALSE; + +/* +I've had a bug report that Clang's ThreadSanitizer presents a warning in this function. Having reviewed this, this does +actually make sense. However, since CPU caps should never differ for a running process, I don't think the trade off of +complicating internal API's by passing around CPU caps versus just disabling the warnings is worthwhile. I'm therefore +just going to disable these warnings. This is disabled via the DRFLAC_NO_THREAD_SANITIZE attribute. +*/ +DRFLAC_NO_THREAD_SANITIZE static void drflac__init_cpu_caps(void) +{ + static drflac_bool32 isCPUCapsInitialized = DRFLAC_FALSE; + + if (!isCPUCapsInitialized) { + /* LZCNT */ +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) + int info[4] = {0}; + drflac__cpuid(info, 0x80000001); + drflac__gIsLZCNTSupported = (info[2] & (1 << 5)) != 0; +#endif + + /* SSE2 */ + drflac__gIsSSE2Supported = drflac_has_sse2(); + + /* SSE4.1 */ + drflac__gIsSSE41Supported = drflac_has_sse41(); + + /* Initialized. */ + isCPUCapsInitialized = DRFLAC_TRUE; + } +} +#else +static drflac_bool32 drflac__gIsNEONSupported = DRFLAC_FALSE; + +static DRFLAC_INLINE drflac_bool32 drflac__has_neon(void) +{ +#if defined(DRFLAC_SUPPORT_NEON) + #if defined(DRFLAC_ARM) && !defined(DRFLAC_NO_NEON) + #if (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64)) + return DRFLAC_TRUE; /* If the compiler is allowed to freely generate NEON code we can assume support. */ + #else + /* TODO: Runtime check. */ + return DRFLAC_FALSE; + #endif + #else + return DRFLAC_FALSE; /* NEON is only supported on ARM architectures. */ + #endif +#else + return DRFLAC_FALSE; /* No compiler support. */ +#endif +} + +DRFLAC_NO_THREAD_SANITIZE static void drflac__init_cpu_caps(void) +{ + drflac__gIsNEONSupported = drflac__has_neon(); + +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) && defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) + drflac__gIsLZCNTSupported = DRFLAC_TRUE; +#endif +} +#endif + + +/* Endian Management */ +static DRFLAC_INLINE drflac_bool32 drflac__is_little_endian(void) +{ +#if defined(DRFLAC_X86) || defined(DRFLAC_X64) + return DRFLAC_TRUE; +#elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN + return DRFLAC_TRUE; +#else + int n = 1; + return (*(char*)&n) == 1; +#endif +} + +static DRFLAC_INLINE drflac_uint16 drflac__swap_endian_uint16(drflac_uint16 n) +{ +#ifdef DRFLAC_HAS_BYTESWAP16_INTRINSIC + #if defined(_MSC_VER) && !defined(__clang__) + return _byteswap_ushort(n); + #elif defined(__GNUC__) || defined(__clang__) + return __builtin_bswap16(n); + #elif defined(__WATCOMC__) && defined(__386__) + return _watcom_bswap16(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + return ((n & 0xFF00) >> 8) | + ((n & 0x00FF) << 8); +#endif +} + +static DRFLAC_INLINE drflac_uint32 drflac__swap_endian_uint32(drflac_uint32 n) +{ +#ifdef DRFLAC_HAS_BYTESWAP32_INTRINSIC + #if defined(_MSC_VER) && !defined(__clang__) + return _byteswap_ulong(n); + #elif defined(__GNUC__) || defined(__clang__) + #if defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 6) && !defined(__ARM_ARCH_6M__) && !defined(DRFLAC_64BIT) /* <-- 64-bit inline assembly has not been tested, so disabling for now. */ + /* Inline assembly optimized implementation for ARM. In my testing, GCC does not generate optimized code with __builtin_bswap32(). */ + drflac_uint32 r; + __asm__ __volatile__ ( + #if defined(DRFLAC_64BIT) + "rev %w[out], %w[in]" : [out]"=r"(r) : [in]"r"(n) /* <-- This is untested. If someone in the community could test this, that would be appreciated! */ + #else + "rev %[out], %[in]" : [out]"=r"(r) : [in]"r"(n) + #endif + ); + return r; + #else + return __builtin_bswap32(n); + #endif + #elif defined(__WATCOMC__) && defined(__386__) + return _watcom_bswap32(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + return ((n & 0xFF000000) >> 24) | + ((n & 0x00FF0000) >> 8) | + ((n & 0x0000FF00) << 8) | + ((n & 0x000000FF) << 24); +#endif +} + +static DRFLAC_INLINE drflac_uint64 drflac__swap_endian_uint64(drflac_uint64 n) +{ +#ifdef DRFLAC_HAS_BYTESWAP64_INTRINSIC + #if defined(_MSC_VER) && !defined(__clang__) + return _byteswap_uint64(n); + #elif defined(__GNUC__) || defined(__clang__) + return __builtin_bswap64(n); + #elif defined(__WATCOMC__) && defined(__386__) + return _watcom_bswap64(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + /* Weird "<< 32" bitshift is required for C89 because it doesn't support 64-bit constants. Should be optimized out by a good compiler. */ + return ((n & ((drflac_uint64)0xFF000000 << 32)) >> 56) | + ((n & ((drflac_uint64)0x00FF0000 << 32)) >> 40) | + ((n & ((drflac_uint64)0x0000FF00 << 32)) >> 24) | + ((n & ((drflac_uint64)0x000000FF << 32)) >> 8) | + ((n & ((drflac_uint64)0xFF000000 )) << 8) | + ((n & ((drflac_uint64)0x00FF0000 )) << 24) | + ((n & ((drflac_uint64)0x0000FF00 )) << 40) | + ((n & ((drflac_uint64)0x000000FF )) << 56); +#endif +} + + +static DRFLAC_INLINE drflac_uint16 drflac__be2host_16(drflac_uint16 n) +{ + if (drflac__is_little_endian()) { + return drflac__swap_endian_uint16(n); + } + + return n; +} + +static DRFLAC_INLINE drflac_uint32 drflac__be2host_32(drflac_uint32 n) +{ + if (drflac__is_little_endian()) { + return drflac__swap_endian_uint32(n); + } + + return n; +} + +static DRFLAC_INLINE drflac_uint32 drflac__be2host_32_ptr_unaligned(const void* pData) +{ + const drflac_uint8* pNum = (drflac_uint8*)pData; + return *(pNum) << 24 | *(pNum+1) << 16 | *(pNum+2) << 8 | *(pNum+3); +} + +static DRFLAC_INLINE drflac_uint64 drflac__be2host_64(drflac_uint64 n) +{ + if (drflac__is_little_endian()) { + return drflac__swap_endian_uint64(n); + } + + return n; +} + + +static DRFLAC_INLINE drflac_uint32 drflac__le2host_32(drflac_uint32 n) +{ + if (!drflac__is_little_endian()) { + return drflac__swap_endian_uint32(n); + } + + return n; +} + +static DRFLAC_INLINE drflac_uint32 drflac__le2host_32_ptr_unaligned(const void* pData) +{ + const drflac_uint8* pNum = (drflac_uint8*)pData; + return *pNum | *(pNum+1) << 8 | *(pNum+2) << 16 | *(pNum+3) << 24; +} + + +static DRFLAC_INLINE drflac_uint32 drflac__unsynchsafe_32(drflac_uint32 n) +{ + drflac_uint32 result = 0; + result |= (n & 0x7F000000) >> 3; + result |= (n & 0x007F0000) >> 2; + result |= (n & 0x00007F00) >> 1; + result |= (n & 0x0000007F) >> 0; + + return result; +} + + + +/* The CRC code below is based on this document: http://zlib.net/crc_v3.txt */ +static drflac_uint8 drflac__crc8_table[] = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, + 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, + 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, + 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, + 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, + 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, + 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, + 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, + 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, + 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, + 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 +}; + +static drflac_uint16 drflac__crc16_table[] = { + 0x0000, 0x8005, 0x800F, 0x000A, 0x801B, 0x001E, 0x0014, 0x8011, + 0x8033, 0x0036, 0x003C, 0x8039, 0x0028, 0x802D, 0x8027, 0x0022, + 0x8063, 0x0066, 0x006C, 0x8069, 0x0078, 0x807D, 0x8077, 0x0072, + 0x0050, 0x8055, 0x805F, 0x005A, 0x804B, 0x004E, 0x0044, 0x8041, + 0x80C3, 0x00C6, 0x00CC, 0x80C9, 0x00D8, 0x80DD, 0x80D7, 0x00D2, + 0x00F0, 0x80F5, 0x80FF, 0x00FA, 0x80EB, 0x00EE, 0x00E4, 0x80E1, + 0x00A0, 0x80A5, 0x80AF, 0x00AA, 0x80BB, 0x00BE, 0x00B4, 0x80B1, + 0x8093, 0x0096, 0x009C, 0x8099, 0x0088, 0x808D, 0x8087, 0x0082, + 0x8183, 0x0186, 0x018C, 0x8189, 0x0198, 0x819D, 0x8197, 0x0192, + 0x01B0, 0x81B5, 0x81BF, 0x01BA, 0x81AB, 0x01AE, 0x01A4, 0x81A1, + 0x01E0, 0x81E5, 0x81EF, 0x01EA, 0x81FB, 0x01FE, 0x01F4, 0x81F1, + 0x81D3, 0x01D6, 0x01DC, 0x81D9, 0x01C8, 0x81CD, 0x81C7, 0x01C2, + 0x0140, 0x8145, 0x814F, 0x014A, 0x815B, 0x015E, 0x0154, 0x8151, + 0x8173, 0x0176, 0x017C, 0x8179, 0x0168, 0x816D, 0x8167, 0x0162, + 0x8123, 0x0126, 0x012C, 0x8129, 0x0138, 0x813D, 0x8137, 0x0132, + 0x0110, 0x8115, 0x811F, 0x011A, 0x810B, 0x010E, 0x0104, 0x8101, + 0x8303, 0x0306, 0x030C, 0x8309, 0x0318, 0x831D, 0x8317, 0x0312, + 0x0330, 0x8335, 0x833F, 0x033A, 0x832B, 0x032E, 0x0324, 0x8321, + 0x0360, 0x8365, 0x836F, 0x036A, 0x837B, 0x037E, 0x0374, 0x8371, + 0x8353, 0x0356, 0x035C, 0x8359, 0x0348, 0x834D, 0x8347, 0x0342, + 0x03C0, 0x83C5, 0x83CF, 0x03CA, 0x83DB, 0x03DE, 0x03D4, 0x83D1, + 0x83F3, 0x03F6, 0x03FC, 0x83F9, 0x03E8, 0x83ED, 0x83E7, 0x03E2, + 0x83A3, 0x03A6, 0x03AC, 0x83A9, 0x03B8, 0x83BD, 0x83B7, 0x03B2, + 0x0390, 0x8395, 0x839F, 0x039A, 0x838B, 0x038E, 0x0384, 0x8381, + 0x0280, 0x8285, 0x828F, 0x028A, 0x829B, 0x029E, 0x0294, 0x8291, + 0x82B3, 0x02B6, 0x02BC, 0x82B9, 0x02A8, 0x82AD, 0x82A7, 0x02A2, + 0x82E3, 0x02E6, 0x02EC, 0x82E9, 0x02F8, 0x82FD, 0x82F7, 0x02F2, + 0x02D0, 0x82D5, 0x82DF, 0x02DA, 0x82CB, 0x02CE, 0x02C4, 0x82C1, + 0x8243, 0x0246, 0x024C, 0x8249, 0x0258, 0x825D, 0x8257, 0x0252, + 0x0270, 0x8275, 0x827F, 0x027A, 0x826B, 0x026E, 0x0264, 0x8261, + 0x0220, 0x8225, 0x822F, 0x022A, 0x823B, 0x023E, 0x0234, 0x8231, + 0x8213, 0x0216, 0x021C, 0x8219, 0x0208, 0x820D, 0x8207, 0x0202 +}; + +static DRFLAC_INLINE drflac_uint8 drflac_crc8_byte(drflac_uint8 crc, drflac_uint8 data) +{ + return drflac__crc8_table[crc ^ data]; +} + +static DRFLAC_INLINE drflac_uint8 drflac_crc8(drflac_uint8 crc, drflac_uint32 data, drflac_uint32 count) +{ +#ifdef DR_FLAC_NO_CRC + (void)crc; + (void)data; + (void)count; + return 0; +#else +#if 0 + /* REFERENCE (use of this implementation requires an explicit flush by doing "drflac_crc8(crc, 0, 8);") */ + drflac_uint8 p = 0x07; + for (int i = count-1; i >= 0; --i) { + drflac_uint8 bit = (data & (1 << i)) >> i; + if (crc & 0x80) { + crc = ((crc << 1) | bit) ^ p; + } else { + crc = ((crc << 1) | bit); + } + } + return crc; +#else + drflac_uint32 wholeBytes; + drflac_uint32 leftoverBits; + drflac_uint64 leftoverDataMask; + + static drflac_uint64 leftoverDataMaskTable[8] = { + 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F + }; + + DRFLAC_ASSERT(count <= 32); + + wholeBytes = count >> 3; + leftoverBits = count - (wholeBytes*8); + leftoverDataMask = leftoverDataMaskTable[leftoverBits]; + + switch (wholeBytes) { + case 4: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0xFF000000UL << leftoverBits)) >> (24 + leftoverBits))); + case 3: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0x00FF0000UL << leftoverBits)) >> (16 + leftoverBits))); + case 2: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0x0000FF00UL << leftoverBits)) >> ( 8 + leftoverBits))); + case 1: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0x000000FFUL << leftoverBits)) >> ( 0 + leftoverBits))); + case 0: if (leftoverBits > 0) crc = (drflac_uint8)((crc << leftoverBits) ^ drflac__crc8_table[(crc >> (8 - leftoverBits)) ^ (data & leftoverDataMask)]); + } + return crc; +#endif +#endif +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16_byte(drflac_uint16 crc, drflac_uint8 data) +{ + return (crc << 8) ^ drflac__crc16_table[(drflac_uint8)(crc >> 8) ^ data]; +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16_cache(drflac_uint16 crc, drflac_cache_t data) +{ +#ifdef DRFLAC_64BIT + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 56) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 48) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 40) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 32) & 0xFF)); +#endif + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 24) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 16) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 8) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 0) & 0xFF)); + + return crc; +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16_bytes(drflac_uint16 crc, drflac_cache_t data, drflac_uint32 byteCount) +{ + switch (byteCount) + { +#ifdef DRFLAC_64BIT + case 8: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 56) & 0xFF)); + case 7: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 48) & 0xFF)); + case 6: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 40) & 0xFF)); + case 5: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 32) & 0xFF)); +#endif + case 4: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 24) & 0xFF)); + case 3: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 16) & 0xFF)); + case 2: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 8) & 0xFF)); + case 1: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 0) & 0xFF)); + } + + return crc; +} + +#if 0 +static DRFLAC_INLINE drflac_uint16 drflac_crc16__32bit(drflac_uint16 crc, drflac_uint32 data, drflac_uint32 count) +{ +#ifdef DR_FLAC_NO_CRC + (void)crc; + (void)data; + (void)count; + return 0; +#else +#if 0 + /* REFERENCE (use of this implementation requires an explicit flush by doing "drflac_crc16(crc, 0, 16);") */ + drflac_uint16 p = 0x8005; + for (int i = count-1; i >= 0; --i) { + drflac_uint16 bit = (data & (1ULL << i)) >> i; + if (r & 0x8000) { + r = ((r << 1) | bit) ^ p; + } else { + r = ((r << 1) | bit); + } + } + + return crc; +#else + drflac_uint32 wholeBytes; + drflac_uint32 leftoverBits; + drflac_uint64 leftoverDataMask; + + static drflac_uint64 leftoverDataMaskTable[8] = { + 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F + }; + + DRFLAC_ASSERT(count <= 64); + + wholeBytes = count >> 3; + leftoverBits = count & 7; + leftoverDataMask = leftoverDataMaskTable[leftoverBits]; + + switch (wholeBytes) { + default: + case 4: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0xFF000000UL << leftoverBits)) >> (24 + leftoverBits))); + case 3: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0x00FF0000UL << leftoverBits)) >> (16 + leftoverBits))); + case 2: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0x0000FF00UL << leftoverBits)) >> ( 8 + leftoverBits))); + case 1: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0x000000FFUL << leftoverBits)) >> ( 0 + leftoverBits))); + case 0: if (leftoverBits > 0) crc = (crc << leftoverBits) ^ drflac__crc16_table[(crc >> (16 - leftoverBits)) ^ (data & leftoverDataMask)]; + } + return crc; +#endif +#endif +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16__64bit(drflac_uint16 crc, drflac_uint64 data, drflac_uint32 count) +{ +#ifdef DR_FLAC_NO_CRC + (void)crc; + (void)data; + (void)count; + return 0; +#else + drflac_uint32 wholeBytes; + drflac_uint32 leftoverBits; + drflac_uint64 leftoverDataMask; + + static drflac_uint64 leftoverDataMaskTable[8] = { + 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F + }; + + DRFLAC_ASSERT(count <= 64); + + wholeBytes = count >> 3; + leftoverBits = count & 7; + leftoverDataMask = leftoverDataMaskTable[leftoverBits]; + + switch (wholeBytes) { + default: + case 8: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0xFF000000 << 32) << leftoverBits)) >> (56 + leftoverBits))); /* Weird "<< 32" bitshift is required for C89 because it doesn't support 64-bit constants. Should be optimized out by a good compiler. */ + case 7: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x00FF0000 << 32) << leftoverBits)) >> (48 + leftoverBits))); + case 6: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x0000FF00 << 32) << leftoverBits)) >> (40 + leftoverBits))); + case 5: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x000000FF << 32) << leftoverBits)) >> (32 + leftoverBits))); + case 4: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0xFF000000 ) << leftoverBits)) >> (24 + leftoverBits))); + case 3: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x00FF0000 ) << leftoverBits)) >> (16 + leftoverBits))); + case 2: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x0000FF00 ) << leftoverBits)) >> ( 8 + leftoverBits))); + case 1: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x000000FF ) << leftoverBits)) >> ( 0 + leftoverBits))); + case 0: if (leftoverBits > 0) crc = (crc << leftoverBits) ^ drflac__crc16_table[(crc >> (16 - leftoverBits)) ^ (data & leftoverDataMask)]; + } + return crc; +#endif +} + + +static DRFLAC_INLINE drflac_uint16 drflac_crc16(drflac_uint16 crc, drflac_cache_t data, drflac_uint32 count) +{ +#ifdef DRFLAC_64BIT + return drflac_crc16__64bit(crc, data, count); +#else + return drflac_crc16__32bit(crc, data, count); +#endif +} +#endif + + +#ifdef DRFLAC_64BIT +#define drflac__be2host__cache_line drflac__be2host_64 +#else +#define drflac__be2host__cache_line drflac__be2host_32 +#endif + +/* +BIT READING ATTEMPT #2 + +This uses a 32- or 64-bit bit-shifted cache - as bits are read, the cache is shifted such that the first valid bit is sitting +on the most significant bit. It uses the notion of an L1 and L2 cache (borrowed from CPU architecture), where the L1 cache +is a 32- or 64-bit unsigned integer (depending on whether or not a 32- or 64-bit build is being compiled) and the L2 is an +array of "cache lines", with each cache line being the same size as the L1. The L2 is a buffer of about 4KB and is where data +from onRead() is read into. +*/ +#define DRFLAC_CACHE_L1_SIZE_BYTES(bs) (sizeof((bs)->cache)) +#define DRFLAC_CACHE_L1_SIZE_BITS(bs) (sizeof((bs)->cache)*8) +#define DRFLAC_CACHE_L1_BITS_REMAINING(bs) (DRFLAC_CACHE_L1_SIZE_BITS(bs) - (bs)->consumedBits) +#define DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount) (~((~(drflac_cache_t)0) >> (_bitCount))) +#define DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, _bitCount) (DRFLAC_CACHE_L1_SIZE_BITS(bs) - (_bitCount)) +#define DRFLAC_CACHE_L1_SELECT(bs, _bitCount) (((bs)->cache) & DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount)) +#define DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, _bitCount) (DRFLAC_CACHE_L1_SELECT((bs), (_bitCount)) >> DRFLAC_CACHE_L1_SELECTION_SHIFT((bs), (_bitCount))) +#define DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE(bs, _bitCount)(DRFLAC_CACHE_L1_SELECT((bs), (_bitCount)) >> (DRFLAC_CACHE_L1_SELECTION_SHIFT((bs), (_bitCount)) & (DRFLAC_CACHE_L1_SIZE_BITS(bs)-1))) +#define DRFLAC_CACHE_L2_SIZE_BYTES(bs) (sizeof((bs)->cacheL2)) +#define DRFLAC_CACHE_L2_LINE_COUNT(bs) (DRFLAC_CACHE_L2_SIZE_BYTES(bs) / sizeof((bs)->cacheL2[0])) +#define DRFLAC_CACHE_L2_LINES_REMAINING(bs) (DRFLAC_CACHE_L2_LINE_COUNT(bs) - (bs)->nextL2Line) + + +#ifndef DR_FLAC_NO_CRC +static DRFLAC_INLINE void drflac__reset_crc16(drflac_bs* bs) +{ + bs->crc16 = 0; + bs->crc16CacheIgnoredBytes = bs->consumedBits >> 3; +} + +static DRFLAC_INLINE void drflac__update_crc16(drflac_bs* bs) +{ + if (bs->crc16CacheIgnoredBytes == 0) { + bs->crc16 = drflac_crc16_cache(bs->crc16, bs->crc16Cache); + } else { + bs->crc16 = drflac_crc16_bytes(bs->crc16, bs->crc16Cache, DRFLAC_CACHE_L1_SIZE_BYTES(bs) - bs->crc16CacheIgnoredBytes); + bs->crc16CacheIgnoredBytes = 0; + } +} + +static DRFLAC_INLINE drflac_uint16 drflac__flush_crc16(drflac_bs* bs) +{ + /* We should never be flushing in a situation where we are not aligned on a byte boundary. */ + DRFLAC_ASSERT((DRFLAC_CACHE_L1_BITS_REMAINING(bs) & 7) == 0); + + /* + The bits that were read from the L1 cache need to be accumulated. The number of bytes needing to be accumulated is determined + by the number of bits that have been consumed. + */ + if (DRFLAC_CACHE_L1_BITS_REMAINING(bs) == 0) { + drflac__update_crc16(bs); + } else { + /* We only accumulate the consumed bits. */ + bs->crc16 = drflac_crc16_bytes(bs->crc16, bs->crc16Cache >> DRFLAC_CACHE_L1_BITS_REMAINING(bs), (bs->consumedBits >> 3) - bs->crc16CacheIgnoredBytes); + + /* + The bits that we just accumulated should never be accumulated again. We need to keep track of how many bytes were accumulated + so we can handle that later. + */ + bs->crc16CacheIgnoredBytes = bs->consumedBits >> 3; + } + + return bs->crc16; +} +#endif + +static DRFLAC_INLINE drflac_bool32 drflac__reload_l1_cache_from_l2(drflac_bs* bs) +{ + size_t bytesRead; + size_t alignedL1LineCount; + + /* Fast path. Try loading straight from L2. */ + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + bs->cache = bs->cacheL2[bs->nextL2Line++]; + return DRFLAC_TRUE; + } + + /* + If we get here it means we've run out of data in the L2 cache. We'll need to fetch more from the client, if there's + any left. + */ + if (bs->unalignedByteCount > 0) { + return DRFLAC_FALSE; /* If we have any unaligned bytes it means there's no more aligned bytes left in the client. */ + } + + bytesRead = bs->onRead(bs->pUserData, bs->cacheL2, DRFLAC_CACHE_L2_SIZE_BYTES(bs)); + + bs->nextL2Line = 0; + if (bytesRead == DRFLAC_CACHE_L2_SIZE_BYTES(bs)) { + bs->cache = bs->cacheL2[bs->nextL2Line++]; + return DRFLAC_TRUE; + } + + + /* + If we get here it means we were unable to retrieve enough data to fill the entire L2 cache. It probably + means we've just reached the end of the file. We need to move the valid data down to the end of the buffer + and adjust the index of the next line accordingly. Also keep in mind that the L2 cache must be aligned to + the size of the L1 so we'll need to seek backwards by any misaligned bytes. + */ + alignedL1LineCount = bytesRead / DRFLAC_CACHE_L1_SIZE_BYTES(bs); + + /* We need to keep track of any unaligned bytes for later use. */ + bs->unalignedByteCount = bytesRead - (alignedL1LineCount * DRFLAC_CACHE_L1_SIZE_BYTES(bs)); + if (bs->unalignedByteCount > 0) { + bs->unalignedCache = bs->cacheL2[alignedL1LineCount]; + } + + if (alignedL1LineCount > 0) { + size_t offset = DRFLAC_CACHE_L2_LINE_COUNT(bs) - alignedL1LineCount; + size_t i; + for (i = alignedL1LineCount; i > 0; --i) { + bs->cacheL2[i-1 + offset] = bs->cacheL2[i-1]; + } + + bs->nextL2Line = (drflac_uint32)offset; + bs->cache = bs->cacheL2[bs->nextL2Line++]; + return DRFLAC_TRUE; + } else { + /* If we get into this branch it means we weren't able to load any L1-aligned data. */ + bs->nextL2Line = DRFLAC_CACHE_L2_LINE_COUNT(bs); + return DRFLAC_FALSE; + } +} + +static drflac_bool32 drflac__reload_cache(drflac_bs* bs) +{ + size_t bytesRead; + +#ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); +#endif + + /* Fast path. Try just moving the next value in the L2 cache to the L1 cache. */ + if (drflac__reload_l1_cache_from_l2(bs)) { + bs->cache = drflac__be2host__cache_line(bs->cache); + bs->consumedBits = 0; +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs->cache; +#endif + return DRFLAC_TRUE; + } + + /* Slow path. */ + + /* + If we get here it means we have failed to load the L1 cache from the L2. Likely we've just reached the end of the stream and the last + few bytes did not meet the alignment requirements for the L2 cache. In this case we need to fall back to a slower path and read the + data from the unaligned cache. + */ + bytesRead = bs->unalignedByteCount; + if (bytesRead == 0) { + bs->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs); /* <-- The stream has been exhausted, so marked the bits as consumed. */ + return DRFLAC_FALSE; + } + + DRFLAC_ASSERT(bytesRead < DRFLAC_CACHE_L1_SIZE_BYTES(bs)); + bs->consumedBits = (drflac_uint32)(DRFLAC_CACHE_L1_SIZE_BYTES(bs) - bytesRead) * 8; + + bs->cache = drflac__be2host__cache_line(bs->unalignedCache); + bs->cache &= DRFLAC_CACHE_L1_SELECTION_MASK(DRFLAC_CACHE_L1_BITS_REMAINING(bs)); /* <-- Make sure the consumed bits are always set to zero. Other parts of the library depend on this property. */ + bs->unalignedByteCount = 0; /* <-- At this point the unaligned bytes have been moved into the cache and we thus have no more unaligned bytes. */ + +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs->cache >> bs->consumedBits; + bs->crc16CacheIgnoredBytes = bs->consumedBits >> 3; +#endif + return DRFLAC_TRUE; +} + +static void drflac__reset_cache(drflac_bs* bs) +{ + bs->nextL2Line = DRFLAC_CACHE_L2_LINE_COUNT(bs); /* <-- This clears the L2 cache. */ + bs->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs); /* <-- This clears the L1 cache. */ + bs->cache = 0; + bs->unalignedByteCount = 0; /* <-- This clears the trailing unaligned bytes. */ + bs->unalignedCache = 0; + +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = 0; + bs->crc16CacheIgnoredBytes = 0; +#endif +} + + +static DRFLAC_INLINE drflac_bool32 drflac__read_uint32(drflac_bs* bs, unsigned int bitCount, drflac_uint32* pResultOut) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResultOut != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 32); + + if (bs->consumedBits == DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + } + + if (bitCount <= DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* + If we want to load all 32-bits from a 32-bit cache we need to do it slightly differently because we can't do + a 32-bit shift on a 32-bit integer. This will never be the case on 64-bit caches, so we can have a slightly + more optimal solution for this. + */ +#ifdef DRFLAC_64BIT + *pResultOut = (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCount); + bs->consumedBits += bitCount; + bs->cache <<= bitCount; +#else + if (bitCount < DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + *pResultOut = (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCount); + bs->consumedBits += bitCount; + bs->cache <<= bitCount; + } else { + /* Cannot shift by 32-bits, so need to do it differently. */ + *pResultOut = (drflac_uint32)bs->cache; + bs->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs); + bs->cache = 0; + } +#endif + + return DRFLAC_TRUE; + } else { + /* It straddles the cached data. It will never cover more than the next chunk. We just read the number in two parts and combine them. */ + drflac_uint32 bitCountHi = DRFLAC_CACHE_L1_BITS_REMAINING(bs); + drflac_uint32 bitCountLo = bitCount - bitCountHi; + drflac_uint32 resultHi; + + DRFLAC_ASSERT(bitCountHi > 0); + DRFLAC_ASSERT(bitCountHi < 32); + resultHi = (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCountHi); + + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + if (bitCountLo > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + *pResultOut = (resultHi << bitCountLo) | (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCountLo); + bs->consumedBits += bitCountLo; + bs->cache <<= bitCountLo; + return DRFLAC_TRUE; + } +} + +static drflac_bool32 drflac__read_int32(drflac_bs* bs, unsigned int bitCount, drflac_int32* pResult) +{ + drflac_uint32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 32); + + if (!drflac__read_uint32(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + /* Do not attempt to shift by 32 as it's undefined. */ + if (bitCount < 32) { + drflac_uint32 signbit; + signbit = ((result >> (bitCount-1)) & 0x01); + result |= (~signbit + 1) << bitCount; + } + + *pResult = (drflac_int32)result; + return DRFLAC_TRUE; +} + +#ifdef DRFLAC_64BIT +static drflac_bool32 drflac__read_uint64(drflac_bs* bs, unsigned int bitCount, drflac_uint64* pResultOut) +{ + drflac_uint32 resultHi; + drflac_uint32 resultLo; + + DRFLAC_ASSERT(bitCount <= 64); + DRFLAC_ASSERT(bitCount > 32); + + if (!drflac__read_uint32(bs, bitCount - 32, &resultHi)) { + return DRFLAC_FALSE; + } + + if (!drflac__read_uint32(bs, 32, &resultLo)) { + return DRFLAC_FALSE; + } + + *pResultOut = (((drflac_uint64)resultHi) << 32) | ((drflac_uint64)resultLo); + return DRFLAC_TRUE; +} +#endif + +/* Function below is unused, but leaving it here in case I need to quickly add it again. */ +#if 0 +static drflac_bool32 drflac__read_int64(drflac_bs* bs, unsigned int bitCount, drflac_int64* pResultOut) +{ + drflac_uint64 result; + drflac_uint64 signbit; + + DRFLAC_ASSERT(bitCount <= 64); + + if (!drflac__read_uint64(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + signbit = ((result >> (bitCount-1)) & 0x01); + result |= (~signbit + 1) << bitCount; + + *pResultOut = (drflac_int64)result; + return DRFLAC_TRUE; +} +#endif + +static drflac_bool32 drflac__read_uint16(drflac_bs* bs, unsigned int bitCount, drflac_uint16* pResult) +{ + drflac_uint32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 16); + + if (!drflac__read_uint32(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + *pResult = (drflac_uint16)result; + return DRFLAC_TRUE; +} + +#if 0 +static drflac_bool32 drflac__read_int16(drflac_bs* bs, unsigned int bitCount, drflac_int16* pResult) +{ + drflac_int32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 16); + + if (!drflac__read_int32(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + *pResult = (drflac_int16)result; + return DRFLAC_TRUE; +} +#endif + +static drflac_bool32 drflac__read_uint8(drflac_bs* bs, unsigned int bitCount, drflac_uint8* pResult) +{ + drflac_uint32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 8); + + if (!drflac__read_uint32(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + *pResult = (drflac_uint8)result; + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__read_int8(drflac_bs* bs, unsigned int bitCount, drflac_int8* pResult) +{ + drflac_int32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 8); + + if (!drflac__read_int32(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + *pResult = (drflac_int8)result; + return DRFLAC_TRUE; +} + + +static drflac_bool32 drflac__seek_bits(drflac_bs* bs, size_t bitsToSeek) +{ + if (bitsToSeek <= DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + bs->consumedBits += (drflac_uint32)bitsToSeek; + bs->cache <<= bitsToSeek; + return DRFLAC_TRUE; + } else { + /* It straddles the cached data. This function isn't called too frequently so I'm favouring simplicity here. */ + bitsToSeek -= DRFLAC_CACHE_L1_BITS_REMAINING(bs); + bs->consumedBits += DRFLAC_CACHE_L1_BITS_REMAINING(bs); + bs->cache = 0; + + /* Simple case. Seek in groups of the same number as bits that fit within a cache line. */ +#ifdef DRFLAC_64BIT + while (bitsToSeek >= DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + drflac_uint64 bin; + if (!drflac__read_uint64(bs, DRFLAC_CACHE_L1_SIZE_BITS(bs), &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek -= DRFLAC_CACHE_L1_SIZE_BITS(bs); + } +#else + while (bitsToSeek >= DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + drflac_uint32 bin; + if (!drflac__read_uint32(bs, DRFLAC_CACHE_L1_SIZE_BITS(bs), &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek -= DRFLAC_CACHE_L1_SIZE_BITS(bs); + } +#endif + + /* Whole leftover bytes. */ + while (bitsToSeek >= 8) { + drflac_uint8 bin; + if (!drflac__read_uint8(bs, 8, &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek -= 8; + } + + /* Leftover bits. */ + if (bitsToSeek > 0) { + drflac_uint8 bin; + if (!drflac__read_uint8(bs, (drflac_uint32)bitsToSeek, &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek = 0; /* <-- Necessary for the assert below. */ + } + + DRFLAC_ASSERT(bitsToSeek == 0); + return DRFLAC_TRUE; + } +} + + +/* This function moves the bit streamer to the first bit after the sync code (bit 15 of the of the frame header). It will also update the CRC-16. */ +static drflac_bool32 drflac__find_and_seek_to_next_sync_code(drflac_bs* bs) +{ + DRFLAC_ASSERT(bs != NULL); + + /* + The sync code is always aligned to 8 bits. This is convenient for us because it means we can do byte-aligned movements. The first + thing to do is align to the next byte. + */ + if (!drflac__seek_bits(bs, DRFLAC_CACHE_L1_BITS_REMAINING(bs) & 7)) { + return DRFLAC_FALSE; + } + + for (;;) { + drflac_uint8 hi; + +#ifndef DR_FLAC_NO_CRC + drflac__reset_crc16(bs); +#endif + + if (!drflac__read_uint8(bs, 8, &hi)) { + return DRFLAC_FALSE; + } + + if (hi == 0xFF) { + drflac_uint8 lo; + if (!drflac__read_uint8(bs, 6, &lo)) { + return DRFLAC_FALSE; + } + + if (lo == 0x3E) { + return DRFLAC_TRUE; + } else { + if (!drflac__seek_bits(bs, DRFLAC_CACHE_L1_BITS_REMAINING(bs) & 7)) { + return DRFLAC_FALSE; + } + } + } + } + + /* Should never get here. */ + /*return DRFLAC_FALSE;*/ +} + + +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) +#define DRFLAC_IMPLEMENT_CLZ_LZCNT +#endif +#if defined(_MSC_VER) && _MSC_VER >= 1400 && (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(__clang__) +#define DRFLAC_IMPLEMENT_CLZ_MSVC +#endif +#if defined(__WATCOMC__) && defined(__386__) +#define DRFLAC_IMPLEMENT_CLZ_WATCOM +#endif +#ifdef __MRC__ +#include +#define DRFLAC_IMPLEMENT_CLZ_MRC +#endif + +static DRFLAC_INLINE drflac_uint32 drflac__clz_software(drflac_cache_t x) +{ + drflac_uint32 n; + static drflac_uint32 clz_table_4[] = { + 0, + 4, + 3, 3, + 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1 + }; + + if (x == 0) { + return sizeof(x)*8; + } + + n = clz_table_4[x >> (sizeof(x)*8 - 4)]; + if (n == 0) { +#ifdef DRFLAC_64BIT + if ((x & ((drflac_uint64)0xFFFFFFFF << 32)) == 0) { n = 32; x <<= 32; } + if ((x & ((drflac_uint64)0xFFFF0000 << 32)) == 0) { n += 16; x <<= 16; } + if ((x & ((drflac_uint64)0xFF000000 << 32)) == 0) { n += 8; x <<= 8; } + if ((x & ((drflac_uint64)0xF0000000 << 32)) == 0) { n += 4; x <<= 4; } +#else + if ((x & 0xFFFF0000) == 0) { n = 16; x <<= 16; } + if ((x & 0xFF000000) == 0) { n += 8; x <<= 8; } + if ((x & 0xF0000000) == 0) { n += 4; x <<= 4; } +#endif + n += clz_table_4[x >> (sizeof(x)*8 - 4)]; + } + + return n - 1; +} + +#ifdef DRFLAC_IMPLEMENT_CLZ_LZCNT +static DRFLAC_INLINE drflac_bool32 drflac__is_lzcnt_supported(void) +{ + /* Fast compile time check for ARM. */ +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) && defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) + return DRFLAC_TRUE; +#elif defined(__MRC__) + return DRFLAC_TRUE; +#else + /* If the compiler itself does not support the intrinsic then we'll need to return false. */ + #ifdef DRFLAC_HAS_LZCNT_INTRINSIC + return drflac__gIsLZCNTSupported; + #else + return DRFLAC_FALSE; + #endif +#endif +} + +static DRFLAC_INLINE drflac_uint32 drflac__clz_lzcnt(drflac_cache_t x) +{ + /* + It's critical for competitive decoding performance that this function be highly optimal. With MSVC we can use the __lzcnt64() and __lzcnt() intrinsics + to achieve good performance, however on GCC and Clang it's a little bit more annoying. The __builtin_clzl() and __builtin_clzll() intrinsics leave + it undefined as to the return value when `x` is 0. We need this to be well defined as returning 32 or 64, depending on whether or not it's a 32- or + 64-bit build. To work around this we would need to add a conditional to check for the x = 0 case, but this creates unnecessary inefficiency. To work + around this problem I have written some inline assembly to emit the LZCNT (x86) or CLZ (ARM) instruction directly which removes the need to include + the conditional. This has worked well in the past, but for some reason Clang's MSVC compatible driver, clang-cl, does not seem to be handling this + in the same way as the normal Clang driver. It seems that `clang-cl` is just outputting the wrong results sometimes, maybe due to some register + getting clobbered? + + I'm not sure if this is a bug with dr_flac's inlined assembly (most likely), a bug in `clang-cl` or just a misunderstanding on my part with inline + assembly rules for `clang-cl`. If somebody can identify an error in dr_flac's inlined assembly I'm happy to get that fixed. + + Fortunately there is an easy workaround for this. Clang implements MSVC-specific intrinsics for compatibility. It also defines _MSC_VER for extra + compatibility. We can therefore just check for _MSC_VER and use the MSVC intrinsic which, fortunately for us, Clang supports. It would still be nice + to know how to fix the inlined assembly for correctness sake, however. + */ + +#if defined(_MSC_VER) /*&& !defined(__clang__)*/ /* <-- Intentionally wanting Clang to use the MSVC __lzcnt64/__lzcnt intrinsics due to above ^. */ + #ifdef DRFLAC_64BIT + return (drflac_uint32)__lzcnt64(x); + #else + return (drflac_uint32)__lzcnt(x); + #endif +#else + #if defined(__GNUC__) || defined(__clang__) + #if defined(DRFLAC_X64) + { + /* + A note on lzcnt. + + We check for the presence of the lzcnt instruction at runtime before calling this function, but we still generate this code. I have had + a report where the assembler does not recognize the lzcnt instruction. To work around this we are going to use `rep; bsr` instead which + has an identical byte encoding as lzcnt, and should hopefully improve compatibility with older assemblers. + */ + drflac_uint64 r; + __asm__ __volatile__ ( + "rep; bsr{q %1, %0| %0, %1}" : "=r"(r) : "r"(x) : "cc" + /*"lzcnt{ %1, %0| %0, %1}" : "=r"(r) : "r"(x) : "cc"*/ + ); + + return (drflac_uint32)r; + } + #elif defined(DRFLAC_X86) + { + drflac_uint32 r; + __asm__ __volatile__ ( + "rep; bsr{l %1, %0| %0, %1}" : "=r"(r) : "r"(x) : "cc" + /*"lzcnt{l %1, %0| %0, %1}" : "=r"(r) : "r"(x) : "cc"*/ + ); + + return r; + } + #elif defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) && !defined(__ARM_ARCH_6M__) && !(defined(__thumb__) && !defined(__thumb2__)) && !defined(DRFLAC_64BIT) /* <-- I haven't tested 64-bit inline assembly, so only enabling this for the 32-bit build for now. */ + { + unsigned int r; + __asm__ __volatile__ ( + #if defined(DRFLAC_64BIT) + "clz %w[out], %w[in]" : [out]"=r"(r) : [in]"r"(x) /* <-- This is untested. If someone in the community could test this, that would be appreciated! */ + #else + "clz %[out], %[in]" : [out]"=r"(r) : [in]"r"(x) + #endif + ); + + return r; + } + #else + if (x == 0) { + return sizeof(x)*8; + } + #ifdef DRFLAC_64BIT + return (drflac_uint32)__builtin_clzll((drflac_uint64)x); + #else + return (drflac_uint32)__builtin_clzl((drflac_uint32)x); + #endif + #endif + #else + /* Unsupported compiler. */ + #error "This compiler does not support the lzcnt intrinsic." + #endif +#endif +} +#endif + +#ifdef DRFLAC_IMPLEMENT_CLZ_MSVC +#include /* For BitScanReverse(). */ + +static DRFLAC_INLINE drflac_uint32 drflac__clz_msvc(drflac_cache_t x) +{ + drflac_uint32 n; + + if (x == 0) { + return sizeof(x)*8; + } + +#ifdef DRFLAC_64BIT + _BitScanReverse64((unsigned long*)&n, x); +#else + _BitScanReverse((unsigned long*)&n, x); +#endif + return sizeof(x)*8 - n - 1; +} +#endif + +#ifdef DRFLAC_IMPLEMENT_CLZ_WATCOM +static __inline drflac_uint32 drflac__clz_watcom (drflac_uint32); +#ifdef DRFLAC_IMPLEMENT_CLZ_WATCOM_LZCNT +/* Use the LZCNT instruction (only available on some processors since the 2010s). */ +#pragma aux drflac__clz_watcom_lzcnt = \ + "db 0F3h, 0Fh, 0BDh, 0C0h" /* lzcnt eax, eax */ \ + parm [eax] \ + value [eax] \ + modify nomemory; +#else +/* Use the 386+-compatible implementation. */ +#pragma aux drflac__clz_watcom = \ + "bsr eax, eax" \ + "xor eax, 31" \ + parm [eax] nomemory \ + value [eax] \ + modify exact [eax] nomemory; +#endif +#endif + +static DRFLAC_INLINE drflac_uint32 drflac__clz(drflac_cache_t x) +{ +#ifdef DRFLAC_IMPLEMENT_CLZ_LZCNT + if (drflac__is_lzcnt_supported()) { + return drflac__clz_lzcnt(x); + } else +#endif + { +#ifdef DRFLAC_IMPLEMENT_CLZ_MSVC + return drflac__clz_msvc(x); +#elif defined(DRFLAC_IMPLEMENT_CLZ_WATCOM_LZCNT) + return drflac__clz_watcom_lzcnt(x); +#elif defined(DRFLAC_IMPLEMENT_CLZ_WATCOM) + return (x == 0) ? sizeof(x)*8 : drflac__clz_watcom(x); +#elif defined(__MRC__) + return __cntlzw(x); +#else + return drflac__clz_software(x); +#endif + } +} + + +static DRFLAC_INLINE drflac_bool32 drflac__seek_past_next_set_bit(drflac_bs* bs, unsigned int* pOffsetOut) +{ + drflac_uint32 zeroCounter = 0; + drflac_uint32 setBitOffsetPlus1; + + while (bs->cache == 0) { + zeroCounter += (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs); + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + } + + if (bs->cache == 1) { + /* Not catching this would lead to undefined behaviour: a shift of a 32-bit number by 32 or more is undefined */ + *pOffsetOut = zeroCounter + (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs) - 1; + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; + } + + setBitOffsetPlus1 = drflac__clz(bs->cache); + setBitOffsetPlus1 += 1; + + if (setBitOffsetPlus1 > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + bs->consumedBits += setBitOffsetPlus1; + bs->cache <<= setBitOffsetPlus1; + + *pOffsetOut = zeroCounter + setBitOffsetPlus1 - 1; + return DRFLAC_TRUE; +} + + + +static drflac_bool32 drflac__seek_to_byte(drflac_bs* bs, drflac_uint64 offsetFromStart) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(offsetFromStart > 0); + + /* + Seeking from the start is not quite as trivial as it sounds because the onSeek callback takes a signed 32-bit integer (which + is intentional because it simplifies the implementation of the onSeek callbacks), however offsetFromStart is unsigned 64-bit. + To resolve we just need to do an initial seek from the start, and then a series of offset seeks to make up the remainder. + */ + if (offsetFromStart > 0x7FFFFFFF) { + drflac_uint64 bytesRemaining = offsetFromStart; + if (!bs->onSeek(bs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + bytesRemaining -= 0x7FFFFFFF; + + while (bytesRemaining > 0x7FFFFFFF) { + if (!bs->onSeek(bs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + bytesRemaining -= 0x7FFFFFFF; + } + + if (bytesRemaining > 0) { + if (!bs->onSeek(bs->pUserData, (int)bytesRemaining, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + } else { + if (!bs->onSeek(bs->pUserData, (int)offsetFromStart, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + } + + /* The cache should be reset to force a reload of fresh data from the client. */ + drflac__reset_cache(bs); + return DRFLAC_TRUE; +} + + +static drflac_result drflac__read_utf8_coded_number(drflac_bs* bs, drflac_uint64* pNumberOut, drflac_uint8* pCRCOut) +{ + drflac_uint8 crc; + drflac_uint64 result; + drflac_uint8 utf8[7] = {0}; + int byteCount; + int i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pNumberOut != NULL); + DRFLAC_ASSERT(pCRCOut != NULL); + + crc = *pCRCOut; + + if (!drflac__read_uint8(bs, 8, utf8)) { + *pNumberOut = 0; + return DRFLAC_AT_END; + } + crc = drflac_crc8(crc, utf8[0], 8); + + if ((utf8[0] & 0x80) == 0) { + *pNumberOut = utf8[0]; + *pCRCOut = crc; + return DRFLAC_SUCCESS; + } + + /*byteCount = 1;*/ + if ((utf8[0] & 0xE0) == 0xC0) { + byteCount = 2; + } else if ((utf8[0] & 0xF0) == 0xE0) { + byteCount = 3; + } else if ((utf8[0] & 0xF8) == 0xF0) { + byteCount = 4; + } else if ((utf8[0] & 0xFC) == 0xF8) { + byteCount = 5; + } else if ((utf8[0] & 0xFE) == 0xFC) { + byteCount = 6; + } else if ((utf8[0] & 0xFF) == 0xFE) { + byteCount = 7; + } else { + *pNumberOut = 0; + return DRFLAC_CRC_MISMATCH; /* Bad UTF-8 encoding. */ + } + + /* Read extra bytes. */ + DRFLAC_ASSERT(byteCount > 1); + + result = (drflac_uint64)(utf8[0] & (0xFF >> (byteCount + 1))); + for (i = 1; i < byteCount; ++i) { + if (!drflac__read_uint8(bs, 8, utf8 + i)) { + *pNumberOut = 0; + return DRFLAC_AT_END; + } + crc = drflac_crc8(crc, utf8[i], 8); + + result = (result << 6) | (utf8[i] & 0x3F); + } + + *pNumberOut = result; + *pCRCOut = crc; + return DRFLAC_SUCCESS; +} + + +static DRFLAC_INLINE drflac_uint32 drflac__ilog2_u32(drflac_uint32 x) +{ +#if 1 /* Needs optimizing. */ + drflac_uint32 result = 0; + while (x > 0) { + result += 1; + x >>= 1; + } + + return result; +#endif +} + +static DRFLAC_INLINE drflac_bool32 drflac__use_64_bit_prediction(drflac_uint32 bitsPerSample, drflac_uint32 order, drflac_uint32 precision) +{ + /* https://web.archive.org/web/20220205005724/https://github.com/ietf-wg-cellar/flac-specification/blob/37a49aa48ba4ba12e8757badfc59c0df35435fec/rfc_backmatter.md */ + return bitsPerSample + precision + drflac__ilog2_u32(order) > 32; +} + + +/* +The next two functions are responsible for calculating the prediction. + +When the bits per sample is >16 we need to use 64-bit integer arithmetic because otherwise we'll run out of precision. It's +safe to assume this will be slower on 32-bit platforms so we use a more optimal solution when the bits per sample is <=16. +*/ +#if defined(__clang__) +__attribute__((no_sanitize("signed-integer-overflow"))) +#endif +static DRFLAC_INLINE drflac_int32 drflac__calculate_prediction_32(drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pDecodedSamples) +{ + drflac_int32 prediction = 0; + + DRFLAC_ASSERT(order <= 32); + + /* 32-bit version. */ + + /* VC++ optimizes this to a single jmp. I've not yet verified this for other compilers. */ + switch (order) + { + case 32: prediction += coefficients[31] * pDecodedSamples[-32]; + case 31: prediction += coefficients[30] * pDecodedSamples[-31]; + case 30: prediction += coefficients[29] * pDecodedSamples[-30]; + case 29: prediction += coefficients[28] * pDecodedSamples[-29]; + case 28: prediction += coefficients[27] * pDecodedSamples[-28]; + case 27: prediction += coefficients[26] * pDecodedSamples[-27]; + case 26: prediction += coefficients[25] * pDecodedSamples[-26]; + case 25: prediction += coefficients[24] * pDecodedSamples[-25]; + case 24: prediction += coefficients[23] * pDecodedSamples[-24]; + case 23: prediction += coefficients[22] * pDecodedSamples[-23]; + case 22: prediction += coefficients[21] * pDecodedSamples[-22]; + case 21: prediction += coefficients[20] * pDecodedSamples[-21]; + case 20: prediction += coefficients[19] * pDecodedSamples[-20]; + case 19: prediction += coefficients[18] * pDecodedSamples[-19]; + case 18: prediction += coefficients[17] * pDecodedSamples[-18]; + case 17: prediction += coefficients[16] * pDecodedSamples[-17]; + case 16: prediction += coefficients[15] * pDecodedSamples[-16]; + case 15: prediction += coefficients[14] * pDecodedSamples[-15]; + case 14: prediction += coefficients[13] * pDecodedSamples[-14]; + case 13: prediction += coefficients[12] * pDecodedSamples[-13]; + case 12: prediction += coefficients[11] * pDecodedSamples[-12]; + case 11: prediction += coefficients[10] * pDecodedSamples[-11]; + case 10: prediction += coefficients[ 9] * pDecodedSamples[-10]; + case 9: prediction += coefficients[ 8] * pDecodedSamples[- 9]; + case 8: prediction += coefficients[ 7] * pDecodedSamples[- 8]; + case 7: prediction += coefficients[ 6] * pDecodedSamples[- 7]; + case 6: prediction += coefficients[ 5] * pDecodedSamples[- 6]; + case 5: prediction += coefficients[ 4] * pDecodedSamples[- 5]; + case 4: prediction += coefficients[ 3] * pDecodedSamples[- 4]; + case 3: prediction += coefficients[ 2] * pDecodedSamples[- 3]; + case 2: prediction += coefficients[ 1] * pDecodedSamples[- 2]; + case 1: prediction += coefficients[ 0] * pDecodedSamples[- 1]; + } + + return (drflac_int32)(prediction >> shift); +} + +static DRFLAC_INLINE drflac_int32 drflac__calculate_prediction_64(drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pDecodedSamples) +{ + drflac_int64 prediction; + + DRFLAC_ASSERT(order <= 32); + + /* 64-bit version. */ + + /* This method is faster on the 32-bit build when compiling with VC++. See note below. */ +#ifndef DRFLAC_64BIT + if (order == 8) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + } + else if (order == 7) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + } + else if (order == 3) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + } + else if (order == 6) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + } + else if (order == 5) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + } + else if (order == 4) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + } + else if (order == 12) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + prediction += coefficients[9] * (drflac_int64)pDecodedSamples[-10]; + prediction += coefficients[10] * (drflac_int64)pDecodedSamples[-11]; + prediction += coefficients[11] * (drflac_int64)pDecodedSamples[-12]; + } + else if (order == 2) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + } + else if (order == 1) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + } + else if (order == 10) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + prediction += coefficients[9] * (drflac_int64)pDecodedSamples[-10]; + } + else if (order == 9) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + } + else if (order == 11) + { + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + prediction += coefficients[9] * (drflac_int64)pDecodedSamples[-10]; + prediction += coefficients[10] * (drflac_int64)pDecodedSamples[-11]; + } + else + { + int j; + + prediction = 0; + for (j = 0; j < (int)order; ++j) { + prediction += coefficients[j] * (drflac_int64)pDecodedSamples[-j-1]; + } + } +#endif + + /* + VC++ optimizes this to a single jmp instruction, but only the 64-bit build. The 32-bit build generates less efficient code for some + reason. The ugly version above is faster so we'll just switch between the two depending on the target platform. + */ +#ifdef DRFLAC_64BIT + prediction = 0; + switch (order) + { + case 32: prediction += coefficients[31] * (drflac_int64)pDecodedSamples[-32]; + case 31: prediction += coefficients[30] * (drflac_int64)pDecodedSamples[-31]; + case 30: prediction += coefficients[29] * (drflac_int64)pDecodedSamples[-30]; + case 29: prediction += coefficients[28] * (drflac_int64)pDecodedSamples[-29]; + case 28: prediction += coefficients[27] * (drflac_int64)pDecodedSamples[-28]; + case 27: prediction += coefficients[26] * (drflac_int64)pDecodedSamples[-27]; + case 26: prediction += coefficients[25] * (drflac_int64)pDecodedSamples[-26]; + case 25: prediction += coefficients[24] * (drflac_int64)pDecodedSamples[-25]; + case 24: prediction += coefficients[23] * (drflac_int64)pDecodedSamples[-24]; + case 23: prediction += coefficients[22] * (drflac_int64)pDecodedSamples[-23]; + case 22: prediction += coefficients[21] * (drflac_int64)pDecodedSamples[-22]; + case 21: prediction += coefficients[20] * (drflac_int64)pDecodedSamples[-21]; + case 20: prediction += coefficients[19] * (drflac_int64)pDecodedSamples[-20]; + case 19: prediction += coefficients[18] * (drflac_int64)pDecodedSamples[-19]; + case 18: prediction += coefficients[17] * (drflac_int64)pDecodedSamples[-18]; + case 17: prediction += coefficients[16] * (drflac_int64)pDecodedSamples[-17]; + case 16: prediction += coefficients[15] * (drflac_int64)pDecodedSamples[-16]; + case 15: prediction += coefficients[14] * (drflac_int64)pDecodedSamples[-15]; + case 14: prediction += coefficients[13] * (drflac_int64)pDecodedSamples[-14]; + case 13: prediction += coefficients[12] * (drflac_int64)pDecodedSamples[-13]; + case 12: prediction += coefficients[11] * (drflac_int64)pDecodedSamples[-12]; + case 11: prediction += coefficients[10] * (drflac_int64)pDecodedSamples[-11]; + case 10: prediction += coefficients[ 9] * (drflac_int64)pDecodedSamples[-10]; + case 9: prediction += coefficients[ 8] * (drflac_int64)pDecodedSamples[- 9]; + case 8: prediction += coefficients[ 7] * (drflac_int64)pDecodedSamples[- 8]; + case 7: prediction += coefficients[ 6] * (drflac_int64)pDecodedSamples[- 7]; + case 6: prediction += coefficients[ 5] * (drflac_int64)pDecodedSamples[- 6]; + case 5: prediction += coefficients[ 4] * (drflac_int64)pDecodedSamples[- 5]; + case 4: prediction += coefficients[ 3] * (drflac_int64)pDecodedSamples[- 4]; + case 3: prediction += coefficients[ 2] * (drflac_int64)pDecodedSamples[- 3]; + case 2: prediction += coefficients[ 1] * (drflac_int64)pDecodedSamples[- 2]; + case 1: prediction += coefficients[ 0] * (drflac_int64)pDecodedSamples[- 1]; + } +#endif + + return (drflac_int32)(prediction >> shift); +} + + +#if 0 +/* +Reference implementation for reading and decoding samples with residual. This is intentionally left unoptimized for the +sake of readability and should only be used as a reference. +*/ +static drflac_bool32 drflac__decode_samples_with_residual__rice__reference(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + for (i = 0; i < count; ++i) { + drflac_uint32 zeroCounter = 0; + for (;;) { + drflac_uint8 bit; + if (!drflac__read_uint8(bs, 1, &bit)) { + return DRFLAC_FALSE; + } + + if (bit == 0) { + zeroCounter += 1; + } else { + break; + } + } + + drflac_uint32 decodedRice; + if (riceParam > 0) { + if (!drflac__read_uint32(bs, riceParam, &decodedRice)) { + return DRFLAC_FALSE; + } + } else { + decodedRice = 0; + } + + decodedRice |= (zeroCounter << riceParam); + if ((decodedRice & 0x01)) { + decodedRice = ~(decodedRice >> 1); + } else { + decodedRice = (decodedRice >> 1); + } + + + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + pSamplesOut[i] = decodedRice + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + i); + } else { + pSamplesOut[i] = decodedRice + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + i); + } + } + + return DRFLAC_TRUE; +} +#endif + +#if 0 +static drflac_bool32 drflac__read_rice_parts__reference(drflac_bs* bs, drflac_uint8 riceParam, drflac_uint32* pZeroCounterOut, drflac_uint32* pRiceParamPartOut) +{ + drflac_uint32 zeroCounter = 0; + drflac_uint32 decodedRice; + + for (;;) { + drflac_uint8 bit; + if (!drflac__read_uint8(bs, 1, &bit)) { + return DRFLAC_FALSE; + } + + if (bit == 0) { + zeroCounter += 1; + } else { + break; + } + } + + if (riceParam > 0) { + if (!drflac__read_uint32(bs, riceParam, &decodedRice)) { + return DRFLAC_FALSE; + } + } else { + decodedRice = 0; + } + + *pZeroCounterOut = zeroCounter; + *pRiceParamPartOut = decodedRice; + return DRFLAC_TRUE; +} +#endif + +#if 0 +static DRFLAC_INLINE drflac_bool32 drflac__read_rice_parts(drflac_bs* bs, drflac_uint8 riceParam, drflac_uint32* pZeroCounterOut, drflac_uint32* pRiceParamPartOut) +{ + drflac_cache_t riceParamMask; + drflac_uint32 zeroCounter; + drflac_uint32 setBitOffsetPlus1; + drflac_uint32 riceParamPart; + drflac_uint32 riceLength; + + DRFLAC_ASSERT(riceParam > 0); /* <-- riceParam should never be 0. drflac__read_rice_parts__param_equals_zero() should be used instead for this case. */ + + riceParamMask = DRFLAC_CACHE_L1_SELECTION_MASK(riceParam); + + zeroCounter = 0; + while (bs->cache == 0) { + zeroCounter += (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs); + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + } + + setBitOffsetPlus1 = drflac__clz(bs->cache); + zeroCounter += setBitOffsetPlus1; + setBitOffsetPlus1 += 1; + + riceLength = setBitOffsetPlus1 + riceParam; + if (riceLength < DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + riceParamPart = (drflac_uint32)((bs->cache & (riceParamMask >> setBitOffsetPlus1)) >> DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, riceLength)); + + bs->consumedBits += riceLength; + bs->cache <<= riceLength; + } else { + drflac_uint32 bitCountLo; + drflac_cache_t resultHi; + + bs->consumedBits += riceLength; + bs->cache <<= setBitOffsetPlus1 & (DRFLAC_CACHE_L1_SIZE_BITS(bs)-1); /* <-- Equivalent to "if (setBitOffsetPlus1 < DRFLAC_CACHE_L1_SIZE_BITS(bs)) { bs->cache <<= setBitOffsetPlus1; }" */ + + /* It straddles the cached data. It will never cover more than the next chunk. We just read the number in two parts and combine them. */ + bitCountLo = bs->consumedBits - DRFLAC_CACHE_L1_SIZE_BITS(bs); + resultHi = DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, riceParam); /* <-- Use DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE() if ever this function allows riceParam=0. */ + + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { +#ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); +#endif + bs->cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs->consumedBits = 0; +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs->cache; +#endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + if (bitCountLo > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + } + + riceParamPart = (drflac_uint32)(resultHi | DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE(bs, bitCountLo)); + + bs->consumedBits += bitCountLo; + bs->cache <<= bitCountLo; + } + + pZeroCounterOut[0] = zeroCounter; + pRiceParamPartOut[0] = riceParamPart; + + return DRFLAC_TRUE; +} +#endif + +static DRFLAC_INLINE drflac_bool32 drflac__read_rice_parts_x1(drflac_bs* bs, drflac_uint8 riceParam, drflac_uint32* pZeroCounterOut, drflac_uint32* pRiceParamPartOut) +{ + drflac_uint32 riceParamPlus1 = riceParam + 1; + /*drflac_cache_t riceParamPlus1Mask = DRFLAC_CACHE_L1_SELECTION_MASK(riceParamPlus1);*/ + drflac_uint32 riceParamPlus1Shift = DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, riceParamPlus1); + drflac_uint32 riceParamPlus1MaxConsumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs) - riceParamPlus1; + + /* + The idea here is to use local variables for the cache in an attempt to encourage the compiler to store them in registers. I have + no idea how this will work in practice... + */ + drflac_cache_t bs_cache = bs->cache; + drflac_uint32 bs_consumedBits = bs->consumedBits; + + /* The first thing to do is find the first unset bit. Most likely a bit will be set in the current cache line. */ + drflac_uint32 lzcount = drflac__clz(bs_cache); + if (lzcount < sizeof(bs_cache)*8) { + pZeroCounterOut[0] = lzcount; + + /* + It is most likely that the riceParam part (which comes after the zero counter) is also on this cache line. When extracting + this, we include the set bit from the unary coded part because it simplifies cache management. This bit will be handled + outside of this function at a higher level. + */ + extract_rice_param_part: + bs_cache <<= lzcount; + bs_consumedBits += lzcount; + + if (bs_consumedBits <= riceParamPlus1MaxConsumedBits) { + /* Getting here means the rice parameter part is wholly contained within the current cache line. */ + pRiceParamPartOut[0] = (drflac_uint32)(bs_cache >> riceParamPlus1Shift); + bs_cache <<= riceParamPlus1; + bs_consumedBits += riceParamPlus1; + } else { + drflac_uint32 riceParamPartHi; + drflac_uint32 riceParamPartLo; + drflac_uint32 riceParamPartLoBitCount; + + /* + Getting here means the rice parameter part straddles the cache line. We need to read from the tail of the current cache + line, reload the cache, and then combine it with the head of the next cache line. + */ + + /* Grab the high part of the rice parameter part. */ + riceParamPartHi = (drflac_uint32)(bs_cache >> riceParamPlus1Shift); + + /* Before reloading the cache we need to grab the size in bits of the low part. */ + riceParamPartLoBitCount = bs_consumedBits - riceParamPlus1MaxConsumedBits; + DRFLAC_ASSERT(riceParamPartLoBitCount > 0 && riceParamPartLoBitCount < 32); + + /* Now reload the cache. */ + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = riceParamPartLoBitCount; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + if (riceParamPartLoBitCount > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits + riceParamPartLoBitCount; + } + + /* We should now have enough information to construct the rice parameter part. */ + riceParamPartLo = (drflac_uint32)(bs_cache >> (DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, riceParamPartLoBitCount))); + pRiceParamPartOut[0] = riceParamPartHi | riceParamPartLo; + + bs_cache <<= riceParamPartLoBitCount; + } + } else { + /* + Getting here means there are no bits set on the cache line. This is a less optimal case because we just wasted a call + to drflac__clz() and we need to reload the cache. + */ + drflac_uint32 zeroCounter = (drflac_uint32)(DRFLAC_CACHE_L1_SIZE_BITS(bs) - bs_consumedBits); + for (;;) { + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = 0; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits; + } + + lzcount = drflac__clz(bs_cache); + zeroCounter += lzcount; + + if (lzcount < sizeof(bs_cache)*8) { + break; + } + } + + pZeroCounterOut[0] = zeroCounter; + goto extract_rice_param_part; + } + + /* Make sure the cache is restored at the end of it all. */ + bs->cache = bs_cache; + bs->consumedBits = bs_consumedBits; + + return DRFLAC_TRUE; +} + +static DRFLAC_INLINE drflac_bool32 drflac__seek_rice_parts(drflac_bs* bs, drflac_uint8 riceParam) +{ + drflac_uint32 riceParamPlus1 = riceParam + 1; + drflac_uint32 riceParamPlus1MaxConsumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs) - riceParamPlus1; + + /* + The idea here is to use local variables for the cache in an attempt to encourage the compiler to store them in registers. I have + no idea how this will work in practice... + */ + drflac_cache_t bs_cache = bs->cache; + drflac_uint32 bs_consumedBits = bs->consumedBits; + + /* The first thing to do is find the first unset bit. Most likely a bit will be set in the current cache line. */ + drflac_uint32 lzcount = drflac__clz(bs_cache); + if (lzcount < sizeof(bs_cache)*8) { + /* + It is most likely that the riceParam part (which comes after the zero counter) is also on this cache line. When extracting + this, we include the set bit from the unary coded part because it simplifies cache management. This bit will be handled + outside of this function at a higher level. + */ + extract_rice_param_part: + bs_cache <<= lzcount; + bs_consumedBits += lzcount; + + if (bs_consumedBits <= riceParamPlus1MaxConsumedBits) { + /* Getting here means the rice parameter part is wholly contained within the current cache line. */ + bs_cache <<= riceParamPlus1; + bs_consumedBits += riceParamPlus1; + } else { + /* + Getting here means the rice parameter part straddles the cache line. We need to read from the tail of the current cache + line, reload the cache, and then combine it with the head of the next cache line. + */ + + /* Before reloading the cache we need to grab the size in bits of the low part. */ + drflac_uint32 riceParamPartLoBitCount = bs_consumedBits - riceParamPlus1MaxConsumedBits; + DRFLAC_ASSERT(riceParamPartLoBitCount > 0 && riceParamPartLoBitCount < 32); + + /* Now reload the cache. */ + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = riceParamPartLoBitCount; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + if (riceParamPartLoBitCount > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits + riceParamPartLoBitCount; + } + + bs_cache <<= riceParamPartLoBitCount; + } + } else { + /* + Getting here means there are no bits set on the cache line. This is a less optimal case because we just wasted a call + to drflac__clz() and we need to reload the cache. + */ + for (;;) { + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = 0; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits; + } + + lzcount = drflac__clz(bs_cache); + if (lzcount < sizeof(bs_cache)*8) { + break; + } + } + + goto extract_rice_param_part; + } + + /* Make sure the cache is restored at the end of it all. */ + bs->cache = bs_cache; + bs->consumedBits = bs_consumedBits; + + return DRFLAC_TRUE; +} + + +static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar_zeroorder(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + drflac_uint32 zeroCountPart0; + drflac_uint32 riceParamPart0; + drflac_uint32 riceParamMask; + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + (void)bitsPerSample; + (void)order; + (void)shift; + (void)coefficients; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + + i = 0; + while (i < count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamPart0 &= riceParamMask; + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + + pSamplesOut[i] = riceParamPart0; + + i += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + drflac_uint32 zeroCountPart0 = 0; + drflac_uint32 zeroCountPart1 = 0; + drflac_uint32 zeroCountPart2 = 0; + drflac_uint32 zeroCountPart3 = 0; + drflac_uint32 riceParamPart0 = 0; + drflac_uint32 riceParamPart1 = 0; + drflac_uint32 riceParamPart2 = 0; + drflac_uint32 riceParamPart3 = 0; + drflac_uint32 riceParamMask; + const drflac_int32* pSamplesOutEnd; + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + if (lpcOrder == 0) { + return drflac__decode_samples_with_residual__rice__scalar_zeroorder(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + pSamplesOutEnd = pSamplesOut + (count & ~3); + + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + while (pSamplesOut < pSamplesOutEnd) { + /* + Rice extraction. It's faster to do this one at a time against local variables than it is to use the x4 version + against an array. Not sure why, but perhaps it's making more efficient use of registers? + */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart1, &riceParamPart1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart2, &riceParamPart2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart3, &riceParamPart3)) { + return DRFLAC_FALSE; + } + + riceParamPart0 &= riceParamMask; + riceParamPart1 &= riceParamMask; + riceParamPart2 &= riceParamMask; + riceParamPart3 &= riceParamMask; + + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart1 |= (zeroCountPart1 << riceParam); + riceParamPart2 |= (zeroCountPart2 << riceParam); + riceParamPart3 |= (zeroCountPart3 << riceParam); + + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + riceParamPart1 = (riceParamPart1 >> 1) ^ t[riceParamPart1 & 0x01]; + riceParamPart2 = (riceParamPart2 >> 1) ^ t[riceParamPart2 & 0x01]; + riceParamPart3 = (riceParamPart3 >> 1) ^ t[riceParamPart3 & 0x01]; + + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 1); + pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 2); + pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 3); + + pSamplesOut += 4; + } + } else { + while (pSamplesOut < pSamplesOutEnd) { + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart1, &riceParamPart1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart2, &riceParamPart2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart3, &riceParamPart3)) { + return DRFLAC_FALSE; + } + + riceParamPart0 &= riceParamMask; + riceParamPart1 &= riceParamMask; + riceParamPart2 &= riceParamMask; + riceParamPart3 &= riceParamMask; + + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart1 |= (zeroCountPart1 << riceParam); + riceParamPart2 |= (zeroCountPart2 << riceParam); + riceParamPart3 |= (zeroCountPart3 << riceParam); + + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + riceParamPart1 = (riceParamPart1 >> 1) ^ t[riceParamPart1 & 0x01]; + riceParamPart2 = (riceParamPart2 >> 1) ^ t[riceParamPart2 & 0x01]; + riceParamPart3 = (riceParamPart3 >> 1) ^ t[riceParamPart3 & 0x01]; + + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 1); + pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 2); + pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 3); + + pSamplesOut += 4; + } + } + + i = (count & ~3); + while (i < count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamPart0 &= riceParamMask; + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + /*riceParamPart0 = (riceParamPart0 >> 1) ^ (~(riceParamPart0 & 0x01) + 1);*/ + + /* Sample reconstruction. */ + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + } else { + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + } + + i += 1; + pSamplesOut += 1; + } + + return DRFLAC_TRUE; +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE __m128i drflac__mm_packs_interleaved_epi32(__m128i a, __m128i b) +{ + __m128i r; + + /* Pack. */ + r = _mm_packs_epi32(a, b); + + /* a3a2 a1a0 b3b2 b1b0 -> a3a2 b3b2 a1a0 b1b0 */ + r = _mm_shuffle_epi32(r, _MM_SHUFFLE(3, 1, 2, 0)); + + /* a3a2 b3b2 a1a0 b1b0 -> a3b3 a2b2 a1b1 a0b0 */ + r = _mm_shufflehi_epi16(r, _MM_SHUFFLE(3, 1, 2, 0)); + r = _mm_shufflelo_epi16(r, _MM_SHUFFLE(3, 1, 2, 0)); + + return r; +} +#endif + +#if defined(DRFLAC_SUPPORT_SSE41) +static DRFLAC_INLINE __m128i drflac__mm_not_si128(__m128i a) +{ + return _mm_xor_si128(a, _mm_cmpeq_epi32(_mm_setzero_si128(), _mm_setzero_si128())); +} + +static DRFLAC_INLINE __m128i drflac__mm_hadd_epi32(__m128i x) +{ + __m128i x64 = _mm_add_epi32(x, _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2))); + __m128i x32 = _mm_shufflelo_epi16(x64, _MM_SHUFFLE(1, 0, 3, 2)); + return _mm_add_epi32(x64, x32); +} + +static DRFLAC_INLINE __m128i drflac__mm_hadd_epi64(__m128i x) +{ + return _mm_add_epi64(x, _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2))); +} + +static DRFLAC_INLINE __m128i drflac__mm_srai_epi64(__m128i x, int count) +{ + /* + To simplify this we are assuming count < 32. This restriction allows us to work on a low side and a high side. The low side + is shifted with zero bits, whereas the right side is shifted with sign bits. + */ + __m128i lo = _mm_srli_epi64(x, count); + __m128i hi = _mm_srai_epi32(x, count); + + hi = _mm_and_si128(hi, _mm_set_epi32(0xFFFFFFFF, 0, 0xFFFFFFFF, 0)); /* The high part needs to have the low part cleared. */ + + return _mm_or_si128(lo, hi); +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41_32(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts0 = 0; + drflac_uint32 zeroCountParts1 = 0; + drflac_uint32 zeroCountParts2 = 0; + drflac_uint32 zeroCountParts3 = 0; + drflac_uint32 riceParamParts0 = 0; + drflac_uint32 riceParamParts1 = 0; + drflac_uint32 riceParamParts2 = 0; + drflac_uint32 riceParamParts3 = 0; + __m128i coefficients128_0; + __m128i coefficients128_4; + __m128i coefficients128_8; + __m128i samples128_0; + __m128i samples128_4; + __m128i samples128_8; + __m128i riceParamMask128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = _mm_set1_epi32(riceParamMask); + + /* Pre-load. */ + coefficients128_0 = _mm_setzero_si128(); + coefficients128_4 = _mm_setzero_si128(); + coefficients128_8 = _mm_setzero_si128(); + + samples128_0 = _mm_setzero_si128(); + samples128_4 = _mm_setzero_si128(); + samples128_8 = _mm_setzero_si128(); + + /* + Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than + what's available in the input buffers. It would be convenient to use a fall-through switch to do this, but this results + in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted + so I think there's opportunity for this to be simplified. + */ +#if 1 + { + int runningOrder = order; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = _mm_loadu_si128((const __m128i*)(coefficients + 0)); + samples128_0 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 4)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_0 = _mm_set_epi32(0, coefficients[2], coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], pSamplesOut[-3], 0); break; + case 2: coefficients128_0 = _mm_set_epi32(0, 0, coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], 0, 0); break; + case 1: coefficients128_0 = _mm_set_epi32(0, 0, 0, coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = _mm_loadu_si128((const __m128i*)(coefficients + 4)); + samples128_4 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 8)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_4 = _mm_set_epi32(0, coefficients[6], coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], pSamplesOut[-7], 0); break; + case 2: coefficients128_4 = _mm_set_epi32(0, 0, coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], 0, 0); break; + case 1: coefficients128_4 = _mm_set_epi32(0, 0, 0, coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = _mm_loadu_si128((const __m128i*)(coefficients + 8)); + samples128_8 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 12)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_8 = _mm_set_epi32(0, coefficients[10], coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], pSamplesOut[-11], 0); break; + case 2: coefficients128_8 = _mm_set_epi32(0, 0, coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], 0, 0); break; + case 1: coefficients128_8 = _mm_set_epi32(0, 0, 0, coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = _mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_4 = _mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_8 = _mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(0, 1, 2, 3)); + } +#else + /* This causes strict-aliasing warnings with GCC. */ + switch (order) + { + case 12: ((drflac_int32*)&coefficients128_8)[0] = coefficients[11]; ((drflac_int32*)&samples128_8)[0] = pDecodedSamples[-12]; + case 11: ((drflac_int32*)&coefficients128_8)[1] = coefficients[10]; ((drflac_int32*)&samples128_8)[1] = pDecodedSamples[-11]; + case 10: ((drflac_int32*)&coefficients128_8)[2] = coefficients[ 9]; ((drflac_int32*)&samples128_8)[2] = pDecodedSamples[-10]; + case 9: ((drflac_int32*)&coefficients128_8)[3] = coefficients[ 8]; ((drflac_int32*)&samples128_8)[3] = pDecodedSamples[- 9]; + case 8: ((drflac_int32*)&coefficients128_4)[0] = coefficients[ 7]; ((drflac_int32*)&samples128_4)[0] = pDecodedSamples[- 8]; + case 7: ((drflac_int32*)&coefficients128_4)[1] = coefficients[ 6]; ((drflac_int32*)&samples128_4)[1] = pDecodedSamples[- 7]; + case 6: ((drflac_int32*)&coefficients128_4)[2] = coefficients[ 5]; ((drflac_int32*)&samples128_4)[2] = pDecodedSamples[- 6]; + case 5: ((drflac_int32*)&coefficients128_4)[3] = coefficients[ 4]; ((drflac_int32*)&samples128_4)[3] = pDecodedSamples[- 5]; + case 4: ((drflac_int32*)&coefficients128_0)[0] = coefficients[ 3]; ((drflac_int32*)&samples128_0)[0] = pDecodedSamples[- 4]; + case 3: ((drflac_int32*)&coefficients128_0)[1] = coefficients[ 2]; ((drflac_int32*)&samples128_0)[1] = pDecodedSamples[- 3]; + case 2: ((drflac_int32*)&coefficients128_0)[2] = coefficients[ 1]; ((drflac_int32*)&samples128_0)[2] = pDecodedSamples[- 2]; + case 1: ((drflac_int32*)&coefficients128_0)[3] = coefficients[ 0]; ((drflac_int32*)&samples128_0)[3] = pDecodedSamples[- 1]; + } +#endif + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + __m128i prediction128; + __m128i zeroCountPart128; + __m128i riceParamPart128; + + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts1, &riceParamParts1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts2, &riceParamParts2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts3, &riceParamParts3)) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = _mm_set_epi32(zeroCountParts3, zeroCountParts2, zeroCountParts1, zeroCountParts0); + riceParamPart128 = _mm_set_epi32(riceParamParts3, riceParamParts2, riceParamParts1, riceParamParts0); + + riceParamPart128 = _mm_and_si128(riceParamPart128, riceParamMask128); + riceParamPart128 = _mm_or_si128(riceParamPart128, _mm_slli_epi32(zeroCountPart128, riceParam)); + riceParamPart128 = _mm_xor_si128(_mm_srli_epi32(riceParamPart128, 1), _mm_add_epi32(drflac__mm_not_si128(_mm_and_si128(riceParamPart128, _mm_set1_epi32(0x01))), _mm_set1_epi32(0x01))); /* <-- SSE2 compatible */ + /*riceParamPart128 = _mm_xor_si128(_mm_srli_epi32(riceParamPart128, 1), _mm_mullo_epi32(_mm_and_si128(riceParamPart128, _mm_set1_epi32(0x01)), _mm_set1_epi32(0xFFFFFFFF)));*/ /* <-- Only supported from SSE4.1 and is slower in my testing... */ + + if (order <= 4) { + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_mullo_epi32(coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi32(prediction128); + prediction128 = _mm_srai_epi32(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + } else if (order <= 8) { + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_mullo_epi32(coefficients128_4, samples128_4); + prediction128 = _mm_add_epi32(prediction128, _mm_mullo_epi32(coefficients128_0, samples128_0)); + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi32(prediction128); + prediction128 = _mm_srai_epi32(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + samples128_4 = _mm_alignr_epi8(samples128_0, samples128_4, 4); + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + } else { + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_mullo_epi32(coefficients128_8, samples128_8); + prediction128 = _mm_add_epi32(prediction128, _mm_mullo_epi32(coefficients128_4, samples128_4)); + prediction128 = _mm_add_epi32(prediction128, _mm_mullo_epi32(coefficients128_0, samples128_0)); + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi32(prediction128); + prediction128 = _mm_srai_epi32(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + samples128_8 = _mm_alignr_epi8(samples128_4, samples128_8, 4); + samples128_4 = _mm_alignr_epi8(samples128_0, samples128_4, 4); + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + } + + /* We store samples in groups of 4. */ + _mm_storeu_si128((__m128i*)pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts0 &= riceParamMask; + riceParamParts0 |= (zeroCountParts0 << riceParam); + riceParamParts0 = (riceParamParts0 >> 1) ^ t[riceParamParts0 & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts0 + drflac__calculate_prediction_32(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41_64(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts0 = 0; + drflac_uint32 zeroCountParts1 = 0; + drflac_uint32 zeroCountParts2 = 0; + drflac_uint32 zeroCountParts3 = 0; + drflac_uint32 riceParamParts0 = 0; + drflac_uint32 riceParamParts1 = 0; + drflac_uint32 riceParamParts2 = 0; + drflac_uint32 riceParamParts3 = 0; + __m128i coefficients128_0; + __m128i coefficients128_4; + __m128i coefficients128_8; + __m128i samples128_0; + __m128i samples128_4; + __m128i samples128_8; + __m128i prediction128; + __m128i riceParamMask128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + DRFLAC_ASSERT(order <= 12); + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = _mm_set1_epi32(riceParamMask); + + prediction128 = _mm_setzero_si128(); + + /* Pre-load. */ + coefficients128_0 = _mm_setzero_si128(); + coefficients128_4 = _mm_setzero_si128(); + coefficients128_8 = _mm_setzero_si128(); + + samples128_0 = _mm_setzero_si128(); + samples128_4 = _mm_setzero_si128(); + samples128_8 = _mm_setzero_si128(); + +#if 1 + { + int runningOrder = order; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = _mm_loadu_si128((const __m128i*)(coefficients + 0)); + samples128_0 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 4)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_0 = _mm_set_epi32(0, coefficients[2], coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], pSamplesOut[-3], 0); break; + case 2: coefficients128_0 = _mm_set_epi32(0, 0, coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], 0, 0); break; + case 1: coefficients128_0 = _mm_set_epi32(0, 0, 0, coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = _mm_loadu_si128((const __m128i*)(coefficients + 4)); + samples128_4 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 8)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_4 = _mm_set_epi32(0, coefficients[6], coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], pSamplesOut[-7], 0); break; + case 2: coefficients128_4 = _mm_set_epi32(0, 0, coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], 0, 0); break; + case 1: coefficients128_4 = _mm_set_epi32(0, 0, 0, coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = _mm_loadu_si128((const __m128i*)(coefficients + 8)); + samples128_8 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 12)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_8 = _mm_set_epi32(0, coefficients[10], coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], pSamplesOut[-11], 0); break; + case 2: coefficients128_8 = _mm_set_epi32(0, 0, coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], 0, 0); break; + case 1: coefficients128_8 = _mm_set_epi32(0, 0, 0, coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = _mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_4 = _mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_8 = _mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(0, 1, 2, 3)); + } +#else + switch (order) + { + case 12: ((drflac_int32*)&coefficients128_8)[0] = coefficients[11]; ((drflac_int32*)&samples128_8)[0] = pDecodedSamples[-12]; + case 11: ((drflac_int32*)&coefficients128_8)[1] = coefficients[10]; ((drflac_int32*)&samples128_8)[1] = pDecodedSamples[-11]; + case 10: ((drflac_int32*)&coefficients128_8)[2] = coefficients[ 9]; ((drflac_int32*)&samples128_8)[2] = pDecodedSamples[-10]; + case 9: ((drflac_int32*)&coefficients128_8)[3] = coefficients[ 8]; ((drflac_int32*)&samples128_8)[3] = pDecodedSamples[- 9]; + case 8: ((drflac_int32*)&coefficients128_4)[0] = coefficients[ 7]; ((drflac_int32*)&samples128_4)[0] = pDecodedSamples[- 8]; + case 7: ((drflac_int32*)&coefficients128_4)[1] = coefficients[ 6]; ((drflac_int32*)&samples128_4)[1] = pDecodedSamples[- 7]; + case 6: ((drflac_int32*)&coefficients128_4)[2] = coefficients[ 5]; ((drflac_int32*)&samples128_4)[2] = pDecodedSamples[- 6]; + case 5: ((drflac_int32*)&coefficients128_4)[3] = coefficients[ 4]; ((drflac_int32*)&samples128_4)[3] = pDecodedSamples[- 5]; + case 4: ((drflac_int32*)&coefficients128_0)[0] = coefficients[ 3]; ((drflac_int32*)&samples128_0)[0] = pDecodedSamples[- 4]; + case 3: ((drflac_int32*)&coefficients128_0)[1] = coefficients[ 2]; ((drflac_int32*)&samples128_0)[1] = pDecodedSamples[- 3]; + case 2: ((drflac_int32*)&coefficients128_0)[2] = coefficients[ 1]; ((drflac_int32*)&samples128_0)[2] = pDecodedSamples[- 2]; + case 1: ((drflac_int32*)&coefficients128_0)[3] = coefficients[ 0]; ((drflac_int32*)&samples128_0)[3] = pDecodedSamples[- 1]; + } +#endif + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + __m128i zeroCountPart128; + __m128i riceParamPart128; + + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts1, &riceParamParts1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts2, &riceParamParts2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts3, &riceParamParts3)) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = _mm_set_epi32(zeroCountParts3, zeroCountParts2, zeroCountParts1, zeroCountParts0); + riceParamPart128 = _mm_set_epi32(riceParamParts3, riceParamParts2, riceParamParts1, riceParamParts0); + + riceParamPart128 = _mm_and_si128(riceParamPart128, riceParamMask128); + riceParamPart128 = _mm_or_si128(riceParamPart128, _mm_slli_epi32(zeroCountPart128, riceParam)); + riceParamPart128 = _mm_xor_si128(_mm_srli_epi32(riceParamPart128, 1), _mm_add_epi32(drflac__mm_not_si128(_mm_and_si128(riceParamPart128, _mm_set1_epi32(1))), _mm_set1_epi32(1))); + + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_xor_si128(prediction128, prediction128); /* Reset to 0. */ + + switch (order) + { + case 12: + case 11: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(1, 1, 0, 0)), _mm_shuffle_epi32(samples128_8, _MM_SHUFFLE(1, 1, 0, 0)))); + case 10: + case 9: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(3, 3, 2, 2)), _mm_shuffle_epi32(samples128_8, _MM_SHUFFLE(3, 3, 2, 2)))); + case 8: + case 7: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(1, 1, 0, 0)), _mm_shuffle_epi32(samples128_4, _MM_SHUFFLE(1, 1, 0, 0)))); + case 6: + case 5: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(3, 3, 2, 2)), _mm_shuffle_epi32(samples128_4, _MM_SHUFFLE(3, 3, 2, 2)))); + case 4: + case 3: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(1, 1, 0, 0)), _mm_shuffle_epi32(samples128_0, _MM_SHUFFLE(1, 1, 0, 0)))); + case 2: + case 1: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(3, 3, 2, 2)), _mm_shuffle_epi32(samples128_0, _MM_SHUFFLE(3, 3, 2, 2)))); + } + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi64(prediction128); + prediction128 = drflac__mm_srai_epi64(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + /* Our value should be sitting in prediction128[0]. We need to combine this with our SSE samples. */ + samples128_8 = _mm_alignr_epi8(samples128_4, samples128_8, 4); + samples128_4 = _mm_alignr_epi8(samples128_0, samples128_4, 4); + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + + /* Slide our rice parameter down so that the value in position 0 contains the next one to process. */ + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + + /* We store samples in groups of 4. */ + _mm_storeu_si128((__m128i*)pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts0 &= riceParamMask; + riceParamParts0 |= (zeroCountParts0 << riceParam); + riceParamParts0 = (riceParamParts0 >> 1) ^ t[riceParamParts0 & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts0 + drflac__calculate_prediction_64(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + /* In my testing the order is rarely > 12, so in this case I'm going to simplify the SSE implementation by only handling order <= 12. */ + if (lpcOrder > 0 && lpcOrder <= 12) { + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + return drflac__decode_samples_with_residual__rice__sse41_64(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } else { + return drflac__decode_samples_with_residual__rice__sse41_32(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } + } else { + return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac__vst2q_s32(drflac_int32* p, int32x4x2_t x) +{ + vst1q_s32(p+0, x.val[0]); + vst1q_s32(p+4, x.val[1]); +} + +static DRFLAC_INLINE void drflac__vst2q_u32(drflac_uint32* p, uint32x4x2_t x) +{ + vst1q_u32(p+0, x.val[0]); + vst1q_u32(p+4, x.val[1]); +} + +static DRFLAC_INLINE void drflac__vst2q_f32(float* p, float32x4x2_t x) +{ + vst1q_f32(p+0, x.val[0]); + vst1q_f32(p+4, x.val[1]); +} + +static DRFLAC_INLINE void drflac__vst2q_s16(drflac_int16* p, int16x4x2_t x) +{ + vst1q_s16(p, vcombine_s16(x.val[0], x.val[1])); +} + +static DRFLAC_INLINE void drflac__vst2q_u16(drflac_uint16* p, uint16x4x2_t x) +{ + vst1q_u16(p, vcombine_u16(x.val[0], x.val[1])); +} + +static DRFLAC_INLINE int32x4_t drflac__vdupq_n_s32x4(drflac_int32 x3, drflac_int32 x2, drflac_int32 x1, drflac_int32 x0) +{ + drflac_int32 x[4]; + x[3] = x3; + x[2] = x2; + x[1] = x1; + x[0] = x0; + return vld1q_s32(x); +} + +static DRFLAC_INLINE int32x4_t drflac__valignrq_s32_1(int32x4_t a, int32x4_t b) +{ + /* Equivalent to SSE's _mm_alignr_epi8(a, b, 4) */ + + /* Reference */ + /*return drflac__vdupq_n_s32x4( + vgetq_lane_s32(a, 0), + vgetq_lane_s32(b, 3), + vgetq_lane_s32(b, 2), + vgetq_lane_s32(b, 1) + );*/ + + return vextq_s32(b, a, 1); +} + +static DRFLAC_INLINE uint32x4_t drflac__valignrq_u32_1(uint32x4_t a, uint32x4_t b) +{ + /* Equivalent to SSE's _mm_alignr_epi8(a, b, 4) */ + + /* Reference */ + /*return drflac__vdupq_n_s32x4( + vgetq_lane_s32(a, 0), + vgetq_lane_s32(b, 3), + vgetq_lane_s32(b, 2), + vgetq_lane_s32(b, 1) + );*/ + + return vextq_u32(b, a, 1); +} + +static DRFLAC_INLINE int32x2_t drflac__vhaddq_s32(int32x4_t x) +{ + /* The sum must end up in position 0. */ + + /* Reference */ + /*return vdupq_n_s32( + vgetq_lane_s32(x, 3) + + vgetq_lane_s32(x, 2) + + vgetq_lane_s32(x, 1) + + vgetq_lane_s32(x, 0) + );*/ + + int32x2_t r = vadd_s32(vget_high_s32(x), vget_low_s32(x)); + return vpadd_s32(r, r); +} + +static DRFLAC_INLINE int64x1_t drflac__vhaddq_s64(int64x2_t x) +{ + return vadd_s64(vget_high_s64(x), vget_low_s64(x)); +} + +static DRFLAC_INLINE int32x4_t drflac__vrevq_s32(int32x4_t x) +{ + /* Reference */ + /*return drflac__vdupq_n_s32x4( + vgetq_lane_s32(x, 0), + vgetq_lane_s32(x, 1), + vgetq_lane_s32(x, 2), + vgetq_lane_s32(x, 3) + );*/ + + return vrev64q_s32(vcombine_s32(vget_high_s32(x), vget_low_s32(x))); +} + +static DRFLAC_INLINE int32x4_t drflac__vnotq_s32(int32x4_t x) +{ + return veorq_s32(x, vdupq_n_s32(0xFFFFFFFF)); +} + +static DRFLAC_INLINE uint32x4_t drflac__vnotq_u32(uint32x4_t x) +{ + return veorq_u32(x, vdupq_n_u32(0xFFFFFFFF)); +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_32(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts[4]; + drflac_uint32 riceParamParts[4]; + int32x4_t coefficients128_0; + int32x4_t coefficients128_4; + int32x4_t coefficients128_8; + int32x4_t samples128_0; + int32x4_t samples128_4; + int32x4_t samples128_8; + uint32x4_t riceParamMask128; + int32x4_t riceParam128; + int32x2_t shift64; + uint32x4_t one128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = vdupq_n_u32(riceParamMask); + + riceParam128 = vdupq_n_s32(riceParam); + shift64 = vdup_n_s32(-shift); /* Negate the shift because we'll be doing a variable shift using vshlq_s32(). */ + one128 = vdupq_n_u32(1); + + /* + Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than + what's available in the input buffers. It would be conenient to use a fall-through switch to do this, but this results + in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted + so I think there's opportunity for this to be simplified. + */ + { + int runningOrder = order; + drflac_int32 tempC[4] = {0, 0, 0, 0}; + drflac_int32 tempS[4] = {0, 0, 0, 0}; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = vld1q_s32(coefficients + 0); + samples128_0 = vld1q_s32(pSamplesOut - 4); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[2]; tempS[1] = pSamplesOut[-3]; /* fallthrough */ + case 2: tempC[1] = coefficients[1]; tempS[2] = pSamplesOut[-2]; /* fallthrough */ + case 1: tempC[0] = coefficients[0]; tempS[3] = pSamplesOut[-1]; /* fallthrough */ + } + + coefficients128_0 = vld1q_s32(tempC); + samples128_0 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = vld1q_s32(coefficients + 4); + samples128_4 = vld1q_s32(pSamplesOut - 8); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[6]; tempS[1] = pSamplesOut[-7]; /* fallthrough */ + case 2: tempC[1] = coefficients[5]; tempS[2] = pSamplesOut[-6]; /* fallthrough */ + case 1: tempC[0] = coefficients[4]; tempS[3] = pSamplesOut[-5]; /* fallthrough */ + } + + coefficients128_4 = vld1q_s32(tempC); + samples128_4 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = vld1q_s32(coefficients + 8); + samples128_8 = vld1q_s32(pSamplesOut - 12); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[10]; tempS[1] = pSamplesOut[-11]; /* fallthrough */ + case 2: tempC[1] = coefficients[ 9]; tempS[2] = pSamplesOut[-10]; /* fallthrough */ + case 1: tempC[0] = coefficients[ 8]; tempS[3] = pSamplesOut[- 9]; /* fallthrough */ + } + + coefficients128_8 = vld1q_s32(tempC); + samples128_8 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = drflac__vrevq_s32(coefficients128_0); + coefficients128_4 = drflac__vrevq_s32(coefficients128_4); + coefficients128_8 = drflac__vrevq_s32(coefficients128_8); + } + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + int32x4_t prediction128; + int32x2_t prediction64; + uint32x4_t zeroCountPart128; + uint32x4_t riceParamPart128; + + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[1], &riceParamParts[1]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[2], &riceParamParts[2]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[3], &riceParamParts[3])) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = vld1q_u32(zeroCountParts); + riceParamPart128 = vld1q_u32(riceParamParts); + + riceParamPart128 = vandq_u32(riceParamPart128, riceParamMask128); + riceParamPart128 = vorrq_u32(riceParamPart128, vshlq_u32(zeroCountPart128, riceParam128)); + riceParamPart128 = veorq_u32(vshrq_n_u32(riceParamPart128, 1), vaddq_u32(drflac__vnotq_u32(vandq_u32(riceParamPart128, one128)), one128)); + + if (order <= 4) { + for (i = 0; i < 4; i += 1) { + prediction128 = vmulq_s32(coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s32(prediction128); + prediction64 = vshl_s32(prediction64, shift64); + prediction64 = vadd_s32(prediction64, vget_low_s32(vreinterpretq_s32_u32(riceParamPart128))); + + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(prediction64, vdup_n_s32(0)), samples128_0); + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + } else if (order <= 8) { + for (i = 0; i < 4; i += 1) { + prediction128 = vmulq_s32(coefficients128_4, samples128_4); + prediction128 = vmlaq_s32(prediction128, coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s32(prediction128); + prediction64 = vshl_s32(prediction64, shift64); + prediction64 = vadd_s32(prediction64, vget_low_s32(vreinterpretq_s32_u32(riceParamPart128))); + + samples128_4 = drflac__valignrq_s32_1(samples128_0, samples128_4); + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(prediction64, vdup_n_s32(0)), samples128_0); + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + } else { + for (i = 0; i < 4; i += 1) { + prediction128 = vmulq_s32(coefficients128_8, samples128_8); + prediction128 = vmlaq_s32(prediction128, coefficients128_4, samples128_4); + prediction128 = vmlaq_s32(prediction128, coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s32(prediction128); + prediction64 = vshl_s32(prediction64, shift64); + prediction64 = vadd_s32(prediction64, vget_low_s32(vreinterpretq_s32_u32(riceParamPart128))); + + samples128_8 = drflac__valignrq_s32_1(samples128_4, samples128_8); + samples128_4 = drflac__valignrq_s32_1(samples128_0, samples128_4); + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(prediction64, vdup_n_s32(0)), samples128_0); + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + } + + /* We store samples in groups of 4. */ + vst1q_s32(pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0])) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts[0] &= riceParamMask; + riceParamParts[0] |= (zeroCountParts[0] << riceParam); + riceParamParts[0] = (riceParamParts[0] >> 1) ^ t[riceParamParts[0] & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts[0] + drflac__calculate_prediction_32(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_64(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts[4]; + drflac_uint32 riceParamParts[4]; + int32x4_t coefficients128_0; + int32x4_t coefficients128_4; + int32x4_t coefficients128_8; + int32x4_t samples128_0; + int32x4_t samples128_4; + int32x4_t samples128_8; + uint32x4_t riceParamMask128; + int32x4_t riceParam128; + int64x1_t shift64; + uint32x4_t one128; + int64x2_t prediction128 = { 0 }; + uint32x4_t zeroCountPart128; + uint32x4_t riceParamPart128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = vdupq_n_u32(riceParamMask); + + riceParam128 = vdupq_n_s32(riceParam); + shift64 = vdup_n_s64(-shift); /* Negate the shift because we'll be doing a variable shift using vshlq_s32(). */ + one128 = vdupq_n_u32(1); + + /* + Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than + what's available in the input buffers. It would be convenient to use a fall-through switch to do this, but this results + in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted + so I think there's opportunity for this to be simplified. + */ + { + int runningOrder = order; + drflac_int32 tempC[4] = {0, 0, 0, 0}; + drflac_int32 tempS[4] = {0, 0, 0, 0}; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = vld1q_s32(coefficients + 0); + samples128_0 = vld1q_s32(pSamplesOut - 4); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[2]; tempS[1] = pSamplesOut[-3]; /* fallthrough */ + case 2: tempC[1] = coefficients[1]; tempS[2] = pSamplesOut[-2]; /* fallthrough */ + case 1: tempC[0] = coefficients[0]; tempS[3] = pSamplesOut[-1]; /* fallthrough */ + } + + coefficients128_0 = vld1q_s32(tempC); + samples128_0 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = vld1q_s32(coefficients + 4); + samples128_4 = vld1q_s32(pSamplesOut - 8); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[6]; tempS[1] = pSamplesOut[-7]; /* fallthrough */ + case 2: tempC[1] = coefficients[5]; tempS[2] = pSamplesOut[-6]; /* fallthrough */ + case 1: tempC[0] = coefficients[4]; tempS[3] = pSamplesOut[-5]; /* fallthrough */ + } + + coefficients128_4 = vld1q_s32(tempC); + samples128_4 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = vld1q_s32(coefficients + 8); + samples128_8 = vld1q_s32(pSamplesOut - 12); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[10]; tempS[1] = pSamplesOut[-11]; /* fallthrough */ + case 2: tempC[1] = coefficients[ 9]; tempS[2] = pSamplesOut[-10]; /* fallthrough */ + case 1: tempC[0] = coefficients[ 8]; tempS[3] = pSamplesOut[- 9]; /* fallthrough */ + } + + coefficients128_8 = vld1q_s32(tempC); + samples128_8 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = drflac__vrevq_s32(coefficients128_0); + coefficients128_4 = drflac__vrevq_s32(coefficients128_4); + coefficients128_8 = drflac__vrevq_s32(coefficients128_8); + } + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[1], &riceParamParts[1]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[2], &riceParamParts[2]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[3], &riceParamParts[3])) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = vld1q_u32(zeroCountParts); + riceParamPart128 = vld1q_u32(riceParamParts); + + riceParamPart128 = vandq_u32(riceParamPart128, riceParamMask128); + riceParamPart128 = vorrq_u32(riceParamPart128, vshlq_u32(zeroCountPart128, riceParam128)); + riceParamPart128 = veorq_u32(vshrq_n_u32(riceParamPart128, 1), vaddq_u32(drflac__vnotq_u32(vandq_u32(riceParamPart128, one128)), one128)); + + for (i = 0; i < 4; i += 1) { + int64x1_t prediction64; + + prediction128 = veorq_s64(prediction128, prediction128); /* Reset to 0. */ + switch (order) + { + case 12: + case 11: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_low_s32(coefficients128_8), vget_low_s32(samples128_8))); + case 10: + case 9: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_high_s32(coefficients128_8), vget_high_s32(samples128_8))); + case 8: + case 7: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_low_s32(coefficients128_4), vget_low_s32(samples128_4))); + case 6: + case 5: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_high_s32(coefficients128_4), vget_high_s32(samples128_4))); + case 4: + case 3: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_low_s32(coefficients128_0), vget_low_s32(samples128_0))); + case 2: + case 1: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_high_s32(coefficients128_0), vget_high_s32(samples128_0))); + } + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s64(prediction128); + prediction64 = vshl_s64(prediction64, shift64); + prediction64 = vadd_s64(prediction64, vdup_n_s64(vgetq_lane_u32(riceParamPart128, 0))); + + /* Our value should be sitting in prediction64[0]. We need to combine this with our SSE samples. */ + samples128_8 = drflac__valignrq_s32_1(samples128_4, samples128_8); + samples128_4 = drflac__valignrq_s32_1(samples128_0, samples128_4); + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(vreinterpret_s32_s64(prediction64), vdup_n_s32(0)), samples128_0); + + /* Slide our rice parameter down so that the value in position 0 contains the next one to process. */ + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + + /* We store samples in groups of 4. */ + vst1q_s32(pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0])) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts[0] &= riceParamMask; + riceParamParts[0] |= (zeroCountParts[0] << riceParam); + riceParamParts[0] = (riceParamParts[0] >> 1) ^ t[riceParamParts[0] & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts[0] + drflac__calculate_prediction_64(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__neon(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + /* In my testing the order is rarely > 12, so in this case I'm going to simplify the NEON implementation by only handling order <= 12. */ + if (lpcOrder > 0 && lpcOrder <= 12) { + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + return drflac__decode_samples_with_residual__rice__neon_64(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } else { + return drflac__decode_samples_with_residual__rice__neon_32(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } + } else { + return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } +} +#endif + +static drflac_bool32 drflac__decode_samples_with_residual__rice(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ +#if defined(DRFLAC_SUPPORT_SSE41) + if (drflac__gIsSSE41Supported) { + return drflac__decode_samples_with_residual__rice__sse41(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported) { + return drflac__decode_samples_with_residual__rice__neon(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } else +#endif + { + /* Scalar fallback. */ + #if 0 + return drflac__decode_samples_with_residual__rice__reference(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + #else + return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + #endif + } +} + +/* Reads and seeks past a string of residual values as Rice codes. The decoder should be sitting on the first bit of the Rice codes. */ +static drflac_bool32 drflac__read_and_seek_residual__rice(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam) +{ + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + + for (i = 0; i < count; ++i) { + if (!drflac__seek_rice_parts(bs, riceParam)) { + return DRFLAC_FALSE; + } + } + + return DRFLAC_TRUE; +} + +#if defined(__clang__) +__attribute__((no_sanitize("signed-integer-overflow"))) +#endif +static drflac_bool32 drflac__decode_samples_with_residual__unencoded(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 unencodedBitsPerSample, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(unencodedBitsPerSample <= 31); /* <-- unencodedBitsPerSample is a 5 bit number, so cannot exceed 31. */ + DRFLAC_ASSERT(pSamplesOut != NULL); + + for (i = 0; i < count; ++i) { + if (unencodedBitsPerSample > 0) { + if (!drflac__read_int32(bs, unencodedBitsPerSample, pSamplesOut + i)) { + return DRFLAC_FALSE; + } + } else { + pSamplesOut[i] = 0; + } + + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + pSamplesOut[i] += drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + i); + } else { + pSamplesOut[i] += drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + i); + } + } + + return DRFLAC_TRUE; +} + + +/* +Reads and decodes the residual for the sub-frame the decoder is currently sitting on. This function should be called +when the decoder is sitting at the very start of the RESIDUAL block. The first residuals will be ignored. The + and parameters are used to determine how many residual values need to be decoded. +*/ +static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 blockSize, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pDecodedSamples) +{ + drflac_uint8 residualMethod; + drflac_uint8 partitionOrder; + drflac_uint32 samplesInPartition; + drflac_uint32 partitionsRemaining; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(blockSize != 0); + DRFLAC_ASSERT(pDecodedSamples != NULL); /* <-- Should we allow NULL, in which case we just seek past the residual rather than do a full decode? */ + + if (!drflac__read_uint8(bs, 2, &residualMethod)) { + return DRFLAC_FALSE; + } + + if (residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE && residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { + return DRFLAC_FALSE; /* Unknown or unsupported residual coding method. */ + } + + /* Ignore the first values. */ + pDecodedSamples += lpcOrder; + + if (!drflac__read_uint8(bs, 4, &partitionOrder)) { + return DRFLAC_FALSE; + } + + /* + From the FLAC spec: + The Rice partition order in a Rice-coded residual section must be less than or equal to 8. + */ + if (partitionOrder > 8) { + return DRFLAC_FALSE; + } + + /* Validation check. */ + if ((blockSize / (1 << partitionOrder)) < lpcOrder) { + return DRFLAC_FALSE; + } + + samplesInPartition = (blockSize / (1 << partitionOrder)) - lpcOrder; + partitionsRemaining = (1 << partitionOrder); + for (;;) { + drflac_uint8 riceParam = 0; + if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE) { + if (!drflac__read_uint8(bs, 4, &riceParam)) { + return DRFLAC_FALSE; + } + if (riceParam == 15) { + riceParam = 0xFF; + } + } else if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { + if (!drflac__read_uint8(bs, 5, &riceParam)) { + return DRFLAC_FALSE; + } + if (riceParam == 31) { + riceParam = 0xFF; + } + } + + if (riceParam != 0xFF) { + if (!drflac__decode_samples_with_residual__rice(bs, bitsPerSample, samplesInPartition, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) { + return DRFLAC_FALSE; + } + } else { + drflac_uint8 unencodedBitsPerSample = 0; + if (!drflac__read_uint8(bs, 5, &unencodedBitsPerSample)) { + return DRFLAC_FALSE; + } + + if (!drflac__decode_samples_with_residual__unencoded(bs, bitsPerSample, samplesInPartition, unencodedBitsPerSample, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) { + return DRFLAC_FALSE; + } + } + + pDecodedSamples += samplesInPartition; + + if (partitionsRemaining == 1) { + break; + } + + partitionsRemaining -= 1; + + if (partitionOrder != 0) { + samplesInPartition = blockSize / (1 << partitionOrder); + } + } + + return DRFLAC_TRUE; +} + +/* +Reads and seeks past the residual for the sub-frame the decoder is currently sitting on. This function should be called +when the decoder is sitting at the very start of the RESIDUAL block. The first residuals will be set to 0. The + and parameters are used to determine how many residual values need to be decoded. +*/ +static drflac_bool32 drflac__read_and_seek_residual(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 order) +{ + drflac_uint8 residualMethod; + drflac_uint8 partitionOrder; + drflac_uint32 samplesInPartition; + drflac_uint32 partitionsRemaining; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(blockSize != 0); + + if (!drflac__read_uint8(bs, 2, &residualMethod)) { + return DRFLAC_FALSE; + } + + if (residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE && residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { + return DRFLAC_FALSE; /* Unknown or unsupported residual coding method. */ + } + + if (!drflac__read_uint8(bs, 4, &partitionOrder)) { + return DRFLAC_FALSE; + } + + /* + From the FLAC spec: + The Rice partition order in a Rice-coded residual section must be less than or equal to 8. + */ + if (partitionOrder > 8) { + return DRFLAC_FALSE; + } + + /* Validation check. */ + if ((blockSize / (1 << partitionOrder)) <= order) { + return DRFLAC_FALSE; + } + + samplesInPartition = (blockSize / (1 << partitionOrder)) - order; + partitionsRemaining = (1 << partitionOrder); + for (;;) + { + drflac_uint8 riceParam = 0; + if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE) { + if (!drflac__read_uint8(bs, 4, &riceParam)) { + return DRFLAC_FALSE; + } + if (riceParam == 15) { + riceParam = 0xFF; + } + } else if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { + if (!drflac__read_uint8(bs, 5, &riceParam)) { + return DRFLAC_FALSE; + } + if (riceParam == 31) { + riceParam = 0xFF; + } + } + + if (riceParam != 0xFF) { + if (!drflac__read_and_seek_residual__rice(bs, samplesInPartition, riceParam)) { + return DRFLAC_FALSE; + } + } else { + drflac_uint8 unencodedBitsPerSample = 0; + if (!drflac__read_uint8(bs, 5, &unencodedBitsPerSample)) { + return DRFLAC_FALSE; + } + + if (!drflac__seek_bits(bs, unencodedBitsPerSample * samplesInPartition)) { + return DRFLAC_FALSE; + } + } + + + if (partitionsRemaining == 1) { + break; + } + + partitionsRemaining -= 1; + samplesInPartition = blockSize / (1 << partitionOrder); + } + + return DRFLAC_TRUE; +} + + +static drflac_bool32 drflac__decode_samples__constant(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 subframeBitsPerSample, drflac_int32* pDecodedSamples) +{ + drflac_uint32 i; + + /* Only a single sample needs to be decoded here. */ + drflac_int32 sample; + if (!drflac__read_int32(bs, subframeBitsPerSample, &sample)) { + return DRFLAC_FALSE; + } + + /* + We don't really need to expand this, but it does simplify the process of reading samples. If this becomes a performance issue (unlikely) + we'll want to look at a more efficient way. + */ + for (i = 0; i < blockSize; ++i) { + pDecodedSamples[i] = sample; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples__verbatim(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 subframeBitsPerSample, drflac_int32* pDecodedSamples) +{ + drflac_uint32 i; + + for (i = 0; i < blockSize; ++i) { + drflac_int32 sample; + if (!drflac__read_int32(bs, subframeBitsPerSample, &sample)) { + return DRFLAC_FALSE; + } + + pDecodedSamples[i] = sample; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples__fixed(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 subframeBitsPerSample, drflac_uint8 lpcOrder, drflac_int32* pDecodedSamples) +{ + drflac_uint32 i; + + static drflac_int32 lpcCoefficientsTable[5][4] = { + {0, 0, 0, 0}, + {1, 0, 0, 0}, + {2, -1, 0, 0}, + {3, -3, 1, 0}, + {4, -6, 4, -1} + }; + + /* Warm up samples and coefficients. */ + for (i = 0; i < lpcOrder; ++i) { + drflac_int32 sample; + if (!drflac__read_int32(bs, subframeBitsPerSample, &sample)) { + return DRFLAC_FALSE; + } + + pDecodedSamples[i] = sample; + } + + if (!drflac__decode_samples_with_residual(bs, subframeBitsPerSample, blockSize, lpcOrder, 0, 4, lpcCoefficientsTable[lpcOrder], pDecodedSamples)) { + return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples__lpc(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 bitsPerSample, drflac_uint8 lpcOrder, drflac_int32* pDecodedSamples) +{ + drflac_uint8 i; + drflac_uint8 lpcPrecision; + drflac_int8 lpcShift; + drflac_int32 coefficients[32]; + + /* Warm up samples. */ + for (i = 0; i < lpcOrder; ++i) { + drflac_int32 sample; + if (!drflac__read_int32(bs, bitsPerSample, &sample)) { + return DRFLAC_FALSE; + } + + pDecodedSamples[i] = sample; + } + + if (!drflac__read_uint8(bs, 4, &lpcPrecision)) { + return DRFLAC_FALSE; + } + if (lpcPrecision == 15) { + return DRFLAC_FALSE; /* Invalid. */ + } + lpcPrecision += 1; + + if (!drflac__read_int8(bs, 5, &lpcShift)) { + return DRFLAC_FALSE; + } + + /* + From the FLAC specification: + + Quantized linear predictor coefficient shift needed in bits (NOTE: this number is signed two's-complement) + + Emphasis on the "signed two's-complement". In practice there does not seem to be any encoders nor decoders supporting negative shifts. For now dr_flac is + not going to support negative shifts as I don't have any reference files. However, when a reference file comes through I will consider adding support. + */ + if (lpcShift < 0) { + return DRFLAC_FALSE; + } + + DRFLAC_ZERO_MEMORY(coefficients, sizeof(coefficients)); + for (i = 0; i < lpcOrder; ++i) { + if (!drflac__read_int32(bs, lpcPrecision, coefficients + i)) { + return DRFLAC_FALSE; + } + } + + if (!drflac__decode_samples_with_residual(bs, bitsPerSample, blockSize, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) { + return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; +} + + +static drflac_bool32 drflac__read_next_flac_frame_header(drflac_bs* bs, drflac_uint8 streaminfoBitsPerSample, drflac_frame_header* header) +{ + const drflac_uint32 sampleRateTable[12] = {0, 88200, 176400, 192000, 8000, 16000, 22050, 24000, 32000, 44100, 48000, 96000}; + const drflac_uint8 bitsPerSampleTable[8] = {0, 8, 12, (drflac_uint8)-1, 16, 20, 24, (drflac_uint8)-1}; /* -1 = reserved. */ + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(header != NULL); + + /* Keep looping until we find a valid sync code. */ + for (;;) { + drflac_uint8 crc8 = 0xCE; /* 0xCE = drflac_crc8(0, 0x3FFE, 14); */ + drflac_uint8 reserved = 0; + drflac_uint8 blockingStrategy = 0; + drflac_uint8 blockSize = 0; + drflac_uint8 sampleRate = 0; + drflac_uint8 channelAssignment = 0; + drflac_uint8 bitsPerSample = 0; + drflac_bool32 isVariableBlockSize; + + if (!drflac__find_and_seek_to_next_sync_code(bs)) { + return DRFLAC_FALSE; + } + + if (!drflac__read_uint8(bs, 1, &reserved)) { + return DRFLAC_FALSE; + } + if (reserved == 1) { + continue; + } + crc8 = drflac_crc8(crc8, reserved, 1); + + if (!drflac__read_uint8(bs, 1, &blockingStrategy)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, blockingStrategy, 1); + + if (!drflac__read_uint8(bs, 4, &blockSize)) { + return DRFLAC_FALSE; + } + if (blockSize == 0) { + continue; + } + crc8 = drflac_crc8(crc8, blockSize, 4); + + if (!drflac__read_uint8(bs, 4, &sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, sampleRate, 4); + + if (!drflac__read_uint8(bs, 4, &channelAssignment)) { + return DRFLAC_FALSE; + } + if (channelAssignment > 10) { + continue; + } + crc8 = drflac_crc8(crc8, channelAssignment, 4); + + if (!drflac__read_uint8(bs, 3, &bitsPerSample)) { + return DRFLAC_FALSE; + } + if (bitsPerSample == 3 || bitsPerSample == 7) { + continue; + } + crc8 = drflac_crc8(crc8, bitsPerSample, 3); + + + if (!drflac__read_uint8(bs, 1, &reserved)) { + return DRFLAC_FALSE; + } + if (reserved == 1) { + continue; + } + crc8 = drflac_crc8(crc8, reserved, 1); + + + isVariableBlockSize = blockingStrategy == 1; + if (isVariableBlockSize) { + drflac_uint64 pcmFrameNumber; + drflac_result result = drflac__read_utf8_coded_number(bs, &pcmFrameNumber, &crc8); + if (result != DRFLAC_SUCCESS) { + if (result == DRFLAC_AT_END) { + return DRFLAC_FALSE; + } else { + continue; + } + } + header->flacFrameNumber = 0; + header->pcmFrameNumber = pcmFrameNumber; + } else { + drflac_uint64 flacFrameNumber = 0; + drflac_result result = drflac__read_utf8_coded_number(bs, &flacFrameNumber, &crc8); + if (result != DRFLAC_SUCCESS) { + if (result == DRFLAC_AT_END) { + return DRFLAC_FALSE; + } else { + continue; + } + } + header->flacFrameNumber = (drflac_uint32)flacFrameNumber; /* <-- Safe cast. */ + header->pcmFrameNumber = 0; + } + + + DRFLAC_ASSERT(blockSize > 0); + if (blockSize == 1) { + header->blockSizeInPCMFrames = 192; + } else if (blockSize <= 5) { + DRFLAC_ASSERT(blockSize >= 2); + header->blockSizeInPCMFrames = 576 * (1 << (blockSize - 2)); + } else if (blockSize == 6) { + if (!drflac__read_uint16(bs, 8, &header->blockSizeInPCMFrames)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->blockSizeInPCMFrames, 8); + header->blockSizeInPCMFrames += 1; + } else if (blockSize == 7) { + if (!drflac__read_uint16(bs, 16, &header->blockSizeInPCMFrames)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->blockSizeInPCMFrames, 16); + if (header->blockSizeInPCMFrames == 0xFFFF) { + return DRFLAC_FALSE; /* Frame is too big. This is the size of the frame minus 1. The STREAMINFO block defines the max block size which is 16-bits. Adding one will make it 17 bits and therefore too big. */ + } + header->blockSizeInPCMFrames += 1; + } else { + DRFLAC_ASSERT(blockSize >= 8); + header->blockSizeInPCMFrames = 256 * (1 << (blockSize - 8)); + } + + + if (sampleRate <= 11) { + header->sampleRate = sampleRateTable[sampleRate]; + } else if (sampleRate == 12) { + if (!drflac__read_uint32(bs, 8, &header->sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->sampleRate, 8); + header->sampleRate *= 1000; + } else if (sampleRate == 13) { + if (!drflac__read_uint32(bs, 16, &header->sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->sampleRate, 16); + } else if (sampleRate == 14) { + if (!drflac__read_uint32(bs, 16, &header->sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->sampleRate, 16); + header->sampleRate *= 10; + } else { + continue; /* Invalid. Assume an invalid block. */ + } + + + header->channelAssignment = channelAssignment; + + header->bitsPerSample = bitsPerSampleTable[bitsPerSample]; + if (header->bitsPerSample == 0) { + header->bitsPerSample = streaminfoBitsPerSample; + } + + if (header->bitsPerSample != streaminfoBitsPerSample) { + /* If this subframe has a different bitsPerSample then streaminfo or the first frame, reject it */ + return DRFLAC_FALSE; + } + + if (!drflac__read_uint8(bs, 8, &header->crc8)) { + return DRFLAC_FALSE; + } + +#ifndef DR_FLAC_NO_CRC + if (header->crc8 != crc8) { + continue; /* CRC mismatch. Loop back to the top and find the next sync code. */ + } +#endif + return DRFLAC_TRUE; + } +} + +static drflac_bool32 drflac__read_subframe_header(drflac_bs* bs, drflac_subframe* pSubframe) +{ + drflac_uint8 header; + int type; + + if (!drflac__read_uint8(bs, 8, &header)) { + return DRFLAC_FALSE; + } + + /* First bit should always be 0. */ + if ((header & 0x80) != 0) { + return DRFLAC_FALSE; + } + + /* + Default to 0 for the LPC order. It's important that we always set this to 0 for non LPC + and FIXED subframes because we'll be using it in a generic validation check later. + */ + pSubframe->lpcOrder = 0; + + type = (header & 0x7E) >> 1; + if (type == 0) { + pSubframe->subframeType = DRFLAC_SUBFRAME_CONSTANT; + } else if (type == 1) { + pSubframe->subframeType = DRFLAC_SUBFRAME_VERBATIM; + } else { + if ((type & 0x20) != 0) { + pSubframe->subframeType = DRFLAC_SUBFRAME_LPC; + pSubframe->lpcOrder = (drflac_uint8)(type & 0x1F) + 1; + } else if ((type & 0x08) != 0) { + pSubframe->subframeType = DRFLAC_SUBFRAME_FIXED; + pSubframe->lpcOrder = (drflac_uint8)(type & 0x07); + if (pSubframe->lpcOrder > 4) { + pSubframe->subframeType = DRFLAC_SUBFRAME_RESERVED; + pSubframe->lpcOrder = 0; + } + } else { + pSubframe->subframeType = DRFLAC_SUBFRAME_RESERVED; + } + } + + if (pSubframe->subframeType == DRFLAC_SUBFRAME_RESERVED) { + return DRFLAC_FALSE; + } + + /* Wasted bits per sample. */ + pSubframe->wastedBitsPerSample = 0; + if ((header & 0x01) == 1) { + unsigned int wastedBitsPerSample; + if (!drflac__seek_past_next_set_bit(bs, &wastedBitsPerSample)) { + return DRFLAC_FALSE; + } + pSubframe->wastedBitsPerSample = (drflac_uint8)wastedBitsPerSample + 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_subframe(drflac_bs* bs, drflac_frame* frame, int subframeIndex, drflac_int32* pDecodedSamplesOut) +{ + drflac_subframe* pSubframe; + drflac_uint32 subframeBitsPerSample; + drflac_bool32 decodeResult; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(frame != NULL); + + pSubframe = frame->subframes + subframeIndex; + if (!drflac__read_subframe_header(bs, pSubframe)) { + return DRFLAC_FALSE; + } + + /* Side channels require an extra bit per sample. Took a while to figure that one out... */ + subframeBitsPerSample = frame->header.bitsPerSample; + if ((frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE || frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE) && subframeIndex == 1) { + subframeBitsPerSample += 1; + } else if (frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE && subframeIndex == 0) { + subframeBitsPerSample += 1; + } + + if (subframeBitsPerSample > 32) { + /* libFLAC and ffmpeg reject 33-bit subframes as well */ + return DRFLAC_FALSE; + } + + /* Need to handle wasted bits per sample. */ + if (pSubframe->wastedBitsPerSample >= subframeBitsPerSample) { + return DRFLAC_FALSE; + } + subframeBitsPerSample -= pSubframe->wastedBitsPerSample; + + pSubframe->pSamplesS32 = pDecodedSamplesOut; + + /* + pDecodedSamplesOut will be pointing to a buffer that was allocated with enough memory to store + maxBlockSizeInPCMFrames samples (as specified in the FLAC header). We need to guard against an + overflow here. At a higher level we are checking maxBlockSizeInPCMFrames from the header, but + here we need to do an additional check to ensure this frame's block size fully encompasses any + warmup samples which is determined by the LPC order. For non LPC and FIXED subframes, the LPC + order will be have been set to 0 in drflac__read_subframe_header(). + */ + if (frame->header.blockSizeInPCMFrames < pSubframe->lpcOrder) { + return DRFLAC_FALSE; + } + + switch (pSubframe->subframeType) + { + case DRFLAC_SUBFRAME_CONSTANT: + { + decodeResult = drflac__decode_samples__constant(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->pSamplesS32); + } break; + + case DRFLAC_SUBFRAME_VERBATIM: + { + decodeResult = drflac__decode_samples__verbatim(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->pSamplesS32); + } break; + + case DRFLAC_SUBFRAME_FIXED: + { + decodeResult = drflac__decode_samples__fixed(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->lpcOrder, pSubframe->pSamplesS32); + } break; + + case DRFLAC_SUBFRAME_LPC: + { + decodeResult = drflac__decode_samples__lpc(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->lpcOrder, pSubframe->pSamplesS32); + } break; + + default: decodeResult = DRFLAC_FALSE; + } + + return decodeResult; +} + +static drflac_bool32 drflac__seek_subframe(drflac_bs* bs, drflac_frame* frame, int subframeIndex) +{ + drflac_subframe* pSubframe; + drflac_uint32 subframeBitsPerSample; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(frame != NULL); + + pSubframe = frame->subframes + subframeIndex; + if (!drflac__read_subframe_header(bs, pSubframe)) { + return DRFLAC_FALSE; + } + + /* Side channels require an extra bit per sample. Took a while to figure that one out... */ + subframeBitsPerSample = frame->header.bitsPerSample; + if ((frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE || frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE) && subframeIndex == 1) { + subframeBitsPerSample += 1; + } else if (frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE && subframeIndex == 0) { + subframeBitsPerSample += 1; + } + + /* Need to handle wasted bits per sample. */ + if (pSubframe->wastedBitsPerSample >= subframeBitsPerSample) { + return DRFLAC_FALSE; + } + subframeBitsPerSample -= pSubframe->wastedBitsPerSample; + + pSubframe->pSamplesS32 = NULL; + + switch (pSubframe->subframeType) + { + case DRFLAC_SUBFRAME_CONSTANT: + { + if (!drflac__seek_bits(bs, subframeBitsPerSample)) { + return DRFLAC_FALSE; + } + } break; + + case DRFLAC_SUBFRAME_VERBATIM: + { + unsigned int bitsToSeek = frame->header.blockSizeInPCMFrames * subframeBitsPerSample; + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; + } + } break; + + case DRFLAC_SUBFRAME_FIXED: + { + unsigned int bitsToSeek = pSubframe->lpcOrder * subframeBitsPerSample; + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; + } + + if (!drflac__read_and_seek_residual(bs, frame->header.blockSizeInPCMFrames, pSubframe->lpcOrder)) { + return DRFLAC_FALSE; + } + } break; + + case DRFLAC_SUBFRAME_LPC: + { + drflac_uint8 lpcPrecision; + + unsigned int bitsToSeek = pSubframe->lpcOrder * subframeBitsPerSample; + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; + } + + if (!drflac__read_uint8(bs, 4, &lpcPrecision)) { + return DRFLAC_FALSE; + } + if (lpcPrecision == 15) { + return DRFLAC_FALSE; /* Invalid. */ + } + lpcPrecision += 1; + + + bitsToSeek = (pSubframe->lpcOrder * lpcPrecision) + 5; /* +5 for shift. */ + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; + } + + if (!drflac__read_and_seek_residual(bs, frame->header.blockSizeInPCMFrames, pSubframe->lpcOrder)) { + return DRFLAC_FALSE; + } + } break; + + default: return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; +} + + +static DRFLAC_INLINE drflac_uint8 drflac__get_channel_count_from_channel_assignment(drflac_int8 channelAssignment) +{ + drflac_uint8 lookup[] = {1, 2, 3, 4, 5, 6, 7, 8, 2, 2, 2}; + + DRFLAC_ASSERT(channelAssignment <= 10); + return lookup[channelAssignment]; +} + +static drflac_result drflac__decode_flac_frame(drflac* pFlac) +{ + int channelCount; + int i; + drflac_uint8 paddingSizeInBits; + drflac_uint16 desiredCRC16; +#ifndef DR_FLAC_NO_CRC + drflac_uint16 actualCRC16; +#endif + + /* This function should be called while the stream is sitting on the first byte after the frame header. */ + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + DRFLAC_ZERO_MEMORY(pFlac->currentFLACFrame.subframes, sizeof(pFlac->currentFLACFrame.subframes)); + + /* The frame block size must never be larger than the maximum block size defined by the FLAC stream. */ + if (pFlac->currentFLACFrame.header.blockSizeInPCMFrames > pFlac->maxBlockSizeInPCMFrames) { + return DRFLAC_ERROR; + } + + /* The number of channels in the frame must match the channel count from the STREAMINFO block. */ + channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + if (channelCount != (int)pFlac->channels) { + return DRFLAC_ERROR; + } + + for (i = 0; i < channelCount; ++i) { + if (!drflac__decode_subframe(&pFlac->bs, &pFlac->currentFLACFrame, i, pFlac->pDecodedSamples + (pFlac->currentFLACFrame.header.blockSizeInPCMFrames * i))) { + return DRFLAC_ERROR; + } + } + + paddingSizeInBits = (drflac_uint8)(DRFLAC_CACHE_L1_BITS_REMAINING(&pFlac->bs) & 7); + if (paddingSizeInBits > 0) { + drflac_uint8 padding = 0; + if (!drflac__read_uint8(&pFlac->bs, paddingSizeInBits, &padding)) { + return DRFLAC_AT_END; + } + } + +#ifndef DR_FLAC_NO_CRC + actualCRC16 = drflac__flush_crc16(&pFlac->bs); +#endif + if (!drflac__read_uint16(&pFlac->bs, 16, &desiredCRC16)) { + return DRFLAC_AT_END; + } + +#ifndef DR_FLAC_NO_CRC + if (actualCRC16 != desiredCRC16) { + return DRFLAC_CRC_MISMATCH; /* CRC mismatch. */ + } +#endif + + pFlac->currentFLACFrame.pcmFramesRemaining = pFlac->currentFLACFrame.header.blockSizeInPCMFrames; + + return DRFLAC_SUCCESS; +} + +static drflac_result drflac__seek_flac_frame(drflac* pFlac) +{ + drflac_result result; + int channelCount; + int i; + drflac_uint16 desiredCRC16; +#ifndef DR_FLAC_NO_CRC + drflac_uint16 actualCRC16; +#endif + + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + + channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + for (i = 0; i < channelCount; ++i) { + if (!drflac__seek_subframe(&pFlac->bs, &pFlac->currentFLACFrame, i)) { + result = DRFLAC_ERROR; + goto error; + } + } + + /* Padding. */ + if (!drflac__seek_bits(&pFlac->bs, DRFLAC_CACHE_L1_BITS_REMAINING(&pFlac->bs) & 7)) { + result = DRFLAC_ERROR; + goto error; + } + + /* CRC. */ +#ifndef DR_FLAC_NO_CRC + actualCRC16 = drflac__flush_crc16(&pFlac->bs); +#endif + if (!drflac__read_uint16(&pFlac->bs, 16, &desiredCRC16)) { + result = DRFLAC_AT_END; + goto error; + } + +#ifndef DR_FLAC_NO_CRC + if (actualCRC16 != desiredCRC16) { + result = DRFLAC_CRC_MISMATCH; /* CRC mismatch. */ + goto error; + } +#endif + + return DRFLAC_SUCCESS; + +error: + DRFLAC_ZERO_MEMORY(pFlac->currentFLACFrame.subframes, sizeof(pFlac->currentFLACFrame.subframes)); + return result; +} + +static drflac_bool32 drflac__read_and_decode_next_flac_frame(drflac* pFlac) +{ + DRFLAC_ASSERT(pFlac != NULL); + + for (;;) { + drflac_result result; + + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + + result = drflac__decode_flac_frame(pFlac); + if (result != DRFLAC_SUCCESS) { + if (result == DRFLAC_CRC_MISMATCH) { + continue; /* CRC mismatch. Skip to the next frame. */ + } else { + return DRFLAC_FALSE; + } + } + + return DRFLAC_TRUE; + } +} + +static void drflac__get_pcm_frame_range_of_current_flac_frame(drflac* pFlac, drflac_uint64* pFirstPCMFrame, drflac_uint64* pLastPCMFrame) +{ + drflac_uint64 firstPCMFrame; + drflac_uint64 lastPCMFrame; + + DRFLAC_ASSERT(pFlac != NULL); + + firstPCMFrame = pFlac->currentFLACFrame.header.pcmFrameNumber; + if (firstPCMFrame == 0) { + firstPCMFrame = ((drflac_uint64)pFlac->currentFLACFrame.header.flacFrameNumber) * pFlac->maxBlockSizeInPCMFrames; + } + + lastPCMFrame = firstPCMFrame + pFlac->currentFLACFrame.header.blockSizeInPCMFrames; + if (lastPCMFrame > 0) { + lastPCMFrame -= 1; /* Needs to be zero based. */ + } + + if (pFirstPCMFrame) { + *pFirstPCMFrame = firstPCMFrame; + } + if (pLastPCMFrame) { + *pLastPCMFrame = lastPCMFrame; + } +} + +static drflac_bool32 drflac__seek_to_first_frame(drflac* pFlac) +{ + drflac_bool32 result; + + DRFLAC_ASSERT(pFlac != NULL); + + result = drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes); + + DRFLAC_ZERO_MEMORY(&pFlac->currentFLACFrame, sizeof(pFlac->currentFLACFrame)); + pFlac->currentPCMFrame = 0; + + return result; +} + +static DRFLAC_INLINE drflac_result drflac__seek_to_next_flac_frame(drflac* pFlac) +{ + /* This function should only ever be called while the decoder is sitting on the first byte past the FRAME_HEADER section. */ + DRFLAC_ASSERT(pFlac != NULL); + return drflac__seek_flac_frame(pFlac); +} + + +static drflac_uint64 drflac__seek_forward_by_pcm_frames(drflac* pFlac, drflac_uint64 pcmFramesToSeek) +{ + drflac_uint64 pcmFramesRead = 0; + while (pcmFramesToSeek > 0) { + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + if (pFlac->currentFLACFrame.pcmFramesRemaining > pcmFramesToSeek) { + pcmFramesRead += pcmFramesToSeek; + pFlac->currentFLACFrame.pcmFramesRemaining -= (drflac_uint32)pcmFramesToSeek; /* <-- Safe cast. Will always be < currentFrame.pcmFramesRemaining < 65536. */ + pcmFramesToSeek = 0; + } else { + pcmFramesRead += pFlac->currentFLACFrame.pcmFramesRemaining; + pcmFramesToSeek -= pFlac->currentFLACFrame.pcmFramesRemaining; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + } + } + } + + pFlac->currentPCMFrame += pcmFramesRead; + return pcmFramesRead; +} + + +static drflac_bool32 drflac__seek_to_pcm_frame__brute_force(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_bool32 isMidFrame = DRFLAC_FALSE; + drflac_uint64 runningPCMFrameCount; + + DRFLAC_ASSERT(pFlac != NULL); + + /* If we are seeking forward we start from the current position. Otherwise we need to start all the way from the start of the file. */ + if (pcmFrameIndex >= pFlac->currentPCMFrame) { + /* Seeking forward. Need to seek from the current position. */ + runningPCMFrameCount = pFlac->currentPCMFrame; + + /* The frame header for the first frame may not yet have been read. We need to do that if necessary. */ + if (pFlac->currentPCMFrame == 0 && pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } else { + isMidFrame = DRFLAC_TRUE; + } + } else { + /* Seeking backwards. Need to seek from the start of the file. */ + runningPCMFrameCount = 0; + + /* Move back to the start. */ + if (!drflac__seek_to_first_frame(pFlac)) { + return DRFLAC_FALSE; + } + + /* Decode the first frame in preparation for sample-exact seeking below. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } + + /* + We need to as quickly as possible find the frame that contains the target sample. To do this, we iterate over each frame and inspect its + header. If based on the header we can determine that the frame contains the sample, we do a full decode of that frame. + */ + for (;;) { + drflac_uint64 pcmFrameCountInThisFLACFrame; + drflac_uint64 firstPCMFrameInFLACFrame = 0; + drflac_uint64 lastPCMFrameInFLACFrame = 0; + + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &firstPCMFrameInFLACFrame, &lastPCMFrameInFLACFrame); + + pcmFrameCountInThisFLACFrame = (lastPCMFrameInFLACFrame - firstPCMFrameInFLACFrame) + 1; + if (pcmFrameIndex < (runningPCMFrameCount + pcmFrameCountInThisFLACFrame)) { + /* + The sample should be in this frame. We need to fully decode it, however if it's an invalid frame (a CRC mismatch), we need to pretend + it never existed and keep iterating. + */ + drflac_uint64 pcmFramesToDecode = pcmFrameIndex - runningPCMFrameCount; + + if (!isMidFrame) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + /* The frame is valid. We just need to skip over some samples to ensure it's sample-exact. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; /* <-- If this fails, something bad has happened (it should never fail). */ + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* We started seeking mid-frame which means we need to skip the frame decoding part. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; + } + } else { + /* + It's not in this frame. We need to seek past the frame, but check if there was a CRC mismatch. If so, we pretend this + frame never existed and leave the running sample count untouched. + */ + if (!isMidFrame) { + drflac_result result = drflac__seek_to_next_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + runningPCMFrameCount += pcmFrameCountInThisFLACFrame; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* + We started seeking mid-frame which means we need to seek by reading to the end of the frame instead of with + drflac__seek_to_next_flac_frame() which only works if the decoder is sitting on the byte just after the frame header. + */ + runningPCMFrameCount += pFlac->currentFLACFrame.pcmFramesRemaining; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + isMidFrame = DRFLAC_FALSE; + } + + /* If we are seeking to the end of the file and we've just hit it, we're done. */ + if (pcmFrameIndex == pFlac->totalPCMFrameCount && runningPCMFrameCount == pFlac->totalPCMFrameCount) { + return DRFLAC_TRUE; + } + } + + next_iteration: + /* Grab the next frame in preparation for the next iteration. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } +} + + +#if !defined(DR_FLAC_NO_CRC) +/* +We use an average compression ratio to determine our approximate start location. FLAC files are generally about 50%-70% the size of their +uncompressed counterparts so we'll use this as a basis. I'm going to split the middle and use a factor of 0.6 to determine the starting +location. +*/ +#define DRFLAC_BINARY_SEARCH_APPROX_COMPRESSION_RATIO 0.6f + +static drflac_bool32 drflac__seek_to_approximate_flac_frame_to_byte(drflac* pFlac, drflac_uint64 targetByte, drflac_uint64 rangeLo, drflac_uint64 rangeHi, drflac_uint64* pLastSuccessfulSeekOffset) +{ + DRFLAC_ASSERT(pFlac != NULL); + DRFLAC_ASSERT(pLastSuccessfulSeekOffset != NULL); + DRFLAC_ASSERT(targetByte >= rangeLo); + DRFLAC_ASSERT(targetByte <= rangeHi); + + *pLastSuccessfulSeekOffset = pFlac->firstFLACFramePosInBytes; + + for (;;) { + /* After rangeLo == rangeHi == targetByte fails, we need to break out. */ + drflac_uint64 lastTargetByte = targetByte; + + /* When seeking to a byte, failure probably means we've attempted to seek beyond the end of the stream. To counter this we just halve it each attempt. */ + if (!drflac__seek_to_byte(&pFlac->bs, targetByte)) { + /* If we couldn't even seek to the first byte in the stream we have a problem. Just abandon the whole thing. */ + if (targetByte == 0) { + drflac__seek_to_first_frame(pFlac); /* Try to recover. */ + return DRFLAC_FALSE; + } + + /* Halve the byte location and continue. */ + targetByte = rangeLo + ((rangeHi - rangeLo)/2); + rangeHi = targetByte; + } else { + /* Getting here should mean that we have seeked to an appropriate byte. */ + + /* Clear the details of the FLAC frame so we don't misreport data. */ + DRFLAC_ZERO_MEMORY(&pFlac->currentFLACFrame, sizeof(pFlac->currentFLACFrame)); + + /* + Now seek to the next FLAC frame. We need to decode the entire frame (not just the header) because it's possible for the header to incorrectly pass the + CRC check and return bad data. We need to decode the entire frame to be more certain. Although this seems unlikely, this has happened to me in testing + so it needs to stay this way for now. + */ +#if 1 + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + /* Halve the byte location and continue. */ + targetByte = rangeLo + ((rangeHi - rangeLo)/2); + rangeHi = targetByte; + } else { + break; + } +#else + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + /* Halve the byte location and continue. */ + targetByte = rangeLo + ((rangeHi - rangeLo)/2); + rangeHi = targetByte; + } else { + break; + } +#endif + } + + /* We already tried this byte and there are no more to try, break out. */ + if(targetByte == lastTargetByte) { + return DRFLAC_FALSE; + } + } + + /* The current PCM frame needs to be updated based on the frame we just seeked to. */ + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &pFlac->currentPCMFrame, NULL); + + DRFLAC_ASSERT(targetByte <= rangeHi); + + *pLastSuccessfulSeekOffset = targetByte; + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(drflac* pFlac, drflac_uint64 offset) +{ + /* This section of code would be used if we were only decoding the FLAC frame header when calling drflac__seek_to_approximate_flac_frame_to_byte(). */ +#if 0 + if (drflac__decode_flac_frame(pFlac) != DRFLAC_SUCCESS) { + /* We failed to decode this frame which may be due to it being corrupt. We'll just use the next valid FLAC frame. */ + if (drflac__read_and_decode_next_flac_frame(pFlac) == DRFLAC_FALSE) { + return DRFLAC_FALSE; + } + } +#endif + + return drflac__seek_forward_by_pcm_frames(pFlac, offset) == offset; +} + + +static drflac_bool32 drflac__seek_to_pcm_frame__binary_search_internal(drflac* pFlac, drflac_uint64 pcmFrameIndex, drflac_uint64 byteRangeLo, drflac_uint64 byteRangeHi) +{ + /* This assumes pFlac->currentPCMFrame is sitting on byteRangeLo upon entry. */ + + drflac_uint64 targetByte; + drflac_uint64 pcmRangeLo = pFlac->totalPCMFrameCount; + drflac_uint64 pcmRangeHi = 0; + drflac_uint64 lastSuccessfulSeekOffset = (drflac_uint64)-1; + drflac_uint64 closestSeekOffsetBeforeTargetPCMFrame = byteRangeLo; + drflac_uint32 seekForwardThreshold = (pFlac->maxBlockSizeInPCMFrames != 0) ? pFlac->maxBlockSizeInPCMFrames*2 : 4096; + + targetByte = byteRangeLo + (drflac_uint64)(((drflac_int64)((pcmFrameIndex - pFlac->currentPCMFrame) * pFlac->channels * pFlac->bitsPerSample)/8.0f) * DRFLAC_BINARY_SEARCH_APPROX_COMPRESSION_RATIO); + if (targetByte > byteRangeHi) { + targetByte = byteRangeHi; + } + + for (;;) { + /* + If only two adjacent byte offsets remain, binary search cannot narrow the range any further. Seek to the closest frame before the target and decode + forward from there. + */ + if ((byteRangeHi - byteRangeLo) == 1) { + if (!drflac__seek_to_approximate_flac_frame_to_byte(pFlac, closestSeekOffsetBeforeTargetPCMFrame, closestSeekOffsetBeforeTargetPCMFrame, byteRangeHi, &lastSuccessfulSeekOffset)) { + break; + } + + if (pFlac->currentPCMFrame <= pcmFrameIndex && drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame)) { + return DRFLAC_TRUE; + } + + break; + } + + if (drflac__seek_to_approximate_flac_frame_to_byte(pFlac, targetByte, byteRangeLo, byteRangeHi, &lastSuccessfulSeekOffset)) { + /* We found a FLAC frame. We need to check if it contains the sample we're looking for. */ + drflac_uint64 newPCMRangeLo; + drflac_uint64 newPCMRangeHi; + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &newPCMRangeLo, &newPCMRangeHi); + + /* If we selected the same frame, it means we should be pretty close. Just decode the rest. */ + if (pcmRangeLo == newPCMRangeLo) { + if (!drflac__seek_to_approximate_flac_frame_to_byte(pFlac, closestSeekOffsetBeforeTargetPCMFrame, closestSeekOffsetBeforeTargetPCMFrame, byteRangeHi, &lastSuccessfulSeekOffset)) { + break; /* Failed to seek to closest frame. */ + } + + if (drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame)) { + return DRFLAC_TRUE; + } else { + break; /* Failed to seek forward. */ + } + } + + pcmRangeLo = newPCMRangeLo; + pcmRangeHi = newPCMRangeHi; + + if (pcmRangeLo <= pcmFrameIndex && pcmRangeHi >= pcmFrameIndex) { + /* The target PCM frame is in this FLAC frame. */ + if (drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame) ) { + return DRFLAC_TRUE; + } else { + break; /* Failed to seek to FLAC frame. */ + } + } else { + if (pcmRangeLo > pcmFrameIndex) { + /* We seeked too far forward. We need to move our target byte backward and try again. */ + byteRangeHi = lastSuccessfulSeekOffset; + if (byteRangeLo > byteRangeHi) { + byteRangeLo = byteRangeHi; + } + + targetByte = byteRangeLo + ((byteRangeHi - byteRangeLo) / 2); + if (targetByte < byteRangeLo) { + targetByte = byteRangeLo; + } + } else /*if (pcmRangeHi < pcmFrameIndex)*/ { + /* We didn't seek far enough. We need to move our target byte forward and try again. */ + + /* If we're close enough we can just seek forward. */ + if ((pcmFrameIndex - pcmRangeLo) < seekForwardThreshold) { + if (drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame)) { + return DRFLAC_TRUE; + } else { + break; /* Failed to seek to FLAC frame. */ + } + } else { + const double approxCompressionRatio = (drflac_int64)(lastSuccessfulSeekOffset - pFlac->firstFLACFramePosInBytes) / ((drflac_int64)(pcmRangeLo * pFlac->channels * pFlac->bitsPerSample)/8.0); + + byteRangeLo = lastSuccessfulSeekOffset; + if (byteRangeHi < byteRangeLo) { + byteRangeHi = byteRangeLo; + } + + targetByte = lastSuccessfulSeekOffset + (drflac_uint64)(((drflac_int64)((pcmFrameIndex-pcmRangeLo) * pFlac->channels * pFlac->bitsPerSample)/8.0) * approxCompressionRatio); + if (targetByte > byteRangeHi) { + targetByte = byteRangeHi; + } + + if (closestSeekOffsetBeforeTargetPCMFrame < lastSuccessfulSeekOffset) { + closestSeekOffsetBeforeTargetPCMFrame = lastSuccessfulSeekOffset; + } + } + } + } + } else { + /* Getting here is really bad. We just recover as best we can, but moving to the first frame in the stream, and then abort. */ + break; + } + } + + drflac__seek_to_first_frame(pFlac); /* <-- Try to recover. */ + return DRFLAC_FALSE; +} + +static drflac_bool32 drflac__seek_to_pcm_frame__binary_search(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_uint64 byteRangeLo; + drflac_uint64 byteRangeHi; + drflac_uint32 seekForwardThreshold = (pFlac->maxBlockSizeInPCMFrames != 0) ? pFlac->maxBlockSizeInPCMFrames*2 : 4096; + + /* Our algorithm currently assumes the FLAC stream is currently sitting at the start. */ + if (drflac__seek_to_first_frame(pFlac) == DRFLAC_FALSE) { + return DRFLAC_FALSE; + } + + /* If we're close enough to the start, just move to the start and seek forward. */ + if (pcmFrameIndex < seekForwardThreshold) { + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFrameIndex) == pcmFrameIndex; + } + + /* + Our starting byte range is the byte position of the first FLAC frame and the approximate end of the file as if it were completely uncompressed. This ensures + the entire file is included, even though most of the time it'll exceed the end of the actual stream. This is OK as the frame searching logic will handle it. + */ + byteRangeLo = pFlac->firstFLACFramePosInBytes; + byteRangeHi = pFlac->firstFLACFramePosInBytes + (drflac_uint64)((drflac_int64)(pFlac->totalPCMFrameCount * pFlac->channels * pFlac->bitsPerSample)/8.0f); + + return drflac__seek_to_pcm_frame__binary_search_internal(pFlac, pcmFrameIndex, byteRangeLo, byteRangeHi); +} +#endif /* !DR_FLAC_NO_CRC */ + +static drflac_bool32 drflac__seek_to_pcm_frame__seek_table(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_uint32 iClosestSeekpoint = 0; + drflac_bool32 isMidFrame = DRFLAC_FALSE; + drflac_uint64 runningPCMFrameCount; + drflac_uint32 iSeekpoint; + + + DRFLAC_ASSERT(pFlac != NULL); + + if (pFlac->pSeekpoints == NULL || pFlac->seekpointCount == 0) { + return DRFLAC_FALSE; + } + + /* Do not use the seektable if pcmFramIndex is not coverd by it. */ + if (pFlac->pSeekpoints[0].firstPCMFrame > pcmFrameIndex) { + return DRFLAC_FALSE; + } + + for (iSeekpoint = 0; iSeekpoint < pFlac->seekpointCount; ++iSeekpoint) { + if (pFlac->pSeekpoints[iSeekpoint].firstPCMFrame >= pcmFrameIndex) { + break; + } + + iClosestSeekpoint = iSeekpoint; + } + + /* There's been cases where the seek table contains only zeros. We need to do some basic validation on the closest seekpoint. */ + if (pFlac->pSeekpoints[iClosestSeekpoint].pcmFrameCount == 0 || pFlac->pSeekpoints[iClosestSeekpoint].pcmFrameCount > pFlac->maxBlockSizeInPCMFrames) { + return DRFLAC_FALSE; + } + if (pFlac->pSeekpoints[iClosestSeekpoint].firstPCMFrame > pFlac->totalPCMFrameCount && pFlac->totalPCMFrameCount > 0) { + return DRFLAC_FALSE; + } + +#if !defined(DR_FLAC_NO_CRC) + /* At this point we should know the closest seek point. We can use a binary search for this. We need to know the total sample count for this. */ + if (pFlac->totalPCMFrameCount > 0) { + drflac_uint64 byteRangeLo; + drflac_uint64 byteRangeHi; + + byteRangeHi = pFlac->firstFLACFramePosInBytes + (drflac_uint64)((drflac_int64)(pFlac->totalPCMFrameCount * pFlac->channels * pFlac->bitsPerSample)/8.0f); + byteRangeLo = pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset; + + /* + If our closest seek point is not the last one, we only need to search between it and the next one. The section below calculates an appropriate starting + value for byteRangeHi which will clamp it appropriately. + + Note that the next seekpoint must have an offset greater than the closest seekpoint because otherwise our binary search algorithm will break down. There + have been cases where a seektable consists of seek points where every byte offset is set to 0 which causes problems. If this happens we need to abort. + */ + if (iClosestSeekpoint < pFlac->seekpointCount-1) { + drflac_uint32 iNextSeekpoint = iClosestSeekpoint + 1; + + /* Basic validation on the seekpoints to ensure they're usable. */ + if (pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset >= pFlac->pSeekpoints[iNextSeekpoint].flacFrameOffset || pFlac->pSeekpoints[iNextSeekpoint].pcmFrameCount == 0) { + return DRFLAC_FALSE; /* The next seekpoint doesn't look right. The seek table cannot be trusted from here. Abort. */ + } + + if (pFlac->pSeekpoints[iNextSeekpoint].firstPCMFrame != (((drflac_uint64)0xFFFFFFFF << 32) | 0xFFFFFFFF)) { /* Make sure it's not a placeholder seekpoint. */ + byteRangeHi = pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iNextSeekpoint].flacFrameOffset - 1; /* byteRangeHi must be zero based. */ + } + } + + if (drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset)) { + if (drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &pFlac->currentPCMFrame, NULL); + + if (drflac__seek_to_pcm_frame__binary_search_internal(pFlac, pcmFrameIndex, byteRangeLo, byteRangeHi)) { + return DRFLAC_TRUE; + } + } + } + } +#endif /* !DR_FLAC_NO_CRC */ + + /* Getting here means we need to use a slower algorithm because the binary search method failed or cannot be used. */ + + /* + If we are seeking forward and the closest seekpoint is _before_ the current sample, we just seek forward from where we are. Otherwise we start seeking + from the seekpoint's first sample. + */ + if (pcmFrameIndex >= pFlac->currentPCMFrame && pFlac->pSeekpoints[iClosestSeekpoint].firstPCMFrame <= pFlac->currentPCMFrame) { + /* Optimized case. Just seek forward from where we are. */ + runningPCMFrameCount = pFlac->currentPCMFrame; + + /* The frame header for the first frame may not yet have been read. We need to do that if necessary. */ + if (pFlac->currentPCMFrame == 0 && pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } else { + isMidFrame = DRFLAC_TRUE; + } + } else { + /* Slower case. Seek to the start of the seekpoint and then seek forward from there. */ + runningPCMFrameCount = pFlac->pSeekpoints[iClosestSeekpoint].firstPCMFrame; + + if (!drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset)) { + return DRFLAC_FALSE; + } + + /* Grab the frame the seekpoint is sitting on in preparation for the sample-exact seeking below. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } + + for (;;) { + drflac_uint64 pcmFrameCountInThisFLACFrame; + drflac_uint64 firstPCMFrameInFLACFrame = 0; + drflac_uint64 lastPCMFrameInFLACFrame = 0; + + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &firstPCMFrameInFLACFrame, &lastPCMFrameInFLACFrame); + + pcmFrameCountInThisFLACFrame = (lastPCMFrameInFLACFrame - firstPCMFrameInFLACFrame) + 1; + if (pcmFrameIndex < (runningPCMFrameCount + pcmFrameCountInThisFLACFrame)) { + /* + The sample should be in this frame. We need to fully decode it, but if it's an invalid frame (a CRC mismatch) we need to pretend + it never existed and keep iterating. + */ + drflac_uint64 pcmFramesToDecode = pcmFrameIndex - runningPCMFrameCount; + + if (!isMidFrame) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + /* The frame is valid. We just need to skip over some samples to ensure it's sample-exact. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; /* <-- If this fails, something bad has happened (it should never fail). */ + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* We started seeking mid-frame which means we need to skip the frame decoding part. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; + } + } else { + /* + It's not in this frame. We need to seek past the frame, but check if there was a CRC mismatch. If so, we pretend this + frame never existed and leave the running sample count untouched. + */ + if (!isMidFrame) { + drflac_result result = drflac__seek_to_next_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + runningPCMFrameCount += pcmFrameCountInThisFLACFrame; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* + We started seeking mid-frame which means we need to seek by reading to the end of the frame instead of with + drflac__seek_to_next_flac_frame() which only works if the decoder is sitting on the byte just after the frame header. + */ + runningPCMFrameCount += pFlac->currentFLACFrame.pcmFramesRemaining; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + isMidFrame = DRFLAC_FALSE; + } + + /* If we are seeking to the end of the file and we've just hit it, we're done. */ + if (pcmFrameIndex == pFlac->totalPCMFrameCount && runningPCMFrameCount == pFlac->totalPCMFrameCount) { + return DRFLAC_TRUE; + } + } + + next_iteration: + /* Grab the next frame in preparation for the next iteration. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } +} + + +#ifndef DR_FLAC_NO_OGG +typedef struct +{ + drflac_uint8 capturePattern[4]; /* Should be "OggS" */ + drflac_uint8 structureVersion; /* Always 0. */ + drflac_uint8 headerType; + drflac_uint64 granulePosition; + drflac_uint32 serialNumber; + drflac_uint32 sequenceNumber; + drflac_uint32 checksum; + drflac_uint8 segmentCount; + drflac_uint8 segmentTable[255]; +} drflac_ogg_page_header; +#endif + +typedef struct +{ + drflac_read_proc onRead; + drflac_seek_proc onSeek; + drflac_tell_proc onTell; + drflac_meta_proc onMeta; + drflac_container container; + void* pUserData; + void* pUserDataMD; + drflac_uint32 sampleRate; + drflac_uint8 channels; + drflac_uint8 bitsPerSample; + drflac_uint64 totalPCMFrameCount; + drflac_uint16 maxBlockSizeInPCMFrames; + drflac_uint64 runningFilePos; + drflac_bool32 hasStreamInfoBlock; + drflac_bool32 hasMetadataBlocks; + drflac_bs bs; /* <-- A bit streamer is required for loading data during initialization. */ + drflac_frame_header firstFrameHeader; /* <-- The header of the first frame that was read during relaxed initalization. Only set if there is no STREAMINFO block. */ + +#ifndef DR_FLAC_NO_OGG + drflac_uint32 oggSerial; + drflac_uint64 oggFirstBytePos; + drflac_ogg_page_header oggBosHeader; +#endif +} drflac_init_info; + +static DRFLAC_INLINE void drflac__decode_block_header(drflac_uint32 blockHeader, drflac_uint8* isLastBlock, drflac_uint8* blockType, drflac_uint32* blockSize) +{ + blockHeader = drflac__be2host_32(blockHeader); + *isLastBlock = (drflac_uint8)((blockHeader & 0x80000000UL) >> 31); + *blockType = (drflac_uint8)((blockHeader & 0x7F000000UL) >> 24); + *blockSize = (blockHeader & 0x00FFFFFFUL); +} + +static DRFLAC_INLINE drflac_bool32 drflac__read_and_decode_block_header(drflac_read_proc onRead, void* pUserData, drflac_uint8* isLastBlock, drflac_uint8* blockType, drflac_uint32* blockSize) +{ + drflac_uint32 blockHeader; + + *blockSize = 0; + if (onRead(pUserData, &blockHeader, 4) != 4) { + return DRFLAC_FALSE; + } + + drflac__decode_block_header(blockHeader, isLastBlock, blockType, blockSize); + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__read_streaminfo(drflac_read_proc onRead, void* pUserData, drflac_streaminfo* pStreamInfo) +{ + drflac_uint32 blockSizes; + drflac_uint64 frameSizes = 0; + drflac_uint64 importantProps; + drflac_uint8 md5[16]; + + /* min/max block size. */ + if (onRead(pUserData, &blockSizes, 4) != 4) { + return DRFLAC_FALSE; + } + + /* min/max frame size. */ + if (onRead(pUserData, &frameSizes, 6) != 6) { + return DRFLAC_FALSE; + } + + /* Sample rate, channels, bits per sample and total sample count. */ + if (onRead(pUserData, &importantProps, 8) != 8) { + return DRFLAC_FALSE; + } + + /* MD5 */ + if (onRead(pUserData, md5, sizeof(md5)) != sizeof(md5)) { + return DRFLAC_FALSE; + } + + blockSizes = drflac__be2host_32(blockSizes); + frameSizes = drflac__be2host_64(frameSizes); + importantProps = drflac__be2host_64(importantProps); + + pStreamInfo->minBlockSizeInPCMFrames = (drflac_uint16)((blockSizes & 0xFFFF0000) >> 16); + pStreamInfo->maxBlockSizeInPCMFrames = (drflac_uint16) (blockSizes & 0x0000FFFF); + pStreamInfo->minFrameSizeInPCMFrames = (drflac_uint32)((frameSizes & (((drflac_uint64)0x00FFFFFF << 16) << 24)) >> 40); + pStreamInfo->maxFrameSizeInPCMFrames = (drflac_uint32)((frameSizes & (((drflac_uint64)0x00FFFFFF << 16) << 0)) >> 16); + pStreamInfo->sampleRate = (drflac_uint32)((importantProps & (((drflac_uint64)0x000FFFFF << 16) << 28)) >> 44); + pStreamInfo->channels = (drflac_uint8 )((importantProps & (((drflac_uint64)0x0000000E << 16) << 24)) >> 41) + 1; + pStreamInfo->bitsPerSample = (drflac_uint8 )((importantProps & (((drflac_uint64)0x0000001F << 16) << 20)) >> 36) + 1; + pStreamInfo->totalPCMFrameCount = ((importantProps & ((((drflac_uint64)0x0000000F << 16) << 16) | 0xFFFFFFFF))); + DRFLAC_COPY_MEMORY(pStreamInfo->md5, md5, sizeof(md5)); + + return DRFLAC_TRUE; +} + + +static void* drflac__malloc_default(size_t sz, void* pUserData) +{ + (void)pUserData; + return DRFLAC_MALLOC(sz); +} + +static void* drflac__realloc_default(void* p, size_t sz, void* pUserData) +{ + (void)pUserData; + return DRFLAC_REALLOC(p, sz); +} + +static void drflac__free_default(void* p, void* pUserData) +{ + (void)pUserData; + DRFLAC_FREE(p); +} + + +static void* drflac__malloc_from_callbacks(size_t sz, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onMalloc != NULL) { + return pAllocationCallbacks->onMalloc(sz, pAllocationCallbacks->pUserData); + } + + /* Try using realloc(). */ + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(NULL, sz, pAllocationCallbacks->pUserData); + } + + return NULL; +} + +static void* drflac__realloc_from_callbacks(void* p, size_t szNew, size_t szOld, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(p, szNew, pAllocationCallbacks->pUserData); + } + + /* Try emulating realloc() in terms of malloc()/free(). */ + if (pAllocationCallbacks->onMalloc != NULL && pAllocationCallbacks->onFree != NULL) { + void* p2; + + p2 = pAllocationCallbacks->onMalloc(szNew, pAllocationCallbacks->pUserData); + if (p2 == NULL) { + return NULL; + } + + if (p != NULL) { + DRFLAC_COPY_MEMORY(p2, p, DRFLAC_MIN(szNew, szOld)); + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } + + return p2; + } + + return NULL; +} + +static void drflac__free_from_callbacks(void* p, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (p == NULL || pAllocationCallbacks == NULL) { + return; + } + + if (pAllocationCallbacks->onFree != NULL) { + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } +} + + +static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_uint64* pFirstFramePos, drflac_uint64* pSeektablePos, drflac_uint32* pSeekpointCount, drflac_allocation_callbacks* pAllocationCallbacks) +{ + /* + We want to keep track of the byte position in the stream of the seektable. At the time of calling this function we know that + we'll be sitting on byte 42. + */ + drflac_uint64 runningFilePos = 42; + drflac_uint64 seektablePos = 0; + drflac_uint32 seektableSize = 0; + drflac_int64 fileSize = 0; + drflac_bool32 hasKnownFileSize = DRFLAC_FALSE; + + /* We'll be doing some memory allocations here against untrusted data. We'll do a basic validation check that they don't exceed the size of the file. */ + if (onTell != NULL && onSeek != NULL) { + if (onSeek(pUserData, 0, DRFLAC_SEEK_END)) { + if (onTell(pUserData, &fileSize)) { + hasKnownFileSize = DRFLAC_TRUE; + } + + onSeek(pUserData, (int)runningFilePos, DRFLAC_SEEK_SET); /* Safe cast because runningFilePos should always be 42 at this point. */ + } + } + + for (;;) { + drflac_metadata metadata; + drflac_uint8 isLastBlock = 0; + drflac_uint8 blockType = 0; + drflac_uint32 blockSize; + if (drflac__read_and_decode_block_header(onRead, pUserData, &isLastBlock, &blockType, &blockSize) == DRFLAC_FALSE) { + return DRFLAC_FALSE; + } + + if (hasKnownFileSize && (blockSize > ((drflac_uint64)fileSize - runningFilePos))) { + return DRFLAC_FALSE; /* Block size exceeds the size of the file. */ + } + + runningFilePos += 4; + + metadata.type = blockType; + metadata.rawDataSize = 0; + metadata.rawDataOffset = runningFilePos; + metadata.pRawData = NULL; + + switch (blockType) + { + case DRFLAC_METADATA_BLOCK_TYPE_APPLICATION: + { + if (blockSize < 4) { + return DRFLAC_FALSE; + } + + if (onMeta) { + void* pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + metadata.data.application.id = drflac__be2host_32(*(drflac_uint32*)pRawData); + metadata.data.application.pData = (const void*)((drflac_uint8*)pRawData + sizeof(drflac_uint32)); + metadata.data.application.dataSize = blockSize - sizeof(drflac_uint32); + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_SEEKTABLE: + { + seektablePos = runningFilePos; + seektableSize = blockSize; + + if (onMeta) { + drflac_uint32 seekpointCount; + drflac_uint32 iSeekpoint; + void* pRawData; + size_t rawDataSize; + + seekpointCount = blockSize/DRFLAC_SEEKPOINT_SIZE_IN_BYTES; + rawDataSize = seekpointCount * sizeof(drflac_seekpoint); + + pRawData = drflac__malloc_from_callbacks(rawDataSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + /* We need to read seekpoint by seekpoint and do some processing. */ + for (iSeekpoint = 0; iSeekpoint < seekpointCount; ++iSeekpoint) { + drflac_seekpoint* pSeekpoint = (drflac_seekpoint*)pRawData + iSeekpoint; + + if (onRead(pUserData, pSeekpoint, DRFLAC_SEEKPOINT_SIZE_IN_BYTES) != DRFLAC_SEEKPOINT_SIZE_IN_BYTES) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + /* Endian swap. */ + pSeekpoint->firstPCMFrame = drflac__be2host_64(pSeekpoint->firstPCMFrame); + pSeekpoint->flacFrameOffset = drflac__be2host_64(pSeekpoint->flacFrameOffset); + pSeekpoint->pcmFrameCount = drflac__be2host_16(pSeekpoint->pcmFrameCount); + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = rawDataSize; + metadata.data.seektable.seekpointCount = seekpointCount; + metadata.data.seektable.pSeekpoints = (const drflac_seekpoint*)pRawData; + + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT: + { + if (blockSize < 8) { + return DRFLAC_FALSE; + } + + if (onMeta) { + void* pRawData; + const char* pRunningData; + const char* pRunningDataEnd; + drflac_uint32 i; + + pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + + pRunningData = (const char*)pRawData; + pRunningDataEnd = (const char*)pRawData + blockSize; + + metadata.data.vorbis_comment.vendorLength = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4; + + /* Need space for the rest of the block */ + if ((pRunningDataEnd - pRunningData) - 4 < (drflac_int64)metadata.data.vorbis_comment.vendorLength) { /* <-- Note the order of operations to avoid overflow to a valid value */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + metadata.data.vorbis_comment.vendor = pRunningData; pRunningData += metadata.data.vorbis_comment.vendorLength; + metadata.data.vorbis_comment.commentCount = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4; + + /* Need space for 'commentCount' comments after the block, which at minimum is a drflac_uint32 per comment */ + if ((pRunningDataEnd - pRunningData) / sizeof(drflac_uint32) < metadata.data.vorbis_comment.commentCount) { /* <-- Note the order of operations to avoid overflow to a valid value */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + metadata.data.vorbis_comment.pComments = pRunningData; + + /* Check that the comments section is valid before passing it to the callback */ + for (i = 0; i < metadata.data.vorbis_comment.commentCount; ++i) { + drflac_uint32 commentLength; + + if (pRunningDataEnd - pRunningData < 4) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + commentLength = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4; + if (pRunningDataEnd - pRunningData < (drflac_int64)commentLength) { /* <-- Note the order of operations to avoid overflow to a valid value */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + pRunningData += commentLength; + } + + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_CUESHEET: + { + if (blockSize < 396) { + return DRFLAC_FALSE; + } + + if (onMeta) { + void* pRawData; + const char* pRunningData; + const char* pRunningDataEnd; + size_t bufferSize; + drflac_uint8 iTrack; + drflac_uint8 iIndex; + void* pTrackData; + + /* + This needs to be loaded in two passes. The first pass is used to calculate the size of the memory allocation + we need for storing the necessary data. The second pass will fill that buffer with usable data. + */ + pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + + pRunningData = (const char*)pRawData; + pRunningDataEnd = (const char*)pRawData + blockSize; + + DRFLAC_COPY_MEMORY(metadata.data.cuesheet.catalog, pRunningData, 128); pRunningData += 128; + metadata.data.cuesheet.leadInSampleCount = drflac__be2host_64(*(const drflac_uint64*)pRunningData); pRunningData += 8; + metadata.data.cuesheet.isCD = (pRunningData[0] & 0x80) != 0; pRunningData += 259; + metadata.data.cuesheet.trackCount = pRunningData[0]; pRunningData += 1; + metadata.data.cuesheet.pTrackData = NULL; /* Will be filled later. */ + + /* Pass 1: Calculate the size of the buffer for the track data. */ + { + const char* pRunningDataSaved = pRunningData; /* Will be restored at the end in preparation for the second pass. */ + + bufferSize = metadata.data.cuesheet.trackCount * DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES; + + for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { + drflac_uint8 indexCount; + drflac_uint32 indexPointSize; + + if (pRunningDataEnd - pRunningData < DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + /* Skip to the index point count */ + pRunningData += 35; + + indexCount = pRunningData[0]; + pRunningData += 1; + + bufferSize += indexCount * sizeof(drflac_cuesheet_track_index); + + /* Quick validation check. */ + indexPointSize = indexCount * DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES; + if (pRunningDataEnd - pRunningData < (drflac_int64)indexPointSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + pRunningData += indexPointSize; + } + + pRunningData = pRunningDataSaved; + } + + /* Pass 2: Allocate a buffer and fill the data. Validation was done in the step above so can be skipped. */ + { + char* pRunningTrackData; + + pTrackData = drflac__malloc_from_callbacks(bufferSize, pAllocationCallbacks); + if (pTrackData == NULL) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + pRunningTrackData = (char*)pTrackData; + + for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { + drflac_uint8 indexCount; + + DRFLAC_COPY_MEMORY(pRunningTrackData, pRunningData, DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES); + pRunningData += DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES-1; /* Skip forward, but not beyond the last byte in the CUESHEET_TRACK block which is the index count. */ + pRunningTrackData += DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES-1; + + /* Grab the index count for the next part. */ + indexCount = pRunningData[0]; + pRunningData += 1; + pRunningTrackData += 1; + + /* Extract each track index. */ + for (iIndex = 0; iIndex < indexCount; ++iIndex) { + drflac_cuesheet_track_index* pTrackIndex = (drflac_cuesheet_track_index*)pRunningTrackData; + + DRFLAC_COPY_MEMORY(pRunningTrackData, pRunningData, DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES); + pRunningData += DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES; + pRunningTrackData += sizeof(drflac_cuesheet_track_index); + + pTrackIndex->offset = drflac__be2host_64(pTrackIndex->offset); + } + } + + metadata.data.cuesheet.pTrackData = pTrackData; + } + + /* The original data is no longer needed. */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + pRawData = NULL; + + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pTrackData, pAllocationCallbacks); + pTrackData = NULL; + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_PICTURE: + { + if (blockSize < 32) { + return DRFLAC_FALSE; + } + + if (onMeta) { + drflac_bool32 result = DRFLAC_TRUE; + drflac_uint32 blockSizeRemaining = blockSize; + char* pMime = NULL; + char* pDescription = NULL; + void* pPictureData = NULL; + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.type, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.type = drflac__be2host_32(metadata.data.picture.type); + + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.mimeLength, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.mimeLength = drflac__be2host_32(metadata.data.picture.mimeLength); + + if (blockSizeRemaining < metadata.data.picture.mimeLength) { + result = DRFLAC_FALSE; + goto done_flac; + } + + pMime = (char*)drflac__malloc_from_callbacks(metadata.data.picture.mimeLength + 1, pAllocationCallbacks); /* +1 for null terminator. */ + if (pMime == NULL) { + result = DRFLAC_FALSE; + goto done_flac; + } + + if (onRead(pUserData, pMime, metadata.data.picture.mimeLength) != metadata.data.picture.mimeLength) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= metadata.data.picture.mimeLength; + pMime[metadata.data.picture.mimeLength] = '\0'; /* Null terminate for safety. */ + metadata.data.picture.mime = (const char*)pMime; + + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.descriptionLength, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.descriptionLength = drflac__be2host_32(metadata.data.picture.descriptionLength); + + if (blockSizeRemaining < metadata.data.picture.descriptionLength) { + result = DRFLAC_FALSE; + goto done_flac; + } + + pDescription = (char*)drflac__malloc_from_callbacks(metadata.data.picture.descriptionLength + 1, pAllocationCallbacks); /* +1 for null terminator. */ + if (pDescription == NULL) { + result = DRFLAC_FALSE; + goto done_flac; + } + + if (onRead(pUserData, pDescription, metadata.data.picture.descriptionLength) != metadata.data.picture.descriptionLength) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= metadata.data.picture.descriptionLength; + pDescription[metadata.data.picture.descriptionLength] = '\0'; /* Null terminate for safety. */ + metadata.data.picture.description = (const char*)pDescription; + + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.width, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.width = drflac__be2host_32(metadata.data.picture.width); + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.height, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.height = drflac__be2host_32(metadata.data.picture.height); + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.colorDepth, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.colorDepth = drflac__be2host_32(metadata.data.picture.colorDepth); + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.indexColorCount, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.indexColorCount = drflac__be2host_32(metadata.data.picture.indexColorCount); + + + /* Picture data. */ + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.pictureDataSize, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.pictureDataSize = drflac__be2host_32(metadata.data.picture.pictureDataSize); + + if (blockSizeRemaining < metadata.data.picture.pictureDataSize) { + result = DRFLAC_FALSE; + goto done_flac; + } + + /* For the actual image data we want to store the offset to the start of the stream. */ + metadata.data.picture.pictureDataOffset = runningFilePos + (blockSize - blockSizeRemaining); + + /* + For the allocation of image data, we can allow memory allocation to fail, in which case we just leave + the pointer as null. If it fails, we need to fall back to seeking past the image data. + */ + #ifndef DR_FLAC_NO_PICTURE_METADATA_MALLOC + pPictureData = drflac__malloc_from_callbacks(metadata.data.picture.pictureDataSize, pAllocationCallbacks); + if (pPictureData != NULL) { + if (onRead(pUserData, pPictureData, metadata.data.picture.pictureDataSize) != metadata.data.picture.pictureDataSize) { + result = DRFLAC_FALSE; + goto done_flac; + } + } else + #endif + { + /* Allocation failed. We need to seek past the picture data. */ + if (!onSeek(pUserData, metadata.data.picture.pictureDataSize, DRFLAC_SEEK_CUR)) { + result = DRFLAC_FALSE; + goto done_flac; + } + } + + blockSizeRemaining -= metadata.data.picture.pictureDataSize; + (void)blockSizeRemaining; + + metadata.data.picture.pPictureData = (const drflac_uint8*)pPictureData; + + + /* Only fire the callback if we actually have a way to read the image data. We must have either a valid offset, or a valid data pointer. */ + if (metadata.data.picture.pictureDataOffset != 0 || metadata.data.picture.pPictureData != NULL) { + onMeta(pUserDataMD, &metadata); + } else { + /* Don't have a valid offset or data pointer, so just pretend we don't have a picture metadata. */ + } + + done_flac: + drflac__free_from_callbacks(pMime, pAllocationCallbacks); + drflac__free_from_callbacks(pDescription, pAllocationCallbacks); + drflac__free_from_callbacks(pPictureData, pAllocationCallbacks); + + if (result != DRFLAC_TRUE) { + return DRFLAC_FALSE; + } + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_PADDING: + { + if (onMeta) { + metadata.data.padding.unused = 0; + + /* Padding doesn't have anything meaningful in it, so just skip over it, but make sure the caller is aware of it by firing the callback. */ + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + isLastBlock = DRFLAC_TRUE; /* An error occurred while seeking. Attempt to recover by treating this as the last block which will in turn terminate the loop. */ + } else { + onMeta(pUserDataMD, &metadata); + } + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_INVALID: + { + /* Invalid chunk. Just skip over this one. */ + if (onMeta) { + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + isLastBlock = DRFLAC_TRUE; /* An error occurred while seeking. Attempt to recover by treating this as the last block which will in turn terminate the loop. */ + } + } + } break; + + default: + { + /* + It's an unknown chunk, but not necessarily invalid. There's a chance more metadata blocks might be defined later on, so we + can at the very least report the chunk to the application and let it look at the raw data. + */ + if (onMeta) { + void* pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData != NULL) { + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + } else { + /* Allocation failed. We need to seek past the block. */ + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } + } break; + } + + /* If we're not handling metadata, just skip over the block. If we are, it will have been handled earlier in the switch statement above. */ + if (onMeta == NULL && blockSize > 0) { + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + isLastBlock = DRFLAC_TRUE; + } + } + + runningFilePos += blockSize; + if (isLastBlock) { + break; + } + } + + *pSeektablePos = seektablePos; + *pSeekpointCount = seektableSize / DRFLAC_SEEKPOINT_SIZE_IN_BYTES; + *pFirstFramePos = runningFilePos; + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__init_private__native(drflac_init_info* pInit, drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_bool32 relaxed) +{ + /* Pre Condition: The bit stream should be sitting just past the 4-byte id header. */ + + drflac_uint8 isLastBlock; + drflac_uint8 blockType; + drflac_uint32 blockSize; + + (void)onSeek; + + pInit->container = drflac_container_native; + + /* The first metadata block should be the STREAMINFO block. */ + if (!drflac__read_and_decode_block_header(onRead, pUserData, &isLastBlock, &blockType, &blockSize)) { + return DRFLAC_FALSE; + } + + if (blockType != DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO || blockSize != 34) { + if (!relaxed) { + /* We're opening in strict mode and the first block is not the STREAMINFO block. Error. */ + return DRFLAC_FALSE; + } else { + /* + Relaxed mode. To open from here we need to just find the first frame and set the sample rate, etc. to whatever is defined + for that frame. + */ + pInit->hasStreamInfoBlock = DRFLAC_FALSE; + pInit->hasMetadataBlocks = DRFLAC_FALSE; + + if (!drflac__read_next_flac_frame_header(&pInit->bs, 0, &pInit->firstFrameHeader)) { + return DRFLAC_FALSE; /* Couldn't find a frame. */ + } + + if (pInit->firstFrameHeader.bitsPerSample == 0) { + return DRFLAC_FALSE; /* Failed to initialize because the first frame depends on the STREAMINFO block, which does not exist. */ + } + + pInit->sampleRate = pInit->firstFrameHeader.sampleRate; + pInit->channels = drflac__get_channel_count_from_channel_assignment(pInit->firstFrameHeader.channelAssignment); + pInit->bitsPerSample = pInit->firstFrameHeader.bitsPerSample; + pInit->maxBlockSizeInPCMFrames = 65535; /* <-- See notes here: https://xiph.org/flac/format.html#metadata_block_streaminfo */ + return DRFLAC_TRUE; + } + } else { + drflac_streaminfo streaminfo; + if (!drflac__read_streaminfo(onRead, pUserData, &streaminfo)) { + return DRFLAC_FALSE; + } + + pInit->hasStreamInfoBlock = DRFLAC_TRUE; + pInit->sampleRate = streaminfo.sampleRate; + pInit->channels = streaminfo.channels; + pInit->bitsPerSample = streaminfo.bitsPerSample; + pInit->totalPCMFrameCount = streaminfo.totalPCMFrameCount; + pInit->maxBlockSizeInPCMFrames = streaminfo.maxBlockSizeInPCMFrames; /* Don't care about the min block size - only the max (used for determining the size of the memory allocation). */ + pInit->hasMetadataBlocks = !isLastBlock; + + if (onMeta) { + drflac_metadata metadata; + metadata.type = DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO; + metadata.pRawData = NULL; + metadata.rawDataSize = 0; + metadata.data.streaminfo = streaminfo; + onMeta(pUserDataMD, &metadata); + } + + return DRFLAC_TRUE; + } +} + +#ifndef DR_FLAC_NO_OGG +#define DRFLAC_OGG_MAX_PAGE_SIZE 65307 +#define DRFLAC_OGG_CAPTURE_PATTERN_CRC32 1605413199 /* CRC-32 of "OggS". */ + +typedef enum +{ + drflac_ogg_recover_on_crc_mismatch, + drflac_ogg_fail_on_crc_mismatch +} drflac_ogg_crc_mismatch_recovery; + +#ifndef DR_FLAC_NO_CRC +static drflac_uint32 drflac__crc32_table[] = { + 0x00000000L, 0x04C11DB7L, 0x09823B6EL, 0x0D4326D9L, + 0x130476DCL, 0x17C56B6BL, 0x1A864DB2L, 0x1E475005L, + 0x2608EDB8L, 0x22C9F00FL, 0x2F8AD6D6L, 0x2B4BCB61L, + 0x350C9B64L, 0x31CD86D3L, 0x3C8EA00AL, 0x384FBDBDL, + 0x4C11DB70L, 0x48D0C6C7L, 0x4593E01EL, 0x4152FDA9L, + 0x5F15ADACL, 0x5BD4B01BL, 0x569796C2L, 0x52568B75L, + 0x6A1936C8L, 0x6ED82B7FL, 0x639B0DA6L, 0x675A1011L, + 0x791D4014L, 0x7DDC5DA3L, 0x709F7B7AL, 0x745E66CDL, + 0x9823B6E0L, 0x9CE2AB57L, 0x91A18D8EL, 0x95609039L, + 0x8B27C03CL, 0x8FE6DD8BL, 0x82A5FB52L, 0x8664E6E5L, + 0xBE2B5B58L, 0xBAEA46EFL, 0xB7A96036L, 0xB3687D81L, + 0xAD2F2D84L, 0xA9EE3033L, 0xA4AD16EAL, 0xA06C0B5DL, + 0xD4326D90L, 0xD0F37027L, 0xDDB056FEL, 0xD9714B49L, + 0xC7361B4CL, 0xC3F706FBL, 0xCEB42022L, 0xCA753D95L, + 0xF23A8028L, 0xF6FB9D9FL, 0xFBB8BB46L, 0xFF79A6F1L, + 0xE13EF6F4L, 0xE5FFEB43L, 0xE8BCCD9AL, 0xEC7DD02DL, + 0x34867077L, 0x30476DC0L, 0x3D044B19L, 0x39C556AEL, + 0x278206ABL, 0x23431B1CL, 0x2E003DC5L, 0x2AC12072L, + 0x128E9DCFL, 0x164F8078L, 0x1B0CA6A1L, 0x1FCDBB16L, + 0x018AEB13L, 0x054BF6A4L, 0x0808D07DL, 0x0CC9CDCAL, + 0x7897AB07L, 0x7C56B6B0L, 0x71159069L, 0x75D48DDEL, + 0x6B93DDDBL, 0x6F52C06CL, 0x6211E6B5L, 0x66D0FB02L, + 0x5E9F46BFL, 0x5A5E5B08L, 0x571D7DD1L, 0x53DC6066L, + 0x4D9B3063L, 0x495A2DD4L, 0x44190B0DL, 0x40D816BAL, + 0xACA5C697L, 0xA864DB20L, 0xA527FDF9L, 0xA1E6E04EL, + 0xBFA1B04BL, 0xBB60ADFCL, 0xB6238B25L, 0xB2E29692L, + 0x8AAD2B2FL, 0x8E6C3698L, 0x832F1041L, 0x87EE0DF6L, + 0x99A95DF3L, 0x9D684044L, 0x902B669DL, 0x94EA7B2AL, + 0xE0B41DE7L, 0xE4750050L, 0xE9362689L, 0xEDF73B3EL, + 0xF3B06B3BL, 0xF771768CL, 0xFA325055L, 0xFEF34DE2L, + 0xC6BCF05FL, 0xC27DEDE8L, 0xCF3ECB31L, 0xCBFFD686L, + 0xD5B88683L, 0xD1799B34L, 0xDC3ABDEDL, 0xD8FBA05AL, + 0x690CE0EEL, 0x6DCDFD59L, 0x608EDB80L, 0x644FC637L, + 0x7A089632L, 0x7EC98B85L, 0x738AAD5CL, 0x774BB0EBL, + 0x4F040D56L, 0x4BC510E1L, 0x46863638L, 0x42472B8FL, + 0x5C007B8AL, 0x58C1663DL, 0x558240E4L, 0x51435D53L, + 0x251D3B9EL, 0x21DC2629L, 0x2C9F00F0L, 0x285E1D47L, + 0x36194D42L, 0x32D850F5L, 0x3F9B762CL, 0x3B5A6B9BL, + 0x0315D626L, 0x07D4CB91L, 0x0A97ED48L, 0x0E56F0FFL, + 0x1011A0FAL, 0x14D0BD4DL, 0x19939B94L, 0x1D528623L, + 0xF12F560EL, 0xF5EE4BB9L, 0xF8AD6D60L, 0xFC6C70D7L, + 0xE22B20D2L, 0xE6EA3D65L, 0xEBA91BBCL, 0xEF68060BL, + 0xD727BBB6L, 0xD3E6A601L, 0xDEA580D8L, 0xDA649D6FL, + 0xC423CD6AL, 0xC0E2D0DDL, 0xCDA1F604L, 0xC960EBB3L, + 0xBD3E8D7EL, 0xB9FF90C9L, 0xB4BCB610L, 0xB07DABA7L, + 0xAE3AFBA2L, 0xAAFBE615L, 0xA7B8C0CCL, 0xA379DD7BL, + 0x9B3660C6L, 0x9FF77D71L, 0x92B45BA8L, 0x9675461FL, + 0x8832161AL, 0x8CF30BADL, 0x81B02D74L, 0x857130C3L, + 0x5D8A9099L, 0x594B8D2EL, 0x5408ABF7L, 0x50C9B640L, + 0x4E8EE645L, 0x4A4FFBF2L, 0x470CDD2BL, 0x43CDC09CL, + 0x7B827D21L, 0x7F436096L, 0x7200464FL, 0x76C15BF8L, + 0x68860BFDL, 0x6C47164AL, 0x61043093L, 0x65C52D24L, + 0x119B4BE9L, 0x155A565EL, 0x18197087L, 0x1CD86D30L, + 0x029F3D35L, 0x065E2082L, 0x0B1D065BL, 0x0FDC1BECL, + 0x3793A651L, 0x3352BBE6L, 0x3E119D3FL, 0x3AD08088L, + 0x2497D08DL, 0x2056CD3AL, 0x2D15EBE3L, 0x29D4F654L, + 0xC5A92679L, 0xC1683BCEL, 0xCC2B1D17L, 0xC8EA00A0L, + 0xD6AD50A5L, 0xD26C4D12L, 0xDF2F6BCBL, 0xDBEE767CL, + 0xE3A1CBC1L, 0xE760D676L, 0xEA23F0AFL, 0xEEE2ED18L, + 0xF0A5BD1DL, 0xF464A0AAL, 0xF9278673L, 0xFDE69BC4L, + 0x89B8FD09L, 0x8D79E0BEL, 0x803AC667L, 0x84FBDBD0L, + 0x9ABC8BD5L, 0x9E7D9662L, 0x933EB0BBL, 0x97FFAD0CL, + 0xAFB010B1L, 0xAB710D06L, 0xA6322BDFL, 0xA2F33668L, + 0xBCB4666DL, 0xB8757BDAL, 0xB5365D03L, 0xB1F740B4L +}; +#endif + +static DRFLAC_INLINE drflac_uint32 drflac_crc32_byte(drflac_uint32 crc32, drflac_uint8 data) +{ +#ifndef DR_FLAC_NO_CRC + return (crc32 << 8) ^ drflac__crc32_table[(drflac_uint8)((crc32 >> 24) & 0xFF) ^ data]; +#else + (void)data; + return crc32; +#endif +} + +#if 0 +static DRFLAC_INLINE drflac_uint32 drflac_crc32_uint32(drflac_uint32 crc32, drflac_uint32 data) +{ + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 24) & 0xFF)); + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 16) & 0xFF)); + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 8) & 0xFF)); + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 0) & 0xFF)); + return crc32; +} + +static DRFLAC_INLINE drflac_uint32 drflac_crc32_uint64(drflac_uint32 crc32, drflac_uint64 data) +{ + crc32 = drflac_crc32_uint32(crc32, (drflac_uint32)((data >> 32) & 0xFFFFFFFF)); + crc32 = drflac_crc32_uint32(crc32, (drflac_uint32)((data >> 0) & 0xFFFFFFFF)); + return crc32; +} +#endif + +static DRFLAC_INLINE drflac_uint32 drflac_crc32_buffer(drflac_uint32 crc32, drflac_uint8* pData, drflac_uint32 dataSize) +{ + /* This can be optimized. */ + drflac_uint32 i; + for (i = 0; i < dataSize; ++i) { + crc32 = drflac_crc32_byte(crc32, pData[i]); + } + return crc32; +} + + +static DRFLAC_INLINE drflac_bool32 drflac_ogg__is_capture_pattern(drflac_uint8 pattern[4]) +{ + return pattern[0] == 'O' && pattern[1] == 'g' && pattern[2] == 'g' && pattern[3] == 'S'; +} + +static DRFLAC_INLINE drflac_uint32 drflac_ogg__get_page_header_size(drflac_ogg_page_header* pHeader) +{ + return 27 + pHeader->segmentCount; +} + +static DRFLAC_INLINE drflac_uint32 drflac_ogg__get_page_body_size(drflac_ogg_page_header* pHeader) +{ + drflac_uint32 pageBodySize = 0; + int i; + + for (i = 0; i < pHeader->segmentCount; ++i) { + pageBodySize += pHeader->segmentTable[i]; + } + + return pageBodySize; +} + +static drflac_result drflac_ogg__read_page_header_after_capture_pattern(drflac_read_proc onRead, void* pUserData, drflac_ogg_page_header* pHeader, drflac_uint32* pBytesRead, drflac_uint32* pCRC32) +{ + drflac_uint8 data[23]; + drflac_uint32 i; + + DRFLAC_ASSERT(*pCRC32 == DRFLAC_OGG_CAPTURE_PATTERN_CRC32); + + if (onRead(pUserData, data, 23) != 23) { + return DRFLAC_AT_END; + } + *pBytesRead += 23; + + /* + It's not actually used, but set the capture pattern to 'OggS' for completeness. Not doing this will cause static analysers to complain about + us trying to access uninitialized data. We could alternatively just comment out this member of the drflac_ogg_page_header structure, but I + like to have it map to the structure of the underlying data. + */ + pHeader->capturePattern[0] = 'O'; + pHeader->capturePattern[1] = 'g'; + pHeader->capturePattern[2] = 'g'; + pHeader->capturePattern[3] = 'S'; + + pHeader->structureVersion = data[0]; + pHeader->headerType = data[1]; + DRFLAC_COPY_MEMORY(&pHeader->granulePosition, &data[ 2], 8); + DRFLAC_COPY_MEMORY(&pHeader->serialNumber, &data[10], 4); + DRFLAC_COPY_MEMORY(&pHeader->sequenceNumber, &data[14], 4); + DRFLAC_COPY_MEMORY(&pHeader->checksum, &data[18], 4); + pHeader->segmentCount = data[22]; + + /* Calculate the CRC. Note that for the calculation the checksum part of the page needs to be set to 0. */ + data[18] = 0; + data[19] = 0; + data[20] = 0; + data[21] = 0; + + for (i = 0; i < 23; ++i) { + *pCRC32 = drflac_crc32_byte(*pCRC32, data[i]); + } + + + if (onRead(pUserData, pHeader->segmentTable, pHeader->segmentCount) != pHeader->segmentCount) { + return DRFLAC_AT_END; + } + *pBytesRead += pHeader->segmentCount; + + for (i = 0; i < pHeader->segmentCount; ++i) { + *pCRC32 = drflac_crc32_byte(*pCRC32, pHeader->segmentTable[i]); + } + + return DRFLAC_SUCCESS; +} + +static drflac_result drflac_ogg__read_page_header(drflac_read_proc onRead, void* pUserData, drflac_ogg_page_header* pHeader, drflac_uint32* pBytesRead, drflac_uint32* pCRC32) +{ + drflac_uint8 id[4]; + + *pBytesRead = 0; + + if (onRead(pUserData, id, 4) != 4) { + return DRFLAC_AT_END; + } + *pBytesRead += 4; + + /* We need to read byte-by-byte until we find the OggS capture pattern. */ + for (;;) { + if (drflac_ogg__is_capture_pattern(id)) { + drflac_result result; + + *pCRC32 = DRFLAC_OGG_CAPTURE_PATTERN_CRC32; + + result = drflac_ogg__read_page_header_after_capture_pattern(onRead, pUserData, pHeader, pBytesRead, pCRC32); + if (result == DRFLAC_SUCCESS) { + return DRFLAC_SUCCESS; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + continue; + } else { + return result; + } + } + } else { + /* The first 4 bytes did not equal the capture pattern. Read the next byte and try again. */ + id[0] = id[1]; + id[1] = id[2]; + id[2] = id[3]; + if (onRead(pUserData, &id[3], 1) != 1) { + return DRFLAC_AT_END; + } + *pBytesRead += 1; + } + } +} + + +/* +The main part of the Ogg encapsulation is the conversion from the physical Ogg bitstream to the native FLAC bitstream. It works +in three general stages: Ogg Physical Bitstream -> Ogg/FLAC Logical Bitstream -> FLAC Native Bitstream. dr_flac is designed +in such a way that the core sections assume everything is delivered in native format. Therefore, for each encapsulation type +dr_flac is supporting there needs to be a layer sitting on top of the onRead and onSeek callbacks that ensures the bits read from +the physical Ogg bitstream are converted and delivered in native FLAC format. +*/ +typedef struct +{ + drflac_read_proc onRead; /* The original onRead callback from drflac_open() and family. */ + drflac_seek_proc onSeek; /* The original onSeek callback from drflac_open() and family. */ + drflac_tell_proc onTell; /* The original onTell callback from drflac_open() and family. */ + void* pUserData; /* The user data passed on onRead and onSeek. This is the user data that was passed on drflac_open() and family. */ + drflac_uint64 currentBytePos; /* The position of the byte we are sitting on in the physical byte stream. Used for efficient seeking. */ + drflac_uint64 firstBytePos; /* The position of the first byte in the physical bitstream. Points to the start of the "OggS" identifier of the FLAC bos page. */ + drflac_uint32 serialNumber; /* The serial number of the FLAC audio pages. This is determined by the initial header page that was read during initialization. */ + drflac_ogg_page_header bosPageHeader; /* Used for seeking. */ + drflac_ogg_page_header currentPageHeader; + drflac_uint32 bytesRemainingInPage; + drflac_uint32 pageDataSize; + drflac_uint8 pageData[DRFLAC_OGG_MAX_PAGE_SIZE]; +} drflac_oggbs; /* oggbs = Ogg Bitstream */ + +static size_t drflac_oggbs__read_physical(drflac_oggbs* oggbs, void* bufferOut, size_t bytesToRead) +{ + size_t bytesActuallyRead = oggbs->onRead(oggbs->pUserData, bufferOut, bytesToRead); + oggbs->currentBytePos += bytesActuallyRead; + + return bytesActuallyRead; +} + +static drflac_bool32 drflac_oggbs__seek_physical(drflac_oggbs* oggbs, drflac_uint64 offset, drflac_seek_origin origin) +{ + if (origin == DRFLAC_SEEK_SET) { + if (offset <= 0x7FFFFFFF) { + if (!oggbs->onSeek(oggbs->pUserData, (int)offset, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos = offset; + + return DRFLAC_TRUE; + } else { + if (!oggbs->onSeek(oggbs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos = offset; + + return drflac_oggbs__seek_physical(oggbs, offset - 0x7FFFFFFF, DRFLAC_SEEK_CUR); + } + } else { + while (offset > 0x7FFFFFFF) { + if (!oggbs->onSeek(oggbs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos += 0x7FFFFFFF; + offset -= 0x7FFFFFFF; + } + + if (!oggbs->onSeek(oggbs->pUserData, (int)offset, DRFLAC_SEEK_CUR)) { /* <-- Safe cast thanks to the loop above. */ + return DRFLAC_FALSE; + } + oggbs->currentBytePos += offset; + + return DRFLAC_TRUE; + } +} + +static drflac_bool32 drflac_oggbs__goto_next_page(drflac_oggbs* oggbs, drflac_ogg_crc_mismatch_recovery recoveryMethod) +{ + drflac_ogg_page_header header; + for (;;) { + drflac_uint32 crc32 = 0; + drflac_uint32 bytesRead; + drflac_uint32 pageBodySize; +#ifndef DR_FLAC_NO_CRC + drflac_uint32 actualCRC32; +#endif + + if (drflac_ogg__read_page_header(oggbs->onRead, oggbs->pUserData, &header, &bytesRead, &crc32) != DRFLAC_SUCCESS) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos += bytesRead; + + pageBodySize = drflac_ogg__get_page_body_size(&header); + if (pageBodySize > DRFLAC_OGG_MAX_PAGE_SIZE) { + continue; /* Invalid page size. Assume it's corrupted and just move to the next page. */ + } + + if (header.serialNumber != oggbs->serialNumber) { + /* It's not a FLAC page. Skip it. */ + if (pageBodySize > 0 && !drflac_oggbs__seek_physical(oggbs, pageBodySize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + continue; + } + + + /* We need to read the entire page and then do a CRC check on it. If there's a CRC mismatch we need to skip this page. */ + if (drflac_oggbs__read_physical(oggbs, oggbs->pageData, pageBodySize) != pageBodySize) { + return DRFLAC_FALSE; + } + oggbs->pageDataSize = pageBodySize; + +#ifndef DR_FLAC_NO_CRC + actualCRC32 = drflac_crc32_buffer(crc32, oggbs->pageData, oggbs->pageDataSize); + if (actualCRC32 != header.checksum) { + if (recoveryMethod == drflac_ogg_recover_on_crc_mismatch) { + continue; /* CRC mismatch. Skip this page. */ + } else { + /* + Even though we are failing on a CRC mismatch, we still want our stream to be in a good state. Therefore we + go to the next valid page to ensure we're in a good state, but return false to let the caller know that the + seek did not fully complete. + */ + drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch); + return DRFLAC_FALSE; + } + } +#else + (void)recoveryMethod; /* <-- Silence a warning. */ +#endif + + oggbs->currentPageHeader = header; + oggbs->bytesRemainingInPage = pageBodySize; + return DRFLAC_TRUE; + } +} + +/* Function below is unused at the moment, but I might be re-adding it later. */ +#if 0 +static drflac_uint8 drflac_oggbs__get_current_segment_index(drflac_oggbs* oggbs, drflac_uint8* pBytesRemainingInSeg) +{ + drflac_uint32 bytesConsumedInPage = drflac_ogg__get_page_body_size(&oggbs->currentPageHeader) - oggbs->bytesRemainingInPage; + drflac_uint8 iSeg = 0; + drflac_uint32 iByte = 0; + while (iByte < bytesConsumedInPage) { + drflac_uint8 segmentSize = oggbs->currentPageHeader.segmentTable[iSeg]; + if (iByte + segmentSize > bytesConsumedInPage) { + break; + } else { + iSeg += 1; + iByte += segmentSize; + } + } + + *pBytesRemainingInSeg = oggbs->currentPageHeader.segmentTable[iSeg] - (drflac_uint8)(bytesConsumedInPage - iByte); + return iSeg; +} + +static drflac_bool32 drflac_oggbs__seek_to_next_packet(drflac_oggbs* oggbs) +{ + /* The current packet ends when we get to the segment with a lacing value of < 255 which is not at the end of a page. */ + for (;;) { + drflac_bool32 atEndOfPage = DRFLAC_FALSE; + + drflac_uint8 bytesRemainingInSeg; + drflac_uint8 iFirstSeg = drflac_oggbs__get_current_segment_index(oggbs, &bytesRemainingInSeg); + + drflac_uint32 bytesToEndOfPacketOrPage = bytesRemainingInSeg; + for (drflac_uint8 iSeg = iFirstSeg; iSeg < oggbs->currentPageHeader.segmentCount; ++iSeg) { + drflac_uint8 segmentSize = oggbs->currentPageHeader.segmentTable[iSeg]; + if (segmentSize < 255) { + if (iSeg == oggbs->currentPageHeader.segmentCount-1) { + atEndOfPage = DRFLAC_TRUE; + } + + break; + } + + bytesToEndOfPacketOrPage += segmentSize; + } + + /* + At this point we will have found either the packet or the end of the page. If were at the end of the page we'll + want to load the next page and keep searching for the end of the packet. + */ + drflac_oggbs__seek_physical(oggbs, bytesToEndOfPacketOrPage, DRFLAC_SEEK_CUR); + oggbs->bytesRemainingInPage -= bytesToEndOfPacketOrPage; + + if (atEndOfPage) { + /* + We're potentially at the next packet, but we need to check the next page first to be sure because the packet may + straddle pages. + */ + if (!drflac_oggbs__goto_next_page(oggbs)) { + return DRFLAC_FALSE; + } + + /* If it's a fresh packet it most likely means we're at the next packet. */ + if ((oggbs->currentPageHeader.headerType & 0x01) == 0) { + return DRFLAC_TRUE; + } + } else { + /* We're at the next packet. */ + return DRFLAC_TRUE; + } + } +} + +static drflac_bool32 drflac_oggbs__seek_to_next_frame(drflac_oggbs* oggbs) +{ + /* The bitstream should be sitting on the first byte just after the header of the frame. */ + + /* What we're actually doing here is seeking to the start of the next packet. */ + return drflac_oggbs__seek_to_next_packet(oggbs); +} +#endif + +static size_t drflac__on_read_ogg(void* pUserData, void* bufferOut, size_t bytesToRead) +{ + drflac_oggbs* oggbs = (drflac_oggbs*)pUserData; + drflac_uint8* pRunningBufferOut = (drflac_uint8*)bufferOut; + size_t bytesRead = 0; + + DRFLAC_ASSERT(oggbs != NULL); + DRFLAC_ASSERT(pRunningBufferOut != NULL); + + /* Reading is done page-by-page. If we've run out of bytes in the page we need to move to the next one. */ + while (bytesRead < bytesToRead) { + size_t bytesRemainingToRead = bytesToRead - bytesRead; + + if (oggbs->bytesRemainingInPage >= bytesRemainingToRead) { + DRFLAC_COPY_MEMORY(pRunningBufferOut, oggbs->pageData + (oggbs->pageDataSize - oggbs->bytesRemainingInPage), bytesRemainingToRead); + bytesRead += bytesRemainingToRead; + oggbs->bytesRemainingInPage -= (drflac_uint32)bytesRemainingToRead; + break; + } + + /* If we get here it means some of the requested data is contained in the next pages. */ + if (oggbs->bytesRemainingInPage > 0) { + DRFLAC_COPY_MEMORY(pRunningBufferOut, oggbs->pageData + (oggbs->pageDataSize - oggbs->bytesRemainingInPage), oggbs->bytesRemainingInPage); + bytesRead += oggbs->bytesRemainingInPage; + pRunningBufferOut += oggbs->bytesRemainingInPage; + oggbs->bytesRemainingInPage = 0; + } + + DRFLAC_ASSERT(bytesRemainingToRead > 0); + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch)) { + break; /* Failed to go to the next page. Might have simply hit the end of the stream. */ + } + } + + return bytesRead; +} + +static drflac_bool32 drflac__on_seek_ogg(void* pUserData, int offset, drflac_seek_origin origin) +{ + drflac_oggbs* oggbs = (drflac_oggbs*)pUserData; + int bytesSeeked = 0; + + DRFLAC_ASSERT(oggbs != NULL); + DRFLAC_ASSERT(offset >= 0); /* <-- Never seek backwards. */ + + /* Seeking is always forward which makes things a lot simpler. */ + if (origin == DRFLAC_SEEK_SET) { + if (!drflac_oggbs__seek_physical(oggbs, (int)oggbs->firstBytePos, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_fail_on_crc_mismatch)) { + return DRFLAC_FALSE; + } + + return drflac__on_seek_ogg(pUserData, offset, DRFLAC_SEEK_CUR); + } else if (origin == DRFLAC_SEEK_CUR) { + while (bytesSeeked < offset) { + int bytesRemainingToSeek = offset - bytesSeeked; + DRFLAC_ASSERT(bytesRemainingToSeek >= 0); + + if (oggbs->bytesRemainingInPage >= (size_t)bytesRemainingToSeek) { + bytesSeeked += bytesRemainingToSeek; + (void)bytesSeeked; /* <-- Silence a dead store warning emitted by Clang Static Analyzer. */ + oggbs->bytesRemainingInPage -= bytesRemainingToSeek; + break; + } + + /* If we get here it means some of the requested data is contained in the next pages. */ + if (oggbs->bytesRemainingInPage > 0) { + bytesSeeked += (int)oggbs->bytesRemainingInPage; + oggbs->bytesRemainingInPage = 0; + } + + DRFLAC_ASSERT(bytesRemainingToSeek > 0); + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_fail_on_crc_mismatch)) { + /* Failed to go to the next page. We either hit the end of the stream or had a CRC mismatch. */ + return DRFLAC_FALSE; + } + } + } else if (origin == DRFLAC_SEEK_END) { + /* Seeking to the end is not supported. */ + return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__on_tell_ogg(void* pUserData, drflac_int64* pCursor) +{ + /* + Not implemented for Ogg containers because we don't currently track the byte position of the logical bitstream. To support this, we'll need + to track the position in drflac__on_read_ogg and drflac__on_seek_ogg. + */ + (void)pUserData; + (void)pCursor; + return DRFLAC_FALSE; +} + + +static drflac_bool32 drflac_ogg__seek_to_pcm_frame(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + drflac_uint64 originalBytePos; + drflac_uint64 runningGranulePosition; + drflac_uint64 runningFrameBytePos; + drflac_uint64 runningPCMFrameCount; + + DRFLAC_ASSERT(oggbs != NULL); + + originalBytePos = oggbs->currentBytePos; /* For recovery. Points to the OggS identifier. */ + + /* First seek to the first frame. */ + if (!drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes)) { + return DRFLAC_FALSE; + } + oggbs->bytesRemainingInPage = 0; + + runningGranulePosition = 0; + for (;;) { + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch)) { + drflac_oggbs__seek_physical(oggbs, originalBytePos, DRFLAC_SEEK_SET); + return DRFLAC_FALSE; /* Never did find that sample... */ + } + + runningFrameBytePos = oggbs->currentBytePos - drflac_ogg__get_page_header_size(&oggbs->currentPageHeader) - oggbs->pageDataSize; + if (oggbs->currentPageHeader.granulePosition >= pcmFrameIndex) { + break; /* The sample is somewhere in the previous page. */ + } + + /* + At this point we know the sample is not in the previous page. It could possibly be in this page. For simplicity we + disregard any pages that do not begin a fresh packet. + */ + if ((oggbs->currentPageHeader.headerType & 0x01) == 0) { /* <-- Is it a fresh page? */ + if (oggbs->currentPageHeader.segmentTable[0] >= 2) { + drflac_uint8 firstBytesInPage[2]; + firstBytesInPage[0] = oggbs->pageData[0]; + firstBytesInPage[1] = oggbs->pageData[1]; + + if ((firstBytesInPage[0] == 0xFF) && (firstBytesInPage[1] & 0xFC) == 0xF8) { /* <-- Does the page begin with a frame's sync code? */ + runningGranulePosition = oggbs->currentPageHeader.granulePosition; + } + + continue; + } + } + } + + /* + We found the page that that is closest to the sample, so now we need to find it. The first thing to do is seek to the + start of that page. In the loop above we checked that it was a fresh page which means this page is also the start of + a new frame. This property means that after we've seeked to the page we can immediately start looping over frames until + we find the one containing the target sample. + */ + if (!drflac_oggbs__seek_physical(oggbs, runningFrameBytePos, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch)) { + return DRFLAC_FALSE; + } + + /* + At this point we'll be sitting on the first byte of the frame header of the first frame in the page. We just keep + looping over these frames until we find the one containing the sample we're after. + */ + runningPCMFrameCount = runningGranulePosition; + for (;;) { + /* + There are two ways to find the sample and seek past irrelevant frames: + 1) Use the native FLAC decoder. + 2) Use Ogg's framing system. + + Both of these options have their own pros and cons. Using the native FLAC decoder is slower because it needs to + do a full decode of the frame. Using Ogg's framing system is faster, but more complicated and involves some code + duplication for the decoding of frame headers. + + Another thing to consider is that using the Ogg framing system will perform direct seeking of the physical Ogg + bitstream. This is important to consider because it means we cannot read data from the drflac_bs object using the + standard drflac__*() APIs because that will read in extra data for its own internal caching which in turn breaks + the positioning of the read pointer of the physical Ogg bitstream. Therefore, anything that would normally be read + using the native FLAC decoding APIs, such as drflac__read_next_flac_frame_header(), need to be re-implemented so as to + avoid the use of the drflac_bs object. + + Considering these issues, I have decided to use the slower native FLAC decoding method for the following reasons: + 1) Seeking is already partially accelerated using Ogg's paging system in the code block above. + 2) Seeking in an Ogg encapsulated FLAC stream is probably quite uncommon. + 3) Simplicity. + */ + drflac_uint64 firstPCMFrameInFLACFrame = 0; + drflac_uint64 lastPCMFrameInFLACFrame = 0; + drflac_uint64 pcmFrameCountInThisFrame; + + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &firstPCMFrameInFLACFrame, &lastPCMFrameInFLACFrame); + + pcmFrameCountInThisFrame = (lastPCMFrameInFLACFrame - firstPCMFrameInFLACFrame) + 1; + + /* If we are seeking to the end of the file and we've just hit it, we're done. */ + if (pcmFrameIndex == pFlac->totalPCMFrameCount && (runningPCMFrameCount + pcmFrameCountInThisFrame) == pFlac->totalPCMFrameCount) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + pFlac->currentPCMFrame = pcmFrameIndex; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + return DRFLAC_TRUE; + } else { + return DRFLAC_FALSE; + } + } + + if (pcmFrameIndex < (runningPCMFrameCount + pcmFrameCountInThisFrame)) { + /* + The sample should be in this FLAC frame. We need to fully decode it, however if it's an invalid frame (a CRC mismatch), we need to pretend + it never existed and keep iterating. + */ + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + /* The frame is valid. We just need to skip over some samples to ensure it's sample-exact. */ + drflac_uint64 pcmFramesToDecode = (size_t)(pcmFrameIndex - runningPCMFrameCount); /* <-- Safe cast because the maximum number of samples in a frame is 65535. */ + if (pcmFramesToDecode == 0) { + return DRFLAC_TRUE; + } + + pFlac->currentPCMFrame = runningPCMFrameCount; + + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; /* <-- If this fails, something bad has happened (it should never fail). */ + } else { + if (result == DRFLAC_CRC_MISMATCH) { + continue; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* + It's not in this frame. We need to seek past the frame, but check if there was a CRC mismatch. If so, we pretend this + frame never existed and leave the running sample count untouched. + */ + drflac_result result = drflac__seek_to_next_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + runningPCMFrameCount += pcmFrameCountInThisFrame; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + continue; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } + } +} + + + +static drflac_bool32 drflac__init_private__ogg(drflac_init_info* pInit, drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_bool32 relaxed) +{ + drflac_ogg_page_header header; + drflac_uint32 crc32 = DRFLAC_OGG_CAPTURE_PATTERN_CRC32; + drflac_uint32 bytesRead = 0; + + /* Pre Condition: The bit stream should be sitting just past the 4-byte OggS capture pattern. */ + (void)relaxed; + + pInit->container = drflac_container_ogg; + pInit->oggFirstBytePos = 0; + + /* + We'll get here if the first 4 bytes of the stream were the OggS capture pattern, however it doesn't necessarily mean the + stream includes FLAC encoded audio. To check for this we need to scan the beginning-of-stream page markers and check if + any match the FLAC specification. Important to keep in mind that the stream may be multiplexed. + */ + if (drflac_ogg__read_page_header_after_capture_pattern(onRead, pUserData, &header, &bytesRead, &crc32) != DRFLAC_SUCCESS) { + return DRFLAC_FALSE; + } + pInit->runningFilePos += bytesRead; + + for (;;) { + int pageBodySize; + + /* Break if we're past the beginning of stream page. */ + if ((header.headerType & 0x02) == 0) { + return DRFLAC_FALSE; + } + + /* Check if it's a FLAC header. */ + pageBodySize = drflac_ogg__get_page_body_size(&header); + if (pageBodySize == 51) { /* 51 = the lacing value of the FLAC header packet. */ + /* It could be a FLAC page... */ + drflac_uint32 bytesRemainingInPage = pageBodySize; + drflac_uint8 packetType; + + if (onRead(pUserData, &packetType, 1) != 1) { + return DRFLAC_FALSE; + } + + bytesRemainingInPage -= 1; + if (packetType == 0x7F) { + /* Increasingly more likely to be a FLAC page... */ + drflac_uint8 sig[4]; + if (onRead(pUserData, sig, 4) != 4) { + return DRFLAC_FALSE; + } + + bytesRemainingInPage -= 4; + if (sig[0] == 'F' && sig[1] == 'L' && sig[2] == 'A' && sig[3] == 'C') { + /* Almost certainly a FLAC page... */ + drflac_uint8 mappingVersion[2]; + if (onRead(pUserData, mappingVersion, 2) != 2) { + return DRFLAC_FALSE; + } + + if (mappingVersion[0] != 1) { + return DRFLAC_FALSE; /* Only supporting version 1.x of the Ogg mapping. */ + } + + /* + The next 2 bytes are the non-audio packets, not including this one. We don't care about this because we're going to + be handling it in a generic way based on the serial number and packet types. + */ + if (!onSeek(pUserData, 2, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + + /* Expecting the native FLAC signature "fLaC". */ + if (onRead(pUserData, sig, 4) != 4) { + return DRFLAC_FALSE; + } + + if (sig[0] == 'f' && sig[1] == 'L' && sig[2] == 'a' && sig[3] == 'C') { + /* The remaining data in the page should be the STREAMINFO block. */ + drflac_streaminfo streaminfo; + drflac_uint8 isLastBlock; + drflac_uint8 blockType; + drflac_uint32 blockSize; + if (!drflac__read_and_decode_block_header(onRead, pUserData, &isLastBlock, &blockType, &blockSize)) { + return DRFLAC_FALSE; + } + + if (blockType != DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO || blockSize != 34) { + return DRFLAC_FALSE; /* Invalid block type. First block must be the STREAMINFO block. */ + } + + if (drflac__read_streaminfo(onRead, pUserData, &streaminfo)) { + /* Success! */ + pInit->hasStreamInfoBlock = DRFLAC_TRUE; + pInit->sampleRate = streaminfo.sampleRate; + pInit->channels = streaminfo.channels; + pInit->bitsPerSample = streaminfo.bitsPerSample; + pInit->totalPCMFrameCount = streaminfo.totalPCMFrameCount; + pInit->maxBlockSizeInPCMFrames = streaminfo.maxBlockSizeInPCMFrames; + pInit->hasMetadataBlocks = !isLastBlock; + + if (onMeta) { + drflac_metadata metadata; + metadata.type = DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO; + metadata.pRawData = NULL; + metadata.rawDataSize = 0; + metadata.data.streaminfo = streaminfo; + onMeta(pUserDataMD, &metadata); + } + + pInit->runningFilePos += pageBodySize; + pInit->oggFirstBytePos = pInit->runningFilePos - 79; /* Subtracting 79 will place us right on top of the "OggS" identifier of the FLAC bos page. */ + pInit->oggSerial = header.serialNumber; + pInit->oggBosHeader = header; + break; + } else { + /* Failed to read STREAMINFO block. Aww, so close... */ + return DRFLAC_FALSE; + } + } else { + /* Invalid file. */ + return DRFLAC_FALSE; + } + } else { + /* Not a FLAC header. Skip it. */ + if (!onSeek(pUserData, bytesRemainingInPage, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + } else { + /* Not a FLAC header. Seek past the entire page and move on to the next. */ + if (!onSeek(pUserData, bytesRemainingInPage, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + } else { + if (!onSeek(pUserData, pageBodySize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + + pInit->runningFilePos += pageBodySize; + + + /* Read the header of the next page. */ + if (drflac_ogg__read_page_header(onRead, pUserData, &header, &bytesRead, &crc32) != DRFLAC_SUCCESS) { + return DRFLAC_FALSE; + } + pInit->runningFilePos += bytesRead; + } + + /* + If we get here it means we found a FLAC audio stream. We should be sitting on the first byte of the header of the next page. The next + packets in the FLAC logical stream contain the metadata. The only thing left to do in the initialization phase for Ogg is to create the + Ogg bistream object. + */ + pInit->hasMetadataBlocks = DRFLAC_TRUE; /* <-- Always have at least VORBIS_COMMENT metadata block. */ + return DRFLAC_TRUE; +} +#endif + +static drflac_bool32 drflac__init_private(drflac_init_info* pInit, drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, void* pUserDataMD) +{ + drflac_bool32 relaxed; + drflac_uint8 id[4]; + + if (pInit == NULL || onRead == NULL || onSeek == NULL) { /* <-- onTell is optional. */ + return DRFLAC_FALSE; + } + + DRFLAC_ZERO_MEMORY(pInit, sizeof(*pInit)); + pInit->onRead = onRead; + pInit->onSeek = onSeek; + pInit->onTell = onTell; + pInit->onMeta = onMeta; + pInit->container = container; + pInit->pUserData = pUserData; + pInit->pUserDataMD = pUserDataMD; + + pInit->bs.onRead = onRead; + pInit->bs.onSeek = onSeek; + pInit->bs.onTell = onTell; + pInit->bs.pUserData = pUserData; + drflac__reset_cache(&pInit->bs); + + + /* If the container is explicitly defined then we can try opening in relaxed mode. */ + relaxed = container != drflac_container_unknown; + + /* Skip over any ID3 tags. */ + for (;;) { + if (onRead(pUserData, id, 4) != 4) { + return DRFLAC_FALSE; /* Ran out of data. */ + } + pInit->runningFilePos += 4; + + if (id[0] == 'I' && id[1] == 'D' && id[2] == '3') { + drflac_uint8 header[6]; + drflac_uint8 flags; + drflac_uint32 headerSize; + + if (onRead(pUserData, header, 6) != 6) { + return DRFLAC_FALSE; /* Ran out of data. */ + } + pInit->runningFilePos += 6; + + flags = header[1]; + + DRFLAC_COPY_MEMORY(&headerSize, header+2, 4); + headerSize = drflac__unsynchsafe_32(drflac__be2host_32(headerSize)); + if (flags & 0x10) { + headerSize += 10; + } + + if (!onSeek(pUserData, headerSize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; /* Failed to seek past the tag. */ + } + pInit->runningFilePos += headerSize; + } else { + break; + } + } + + if (id[0] == 'f' && id[1] == 'L' && id[2] == 'a' && id[3] == 'C') { + return drflac__init_private__native(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#ifndef DR_FLAC_NO_OGG + if (id[0] == 'O' && id[1] == 'g' && id[2] == 'g' && id[3] == 'S') { + return drflac__init_private__ogg(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#endif + + /* If we get here it means we likely don't have a header. Try opening in relaxed mode, if applicable. */ + if (relaxed) { + if (container == drflac_container_native) { + return drflac__init_private__native(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#ifndef DR_FLAC_NO_OGG + if (container == drflac_container_ogg) { + return drflac__init_private__ogg(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#endif + } + + /* Unsupported container. */ + return DRFLAC_FALSE; +} + +static void drflac__init_from_info(drflac* pFlac, const drflac_init_info* pInit) +{ + DRFLAC_ASSERT(pFlac != NULL); + DRFLAC_ASSERT(pInit != NULL); + + DRFLAC_ZERO_MEMORY(pFlac, sizeof(*pFlac)); + pFlac->bs = pInit->bs; + pFlac->onMeta = pInit->onMeta; + pFlac->pUserDataMD = pInit->pUserDataMD; + pFlac->maxBlockSizeInPCMFrames = pInit->maxBlockSizeInPCMFrames; + pFlac->sampleRate = pInit->sampleRate; + pFlac->channels = (drflac_uint8)pInit->channels; + pFlac->bitsPerSample = (drflac_uint8)pInit->bitsPerSample; + pFlac->totalPCMFrameCount = pInit->totalPCMFrameCount; + pFlac->container = pInit->container; +} + + +static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, void* pUserDataMD, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac_init_info init; + drflac_uint32 allocationSize; + drflac_uint32 wholeSIMDVectorCountPerChannel; + drflac_uint32 decodedSamplesAllocationSize; +#ifndef DR_FLAC_NO_OGG + drflac_oggbs* pOggbs = NULL; +#endif + drflac_uint64 firstFramePos; + drflac_uint64 seektablePos; + drflac_uint32 seekpointCount; + drflac_allocation_callbacks allocationCallbacks; + drflac* pFlac; + + /* CPU support first. */ + drflac__init_cpu_caps(); + + if (!drflac__init_private(&init, onRead, onSeek, onTell, onMeta, container, pUserData, pUserDataMD)) { + return NULL; + } + + if (pAllocationCallbacks != NULL) { + allocationCallbacks = *pAllocationCallbacks; + if (allocationCallbacks.onFree == NULL || (allocationCallbacks.onMalloc == NULL && allocationCallbacks.onRealloc == NULL)) { + return NULL; /* Invalid allocation callbacks. */ + } + } else { + allocationCallbacks.pUserData = NULL; + allocationCallbacks.onMalloc = drflac__malloc_default; + allocationCallbacks.onRealloc = drflac__realloc_default; + allocationCallbacks.onFree = drflac__free_default; + } + + + /* + The size of the allocation for the drflac object needs to be large enough to fit the following: + 1) The main members of the drflac structure + 2) A block of memory large enough to store the decoded samples of the largest frame in the stream + 3) If the container is Ogg, a drflac_oggbs object + + The complicated part of the allocation is making sure there's enough room the decoded samples, taking into consideration + the different SIMD instruction sets. + */ + allocationSize = sizeof(drflac); + + /* + The allocation size for decoded frames depends on the number of 32-bit integers that fit inside the largest SIMD vector + we are supporting. + */ + if ((init.maxBlockSizeInPCMFrames % (DRFLAC_MAX_SIMD_VECTOR_SIZE / sizeof(drflac_int32))) == 0) { + wholeSIMDVectorCountPerChannel = (init.maxBlockSizeInPCMFrames / (DRFLAC_MAX_SIMD_VECTOR_SIZE / sizeof(drflac_int32))); + } else { + wholeSIMDVectorCountPerChannel = (init.maxBlockSizeInPCMFrames / (DRFLAC_MAX_SIMD_VECTOR_SIZE / sizeof(drflac_int32))) + 1; + } + + decodedSamplesAllocationSize = wholeSIMDVectorCountPerChannel * DRFLAC_MAX_SIMD_VECTOR_SIZE * init.channels; + + allocationSize += decodedSamplesAllocationSize; + allocationSize += DRFLAC_MAX_SIMD_VECTOR_SIZE; /* Allocate extra bytes to ensure we have enough for alignment. */ + +#ifndef DR_FLAC_NO_OGG + /* There's additional data required for Ogg streams. */ + if (init.container == drflac_container_ogg) { + allocationSize += sizeof(drflac_oggbs); + + pOggbs = (drflac_oggbs*)drflac__malloc_from_callbacks(sizeof(*pOggbs), &allocationCallbacks); + if (pOggbs == NULL) { + return NULL; /*DRFLAC_OUT_OF_MEMORY;*/ + } + + DRFLAC_ZERO_MEMORY(pOggbs, sizeof(*pOggbs)); + pOggbs->onRead = onRead; + pOggbs->onSeek = onSeek; + pOggbs->onTell = onTell; + pOggbs->pUserData = pUserData; + pOggbs->currentBytePos = init.oggFirstBytePos; + pOggbs->firstBytePos = init.oggFirstBytePos; + pOggbs->serialNumber = init.oggSerial; + pOggbs->bosPageHeader = init.oggBosHeader; + pOggbs->bytesRemainingInPage = 0; + } +#endif + + /* + This part is a bit awkward. We need to load the seektable so that it can be referenced in-memory, but I want the drflac object to + consist of only a single heap allocation. To this, the size of the seek table needs to be known, which we determine when reading + and decoding the metadata. + */ + firstFramePos = 42; /* <-- We know we are at byte 42 at this point. */ + seektablePos = 0; + seekpointCount = 0; + if (init.hasMetadataBlocks) { + drflac_read_proc onReadOverride = onRead; + drflac_seek_proc onSeekOverride = onSeek; + drflac_tell_proc onTellOverride = onTell; + void* pUserDataOverride = pUserData; + +#ifndef DR_FLAC_NO_OGG + if (init.container == drflac_container_ogg) { + onReadOverride = drflac__on_read_ogg; + onSeekOverride = drflac__on_seek_ogg; + onTellOverride = drflac__on_tell_ogg; + pUserDataOverride = (void*)pOggbs; + } +#endif + + if (!drflac__read_and_decode_metadata(onReadOverride, onSeekOverride, onTellOverride, onMeta, pUserDataOverride, pUserDataMD, &firstFramePos, &seektablePos, &seekpointCount, &allocationCallbacks)) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif + return NULL; + } + + if ((0xFFFFFFFF - (seekpointCount * sizeof(drflac_seekpoint))) < allocationSize) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif + return NULL; + } + + allocationSize += seekpointCount * sizeof(drflac_seekpoint); + } + + pFlac = (drflac*)drflac__malloc_from_callbacks((size_t)allocationSize, &allocationCallbacks); + if (pFlac == NULL) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif + return NULL; + } + + drflac__init_from_info(pFlac, &init); + pFlac->allocationCallbacks = allocationCallbacks; + pFlac->pDecodedSamples = (drflac_int32*)drflac_align((size_t)pFlac->pExtraData, DRFLAC_MAX_SIMD_VECTOR_SIZE); + +#ifndef DR_FLAC_NO_OGG + if (init.container == drflac_container_ogg) { + drflac_oggbs* pInternalOggbs = (drflac_oggbs*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize + (seekpointCount * sizeof(drflac_seekpoint))); + DRFLAC_COPY_MEMORY(pInternalOggbs, pOggbs, sizeof(*pOggbs)); + + /* At this point the pOggbs object has been handed over to pInternalOggbs and can be freed. */ + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + pOggbs = NULL; + + /* The Ogg bistream needs to be layered on top of the original bitstream. */ + pFlac->bs.onRead = drflac__on_read_ogg; + pFlac->bs.onSeek = drflac__on_seek_ogg; + pFlac->bs.onTell = drflac__on_tell_ogg; + pFlac->bs.pUserData = (void*)pInternalOggbs; + pFlac->_oggbs = (void*)pInternalOggbs; + } +#endif + + pFlac->firstFLACFramePosInBytes = firstFramePos; + + /* NOTE: Seektables are not currently compatible with Ogg encapsulation (Ogg has its own accelerated seeking system). I may change this later, so I'm leaving this here for now. */ +#ifndef DR_FLAC_NO_OGG + if (init.container == drflac_container_ogg) + { + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + } + else +#endif + { + /* If we have a seektable we need to load it now, making sure we move back to where we were previously. */ + if (seektablePos != 0) { + pFlac->seekpointCount = seekpointCount; + pFlac->pSeekpoints = (drflac_seekpoint*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize); + + DRFLAC_ASSERT(pFlac->bs.onSeek != NULL); + DRFLAC_ASSERT(pFlac->bs.onRead != NULL); + + /* Seek to the seektable, then just read directly into our seektable buffer. */ + if (pFlac->bs.onSeek(pFlac->bs.pUserData, (int)seektablePos, DRFLAC_SEEK_SET)) { + drflac_uint32 iSeekpoint; + + for (iSeekpoint = 0; iSeekpoint < seekpointCount; iSeekpoint += 1) { + if (pFlac->bs.onRead(pFlac->bs.pUserData, pFlac->pSeekpoints + iSeekpoint, DRFLAC_SEEKPOINT_SIZE_IN_BYTES) == DRFLAC_SEEKPOINT_SIZE_IN_BYTES) { + /* Endian swap. */ + pFlac->pSeekpoints[iSeekpoint].firstPCMFrame = drflac__be2host_64(pFlac->pSeekpoints[iSeekpoint].firstPCMFrame); + pFlac->pSeekpoints[iSeekpoint].flacFrameOffset = drflac__be2host_64(pFlac->pSeekpoints[iSeekpoint].flacFrameOffset); + pFlac->pSeekpoints[iSeekpoint].pcmFrameCount = drflac__be2host_16(pFlac->pSeekpoints[iSeekpoint].pcmFrameCount); + } else { + /* Failed to read the seektable. Pretend we don't have one. */ + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + break; + } + } + + /* We need to seek back to where we were. If this fails it's a critical error. */ + if (!pFlac->bs.onSeek(pFlac->bs.pUserData, (int)pFlac->firstFLACFramePosInBytes, DRFLAC_SEEK_SET)) { + drflac__free_from_callbacks(pFlac, &allocationCallbacks); + return NULL; + } + } else { + /* Failed to seek to the seektable. Ominous sign, but for now we can just pretend we don't have one. */ + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + } + } + } + + + /* + If we get here, but don't have a STREAMINFO block, it means we've opened the stream in relaxed mode and need to decode + the first frame. + */ + if (!init.hasStreamInfoBlock) { + pFlac->currentFLACFrame.header = init.firstFrameHeader; + for (;;) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + break; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + drflac__free_from_callbacks(pFlac, &allocationCallbacks); + return NULL; + } + continue; + } else { + drflac__free_from_callbacks(pFlac, &allocationCallbacks); + return NULL; + } + } + } + } + + return pFlac; +} + + + +#ifndef DR_FLAC_NO_STDIO +#include +#ifndef DR_FLAC_NO_WCHAR +#include /* For wcslen(), wcsrtombs() */ +#endif + +/* Errno */ +/* drflac_result_from_errno() is only used for fopen() and wfopen() so putting it inside DR_WAV_NO_STDIO for now. If something else needs this later we can move it out. */ +#include +static drflac_result drflac_result_from_errno(int e) +{ + switch (e) + { + case 0: return DRFLAC_SUCCESS; + #ifdef EPERM + case EPERM: return DRFLAC_INVALID_OPERATION; + #endif + #ifdef ENOENT + case ENOENT: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef ESRCH + case ESRCH: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef EINTR + case EINTR: return DRFLAC_INTERRUPT; + #endif + #ifdef EIO + case EIO: return DRFLAC_IO_ERROR; + #endif + #ifdef ENXIO + case ENXIO: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef E2BIG + case E2BIG: return DRFLAC_INVALID_ARGS; + #endif + #ifdef ENOEXEC + case ENOEXEC: return DRFLAC_INVALID_FILE; + #endif + #ifdef EBADF + case EBADF: return DRFLAC_INVALID_FILE; + #endif + #ifdef ECHILD + case ECHILD: return DRFLAC_ERROR; + #endif + #ifdef EAGAIN + case EAGAIN: return DRFLAC_UNAVAILABLE; + #endif + #ifdef ENOMEM + case ENOMEM: return DRFLAC_OUT_OF_MEMORY; + #endif + #ifdef EACCES + case EACCES: return DRFLAC_ACCESS_DENIED; + #endif + #ifdef EFAULT + case EFAULT: return DRFLAC_BAD_ADDRESS; + #endif + #ifdef ENOTBLK + case ENOTBLK: return DRFLAC_ERROR; + #endif + #ifdef EBUSY + case EBUSY: return DRFLAC_BUSY; + #endif + #ifdef EEXIST + case EEXIST: return DRFLAC_ALREADY_EXISTS; + #endif + #ifdef EXDEV + case EXDEV: return DRFLAC_ERROR; + #endif + #ifdef ENODEV + case ENODEV: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef ENOTDIR + case ENOTDIR: return DRFLAC_NOT_DIRECTORY; + #endif + #ifdef EISDIR + case EISDIR: return DRFLAC_IS_DIRECTORY; + #endif + #ifdef EINVAL + case EINVAL: return DRFLAC_INVALID_ARGS; + #endif + #ifdef ENFILE + case ENFILE: return DRFLAC_TOO_MANY_OPEN_FILES; + #endif + #ifdef EMFILE + case EMFILE: return DRFLAC_TOO_MANY_OPEN_FILES; + #endif + #ifdef ENOTTY + case ENOTTY: return DRFLAC_INVALID_OPERATION; + #endif + #ifdef ETXTBSY + case ETXTBSY: return DRFLAC_BUSY; + #endif + #ifdef EFBIG + case EFBIG: return DRFLAC_TOO_BIG; + #endif + #ifdef ENOSPC + case ENOSPC: return DRFLAC_NO_SPACE; + #endif + #ifdef ESPIPE + case ESPIPE: return DRFLAC_BAD_SEEK; + #endif + #ifdef EROFS + case EROFS: return DRFLAC_ACCESS_DENIED; + #endif + #ifdef EMLINK + case EMLINK: return DRFLAC_TOO_MANY_LINKS; + #endif + #ifdef EPIPE + case EPIPE: return DRFLAC_BAD_PIPE; + #endif + #ifdef EDOM + case EDOM: return DRFLAC_OUT_OF_RANGE; + #endif + #ifdef ERANGE + case ERANGE: return DRFLAC_OUT_OF_RANGE; + #endif + #ifdef EDEADLK + case EDEADLK: return DRFLAC_DEADLOCK; + #endif + #ifdef ENAMETOOLONG + case ENAMETOOLONG: return DRFLAC_PATH_TOO_LONG; + #endif + #ifdef ENOLCK + case ENOLCK: return DRFLAC_ERROR; + #endif + #ifdef ENOSYS + case ENOSYS: return DRFLAC_NOT_IMPLEMENTED; + #endif + #if defined(ENOTEMPTY) && ENOTEMPTY != EEXIST /* In AIX, ENOTEMPTY and EEXIST use the same value. */ + case ENOTEMPTY: return DRFLAC_DIRECTORY_NOT_EMPTY; + #endif + #ifdef ELOOP + case ELOOP: return DRFLAC_TOO_MANY_LINKS; + #endif + #ifdef ENOMSG + case ENOMSG: return DRFLAC_NO_MESSAGE; + #endif + #ifdef EIDRM + case EIDRM: return DRFLAC_ERROR; + #endif + #ifdef ECHRNG + case ECHRNG: return DRFLAC_ERROR; + #endif + #ifdef EL2NSYNC + case EL2NSYNC: return DRFLAC_ERROR; + #endif + #ifdef EL3HLT + case EL3HLT: return DRFLAC_ERROR; + #endif + #ifdef EL3RST + case EL3RST: return DRFLAC_ERROR; + #endif + #ifdef ELNRNG + case ELNRNG: return DRFLAC_OUT_OF_RANGE; + #endif + #ifdef EUNATCH + case EUNATCH: return DRFLAC_ERROR; + #endif + #ifdef ENOCSI + case ENOCSI: return DRFLAC_ERROR; + #endif + #ifdef EL2HLT + case EL2HLT: return DRFLAC_ERROR; + #endif + #ifdef EBADE + case EBADE: return DRFLAC_ERROR; + #endif + #ifdef EBADR + case EBADR: return DRFLAC_ERROR; + #endif + #ifdef EXFULL + case EXFULL: return DRFLAC_ERROR; + #endif + #ifdef ENOANO + case ENOANO: return DRFLAC_ERROR; + #endif + #ifdef EBADRQC + case EBADRQC: return DRFLAC_ERROR; + #endif + #ifdef EBADSLT + case EBADSLT: return DRFLAC_ERROR; + #endif + #ifdef EBFONT + case EBFONT: return DRFLAC_INVALID_FILE; + #endif + #ifdef ENOSTR + case ENOSTR: return DRFLAC_ERROR; + #endif + #ifdef ENODATA + case ENODATA: return DRFLAC_NO_DATA_AVAILABLE; + #endif + #ifdef ETIME + case ETIME: return DRFLAC_TIMEOUT; + #endif + #ifdef ENOSR + case ENOSR: return DRFLAC_NO_DATA_AVAILABLE; + #endif + #ifdef ENONET + case ENONET: return DRFLAC_NO_NETWORK; + #endif + #ifdef ENOPKG + case ENOPKG: return DRFLAC_ERROR; + #endif + #ifdef EREMOTE + case EREMOTE: return DRFLAC_ERROR; + #endif + #ifdef ENOLINK + case ENOLINK: return DRFLAC_ERROR; + #endif + #ifdef EADV + case EADV: return DRFLAC_ERROR; + #endif + #ifdef ESRMNT + case ESRMNT: return DRFLAC_ERROR; + #endif + #ifdef ECOMM + case ECOMM: return DRFLAC_ERROR; + #endif + #ifdef EPROTO + case EPROTO: return DRFLAC_ERROR; + #endif + #ifdef EMULTIHOP + case EMULTIHOP: return DRFLAC_ERROR; + #endif + #ifdef EDOTDOT + case EDOTDOT: return DRFLAC_ERROR; + #endif + #ifdef EBADMSG + case EBADMSG: return DRFLAC_BAD_MESSAGE; + #endif + #ifdef EOVERFLOW + case EOVERFLOW: return DRFLAC_TOO_BIG; + #endif + #ifdef ENOTUNIQ + case ENOTUNIQ: return DRFLAC_NOT_UNIQUE; + #endif + #ifdef EBADFD + case EBADFD: return DRFLAC_ERROR; + #endif + #ifdef EREMCHG + case EREMCHG: return DRFLAC_ERROR; + #endif + #ifdef ELIBACC + case ELIBACC: return DRFLAC_ACCESS_DENIED; + #endif + #ifdef ELIBBAD + case ELIBBAD: return DRFLAC_INVALID_FILE; + #endif + #ifdef ELIBSCN + case ELIBSCN: return DRFLAC_INVALID_FILE; + #endif + #ifdef ELIBMAX + case ELIBMAX: return DRFLAC_ERROR; + #endif + #ifdef ELIBEXEC + case ELIBEXEC: return DRFLAC_ERROR; + #endif + #ifdef EILSEQ + case EILSEQ: return DRFLAC_INVALID_DATA; + #endif + #ifdef ERESTART + case ERESTART: return DRFLAC_ERROR; + #endif + #ifdef ESTRPIPE + case ESTRPIPE: return DRFLAC_ERROR; + #endif + #ifdef EUSERS + case EUSERS: return DRFLAC_ERROR; + #endif + #ifdef ENOTSOCK + case ENOTSOCK: return DRFLAC_NOT_SOCKET; + #endif + #ifdef EDESTADDRREQ + case EDESTADDRREQ: return DRFLAC_NO_ADDRESS; + #endif + #ifdef EMSGSIZE + case EMSGSIZE: return DRFLAC_TOO_BIG; + #endif + #ifdef EPROTOTYPE + case EPROTOTYPE: return DRFLAC_BAD_PROTOCOL; + #endif + #ifdef ENOPROTOOPT + case ENOPROTOOPT: return DRFLAC_PROTOCOL_UNAVAILABLE; + #endif + #ifdef EPROTONOSUPPORT + case EPROTONOSUPPORT: return DRFLAC_PROTOCOL_NOT_SUPPORTED; + #endif + #ifdef ESOCKTNOSUPPORT + case ESOCKTNOSUPPORT: return DRFLAC_SOCKET_NOT_SUPPORTED; + #endif + #ifdef EOPNOTSUPP + case EOPNOTSUPP: return DRFLAC_INVALID_OPERATION; + #endif + #ifdef EPFNOSUPPORT + case EPFNOSUPPORT: return DRFLAC_PROTOCOL_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EAFNOSUPPORT + case EAFNOSUPPORT: return DRFLAC_ADDRESS_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EADDRINUSE + case EADDRINUSE: return DRFLAC_ALREADY_IN_USE; + #endif + #ifdef EADDRNOTAVAIL + case EADDRNOTAVAIL: return DRFLAC_ERROR; + #endif + #ifdef ENETDOWN + case ENETDOWN: return DRFLAC_NO_NETWORK; + #endif + #ifdef ENETUNREACH + case ENETUNREACH: return DRFLAC_NO_NETWORK; + #endif + #ifdef ENETRESET + case ENETRESET: return DRFLAC_NO_NETWORK; + #endif + #ifdef ECONNABORTED + case ECONNABORTED: return DRFLAC_NO_NETWORK; + #endif + #ifdef ECONNRESET + case ECONNRESET: return DRFLAC_CONNECTION_RESET; + #endif + #ifdef ENOBUFS + case ENOBUFS: return DRFLAC_NO_SPACE; + #endif + #ifdef EISCONN + case EISCONN: return DRFLAC_ALREADY_CONNECTED; + #endif + #ifdef ENOTCONN + case ENOTCONN: return DRFLAC_NOT_CONNECTED; + #endif + #ifdef ESHUTDOWN + case ESHUTDOWN: return DRFLAC_ERROR; + #endif + #ifdef ETOOMANYREFS + case ETOOMANYREFS: return DRFLAC_ERROR; + #endif + #ifdef ETIMEDOUT + case ETIMEDOUT: return DRFLAC_TIMEOUT; + #endif + #ifdef ECONNREFUSED + case ECONNREFUSED: return DRFLAC_CONNECTION_REFUSED; + #endif + #ifdef EHOSTDOWN + case EHOSTDOWN: return DRFLAC_NO_HOST; + #endif + #ifdef EHOSTUNREACH + case EHOSTUNREACH: return DRFLAC_NO_HOST; + #endif + #ifdef EALREADY + case EALREADY: return DRFLAC_IN_PROGRESS; + #endif + #ifdef EINPROGRESS + case EINPROGRESS: return DRFLAC_IN_PROGRESS; + #endif + #ifdef ESTALE + case ESTALE: return DRFLAC_INVALID_FILE; + #endif + #ifdef EUCLEAN + case EUCLEAN: return DRFLAC_ERROR; + #endif + #ifdef ENOTNAM + case ENOTNAM: return DRFLAC_ERROR; + #endif + #ifdef ENAVAIL + case ENAVAIL: return DRFLAC_ERROR; + #endif + #ifdef EISNAM + case EISNAM: return DRFLAC_ERROR; + #endif + #ifdef EREMOTEIO + case EREMOTEIO: return DRFLAC_IO_ERROR; + #endif + #ifdef EDQUOT + case EDQUOT: return DRFLAC_NO_SPACE; + #endif + #ifdef ENOMEDIUM + case ENOMEDIUM: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef EMEDIUMTYPE + case EMEDIUMTYPE: return DRFLAC_ERROR; + #endif + #ifdef ECANCELED + case ECANCELED: return DRFLAC_CANCELLED; + #endif + #ifdef ENOKEY + case ENOKEY: return DRFLAC_ERROR; + #endif + #ifdef EKEYEXPIRED + case EKEYEXPIRED: return DRFLAC_ERROR; + #endif + #ifdef EKEYREVOKED + case EKEYREVOKED: return DRFLAC_ERROR; + #endif + #ifdef EKEYREJECTED + case EKEYREJECTED: return DRFLAC_ERROR; + #endif + #ifdef EOWNERDEAD + case EOWNERDEAD: return DRFLAC_ERROR; + #endif + #ifdef ENOTRECOVERABLE + case ENOTRECOVERABLE: return DRFLAC_ERROR; + #endif + #ifdef ERFKILL + case ERFKILL: return DRFLAC_ERROR; + #endif + #ifdef EHWPOISON + case EHWPOISON: return DRFLAC_ERROR; + #endif + default: return DRFLAC_ERROR; + } +} +/* End Errno */ + +/* fopen */ +static drflac_result drflac_fopen(FILE** ppFile, const char* pFilePath, const char* pOpenMode) +{ +#if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err; +#endif + + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRFLAC_INVALID_ARGS; + } + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + err = fopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drflac_result_from_errno(err); + } +#else +#if defined(_WIN32) || defined(__APPLE__) + *ppFile = fopen(pFilePath, pOpenMode); +#else + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 && defined(_LARGEFILE64_SOURCE) + *ppFile = fopen64(pFilePath, pOpenMode); + #else + *ppFile = fopen(pFilePath, pOpenMode); + #endif +#endif + if (*ppFile == NULL) { + drflac_result result = drflac_result_from_errno(errno); + if (result == DRFLAC_SUCCESS) { + result = DRFLAC_ERROR; /* Just a safety check to make sure we never ever return success when pFile == NULL. */ + } + + return result; + } +#endif + + return DRFLAC_SUCCESS; +} + +/* +_wfopen() isn't always available in all compilation environments. + + * Windows only. + * MSVC seems to support it universally as far back as VC6 from what I can tell (haven't checked further back). + * MinGW-64 (both 32- and 64-bit) seems to support it. + * MinGW wraps it in !defined(__STRICT_ANSI__). + * OpenWatcom wraps it in !defined(_NO_EXT_KEYS). + +This can be reviewed as compatibility issues arise. The preference is to use _wfopen_s() and _wfopen() as opposed to the wcsrtombs() +fallback, so if you notice your compiler not detecting this properly I'm happy to look at adding support. +*/ +#if defined(_WIN32) + #if defined(_MSC_VER) || defined(__MINGW64__) || (!defined(__STRICT_ANSI__) && !defined(_NO_EXT_KEYS)) + #define DRFLAC_HAS_WFOPEN + #endif +#endif + +#ifndef DR_FLAC_NO_WCHAR +static drflac_result drflac_wfopen(FILE** ppFile, const wchar_t* pFilePath, const wchar_t* pOpenMode, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRFLAC_INVALID_ARGS; + } + +#if defined(DRFLAC_HAS_WFOPEN) + { + /* Use _wfopen() on Windows. */ + #if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err = _wfopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drflac_result_from_errno(err); + } + #else + *ppFile = _wfopen(pFilePath, pOpenMode); + if (*ppFile == NULL) { + return drflac_result_from_errno(errno); + } + #endif + (void)pAllocationCallbacks; + } +#else + /* + Use fopen() on anything other than Windows. Requires a conversion. This is annoying because + fopen() is locale specific. The only real way I can think of to do this is with wcsrtombs(). Note + that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for + maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler + error I'll look into improving compatibility. + */ + + /* + Some compilers don't support wchar_t or wcsrtombs() which we're using below. In this case we just + need to abort with an error. If you encounter a compiler lacking such support, add it to this list + and submit a bug report and it'll be added to the library upstream. + */ + #if defined(__DJGPP__) + { + /* Nothing to do here. This will fall through to the error check below. */ + } + #else + { + mbstate_t mbs; + size_t lenMB; + const wchar_t* pFilePathTemp = pFilePath; + char* pFilePathMB = NULL; + char pOpenModeMB[32] = {0}; + + /* Get the length first. */ + DRFLAC_ZERO_OBJECT(&mbs); + lenMB = wcsrtombs(NULL, &pFilePathTemp, 0, &mbs); + if (lenMB == (size_t)-1) { + return drflac_result_from_errno(errno); + } + + pFilePathMB = (char*)drflac__malloc_from_callbacks(lenMB + 1, pAllocationCallbacks); + if (pFilePathMB == NULL) { + return DRFLAC_OUT_OF_MEMORY; + } + + pFilePathTemp = pFilePath; + DRFLAC_ZERO_OBJECT(&mbs); + wcsrtombs(pFilePathMB, &pFilePathTemp, lenMB + 1, &mbs); + + /* The open mode should always consist of ASCII characters so we should be able to do a trivial conversion. */ + { + size_t i = 0; + for (;;) { + if (pOpenMode[i] == 0) { + pOpenModeMB[i] = '\0'; + break; + } + + pOpenModeMB[i] = (char)pOpenMode[i]; + i += 1; + } + } + + *ppFile = fopen(pFilePathMB, pOpenModeMB); + + drflac__free_from_callbacks(pFilePathMB, pAllocationCallbacks); + } + #endif + + if (*ppFile == NULL) { + return DRFLAC_ERROR; + } +#endif + + return DRFLAC_SUCCESS; +} +#endif +/* End fopen */ + +static size_t drflac__on_read_stdio(void* pUserData, void* bufferOut, size_t bytesToRead) +{ + return fread(bufferOut, 1, bytesToRead, (FILE*)pUserData); +} + +static drflac_bool32 drflac__on_seek_stdio(void* pUserData, int offset, drflac_seek_origin origin) +{ + int whence = SEEK_SET; + if (origin == DRFLAC_SEEK_CUR) { + whence = SEEK_CUR; + } else if (origin == DRFLAC_SEEK_END) { + whence = SEEK_END; + } + + return fseek((FILE*)pUserData, offset, whence) == 0; +} + +static drflac_bool32 drflac__on_tell_stdio(void* pUserData, drflac_int64* pCursor) +{ + FILE* pFileStdio = (FILE*)pUserData; + drflac_int64 result; + + /* These were all validated at a higher level. */ + DRFLAC_ASSERT(pFileStdio != NULL); + DRFLAC_ASSERT(pCursor != NULL); + +#if defined(_WIN32) && !defined(NXDK) + #if defined(_MSC_VER) && _MSC_VER > 1200 + result = _ftelli64(pFileStdio); + #else + result = ftell(pFileStdio); + #endif +#else + result = ftell(pFileStdio); +#endif + + *pCursor = result; + + return DRFLAC_TRUE; +} + + + +DRFLAC_API drflac* drflac_open_file(const char* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_fopen(&pFile, pFileName, "rb") != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, (void*)pFile, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return NULL; + } + + return pFlac; +} + +#ifndef DR_FLAC_NO_WCHAR +DRFLAC_API drflac* drflac_open_file_w(const wchar_t* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_wfopen(&pFile, pFileName, L"rb", pAllocationCallbacks) != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, (void*)pFile, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return NULL; + } + + return pFlac; +} +#endif + +DRFLAC_API drflac* drflac_open_file_with_metadata(const char* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_fopen(&pFile, pFileName, "rb") != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open_with_metadata_private(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, onMeta, drflac_container_unknown, (void*)pFile, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return pFlac; + } + + return pFlac; +} + +#ifndef DR_FLAC_NO_WCHAR +DRFLAC_API drflac* drflac_open_file_with_metadata_w(const wchar_t* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_wfopen(&pFile, pFileName, L"rb", pAllocationCallbacks) != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open_with_metadata_private(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, onMeta, drflac_container_unknown, (void*)pFile, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return pFlac; + } + + return pFlac; +} +#endif +#endif /* DR_FLAC_NO_STDIO */ + +static size_t drflac__on_read_memory(void* pUserData, void* bufferOut, size_t bytesToRead) +{ + drflac__memory_stream* memoryStream = (drflac__memory_stream*)pUserData; + size_t bytesRemaining; + + DRFLAC_ASSERT(memoryStream != NULL); + DRFLAC_ASSERT(memoryStream->dataSize >= memoryStream->currentReadPos); + + bytesRemaining = memoryStream->dataSize - memoryStream->currentReadPos; + if (bytesToRead > bytesRemaining) { + bytesToRead = bytesRemaining; + } + + if (bytesToRead > 0) { + DRFLAC_COPY_MEMORY(bufferOut, memoryStream->data + memoryStream->currentReadPos, bytesToRead); + memoryStream->currentReadPos += bytesToRead; + } + + return bytesToRead; +} + +static drflac_bool32 drflac__on_seek_memory(void* pUserData, int offset, drflac_seek_origin origin) +{ + drflac__memory_stream* memoryStream = (drflac__memory_stream*)pUserData; + drflac_int64 newCursor; + + DRFLAC_ASSERT(memoryStream != NULL); + + if (origin == DRFLAC_SEEK_SET) { + newCursor = 0; + } else if (origin == DRFLAC_SEEK_CUR) { + newCursor = (drflac_int64)memoryStream->currentReadPos; + } else if (origin == DRFLAC_SEEK_END) { + newCursor = (drflac_int64)memoryStream->dataSize; + } else { + DRFLAC_ASSERT(!"Invalid seek origin"); + return DRFLAC_FALSE; + } + + newCursor += offset; + + if (newCursor < 0) { + return DRFLAC_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > memoryStream->dataSize) { + return DRFLAC_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + memoryStream->currentReadPos = (size_t)newCursor; + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__on_tell_memory(void* pUserData, drflac_int64* pCursor) +{ + drflac__memory_stream* memoryStream = (drflac__memory_stream*)pUserData; + + DRFLAC_ASSERT(memoryStream != NULL); + DRFLAC_ASSERT(pCursor != NULL); + + *pCursor = (drflac_int64)memoryStream->currentReadPos; + return DRFLAC_TRUE; +} + +DRFLAC_API drflac* drflac_open_memory(const void* pData, size_t dataSize, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac__memory_stream memoryStream; + drflac* pFlac; + + memoryStream.data = (const drflac_uint8*)pData; + memoryStream.dataSize = dataSize; + memoryStream.currentReadPos = 0; + pFlac = drflac_open(drflac__on_read_memory, drflac__on_seek_memory, drflac__on_tell_memory, &memoryStream, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + pFlac->memoryStream = memoryStream; + + /* This is an awful hack... */ +#ifndef DR_FLAC_NO_OGG + if (pFlac->container == drflac_container_ogg) + { + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + oggbs->pUserData = &pFlac->memoryStream; + } + else +#endif + { + pFlac->bs.pUserData = &pFlac->memoryStream; + } + + return pFlac; +} + +DRFLAC_API drflac* drflac_open_memory_with_metadata(const void* pData, size_t dataSize, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac__memory_stream memoryStream; + drflac* pFlac; + + memoryStream.data = (const drflac_uint8*)pData; + memoryStream.dataSize = dataSize; + memoryStream.currentReadPos = 0; + pFlac = drflac_open_with_metadata_private(drflac__on_read_memory, drflac__on_seek_memory, drflac__on_tell_memory, onMeta, drflac_container_unknown, &memoryStream, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + pFlac->memoryStream = memoryStream; + + /* This is an awful hack... */ +#ifndef DR_FLAC_NO_OGG + if (pFlac->container == drflac_container_ogg) + { + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + oggbs->pUserData = &pFlac->memoryStream; + } + else +#endif + { + pFlac->bs.pUserData = &pFlac->memoryStream; + } + + return pFlac; +} + + + +DRFLAC_API drflac* drflac_open(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, NULL, drflac_container_unknown, pUserData, pUserData, pAllocationCallbacks); +} +DRFLAC_API drflac* drflac_open_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, NULL, container, pUserData, pUserData, pAllocationCallbacks); +} + +DRFLAC_API drflac* drflac_open_with_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, onMeta, drflac_container_unknown, pUserData, pUserData, pAllocationCallbacks); +} +DRFLAC_API drflac* drflac_open_with_metadata_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, onMeta, container, pUserData, pUserData, pAllocationCallbacks); +} + +DRFLAC_API void drflac_close(drflac* pFlac) +{ + if (pFlac == NULL) { + return; + } + +#ifndef DR_FLAC_NO_STDIO + /* + If we opened the file with drflac_open_file() we will want to close the file handle. We can know whether or not drflac_open_file() + was used by looking at the callbacks. + */ + if (pFlac->bs.onRead == drflac__on_read_stdio) { + fclose((FILE*)pFlac->bs.pUserData); + } + +#ifndef DR_FLAC_NO_OGG + /* Need to clean up Ogg streams a bit differently due to the way the bit streaming is chained. */ + if (pFlac->container == drflac_container_ogg) { + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + DRFLAC_ASSERT(pFlac->bs.onRead == drflac__on_read_ogg); + + if (oggbs->onRead == drflac__on_read_stdio) { + fclose((FILE*)oggbs->pUserData); + } + } +#endif +#endif + + drflac__free_from_callbacks(pFlac, &pFlac->allocationCallbacks); +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 left = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 left0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 left1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 left2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 left3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 right0 = left0 - side0; + drflac_uint32 right1 = left1 - side1; + drflac_uint32 right2 = left2 - side2; + drflac_uint32 right3 = left3 - side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0; + pOutputSamples[i*8+1] = (drflac_int32)right0; + pOutputSamples[i*8+2] = (drflac_int32)left1; + pOutputSamples[i*8+3] = (drflac_int32)right1; + pOutputSamples[i*8+4] = (drflac_int32)left2; + pOutputSamples[i*8+5] = (drflac_int32)right2; + pOutputSamples[i*8+6] = (drflac_int32)left3; + pOutputSamples[i*8+7] = (drflac_int32)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i right = _mm_sub_epi32(left, side); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t left; + uint32x4_t side; + uint32x4_t right; + + left = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + right = vsubq_u32(left, side); + + drflac__vst2q_u32((drflac_uint32*)pOutputSamples + i*8, vzipq_u32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_left_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_left_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_left_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_left_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 side = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 right = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 side0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 side1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 side2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 side3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 right0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 right1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 right2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 right3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 left0 = right0 + side0; + drflac_uint32 left1 = right1 + side1; + drflac_uint32 left2 = right2 + side2; + drflac_uint32 left3 = right3 + side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0; + pOutputSamples[i*8+1] = (drflac_int32)right0; + pOutputSamples[i*8+2] = (drflac_int32)left1; + pOutputSamples[i*8+3] = (drflac_int32)right1; + pOutputSamples[i*8+4] = (drflac_int32)left2; + pOutputSamples[i*8+5] = (drflac_int32)right2; + pOutputSamples[i*8+6] = (drflac_int32)left3; + pOutputSamples[i*8+7] = (drflac_int32)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i left = _mm_add_epi32(right, side); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t side; + uint32x4_t right; + uint32x4_t left; + + side = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + right = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + left = vaddq_u32(right, side); + + drflac__vst2q_u32((drflac_uint32*)pOutputSamples + i*8, vzipq_u32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_right_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_right_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_right_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_right_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample); + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_int32 shift = unusedBitsPerSample; + + if (shift > 0) { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (mid0 + side0) << shift; + temp1L = (mid1 + side1) << shift; + temp2L = (mid2 + side2) << shift; + temp3L = (mid3 + side3) << shift; + + temp0R = (mid0 - side0) << shift; + temp1R = (mid1 - side1) << shift; + temp2R = (mid2 - side2) << shift; + temp3R = (mid3 - side3) << shift; + + pOutputSamples[i*8+0] = (drflac_int32)temp0L; + pOutputSamples[i*8+1] = (drflac_int32)temp0R; + pOutputSamples[i*8+2] = (drflac_int32)temp1L; + pOutputSamples[i*8+3] = (drflac_int32)temp1R; + pOutputSamples[i*8+4] = (drflac_int32)temp2L; + pOutputSamples[i*8+5] = (drflac_int32)temp2R; + pOutputSamples[i*8+6] = (drflac_int32)temp3L; + pOutputSamples[i*8+7] = (drflac_int32)temp3R; + } + } else { + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (drflac_uint32)((drflac_int32)(mid0 + side0) >> 1); + temp1L = (drflac_uint32)((drflac_int32)(mid1 + side1) >> 1); + temp2L = (drflac_uint32)((drflac_int32)(mid2 + side2) >> 1); + temp3L = (drflac_uint32)((drflac_int32)(mid3 + side3) >> 1); + + temp0R = (drflac_uint32)((drflac_int32)(mid0 - side0) >> 1); + temp1R = (drflac_uint32)((drflac_int32)(mid1 - side1) >> 1); + temp2R = (drflac_uint32)((drflac_int32)(mid2 - side2) >> 1); + temp3R = (drflac_uint32)((drflac_int32)(mid3 - side3) >> 1); + + pOutputSamples[i*8+0] = (drflac_int32)temp0L; + pOutputSamples[i*8+1] = (drflac_int32)temp0R; + pOutputSamples[i*8+2] = (drflac_int32)temp1L; + pOutputSamples[i*8+3] = (drflac_int32)temp1R; + pOutputSamples[i*8+4] = (drflac_int32)temp2L; + pOutputSamples[i*8+5] = (drflac_int32)temp2R; + pOutputSamples[i*8+6] = (drflac_int32)temp3L; + pOutputSamples[i*8+7] = (drflac_int32)temp3R; + } + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample); + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_int32 shift = unusedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_srai_epi32(_mm_add_epi32(mid, side), 1); + right = _mm_srai_epi32(_mm_sub_epi32(mid, side), 1); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)(mid + side) >> 1; + pOutputSamples[i*2+1] = (drflac_int32)(mid - side) >> 1; + } + } else { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_slli_epi32(_mm_add_epi32(mid, side), shift); + right = _mm_slli_epi32(_mm_sub_epi32(mid, side), shift); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift); + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift); + } + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_int32 shift = unusedBitsPerSample; + int32x4_t wbpsShift0_4; /* wbps = Wasted Bits Per Sample */ + int32x4_t wbpsShift1_4; /* wbps = Wasted Bits Per Sample */ + uint32x4_t one4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + wbpsShift0_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + wbpsShift1_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + one4 = vdupq_n_u32(1); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, one4)); + + left = vshrq_n_s32(vreinterpretq_s32_u32(vaddq_u32(mid, side)), 1); + right = vshrq_n_s32(vreinterpretq_s32_u32(vsubq_u32(mid, side)), 1); + + drflac__vst2q_s32(pOutputSamples + i*8, vzipq_s32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)(mid + side) >> 1; + pOutputSamples[i*2+1] = (drflac_int32)(mid - side) >> 1; + } + } else { + int32x4_t shift4; + + shift -= 1; + shift4 = vdupq_n_s32(shift); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, one4)); + + left = vreinterpretq_s32_u32(vshlq_u32(vaddq_u32(mid, side), shift4)); + right = vreinterpretq_s32_u32(vshlq_u32(vsubq_u32(mid, side), shift4)); + + drflac__vst2q_s32(pOutputSamples + i*8, vzipq_s32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift); + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift); + } + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_mid_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_mid_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_mid_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_mid_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample)); + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample)); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 tempL0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 tempL1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 tempL2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 tempL3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 tempR0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 tempR1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 tempR2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 tempR3 = pInputSamples1U32[i*4+3] << shift1; + + pOutputSamples[i*8+0] = (drflac_int32)tempL0; + pOutputSamples[i*8+1] = (drflac_int32)tempR0; + pOutputSamples[i*8+2] = (drflac_int32)tempL1; + pOutputSamples[i*8+3] = (drflac_int32)tempR1; + pOutputSamples[i*8+4] = (drflac_int32)tempL2; + pOutputSamples[i*8+5] = (drflac_int32)tempR2; + pOutputSamples[i*8+6] = (drflac_int32)tempL3; + pOutputSamples[i*8+7] = (drflac_int32)tempR3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0); + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0); + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1); + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + int32x4_t shift4_0 = vdupq_n_s32(shift0); + int32x4_t shift4_1 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + int32x4_t left; + int32x4_t right; + + left = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift4_0)); + right = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift4_1)); + + drflac__vst2q_s32(pOutputSamples + i*8, vzipq_s32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0); + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_independent_stereo__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_independent_stereo__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_independent_stereo__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_independent_stereo__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +static drflac_bool32 drflac__is_current_flac_frame_valid(drflac* pFlac) +{ + drflac_uint32 iChannel; + + if (pFlac->currentFLACFrame.header.blockSizeInPCMFrames > pFlac->maxBlockSizeInPCMFrames || pFlac->currentFLACFrame.pcmFramesRemaining > pFlac->currentFLACFrame.header.blockSizeInPCMFrames) { + return DRFLAC_FALSE; + } + + for (iChannel = 0; iChannel < pFlac->channels; iChannel += 1) { + if (pFlac->currentFLACFrame.subframes[iChannel].pSamplesS32 == NULL) { + return DRFLAC_FALSE; + } + } + + return DRFLAC_TRUE; +} + +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s32(drflac* pFlac, drflac_uint64 framesToRead, drflac_int32* pBufferOut) +{ + drflac_uint64 framesRead; + drflac_uint32 unusedBitsPerSample; + + if (pFlac == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drflac__seek_forward_by_pcm_frames(pFlac, framesToRead); + } + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 32); + unusedBitsPerSample = 32 - pFlac->bitsPerSample; + + framesRead = 0; + while (framesToRead > 0) { + /* If we've run out of samples in this frame, go to the next. */ + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + drflac_uint64 iFirstPCMFrame = pFlac->currentFLACFrame.header.blockSizeInPCMFrames - pFlac->currentFLACFrame.pcmFramesRemaining; + drflac_uint64 frameCountThisIteration = framesToRead; + + if (frameCountThisIteration > pFlac->currentFLACFrame.pcmFramesRemaining) { + frameCountThisIteration = pFlac->currentFLACFrame.pcmFramesRemaining; + } + + if (channelCount == 2) { + const drflac_int32* pDecodedSamples0 = pFlac->currentFLACFrame.subframes[0].pSamplesS32 + iFirstPCMFrame; + const drflac_int32* pDecodedSamples1 = pFlac->currentFLACFrame.subframes[1].pSamplesS32 + iFirstPCMFrame; + + switch (pFlac->currentFLACFrame.header.channelAssignment) + { + case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: + { + drflac_read_pcm_frames_s32__decode_left_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: + { + drflac_read_pcm_frames_s32__decode_right_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: + { + drflac_read_pcm_frames_s32__decode_mid_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: + default: + { + drflac_read_pcm_frames_s32__decode_independent_stereo(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + } + } else { + /* Generic interleaving. */ + drflac_uint64 i; + for (i = 0; i < frameCountThisIteration; ++i) { + unsigned int j; + for (j = 0; j < channelCount; ++j) { + pBufferOut[(i*channelCount)+j] = (drflac_int32)((drflac_uint32)(pFlac->currentFLACFrame.subframes[j].pSamplesS32[iFirstPCMFrame + i]) << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[j].wastedBitsPerSample)); + } + } + } + + framesRead += frameCountThisIteration; + pBufferOut += frameCountThisIteration * channelCount; + framesToRead -= frameCountThisIteration; + pFlac->currentPCMFrame += frameCountThisIteration; + pFlac->currentFLACFrame.pcmFramesRemaining -= (drflac_uint32)frameCountThisIteration; + } + } + + return framesRead; +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 left = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 left0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 left1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 left2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 left3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 right0 = left0 - side0; + drflac_uint32 right1 = left1 - side1; + drflac_uint32 right2 = left2 - side2; + drflac_uint32 right3 = left3 - side3; + + left0 >>= 16; + left1 >>= 16; + left2 >>= 16; + left3 >>= 16; + + right0 >>= 16; + right1 >>= 16; + right2 >>= 16; + right3 >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)left0; + pOutputSamples[i*8+1] = (drflac_int16)right0; + pOutputSamples[i*8+2] = (drflac_int16)left1; + pOutputSamples[i*8+3] = (drflac_int16)right1; + pOutputSamples[i*8+4] = (drflac_int16)left2; + pOutputSamples[i*8+5] = (drflac_int16)right2; + pOutputSamples[i*8+6] = (drflac_int16)left3; + pOutputSamples[i*8+7] = (drflac_int16)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i right = _mm_sub_epi32(left, side); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t left; + uint32x4_t side; + uint32x4_t right; + + left = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + right = vsubq_u32(left, side); + + left = vshrq_n_u32(left, 16); + right = vshrq_n_u32(right, 16); + + drflac__vst2q_u16((drflac_uint16*)pOutputSamples + i*8, vzip_u16(vmovn_u32(left), vmovn_u32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_left_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_left_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_left_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_left_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 side = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 right = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 side0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 side1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 side2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 side3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 right0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 right1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 right2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 right3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 left0 = right0 + side0; + drflac_uint32 left1 = right1 + side1; + drflac_uint32 left2 = right2 + side2; + drflac_uint32 left3 = right3 + side3; + + left0 >>= 16; + left1 >>= 16; + left2 >>= 16; + left3 >>= 16; + + right0 >>= 16; + right1 >>= 16; + right2 >>= 16; + right3 >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)left0; + pOutputSamples[i*8+1] = (drflac_int16)right0; + pOutputSamples[i*8+2] = (drflac_int16)left1; + pOutputSamples[i*8+3] = (drflac_int16)right1; + pOutputSamples[i*8+4] = (drflac_int16)left2; + pOutputSamples[i*8+5] = (drflac_int16)right2; + pOutputSamples[i*8+6] = (drflac_int16)left3; + pOutputSamples[i*8+7] = (drflac_int16)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i left = _mm_add_epi32(right, side); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t side; + uint32x4_t right; + uint32x4_t left; + + side = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + right = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + left = vaddq_u32(right, side); + + left = vshrq_n_u32(left, 16); + right = vshrq_n_u32(right, 16); + + drflac__vst2q_u16((drflac_uint16*)pOutputSamples + i*8, vzip_u16(vmovn_u32(left), vmovn_u32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_right_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_right_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_right_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_right_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + drflac_uint32 mid = (drflac_uint32)pInputSamples0[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample) >> 16); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + + if (shift > 0) { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (mid0 + side0) << shift; + temp1L = (mid1 + side1) << shift; + temp2L = (mid2 + side2) << shift; + temp3L = (mid3 + side3) << shift; + + temp0R = (mid0 - side0) << shift; + temp1R = (mid1 - side1) << shift; + temp2R = (mid2 - side2) << shift; + temp3R = (mid3 - side3) << shift; + + temp0L >>= 16; + temp1L >>= 16; + temp2L >>= 16; + temp3L >>= 16; + + temp0R >>= 16; + temp1R >>= 16; + temp2R >>= 16; + temp3R >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)temp0L; + pOutputSamples[i*8+1] = (drflac_int16)temp0R; + pOutputSamples[i*8+2] = (drflac_int16)temp1L; + pOutputSamples[i*8+3] = (drflac_int16)temp1R; + pOutputSamples[i*8+4] = (drflac_int16)temp2L; + pOutputSamples[i*8+5] = (drflac_int16)temp2R; + pOutputSamples[i*8+6] = (drflac_int16)temp3L; + pOutputSamples[i*8+7] = (drflac_int16)temp3R; + } + } else { + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = ((drflac_int32)(mid0 + side0) >> 1); + temp1L = ((drflac_int32)(mid1 + side1) >> 1); + temp2L = ((drflac_int32)(mid2 + side2) >> 1); + temp3L = ((drflac_int32)(mid3 + side3) >> 1); + + temp0R = ((drflac_int32)(mid0 - side0) >> 1); + temp1R = ((drflac_int32)(mid1 - side1) >> 1); + temp2R = ((drflac_int32)(mid2 - side2) >> 1); + temp3R = ((drflac_int32)(mid3 - side3) >> 1); + + temp0L >>= 16; + temp1L >>= 16; + temp2L >>= 16; + temp3L >>= 16; + + temp0R >>= 16; + temp1R >>= 16; + temp2R >>= 16; + temp3R >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)temp0L; + pOutputSamples[i*8+1] = (drflac_int16)temp0R; + pOutputSamples[i*8+2] = (drflac_int16)temp1L; + pOutputSamples[i*8+3] = (drflac_int16)temp1R; + pOutputSamples[i*8+4] = (drflac_int16)temp2L; + pOutputSamples[i*8+5] = (drflac_int16)temp2R; + pOutputSamples[i*8+6] = (drflac_int16)temp3L; + pOutputSamples[i*8+7] = (drflac_int16)temp3R; + } + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample) >> 16); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_srai_epi32(_mm_add_epi32(mid, side), 1); + right = _mm_srai_epi32(_mm_sub_epi32(mid, side), 1); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_int32)(mid + side) >> 1) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_int32)(mid - side) >> 1) >> 16); + } + } else { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_slli_epi32(_mm_add_epi32(mid, side), shift); + right = _mm_slli_epi32(_mm_sub_epi32(mid, side), shift); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((mid + side) << shift) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((mid - side) << shift) >> 16); + } + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + int32x4_t wbpsShift0_4; /* wbps = Wasted Bits Per Sample */ + int32x4_t wbpsShift1_4; /* wbps = Wasted Bits Per Sample */ + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + wbpsShift0_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + wbpsShift1_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + left = vshrq_n_s32(vreinterpretq_s32_u32(vaddq_u32(mid, side)), 1); + right = vshrq_n_s32(vreinterpretq_s32_u32(vsubq_u32(mid, side)), 1); + + left = vshrq_n_s32(left, 16); + right = vshrq_n_s32(right, 16); + + drflac__vst2q_s16(pOutputSamples + i*8, vzip_s16(vmovn_s32(left), vmovn_s32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_int32)(mid + side) >> 1) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_int32)(mid - side) >> 1) >> 16); + } + } else { + int32x4_t shift4; + + shift -= 1; + shift4 = vdupq_n_s32(shift); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + left = vreinterpretq_s32_u32(vshlq_u32(vaddq_u32(mid, side), shift4)); + right = vreinterpretq_s32_u32(vshlq_u32(vsubq_u32(mid, side), shift4)); + + left = vshrq_n_s32(left, 16); + right = vshrq_n_s32(right, 16); + + drflac__vst2q_s16(pOutputSamples + i*8, vzip_s16(vmovn_s32(left), vmovn_s32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((mid + side) << shift) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((mid - side) << shift) >> 16); + } + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_mid_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_mid_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_mid_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_mid_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((drflac_int32)((drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample)) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((drflac_int32)((drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample)) >> 16); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 tempL0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 tempL1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 tempL2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 tempL3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 tempR0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 tempR1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 tempR2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 tempR3 = pInputSamples1U32[i*4+3] << shift1; + + tempL0 >>= 16; + tempL1 >>= 16; + tempL2 >>= 16; + tempL3 >>= 16; + + tempR0 >>= 16; + tempR1 >>= 16; + tempR2 >>= 16; + tempR3 >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)tempL0; + pOutputSamples[i*8+1] = (drflac_int16)tempR0; + pOutputSamples[i*8+2] = (drflac_int16)tempL1; + pOutputSamples[i*8+3] = (drflac_int16)tempR1; + pOutputSamples[i*8+4] = (drflac_int16)tempL2; + pOutputSamples[i*8+5] = (drflac_int16)tempR2; + pOutputSamples[i*8+6] = (drflac_int16)tempL3; + pOutputSamples[i*8+7] = (drflac_int16)tempR3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((pInputSamples0U32[i] << shift0) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((pInputSamples1U32[i] << shift1) >> 16); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + /* At this point we have results. We can now pack and interleave these into a single __m128i object and then store the in the output buffer. */ + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((pInputSamples0U32[i] << shift0) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((pInputSamples1U32[i] << shift1) >> 16); + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + int32x4_t shift0_4 = vdupq_n_s32(shift0); + int32x4_t shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + int32x4_t left; + int32x4_t right; + + left = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4)); + right = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4)); + + left = vshrq_n_s32(left, 16); + right = vshrq_n_s32(right, 16); + + drflac__vst2q_s16(pOutputSamples + i*8, vzip_s16(vmovn_s32(left), vmovn_s32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((pInputSamples0U32[i] << shift0) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((pInputSamples1U32[i] << shift1) >> 16); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_independent_stereo__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_independent_stereo__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_independent_stereo__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_independent_stereo__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s16(drflac* pFlac, drflac_uint64 framesToRead, drflac_int16* pBufferOut) +{ + drflac_uint64 framesRead; + drflac_uint32 unusedBitsPerSample; + + if (pFlac == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drflac__seek_forward_by_pcm_frames(pFlac, framesToRead); + } + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 32); + unusedBitsPerSample = 32 - pFlac->bitsPerSample; + + framesRead = 0; + while (framesToRead > 0) { + /* If we've run out of samples in this frame, go to the next. */ + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + drflac_uint64 iFirstPCMFrame = pFlac->currentFLACFrame.header.blockSizeInPCMFrames - pFlac->currentFLACFrame.pcmFramesRemaining; + drflac_uint64 frameCountThisIteration = framesToRead; + + if (frameCountThisIteration > pFlac->currentFLACFrame.pcmFramesRemaining) { + frameCountThisIteration = pFlac->currentFLACFrame.pcmFramesRemaining; + } + + if (channelCount == 2) { + const drflac_int32* pDecodedSamples0 = pFlac->currentFLACFrame.subframes[0].pSamplesS32 + iFirstPCMFrame; + const drflac_int32* pDecodedSamples1 = pFlac->currentFLACFrame.subframes[1].pSamplesS32 + iFirstPCMFrame; + + switch (pFlac->currentFLACFrame.header.channelAssignment) + { + case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: + { + drflac_read_pcm_frames_s16__decode_left_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: + { + drflac_read_pcm_frames_s16__decode_right_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: + { + drflac_read_pcm_frames_s16__decode_mid_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: + default: + { + drflac_read_pcm_frames_s16__decode_independent_stereo(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + } + } else { + /* Generic interleaving. */ + drflac_uint64 i; + for (i = 0; i < frameCountThisIteration; ++i) { + unsigned int j; + for (j = 0; j < channelCount; ++j) { + drflac_int32 sampleS32 = (drflac_int32)((drflac_uint32)(pFlac->currentFLACFrame.subframes[j].pSamplesS32[iFirstPCMFrame + i]) << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[j].wastedBitsPerSample)); + pBufferOut[(i*channelCount)+j] = (drflac_int16)(sampleS32 >> 16); + } + } + } + + framesRead += frameCountThisIteration; + pBufferOut += frameCountThisIteration * channelCount; + framesToRead -= frameCountThisIteration; + pFlac->currentPCMFrame += frameCountThisIteration; + pFlac->currentFLACFrame.pcmFramesRemaining -= (drflac_uint32)frameCountThisIteration; + } + } + + return framesRead; +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 left = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (float)((drflac_int32)left / 2147483648.0); + pOutputSamples[i*2+1] = (float)((drflac_int32)right / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + float factor = 1 / 2147483648.0; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 left0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 left1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 left2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 left3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 right0 = left0 - side0; + drflac_uint32 right1 = left1 - side1; + drflac_uint32 right2 = left2 - side2; + drflac_uint32 right3 = left3 - side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0 * factor; + pOutputSamples[i*8+1] = (drflac_int32)right0 * factor; + pOutputSamples[i*8+2] = (drflac_int32)left1 * factor; + pOutputSamples[i*8+3] = (drflac_int32)right1 * factor; + pOutputSamples[i*8+4] = (drflac_int32)left2 * factor; + pOutputSamples[i*8+5] = (drflac_int32)right2 * factor; + pOutputSamples[i*8+6] = (drflac_int32)left3 * factor; + pOutputSamples[i*8+7] = (drflac_int32)right3 * factor; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left * factor; + pOutputSamples[i*2+1] = (drflac_int32)right * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + __m128 factor; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = _mm_set1_ps(1.0f / 8388608.0f); + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i right = _mm_sub_epi32(left, side); + __m128 leftf = _mm_mul_ps(_mm_cvtepi32_ps(left), factor); + __m128 rightf = _mm_mul_ps(_mm_cvtepi32_ps(right), factor); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + float32x4_t factor4; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor4 = vdupq_n_f32(1.0f / 8388608.0f); + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t left; + uint32x4_t side; + uint32x4_t right; + float32x4_t leftf; + float32x4_t rightf; + + left = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + right = vsubq_u32(left, side); + leftf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(left)), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(right)), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_left_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_left_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_left_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_left_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 side = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 right = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (float)((drflac_int32)left / 2147483648.0); + pOutputSamples[i*2+1] = (float)((drflac_int32)right / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + float factor = 1 / 2147483648.0; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 side0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 side1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 side2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 side3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 right0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 right1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 right2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 right3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 left0 = right0 + side0; + drflac_uint32 left1 = right1 + side1; + drflac_uint32 left2 = right2 + side2; + drflac_uint32 left3 = right3 + side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0 * factor; + pOutputSamples[i*8+1] = (drflac_int32)right0 * factor; + pOutputSamples[i*8+2] = (drflac_int32)left1 * factor; + pOutputSamples[i*8+3] = (drflac_int32)right1 * factor; + pOutputSamples[i*8+4] = (drflac_int32)left2 * factor; + pOutputSamples[i*8+5] = (drflac_int32)right2 * factor; + pOutputSamples[i*8+6] = (drflac_int32)left3 * factor; + pOutputSamples[i*8+7] = (drflac_int32)right3 * factor; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left * factor; + pOutputSamples[i*2+1] = (drflac_int32)right * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + __m128 factor; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = _mm_set1_ps(1.0f / 8388608.0f); + + for (i = 0; i < frameCount4; ++i) { + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i left = _mm_add_epi32(right, side); + __m128 leftf = _mm_mul_ps(_mm_cvtepi32_ps(left), factor); + __m128 rightf = _mm_mul_ps(_mm_cvtepi32_ps(right), factor); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + float32x4_t factor4; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor4 = vdupq_n_f32(1.0f / 8388608.0f); + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t side; + uint32x4_t right; + uint32x4_t left; + float32x4_t leftf; + float32x4_t rightf; + + side = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + right = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + left = vaddq_u32(right, side); + leftf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(left)), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(right)), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_right_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_right_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_right_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_right_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + drflac_uint32 mid = (drflac_uint32)pInputSamples0[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (float)((((drflac_int32)(mid + side) >> 1) << (unusedBitsPerSample)) / 2147483648.0); + pOutputSamples[i*2+1] = (float)((((drflac_int32)(mid - side) >> 1) << (unusedBitsPerSample)) / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + float factor = 1 / 2147483648.0; + + if (shift > 0) { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (mid0 + side0) << shift; + temp1L = (mid1 + side1) << shift; + temp2L = (mid2 + side2) << shift; + temp3L = (mid3 + side3) << shift; + + temp0R = (mid0 - side0) << shift; + temp1R = (mid1 - side1) << shift; + temp2R = (mid2 - side2) << shift; + temp3R = (mid3 - side3) << shift; + + pOutputSamples[i*8+0] = (drflac_int32)temp0L * factor; + pOutputSamples[i*8+1] = (drflac_int32)temp0R * factor; + pOutputSamples[i*8+2] = (drflac_int32)temp1L * factor; + pOutputSamples[i*8+3] = (drflac_int32)temp1R * factor; + pOutputSamples[i*8+4] = (drflac_int32)temp2L * factor; + pOutputSamples[i*8+5] = (drflac_int32)temp2R * factor; + pOutputSamples[i*8+6] = (drflac_int32)temp3L * factor; + pOutputSamples[i*8+7] = (drflac_int32)temp3R * factor; + } + } else { + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (drflac_uint32)((drflac_int32)(mid0 + side0) >> 1); + temp1L = (drflac_uint32)((drflac_int32)(mid1 + side1) >> 1); + temp2L = (drflac_uint32)((drflac_int32)(mid2 + side2) >> 1); + temp3L = (drflac_uint32)((drflac_int32)(mid3 + side3) >> 1); + + temp0R = (drflac_uint32)((drflac_int32)(mid0 - side0) >> 1); + temp1R = (drflac_uint32)((drflac_int32)(mid1 - side1) >> 1); + temp2R = (drflac_uint32)((drflac_int32)(mid2 - side2) >> 1); + temp3R = (drflac_uint32)((drflac_int32)(mid3 - side3) >> 1); + + pOutputSamples[i*8+0] = (drflac_int32)temp0L * factor; + pOutputSamples[i*8+1] = (drflac_int32)temp0R * factor; + pOutputSamples[i*8+2] = (drflac_int32)temp1L * factor; + pOutputSamples[i*8+3] = (drflac_int32)temp1R * factor; + pOutputSamples[i*8+4] = (drflac_int32)temp2L * factor; + pOutputSamples[i*8+5] = (drflac_int32)temp2R * factor; + pOutputSamples[i*8+6] = (drflac_int32)temp3L * factor; + pOutputSamples[i*8+7] = (drflac_int32)temp3R * factor; + } + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample) * factor; + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample) * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample - 8; + float factor; + __m128 factor128; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = 1.0f / 8388608.0f; + factor128 = _mm_set1_ps(factor); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i tempL; + __m128i tempR; + __m128 leftf; + __m128 rightf; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + tempL = _mm_srai_epi32(_mm_add_epi32(mid, side), 1); + tempR = _mm_srai_epi32(_mm_sub_epi32(mid, side), 1); + + leftf = _mm_mul_ps(_mm_cvtepi32_ps(tempL), factor128); + rightf = _mm_mul_ps(_mm_cvtepi32_ps(tempR), factor128); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = ((drflac_int32)(mid + side) >> 1) * factor; + pOutputSamples[i*2+1] = ((drflac_int32)(mid - side) >> 1) * factor; + } + } else { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i tempL; + __m128i tempR; + __m128 leftf; + __m128 rightf; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + tempL = _mm_slli_epi32(_mm_add_epi32(mid, side), shift); + tempR = _mm_slli_epi32(_mm_sub_epi32(mid, side), shift); + + leftf = _mm_mul_ps(_mm_cvtepi32_ps(tempL), factor128); + rightf = _mm_mul_ps(_mm_cvtepi32_ps(tempR), factor128); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift) * factor; + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift) * factor; + } + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample - 8; + float factor; + float32x4_t factor4; + int32x4_t shift4; + int32x4_t wbps0_4; /* Wasted Bits Per Sample */ + int32x4_t wbps1_4; /* Wasted Bits Per Sample */ + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = 1.0f / 8388608.0f; + factor4 = vdupq_n_f32(factor); + wbps0_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + wbps1_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + int32x4_t lefti; + int32x4_t righti; + float32x4_t leftf; + float32x4_t rightf; + + uint32x4_t mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbps0_4); + uint32x4_t side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbps1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + lefti = vshrq_n_s32(vreinterpretq_s32_u32(vaddq_u32(mid, side)), 1); + righti = vshrq_n_s32(vreinterpretq_s32_u32(vsubq_u32(mid, side)), 1); + + leftf = vmulq_f32(vcvtq_f32_s32(lefti), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(righti), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = ((drflac_int32)(mid + side) >> 1) * factor; + pOutputSamples[i*2+1] = ((drflac_int32)(mid - side) >> 1) * factor; + } + } else { + shift -= 1; + shift4 = vdupq_n_s32(shift); + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t lefti; + int32x4_t righti; + float32x4_t leftf; + float32x4_t rightf; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbps0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbps1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + lefti = vreinterpretq_s32_u32(vshlq_u32(vaddq_u32(mid, side), shift4)); + righti = vreinterpretq_s32_u32(vshlq_u32(vsubq_u32(mid, side), shift4)); + + leftf = vmulq_f32(vcvtq_f32_s32(lefti), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(righti), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift) * factor; + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift) * factor; + } + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_mid_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_mid_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_mid_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_mid_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + pOutputSamples[i*2+0] = (float)((drflac_int32)((drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample)) / 2147483648.0); + pOutputSamples[i*2+1] = (float)((drflac_int32)((drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample)) / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + float factor = 1 / 2147483648.0; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 tempL0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 tempL1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 tempL2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 tempL3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 tempR0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 tempR1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 tempR2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 tempR3 = pInputSamples1U32[i*4+3] << shift1; + + pOutputSamples[i*8+0] = (drflac_int32)tempL0 * factor; + pOutputSamples[i*8+1] = (drflac_int32)tempR0 * factor; + pOutputSamples[i*8+2] = (drflac_int32)tempL1 * factor; + pOutputSamples[i*8+3] = (drflac_int32)tempR1 * factor; + pOutputSamples[i*8+4] = (drflac_int32)tempL2 * factor; + pOutputSamples[i*8+5] = (drflac_int32)tempR2 * factor; + pOutputSamples[i*8+6] = (drflac_int32)tempL3 * factor; + pOutputSamples[i*8+7] = (drflac_int32)tempR3 * factor; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0) * factor; + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1) * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + + float factor = 1.0f / 8388608.0f; + __m128 factor128 = _mm_set1_ps(factor); + + for (i = 0; i < frameCount4; ++i) { + __m128i lefti; + __m128i righti; + __m128 leftf; + __m128 rightf; + + lefti = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + righti = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + + leftf = _mm_mul_ps(_mm_cvtepi32_ps(lefti), factor128); + rightf = _mm_mul_ps(_mm_cvtepi32_ps(righti), factor128); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0) * factor; + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1) * factor; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + + float factor = 1.0f / 8388608.0f; + float32x4_t factor4 = vdupq_n_f32(factor); + int32x4_t shift0_4 = vdupq_n_s32(shift0); + int32x4_t shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + int32x4_t lefti; + int32x4_t righti; + float32x4_t leftf; + float32x4_t rightf; + + lefti = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4)); + righti = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4)); + + leftf = vmulq_f32(vcvtq_f32_s32(lefti), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(righti), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0) * factor; + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1) * factor; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_independent_stereo__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_independent_stereo__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_independent_stereo__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_independent_stereo__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_f32(drflac* pFlac, drflac_uint64 framesToRead, float* pBufferOut) +{ + drflac_uint64 framesRead; + drflac_uint32 unusedBitsPerSample; + + if (pFlac == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drflac__seek_forward_by_pcm_frames(pFlac, framesToRead); + } + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 32); + unusedBitsPerSample = 32 - pFlac->bitsPerSample; + + framesRead = 0; + while (framesToRead > 0) { + /* If we've run out of samples in this frame, go to the next. */ + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + drflac_uint64 iFirstPCMFrame = pFlac->currentFLACFrame.header.blockSizeInPCMFrames - pFlac->currentFLACFrame.pcmFramesRemaining; + drflac_uint64 frameCountThisIteration = framesToRead; + + if (frameCountThisIteration > pFlac->currentFLACFrame.pcmFramesRemaining) { + frameCountThisIteration = pFlac->currentFLACFrame.pcmFramesRemaining; + } + + if (channelCount == 2) { + const drflac_int32* pDecodedSamples0 = pFlac->currentFLACFrame.subframes[0].pSamplesS32 + iFirstPCMFrame; + const drflac_int32* pDecodedSamples1 = pFlac->currentFLACFrame.subframes[1].pSamplesS32 + iFirstPCMFrame; + + switch (pFlac->currentFLACFrame.header.channelAssignment) + { + case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: + { + drflac_read_pcm_frames_f32__decode_left_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: + { + drflac_read_pcm_frames_f32__decode_right_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: + { + drflac_read_pcm_frames_f32__decode_mid_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: + default: + { + drflac_read_pcm_frames_f32__decode_independent_stereo(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + } + } else { + /* Generic interleaving. */ + drflac_uint64 i; + for (i = 0; i < frameCountThisIteration; ++i) { + unsigned int j; + for (j = 0; j < channelCount; ++j) { + drflac_int32 sampleS32 = (drflac_int32)((drflac_uint32)(pFlac->currentFLACFrame.subframes[j].pSamplesS32[iFirstPCMFrame + i]) << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[j].wastedBitsPerSample)); + pBufferOut[(i*channelCount)+j] = (float)(sampleS32 / 2147483648.0); + } + } + } + + framesRead += frameCountThisIteration; + pBufferOut += frameCountThisIteration * channelCount; + framesToRead -= frameCountThisIteration; + pFlac->currentPCMFrame += frameCountThisIteration; + pFlac->currentFLACFrame.pcmFramesRemaining -= (unsigned int)frameCountThisIteration; + } + } + + return framesRead; +} + + +DRFLAC_API drflac_bool32 drflac_seek_to_pcm_frame(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + if (pFlac == NULL) { + return DRFLAC_FALSE; + } + + /* Don't do anything if we're already on the seek point. */ + if (pFlac->currentPCMFrame == pcmFrameIndex) { + return DRFLAC_TRUE; + } + + /* + If we don't know where the first frame begins then we can't seek. This will happen when the STREAMINFO block was not present + when the decoder was opened. + */ + if (pFlac->firstFLACFramePosInBytes == 0) { + return DRFLAC_FALSE; + } + + if (pcmFrameIndex == 0) { + pFlac->currentPCMFrame = 0; + return drflac__seek_to_first_frame(pFlac); + } else { + drflac_bool32 wasSuccessful = DRFLAC_FALSE; + drflac_uint64 originalPCMFrame = pFlac->currentPCMFrame; + + /* Clamp the sample to the end. */ + if (pcmFrameIndex > pFlac->totalPCMFrameCount) { + pcmFrameIndex = pFlac->totalPCMFrameCount; + } + + /* If the target sample and the current sample are in the same frame we just move the position forward. */ + if (drflac__is_current_flac_frame_valid(pFlac)) { + if (pcmFrameIndex > pFlac->currentPCMFrame) { + /* Forward. */ + drflac_uint32 offset = (drflac_uint32)(pcmFrameIndex - pFlac->currentPCMFrame); + if (pFlac->currentFLACFrame.pcmFramesRemaining > offset) { + pFlac->currentFLACFrame.pcmFramesRemaining -= offset; + pFlac->currentPCMFrame = pcmFrameIndex; + return DRFLAC_TRUE; + } + } else { + /* Backward. */ + drflac_uint32 offsetAbs = (drflac_uint32)(pFlac->currentPCMFrame - pcmFrameIndex); + drflac_uint32 currentFLACFramePCMFrameCount = pFlac->currentFLACFrame.header.blockSizeInPCMFrames; + drflac_uint32 currentFLACFramePCMFramesConsumed = currentFLACFramePCMFrameCount - pFlac->currentFLACFrame.pcmFramesRemaining; + if (currentFLACFramePCMFramesConsumed > offsetAbs) { + pFlac->currentFLACFrame.pcmFramesRemaining += offsetAbs; + pFlac->currentPCMFrame = pcmFrameIndex; + return DRFLAC_TRUE; + } + } + } + + /* + Different techniques depending on encapsulation. Using the native FLAC seektable with Ogg encapsulation is a bit awkward so + we'll instead use Ogg's natural seeking facility. + */ +#ifndef DR_FLAC_NO_OGG + if (pFlac->container == drflac_container_ogg) + { + wasSuccessful = drflac_ogg__seek_to_pcm_frame(pFlac, pcmFrameIndex); + } + else +#endif + { + /* First try seeking via the seek table. If this fails, fall back to a brute force seek which is much slower. */ + if (/*!wasSuccessful && */!pFlac->_noSeekTableSeek) { + wasSuccessful = drflac__seek_to_pcm_frame__seek_table(pFlac, pcmFrameIndex); + } + +#if !defined(DR_FLAC_NO_CRC) + /* Fall back to binary search if seek table seeking fails. This requires the length of the stream to be known. */ + if (!wasSuccessful && !pFlac->_noBinarySearchSeek && pFlac->totalPCMFrameCount > 0) { + wasSuccessful = drflac__seek_to_pcm_frame__binary_search(pFlac, pcmFrameIndex); + } +#endif + + /* Fall back to brute force if all else fails. */ + if (!wasSuccessful && !pFlac->_noBruteForceSeek) { + wasSuccessful = drflac__seek_to_pcm_frame__brute_force(pFlac, pcmFrameIndex); + } + } + + if (wasSuccessful) { + pFlac->currentPCMFrame = pcmFrameIndex; + } else { + /* Seek failed. Try putting the decoder back to it's original state. */ + if (drflac_seek_to_pcm_frame(pFlac, originalPCMFrame) == DRFLAC_FALSE) { + /* Failed to seek back to the original PCM frame. Fall back to 0. */ + drflac_seek_to_pcm_frame(pFlac, 0); + } + } + + return wasSuccessful; + } +} + + + +/* High Level APIs */ + +/* SIZE_MAX */ +#if defined(SIZE_MAX) + #define DRFLAC_SIZE_MAX SIZE_MAX +#else + #if defined(DRFLAC_64BIT) + #define DRFLAC_SIZE_MAX ((drflac_uint64)0xFFFFFFFFFFFFFFFF) + #else + #define DRFLAC_SIZE_MAX 0xFFFFFFFF + #endif +#endif +/* End SIZE_MAX */ + + +/* Using a macro as the definition of the drflac__full_decode_and_close_*() API family. Sue me. */ +#define DRFLAC_DEFINE_FULL_READ_AND_CLOSE(extension, type) \ +static type* drflac__full_read_and_close_ ## extension (drflac* pFlac, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut)\ +{ \ + type* pSampleData = NULL; \ + drflac_uint64 totalPCMFrameCount; \ + type buffer[4096]; \ + drflac_uint64 pcmFramesRead; \ + size_t sampleDataBufferSize = sizeof(buffer); \ + \ + DRFLAC_ASSERT(pFlac != NULL); \ + \ + totalPCMFrameCount = 0; \ + \ + pSampleData = (type*)drflac__malloc_from_callbacks(sampleDataBufferSize, &pFlac->allocationCallbacks); \ + if (pSampleData == NULL) { \ + goto on_error; \ + } \ + \ + while ((pcmFramesRead = (drflac_uint64)drflac_read_pcm_frames_##extension(pFlac, sizeof(buffer)/sizeof(buffer[0])/pFlac->channels, buffer)) > 0) { \ + if (((totalPCMFrameCount + pcmFramesRead) * pFlac->channels * sizeof(type)) > sampleDataBufferSize) { \ + type* pNewSampleData; \ + size_t newSampleDataBufferSize; \ + \ + newSampleDataBufferSize = sampleDataBufferSize * 2; \ + pNewSampleData = (type*)drflac__realloc_from_callbacks(pSampleData, newSampleDataBufferSize, sampleDataBufferSize, &pFlac->allocationCallbacks); \ + if (pNewSampleData == NULL) { \ + drflac__free_from_callbacks(pSampleData, &pFlac->allocationCallbacks); \ + goto on_error; \ + } \ + \ + sampleDataBufferSize = newSampleDataBufferSize; \ + pSampleData = pNewSampleData; \ + } \ + \ + DRFLAC_COPY_MEMORY(pSampleData + (totalPCMFrameCount*pFlac->channels), buffer, (size_t)(pcmFramesRead*pFlac->channels*sizeof(type))); \ + totalPCMFrameCount += pcmFramesRead; \ + } \ + \ + /* At this point everything should be decoded, but we just want to fill the unused part buffer with silence - need to \ + protect those ears from random noise! */ \ + DRFLAC_ZERO_MEMORY(pSampleData + (totalPCMFrameCount*pFlac->channels), (size_t)(sampleDataBufferSize - totalPCMFrameCount*pFlac->channels*sizeof(type))); \ + \ + if (sampleRateOut) *sampleRateOut = pFlac->sampleRate; \ + if (channelsOut) *channelsOut = pFlac->channels; \ + if (totalPCMFrameCountOut) *totalPCMFrameCountOut = totalPCMFrameCount; \ + \ + drflac_close(pFlac); \ + return pSampleData; \ + \ +on_error: \ + drflac_close(pFlac); \ + return NULL; \ +} + +DRFLAC_DEFINE_FULL_READ_AND_CLOSE(s32, drflac_int32) +DRFLAC_DEFINE_FULL_READ_AND_CLOSE(s16, drflac_int16) +DRFLAC_DEFINE_FULL_READ_AND_CLOSE(f32, float) + +DRFLAC_API drflac_int32* drflac_open_and_read_pcm_frames_s32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalPCMFrameCountOut) { + *totalPCMFrameCountOut = 0; + } + + pFlac = drflac_open(onRead, onSeek, onTell, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s32(pFlac, channelsOut, sampleRateOut, totalPCMFrameCountOut); +} + +DRFLAC_API drflac_int16* drflac_open_and_read_pcm_frames_s16(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalPCMFrameCountOut) { + *totalPCMFrameCountOut = 0; + } + + pFlac = drflac_open(onRead, onSeek, onTell, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s16(pFlac, channelsOut, sampleRateOut, totalPCMFrameCountOut); +} + +DRFLAC_API float* drflac_open_and_read_pcm_frames_f32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalPCMFrameCountOut) { + *totalPCMFrameCountOut = 0; + } + + pFlac = drflac_open(onRead, onSeek, onTell, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_f32(pFlac, channelsOut, sampleRateOut, totalPCMFrameCountOut); +} + +#ifndef DR_FLAC_NO_STDIO +DRFLAC_API drflac_int32* drflac_open_file_and_read_pcm_frames_s32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_file(filename, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s32(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API drflac_int16* drflac_open_file_and_read_pcm_frames_s16(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_file(filename, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s16(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API float* drflac_open_file_and_read_pcm_frames_f32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_file(filename, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_f32(pFlac, channels, sampleRate, totalPCMFrameCount); +} +#endif + +DRFLAC_API drflac_int32* drflac_open_memory_and_read_pcm_frames_s32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_memory(data, dataSize, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s32(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API drflac_int16* drflac_open_memory_and_read_pcm_frames_s16(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_memory(data, dataSize, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s16(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API float* drflac_open_memory_and_read_pcm_frames_f32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_memory(data, dataSize, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_f32(pFlac, channels, sampleRate, totalPCMFrameCount); +} + + +DRFLAC_API void drflac_free(void* p, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + drflac__free_from_callbacks(p, pAllocationCallbacks); + } else { + drflac__free_default(p, NULL); + } +} + + + + +DRFLAC_API void drflac_init_vorbis_comment_iterator(drflac_vorbis_comment_iterator* pIter, drflac_uint32 commentCount, const void* pComments) +{ + if (pIter == NULL) { + return; + } + + pIter->countRemaining = commentCount; + pIter->pRunningData = (const char*)pComments; +} + +DRFLAC_API const char* drflac_next_vorbis_comment(drflac_vorbis_comment_iterator* pIter, drflac_uint32* pCommentLengthOut) +{ + drflac_int32 length; + const char* pComment; + + /* Safety. */ + if (pCommentLengthOut) { + *pCommentLengthOut = 0; + } + + if (pIter == NULL || pIter->countRemaining == 0 || pIter->pRunningData == NULL) { + return NULL; + } + + length = drflac__le2host_32_ptr_unaligned(pIter->pRunningData); + pIter->pRunningData += 4; + + pComment = pIter->pRunningData; + pIter->pRunningData += length; + pIter->countRemaining -= 1; + + if (pCommentLengthOut) { + *pCommentLengthOut = length; + } + + return pComment; +} + + + + +DRFLAC_API void drflac_init_cuesheet_track_iterator(drflac_cuesheet_track_iterator* pIter, drflac_uint32 trackCount, const void* pTrackData) +{ + if (pIter == NULL) { + return; + } + + pIter->countRemaining = trackCount; + pIter->pRunningData = (const char*)pTrackData; +} + +DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterator* pIter, drflac_cuesheet_track* pCuesheetTrack) +{ + drflac_cuesheet_track cuesheetTrack; + const char* pRunningData; + drflac_uint64 offsetHi; + drflac_uint64 offsetLo; + + if (pIter == NULL || pIter->countRemaining == 0 || pIter->pRunningData == NULL) { + return DRFLAC_FALSE; + } + + pRunningData = pIter->pRunningData; + + offsetHi = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4; + offsetLo = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4; + cuesheetTrack.offset = offsetLo | (offsetHi << 32); + cuesheetTrack.trackNumber = pRunningData[0]; pRunningData += 1; + DRFLAC_COPY_MEMORY(cuesheetTrack.ISRC, pRunningData, sizeof(cuesheetTrack.ISRC)); pRunningData += 12; + cuesheetTrack.isAudio = (pRunningData[0] & 0x80) != 0; + cuesheetTrack.preEmphasis = (pRunningData[0] & 0x40) != 0; pRunningData += 14; + cuesheetTrack.indexCount = pRunningData[0]; pRunningData += 1; + cuesheetTrack.pIndexPoints = (const drflac_cuesheet_track_index*)pRunningData; pRunningData += cuesheetTrack.indexCount * sizeof(drflac_cuesheet_track_index); + + pIter->pRunningData = pRunningData; + pIter->countRemaining -= 1; + + if (pCuesheetTrack) { + *pCuesheetTrack = cuesheetTrack; + } + + return DRFLAC_TRUE; +} + +#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop +#endif +#endif /* dr_flac_c */ +#endif /* DR_FLAC_IMPLEMENTATION */ + + +/* +REVISION HISTORY +================ +v0.13.4 - TBD + - Add a bounds check when allocating memory during metadata processing. + - Fix a possible overflow error when parsing picture metadata. + - Fix an error with seek point parsing. + - Fix a possible deadlock when seeking. + - Fix an error where the decoder can be put into a bad state when seeking fails which then results in a crash when reading and seeking. + +v0.13.3 - 2026-01-17 + - Fix a compiler compatibility issue with some inlined assembly. + - Fix a compilation warning. + +v0.13.2 - 2025-12-02 + - Improve robustness of the parsing of picture metadata to improve support for memory constrained embedded devices. + - Fix a warning about an assigned by unused variable. + - Improvements to drflac_open_and_read_pcm_frames_*() and family to avoid excessively large memory allocations from malformed files. + +v0.13.1 - 2025-09-10 + - Fix an error with the NXDK build. + +v0.13.0 - 2025-07-23 + - API CHANGE: Seek origin enums have been renamed to match the naming convention used by other dr_libs libraries: + - drflac_seek_origin_start -> DRFLAC_SEEK_SET + - drflac_seek_origin_current -> DRFLAC_SEEK_CUR + - DRFLAC_SEEK_END (new) + - API CHANGE: A new seek origin has been added to allow seeking from the end of the file. If you implement your own `onSeek` callback, you should now detect and handle `DRFLAC_SEEK_END`. If seeking to the end is not supported, return `DRFLAC_FALSE`. If you only use `*_open_file()` or `*_open_memory()`, you need not change anything. + - API CHANGE: An `onTell` callback has been added to the following functions: + - drflac_open() + - drflac_open_relaxed() + - drflac_open_with_metadata() + - drflac_open_with_metadata_relaxed() + - drflac_open_and_read_pcm_frames_s32() + - drflac_open_and_read_pcm_frames_s16() + - drflac_open_and_read_pcm_frames_f32() + - Fix compilation for AIX OS. + +v0.12.43 - 2024-12-17 + - Fix a possible buffer overflow during decoding. + - Improve detection of ARM64EC + +v0.12.42 - 2023-11-02 + - Fix build for ARMv6-M. + - Fix a compilation warning with GCC. + +v0.12.41 - 2023-06-17 + - Fix an incorrect date in revision history. No functional change. + +v0.12.40 - 2023-05-22 + - Minor code restructure. No functional change. + +v0.12.39 - 2022-09-17 + - Fix compilation with DJGPP. + - Fix compilation error with Visual Studio 2019 and the ARM build. + - Fix an error with SSE 4.1 detection. + - Add support for disabling wchar_t with DR_WAV_NO_WCHAR. + - Improve compatibility with compilers which lack support for explicit struct packing. + - Improve compatibility with low-end and embedded hardware by reducing the amount of stack + allocation when loading an Ogg encapsulated file. + +v0.12.38 - 2022-04-10 + - Fix compilation error on older versions of GCC. + +v0.12.37 - 2022-02-12 + - Improve ARM detection. + +v0.12.36 - 2022-02-07 + - Fix a compilation error with the ARM build. + +v0.12.35 - 2022-02-06 + - Fix a bug due to underestimating the amount of precision required for the prediction stage. + - Fix some bugs found from fuzz testing. + +v0.12.34 - 2022-01-07 + - Fix some misalignment bugs when reading metadata. + +v0.12.33 - 2021-12-22 + - Fix a bug with seeking when the seek table does not start at PCM frame 0. + +v0.12.32 - 2021-12-11 + - Fix a warning with Clang. + +v0.12.31 - 2021-08-16 + - Silence some warnings. + +v0.12.30 - 2021-07-31 + - Fix platform detection for ARM64. + +v0.12.29 - 2021-04-02 + - Fix a bug where the running PCM frame index is set to an invalid value when over-seeking. + - Fix a decoding error due to an incorrect validation check. + +v0.12.28 - 2021-02-21 + - Fix a warning due to referencing _MSC_VER when it is undefined. + +v0.12.27 - 2021-01-31 + - Fix a static analysis warning. + +v0.12.26 - 2021-01-17 + - Fix a compilation warning due to _BSD_SOURCE being deprecated. + +v0.12.25 - 2020-12-26 + - Update documentation. + +v0.12.24 - 2020-11-29 + - Fix ARM64/NEON detection when compiling with MSVC. + +v0.12.23 - 2020-11-21 + - Fix compilation with OpenWatcom. + +v0.12.22 - 2020-11-01 + - Fix an error with the previous release. + +v0.12.21 - 2020-11-01 + - Fix a possible deadlock when seeking. + - Improve compiler support for older versions of GCC. + +v0.12.20 - 2020-09-08 + - Fix a compilation error on older compilers. + +v0.12.19 - 2020-08-30 + - Fix a bug due to an undefined 32-bit shift. + +v0.12.18 - 2020-08-14 + - Fix a crash when compiling with clang-cl. + +v0.12.17 - 2020-08-02 + - Simplify sized types. + +v0.12.16 - 2020-07-25 + - Fix a compilation warning. + +v0.12.15 - 2020-07-06 + - Check for negative LPC shifts and return an error. + +v0.12.14 - 2020-06-23 + - Add include guard for the implementation section. + +v0.12.13 - 2020-05-16 + - Add compile-time and run-time version querying. + - DRFLAC_VERSION_MINOR + - DRFLAC_VERSION_MAJOR + - DRFLAC_VERSION_REVISION + - DRFLAC_VERSION_STRING + - drflac_version() + - drflac_version_string() + +v0.12.12 - 2020-04-30 + - Fix compilation errors with VC6. + +v0.12.11 - 2020-04-19 + - Fix some pedantic warnings. + - Fix some undefined behaviour warnings. + +v0.12.10 - 2020-04-10 + - Fix some bugs when trying to seek with an invalid seek table. + +v0.12.9 - 2020-04-05 + - Fix warnings. + +v0.12.8 - 2020-04-04 + - Add drflac_open_file_w() and drflac_open_file_with_metadata_w(). + - Fix some static analysis warnings. + - Minor documentation updates. + +v0.12.7 - 2020-03-14 + - Fix compilation errors with VC6. + +v0.12.6 - 2020-03-07 + - Fix compilation error with Visual Studio .NET 2003. + +v0.12.5 - 2020-01-30 + - Silence some static analysis warnings. + +v0.12.4 - 2020-01-29 + - Silence some static analysis warnings. + +v0.12.3 - 2019-12-02 + - Fix some warnings when compiling with GCC and the -Og flag. + - Fix a crash in out-of-memory situations. + - Fix potential integer overflow bug. + - Fix some static analysis warnings. + - Fix a possible crash when using custom memory allocators without a custom realloc() implementation. + - Fix a bug with binary search seeking where the bits per sample is not a multiple of 8. + +v0.12.2 - 2019-10-07 + - Internal code clean up. + +v0.12.1 - 2019-09-29 + - Fix some Clang Static Analyzer warnings. + - Fix an unused variable warning. + +v0.12.0 - 2019-09-23 + - API CHANGE: Add support for user defined memory allocation routines. This system allows the program to specify their own memory allocation + routines with a user data pointer for client-specific contextual data. This adds an extra parameter to the end of the following APIs: + - drflac_open() + - drflac_open_relaxed() + - drflac_open_with_metadata() + - drflac_open_with_metadata_relaxed() + - drflac_open_file() + - drflac_open_file_with_metadata() + - drflac_open_memory() + - drflac_open_memory_with_metadata() + - drflac_open_and_read_pcm_frames_s32() + - drflac_open_and_read_pcm_frames_s16() + - drflac_open_and_read_pcm_frames_f32() + - drflac_open_file_and_read_pcm_frames_s32() + - drflac_open_file_and_read_pcm_frames_s16() + - drflac_open_file_and_read_pcm_frames_f32() + - drflac_open_memory_and_read_pcm_frames_s32() + - drflac_open_memory_and_read_pcm_frames_s16() + - drflac_open_memory_and_read_pcm_frames_f32() + Set this extra parameter to NULL to use defaults which is the same as the previous behaviour. Setting this NULL will use + DRFLAC_MALLOC, DRFLAC_REALLOC and DRFLAC_FREE. + - Remove deprecated APIs: + - drflac_read_s32() + - drflac_read_s16() + - drflac_read_f32() + - drflac_seek_to_sample() + - drflac_open_and_decode_s32() + - drflac_open_and_decode_s16() + - drflac_open_and_decode_f32() + - drflac_open_and_decode_file_s32() + - drflac_open_and_decode_file_s16() + - drflac_open_and_decode_file_f32() + - drflac_open_and_decode_memory_s32() + - drflac_open_and_decode_memory_s16() + - drflac_open_and_decode_memory_f32() + - Remove drflac.totalSampleCount which is now replaced with drflac.totalPCMFrameCount. You can emulate drflac.totalSampleCount + by doing pFlac->totalPCMFrameCount*pFlac->channels. + - Rename drflac.currentFrame to drflac.currentFLACFrame to remove ambiguity with PCM frames. + - Fix errors when seeking to the end of a stream. + - Optimizations to seeking. + - SSE improvements and optimizations. + - ARM NEON optimizations. + - Optimizations to drflac_read_pcm_frames_s16(). + - Optimizations to drflac_read_pcm_frames_s32(). + +v0.11.10 - 2019-06-26 + - Fix a compiler error. + +v0.11.9 - 2019-06-16 + - Silence some ThreadSanitizer warnings. + +v0.11.8 - 2019-05-21 + - Fix warnings. + +v0.11.7 - 2019-05-06 + - C89 fixes. + +v0.11.6 - 2019-05-05 + - Add support for C89. + - Fix a compiler warning when CRC is disabled. + - Change license to choice of public domain or MIT-0. + +v0.11.5 - 2019-04-19 + - Fix a compiler error with GCC. + +v0.11.4 - 2019-04-17 + - Fix some warnings with GCC when compiling with -std=c99. + +v0.11.3 - 2019-04-07 + - Silence warnings with GCC. + +v0.11.2 - 2019-03-10 + - Fix a warning. + +v0.11.1 - 2019-02-17 + - Fix a potential bug with seeking. + +v0.11.0 - 2018-12-16 + - API CHANGE: Deprecated drflac_read_s32(), drflac_read_s16() and drflac_read_f32() and replaced them with + drflac_read_pcm_frames_s32(), drflac_read_pcm_frames_s16() and drflac_read_pcm_frames_f32(). The new APIs take + and return PCM frame counts instead of sample counts. To upgrade you will need to change the input count by + dividing it by the channel count, and then do the same with the return value. + - API_CHANGE: Deprecated drflac_seek_to_sample() and replaced with drflac_seek_to_pcm_frame(). Same rules as + the changes to drflac_read_*() apply. + - API CHANGE: Deprecated drflac_open_and_decode_*() and replaced with drflac_open_*_and_read_*(). Same rules as + the changes to drflac_read_*() apply. + - Optimizations. + +v0.10.0 - 2018-09-11 + - Remove the DR_FLAC_NO_WIN32_IO option and the Win32 file IO functionality. If you need to use Win32 file IO you + need to do it yourself via the callback API. + - Fix the clang build. + - Fix undefined behavior. + - Fix errors with CUESHEET metdata blocks. + - Add an API for iterating over each cuesheet track in the CUESHEET metadata block. This works the same way as the + Vorbis comment API. + - Other miscellaneous bug fixes, mostly relating to invalid FLAC streams. + - Minor optimizations. + +v0.9.11 - 2018-08-29 + - Fix a bug with sample reconstruction. + +v0.9.10 - 2018-08-07 + - Improve 64-bit detection. + +v0.9.9 - 2018-08-05 + - Fix C++ build on older versions of GCC. + +v0.9.8 - 2018-07-24 + - Fix compilation errors. + +v0.9.7 - 2018-07-05 + - Fix a warning. + +v0.9.6 - 2018-06-29 + - Fix some typos. + +v0.9.5 - 2018-06-23 + - Fix some warnings. + +v0.9.4 - 2018-06-14 + - Optimizations to seeking. + - Clean up. + +v0.9.3 - 2018-05-22 + - Bug fix. + +v0.9.2 - 2018-05-12 + - Fix a compilation error due to a missing break statement. + +v0.9.1 - 2018-04-29 + - Fix compilation error with Clang. + +v0.9 - 2018-04-24 + - Fix Clang build. + - Start using major.minor.revision versioning. + +v0.8g - 2018-04-19 + - Fix build on non-x86/x64 architectures. + +v0.8f - 2018-02-02 + - Stop pretending to support changing rate/channels mid stream. + +v0.8e - 2018-02-01 + - Fix a crash when the block size of a frame is larger than the maximum block size defined by the FLAC stream. + - Fix a crash the the Rice partition order is invalid. + +v0.8d - 2017-09-22 + - Add support for decoding streams with ID3 tags. ID3 tags are just skipped. + +v0.8c - 2017-09-07 + - Fix warning on non-x86/x64 architectures. + +v0.8b - 2017-08-19 + - Fix build on non-x86/x64 architectures. + +v0.8a - 2017-08-13 + - A small optimization for the Clang build. + +v0.8 - 2017-08-12 + - API CHANGE: Rename dr_* types to drflac_*. + - Optimizations. This brings dr_flac back to about the same class of efficiency as the reference implementation. + - Add support for custom implementations of malloc(), realloc(), etc. + - Add CRC checking to Ogg encapsulated streams. + - Fix VC++ 6 build. This is only for the C++ compiler. The C compiler is not currently supported. + - Bug fixes. + +v0.7 - 2017-07-23 + - Add support for opening a stream without a header block. To do this, use drflac_open_relaxed() / drflac_open_with_metadata_relaxed(). + +v0.6 - 2017-07-22 + - Add support for recovering from invalid frames. With this change, dr_flac will simply skip over invalid frames as if they + never existed. Frames are checked against their sync code, the CRC-8 of the frame header and the CRC-16 of the whole frame. + +v0.5 - 2017-07-16 + - Fix typos. + - Change drflac_bool* types to unsigned. + - Add CRC checking. This makes dr_flac slower, but can be disabled with #define DR_FLAC_NO_CRC. + +v0.4f - 2017-03-10 + - Fix a couple of bugs with the bitstreaming code. + +v0.4e - 2017-02-17 + - Fix some warnings. + +v0.4d - 2016-12-26 + - Add support for 32-bit floating-point PCM decoding. + - Use drflac_int* and drflac_uint* sized types to improve compiler support. + - Minor improvements to documentation. + +v0.4c - 2016-12-26 + - Add support for signed 16-bit integer PCM decoding. + +v0.4b - 2016-10-23 + - A minor change to drflac_bool8 and drflac_bool32 types. + +v0.4a - 2016-10-11 + - Rename drBool32 to drflac_bool32 for styling consistency. + +v0.4 - 2016-09-29 + - API/ABI CHANGE: Use fixed size 32-bit booleans instead of the built-in bool type. + - API CHANGE: Rename drflac_open_and_decode*() to drflac_open_and_decode*_s32(). + - API CHANGE: Swap the order of "channels" and "sampleRate" parameters in drflac_open_and_decode*(). Rationale for this is to + keep it consistent with drflac_audio. + +v0.3f - 2016-09-21 + - Fix a warning with GCC. + +v0.3e - 2016-09-18 + - Fixed a bug where GCC 4.3+ was not getting properly identified. + - Fixed a few typos. + - Changed date formats to ISO 8601 (YYYY-MM-DD). + +v0.3d - 2016-06-11 + - Minor clean up. + +v0.3c - 2016-05-28 + - Fixed compilation error. + +v0.3b - 2016-05-16 + - Fixed Linux/GCC build. + - Updated documentation. + +v0.3a - 2016-05-15 + - Minor fixes to documentation. + +v0.3 - 2016-05-11 + - Optimizations. Now at about parity with the reference implementation on 32-bit builds. + - Lots of clean up. + +v0.2b - 2016-05-10 + - Bug fixes. + +v0.2a - 2016-05-10 + - Made drflac_open_and_decode() more robust. + - Removed an unused debugging variable + +v0.2 - 2016-05-09 + - Added support for Ogg encapsulation. + - API CHANGE. Have the onSeek callback take a third argument which specifies whether or not the seek + should be relative to the start or the current position. Also changes the seeking rules such that + seeking offsets will never be negative. + - Have drflac_open_and_decode() fail gracefully if the stream has an unknown total sample count. + +v0.1b - 2016-05-07 + - Properly close the file handle in drflac_open_file() and family when the decoder fails to initialize. + - Removed a stale comment. + +v0.1a - 2016-05-05 + - Minor formatting changes. + - Fixed a warning on the GCC build. + +v0.1 - 2016-05-03 + - Initial versioned release. +*/ + +/* +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2023 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ diff --git a/native/vendor/dr_libs/dr_mp3.h b/native/vendor/dr_libs/dr_mp3.h new file mode 100644 index 0000000..a631ae4 --- /dev/null +++ b/native/vendor/dr_libs/dr_mp3.h @@ -0,0 +1,5430 @@ +/* +MP3 audio decoder. Choice of public domain or MIT-0. See license statements at the end of this file. +dr_mp3 - v0.7.4 - TBD + +David Reid - mackron@gmail.com + +GitHub: https://github.com/mackron/dr_libs + +Based on minimp3 (https://github.com/lieff/minimp3) which is where the real work was done. See the bottom of this file for differences between minimp3 and dr_mp3. +*/ + +/* +Introduction +============= +dr_mp3 is a single file library. To use it, do something like the following in one .c file. + + ```c + #define DR_MP3_IMPLEMENTATION + #include "dr_mp3.h" + ``` + +You can then #include this file in other parts of the program as you would with any other header file. To decode audio data, do something like the following: + + ```c + drmp3 mp3; + if (!drmp3_init_file(&mp3, "MySong.mp3", NULL)) { + // Failed to open file + } + + ... + + drmp3_uint64 framesRead = drmp3_read_pcm_frames_f32(pMP3, framesToRead, pFrames); + ``` + +The drmp3 object is transparent so you can get access to the channel count and sample rate like so: + + ``` + drmp3_uint32 channels = mp3.channels; + drmp3_uint32 sampleRate = mp3.sampleRate; + ``` + +The example above initializes a decoder from a file, but you can also initialize it from a block of memory and read and seek callbacks with +`drmp3_init_memory()` and `drmp3_init()` respectively. + +You do not need to do any annoying memory management when reading PCM frames - this is all managed internally. You can request any number of PCM frames in each +call to `drmp3_read_pcm_frames_f32()` and it will return as many PCM frames as it can, up to the requested amount. + +You can also decode an entire file in one go with `drmp3_open_and_read_pcm_frames_f32()`, `drmp3_open_memory_and_read_pcm_frames_f32()` and +`drmp3_open_file_and_read_pcm_frames_f32()`. + + +Build Options +============= +#define these options before including this file. + +#define DR_MP3_NO_STDIO + Disable drmp3_init_file(), etc. + +#define DR_MP3_NO_SIMD + Disable SIMD optimizations. +*/ + +#ifndef dr_mp3_h +#define dr_mp3_h + +#ifdef __cplusplus +extern "C" { +#endif + +#define DRMP3_STRINGIFY(x) #x +#define DRMP3_XSTRINGIFY(x) DRMP3_STRINGIFY(x) + +#define DRMP3_VERSION_MAJOR 0 +#define DRMP3_VERSION_MINOR 7 +#define DRMP3_VERSION_REVISION 4 +#define DRMP3_VERSION_STRING DRMP3_XSTRINGIFY(DRMP3_VERSION_MAJOR) "." DRMP3_XSTRINGIFY(DRMP3_VERSION_MINOR) "." DRMP3_XSTRINGIFY(DRMP3_VERSION_REVISION) + +#include /* For size_t. */ + +/* Sized Types */ +typedef signed char drmp3_int8; +typedef unsigned char drmp3_uint8; +typedef signed short drmp3_int16; +typedef unsigned short drmp3_uint16; +typedef signed int drmp3_int32; +typedef unsigned int drmp3_uint32; +#if defined(_MSC_VER) && !defined(__clang__) + typedef signed __int64 drmp3_int64; + typedef unsigned __int64 drmp3_uint64; +#else + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wlong-long" + #if defined(__clang__) + #pragma GCC diagnostic ignored "-Wc++11-long-long" + #endif + #endif + typedef signed long long drmp3_int64; + typedef unsigned long long drmp3_uint64; + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop + #endif +#endif +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) || defined(__powerpc64__) + typedef drmp3_uint64 drmp3_uintptr; +#else + typedef drmp3_uint32 drmp3_uintptr; +#endif +typedef drmp3_uint8 drmp3_bool8; +typedef drmp3_uint32 drmp3_bool32; +#define DRMP3_TRUE 1 +#define DRMP3_FALSE 0 + +/* Weird shifting syntax is for VC6 compatibility. */ +#define DRMP3_UINT64_MAX (((drmp3_uint64)0xFFFFFFFF << 32) | (drmp3_uint64)0xFFFFFFFF) +/* End Sized Types */ + +/* Decorations */ +#if !defined(DRMP3_API) + #if defined(DRMP3_DLL) + #if defined(_WIN32) + #define DRMP3_DLL_IMPORT __declspec(dllimport) + #define DRMP3_DLL_EXPORT __declspec(dllexport) + #define DRMP3_DLL_PRIVATE static + #else + #if defined(__GNUC__) && __GNUC__ >= 4 + #define DRMP3_DLL_IMPORT __attribute__((visibility("default"))) + #define DRMP3_DLL_EXPORT __attribute__((visibility("default"))) + #define DRMP3_DLL_PRIVATE __attribute__((visibility("hidden"))) + #else + #define DRMP3_DLL_IMPORT + #define DRMP3_DLL_EXPORT + #define DRMP3_DLL_PRIVATE static + #endif + #endif + + #if defined(DR_MP3_IMPLEMENTATION) + #define DRMP3_API DRMP3_DLL_EXPORT + #else + #define DRMP3_API DRMP3_DLL_IMPORT + #endif + #define DRMP3_PRIVATE DRMP3_DLL_PRIVATE + #else + #define DRMP3_API extern + #define DRMP3_PRIVATE static + #endif +#endif +/* End Decorations */ + +/* Result Codes */ +typedef drmp3_int32 drmp3_result; +#define DRMP3_SUCCESS 0 +#define DRMP3_ERROR -1 /* A generic error. */ +#define DRMP3_INVALID_ARGS -2 +#define DRMP3_INVALID_OPERATION -3 +#define DRMP3_OUT_OF_MEMORY -4 +#define DRMP3_OUT_OF_RANGE -5 +#define DRMP3_ACCESS_DENIED -6 +#define DRMP3_DOES_NOT_EXIST -7 +#define DRMP3_ALREADY_EXISTS -8 +#define DRMP3_TOO_MANY_OPEN_FILES -9 +#define DRMP3_INVALID_FILE -10 +#define DRMP3_TOO_BIG -11 +#define DRMP3_PATH_TOO_LONG -12 +#define DRMP3_NAME_TOO_LONG -13 +#define DRMP3_NOT_DIRECTORY -14 +#define DRMP3_IS_DIRECTORY -15 +#define DRMP3_DIRECTORY_NOT_EMPTY -16 +#define DRMP3_END_OF_FILE -17 +#define DRMP3_NO_SPACE -18 +#define DRMP3_BUSY -19 +#define DRMP3_IO_ERROR -20 +#define DRMP3_INTERRUPT -21 +#define DRMP3_UNAVAILABLE -22 +#define DRMP3_ALREADY_IN_USE -23 +#define DRMP3_BAD_ADDRESS -24 +#define DRMP3_BAD_SEEK -25 +#define DRMP3_BAD_PIPE -26 +#define DRMP3_DEADLOCK -27 +#define DRMP3_TOO_MANY_LINKS -28 +#define DRMP3_NOT_IMPLEMENTED -29 +#define DRMP3_NO_MESSAGE -30 +#define DRMP3_BAD_MESSAGE -31 +#define DRMP3_NO_DATA_AVAILABLE -32 +#define DRMP3_INVALID_DATA -33 +#define DRMP3_TIMEOUT -34 +#define DRMP3_NO_NETWORK -35 +#define DRMP3_NOT_UNIQUE -36 +#define DRMP3_NOT_SOCKET -37 +#define DRMP3_NO_ADDRESS -38 +#define DRMP3_BAD_PROTOCOL -39 +#define DRMP3_PROTOCOL_UNAVAILABLE -40 +#define DRMP3_PROTOCOL_NOT_SUPPORTED -41 +#define DRMP3_PROTOCOL_FAMILY_NOT_SUPPORTED -42 +#define DRMP3_ADDRESS_FAMILY_NOT_SUPPORTED -43 +#define DRMP3_SOCKET_NOT_SUPPORTED -44 +#define DRMP3_CONNECTION_RESET -45 +#define DRMP3_ALREADY_CONNECTED -46 +#define DRMP3_NOT_CONNECTED -47 +#define DRMP3_CONNECTION_REFUSED -48 +#define DRMP3_NO_HOST -49 +#define DRMP3_IN_PROGRESS -50 +#define DRMP3_CANCELLED -51 +#define DRMP3_MEMORY_ALREADY_MAPPED -52 +#define DRMP3_AT_END -53 +/* End Result Codes */ + +#define DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME 1152 +#define DRMP3_MAX_SAMPLES_PER_FRAME (DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME*2) + +/* Inline */ +#ifdef _MSC_VER + #define DRMP3_INLINE __forceinline +#elif defined(__GNUC__) + /* + I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when + the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some + case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the + command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue + I am using "__inline__" only when we're compiling in strict ANSI mode. + */ + #if defined(__STRICT_ANSI__) + #define DRMP3_GNUC_INLINE_HINT __inline__ + #else + #define DRMP3_GNUC_INLINE_HINT inline + #endif + + #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) + #define DRMP3_INLINE DRMP3_GNUC_INLINE_HINT __attribute__((always_inline)) + #else + #define DRMP3_INLINE DRMP3_GNUC_INLINE_HINT + #endif +#elif defined(__WATCOMC__) + #define DRMP3_INLINE __inline +#else + #define DRMP3_INLINE +#endif +/* End Inline */ + + +DRMP3_API void drmp3_version(drmp3_uint32* pMajor, drmp3_uint32* pMinor, drmp3_uint32* pRevision); +DRMP3_API const char* drmp3_version_string(void); + + +/* Allocation Callbacks */ +typedef struct +{ + void* pUserData; + void* (* onMalloc)(size_t sz, void* pUserData); + void* (* onRealloc)(void* p, size_t sz, void* pUserData); + void (* onFree)(void* p, void* pUserData); +} drmp3_allocation_callbacks; +/* End Allocation Callbacks */ + + +/* +Low Level Push API +================== +*/ +#define DRMP3_MAX_BITRESERVOIR_BYTES 511 +#define DRMP3_MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#define DRMP3_MAX_L3_FRAME_PAYLOAD_BYTES DRMP3_MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +typedef struct +{ + int frame_bytes, channels, sample_rate, layer, bitrate_kbps; +} drmp3dec_frame_info; + +typedef struct +{ + const drmp3_uint8 *buf; + int pos, limit; +} drmp3_bs; + +typedef struct +{ + const drmp3_uint8 *sfbtab; + drmp3_uint16 part_23_length, big_values, scalefac_compress; + drmp3_uint8 global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + drmp3_uint8 table_select[3], region_count[3], subblock_gain[3]; + drmp3_uint8 preflag, scalefac_scale, count1_table, scfsi; +} drmp3_L3_gr_info; + +typedef struct +{ + drmp3_bs bs; + drmp3_uint8 maindata[DRMP3_MAX_BITRESERVOIR_BYTES + DRMP3_MAX_L3_FRAME_PAYLOAD_BYTES]; + drmp3_L3_gr_info gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + drmp3_uint8 ist_pos[2][39]; +} drmp3dec_scratch; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + drmp3_uint8 header[4], reserv_buf[511]; + drmp3dec_scratch scratch; +} drmp3dec; + +/* Initializes a low level decoder. */ +DRMP3_API void drmp3dec_init(drmp3dec *dec); + +/* Reads a frame from a low level decoder. */ +DRMP3_API int drmp3dec_decode_frame(drmp3dec *dec, const drmp3_uint8 *mp3, int mp3_bytes, void *pcm, drmp3dec_frame_info *info); + +/* Helper for converting between f32 and s16. */ +DRMP3_API void drmp3dec_f32_to_s16(const float *in, drmp3_int16 *out, size_t num_samples); + + + +/* +Main API (Pull API) +=================== +*/ +typedef enum +{ + DRMP3_SEEK_SET, + DRMP3_SEEK_CUR, + DRMP3_SEEK_END +} drmp3_seek_origin; + +typedef struct +{ + drmp3_uint64 seekPosInBytes; /* Points to the first byte of an MP3 frame. */ + drmp3_uint64 pcmFrameIndex; /* The index of the PCM frame this seek point targets. */ + drmp3_uint16 mp3FramesToDiscard; /* The number of whole MP3 frames to be discarded before pcmFramesToDiscard. */ + drmp3_uint16 pcmFramesToDiscard; /* The number of leading samples to read and discard. These are discarded after mp3FramesToDiscard. */ +} drmp3_seek_point; + +typedef enum +{ + DRMP3_METADATA_TYPE_ID3V1, + DRMP3_METADATA_TYPE_ID3V2, + DRMP3_METADATA_TYPE_APE, + DRMP3_METADATA_TYPE_XING, + DRMP3_METADATA_TYPE_VBRI +} drmp3_metadata_type; + +typedef struct +{ + drmp3_metadata_type type; + const void* pRawData; /* A pointer to the raw data. */ + size_t rawDataSize; +} drmp3_metadata; + + +/* +Callback for when data is read. Return value is the number of bytes actually read. + +pUserData [in] The user data that was passed to drmp3_init(), and family. +pBufferOut [out] The output buffer. +bytesToRead [in] The number of bytes to read. + +Returns the number of bytes actually read. + +A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback until +either the entire bytesToRead is filled or you have reached the end of the stream. +*/ +typedef size_t (* drmp3_read_proc)(void* pUserData, void* pBufferOut, size_t bytesToRead); + +/* +Callback for when data needs to be seeked. + +pUserData [in] The user data that was passed to drmp3_init(), and family. +offset [in] The number of bytes to move, relative to the origin. Can be negative. +origin [in] The origin of the seek. + +Returns whether or not the seek was successful. +*/ +typedef drmp3_bool32 (* drmp3_seek_proc)(void* pUserData, int offset, drmp3_seek_origin origin); + +/* +Callback for retrieving the current cursor position. + +pUserData [in] The user data that was passed to drmp3_init(), and family. +pCursor [out] The cursor position in bytes from the start of the stream. + +Returns whether or not the cursor position was successfully retrieved. +*/ +typedef drmp3_bool32 (* drmp3_tell_proc)(void* pUserData, drmp3_int64* pCursor); + + +/* +Callback for when metadata is read. + +Only the raw data is provided. The client is responsible for parsing the contents of the data themsevles. +*/ +typedef void (* drmp3_meta_proc)(void* pUserData, const drmp3_metadata* pMetadata); + + +typedef struct +{ + drmp3_uint32 channels; + drmp3_uint32 sampleRate; +} drmp3_config; + +typedef struct +{ + drmp3dec decoder; + drmp3_uint32 channels; + drmp3_uint32 sampleRate; + drmp3_read_proc onRead; + drmp3_seek_proc onSeek; + drmp3_meta_proc onMeta; + void* pUserData; + void* pUserDataMeta; + drmp3_allocation_callbacks allocationCallbacks; + drmp3_uint32 mp3FrameChannels; /* The number of channels in the currently loaded MP3 frame. Internal use only. */ + drmp3_uint32 mp3FrameSampleRate; /* The sample rate of the currently loaded MP3 frame. Internal use only. */ + drmp3_uint32 pcmFramesConsumedInMP3Frame; + drmp3_uint32 pcmFramesRemainingInMP3Frame; + drmp3_uint8 pcmFrames[sizeof(float)*DRMP3_MAX_SAMPLES_PER_FRAME]; /* <-- Multipled by sizeof(float) to ensure there's enough room for DR_MP3_FLOAT_OUTPUT. */ + drmp3_uint64 currentPCMFrame; /* The current PCM frame, globally. */ + drmp3_uint64 streamCursor; /* The current byte the decoder is sitting on in the raw stream. */ + drmp3_uint64 streamLength; /* The length of the stream in bytes. dr_mp3 will not read beyond this. If a ID3v1 or APE tag is present, this will be set to the first byte of the tag. */ + drmp3_uint64 streamStartOffset; /* The offset of the start of the MP3 data. This is used for skipping ID3v2 and VBR tags. */ + drmp3_seek_point* pSeekPoints; /* NULL by default. Set with drmp3_bind_seek_table(). Memory is owned by the client. dr_mp3 will never attempt to free this pointer. */ + drmp3_uint32 seekPointCount; /* The number of items in pSeekPoints. When set to 0 assumes to no seek table. Defaults to zero. */ + drmp3_uint32 delayInPCMFrames; + drmp3_uint32 paddingInPCMFrames; + drmp3_uint64 totalPCMFrameCount; /* Set to DRMP3_UINT64_MAX if the length is unknown. Includes delay and padding. */ + drmp3_bool32 isVBR; + drmp3_bool32 isCBR; + size_t dataSize; + size_t dataCapacity; + size_t dataConsumed; + drmp3_uint8* pData; + drmp3_bool32 atEnd; + struct + { + const drmp3_uint8* pData; + size_t dataSize; + size_t currentReadPos; + } memory; /* Only used for decoders that were opened against a block of memory. */ +} drmp3; + +/* +Initializes an MP3 decoder. + +onRead [in] The function to call when data needs to be read from the client. +onSeek [in] The function to call when the read position of the client data needs to move. +onTell [in] The function to call when the read position of the client data needs to be retrieved. +pUserData [in, optional] A pointer to application defined data that will be passed to onRead and onSeek. + +Returns true if successful; false otherwise. + +Close the loader with drmp3_uninit(). + +See also: drmp3_init_file(), drmp3_init_memory(), drmp3_uninit() +*/ +DRMP3_API drmp3_bool32 drmp3_init(drmp3* pMP3, drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, drmp3_meta_proc onMeta, void* pUserData, const drmp3_allocation_callbacks* pAllocationCallbacks); + +/* +Initializes an MP3 decoder from a block of memory. + +This does not create a copy of the data. It is up to the application to ensure the buffer remains valid for +the lifetime of the drmp3 object. + +The buffer should contain the contents of the entire MP3 file. +*/ +DRMP3_API drmp3_bool32 drmp3_init_memory_with_metadata(drmp3* pMP3, const void* pData, size_t dataSize, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_bool32 drmp3_init_memory(drmp3* pMP3, const void* pData, size_t dataSize, const drmp3_allocation_callbacks* pAllocationCallbacks); + +#ifndef DR_MP3_NO_STDIO +/* +Initializes an MP3 decoder from a file. + +This holds the internal FILE object until drmp3_uninit() is called. Keep this in mind if you're caching drmp3 +objects because the operating system may restrict the number of file handles an application can have open at +any given time. +*/ +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata(drmp3* pMP3, const char* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata_w(drmp3* pMP3, const wchar_t* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks); + +DRMP3_API drmp3_bool32 drmp3_init_file(drmp3* pMP3, const char* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_bool32 drmp3_init_file_w(drmp3* pMP3, const wchar_t* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks); +#endif + +/* +Uninitializes an MP3 decoder. +*/ +DRMP3_API void drmp3_uninit(drmp3* pMP3); + +/* +Reads PCM frames as interleaved 32-bit IEEE floating point PCM. + +Note that framesToRead specifies the number of PCM frames to read, _not_ the number of MP3 frames. +*/ +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut); + +/* +Reads PCM frames as interleaved signed 16-bit integer PCM. + +Note that framesToRead specifies the number of PCM frames to read, _not_ the number of MP3 frames. +*/ +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_s16(drmp3* pMP3, drmp3_uint64 framesToRead, drmp3_int16* pBufferOut); + +/* +Seeks to a specific frame. + +Note that this is _not_ an MP3 frame, but rather a PCM frame. +*/ +DRMP3_API drmp3_bool32 drmp3_seek_to_pcm_frame(drmp3* pMP3, drmp3_uint64 frameIndex); + +/* +Calculates the total number of PCM frames in the MP3 stream. Cannot be used for infinite streams such as internet +radio. Runs in linear time. Returns 0 on error. +*/ +DRMP3_API drmp3_uint64 drmp3_get_pcm_frame_count(drmp3* pMP3); + +/* +Calculates the total number of MP3 frames in the MP3 stream. Cannot be used for infinite streams such as internet +radio. Runs in linear time. Returns 0 on error. +*/ +DRMP3_API drmp3_uint64 drmp3_get_mp3_frame_count(drmp3* pMP3); + +/* +Calculates the total number of MP3 and PCM frames in the MP3 stream. Cannot be used for infinite streams such as internet +radio. Runs in linear time. Returns 0 on error. + +This is equivalent to calling drmp3_get_mp3_frame_count() and drmp3_get_pcm_frame_count() except that it's more efficient. +*/ +DRMP3_API drmp3_bool32 drmp3_get_mp3_and_pcm_frame_count(drmp3* pMP3, drmp3_uint64* pMP3FrameCount, drmp3_uint64* pPCMFrameCount); + +/* +Calculates the seekpoints based on PCM frames. This is slow. + +pSeekpoint count is a pointer to a uint32 containing the seekpoint count. On input it contains the desired count. +On output it contains the actual count. The reason for this design is that the client may request too many +seekpoints, in which case dr_mp3 will return a corrected count. + +Note that seektable seeking is not quite sample exact when the MP3 stream contains inconsistent sample rates. +*/ +DRMP3_API drmp3_bool32 drmp3_calculate_seek_points(drmp3* pMP3, drmp3_uint32* pSeekPointCount, drmp3_seek_point* pSeekPoints); + +/* +Binds a seek table to the decoder. + +This does _not_ make a copy of pSeekPoints - it only references it. It is up to the application to ensure this +remains valid while it is bound to the decoder. + +Use drmp3_calculate_seek_points() to calculate the seek points. +*/ +DRMP3_API drmp3_bool32 drmp3_bind_seek_table(drmp3* pMP3, drmp3_uint32 seekPointCount, drmp3_seek_point* pSeekPoints); + + +/* +Opens an decodes an entire MP3 stream as a single operation. + +On output pConfig will receive the channel count and sample rate of the stream. + +Free the returned pointer with drmp3_free(). +*/ +DRMP3_API float* drmp3_open_and_read_pcm_frames_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_int16* drmp3_open_and_read_pcm_frames_s16(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); + +DRMP3_API float* drmp3_open_memory_and_read_pcm_frames_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_int16* drmp3_open_memory_and_read_pcm_frames_s16(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); + +#ifndef DR_MP3_NO_STDIO +DRMP3_API float* drmp3_open_file_and_read_pcm_frames_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_int16* drmp3_open_file_and_read_pcm_frames_s16(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +#endif + +/* +Allocates a block of memory on the heap. +*/ +DRMP3_API void* drmp3_malloc(size_t sz, const drmp3_allocation_callbacks* pAllocationCallbacks); + +/* +Frees any memory that was allocated by a public drmp3 API. +*/ +DRMP3_API void drmp3_free(void* p, const drmp3_allocation_callbacks* pAllocationCallbacks); + +#ifdef __cplusplus +} +#endif +#endif /* dr_mp3_h */ + + +/************************************************************************************************************************************************************ + ************************************************************************************************************************************************************ + + IMPLEMENTATION + + ************************************************************************************************************************************************************ + ************************************************************************************************************************************************************/ +#if defined(DR_MP3_IMPLEMENTATION) +#ifndef dr_mp3_c +#define dr_mp3_c + +#include +#include +#include /* For INT_MAX */ + +DRMP3_API void drmp3_version(drmp3_uint32* pMajor, drmp3_uint32* pMinor, drmp3_uint32* pRevision) +{ + if (pMajor) { + *pMajor = DRMP3_VERSION_MAJOR; + } + + if (pMinor) { + *pMinor = DRMP3_VERSION_MINOR; + } + + if (pRevision) { + *pRevision = DRMP3_VERSION_REVISION; + } +} + +DRMP3_API const char* drmp3_version_string(void) +{ + return DRMP3_VERSION_STRING; +} + +/* Disable SIMD when compiling with TCC for now. */ +#if defined(__TINYC__) +#define DR_MP3_NO_SIMD +#endif + +#define DRMP3_OFFSET_PTR(p, offset) ((void*)((drmp3_uint8*)(p) + (offset))) + +#ifndef DRMP3_MAX_FRAME_SYNC_MATCHES +#define DRMP3_MAX_FRAME_SYNC_MATCHES 10 +#endif + +#define DRMP3_SHORT_BLOCK_TYPE 2 +#define DRMP3_STOP_BLOCK_TYPE 3 +#define DRMP3_MODE_MONO 3 +#define DRMP3_MODE_JOINT_STEREO 1 +#define DRMP3_HDR_SIZE 4 +#define DRMP3_HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define DRMP3_HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define DRMP3_HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define DRMP3_HDR_IS_CRC(h) (!((h[1]) & 1)) +#define DRMP3_HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define DRMP3_HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define DRMP3_HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define DRMP3_HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define DRMP3_HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define DRMP3_HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define DRMP3_HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define DRMP3_HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define DRMP3_HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define DRMP3_HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define DRMP3_HDR_GET_MY_SAMPLE_RATE(h) (DRMP3_HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define DRMP3_HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define DRMP3_HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define DRMP3_BITS_DEQUANTIZER_OUT -1 +#define DRMP3_MAX_SCF (255 + DRMP3_BITS_DEQUANTIZER_OUT*4 - 210) +#define DRMP3_MAX_SCFI ((DRMP3_MAX_SCF + 3) & ~3) + +#define DRMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define DRMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(DR_MP3_NO_SIMD) + +#if !defined(DR_MP3_ONLY_SIMD) && ((defined(_MSC_VER) && _MSC_VER >= 1400) && defined(_M_X64)) || ((defined(__i386) || defined(_M_IX86) || defined(__i386__) || defined(__x86_64__)) && ((defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__))) +#define DR_MP3_ONLY_SIMD +#endif +#if !defined(DR_MP3_ONLY_SIMD) && (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC)) +#define DR_MP3_ONLY_SIMD +#endif + +#if ((defined(_MSC_VER) && _MSC_VER >= 1400) && defined(_M_X64)) || ((defined(__i386) || defined(_M_IX86) || defined(__i386__) || defined(__x86_64__)) && ((defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__))) +#if defined(_MSC_VER) +#include +#endif +#include +#define DRMP3_HAVE_SSE 1 +#define DRMP3_HAVE_SIMD 1 +#define DRMP3_VSTORE _mm_storeu_ps +#define DRMP3_VLD _mm_loadu_ps +#define DRMP3_VSET _mm_set1_ps +#define DRMP3_VADD _mm_add_ps +#define DRMP3_VSUB _mm_sub_ps +#define DRMP3_VMUL _mm_mul_ps +#define DRMP3_VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define DRMP3_VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define DRMP3_VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define DRMP3_VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 drmp3_f4; +#if (defined(_MSC_VER) || defined(DR_MP3_ONLY_SIMD)) && !defined(__clang__) +#define drmp3_cpuid __cpuid +#else +static __inline__ __attribute__((always_inline)) void drmp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif +} +#endif +static int drmp3_have_simd(void) +{ +#ifdef DR_MP3_ONLY_SIMD + return 1; +#else + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif + if (g_have_simd) + goto end; + drmp3_cpuid(CPUInfo, 0); + if (CPUInfo[0] > 0) + { + drmp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + return g_have_simd - 1; + } + +end: + return g_have_simd - 1; +#endif +} +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) +#include +#define DRMP3_HAVE_SSE 0 +#define DRMP3_HAVE_SIMD 1 +#define DRMP3_VSTORE vst1q_f32 +#define DRMP3_VLD vld1q_f32 +#define DRMP3_VSET vmovq_n_f32 +#define DRMP3_VADD vaddq_f32 +#define DRMP3_VSUB vsubq_f32 +#define DRMP3_VMUL vmulq_f32 +#define DRMP3_VMAC(a, x, y) vmlaq_f32(a, x, y) +#define DRMP3_VMSB(a, x, y) vmlsq_f32(a, x, y) +#define DRMP3_VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define DRMP3_VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t drmp3_f4; +static int drmp3_have_simd(void) +{ /* TODO: detect neon for !DR_MP3_ONLY_SIMD */ + return 1; +} +#else +#define DRMP3_HAVE_SSE 0 +#define DRMP3_HAVE_SIMD 0 +#ifdef DR_MP3_ONLY_SIMD +#error DR_MP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif +#endif + +#else + +#define DRMP3_HAVE_SIMD 0 + +#endif + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(_M_ARM64EC) && !defined(__ARM_ARCH_6M__) +#define DRMP3_HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) drmp3_int32 drmp3_clip_int16_arm(drmp3_int32 a) +{ + drmp3_int32 x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#else +#define DRMP3_HAVE_ARMV6 0 +#endif + + +/* Standard library stuff. */ +#ifndef DRMP3_ASSERT +#include +#define DRMP3_ASSERT(expression) assert(expression) +#endif +#ifndef DRMP3_COPY_MEMORY +#define DRMP3_COPY_MEMORY(dst, src, sz) memcpy((dst), (src), (sz)) +#endif +#ifndef DRMP3_MOVE_MEMORY +#define DRMP3_MOVE_MEMORY(dst, src, sz) memmove((dst), (src), (sz)) +#endif +#ifndef DRMP3_ZERO_MEMORY +#define DRMP3_ZERO_MEMORY(p, sz) memset((p), 0, (sz)) +#endif +#define DRMP3_ZERO_OBJECT(p) DRMP3_ZERO_MEMORY((p), sizeof(*(p))) +#ifndef DRMP3_MALLOC +#define DRMP3_MALLOC(sz) malloc((sz)) +#endif +#ifndef DRMP3_REALLOC +#define DRMP3_REALLOC(p, sz) realloc((p), (sz)) +#endif +#ifndef DRMP3_FREE +#define DRMP3_FREE(p) free((p)) +#endif + + + +typedef struct +{ + float scf[3*64]; + drmp3_uint8 total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} drmp3_L12_scale_info; + +typedef struct +{ + drmp3_uint8 tab_offset, code_tab_width, band_count; +} drmp3_L12_subband_alloc; + +static void drmp3_bs_init(drmp3_bs *bs, const drmp3_uint8 *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static drmp3_uint32 drmp3_bs_get_bits(drmp3_bs *bs, int n) +{ + drmp3_uint32 next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const drmp3_uint8 *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int drmp3_hdr_valid(const drmp3_uint8 *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (DRMP3_HDR_GET_LAYER(h) != 0) && + (DRMP3_HDR_GET_BITRATE(h) != 15) && + (DRMP3_HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int drmp3_hdr_compare(const drmp3_uint8 *h1, const drmp3_uint8 *h2) +{ + return drmp3_hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(DRMP3_HDR_IS_FREE_FORMAT(h1) ^ DRMP3_HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned drmp3_hdr_bitrate_kbps(const drmp3_uint8 *h) +{ + static const drmp3_uint8 halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!DRMP3_HDR_TEST_MPEG1(h)][DRMP3_HDR_GET_LAYER(h) - 1][DRMP3_HDR_GET_BITRATE(h)]; +} + +static unsigned drmp3_hdr_sample_rate_hz(const drmp3_uint8 *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[DRMP3_HDR_GET_SAMPLE_RATE(h)] >> (int)!DRMP3_HDR_TEST_MPEG1(h) >> (int)!DRMP3_HDR_TEST_NOT_MPEG25(h); +} + +static unsigned drmp3_hdr_frame_samples(const drmp3_uint8 *h) +{ + return DRMP3_HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)DRMP3_HDR_IS_FRAME_576(h)); +} + +static int drmp3_hdr_frame_bytes(const drmp3_uint8 *h, int free_format_size) +{ + int frame_bytes = drmp3_hdr_frame_samples(h)*drmp3_hdr_bitrate_kbps(h)*125/drmp3_hdr_sample_rate_hz(h); + if (DRMP3_HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int drmp3_hdr_padding(const drmp3_uint8 *h) +{ + return DRMP3_HDR_TEST_PADDING(h) ? (DRMP3_HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef DR_MP3_ONLY_MP3 +static const drmp3_L12_subband_alloc *drmp3_L12_subband_alloc_table(const drmp3_uint8 *hdr, drmp3_L12_scale_info *sci) +{ + const drmp3_L12_subband_alloc *alloc; + int mode = DRMP3_HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == DRMP3_MODE_MONO) ? 0 : (mode == DRMP3_MODE_JOINT_STEREO) ? (DRMP3_HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (DRMP3_HDR_IS_LAYER_1(hdr)) + { + static const drmp3_L12_subband_alloc g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!DRMP3_HDR_TEST_MPEG1(hdr)) + { + static const drmp3_L12_subband_alloc g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const drmp3_L12_subband_alloc g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = DRMP3_HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = drmp3_hdr_bitrate_kbps(hdr) >> (int)(mode != DRMP3_MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const drmp3_L12_subband_alloc g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (drmp3_uint8)nbands; + sci->stereo_bands = (drmp3_uint8)DRMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void drmp3_L12_read_scalefactors(drmp3_bs *bs, drmp3_uint8 *pba, drmp3_uint8 *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DRMP3_DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DRMP3_DQ(3),DRMP3_DQ(7),DRMP3_DQ(15),DRMP3_DQ(31),DRMP3_DQ(63),DRMP3_DQ(127),DRMP3_DQ(255),DRMP3_DQ(511),DRMP3_DQ(1023),DRMP3_DQ(2047),DRMP3_DQ(4095),DRMP3_DQ(8191),DRMP3_DQ(16383),DRMP3_DQ(32767),DRMP3_DQ(65535),DRMP3_DQ(3),DRMP3_DQ(5),DRMP3_DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = drmp3_bs_get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(int)(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void drmp3_L12_read_scale_info(const drmp3_uint8 *hdr, drmp3_bs *bs, drmp3_L12_scale_info *sci) +{ + static const drmp3_uint8 g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const drmp3_L12_subband_alloc *subband_alloc = drmp3_L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const drmp3_uint8 *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + drmp3_uint8 ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[drmp3_bs_get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[drmp3_bs_get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = (drmp3_uint8)(sci->bitalloc[i] ? DRMP3_HDR_IS_LAYER_1(hdr) ? 2 : drmp3_bs_get_bits(bs, 2) : 6); + } + + drmp3_L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int drmp3_L12_dequantize_granule(float *grbuf, drmp3_bs *bs, drmp3_L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)drmp3_bs_get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = drmp3_bs_get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void drmp3_L12_apply_scf_384(drmp3_L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + DRMP3_COPY_MEMORY(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif + +static int drmp3_L3_read_side_info(drmp3_bs *bs, drmp3_L3_gr_info *gr, const drmp3_uint8 *hdr) +{ + static const drmp3_uint8 g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const drmp3_uint8 g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const drmp3_uint8 g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int gr_count = DRMP3_HDR_IS_MONO(hdr) ? 1 : 2; + int sr_idx = DRMP3_HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + + if (DRMP3_HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = drmp3_bs_get_bits(bs, 9); + scfsi = drmp3_bs_get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = drmp3_bs_get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (DRMP3_HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (drmp3_uint16)drmp3_bs_get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (drmp3_uint16)drmp3_bs_get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (drmp3_uint8)drmp3_bs_get_bits(bs, 8); + gr->scalefac_compress = (drmp3_uint16)drmp3_bs_get_bits(bs, DRMP3_HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (drmp3_bs_get_bits(bs, 1)) + { + gr->block_type = (drmp3_uint8)drmp3_bs_get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (drmp3_uint8)drmp3_bs_get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == DRMP3_SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = DRMP3_HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = drmp3_bs_get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + gr->subblock_gain[1] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + gr->subblock_gain[2] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = drmp3_bs_get_bits(bs, 15); + gr->region_count[0] = (drmp3_uint8)drmp3_bs_get_bits(bs, 4); + gr->region_count[1] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (drmp3_uint8)(tables >> 10); + gr->table_select[1] = (drmp3_uint8)((tables >> 5) & 31); + gr->table_select[2] = (drmp3_uint8)((tables) & 31); + gr->preflag = (drmp3_uint8)(DRMP3_HDR_TEST_MPEG1(hdr) ? drmp3_bs_get_bits(bs, 1) : (gr->scalefac_compress >= 500)); + gr->scalefac_scale = (drmp3_uint8)drmp3_bs_get_bits(bs, 1); + gr->count1_table = (drmp3_uint8)drmp3_bs_get_bits(bs, 1); + gr->scfsi = (drmp3_uint8)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void drmp3_L3_read_scalefactors(drmp3_uint8 *scf, drmp3_uint8 *ist_pos, const drmp3_uint8 *scf_size, const drmp3_uint8 *scf_count, drmp3_bs *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + DRMP3_COPY_MEMORY(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + DRMP3_ZERO_MEMORY(scf, cnt); + DRMP3_ZERO_MEMORY(ist_pos, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = drmp3_bs_get_bits(bitbuf, bits); + ist_pos[k] = (drmp3_uint8)(s == max_scf ? -1 : s); + scf[k] = (drmp3_uint8)s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float drmp3_L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = DRMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +/* +I've had reports of GCC 14 throwing an incorrect -Wstringop-overflow warning here. This is an attempt +to silence this warning. +*/ +#if (defined(__GNUC__) && (__GNUC__ >= 13)) && !defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wstringop-overflow" +#endif +static void drmp3_L3_decode_scalefactors(const drmp3_uint8 *hdr, drmp3_uint8 *ist_pos, drmp3_bs *bs, const drmp3_L3_gr_info *gr, float *scf, int ch) +{ + static const drmp3_uint8 g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const drmp3_uint8 *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + drmp3_uint8 scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (DRMP3_HDR_TEST_MPEG1(hdr)) + { + static const drmp3_uint8 g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (drmp3_uint8)(part >> 2); + scf_size[3] = scf_size[2] = (drmp3_uint8)(part & 3); + } else + { + static const drmp3_uint8 g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = DRMP3_HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (drmp3_uint8)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + drmp3_L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] = (drmp3_uint8)(iscf[gr->n_long_sfb + i + 0] + (gr->subblock_gain[0] << sh)); + iscf[gr->n_long_sfb + i + 1] = (drmp3_uint8)(iscf[gr->n_long_sfb + i + 1] + (gr->subblock_gain[1] << sh)); + iscf[gr->n_long_sfb + i + 2] = (drmp3_uint8)(iscf[gr->n_long_sfb + i + 2] + (gr->subblock_gain[2] << sh)); + } + } else if (gr->preflag) + { + static const drmp3_uint8 g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] = (drmp3_uint8)(iscf[11 + i] + g_preamp[i]); + } + } + + gain_exp = gr->global_gain + DRMP3_BITS_DEQUANTIZER_OUT*4 - 210 - (DRMP3_HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = drmp3_L3_ldexp_q2(1 << (DRMP3_MAX_SCFI/4), DRMP3_MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = drmp3_L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} +#if (defined(__GNUC__) && (__GNUC__ >= 13)) && !defined(__clang__) + #pragma GCC diagnostic pop +#endif + +static const float g_drmp3_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float drmp3_L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_drmp3_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_drmp3_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void drmp3_L3_huffman(float *dst, drmp3_bs *bs, const drmp3_L3_gr_info *gr_info, const float *scf, int layer3gr_limit) +{ + static const drmp3_int16 tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const drmp3_uint8 tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205}; + static const drmp3_uint8 tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const drmp3_int16 tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const drmp3_uint8 g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define DRMP3_PEEK_BITS(n) (bs_cache >> (32 - (n))) +#define DRMP3_FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define DRMP3_CHECK_BITS while (bs_sh >= 0) { bs_cache |= (drmp3_uint32)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define DRMP3_BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const drmp3_uint8 *sfb = gr_info->sfbtab; + const drmp3_uint8 *bs_next_ptr = bs->buf + bs->pos/8; + drmp3_uint32 bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const drmp3_int16 *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = DRMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[DRMP3_PEEK_BITS(w)]; + while (leaf < 0) + { + DRMP3_FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[DRMP3_PEEK_BITS(w) - (leaf >> 3)]; + } + DRMP3_FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += DRMP3_PEEK_BITS(linbits); + DRMP3_FLUSH_BITS(linbits); + DRMP3_CHECK_BITS; + *dst = one*drmp3_L3_pow_43(lsb)*((drmp3_int32)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_drmp3_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + DRMP3_FLUSH_BITS(lsb ? 1 : 0); + } + DRMP3_CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = DRMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[DRMP3_PEEK_BITS(w)]; + while (leaf < 0) + { + DRMP3_FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[DRMP3_PEEK_BITS(w) - (leaf >> 3)]; + } + DRMP3_FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_drmp3_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + DRMP3_FLUSH_BITS(lsb ? 1 : 0); + } + DRMP3_CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const drmp3_uint8 *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[DRMP3_PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + DRMP3_FLUSH_BITS(leaf & 7); + if (DRMP3_BSPOS > layer3gr_limit) + { + break; + } +#define DRMP3_RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DRMP3_DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((drmp3_int32)bs_cache < 0) ? -one : one; DRMP3_FLUSH_BITS(1) } + DRMP3_RELOAD_SCALEFACTOR; + DRMP3_DEQ_COUNT1(0); + DRMP3_DEQ_COUNT1(1); + DRMP3_RELOAD_SCALEFACTOR; + DRMP3_DEQ_COUNT1(2); + DRMP3_DEQ_COUNT1(3); + DRMP3_CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void drmp3_L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) + { + for (; i < n - 3; i += 4) + { + drmp3_f4 vl = DRMP3_VLD(left + i); + drmp3_f4 vr = DRMP3_VLD(right + i); + DRMP3_VSTORE(left + i, DRMP3_VADD(vl, vr)); + DRMP3_VSTORE(right + i, DRMP3_VSUB(vl, vr)); + } +#ifdef __GNUC__ + /* Workaround for spurious -Waggressive-loop-optimizations warning from gcc. + * For more info see: https://github.com/lieff/minimp3/issues/88 + */ + if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) + return; +#endif + } +#endif + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void drmp3_L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void drmp3_L3_stereo_top_band(const float *right, const drmp3_uint8 *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void drmp3_L3_stereo_process(float *left, const drmp3_uint8 *ist_pos, const drmp3_uint8 *sfb, const drmp3_uint8 *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = DRMP3_HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = DRMP3_HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (DRMP3_HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = drmp3_L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + drmp3_L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (DRMP3_HDR_TEST_MS_STEREO(hdr)) + { + drmp3_L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void drmp3_L3_intensity_stereo(float *left, drmp3_uint8 *ist_pos, const drmp3_L3_gr_info *gr, const drmp3_uint8 *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + drmp3_L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = DRMP3_MAX(DRMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = DRMP3_HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = (drmp3_uint8)(max_band[i] >= prev ? default_pos : ist_pos[prev]); + } + drmp3_L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void drmp3_L3_reorder(float *grbuf, float *scratch, const drmp3_uint8 *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + DRMP3_COPY_MEMORY(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void drmp3_L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (; i < 8; i += 4) + { + drmp3_f4 vu = DRMP3_VLD(grbuf + 18 + i); + drmp3_f4 vd = DRMP3_VLD(grbuf + 14 - i); + drmp3_f4 vc0 = DRMP3_VLD(g_aa[0] + i); + drmp3_f4 vc1 = DRMP3_VLD(g_aa[1] + i); + vd = DRMP3_VREV(vd); + DRMP3_VSTORE(grbuf + 18 + i, DRMP3_VSUB(DRMP3_VMUL(vu, vc0), DRMP3_VMUL(vd, vc1))); + vd = DRMP3_VADD(DRMP3_VMUL(vu, vc1), DRMP3_VMUL(vd, vc0)); + DRMP3_VSTORE(grbuf + 14 - i, DRMP3_VREV(vd)); + } +#endif +#ifndef DR_MP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif + } +} + +static void drmp3_L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void drmp3_L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + drmp3_L3_dct3_9(co); + drmp3_L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (; i < 8; i += 4) + { + drmp3_f4 vovl = DRMP3_VLD(overlap + i); + drmp3_f4 vc = DRMP3_VLD(co + i); + drmp3_f4 vs = DRMP3_VLD(si + i); + drmp3_f4 vr0 = DRMP3_VLD(g_twid9 + i); + drmp3_f4 vr1 = DRMP3_VLD(g_twid9 + 9 + i); + drmp3_f4 vw0 = DRMP3_VLD(window + i); + drmp3_f4 vw1 = DRMP3_VLD(window + 9 + i); + drmp3_f4 vsum = DRMP3_VADD(DRMP3_VMUL(vc, vr1), DRMP3_VMUL(vs, vr0)); + DRMP3_VSTORE(overlap + i, DRMP3_VSUB(DRMP3_VMUL(vc, vr0), DRMP3_VMUL(vs, vr1))); + DRMP3_VSTORE(grbuf + i, DRMP3_VSUB(DRMP3_VMUL(vovl, vw0), DRMP3_VMUL(vsum, vw1))); + vsum = DRMP3_VADD(DRMP3_VMUL(vovl, vw1), DRMP3_VMUL(vsum, vw0)); + DRMP3_VSTORE(grbuf + 14 - i, DRMP3_VREV(vsum)); + } +#endif + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void drmp3_L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void drmp3_L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + drmp3_L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + drmp3_L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void drmp3_L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + DRMP3_COPY_MEMORY(tmp, grbuf, sizeof(tmp)); + DRMP3_COPY_MEMORY(grbuf, overlap, 6*sizeof(float)); + drmp3_L3_imdct12(tmp, grbuf + 6, overlap + 6); + drmp3_L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + drmp3_L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void drmp3_L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void drmp3_L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + drmp3_L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == DRMP3_SHORT_BLOCK_TYPE) + drmp3_L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + drmp3_L3_imdct36(grbuf, overlap, g_mdct_window[block_type == DRMP3_STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void drmp3_L3_save_reservoir(drmp3dec *h, drmp3dec_scratch *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > DRMP3_MAX_BITRESERVOIR_BYTES) + { + pos += remains - DRMP3_MAX_BITRESERVOIR_BYTES; + remains = DRMP3_MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + DRMP3_MOVE_MEMORY(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int drmp3_L3_restore_reservoir(drmp3dec *h, drmp3_bs *bs, drmp3dec_scratch *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = DRMP3_MIN(h->reserv, main_data_begin); + DRMP3_COPY_MEMORY(s->maindata, h->reserv_buf + DRMP3_MAX(0, h->reserv - main_data_begin), DRMP3_MIN(h->reserv, main_data_begin)); + DRMP3_COPY_MEMORY(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + drmp3_bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void drmp3_L3_decode(drmp3dec *h, drmp3dec_scratch *s, drmp3_L3_gr_info *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + drmp3_L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + drmp3_L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (DRMP3_HDR_TEST_I_STEREO(h->header)) + { + drmp3_L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (DRMP3_HDR_IS_MS_STEREO(h->header)) + { + drmp3_L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(DRMP3_HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + drmp3_L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + drmp3_L3_antialias(s->grbuf[ch], aa_bands); + drmp3_L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + drmp3_L3_change_sign(s->grbuf[ch]); + } +} + +static void drmp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (; k < n; k += 4) + { + drmp3_f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + drmp3_f4 x0 = DRMP3_VLD(&y[i*18]); + drmp3_f4 x1 = DRMP3_VLD(&y[(15 - i)*18]); + drmp3_f4 x2 = DRMP3_VLD(&y[(16 + i)*18]); + drmp3_f4 x3 = DRMP3_VLD(&y[(31 - i)*18]); + drmp3_f4 t0 = DRMP3_VADD(x0, x3); + drmp3_f4 t1 = DRMP3_VADD(x1, x2); + drmp3_f4 t2 = DRMP3_VMUL_S(DRMP3_VSUB(x1, x2), g_sec[3*i + 0]); + drmp3_f4 t3 = DRMP3_VMUL_S(DRMP3_VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = DRMP3_VADD(t0, t1); + x[8] = DRMP3_VMUL_S(DRMP3_VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = DRMP3_VADD(t3, t2); + x[24] = DRMP3_VMUL_S(DRMP3_VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + drmp3_f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = DRMP3_VSUB(x0, x7); x0 = DRMP3_VADD(x0, x7); + x7 = DRMP3_VSUB(x1, x6); x1 = DRMP3_VADD(x1, x6); + x6 = DRMP3_VSUB(x2, x5); x2 = DRMP3_VADD(x2, x5); + x5 = DRMP3_VSUB(x3, x4); x3 = DRMP3_VADD(x3, x4); + x4 = DRMP3_VSUB(x0, x3); x0 = DRMP3_VADD(x0, x3); + x3 = DRMP3_VSUB(x1, x2); x1 = DRMP3_VADD(x1, x2); + x[0] = DRMP3_VADD(x0, x1); + x[4] = DRMP3_VMUL_S(DRMP3_VSUB(x0, x1), 0.70710677f); + x5 = DRMP3_VADD(x5, x6); + x6 = DRMP3_VMUL_S(DRMP3_VADD(x6, x7), 0.70710677f); + x7 = DRMP3_VADD(x7, xt); + x3 = DRMP3_VMUL_S(DRMP3_VADD(x3, x4), 0.70710677f); + x5 = DRMP3_VSUB(x5, DRMP3_VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = DRMP3_VADD(x7, DRMP3_VMUL_S(x5, 0.382683432f)); + x5 = DRMP3_VSUB(x5, DRMP3_VMUL_S(x7, 0.198912367f)); + x0 = DRMP3_VSUB(xt, x6); xt = DRMP3_VADD(xt, x6); + x[1] = DRMP3_VMUL_S(DRMP3_VADD(xt, x7), 0.50979561f); + x[2] = DRMP3_VMUL_S(DRMP3_VADD(x4, x3), 0.54119611f); + x[3] = DRMP3_VMUL_S(DRMP3_VSUB(x0, x5), 0.60134488f); + x[5] = DRMP3_VMUL_S(DRMP3_VADD(x0, x5), 0.89997619f); + x[6] = DRMP3_VMUL_S(DRMP3_VSUB(x4, x3), 1.30656302f); + x[7] = DRMP3_VMUL_S(DRMP3_VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if DRMP3_HAVE_SSE +#define DRMP3_VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else +#define DRMP3_VSAVE2(i, v) vst1_f32((float32_t *)&y[(i)*18], vget_low_f32(v)) +#endif + for (i = 0; i < 7; i++, y += 4*18) + { + drmp3_f4 s = DRMP3_VADD(t[3][i], t[3][i + 1]); + DRMP3_VSAVE2(0, t[0][i]); + DRMP3_VSAVE2(1, DRMP3_VADD(t[2][i], s)); + DRMP3_VSAVE2(2, DRMP3_VADD(t[1][i], t[1][i + 1])); + DRMP3_VSAVE2(3, DRMP3_VADD(t[2][1 + i], s)); + } + DRMP3_VSAVE2(0, t[0][7]); + DRMP3_VSAVE2(1, DRMP3_VADD(t[2][7], t[3][7])); + DRMP3_VSAVE2(2, t[1][7]); + DRMP3_VSAVE2(3, t[3][7]); + } else + { +#define DRMP3_VSAVE4(i, v) DRMP3_VSTORE(&y[(i)*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + drmp3_f4 s = DRMP3_VADD(t[3][i], t[3][i + 1]); + DRMP3_VSAVE4(0, t[0][i]); + DRMP3_VSAVE4(1, DRMP3_VADD(t[2][i], s)); + DRMP3_VSAVE4(2, DRMP3_VADD(t[1][i], t[1][i + 1])); + DRMP3_VSAVE4(3, DRMP3_VADD(t[2][1 + i], s)); + } + DRMP3_VSAVE4(0, t[0][7]); + DRMP3_VSAVE4(1, DRMP3_VADD(t[2][7], t[3][7])); + DRMP3_VSAVE4(2, t[1][7]); + DRMP3_VSAVE4(3, t[3][7]); + } + } else +#endif +#ifdef DR_MP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif +} + +#ifndef DR_MP3_FLOAT_OUTPUT +typedef drmp3_int16 drmp3d_sample_t; + +static drmp3_int16 drmp3d_scale_pcm(float sample) +{ + drmp3_int16 s; +#if DRMP3_HAVE_ARMV6 + drmp3_int32 s32 = (drmp3_int32)(sample + .5f); + s32 -= (s32 < 0); + s = (drmp3_int16)drmp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5f) return (drmp3_int16) 32767; + if (sample <= -32767.5f) return (drmp3_int16)-32768; + s = (drmp3_int16)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else +typedef float drmp3d_sample_t; + +static float drmp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif + +static void drmp3d_synth_pair(drmp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = drmp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = drmp3d_scale_pcm(a); +} + +static void drmp3d_synth(float *xl, drmp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + drmp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + drmp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + drmp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + drmp3d_synth_pair(dstl, nch, lins + 4*15); + drmp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (i = 14; i >= 0; i--) + { +#define DRMP3_VLOAD(k) drmp3_f4 w0 = DRMP3_VSET(*w++); drmp3_f4 w1 = DRMP3_VSET(*w++); drmp3_f4 vz = DRMP3_VLD(&zlin[4*i - 64*k]); drmp3_f4 vy = DRMP3_VLD(&zlin[4*i - 64*(15 - k)]); +#define DRMP3_V0(k) { DRMP3_VLOAD(k) b = DRMP3_VADD(DRMP3_VMUL(vz, w1), DRMP3_VMUL(vy, w0)) ; a = DRMP3_VSUB(DRMP3_VMUL(vz, w0), DRMP3_VMUL(vy, w1)); } +#define DRMP3_V1(k) { DRMP3_VLOAD(k) b = DRMP3_VADD(b, DRMP3_VADD(DRMP3_VMUL(vz, w1), DRMP3_VMUL(vy, w0))); a = DRMP3_VADD(a, DRMP3_VSUB(DRMP3_VMUL(vz, w0), DRMP3_VMUL(vy, w1))); } +#define DRMP3_V2(k) { DRMP3_VLOAD(k) b = DRMP3_VADD(b, DRMP3_VADD(DRMP3_VMUL(vz, w1), DRMP3_VMUL(vy, w0))); a = DRMP3_VADD(a, DRMP3_VSUB(DRMP3_VMUL(vy, w1), DRMP3_VMUL(vz, w0))); } + drmp3_f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + DRMP3_V0(0) DRMP3_V2(1) DRMP3_V1(2) DRMP3_V2(3) DRMP3_V1(4) DRMP3_V2(5) DRMP3_V1(6) DRMP3_V2(7) + + { +#ifndef DR_MP3_FLOAT_OUTPUT +#if DRMP3_HAVE_SSE + static const drmp3_f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const drmp3_f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 6); +#else + int16x4_t pcma, pcmb; + a = DRMP3_VADD(a, DRMP3_VSET(0.5f)); + b = DRMP3_VADD(b, DRMP3_VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, DRMP3_VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, DRMP3_VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif +#else + #if DRMP3_HAVE_SSE + static const drmp3_f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + #else + const drmp3_f4 g_scale = vdupq_n_f32(1.0f/32768.0f); + #endif + a = DRMP3_VMUL(a, g_scale); + b = DRMP3_VMUL(b, g_scale); +#if DRMP3_HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif +#endif /* DR_MP3_FLOAT_OUTPUT */ + } + } else +#endif +#ifdef DR_MP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else + for (i = 14; i >= 0; i--) + { +#define DRMP3_LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define DRMP3_S0(k) { int j; DRMP3_LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define DRMP3_S1(k) { int j; DRMP3_LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define DRMP3_S2(k) { int j; DRMP3_LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + DRMP3_S0(0) DRMP3_S2(1) DRMP3_S1(2) DRMP3_S2(3) DRMP3_S1(4) DRMP3_S2(5) DRMP3_S1(6) DRMP3_S2(7) + + dstr[(15 - i)*nch] = drmp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = drmp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = drmp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = drmp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = drmp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = drmp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = drmp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = drmp3d_scale_pcm(b[2]); + } +#endif +} + +static void drmp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, drmp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + drmp3d_DCT_II(grbuf + 576*i, nbands); + } + + DRMP3_COPY_MEMORY(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + drmp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef DR_MP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif + { + DRMP3_COPY_MEMORY(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int drmp3d_match_frame(const drmp3_uint8 *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < DRMP3_MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += drmp3_hdr_frame_bytes(hdr + i, frame_bytes) + drmp3_hdr_padding(hdr + i); + if (i + DRMP3_HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!drmp3_hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int drmp3d_find_frame(const drmp3_uint8 *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - DRMP3_HDR_SIZE; i++, mp3++) + { + if (drmp3_hdr_valid(mp3)) + { + int frame_bytes = drmp3_hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + drmp3_hdr_padding(mp3); + + for (k = DRMP3_HDR_SIZE; !frame_bytes && k < DRMP3_MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - DRMP3_HDR_SIZE; k++) + { + if (drmp3_hdr_compare(mp3, mp3 + k)) + { + int fb = k - drmp3_hdr_padding(mp3); + int nextfb = fb + drmp3_hdr_padding(mp3 + k); + if (i + k + nextfb + DRMP3_HDR_SIZE > mp3_bytes || !drmp3_hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + drmp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +DRMP3_API void drmp3dec_init(drmp3dec *dec) +{ + dec->header[0] = 0; +} + +DRMP3_API int drmp3dec_decode_frame(drmp3dec *dec, const drmp3_uint8 *mp3, int mp3_bytes, void *pcm, drmp3dec_frame_info *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const drmp3_uint8 *hdr; + drmp3_bs bs_frame[1]; + + if (mp3_bytes > 4 && dec->header[0] == 0xff && drmp3_hdr_compare(dec->header, mp3)) + { + frame_size = drmp3_hdr_frame_bytes(mp3, dec->free_format_bytes) + drmp3_hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + DRMP3_HDR_SIZE > mp3_bytes || !drmp3_hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + DRMP3_ZERO_MEMORY(dec, sizeof(drmp3dec)); + i = drmp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + DRMP3_COPY_MEMORY(dec->header, hdr, DRMP3_HDR_SIZE); + info->frame_bytes = i + frame_size; + info->channels = DRMP3_HDR_IS_MONO(hdr) ? 1 : 2; + info->sample_rate = drmp3_hdr_sample_rate_hz(hdr); + info->layer = 4 - DRMP3_HDR_GET_LAYER(hdr); + info->bitrate_kbps = drmp3_hdr_bitrate_kbps(hdr); + + drmp3_bs_init(bs_frame, hdr + DRMP3_HDR_SIZE, frame_size - DRMP3_HDR_SIZE); + if (DRMP3_HDR_IS_CRC(hdr)) + { + drmp3_bs_get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = drmp3_L3_read_side_info(bs_frame, dec->scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + drmp3dec_init(dec); + return 0; + } + success = drmp3_L3_restore_reservoir(dec, bs_frame, &dec->scratch, main_data_begin); + if (success && pcm != NULL) + { + for (igr = 0; igr < (DRMP3_HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm = DRMP3_OFFSET_PTR(pcm, sizeof(drmp3d_sample_t)*576*info->channels)) + { + DRMP3_ZERO_MEMORY(dec->scratch.grbuf[0], 576*2*sizeof(float)); + drmp3_L3_decode(dec, &dec->scratch, dec->scratch.gr_info + igr*info->channels, info->channels); + drmp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 18, info->channels, (drmp3d_sample_t*)pcm, dec->scratch.syn[0]); + } + } + drmp3_L3_save_reservoir(dec, &dec->scratch); + } else + { +#ifdef DR_MP3_ONLY_MP3 + return 0; +#else + drmp3_L12_scale_info sci[1]; + + if (pcm == NULL) { + return drmp3_hdr_frame_samples(hdr); + } + + drmp3_L12_read_scale_info(hdr, bs_frame, sci); + + DRMP3_ZERO_MEMORY(dec->scratch.grbuf[0], 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += drmp3_L12_dequantize_granule(dec->scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + drmp3_L12_apply_scf_384(sci, sci->scf + igr, dec->scratch.grbuf[0]); + drmp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 12, info->channels, (drmp3d_sample_t*)pcm, dec->scratch.syn[0]); + DRMP3_ZERO_MEMORY(dec->scratch.grbuf[0], 576*2*sizeof(float)); + pcm = DRMP3_OFFSET_PTR(pcm, sizeof(drmp3d_sample_t)*384*info->channels); + } + if (bs_frame->pos > bs_frame->limit) + { + drmp3dec_init(dec); + return 0; + } + } +#endif + } + + return success*drmp3_hdr_frame_samples(dec->header); +} + +DRMP3_API void drmp3dec_f32_to_s16(const float *in, drmp3_int16 *out, size_t num_samples) +{ + size_t i = 0; +#if DRMP3_HAVE_SIMD + size_t aligned_count = num_samples & ~7; + for(; i < aligned_count; i+=8) + { + drmp3_f4 scale = DRMP3_VSET(32768.0f); + drmp3_f4 a = DRMP3_VMUL(DRMP3_VLD(&in[i ]), scale); + drmp3_f4 b = DRMP3_VMUL(DRMP3_VLD(&in[i+4]), scale); +#if DRMP3_HAVE_SSE + drmp3_f4 s16max = DRMP3_VSET( 32767.0f); + drmp3_f4 s16min = DRMP3_VSET(-32768.0f); + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, s16max), s16min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, s16max), s16min))); + out[i ] = (drmp3_int16)_mm_extract_epi16(pcm8, 0); + out[i+1] = (drmp3_int16)_mm_extract_epi16(pcm8, 1); + out[i+2] = (drmp3_int16)_mm_extract_epi16(pcm8, 2); + out[i+3] = (drmp3_int16)_mm_extract_epi16(pcm8, 3); + out[i+4] = (drmp3_int16)_mm_extract_epi16(pcm8, 4); + out[i+5] = (drmp3_int16)_mm_extract_epi16(pcm8, 5); + out[i+6] = (drmp3_int16)_mm_extract_epi16(pcm8, 6); + out[i+7] = (drmp3_int16)_mm_extract_epi16(pcm8, 7); +#else + int16x4_t pcma, pcmb; + a = DRMP3_VADD(a, DRMP3_VSET(0.5f)); + b = DRMP3_VADD(b, DRMP3_VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, DRMP3_VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, DRMP3_VSET(0))))); + vst1_lane_s16(out+i , pcma, 0); + vst1_lane_s16(out+i+1, pcma, 1); + vst1_lane_s16(out+i+2, pcma, 2); + vst1_lane_s16(out+i+3, pcma, 3); + vst1_lane_s16(out+i+4, pcmb, 0); + vst1_lane_s16(out+i+5, pcmb, 1); + vst1_lane_s16(out+i+6, pcmb, 2); + vst1_lane_s16(out+i+7, pcmb, 3); +#endif + } +#endif + for(; i < num_samples; i++) + { + float sample = in[i] * 32768.0f; + if (sample >= 32766.5f) + out[i] = (drmp3_int16) 32767; + else if (sample <= -32767.5f) + out[i] = (drmp3_int16)-32768; + else + { + short s = (drmp3_int16)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ + out[i] = s; + } + } +} + + + +/************************************************************************************************************************************************************ + + Main Public API + + ************************************************************************************************************************************************************/ +/* SIZE_MAX */ +#if defined(SIZE_MAX) + #define DRMP3_SIZE_MAX SIZE_MAX +#else + #if defined(_WIN64) || defined(_LP64) || defined(__LP64__) + #define DRMP3_SIZE_MAX ((drmp3_uint64)0xFFFFFFFFFFFFFFFF) + #else + #define DRMP3_SIZE_MAX 0xFFFFFFFF + #endif +#endif +/* End SIZE_MAX */ + +/* Options. */ +#ifndef DRMP3_SEEK_LEADING_MP3_FRAMES +#define DRMP3_SEEK_LEADING_MP3_FRAMES 2 +#endif + +#define DRMP3_MIN_DATA_CHUNK_SIZE 16384 + +/* The size in bytes of each chunk of data to read from the MP3 stream. minimp3 recommends at least 16K, but in an attempt to reduce data movement I'm making this slightly larger. */ +#ifndef DRMP3_DATA_CHUNK_SIZE +#define DRMP3_DATA_CHUNK_SIZE (DRMP3_MIN_DATA_CHUNK_SIZE*4) +#endif + + +#define DRMP3_COUNTOF(x) (sizeof(x) / sizeof(x[0])) +#define DRMP3_CLAMP(x, lo, hi) (DRMP3_MAX(lo, DRMP3_MIN(x, hi))) + +#ifndef DRMP3_PI_D +#define DRMP3_PI_D 3.14159265358979323846264 +#endif + +#define DRMP3_DEFAULT_RESAMPLER_LPF_ORDER 2 + +static DRMP3_INLINE float drmp3_mix_f32(float x, float y, float a) +{ + return x*(1-a) + y*a; +} +static DRMP3_INLINE float drmp3_mix_f32_fast(float x, float y, float a) +{ + float r0 = (y - x); + float r1 = r0*a; + return x + r1; + /*return x + (y - x)*a;*/ +} + + +/* +Greatest common factor using Euclid's algorithm iteratively. +*/ +static DRMP3_INLINE drmp3_uint32 drmp3_gcf_u32(drmp3_uint32 a, drmp3_uint32 b) +{ + for (;;) { + if (b == 0) { + break; + } else { + drmp3_uint32 t = a; + a = b; + b = t % a; + } + } + + return a; +} + + +static void* drmp3__malloc_default(size_t sz, void* pUserData) +{ + (void)pUserData; + return DRMP3_MALLOC(sz); +} + +static void* drmp3__realloc_default(void* p, size_t sz, void* pUserData) +{ + (void)pUserData; + return DRMP3_REALLOC(p, sz); +} + +static void drmp3__free_default(void* p, void* pUserData) +{ + (void)pUserData; + DRMP3_FREE(p); +} + + +static void* drmp3__malloc_from_callbacks(size_t sz, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onMalloc != NULL) { + return pAllocationCallbacks->onMalloc(sz, pAllocationCallbacks->pUserData); + } + + /* Try using realloc(). */ + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(NULL, sz, pAllocationCallbacks->pUserData); + } + + return NULL; +} + +static void* drmp3__realloc_from_callbacks(void* p, size_t szNew, size_t szOld, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(p, szNew, pAllocationCallbacks->pUserData); + } + + /* Try emulating realloc() in terms of malloc()/free(). */ + if (pAllocationCallbacks->onMalloc != NULL && pAllocationCallbacks->onFree != NULL) { + void* p2; + + p2 = pAllocationCallbacks->onMalloc(szNew, pAllocationCallbacks->pUserData); + if (p2 == NULL) { + return NULL; + } + + if (p != NULL) { + DRMP3_COPY_MEMORY(p2, p, szOld); + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } + + return p2; + } + + return NULL; +} + +static void drmp3__free_from_callbacks(void* p, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (p == NULL || pAllocationCallbacks == NULL) { + return; + } + + if (pAllocationCallbacks->onFree != NULL) { + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } +} + + +static drmp3_allocation_callbacks drmp3_copy_allocation_callbacks_or_defaults(const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + /* Copy. */ + return *pAllocationCallbacks; + } else { + /* Defaults. */ + drmp3_allocation_callbacks allocationCallbacks; + allocationCallbacks.pUserData = NULL; + allocationCallbacks.onMalloc = drmp3__malloc_default; + allocationCallbacks.onRealloc = drmp3__realloc_default; + allocationCallbacks.onFree = drmp3__free_default; + return allocationCallbacks; + } +} + + + +static size_t drmp3__on_read(drmp3* pMP3, void* pBufferOut, size_t bytesToRead) +{ + size_t bytesRead; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + /* + Don't try reading 0 bytes from the callback. This can happen when the stream is clamped against + ID3v1 or APE tags at the end of the stream. + */ + if (bytesToRead == 0) { + return 0; + } + + bytesRead = pMP3->onRead(pMP3->pUserData, pBufferOut, bytesToRead); + pMP3->streamCursor += bytesRead; + + return bytesRead; +} + +static size_t drmp3__on_read_clamped(drmp3* pMP3, void* pBufferOut, size_t bytesToRead) +{ + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + if (pMP3->streamLength == DRMP3_UINT64_MAX) { + return drmp3__on_read(pMP3, pBufferOut, bytesToRead); + } else { + drmp3_uint64 bytesRemaining; + + bytesRemaining = (pMP3->streamLength - pMP3->streamCursor); + if (bytesToRead > bytesRemaining) { + bytesToRead = (size_t)bytesRemaining; + } + + return drmp3__on_read(pMP3, pBufferOut, bytesToRead); + } +} + +static drmp3_bool32 drmp3__on_seek(drmp3* pMP3, int offset, drmp3_seek_origin origin) +{ + DRMP3_ASSERT(offset >= 0); + DRMP3_ASSERT(origin == DRMP3_SEEK_SET || origin == DRMP3_SEEK_CUR); + + if (!pMP3->onSeek(pMP3->pUserData, offset, origin)) { + return DRMP3_FALSE; + } + + if (origin == DRMP3_SEEK_SET) { + pMP3->streamCursor = (drmp3_uint64)offset; + } else{ + pMP3->streamCursor += offset; + } + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3__on_seek_64(drmp3* pMP3, drmp3_uint64 offset, drmp3_seek_origin origin) +{ + if (offset <= 0x7FFFFFFF) { + return drmp3__on_seek(pMP3, (int)offset, origin); + } + + /* Getting here "offset" is too large for a 32-bit integer. We just keep seeking forward until we hit the offset. */ + if (!drmp3__on_seek(pMP3, 0x7FFFFFFF, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; + } + + offset -= 0x7FFFFFFF; + while (offset > 0) { + if (offset <= 0x7FFFFFFF) { + if (!drmp3__on_seek(pMP3, (int)offset, DRMP3_SEEK_CUR)) { + return DRMP3_FALSE; + } + offset = 0; + } else { + if (!drmp3__on_seek(pMP3, 0x7FFFFFFF, DRMP3_SEEK_CUR)) { + return DRMP3_FALSE; + } + offset -= 0x7FFFFFFF; + } + } + + return DRMP3_TRUE; +} + +static void drmp3__on_meta(drmp3* pMP3, drmp3_metadata_type type, const void* pRawData, size_t rawDataSize) +{ + if (pMP3->onMeta) { + drmp3_metadata metadata; + + DRMP3_ZERO_OBJECT(&metadata); + metadata.type = type; + metadata.pRawData = pRawData; + metadata.rawDataSize = rawDataSize; + + pMP3->onMeta(pMP3->pUserDataMeta, &metadata); + } +} + + +static drmp3_uint32 drmp3_decode_next_frame_ex__callbacks(drmp3* pMP3, drmp3d_sample_t* pPCMFrames, drmp3dec_frame_info* pMP3FrameInfo, const drmp3_uint8** ppMP3FrameData) +{ + drmp3_uint32 pcmFramesRead = 0; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + if (pMP3->atEnd) { + return 0; + } + + for (;;) { + drmp3dec_frame_info info; + + /* minimp3 recommends doing data submission in chunks of at least 16K. If we don't have at least 16K bytes available, get more. */ + if (pMP3->dataSize < DRMP3_MIN_DATA_CHUNK_SIZE) { + size_t bytesRead; + + /* First we need to move the data down. */ + if (pMP3->pData != NULL) { + DRMP3_MOVE_MEMORY(pMP3->pData, pMP3->pData + pMP3->dataConsumed, pMP3->dataSize); + } + + pMP3->dataConsumed = 0; + + if (pMP3->dataCapacity < DRMP3_DATA_CHUNK_SIZE) { + drmp3_uint8* pNewData; + size_t newDataCap; + + newDataCap = DRMP3_DATA_CHUNK_SIZE; + + pNewData = (drmp3_uint8*)drmp3__realloc_from_callbacks(pMP3->pData, newDataCap, pMP3->dataCapacity, &pMP3->allocationCallbacks); + if (pNewData == NULL) { + return 0; /* Out of memory. */ + } + + pMP3->pData = pNewData; + pMP3->dataCapacity = newDataCap; + } + + bytesRead = drmp3__on_read_clamped(pMP3, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); + if (bytesRead == 0) { + if (pMP3->dataSize == 0) { + pMP3->atEnd = DRMP3_TRUE; + return 0; /* No data. */ + } + } + + pMP3->dataSize += bytesRead; + } + + if (pMP3->dataSize > INT_MAX) { + pMP3->atEnd = DRMP3_TRUE; + return 0; /* File too big. */ + } + + DRMP3_ASSERT(pMP3->pData != NULL); + DRMP3_ASSERT(pMP3->dataCapacity > 0); + + /* Do a runtime check here to try silencing a false-positive from clang-analyzer. */ + if (pMP3->pData == NULL) { + return 0; + } + + pcmFramesRead = drmp3dec_decode_frame(&pMP3->decoder, pMP3->pData + pMP3->dataConsumed, (int)pMP3->dataSize, pPCMFrames, &info); /* <-- Safe size_t -> int conversion thanks to the check above. */ + + /* Consume the data. */ + pMP3->dataConsumed += (size_t)info.frame_bytes; + pMP3->dataSize -= (size_t)info.frame_bytes; + + /* pcmFramesRead will be equal to 0 if decoding failed. If it is zero and info.frame_bytes > 0 then we have successfully decoded the frame. */ + if (pcmFramesRead > 0) { + pcmFramesRead = drmp3_hdr_frame_samples(pMP3->decoder.header); + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = pcmFramesRead; + pMP3->mp3FrameChannels = info.channels; + pMP3->mp3FrameSampleRate = info.sample_rate; + + if (pMP3FrameInfo != NULL) { + *pMP3FrameInfo = info; + } + + if (ppMP3FrameData != NULL) { + *ppMP3FrameData = pMP3->pData + pMP3->dataConsumed - (size_t)info.frame_bytes; + } + + break; + } else if (info.frame_bytes == 0) { + /* Need more data. minimp3 recommends doing data submission in 16K chunks. */ + size_t bytesRead; + + /* First we need to move the data down. */ + DRMP3_MOVE_MEMORY(pMP3->pData, pMP3->pData + pMP3->dataConsumed, pMP3->dataSize); + pMP3->dataConsumed = 0; + + if (pMP3->dataCapacity == pMP3->dataSize) { + /* No room. Expand. */ + drmp3_uint8* pNewData; + size_t newDataCap; + + newDataCap = pMP3->dataCapacity + DRMP3_DATA_CHUNK_SIZE; + + pNewData = (drmp3_uint8*)drmp3__realloc_from_callbacks(pMP3->pData, newDataCap, pMP3->dataCapacity, &pMP3->allocationCallbacks); + if (pNewData == NULL) { + return 0; /* Out of memory. */ + } + + pMP3->pData = pNewData; + pMP3->dataCapacity = newDataCap; + } + + /* Fill in a chunk. */ + bytesRead = drmp3__on_read_clamped(pMP3, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); + if (bytesRead == 0) { + pMP3->atEnd = DRMP3_TRUE; + return 0; /* Error reading more data. */ + } + + pMP3->dataSize += bytesRead; + } + }; + + return pcmFramesRead; +} + +static drmp3_uint32 drmp3_decode_next_frame_ex__memory(drmp3* pMP3, drmp3d_sample_t* pPCMFrames, drmp3dec_frame_info* pMP3FrameInfo, const drmp3_uint8** ppMP3FrameData) +{ + drmp3_uint32 pcmFramesRead = 0; + drmp3dec_frame_info info; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->memory.pData != NULL); + + if (pMP3->atEnd) { + return 0; + } + + for (;;) { + pcmFramesRead = drmp3dec_decode_frame(&pMP3->decoder, pMP3->memory.pData + pMP3->memory.currentReadPos, (int)(pMP3->memory.dataSize - pMP3->memory.currentReadPos), pPCMFrames, &info); + if (pcmFramesRead > 0) { + pcmFramesRead = drmp3_hdr_frame_samples(pMP3->decoder.header); + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = pcmFramesRead; + pMP3->mp3FrameChannels = info.channels; + pMP3->mp3FrameSampleRate = info.sample_rate; + + if (pMP3FrameInfo != NULL) { + *pMP3FrameInfo = info; + } + + if (ppMP3FrameData != NULL) { + *ppMP3FrameData = pMP3->memory.pData + pMP3->memory.currentReadPos; + } + + break; + } else if (info.frame_bytes > 0) { + /* No frames were read, but it looks like we skipped past one. Read the next MP3 frame. */ + pMP3->memory.currentReadPos += (size_t)info.frame_bytes; + pMP3->streamCursor += (size_t)info.frame_bytes; + } else { + /* Nothing at all was read. Abort. */ + break; + } + } + + /* Consume the data. */ + pMP3->memory.currentReadPos += (size_t)info.frame_bytes; + pMP3->streamCursor += (size_t)info.frame_bytes; + + return pcmFramesRead; +} + +static drmp3_uint32 drmp3_decode_next_frame_ex(drmp3* pMP3, drmp3d_sample_t* pPCMFrames, drmp3dec_frame_info* pMP3FrameInfo, const drmp3_uint8** ppMP3FrameData) +{ + if (pMP3->memory.pData != NULL && pMP3->memory.dataSize > 0) { + return drmp3_decode_next_frame_ex__memory(pMP3, pPCMFrames, pMP3FrameInfo, ppMP3FrameData); + } else { + return drmp3_decode_next_frame_ex__callbacks(pMP3, pPCMFrames, pMP3FrameInfo, ppMP3FrameData); + } +} + +static drmp3_uint32 drmp3_decode_next_frame(drmp3* pMP3) +{ + DRMP3_ASSERT(pMP3 != NULL); + return drmp3_decode_next_frame_ex(pMP3, (drmp3d_sample_t*)pMP3->pcmFrames, NULL, NULL); +} + +#if 0 +static drmp3_uint32 drmp3_seek_next_frame(drmp3* pMP3) +{ + drmp3_uint32 pcmFrameCount; + + DRMP3_ASSERT(pMP3 != NULL); + + pcmFrameCount = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFrameCount == 0) { + return 0; + } + + /* We have essentially just skipped past the frame, so just set the remaining samples to 0. */ + pMP3->currentPCMFrame += pcmFrameCount; + pMP3->pcmFramesConsumedInMP3Frame = pcmFrameCount; + pMP3->pcmFramesRemainingInMP3Frame = 0; + + return pcmFrameCount; +} +#endif + +static drmp3_bool32 drmp3_init_internal(drmp3* pMP3, drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, drmp3_meta_proc onMeta, void* pUserData, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3dec_frame_info firstFrameInfo; + const drmp3_uint8* pFirstFrameData; + drmp3_uint32 firstFramePCMFrameCount; + drmp3_uint32 detectedMP3FrameCount = 0xFFFFFFFF; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(onRead != NULL); + + /* This function assumes the output object has already been reset to 0. Do not do that here, otherwise things will break. */ + drmp3dec_init(&pMP3->decoder); + + pMP3->onRead = onRead; + pMP3->onSeek = onSeek; + pMP3->onMeta = onMeta; + pMP3->pUserData = pUserData; + pMP3->pUserDataMeta = pUserDataMeta; + pMP3->allocationCallbacks = drmp3_copy_allocation_callbacks_or_defaults(pAllocationCallbacks); + + if (pMP3->allocationCallbacks.onFree == NULL || (pMP3->allocationCallbacks.onMalloc == NULL && pMP3->allocationCallbacks.onRealloc == NULL)) { + return DRMP3_FALSE; /* Invalid allocation callbacks. */ + } + + pMP3->streamCursor = 0; + pMP3->streamLength = DRMP3_UINT64_MAX; + pMP3->streamStartOffset = 0; + pMP3->delayInPCMFrames = 0; + pMP3->paddingInPCMFrames = 0; + pMP3->totalPCMFrameCount = DRMP3_UINT64_MAX; + + /* We'll first check for any ID3v1 or APE tags. */ + #if 1 + if (onSeek != NULL && onTell != NULL) { + if (onSeek(pUserData, 0, DRMP3_SEEK_END)) { + drmp3_int64 streamLen; + int streamEndOffset = 0; + + /* First get the length of the stream. We need this so we can ensure the stream is big enough to store the tags. */ + if (onTell(pUserData, &streamLen)) { + /* ID3v1 */ + if (streamLen > 128) { + char id3[3]; + if (onSeek(pUserData, streamEndOffset - 128, DRMP3_SEEK_END)) { + if (onRead(pUserData, id3, 3) == 3 && id3[0] == 'T' && id3[1] == 'A' && id3[2] == 'G') { + /* We have an ID3v1 tag. */ + streamEndOffset -= 128; + streamLen -= 128; + + /* Fire a metadata callback for the TAG data. */ + if (onMeta != NULL) { + drmp3_uint8 tag[128]; + tag[0] = 'T'; tag[1] = 'A'; tag[2] = 'G'; + + if (onRead(pUserData, tag + 3, 125) == 125) { + drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_ID3V1, tag, 128); + } + } + } else { + /* No ID3v1 tag. */ + } + } else { + /* Failed to seek to the ID3v1 tag. */ + } + } else { + /* Stream too short. No ID3v1 tag. */ + } + + /* APE */ + if (streamLen > 32) { + char ape[32]; /* The footer. */ + if (onSeek(pUserData, streamEndOffset - 32, DRMP3_SEEK_END)) { + if (onRead(pUserData, ape, 32) == 32 && ape[0] == 'A' && ape[1] == 'P' && ape[2] == 'E' && ape[3] == 'T' && ape[4] == 'A' && ape[5] == 'G' && ape[6] == 'E' && ape[7] == 'X') { + /* We have an APE tag. */ + drmp3_uint32 tagSize = + ((drmp3_uint32)ape[24] << 0) | + ((drmp3_uint32)ape[25] << 8) | + ((drmp3_uint32)ape[26] << 16) | + ((drmp3_uint32)ape[27] << 24); + + if (32 + tagSize < streamLen) { + streamEndOffset -= 32 + tagSize; + streamLen -= 32 + tagSize; + + /* Fire a metadata callback for the APE data. Must include both the main content and footer. */ + if (onMeta != NULL) { + /* We first need to seek to the start of the APE tag. */ + if (onSeek(pUserData, streamEndOffset, DRMP3_SEEK_END)) { + size_t apeTagSize = (size_t)tagSize + 32; + drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(apeTagSize, pAllocationCallbacks); + if (pTagData != NULL) { + if (onRead(pUserData, pTagData, apeTagSize) == apeTagSize) { + drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_APE, pTagData, apeTagSize); + } + + drmp3_free(pTagData, pAllocationCallbacks); + } + } + } + } else { + /* The tag size is larger than the stream. Invalid APE tag. */ + } + } + } + } else { + /* Stream too short. No APE tag. */ + } + + /* Seek back to the start. */ + if (!onSeek(pUserData, 0, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek back to the start. */ + } + + pMP3->streamLength = (drmp3_uint64)streamLen; + + if (pMP3->memory.pData != NULL) { + pMP3->memory.dataSize = (size_t)pMP3->streamLength; + } + } else { + /* Failed to get the length of the stream. ID3v1 and APE tags cannot be skipped. */ + if (!onSeek(pUserData, 0, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek back to the start. */ + } + } + } else { + /* Failed to seek to the end. Cannot skip ID3v1 or APE tags. */ + } + } else { + /* No onSeek or onTell callback. Cannot skip ID3v1 or APE tags. */ + } + #endif + + + /* ID3v2 tags */ + #if 1 + { + char header[10]; + if (onRead(pUserData, header, 10) == 10) { + if (header[0] == 'I' && header[1] == 'D' && header[2] == '3') { + drmp3_uint32 tagSize = + (((drmp3_uint32)header[6] & 0x7F) << 21) | + (((drmp3_uint32)header[7] & 0x7F) << 14) | + (((drmp3_uint32)header[8] & 0x7F) << 7) | + (((drmp3_uint32)header[9] & 0x7F) << 0); + + /* Account for the footer. */ + if (header[5] & 0x10) { + tagSize += 10; + } + + /* Read the tag content and fire a metadata callback. */ + if (onMeta != NULL) { + size_t tagSizeWithHeader = 10 + tagSize; + drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(tagSizeWithHeader, pAllocationCallbacks); + if (pTagData != NULL) { + DRMP3_COPY_MEMORY(pTagData, header, 10); + + if (onRead(pUserData, pTagData + 10, tagSize) == tagSize) { + drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_ID3V2, pTagData, tagSizeWithHeader); + } + + drmp3_free(pTagData, pAllocationCallbacks); + } + } else { + /* Don't have a metadata callback, so just skip the tag. */ + if (onSeek != NULL) { + if (!onSeek(pUserData, tagSize, DRMP3_SEEK_CUR)) { + return DRMP3_FALSE; /* Failed to seek past the ID3v2 tag. */ + } + } else { + /* Don't have a seek callback. Read and discard. */ + char discard[1024]; + + while (tagSize > 0) { + size_t bytesToRead = tagSize; + if (bytesToRead > sizeof(discard)) { + bytesToRead = sizeof(discard); + } + + if (onRead(pUserData, discard, bytesToRead) != bytesToRead) { + return DRMP3_FALSE; /* Failed to read data. */ + } + + tagSize -= (drmp3_uint32)bytesToRead; + } + } + } + + pMP3->streamStartOffset += 10 + tagSize; /* +10 for the header. */ + pMP3->streamCursor = pMP3->streamStartOffset; + } else { + /* Not an ID3v2 tag. Seek back to the start. */ + if (onSeek != NULL) { + if (!onSeek(pUserData, 0, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek back to the start. */ + } + } else { + /* Don't have a seek callback to move backwards. We'll just fall through and let the decoding process re-sync. The ideal solution here would be to read into the cache. */ + + /* + TODO: Copy the header into the cache. Will need to allocate space. See drmp3_decode_next_frame_ex__callbacks. There is not need + to handle the memory case because that will always have a seek implementation and will never hit this code path. + */ + } + } + } else { + /* Failed to read the header. We can return false here. If we couldn't read 10 bytes there's no way we'll have a valid MP3 stream. */ + return DRMP3_FALSE; + } + } + #endif + + /* + Decode the first frame to confirm that it is indeed a valid MP3 stream. Note that it's possible the first frame + is actually a Xing/LAME/VBRI header. If this is the case we need to skip over it. + */ + firstFramePCMFrameCount = drmp3_decode_next_frame_ex(pMP3, (drmp3d_sample_t*)pMP3->pcmFrames, &firstFrameInfo, &pFirstFrameData); + if (firstFramePCMFrameCount > 0) { + DRMP3_ASSERT(pFirstFrameData != NULL); + + /* + It might be a header. If so, we need to clear out the cached PCM frames in order to trigger a reload of fresh + data when decoding starts. We can assume all validation has already been performed to check if this is a valid + MP3 frame and that there is more than 0 bytes making up the frame. + + We're going to be basing this parsing code off the minimp3_ex implementation. + */ + #if 1 + DRMP3_ASSERT(firstFrameInfo.frame_bytes > 0); + { + drmp3_bs bs; + drmp3_L3_gr_info grInfo[4]; + + drmp3_bs_init(&bs, pFirstFrameData + DRMP3_HDR_SIZE, firstFrameInfo.frame_bytes - DRMP3_HDR_SIZE); + + if (DRMP3_HDR_IS_CRC(pFirstFrameData)) { + drmp3_bs_get_bits(&bs, 16); /* CRC. */ + } + + if (drmp3_L3_read_side_info(&bs, grInfo, pFirstFrameData) >= 0) { + drmp3_bool32 isXing = DRMP3_FALSE; + drmp3_bool32 isInfo = DRMP3_FALSE; + const drmp3_uint8* pTagData; + const drmp3_uint8* pTagDataBeg; + const void* pDataBufferEnd = NULL; + size_t frameBytes; + + pTagDataBeg = pFirstFrameData + DRMP3_HDR_SIZE + (bs.pos/8); + pTagData = pTagDataBeg; + + /* + We need to determine how many bytes are actually available in pTagData. Unfortunately this is different depending on + whether or not it's being decoded from memory or callbacks. + */ + if (pMP3->memory.pData != NULL && pMP3->memory.dataSize > 0) { + pDataBufferEnd = pMP3->memory.pData + pMP3->memory.dataSize; + } else { + pDataBufferEnd = pMP3->pData + pMP3->dataCapacity; + } + + frameBytes = DRMP3_MIN((size_t)firstFrameInfo.frame_bytes, (size_t)((drmp3_uint8*)pDataBufferEnd - pFirstFrameData)); + + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 8) { + goto done_xing_info; /* Frame too small for a Xing/Info tag. */ + } + + /* Check for both "Xing" and "Info" identifiers. */ + isXing = (pTagData[0] == 'X' && pTagData[1] == 'i' && pTagData[2] == 'n' && pTagData[3] == 'g'); + isInfo = (pTagData[0] == 'I' && pTagData[1] == 'n' && pTagData[2] == 'f' && pTagData[3] == 'o'); + + if (isXing || isInfo) { + drmp3_uint32 bytes = 0; + drmp3_uint32 flags = pTagData[7]; + + pTagData += 8; /* Skip past the ID and flags. */ + + if (flags & 0x01) { /* FRAMES flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 4) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + detectedMP3FrameCount = (drmp3_uint32)pTagData[0] << 24 | (drmp3_uint32)pTagData[1] << 16 | (drmp3_uint32)pTagData[2] << 8 | (drmp3_uint32)pTagData[3]; + pTagData += 4; + } + + if (flags & 0x02) { /* BYTES flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 4) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + bytes = (drmp3_uint32)pTagData[0] << 24 | (drmp3_uint32)pTagData[1] << 16 | (drmp3_uint32)pTagData[2] << 8 | (drmp3_uint32)pTagData[3]; + (void)bytes; /* <-- Just to silence a warning about `bytes` being assigned but unused. Want to leave this here in case I want to make use of it later. */ + pTagData += 4; + } + + if (flags & 0x04) { /* TOC flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 100) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + /* TODO: Extract and bind seek points. */ + pTagData += 100; + } + + if (flags & 0x08) { /* SCALE flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 4) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + pTagData += 4; + } + + /* At this point we're done with the Xing/Info header. Now we can look at the LAME data. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 1) { + goto done_xing_info; /* Not enough data left to check for a LAME header. */ + } + + if (pTagData[0]) { + int delayInPCMFrames; + int paddingInPCMFrames; + + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 36) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + pTagData += 21; + + delayInPCMFrames = (( (drmp3_uint32)pTagData[0] << 4) | ((drmp3_uint32)pTagData[1] >> 4)) + (528 + 1); + paddingInPCMFrames = ((((drmp3_uint32)pTagData[1] & 0xF) << 8) | ((drmp3_uint32)pTagData[2] )) - (528 + 1); + if (paddingInPCMFrames < 0) { + paddingInPCMFrames = 0; /* Padding cannot be negative. Probably a malformed file. Ignore. */ + } + + pMP3->delayInPCMFrames = (drmp3_uint32)delayInPCMFrames; + pMP3->paddingInPCMFrames = (drmp3_uint32)paddingInPCMFrames; + } + + /* + My understanding is that if the "Xing" header is present we can consider this to be a VBR stream and if the "Info" header is + present it's a CBR stream. If this is not the case let me know! I'm just tracking this for the time being in case I want to + look at doing some CBR optimizations later on, such as faster seeking. + */ + if (isXing) { + pMP3->isVBR = DRMP3_TRUE; + } else if (isInfo) { + pMP3->isCBR = DRMP3_TRUE; + } + + /* Post the raw data of the tag to the metadata callback. */ + if (onMeta != NULL) { + drmp3_metadata_type metadataType = isXing ? DRMP3_METADATA_TYPE_XING : DRMP3_METADATA_TYPE_VBRI; + size_t tagDataSize; + + tagDataSize = (size_t)firstFrameInfo.frame_bytes; + tagDataSize -= (size_t)(pTagDataBeg - pFirstFrameData); + + drmp3__on_meta(pMP3, metadataType, pTagDataBeg, tagDataSize); + } + + /* Since this was identified as a tag, we don't want to treat it as audio. We need to clear out the PCM cache. */ + pMP3->pcmFramesRemainingInMP3Frame = 0; + + /* The start offset needs to be moved to the end of this frame so it's not included in any audio processing after seeking. */ + pMP3->streamStartOffset += (drmp3_uint32)(firstFrameInfo.frame_bytes); + pMP3->streamCursor = pMP3->streamStartOffset; + + /* + The internal decoder needs to be reset to clear out any state. If we don't reset this state, it's possible for + there to be inconsistencies in the number of samples read when reading to the end of the stream depending on + whether or not the caller seeks to the start of the stream. + */ + drmp3dec_init(&pMP3->decoder); + } + + done_xing_info:; + } else { + /* Failed to read the side info. */ + } + } + #endif + } else { + /* Not a valid MP3 stream. */ + drmp3__free_from_callbacks(pMP3->pData, &pMP3->allocationCallbacks); /* The call above may have allocated memory. Need to make sure it's freed before aborting. */ + return DRMP3_FALSE; + } + + if (detectedMP3FrameCount != 0xFFFFFFFF) { + pMP3->totalPCMFrameCount = (drmp3_uint64)detectedMP3FrameCount * firstFramePCMFrameCount; + } + + pMP3->channels = pMP3->mp3FrameChannels; + pMP3->sampleRate = pMP3->mp3FrameSampleRate; + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init(drmp3* pMP3, drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, drmp3_meta_proc onMeta, void* pUserData, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pMP3 == NULL || onRead == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + return drmp3_init_internal(pMP3, onRead, onSeek, onTell, onMeta, pUserData, pUserData, pAllocationCallbacks); +} + + +static size_t drmp3__on_read_memory(void* pUserData, void* pBufferOut, size_t bytesToRead) +{ + drmp3* pMP3 = (drmp3*)pUserData; + size_t bytesRemaining; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->memory.dataSize >= pMP3->memory.currentReadPos); + + bytesRemaining = pMP3->memory.dataSize - pMP3->memory.currentReadPos; + if (bytesToRead > bytesRemaining) { + bytesToRead = bytesRemaining; + } + + if (bytesToRead > 0) { + DRMP3_COPY_MEMORY(pBufferOut, pMP3->memory.pData + pMP3->memory.currentReadPos, bytesToRead); + pMP3->memory.currentReadPos += bytesToRead; + } + + return bytesToRead; +} + +static drmp3_bool32 drmp3__on_seek_memory(void* pUserData, int byteOffset, drmp3_seek_origin origin) +{ + drmp3* pMP3 = (drmp3*)pUserData; + drmp3_int64 newCursor; + + DRMP3_ASSERT(pMP3 != NULL); + + if (origin == DRMP3_SEEK_SET) { + newCursor = 0; + } else if (origin == DRMP3_SEEK_CUR) { + newCursor = (drmp3_int64)pMP3->memory.currentReadPos; + } else if (origin == DRMP3_SEEK_END) { + newCursor = (drmp3_int64)pMP3->memory.dataSize; + } else { + DRMP3_ASSERT(!"Invalid seek origin"); + return DRMP3_FALSE; + } + + newCursor += byteOffset; + + if (newCursor < 0) { + return DRMP3_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > pMP3->memory.dataSize) { + return DRMP3_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + pMP3->memory.currentReadPos = (size_t)newCursor; + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3__on_tell_memory(void* pUserData, drmp3_int64* pCursor) +{ + drmp3* pMP3 = (drmp3*)pUserData; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pCursor != NULL); + + *pCursor = (drmp3_int64)pMP3->memory.currentReadPos; + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_memory_with_metadata(drmp3* pMP3, const void* pData, size_t dataSize, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3_bool32 result; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + + if (pData == NULL || dataSize == 0) { + return DRMP3_FALSE; + } + + pMP3->memory.pData = (const drmp3_uint8*)pData; + pMP3->memory.dataSize = dataSize; + pMP3->memory.currentReadPos = 0; + + result = drmp3_init_internal(pMP3, drmp3__on_read_memory, drmp3__on_seek_memory, drmp3__on_tell_memory, onMeta, pMP3, pUserDataMeta, pAllocationCallbacks); + if (result == DRMP3_FALSE) { + return DRMP3_FALSE; + } + + /* Adjust the length of the memory stream to account for ID3v1 and APE tags. */ + if (pMP3->streamLength <= (drmp3_uint64)DRMP3_SIZE_MAX) { + pMP3->memory.dataSize = (size_t)pMP3->streamLength; /* Safe cast. */ + } + + if (pMP3->streamStartOffset > (drmp3_uint64)DRMP3_SIZE_MAX) { + return DRMP3_FALSE; /* Tags too big. */ + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_memory(drmp3* pMP3, const void* pData, size_t dataSize, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + return drmp3_init_memory_with_metadata(pMP3, pData, dataSize, NULL, NULL, pAllocationCallbacks); +} + + +#ifndef DR_MP3_NO_STDIO +#include +#include /* For wcslen(), wcsrtombs() */ + +/* Errno */ +/* drmp3_result_from_errno() is only used inside DR_MP3_NO_STDIO for now. Move this out if it's ever used elsewhere. */ +#include +static drmp3_result drmp3_result_from_errno(int e) +{ + switch (e) + { + case 0: return DRMP3_SUCCESS; + #ifdef EPERM + case EPERM: return DRMP3_INVALID_OPERATION; + #endif + #ifdef ENOENT + case ENOENT: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef ESRCH + case ESRCH: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef EINTR + case EINTR: return DRMP3_INTERRUPT; + #endif + #ifdef EIO + case EIO: return DRMP3_IO_ERROR; + #endif + #ifdef ENXIO + case ENXIO: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef E2BIG + case E2BIG: return DRMP3_INVALID_ARGS; + #endif + #ifdef ENOEXEC + case ENOEXEC: return DRMP3_INVALID_FILE; + #endif + #ifdef EBADF + case EBADF: return DRMP3_INVALID_FILE; + #endif + #ifdef ECHILD + case ECHILD: return DRMP3_ERROR; + #endif + #ifdef EAGAIN + case EAGAIN: return DRMP3_UNAVAILABLE; + #endif + #ifdef ENOMEM + case ENOMEM: return DRMP3_OUT_OF_MEMORY; + #endif + #ifdef EACCES + case EACCES: return DRMP3_ACCESS_DENIED; + #endif + #ifdef EFAULT + case EFAULT: return DRMP3_BAD_ADDRESS; + #endif + #ifdef ENOTBLK + case ENOTBLK: return DRMP3_ERROR; + #endif + #ifdef EBUSY + case EBUSY: return DRMP3_BUSY; + #endif + #ifdef EEXIST + case EEXIST: return DRMP3_ALREADY_EXISTS; + #endif + #ifdef EXDEV + case EXDEV: return DRMP3_ERROR; + #endif + #ifdef ENODEV + case ENODEV: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef ENOTDIR + case ENOTDIR: return DRMP3_NOT_DIRECTORY; + #endif + #ifdef EISDIR + case EISDIR: return DRMP3_IS_DIRECTORY; + #endif + #ifdef EINVAL + case EINVAL: return DRMP3_INVALID_ARGS; + #endif + #ifdef ENFILE + case ENFILE: return DRMP3_TOO_MANY_OPEN_FILES; + #endif + #ifdef EMFILE + case EMFILE: return DRMP3_TOO_MANY_OPEN_FILES; + #endif + #ifdef ENOTTY + case ENOTTY: return DRMP3_INVALID_OPERATION; + #endif + #ifdef ETXTBSY + case ETXTBSY: return DRMP3_BUSY; + #endif + #ifdef EFBIG + case EFBIG: return DRMP3_TOO_BIG; + #endif + #ifdef ENOSPC + case ENOSPC: return DRMP3_NO_SPACE; + #endif + #ifdef ESPIPE + case ESPIPE: return DRMP3_BAD_SEEK; + #endif + #ifdef EROFS + case EROFS: return DRMP3_ACCESS_DENIED; + #endif + #ifdef EMLINK + case EMLINK: return DRMP3_TOO_MANY_LINKS; + #endif + #ifdef EPIPE + case EPIPE: return DRMP3_BAD_PIPE; + #endif + #ifdef EDOM + case EDOM: return DRMP3_OUT_OF_RANGE; + #endif + #ifdef ERANGE + case ERANGE: return DRMP3_OUT_OF_RANGE; + #endif + #ifdef EDEADLK + case EDEADLK: return DRMP3_DEADLOCK; + #endif + #ifdef ENAMETOOLONG + case ENAMETOOLONG: return DRMP3_PATH_TOO_LONG; + #endif + #ifdef ENOLCK + case ENOLCK: return DRMP3_ERROR; + #endif + #ifdef ENOSYS + case ENOSYS: return DRMP3_NOT_IMPLEMENTED; + #endif + #if defined(ENOTEMPTY) && ENOTEMPTY != EEXIST /* In AIX, ENOTEMPTY and EEXIST use the same value. */ + case ENOTEMPTY: return DRMP3_DIRECTORY_NOT_EMPTY; + #endif + #ifdef ELOOP + case ELOOP: return DRMP3_TOO_MANY_LINKS; + #endif + #ifdef ENOMSG + case ENOMSG: return DRMP3_NO_MESSAGE; + #endif + #ifdef EIDRM + case EIDRM: return DRMP3_ERROR; + #endif + #ifdef ECHRNG + case ECHRNG: return DRMP3_ERROR; + #endif + #ifdef EL2NSYNC + case EL2NSYNC: return DRMP3_ERROR; + #endif + #ifdef EL3HLT + case EL3HLT: return DRMP3_ERROR; + #endif + #ifdef EL3RST + case EL3RST: return DRMP3_ERROR; + #endif + #ifdef ELNRNG + case ELNRNG: return DRMP3_OUT_OF_RANGE; + #endif + #ifdef EUNATCH + case EUNATCH: return DRMP3_ERROR; + #endif + #ifdef ENOCSI + case ENOCSI: return DRMP3_ERROR; + #endif + #ifdef EL2HLT + case EL2HLT: return DRMP3_ERROR; + #endif + #ifdef EBADE + case EBADE: return DRMP3_ERROR; + #endif + #ifdef EBADR + case EBADR: return DRMP3_ERROR; + #endif + #ifdef EXFULL + case EXFULL: return DRMP3_ERROR; + #endif + #ifdef ENOANO + case ENOANO: return DRMP3_ERROR; + #endif + #ifdef EBADRQC + case EBADRQC: return DRMP3_ERROR; + #endif + #ifdef EBADSLT + case EBADSLT: return DRMP3_ERROR; + #endif + #ifdef EBFONT + case EBFONT: return DRMP3_INVALID_FILE; + #endif + #ifdef ENOSTR + case ENOSTR: return DRMP3_ERROR; + #endif + #ifdef ENODATA + case ENODATA: return DRMP3_NO_DATA_AVAILABLE; + #endif + #ifdef ETIME + case ETIME: return DRMP3_TIMEOUT; + #endif + #ifdef ENOSR + case ENOSR: return DRMP3_NO_DATA_AVAILABLE; + #endif + #ifdef ENONET + case ENONET: return DRMP3_NO_NETWORK; + #endif + #ifdef ENOPKG + case ENOPKG: return DRMP3_ERROR; + #endif + #ifdef EREMOTE + case EREMOTE: return DRMP3_ERROR; + #endif + #ifdef ENOLINK + case ENOLINK: return DRMP3_ERROR; + #endif + #ifdef EADV + case EADV: return DRMP3_ERROR; + #endif + #ifdef ESRMNT + case ESRMNT: return DRMP3_ERROR; + #endif + #ifdef ECOMM + case ECOMM: return DRMP3_ERROR; + #endif + #ifdef EPROTO + case EPROTO: return DRMP3_ERROR; + #endif + #ifdef EMULTIHOP + case EMULTIHOP: return DRMP3_ERROR; + #endif + #ifdef EDOTDOT + case EDOTDOT: return DRMP3_ERROR; + #endif + #ifdef EBADMSG + case EBADMSG: return DRMP3_BAD_MESSAGE; + #endif + #ifdef EOVERFLOW + case EOVERFLOW: return DRMP3_TOO_BIG; + #endif + #ifdef ENOTUNIQ + case ENOTUNIQ: return DRMP3_NOT_UNIQUE; + #endif + #ifdef EBADFD + case EBADFD: return DRMP3_ERROR; + #endif + #ifdef EREMCHG + case EREMCHG: return DRMP3_ERROR; + #endif + #ifdef ELIBACC + case ELIBACC: return DRMP3_ACCESS_DENIED; + #endif + #ifdef ELIBBAD + case ELIBBAD: return DRMP3_INVALID_FILE; + #endif + #ifdef ELIBSCN + case ELIBSCN: return DRMP3_INVALID_FILE; + #endif + #ifdef ELIBMAX + case ELIBMAX: return DRMP3_ERROR; + #endif + #ifdef ELIBEXEC + case ELIBEXEC: return DRMP3_ERROR; + #endif + #ifdef EILSEQ + case EILSEQ: return DRMP3_INVALID_DATA; + #endif + #ifdef ERESTART + case ERESTART: return DRMP3_ERROR; + #endif + #ifdef ESTRPIPE + case ESTRPIPE: return DRMP3_ERROR; + #endif + #ifdef EUSERS + case EUSERS: return DRMP3_ERROR; + #endif + #ifdef ENOTSOCK + case ENOTSOCK: return DRMP3_NOT_SOCKET; + #endif + #ifdef EDESTADDRREQ + case EDESTADDRREQ: return DRMP3_NO_ADDRESS; + #endif + #ifdef EMSGSIZE + case EMSGSIZE: return DRMP3_TOO_BIG; + #endif + #ifdef EPROTOTYPE + case EPROTOTYPE: return DRMP3_BAD_PROTOCOL; + #endif + #ifdef ENOPROTOOPT + case ENOPROTOOPT: return DRMP3_PROTOCOL_UNAVAILABLE; + #endif + #ifdef EPROTONOSUPPORT + case EPROTONOSUPPORT: return DRMP3_PROTOCOL_NOT_SUPPORTED; + #endif + #ifdef ESOCKTNOSUPPORT + case ESOCKTNOSUPPORT: return DRMP3_SOCKET_NOT_SUPPORTED; + #endif + #ifdef EOPNOTSUPP + case EOPNOTSUPP: return DRMP3_INVALID_OPERATION; + #endif + #ifdef EPFNOSUPPORT + case EPFNOSUPPORT: return DRMP3_PROTOCOL_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EAFNOSUPPORT + case EAFNOSUPPORT: return DRMP3_ADDRESS_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EADDRINUSE + case EADDRINUSE: return DRMP3_ALREADY_IN_USE; + #endif + #ifdef EADDRNOTAVAIL + case EADDRNOTAVAIL: return DRMP3_ERROR; + #endif + #ifdef ENETDOWN + case ENETDOWN: return DRMP3_NO_NETWORK; + #endif + #ifdef ENETUNREACH + case ENETUNREACH: return DRMP3_NO_NETWORK; + #endif + #ifdef ENETRESET + case ENETRESET: return DRMP3_NO_NETWORK; + #endif + #ifdef ECONNABORTED + case ECONNABORTED: return DRMP3_NO_NETWORK; + #endif + #ifdef ECONNRESET + case ECONNRESET: return DRMP3_CONNECTION_RESET; + #endif + #ifdef ENOBUFS + case ENOBUFS: return DRMP3_NO_SPACE; + #endif + #ifdef EISCONN + case EISCONN: return DRMP3_ALREADY_CONNECTED; + #endif + #ifdef ENOTCONN + case ENOTCONN: return DRMP3_NOT_CONNECTED; + #endif + #ifdef ESHUTDOWN + case ESHUTDOWN: return DRMP3_ERROR; + #endif + #ifdef ETOOMANYREFS + case ETOOMANYREFS: return DRMP3_ERROR; + #endif + #ifdef ETIMEDOUT + case ETIMEDOUT: return DRMP3_TIMEOUT; + #endif + #ifdef ECONNREFUSED + case ECONNREFUSED: return DRMP3_CONNECTION_REFUSED; + #endif + #ifdef EHOSTDOWN + case EHOSTDOWN: return DRMP3_NO_HOST; + #endif + #ifdef EHOSTUNREACH + case EHOSTUNREACH: return DRMP3_NO_HOST; + #endif + #ifdef EALREADY + case EALREADY: return DRMP3_IN_PROGRESS; + #endif + #ifdef EINPROGRESS + case EINPROGRESS: return DRMP3_IN_PROGRESS; + #endif + #ifdef ESTALE + case ESTALE: return DRMP3_INVALID_FILE; + #endif + #ifdef EUCLEAN + case EUCLEAN: return DRMP3_ERROR; + #endif + #ifdef ENOTNAM + case ENOTNAM: return DRMP3_ERROR; + #endif + #ifdef ENAVAIL + case ENAVAIL: return DRMP3_ERROR; + #endif + #ifdef EISNAM + case EISNAM: return DRMP3_ERROR; + #endif + #ifdef EREMOTEIO + case EREMOTEIO: return DRMP3_IO_ERROR; + #endif + #ifdef EDQUOT + case EDQUOT: return DRMP3_NO_SPACE; + #endif + #ifdef ENOMEDIUM + case ENOMEDIUM: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef EMEDIUMTYPE + case EMEDIUMTYPE: return DRMP3_ERROR; + #endif + #ifdef ECANCELED + case ECANCELED: return DRMP3_CANCELLED; + #endif + #ifdef ENOKEY + case ENOKEY: return DRMP3_ERROR; + #endif + #ifdef EKEYEXPIRED + case EKEYEXPIRED: return DRMP3_ERROR; + #endif + #ifdef EKEYREVOKED + case EKEYREVOKED: return DRMP3_ERROR; + #endif + #ifdef EKEYREJECTED + case EKEYREJECTED: return DRMP3_ERROR; + #endif + #ifdef EOWNERDEAD + case EOWNERDEAD: return DRMP3_ERROR; + #endif + #ifdef ENOTRECOVERABLE + case ENOTRECOVERABLE: return DRMP3_ERROR; + #endif + #ifdef ERFKILL + case ERFKILL: return DRMP3_ERROR; + #endif + #ifdef EHWPOISON + case EHWPOISON: return DRMP3_ERROR; + #endif + default: return DRMP3_ERROR; + } +} +/* End Errno */ + +/* fopen */ +static drmp3_result drmp3_fopen(FILE** ppFile, const char* pFilePath, const char* pOpenMode) +{ +#if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err; +#endif + + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRMP3_INVALID_ARGS; + } + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + err = fopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drmp3_result_from_errno(err); + } +#else +#if defined(_WIN32) || defined(__APPLE__) + *ppFile = fopen(pFilePath, pOpenMode); +#else + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 && defined(_LARGEFILE64_SOURCE) + *ppFile = fopen64(pFilePath, pOpenMode); + #else + *ppFile = fopen(pFilePath, pOpenMode); + #endif +#endif + if (*ppFile == NULL) { + drmp3_result result = drmp3_result_from_errno(errno); + if (result == DRMP3_SUCCESS) { + result = DRMP3_ERROR; /* Just a safety check to make sure we never ever return success when pFile == NULL. */ + } + + return result; + } +#endif + + return DRMP3_SUCCESS; +} + +/* +_wfopen() isn't always available in all compilation environments. + + * Windows only. + * MSVC seems to support it universally as far back as VC6 from what I can tell (haven't checked further back). + * MinGW-64 (both 32- and 64-bit) seems to support it. + * MinGW wraps it in !defined(__STRICT_ANSI__). + * OpenWatcom wraps it in !defined(_NO_EXT_KEYS). + +This can be reviewed as compatibility issues arise. The preference is to use _wfopen_s() and _wfopen() as opposed to the wcsrtombs() +fallback, so if you notice your compiler not detecting this properly I'm happy to look at adding support. +*/ +#if defined(_WIN32) + #if defined(_MSC_VER) || defined(__MINGW64__) || (!defined(__STRICT_ANSI__) && !defined(_NO_EXT_KEYS)) + #define DRMP3_HAS_WFOPEN + #endif +#endif + +static drmp3_result drmp3_wfopen(FILE** ppFile, const wchar_t* pFilePath, const wchar_t* pOpenMode, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRMP3_INVALID_ARGS; + } + +#if defined(DRMP3_HAS_WFOPEN) + { + /* Use _wfopen() on Windows. */ + #if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err = _wfopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drmp3_result_from_errno(err); + } + #else + *ppFile = _wfopen(pFilePath, pOpenMode); + if (*ppFile == NULL) { + return drmp3_result_from_errno(errno); + } + #endif + (void)pAllocationCallbacks; + } +#else + /* + Use fopen() on anything other than Windows. Requires a conversion. This is annoying because + fopen() is locale specific. The only real way I can think of to do this is with wcsrtombs(). Note + that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for + maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler + error I'll look into improving compatibility. + */ + + /* + Some compilers don't support wchar_t or wcsrtombs() which we're using below. In this case we just + need to abort with an error. If you encounter a compiler lacking such support, add it to this list + and submit a bug report and it'll be added to the library upstream. + */ + #if defined(__DJGPP__) + { + /* Nothing to do here. This will fall through to the error check below. */ + } + #else + { + mbstate_t mbs; + size_t lenMB; + const wchar_t* pFilePathTemp = pFilePath; + char* pFilePathMB = NULL; + char pOpenModeMB[32] = {0}; + + /* Get the length first. */ + DRMP3_ZERO_OBJECT(&mbs); + lenMB = wcsrtombs(NULL, &pFilePathTemp, 0, &mbs); + if (lenMB == (size_t)-1) { + return drmp3_result_from_errno(errno); + } + + pFilePathMB = (char*)drmp3__malloc_from_callbacks(lenMB + 1, pAllocationCallbacks); + if (pFilePathMB == NULL) { + return DRMP3_OUT_OF_MEMORY; + } + + pFilePathTemp = pFilePath; + DRMP3_ZERO_OBJECT(&mbs); + wcsrtombs(pFilePathMB, &pFilePathTemp, lenMB + 1, &mbs); + + /* The open mode should always consist of ASCII characters so we should be able to do a trivial conversion. */ + { + size_t i = 0; + for (;;) { + if (pOpenMode[i] == 0) { + pOpenModeMB[i] = '\0'; + break; + } + + pOpenModeMB[i] = (char)pOpenMode[i]; + i += 1; + } + } + + *ppFile = fopen(pFilePathMB, pOpenModeMB); + + drmp3__free_from_callbacks(pFilePathMB, pAllocationCallbacks); + } + #endif + + if (*ppFile == NULL) { + return DRMP3_ERROR; + } +#endif + + return DRMP3_SUCCESS; +} +/* End fopen */ + + +static size_t drmp3__on_read_stdio(void* pUserData, void* pBufferOut, size_t bytesToRead) +{ + return fread(pBufferOut, 1, bytesToRead, (FILE*)pUserData); +} + +static drmp3_bool32 drmp3__on_seek_stdio(void* pUserData, int offset, drmp3_seek_origin origin) +{ + int whence = SEEK_SET; + if (origin == DRMP3_SEEK_CUR) { + whence = SEEK_CUR; + } else if (origin == DRMP3_SEEK_END) { + whence = SEEK_END; + } + + return fseek((FILE*)pUserData, offset, whence) == 0; +} + +static drmp3_bool32 drmp3__on_tell_stdio(void* pUserData, drmp3_int64* pCursor) +{ + FILE* pFileStdio = (FILE*)pUserData; + drmp3_int64 result; + + /* These were all validated at a higher level. */ + DRMP3_ASSERT(pFileStdio != NULL); + DRMP3_ASSERT(pCursor != NULL); + +#if defined(_WIN32) && !defined(NXDK) + #if defined(_MSC_VER) && _MSC_VER > 1200 + result = _ftelli64(pFileStdio); + #else + result = ftell(pFileStdio); + #endif +#else + result = ftell(pFileStdio); +#endif + + *pCursor = result; + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata(drmp3* pMP3, const char* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3_bool32 result; + FILE* pFile; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + + if (drmp3_fopen(&pFile, pFilePath, "rb") != DRMP3_SUCCESS) { + return DRMP3_FALSE; + } + + result = drmp3_init_internal(pMP3, drmp3__on_read_stdio, drmp3__on_seek_stdio, drmp3__on_tell_stdio, onMeta, (void*)pFile, pUserDataMeta, pAllocationCallbacks); + if (result != DRMP3_TRUE) { + fclose(pFile); + return result; + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata_w(drmp3* pMP3, const wchar_t* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3_bool32 result; + FILE* pFile; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + + if (drmp3_wfopen(&pFile, pFilePath, L"rb", pAllocationCallbacks) != DRMP3_SUCCESS) { + return DRMP3_FALSE; + } + + result = drmp3_init_internal(pMP3, drmp3__on_read_stdio, drmp3__on_seek_stdio, drmp3__on_tell_stdio, onMeta, (void*)pFile, pUserDataMeta, pAllocationCallbacks); + if (result != DRMP3_TRUE) { + fclose(pFile); + return result; + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_file(drmp3* pMP3, const char* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + return drmp3_init_file_with_metadata(pMP3, pFilePath, NULL, NULL, pAllocationCallbacks); +} + +DRMP3_API drmp3_bool32 drmp3_init_file_w(drmp3* pMP3, const wchar_t* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + return drmp3_init_file_with_metadata_w(pMP3, pFilePath, NULL, NULL, pAllocationCallbacks); +} +#endif + +DRMP3_API void drmp3_uninit(drmp3* pMP3) +{ + if (pMP3 == NULL) { + return; + } + +#ifndef DR_MP3_NO_STDIO + if (pMP3->onRead == drmp3__on_read_stdio) { + FILE* pFile = (FILE*)pMP3->pUserData; + if (pFile != NULL) { + fclose(pFile); + pMP3->pUserData = NULL; /* Make sure the file handle is cleared to NULL to we don't attempt to close it a second time. */ + } + } +#endif + + drmp3__free_from_callbacks(pMP3->pData, &pMP3->allocationCallbacks); +} + +#if defined(DR_MP3_FLOAT_OUTPUT) +static void drmp3_f32_to_s16(drmp3_int16* dst, const float* src, drmp3_uint64 sampleCount) +{ + drmp3_uint64 i; + drmp3_uint64 i4; + drmp3_uint64 sampleCount4; + + /* Unrolled. */ + i = 0; + sampleCount4 = sampleCount >> 2; + for (i4 = 0; i4 < sampleCount4; i4 += 1) { + float x0 = src[i+0]; + float x1 = src[i+1]; + float x2 = src[i+2]; + float x3 = src[i+3]; + + x0 = ((x0 < -1) ? -1 : ((x0 > 1) ? 1 : x0)); + x1 = ((x1 < -1) ? -1 : ((x1 > 1) ? 1 : x1)); + x2 = ((x2 < -1) ? -1 : ((x2 > 1) ? 1 : x2)); + x3 = ((x3 < -1) ? -1 : ((x3 > 1) ? 1 : x3)); + + x0 = x0 * 32767.0f; + x1 = x1 * 32767.0f; + x2 = x2 * 32767.0f; + x3 = x3 * 32767.0f; + + dst[i+0] = (drmp3_int16)x0; + dst[i+1] = (drmp3_int16)x1; + dst[i+2] = (drmp3_int16)x2; + dst[i+3] = (drmp3_int16)x3; + + i += 4; + } + + /* Leftover. */ + for (; i < sampleCount; i += 1) { + float x = src[i]; + x = ((x < -1) ? -1 : ((x > 1) ? 1 : x)); /* clip */ + x = x * 32767.0f; /* -1..1 to -32767..32767 */ + + dst[i] = (drmp3_int16)x; + } +} +#endif + +#if !defined(DR_MP3_FLOAT_OUTPUT) +static void drmp3_s16_to_f32(float* dst, const drmp3_int16* src, drmp3_uint64 sampleCount) +{ + drmp3_uint64 i; + for (i = 0; i < sampleCount; i += 1) { + float x = (float)src[i]; + x = x * 0.000030517578125f; /* -32768..32767 to -1..0.999969482421875 */ + dst[i] = x; + } +} +#endif + + +static drmp3_uint64 drmp3_read_pcm_frames_raw(drmp3* pMP3, drmp3_uint64 framesToRead, void* pBufferOut) +{ + drmp3_uint64 totalFramesRead = 0; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + while (framesToRead > 0) { + drmp3_uint32 framesToConsume; + + /* Skip frames if necessary. */ + if (pMP3->currentPCMFrame < pMP3->delayInPCMFrames) { + drmp3_uint32 framesToSkip = (drmp3_uint32)DRMP3_MIN(pMP3->pcmFramesRemainingInMP3Frame, pMP3->delayInPCMFrames - pMP3->currentPCMFrame); + + pMP3->currentPCMFrame += framesToSkip; + pMP3->pcmFramesConsumedInMP3Frame += framesToSkip; + pMP3->pcmFramesRemainingInMP3Frame -= framesToSkip; + } + + framesToConsume = (drmp3_uint32)DRMP3_MIN(pMP3->pcmFramesRemainingInMP3Frame, framesToRead); + + /* Clamp the number of frames to read to the padding. */ + if (pMP3->totalPCMFrameCount != DRMP3_UINT64_MAX && pMP3->totalPCMFrameCount > pMP3->paddingInPCMFrames) { + if (pMP3->currentPCMFrame < (pMP3->totalPCMFrameCount - pMP3->paddingInPCMFrames)) { + drmp3_uint64 framesRemainigToPadding = (pMP3->totalPCMFrameCount - pMP3->paddingInPCMFrames) - pMP3->currentPCMFrame; + if (framesToConsume > framesRemainigToPadding) { + framesToConsume = (drmp3_uint32)framesRemainigToPadding; + } + } else { + /* We're into the padding. Abort. */ + break; + } + } + + if (pBufferOut != NULL) { + #if defined(DR_MP3_FLOAT_OUTPUT) + { + /* f32 */ + float* pFramesOutF32 = (float*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(float) * totalFramesRead * pMP3->channels); + float* pFramesInF32 = (float*)DRMP3_OFFSET_PTR(&pMP3->pcmFrames[0], sizeof(float) * pMP3->pcmFramesConsumedInMP3Frame * pMP3->mp3FrameChannels); + DRMP3_COPY_MEMORY(pFramesOutF32, pFramesInF32, sizeof(float) * framesToConsume * pMP3->channels); + } + #else + { + /* s16 */ + drmp3_int16* pFramesOutS16 = (drmp3_int16*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(drmp3_int16) * totalFramesRead * pMP3->channels); + drmp3_int16* pFramesInS16 = (drmp3_int16*)DRMP3_OFFSET_PTR(&pMP3->pcmFrames[0], sizeof(drmp3_int16) * pMP3->pcmFramesConsumedInMP3Frame * pMP3->mp3FrameChannels); + DRMP3_COPY_MEMORY(pFramesOutS16, pFramesInS16, sizeof(drmp3_int16) * framesToConsume * pMP3->channels); + } + #endif + } + + pMP3->currentPCMFrame += framesToConsume; + pMP3->pcmFramesConsumedInMP3Frame += framesToConsume; + pMP3->pcmFramesRemainingInMP3Frame -= framesToConsume; + totalFramesRead += framesToConsume; + framesToRead -= framesToConsume; + + if (framesToRead == 0) { + break; + } + + /* If the cursor is already at the padding we need to abort. */ + if (pMP3->totalPCMFrameCount != DRMP3_UINT64_MAX && pMP3->totalPCMFrameCount > pMP3->paddingInPCMFrames && pMP3->currentPCMFrame >= (pMP3->totalPCMFrameCount - pMP3->paddingInPCMFrames)) { + break; + } + + DRMP3_ASSERT(pMP3->pcmFramesRemainingInMP3Frame == 0); + + /* At this point we have exhausted our in-memory buffer so we need to re-fill. */ + if (drmp3_decode_next_frame(pMP3) == 0) { + break; + } + } + + return totalFramesRead; +} + + +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut) +{ + if (pMP3 == NULL || pMP3->onRead == NULL) { + return 0; + } + +#if defined(DR_MP3_FLOAT_OUTPUT) + /* Fast path. No conversion required. */ + return drmp3_read_pcm_frames_raw(pMP3, framesToRead, pBufferOut); +#else + /* Slow path. Convert from s16 to f32. */ + { + drmp3_int16 pTempS16[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + drmp3_uint64 totalPCMFramesRead = 0; + + while (totalPCMFramesRead < framesToRead) { + drmp3_uint64 framesJustRead; + drmp3_uint64 framesRemaining = framesToRead - totalPCMFramesRead; + drmp3_uint64 framesToReadNow = DRMP3_COUNTOF(pTempS16) / pMP3->channels; + if (framesToReadNow > framesRemaining) { + framesToReadNow = framesRemaining; + } + + framesJustRead = drmp3_read_pcm_frames_raw(pMP3, framesToReadNow, pTempS16); + if (framesJustRead == 0) { + break; + } + + drmp3_s16_to_f32((float*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(float) * totalPCMFramesRead * pMP3->channels), pTempS16, framesJustRead * pMP3->channels); + totalPCMFramesRead += framesJustRead; + } + + return totalPCMFramesRead; + } +#endif +} + +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_s16(drmp3* pMP3, drmp3_uint64 framesToRead, drmp3_int16* pBufferOut) +{ + if (pMP3 == NULL || pMP3->onRead == NULL) { + return 0; + } + +#if !defined(DR_MP3_FLOAT_OUTPUT) + /* Fast path. No conversion required. */ + return drmp3_read_pcm_frames_raw(pMP3, framesToRead, pBufferOut); +#else + /* Slow path. Convert from f32 to s16. */ + { + float pTempF32[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + drmp3_uint64 totalPCMFramesRead = 0; + + while (totalPCMFramesRead < framesToRead) { + drmp3_uint64 framesJustRead; + drmp3_uint64 framesRemaining = framesToRead - totalPCMFramesRead; + drmp3_uint64 framesToReadNow = DRMP3_COUNTOF(pTempF32) / pMP3->channels; + if (framesToReadNow > framesRemaining) { + framesToReadNow = framesRemaining; + } + + framesJustRead = drmp3_read_pcm_frames_raw(pMP3, framesToReadNow, pTempF32); + if (framesJustRead == 0) { + break; + } + + drmp3_f32_to_s16((drmp3_int16*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(drmp3_int16) * totalPCMFramesRead * pMP3->channels), pTempF32, framesJustRead * pMP3->channels); + totalPCMFramesRead += framesJustRead; + } + + return totalPCMFramesRead; + } +#endif +} + +static void drmp3_reset(drmp3* pMP3) +{ + DRMP3_ASSERT(pMP3 != NULL); + + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = 0; + pMP3->currentPCMFrame = 0; + pMP3->dataSize = 0; + pMP3->atEnd = DRMP3_FALSE; + drmp3dec_init(&pMP3->decoder); +} + +static drmp3_bool32 drmp3_seek_to_start_of_stream(drmp3* pMP3) +{ + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onSeek != NULL); + + /* Seek to the start of the stream to begin with. */ + if (!drmp3__on_seek_64(pMP3, pMP3->streamStartOffset, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; + } + + /* Clear any cached data. */ + drmp3_reset(pMP3); + return DRMP3_TRUE; +} + + +static drmp3_bool32 drmp3_seek_forward_by_pcm_frames__brute_force(drmp3* pMP3, drmp3_uint64 frameOffset) +{ + drmp3_uint64 framesRead; + + /* + Just using a dumb read-and-discard for now. What would be nice is to parse only the header of the MP3 frame, and then skip over leading + frames without spending the time doing a full decode. I cannot see an easy way to do this in minimp3, however, so it may involve some + kind of manual processing. + */ +#if defined(DR_MP3_FLOAT_OUTPUT) + framesRead = drmp3_read_pcm_frames_f32(pMP3, frameOffset, NULL); +#else + framesRead = drmp3_read_pcm_frames_s16(pMP3, frameOffset, NULL); +#endif + if (framesRead != frameOffset) { + return DRMP3_FALSE; + } + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3_seek_to_pcm_frame__brute_force(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + DRMP3_ASSERT(pMP3 != NULL); + + if (frameIndex == pMP3->currentPCMFrame) { + return DRMP3_TRUE; + } + + /* + If we're moving foward we just read from where we're at. Otherwise we need to move back to the start of + the stream and read from the beginning. + */ + if (frameIndex < pMP3->currentPCMFrame) { + /* Moving backward. Move to the start of the stream and then move forward. */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + } + + DRMP3_ASSERT(frameIndex >= pMP3->currentPCMFrame); + return drmp3_seek_forward_by_pcm_frames__brute_force(pMP3, (frameIndex - pMP3->currentPCMFrame)); +} + +static drmp3_bool32 drmp3_find_closest_seek_point(drmp3* pMP3, drmp3_uint64 frameIndex, drmp3_uint32* pSeekPointIndex) +{ + drmp3_uint32 iSeekPoint; + + DRMP3_ASSERT(pSeekPointIndex != NULL); + + *pSeekPointIndex = 0; + + if (frameIndex < pMP3->pSeekPoints[0].pcmFrameIndex) { + return DRMP3_FALSE; + } + + /* Linear search for simplicity to begin with while I'm getting this thing working. Once it's all working change this to a binary search. */ + for (iSeekPoint = 0; iSeekPoint < pMP3->seekPointCount; ++iSeekPoint) { + if (pMP3->pSeekPoints[iSeekPoint].pcmFrameIndex > frameIndex) { + break; /* Found it. */ + } + + *pSeekPointIndex = iSeekPoint; + } + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3_seek_to_pcm_frame__seek_table(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + drmp3_seek_point seekPoint; + drmp3_uint32 priorSeekPointIndex; + drmp3_uint16 iMP3Frame; + drmp3_uint64 leftoverFrames; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->pSeekPoints != NULL); + DRMP3_ASSERT(pMP3->seekPointCount > 0); + + /* If there is no prior seekpoint it means the target PCM frame comes before the first seek point. Just assume a seekpoint at the start of the file in this case. */ + if (drmp3_find_closest_seek_point(pMP3, frameIndex, &priorSeekPointIndex)) { + seekPoint = pMP3->pSeekPoints[priorSeekPointIndex]; + } else { + seekPoint.seekPosInBytes = 0; + seekPoint.pcmFrameIndex = 0; + seekPoint.mp3FramesToDiscard = 0; + seekPoint.pcmFramesToDiscard = 0; + } + + /* First thing to do is seek to the first byte of the relevant MP3 frame. */ + if (!drmp3__on_seek_64(pMP3, seekPoint.seekPosInBytes, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek. */ + } + + /* Clear any cached data. */ + drmp3_reset(pMP3); + + /* Whole MP3 frames need to be discarded first. */ + for (iMP3Frame = 0; iMP3Frame < seekPoint.mp3FramesToDiscard; ++iMP3Frame) { + drmp3_uint32 pcmFramesRead; + drmp3d_sample_t* pPCMFrames; + + /* Pass in non-null for the last frame because we want to ensure the sample rate converter is preloaded correctly. */ + pPCMFrames = NULL; + if (iMP3Frame == seekPoint.mp3FramesToDiscard-1) { + pPCMFrames = (drmp3d_sample_t*)pMP3->pcmFrames; + } + + /* We first need to decode the next frame. */ + pcmFramesRead = drmp3_decode_next_frame_ex(pMP3, pPCMFrames, NULL, NULL); + if (pcmFramesRead == 0) { + return DRMP3_FALSE; + } + } + + /* We seeked to an MP3 frame in the raw stream so we need to make sure the current PCM frame is set correctly. */ + pMP3->currentPCMFrame = seekPoint.pcmFrameIndex - seekPoint.pcmFramesToDiscard; + + /* + Now at this point we can follow the same process as the brute force technique where we just skip over unnecessary MP3 frames and then + read-and-discard at least 2 whole MP3 frames. + */ + leftoverFrames = frameIndex - pMP3->currentPCMFrame; + return drmp3_seek_forward_by_pcm_frames__brute_force(pMP3, leftoverFrames); +} + +DRMP3_API drmp3_bool32 drmp3_seek_to_pcm_frame(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + if (pMP3 == NULL || pMP3->onSeek == NULL) { + return DRMP3_FALSE; + } + + if (frameIndex == 0) { + return drmp3_seek_to_start_of_stream(pMP3); + } + + /* Use the seek table if we have one. */ + if (pMP3->pSeekPoints != NULL && pMP3->seekPointCount > 0) { + return drmp3_seek_to_pcm_frame__seek_table(pMP3, frameIndex); + } else { + return drmp3_seek_to_pcm_frame__brute_force(pMP3, frameIndex); + } +} + +DRMP3_API drmp3_bool32 drmp3_get_mp3_and_pcm_frame_count(drmp3* pMP3, drmp3_uint64* pMP3FrameCount, drmp3_uint64* pPCMFrameCount) +{ + drmp3_uint64 currentPCMFrame; + drmp3_uint64 totalPCMFrameCount; + drmp3_uint64 totalMP3FrameCount; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + /* + The way this works is we move back to the start of the stream, iterate over each MP3 frame and calculate the frame count based + on our output sample rate, the seek back to the PCM frame we were sitting on before calling this function. + */ + + /* The stream must support seeking for this to work. */ + if (pMP3->onSeek == NULL) { + return DRMP3_FALSE; + } + + /* We'll need to seek back to where we were, so grab the PCM frame we're currently sitting on so we can restore later. */ + currentPCMFrame = pMP3->currentPCMFrame; + + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + + totalPCMFrameCount = 0; + totalMP3FrameCount = 0; + + for (;;) { + drmp3_uint32 pcmFramesInCurrentMP3Frame; + + pcmFramesInCurrentMP3Frame = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFramesInCurrentMP3Frame == 0) { + break; + } + + totalPCMFrameCount += pcmFramesInCurrentMP3Frame; + totalMP3FrameCount += 1; + } + + /* Finally, we need to seek back to where we were. */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + + if (!drmp3_seek_to_pcm_frame(pMP3, currentPCMFrame)) { + return DRMP3_FALSE; + } + + if (pMP3FrameCount != NULL) { + *pMP3FrameCount = totalMP3FrameCount; + } + if (pPCMFrameCount != NULL) { + *pPCMFrameCount = totalPCMFrameCount; + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_uint64 drmp3_get_pcm_frame_count(drmp3* pMP3) +{ + drmp3_uint64 totalPCMFrameCount; + + if (pMP3 == NULL) { + return 0; + } + + if (pMP3->totalPCMFrameCount != DRMP3_UINT64_MAX) { + totalPCMFrameCount = pMP3->totalPCMFrameCount; + + if (totalPCMFrameCount >= pMP3->delayInPCMFrames) { + totalPCMFrameCount -= pMP3->delayInPCMFrames; + } else { + /* The delay is greater than the frame count reported by the Xing/Info tag. Assume it's invalid and ignore. */ + } + + if (totalPCMFrameCount >= pMP3->paddingInPCMFrames) { + totalPCMFrameCount -= pMP3->paddingInPCMFrames; + } else { + /* The padding is greater than the frame count reported by the Xing/Info tag. Assume it's invalid and ignore. */ + } + + return totalPCMFrameCount; + } else { + /* Unknown frame count. Need to calculate it. */ + if (!drmp3_get_mp3_and_pcm_frame_count(pMP3, NULL, &totalPCMFrameCount)) { + return 0; + } + + return totalPCMFrameCount; + } +} + +DRMP3_API drmp3_uint64 drmp3_get_mp3_frame_count(drmp3* pMP3) +{ + drmp3_uint64 totalMP3FrameCount; + if (!drmp3_get_mp3_and_pcm_frame_count(pMP3, &totalMP3FrameCount, NULL)) { + return 0; + } + + return totalMP3FrameCount; +} + +static void drmp3__accumulate_running_pcm_frame_count(drmp3* pMP3, drmp3_uint32 pcmFrameCountIn, drmp3_uint64* pRunningPCMFrameCount, float* pRunningPCMFrameCountFractionalPart) +{ + float srcRatio; + float pcmFrameCountOutF; + drmp3_uint32 pcmFrameCountOut; + + srcRatio = (float)pMP3->mp3FrameSampleRate / (float)pMP3->sampleRate; + DRMP3_ASSERT(srcRatio > 0); + + pcmFrameCountOutF = *pRunningPCMFrameCountFractionalPart + (pcmFrameCountIn / srcRatio); + pcmFrameCountOut = (drmp3_uint32)pcmFrameCountOutF; + *pRunningPCMFrameCountFractionalPart = pcmFrameCountOutF - pcmFrameCountOut; + *pRunningPCMFrameCount += pcmFrameCountOut; +} + +typedef struct +{ + drmp3_uint64 bytePos; + drmp3_uint64 pcmFrameIndex; /* <-- After sample rate conversion. */ +} drmp3__seeking_mp3_frame_info; + +DRMP3_API drmp3_bool32 drmp3_calculate_seek_points(drmp3* pMP3, drmp3_uint32* pSeekPointCount, drmp3_seek_point* pSeekPoints) +{ + drmp3_uint32 seekPointCount; + drmp3_uint64 currentPCMFrame; + drmp3_uint64 totalMP3FrameCount; + drmp3_uint64 totalPCMFrameCount; + + if (pMP3 == NULL || pSeekPointCount == NULL || pSeekPoints == NULL) { + return DRMP3_FALSE; /* Invalid args. */ + } + + seekPointCount = *pSeekPointCount; + if (seekPointCount == 0) { + return DRMP3_FALSE; /* The client has requested no seek points. Consider this to be invalid arguments since the client has probably not intended this. */ + } + + /* We'll need to seek back to the current sample after calculating the seekpoints so we need to go ahead and grab the current location at the top. */ + currentPCMFrame = pMP3->currentPCMFrame; + + /* We never do more than the total number of MP3 frames and we limit it to 32-bits. */ + if (!drmp3_get_mp3_and_pcm_frame_count(pMP3, &totalMP3FrameCount, &totalPCMFrameCount)) { + return DRMP3_FALSE; + } + + /* If there's less than DRMP3_SEEK_LEADING_MP3_FRAMES+1 frames we just report 1 seek point which will be the very start of the stream. */ + if (totalMP3FrameCount < DRMP3_SEEK_LEADING_MP3_FRAMES+1) { + seekPointCount = 1; + pSeekPoints[0].seekPosInBytes = 0; + pSeekPoints[0].pcmFrameIndex = 0; + pSeekPoints[0].mp3FramesToDiscard = 0; + pSeekPoints[0].pcmFramesToDiscard = 0; + } else { + drmp3_uint64 pcmFramesBetweenSeekPoints; + drmp3__seeking_mp3_frame_info mp3FrameInfo[DRMP3_SEEK_LEADING_MP3_FRAMES+1]; + drmp3_uint64 runningPCMFrameCount = 0; + float runningPCMFrameCountFractionalPart = 0; + drmp3_uint64 nextTargetPCMFrame; + drmp3_uint32 iMP3Frame; + drmp3_uint32 iSeekPoint; + + if (seekPointCount > totalMP3FrameCount-1) { + seekPointCount = (drmp3_uint32)totalMP3FrameCount-1; + } + + pcmFramesBetweenSeekPoints = totalPCMFrameCount / (seekPointCount+1); + + /* + Here is where we actually calculate the seek points. We need to start by moving the start of the stream. We then enumerate over each + MP3 frame. + */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + + /* + We need to cache the byte positions of the previous MP3 frames. As a new MP3 frame is iterated, we cycle the byte positions in this + array. The value in the first item in this array is the byte position that will be reported in the next seek point. + */ + + /* We need to initialize the array of MP3 byte positions for the leading MP3 frames. */ + for (iMP3Frame = 0; iMP3Frame < DRMP3_SEEK_LEADING_MP3_FRAMES+1; ++iMP3Frame) { + drmp3_uint32 pcmFramesInCurrentMP3FrameIn; + + /* The byte position of the next frame will be the stream's cursor position, minus whatever is sitting in the buffer. */ + DRMP3_ASSERT(pMP3->streamCursor >= pMP3->dataSize); + mp3FrameInfo[iMP3Frame].bytePos = pMP3->streamCursor - pMP3->dataSize; + mp3FrameInfo[iMP3Frame].pcmFrameIndex = runningPCMFrameCount; + + /* We need to get information about this frame so we can know how many samples it contained. */ + pcmFramesInCurrentMP3FrameIn = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFramesInCurrentMP3FrameIn == 0) { + return DRMP3_FALSE; /* This should never happen. */ + } + + drmp3__accumulate_running_pcm_frame_count(pMP3, pcmFramesInCurrentMP3FrameIn, &runningPCMFrameCount, &runningPCMFrameCountFractionalPart); + } + + /* + At this point we will have extracted the byte positions of the leading MP3 frames. We can now start iterating over each seek point and + calculate them. + */ + nextTargetPCMFrame = 0; + for (iSeekPoint = 0; iSeekPoint < seekPointCount; ++iSeekPoint) { + nextTargetPCMFrame += pcmFramesBetweenSeekPoints; + + for (;;) { + if (nextTargetPCMFrame < runningPCMFrameCount) { + /* The next seek point is in the current MP3 frame. */ + pSeekPoints[iSeekPoint].seekPosInBytes = mp3FrameInfo[0].bytePos; + pSeekPoints[iSeekPoint].pcmFrameIndex = nextTargetPCMFrame; + pSeekPoints[iSeekPoint].mp3FramesToDiscard = DRMP3_SEEK_LEADING_MP3_FRAMES; + pSeekPoints[iSeekPoint].pcmFramesToDiscard = (drmp3_uint16)(nextTargetPCMFrame - mp3FrameInfo[DRMP3_SEEK_LEADING_MP3_FRAMES-1].pcmFrameIndex); + break; + } else { + size_t i; + drmp3_uint32 pcmFramesInCurrentMP3FrameIn; + + /* + The next seek point is not in the current MP3 frame, so continue on to the next one. The first thing to do is cycle the cached + MP3 frame info. + */ + for (i = 0; i < DRMP3_COUNTOF(mp3FrameInfo)-1; ++i) { + mp3FrameInfo[i] = mp3FrameInfo[i+1]; + } + + /* Cache previous MP3 frame info. */ + mp3FrameInfo[DRMP3_COUNTOF(mp3FrameInfo)-1].bytePos = pMP3->streamCursor - pMP3->dataSize; + mp3FrameInfo[DRMP3_COUNTOF(mp3FrameInfo)-1].pcmFrameIndex = runningPCMFrameCount; + + /* + Go to the next MP3 frame. This shouldn't ever fail, but just in case it does we just set the seek point and break. If it happens, it + should only ever do it for the last seek point. + */ + pcmFramesInCurrentMP3FrameIn = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFramesInCurrentMP3FrameIn == 0) { + pSeekPoints[iSeekPoint].seekPosInBytes = mp3FrameInfo[0].bytePos; + pSeekPoints[iSeekPoint].pcmFrameIndex = nextTargetPCMFrame; + pSeekPoints[iSeekPoint].mp3FramesToDiscard = DRMP3_SEEK_LEADING_MP3_FRAMES; + pSeekPoints[iSeekPoint].pcmFramesToDiscard = (drmp3_uint16)(nextTargetPCMFrame - mp3FrameInfo[DRMP3_SEEK_LEADING_MP3_FRAMES-1].pcmFrameIndex); + break; + } + + drmp3__accumulate_running_pcm_frame_count(pMP3, pcmFramesInCurrentMP3FrameIn, &runningPCMFrameCount, &runningPCMFrameCountFractionalPart); + } + } + } + + /* Finally, we need to seek back to where we were. */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + if (!drmp3_seek_to_pcm_frame(pMP3, currentPCMFrame)) { + return DRMP3_FALSE; + } + } + + *pSeekPointCount = seekPointCount; + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_bind_seek_table(drmp3* pMP3, drmp3_uint32 seekPointCount, drmp3_seek_point* pSeekPoints) +{ + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + if (seekPointCount == 0 || pSeekPoints == NULL) { + /* Unbinding. */ + pMP3->seekPointCount = 0; + pMP3->pSeekPoints = NULL; + } else { + /* Binding. */ + pMP3->seekPointCount = seekPointCount; + pMP3->pSeekPoints = pSeekPoints; + } + + return DRMP3_TRUE; +} + + +static float* drmp3__full_read_and_close_f32(drmp3* pMP3, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +{ + drmp3_uint64 totalFramesRead = 0; + drmp3_uint64 framesCapacity = 0; + float* pFrames = NULL; + float temp[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + + DRMP3_ASSERT(pMP3 != NULL); + + for (;;) { + drmp3_uint64 framesToReadRightNow = DRMP3_COUNTOF(temp) / pMP3->channels; + drmp3_uint64 framesJustRead = drmp3_read_pcm_frames_f32(pMP3, framesToReadRightNow, temp); + if (framesJustRead == 0) { + break; + } + + /* Reallocate the output buffer if there's not enough room. */ + if (framesCapacity < totalFramesRead + framesJustRead) { + drmp3_uint64 oldFramesBufferSize; + drmp3_uint64 newFramesBufferSize; + drmp3_uint64 newFramesCap; + float* pNewFrames; + + newFramesCap = framesCapacity * 2; + if (newFramesCap < totalFramesRead + framesJustRead) { + newFramesCap = totalFramesRead + framesJustRead; + } + + oldFramesBufferSize = framesCapacity * pMP3->channels * sizeof(float); + newFramesBufferSize = newFramesCap * pMP3->channels * sizeof(float); + if (newFramesBufferSize > (drmp3_uint64)DRMP3_SIZE_MAX) { + break; + } + + pNewFrames = (float*)drmp3__realloc_from_callbacks(pFrames, (size_t)newFramesBufferSize, (size_t)oldFramesBufferSize, &pMP3->allocationCallbacks); + if (pNewFrames == NULL) { + drmp3__free_from_callbacks(pFrames, &pMP3->allocationCallbacks); + pFrames = NULL; + totalFramesRead = 0; + break; + } + + pFrames = pNewFrames; + framesCapacity = newFramesCap; + } + + DRMP3_COPY_MEMORY(pFrames + totalFramesRead*pMP3->channels, temp, (size_t)(framesJustRead*pMP3->channels*sizeof(float))); + totalFramesRead += framesJustRead; + + /* If the number of frames we asked for is less that what we actually read it means we've reached the end. */ + if (framesJustRead != framesToReadRightNow) { + break; + } + } + + if (pConfig != NULL) { + pConfig->channels = pMP3->channels; + pConfig->sampleRate = pMP3->sampleRate; + } + + drmp3_uninit(pMP3); + + if (pTotalFrameCount) { + *pTotalFrameCount = totalFramesRead; + } + + return pFrames; +} + +static drmp3_int16* drmp3__full_read_and_close_s16(drmp3* pMP3, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +{ + drmp3_uint64 totalFramesRead = 0; + drmp3_uint64 framesCapacity = 0; + drmp3_int16* pFrames = NULL; + drmp3_int16 temp[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + + DRMP3_ASSERT(pMP3 != NULL); + + for (;;) { + drmp3_uint64 framesToReadRightNow = DRMP3_COUNTOF(temp) / pMP3->channels; + drmp3_uint64 framesJustRead = drmp3_read_pcm_frames_s16(pMP3, framesToReadRightNow, temp); + if (framesJustRead == 0) { + break; + } + + /* Reallocate the output buffer if there's not enough room. */ + if (framesCapacity < totalFramesRead + framesJustRead) { + drmp3_uint64 newFramesBufferSize; + drmp3_uint64 oldFramesBufferSize; + drmp3_uint64 newFramesCap; + drmp3_int16* pNewFrames; + + newFramesCap = framesCapacity * 2; + if (newFramesCap < totalFramesRead + framesJustRead) { + newFramesCap = totalFramesRead + framesJustRead; + } + + oldFramesBufferSize = framesCapacity * pMP3->channels * sizeof(drmp3_int16); + newFramesBufferSize = newFramesCap * pMP3->channels * sizeof(drmp3_int16); + if (newFramesBufferSize > (drmp3_uint64)DRMP3_SIZE_MAX) { + break; + } + + pNewFrames = (drmp3_int16*)drmp3__realloc_from_callbacks(pFrames, (size_t)newFramesBufferSize, (size_t)oldFramesBufferSize, &pMP3->allocationCallbacks); + if (pNewFrames == NULL) { + drmp3__free_from_callbacks(pFrames, &pMP3->allocationCallbacks); + pFrames = NULL; + totalFramesRead = 0; + break; + } + + pFrames = pNewFrames; + framesCapacity = newFramesCap; + } + + DRMP3_COPY_MEMORY(pFrames + totalFramesRead*pMP3->channels, temp, (size_t)(framesJustRead*pMP3->channels*sizeof(drmp3_int16))); + totalFramesRead += framesJustRead; + + /* If the number of frames we asked for is less that what we actually read it means we've reached the end. */ + if (framesJustRead != framesToReadRightNow) { + break; + } + } + + if (pConfig != NULL) { + pConfig->channels = pMP3->channels; + pConfig->sampleRate = pMP3->sampleRate; + } + + drmp3_uninit(pMP3); + + if (pTotalFrameCount) { + *pTotalFrameCount = totalFramesRead; + } + + return pFrames; +} + + +DRMP3_API float* drmp3_open_and_read_pcm_frames_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init(&mp3, onRead, onSeek, onTell, NULL, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); +} + +DRMP3_API drmp3_int16* drmp3_open_and_read_pcm_frames_s16(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init(&mp3, onRead, onSeek, onTell, NULL, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_s16(&mp3, pConfig, pTotalFrameCount); +} + + +DRMP3_API float* drmp3_open_memory_and_read_pcm_frames_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_memory(&mp3, pData, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); +} + +DRMP3_API drmp3_int16* drmp3_open_memory_and_read_pcm_frames_s16(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_memory(&mp3, pData, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_s16(&mp3, pConfig, pTotalFrameCount); +} + + +#ifndef DR_MP3_NO_STDIO +DRMP3_API float* drmp3_open_file_and_read_pcm_frames_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_file(&mp3, filePath, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); +} + +DRMP3_API drmp3_int16* drmp3_open_file_and_read_pcm_frames_s16(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_file(&mp3, filePath, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_s16(&mp3, pConfig, pTotalFrameCount); +} +#endif + +DRMP3_API void* drmp3_malloc(size_t sz, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + return drmp3__malloc_from_callbacks(sz, pAllocationCallbacks); + } else { + return drmp3__malloc_default(sz, NULL); + } +} + +DRMP3_API void drmp3_free(void* p, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + drmp3__free_from_callbacks(p, pAllocationCallbacks); + } else { + drmp3__free_default(p, NULL); + } +} + +#endif /* dr_mp3_c */ +#endif /*DR_MP3_IMPLEMENTATION*/ + +/* +DIFFERENCES BETWEEN minimp3 AND dr_mp3 +====================================== +- First, keep in mind that minimp3 (https://github.com/lieff/minimp3) is where all the real work was done. All of the + code relating to the actual decoding remains mostly unmodified, apart from some namespacing changes. +- dr_mp3 adds a pulling style API which allows you to deliver raw data via callbacks. So, rather than pushing data + to the decoder, the decoder _pulls_ data from your callbacks. +- In addition to callbacks, a decoder can be initialized from a block of memory and a file. +- The dr_mp3 pull API reads PCM frames rather than whole MP3 frames. +- dr_mp3 adds convenience APIs for opening and decoding entire files in one go. +- dr_mp3 is fully namespaced, including the implementation section, which is more suitable when compiling projects + as a single translation unit (aka unity builds). At the time of writing this, a unity build is not possible when + using minimp3 in conjunction with stb_vorbis. dr_mp3 addresses this. +*/ + +/* +REVISION HISTORY +================ +v0.7.4 - TBD + - Fix an overflow error with "Xing" and "Info" tag parsing. + - Add some validation checks for "Xing" and "Info" tag parsing. + - Reduce size of some stack allocations. + - Improvements to SIMD detection. + +v0.7.3 - 2026-01-17 + - Fix an error in drmp3_open_and_read_pcm_frames_s16() and family when memory allocation fails. + - Fix some compilation warnings. + +v0.7.2 - 2025-12-02 + - Reduce stack space to improve robustness on embedded systems. + - Fix a compilation error with MSVC Clang toolset relating to cpuid. + - Fix an error with APE tag parsing. + +v0.7.1 - 2025-09-10 + - Silence a warning with GCC. + - Fix an error with the NXDK build. + - Fix a decoding inconsistency when seeking. Prior to this change, reading to the end of the stream immediately after initializing will result in a different number of samples read than if the stream is seeked to the start and read to the end. + +v0.7.0 - 2025-07-23 + - The old `DRMP3_IMPLEMENTATION` has been removed. Use `DR_MP3_IMPLEMENTATION` instead. The reason for this change is that in the future everything will eventually be using the underscored naming convention in the future, so `drmp3` will become `dr_mp3`. + - API CHANGE: Seek origins have been renamed to match the naming convention used by dr_wav and my other libraries. + - drmp3_seek_origin_start -> DRMP3_SEEK_SET + - drmp3_seek_origin_current -> DRMP3_SEEK_CUR + - DRMP3_SEEK_END (new) + - API CHANGE: Add DRMP3_SEEK_END as a seek origin for the seek callback. This is required for detection of ID3v1 and APE tags. + - API CHANGE: Add onTell callback to `drmp3_init()`. This is needed in order to track the location of ID3v1 and APE tags. + - API CHANGE: Add onMeta callback to `drmp3_init()`. This is used for reporting tag data back to the caller. Currently this only reports the raw tag data which means applications need to parse the data themselves. + - API CHANGE: Rename `drmp3dec_frame_info.hz` to `drmp3dec_frame_info.sample_rate`. + - Add detection of ID3v2, ID3v1, APE and Xing/VBRI tags. This should fix errors with some files where the decoder was reading tags as audio data. + - Delay and padding samples from LAME tags are now handled. + - Fix compilation for AIX OS. + +v0.6.40 - 2024-12-17 + - Improve detection of ARM64EC + +v0.6.39 - 2024-02-27 + - Fix a Wdouble-promotion warning. + +v0.6.38 - 2023-11-02 + - Fix build for ARMv6-M. + +v0.6.37 - 2023-07-07 + - Silence a static analysis warning. + +v0.6.36 - 2023-06-17 + - Fix an incorrect date in revision history. No functional change. + +v0.6.35 - 2023-05-22 + - Minor code restructure. No functional change. + +v0.6.34 - 2022-09-17 + - Fix compilation with DJGPP. + - Fix compilation when compiling with x86 with no SSE2. + - Remove an unnecessary variable from the drmp3 structure. + +v0.6.33 - 2022-04-10 + - Fix compilation error with the MSVC ARM64 build. + - Fix compilation error on older versions of GCC. + - Remove some unused functions. + +v0.6.32 - 2021-12-11 + - Fix a warning with Clang. + +v0.6.31 - 2021-08-22 + - Fix a bug when loading from memory. + +v0.6.30 - 2021-08-16 + - Silence some warnings. + - Replace memory operations with DRMP3_* macros. + +v0.6.29 - 2021-08-08 + - Bring up to date with minimp3. + +v0.6.28 - 2021-07-31 + - Fix platform detection for ARM64. + - Fix a compilation error with C89. + +v0.6.27 - 2021-02-21 + - Fix a warning due to referencing _MSC_VER when it is undefined. + +v0.6.26 - 2021-01-31 + - Bring up to date with minimp3. + +v0.6.25 - 2020-12-26 + - Remove DRMP3_DEFAULT_CHANNELS and DRMP3_DEFAULT_SAMPLE_RATE which are leftovers from some removed APIs. + +v0.6.24 - 2020-12-07 + - Fix a typo in version date for 0.6.23. + +v0.6.23 - 2020-12-03 + - Fix an error where a file can be closed twice when initialization of the decoder fails. + +v0.6.22 - 2020-12-02 + - Fix an error where it's possible for a file handle to be left open when initialization of the decoder fails. + +v0.6.21 - 2020-11-28 + - Bring up to date with minimp3. + +v0.6.20 - 2020-11-21 + - Fix compilation with OpenWatcom. + +v0.6.19 - 2020-11-13 + - Minor code clean up. + +v0.6.18 - 2020-11-01 + - Improve compiler support for older versions of GCC. + +v0.6.17 - 2020-09-28 + - Bring up to date with minimp3. + +v0.6.16 - 2020-08-02 + - Simplify sized types. + +v0.6.15 - 2020-07-25 + - Fix a compilation warning. + +v0.6.14 - 2020-07-23 + - Fix undefined behaviour with memmove(). + +v0.6.13 - 2020-07-06 + - Fix a bug when converting from s16 to f32 in drmp3_read_pcm_frames_f32(). + +v0.6.12 - 2020-06-23 + - Add include guard for the implementation section. + +v0.6.11 - 2020-05-26 + - Fix use of uninitialized variable error. + +v0.6.10 - 2020-05-16 + - Add compile-time and run-time version querying. + - DRMP3_VERSION_MINOR + - DRMP3_VERSION_MAJOR + - DRMP3_VERSION_REVISION + - DRMP3_VERSION_STRING + - drmp3_version() + - drmp3_version_string() + +v0.6.9 - 2020-04-30 + - Change the `pcm` parameter of drmp3dec_decode_frame() to a `const drmp3_uint8*` for consistency with internal APIs. + +v0.6.8 - 2020-04-26 + - Optimizations to decoding when initializing from memory. + +v0.6.7 - 2020-04-25 + - Fix a compilation error with DR_MP3_NO_STDIO + - Optimization to decoding by reducing some data movement. + +v0.6.6 - 2020-04-23 + - Fix a minor bug with the running PCM frame counter. + +v0.6.5 - 2020-04-19 + - Fix compilation error on ARM builds. + +v0.6.4 - 2020-04-19 + - Bring up to date with changes to minimp3. + +v0.6.3 - 2020-04-13 + - Fix some pedantic warnings. + +v0.6.2 - 2020-04-10 + - Fix a crash in drmp3_open_*_and_read_pcm_frames_*() if the output config object is NULL. + +v0.6.1 - 2020-04-05 + - Fix warnings. + +v0.6.0 - 2020-04-04 + - API CHANGE: Remove the pConfig parameter from the following APIs: + - drmp3_init() + - drmp3_init_memory() + - drmp3_init_file() + - Add drmp3_init_file_w() for opening a file from a wchar_t encoded path. + +v0.5.6 - 2020-02-12 + - Bring up to date with minimp3. + +v0.5.5 - 2020-01-29 + - Fix a memory allocation bug in high level s16 decoding APIs. + +v0.5.4 - 2019-12-02 + - Fix a possible null pointer dereference when using custom memory allocators for realloc(). + +v0.5.3 - 2019-11-14 + - Fix typos in documentation. + +v0.5.2 - 2019-11-02 + - Bring up to date with minimp3. + +v0.5.1 - 2019-10-08 + - Fix a warning with GCC. + +v0.5.0 - 2019-10-07 + - API CHANGE: Add support for user defined memory allocation routines. This system allows the program to specify their own memory allocation + routines with a user data pointer for client-specific contextual data. This adds an extra parameter to the end of the following APIs: + - drmp3_init() + - drmp3_init_file() + - drmp3_init_memory() + - drmp3_open_and_read_pcm_frames_f32() + - drmp3_open_and_read_pcm_frames_s16() + - drmp3_open_memory_and_read_pcm_frames_f32() + - drmp3_open_memory_and_read_pcm_frames_s16() + - drmp3_open_file_and_read_pcm_frames_f32() + - drmp3_open_file_and_read_pcm_frames_s16() + - API CHANGE: Renamed the following APIs: + - drmp3_open_and_read_f32() -> drmp3_open_and_read_pcm_frames_f32() + - drmp3_open_and_read_s16() -> drmp3_open_and_read_pcm_frames_s16() + - drmp3_open_memory_and_read_f32() -> drmp3_open_memory_and_read_pcm_frames_f32() + - drmp3_open_memory_and_read_s16() -> drmp3_open_memory_and_read_pcm_frames_s16() + - drmp3_open_file_and_read_f32() -> drmp3_open_file_and_read_pcm_frames_f32() + - drmp3_open_file_and_read_s16() -> drmp3_open_file_and_read_pcm_frames_s16() + +v0.4.7 - 2019-07-28 + - Fix a compiler error. + +v0.4.6 - 2019-06-14 + - Fix a compiler error. + +v0.4.5 - 2019-06-06 + - Bring up to date with minimp3. + +v0.4.4 - 2019-05-06 + - Fixes to the VC6 build. + +v0.4.3 - 2019-05-05 + - Use the channel count and/or sample rate of the first MP3 frame instead of DRMP3_DEFAULT_CHANNELS and + DRMP3_DEFAULT_SAMPLE_RATE when they are set to 0. To use the old behaviour, just set the relevant property to + DRMP3_DEFAULT_CHANNELS or DRMP3_DEFAULT_SAMPLE_RATE. + - Add s16 reading APIs + - drmp3_read_pcm_frames_s16 + - drmp3_open_memory_and_read_pcm_frames_s16 + - drmp3_open_and_read_pcm_frames_s16 + - drmp3_open_file_and_read_pcm_frames_s16 + - Add drmp3_get_mp3_and_pcm_frame_count() to the public header section. + - Add support for C89. + - Change license to choice of public domain or MIT-0. + +v0.4.2 - 2019-02-21 + - Fix a warning. + +v0.4.1 - 2018-12-30 + - Fix a warning. + +v0.4.0 - 2018-12-16 + - API CHANGE: Rename some APIs: + - drmp3_read_f32 -> to drmp3_read_pcm_frames_f32 + - drmp3_seek_to_frame -> drmp3_seek_to_pcm_frame + - drmp3_open_and_decode_f32 -> drmp3_open_and_read_pcm_frames_f32 + - drmp3_open_and_decode_memory_f32 -> drmp3_open_memory_and_read_pcm_frames_f32 + - drmp3_open_and_decode_file_f32 -> drmp3_open_file_and_read_pcm_frames_f32 + - Add drmp3_get_pcm_frame_count(). + - Add drmp3_get_mp3_frame_count(). + - Improve seeking performance. + +v0.3.2 - 2018-09-11 + - Fix a couple of memory leaks. + - Bring up to date with minimp3. + +v0.3.1 - 2018-08-25 + - Fix C++ build. + +v0.3.0 - 2018-08-25 + - Bring up to date with minimp3. This has a minor API change: the "pcm" parameter of drmp3dec_decode_frame() has + been changed from short* to void* because it can now output both s16 and f32 samples, depending on whether or + not the DR_MP3_FLOAT_OUTPUT option is set. + +v0.2.11 - 2018-08-08 + - Fix a bug where the last part of a file is not read. + +v0.2.10 - 2018-08-07 + - Improve 64-bit detection. + +v0.2.9 - 2018-08-05 + - Fix C++ build on older versions of GCC. + - Bring up to date with minimp3. + +v0.2.8 - 2018-08-02 + - Fix compilation errors with older versions of GCC. + +v0.2.7 - 2018-07-13 + - Bring up to date with minimp3. + +v0.2.6 - 2018-07-12 + - Bring up to date with minimp3. + +v0.2.5 - 2018-06-22 + - Bring up to date with minimp3. + +v0.2.4 - 2018-05-12 + - Bring up to date with minimp3. + +v0.2.3 - 2018-04-29 + - Fix TCC build. + +v0.2.2 - 2018-04-28 + - Fix bug when opening a decoder from memory. + +v0.2.1 - 2018-04-27 + - Efficiency improvements when the decoder reaches the end of the stream. + +v0.2 - 2018-04-21 + - Bring up to date with minimp3. + - Start using major.minor.revision versioning. + +v0.1d - 2018-03-30 + - Bring up to date with minimp3. + +v0.1c - 2018-03-11 + - Fix C++ build error. + +v0.1b - 2018-03-07 + - Bring up to date with minimp3. + +v0.1a - 2018-02-28 + - Fix compilation error on GCC/Clang. + - Fix some warnings. + +v0.1 - 2018-02-xx + - Initial versioned release. +*/ + +/* +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2023 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ diff --git a/native/vendor/dr_libs/dr_wav.h b/native/vendor/dr_libs/dr_wav.h new file mode 100644 index 0000000..bfe8426 --- /dev/null +++ b/native/vendor/dr_libs/dr_wav.h @@ -0,0 +1,9209 @@ +/* +WAV audio loader and writer. Choice of public domain or MIT-0. See license statements at the end of this file. +dr_wav - v0.14.6 - TBD + +David Reid - mackron@gmail.com + +GitHub: https://github.com/mackron/dr_libs +*/ + +/* +Introduction +============ +This is a single file library. To use it, do something like the following in one .c file. + + ```c + #define DR_WAV_IMPLEMENTATION + #include "dr_wav.h" + ``` + +You can then #include this file in other parts of the program as you would with any other header file. Do something like the following to read audio data: + + ```c + drwav wav; + if (!drwav_init_file(&wav, "my_song.wav", NULL)) { + // Error opening WAV file. + } + + drwav_int32* pDecodedInterleavedPCMFrames = malloc(wav.totalPCMFrameCount * wav.channels * sizeof(drwav_int32)); + size_t numberOfSamplesActuallyDecoded = drwav_read_pcm_frames_s32(&wav, wav.totalPCMFrameCount, pDecodedInterleavedPCMFrames); + + ... + + drwav_uninit(&wav); + ``` + +If you just want to quickly open and read the audio data in a single operation you can do something like this: + + ```c + unsigned int channels; + unsigned int sampleRate; + drwav_uint64 totalPCMFrameCount; + float* pSampleData = drwav_open_file_and_read_pcm_frames_f32("my_song.wav", &channels, &sampleRate, &totalPCMFrameCount, NULL); + if (pSampleData == NULL) { + // Error opening and reading WAV file. + } + + ... + + drwav_free(pSampleData, NULL); + ``` + +The examples above use versions of the API that convert the audio data to a consistent format (32-bit signed PCM, in this case), but you can still output the +audio data in its internal format (see notes below for supported formats): + + ```c + size_t framesRead = drwav_read_pcm_frames(&wav, wav.totalPCMFrameCount, pDecodedInterleavedPCMFrames); + ``` + +You can also read the raw bytes of audio data, which could be useful if dr_wav does not have native support for a particular data format: + + ```c + size_t bytesRead = drwav_read_raw(&wav, bytesToRead, pRawDataBuffer); + ``` + +dr_wav can also be used to output WAV files. This does not currently support compressed formats. To use this, look at `drwav_init_write()`, +`drwav_init_file_write()`, etc. Use `drwav_write_pcm_frames()` to write samples, or `drwav_write_raw()` to write raw data in the "data" chunk. + + ```c + drwav_data_format format; + format.container = drwav_container_riff; // <-- drwav_container_riff = normal WAV files, drwav_container_w64 = Sony Wave64. + format.format = DR_WAVE_FORMAT_PCM; // <-- Any of the DR_WAVE_FORMAT_* codes. + format.channels = 2; + format.sampleRate = 44100; + format.bitsPerSample = 16; + drwav_init_file_write(&wav, "data/recording.wav", &format, NULL); + + ... + + drwav_uint64 framesWritten = drwav_write_pcm_frames(pWav, frameCount, pSamples); + ``` + +Note that writing to AIFF or RIFX is not supported. + +dr_wav has support for decoding from a number of different encapsulation formats. See below for details. + + +Build Options +============= +#define these options before including this file. + +#define DR_WAV_NO_CONVERSION_API + Disables conversion APIs such as `drwav_read_pcm_frames_f32()` and `drwav_s16_to_f32()`. + +#define DR_WAV_NO_STDIO + Disables APIs that initialize a decoder from a file such as `drwav_init_file()`, `drwav_init_file_write()`, etc. + +#define DR_WAV_NO_WCHAR + Disables all functions ending with `_w`. Use this if your compiler does not provide wchar.h. Not required if DR_WAV_NO_STDIO is also defined. + + +Supported Encapsulations +======================== +- RIFF (Regular WAV) +- RIFX (Big-Endian) +- AIFF (Does not currently support ADPCM) +- RF64 +- W64 + +Note that AIFF and RIFX do not support write mode, nor do they support reading of metadata. + + +Supported Encodings +=================== +- Unsigned 8-bit PCM +- Signed 12-bit PCM +- Signed 16-bit PCM +- Signed 24-bit PCM +- Signed 32-bit PCM +- IEEE 32-bit floating point +- IEEE 64-bit floating point +- A-law and u-law +- Microsoft ADPCM +- IMA ADPCM (DVI, format code 0x11) + +8-bit PCM encodings are always assumed to be unsigned. Signed 8-bit encoding can only be read with `drwav_read_raw()`. + +Note that ADPCM is not currently supported with AIFF. Contributions welcome. + + +Notes +===== +- Samples are always interleaved. +- The default read function does not do any data conversion. Use `drwav_read_pcm_frames_f32()`, `drwav_read_pcm_frames_s32()` and `drwav_read_pcm_frames_s16()` + to read and convert audio data to 32-bit floating point, signed 32-bit integer and signed 16-bit integer samples respectively. +- dr_wav will try to read the WAV file as best it can, even if it's not strictly conformant to the WAV format. +*/ + +#ifndef dr_wav_h +#define dr_wav_h + +#ifdef __cplusplus +extern "C" { +#endif + +#define DRWAV_STRINGIFY(x) #x +#define DRWAV_XSTRINGIFY(x) DRWAV_STRINGIFY(x) + +#define DRWAV_VERSION_MAJOR 0 +#define DRWAV_VERSION_MINOR 14 +#define DRWAV_VERSION_REVISION 6 +#define DRWAV_VERSION_STRING DRWAV_XSTRINGIFY(DRWAV_VERSION_MAJOR) "." DRWAV_XSTRINGIFY(DRWAV_VERSION_MINOR) "." DRWAV_XSTRINGIFY(DRWAV_VERSION_REVISION) + +#include /* For size_t. */ + +/* Sized Types */ +typedef signed char drwav_int8; +typedef unsigned char drwav_uint8; +typedef signed short drwav_int16; +typedef unsigned short drwav_uint16; +typedef signed int drwav_int32; +typedef unsigned int drwav_uint32; +#if defined(_MSC_VER) && !defined(__clang__) + typedef signed __int64 drwav_int64; + typedef unsigned __int64 drwav_uint64; +#else + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wlong-long" + #if defined(__clang__) + #pragma GCC diagnostic ignored "-Wc++11-long-long" + #endif + #endif + typedef signed long long drwav_int64; + typedef unsigned long long drwav_uint64; + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop + #endif +#endif +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) || defined(__powerpc64__) + typedef drwav_uint64 drwav_uintptr; +#else + typedef drwav_uint32 drwav_uintptr; +#endif +typedef drwav_uint8 drwav_bool8; +typedef drwav_uint32 drwav_bool32; +#define DRWAV_TRUE 1 +#define DRWAV_FALSE 0 +/* End Sized Types */ + +/* Decorations */ +#if !defined(DRWAV_API) + #if defined(DRWAV_DLL) + #if defined(_WIN32) + #define DRWAV_DLL_IMPORT __declspec(dllimport) + #define DRWAV_DLL_EXPORT __declspec(dllexport) + #define DRWAV_DLL_PRIVATE static + #else + #if defined(__GNUC__) && __GNUC__ >= 4 + #define DRWAV_DLL_IMPORT __attribute__((visibility("default"))) + #define DRWAV_DLL_EXPORT __attribute__((visibility("default"))) + #define DRWAV_DLL_PRIVATE __attribute__((visibility("hidden"))) + #else + #define DRWAV_DLL_IMPORT + #define DRWAV_DLL_EXPORT + #define DRWAV_DLL_PRIVATE static + #endif + #endif + + #if defined(DR_WAV_IMPLEMENTATION) || defined(DRWAV_IMPLEMENTATION) + #define DRWAV_API DRWAV_DLL_EXPORT + #else + #define DRWAV_API DRWAV_DLL_IMPORT + #endif + #define DRWAV_PRIVATE DRWAV_DLL_PRIVATE + #else + #define DRWAV_API extern + #define DRWAV_PRIVATE static + #endif +#endif +/* End Decorations */ + +/* Result Codes */ +typedef drwav_int32 drwav_result; +#define DRWAV_SUCCESS 0 +#define DRWAV_ERROR -1 /* A generic error. */ +#define DRWAV_INVALID_ARGS -2 +#define DRWAV_INVALID_OPERATION -3 +#define DRWAV_OUT_OF_MEMORY -4 +#define DRWAV_OUT_OF_RANGE -5 +#define DRWAV_ACCESS_DENIED -6 +#define DRWAV_DOES_NOT_EXIST -7 +#define DRWAV_ALREADY_EXISTS -8 +#define DRWAV_TOO_MANY_OPEN_FILES -9 +#define DRWAV_INVALID_FILE -10 +#define DRWAV_TOO_BIG -11 +#define DRWAV_PATH_TOO_LONG -12 +#define DRWAV_NAME_TOO_LONG -13 +#define DRWAV_NOT_DIRECTORY -14 +#define DRWAV_IS_DIRECTORY -15 +#define DRWAV_DIRECTORY_NOT_EMPTY -16 +#define DRWAV_END_OF_FILE -17 +#define DRWAV_NO_SPACE -18 +#define DRWAV_BUSY -19 +#define DRWAV_IO_ERROR -20 +#define DRWAV_INTERRUPT -21 +#define DRWAV_UNAVAILABLE -22 +#define DRWAV_ALREADY_IN_USE -23 +#define DRWAV_BAD_ADDRESS -24 +#define DRWAV_BAD_SEEK -25 +#define DRWAV_BAD_PIPE -26 +#define DRWAV_DEADLOCK -27 +#define DRWAV_TOO_MANY_LINKS -28 +#define DRWAV_NOT_IMPLEMENTED -29 +#define DRWAV_NO_MESSAGE -30 +#define DRWAV_BAD_MESSAGE -31 +#define DRWAV_NO_DATA_AVAILABLE -32 +#define DRWAV_INVALID_DATA -33 +#define DRWAV_TIMEOUT -34 +#define DRWAV_NO_NETWORK -35 +#define DRWAV_NOT_UNIQUE -36 +#define DRWAV_NOT_SOCKET -37 +#define DRWAV_NO_ADDRESS -38 +#define DRWAV_BAD_PROTOCOL -39 +#define DRWAV_PROTOCOL_UNAVAILABLE -40 +#define DRWAV_PROTOCOL_NOT_SUPPORTED -41 +#define DRWAV_PROTOCOL_FAMILY_NOT_SUPPORTED -42 +#define DRWAV_ADDRESS_FAMILY_NOT_SUPPORTED -43 +#define DRWAV_SOCKET_NOT_SUPPORTED -44 +#define DRWAV_CONNECTION_RESET -45 +#define DRWAV_ALREADY_CONNECTED -46 +#define DRWAV_NOT_CONNECTED -47 +#define DRWAV_CONNECTION_REFUSED -48 +#define DRWAV_NO_HOST -49 +#define DRWAV_IN_PROGRESS -50 +#define DRWAV_CANCELLED -51 +#define DRWAV_MEMORY_ALREADY_MAPPED -52 +#define DRWAV_AT_END -53 +/* End Result Codes */ + +/* Common data formats. */ +#define DR_WAVE_FORMAT_PCM 0x1 +#define DR_WAVE_FORMAT_ADPCM 0x2 +#define DR_WAVE_FORMAT_IEEE_FLOAT 0x3 +#define DR_WAVE_FORMAT_ALAW 0x6 +#define DR_WAVE_FORMAT_MULAW 0x7 +#define DR_WAVE_FORMAT_DTS 0x8 +#define DR_WAVE_FORMAT_DVI_ADPCM 0x11 +#define DR_WAVE_FORMAT_EXTENSIBLE 0xFFFE + +/* Flags to pass into drwav_init_ex(), etc. */ +#define DRWAV_SEQUENTIAL 0x00000001 +#define DRWAV_WITH_METADATA 0x00000002 + +DRWAV_API void drwav_version(drwav_uint32* pMajor, drwav_uint32* pMinor, drwav_uint32* pRevision); +DRWAV_API const char* drwav_version_string(void); + +/* Allocation Callbacks */ +typedef struct +{ + void* pUserData; + void* (* onMalloc)(size_t sz, void* pUserData); + void* (* onRealloc)(void* p, size_t sz, void* pUserData); + void (* onFree)(void* p, void* pUserData); +} drwav_allocation_callbacks; +/* End Allocation Callbacks */ + +typedef enum +{ + DRWAV_SEEK_SET, + DRWAV_SEEK_CUR, + DRWAV_SEEK_END +} drwav_seek_origin; + +typedef enum +{ + drwav_container_riff, + drwav_container_rifx, + drwav_container_w64, + drwav_container_rf64, + drwav_container_aiff +} drwav_container; + +typedef struct +{ + union + { + drwav_uint8 fourcc[4]; + drwav_uint8 guid[16]; + } id; + + /* The size in bytes of the chunk. */ + drwav_uint64 sizeInBytes; + + /* + RIFF = 2 byte alignment. + W64 = 8 byte alignment. + */ + unsigned int paddingSize; +} drwav_chunk_header; + +typedef struct +{ + /* + The format tag exactly as specified in the wave file's "fmt" chunk. This can be used by applications + that require support for data formats not natively supported by dr_wav. + */ + drwav_uint16 formatTag; + + /* The number of channels making up the audio data. When this is set to 1 it is mono, 2 is stereo, etc. */ + drwav_uint16 channels; + + /* The sample rate. Usually set to something like 44100. */ + drwav_uint32 sampleRate; + + /* Average bytes per second. You probably don't need this, but it's left here for informational purposes. */ + drwav_uint32 avgBytesPerSec; + + /* Block align. This is equal to the number of channels * bytes per sample. */ + drwav_uint16 blockAlign; + + /* Bits per sample. */ + drwav_uint16 bitsPerSample; + + /* The size of the extended data. Only used internally for validation, but left here for informational purposes. */ + drwav_uint16 extendedSize; + + /* + The number of valid bits per sample. When is equal to WAVE_FORMAT_EXTENSIBLE, + is always rounded up to the nearest multiple of 8. This variable contains information about exactly how + many bits are valid per sample. Mainly used for informational purposes. + */ + drwav_uint16 validBitsPerSample; + + /* The channel mask. Not used at the moment. */ + drwav_uint32 channelMask; + + /* The sub-format, exactly as specified by the wave file. */ + drwav_uint8 subFormat[16]; +} drwav_fmt; + +DRWAV_API drwav_uint16 drwav_fmt_get_format(const drwav_fmt* pFMT); + + +/* +Callback for when data is read. Return value is the number of bytes actually read. + +pUserData [in] The user data that was passed to drwav_init() and family. +pBufferOut [out] The output buffer. +bytesToRead [in] The number of bytes to read. + +Returns the number of bytes actually read. + +A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback until +either the entire bytesToRead is filled or you have reached the end of the stream. +*/ +typedef size_t (* drwav_read_proc)(void* pUserData, void* pBufferOut, size_t bytesToRead); + +/* +Callback for when data is written. Returns value is the number of bytes actually written. + +pUserData [in] The user data that was passed to drwav_init_write() and family. +pData [out] A pointer to the data to write. +bytesToWrite [in] The number of bytes to write. + +Returns the number of bytes actually written. + +If the return value differs from bytesToWrite, it indicates an error. +*/ +typedef size_t (* drwav_write_proc)(void* pUserData, const void* pData, size_t bytesToWrite); + +/* +Callback for when data needs to be seeked. + +pUserData [in] The user data that was passed to drwav_init() and family. +offset [in] The number of bytes to move, relative to the origin. Will never be negative. +origin [in] The origin of the seek - the current position or the start of the stream. + +Returns whether or not the seek was successful. + +Whether or not it is relative to the beginning or current position is determined by the "origin" parameter which will be either DRWAV_SEEK_SET or +DRWAV_SEEK_CUR. +*/ +typedef drwav_bool32 (* drwav_seek_proc)(void* pUserData, int offset, drwav_seek_origin origin); + +/* +Callback for when the current position in the stream needs to be retrieved. + +pUserData [in] The user data that was passed to drwav_init() and family. +pCursor [out] A pointer to a variable to receive the current position in the stream. + +Returns whether or not the operation was successful. +*/ +typedef drwav_bool32 (* drwav_tell_proc)(void* pUserData, drwav_int64* pCursor); + +/* +Callback for when drwav_init_ex() finds a chunk. + +pChunkUserData [in] The user data that was passed to the pChunkUserData parameter of drwav_init_ex() and family. +onRead [in] A pointer to the function to call when reading. +onSeek [in] A pointer to the function to call when seeking. +pReadSeekUserData [in] The user data that was passed to the pReadSeekUserData parameter of drwav_init_ex() and family. +pChunkHeader [in] A pointer to an object containing basic header information about the chunk. Use this to identify the chunk. +container [in] Whether or not the WAV file is a RIFF or Wave64 container. If you're unsure of the difference, assume RIFF. +pFMT [in] A pointer to the object containing the contents of the "fmt" chunk. + +Returns the number of bytes read + seeked. + +To read data from the chunk, call onRead(), passing in pReadSeekUserData as the first parameter. Do the same for seeking with onSeek(). The return value must +be the total number of bytes you have read _plus_ seeked. + +Use the `container` argument to discriminate the fields in `pChunkHeader->id`. If the container is `drwav_container_riff` or `drwav_container_rf64` you should +use `id.fourcc`, otherwise you should use `id.guid`. + +The `pFMT` parameter can be used to determine the data format of the wave file. Use `drwav_fmt_get_format()` to get the sample format, which will be one of the +`DR_WAVE_FORMAT_*` identifiers. + +The read pointer will be sitting on the first byte after the chunk's header. You must not attempt to read beyond the boundary of the chunk. +*/ +typedef drwav_uint64 (* drwav_chunk_proc)(void* pChunkUserData, drwav_read_proc onRead, drwav_seek_proc onSeek, void* pReadSeekUserData, const drwav_chunk_header* pChunkHeader, drwav_container container, const drwav_fmt* pFMT); + + +/* Structure for internal use. Only used for loaders opened with drwav_init_memory(). */ +typedef struct +{ + const drwav_uint8* data; + size_t dataSize; + size_t currentReadPos; +} drwav__memory_stream; + +/* Structure for internal use. Only used for writers opened with drwav_init_memory_write(). */ +typedef struct +{ + void** ppData; + size_t* pDataSize; + size_t dataSize; + size_t dataCapacity; + size_t currentWritePos; +} drwav__memory_stream_write; + +typedef struct +{ + drwav_container container; /* RIFF, W64. */ + drwav_uint32 format; /* DR_WAVE_FORMAT_* */ + drwav_uint32 channels; + drwav_uint32 sampleRate; + drwav_uint32 bitsPerSample; +} drwav_data_format; + +typedef enum +{ + drwav_metadata_type_none = 0, + + /* + Unknown simply means a chunk that drwav does not handle specifically. You can still ask to + receive these chunks as metadata objects. It is then up to you to interpret the chunk's data. + You can also write unknown metadata to a wav file. Be careful writing unknown chunks if you + have also edited the audio data. The unknown chunks could represent offsets/sizes that no + longer correctly correspond to the audio data. + */ + drwav_metadata_type_unknown = 1 << 0, + + /* Only 1 of each of these metadata items are allowed in a wav file. */ + drwav_metadata_type_smpl = 1 << 1, + drwav_metadata_type_inst = 1 << 2, + drwav_metadata_type_cue = 1 << 3, + drwav_metadata_type_acid = 1 << 4, + drwav_metadata_type_bext = 1 << 5, + + /* + Wav files often have a LIST chunk. This is a chunk that contains a set of subchunks. For this + higher-level metadata API, we don't make a distinction between a regular chunk and a LIST + subchunk. Instead, they are all just 'metadata' items. + + There can be multiple of these metadata items in a wav file. + */ + drwav_metadata_type_list_label = 1 << 6, + drwav_metadata_type_list_note = 1 << 7, + drwav_metadata_type_list_labelled_cue_region = 1 << 8, + + drwav_metadata_type_list_info_software = 1 << 9, + drwav_metadata_type_list_info_copyright = 1 << 10, + drwav_metadata_type_list_info_title = 1 << 11, + drwav_metadata_type_list_info_artist = 1 << 12, + drwav_metadata_type_list_info_comment = 1 << 13, + drwav_metadata_type_list_info_date = 1 << 14, + drwav_metadata_type_list_info_genre = 1 << 15, + drwav_metadata_type_list_info_album = 1 << 16, + drwav_metadata_type_list_info_tracknumber = 1 << 17, + drwav_metadata_type_list_info_location = 1 << 18, + drwav_metadata_type_list_info_organization = 1 << 19, + drwav_metadata_type_list_info_keywords = 1 << 20, + drwav_metadata_type_list_info_medium = 1 << 21, + drwav_metadata_type_list_info_description = 1 << 22, + + /* Other type constants for convenience. */ + drwav_metadata_type_list_all_info_strings = drwav_metadata_type_list_info_software + | drwav_metadata_type_list_info_copyright + | drwav_metadata_type_list_info_title + | drwav_metadata_type_list_info_artist + | drwav_metadata_type_list_info_comment + | drwav_metadata_type_list_info_date + | drwav_metadata_type_list_info_genre + | drwav_metadata_type_list_info_album + | drwav_metadata_type_list_info_tracknumber + | drwav_metadata_type_list_info_location + | drwav_metadata_type_list_info_organization + | drwav_metadata_type_list_info_keywords + | drwav_metadata_type_list_info_medium + | drwav_metadata_type_list_info_description, + + drwav_metadata_type_list_all_adtl = drwav_metadata_type_list_label + | drwav_metadata_type_list_note + | drwav_metadata_type_list_labelled_cue_region, + + drwav_metadata_type_all = -2, /*0xFFFFFFFF & ~drwav_metadata_type_unknown,*/ + drwav_metadata_type_all_including_unknown = -1 /*0xFFFFFFFF,*/ +} drwav_metadata_type; + +/* +Sampler Metadata + +The sampler chunk contains information about how a sound should be played in the context of a whole +audio production, and when used in a sampler. See https://en.wikipedia.org/wiki/Sample-based_synthesis. +*/ +typedef enum +{ + drwav_smpl_loop_type_forward = 0, + drwav_smpl_loop_type_pingpong = 1, + drwav_smpl_loop_type_backward = 2 +} drwav_smpl_loop_type; + +typedef struct +{ + /* The ID of the associated cue point, see drwav_cue and drwav_cue_point. As with all cue point IDs, this can correspond to a label chunk to give this loop a name, see drwav_list_label_or_note. */ + drwav_uint32 cuePointId; + + /* See drwav_smpl_loop_type. */ + drwav_uint32 type; + + /* The offset of the first sample to be played in the loop. */ + drwav_uint32 firstSampleOffset; + + /* The offset into the audio data of the last sample to be played in the loop. */ + drwav_uint32 lastSampleOffset; + + /* A value to represent that playback should occur at a point between samples. This value ranges from 0 to UINT32_MAX. Where a value of 0 means no fraction, and a value of (UINT32_MAX / 2) would mean half a sample. */ + drwav_uint32 sampleFraction; + + /* Number of times to play the loop. 0 means loop infinitely. */ + drwav_uint32 playCount; +} drwav_smpl_loop; + +typedef struct +{ + /* IDs for a particular MIDI manufacturer. 0 if not used. */ + drwav_uint32 manufacturerId; + drwav_uint32 productId; + + /* The period of 1 sample in nanoseconds. */ + drwav_uint32 samplePeriodNanoseconds; + + /* The MIDI root note of this file. 0 to 127. */ + drwav_uint32 midiUnityNote; + + /* The fraction of a semitone up from the given MIDI note. This is a value from 0 to UINT32_MAX, where 0 means no change and (UINT32_MAX / 2) is half a semitone (AKA 50 cents). */ + drwav_uint32 midiPitchFraction; + + /* Data relating to SMPTE standards which are used for syncing audio and video. 0 if not used. */ + drwav_uint32 smpteFormat; + drwav_uint32 smpteOffset; + + /* drwav_smpl_loop loops. */ + drwav_uint32 sampleLoopCount; + + /* Optional sampler-specific data. */ + drwav_uint32 samplerSpecificDataSizeInBytes; + + drwav_smpl_loop* pLoops; + drwav_uint8* pSamplerSpecificData; +} drwav_smpl; + +/* +Instrument Metadata + +The inst metadata contains data about how a sound should be played as part of an instrument. This +commonly read by samplers. See https://en.wikipedia.org/wiki/Sample-based_synthesis. +*/ +typedef struct +{ + drwav_int8 midiUnityNote; /* The root note of the audio as a MIDI note number. 0 to 127. */ + drwav_int8 fineTuneCents; /* -50 to +50 */ + drwav_int8 gainDecibels; /* -64 to +64 */ + drwav_int8 lowNote; /* 0 to 127 */ + drwav_int8 highNote; /* 0 to 127 */ + drwav_int8 lowVelocity; /* 1 to 127 */ + drwav_int8 highVelocity; /* 1 to 127 */ +} drwav_inst; + +/* +Cue Metadata + +Cue points are markers at specific points in the audio. They often come with an associated piece of +drwav_list_label_or_note metadata which contains the text for the marker. +*/ +typedef struct +{ + /* Unique identification value. */ + drwav_uint32 id; + + /* Set to 0. This is only relevant if there is a 'playlist' chunk - which is not supported by dr_wav. */ + drwav_uint32 playOrderPosition; + + /* Should always be "data". This represents the fourcc value of the chunk that this cue point corresponds to. dr_wav only supports a single data chunk so this should always be "data". */ + drwav_uint8 dataChunkId[4]; + + /* Set to 0. This is only relevant if there is a wave list chunk. dr_wav, like lots of readers/writers, do not support this. */ + drwav_uint32 chunkStart; + + /* Set to 0 for uncompressed formats. Else the last byte in compressed wave data where decompression can begin to find the value of the corresponding sample value. */ + drwav_uint32 blockStart; + + /* For uncompressed formats this is the offset of the cue point into the audio data. For compressed formats this is relative to the block specified with blockStart. */ + drwav_uint32 sampleOffset; +} drwav_cue_point; + +typedef struct +{ + drwav_uint32 cuePointCount; + drwav_cue_point *pCuePoints; +} drwav_cue; + +/* +Acid Metadata + +This chunk contains some information about the time signature and the tempo of the audio. +*/ +typedef enum +{ + drwav_acid_flag_one_shot = 1, /* If this is not set, then it is a loop instead of a one-shot. */ + drwav_acid_flag_root_note_set = 2, + drwav_acid_flag_stretch = 4, + drwav_acid_flag_disk_based = 8, + drwav_acid_flag_acidizer = 16 /* Not sure what this means. */ +} drwav_acid_flag; + +typedef struct +{ + /* A bit-field, see drwav_acid_flag. */ + drwav_uint32 flags; + + /* Valid if flags contains drwav_acid_flag_root_note_set. It represents the MIDI root note the file - a value from 0 to 127. */ + drwav_uint16 midiUnityNote; + + /* Reserved values that should probably be ignored. reserved1 seems to often be 128 and reserved2 is 0. */ + drwav_uint16 reserved1; + float reserved2; + + /* Number of beats. */ + drwav_uint32 numBeats; + + /* The time signature of the audio. */ + drwav_uint16 meterDenominator; + drwav_uint16 meterNumerator; + + /* Beats per minute of the track. Setting a value of 0 suggests that there is no tempo. */ + float tempo; +} drwav_acid; + +/* +Cue Label or Note metadata + +These are 2 different types of metadata, but they have the exact same format. Labels tend to be the +more common and represent a short name for a cue point. Notes might be used to represent a longer +comment. +*/ +typedef struct +{ + /* The ID of a cue point that this label or note corresponds to. */ + drwav_uint32 cuePointId; + + /* Size of the string not including any null terminator. */ + drwav_uint32 stringLength; + + /* The string. The *init_with_metadata functions null terminate this for convenience. */ + char* pString; +} drwav_list_label_or_note; + +/* +BEXT metadata, also known as Broadcast Wave Format (BWF) + +This metadata adds some extra description to an audio file. You must check the version field to +determine if the UMID or the loudness fields are valid. +*/ +typedef struct +{ + /* + These top 3 fields, and the umid field are actually defined in the standard as a statically + sized buffers. In order to reduce the size of this struct (and therefore the union in the + metadata struct), we instead store these as pointers. + */ + char* pDescription; /* Can be NULL or a null-terminated string, must be <= 256 characters. */ + char* pOriginatorName; /* Can be NULL or a null-terminated string, must be <= 32 characters. */ + char* pOriginatorReference; /* Can be NULL or a null-terminated string, must be <= 32 characters. */ + char pOriginationDate[10]; /* ASCII "yyyy:mm:dd". */ + char pOriginationTime[8]; /* ASCII "hh:mm:ss". */ + drwav_uint64 timeReference; /* First sample count since midnight. */ + drwav_uint16 version; /* Version of the BWF, check this to see if the fields below are valid. */ + + /* + Unrestricted ASCII characters containing a collection of strings terminated by CR/LF. Each + string shall contain a description of a coding process applied to the audio data. + */ + char* pCodingHistory; + drwav_uint32 codingHistorySize; + + /* Fields below this point are only valid if the version is 1 or above. */ + drwav_uint8* pUMID; /* Exactly 64 bytes of SMPTE UMID */ + + /* Fields below this point are only valid if the version is 2 or above. */ + drwav_uint16 loudnessValue; /* Integrated Loudness Value of the file in LUFS (multiplied by 100). */ + drwav_uint16 loudnessRange; /* Loudness Range of the file in LU (multiplied by 100). */ + drwav_uint16 maxTruePeakLevel; /* Maximum True Peak Level of the file expressed as dBTP (multiplied by 100). */ + drwav_uint16 maxMomentaryLoudness; /* Highest value of the Momentary Loudness Level of the file in LUFS (multiplied by 100). */ + drwav_uint16 maxShortTermLoudness; /* Highest value of the Short-Term Loudness Level of the file in LUFS (multiplied by 100). */ +} drwav_bext; + +/* +Info Text Metadata + +There a many different types of information text that can be saved in this format. This is where +things like the album name, the artists, the year it was produced, etc are saved. See +drwav_metadata_type for the full list of types that dr_wav supports. +*/ +typedef struct +{ + /* Size of the string not including any null terminator. */ + drwav_uint32 stringLength; + + /* The string. The *init_with_metadata functions null terminate this for convenience. */ + char* pString; +} drwav_list_info_text; + +/* +Labelled Cue Region Metadata + +The labelled cue region metadata is used to associate some region of audio with text. The region +starts at a cue point, and extends for the given number of samples. +*/ +typedef struct +{ + /* The ID of a cue point that this object corresponds to. */ + drwav_uint32 cuePointId; + + /* The number of samples from the cue point forwards that should be considered this region */ + drwav_uint32 sampleLength; + + /* Four characters used to say what the purpose of this region is. */ + drwav_uint8 purposeId[4]; + + /* Unsure of the exact meanings of these. It appears to be acceptable to set them all to 0. */ + drwav_uint16 country; + drwav_uint16 language; + drwav_uint16 dialect; + drwav_uint16 codePage; + + /* Size of the string not including any null terminator. */ + drwav_uint32 stringLength; + + /* The string. The *init_with_metadata functions null terminate this for convenience. */ + char* pString; +} drwav_list_labelled_cue_region; + +/* +Unknown Metadata + +This chunk just represents a type of chunk that dr_wav does not understand. + +Unknown metadata has a location attached to it. This is because wav files can have a LIST chunk +that contains subchunks. These LIST chunks can be one of two types. An adtl list, or an INFO +list. This enum is used to specify the location of a chunk that dr_wav currently doesn't support. +*/ +typedef enum +{ + drwav_metadata_location_invalid, + drwav_metadata_location_top_level, + drwav_metadata_location_inside_info_list, + drwav_metadata_location_inside_adtl_list +} drwav_metadata_location; + +typedef struct +{ + drwav_uint8 id[4]; + drwav_metadata_location chunkLocation; + drwav_uint32 dataSizeInBytes; + drwav_uint8* pData; +} drwav_unknown_metadata; + +/* +Metadata is saved as a union of all the supported types. +*/ +typedef struct +{ + /* Determines which item in the union is valid. */ + drwav_metadata_type type; + + union + { + drwav_cue cue; + drwav_smpl smpl; + drwav_acid acid; + drwav_inst inst; + drwav_bext bext; + drwav_list_label_or_note labelOrNote; /* List label or list note. */ + drwav_list_labelled_cue_region labelledCueRegion; + drwav_list_info_text infoText; /* Any of the list info types. */ + drwav_unknown_metadata unknown; + } data; +} drwav_metadata; + +typedef struct +{ + /* A pointer to the function to call when more data is needed. */ + drwav_read_proc onRead; + + /* A pointer to the function to call when data needs to be written. Only used when the drwav object is opened in write mode. */ + drwav_write_proc onWrite; + + /* A pointer to the function to call when the wav file needs to be seeked. */ + drwav_seek_proc onSeek; + + /* A pointer to the function to call when the position of the stream needs to be retrieved. */ + drwav_tell_proc onTell; + + /* The user data to pass to callbacks. */ + void* pUserData; + + /* Allocation callbacks. */ + drwav_allocation_callbacks allocationCallbacks; + + + /* Whether or not the WAV file is formatted as a standard RIFF file or W64. */ + drwav_container container; + + + /* Structure containing format information exactly as specified by the wav file. */ + drwav_fmt fmt; + + /* The sample rate. Will be set to something like 44100. */ + drwav_uint32 sampleRate; + + /* The number of channels. This will be set to 1 for monaural streams, 2 for stereo, etc. */ + drwav_uint16 channels; + + /* The bits per sample. Will be set to something like 16, 24, etc. */ + drwav_uint16 bitsPerSample; + + /* Equal to fmt.formatTag, or the value specified by fmt.subFormat if fmt.formatTag is equal to 65534 (WAVE_FORMAT_EXTENSIBLE). */ + drwav_uint16 translatedFormatTag; + + /* The total number of PCM frames making up the audio data. */ + drwav_uint64 totalPCMFrameCount; + + + /* The size in bytes of the data chunk. */ + drwav_uint64 dataChunkDataSize; + + /* The position in the stream of the first data byte of the data chunk. This is used for seeking. */ + drwav_uint64 dataChunkDataPos; + + /* The number of bytes remaining in the data chunk. */ + drwav_uint64 bytesRemaining; + + /* The current read position in PCM frames. */ + drwav_uint64 readCursorInPCMFrames; + + + /* + Only used in sequential write mode. Keeps track of the desired size of the "data" chunk at the point of initialization time. Always + set to 0 for non-sequential writes and when the drwav object is opened in read mode. Used for validation. + */ + drwav_uint64 dataChunkDataSizeTargetWrite; + + /* Keeps track of whether or not the wav writer was initialized in sequential mode. */ + drwav_bool32 isSequentialWrite; + + + /* A array of metadata. This is valid after the *init_with_metadata call returns. It will be valid until drwav_uninit() is called. You can take ownership of this data with drwav_take_ownership_of_metadata(). */ + drwav_metadata* pMetadata; + drwav_uint32 metadataCount; + + + /* A hack to avoid a DRWAV_MALLOC() when opening a decoder with drwav_init_memory(). */ + drwav__memory_stream memoryStream; + drwav__memory_stream_write memoryStreamWrite; + + + /* Microsoft ADPCM specific data. */ + struct + { + drwav_uint32 bytesRemainingInBlock; + drwav_uint16 predictor[2]; + drwav_int32 delta[2]; + drwav_int32 cachedFrames[4]; /* Samples are stored in this cache during decoding. */ + drwav_uint32 cachedFrameCount; + drwav_int32 prevFrames[2][2]; /* The previous 2 samples for each channel (2 channels at most). */ + } msadpcm; + + /* IMA ADPCM specific data. */ + struct + { + drwav_uint32 bytesRemainingInBlock; + drwav_int32 predictor[2]; + drwav_int32 stepIndex[2]; + drwav_int32 cachedFrames[16]; /* Samples are stored in this cache during decoding. */ + drwav_uint32 cachedFrameCount; + } ima; + + /* AIFF specific data. */ + struct + { + drwav_bool8 isLE; /* Will be set to true if the audio data is little-endian encoded. */ + drwav_bool8 isUnsigned; /* Only used for 8-bit samples. When set to true, will be treated as unsigned. */ + } aiff; +} drwav; + + +/* +Initializes a pre-allocated drwav object for reading. + +pWav [out] A pointer to the drwav object being initialized. +onRead [in] The function to call when data needs to be read from the client. +onSeek [in] The function to call when the read position of the client data needs to move. +onChunk [in, optional] The function to call when a chunk is enumerated at initialized time. +pUserData, pReadSeekUserData [in, optional] A pointer to application defined data that will be passed to onRead and onSeek. +pChunkUserData [in, optional] A pointer to application defined data that will be passed to onChunk. +flags [in, optional] A set of flags for controlling how things are loaded. + +Returns true if successful; false otherwise. + +Close the loader with drwav_uninit(). + +This is the lowest level function for initializing a WAV file. You can also use drwav_init_file() and drwav_init_memory() +to open the stream from a file or from a block of memory respectively. + +Possible values for flags: + DRWAV_SEQUENTIAL: Never perform a backwards seek while loading. This disables the chunk callback and will cause this function + to return as soon as the data chunk is found. Any chunks after the data chunk will be ignored. + +drwav_init() is equivalent to "drwav_init_ex(pWav, onRead, onSeek, NULL, pUserData, NULL, 0);". + +The onChunk callback is not called for the WAVE or FMT chunks. The contents of the FMT chunk can be read from pWav->fmt +after the function returns. + +See also: drwav_init_file(), drwav_init_memory(), drwav_uninit() +*/ +DRWAV_API drwav_bool32 drwav_init(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_ex(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, drwav_chunk_proc onChunk, void* pReadSeekTellUserData, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_with_metadata(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); + +/* +Initializes a pre-allocated drwav object for writing. + +onWrite [in] The function to call when data needs to be written. +onSeek [in] The function to call when the write position needs to move. +pUserData [in, optional] A pointer to application defined data that will be passed to onWrite and onSeek. +metadata, numMetadata [in, optional] An array of metadata objects that should be written to the file. The array is not edited. You are responsible for this metadata memory and it must maintain valid until drwav_uninit() is called. + +Returns true if successful; false otherwise. + +Close the writer with drwav_uninit(). + +This is the lowest level function for initializing a WAV file. You can also use drwav_init_file_write() and drwav_init_memory_write() +to open the stream from a file or from a block of memory respectively. + +If the total sample count is known, you can use drwav_init_write_sequential(). This avoids the need for dr_wav to perform +a post-processing step for storing the total sample count and the size of the data chunk which requires a backwards seek. + +See also: drwav_init_file_write(), drwav_init_memory_write(), drwav_uninit() +*/ +DRWAV_API drwav_bool32 drwav_init_write(drwav* pWav, const drwav_data_format* pFormat, drwav_write_proc onWrite, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_write_sequential(drwav* pWav, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, drwav_write_proc onWrite, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_write_sequential_pcm_frames(drwav* pWav, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, drwav_write_proc onWrite, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_write_with_metadata(drwav* pWav, const drwav_data_format* pFormat, drwav_write_proc onWrite, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks, drwav_metadata* pMetadata, drwav_uint32 metadataCount); + +/* +Utility function to determine the target size of the entire data to be written (including all headers and chunks). + +Returns the target size in bytes. + +The metadata argument can be NULL meaning no metadata exists. + +Useful if the application needs to know the size to allocate. + +Only writing to the RIFF chunk and one data chunk is currently supported. + +See also: drwav_init_write(), drwav_init_file_write(), drwav_init_memory_write() +*/ +DRWAV_API drwav_uint64 drwav_target_write_size_bytes(const drwav_data_format* pFormat, drwav_uint64 totalFrameCount, drwav_metadata* pMetadata, drwav_uint32 metadataCount); + +/* +Take ownership of the metadata objects that were allocated via one of the init_with_metadata() function calls. The init_with_metdata functions perform a single heap allocation for this metadata. + +Useful if you want the data to persist beyond the lifetime of the drwav object. + +You must free the data returned from this function using drwav_free(). +*/ +DRWAV_API drwav_metadata* drwav_take_ownership_of_metadata(drwav* pWav); + +/* +Uninitializes the given drwav object. + +Use this only for objects initialized with drwav_init*() functions (drwav_init(), drwav_init_ex(), drwav_init_write(), drwav_init_write_sequential()). +*/ +DRWAV_API drwav_result drwav_uninit(drwav* pWav); + + +/* +Reads raw audio data. + +This is the lowest level function for reading audio data. It simply reads the given number of +bytes of the raw internal sample data. + +Consider using drwav_read_pcm_frames_s16(), drwav_read_pcm_frames_s32() or drwav_read_pcm_frames_f32() for +reading sample data in a consistent format. + +pBufferOut can be NULL in which case a seek will be performed. + +Returns the number of bytes actually read. +*/ +DRWAV_API size_t drwav_read_raw(drwav* pWav, size_t bytesToRead, void* pBufferOut); + +/* +Reads up to the specified number of PCM frames from the WAV file. + +The output data will be in the file's internal format, converted to native-endian byte order. Use +drwav_read_pcm_frames_s16/f32/s32() to read data in a specific format. + +If the return value is less than it means the end of the file has been reached or +you have requested more PCM frames than can possibly fit in the output buffer. + +This function will only work when sample data is of a fixed size and uncompressed. If you are +using a compressed format consider using drwav_read_raw() or drwav_read_pcm_frames_s16/s32/f32(). + +pBufferOut can be NULL in which case a seek will be performed. +*/ +DRWAV_API drwav_uint64 drwav_read_pcm_frames(drwav* pWav, drwav_uint64 framesToRead, void* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_le(drwav* pWav, drwav_uint64 framesToRead, void* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_be(drwav* pWav, drwav_uint64 framesToRead, void* pBufferOut); + +/* +Seeks to the given PCM frame. + +Returns true if successful; false otherwise. +*/ +DRWAV_API drwav_bool32 drwav_seek_to_pcm_frame(drwav* pWav, drwav_uint64 targetFrameIndex); + +/* +Retrieves the current read position in pcm frames. +*/ +DRWAV_API drwav_result drwav_get_cursor_in_pcm_frames(drwav* pWav, drwav_uint64* pCursor); + +/* +Retrieves the length of the file. +*/ +DRWAV_API drwav_result drwav_get_length_in_pcm_frames(drwav* pWav, drwav_uint64* pLength); + + +/* +Writes raw audio data. + +Returns the number of bytes actually written. If this differs from bytesToWrite, it indicates an error. +*/ +DRWAV_API size_t drwav_write_raw(drwav* pWav, size_t bytesToWrite, const void* pData); + +/* +Writes PCM frames. + +Returns the number of PCM frames written. + +Input samples need to be in native-endian byte order. On big-endian architectures the input data will be converted to +little-endian. Use drwav_write_raw() to write raw audio data without performing any conversion. +*/ +DRWAV_API drwav_uint64 drwav_write_pcm_frames(drwav* pWav, drwav_uint64 framesToWrite, const void* pData); +DRWAV_API drwav_uint64 drwav_write_pcm_frames_le(drwav* pWav, drwav_uint64 framesToWrite, const void* pData); +DRWAV_API drwav_uint64 drwav_write_pcm_frames_be(drwav* pWav, drwav_uint64 framesToWrite, const void* pData); + +/* Conversion Utilities */ +#ifndef DR_WAV_NO_CONVERSION_API + +/* +Reads a chunk of audio data and converts it to signed 16-bit PCM samples. + +pBufferOut can be NULL in which case a seek will be performed. + +Returns the number of PCM frames actually read. + +If the return value is less than it means the end of the file has been reached. +*/ +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s16(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s16le(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s16be(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut); + +/* Low-level function for converting unsigned 8-bit PCM samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_u8_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting signed 24-bit PCM samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_s24_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting signed 32-bit PCM samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_s32_to_s16(drwav_int16* pOut, const drwav_int32* pIn, size_t sampleCount); + +/* Low-level function for converting IEEE 32-bit floating point samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_f32_to_s16(drwav_int16* pOut, const float* pIn, size_t sampleCount); + +/* Low-level function for converting IEEE 64-bit floating point samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_f64_to_s16(drwav_int16* pOut, const double* pIn, size_t sampleCount); + +/* Low-level function for converting A-law samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_alaw_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting u-law samples to signed 16-bit PCM samples. */ +DRWAV_API void drwav_mulaw_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount); + + +/* +Reads a chunk of audio data and converts it to IEEE 32-bit floating point samples. + +pBufferOut can be NULL in which case a seek will be performed. + +Returns the number of PCM frames actually read. + +If the return value is less than it means the end of the file has been reached. +*/ +DRWAV_API drwav_uint64 drwav_read_pcm_frames_f32(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_f32le(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_f32be(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut); + +/* Low-level function for converting unsigned 8-bit PCM samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_u8_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting signed 16-bit PCM samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_s16_to_f32(float* pOut, const drwav_int16* pIn, size_t sampleCount); + +/* Low-level function for converting signed 24-bit PCM samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_s24_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting signed 32-bit PCM samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_s32_to_f32(float* pOut, const drwav_int32* pIn, size_t sampleCount); + +/* Low-level function for converting IEEE 64-bit floating point samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_f64_to_f32(float* pOut, const double* pIn, size_t sampleCount); + +/* Low-level function for converting A-law samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_alaw_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting u-law samples to IEEE 32-bit floating point samples. */ +DRWAV_API void drwav_mulaw_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount); + + +/* +Reads a chunk of audio data and converts it to signed 32-bit PCM samples. + +pBufferOut can be NULL in which case a seek will be performed. + +Returns the number of PCM frames actually read. + +If the return value is less than it means the end of the file has been reached. +*/ +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s32(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s32le(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut); +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s32be(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut); + +/* Low-level function for converting unsigned 8-bit PCM samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_u8_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting signed 16-bit PCM samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_s16_to_s32(drwav_int32* pOut, const drwav_int16* pIn, size_t sampleCount); + +/* Low-level function for converting signed 24-bit PCM samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_s24_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting IEEE 32-bit floating point samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_f32_to_s32(drwav_int32* pOut, const float* pIn, size_t sampleCount); + +/* Low-level function for converting IEEE 64-bit floating point samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_f64_to_s32(drwav_int32* pOut, const double* pIn, size_t sampleCount); + +/* Low-level function for converting A-law samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_alaw_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount); + +/* Low-level function for converting u-law samples to signed 32-bit PCM samples. */ +DRWAV_API void drwav_mulaw_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount); + +#endif /* DR_WAV_NO_CONVERSION_API */ + + +/* High-Level Convenience Helpers */ + +#ifndef DR_WAV_NO_STDIO +/* +Helper for initializing a wave file for reading using stdio. + +This holds the internal FILE object until drwav_uninit() is called. Keep this in mind if you're caching drwav +objects because the operating system may restrict the number of file handles an application can have open at +any given time. +*/ +DRWAV_API drwav_bool32 drwav_init_file(drwav* pWav, const char* filename, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_ex(drwav* pWav, const char* filename, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_w(drwav* pWav, const wchar_t* filename, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_ex_w(drwav* pWav, const wchar_t* filename, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_with_metadata(drwav* pWav, const char* filename, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_with_metadata_w(drwav* pWav, const wchar_t* filename, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); + + +/* +Helper for initializing a wave file for writing using stdio. + +This holds the internal FILE object until drwav_uninit() is called. Keep this in mind if you're caching drwav +objects because the operating system may restrict the number of file handles an application can have open at +any given time. +*/ +DRWAV_API drwav_bool32 drwav_init_file_write(drwav* pWav, const char* filename, const drwav_data_format* pFormat, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_write_sequential(drwav* pWav, const char* filename, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_write_sequential_pcm_frames(drwav* pWav, const char* filename, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_write_w(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_write_sequential_w(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_file_write_sequential_pcm_frames_w(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, const drwav_allocation_callbacks* pAllocationCallbacks); +#endif /* DR_WAV_NO_STDIO */ + +/* +Helper for initializing a loader from a pre-allocated memory buffer. + +This does not create a copy of the data. It is up to the application to ensure the buffer remains valid for +the lifetime of the drwav object. + +The buffer should contain the contents of the entire wave file, not just the sample data. +*/ +DRWAV_API drwav_bool32 drwav_init_memory(drwav* pWav, const void* data, size_t dataSize, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_memory_ex(drwav* pWav, const void* data, size_t dataSize, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_memory_with_metadata(drwav* pWav, const void* data, size_t dataSize, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks); + +/* +Helper for initializing a writer which outputs data to a memory buffer. + +dr_wav will manage the memory allocations, however it is up to the caller to free the data with drwav_free(). + +The buffer will remain allocated even after drwav_uninit() is called. The buffer should not be considered valid +until after drwav_uninit() has been called. +*/ +DRWAV_API drwav_bool32 drwav_init_memory_write(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_memory_write_sequential(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_bool32 drwav_init_memory_write_sequential_pcm_frames(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, const drwav_allocation_callbacks* pAllocationCallbacks); + + +#ifndef DR_WAV_NO_CONVERSION_API +/* +Opens and reads an entire wav file in a single operation. + +The return value is a heap-allocated buffer containing the audio data. Use drwav_free() to free the buffer. +*/ +DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +#ifndef DR_WAV_NO_STDIO +/* +Opens and decodes an entire wav file in a single operation. + +The return value is a heap-allocated buffer containing the audio data. Use drwav_free() to free the buffer. +*/ +DRWAV_API drwav_int16* drwav_open_file_and_read_pcm_frames_s16(const char* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API float* drwav_open_file_and_read_pcm_frames_f32(const char* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int32* drwav_open_file_and_read_pcm_frames_s32(const char* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int16* drwav_open_file_and_read_pcm_frames_s16_w(const wchar_t* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API float* drwav_open_file_and_read_pcm_frames_f32_w(const wchar_t* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int32* drwav_open_file_and_read_pcm_frames_s32_w(const wchar_t* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +#endif +/* +Opens and decodes an entire wav file from a block of memory in a single operation. + +The return value is a heap-allocated buffer containing the audio data. Use drwav_free() to free the buffer. +*/ +DRWAV_API drwav_int16* drwav_open_memory_and_read_pcm_frames_s16(const void* data, size_t dataSize, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API float* drwav_open_memory_and_read_pcm_frames_f32(const void* data, size_t dataSize, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +DRWAV_API drwav_int32* drwav_open_memory_and_read_pcm_frames_s32(const void* data, size_t dataSize, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks); +#endif + +/* Frees data that was allocated internally by dr_wav. */ +DRWAV_API void drwav_free(void* p, const drwav_allocation_callbacks* pAllocationCallbacks); + +/* Converts bytes from a wav stream to a sized type of native endian. */ +DRWAV_API drwav_uint16 drwav_bytes_to_u16(const drwav_uint8* data); +DRWAV_API drwav_int16 drwav_bytes_to_s16(const drwav_uint8* data); +DRWAV_API drwav_uint32 drwav_bytes_to_u32(const drwav_uint8* data); +DRWAV_API drwav_int32 drwav_bytes_to_s32(const drwav_uint8* data); +DRWAV_API drwav_uint64 drwav_bytes_to_u64(const drwav_uint8* data); +DRWAV_API drwav_int64 drwav_bytes_to_s64(const drwav_uint8* data); +DRWAV_API float drwav_bytes_to_f32(const drwav_uint8* data); + +/* Compares a GUID for the purpose of checking the type of a Wave64 chunk. */ +DRWAV_API drwav_bool32 drwav_guid_equal(const drwav_uint8 a[16], const drwav_uint8 b[16]); + +/* Compares a four-character-code for the purpose of checking the type of a RIFF chunk. */ +DRWAV_API drwav_bool32 drwav_fourcc_equal(const drwav_uint8* a, const char* b); + +#ifdef __cplusplus +} +#endif +#endif /* dr_wav_h */ + + +/************************************************************************************************************************************************************ + ************************************************************************************************************************************************************ + + IMPLEMENTATION + + ************************************************************************************************************************************************************ + ************************************************************************************************************************************************************/ +#if defined(DR_WAV_IMPLEMENTATION) || defined(DRWAV_IMPLEMENTATION) +#ifndef dr_wav_c +#define dr_wav_c + +#ifdef __MRC__ +/* MrC currently doesn't compile dr_wav correctly with any optimizations enabled. */ +#pragma options opt off +#endif + +#include +#include +#include /* For INT_MAX */ + +#ifndef DR_WAV_NO_STDIO +#include +#ifndef DR_WAV_NO_WCHAR +#include +#endif +#endif + +/* Standard library stuff. */ +#ifndef DRWAV_ASSERT +#include +#define DRWAV_ASSERT(expression) assert(expression) +#endif +#ifndef DRWAV_MALLOC +#define DRWAV_MALLOC(sz) malloc((sz)) +#endif +#ifndef DRWAV_REALLOC +#define DRWAV_REALLOC(p, sz) realloc((p), (sz)) +#endif +#ifndef DRWAV_FREE +#define DRWAV_FREE(p) free((p)) +#endif +#ifndef DRWAV_COPY_MEMORY +#define DRWAV_COPY_MEMORY(dst, src, sz) memcpy((dst), (src), (sz)) +#endif +#ifndef DRWAV_ZERO_MEMORY +#define DRWAV_ZERO_MEMORY(p, sz) memset((p), 0, (sz)) +#endif +#ifndef DRWAV_ZERO_OBJECT +#define DRWAV_ZERO_OBJECT(p) DRWAV_ZERO_MEMORY((p), sizeof(*p)) +#endif + +#define drwav_countof(x) (sizeof(x) / sizeof(x[0])) +#define drwav_align(x, a) ((((x) + (a) - 1) / (a)) * (a)) +#define drwav_min(a, b) (((a) < (b)) ? (a) : (b)) +#define drwav_max(a, b) (((a) > (b)) ? (a) : (b)) +#define drwav_clamp(x, lo, hi) (drwav_max((lo), drwav_min((hi), (x)))) +#define drwav_offset_ptr(p, offset) (((drwav_uint8*)(p)) + (offset)) + +#define DRWAV_MAX_SIMD_VECTOR_SIZE 32 + +/* Architecture Detection */ +#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)) + #define DRWAV_X64 +#elif defined(__i386) || defined(_M_IX86) + #define DRWAV_X86 +#elif defined(__arm__) || defined(_M_ARM) + #define DRWAV_ARM +#endif +/* End Architecture Detection */ + +/* Inline */ +#ifdef _MSC_VER + #define DRWAV_INLINE __forceinline +#elif defined(__GNUC__) + /* + I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when + the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some + case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the + command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue + I am using "__inline__" only when we're compiling in strict ANSI mode. + */ + #if defined(__STRICT_ANSI__) + #define DRWAV_GNUC_INLINE_HINT __inline__ + #else + #define DRWAV_GNUC_INLINE_HINT inline + #endif + + #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) + #define DRWAV_INLINE DRWAV_GNUC_INLINE_HINT __attribute__((always_inline)) + #else + #define DRWAV_INLINE DRWAV_GNUC_INLINE_HINT + #endif +#elif defined(__WATCOMC__) + #define DRWAV_INLINE __inline +#else + #define DRWAV_INLINE +#endif +/* End Inline */ + +/* SIZE_MAX */ +#if defined(SIZE_MAX) + #define DRWAV_SIZE_MAX SIZE_MAX +#else + #if defined(_WIN64) || defined(_LP64) || defined(__LP64__) + #define DRWAV_SIZE_MAX ((drwav_uint64)0xFFFFFFFFFFFFFFFF) + #else + #define DRWAV_SIZE_MAX 0xFFFFFFFF + #endif +#endif +/* End SIZE_MAX */ + +/* Weird bit manipulation is for C89 compatibility (no direct support for 64-bit integers). */ +#define DRWAV_INT64_MIN ((drwav_int64) ((drwav_uint64)0x80000000 << 32)) +#define DRWAV_INT64_MAX ((drwav_int64)(((drwav_uint64)0x7FFFFFFF << 32) | 0xFFFFFFFF)) + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + #define DRWAV_HAS_BYTESWAP16_INTRINSIC + #define DRWAV_HAS_BYTESWAP32_INTRINSIC + #define DRWAV_HAS_BYTESWAP64_INTRINSIC +#elif defined(__clang__) + #if defined(__has_builtin) + #if __has_builtin(__builtin_bswap16) + #define DRWAV_HAS_BYTESWAP16_INTRINSIC + #endif + #if __has_builtin(__builtin_bswap32) + #define DRWAV_HAS_BYTESWAP32_INTRINSIC + #endif + #if __has_builtin(__builtin_bswap64) + #define DRWAV_HAS_BYTESWAP64_INTRINSIC + #endif + #endif +#elif defined(__GNUC__) + #if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) + #define DRWAV_HAS_BYTESWAP32_INTRINSIC + #define DRWAV_HAS_BYTESWAP64_INTRINSIC + #endif + #if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) + #define DRWAV_HAS_BYTESWAP16_INTRINSIC + #endif +#endif + +DRWAV_API void drwav_version(drwav_uint32* pMajor, drwav_uint32* pMinor, drwav_uint32* pRevision) +{ + if (pMajor) { + *pMajor = DRWAV_VERSION_MAJOR; + } + + if (pMinor) { + *pMinor = DRWAV_VERSION_MINOR; + } + + if (pRevision) { + *pRevision = DRWAV_VERSION_REVISION; + } +} + +DRWAV_API const char* drwav_version_string(void) +{ + return DRWAV_VERSION_STRING; +} + +/* +These limits are used for basic validation when initializing the decoder. If you exceed these limits, first of all: what on Earth are +you doing?! (Let me know, I'd be curious!) Second, you can adjust these by #define-ing them before the dr_wav implementation. +*/ +#ifndef DRWAV_MAX_SAMPLE_RATE +#define DRWAV_MAX_SAMPLE_RATE 384000 +#endif +#ifndef DRWAV_MAX_CHANNELS +#define DRWAV_MAX_CHANNELS 256 +#endif +#ifndef DRWAV_MAX_BITS_PER_SAMPLE +#define DRWAV_MAX_BITS_PER_SAMPLE 64 +#endif + +static const drwav_uint8 drwavGUID_W64_RIFF[16] = {0x72,0x69,0x66,0x66, 0x2E,0x91, 0xCF,0x11, 0xA5,0xD6, 0x28,0xDB,0x04,0xC1,0x00,0x00}; /* 66666972-912E-11CF-A5D6-28DB04C10000 */ +static const drwav_uint8 drwavGUID_W64_WAVE[16] = {0x77,0x61,0x76,0x65, 0xF3,0xAC, 0xD3,0x11, 0x8C,0xD1, 0x00,0xC0,0x4F,0x8E,0xDB,0x8A}; /* 65766177-ACF3-11D3-8CD1-00C04F8EDB8A */ +/*static const drwav_uint8 drwavGUID_W64_JUNK[16] = {0x6A,0x75,0x6E,0x6B, 0xF3,0xAC, 0xD3,0x11, 0x8C,0xD1, 0x00,0xC0,0x4F,0x8E,0xDB,0x8A};*/ /* 6B6E756A-ACF3-11D3-8CD1-00C04F8EDB8A */ +static const drwav_uint8 drwavGUID_W64_FMT [16] = {0x66,0x6D,0x74,0x20, 0xF3,0xAC, 0xD3,0x11, 0x8C,0xD1, 0x00,0xC0,0x4F,0x8E,0xDB,0x8A}; /* 20746D66-ACF3-11D3-8CD1-00C04F8EDB8A */ +static const drwav_uint8 drwavGUID_W64_FACT[16] = {0x66,0x61,0x63,0x74, 0xF3,0xAC, 0xD3,0x11, 0x8C,0xD1, 0x00,0xC0,0x4F,0x8E,0xDB,0x8A}; /* 74636166-ACF3-11D3-8CD1-00C04F8EDB8A */ +static const drwav_uint8 drwavGUID_W64_DATA[16] = {0x64,0x61,0x74,0x61, 0xF3,0xAC, 0xD3,0x11, 0x8C,0xD1, 0x00,0xC0,0x4F,0x8E,0xDB,0x8A}; /* 61746164-ACF3-11D3-8CD1-00C04F8EDB8A */ +/*static const drwav_uint8 drwavGUID_W64_SMPL[16] = {0x73,0x6D,0x70,0x6C, 0xF3,0xAC, 0xD3,0x11, 0x8C,0xD1, 0x00,0xC0,0x4F,0x8E,0xDB,0x8A};*/ /* 6C706D73-ACF3-11D3-8CD1-00C04F8EDB8A */ + + +static DRWAV_INLINE int drwav__is_little_endian(void) +{ +#if defined(DRWAV_X86) || defined(DRWAV_X64) + return DRWAV_TRUE; +#elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN + return DRWAV_TRUE; +#else + int n = 1; + return (*(char*)&n) == 1; +#endif +} + + +static DRWAV_INLINE void drwav_bytes_to_guid(const drwav_uint8* data, drwav_uint8* guid) +{ + int i; + for (i = 0; i < 16; ++i) { + guid[i] = data[i]; + } +} + + +static DRWAV_INLINE drwav_uint16 drwav__bswap16(drwav_uint16 n) +{ +#ifdef DRWAV_HAS_BYTESWAP16_INTRINSIC + #if defined(_MSC_VER) + return _byteswap_ushort(n); + #elif defined(__GNUC__) || defined(__clang__) + return __builtin_bswap16(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + return ((n & 0xFF00) >> 8) | + ((n & 0x00FF) << 8); +#endif +} + +static DRWAV_INLINE drwav_uint32 drwav__bswap32(drwav_uint32 n) +{ +#ifdef DRWAV_HAS_BYTESWAP32_INTRINSIC + #if defined(_MSC_VER) + return _byteswap_ulong(n); + #elif defined(__GNUC__) || defined(__clang__) + #if defined(DRWAV_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 6) && !defined(DRWAV_64BIT) /* <-- 64-bit inline assembly has not been tested, so disabling for now. */ + /* Inline assembly optimized implementation for ARM. In my testing, GCC does not generate optimized code with __builtin_bswap32(). */ + drwav_uint32 r; + __asm__ __volatile__ ( + #if defined(DRWAV_64BIT) + "rev %w[out], %w[in]" : [out]"=r"(r) : [in]"r"(n) /* <-- This is untested. If someone in the community could test this, that would be appreciated! */ + #else + "rev %[out], %[in]" : [out]"=r"(r) : [in]"r"(n) + #endif + ); + return r; + #else + return __builtin_bswap32(n); + #endif + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + return ((n & 0xFF000000) >> 24) | + ((n & 0x00FF0000) >> 8) | + ((n & 0x0000FF00) << 8) | + ((n & 0x000000FF) << 24); +#endif +} + +static DRWAV_INLINE drwav_uint64 drwav__bswap64(drwav_uint64 n) +{ +#ifdef DRWAV_HAS_BYTESWAP64_INTRINSIC + #if defined(_MSC_VER) + return _byteswap_uint64(n); + #elif defined(__GNUC__) || defined(__clang__) + return __builtin_bswap64(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + /* Weird "<< 32" bitshift is required for C89 because it doesn't support 64-bit constants. Should be optimized out by a good compiler. */ + return ((n & ((drwav_uint64)0xFF000000 << 32)) >> 56) | + ((n & ((drwav_uint64)0x00FF0000 << 32)) >> 40) | + ((n & ((drwav_uint64)0x0000FF00 << 32)) >> 24) | + ((n & ((drwav_uint64)0x000000FF << 32)) >> 8) | + ((n & ((drwav_uint64)0xFF000000 )) << 8) | + ((n & ((drwav_uint64)0x00FF0000 )) << 24) | + ((n & ((drwav_uint64)0x0000FF00 )) << 40) | + ((n & ((drwav_uint64)0x000000FF )) << 56); +#endif +} + + +static DRWAV_INLINE drwav_int16 drwav__bswap_s16(drwav_int16 n) +{ + return (drwav_int16)drwav__bswap16((drwav_uint16)n); +} + +static DRWAV_INLINE void drwav__bswap_samples_s16(drwav_int16* pSamples, drwav_uint64 sampleCount) +{ + drwav_uint64 iSample; + for (iSample = 0; iSample < sampleCount; iSample += 1) { + pSamples[iSample] = drwav__bswap_s16(pSamples[iSample]); + } +} + + +static DRWAV_INLINE void drwav__bswap_s24(drwav_uint8* p) +{ + drwav_uint8 t; + t = p[0]; + p[0] = p[2]; + p[2] = t; +} + +static DRWAV_INLINE void drwav__bswap_samples_s24(drwav_uint8* pSamples, drwav_uint64 sampleCount) +{ + drwav_uint64 iSample; + for (iSample = 0; iSample < sampleCount; iSample += 1) { + drwav_uint8* pSample = pSamples + (iSample*3); + drwav__bswap_s24(pSample); + } +} + + +static DRWAV_INLINE drwav_int32 drwav__bswap_s32(drwav_int32 n) +{ + return (drwav_int32)drwav__bswap32((drwav_uint32)n); +} + +static DRWAV_INLINE void drwav__bswap_samples_s32(drwav_int32* pSamples, drwav_uint64 sampleCount) +{ + drwav_uint64 iSample; + for (iSample = 0; iSample < sampleCount; iSample += 1) { + pSamples[iSample] = drwav__bswap_s32(pSamples[iSample]); + } +} + + +static DRWAV_INLINE drwav_int64 drwav__bswap_s64(drwav_int64 n) +{ + return (drwav_int64)drwav__bswap64((drwav_uint64)n); +} + +static DRWAV_INLINE void drwav__bswap_samples_s64(drwav_int64* pSamples, drwav_uint64 sampleCount) +{ + drwav_uint64 iSample; + for (iSample = 0; iSample < sampleCount; iSample += 1) { + pSamples[iSample] = drwav__bswap_s64(pSamples[iSample]); + } +} + + +static DRWAV_INLINE float drwav__bswap_f32(float n) +{ + union { + drwav_uint32 i; + float f; + } x; + x.f = n; + x.i = drwav__bswap32(x.i); + + return x.f; +} + +static DRWAV_INLINE void drwav__bswap_samples_f32(float* pSamples, drwav_uint64 sampleCount) +{ + drwav_uint64 iSample; + for (iSample = 0; iSample < sampleCount; iSample += 1) { + pSamples[iSample] = drwav__bswap_f32(pSamples[iSample]); + } +} + + +static DRWAV_INLINE void drwav__bswap_samples(void* pSamples, drwav_uint64 sampleCount, drwav_uint32 bytesPerSample) +{ + switch (bytesPerSample) + { + case 1: + { + /* No-op. */ + } break; + case 2: + { + drwav__bswap_samples_s16((drwav_int16*)pSamples, sampleCount); + } break; + case 3: + { + drwav__bswap_samples_s24((drwav_uint8*)pSamples, sampleCount); + } break; + case 4: + { + drwav__bswap_samples_s32((drwav_int32*)pSamples, sampleCount); + } break; + case 8: + { + drwav__bswap_samples_s64((drwav_int64*)pSamples, sampleCount); + } break; + default: + { + drwav_uint64 iSample; + + for (iSample = 0; iSample < sampleCount; iSample += 1) { + drwav_uint8* pSample = (drwav_uint8*)pSamples + (iSample * bytesPerSample); + drwav_uint32 iByte; + + for (iByte = 0; iByte < bytesPerSample / 2; iByte += 1) { + drwav_uint8 temp = pSample[iByte]; + pSample[iByte] = pSample[bytesPerSample - iByte - 1]; + pSample[bytesPerSample - iByte - 1] = temp; + } + } + } break; + } +} + + + +DRWAV_PRIVATE DRWAV_INLINE drwav_bool32 drwav_is_container_be(drwav_container container) +{ + if (container == drwav_container_rifx || container == drwav_container_aiff) { + return DRWAV_TRUE; + } else { + return DRWAV_FALSE; + } +} + + +DRWAV_PRIVATE DRWAV_INLINE drwav_uint16 drwav_bytes_to_u16_le(const drwav_uint8* data) +{ + return ((drwav_uint16)data[0] << 0) | ((drwav_uint16)data[1] << 8); +} + +DRWAV_PRIVATE DRWAV_INLINE drwav_uint16 drwav_bytes_to_u16_be(const drwav_uint8* data) +{ + return ((drwav_uint16)data[1] << 0) | ((drwav_uint16)data[0] << 8); +} + +DRWAV_PRIVATE DRWAV_INLINE drwav_uint16 drwav_bytes_to_u16_ex(const drwav_uint8* data, drwav_container container) +{ + if (drwav_is_container_be(container)) { + return drwav_bytes_to_u16_be(data); + } else { + return drwav_bytes_to_u16_le(data); + } +} + + +DRWAV_PRIVATE DRWAV_INLINE drwav_uint32 drwav_bytes_to_u32_le(const drwav_uint8* data) +{ + return ((drwav_uint32)data[0] << 0) | ((drwav_uint32)data[1] << 8) | ((drwav_uint32)data[2] << 16) | ((drwav_uint32)data[3] << 24); +} + +DRWAV_PRIVATE DRWAV_INLINE drwav_uint32 drwav_bytes_to_u32_be(const drwav_uint8* data) +{ + return ((drwav_uint32)data[3] << 0) | ((drwav_uint32)data[2] << 8) | ((drwav_uint32)data[1] << 16) | ((drwav_uint32)data[0] << 24); +} + +DRWAV_PRIVATE DRWAV_INLINE drwav_uint32 drwav_bytes_to_u32_ex(const drwav_uint8* data, drwav_container container) +{ + if (drwav_is_container_be(container)) { + return drwav_bytes_to_u32_be(data); + } else { + return drwav_bytes_to_u32_le(data); + } +} + + + +DRWAV_PRIVATE drwav_int64 drwav_aiff_extented_to_s64(const drwav_uint8* data) +{ + drwav_uint32 exponent = ((drwav_uint32)data[0] << 8) | data[1]; + drwav_uint64 hi = ((drwav_uint64)data[2] << 24) | ((drwav_uint64)data[3] << 16) | ((drwav_uint64)data[4] << 8) | ((drwav_uint64)data[5] << 0); + drwav_uint64 lo = ((drwav_uint64)data[6] << 24) | ((drwav_uint64)data[7] << 16) | ((drwav_uint64)data[8] << 8) | ((drwav_uint64)data[9] << 0); + drwav_uint64 significand = (hi << 32) | lo; + int sign = exponent >> 15; + + /* Remove sign bit. */ + exponent &= 0x7FFF; + + /* Special cases. */ + if (exponent == 0 && significand == 0) { + return 0; + } else if (exponent == 0x7FFF) { + return sign ? DRWAV_INT64_MIN : DRWAV_INT64_MAX; /* Infinite. */ + } + + exponent -= 16383; + + if (exponent > 63) { + return sign ? DRWAV_INT64_MIN : DRWAV_INT64_MAX; /* Too big for a 64-bit integer. */ + } else if (exponent < 1) { + return 0; /* Number is less than 1, so rounds down to 0. */ + } + + significand >>= (63 - exponent); + + if (sign) { + return -(drwav_int64)significand; + } else { + return (drwav_int64)significand; + } +} + + +DRWAV_PRIVATE void* drwav__malloc_default(size_t sz, void* pUserData) +{ + (void)pUserData; + return DRWAV_MALLOC(sz); +} + +DRWAV_PRIVATE void* drwav__realloc_default(void* p, size_t sz, void* pUserData) +{ + (void)pUserData; + return DRWAV_REALLOC(p, sz); +} + +DRWAV_PRIVATE void drwav__free_default(void* p, void* pUserData) +{ + (void)pUserData; + DRWAV_FREE(p); +} + + +DRWAV_PRIVATE void* drwav__malloc_from_callbacks(size_t sz, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onMalloc != NULL) { + return pAllocationCallbacks->onMalloc(sz, pAllocationCallbacks->pUserData); + } + + /* Try using realloc(). */ + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(NULL, sz, pAllocationCallbacks->pUserData); + } + + return NULL; +} + +DRWAV_PRIVATE void* drwav__realloc_from_callbacks(void* p, size_t szNew, size_t szOld, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(p, szNew, pAllocationCallbacks->pUserData); + } + + /* Try emulating realloc() in terms of malloc()/free(). */ + if (pAllocationCallbacks->onMalloc != NULL && pAllocationCallbacks->onFree != NULL) { + void* p2; + + p2 = pAllocationCallbacks->onMalloc(szNew, pAllocationCallbacks->pUserData); + if (p2 == NULL) { + return NULL; + } + + if (p != NULL) { + DRWAV_COPY_MEMORY(p2, p, szOld); + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } + + return p2; + } + + return NULL; +} + +DRWAV_PRIVATE void drwav__free_from_callbacks(void* p, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (p == NULL || pAllocationCallbacks == NULL) { + return; + } + + if (pAllocationCallbacks->onFree != NULL) { + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } +} + + +DRWAV_PRIVATE drwav_allocation_callbacks drwav_copy_allocation_callbacks_or_defaults(const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + /* Copy. */ + return *pAllocationCallbacks; + } else { + /* Defaults. */ + drwav_allocation_callbacks allocationCallbacks; + allocationCallbacks.pUserData = NULL; + allocationCallbacks.onMalloc = drwav__malloc_default; + allocationCallbacks.onRealloc = drwav__realloc_default; + allocationCallbacks.onFree = drwav__free_default; + return allocationCallbacks; + } +} + + +static DRWAV_INLINE drwav_bool32 drwav__is_compressed_format_tag(drwav_uint16 formatTag) +{ + return + formatTag == DR_WAVE_FORMAT_ADPCM || + formatTag == DR_WAVE_FORMAT_DVI_ADPCM; +} + +DRWAV_PRIVATE unsigned int drwav__chunk_padding_size_riff(drwav_uint64 chunkSize) +{ + return (unsigned int)(chunkSize % 2); +} + +DRWAV_PRIVATE unsigned int drwav__chunk_padding_size_w64(drwav_uint64 chunkSize) +{ + return (unsigned int)(chunkSize % 8); +} + +DRWAV_PRIVATE unsigned int drwav_calculate_padding_size(drwav_container container, drwav_uint64 chunkSize) +{ + if (container == drwav_container_riff || container == drwav_container_rf64) { + return drwav__chunk_padding_size_riff(chunkSize); + } else { + return drwav__chunk_padding_size_w64(chunkSize); + } +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav_uint64 samplesToRead, drwav_int16* pBufferOut); +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ima(drwav* pWav, drwav_uint64 samplesToRead, drwav_int16* pBufferOut); +DRWAV_PRIVATE drwav_bool32 drwav_init_write__internal(drwav* pWav, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount); + +DRWAV_PRIVATE drwav_result drwav__read_chunk_header(drwav_read_proc onRead, void* pUserData, drwav_container container, drwav_uint64* pRunningBytesReadOut, drwav_chunk_header* pHeaderOut) +{ + if (container == drwav_container_riff || container == drwav_container_rifx || container == drwav_container_rf64 || container == drwav_container_aiff) { + drwav_uint8 sizeInBytes[4]; + + if (onRead(pUserData, pHeaderOut->id.fourcc, 4) != 4) { + return DRWAV_AT_END; + } + + if (onRead(pUserData, sizeInBytes, 4) != 4) { + return DRWAV_INVALID_FILE; + } + + pHeaderOut->sizeInBytes = drwav_bytes_to_u32_ex(sizeInBytes, container); + pHeaderOut->paddingSize = drwav__chunk_padding_size_riff(pHeaderOut->sizeInBytes); + + *pRunningBytesReadOut += 8; + } else if (container == drwav_container_w64) { + drwav_uint8 sizeInBytes[8]; + + if (onRead(pUserData, pHeaderOut->id.guid, 16) != 16) { + return DRWAV_AT_END; + } + + if (onRead(pUserData, sizeInBytes, 8) != 8) { + return DRWAV_INVALID_FILE; + } + + pHeaderOut->sizeInBytes = drwav_bytes_to_u64(sizeInBytes); + + /* Subtract 24 from the size because with w64 the reported chunk size includes the size of the header itself. */ + if (pHeaderOut->sizeInBytes >= 24) { + pHeaderOut->sizeInBytes -= 24; + } else { + return DRWAV_INVALID_FILE; + } + + pHeaderOut->paddingSize = drwav__chunk_padding_size_w64(pHeaderOut->sizeInBytes); + *pRunningBytesReadOut += 24; + } else { + return DRWAV_INVALID_FILE; + } + + return DRWAV_SUCCESS; +} + +DRWAV_PRIVATE drwav_bool32 drwav__seek_forward(drwav_seek_proc onSeek, drwav_uint64 offset, void* pUserData) +{ + drwav_uint64 bytesRemainingToSeek = offset; + while (bytesRemainingToSeek > 0) { + if (bytesRemainingToSeek > 0x7FFFFFFF) { + if (!onSeek(pUserData, 0x7FFFFFFF, DRWAV_SEEK_CUR)) { + return DRWAV_FALSE; + } + bytesRemainingToSeek -= 0x7FFFFFFF; + } else { + if (!onSeek(pUserData, (int)bytesRemainingToSeek, DRWAV_SEEK_CUR)) { + return DRWAV_FALSE; + } + bytesRemainingToSeek = 0; + } + } + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE drwav_bool32 drwav__seek_from_start(drwav_seek_proc onSeek, drwav_uint64 offset, void* pUserData) +{ + if (offset <= 0x7FFFFFFF) { + return onSeek(pUserData, (int)offset, DRWAV_SEEK_SET); + } + + /* Larger than 32-bit seek. */ + if (!onSeek(pUserData, 0x7FFFFFFF, DRWAV_SEEK_SET)) { + return DRWAV_FALSE; + } + offset -= 0x7FFFFFFF; + + for (;;) { + if (offset <= 0x7FFFFFFF) { + return onSeek(pUserData, (int)offset, DRWAV_SEEK_CUR); + } + + if (!onSeek(pUserData, 0x7FFFFFFF, DRWAV_SEEK_CUR)) { + return DRWAV_FALSE; + } + offset -= 0x7FFFFFFF; + } + + /* Should never get here. */ + /*return DRWAV_TRUE; */ +} + + + +DRWAV_PRIVATE size_t drwav__on_read(drwav_read_proc onRead, void* pUserData, void* pBufferOut, size_t bytesToRead, drwav_uint64* pCursor) +{ + size_t bytesRead; + + DRWAV_ASSERT(onRead != NULL); + DRWAV_ASSERT(pCursor != NULL); + + bytesRead = onRead(pUserData, pBufferOut, bytesToRead); + *pCursor += bytesRead; + return bytesRead; +} + +#if 0 +DRWAV_PRIVATE drwav_bool32 drwav__on_seek(drwav_seek_proc onSeek, void* pUserData, int offset, drwav_seek_origin origin, drwav_uint64* pCursor) +{ + DRWAV_ASSERT(onSeek != NULL); + DRWAV_ASSERT(pCursor != NULL); + + if (!onSeek(pUserData, offset, origin)) { + return DRWAV_FALSE; + } + + if (origin == DRWAV_SEEK_SET) { + *pCursor = offset; + } else { + *pCursor += offset; + } + + return DRWAV_TRUE; +} +#endif + + +#define DRWAV_SMPL_BYTES 36 +#define DRWAV_SMPL_LOOP_BYTES 24 +#define DRWAV_INST_BYTES 7 +#define DRWAV_ACID_BYTES 24 +#define DRWAV_CUE_BYTES 4 +#define DRWAV_BEXT_BYTES 602 +#define DRWAV_BEXT_DESCRIPTION_BYTES 256 +#define DRWAV_BEXT_ORIGINATOR_NAME_BYTES 32 +#define DRWAV_BEXT_ORIGINATOR_REF_BYTES 32 +#define DRWAV_BEXT_RESERVED_BYTES 180 +#define DRWAV_BEXT_UMID_BYTES 64 +#define DRWAV_CUE_POINT_BYTES 24 +#define DRWAV_LIST_LABEL_OR_NOTE_BYTES 4 +#define DRWAV_LIST_LABELLED_TEXT_BYTES 20 + +#define DRWAV_METADATA_ALIGNMENT 8 + +typedef enum +{ + drwav__metadata_parser_stage_count, + drwav__metadata_parser_stage_read +} drwav__metadata_parser_stage; + +typedef struct +{ + drwav_read_proc onRead; + drwav_seek_proc onSeek; + void *pReadSeekUserData; + drwav__metadata_parser_stage stage; + drwav_metadata *pMetadata; + drwav_uint32 metadataCount; + drwav_uint8 *pData; + drwav_uint8 *pDataCursor; + drwav_uint64 metadataCursor; + drwav_uint64 extraCapacity; +} drwav__metadata_parser; + +DRWAV_PRIVATE size_t drwav__metadata_memory_capacity(drwav__metadata_parser* pParser) +{ + drwav_uint64 cap = sizeof(drwav_metadata) * (drwav_uint64)pParser->metadataCount + pParser->extraCapacity; + if (cap > DRWAV_SIZE_MAX) { + return 0; /* Too big. */ + } + + return (size_t)cap; /* Safe cast thanks to the check above. */ +} + +DRWAV_PRIVATE drwav_uint8* drwav__metadata_get_memory(drwav__metadata_parser* pParser, size_t size, size_t align) +{ + drwav_uint8* pResult; + + if (align) { + drwav_uintptr modulo = (drwav_uintptr)pParser->pDataCursor % align; + if (modulo != 0) { + pParser->pDataCursor += align - modulo; + } + } + + pResult = pParser->pDataCursor; + + /* + Getting to the point where this function is called means there should always be memory + available. Out of memory checks should have been done at an earlier stage. + */ + DRWAV_ASSERT((pResult + size) <= (pParser->pData + drwav__metadata_memory_capacity(pParser))); + + pParser->pDataCursor += size; + return pResult; +} + +DRWAV_PRIVATE void drwav__metadata_request_extra_memory_for_stage_2(drwav__metadata_parser* pParser, size_t bytes, size_t align) +{ + size_t extra = bytes + (align ? (align - 1) : 0); + pParser->extraCapacity += extra; +} + +DRWAV_PRIVATE drwav_result drwav__metadata_alloc(drwav__metadata_parser* pParser, drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pParser->extraCapacity != 0 || pParser->metadataCount != 0) { + pAllocationCallbacks->onFree(pParser->pData, pAllocationCallbacks->pUserData); + + pParser->pData = (drwav_uint8*)pAllocationCallbacks->onMalloc(drwav__metadata_memory_capacity(pParser), pAllocationCallbacks->pUserData); + pParser->pDataCursor = pParser->pData; + + if (pParser->pData == NULL) { + return DRWAV_OUT_OF_MEMORY; + } + + /* + We don't need to worry about specifying an alignment here because malloc always returns something + of suitable alignment. This also means pParser->pMetadata is all that we need to store in order + for us to free when we are done. + */ + pParser->pMetadata = (drwav_metadata*)drwav__metadata_get_memory(pParser, sizeof(drwav_metadata) * pParser->metadataCount, 1); + pParser->metadataCursor = 0; + } + + return DRWAV_SUCCESS; +} + +DRWAV_PRIVATE size_t drwav__metadata_parser_read(drwav__metadata_parser* pParser, void* pBufferOut, size_t bytesToRead, drwav_uint64* pCursor) +{ + if (pCursor != NULL) { + return drwav__on_read(pParser->onRead, pParser->pReadSeekUserData, pBufferOut, bytesToRead, pCursor); + } else { + return pParser->onRead(pParser->pReadSeekUserData, pBufferOut, bytesToRead); + } +} + +DRWAV_PRIVATE drwav_bool32 drwav__metadata_validate_smpl_chunk(const drwav_chunk_header* pChunkHeader, drwav_uint32 loopCount, drwav_uint32 samplerSpecificDataSizeInBytes, drwav_uint64* pTrailingDataSizeInBytes) +{ + drwav_uint64 remainingDataSizeInBytes; + + DRWAV_ASSERT(pChunkHeader != NULL); + + if (pChunkHeader->sizeInBytes < DRWAV_SMPL_BYTES) { + return DRWAV_FALSE; + } + + remainingDataSizeInBytes = pChunkHeader->sizeInBytes - DRWAV_SMPL_BYTES; + if ((drwav_uint64)loopCount > remainingDataSizeInBytes / DRWAV_SMPL_LOOP_BYTES) { + return DRWAV_FALSE; + } + + remainingDataSizeInBytes -= (drwav_uint64)loopCount * DRWAV_SMPL_LOOP_BYTES; + if ((drwav_uint64)samplerSpecificDataSizeInBytes > remainingDataSizeInBytes) { + return DRWAV_FALSE; + } + + if (pTrailingDataSizeInBytes != NULL) { + *pTrailingDataSizeInBytes = remainingDataSizeInBytes - samplerSpecificDataSizeInBytes; + } + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE drwav_uint64 drwav__read_smpl_to_metadata_obj(drwav__metadata_parser* pParser, const drwav_chunk_header* pChunkHeader, drwav_metadata* pMetadata) +{ + drwav_uint8 smplHeaderData[DRWAV_SMPL_BYTES]; + drwav_uint64 totalBytesRead = 0; + size_t bytesJustRead; + + if (pMetadata == NULL) { + return 0; + } + + bytesJustRead = drwav__metadata_parser_read(pParser, smplHeaderData, sizeof(smplHeaderData), &totalBytesRead); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + DRWAV_ASSERT(pChunkHeader != NULL); + + if (pMetadata != NULL && bytesJustRead == sizeof(smplHeaderData)) { + drwav_uint32 iSampleLoop; + drwav_uint32 loopCount; + drwav_uint32 samplerSpecificDataSizeInBytes; + drwav_uint64 trailingDataSizeInBytes; + + /* + When we calculated the amount of memory required for the "smpl" chunk we excluded the chunk entirely + if its loop or sampler-specific data exceeded the chunk size. When this happens, the second stage will + still hit this path but the `pMetadata` will either point at the end of the allocation or at another + chunk. We need to repeat the validation before dereferencing the pMetadata object. + */ + loopCount = drwav_bytes_to_u32(smplHeaderData + 28); + samplerSpecificDataSizeInBytes = drwav_bytes_to_u32(smplHeaderData + 32); + if (!drwav__metadata_validate_smpl_chunk(pChunkHeader, loopCount, samplerSpecificDataSizeInBytes, &trailingDataSizeInBytes)) { + return totalBytesRead; + } + + pMetadata->type = drwav_metadata_type_smpl; + pMetadata->data.smpl.manufacturerId = drwav_bytes_to_u32(smplHeaderData + 0); + pMetadata->data.smpl.productId = drwav_bytes_to_u32(smplHeaderData + 4); + pMetadata->data.smpl.samplePeriodNanoseconds = drwav_bytes_to_u32(smplHeaderData + 8); + pMetadata->data.smpl.midiUnityNote = drwav_bytes_to_u32(smplHeaderData + 12); + pMetadata->data.smpl.midiPitchFraction = drwav_bytes_to_u32(smplHeaderData + 16); + pMetadata->data.smpl.smpteFormat = drwav_bytes_to_u32(smplHeaderData + 20); + pMetadata->data.smpl.smpteOffset = drwav_bytes_to_u32(smplHeaderData + 24); + pMetadata->data.smpl.sampleLoopCount = loopCount; + pMetadata->data.smpl.samplerSpecificDataSizeInBytes = samplerSpecificDataSizeInBytes; + + pMetadata->data.smpl.pLoops = (drwav_smpl_loop*)drwav__metadata_get_memory(pParser, sizeof(drwav_smpl_loop) * pMetadata->data.smpl.sampleLoopCount, DRWAV_METADATA_ALIGNMENT); + + for (iSampleLoop = 0; iSampleLoop < pMetadata->data.smpl.sampleLoopCount; ++iSampleLoop) { + drwav_uint8 smplLoopData[DRWAV_SMPL_LOOP_BYTES]; + bytesJustRead = drwav__metadata_parser_read(pParser, smplLoopData, sizeof(smplLoopData), &totalBytesRead); + + if (bytesJustRead == sizeof(smplLoopData)) { + pMetadata->data.smpl.pLoops[iSampleLoop].cuePointId = drwav_bytes_to_u32(smplLoopData + 0); + pMetadata->data.smpl.pLoops[iSampleLoop].type = drwav_bytes_to_u32(smplLoopData + 4); + pMetadata->data.smpl.pLoops[iSampleLoop].firstSampleOffset = drwav_bytes_to_u32(smplLoopData + 8); + pMetadata->data.smpl.pLoops[iSampleLoop].lastSampleOffset = drwav_bytes_to_u32(smplLoopData + 12); + pMetadata->data.smpl.pLoops[iSampleLoop].sampleFraction = drwav_bytes_to_u32(smplLoopData + 16); + pMetadata->data.smpl.pLoops[iSampleLoop].playCount = drwav_bytes_to_u32(smplLoopData + 20); + } else { + return totalBytesRead; + } + } + + if (pMetadata->data.smpl.samplerSpecificDataSizeInBytes > 0) { + pMetadata->data.smpl.pSamplerSpecificData = drwav__metadata_get_memory(pParser, pMetadata->data.smpl.samplerSpecificDataSizeInBytes, 1); + DRWAV_ASSERT(pMetadata->data.smpl.pSamplerSpecificData != NULL); + + bytesJustRead = drwav__metadata_parser_read(pParser, pMetadata->data.smpl.pSamplerSpecificData, pMetadata->data.smpl.samplerSpecificDataSizeInBytes, &totalBytesRead); + if (bytesJustRead != pMetadata->data.smpl.samplerSpecificDataSizeInBytes) { + return totalBytesRead; + } + } + + if (trailingDataSizeInBytes > 0) { + if (!drwav__seek_forward(pParser->onSeek, trailingDataSizeInBytes, pParser->pReadSeekUserData)) { + return totalBytesRead; + } + totalBytesRead += trailingDataSizeInBytes; + } + } + + return totalBytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__read_cue_to_metadata_obj(drwav__metadata_parser* pParser, const drwav_chunk_header* pChunkHeader, drwav_metadata* pMetadata) +{ + drwav_uint8 cueHeaderSectionData[DRWAV_CUE_BYTES]; + drwav_uint64 totalBytesRead = 0; + size_t bytesJustRead; + + if (pMetadata == NULL) { + return 0; + } + + bytesJustRead = drwav__metadata_parser_read(pParser, cueHeaderSectionData, sizeof(cueHeaderSectionData), &totalBytesRead); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + + if (bytesJustRead == sizeof(cueHeaderSectionData)) { + pMetadata->type = drwav_metadata_type_cue; + pMetadata->data.cue.cuePointCount = drwav_bytes_to_u32(cueHeaderSectionData); + + /* + We need to validate the cue point count against the size of the chunk so we don't read + beyond the chunk. + */ + if (pMetadata->data.cue.cuePointCount == (pChunkHeader->sizeInBytes - DRWAV_CUE_BYTES) / DRWAV_CUE_POINT_BYTES) { + pMetadata->data.cue.pCuePoints = (drwav_cue_point*)drwav__metadata_get_memory(pParser, sizeof(drwav_cue_point) * pMetadata->data.cue.cuePointCount, DRWAV_METADATA_ALIGNMENT); + DRWAV_ASSERT(pMetadata->data.cue.pCuePoints != NULL); + + if (pMetadata->data.cue.cuePointCount > 0) { + drwav_uint32 iCuePoint; + + for (iCuePoint = 0; iCuePoint < pMetadata->data.cue.cuePointCount; ++iCuePoint) { + drwav_uint8 cuePointData[DRWAV_CUE_POINT_BYTES]; + bytesJustRead = drwav__metadata_parser_read(pParser, cuePointData, sizeof(cuePointData), &totalBytesRead); + + if (bytesJustRead == sizeof(cuePointData)) { + pMetadata->data.cue.pCuePoints[iCuePoint].id = drwav_bytes_to_u32(cuePointData + 0); + pMetadata->data.cue.pCuePoints[iCuePoint].playOrderPosition = drwav_bytes_to_u32(cuePointData + 4); + pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[0] = cuePointData[8]; + pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[1] = cuePointData[9]; + pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[2] = cuePointData[10]; + pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId[3] = cuePointData[11]; + pMetadata->data.cue.pCuePoints[iCuePoint].chunkStart = drwav_bytes_to_u32(cuePointData + 12); + pMetadata->data.cue.pCuePoints[iCuePoint].blockStart = drwav_bytes_to_u32(cuePointData + 16); + pMetadata->data.cue.pCuePoints[iCuePoint].sampleOffset = drwav_bytes_to_u32(cuePointData + 20); + } else { + break; + } + } + } + } + } + + return totalBytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__read_inst_to_metadata_obj(drwav__metadata_parser* pParser, drwav_metadata* pMetadata) +{ + drwav_uint8 instData[DRWAV_INST_BYTES]; + drwav_uint64 bytesRead; + + if (pMetadata == NULL) { + return 0; + } + + bytesRead = drwav__metadata_parser_read(pParser, instData, sizeof(instData), NULL); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + + if (bytesRead == sizeof(instData)) { + pMetadata->type = drwav_metadata_type_inst; + pMetadata->data.inst.midiUnityNote = (drwav_int8)instData[0]; + pMetadata->data.inst.fineTuneCents = (drwav_int8)instData[1]; + pMetadata->data.inst.gainDecibels = (drwav_int8)instData[2]; + pMetadata->data.inst.lowNote = (drwav_int8)instData[3]; + pMetadata->data.inst.highNote = (drwav_int8)instData[4]; + pMetadata->data.inst.lowVelocity = (drwav_int8)instData[5]; + pMetadata->data.inst.highVelocity = (drwav_int8)instData[6]; + } + + return bytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__read_acid_to_metadata_obj(drwav__metadata_parser* pParser, drwav_metadata* pMetadata) +{ + drwav_uint8 acidData[DRWAV_ACID_BYTES]; + drwav_uint64 bytesRead; + + if (pMetadata == NULL) { + return 0; + } + + bytesRead = drwav__metadata_parser_read(pParser, acidData, sizeof(acidData), NULL); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + + if (bytesRead == sizeof(acidData)) { + pMetadata->type = drwav_metadata_type_acid; + pMetadata->data.acid.flags = drwav_bytes_to_u32(acidData + 0); + pMetadata->data.acid.midiUnityNote = drwav_bytes_to_u16(acidData + 4); + pMetadata->data.acid.reserved1 = drwav_bytes_to_u16(acidData + 6); + pMetadata->data.acid.reserved2 = drwav_bytes_to_f32(acidData + 8); + pMetadata->data.acid.numBeats = drwav_bytes_to_u32(acidData + 12); + pMetadata->data.acid.meterDenominator = drwav_bytes_to_u16(acidData + 16); + pMetadata->data.acid.meterNumerator = drwav_bytes_to_u16(acidData + 18); + pMetadata->data.acid.tempo = drwav_bytes_to_f32(acidData + 20); + } + + return bytesRead; +} + +DRWAV_PRIVATE size_t drwav__strlen(const char* str) +{ + size_t result = 0; + + while (*str++) { + result += 1; + } + + return result; +} + +DRWAV_PRIVATE size_t drwav__strlen_clamped(const char* str, size_t maxToRead) +{ + size_t result = 0; + + while (*str++ && result < maxToRead) { + result += 1; + } + + return result; +} + +DRWAV_PRIVATE char* drwav__metadata_copy_string(drwav__metadata_parser* pParser, const char* str, size_t maxToRead) +{ + size_t len = drwav__strlen_clamped(str, maxToRead); + + if (len) { + char* result = (char*)drwav__metadata_get_memory(pParser, len + 1, 1); + DRWAV_ASSERT(result != NULL); + + DRWAV_COPY_MEMORY(result, str, len); + result[len] = '\0'; + + return result; + } else { + return NULL; + } +} + +typedef struct +{ + const void* pBuffer; + size_t sizeInBytes; + size_t cursor; +} drwav_buffer_reader; + +DRWAV_PRIVATE drwav_result drwav_buffer_reader_init(const void* pBuffer, size_t sizeInBytes, drwav_buffer_reader* pReader) +{ + DRWAV_ASSERT(pBuffer != NULL); + DRWAV_ASSERT(pReader != NULL); + + DRWAV_ZERO_OBJECT(pReader); + + pReader->pBuffer = pBuffer; + pReader->sizeInBytes = sizeInBytes; + pReader->cursor = 0; + + return DRWAV_SUCCESS; +} + +DRWAV_PRIVATE const void* drwav_buffer_reader_ptr(const drwav_buffer_reader* pReader) +{ + DRWAV_ASSERT(pReader != NULL); + + return drwav_offset_ptr(pReader->pBuffer, pReader->cursor); +} + +DRWAV_PRIVATE drwav_result drwav_buffer_reader_seek(drwav_buffer_reader* pReader, size_t bytesToSeek) +{ + DRWAV_ASSERT(pReader != NULL); + + if (pReader->cursor + bytesToSeek > pReader->sizeInBytes) { + return DRWAV_BAD_SEEK; /* Seeking too far forward. */ + } + + pReader->cursor += bytesToSeek; + + return DRWAV_SUCCESS; +} + +DRWAV_PRIVATE drwav_result drwav_buffer_reader_read(drwav_buffer_reader* pReader, void* pDst, size_t bytesToRead, size_t* pBytesRead) +{ + drwav_result result = DRWAV_SUCCESS; + size_t bytesRemaining; + + DRWAV_ASSERT(pReader != NULL); + + if (pBytesRead != NULL) { + *pBytesRead = 0; + } + + bytesRemaining = (pReader->sizeInBytes - pReader->cursor); + if (bytesToRead > bytesRemaining) { + bytesToRead = bytesRemaining; + } + + if (pDst == NULL) { + /* Seek. */ + result = drwav_buffer_reader_seek(pReader, bytesToRead); + } else { + /* Read. */ + DRWAV_COPY_MEMORY(pDst, drwav_buffer_reader_ptr(pReader), bytesToRead); + pReader->cursor += bytesToRead; + } + + DRWAV_ASSERT(pReader->cursor <= pReader->sizeInBytes); + + if (result == DRWAV_SUCCESS) { + if (pBytesRead != NULL) { + *pBytesRead = bytesToRead; + } + } + + return DRWAV_SUCCESS; +} + +DRWAV_PRIVATE drwav_result drwav_buffer_reader_read_u16(drwav_buffer_reader* pReader, drwav_uint16* pDst) +{ + drwav_result result; + size_t bytesRead; + drwav_uint8 data[2]; + + DRWAV_ASSERT(pReader != NULL); + DRWAV_ASSERT(pDst != NULL); + + *pDst = 0; /* Safety. */ + + result = drwav_buffer_reader_read(pReader, data, sizeof(*pDst), &bytesRead); + if (result != DRWAV_SUCCESS || bytesRead != sizeof(*pDst)) { + return result; + } + + *pDst = drwav_bytes_to_u16(data); + + return DRWAV_SUCCESS; +} + +DRWAV_PRIVATE drwav_result drwav_buffer_reader_read_u32(drwav_buffer_reader* pReader, drwav_uint32* pDst) +{ + drwav_result result; + size_t bytesRead; + drwav_uint8 data[4]; + + DRWAV_ASSERT(pReader != NULL); + DRWAV_ASSERT(pDst != NULL); + + *pDst = 0; /* Safety. */ + + result = drwav_buffer_reader_read(pReader, data, sizeof(*pDst), &bytesRead); + if (result != DRWAV_SUCCESS || bytesRead != sizeof(*pDst)) { + return result; + } + + *pDst = drwav_bytes_to_u32(data); + + return DRWAV_SUCCESS; +} + + + +DRWAV_PRIVATE drwav_uint64 drwav__read_bext_to_metadata_obj(drwav__metadata_parser* pParser, drwav_metadata* pMetadata, drwav_uint64 chunkSize) +{ + drwav_uint8 bextData[DRWAV_BEXT_BYTES]; + size_t bytesRead = drwav__metadata_parser_read(pParser, bextData, sizeof(bextData), NULL); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + + if (bytesRead == sizeof(bextData)) { + drwav_buffer_reader reader; + drwav_uint32 timeReferenceLow; + drwav_uint32 timeReferenceHigh; + size_t extraBytes; + + pMetadata->type = drwav_metadata_type_bext; + + if (drwav_buffer_reader_init(bextData, bytesRead, &reader) == DRWAV_SUCCESS) { + pMetadata->data.bext.pDescription = drwav__metadata_copy_string(pParser, (const char*)drwav_buffer_reader_ptr(&reader), DRWAV_BEXT_DESCRIPTION_BYTES); + drwav_buffer_reader_seek(&reader, DRWAV_BEXT_DESCRIPTION_BYTES); + + pMetadata->data.bext.pOriginatorName = drwav__metadata_copy_string(pParser, (const char*)drwav_buffer_reader_ptr(&reader), DRWAV_BEXT_ORIGINATOR_NAME_BYTES); + drwav_buffer_reader_seek(&reader, DRWAV_BEXT_ORIGINATOR_NAME_BYTES); + + pMetadata->data.bext.pOriginatorReference = drwav__metadata_copy_string(pParser, (const char*)drwav_buffer_reader_ptr(&reader), DRWAV_BEXT_ORIGINATOR_REF_BYTES); + drwav_buffer_reader_seek(&reader, DRWAV_BEXT_ORIGINATOR_REF_BYTES); + + drwav_buffer_reader_read(&reader, pMetadata->data.bext.pOriginationDate, sizeof(pMetadata->data.bext.pOriginationDate), NULL); + drwav_buffer_reader_read(&reader, pMetadata->data.bext.pOriginationTime, sizeof(pMetadata->data.bext.pOriginationTime), NULL); + + drwav_buffer_reader_read_u32(&reader, &timeReferenceLow); + drwav_buffer_reader_read_u32(&reader, &timeReferenceHigh); + pMetadata->data.bext.timeReference = ((drwav_uint64)timeReferenceHigh << 32) + timeReferenceLow; + + drwav_buffer_reader_read_u16(&reader, &pMetadata->data.bext.version); + + pMetadata->data.bext.pUMID = drwav__metadata_get_memory(pParser, DRWAV_BEXT_UMID_BYTES, 1); + drwav_buffer_reader_read(&reader, pMetadata->data.bext.pUMID, DRWAV_BEXT_UMID_BYTES, NULL); + + drwav_buffer_reader_read_u16(&reader, &pMetadata->data.bext.loudnessValue); + drwav_buffer_reader_read_u16(&reader, &pMetadata->data.bext.loudnessRange); + drwav_buffer_reader_read_u16(&reader, &pMetadata->data.bext.maxTruePeakLevel); + drwav_buffer_reader_read_u16(&reader, &pMetadata->data.bext.maxMomentaryLoudness); + drwav_buffer_reader_read_u16(&reader, &pMetadata->data.bext.maxShortTermLoudness); + + DRWAV_ASSERT((drwav_offset_ptr(drwav_buffer_reader_ptr(&reader), DRWAV_BEXT_RESERVED_BYTES)) == (bextData + DRWAV_BEXT_BYTES)); + + extraBytes = (size_t)(chunkSize - DRWAV_BEXT_BYTES); + if (extraBytes > 0) { + pMetadata->data.bext.pCodingHistory = (char*)drwav__metadata_get_memory(pParser, extraBytes + 1, 1); + DRWAV_ASSERT(pMetadata->data.bext.pCodingHistory != NULL); + + pMetadata->data.bext.codingHistorySize = (drwav_uint32)drwav__metadata_parser_read(pParser, pMetadata->data.bext.pCodingHistory, extraBytes, NULL); + pMetadata->data.bext.pCodingHistory[pMetadata->data.bext.codingHistorySize] = '\0'; /* <-- Explicit null terminator in case of a badly formed file. */ + + bytesRead += pMetadata->data.bext.codingHistorySize; + } else { + pMetadata->data.bext.pCodingHistory = NULL; + pMetadata->data.bext.codingHistorySize = 0; + } + } + } + + return bytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__read_list_label_or_note_to_metadata_obj(drwav__metadata_parser* pParser, drwav_metadata* pMetadata, drwav_uint64 chunkSize, drwav_metadata_type type) +{ + drwav_uint8 cueIDBuffer[DRWAV_LIST_LABEL_OR_NOTE_BYTES]; + drwav_uint64 totalBytesRead = 0; + size_t bytesJustRead = drwav__metadata_parser_read(pParser, cueIDBuffer, sizeof(cueIDBuffer), &totalBytesRead); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + + if (bytesJustRead == sizeof(cueIDBuffer)) { + drwav_uint32 sizeIncludingNullTerminator; + + pMetadata->type = type; + pMetadata->data.labelOrNote.cuePointId = drwav_bytes_to_u32(cueIDBuffer); + + sizeIncludingNullTerminator = (drwav_uint32)chunkSize - DRWAV_LIST_LABEL_OR_NOTE_BYTES; + if (sizeIncludingNullTerminator > 0) { + pMetadata->data.labelOrNote.stringLength = sizeIncludingNullTerminator - 1; + pMetadata->data.labelOrNote.pString = (char*)drwav__metadata_get_memory(pParser, sizeIncludingNullTerminator, 1); + DRWAV_ASSERT(pMetadata->data.labelOrNote.pString != NULL); + + drwav__metadata_parser_read(pParser, pMetadata->data.labelOrNote.pString, sizeIncludingNullTerminator, &totalBytesRead); + } else { + pMetadata->data.labelOrNote.stringLength = 0; + pMetadata->data.labelOrNote.pString = NULL; + } + } + + return totalBytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__read_list_labelled_cue_region_to_metadata_obj(drwav__metadata_parser* pParser, drwav_metadata* pMetadata, drwav_uint64 chunkSize) +{ + drwav_uint8 buffer[DRWAV_LIST_LABELLED_TEXT_BYTES]; + drwav_uint64 totalBytesRead = 0; + size_t bytesJustRead = drwav__metadata_parser_read(pParser, buffer, sizeof(buffer), &totalBytesRead); + + DRWAV_ASSERT(pParser->stage == drwav__metadata_parser_stage_read); + + if (bytesJustRead == sizeof(buffer)) { + drwav_uint32 sizeIncludingNullTerminator; + + pMetadata->type = drwav_metadata_type_list_labelled_cue_region; + pMetadata->data.labelledCueRegion.cuePointId = drwav_bytes_to_u32(buffer + 0); + pMetadata->data.labelledCueRegion.sampleLength = drwav_bytes_to_u32(buffer + 4); + pMetadata->data.labelledCueRegion.purposeId[0] = buffer[8]; + pMetadata->data.labelledCueRegion.purposeId[1] = buffer[9]; + pMetadata->data.labelledCueRegion.purposeId[2] = buffer[10]; + pMetadata->data.labelledCueRegion.purposeId[3] = buffer[11]; + pMetadata->data.labelledCueRegion.country = drwav_bytes_to_u16(buffer + 12); + pMetadata->data.labelledCueRegion.language = drwav_bytes_to_u16(buffer + 14); + pMetadata->data.labelledCueRegion.dialect = drwav_bytes_to_u16(buffer + 16); + pMetadata->data.labelledCueRegion.codePage = drwav_bytes_to_u16(buffer + 18); + + sizeIncludingNullTerminator = (drwav_uint32)chunkSize - DRWAV_LIST_LABELLED_TEXT_BYTES; + if (sizeIncludingNullTerminator > 0) { + pMetadata->data.labelledCueRegion.stringLength = sizeIncludingNullTerminator - 1; + pMetadata->data.labelledCueRegion.pString = (char*)drwav__metadata_get_memory(pParser, sizeIncludingNullTerminator, 1); + DRWAV_ASSERT(pMetadata->data.labelledCueRegion.pString != NULL); + + drwav__metadata_parser_read(pParser, pMetadata->data.labelledCueRegion.pString, sizeIncludingNullTerminator, &totalBytesRead); + } else { + pMetadata->data.labelledCueRegion.stringLength = 0; + pMetadata->data.labelledCueRegion.pString = NULL; + } + } + + return totalBytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_info_text_chunk(drwav__metadata_parser* pParser, drwav_uint64 chunkSize, drwav_metadata_type type) +{ + drwav_uint64 bytesRead = 0; + drwav_uint32 stringSizeWithNullTerminator = (drwav_uint32)chunkSize; + + if (pParser->stage == drwav__metadata_parser_stage_count) { + pParser->metadataCount += 1; + drwav__metadata_request_extra_memory_for_stage_2(pParser, stringSizeWithNullTerminator, 1); + } else { + drwav_metadata* pMetadata = &pParser->pMetadata[pParser->metadataCursor]; + pMetadata->type = type; + if (stringSizeWithNullTerminator > 0) { + pMetadata->data.infoText.stringLength = stringSizeWithNullTerminator - 1; + pMetadata->data.infoText.pString = (char*)drwav__metadata_get_memory(pParser, stringSizeWithNullTerminator, 1); + DRWAV_ASSERT(pMetadata->data.infoText.pString != NULL); + + bytesRead = drwav__metadata_parser_read(pParser, pMetadata->data.infoText.pString, (size_t)stringSizeWithNullTerminator, NULL); + if (bytesRead == chunkSize) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } else { + pMetadata->data.infoText.stringLength = 0; + pMetadata->data.infoText.pString = NULL; + pParser->metadataCursor += 1; + } + } + + return bytesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_unknown_chunk(drwav__metadata_parser* pParser, const drwav_uint8* pChunkId, drwav_uint64 chunkSize, drwav_metadata_location location) +{ + drwav_uint64 bytesRead = 0; + + if (location == drwav_metadata_location_invalid) { + return 0; + } + + if (drwav_fourcc_equal(pChunkId, "data") || drwav_fourcc_equal(pChunkId, "fmt ") || drwav_fourcc_equal(pChunkId, "fact")) { + return 0; + } + + if (pParser->stage == drwav__metadata_parser_stage_count) { + pParser->metadataCount += 1; + drwav__metadata_request_extra_memory_for_stage_2(pParser, (size_t)chunkSize, 1); + } else { + drwav_metadata* pMetadata = &pParser->pMetadata[pParser->metadataCursor]; + pMetadata->type = drwav_metadata_type_unknown; + pMetadata->data.unknown.chunkLocation = location; + pMetadata->data.unknown.id[0] = pChunkId[0]; + pMetadata->data.unknown.id[1] = pChunkId[1]; + pMetadata->data.unknown.id[2] = pChunkId[2]; + pMetadata->data.unknown.id[3] = pChunkId[3]; + pMetadata->data.unknown.dataSizeInBytes = (drwav_uint32)chunkSize; + pMetadata->data.unknown.pData = (drwav_uint8 *)drwav__metadata_get_memory(pParser, (size_t)chunkSize, 1); + DRWAV_ASSERT(pMetadata->data.unknown.pData != NULL); + + bytesRead = drwav__metadata_parser_read(pParser, pMetadata->data.unknown.pData, pMetadata->data.unknown.dataSizeInBytes, NULL); + if (bytesRead == pMetadata->data.unknown.dataSizeInBytes) { + pParser->metadataCursor += 1; + } else { + /* Failed to read. */ + } + } + + return bytesRead; +} + +DRWAV_PRIVATE drwav_bool32 drwav__chunk_matches(drwav_metadata_type allowedMetadataTypes, const drwav_uint8* pChunkID, drwav_metadata_type type, const char* pID) +{ + return (allowedMetadataTypes & type) && drwav_fourcc_equal(pChunkID, pID); +} + +DRWAV_PRIVATE drwav_uint64 drwav__metadata_process_chunk(drwav__metadata_parser* pParser, const drwav_chunk_header* pChunkHeader, drwav_metadata_type allowedMetadataTypes) +{ + const drwav_uint8 *pChunkID = pChunkHeader->id.fourcc; + drwav_uint64 bytesRead = 0; + + if (drwav__chunk_matches(allowedMetadataTypes, pChunkID, drwav_metadata_type_smpl, "smpl")) { + if (pChunkHeader->sizeInBytes >= DRWAV_SMPL_BYTES) { + if (pParser->stage == drwav__metadata_parser_stage_count) { + drwav_uint8 buffer[4]; + size_t bytesJustRead; + + if (!pParser->onSeek(pParser->pReadSeekUserData, 28, DRWAV_SEEK_CUR)) { + return bytesRead; + } + bytesRead += 28; + + bytesJustRead = drwav__metadata_parser_read(pParser, buffer, sizeof(buffer), &bytesRead); + if (bytesJustRead == sizeof(buffer)) { + drwav_uint32 loopCount = drwav_bytes_to_u32(buffer); + + bytesJustRead = drwav__metadata_parser_read(pParser, buffer, sizeof(buffer), &bytesRead); + if (bytesJustRead == sizeof(buffer)) { + drwav_uint32 samplerSpecificDataSizeInBytes = drwav_bytes_to_u32(buffer); + + if (drwav__metadata_validate_smpl_chunk(pChunkHeader, loopCount, samplerSpecificDataSizeInBytes, NULL)) { + pParser->metadataCount += 1; + drwav__metadata_request_extra_memory_for_stage_2(pParser, sizeof(drwav_smpl_loop) * loopCount, DRWAV_METADATA_ALIGNMENT); + drwav__metadata_request_extra_memory_for_stage_2(pParser, samplerSpecificDataSizeInBytes, 1); + } else { + /* Incorrectly formed chunk. Loop or sampler-specific data exceeds the size of the chunk. */ + } + } + } + } else { + bytesRead = drwav__read_smpl_to_metadata_obj(pParser, pChunkHeader, &pParser->pMetadata[pParser->metadataCursor]); + if (bytesRead == pChunkHeader->sizeInBytes) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav__chunk_matches(allowedMetadataTypes, pChunkID, drwav_metadata_type_inst, "inst")) { + if (pChunkHeader->sizeInBytes == DRWAV_INST_BYTES) { + if (pParser->stage == drwav__metadata_parser_stage_count) { + pParser->metadataCount += 1; + } else { + bytesRead = drwav__read_inst_to_metadata_obj(pParser, &pParser->pMetadata[pParser->metadataCursor]); + if (bytesRead == pChunkHeader->sizeInBytes) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav__chunk_matches(allowedMetadataTypes, pChunkID, drwav_metadata_type_acid, "acid")) { + if (pChunkHeader->sizeInBytes == DRWAV_ACID_BYTES) { + if (pParser->stage == drwav__metadata_parser_stage_count) { + pParser->metadataCount += 1; + } else { + bytesRead = drwav__read_acid_to_metadata_obj(pParser, &pParser->pMetadata[pParser->metadataCursor]); + if (bytesRead == pChunkHeader->sizeInBytes) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav__chunk_matches(allowedMetadataTypes, pChunkID, drwav_metadata_type_cue, "cue ")) { + if (pChunkHeader->sizeInBytes >= DRWAV_CUE_BYTES) { + if (pParser->stage == drwav__metadata_parser_stage_count) { + size_t cueCount; + + pParser->metadataCount += 1; + cueCount = (size_t)(pChunkHeader->sizeInBytes - DRWAV_CUE_BYTES) / DRWAV_CUE_POINT_BYTES; + drwav__metadata_request_extra_memory_for_stage_2(pParser, sizeof(drwav_cue_point) * cueCount, DRWAV_METADATA_ALIGNMENT); + } else { + bytesRead = drwav__read_cue_to_metadata_obj(pParser, pChunkHeader, &pParser->pMetadata[pParser->metadataCursor]); + if (bytesRead == pChunkHeader->sizeInBytes) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav__chunk_matches(allowedMetadataTypes, pChunkID, drwav_metadata_type_bext, "bext")) { + if (pChunkHeader->sizeInBytes >= DRWAV_BEXT_BYTES) { + if (pParser->stage == drwav__metadata_parser_stage_count) { + /* The description field is the largest one in a bext chunk, so that is the max size of this temporary buffer. */ + char buffer[DRWAV_BEXT_DESCRIPTION_BYTES + 1]; + size_t allocSizeNeeded = DRWAV_BEXT_UMID_BYTES; /* We know we will need SMPTE umid size. */ + size_t bytesJustRead; + + buffer[DRWAV_BEXT_DESCRIPTION_BYTES] = '\0'; + bytesJustRead = drwav__metadata_parser_read(pParser, buffer, DRWAV_BEXT_DESCRIPTION_BYTES, &bytesRead); + if (bytesJustRead != DRWAV_BEXT_DESCRIPTION_BYTES) { + return bytesRead; + } + allocSizeNeeded += drwav__strlen(buffer) + 1; + + buffer[DRWAV_BEXT_ORIGINATOR_NAME_BYTES] = '\0'; + bytesJustRead = drwav__metadata_parser_read(pParser, buffer, DRWAV_BEXT_ORIGINATOR_NAME_BYTES, &bytesRead); + if (bytesJustRead != DRWAV_BEXT_ORIGINATOR_NAME_BYTES) { + return bytesRead; + } + allocSizeNeeded += drwav__strlen(buffer) + 1; + + buffer[DRWAV_BEXT_ORIGINATOR_REF_BYTES] = '\0'; + bytesJustRead = drwav__metadata_parser_read(pParser, buffer, DRWAV_BEXT_ORIGINATOR_REF_BYTES, &bytesRead); + if (bytesJustRead != DRWAV_BEXT_ORIGINATOR_REF_BYTES) { + return bytesRead; + } + allocSizeNeeded += drwav__strlen(buffer) + 1; + + /* Coding history. */ + allocSizeNeeded += (size_t)pChunkHeader->sizeInBytes - DRWAV_BEXT_BYTES + 1; + + drwav__metadata_request_extra_memory_for_stage_2(pParser, allocSizeNeeded, 1); + + pParser->metadataCount += 1; + } else { + bytesRead = drwav__read_bext_to_metadata_obj(pParser, &pParser->pMetadata[pParser->metadataCursor], pChunkHeader->sizeInBytes); + if (bytesRead == pChunkHeader->sizeInBytes) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav_fourcc_equal(pChunkID, "LIST") || drwav_fourcc_equal(pChunkID, "list")) { + drwav_metadata_location listType = drwav_metadata_location_invalid; + while (bytesRead < pChunkHeader->sizeInBytes) { + drwav_uint8 subchunkId[4]; + drwav_uint8 subchunkSizeBuffer[4]; + drwav_uint64 subchunkDataSize; + drwav_uint64 subchunkBytesRead = 0; + drwav_uint64 bytesJustRead = drwav__metadata_parser_read(pParser, subchunkId, sizeof(subchunkId), &bytesRead); + if (bytesJustRead != sizeof(subchunkId)) { + break; + } + + /* + The first thing in a list chunk should be "adtl" or "INFO". + + - adtl means this list is a Associated Data List Chunk and will contain labels, notes + or labelled cue regions. + - INFO means this list is an Info List Chunk containing info text chunks such as IPRD + which would specifies the album of this wav file. + + No data follows the adtl or INFO id so we just make note of what type this list is and + continue. + */ + if (drwav_fourcc_equal(subchunkId, "adtl")) { + listType = drwav_metadata_location_inside_adtl_list; + continue; + } else if (drwav_fourcc_equal(subchunkId, "INFO")) { + listType = drwav_metadata_location_inside_info_list; + continue; + } + + bytesJustRead = drwav__metadata_parser_read(pParser, subchunkSizeBuffer, sizeof(subchunkSizeBuffer), &bytesRead); + if (bytesJustRead != sizeof(subchunkSizeBuffer)) { + break; + } + subchunkDataSize = drwav_bytes_to_u32(subchunkSizeBuffer); + + if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_label, "labl") || drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_note, "note")) { + if (subchunkDataSize >= DRWAV_LIST_LABEL_OR_NOTE_BYTES) { + drwav_uint64 stringSizeWithNullTerm = subchunkDataSize - DRWAV_LIST_LABEL_OR_NOTE_BYTES; + if (pParser->stage == drwav__metadata_parser_stage_count) { + pParser->metadataCount += 1; + drwav__metadata_request_extra_memory_for_stage_2(pParser, (size_t)stringSizeWithNullTerm, 1); + } else { + subchunkBytesRead = drwav__read_list_label_or_note_to_metadata_obj(pParser, &pParser->pMetadata[pParser->metadataCursor], subchunkDataSize, drwav_fourcc_equal(subchunkId, "labl") ? drwav_metadata_type_list_label : drwav_metadata_type_list_note); + if (subchunkBytesRead == subchunkDataSize) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_labelled_cue_region, "ltxt")) { + if (subchunkDataSize >= DRWAV_LIST_LABELLED_TEXT_BYTES) { + drwav_uint64 stringSizeWithNullTerminator = subchunkDataSize - DRWAV_LIST_LABELLED_TEXT_BYTES; + if (pParser->stage == drwav__metadata_parser_stage_count) { + pParser->metadataCount += 1; + drwav__metadata_request_extra_memory_for_stage_2(pParser, (size_t)stringSizeWithNullTerminator, 1); + } else { + subchunkBytesRead = drwav__read_list_labelled_cue_region_to_metadata_obj(pParser, &pParser->pMetadata[pParser->metadataCursor], subchunkDataSize); + if (subchunkBytesRead == subchunkDataSize) { + pParser->metadataCursor += 1; + } else { + /* Failed to parse. */ + } + } + } else { + /* Incorrectly formed chunk. */ + } + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_software, "ISFT")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_software); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_copyright, "ICOP")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_copyright); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_title, "INAM")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_title); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_artist, "IART")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_artist); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_comment, "ICMT")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_comment); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_date, "ICRD")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_date); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_genre, "IGNR")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_genre); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_album, "IPRD")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_album); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_tracknumber, "ITRK")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_tracknumber); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_location, "IARL")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_location); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_organization, "ICMS")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_organization); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_keywords, "IKEY")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_keywords); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_medium, "IMED")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_medium); + } else if (drwav__chunk_matches(allowedMetadataTypes, subchunkId, drwav_metadata_type_list_info_description, "ISBJ")) { + subchunkBytesRead = drwav__metadata_process_info_text_chunk(pParser, subchunkDataSize, drwav_metadata_type_list_info_description); + } else if ((allowedMetadataTypes & drwav_metadata_type_unknown) != 0) { + subchunkBytesRead = drwav__metadata_process_unknown_chunk(pParser, subchunkId, subchunkDataSize, listType); + } + + bytesRead += subchunkBytesRead; + DRWAV_ASSERT(subchunkBytesRead <= subchunkDataSize); + + if (subchunkBytesRead < subchunkDataSize) { + drwav_uint64 bytesToSeek = subchunkDataSize - subchunkBytesRead; + + if (!pParser->onSeek(pParser->pReadSeekUserData, (int)bytesToSeek, DRWAV_SEEK_CUR)) { + break; + } + bytesRead += bytesToSeek; + } + + if ((subchunkDataSize % 2) == 1) { + if (!pParser->onSeek(pParser->pReadSeekUserData, 1, DRWAV_SEEK_CUR)) { + break; + } + bytesRead += 1; + } + } + } else if ((allowedMetadataTypes & drwav_metadata_type_unknown) != 0) { + bytesRead = drwav__metadata_process_unknown_chunk(pParser, pChunkID, pChunkHeader->sizeInBytes, drwav_metadata_location_top_level); + } + + return bytesRead; +} + + +DRWAV_PRIVATE drwav_uint32 drwav_get_bytes_per_pcm_frame(drwav* pWav) +{ + drwav_uint32 bytesPerFrame; + + /* + The bytes per frame is a bit ambiguous. It can be either be based on the bits per sample, or the block align. The way I'm doing it here + is that if the bits per sample is a multiple of 8, use floor(bitsPerSample*channels/8), otherwise fall back to the block align. + */ + if ((pWav->bitsPerSample & 0x7) == 0) { + /* Bits per sample is a multiple of 8. */ + bytesPerFrame = (pWav->bitsPerSample * pWav->fmt.channels) >> 3; + } else { + bytesPerFrame = pWav->fmt.blockAlign; + } + + /* Validation for known formats. a-law and mu-law should be 1 byte per channel. If it's not, it's not decodable. */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ALAW || pWav->translatedFormatTag == DR_WAVE_FORMAT_MULAW) { + if (bytesPerFrame != pWav->fmt.channels) { + return 0; /* Invalid file. */ + } + } + + return bytesPerFrame; +} + +DRWAV_API drwav_uint16 drwav_fmt_get_format(const drwav_fmt* pFMT) +{ + if (pFMT == NULL) { + return 0; + } + + if (pFMT->formatTag != DR_WAVE_FORMAT_EXTENSIBLE) { + return pFMT->formatTag; + } else { + return drwav_bytes_to_u16(pFMT->subFormat); /* Only the first two bytes are required. */ + } +} + +DRWAV_PRIVATE drwav_bool32 drwav_preinit(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pReadSeekTellUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pWav == NULL || onRead == NULL || onSeek == NULL) { /* <-- onTell is optional. */ + return DRWAV_FALSE; + } + + DRWAV_ZERO_MEMORY(pWav, sizeof(*pWav)); + pWav->onRead = onRead; + pWav->onSeek = onSeek; + pWav->onTell = onTell; + pWav->pUserData = pReadSeekTellUserData; + pWav->allocationCallbacks = drwav_copy_allocation_callbacks_or_defaults(pAllocationCallbacks); + + if (pWav->allocationCallbacks.onFree == NULL || (pWav->allocationCallbacks.onMalloc == NULL && pWav->allocationCallbacks.onRealloc == NULL)) { + return DRWAV_FALSE; /* Invalid allocation callbacks. */ + } + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE drwav_bool32 drwav_init__internal(drwav* pWav, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags) +{ + /* This function assumes drwav_preinit() has been called beforehand. */ + drwav_result result; + drwav_uint64 cursor; /* <-- Keeps track of the byte position so we can seek to specific locations. */ + drwav_bool32 sequential; + drwav_uint8 riff[4]; + drwav_fmt fmt; + unsigned short translatedFormatTag; + drwav_uint64 dataChunkSize = 0; /* <-- Important! Don't explicitly set this to 0 anywhere else. Calculation of the size of the data chunk is performed in different paths depending on the container. */ + drwav_uint64 sampleCountFromFactChunk = 0; /* Same as dataChunkSize - make sure this is the only place this is initialized to 0. */ + drwav_uint64 metadataStartPos; + drwav__metadata_parser metadataParser; + drwav_bool8 isProcessingMetadata = DRWAV_FALSE; + drwav_bool8 foundChunk_fmt = DRWAV_FALSE; + drwav_bool8 foundChunk_data = DRWAV_FALSE; + drwav_bool8 isAIFCFormType = DRWAV_FALSE; /* Only used with AIFF. */ + drwav_uint64 aiffFrameCount = 0; + drwav_int64 fileSize; + drwav_bool32 hasKnownFileSize = DRWAV_FALSE; + + cursor = 0; + sequential = (flags & DRWAV_SEQUENTIAL) != 0; + + /* + Whether or not we are processing metadata controls how we load. We can load more efficiently when + metadata is not being processed. Seqential mode cannot support metadata because it involves seeking + backwards. + */ + isProcessingMetadata = !sequential && ((flags & DRWAV_WITH_METADATA) != 0); + + /* + When processing metadata we'll be doing some memory allocations here against untrusted data. We'll do + a basic validation check that they don't exceed the size of the file. + */ + if (isProcessingMetadata && pWav->onTell != NULL && pWav->onSeek != NULL) { + if (pWav->onSeek(pWav->pUserData, 0, DRWAV_SEEK_END)) { + if (pWav->onTell(pWav->pUserData, &fileSize)) { + hasKnownFileSize = DRWAV_TRUE; + } + + pWav->onSeek(pWav->pUserData, 0, DRWAV_SEEK_SET); + } + } + + DRWAV_ZERO_OBJECT(&fmt); + + /* The first 4 bytes should be the RIFF identifier. */ + if (drwav__on_read(pWav->onRead, pWav->pUserData, riff, sizeof(riff), &cursor) != sizeof(riff)) { + return DRWAV_FALSE; + } + + /* + The first 4 bytes can be used to identify the container. For RIFF files it will start with "RIFF" and for + w64 it will start with "riff". + */ + if (drwav_fourcc_equal(riff, "RIFF")) { + pWav->container = drwav_container_riff; + } else if (drwav_fourcc_equal(riff, "RIFX")) { + pWav->container = drwav_container_rifx; + } else if (drwav_fourcc_equal(riff, "riff")) { + int i; + drwav_uint8 riff2[12]; + + pWav->container = drwav_container_w64; + + /* Check the rest of the GUID for validity. */ + if (drwav__on_read(pWav->onRead, pWav->pUserData, riff2, sizeof(riff2), &cursor) != sizeof(riff2)) { + return DRWAV_FALSE; + } + + for (i = 0; i < 12; ++i) { + if (riff2[i] != drwavGUID_W64_RIFF[i+4]) { + return DRWAV_FALSE; + } + } + } else if (drwav_fourcc_equal(riff, "RF64")) { + pWav->container = drwav_container_rf64; + } else if (drwav_fourcc_equal(riff, "FORM")) { + pWav->container = drwav_container_aiff; + } else { + return DRWAV_FALSE; /* Unknown or unsupported container. */ + } + + + if (pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx || pWav->container == drwav_container_rf64) { + drwav_uint8 chunkSizeBytes[4]; + drwav_uint8 wave[4]; + + if (drwav__on_read(pWav->onRead, pWav->pUserData, chunkSizeBytes, sizeof(chunkSizeBytes), &cursor) != sizeof(chunkSizeBytes)) { + return DRWAV_FALSE; + } + + if (pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx) { + if (drwav_bytes_to_u32_ex(chunkSizeBytes, pWav->container) < 36) { + /* + I've had a report of a WAV file failing to load when the size of the WAVE chunk is not encoded + and is instead just set to 0. I'm going to relax the validation here to allow these files to + load. Considering the chunk size isn't actually used this should be safe. With this change my + test suite still passes. + */ + /*return DRWAV_FALSE;*/ /* Chunk size should always be at least 36 bytes. */ + } + } else if (pWav->container == drwav_container_rf64) { + if (drwav_bytes_to_u32_le(chunkSizeBytes) != 0xFFFFFFFF) { + return DRWAV_FALSE; /* Chunk size should always be set to -1/0xFFFFFFFF for RF64. The actual size is retrieved later. */ + } + } else { + return DRWAV_FALSE; /* Should never hit this. */ + } + + if (drwav__on_read(pWav->onRead, pWav->pUserData, wave, sizeof(wave), &cursor) != sizeof(wave)) { + return DRWAV_FALSE; + } + + if (!drwav_fourcc_equal(wave, "WAVE")) { + return DRWAV_FALSE; /* Expecting "WAVE". */ + } + } else if (pWav->container == drwav_container_w64) { + drwav_uint8 chunkSizeBytes[8]; + drwav_uint8 wave[16]; + + if (drwav__on_read(pWav->onRead, pWav->pUserData, chunkSizeBytes, sizeof(chunkSizeBytes), &cursor) != sizeof(chunkSizeBytes)) { + return DRWAV_FALSE; + } + + if (drwav_bytes_to_u64(chunkSizeBytes) < 80) { + return DRWAV_FALSE; + } + + if (drwav__on_read(pWav->onRead, pWav->pUserData, wave, sizeof(wave), &cursor) != sizeof(wave)) { + return DRWAV_FALSE; + } + + if (!drwav_guid_equal(wave, drwavGUID_W64_WAVE)) { + return DRWAV_FALSE; + } + } else if (pWav->container == drwav_container_aiff) { + drwav_uint8 chunkSizeBytes[4]; + drwav_uint8 aiff[4]; + + if (drwav__on_read(pWav->onRead, pWav->pUserData, chunkSizeBytes, sizeof(chunkSizeBytes), &cursor) != sizeof(chunkSizeBytes)) { + return DRWAV_FALSE; + } + + if (drwav_bytes_to_u32_be(chunkSizeBytes) < 18) { + return DRWAV_FALSE; + } + + if (drwav__on_read(pWav->onRead, pWav->pUserData, aiff, sizeof(aiff), &cursor) != sizeof(aiff)) { + return DRWAV_FALSE; + } + + if (drwav_fourcc_equal(aiff, "AIFF")) { + isAIFCFormType = DRWAV_FALSE; + } else if (drwav_fourcc_equal(aiff, "AIFC")) { + isAIFCFormType = DRWAV_TRUE; + } else { + return DRWAV_FALSE; /* Expecting "AIFF" or "AIFC". */ + } + } else { + return DRWAV_FALSE; + } + + + /* For RF64, the "ds64" chunk must come next, before the "fmt " chunk. */ + if (pWav->container == drwav_container_rf64) { + drwav_uint8 sizeBytes[8]; + drwav_uint64 bytesRemainingInChunk; + drwav_chunk_header header; + result = drwav__read_chunk_header(pWav->onRead, pWav->pUserData, pWav->container, &cursor, &header); + if (result != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + if (!drwav_fourcc_equal(header.id.fourcc, "ds64")) { + return DRWAV_FALSE; /* Expecting "ds64". */ + } + + bytesRemainingInChunk = header.sizeInBytes + header.paddingSize; + + /* We don't care about the size of the RIFF chunk - skip it. */ + if (!drwav__seek_forward(pWav->onSeek, 8, pWav->pUserData)) { + return DRWAV_FALSE; + } + bytesRemainingInChunk -= 8; + cursor += 8; + + + /* Next 8 bytes is the size of the "data" chunk. */ + if (drwav__on_read(pWav->onRead, pWav->pUserData, sizeBytes, sizeof(sizeBytes), &cursor) != sizeof(sizeBytes)) { + return DRWAV_FALSE; + } + bytesRemainingInChunk -= 8; + dataChunkSize = drwav_bytes_to_u64(sizeBytes); + + + /* Next 8 bytes is the same count which we would usually derived from the FACT chunk if it was available. */ + if (drwav__on_read(pWav->onRead, pWav->pUserData, sizeBytes, sizeof(sizeBytes), &cursor) != sizeof(sizeBytes)) { + return DRWAV_FALSE; + } + bytesRemainingInChunk -= 8; + sampleCountFromFactChunk = drwav_bytes_to_u64(sizeBytes); + + + /* Skip over everything else. */ + if (!drwav__seek_forward(pWav->onSeek, bytesRemainingInChunk, pWav->pUserData)) { + return DRWAV_FALSE; + } + cursor += bytesRemainingInChunk; + } + + + metadataStartPos = cursor; + + /* Don't allow processing of metadata with untested containers. */ + if (pWav->container != drwav_container_riff && pWav->container != drwav_container_rf64) { + isProcessingMetadata = DRWAV_FALSE; + } + + DRWAV_ZERO_MEMORY(&metadataParser, sizeof(metadataParser)); + if (isProcessingMetadata) { + metadataParser.onRead = pWav->onRead; + metadataParser.onSeek = pWav->onSeek; + metadataParser.pReadSeekUserData = pWav->pUserData; + metadataParser.stage = drwav__metadata_parser_stage_count; + } + + + /* + From here on out, chunks might be in any order. In order to robustly handle metadata we'll need + to loop through every chunk and handle them as we find them. In sequential mode we need to get + out of the loop as soon as we find the data chunk because we won't be able to seek back. + */ + for (;;) { /* For each chunk... */ + drwav_chunk_header header; + drwav_uint64 chunkSize; + + result = drwav__read_chunk_header(pWav->onRead, pWav->pUserData, pWav->container, &cursor, &header); + if (result != DRWAV_SUCCESS) { + break; + } + + chunkSize = header.sizeInBytes; + + + /* + Always tell the caller about this chunk. We cannot do this in sequential mode because the + callback is allowed to read from the file, in which case we'll need to rewind. + */ + if (!sequential && onChunk != NULL) { + drwav_uint64 callbackBytesRead = onChunk(pChunkUserData, pWav->onRead, pWav->onSeek, pWav->pUserData, &header, pWav->container, &fmt); + + /* + dr_wav may need to read the contents of the chunk, so we now need to seek back to the position before + we called the callback. + */ + if (callbackBytesRead > 0) { + if (drwav__seek_from_start(pWav->onSeek, cursor, pWav->pUserData) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + } + } + + + /* Explicitly handle known chunks first. */ + + /* "fmt " */ + if (((pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx || pWav->container == drwav_container_rf64) && drwav_fourcc_equal(header.id.fourcc, "fmt ")) || + ((pWav->container == drwav_container_w64) && drwav_guid_equal(header.id.guid, drwavGUID_W64_FMT))) { + drwav_uint8 fmtData[16]; + + if (header.sizeInBytes < sizeof(fmtData)) { + return DRWAV_FALSE; /* Invalid fmt chunk. */ + } + + foundChunk_fmt = DRWAV_TRUE; + + if (pWav->onRead(pWav->pUserData, fmtData, sizeof(fmtData)) != sizeof(fmtData)) { + return DRWAV_FALSE; + } + cursor += sizeof(fmtData); + + fmt.formatTag = drwav_bytes_to_u16_ex(fmtData + 0, pWav->container); + fmt.channels = drwav_bytes_to_u16_ex(fmtData + 2, pWav->container); + fmt.sampleRate = drwav_bytes_to_u32_ex(fmtData + 4, pWav->container); + fmt.avgBytesPerSec = drwav_bytes_to_u32_ex(fmtData + 8, pWav->container); + fmt.blockAlign = drwav_bytes_to_u16_ex(fmtData + 12, pWav->container); + fmt.bitsPerSample = drwav_bytes_to_u16_ex(fmtData + 14, pWav->container); + + fmt.extendedSize = 0; + fmt.validBitsPerSample = 0; + fmt.channelMask = 0; + DRWAV_ZERO_MEMORY(fmt.subFormat, sizeof(fmt.subFormat)); + + if (header.sizeInBytes > 16) { + drwav_uint8 fmt_cbSize[2]; + int bytesReadSoFar = 0; + drwav_uint64 leftoverBytes; + + if (pWav->onRead(pWav->pUserData, fmt_cbSize, sizeof(fmt_cbSize)) != sizeof(fmt_cbSize)) { + return DRWAV_FALSE; /* Expecting more data. */ + } + cursor += sizeof(fmt_cbSize); + + bytesReadSoFar = 18; + + fmt.extendedSize = drwav_bytes_to_u16_ex(fmt_cbSize, pWav->container); + if (fmt.extendedSize > 0) { + /* Simple validation. */ + if (fmt.formatTag == DR_WAVE_FORMAT_EXTENSIBLE) { + if (fmt.extendedSize != 22) { + return DRWAV_FALSE; + } + } + + if (fmt.formatTag == DR_WAVE_FORMAT_EXTENSIBLE) { + drwav_uint8 fmtext[22]; + + if (pWav->onRead(pWav->pUserData, fmtext, fmt.extendedSize) != fmt.extendedSize) { + return DRWAV_FALSE; /* Expecting more data. */ + } + + fmt.validBitsPerSample = drwav_bytes_to_u16_ex(fmtext + 0, pWav->container); + fmt.channelMask = drwav_bytes_to_u32_ex(fmtext + 2, pWav->container); + drwav_bytes_to_guid(fmtext + 6, fmt.subFormat); + } else { + if (pWav->onSeek(pWav->pUserData, fmt.extendedSize, DRWAV_SEEK_CUR) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + } + cursor += fmt.extendedSize; + + bytesReadSoFar += fmt.extendedSize; + } + + /* Seek past any leftover bytes. For w64 the leftover will be defined based on the chunk size. */ + leftoverBytes = header.sizeInBytes - bytesReadSoFar; + if (leftoverBytes > 0x7FFFFFFF) { + return DRWAV_FALSE; + } + + if (pWav->onSeek(pWav->pUserData, (int)leftoverBytes, DRWAV_SEEK_CUR) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + + cursor += leftoverBytes; + } + + if (header.paddingSize > 0) { + if (drwav__seek_forward(pWav->onSeek, header.paddingSize, pWav->pUserData) == DRWAV_FALSE) { + break; + } + cursor += header.paddingSize; + } + + /* Go to the next chunk. Don't include this chunk in metadata. */ + continue; + } + + /* "data" */ + if (((pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx || pWav->container == drwav_container_rf64) && drwav_fourcc_equal(header.id.fourcc, "data")) || + ((pWav->container == drwav_container_w64) && drwav_guid_equal(header.id.guid, drwavGUID_W64_DATA))) { + foundChunk_data = DRWAV_TRUE; + + pWav->dataChunkDataPos = cursor; + + if (pWav->container != drwav_container_rf64) { /* The data chunk size for RF64 will always be set to 0xFFFFFFFF here. It was set to it's true value earlier. */ + dataChunkSize = chunkSize; + } + + /* If we're running in sequential mode, or we're not reading metadata, we have enough now that we can get out of the loop. */ + if (sequential || !isProcessingMetadata) { + break; /* No need to keep reading beyond the data chunk. */ + } else { + chunkSize += header.paddingSize; /* <-- Make sure we seek past the padding. */ + if (drwav__seek_forward(pWav->onSeek, chunkSize, pWav->pUserData) == DRWAV_FALSE) { + break; + } + cursor += chunkSize; + + continue; /* There may be some more metadata to read. */ + } + } + + /* "fact". This is optional. Can use this to get the sample count which is useful for compressed formats. For RF64 we retrieved the sample count from the ds64 chunk earlier. */ + if (((pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx || pWav->container == drwav_container_rf64) && drwav_fourcc_equal(header.id.fourcc, "fact")) || + ((pWav->container == drwav_container_w64) && drwav_guid_equal(header.id.guid, drwavGUID_W64_FACT))) { + if (pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx) { + drwav_uint8 sampleCount[4]; + + if (chunkSize < 4) { + return DRWAV_FALSE; + } + + if (drwav__on_read(pWav->onRead, pWav->pUserData, &sampleCount, 4, &cursor) != 4) { + return DRWAV_FALSE; + } + + chunkSize -= 4; + + /* + The sample count in the "fact" chunk is either unreliable, or I'm not understanding it properly. For now I am only enabling this + for Microsoft ADPCM formats. + */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM) { + sampleCountFromFactChunk = drwav_bytes_to_u32_ex(sampleCount, pWav->container); + } else { + sampleCountFromFactChunk = 0; + } + } else if (pWav->container == drwav_container_w64) { + if (chunkSize < 8) { + return DRWAV_FALSE; + } + + if (drwav__on_read(pWav->onRead, pWav->pUserData, &sampleCountFromFactChunk, 8, &cursor) != 8) { + return DRWAV_FALSE; + } + + chunkSize -= 8; + } else if (pWav->container == drwav_container_rf64) { + /* We retrieved the sample count from the ds64 chunk earlier so no need to do that here. */ + } + + /* Seek to the next chunk in preparation for the next iteration. */ + chunkSize += header.paddingSize; /* <-- Make sure we seek past the padding. */ + if (drwav__seek_forward(pWav->onSeek, chunkSize, pWav->pUserData) == DRWAV_FALSE) { + break; + } + cursor += chunkSize; + + continue; + } + + + /* "COMM". AIFF/AIFC only. */ + if (pWav->container == drwav_container_aiff && drwav_fourcc_equal(header.id.fourcc, "COMM")) { + drwav_uint8 commData[24]; + drwav_uint32 commDataBytesToRead; + drwav_uint16 channels; + drwav_uint32 frameCount; + drwav_uint16 sampleSizeInBits; + drwav_int64 sampleRate; + drwav_uint16 compressionFormat; + + foundChunk_fmt = DRWAV_TRUE; + + if (isAIFCFormType) { + commDataBytesToRead = 24; + if (header.sizeInBytes < commDataBytesToRead) { + return DRWAV_FALSE; /* Invalid COMM chunk. */ + } + } else { + commDataBytesToRead = 18; + if (header.sizeInBytes != commDataBytesToRead) { + return DRWAV_FALSE; /* INVALID COMM chunk. */ + } + } + + if (drwav__on_read(pWav->onRead, pWav->pUserData, commData, commDataBytesToRead, &cursor) != commDataBytesToRead) { + return DRWAV_FALSE; + } + + + channels = drwav_bytes_to_u16_ex (commData + 0, pWav->container); + frameCount = drwav_bytes_to_u32_ex (commData + 2, pWav->container); + sampleSizeInBits = drwav_bytes_to_u16_ex (commData + 6, pWav->container); + sampleRate = drwav_aiff_extented_to_s64(commData + 8); + + if (sampleRate < 0 || sampleRate > 0xFFFFFFFF) { + return DRWAV_FALSE; /* Invalid sample rate. */ + } + + if (isAIFCFormType) { + const drwav_uint8* type = commData + 18; + + if (drwav_fourcc_equal(type, "NONE")) { + compressionFormat = DR_WAVE_FORMAT_PCM; /* PCM, big-endian. */ + } else if (drwav_fourcc_equal(type, "raw ")) { + compressionFormat = DR_WAVE_FORMAT_PCM; + + /* In my testing, it looks like when the "raw " compression type is used, 8-bit samples should be considered unsigned. */ + if (sampleSizeInBits == 8) { + pWav->aiff.isUnsigned = DRWAV_TRUE; + } + } else if (drwav_fourcc_equal(type, "sowt")) { + compressionFormat = DR_WAVE_FORMAT_PCM; /* PCM, little-endian. */ + pWav->aiff.isLE = DRWAV_TRUE; + } else if (drwav_fourcc_equal(type, "fl32") || drwav_fourcc_equal(type, "fl64") || drwav_fourcc_equal(type, "FL32") || drwav_fourcc_equal(type, "FL64")) { + compressionFormat = DR_WAVE_FORMAT_IEEE_FLOAT; + } else if (drwav_fourcc_equal(type, "alaw") || drwav_fourcc_equal(type, "ALAW")) { + compressionFormat = DR_WAVE_FORMAT_ALAW; + } else if (drwav_fourcc_equal(type, "ulaw") || drwav_fourcc_equal(type, "ULAW")) { + compressionFormat = DR_WAVE_FORMAT_MULAW; + } else if (drwav_fourcc_equal(type, "ima4")) { + compressionFormat = DR_WAVE_FORMAT_DVI_ADPCM; + sampleSizeInBits = 4; + + /* + I haven't been able to figure out how to get correct decoding for IMA ADPCM. Until this is figured out + we'll need to abort when we encounter such an encoding. Advice welcome! + */ + (void)compressionFormat; + (void)sampleSizeInBits; + + return DRWAV_FALSE; + } else { + return DRWAV_FALSE; /* Unknown or unsupported compression format. Need to abort. */ + } + } else { + compressionFormat = DR_WAVE_FORMAT_PCM; /* It's a standard AIFF form which is always compressed. */ + } + + /* With AIFF we want to use the explicitly defined frame count rather than deriving it from the size of the chunk. */ + aiffFrameCount = frameCount; + + /* We should now have enough information to fill out our fmt structure. */ + fmt.formatTag = compressionFormat; + fmt.channels = channels; + fmt.sampleRate = (drwav_uint32)sampleRate; + fmt.bitsPerSample = (sampleSizeInBits + 7) & ~7; /* In AIFF, samples are padded to 8-bit boundaries. We need to round up our bits per sample here. */ + fmt.blockAlign = (drwav_uint16)((drwav_uint32)fmt.channels * fmt.bitsPerSample / 8); + fmt.avgBytesPerSec = fmt.blockAlign * fmt.sampleRate; + + /* + Weird one. I've seen some alaw and ulaw encoded files that for some reason set the bits per sample to 16 when + it should be 8. To get this working I need to explicitly check for this and change it. + */ + if (compressionFormat == DR_WAVE_FORMAT_ALAW || compressionFormat == DR_WAVE_FORMAT_MULAW) { + if (fmt.bitsPerSample > 8) { + fmt.bitsPerSample = 8; + fmt.blockAlign = fmt.channels; + } + } + + /* If the form type is AIFC there will be some additional data in the chunk. We need to seek past it. */ + if (isAIFCFormType) { + if (drwav__seek_forward(pWav->onSeek, (chunkSize - commDataBytesToRead), pWav->pUserData) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + cursor += (chunkSize - commDataBytesToRead); + } + + /* Don't fall through or else we'll end up treating this chunk as metadata which is incorrect. */ + continue; + } + + + /* "SSND". AIFF/AIFC only. This is the AIFF equivalent of the "data" chunk. */ + if (pWav->container == drwav_container_aiff && drwav_fourcc_equal(header.id.fourcc, "SSND")) { + drwav_uint8 offsetAndBlockSizeData[8]; + drwav_uint32 offset; + + foundChunk_data = DRWAV_TRUE; + + if (drwav__on_read(pWav->onRead, pWav->pUserData, offsetAndBlockSizeData, sizeof(offsetAndBlockSizeData), &cursor) != sizeof(offsetAndBlockSizeData)) { + return DRWAV_FALSE; + } + + /* The position of the audio data starts at an offset. */ + offset = drwav_bytes_to_u32_ex(offsetAndBlockSizeData + 0, pWav->container); + pWav->dataChunkDataPos = cursor + offset; + + /* The data chunk size needs to be reduced by the offset or else seeking will break. */ + dataChunkSize = chunkSize; + if (dataChunkSize > offset) { + dataChunkSize -= offset; + } else { + dataChunkSize = 0; + } + + if (sequential) { + if (foundChunk_fmt) { /* <-- Name is misleading, but will be set to true if the COMM chunk has been parsed. */ + /* + Getting here means we're opening in sequential mode and we've found the SSND (data) and COMM (fmt) chunks. We need + to get out of the loop here or else we'll end up going past the data chunk and will have no way of getting back to + it since we're not allowed to seek backwards. + + One subtle detail here is that there is an offset with the SSND chunk. We need to make sure we seek past this offset + so we're left sitting on the first byte of actual audio data. + */ + if (drwav__seek_forward(pWav->onSeek, offset, pWav->pUserData) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + cursor += offset; + + break; + } else { + /* + Getting here means the COMM chunk was not found. In sequential mode, if we haven't yet found the COMM chunk + we'll need to abort because we can't be doing a backwards seek back to the SSND chunk in order to read the + data. For this reason, this configuration of AIFF files are not supported with sequential mode. + */ + return DRWAV_FALSE; + } + } else { + chunkSize += header.paddingSize; /* <-- Make sure we seek past the padding. */ + chunkSize -= sizeof(offsetAndBlockSizeData); /* <-- This was read earlier. */ + + if (drwav__seek_forward(pWav->onSeek, chunkSize, pWav->pUserData) == DRWAV_FALSE) { + break; + } + cursor += chunkSize; + + continue; /* There may be some more metadata to read. */ + } + } + + + /* Getting here means it's not a chunk that we care about internally, but might need to be handled as metadata by the caller. */ + if (isProcessingMetadata) { + if (hasKnownFileSize && header.sizeInBytes > (drwav_uint64)fileSize) { + return DRWAV_FALSE; + } + + drwav__metadata_process_chunk(&metadataParser, &header, drwav_metadata_type_all_including_unknown); + + /* Go back to the start of the chunk so we can normalize the position of the cursor. */ + if (drwav__seek_from_start(pWav->onSeek, cursor, pWav->pUserData) == DRWAV_FALSE) { + break; /* Failed to seek. Can't reliable read the remaining chunks. Get out. */ + } + } + + + /* Make sure we skip past the content of this chunk before we go to the next one. */ + chunkSize += header.paddingSize; /* <-- Make sure we seek past the padding. */ + if (drwav__seek_forward(pWav->onSeek, chunkSize, pWav->pUserData) == DRWAV_FALSE) { + break; + } + cursor += chunkSize; + } + + /* There's some mandatory chunks that must exist. If they were not found in the iteration above we must abort. */ + if (!foundChunk_fmt || !foundChunk_data) { + return DRWAV_FALSE; + } + + /* Basic validation. */ + if ((fmt.sampleRate == 0 || fmt.sampleRate > DRWAV_MAX_SAMPLE_RATE ) || + (fmt.channels == 0 || fmt.channels > DRWAV_MAX_CHANNELS ) || + (fmt.bitsPerSample == 0 || fmt.bitsPerSample > DRWAV_MAX_BITS_PER_SAMPLE) || + fmt.blockAlign == 0) { + return DRWAV_FALSE; /* Probably an invalid WAV file. */ + } + + /* Translate the internal format. */ + translatedFormatTag = fmt.formatTag; + if (translatedFormatTag == DR_WAVE_FORMAT_EXTENSIBLE) { + translatedFormatTag = drwav_bytes_to_u16_ex(fmt.subFormat + 0, pWav->container); + } + + /* We may have moved passed the data chunk. If so we need to move back. If running in sequential mode we can assume we are already sitting on the data chunk. */ + if (!sequential) { + if (!drwav__seek_from_start(pWav->onSeek, pWav->dataChunkDataPos, pWav->pUserData)) { + return DRWAV_FALSE; + } + cursor = pWav->dataChunkDataPos; + } + + + /* + At this point we should have done the initial parsing of each of our chunks, but we now need to + do a second pass to extract the actual contents of the metadata (the first pass just calculated + the length of the memory allocation). + + We only do this if we've actually got metadata to parse. + */ + if (isProcessingMetadata && metadataParser.metadataCount > 0) { + if (drwav__seek_from_start(pWav->onSeek, metadataStartPos, pWav->pUserData) == DRWAV_FALSE) { + return DRWAV_FALSE; + } + + result = drwav__metadata_alloc(&metadataParser, &pWav->allocationCallbacks); + if (result != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + metadataParser.stage = drwav__metadata_parser_stage_read; + + for (;;) { + drwav_chunk_header header; + drwav_uint64 metadataBytesRead; + + result = drwav__read_chunk_header(pWav->onRead, pWav->pUserData, pWav->container, &cursor, &header); + if (result != DRWAV_SUCCESS) { + break; + } + + metadataBytesRead = drwav__metadata_process_chunk(&metadataParser, &header, drwav_metadata_type_all_including_unknown); + + if (metadataParser.metadataCursor == metadataParser.metadataCount) { + break; + } + + /* Move to the end of the chunk so we can keep iterating. */ + if (drwav__seek_forward(pWav->onSeek, (header.sizeInBytes + header.paddingSize) - metadataBytesRead, pWav->pUserData) == DRWAV_FALSE) { + drwav_free(metadataParser.pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; + } + } + + /* Getting here means we're finished parsing the metadata. */ + pWav->pMetadata = metadataParser.pMetadata; + pWav->metadataCount = metadataParser.metadataCount; + } + + /* + It's possible for the size reported in the data chunk to be greater than that of the file. We + need to do a validation check here to make sure we don't exceed the file size. To skip this + check, set the onTell callback to NULL. + */ + if (pWav->onTell != NULL && pWav->onSeek != NULL) { + if (pWav->onSeek(pWav->pUserData, 0, DRWAV_SEEK_END) == DRWAV_TRUE) { + drwav_int64 fileSize; + if (pWav->onTell(pWav->pUserData, &fileSize)) { + if (dataChunkSize + pWav->dataChunkDataPos > (drwav_uint64)fileSize) { + dataChunkSize = (drwav_uint64)fileSize - pWav->dataChunkDataPos; + } + } + } else { + /* + Failed to seek to the end of the file. It might not be supported by the backend so in + this case we cannot perform the validation check. + */ + } + } + + /* + I've seen a WAV file in the wild where a RIFF-ecapsulated file has the size of it's "RIFF" and + "data" chunks set to 0xFFFFFFFF when the file is definitely not that big. In this case we're + going to have to calculate the size by reading and discarding bytes, and then seeking back. We + cannot do this in sequential mode. We just assume that the rest of the file is audio data. + */ + if (dataChunkSize == 0xFFFFFFFF && (pWav->container == drwav_container_riff || pWav->container == drwav_container_rifx) && pWav->isSequentialWrite == DRWAV_FALSE) { + dataChunkSize = 0; + + for (;;) { + drwav_uint8 temp[4096]; + size_t bytesRead = pWav->onRead(pWav->pUserData, temp, sizeof(temp)); + dataChunkSize += bytesRead; + + if (bytesRead < sizeof(temp)) { + break; + } + } + } + + /* At this point we want to be sitting on the first byte of the raw audio data. */ + if (drwav__seek_from_start(pWav->onSeek, pWav->dataChunkDataPos, pWav->pUserData) == DRWAV_FALSE) { + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; + } + + + pWav->fmt = fmt; + pWav->sampleRate = fmt.sampleRate; + pWav->channels = fmt.channels; + pWav->bitsPerSample = fmt.bitsPerSample; + pWav->translatedFormatTag = translatedFormatTag; + + /* + I've had a report where files would start glitching after seeking. The reason for this is the data + chunk is not a clean multiple of the PCM frame size in bytes. Where this becomes a problem is when + seeking, because the number of bytes remaining in the data chunk is used to calculate the current + byte position. If this byte position is not aligned to the number of bytes in a PCM frame, it will + result in the seek not being cleanly positioned at the start of the PCM frame thereby resulting in + all decoded frames after that being corrupted. + + To address this, we need to round the data chunk size down to the nearest multiple of the frame size. + */ + if (!drwav__is_compressed_format_tag(translatedFormatTag)) { + drwav_uint32 bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame > 0) { + dataChunkSize -= (dataChunkSize % bytesPerFrame); + } + } + + pWav->bytesRemaining = dataChunkSize; + pWav->dataChunkDataSize = dataChunkSize; + + if (sampleCountFromFactChunk != 0) { + pWav->totalPCMFrameCount = sampleCountFromFactChunk; + } else if (aiffFrameCount != 0) { + pWav->totalPCMFrameCount = aiffFrameCount; + } else { + drwav_uint32 bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; /* Invalid file. */ + } + + pWav->totalPCMFrameCount = dataChunkSize / bytesPerFrame; + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM) { + drwav_uint64 totalBlockHeaderSizeInBytes; + drwav_uint64 blockCount = dataChunkSize / fmt.blockAlign; + + /* Make sure any trailing partial block is accounted for. */ + if ((blockCount * fmt.blockAlign) < dataChunkSize) { + blockCount += 1; + } + + /* We decode two samples per byte. There will be blockCount headers in the data chunk. This is enough to know how to calculate the total PCM frame count. */ + totalBlockHeaderSizeInBytes = blockCount * (6*fmt.channels); + if (totalBlockHeaderSizeInBytes >= dataChunkSize) { /* <-- We'll be subtracting totalBlockHeaderSizeInBytes from dataChunkSize next so it must be validated. */ + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; /* Invalid file. */ + } + + pWav->totalPCMFrameCount = ((dataChunkSize - totalBlockHeaderSizeInBytes) * 2) / fmt.channels; + } + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + drwav_uint64 totalBlockHeaderSizeInBytes; + drwav_uint64 blockCount = dataChunkSize / fmt.blockAlign; + + /* Make sure any trailing partial block is accounted for. */ + if ((blockCount * fmt.blockAlign) < dataChunkSize) { + blockCount += 1; + } + + /* We decode two samples per byte. There will be blockCount headers in the data chunk. This is enough to know how to calculate the total PCM frame count. */ + totalBlockHeaderSizeInBytes = blockCount * (4*fmt.channels); + if (totalBlockHeaderSizeInBytes >= dataChunkSize) { /* <-- We'll be subtracting totalBlockHeaderSizeInBytes from dataChunkSize next so it must be validated. */ + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; /* Invalid file. */ + } + + pWav->totalPCMFrameCount = ((dataChunkSize - totalBlockHeaderSizeInBytes) * 2) / fmt.channels; + + /* The header includes a decoded sample for each channel which acts as the initial predictor sample. */ + pWav->totalPCMFrameCount += blockCount; + } + } + + /* Some formats only support a certain number of channels. */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM || pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + if (pWav->channels > 2) { + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; + } + } + + /* The number of bytes per frame must be known. If not, it's an invalid file and not decodable. */ + if (drwav_get_bytes_per_pcm_frame(pWav) == 0) { + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + return DRWAV_FALSE; + } + +#ifdef DR_WAV_LIBSNDFILE_COMPAT + /* + I use libsndfile as a benchmark for testing, however in the version I'm using (from the Windows installer on the libsndfile website), + it appears the total sample count libsndfile uses for MS-ADPCM is incorrect. It would seem they are computing the total sample count + from the number of blocks, however this results in the inclusion of extra silent samples at the end of the last block. The correct + way to know the total sample count is to inspect the "fact" chunk, which should always be present for compressed formats, and should + always include the sample count. This little block of code below is only used to emulate the libsndfile logic so I can properly run my + correctness tests against libsndfile, and is disabled by default. + */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM) { + drwav_uint64 blockCount = dataChunkSize / fmt.blockAlign; + pWav->totalPCMFrameCount = (((blockCount * (fmt.blockAlign - (6*pWav->channels))) * 2)) / fmt.channels; /* x2 because two samples per byte. */ + } + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + drwav_uint64 blockCount = dataChunkSize / fmt.blockAlign; + pWav->totalPCMFrameCount = (((blockCount * (fmt.blockAlign - (4*pWav->channels))) * 2) + (blockCount * pWav->channels)) / fmt.channels; + } +#endif + + return DRWAV_TRUE; +} + +DRWAV_API drwav_bool32 drwav_init(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_ex(pWav, onRead, onSeek, onTell, NULL, pUserData, NULL, 0, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_ex(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, drwav_chunk_proc onChunk, void* pReadSeekTellUserData, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (!drwav_preinit(pWav, onRead, onSeek, onTell, pReadSeekTellUserData, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + return drwav_init__internal(pWav, onChunk, pChunkUserData, flags); +} + +DRWAV_API drwav_bool32 drwav_init_with_metadata(drwav* pWav, drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (!drwav_preinit(pWav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + return drwav_init__internal(pWav, NULL, NULL, flags | DRWAV_WITH_METADATA); +} + +DRWAV_API drwav_metadata* drwav_take_ownership_of_metadata(drwav* pWav) +{ + drwav_metadata *result = pWav->pMetadata; + + pWav->pMetadata = NULL; + pWav->metadataCount = 0; + + return result; +} + + +DRWAV_PRIVATE size_t drwav__write(drwav* pWav, const void* pData, size_t dataSize) +{ + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->onWrite != NULL); + + /* Generic write. Assumes no byte reordering required. */ + return pWav->onWrite(pWav->pUserData, pData, dataSize); +} + +DRWAV_PRIVATE size_t drwav__write_byte(drwav* pWav, drwav_uint8 byte) +{ + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->onWrite != NULL); + + return pWav->onWrite(pWav->pUserData, &byte, 1); +} + +DRWAV_PRIVATE size_t drwav__write_u16ne_to_le(drwav* pWav, drwav_uint16 value) +{ + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->onWrite != NULL); + + if (!drwav__is_little_endian()) { + value = drwav__bswap16(value); + } + + return drwav__write(pWav, &value, 2); +} + +DRWAV_PRIVATE size_t drwav__write_u32ne_to_le(drwav* pWav, drwav_uint32 value) +{ + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->onWrite != NULL); + + if (!drwav__is_little_endian()) { + value = drwav__bswap32(value); + } + + return drwav__write(pWav, &value, 4); +} + +DRWAV_PRIVATE size_t drwav__write_u64ne_to_le(drwav* pWav, drwav_uint64 value) +{ + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->onWrite != NULL); + + if (!drwav__is_little_endian()) { + value = drwav__bswap64(value); + } + + return drwav__write(pWav, &value, 8); +} + +DRWAV_PRIVATE size_t drwav__write_f32ne_to_le(drwav* pWav, float value) +{ + union { + drwav_uint32 u32; + float f32; + } u; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->onWrite != NULL); + + u.f32 = value; + + if (!drwav__is_little_endian()) { + u.u32 = drwav__bswap32(u.u32); + } + + return drwav__write(pWav, &u.u32, 4); +} + +DRWAV_PRIVATE size_t drwav__write_or_count(drwav* pWav, const void* pData, size_t dataSize) +{ + if (pWav == NULL) { + return dataSize; + } + + return drwav__write(pWav, pData, dataSize); +} + +DRWAV_PRIVATE size_t drwav__write_or_count_byte(drwav* pWav, drwav_uint8 byte) +{ + if (pWav == NULL) { + return 1; + } + + return drwav__write_byte(pWav, byte); +} + +DRWAV_PRIVATE size_t drwav__write_or_count_u16ne_to_le(drwav* pWav, drwav_uint16 value) +{ + if (pWav == NULL) { + return 2; + } + + return drwav__write_u16ne_to_le(pWav, value); +} + +DRWAV_PRIVATE size_t drwav__write_or_count_u32ne_to_le(drwav* pWav, drwav_uint32 value) +{ + if (pWav == NULL) { + return 4; + } + + return drwav__write_u32ne_to_le(pWav, value); +} + +#if 0 /* Unused for now. */ +DRWAV_PRIVATE size_t drwav__write_or_count_u64ne_to_le(drwav* pWav, drwav_uint64 value) +{ + if (pWav == NULL) { + return 8; + } + + return drwav__write_u64ne_to_le(pWav, value); +} +#endif + +DRWAV_PRIVATE size_t drwav__write_or_count_f32ne_to_le(drwav* pWav, float value) +{ + if (pWav == NULL) { + return 4; + } + + return drwav__write_f32ne_to_le(pWav, value); +} + +DRWAV_PRIVATE size_t drwav__write_or_count_string_to_fixed_size_buf(drwav* pWav, char* str, size_t bufFixedSize) +{ + size_t len; + + if (pWav == NULL) { + return bufFixedSize; + } + + len = drwav__strlen_clamped(str, bufFixedSize); + drwav__write_or_count(pWav, str, len); + + if (len < bufFixedSize) { + size_t i; + for (i = 0; i < bufFixedSize - len; ++i) { + drwav__write_byte(pWav, 0); + } + } + + return bufFixedSize; +} + + +/* pWav can be NULL meaning just count the bytes that would be written. */ +DRWAV_PRIVATE size_t drwav__write_or_count_metadata(drwav* pWav, drwav_metadata* pMetadatas, drwav_uint32 metadataCount) +{ + size_t bytesWritten = 0; + drwav_bool32 hasListAdtl = DRWAV_FALSE; + drwav_bool32 hasListInfo = DRWAV_FALSE; + drwav_uint32 iMetadata; + + if (pMetadatas == NULL || metadataCount == 0) { + return 0; + } + + for (iMetadata = 0; iMetadata < metadataCount; ++iMetadata) { + drwav_metadata* pMetadata = &pMetadatas[iMetadata]; + drwav_uint32 chunkSize = 0; + + if ((pMetadata->type & drwav_metadata_type_list_all_info_strings) || (pMetadata->type == drwav_metadata_type_unknown && pMetadata->data.unknown.chunkLocation == drwav_metadata_location_inside_info_list)) { + hasListInfo = DRWAV_TRUE; + } + + if ((pMetadata->type & drwav_metadata_type_list_all_adtl) || (pMetadata->type == drwav_metadata_type_unknown && pMetadata->data.unknown.chunkLocation == drwav_metadata_location_inside_adtl_list)) { + hasListAdtl = DRWAV_TRUE; + } + + switch (pMetadata->type) { + case drwav_metadata_type_smpl: + { + drwav_uint32 iLoop; + + chunkSize = DRWAV_SMPL_BYTES + DRWAV_SMPL_LOOP_BYTES * pMetadata->data.smpl.sampleLoopCount + pMetadata->data.smpl.samplerSpecificDataSizeInBytes; + + bytesWritten += drwav__write_or_count(pWav, "smpl", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.manufacturerId); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.productId); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.samplePeriodNanoseconds); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.midiUnityNote); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.midiPitchFraction); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.smpteFormat); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.smpteOffset); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.sampleLoopCount); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.samplerSpecificDataSizeInBytes); + + for (iLoop = 0; iLoop < pMetadata->data.smpl.sampleLoopCount; ++iLoop) { + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].cuePointId); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].type); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].firstSampleOffset); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].lastSampleOffset); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].sampleFraction); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.smpl.pLoops[iLoop].playCount); + } + + if (pMetadata->data.smpl.samplerSpecificDataSizeInBytes > 0) { + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.smpl.pSamplerSpecificData, pMetadata->data.smpl.samplerSpecificDataSizeInBytes); + } + } break; + + case drwav_metadata_type_inst: + { + chunkSize = DRWAV_INST_BYTES; + + bytesWritten += drwav__write_or_count(pWav, "inst", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.midiUnityNote, 1); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.fineTuneCents, 1); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.gainDecibels, 1); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.lowNote, 1); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.highNote, 1); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.lowVelocity, 1); + bytesWritten += drwav__write_or_count(pWav, &pMetadata->data.inst.highVelocity, 1); + } break; + + case drwav_metadata_type_cue: + { + drwav_uint32 iCuePoint; + + chunkSize = DRWAV_CUE_BYTES + DRWAV_CUE_POINT_BYTES * pMetadata->data.cue.cuePointCount; + + bytesWritten += drwav__write_or_count(pWav, "cue ", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.cuePointCount); + for (iCuePoint = 0; iCuePoint < pMetadata->data.cue.cuePointCount; ++iCuePoint) { + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].id); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].playOrderPosition); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].dataChunkId, 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].chunkStart); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].blockStart); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.cue.pCuePoints[iCuePoint].sampleOffset); + } + } break; + + case drwav_metadata_type_acid: + { + chunkSize = DRWAV_ACID_BYTES; + + bytesWritten += drwav__write_or_count(pWav, "acid", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.acid.flags); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.acid.midiUnityNote); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.acid.reserved1); + bytesWritten += drwav__write_or_count_f32ne_to_le(pWav, pMetadata->data.acid.reserved2); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.acid.numBeats); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.acid.meterDenominator); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.acid.meterNumerator); + bytesWritten += drwav__write_or_count_f32ne_to_le(pWav, pMetadata->data.acid.tempo); + } break; + + case drwav_metadata_type_bext: + { + char reservedBuf[DRWAV_BEXT_RESERVED_BYTES]; + drwav_uint32 timeReferenceLow; + drwav_uint32 timeReferenceHigh; + + chunkSize = DRWAV_BEXT_BYTES + pMetadata->data.bext.codingHistorySize; + + bytesWritten += drwav__write_or_count(pWav, "bext", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + + bytesWritten += drwav__write_or_count_string_to_fixed_size_buf(pWav, pMetadata->data.bext.pDescription, DRWAV_BEXT_DESCRIPTION_BYTES); + bytesWritten += drwav__write_or_count_string_to_fixed_size_buf(pWav, pMetadata->data.bext.pOriginatorName, DRWAV_BEXT_ORIGINATOR_NAME_BYTES); + bytesWritten += drwav__write_or_count_string_to_fixed_size_buf(pWav, pMetadata->data.bext.pOriginatorReference, DRWAV_BEXT_ORIGINATOR_REF_BYTES); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.bext.pOriginationDate, sizeof(pMetadata->data.bext.pOriginationDate)); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.bext.pOriginationTime, sizeof(pMetadata->data.bext.pOriginationTime)); + + timeReferenceLow = (drwav_uint32)(pMetadata->data.bext.timeReference & 0xFFFFFFFF); + timeReferenceHigh = (drwav_uint32)(pMetadata->data.bext.timeReference >> 32); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, timeReferenceLow); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, timeReferenceHigh); + + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.bext.version); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.bext.pUMID, DRWAV_BEXT_UMID_BYTES); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.bext.loudnessValue); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.bext.loudnessRange); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.bext.maxTruePeakLevel); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.bext.maxMomentaryLoudness); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.bext.maxShortTermLoudness); + + DRWAV_ZERO_MEMORY(reservedBuf, sizeof(reservedBuf)); + bytesWritten += drwav__write_or_count(pWav, reservedBuf, sizeof(reservedBuf)); + + if (pMetadata->data.bext.codingHistorySize > 0) { + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.bext.pCodingHistory, pMetadata->data.bext.codingHistorySize); + } + } break; + + case drwav_metadata_type_unknown: + { + if (pMetadata->data.unknown.chunkLocation == drwav_metadata_location_top_level) { + chunkSize = pMetadata->data.unknown.dataSizeInBytes; + + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.unknown.id, 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.unknown.pData, pMetadata->data.unknown.dataSizeInBytes); + } + } break; + + default: break; + } + if ((chunkSize % 2) != 0) { + bytesWritten += drwav__write_or_count_byte(pWav, 0); + } + } + + if (hasListInfo) { + drwav_uint32 chunkSize = 4; /* Start with 4 bytes for "INFO". */ + for (iMetadata = 0; iMetadata < metadataCount; ++iMetadata) { + drwav_metadata* pMetadata = &pMetadatas[iMetadata]; + + if ((pMetadata->type & drwav_metadata_type_list_all_info_strings)) { + chunkSize += 8; /* For id and string size. */ + chunkSize += pMetadata->data.infoText.stringLength + 1; /* Include null terminator. */ + } else if (pMetadata->type == drwav_metadata_type_unknown && pMetadata->data.unknown.chunkLocation == drwav_metadata_location_inside_info_list) { + chunkSize += 8; /* For id string size. */ + chunkSize += pMetadata->data.unknown.dataSizeInBytes; + } + + if ((chunkSize % 2) != 0) { + chunkSize += 1; + } + } + + bytesWritten += drwav__write_or_count(pWav, "LIST", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + bytesWritten += drwav__write_or_count(pWav, "INFO", 4); + + for (iMetadata = 0; iMetadata < metadataCount; ++iMetadata) { + drwav_metadata* pMetadata = &pMetadatas[iMetadata]; + drwav_uint32 subchunkSize = 0; + + if (pMetadata->type & drwav_metadata_type_list_all_info_strings) { + const char* pID = NULL; + + switch (pMetadata->type) { + case drwav_metadata_type_list_info_software: pID = "ISFT"; break; + case drwav_metadata_type_list_info_copyright: pID = "ICOP"; break; + case drwav_metadata_type_list_info_title: pID = "INAM"; break; + case drwav_metadata_type_list_info_artist: pID = "IART"; break; + case drwav_metadata_type_list_info_comment: pID = "ICMT"; break; + case drwav_metadata_type_list_info_date: pID = "ICRD"; break; + case drwav_metadata_type_list_info_genre: pID = "IGNR"; break; + case drwav_metadata_type_list_info_album: pID = "IPRD"; break; + case drwav_metadata_type_list_info_tracknumber: pID = "ITRK"; break; + case drwav_metadata_type_list_info_location: pID = "IARL"; break; + case drwav_metadata_type_list_info_organization: pID = "ICMS"; break; + case drwav_metadata_type_list_info_keywords: pID = "IKEY"; break; + case drwav_metadata_type_list_info_medium: pID = "IMED"; break; + case drwav_metadata_type_list_info_description: pID = "ISBJ"; break; + default: break; + } + + DRWAV_ASSERT(pID != NULL); + + if (pMetadata->data.infoText.stringLength) { + subchunkSize = pMetadata->data.infoText.stringLength + 1; + bytesWritten += drwav__write_or_count(pWav, pID, 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, subchunkSize); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.infoText.pString, pMetadata->data.infoText.stringLength); + bytesWritten += drwav__write_or_count_byte(pWav, '\0'); + } + } else if (pMetadata->type == drwav_metadata_type_unknown && pMetadata->data.unknown.chunkLocation == drwav_metadata_location_inside_info_list) { + if (pMetadata->data.unknown.dataSizeInBytes) { + subchunkSize = pMetadata->data.unknown.dataSizeInBytes; + + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.unknown.id, 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.unknown.dataSizeInBytes); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.unknown.pData, subchunkSize); + } + } + + if ((subchunkSize % 2) != 0) { + bytesWritten += drwav__write_or_count_byte(pWav, 0); + } + } + } + + if (hasListAdtl) { + drwav_uint32 chunkSize = 4; /* start with 4 bytes for "adtl" */ + + for (iMetadata = 0; iMetadata < metadataCount; ++iMetadata) { + drwav_metadata* pMetadata = &pMetadatas[iMetadata]; + + switch (pMetadata->type) + { + case drwav_metadata_type_list_label: + case drwav_metadata_type_list_note: + { + chunkSize += 8; /* for id and chunk size */ + chunkSize += DRWAV_LIST_LABEL_OR_NOTE_BYTES; + + if (pMetadata->data.labelOrNote.stringLength > 0) { + chunkSize += pMetadata->data.labelOrNote.stringLength + 1; + } + } break; + + case drwav_metadata_type_list_labelled_cue_region: + { + chunkSize += 8; /* for id and chunk size */ + chunkSize += DRWAV_LIST_LABELLED_TEXT_BYTES; + + if (pMetadata->data.labelledCueRegion.stringLength > 0) { + chunkSize += pMetadata->data.labelledCueRegion.stringLength + 1; + } + } break; + + case drwav_metadata_type_unknown: + { + if (pMetadata->data.unknown.chunkLocation == drwav_metadata_location_inside_adtl_list) { + chunkSize += 8; /* for id and chunk size */ + chunkSize += pMetadata->data.unknown.dataSizeInBytes; + } + } break; + + default: break; + } + + if ((chunkSize % 2) != 0) { + chunkSize += 1; + } + } + + bytesWritten += drwav__write_or_count(pWav, "LIST", 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, chunkSize); + bytesWritten += drwav__write_or_count(pWav, "adtl", 4); + + for (iMetadata = 0; iMetadata < metadataCount; ++iMetadata) { + drwav_metadata* pMetadata = &pMetadatas[iMetadata]; + drwav_uint32 subchunkSize = 0; + + switch (pMetadata->type) + { + case drwav_metadata_type_list_label: + case drwav_metadata_type_list_note: + { + if (pMetadata->data.labelOrNote.stringLength > 0) { + const char *pID = NULL; + + if (pMetadata->type == drwav_metadata_type_list_label) { + pID = "labl"; + } + else if (pMetadata->type == drwav_metadata_type_list_note) { + pID = "note"; + } + + DRWAV_ASSERT(pID != NULL); + DRWAV_ASSERT(pMetadata->data.labelOrNote.pString != NULL); + + subchunkSize = DRWAV_LIST_LABEL_OR_NOTE_BYTES; + + bytesWritten += drwav__write_or_count(pWav, pID, 4); + subchunkSize += pMetadata->data.labelOrNote.stringLength + 1; + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, subchunkSize); + + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.labelOrNote.cuePointId); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.labelOrNote.pString, pMetadata->data.labelOrNote.stringLength); + bytesWritten += drwav__write_or_count_byte(pWav, '\0'); + } + } break; + + case drwav_metadata_type_list_labelled_cue_region: + { + subchunkSize = DRWAV_LIST_LABELLED_TEXT_BYTES; + + bytesWritten += drwav__write_or_count(pWav, "ltxt", 4); + if (pMetadata->data.labelledCueRegion.stringLength > 0) { + subchunkSize += pMetadata->data.labelledCueRegion.stringLength + 1; + } + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, subchunkSize); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.labelledCueRegion.cuePointId); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, pMetadata->data.labelledCueRegion.sampleLength); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.labelledCueRegion.purposeId, 4); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.labelledCueRegion.country); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.labelledCueRegion.language); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.labelledCueRegion.dialect); + bytesWritten += drwav__write_or_count_u16ne_to_le(pWav, pMetadata->data.labelledCueRegion.codePage); + + if (pMetadata->data.labelledCueRegion.stringLength > 0) { + DRWAV_ASSERT(pMetadata->data.labelledCueRegion.pString != NULL); + + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.labelledCueRegion.pString, pMetadata->data.labelledCueRegion.stringLength); + bytesWritten += drwav__write_or_count_byte(pWav, '\0'); + } + } break; + + case drwav_metadata_type_unknown: + { + if (pMetadata->data.unknown.chunkLocation == drwav_metadata_location_inside_adtl_list) { + subchunkSize = pMetadata->data.unknown.dataSizeInBytes; + + DRWAV_ASSERT(pMetadata->data.unknown.pData != NULL); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.unknown.id, 4); + bytesWritten += drwav__write_or_count_u32ne_to_le(pWav, subchunkSize); + bytesWritten += drwav__write_or_count(pWav, pMetadata->data.unknown.pData, subchunkSize); + } + } break; + + default: break; + } + + if ((subchunkSize % 2) != 0) { + bytesWritten += drwav__write_or_count_byte(pWav, 0); + } + } + } + + DRWAV_ASSERT((bytesWritten % 2) == 0); + + return bytesWritten; +} + +DRWAV_PRIVATE drwav_uint32 drwav__riff_chunk_size_riff(drwav_uint64 dataChunkSize, drwav_metadata* pMetadata, drwav_uint32 metadataCount) +{ + drwav_uint64 chunkSize = 4 + 24 + (drwav_uint64)drwav__write_or_count_metadata(NULL, pMetadata, metadataCount) + 8 + dataChunkSize + drwav__chunk_padding_size_riff(dataChunkSize); /* 4 = "WAVE". 24 = "fmt " chunk. 8 = "data" + u32 data size. */ + if (chunkSize > 0xFFFFFFFFUL) { + chunkSize = 0xFFFFFFFFUL; + } + + return (drwav_uint32)chunkSize; /* Safe cast due to the clamp above. */ +} + +DRWAV_PRIVATE drwav_uint32 drwav__data_chunk_size_riff(drwav_uint64 dataChunkSize) +{ + if (dataChunkSize <= 0xFFFFFFFFUL) { + return (drwav_uint32)dataChunkSize; + } else { + return 0xFFFFFFFFUL; + } +} + +DRWAV_PRIVATE drwav_uint64 drwav__riff_chunk_size_w64(drwav_uint64 dataChunkSize) +{ + drwav_uint64 dataSubchunkPaddingSize = drwav__chunk_padding_size_w64(dataChunkSize); + + return 80 + 24 + dataChunkSize + dataSubchunkPaddingSize; /* +24 because W64 includes the size of the GUID and size fields. */ +} + +DRWAV_PRIVATE drwav_uint64 drwav__data_chunk_size_w64(drwav_uint64 dataChunkSize) +{ + return 24 + dataChunkSize; /* +24 because W64 includes the size of the GUID and size fields. */ +} + +DRWAV_PRIVATE drwav_uint64 drwav__riff_chunk_size_rf64(drwav_uint64 dataChunkSize, drwav_metadata *metadata, drwav_uint32 numMetadata) +{ + drwav_uint64 chunkSize = 4 + 36 + 24 + (drwav_uint64)drwav__write_or_count_metadata(NULL, metadata, numMetadata) + 8 + dataChunkSize + drwav__chunk_padding_size_riff(dataChunkSize); /* 4 = "WAVE". 36 = "ds64" chunk. 24 = "fmt " chunk. 8 = "data" + u32 data size. */ + if (chunkSize > 0xFFFFFFFFUL) { + chunkSize = 0xFFFFFFFFUL; + } + + return chunkSize; +} + +DRWAV_PRIVATE drwav_uint64 drwav__data_chunk_size_rf64(drwav_uint64 dataChunkSize) +{ + return dataChunkSize; +} + + + +DRWAV_PRIVATE drwav_bool32 drwav_preinit_write(drwav* pWav, const drwav_data_format* pFormat, drwav_bool32 isSequential, drwav_write_proc onWrite, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pWav == NULL || onWrite == NULL) { + return DRWAV_FALSE; + } + + if (!isSequential && onSeek == NULL) { + return DRWAV_FALSE; /* <-- onSeek is required when in non-sequential mode. */ + } + + /* Not currently supporting compressed formats. Will need to add support for the "fact" chunk before we enable this. */ + if (pFormat->format == DR_WAVE_FORMAT_EXTENSIBLE) { + return DRWAV_FALSE; + } + if (pFormat->format == DR_WAVE_FORMAT_ADPCM || pFormat->format == DR_WAVE_FORMAT_DVI_ADPCM) { + return DRWAV_FALSE; + } + + DRWAV_ZERO_MEMORY(pWav, sizeof(*pWav)); + pWav->onWrite = onWrite; + pWav->onSeek = onSeek; + pWav->pUserData = pUserData; + pWav->allocationCallbacks = drwav_copy_allocation_callbacks_or_defaults(pAllocationCallbacks); + + if (pWav->allocationCallbacks.onFree == NULL || (pWav->allocationCallbacks.onMalloc == NULL && pWav->allocationCallbacks.onRealloc == NULL)) { + return DRWAV_FALSE; /* Invalid allocation callbacks. */ + } + + pWav->fmt.formatTag = (drwav_uint16)pFormat->format; + pWav->fmt.channels = (drwav_uint16)pFormat->channels; + pWav->fmt.sampleRate = pFormat->sampleRate; + pWav->fmt.avgBytesPerSec = (drwav_uint32)((pFormat->bitsPerSample * pFormat->sampleRate * pFormat->channels) / 8); + pWav->fmt.blockAlign = (drwav_uint16)((pFormat->channels * pFormat->bitsPerSample) / 8); + pWav->fmt.bitsPerSample = (drwav_uint16)pFormat->bitsPerSample; + pWav->fmt.extendedSize = 0; + pWav->isSequentialWrite = isSequential; + + return DRWAV_TRUE; +} + + +DRWAV_PRIVATE drwav_bool32 drwav_init_write__internal(drwav* pWav, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount) +{ + /* The function assumes drwav_preinit_write() was called beforehand. */ + + size_t runningPos = 0; + drwav_uint64 initialDataChunkSize = 0; + drwav_uint64 chunkSizeFMT; + + /* + The initial values for the "RIFF" and "data" chunks depends on whether or not we are initializing in sequential mode or not. In + sequential mode we set this to its final values straight away since they can be calculated from the total sample count. In non- + sequential mode we initialize it all to zero and fill it out in drwav_uninit() using a backwards seek. + */ + if (pWav->isSequentialWrite) { + initialDataChunkSize = (totalSampleCount * pWav->fmt.bitsPerSample) / 8; + + /* + The RIFF container has a limit on the number of samples. drwav is not allowing this. There's no practical limits for Wave64 + so for the sake of simplicity I'm not doing any validation for that. + */ + if (pFormat->container == drwav_container_riff) { + if (initialDataChunkSize > (0xFFFFFFFFUL - 36)) { + return DRWAV_FALSE; /* Not enough room to store every sample. */ + } + } + } + + pWav->dataChunkDataSizeTargetWrite = initialDataChunkSize; + + + /* "RIFF" chunk. */ + if (pFormat->container == drwav_container_riff) { + drwav_uint32 chunkSizeRIFF = 36 + (drwav_uint32)initialDataChunkSize; /* +36 = "WAVE" + [sizeof "fmt " chunk] + [data chunk header] */ + runningPos += drwav__write(pWav, "RIFF", 4); + runningPos += drwav__write_u32ne_to_le(pWav, chunkSizeRIFF); + runningPos += drwav__write(pWav, "WAVE", 4); + } else if (pFormat->container == drwav_container_w64) { + drwav_uint64 chunkSizeRIFF = 80 + 24 + initialDataChunkSize; /* +24 because W64 includes the size of the GUID and size fields. */ + runningPos += drwav__write(pWav, drwavGUID_W64_RIFF, 16); + runningPos += drwav__write_u64ne_to_le(pWav, chunkSizeRIFF); + runningPos += drwav__write(pWav, drwavGUID_W64_WAVE, 16); + } else if (pFormat->container == drwav_container_rf64) { + runningPos += drwav__write(pWav, "RF64", 4); + runningPos += drwav__write_u32ne_to_le(pWav, 0xFFFFFFFF); /* Always 0xFFFFFFFF for RF64. Set to a proper value in the "ds64" chunk. */ + runningPos += drwav__write(pWav, "WAVE", 4); + } else { + return DRWAV_FALSE; /* Container not supported for writing. */ + } + + + /* "ds64" chunk (RF64 only). */ + if (pFormat->container == drwav_container_rf64) { + drwav_uint32 initialds64ChunkSize = 28; /* 28 = [Size of RIFF (8 bytes)] + [Size of DATA (8 bytes)] + [Sample Count (8 bytes)] + [Table Length (4 bytes)]. Table length always set to 0. */ + drwav_uint64 initialRiffChunkSize = 8 + initialds64ChunkSize + initialDataChunkSize; /* +8 for the ds64 header. */ + + runningPos += drwav__write(pWav, "ds64", 4); + runningPos += drwav__write_u32ne_to_le(pWav, initialds64ChunkSize); /* Size of ds64. */ + runningPos += drwav__write_u64ne_to_le(pWav, initialRiffChunkSize); /* Size of RIFF. Set to true value at the end. */ + runningPos += drwav__write_u64ne_to_le(pWav, initialDataChunkSize); /* Size of DATA. Set to true value at the end. */ + runningPos += drwav__write_u64ne_to_le(pWav, totalSampleCount); /* Sample count. */ + runningPos += drwav__write_u32ne_to_le(pWav, 0); /* Table length. Always set to zero in our case since we're not doing any other chunks than "DATA". */ + } + + + /* "fmt " chunk. */ + if (pFormat->container == drwav_container_riff || pFormat->container == drwav_container_rf64) { + chunkSizeFMT = 16; + runningPos += drwav__write(pWav, "fmt ", 4); + runningPos += drwav__write_u32ne_to_le(pWav, (drwav_uint32)chunkSizeFMT); + } else if (pFormat->container == drwav_container_w64) { + chunkSizeFMT = 40; + runningPos += drwav__write(pWav, drwavGUID_W64_FMT, 16); + runningPos += drwav__write_u64ne_to_le(pWav, chunkSizeFMT); + } + + runningPos += drwav__write_u16ne_to_le(pWav, pWav->fmt.formatTag); + runningPos += drwav__write_u16ne_to_le(pWav, pWav->fmt.channels); + runningPos += drwav__write_u32ne_to_le(pWav, pWav->fmt.sampleRate); + runningPos += drwav__write_u32ne_to_le(pWav, pWav->fmt.avgBytesPerSec); + runningPos += drwav__write_u16ne_to_le(pWav, pWav->fmt.blockAlign); + runningPos += drwav__write_u16ne_to_le(pWav, pWav->fmt.bitsPerSample); + + /* TODO: is a 'fact' chunk required for DR_WAVE_FORMAT_IEEE_FLOAT? */ + + if (!pWav->isSequentialWrite && pWav->pMetadata != NULL && pWav->metadataCount > 0 && (pFormat->container == drwav_container_riff || pFormat->container == drwav_container_rf64)) { + runningPos += drwav__write_or_count_metadata(pWav, pWav->pMetadata, pWav->metadataCount); + } + + pWav->dataChunkDataPos = runningPos; + + /* "data" chunk. */ + if (pFormat->container == drwav_container_riff) { + drwav_uint32 chunkSizeDATA = (drwav_uint32)initialDataChunkSize; + runningPos += drwav__write(pWav, "data", 4); + runningPos += drwav__write_u32ne_to_le(pWav, chunkSizeDATA); + } else if (pFormat->container == drwav_container_w64) { + drwav_uint64 chunkSizeDATA = 24 + initialDataChunkSize; /* +24 because W64 includes the size of the GUID and size fields. */ + runningPos += drwav__write(pWav, drwavGUID_W64_DATA, 16); + runningPos += drwav__write_u64ne_to_le(pWav, chunkSizeDATA); + } else if (pFormat->container == drwav_container_rf64) { + runningPos += drwav__write(pWav, "data", 4); + runningPos += drwav__write_u32ne_to_le(pWav, 0xFFFFFFFF); /* Always set to 0xFFFFFFFF for RF64. The true size of the data chunk is specified in the ds64 chunk. */ + } + + /* Set some properties for the client's convenience. */ + pWav->container = pFormat->container; + pWav->channels = (drwav_uint16)pFormat->channels; + pWav->sampleRate = pFormat->sampleRate; + pWav->bitsPerSample = (drwav_uint16)pFormat->bitsPerSample; + pWav->translatedFormatTag = (drwav_uint16)pFormat->format; + pWav->dataChunkDataPos = runningPos; + + return DRWAV_TRUE; +} + + +DRWAV_API drwav_bool32 drwav_init_write(drwav* pWav, const drwav_data_format* pFormat, drwav_write_proc onWrite, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (!drwav_preinit_write(pWav, pFormat, DRWAV_FALSE, onWrite, onSeek, pUserData, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + return drwav_init_write__internal(pWav, pFormat, 0); /* DRWAV_FALSE = Not Sequential */ +} + +DRWAV_API drwav_bool32 drwav_init_write_sequential(drwav* pWav, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, drwav_write_proc onWrite, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (!drwav_preinit_write(pWav, pFormat, DRWAV_TRUE, onWrite, NULL, pUserData, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + return drwav_init_write__internal(pWav, pFormat, totalSampleCount); /* DRWAV_TRUE = Sequential */ +} + +DRWAV_API drwav_bool32 drwav_init_write_sequential_pcm_frames(drwav* pWav, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, drwav_write_proc onWrite, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pFormat == NULL) { + return DRWAV_FALSE; + } + + return drwav_init_write_sequential(pWav, pFormat, totalPCMFrameCount*pFormat->channels, onWrite, pUserData, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_write_with_metadata(drwav* pWav, const drwav_data_format* pFormat, drwav_write_proc onWrite, drwav_seek_proc onSeek, void* pUserData, const drwav_allocation_callbacks* pAllocationCallbacks, drwav_metadata* pMetadata, drwav_uint32 metadataCount) +{ + if (!drwav_preinit_write(pWav, pFormat, DRWAV_FALSE, onWrite, onSeek, pUserData, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + pWav->pMetadata = pMetadata; + pWav->metadataCount = metadataCount; + + return drwav_init_write__internal(pWav, pFormat, 0); +} + + +DRWAV_API drwav_uint64 drwav_target_write_size_bytes(const drwav_data_format* pFormat, drwav_uint64 totalFrameCount, drwav_metadata* pMetadata, drwav_uint32 metadataCount) +{ + /* Casting totalFrameCount to drwav_int64 for VC6 compatibility. No issues in practice because nobody is going to exhaust the whole 63 bits. */ + drwav_uint64 targetDataSizeBytes = (drwav_uint64)((drwav_int64)totalFrameCount * pFormat->channels * pFormat->bitsPerSample/8.0); + drwav_uint64 riffChunkSizeBytes; + drwav_uint64 fileSizeBytes = 0; + + if (pFormat->container == drwav_container_riff) { + riffChunkSizeBytes = drwav__riff_chunk_size_riff(targetDataSizeBytes, pMetadata, metadataCount); + fileSizeBytes = (8 + riffChunkSizeBytes); /* +8 because WAV doesn't include the size of the ChunkID and ChunkSize fields. */ + } else if (pFormat->container == drwav_container_w64) { + riffChunkSizeBytes = drwav__riff_chunk_size_w64(targetDataSizeBytes); + fileSizeBytes = riffChunkSizeBytes; + } else if (pFormat->container == drwav_container_rf64) { + riffChunkSizeBytes = drwav__riff_chunk_size_rf64(targetDataSizeBytes, pMetadata, metadataCount); + fileSizeBytes = (8 + riffChunkSizeBytes); /* +8 because WAV doesn't include the size of the ChunkID and ChunkSize fields. */ + } + + return fileSizeBytes; +} + + +#ifndef DR_WAV_NO_STDIO + +/* Errno */ +/* drwav_result_from_errno() is only used for fopen() and wfopen() so putting it inside DR_WAV_NO_STDIO for now. If something else needs this later we can move it out. */ +#include +DRWAV_PRIVATE drwav_result drwav_result_from_errno(int e) +{ + switch (e) + { + case 0: return DRWAV_SUCCESS; + #ifdef EPERM + case EPERM: return DRWAV_INVALID_OPERATION; + #endif + #ifdef ENOENT + case ENOENT: return DRWAV_DOES_NOT_EXIST; + #endif + #ifdef ESRCH + case ESRCH: return DRWAV_DOES_NOT_EXIST; + #endif + #ifdef EINTR + case EINTR: return DRWAV_INTERRUPT; + #endif + #ifdef EIO + case EIO: return DRWAV_IO_ERROR; + #endif + #ifdef ENXIO + case ENXIO: return DRWAV_DOES_NOT_EXIST; + #endif + #ifdef E2BIG + case E2BIG: return DRWAV_INVALID_ARGS; + #endif + #ifdef ENOEXEC + case ENOEXEC: return DRWAV_INVALID_FILE; + #endif + #ifdef EBADF + case EBADF: return DRWAV_INVALID_FILE; + #endif + #ifdef ECHILD + case ECHILD: return DRWAV_ERROR; + #endif + #ifdef EAGAIN + case EAGAIN: return DRWAV_UNAVAILABLE; + #endif + #ifdef ENOMEM + case ENOMEM: return DRWAV_OUT_OF_MEMORY; + #endif + #ifdef EACCES + case EACCES: return DRWAV_ACCESS_DENIED; + #endif + #ifdef EFAULT + case EFAULT: return DRWAV_BAD_ADDRESS; + #endif + #ifdef ENOTBLK + case ENOTBLK: return DRWAV_ERROR; + #endif + #ifdef EBUSY + case EBUSY: return DRWAV_BUSY; + #endif + #ifdef EEXIST + case EEXIST: return DRWAV_ALREADY_EXISTS; + #endif + #ifdef EXDEV + case EXDEV: return DRWAV_ERROR; + #endif + #ifdef ENODEV + case ENODEV: return DRWAV_DOES_NOT_EXIST; + #endif + #ifdef ENOTDIR + case ENOTDIR: return DRWAV_NOT_DIRECTORY; + #endif + #ifdef EISDIR + case EISDIR: return DRWAV_IS_DIRECTORY; + #endif + #ifdef EINVAL + case EINVAL: return DRWAV_INVALID_ARGS; + #endif + #ifdef ENFILE + case ENFILE: return DRWAV_TOO_MANY_OPEN_FILES; + #endif + #ifdef EMFILE + case EMFILE: return DRWAV_TOO_MANY_OPEN_FILES; + #endif + #ifdef ENOTTY + case ENOTTY: return DRWAV_INVALID_OPERATION; + #endif + #ifdef ETXTBSY + case ETXTBSY: return DRWAV_BUSY; + #endif + #ifdef EFBIG + case EFBIG: return DRWAV_TOO_BIG; + #endif + #ifdef ENOSPC + case ENOSPC: return DRWAV_NO_SPACE; + #endif + #ifdef ESPIPE + case ESPIPE: return DRWAV_BAD_SEEK; + #endif + #ifdef EROFS + case EROFS: return DRWAV_ACCESS_DENIED; + #endif + #ifdef EMLINK + case EMLINK: return DRWAV_TOO_MANY_LINKS; + #endif + #ifdef EPIPE + case EPIPE: return DRWAV_BAD_PIPE; + #endif + #ifdef EDOM + case EDOM: return DRWAV_OUT_OF_RANGE; + #endif + #ifdef ERANGE + case ERANGE: return DRWAV_OUT_OF_RANGE; + #endif + #ifdef EDEADLK + case EDEADLK: return DRWAV_DEADLOCK; + #endif + #ifdef ENAMETOOLONG + case ENAMETOOLONG: return DRWAV_PATH_TOO_LONG; + #endif + #ifdef ENOLCK + case ENOLCK: return DRWAV_ERROR; + #endif + #ifdef ENOSYS + case ENOSYS: return DRWAV_NOT_IMPLEMENTED; + #endif + #if defined(ENOTEMPTY) && ENOTEMPTY != EEXIST /* In AIX, ENOTEMPTY and EEXIST use the same value. */ + case ENOTEMPTY: return DRWAV_DIRECTORY_NOT_EMPTY; + #endif + #ifdef ELOOP + case ELOOP: return DRWAV_TOO_MANY_LINKS; + #endif + #ifdef ENOMSG + case ENOMSG: return DRWAV_NO_MESSAGE; + #endif + #ifdef EIDRM + case EIDRM: return DRWAV_ERROR; + #endif + #ifdef ECHRNG + case ECHRNG: return DRWAV_ERROR; + #endif + #ifdef EL2NSYNC + case EL2NSYNC: return DRWAV_ERROR; + #endif + #ifdef EL3HLT + case EL3HLT: return DRWAV_ERROR; + #endif + #ifdef EL3RST + case EL3RST: return DRWAV_ERROR; + #endif + #ifdef ELNRNG + case ELNRNG: return DRWAV_OUT_OF_RANGE; + #endif + #ifdef EUNATCH + case EUNATCH: return DRWAV_ERROR; + #endif + #ifdef ENOCSI + case ENOCSI: return DRWAV_ERROR; + #endif + #ifdef EL2HLT + case EL2HLT: return DRWAV_ERROR; + #endif + #ifdef EBADE + case EBADE: return DRWAV_ERROR; + #endif + #ifdef EBADR + case EBADR: return DRWAV_ERROR; + #endif + #ifdef EXFULL + case EXFULL: return DRWAV_ERROR; + #endif + #ifdef ENOANO + case ENOANO: return DRWAV_ERROR; + #endif + #ifdef EBADRQC + case EBADRQC: return DRWAV_ERROR; + #endif + #ifdef EBADSLT + case EBADSLT: return DRWAV_ERROR; + #endif + #ifdef EBFONT + case EBFONT: return DRWAV_INVALID_FILE; + #endif + #ifdef ENOSTR + case ENOSTR: return DRWAV_ERROR; + #endif + #ifdef ENODATA + case ENODATA: return DRWAV_NO_DATA_AVAILABLE; + #endif + #ifdef ETIME + case ETIME: return DRWAV_TIMEOUT; + #endif + #ifdef ENOSR + case ENOSR: return DRWAV_NO_DATA_AVAILABLE; + #endif + #ifdef ENONET + case ENONET: return DRWAV_NO_NETWORK; + #endif + #ifdef ENOPKG + case ENOPKG: return DRWAV_ERROR; + #endif + #ifdef EREMOTE + case EREMOTE: return DRWAV_ERROR; + #endif + #ifdef ENOLINK + case ENOLINK: return DRWAV_ERROR; + #endif + #ifdef EADV + case EADV: return DRWAV_ERROR; + #endif + #ifdef ESRMNT + case ESRMNT: return DRWAV_ERROR; + #endif + #ifdef ECOMM + case ECOMM: return DRWAV_ERROR; + #endif + #ifdef EPROTO + case EPROTO: return DRWAV_ERROR; + #endif + #ifdef EMULTIHOP + case EMULTIHOP: return DRWAV_ERROR; + #endif + #ifdef EDOTDOT + case EDOTDOT: return DRWAV_ERROR; + #endif + #ifdef EBADMSG + case EBADMSG: return DRWAV_BAD_MESSAGE; + #endif + #ifdef EOVERFLOW + case EOVERFLOW: return DRWAV_TOO_BIG; + #endif + #ifdef ENOTUNIQ + case ENOTUNIQ: return DRWAV_NOT_UNIQUE; + #endif + #ifdef EBADFD + case EBADFD: return DRWAV_ERROR; + #endif + #ifdef EREMCHG + case EREMCHG: return DRWAV_ERROR; + #endif + #ifdef ELIBACC + case ELIBACC: return DRWAV_ACCESS_DENIED; + #endif + #ifdef ELIBBAD + case ELIBBAD: return DRWAV_INVALID_FILE; + #endif + #ifdef ELIBSCN + case ELIBSCN: return DRWAV_INVALID_FILE; + #endif + #ifdef ELIBMAX + case ELIBMAX: return DRWAV_ERROR; + #endif + #ifdef ELIBEXEC + case ELIBEXEC: return DRWAV_ERROR; + #endif + #ifdef EILSEQ + case EILSEQ: return DRWAV_INVALID_DATA; + #endif + #ifdef ERESTART + case ERESTART: return DRWAV_ERROR; + #endif + #ifdef ESTRPIPE + case ESTRPIPE: return DRWAV_ERROR; + #endif + #ifdef EUSERS + case EUSERS: return DRWAV_ERROR; + #endif + #ifdef ENOTSOCK + case ENOTSOCK: return DRWAV_NOT_SOCKET; + #endif + #ifdef EDESTADDRREQ + case EDESTADDRREQ: return DRWAV_NO_ADDRESS; + #endif + #ifdef EMSGSIZE + case EMSGSIZE: return DRWAV_TOO_BIG; + #endif + #ifdef EPROTOTYPE + case EPROTOTYPE: return DRWAV_BAD_PROTOCOL; + #endif + #ifdef ENOPROTOOPT + case ENOPROTOOPT: return DRWAV_PROTOCOL_UNAVAILABLE; + #endif + #ifdef EPROTONOSUPPORT + case EPROTONOSUPPORT: return DRWAV_PROTOCOL_NOT_SUPPORTED; + #endif + #ifdef ESOCKTNOSUPPORT + case ESOCKTNOSUPPORT: return DRWAV_SOCKET_NOT_SUPPORTED; + #endif + #ifdef EOPNOTSUPP + case EOPNOTSUPP: return DRWAV_INVALID_OPERATION; + #endif + #ifdef EPFNOSUPPORT + case EPFNOSUPPORT: return DRWAV_PROTOCOL_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EAFNOSUPPORT + case EAFNOSUPPORT: return DRWAV_ADDRESS_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EADDRINUSE + case EADDRINUSE: return DRWAV_ALREADY_IN_USE; + #endif + #ifdef EADDRNOTAVAIL + case EADDRNOTAVAIL: return DRWAV_ERROR; + #endif + #ifdef ENETDOWN + case ENETDOWN: return DRWAV_NO_NETWORK; + #endif + #ifdef ENETUNREACH + case ENETUNREACH: return DRWAV_NO_NETWORK; + #endif + #ifdef ENETRESET + case ENETRESET: return DRWAV_NO_NETWORK; + #endif + #ifdef ECONNABORTED + case ECONNABORTED: return DRWAV_NO_NETWORK; + #endif + #ifdef ECONNRESET + case ECONNRESET: return DRWAV_CONNECTION_RESET; + #endif + #ifdef ENOBUFS + case ENOBUFS: return DRWAV_NO_SPACE; + #endif + #ifdef EISCONN + case EISCONN: return DRWAV_ALREADY_CONNECTED; + #endif + #ifdef ENOTCONN + case ENOTCONN: return DRWAV_NOT_CONNECTED; + #endif + #ifdef ESHUTDOWN + case ESHUTDOWN: return DRWAV_ERROR; + #endif + #ifdef ETOOMANYREFS + case ETOOMANYREFS: return DRWAV_ERROR; + #endif + #ifdef ETIMEDOUT + case ETIMEDOUT: return DRWAV_TIMEOUT; + #endif + #ifdef ECONNREFUSED + case ECONNREFUSED: return DRWAV_CONNECTION_REFUSED; + #endif + #ifdef EHOSTDOWN + case EHOSTDOWN: return DRWAV_NO_HOST; + #endif + #ifdef EHOSTUNREACH + case EHOSTUNREACH: return DRWAV_NO_HOST; + #endif + #ifdef EALREADY + case EALREADY: return DRWAV_IN_PROGRESS; + #endif + #ifdef EINPROGRESS + case EINPROGRESS: return DRWAV_IN_PROGRESS; + #endif + #ifdef ESTALE + case ESTALE: return DRWAV_INVALID_FILE; + #endif + #ifdef EUCLEAN + case EUCLEAN: return DRWAV_ERROR; + #endif + #ifdef ENOTNAM + case ENOTNAM: return DRWAV_ERROR; + #endif + #ifdef ENAVAIL + case ENAVAIL: return DRWAV_ERROR; + #endif + #ifdef EISNAM + case EISNAM: return DRWAV_ERROR; + #endif + #ifdef EREMOTEIO + case EREMOTEIO: return DRWAV_IO_ERROR; + #endif + #ifdef EDQUOT + case EDQUOT: return DRWAV_NO_SPACE; + #endif + #ifdef ENOMEDIUM + case ENOMEDIUM: return DRWAV_DOES_NOT_EXIST; + #endif + #ifdef EMEDIUMTYPE + case EMEDIUMTYPE: return DRWAV_ERROR; + #endif + #ifdef ECANCELED + case ECANCELED: return DRWAV_CANCELLED; + #endif + #ifdef ENOKEY + case ENOKEY: return DRWAV_ERROR; + #endif + #ifdef EKEYEXPIRED + case EKEYEXPIRED: return DRWAV_ERROR; + #endif + #ifdef EKEYREVOKED + case EKEYREVOKED: return DRWAV_ERROR; + #endif + #ifdef EKEYREJECTED + case EKEYREJECTED: return DRWAV_ERROR; + #endif + #ifdef EOWNERDEAD + case EOWNERDEAD: return DRWAV_ERROR; + #endif + #ifdef ENOTRECOVERABLE + case ENOTRECOVERABLE: return DRWAV_ERROR; + #endif + #ifdef ERFKILL + case ERFKILL: return DRWAV_ERROR; + #endif + #ifdef EHWPOISON + case EHWPOISON: return DRWAV_ERROR; + #endif + default: return DRWAV_ERROR; + } +} +/* End Errno */ + +/* fopen */ +DRWAV_PRIVATE drwav_result drwav_fopen(FILE** ppFile, const char* pFilePath, const char* pOpenMode) +{ +#if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err; +#endif + + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRWAV_INVALID_ARGS; + } + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + err = fopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drwav_result_from_errno(err); + } +#else +#if defined(_WIN32) || defined(__APPLE__) + *ppFile = fopen(pFilePath, pOpenMode); +#else + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 && defined(_LARGEFILE64_SOURCE) + *ppFile = fopen64(pFilePath, pOpenMode); + #else + *ppFile = fopen(pFilePath, pOpenMode); + #endif +#endif + if (*ppFile == NULL) { + drwav_result result = drwav_result_from_errno(errno); + if (result == DRWAV_SUCCESS) { + result = DRWAV_ERROR; /* Just a safety check to make sure we never ever return success when pFile == NULL. */ + } + + return result; + } +#endif + + return DRWAV_SUCCESS; +} + +/* +_wfopen() isn't always available in all compilation environments. + + * Windows only. + * MSVC seems to support it universally as far back as VC6 from what I can tell (haven't checked further back). + * MinGW-64 (both 32- and 64-bit) seems to support it. + * MinGW wraps it in !defined(__STRICT_ANSI__). + * OpenWatcom wraps it in !defined(_NO_EXT_KEYS). + +This can be reviewed as compatibility issues arise. The preference is to use _wfopen_s() and _wfopen() as opposed to the wcsrtombs() +fallback, so if you notice your compiler not detecting this properly I'm happy to look at adding support. +*/ +#if defined(_WIN32) + #if defined(_MSC_VER) || defined(__MINGW64__) || (!defined(__STRICT_ANSI__) && !defined(_NO_EXT_KEYS)) + #define DRWAV_HAS_WFOPEN + #endif +#endif + +#ifndef DR_WAV_NO_WCHAR +DRWAV_PRIVATE drwav_result drwav_wfopen(FILE** ppFile, const wchar_t* pFilePath, const wchar_t* pOpenMode, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRWAV_INVALID_ARGS; + } + +#if defined(DRWAV_HAS_WFOPEN) + { + /* Use _wfopen() on Windows. */ + #if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err = _wfopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drwav_result_from_errno(err); + } + #else + *ppFile = _wfopen(pFilePath, pOpenMode); + if (*ppFile == NULL) { + return drwav_result_from_errno(errno); + } + #endif + (void)pAllocationCallbacks; + } +#else + /* + Use fopen() on anything other than Windows. Requires a conversion. This is annoying because + fopen() is locale specific. The only real way I can think of to do this is with wcsrtombs(). Note + that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for + maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler + error I'll look into improving compatibility. + */ + + /* + Some compilers don't support wchar_t or wcsrtombs() which we're using below. In this case we just + need to abort with an error. If you encounter a compiler lacking such support, add it to this list + and submit a bug report and it'll be added to the library upstream. + */ + #if defined(__DJGPP__) + { + /* Nothing to do here. This will fall through to the error check below. */ + } + #else + { + mbstate_t mbs; + size_t lenMB; + const wchar_t* pFilePathTemp = pFilePath; + char* pFilePathMB = NULL; + char pOpenModeMB[32] = {0}; + + /* Get the length first. */ + DRWAV_ZERO_OBJECT(&mbs); + lenMB = wcsrtombs(NULL, &pFilePathTemp, 0, &mbs); + if (lenMB == (size_t)-1) { + return drwav_result_from_errno(errno); + } + + pFilePathMB = (char*)drwav__malloc_from_callbacks(lenMB + 1, pAllocationCallbacks); + if (pFilePathMB == NULL) { + return DRWAV_OUT_OF_MEMORY; + } + + pFilePathTemp = pFilePath; + DRWAV_ZERO_OBJECT(&mbs); + wcsrtombs(pFilePathMB, &pFilePathTemp, lenMB + 1, &mbs); + + /* The open mode should always consist of ASCII characters so we should be able to do a trivial conversion. */ + { + size_t i = 0; + for (;;) { + if (pOpenMode[i] == 0) { + pOpenModeMB[i] = '\0'; + break; + } + + pOpenModeMB[i] = (char)pOpenMode[i]; + i += 1; + } + } + + *ppFile = fopen(pFilePathMB, pOpenModeMB); + + drwav__free_from_callbacks(pFilePathMB, pAllocationCallbacks); + } + #endif + + if (*ppFile == NULL) { + return DRWAV_ERROR; + } +#endif + + return DRWAV_SUCCESS; +} +#endif +/* End fopen */ + + +DRWAV_PRIVATE size_t drwav__on_read_stdio(void* pUserData, void* pBufferOut, size_t bytesToRead) +{ + return fread(pBufferOut, 1, bytesToRead, (FILE*)pUserData); +} + +DRWAV_PRIVATE size_t drwav__on_write_stdio(void* pUserData, const void* pData, size_t bytesToWrite) +{ + return fwrite(pData, 1, bytesToWrite, (FILE*)pUserData); +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_seek_stdio(void* pUserData, int offset, drwav_seek_origin origin) +{ + int whence = SEEK_SET; + if (origin == DRWAV_SEEK_CUR) { + whence = SEEK_CUR; + } else if (origin == DRWAV_SEEK_END) { + whence = SEEK_END; + } + + return fseek((FILE*)pUserData, offset, whence) == 0; +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_tell_stdio(void* pUserData, drwav_int64* pCursor) +{ + FILE* pFileStdio = (FILE*)pUserData; + drwav_int64 result; + + /* These were all validated at a higher level. */ + DRWAV_ASSERT(pFileStdio != NULL); + DRWAV_ASSERT(pCursor != NULL); + +#if defined(_WIN32) && !defined(NXDK) + #if defined(_MSC_VER) && _MSC_VER > 1200 + result = _ftelli64(pFileStdio); + #else + result = ftell(pFileStdio); + #endif +#else + result = ftell(pFileStdio); +#endif + + *pCursor = result; + + return DRWAV_TRUE; +} + +DRWAV_API drwav_bool32 drwav_init_file(drwav* pWav, const char* filename, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_file_ex(pWav, filename, NULL, NULL, 0, pAllocationCallbacks); +} + + +DRWAV_PRIVATE drwav_bool32 drwav_init_file__internal_FILE(drwav* pWav, FILE* pFile, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav_bool32 result; + + result = drwav_preinit(pWav, drwav__on_read_stdio, drwav__on_seek_stdio, drwav__on_tell_stdio, (void*)pFile, pAllocationCallbacks); + if (result != DRWAV_TRUE) { + fclose(pFile); + return result; + } + + result = drwav_init__internal(pWav, onChunk, pChunkUserData, flags); + if (result != DRWAV_TRUE) { + fclose(pFile); + return result; + } + + return DRWAV_TRUE; +} + +DRWAV_API drwav_bool32 drwav_init_file_ex(drwav* pWav, const char* filename, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + FILE* pFile; + if (drwav_fopen(&pFile, filename, "rb") != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + /* This takes ownership of the FILE* object. */ + return drwav_init_file__internal_FILE(pWav, pFile, onChunk, pChunkUserData, flags, pAllocationCallbacks); +} + +#ifndef DR_WAV_NO_WCHAR +DRWAV_API drwav_bool32 drwav_init_file_w(drwav* pWav, const wchar_t* filename, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_file_ex_w(pWav, filename, NULL, NULL, 0, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_file_ex_w(drwav* pWav, const wchar_t* filename, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + FILE* pFile; + if (drwav_wfopen(&pFile, filename, L"rb", pAllocationCallbacks) != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + /* This takes ownership of the FILE* object. */ + return drwav_init_file__internal_FILE(pWav, pFile, onChunk, pChunkUserData, flags, pAllocationCallbacks); +} +#endif + +DRWAV_API drwav_bool32 drwav_init_file_with_metadata(drwav* pWav, const char* filename, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + FILE* pFile; + if (drwav_fopen(&pFile, filename, "rb") != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + /* This takes ownership of the FILE* object. */ + return drwav_init_file__internal_FILE(pWav, pFile, NULL, NULL, flags | DRWAV_WITH_METADATA, pAllocationCallbacks); +} + +#ifndef DR_WAV_NO_WCHAR +DRWAV_API drwav_bool32 drwav_init_file_with_metadata_w(drwav* pWav, const wchar_t* filename, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + FILE* pFile; + if (drwav_wfopen(&pFile, filename, L"rb", pAllocationCallbacks) != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + /* This takes ownership of the FILE* object. */ + return drwav_init_file__internal_FILE(pWav, pFile, NULL, NULL, flags | DRWAV_WITH_METADATA, pAllocationCallbacks); +} +#endif + + +DRWAV_PRIVATE drwav_bool32 drwav_init_file_write__internal_FILE(drwav* pWav, FILE* pFile, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, drwav_bool32 isSequential, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav_bool32 result; + + result = drwav_preinit_write(pWav, pFormat, isSequential, drwav__on_write_stdio, drwav__on_seek_stdio, (void*)pFile, pAllocationCallbacks); + if (result != DRWAV_TRUE) { + fclose(pFile); + return result; + } + + result = drwav_init_write__internal(pWav, pFormat, totalSampleCount); + if (result != DRWAV_TRUE) { + fclose(pFile); + return result; + } + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE drwav_bool32 drwav_init_file_write__internal(drwav* pWav, const char* filename, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, drwav_bool32 isSequential, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + FILE* pFile; + if (drwav_fopen(&pFile, filename, "wb") != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + /* This takes ownership of the FILE* object. */ + return drwav_init_file_write__internal_FILE(pWav, pFile, pFormat, totalSampleCount, isSequential, pAllocationCallbacks); +} + +#ifndef DR_WAV_NO_WCHAR +DRWAV_PRIVATE drwav_bool32 drwav_init_file_write_w__internal(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, drwav_bool32 isSequential, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + FILE* pFile; + if (drwav_wfopen(&pFile, filename, L"wb", pAllocationCallbacks) != DRWAV_SUCCESS) { + return DRWAV_FALSE; + } + + /* This takes ownership of the FILE* object. */ + return drwav_init_file_write__internal_FILE(pWav, pFile, pFormat, totalSampleCount, isSequential, pAllocationCallbacks); +} +#endif + +DRWAV_API drwav_bool32 drwav_init_file_write(drwav* pWav, const char* filename, const drwav_data_format* pFormat, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_file_write__internal(pWav, filename, pFormat, 0, DRWAV_FALSE, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_file_write_sequential(drwav* pWav, const char* filename, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_file_write__internal(pWav, filename, pFormat, totalSampleCount, DRWAV_TRUE, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_file_write_sequential_pcm_frames(drwav* pWav, const char* filename, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pFormat == NULL) { + return DRWAV_FALSE; + } + + return drwav_init_file_write_sequential(pWav, filename, pFormat, totalPCMFrameCount*pFormat->channels, pAllocationCallbacks); +} + +#ifndef DR_WAV_NO_WCHAR +DRWAV_API drwav_bool32 drwav_init_file_write_w(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_file_write_w__internal(pWav, filename, pFormat, 0, DRWAV_FALSE, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_file_write_sequential_w(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_file_write_w__internal(pWav, filename, pFormat, totalSampleCount, DRWAV_TRUE, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_file_write_sequential_pcm_frames_w(drwav* pWav, const wchar_t* filename, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pFormat == NULL) { + return DRWAV_FALSE; + } + + return drwav_init_file_write_sequential_w(pWav, filename, pFormat, totalPCMFrameCount*pFormat->channels, pAllocationCallbacks); +} +#endif +#endif /* DR_WAV_NO_STDIO */ + + +DRWAV_PRIVATE size_t drwav__on_read_memory(void* pUserData, void* pBufferOut, size_t bytesToRead) +{ + drwav* pWav = (drwav*)pUserData; + size_t bytesRemaining; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->memoryStream.dataSize >= pWav->memoryStream.currentReadPos); + + bytesRemaining = pWav->memoryStream.dataSize - pWav->memoryStream.currentReadPos; + if (bytesToRead > bytesRemaining) { + bytesToRead = bytesRemaining; + } + + if (bytesToRead > 0) { + DRWAV_COPY_MEMORY(pBufferOut, pWav->memoryStream.data + pWav->memoryStream.currentReadPos, bytesToRead); + pWav->memoryStream.currentReadPos += bytesToRead; + } + + return bytesToRead; +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_seek_memory(void* pUserData, int offset, drwav_seek_origin origin) +{ + drwav* pWav = (drwav*)pUserData; + drwav_int64 newCursor; + + DRWAV_ASSERT(pWav != NULL); + + if (origin == DRWAV_SEEK_SET) { + newCursor = 0; + } else if (origin == DRWAV_SEEK_CUR) { + newCursor = (drwav_int64)pWav->memoryStream.currentReadPos; + } else if (origin == DRWAV_SEEK_END) { + newCursor = (drwav_int64)pWav->memoryStream.dataSize; + } else { + DRWAV_ASSERT(!"Invalid seek origin"); + return DRWAV_FALSE; + } + + newCursor += offset; + + if (newCursor < 0) { + return DRWAV_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > pWav->memoryStream.dataSize) { + return DRWAV_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + pWav->memoryStream.currentReadPos = (size_t)newCursor; + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE size_t drwav__on_write_memory(void* pUserData, const void* pDataIn, size_t bytesToWrite) +{ + drwav* pWav = (drwav*)pUserData; + size_t bytesRemaining; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pWav->memoryStreamWrite.dataCapacity >= pWav->memoryStreamWrite.currentWritePos); + + bytesRemaining = pWav->memoryStreamWrite.dataCapacity - pWav->memoryStreamWrite.currentWritePos; + if (bytesRemaining < bytesToWrite) { + /* Need to reallocate. */ + void* pNewData; + size_t newDataCapacity = (pWav->memoryStreamWrite.dataCapacity == 0) ? 256 : pWav->memoryStreamWrite.dataCapacity * 2; + + /* If doubling wasn't enough, just make it the minimum required size to write the data. */ + if ((newDataCapacity - pWav->memoryStreamWrite.currentWritePos) < bytesToWrite) { + newDataCapacity = pWav->memoryStreamWrite.currentWritePos + bytesToWrite; + } + + pNewData = drwav__realloc_from_callbacks(*pWav->memoryStreamWrite.ppData, newDataCapacity, pWav->memoryStreamWrite.dataCapacity, &pWav->allocationCallbacks); + if (pNewData == NULL) { + return 0; + } + + *pWav->memoryStreamWrite.ppData = pNewData; + pWav->memoryStreamWrite.dataCapacity = newDataCapacity; + } + + DRWAV_COPY_MEMORY(((drwav_uint8*)(*pWav->memoryStreamWrite.ppData)) + pWav->memoryStreamWrite.currentWritePos, pDataIn, bytesToWrite); + + pWav->memoryStreamWrite.currentWritePos += bytesToWrite; + if (pWav->memoryStreamWrite.dataSize < pWav->memoryStreamWrite.currentWritePos) { + pWav->memoryStreamWrite.dataSize = pWav->memoryStreamWrite.currentWritePos; + } + + *pWav->memoryStreamWrite.pDataSize = pWav->memoryStreamWrite.dataSize; + + return bytesToWrite; +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_seek_memory_write(void* pUserData, int offset, drwav_seek_origin origin) +{ + drwav* pWav = (drwav*)pUserData; + drwav_int64 newCursor; + + DRWAV_ASSERT(pWav != NULL); + + if (origin == DRWAV_SEEK_SET) { + newCursor = 0; + } else if (origin == DRWAV_SEEK_CUR) { + newCursor = (drwav_int64)pWav->memoryStreamWrite.currentWritePos; + } else if (origin == DRWAV_SEEK_END) { + newCursor = (drwav_int64)pWav->memoryStreamWrite.dataSize; + } else { + DRWAV_ASSERT(!"Invalid seek origin"); + return DRWAV_FALSE; + } + + newCursor += offset; + + if (newCursor < 0) { + return DRWAV_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > pWav->memoryStreamWrite.dataSize) { + return DRWAV_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + pWav->memoryStreamWrite.currentWritePos = (size_t)newCursor; + + return DRWAV_TRUE; +} + +DRWAV_PRIVATE drwav_bool32 drwav__on_tell_memory(void* pUserData, drwav_int64* pCursor) +{ + drwav* pWav = (drwav*)pUserData; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(pCursor != NULL); + + *pCursor = (drwav_int64)pWav->memoryStream.currentReadPos; + return DRWAV_TRUE; +} + +DRWAV_API drwav_bool32 drwav_init_memory(drwav* pWav, const void* data, size_t dataSize, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_memory_ex(pWav, data, dataSize, NULL, NULL, 0, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_memory_ex(drwav* pWav, const void* data, size_t dataSize, drwav_chunk_proc onChunk, void* pChunkUserData, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (data == NULL || dataSize == 0) { + return DRWAV_FALSE; + } + + if (!drwav_preinit(pWav, drwav__on_read_memory, drwav__on_seek_memory, drwav__on_tell_memory, pWav, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + pWav->memoryStream.data = (const drwav_uint8*)data; + pWav->memoryStream.dataSize = dataSize; + pWav->memoryStream.currentReadPos = 0; + + return drwav_init__internal(pWav, onChunk, pChunkUserData, flags); +} + +DRWAV_API drwav_bool32 drwav_init_memory_with_metadata(drwav* pWav, const void* data, size_t dataSize, drwav_uint32 flags, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (data == NULL || dataSize == 0) { + return DRWAV_FALSE; + } + + if (!drwav_preinit(pWav, drwav__on_read_memory, drwav__on_seek_memory, drwav__on_tell_memory, pWav, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + pWav->memoryStream.data = (const drwav_uint8*)data; + pWav->memoryStream.dataSize = dataSize; + pWav->memoryStream.currentReadPos = 0; + + return drwav_init__internal(pWav, NULL, NULL, flags | DRWAV_WITH_METADATA); +} + + +DRWAV_PRIVATE drwav_bool32 drwav_init_memory_write__internal(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, drwav_bool32 isSequential, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (ppData == NULL || pDataSize == NULL) { + return DRWAV_FALSE; + } + + *ppData = NULL; /* Important because we're using realloc()! */ + *pDataSize = 0; + + if (!drwav_preinit_write(pWav, pFormat, isSequential, drwav__on_write_memory, drwav__on_seek_memory_write, pWav, pAllocationCallbacks)) { + return DRWAV_FALSE; + } + + pWav->memoryStreamWrite.ppData = ppData; + pWav->memoryStreamWrite.pDataSize = pDataSize; + pWav->memoryStreamWrite.dataSize = 0; + pWav->memoryStreamWrite.dataCapacity = 0; + pWav->memoryStreamWrite.currentWritePos = 0; + + return drwav_init_write__internal(pWav, pFormat, totalSampleCount); +} + +DRWAV_API drwav_bool32 drwav_init_memory_write(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_memory_write__internal(pWav, ppData, pDataSize, pFormat, 0, DRWAV_FALSE, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_memory_write_sequential(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, drwav_uint64 totalSampleCount, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + return drwav_init_memory_write__internal(pWav, ppData, pDataSize, pFormat, totalSampleCount, DRWAV_TRUE, pAllocationCallbacks); +} + +DRWAV_API drwav_bool32 drwav_init_memory_write_sequential_pcm_frames(drwav* pWav, void** ppData, size_t* pDataSize, const drwav_data_format* pFormat, drwav_uint64 totalPCMFrameCount, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pFormat == NULL) { + return DRWAV_FALSE; + } + + return drwav_init_memory_write_sequential(pWav, ppData, pDataSize, pFormat, totalPCMFrameCount*pFormat->channels, pAllocationCallbacks); +} + +DRWAV_PRIVATE drwav_uint32 drwav_write_padding(drwav* pWav) +{ + /* Do not adjust pWav->dataChunkDataSize - this should not include the padding. */ + drwav_uint32 paddingSize = drwav_calculate_padding_size(pWav->container, pWav->dataChunkDataSize); + + if (paddingSize > 0) { + drwav_uint64 paddingData = 0; + drwav__write(pWav, &paddingData, paddingSize); /* Byte order does not matter for this. */ + } + + return paddingSize; +} + +DRWAV_PRIVATE void drwav_write_chunk_sizes(drwav* pWav) +{ + /* + When using sequential mode, these will have been filled in at initialization time. We only need + to do this when using non-sequential mode. + */ + if (pWav->onSeek != NULL && !pWav->isSequentialWrite) { + if (pWav->container == drwav_container_riff) { + /* The "RIFF" chunk size. */ + if (pWav->onSeek(pWav->pUserData, 4, DRWAV_SEEK_SET)) { + drwav_uint32 riffChunkSize = drwav__riff_chunk_size_riff(pWav->dataChunkDataSize, pWav->pMetadata, pWav->metadataCount); + drwav__write_u32ne_to_le(pWav, riffChunkSize); + } + + /* The "data" chunk size. */ + if (pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos - 4, DRWAV_SEEK_SET)) { + drwav_uint32 dataChunkSize = drwav__data_chunk_size_riff(pWav->dataChunkDataSize); + drwav__write_u32ne_to_le(pWav, dataChunkSize); + } + } else if (pWav->container == drwav_container_w64) { + /* The "RIFF" chunk size. */ + if (pWav->onSeek(pWav->pUserData, 16, DRWAV_SEEK_SET)) { + drwav_uint64 riffChunkSize = drwav__riff_chunk_size_w64(pWav->dataChunkDataSize); + drwav__write_u64ne_to_le(pWav, riffChunkSize); + } + + /* The "data" chunk size. */ + if (pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos - 8, DRWAV_SEEK_SET)) { + drwav_uint64 dataChunkSize = drwav__data_chunk_size_w64(pWav->dataChunkDataSize); + drwav__write_u64ne_to_le(pWav, dataChunkSize); + } + } else if (pWav->container == drwav_container_rf64) { + /* We only need to update the ds64 chunk. The "RIFF" and "data" chunks always have their sizes set to 0xFFFFFFFF for RF64. */ + int ds64BodyPos = 12 + 8; + + /* The "RIFF" chunk size. */ + if (pWav->onSeek(pWav->pUserData, ds64BodyPos + 0, DRWAV_SEEK_SET)) { + drwav_uint64 riffChunkSize = drwav__riff_chunk_size_rf64(pWav->dataChunkDataSize, pWav->pMetadata, pWav->metadataCount); + drwav__write_u64ne_to_le(pWav, riffChunkSize); + } + + /* The "data" chunk size. */ + if (pWav->onSeek(pWav->pUserData, ds64BodyPos + 8, DRWAV_SEEK_SET)) { + drwav_uint64 dataChunkSize = drwav__data_chunk_size_rf64(pWav->dataChunkDataSize); + drwav__write_u64ne_to_le(pWav, dataChunkSize); + } + } + } +} + +DRWAV_API drwav_result drwav_uninit(drwav* pWav) +{ + drwav_result result = DRWAV_SUCCESS; + + if (pWav == NULL) { + return DRWAV_INVALID_ARGS; + } + + if (pWav->onWrite != NULL) { + if (pWav->isSequentialWrite) { + /* + Padding will not have been written in `drwav_write_*()` in sequential mode so we'll want to + do it explicitly here. + */ + drwav_write_padding(pWav); + + /* Validation for sequential mode. */ + if (pWav->dataChunkDataSize != pWav->dataChunkDataSizeTargetWrite) { + result = DRWAV_INVALID_FILE; + } + } + } else { + drwav_free(pWav->pMetadata, &pWav->allocationCallbacks); + } + +#ifndef DR_WAV_NO_STDIO + /* + If we opened the file with drwav_open_file() we will want to close the file handle. We can know whether or not drwav_open_file() + was used by looking at the onRead and onSeek callbacks. + */ + if (pWav->onRead == drwav__on_read_stdio || pWav->onWrite == drwav__on_write_stdio) { + fclose((FILE*)pWav->pUserData); + } +#endif + + return result; +} + + + +DRWAV_API size_t drwav_read_raw(drwav* pWav, size_t bytesToRead, void* pBufferOut) +{ + size_t bytesRead; + drwav_uint32 bytesPerFrame; + + if (pWav == NULL || bytesToRead == 0) { + return 0; /* Invalid args. */ + } + + if (bytesToRead > pWav->bytesRemaining) { + bytesToRead = (size_t)pWav->bytesRemaining; + } + + if (bytesToRead == 0) { + return 0; /* At end. */ + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; /* Could not determine the bytes per frame. */ + } + + if (pBufferOut != NULL) { + bytesRead = pWav->onRead(pWav->pUserData, pBufferOut, bytesToRead); + } else { + /* We need to seek. If we fail, we need to read-and-discard to make sure we get a good byte count. */ + bytesRead = 0; + while (bytesRead < bytesToRead) { + size_t bytesToSeek = (bytesToRead - bytesRead); + if (bytesToSeek > 0x7FFFFFFF) { + bytesToSeek = 0x7FFFFFFF; + } + + if (pWav->onSeek(pWav->pUserData, (int)bytesToSeek, DRWAV_SEEK_CUR) == DRWAV_FALSE) { + break; + } + + bytesRead += bytesToSeek; + } + + /* When we get here we may need to read-and-discard some data. */ + while (bytesRead < bytesToRead) { + drwav_uint8 buffer[4096]; + size_t bytesSeeked; + size_t bytesToSeek = (bytesToRead - bytesRead); + if (bytesToSeek > sizeof(buffer)) { + bytesToSeek = sizeof(buffer); + } + + bytesSeeked = pWav->onRead(pWav->pUserData, buffer, bytesToSeek); + bytesRead += bytesSeeked; + + if (bytesSeeked < bytesToSeek) { + break; /* Reached the end. */ + } + } + } + + pWav->readCursorInPCMFrames += bytesRead / bytesPerFrame; + + pWav->bytesRemaining -= bytesRead; + return bytesRead; +} + + + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_le(drwav* pWav, drwav_uint64 framesToRead, void* pBufferOut) +{ + drwav_uint32 bytesPerFrame; + drwav_uint64 bytesToRead; /* Intentionally uint64 instead of size_t so we can do a check that we're not reading too much on 32-bit builds. */ + drwav_uint64 framesRemainingInFile; + + if (pWav == NULL || framesToRead == 0) { + return 0; + } + + /* Cannot use this function for compressed formats. */ + if (drwav__is_compressed_format_tag(pWav->translatedFormatTag)) { + return 0; + } + + framesRemainingInFile = pWav->totalPCMFrameCount - pWav->readCursorInPCMFrames; + if (framesToRead > framesRemainingInFile) { + framesToRead = framesRemainingInFile; + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + /* Don't try to read more samples than can potentially fit in the output buffer. */ + bytesToRead = framesToRead * bytesPerFrame; + if (bytesToRead > DRWAV_SIZE_MAX) { + bytesToRead = (DRWAV_SIZE_MAX / bytesPerFrame) * bytesPerFrame; /* Round the number of bytes to read to a clean frame boundary. */ + } + + /* + Doing an explicit check here just to make it clear that we don't want to be attempt to read anything if there's no bytes to read. There + *could* be a time where it evaluates to 0 due to overflowing. + */ + if (bytesToRead == 0) { + return 0; + } + + return drwav_read_raw(pWav, (size_t)bytesToRead, pBufferOut) / bytesPerFrame; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_be(drwav* pWav, drwav_uint64 framesToRead, void* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_le(pWav, framesToRead, pBufferOut); + + if (pBufferOut != NULL) { + drwav_uint32 bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; /* Could not get the bytes per frame which means bytes per sample cannot be determined and we don't know how to byte swap. */ + } + + drwav__bswap_samples(pBufferOut, framesRead*pWav->channels, bytesPerFrame/pWav->channels); + } + + return framesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames(drwav* pWav, drwav_uint64 framesToRead, void* pBufferOut) +{ + drwav_uint64 framesRead = 0; + + if (drwav_is_container_be(pWav->container)) { + /* + Special case for AIFF. AIFF is a big-endian encoded format, but it supports a format that is + PCM in little-endian encoding. In this case, we fall through this branch and treate it as + little-endian. + */ + if (pWav->container != drwav_container_aiff || pWav->aiff.isLE == DRWAV_FALSE) { + if (drwav__is_little_endian()) { + framesRead = drwav_read_pcm_frames_be(pWav, framesToRead, pBufferOut); + } else { + framesRead = drwav_read_pcm_frames_le(pWav, framesToRead, pBufferOut); + } + + goto post_process; + } + } + + /* Getting here means the data should be considered little-endian. */ + if (drwav__is_little_endian()) { + framesRead = drwav_read_pcm_frames_le(pWav, framesToRead, pBufferOut); + } else { + framesRead = drwav_read_pcm_frames_be(pWav, framesToRead, pBufferOut); + } + + /* + Here is where we check if we need to do a signed/unsigned conversion for AIFF. The reason we need to do this + is because dr_wav always assumes an 8-bit sample is unsigned, whereas AIFF can have signed 8-bit formats. + */ + post_process: + { + if (pWav->container == drwav_container_aiff && pWav->bitsPerSample == 8 && pWav->aiff.isUnsigned == DRWAV_FALSE) { + if (pBufferOut != NULL) { + drwav_uint64 iSample; + + for (iSample = 0; iSample < framesRead * pWav->channels; iSample += 1) { + ((drwav_uint8*)pBufferOut)[iSample] += 128; + } + } + } + } + + return framesRead; +} + + + +DRWAV_PRIVATE drwav_bool32 drwav_seek_to_first_pcm_frame(drwav* pWav) +{ + if (pWav->onWrite != NULL) { + return DRWAV_FALSE; /* No seeking in write mode. */ + } + + if (!pWav->onSeek(pWav->pUserData, (int)pWav->dataChunkDataPos, DRWAV_SEEK_SET)) { + return DRWAV_FALSE; + } + + if (drwav__is_compressed_format_tag(pWav->translatedFormatTag)) { + /* Cached data needs to be cleared for compressed formats. */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM) { + DRWAV_ZERO_OBJECT(&pWav->msadpcm); + } else if (pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + DRWAV_ZERO_OBJECT(&pWav->ima); + } else { + DRWAV_ASSERT(DRWAV_FALSE); /* If this assertion is triggered it means I've implemented a new compressed format but forgot to add a branch for it here. */ + } + } + + pWav->readCursorInPCMFrames = 0; + pWav->bytesRemaining = pWav->dataChunkDataSize; + + return DRWAV_TRUE; +} + +DRWAV_API drwav_bool32 drwav_seek_to_pcm_frame(drwav* pWav, drwav_uint64 targetFrameIndex) +{ + /* Seeking should be compatible with wave files > 2GB. */ + + if (pWav == NULL || pWav->onSeek == NULL) { + return DRWAV_FALSE; + } + + /* No seeking in write mode. */ + if (pWav->onWrite != NULL) { + return DRWAV_FALSE; + } + + /* If there are no samples, just return DRWAV_TRUE without doing anything. */ + if (pWav->totalPCMFrameCount == 0) { + return DRWAV_TRUE; + } + + /* Make sure the sample is clamped. */ + if (targetFrameIndex > pWav->totalPCMFrameCount) { + targetFrameIndex = pWav->totalPCMFrameCount; + } + + /* + For compressed formats we just use a slow generic seek. If we are seeking forward we just seek forward. If we are going backwards we need + to seek back to the start. + */ + if (drwav__is_compressed_format_tag(pWav->translatedFormatTag)) { + /* TODO: This can be optimized. */ + + /* + If we're seeking forward it's simple - just keep reading samples until we hit the sample we're requesting. If we're seeking backwards, + we first need to seek back to the start and then just do the same thing as a forward seek. + */ + if (targetFrameIndex < pWav->readCursorInPCMFrames) { + if (!drwav_seek_to_first_pcm_frame(pWav)) { + return DRWAV_FALSE; + } + } + + if (targetFrameIndex > pWav->readCursorInPCMFrames) { + drwav_uint64 offsetInFrames = targetFrameIndex - pWav->readCursorInPCMFrames; + + drwav_int16 devnull[2048]; + while (offsetInFrames > 0) { + drwav_uint64 framesRead = 0; + drwav_uint64 framesToRead = offsetInFrames; + if (framesToRead > drwav_countof(devnull)/pWav->channels) { + framesToRead = drwav_countof(devnull)/pWav->channels; + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM) { + framesRead = drwav_read_pcm_frames_s16__msadpcm(pWav, framesToRead, devnull); + } else if (pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + framesRead = drwav_read_pcm_frames_s16__ima(pWav, framesToRead, devnull); + } else { + DRWAV_ASSERT(DRWAV_FALSE); /* If this assertion is triggered it means I've implemented a new compressed format but forgot to add a branch for it here. */ + } + + if (framesRead != framesToRead) { + return DRWAV_FALSE; + } + + offsetInFrames -= framesRead; + } + } + } else { + drwav_uint64 totalSizeInBytes; + drwav_uint64 currentBytePos; + drwav_uint64 targetBytePos; + drwav_uint64 offset; + drwav_uint32 bytesPerFrame; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return DRWAV_FALSE; /* Not able to calculate offset. */ + } + + totalSizeInBytes = pWav->totalPCMFrameCount * bytesPerFrame; + /*DRWAV_ASSERT(totalSizeInBytes >= pWav->bytesRemaining);*/ + + currentBytePos = totalSizeInBytes - pWav->bytesRemaining; + targetBytePos = targetFrameIndex * bytesPerFrame; + + if (currentBytePos < targetBytePos) { + /* Offset forwards. */ + offset = (targetBytePos - currentBytePos); + } else { + /* Offset backwards. */ + if (!drwav_seek_to_first_pcm_frame(pWav)) { + return DRWAV_FALSE; + } + offset = targetBytePos; + } + + while (offset > 0) { + int offset32 = ((offset > INT_MAX) ? INT_MAX : (int)offset); + if (!pWav->onSeek(pWav->pUserData, offset32, DRWAV_SEEK_CUR)) { + return DRWAV_FALSE; + } + + pWav->readCursorInPCMFrames += offset32 / bytesPerFrame; + pWav->bytesRemaining -= offset32; + offset -= offset32; + } + } + + return DRWAV_TRUE; +} + +DRWAV_API drwav_result drwav_get_cursor_in_pcm_frames(drwav* pWav, drwav_uint64* pCursor) +{ + if (pCursor == NULL) { + return DRWAV_INVALID_ARGS; + } + + *pCursor = 0; /* Safety. */ + + if (pWav == NULL) { + return DRWAV_INVALID_ARGS; + } + + *pCursor = pWav->readCursorInPCMFrames; + + return DRWAV_SUCCESS; +} + +DRWAV_API drwav_result drwav_get_length_in_pcm_frames(drwav* pWav, drwav_uint64* pLength) +{ + if (pLength == NULL) { + return DRWAV_INVALID_ARGS; + } + + *pLength = 0; /* Safety. */ + + if (pWav == NULL) { + return DRWAV_INVALID_ARGS; + } + + *pLength = pWav->totalPCMFrameCount; + + return DRWAV_SUCCESS; +} + + +DRWAV_API size_t drwav_write_raw(drwav* pWav, size_t bytesToWrite, const void* pData) +{ + size_t bytesWritten; + + if (pWav == NULL || bytesToWrite == 0 || pData == NULL) { + return 0; + } + + bytesWritten = pWav->onWrite(pWav->pUserData, pData, bytesToWrite); + pWav->dataChunkDataSize += bytesWritten; + + if (!pWav->isSequentialWrite) { + drwav_uint32 padding; + + /* Padding. */ + padding = drwav_write_padding(pWav); + + /* Chunk sizes. */ + drwav_write_chunk_sizes(pWav); + + /* Now seek back to just before the padding in preparation for the next writes. */ + if (pWav->onSeek != NULL) { + pWav->onSeek(pWav->pUserData, -(int)padding, DRWAV_SEEK_END); /* Safe cast. */ + } + } + + return bytesWritten; +} + +DRWAV_API drwav_uint64 drwav_write_pcm_frames_le(drwav* pWav, drwav_uint64 framesToWrite, const void* pData) +{ + drwav_uint64 bytesToWrite; + drwav_uint64 bytesWritten; + const drwav_uint8* pRunningData; + + if (pWav == NULL || framesToWrite == 0 || pData == NULL) { + return 0; + } + + bytesToWrite = ((framesToWrite * pWav->channels * pWav->bitsPerSample) / 8); + if (bytesToWrite > DRWAV_SIZE_MAX) { + return 0; + } + + bytesWritten = 0; + pRunningData = (const drwav_uint8*)pData; + + while (bytesToWrite > 0) { + size_t bytesJustWritten; + drwav_uint64 bytesToWriteThisIteration; + + bytesToWriteThisIteration = bytesToWrite; + DRWAV_ASSERT(bytesToWriteThisIteration <= DRWAV_SIZE_MAX); /* <-- This is checked above. */ + + bytesJustWritten = drwav_write_raw(pWav, (size_t)bytesToWriteThisIteration, pRunningData); + if (bytesJustWritten == 0) { + break; + } + + bytesToWrite -= bytesJustWritten; + bytesWritten += bytesJustWritten; + pRunningData += bytesJustWritten; + } + + return (bytesWritten * 8) / pWav->bitsPerSample / pWav->channels; +} + +DRWAV_API drwav_uint64 drwav_write_pcm_frames_be(drwav* pWav, drwav_uint64 framesToWrite, const void* pData) +{ + drwav_uint64 bytesToWrite; + drwav_uint64 bytesWritten; + drwav_uint32 bytesPerSample; + const drwav_uint8* pRunningData; + + if (pWav == NULL || framesToWrite == 0 || pData == NULL) { + return 0; + } + + bytesToWrite = ((framesToWrite * pWav->channels * pWav->bitsPerSample) / 8); + if (bytesToWrite > DRWAV_SIZE_MAX) { + return 0; + } + + bytesWritten = 0; + pRunningData = (const drwav_uint8*)pData; + + bytesPerSample = drwav_get_bytes_per_pcm_frame(pWav) / pWav->channels; + if (bytesPerSample == 0) { + return 0; /* Cannot determine bytes per sample, or bytes per sample is less than one byte. */ + } + + while (bytesToWrite > 0) { + drwav_uint8 temp[4096]; + drwav_uint32 sampleCount; + size_t bytesJustWritten; + drwav_uint64 bytesToWriteThisIteration; + + bytesToWriteThisIteration = bytesToWrite; + DRWAV_ASSERT(bytesToWriteThisIteration <= DRWAV_SIZE_MAX); /* <-- This is checked above. */ + + /* + WAV files are always little-endian. We need to byte swap on big-endian architectures. Since our input buffer is read-only we need + to use an intermediary buffer for the conversion. + */ + sampleCount = sizeof(temp)/bytesPerSample; + + if (bytesToWriteThisIteration > ((drwav_uint64)sampleCount)*bytesPerSample) { + bytesToWriteThisIteration = ((drwav_uint64)sampleCount)*bytesPerSample; + } + + DRWAV_COPY_MEMORY(temp, pRunningData, (size_t)bytesToWriteThisIteration); + drwav__bswap_samples(temp, sampleCount, bytesPerSample); + + bytesJustWritten = drwav_write_raw(pWav, (size_t)bytesToWriteThisIteration, temp); + if (bytesJustWritten == 0) { + break; + } + + bytesToWrite -= bytesJustWritten; + bytesWritten += bytesJustWritten; + pRunningData += bytesJustWritten; + } + + return (bytesWritten * 8) / pWav->bitsPerSample / pWav->channels; +} + +DRWAV_API drwav_uint64 drwav_write_pcm_frames(drwav* pWav, drwav_uint64 framesToWrite, const void* pData) +{ + if (drwav__is_little_endian()) { + return drwav_write_pcm_frames_le(pWav, framesToWrite, pData); + } else { + return drwav_write_pcm_frames_be(pWav, framesToWrite, pData); + } +} + + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__msadpcm(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 totalFramesRead = 0; + + static const drwav_int32 adaptationTable[] = { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + static const drwav_int32 coeff1Table[] = { 256, 512, 0, 192, 240, 460, 392 }; + static const drwav_int32 coeff2Table[] = { 0, -256, 0, 64, 0, -208, -232 }; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(framesToRead > 0); + + /* TODO: Lots of room for optimization here. */ + + while (pWav->readCursorInPCMFrames < pWav->totalPCMFrameCount) { + DRWAV_ASSERT(framesToRead > 0); /* This loop iteration will never get hit with framesToRead == 0 because it's asserted at the top, and we check for 0 inside the loop just below. */ + + /* If there are no cached frames we need to load a new block. */ + if (pWav->msadpcm.cachedFrameCount == 0 && pWav->msadpcm.bytesRemainingInBlock == 0) { + if (pWav->channels == 1) { + /* Mono. */ + drwav_uint8 header[7]; + if (pWav->onRead(pWav->pUserData, header, sizeof(header)) != sizeof(header)) { + return totalFramesRead; + } + pWav->msadpcm.bytesRemainingInBlock = pWav->fmt.blockAlign - sizeof(header); + + pWav->msadpcm.predictor[0] = header[0]; + pWav->msadpcm.delta[0] = drwav_bytes_to_s16(header + 1); + pWav->msadpcm.prevFrames[0][1] = (drwav_int32)drwav_bytes_to_s16(header + 3); + pWav->msadpcm.prevFrames[0][0] = (drwav_int32)drwav_bytes_to_s16(header + 5); + pWav->msadpcm.cachedFrames[2] = pWav->msadpcm.prevFrames[0][0]; + pWav->msadpcm.cachedFrames[3] = pWav->msadpcm.prevFrames[0][1]; + pWav->msadpcm.cachedFrameCount = 2; + + /* The predictor is used as an index into coeff1Table so we'll need to validate to ensure it never overflows. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Invalid file. */ + } + } else { + /* Stereo. */ + drwav_uint8 header[14]; + if (pWav->onRead(pWav->pUserData, header, sizeof(header)) != sizeof(header)) { + return totalFramesRead; + } + pWav->msadpcm.bytesRemainingInBlock = pWav->fmt.blockAlign - sizeof(header); + + pWav->msadpcm.predictor[0] = header[0]; + pWav->msadpcm.predictor[1] = header[1]; + pWav->msadpcm.delta[0] = drwav_bytes_to_s16(header + 2); + pWav->msadpcm.delta[1] = drwav_bytes_to_s16(header + 4); + pWav->msadpcm.prevFrames[0][1] = (drwav_int32)drwav_bytes_to_s16(header + 6); + pWav->msadpcm.prevFrames[1][1] = (drwav_int32)drwav_bytes_to_s16(header + 8); + pWav->msadpcm.prevFrames[0][0] = (drwav_int32)drwav_bytes_to_s16(header + 10); + pWav->msadpcm.prevFrames[1][0] = (drwav_int32)drwav_bytes_to_s16(header + 12); + + pWav->msadpcm.cachedFrames[0] = pWav->msadpcm.prevFrames[0][0]; + pWav->msadpcm.cachedFrames[1] = pWav->msadpcm.prevFrames[1][0]; + pWav->msadpcm.cachedFrames[2] = pWav->msadpcm.prevFrames[0][1]; + pWav->msadpcm.cachedFrames[3] = pWav->msadpcm.prevFrames[1][1]; + pWav->msadpcm.cachedFrameCount = 2; + + /* The predictor is used as an index into coeff1Table so we'll need to validate to ensure it never overflows. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table) || + pWav->msadpcm.predictor[1] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[1] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Invalid file. */ + } + } + } + + /* Output anything that's cached. */ + while (framesToRead > 0 && pWav->msadpcm.cachedFrameCount > 0 && pWav->readCursorInPCMFrames < pWav->totalPCMFrameCount) { + if (pBufferOut != NULL) { + drwav_uint32 iSample = 0; + for (iSample = 0; iSample < pWav->channels; iSample += 1) { + pBufferOut[iSample] = (drwav_int16)pWav->msadpcm.cachedFrames[(drwav_countof(pWav->msadpcm.cachedFrames) - (pWav->msadpcm.cachedFrameCount*pWav->channels)) + iSample]; + } + + pBufferOut += pWav->channels; + } + + framesToRead -= 1; + totalFramesRead += 1; + pWav->readCursorInPCMFrames += 1; + pWav->msadpcm.cachedFrameCount -= 1; + } + + if (framesToRead == 0) { + break; + } + + + /* + If there's nothing left in the cache, just go ahead and load more. If there's nothing left to load in the current block we just continue to the next + loop iteration which will trigger the loading of a new block. + */ + if (pWav->msadpcm.cachedFrameCount == 0) { + if (pWav->msadpcm.bytesRemainingInBlock == 0) { + continue; + } else { + drwav_uint8 nibbles; + drwav_int32 nibble0; + drwav_int32 nibble1; + + if (pWav->onRead(pWav->pUserData, &nibbles, 1) != 1) { + return totalFramesRead; + } + pWav->msadpcm.bytesRemainingInBlock -= 1; + + /* TODO: Optimize away these if statements. */ + nibble0 = ((nibbles & 0xF0) >> 4); if ((nibbles & 0x80)) { nibble0 |= 0xFFFFFFF0UL; } + nibble1 = ((nibbles & 0x0F) >> 0); if ((nibbles & 0x08)) { nibble1 |= 0xFFFFFFF0UL; } + + if (pWav->channels == 1) { + /* Mono. */ + drwav_int32 newSample0; + drwav_int32 newSample1; + + /* The predictor is read from the file and then indexed into a table. Check that it's in bounds. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table)) { + return totalFramesRead; + } + + newSample0 = ((pWav->msadpcm.prevFrames[0][1] * coeff1Table[pWav->msadpcm.predictor[0]]) + (pWav->msadpcm.prevFrames[0][0] * coeff2Table[pWav->msadpcm.predictor[0]])) >> 8; + newSample0 += nibble0 * pWav->msadpcm.delta[0]; + newSample0 = drwav_clamp(newSample0, -32768, 32767); + + pWav->msadpcm.delta[0] = (drwav_int32)drwav_clamp(((drwav_int64)adaptationTable[((nibbles & 0xF0) >> 4)] * pWav->msadpcm.delta[0]) >> 8, 16, 0x7FFFFFFF); + + pWav->msadpcm.prevFrames[0][0] = pWav->msadpcm.prevFrames[0][1]; + pWav->msadpcm.prevFrames[0][1] = newSample0; + + + newSample1 = ((pWav->msadpcm.prevFrames[0][1] * coeff1Table[pWav->msadpcm.predictor[0]]) + (pWav->msadpcm.prevFrames[0][0] * coeff2Table[pWav->msadpcm.predictor[0]])) >> 8; + newSample1 += nibble1 * pWav->msadpcm.delta[0]; + newSample1 = drwav_clamp(newSample1, -32768, 32767); + + pWav->msadpcm.delta[0] = (drwav_int32)drwav_clamp(((drwav_int64)adaptationTable[((nibbles & 0x0F) >> 0)] * pWav->msadpcm.delta[0]) >> 8, 16, 0x7FFFFFFF); + + pWav->msadpcm.prevFrames[0][0] = pWav->msadpcm.prevFrames[0][1]; + pWav->msadpcm.prevFrames[0][1] = newSample1; + + pWav->msadpcm.cachedFrames[2] = newSample0; + pWav->msadpcm.cachedFrames[3] = newSample1; + pWav->msadpcm.cachedFrameCount = 2; + } else { + /* Stereo. */ + drwav_int32 newSample0; + drwav_int32 newSample1; + + /* Left. */ + if (pWav->msadpcm.predictor[0] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[0] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Out of bounds. Invalid file. */ + } + + newSample0 = ((pWav->msadpcm.prevFrames[0][1] * coeff1Table[pWav->msadpcm.predictor[0]]) + (pWav->msadpcm.prevFrames[0][0] * coeff2Table[pWav->msadpcm.predictor[0]])) >> 8; + newSample0 += nibble0 * pWav->msadpcm.delta[0]; + newSample0 = drwav_clamp(newSample0, -32768, 32767); + + pWav->msadpcm.delta[0] = (drwav_int32)drwav_clamp(((drwav_int64)adaptationTable[((nibbles & 0xF0) >> 4)] * pWav->msadpcm.delta[0]) >> 8, 16, 0x7FFFFFFF); + + pWav->msadpcm.prevFrames[0][0] = pWav->msadpcm.prevFrames[0][1]; + pWav->msadpcm.prevFrames[0][1] = newSample0; + + + /* Right. */ + if (pWav->msadpcm.predictor[1] >= drwav_countof(coeff1Table) || pWav->msadpcm.predictor[1] >= drwav_countof(coeff2Table)) { + return totalFramesRead; /* Out of bounds. Invalid file. */ + } + + newSample1 = ((pWav->msadpcm.prevFrames[1][1] * coeff1Table[pWav->msadpcm.predictor[1]]) + (pWav->msadpcm.prevFrames[1][0] * coeff2Table[pWav->msadpcm.predictor[1]])) >> 8; + newSample1 += nibble1 * pWav->msadpcm.delta[1]; + newSample1 = drwav_clamp(newSample1, -32768, 32767); + + pWav->msadpcm.delta[1] = (drwav_int32)drwav_clamp(((drwav_int64)adaptationTable[((nibbles & 0x0F) >> 0)] * pWav->msadpcm.delta[1]) >> 8, 16, 0x7FFFFFFF); + + pWav->msadpcm.prevFrames[1][0] = pWav->msadpcm.prevFrames[1][1]; + pWav->msadpcm.prevFrames[1][1] = newSample1; + + pWav->msadpcm.cachedFrames[2] = newSample0; + pWav->msadpcm.cachedFrames[3] = newSample1; + pWav->msadpcm.cachedFrameCount = 1; + } + } + } + } + + return totalFramesRead; +} + + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ima(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 totalFramesRead = 0; + drwav_uint32 iChannel; + + static const drwav_int32 indexTable[16] = { + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + static const drwav_int32 stepTable[89] = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, + 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, + 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, + 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, + 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 + }; + + DRWAV_ASSERT(pWav != NULL); + DRWAV_ASSERT(framesToRead > 0); + + /* TODO: Lots of room for optimization here. */ + + while (pWav->readCursorInPCMFrames < pWav->totalPCMFrameCount) { + DRWAV_ASSERT(framesToRead > 0); /* This loop iteration will never get hit with framesToRead == 0 because it's asserted at the top, and we check for 0 inside the loop just below. */ + + /* If there are no cached samples we need to load a new block. */ + if (pWav->ima.cachedFrameCount == 0 && pWav->ima.bytesRemainingInBlock == 0) { + if (pWav->channels == 1) { + /* Mono. */ + drwav_uint8 header[4]; + if (pWav->onRead(pWav->pUserData, header, sizeof(header)) != sizeof(header)) { + return totalFramesRead; + } + pWav->ima.bytesRemainingInBlock = pWav->fmt.blockAlign - sizeof(header); + + if (header[2] >= drwav_countof(stepTable)) { + pWav->onSeek(pWav->pUserData, pWav->ima.bytesRemainingInBlock, DRWAV_SEEK_CUR); + pWav->ima.bytesRemainingInBlock = 0; + return totalFramesRead; /* Invalid data. */ + } + + pWav->ima.predictor[0] = (drwav_int16)drwav_bytes_to_u16(header + 0); + pWav->ima.stepIndex[0] = drwav_clamp(header[2], 0, (drwav_int32)drwav_countof(stepTable)-1); /* Clamp not necessary because we checked above, but adding here to silence a static analysis warning. */ + pWav->ima.cachedFrames[drwav_countof(pWav->ima.cachedFrames) - 1] = pWav->ima.predictor[0]; + pWav->ima.cachedFrameCount = 1; + } else { + /* Stereo. */ + drwav_uint8 header[8]; + if (pWav->onRead(pWav->pUserData, header, sizeof(header)) != sizeof(header)) { + return totalFramesRead; + } + pWav->ima.bytesRemainingInBlock = pWav->fmt.blockAlign - sizeof(header); + + if (header[2] >= drwav_countof(stepTable) || header[6] >= drwav_countof(stepTable)) { + pWav->onSeek(pWav->pUserData, pWav->ima.bytesRemainingInBlock, DRWAV_SEEK_CUR); + pWav->ima.bytesRemainingInBlock = 0; + return totalFramesRead; /* Invalid data. */ + } + + pWav->ima.predictor[0] = drwav_bytes_to_s16(header + 0); + pWav->ima.stepIndex[0] = drwav_clamp(header[2], 0, (drwav_int32)drwav_countof(stepTable)-1); /* Clamp not necessary because we checked above, but adding here to silence a static analysis warning. */ + pWav->ima.predictor[1] = drwav_bytes_to_s16(header + 4); + pWav->ima.stepIndex[1] = drwav_clamp(header[6], 0, (drwav_int32)drwav_countof(stepTable)-1); /* Clamp not necessary because we checked above, but adding here to silence a static analysis warning. */ + + pWav->ima.cachedFrames[drwav_countof(pWav->ima.cachedFrames) - 2] = pWav->ima.predictor[0]; + pWav->ima.cachedFrames[drwav_countof(pWav->ima.cachedFrames) - 1] = pWav->ima.predictor[1]; + pWav->ima.cachedFrameCount = 1; + } + } + + /* Output anything that's cached. */ + while (framesToRead > 0 && pWav->ima.cachedFrameCount > 0 && pWav->readCursorInPCMFrames < pWav->totalPCMFrameCount) { + if (pBufferOut != NULL) { + drwav_uint32 iSample; + for (iSample = 0; iSample < pWav->channels; iSample += 1) { + pBufferOut[iSample] = (drwav_int16)pWav->ima.cachedFrames[(drwav_countof(pWav->ima.cachedFrames) - (pWav->ima.cachedFrameCount*pWav->channels)) + iSample]; + } + pBufferOut += pWav->channels; + } + + framesToRead -= 1; + totalFramesRead += 1; + pWav->readCursorInPCMFrames += 1; + pWav->ima.cachedFrameCount -= 1; + } + + if (framesToRead == 0) { + break; + } + + /* + If there's nothing left in the cache, just go ahead and load more. If there's nothing left to load in the current block we just continue to the next + loop iteration which will trigger the loading of a new block. + */ + if (pWav->ima.cachedFrameCount == 0) { + if (pWav->ima.bytesRemainingInBlock == 0) { + continue; + } else { + /* + From what I can tell with stereo streams, it looks like every 4 bytes (8 samples) is for one channel. So it goes 4 bytes for the + left channel, 4 bytes for the right channel. + */ + pWav->ima.cachedFrameCount = 8; + for (iChannel = 0; iChannel < pWav->channels; ++iChannel) { + drwav_uint32 iByte; + drwav_uint8 nibbles[4]; + if (pWav->onRead(pWav->pUserData, &nibbles, 4) != 4) { + pWav->ima.cachedFrameCount = 0; + return totalFramesRead; + } + pWav->ima.bytesRemainingInBlock -= 4; + + for (iByte = 0; iByte < 4; ++iByte) { + drwav_uint8 nibble0 = ((nibbles[iByte] & 0x0F) >> 0); + drwav_uint8 nibble1 = ((nibbles[iByte] & 0xF0) >> 4); + + drwav_int32 step = stepTable[pWav->ima.stepIndex[iChannel]]; + drwav_int32 predictor = pWav->ima.predictor[iChannel]; + + drwav_int32 diff = step >> 3; + if (nibble0 & 1) diff += step >> 2; + if (nibble0 & 2) diff += step >> 1; + if (nibble0 & 4) diff += step; + if (nibble0 & 8) diff = -diff; + + predictor = drwav_clamp(predictor + diff, -32768, 32767); + pWav->ima.predictor[iChannel] = predictor; + pWav->ima.stepIndex[iChannel] = drwav_clamp(pWav->ima.stepIndex[iChannel] + indexTable[nibble0], 0, (drwav_int32)drwav_countof(stepTable)-1); + pWav->ima.cachedFrames[(drwav_countof(pWav->ima.cachedFrames) - (pWav->ima.cachedFrameCount*pWav->channels)) + (iByte*2+0)*pWav->channels + iChannel] = predictor; + + + step = stepTable[pWav->ima.stepIndex[iChannel]]; + predictor = pWav->ima.predictor[iChannel]; + + diff = step >> 3; + if (nibble1 & 1) diff += step >> 2; + if (nibble1 & 2) diff += step >> 1; + if (nibble1 & 4) diff += step; + if (nibble1 & 8) diff = -diff; + + predictor = drwav_clamp(predictor + diff, -32768, 32767); + pWav->ima.predictor[iChannel] = predictor; + pWav->ima.stepIndex[iChannel] = drwav_clamp(pWav->ima.stepIndex[iChannel] + indexTable[nibble1], 0, (drwav_int32)drwav_countof(stepTable)-1); + pWav->ima.cachedFrames[(drwav_countof(pWav->ima.cachedFrames) - (pWav->ima.cachedFrameCount*pWav->channels)) + (iByte*2+1)*pWav->channels + iChannel] = predictor; + } + } + } + } + } + + return totalFramesRead; +} + + +#ifndef DR_WAV_NO_CONVERSION_API +static const unsigned short g_drwavAlawTable[256] = { + 0xEA80, 0xEB80, 0xE880, 0xE980, 0xEE80, 0xEF80, 0xEC80, 0xED80, 0xE280, 0xE380, 0xE080, 0xE180, 0xE680, 0xE780, 0xE480, 0xE580, + 0xF540, 0xF5C0, 0xF440, 0xF4C0, 0xF740, 0xF7C0, 0xF640, 0xF6C0, 0xF140, 0xF1C0, 0xF040, 0xF0C0, 0xF340, 0xF3C0, 0xF240, 0xF2C0, + 0xAA00, 0xAE00, 0xA200, 0xA600, 0xBA00, 0xBE00, 0xB200, 0xB600, 0x8A00, 0x8E00, 0x8200, 0x8600, 0x9A00, 0x9E00, 0x9200, 0x9600, + 0xD500, 0xD700, 0xD100, 0xD300, 0xDD00, 0xDF00, 0xD900, 0xDB00, 0xC500, 0xC700, 0xC100, 0xC300, 0xCD00, 0xCF00, 0xC900, 0xCB00, + 0xFEA8, 0xFEB8, 0xFE88, 0xFE98, 0xFEE8, 0xFEF8, 0xFEC8, 0xFED8, 0xFE28, 0xFE38, 0xFE08, 0xFE18, 0xFE68, 0xFE78, 0xFE48, 0xFE58, + 0xFFA8, 0xFFB8, 0xFF88, 0xFF98, 0xFFE8, 0xFFF8, 0xFFC8, 0xFFD8, 0xFF28, 0xFF38, 0xFF08, 0xFF18, 0xFF68, 0xFF78, 0xFF48, 0xFF58, + 0xFAA0, 0xFAE0, 0xFA20, 0xFA60, 0xFBA0, 0xFBE0, 0xFB20, 0xFB60, 0xF8A0, 0xF8E0, 0xF820, 0xF860, 0xF9A0, 0xF9E0, 0xF920, 0xF960, + 0xFD50, 0xFD70, 0xFD10, 0xFD30, 0xFDD0, 0xFDF0, 0xFD90, 0xFDB0, 0xFC50, 0xFC70, 0xFC10, 0xFC30, 0xFCD0, 0xFCF0, 0xFC90, 0xFCB0, + 0x1580, 0x1480, 0x1780, 0x1680, 0x1180, 0x1080, 0x1380, 0x1280, 0x1D80, 0x1C80, 0x1F80, 0x1E80, 0x1980, 0x1880, 0x1B80, 0x1A80, + 0x0AC0, 0x0A40, 0x0BC0, 0x0B40, 0x08C0, 0x0840, 0x09C0, 0x0940, 0x0EC0, 0x0E40, 0x0FC0, 0x0F40, 0x0CC0, 0x0C40, 0x0DC0, 0x0D40, + 0x5600, 0x5200, 0x5E00, 0x5A00, 0x4600, 0x4200, 0x4E00, 0x4A00, 0x7600, 0x7200, 0x7E00, 0x7A00, 0x6600, 0x6200, 0x6E00, 0x6A00, + 0x2B00, 0x2900, 0x2F00, 0x2D00, 0x2300, 0x2100, 0x2700, 0x2500, 0x3B00, 0x3900, 0x3F00, 0x3D00, 0x3300, 0x3100, 0x3700, 0x3500, + 0x0158, 0x0148, 0x0178, 0x0168, 0x0118, 0x0108, 0x0138, 0x0128, 0x01D8, 0x01C8, 0x01F8, 0x01E8, 0x0198, 0x0188, 0x01B8, 0x01A8, + 0x0058, 0x0048, 0x0078, 0x0068, 0x0018, 0x0008, 0x0038, 0x0028, 0x00D8, 0x00C8, 0x00F8, 0x00E8, 0x0098, 0x0088, 0x00B8, 0x00A8, + 0x0560, 0x0520, 0x05E0, 0x05A0, 0x0460, 0x0420, 0x04E0, 0x04A0, 0x0760, 0x0720, 0x07E0, 0x07A0, 0x0660, 0x0620, 0x06E0, 0x06A0, + 0x02B0, 0x0290, 0x02F0, 0x02D0, 0x0230, 0x0210, 0x0270, 0x0250, 0x03B0, 0x0390, 0x03F0, 0x03D0, 0x0330, 0x0310, 0x0370, 0x0350 +}; + +static const unsigned short g_drwavMulawTable[256] = { + 0x8284, 0x8684, 0x8A84, 0x8E84, 0x9284, 0x9684, 0x9A84, 0x9E84, 0xA284, 0xA684, 0xAA84, 0xAE84, 0xB284, 0xB684, 0xBA84, 0xBE84, + 0xC184, 0xC384, 0xC584, 0xC784, 0xC984, 0xCB84, 0xCD84, 0xCF84, 0xD184, 0xD384, 0xD584, 0xD784, 0xD984, 0xDB84, 0xDD84, 0xDF84, + 0xE104, 0xE204, 0xE304, 0xE404, 0xE504, 0xE604, 0xE704, 0xE804, 0xE904, 0xEA04, 0xEB04, 0xEC04, 0xED04, 0xEE04, 0xEF04, 0xF004, + 0xF0C4, 0xF144, 0xF1C4, 0xF244, 0xF2C4, 0xF344, 0xF3C4, 0xF444, 0xF4C4, 0xF544, 0xF5C4, 0xF644, 0xF6C4, 0xF744, 0xF7C4, 0xF844, + 0xF8A4, 0xF8E4, 0xF924, 0xF964, 0xF9A4, 0xF9E4, 0xFA24, 0xFA64, 0xFAA4, 0xFAE4, 0xFB24, 0xFB64, 0xFBA4, 0xFBE4, 0xFC24, 0xFC64, + 0xFC94, 0xFCB4, 0xFCD4, 0xFCF4, 0xFD14, 0xFD34, 0xFD54, 0xFD74, 0xFD94, 0xFDB4, 0xFDD4, 0xFDF4, 0xFE14, 0xFE34, 0xFE54, 0xFE74, + 0xFE8C, 0xFE9C, 0xFEAC, 0xFEBC, 0xFECC, 0xFEDC, 0xFEEC, 0xFEFC, 0xFF0C, 0xFF1C, 0xFF2C, 0xFF3C, 0xFF4C, 0xFF5C, 0xFF6C, 0xFF7C, + 0xFF88, 0xFF90, 0xFF98, 0xFFA0, 0xFFA8, 0xFFB0, 0xFFB8, 0xFFC0, 0xFFC8, 0xFFD0, 0xFFD8, 0xFFE0, 0xFFE8, 0xFFF0, 0xFFF8, 0x0000, + 0x7D7C, 0x797C, 0x757C, 0x717C, 0x6D7C, 0x697C, 0x657C, 0x617C, 0x5D7C, 0x597C, 0x557C, 0x517C, 0x4D7C, 0x497C, 0x457C, 0x417C, + 0x3E7C, 0x3C7C, 0x3A7C, 0x387C, 0x367C, 0x347C, 0x327C, 0x307C, 0x2E7C, 0x2C7C, 0x2A7C, 0x287C, 0x267C, 0x247C, 0x227C, 0x207C, + 0x1EFC, 0x1DFC, 0x1CFC, 0x1BFC, 0x1AFC, 0x19FC, 0x18FC, 0x17FC, 0x16FC, 0x15FC, 0x14FC, 0x13FC, 0x12FC, 0x11FC, 0x10FC, 0x0FFC, + 0x0F3C, 0x0EBC, 0x0E3C, 0x0DBC, 0x0D3C, 0x0CBC, 0x0C3C, 0x0BBC, 0x0B3C, 0x0ABC, 0x0A3C, 0x09BC, 0x093C, 0x08BC, 0x083C, 0x07BC, + 0x075C, 0x071C, 0x06DC, 0x069C, 0x065C, 0x061C, 0x05DC, 0x059C, 0x055C, 0x051C, 0x04DC, 0x049C, 0x045C, 0x041C, 0x03DC, 0x039C, + 0x036C, 0x034C, 0x032C, 0x030C, 0x02EC, 0x02CC, 0x02AC, 0x028C, 0x026C, 0x024C, 0x022C, 0x020C, 0x01EC, 0x01CC, 0x01AC, 0x018C, + 0x0174, 0x0164, 0x0154, 0x0144, 0x0134, 0x0124, 0x0114, 0x0104, 0x00F4, 0x00E4, 0x00D4, 0x00C4, 0x00B4, 0x00A4, 0x0094, 0x0084, + 0x0078, 0x0070, 0x0068, 0x0060, 0x0058, 0x0050, 0x0048, 0x0040, 0x0038, 0x0030, 0x0028, 0x0020, 0x0018, 0x0010, 0x0008, 0x0000 +}; + +static DRWAV_INLINE drwav_int16 drwav__alaw_to_s16(drwav_uint8 sampleIn) +{ + return (short)g_drwavAlawTable[sampleIn]; +} + +static DRWAV_INLINE drwav_int16 drwav__mulaw_to_s16(drwav_uint8 sampleIn) +{ + return (short)g_drwavMulawTable[sampleIn]; +} + + + +DRWAV_PRIVATE void drwav__pcm_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t totalSampleCount, unsigned int bytesPerSample) +{ + size_t i; + + /* Special case for 8-bit sample data because it's treated as unsigned. */ + if (bytesPerSample == 1) { + drwav_u8_to_s16(pOut, pIn, totalSampleCount); + return; + } + + + /* Slightly more optimal implementation for common formats. */ + if (bytesPerSample == 2) { + for (i = 0; i < totalSampleCount; ++i) { + *pOut++ = ((const drwav_int16*)pIn)[i]; + } + return; + } + if (bytesPerSample == 3) { + drwav_s24_to_s16(pOut, pIn, totalSampleCount); + return; + } + if (bytesPerSample == 4) { + drwav_s32_to_s16(pOut, (const drwav_int32*)pIn, totalSampleCount); + return; + } + + + /* Anything more than 64 bits per sample is not supported. */ + if (bytesPerSample > 8) { + DRWAV_ZERO_MEMORY(pOut, totalSampleCount * sizeof(*pOut)); + return; + } + + + /* Generic, slow converter. */ + for (i = 0; i < totalSampleCount; ++i) { + drwav_uint64 sample = 0; + unsigned int shift = (8 - bytesPerSample) * 8; + + unsigned int j; + for (j = 0; j < bytesPerSample; j += 1) { + DRWAV_ASSERT(j < 8); + sample |= (drwav_uint64)(pIn[j]) << shift; + shift += 8; + } + + if (!drwav__is_little_endian()) { + sample = drwav__bswap64(sample); + } + + pIn += j; + *pOut++ = (drwav_int16)((drwav_int64)sample >> 48); + } +} + +DRWAV_PRIVATE void drwav__ieee_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t totalSampleCount, unsigned int bytesPerSample) +{ + if (bytesPerSample == 4) { + drwav_f32_to_s16(pOut, (const float*)pIn, totalSampleCount); + return; + } else if (bytesPerSample == 8) { + drwav_f64_to_s16(pOut, (const double*)pIn, totalSampleCount); + return; + } else { + /* Only supporting 32- and 64-bit float. Output silence in all other cases. Contributions welcome for 16-bit float. */ + DRWAV_ZERO_MEMORY(pOut, totalSampleCount * sizeof(*pOut)); + return; + } +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__pcm(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + /* Fast path. */ + if ((pWav->translatedFormatTag == DR_WAVE_FORMAT_PCM && pWav->bitsPerSample == 16) || pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, pBufferOut); + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav__pcm_to_s16(pBufferOut, sampleData, (size_t)samplesRead, bytesPerSample); + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__ieee(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + if (pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, NULL); + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav__ieee_to_s16(pBufferOut, sampleData, (size_t)samplesRead, bytesPerSample); /* Safe cast. */ + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__alaw(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + if (pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, NULL); + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav_alaw_to_s16(pBufferOut, sampleData, (size_t)samplesRead); + + /* + For some reason libsndfile seems to be returning samples of the opposite sign for a-law, but only + with AIFF files. For WAV files it seems to be the same as dr_wav. This is resulting in dr_wav's + automated tests failing. I'm not sure which is correct, but will assume dr_wav. If we're enforcing + libsndfile compatibility we'll swap the signs here. + */ + #ifdef DR_WAV_LIBSNDFILE_COMPAT + { + if (pWav->container == drwav_container_aiff) { + drwav_uint64 iSample; + for (iSample = 0; iSample < samplesRead; iSample += 1) { + pBufferOut[iSample] = -pBufferOut[iSample]; + } + } + } + #endif + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s16__mulaw(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + if (pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, NULL); + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav_mulaw_to_s16(pBufferOut, sampleData, (size_t)samplesRead); + + /* + Just like with alaw, for some reason the signs between libsndfile and dr_wav are opposite. We just need to + swap the sign if we're compiling with libsndfile compatiblity so our automated tests don't fail. + */ + #ifdef DR_WAV_LIBSNDFILE_COMPAT + { + if (pWav->container == drwav_container_aiff) { + drwav_uint64 iSample; + for (iSample = 0; iSample < samplesRead; iSample += 1) { + pBufferOut[iSample] = -pBufferOut[iSample]; + } + } + } + #endif + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s16(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + if (pWav == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, NULL); + } + + /* Don't try to read more samples than can potentially fit in the output buffer. */ + if (framesToRead * pWav->channels * sizeof(drwav_int16) > DRWAV_SIZE_MAX) { + framesToRead = DRWAV_SIZE_MAX / sizeof(drwav_int16) / pWav->channels; + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_PCM) { + return drwav_read_pcm_frames_s16__pcm(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_IEEE_FLOAT) { + return drwav_read_pcm_frames_s16__ieee(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ALAW) { + return drwav_read_pcm_frames_s16__alaw(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_MULAW) { + return drwav_read_pcm_frames_s16__mulaw(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM) { + return drwav_read_pcm_frames_s16__msadpcm(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + return drwav_read_pcm_frames_s16__ima(pWav, framesToRead, pBufferOut); + } + + return 0; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s16le(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_s16(pWav, framesToRead, pBufferOut); + if (pBufferOut != NULL && drwav__is_little_endian() == DRWAV_FALSE) { + drwav__bswap_samples_s16(pBufferOut, framesRead*pWav->channels); + } + + return framesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s16be(drwav* pWav, drwav_uint64 framesToRead, drwav_int16* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_s16(pWav, framesToRead, pBufferOut); + if (pBufferOut != NULL && drwav__is_little_endian() == DRWAV_TRUE) { + drwav__bswap_samples_s16(pBufferOut, framesRead*pWav->channels); + } + + return framesRead; +} + + +DRWAV_API void drwav_u8_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + int r; + size_t i; + for (i = 0; i < sampleCount; ++i) { + int x = pIn[i]; + r = x << 8; + r = r - 32768; + pOut[i] = (short)r; + } +} + +DRWAV_API void drwav_s24_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + int r; + size_t i; + for (i = 0; i < sampleCount; ++i) { + int x = ((int)(((unsigned int)(((const drwav_uint8*)pIn)[i*3+0]) << 8) | ((unsigned int)(((const drwav_uint8*)pIn)[i*3+1]) << 16) | ((unsigned int)(((const drwav_uint8*)pIn)[i*3+2])) << 24)) >> 8; + r = x >> 8; + pOut[i] = (short)r; + } +} + +DRWAV_API void drwav_s32_to_s16(drwav_int16* pOut, const drwav_int32* pIn, size_t sampleCount) +{ + int r; + size_t i; + for (i = 0; i < sampleCount; ++i) { + int x = pIn[i]; + r = x >> 16; + pOut[i] = (short)r; + } +} + +DRWAV_API void drwav_f32_to_s16(drwav_int16* pOut, const float* pIn, size_t sampleCount) +{ + size_t i; + for (i = 0; i < sampleCount; ++i) { + float x = pIn[i]; + if (x != x) { + pOut[i] = 0; /* NaN */ + } else if (x <= -1) { + pOut[i] = (-32767 - 1); + } else if (x >= 1) { + pOut[i] = 32767; + } else { + pOut[i] = (drwav_int16)(x * 32768.0f); + } + } +} + +DRWAV_API void drwav_f64_to_s16(drwav_int16* pOut, const double* pIn, size_t sampleCount) +{ + size_t i; + for (i = 0; i < sampleCount; ++i) { + double x = pIn[i]; + if (x != x) { + pOut[i] = 0; /* NaN */ + } else if (x <= -1) { + pOut[i] = (-32767 - 1); + } else if (x >= 1) { + pOut[i] = 32767; + } else { + pOut[i] = (drwav_int16)(x * 32768.0); + } + } +} + +DRWAV_API void drwav_alaw_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + for (i = 0; i < sampleCount; ++i) { + pOut[i] = drwav__alaw_to_s16(pIn[i]); + } +} + +DRWAV_API void drwav_mulaw_to_s16(drwav_int16* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + for (i = 0; i < sampleCount; ++i) { + pOut[i] = drwav__mulaw_to_s16(pIn[i]); + } +} + + +DRWAV_PRIVATE void drwav__pcm_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount, unsigned int bytesPerSample) +{ + unsigned int i; + + /* Special case for 8-bit sample data because it's treated as unsigned. */ + if (bytesPerSample == 1) { + drwav_u8_to_f32(pOut, pIn, sampleCount); + return; + } + + /* Slightly more optimal implementation for common formats. */ + if (bytesPerSample == 2) { + drwav_s16_to_f32(pOut, (const drwav_int16*)pIn, sampleCount); + return; + } + if (bytesPerSample == 3) { + drwav_s24_to_f32(pOut, pIn, sampleCount); + return; + } + if (bytesPerSample == 4) { + drwav_s32_to_f32(pOut, (const drwav_int32*)pIn, sampleCount); + return; + } + + + /* Anything more than 64 bits per sample is not supported. */ + if (bytesPerSample > 8) { + DRWAV_ZERO_MEMORY(pOut, sampleCount * sizeof(*pOut)); + return; + } + + + /* Generic, slow converter. */ + for (i = 0; i < sampleCount; ++i) { + drwav_uint64 sample = 0; + unsigned int shift = (8 - bytesPerSample) * 8; + + unsigned int j; + for (j = 0; j < bytesPerSample; j += 1) { + DRWAV_ASSERT(j < 8); + sample |= (drwav_uint64)(pIn[j]) << shift; + shift += 8; + } + + if (!drwav__is_little_endian()) { + sample = drwav__bswap64(sample); + } + + pIn += j; + *pOut++ = (float)((drwav_int64)sample / 9223372036854775807.0); + } +} + +DRWAV_PRIVATE void drwav__ieee_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount, unsigned int bytesPerSample) +{ + if (bytesPerSample == 4) { + unsigned int i; + for (i = 0; i < sampleCount; ++i) { + *pOut++ = ((const float*)pIn)[i]; + } + return; + } else if (bytesPerSample == 8) { + drwav_f64_to_f32(pOut, (const double*)pIn, sampleCount); + return; + } else { + /* Only supporting 32- and 64-bit float. Output silence in all other cases. Contributions welcome for 16-bit float. */ + DRWAV_ZERO_MEMORY(pOut, sampleCount * sizeof(*pOut)); + return; + } +} + + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_f32__pcm(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav__pcm_to_f32(pBufferOut, sampleData, (size_t)samplesRead, bytesPerSample); + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_f32__msadpcm_ima(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + /* + We're just going to borrow the implementation from the drwav_read_s16() since ADPCM is a little bit more complicated than other formats and I don't + want to duplicate that code. + */ + drwav_uint64 totalFramesRead; + drwav_int16 samples16[2048]; + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, drwav_countof(samples16)/pWav->channels); + drwav_uint64 framesRead = drwav_read_pcm_frames_s16(pWav, framesToReadThisIteration, samples16); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + drwav_s16_to_f32(pBufferOut, samples16, (size_t)(framesRead*pWav->channels)); /* <-- Safe cast because we're clamping to 2048. */ + + pBufferOut += framesRead*pWav->channels; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_f32__ieee(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + /* Fast path. */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_IEEE_FLOAT && pWav->bitsPerSample == 32) { + return drwav_read_pcm_frames(pWav, framesToRead, pBufferOut); + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav__ieee_to_f32(pBufferOut, sampleData, (size_t)samplesRead, bytesPerSample); + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_f32__alaw(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav_alaw_to_f32(pBufferOut, sampleData, (size_t)samplesRead); + + #ifdef DR_WAV_LIBSNDFILE_COMPAT + { + if (pWav->container == drwav_container_aiff) { + drwav_uint64 iSample; + for (iSample = 0; iSample < samplesRead; iSample += 1) { + pBufferOut[iSample] = -pBufferOut[iSample]; + } + } + } + #endif + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_f32__mulaw(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav_mulaw_to_f32(pBufferOut, sampleData, (size_t)samplesRead); + + #ifdef DR_WAV_LIBSNDFILE_COMPAT + { + if (pWav->container == drwav_container_aiff) { + drwav_uint64 iSample; + for (iSample = 0; iSample < samplesRead; iSample += 1) { + pBufferOut[iSample] = -pBufferOut[iSample]; + } + } + } + #endif + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_f32(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + if (pWav == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, NULL); + } + + /* Don't try to read more samples than can potentially fit in the output buffer. */ + if (framesToRead * pWav->channels * sizeof(float) > DRWAV_SIZE_MAX) { + framesToRead = DRWAV_SIZE_MAX / sizeof(float) / pWav->channels; + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_PCM) { + return drwav_read_pcm_frames_f32__pcm(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM || pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + return drwav_read_pcm_frames_f32__msadpcm_ima(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_IEEE_FLOAT) { + return drwav_read_pcm_frames_f32__ieee(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ALAW) { + return drwav_read_pcm_frames_f32__alaw(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_MULAW) { + return drwav_read_pcm_frames_f32__mulaw(pWav, framesToRead, pBufferOut); + } + + return 0; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_f32le(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_f32(pWav, framesToRead, pBufferOut); + if (pBufferOut != NULL && drwav__is_little_endian() == DRWAV_FALSE) { + drwav__bswap_samples_f32(pBufferOut, framesRead*pWav->channels); + } + + return framesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_f32be(drwav* pWav, drwav_uint64 framesToRead, float* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_f32(pWav, framesToRead, pBufferOut); + if (pBufferOut != NULL && drwav__is_little_endian() == DRWAV_TRUE) { + drwav__bswap_samples_f32(pBufferOut, framesRead*pWav->channels); + } + + return framesRead; +} + + +DRWAV_API void drwav_u8_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + +#ifdef DR_WAV_LIBSNDFILE_COMPAT + /* + It appears libsndfile uses slightly different logic for the u8 -> f32 conversion to dr_wav, which in my opinion is incorrect. It appears + libsndfile performs the conversion something like "f32 = (u8 / 256) * 2 - 1", however I think it should be "f32 = (u8 / 255) * 2 - 1" (note + the divisor of 256 vs 255). I use libsndfile as a benchmark for testing, so I'm therefore leaving this block here just for my automated + correctness testing. This is disabled by default. + */ + for (i = 0; i < sampleCount; ++i) { + *pOut++ = (pIn[i] / 256.0f) * 2 - 1; + } +#else + for (i = 0; i < sampleCount; ++i) { + float x = pIn[i]; + x = x * 0.00784313725490196078f; /* 0..255 to 0..2 */ + x = x - 1; /* 0..2 to -1..1 */ + + *pOut++ = x; + } +#endif +} + +DRWAV_API void drwav_s16_to_f32(float* pOut, const drwav_int16* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = pIn[i] * 0.000030517578125f; + } +} + +DRWAV_API void drwav_s24_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + double x; + drwav_uint32 a = ((drwav_uint32)(pIn[i*3+0]) << 8); + drwav_uint32 b = ((drwav_uint32)(pIn[i*3+1]) << 16); + drwav_uint32 c = ((drwav_uint32)(pIn[i*3+2]) << 24); + + x = (double)((drwav_int32)(a | b | c) >> 8); + *pOut++ = (float)(x * 0.00000011920928955078125); + } +} + +DRWAV_API void drwav_s32_to_f32(float* pOut, const drwav_int32* pIn, size_t sampleCount) +{ + size_t i; + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = (float)(pIn[i] / 2147483648.0); + } +} + +DRWAV_API void drwav_f64_to_f32(float* pOut, const double* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = (float)pIn[i]; + } +} + +DRWAV_API void drwav_alaw_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = drwav__alaw_to_s16(pIn[i]) / 32768.0f; + } +} + +DRWAV_API void drwav_mulaw_to_f32(float* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = drwav__mulaw_to_s16(pIn[i]) / 32768.0f; + } +} + + + +DRWAV_PRIVATE void drwav__pcm_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t totalSampleCount, unsigned int bytesPerSample) +{ + unsigned int i; + + /* Special case for 8-bit sample data because it's treated as unsigned. */ + if (bytesPerSample == 1) { + drwav_u8_to_s32(pOut, pIn, totalSampleCount); + return; + } + + /* Slightly more optimal implementation for common formats. */ + if (bytesPerSample == 2) { + drwav_s16_to_s32(pOut, (const drwav_int16*)pIn, totalSampleCount); + return; + } + if (bytesPerSample == 3) { + drwav_s24_to_s32(pOut, pIn, totalSampleCount); + return; + } + if (bytesPerSample == 4) { + for (i = 0; i < totalSampleCount; ++i) { + *pOut++ = ((const drwav_int32*)pIn)[i]; + } + return; + } + + + /* Anything more than 64 bits per sample is not supported. */ + if (bytesPerSample > 8) { + DRWAV_ZERO_MEMORY(pOut, totalSampleCount * sizeof(*pOut)); + return; + } + + + /* Generic, slow converter. */ + for (i = 0; i < totalSampleCount; ++i) { + drwav_uint64 sample = 0; + unsigned int shift = (8 - bytesPerSample) * 8; + + unsigned int j; + for (j = 0; j < bytesPerSample; j += 1) { + DRWAV_ASSERT(j < 8); + sample |= (drwav_uint64)(pIn[j]) << shift; + shift += 8; + } + + if (!drwav__is_little_endian()) { + sample = drwav__bswap64(sample); + } + + pIn += j; + *pOut++ = (drwav_int32)((drwav_int64)sample >> 32); + } +} + +DRWAV_PRIVATE void drwav__ieee_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t totalSampleCount, unsigned int bytesPerSample) +{ + if (bytesPerSample == 4) { + drwav_f32_to_s32(pOut, (const float*)pIn, totalSampleCount); + return; + } else if (bytesPerSample == 8) { + drwav_f64_to_s32(pOut, (const double*)pIn, totalSampleCount); + return; + } else { + /* Only supporting 32- and 64-bit float. Output silence in all other cases. Contributions welcome for 16-bit float. */ + DRWAV_ZERO_MEMORY(pOut, totalSampleCount * sizeof(*pOut)); + return; + } +} + + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s32__pcm(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + /* Fast path. */ + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_PCM && pWav->bitsPerSample == 32) { + return drwav_read_pcm_frames(pWav, framesToRead, pBufferOut); + } + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav__pcm_to_s32(pBufferOut, sampleData, (size_t)samplesRead, bytesPerSample); + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s32__msadpcm_ima(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + /* + We're just going to borrow the implementation from the drwav_read_s16() since ADPCM is a little bit more complicated than other formats and I don't + want to duplicate that code. + */ + drwav_uint64 totalFramesRead = 0; + drwav_int16 samples16[2048]; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, drwav_countof(samples16)/pWav->channels); + drwav_uint64 framesRead = drwav_read_pcm_frames_s16(pWav, framesToReadThisIteration, samples16); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + drwav_s16_to_s32(pBufferOut, samples16, (size_t)(framesRead*pWav->channels)); /* <-- Safe cast because we're clamping to 2048. */ + + pBufferOut += framesRead*pWav->channels; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s32__ieee(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav__ieee_to_s32(pBufferOut, sampleData, (size_t)samplesRead, bytesPerSample); + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s32__alaw(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav_alaw_to_s32(pBufferOut, sampleData, (size_t)samplesRead); + + #ifdef DR_WAV_LIBSNDFILE_COMPAT + { + if (pWav->container == drwav_container_aiff) { + drwav_uint64 iSample; + for (iSample = 0; iSample < samplesRead; iSample += 1) { + pBufferOut[iSample] = -pBufferOut[iSample]; + } + } + } + #endif + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_PRIVATE drwav_uint64 drwav_read_pcm_frames_s32__mulaw(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + drwav_uint64 totalFramesRead; + drwav_uint8 sampleData[4096] = {0}; + drwav_uint32 bytesPerFrame; + drwav_uint32 bytesPerSample; + drwav_uint64 samplesRead; + + bytesPerFrame = drwav_get_bytes_per_pcm_frame(pWav); + if (bytesPerFrame == 0) { + return 0; + } + + bytesPerSample = bytesPerFrame / pWav->channels; + if (bytesPerSample == 0 || (bytesPerFrame % pWav->channels) != 0) { + return 0; /* Only byte-aligned formats are supported. */ + } + + totalFramesRead = 0; + + while (framesToRead > 0) { + drwav_uint64 framesToReadThisIteration = drwav_min(framesToRead, sizeof(sampleData)/bytesPerFrame); + drwav_uint64 framesRead = drwav_read_pcm_frames(pWav, framesToReadThisIteration, sampleData); + if (framesRead == 0) { + break; + } + + DRWAV_ASSERT(framesRead <= framesToReadThisIteration); /* If this fails it means there's a bug in drwav_read_pcm_frames(). */ + + /* Validation to ensure we don't read too much from out intermediary buffer. This is to protect from invalid files. */ + samplesRead = framesRead * pWav->channels; + if ((samplesRead * bytesPerSample) > sizeof(sampleData)) { + DRWAV_ASSERT(DRWAV_FALSE); /* This should never happen with a valid file. */ + break; + } + + drwav_mulaw_to_s32(pBufferOut, sampleData, (size_t)samplesRead); + + #ifdef DR_WAV_LIBSNDFILE_COMPAT + { + if (pWav->container == drwav_container_aiff) { + drwav_uint64 iSample; + for (iSample = 0; iSample < samplesRead; iSample += 1) { + pBufferOut[iSample] = -pBufferOut[iSample]; + } + } + } + #endif + + pBufferOut += samplesRead; + framesToRead -= framesRead; + totalFramesRead += framesRead; + } + + return totalFramesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s32(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + if (pWav == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drwav_read_pcm_frames(pWav, framesToRead, NULL); + } + + /* Don't try to read more samples than can potentially fit in the output buffer. */ + if (framesToRead * pWav->channels * sizeof(drwav_int32) > DRWAV_SIZE_MAX) { + framesToRead = DRWAV_SIZE_MAX / sizeof(drwav_int32) / pWav->channels; + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_PCM) { + return drwav_read_pcm_frames_s32__pcm(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ADPCM || pWav->translatedFormatTag == DR_WAVE_FORMAT_DVI_ADPCM) { + return drwav_read_pcm_frames_s32__msadpcm_ima(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_IEEE_FLOAT) { + return drwav_read_pcm_frames_s32__ieee(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_ALAW) { + return drwav_read_pcm_frames_s32__alaw(pWav, framesToRead, pBufferOut); + } + + if (pWav->translatedFormatTag == DR_WAVE_FORMAT_MULAW) { + return drwav_read_pcm_frames_s32__mulaw(pWav, framesToRead, pBufferOut); + } + + return 0; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s32le(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_s32(pWav, framesToRead, pBufferOut); + if (pBufferOut != NULL && drwav__is_little_endian() == DRWAV_FALSE) { + drwav__bswap_samples_s32(pBufferOut, framesRead*pWav->channels); + } + + return framesRead; +} + +DRWAV_API drwav_uint64 drwav_read_pcm_frames_s32be(drwav* pWav, drwav_uint64 framesToRead, drwav_int32* pBufferOut) +{ + drwav_uint64 framesRead = drwav_read_pcm_frames_s32(pWav, framesToRead, pBufferOut); + if (pBufferOut != NULL && drwav__is_little_endian() == DRWAV_TRUE) { + drwav__bswap_samples_s32(pBufferOut, framesRead*pWav->channels); + } + + return framesRead; +} + + +DRWAV_API void drwav_u8_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = ((int)pIn[i] - 128) * 16777216; + } +} + +DRWAV_API void drwav_s16_to_s32(drwav_int32* pOut, const drwav_int16* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = (drwav_int32)pIn[i] * 65536; + } +} + +DRWAV_API void drwav_s24_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + unsigned int s0 = pIn[i*3 + 0]; + unsigned int s1 = pIn[i*3 + 1]; + unsigned int s2 = pIn[i*3 + 2]; + + drwav_int32 sample32 = (drwav_int32)((s0 << 8) | (s1 << 16) | (s2 << 24)); + *pOut++ = sample32; + } +} + +DRWAV_API void drwav_f32_to_s32(drwav_int32* pOut, const float* pIn, size_t sampleCount) +{ + size_t i; + for (i = 0; i < sampleCount; ++i) { + float x = pIn[i]; + if (x != x) { + pOut[i] = 0; /* NaN */ + } else if (x <= -1) { + pOut[i] = (-2147483647 - 1); + } else if (x >= 1) { + pOut[i] = 2147483647; + } else { + pOut[i] = (drwav_int32)(x * 2147483648.0f); + } + } +} + +DRWAV_API void drwav_f64_to_s32(drwav_int32* pOut, const double* pIn, size_t sampleCount) +{ + size_t i; + for (i = 0; i < sampleCount; ++i) { + double x = pIn[i]; + if (x != x) { + pOut[i] = 0; /* NaN */ + } else if (x <= -1) { + pOut[i] = (-2147483647 - 1); + } else if (x >= 1) { + pOut[i] = 2147483647; + } else { + pOut[i] = (drwav_int32)(x * 2147483648.0); + } + } +} + +DRWAV_API void drwav_alaw_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i = 0; i < sampleCount; ++i) { + *pOut++ = (drwav_int32)drwav__alaw_to_s16(pIn[i]) * 65536; + } +} + +DRWAV_API void drwav_mulaw_to_s32(drwav_int32* pOut, const drwav_uint8* pIn, size_t sampleCount) +{ + size_t i; + + if (pOut == NULL || pIn == NULL) { + return; + } + + for (i= 0; i < sampleCount; ++i) { + *pOut++ = (drwav_int32)drwav__mulaw_to_s16(pIn[i]) * 65536; + } +} + + + +DRWAV_PRIVATE drwav_int16* drwav__read_pcm_frames_and_close_s16(drwav* pWav, unsigned int* channels, unsigned int* sampleRate, drwav_uint64* totalFrameCount) +{ + drwav_uint64 sampleDataSize; + drwav_int16* pSampleData; + drwav_uint64 framesRead; + + DRWAV_ASSERT(pWav != NULL); + + /* Check for overflow before multiplication. */ + if (pWav->channels == 0 || pWav->totalPCMFrameCount > DRWAV_SIZE_MAX / pWav->channels / sizeof(drwav_int16)) { + drwav_uninit(pWav); + return NULL; /* Overflow or invalid channels. */ + } + + sampleDataSize = pWav->totalPCMFrameCount * pWav->channels * sizeof(drwav_int16); + if (sampleDataSize > DRWAV_SIZE_MAX) { + drwav_uninit(pWav); + return NULL; /* File's too big. */ + } + + pSampleData = (drwav_int16*)drwav__malloc_from_callbacks((size_t)sampleDataSize, &pWav->allocationCallbacks); /* <-- Safe cast due to the check above. */ + if (pSampleData == NULL) { + drwav_uninit(pWav); + return NULL; /* Failed to allocate memory. */ + } + + framesRead = drwav_read_pcm_frames_s16(pWav, (size_t)pWav->totalPCMFrameCount, pSampleData); + if (framesRead != pWav->totalPCMFrameCount) { + drwav__free_from_callbacks(pSampleData, &pWav->allocationCallbacks); + drwav_uninit(pWav); + return NULL; /* There was an error reading the samples. */ + } + + drwav_uninit(pWav); + + if (sampleRate) { + *sampleRate = pWav->sampleRate; + } + if (channels) { + *channels = pWav->channels; + } + if (totalFrameCount) { + *totalFrameCount = pWav->totalPCMFrameCount; + } + + return pSampleData; +} + +DRWAV_PRIVATE float* drwav__read_pcm_frames_and_close_f32(drwav* pWav, unsigned int* channels, unsigned int* sampleRate, drwav_uint64* totalFrameCount) +{ + drwav_uint64 sampleDataSize; + float* pSampleData; + drwav_uint64 framesRead; + + DRWAV_ASSERT(pWav != NULL); + + /* Check for overflow before multiplication. */ + if (pWav->channels == 0 || pWav->totalPCMFrameCount > DRWAV_SIZE_MAX / pWav->channels / sizeof(float)) { + drwav_uninit(pWav); + return NULL; /* Overflow or invalid channels. */ + } + + sampleDataSize = pWav->totalPCMFrameCount * pWav->channels * sizeof(float); + if (sampleDataSize > DRWAV_SIZE_MAX) { + drwav_uninit(pWav); + return NULL; /* File's too big. */ + } + + pSampleData = (float*)drwav__malloc_from_callbacks((size_t)sampleDataSize, &pWav->allocationCallbacks); /* <-- Safe cast due to the check above. */ + if (pSampleData == NULL) { + drwav_uninit(pWav); + return NULL; /* Failed to allocate memory. */ + } + + framesRead = drwav_read_pcm_frames_f32(pWav, (size_t)pWav->totalPCMFrameCount, pSampleData); + if (framesRead != pWav->totalPCMFrameCount) { + drwav__free_from_callbacks(pSampleData, &pWav->allocationCallbacks); + drwav_uninit(pWav); + return NULL; /* There was an error reading the samples. */ + } + + drwav_uninit(pWav); + + if (sampleRate) { + *sampleRate = pWav->sampleRate; + } + if (channels) { + *channels = pWav->channels; + } + if (totalFrameCount) { + *totalFrameCount = pWav->totalPCMFrameCount; + } + + return pSampleData; +} + +DRWAV_PRIVATE drwav_int32* drwav__read_pcm_frames_and_close_s32(drwav* pWav, unsigned int* channels, unsigned int* sampleRate, drwav_uint64* totalFrameCount) +{ + drwav_uint64 sampleDataSize; + drwav_int32* pSampleData; + drwav_uint64 framesRead; + + DRWAV_ASSERT(pWav != NULL); + + /* Check for overflow before multiplication. */ + if (pWav->channels == 0 || pWav->totalPCMFrameCount > DRWAV_SIZE_MAX / pWav->channels / sizeof(drwav_int32)) { + drwav_uninit(pWav); + return NULL; /* Overflow or invalid channels. */ + } + + sampleDataSize = pWav->totalPCMFrameCount * pWav->channels * sizeof(drwav_int32); + if (sampleDataSize > DRWAV_SIZE_MAX) { + drwav_uninit(pWav); + return NULL; /* File's too big. */ + } + + pSampleData = (drwav_int32*)drwav__malloc_from_callbacks((size_t)sampleDataSize, &pWav->allocationCallbacks); /* <-- Safe cast due to the check above. */ + if (pSampleData == NULL) { + drwav_uninit(pWav); + return NULL; /* Failed to allocate memory. */ + } + + framesRead = drwav_read_pcm_frames_s32(pWav, (size_t)pWav->totalPCMFrameCount, pSampleData); + if (framesRead != pWav->totalPCMFrameCount) { + drwav__free_from_callbacks(pSampleData, &pWav->allocationCallbacks); + drwav_uninit(pWav); + return NULL; /* There was an error reading the samples. */ + } + + drwav_uninit(pWav); + + if (sampleRate) { + *sampleRate = pWav->sampleRate; + } + if (channels) { + *channels = pWav->channels; + } + if (totalFrameCount) { + *totalFrameCount = pWav->totalPCMFrameCount; + } + + return pSampleData; +} + + + +DRWAV_API drwav_int16* drwav_open_and_read_pcm_frames_s16(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init(&wav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s16(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API float* drwav_open_and_read_pcm_frames_f32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init(&wav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_f32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API drwav_int32* drwav_open_and_read_pcm_frames_s32(drwav_read_proc onRead, drwav_seek_proc onSeek, drwav_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init(&wav, onRead, onSeek, onTell, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +#ifndef DR_WAV_NO_STDIO +DRWAV_API drwav_int16* drwav_open_file_and_read_pcm_frames_s16(const char* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_file(&wav, filename, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s16(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API float* drwav_open_file_and_read_pcm_frames_f32(const char* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_file(&wav, filename, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_f32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API drwav_int32* drwav_open_file_and_read_pcm_frames_s32(const char* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_file(&wav, filename, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + + +#ifndef DR_WAV_NO_WCHAR +DRWAV_API drwav_int16* drwav_open_file_and_read_pcm_frames_s16_w(const wchar_t* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (channelsOut) { + *channelsOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_file_w(&wav, filename, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s16(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API float* drwav_open_file_and_read_pcm_frames_f32_w(const wchar_t* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (channelsOut) { + *channelsOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_file_w(&wav, filename, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_f32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API drwav_int32* drwav_open_file_and_read_pcm_frames_s32_w(const wchar_t* filename, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (channelsOut) { + *channelsOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_file_w(&wav, filename, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} +#endif /* DR_WAV_NO_WCHAR */ +#endif /* DR_WAV_NO_STDIO */ + +DRWAV_API drwav_int16* drwav_open_memory_and_read_pcm_frames_s16(const void* data, size_t dataSize, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_memory(&wav, data, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s16(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API float* drwav_open_memory_and_read_pcm_frames_f32(const void* data, size_t dataSize, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_memory(&wav, data, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_f32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} + +DRWAV_API drwav_int32* drwav_open_memory_and_read_pcm_frames_s32(const void* data, size_t dataSize, unsigned int* channelsOut, unsigned int* sampleRateOut, drwav_uint64* totalFrameCountOut, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + drwav wav; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalFrameCountOut) { + *totalFrameCountOut = 0; + } + + if (!drwav_init_memory(&wav, data, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drwav__read_pcm_frames_and_close_s32(&wav, channelsOut, sampleRateOut, totalFrameCountOut); +} +#endif /* DR_WAV_NO_CONVERSION_API */ + + +DRWAV_API void drwav_free(void* p, const drwav_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + drwav__free_from_callbacks(p, pAllocationCallbacks); + } else { + drwav__free_default(p, NULL); + } +} + +DRWAV_API drwav_uint16 drwav_bytes_to_u16(const drwav_uint8* data) +{ + return ((drwav_uint16)data[0] << 0) | ((drwav_uint16)data[1] << 8); +} + +DRWAV_API drwav_int16 drwav_bytes_to_s16(const drwav_uint8* data) +{ + return (drwav_int16)drwav_bytes_to_u16(data); +} + +DRWAV_API drwav_uint32 drwav_bytes_to_u32(const drwav_uint8* data) +{ + return drwav_bytes_to_u32_le(data); +} + +DRWAV_API float drwav_bytes_to_f32(const drwav_uint8* data) +{ + union { + drwav_uint32 u32; + float f32; + } value; + + value.u32 = drwav_bytes_to_u32(data); + return value.f32; +} + +DRWAV_API drwav_int32 drwav_bytes_to_s32(const drwav_uint8* data) +{ + return (drwav_int32)drwav_bytes_to_u32(data); +} + +DRWAV_API drwav_uint64 drwav_bytes_to_u64(const drwav_uint8* data) +{ + return + ((drwav_uint64)data[0] << 0) | ((drwav_uint64)data[1] << 8) | ((drwav_uint64)data[2] << 16) | ((drwav_uint64)data[3] << 24) | + ((drwav_uint64)data[4] << 32) | ((drwav_uint64)data[5] << 40) | ((drwav_uint64)data[6] << 48) | ((drwav_uint64)data[7] << 56); +} + +DRWAV_API drwav_int64 drwav_bytes_to_s64(const drwav_uint8* data) +{ + return (drwav_int64)drwav_bytes_to_u64(data); +} + + +DRWAV_API drwav_bool32 drwav_guid_equal(const drwav_uint8 a[16], const drwav_uint8 b[16]) +{ + int i; + for (i = 0; i < 16; i += 1) { + if (a[i] != b[i]) { + return DRWAV_FALSE; + } + } + + return DRWAV_TRUE; +} + +DRWAV_API drwav_bool32 drwav_fourcc_equal(const drwav_uint8* a, const char* b) +{ + return + a[0] == b[0] && + a[1] == b[1] && + a[2] == b[2] && + a[3] == b[3]; +} + +#ifdef __MRC__ +/* Undo the pragma at the beginning of this file. */ +#pragma options opt reset +#endif + +#endif /* dr_wav_c */ +#endif /* DR_WAV_IMPLEMENTATION */ + +/* +REVISION HISTORY +================ +v0.14.6 - TBD + - Encoders will now write out header information each write so that a valid file is still produced when an explicit `drwav_uninit()` is not called. + - Fix an error when loading files with a malformed "bext" chunk. + - Fix an error when loading files with a malformed "fmt" chunk. + - Fix an error when loading files with a malformed "fact" chunk. + - Fix an error when loading files with a malformed "smpl" chunk. + - Fix an underflow error with badly formed ADPCM encoded files. + - Fix an underflow error with badly formed W64 files. + - Fix an error when converting from >32 bit samples to s16/f32/s32 on big-endian architectures. + - Fix an error with conversion from u8, 16, alaw and mulaw to s32. + - Fix an error with AIFF files with an unusual bit depth. + - Fix some NaN conversion errors when converting from floating point to s16 and s32. + - Add some bound checking when processing metadata chunks. + +v0.14.5 - 2026-03-03 + - Fix a crash when loading files with a malformed "smpl" chunk. + - Fix a signed overflow bug with the MS-ADPCM decoder. + +v0.14.4 - 2026-01-17 + - Fix some compilation warnings. + +v0.14.3 - 2025-12-14 + - Fix a possible out-of-bounds read when reading from MS-ADPCM encoded files. + - Fix a possible integer overflow error. + +v0.14.2 - 2025-12-02 + - Fix a compilation warning. + +v0.14.1 - 2025-09-10 + - Fix an error with the NXDK build. + +v0.14.0 - 2025-07-23 + - API CHANGE: Seek origin enums have been renamed to the following: + - drwav_seek_origin_start -> DRWAV_SEEK_SET + - drwav_seek_origin_current -> DRWAV_SEEK_CUR + - DRWAV_SEEK_END (new) + - API CHANGE: A new seek origin has been added to allow seeking from the end of the file. If you implement your own `onSeek` callback, you must now handle `DRWAV_SEEK_END`. If you only use `*_init_file()` or `*_init_memory()`, you need not change anything. + - API CHANGE: An `onTell` callback has been added to the following functions: + - drwav_init() + - drwav_init_ex() + - drwav_init_with_metadata() + - drwav_open_and_read_pcm_frames_s16() + - drwav_open_and_read_pcm_frames_f32() + - drwav_open_and_read_pcm_frames_s32() + - API CHANGE: The `firstSampleByteOffset`, `lastSampleByteOffset` and `sampleByteOffset` members of `drwav_cue_point` have been renamed to `firstSampleOffset`, `lastSampleOffset` and `sampleOffset`, respectively. + - Fix a static analysis warning. + - Fix compilation for AIX OS. + +v0.13.17 - 2024-12-17 + - Fix a possible crash when reading from MS-ADPCM encoded files. + - Improve detection of ARM64EC + +v0.13.16 - 2024-02-27 + - Fix a Wdouble-promotion warning. + +v0.13.15 - 2024-01-23 + - Relax some unnecessary validation that prevented some files from loading. + +v0.13.14 - 2023-12-02 + - Fix a warning about an unused variable. + +v0.13.13 - 2023-11-02 + - Fix a warning when compiling with Clang. + +v0.13.12 - 2023-08-07 + - Fix a possible crash in drwav_read_pcm_frames(). + +v0.13.11 - 2023-07-07 + - AIFF compatibility improvements. + +v0.13.10 - 2023-05-29 + - Fix a bug where drwav_init_with_metadata() does not decode any frames after initializtion. + +v0.13.9 - 2023-05-22 + - Add support for AIFF decoding (writing and metadata not supported). + - Add support for RIFX decoding (writing and metadata not supported). + - Fix a bug where metadata is not processed if it's located before the "fmt " chunk. + - Add a workaround for a type of malformed WAV file where the size of the "RIFF" and "data" chunks + are incorrectly set to 0xFFFFFFFF. + +v0.13.8 - 2023-03-25 + - Fix a possible null pointer dereference. + - Fix a crash when loading files with badly formed metadata. + +v0.13.7 - 2022-09-17 + - Fix compilation with DJGPP. + - Add support for disabling wchar_t with DR_WAV_NO_WCHAR. + +v0.13.6 - 2022-04-10 + - Fix compilation error on older versions of GCC. + - Remove some dependencies on the standard library. + +v0.13.5 - 2022-01-26 + - Fix an error when seeking to the end of the file. + +v0.13.4 - 2021-12-08 + - Fix some static analysis warnings. + +v0.13.3 - 2021-11-24 + - Fix an incorrect assertion when trying to endian swap 1-byte sample formats. This is now a no-op + rather than a failed assertion. + - Fix a bug with parsing of the bext chunk. + - Fix some static analysis warnings. + +v0.13.2 - 2021-10-02 + - Fix a possible buffer overflow when reading from compressed formats. + +v0.13.1 - 2021-07-31 + - Fix platform detection for ARM64. + +v0.13.0 - 2021-07-01 + - Improve support for reading and writing metadata. Use the `_with_metadata()` APIs to initialize + a WAV decoder and store the metadata within the `drwav` object. Use the `pMetadata` and + `metadataCount` members of the `drwav` object to read the data. The old way of handling metadata + via a callback is still usable and valid. + - API CHANGE: drwav_target_write_size_bytes() now takes extra parameters for calculating the + required write size when writing metadata. + - Add drwav_get_cursor_in_pcm_frames() + - Add drwav_get_length_in_pcm_frames() + - Fix a bug where drwav_read_raw() can call the read callback with a byte count of zero. + +v0.12.20 - 2021-06-11 + - Fix some undefined behavior. + +v0.12.19 - 2021-02-21 + - Fix a warning due to referencing _MSC_VER when it is undefined. + - Minor improvements to the management of some internal state concerning the data chunk cursor. + +v0.12.18 - 2021-01-31 + - Clean up some static analysis warnings. + +v0.12.17 - 2021-01-17 + - Minor fix to sample code in documentation. + - Correctly qualify a private API as private rather than public. + - Code cleanup. + +v0.12.16 - 2020-12-02 + - Fix a bug when trying to read more bytes than can fit in a size_t. + +v0.12.15 - 2020-11-21 + - Fix compilation with OpenWatcom. + +v0.12.14 - 2020-11-13 + - Minor code clean up. + +v0.12.13 - 2020-11-01 + - Improve compiler support for older versions of GCC. + +v0.12.12 - 2020-09-28 + - Add support for RF64. + - Fix a bug in writing mode where the size of the RIFF chunk incorrectly includes the header section. + +v0.12.11 - 2020-09-08 + - Fix a compilation error on older compilers. + +v0.12.10 - 2020-08-24 + - Fix a bug when seeking with ADPCM formats. + +v0.12.9 - 2020-08-02 + - Simplify sized types. + +v0.12.8 - 2020-07-25 + - Fix a compilation warning. + +v0.12.7 - 2020-07-15 + - Fix some bugs on big-endian architectures. + - Fix an error in s24 to f32 conversion. + +v0.12.6 - 2020-06-23 + - Change drwav_read_*() to allow NULL to be passed in as the output buffer which is equivalent to a forward seek. + - Fix a buffer overflow when trying to decode invalid IMA-ADPCM files. + - Add include guard for the implementation section. + +v0.12.5 - 2020-05-27 + - Minor documentation fix. + +v0.12.4 - 2020-05-16 + - Replace assert() with DRWAV_ASSERT(). + - Add compile-time and run-time version querying. + - DRWAV_VERSION_MINOR + - DRWAV_VERSION_MAJOR + - DRWAV_VERSION_REVISION + - DRWAV_VERSION_STRING + - drwav_version() + - drwav_version_string() + +v0.12.3 - 2020-04-30 + - Fix compilation errors with VC6. + +v0.12.2 - 2020-04-21 + - Fix a bug where drwav_init_file() does not close the file handle after attempting to load an erroneous file. + +v0.12.1 - 2020-04-13 + - Fix some pedantic warnings. + +v0.12.0 - 2020-04-04 + - API CHANGE: Add container and format parameters to the chunk callback. + - Minor documentation updates. + +v0.11.5 - 2020-03-07 + - Fix compilation error with Visual Studio .NET 2003. + +v0.11.4 - 2020-01-29 + - Fix some static analysis warnings. + - Fix a bug when reading f32 samples from an A-law encoded stream. + +v0.11.3 - 2020-01-12 + - Minor changes to some f32 format conversion routines. + - Minor bug fix for ADPCM conversion when end of file is reached. + +v0.11.2 - 2019-12-02 + - Fix a possible crash when using custom memory allocators without a custom realloc() implementation. + - Fix an integer overflow bug. + - Fix a null pointer dereference bug. + - Add limits to sample rate, channels and bits per sample to tighten up some validation. + +v0.11.1 - 2019-10-07 + - Internal code clean up. + +v0.11.0 - 2019-10-06 + - API CHANGE: Add support for user defined memory allocation routines. This system allows the program to specify their own memory allocation + routines with a user data pointer for client-specific contextual data. This adds an extra parameter to the end of the following APIs: + - drwav_init() + - drwav_init_ex() + - drwav_init_file() + - drwav_init_file_ex() + - drwav_init_file_w() + - drwav_init_file_w_ex() + - drwav_init_memory() + - drwav_init_memory_ex() + - drwav_init_write() + - drwav_init_write_sequential() + - drwav_init_write_sequential_pcm_frames() + - drwav_init_file_write() + - drwav_init_file_write_sequential() + - drwav_init_file_write_sequential_pcm_frames() + - drwav_init_file_write_w() + - drwav_init_file_write_sequential_w() + - drwav_init_file_write_sequential_pcm_frames_w() + - drwav_init_memory_write() + - drwav_init_memory_write_sequential() + - drwav_init_memory_write_sequential_pcm_frames() + - drwav_open_and_read_pcm_frames_s16() + - drwav_open_and_read_pcm_frames_f32() + - drwav_open_and_read_pcm_frames_s32() + - drwav_open_file_and_read_pcm_frames_s16() + - drwav_open_file_and_read_pcm_frames_f32() + - drwav_open_file_and_read_pcm_frames_s32() + - drwav_open_file_and_read_pcm_frames_s16_w() + - drwav_open_file_and_read_pcm_frames_f32_w() + - drwav_open_file_and_read_pcm_frames_s32_w() + - drwav_open_memory_and_read_pcm_frames_s16() + - drwav_open_memory_and_read_pcm_frames_f32() + - drwav_open_memory_and_read_pcm_frames_s32() + Set this extra parameter to NULL to use defaults which is the same as the previous behaviour. Setting this NULL will use + DRWAV_MALLOC, DRWAV_REALLOC and DRWAV_FREE. + - Add support for reading and writing PCM frames in an explicit endianness. New APIs: + - drwav_read_pcm_frames_le() + - drwav_read_pcm_frames_be() + - drwav_read_pcm_frames_s16le() + - drwav_read_pcm_frames_s16be() + - drwav_read_pcm_frames_f32le() + - drwav_read_pcm_frames_f32be() + - drwav_read_pcm_frames_s32le() + - drwav_read_pcm_frames_s32be() + - drwav_write_pcm_frames_le() + - drwav_write_pcm_frames_be() + - Remove deprecated APIs. + - API CHANGE: The following APIs now return native-endian data. Previously they returned little-endian data. + - drwav_read_pcm_frames() + - drwav_read_pcm_frames_s16() + - drwav_read_pcm_frames_s32() + - drwav_read_pcm_frames_f32() + - drwav_open_and_read_pcm_frames_s16() + - drwav_open_and_read_pcm_frames_s32() + - drwav_open_and_read_pcm_frames_f32() + - drwav_open_file_and_read_pcm_frames_s16() + - drwav_open_file_and_read_pcm_frames_s32() + - drwav_open_file_and_read_pcm_frames_f32() + - drwav_open_file_and_read_pcm_frames_s16_w() + - drwav_open_file_and_read_pcm_frames_s32_w() + - drwav_open_file_and_read_pcm_frames_f32_w() + - drwav_open_memory_and_read_pcm_frames_s16() + - drwav_open_memory_and_read_pcm_frames_s32() + - drwav_open_memory_and_read_pcm_frames_f32() + +v0.10.1 - 2019-08-31 + - Correctly handle partial trailing ADPCM blocks. + +v0.10.0 - 2019-08-04 + - Remove deprecated APIs. + - Add wchar_t variants for file loading APIs: + drwav_init_file_w() + drwav_init_file_ex_w() + drwav_init_file_write_w() + drwav_init_file_write_sequential_w() + - Add drwav_target_write_size_bytes() which calculates the total size in bytes of a WAV file given a format and sample count. + - Add APIs for specifying the PCM frame count instead of the sample count when opening in sequential write mode: + drwav_init_write_sequential_pcm_frames() + drwav_init_file_write_sequential_pcm_frames() + drwav_init_file_write_sequential_pcm_frames_w() + drwav_init_memory_write_sequential_pcm_frames() + - Deprecate drwav_open*() and drwav_close(): + drwav_open() + drwav_open_ex() + drwav_open_write() + drwav_open_write_sequential() + drwav_open_file() + drwav_open_file_ex() + drwav_open_file_write() + drwav_open_file_write_sequential() + drwav_open_memory() + drwav_open_memory_ex() + drwav_open_memory_write() + drwav_open_memory_write_sequential() + drwav_close() + - Minor documentation updates. + +v0.9.2 - 2019-05-21 + - Fix warnings. + +v0.9.1 - 2019-05-05 + - Add support for C89. + - Change license to choice of public domain or MIT-0. + +v0.9.0 - 2018-12-16 + - API CHANGE: Add new reading APIs for reading by PCM frames instead of samples. Old APIs have been deprecated and + will be removed in v0.10.0. Deprecated APIs and their replacements: + drwav_read() -> drwav_read_pcm_frames() + drwav_read_s16() -> drwav_read_pcm_frames_s16() + drwav_read_f32() -> drwav_read_pcm_frames_f32() + drwav_read_s32() -> drwav_read_pcm_frames_s32() + drwav_seek_to_sample() -> drwav_seek_to_pcm_frame() + drwav_write() -> drwav_write_pcm_frames() + drwav_open_and_read_s16() -> drwav_open_and_read_pcm_frames_s16() + drwav_open_and_read_f32() -> drwav_open_and_read_pcm_frames_f32() + drwav_open_and_read_s32() -> drwav_open_and_read_pcm_frames_s32() + drwav_open_file_and_read_s16() -> drwav_open_file_and_read_pcm_frames_s16() + drwav_open_file_and_read_f32() -> drwav_open_file_and_read_pcm_frames_f32() + drwav_open_file_and_read_s32() -> drwav_open_file_and_read_pcm_frames_s32() + drwav_open_memory_and_read_s16() -> drwav_open_memory_and_read_pcm_frames_s16() + drwav_open_memory_and_read_f32() -> drwav_open_memory_and_read_pcm_frames_f32() + drwav_open_memory_and_read_s32() -> drwav_open_memory_and_read_pcm_frames_s32() + drwav::totalSampleCount -> drwav::totalPCMFrameCount + - API CHANGE: Rename drwav_open_and_read_file_*() to drwav_open_file_and_read_*(). + - API CHANGE: Rename drwav_open_and_read_memory_*() to drwav_open_memory_and_read_*(). + - Add built-in support for smpl chunks. + - Add support for firing a callback for each chunk in the file at initialization time. + - This is enabled through the drwav_init_ex(), etc. family of APIs. + - Handle invalid FMT chunks more robustly. + +v0.8.5 - 2018-09-11 + - Const correctness. + - Fix a potential stack overflow. + +v0.8.4 - 2018-08-07 + - Improve 64-bit detection. + +v0.8.3 - 2018-08-05 + - Fix C++ build on older versions of GCC. + +v0.8.2 - 2018-08-02 + - Fix some big-endian bugs. + +v0.8.1 - 2018-06-29 + - Add support for sequential writing APIs. + - Disable seeking in write mode. + - Fix bugs with Wave64. + - Fix typos. + +v0.8 - 2018-04-27 + - Bug fix. + - Start using major.minor.revision versioning. + +v0.7f - 2018-02-05 + - Restrict ADPCM formats to a maximum of 2 channels. + +v0.7e - 2018-02-02 + - Fix a crash. + +v0.7d - 2018-02-01 + - Fix a crash. + +v0.7c - 2018-02-01 + - Set drwav.bytesPerSample to 0 for all compressed formats. + - Fix a crash when reading 16-bit floating point WAV files. In this case dr_wav will output silence for + all format conversion reading APIs (*_s16, *_s32, *_f32 APIs). + - Fix some divide-by-zero errors. + +v0.7b - 2018-01-22 + - Fix errors with seeking of compressed formats. + - Fix compilation error when DR_WAV_NO_CONVERSION_API + +v0.7a - 2017-11-17 + - Fix some GCC warnings. + +v0.7 - 2017-11-04 + - Add writing APIs. + +v0.6 - 2017-08-16 + - API CHANGE: Rename dr_* types to drwav_*. + - Add support for custom implementations of malloc(), realloc(), etc. + - Add support for Microsoft ADPCM. + - Add support for IMA ADPCM (DVI, format code 0x11). + - Optimizations to drwav_read_s16(). + - Bug fixes. + +v0.5g - 2017-07-16 + - Change underlying type for booleans to unsigned. + +v0.5f - 2017-04-04 + - Fix a minor bug with drwav_open_and_read_s16() and family. + +v0.5e - 2016-12-29 + - Added support for reading samples as signed 16-bit integers. Use the _s16() family of APIs for this. + - Minor fixes to documentation. + +v0.5d - 2016-12-28 + - Use drwav_int* and drwav_uint* sized types to improve compiler support. + +v0.5c - 2016-11-11 + - Properly handle JUNK chunks that come before the FMT chunk. + +v0.5b - 2016-10-23 + - A minor change to drwav_bool8 and drwav_bool32 types. + +v0.5a - 2016-10-11 + - Fixed a bug with drwav_open_and_read() and family due to incorrect argument ordering. + - Improve A-law and mu-law efficiency. + +v0.5 - 2016-09-29 + - API CHANGE. Swap the order of "channels" and "sampleRate" parameters in drwav_open_and_read*(). Rationale for this is to + keep it consistent with dr_audio and dr_flac. + +v0.4b - 2016-09-18 + - Fixed a typo in documentation. + +v0.4a - 2016-09-18 + - Fixed a typo. + - Change date format to ISO 8601 (YYYY-MM-DD) + +v0.4 - 2016-07-13 + - API CHANGE. Make onSeek consistent with dr_flac. + - API CHANGE. Rename drwav_seek() to drwav_seek_to_sample() for clarity and consistency with dr_flac. + - Added support for Sony Wave64. + +v0.3a - 2016-05-28 + - API CHANGE. Return drwav_bool32 instead of int in onSeek callback. + - Fixed a memory leak. + +v0.3 - 2016-05-22 + - Lots of API changes for consistency. + +v0.2a - 2016-05-16 + - Fixed Linux/GCC build. + +v0.2 - 2016-05-11 + - Added support for reading data as signed 32-bit PCM for consistency with dr_flac. + +v0.1a - 2016-05-07 + - Fixed a bug in drwav_open_file() where the file handle would not be closed if the loader failed to initialize. + +v0.1 - 2016-05-04 + - Initial versioned release. +*/ + +/* +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2023 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ diff --git a/native/vendor/r8brain/CDSPBlockConvolver.h b/native/vendor/r8brain/CDSPBlockConvolver.h new file mode 100644 index 0000000..a7d37d8 --- /dev/null +++ b/native/vendor/r8brain/CDSPBlockConvolver.h @@ -0,0 +1,638 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPBlockConvolver.h + * + * @brief Single-block overlap-save convolution processor class. + * + * This file includes single-block overlap-save convolution processor class. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPBLOCKCONVOLVER_INCLUDED +#define R8B_CDSPBLOCKCONVOLVER_INCLUDED + +#include "CDSPFIRFilter.h" +#include "CDSPProcessor.h" + +namespace r8b { + +/** + * @brief Single-block overlap-save convolution processing class. + * + * Class that implements single-block overlap-save convolution processing. The + * length of a single FFT block used depends on the length of the filter + * kernel. + * + * The rationale behind "single-block" processing is that increasing the FFT + * block length by 2 is more efficient than performing convolution at the same + * FFT block length but using two blocks. + * + * This class also implements a built-in resampling by any whole-number + * factor, which simplifies the overall resampling objects topology. + */ + +class CDSPBlockConvolver : public CDSPProcessor +{ +public: + /** + * @brief Initializes internal variables and constants of *this* object. + * + * @param aFilter Pre-calculated filter data. Reference to this object is + * inhertied by *this* object, and the object will be released when *this* + * object is destroyed. If upsampling is used, filter's gain should be + * equal to the upsampling factor. + * @param aUpFactor The upsampling factor, positive value. E.g., value of + * 2 means 2x upsampling should be performed over the input data. + * @param aDownFactor The downsampling factor, positive value. E.g., value + * of 2 means 2x downsampling should be performed over the output data. + * @param PrevLatency Latency, in samples (any value greater or equal to + * 0), which was left in the output signal by a previous process. This + * value is usually non-zero if the minimum-phase filters are in use. This + * value is always zero if the linear-phase filters are in use. + * @param aDoConsumeLatency `true` if the output latency should be + * consumed. Does not apply to the fractional part of the latency (if such + * part is available). + */ + + CDSPBlockConvolver( CDSPFIRFilter& aFilter, const int aUpFactor, + const int aDownFactor, const double PrevLatency = 0.0, + const bool aDoConsumeLatency = true ) + : Filter( &aFilter ) + , UpFactor( aUpFactor ) + , DownFactor( aDownFactor ) + , BlockLen2( 2 << Filter -> getBlockLenBits() ) + , DoConsumeLatency( aDoConsumeLatency ) + { + R8BASSERT( UpFactor > 0 ); + R8BASSERT( DownFactor > 0 ); + R8BASSERT( PrevLatency >= 0.0 ); + + int fftinBits; + UpShift = getBitOccupancy( UpFactor ) - 1; + + if(( 1 << UpShift ) == UpFactor ) + { + fftinBits = Filter -> getBlockLenBits() + 1 - UpShift; + PrevInputLen = ( Filter -> getKernelLen() - 1 + UpFactor - 1 ) / + UpFactor; + + InputLen = BlockLen2 - PrevInputLen * UpFactor; + } + else + { + UpShift = -1; + fftinBits = Filter -> getBlockLenBits() + 1; + PrevInputLen = Filter -> getKernelLen() - 1; + InputLen = BlockLen2 - PrevInputLen; + } + + OutOffset = ( Filter -> isZeroPhase() ? Filter -> getLatency() : 0 ); + LatencyFrac = Filter -> getLatencyFrac() + PrevLatency * UpFactor; + Latency = (int) LatencyFrac; + const int InLatency = Latency + Filter -> getLatency() - OutOffset; + LatencyFrac -= Latency; + LatencyFrac /= DownFactor; + + Latency += InputLen + Filter -> getLatency(); + + int fftoutBits; + InputDelay = 0; + DownSkipInit = 0; + DownShift = getBitOccupancy( DownFactor ) - 1; + + if(( 1 << DownShift ) == DownFactor ) + { + fftoutBits = Filter -> getBlockLenBits() + 1 - DownShift; + + if( DownFactor > 1 ) + { + if( UpShift > 0 ) + { + // This case never happens in practice due to mutual + // exclusion of "power of 2" DownFactor and UpFactor + // values. + + R8BASSERT( UpShift == 0 ); + } + else + { + // Make sure InputLen is divisible by DownFactor. + + const int ilc = InputLen & ( DownFactor - 1 ); + PrevInputLen += ilc; + InputLen -= ilc; + Latency -= ilc; + + // Correct InputDelay for input and filter's latency. + + const int lc = InLatency & ( DownFactor - 1 ); + + if( lc > 0 ) + { + InputDelay = DownFactor - lc; + } + + if( !DoConsumeLatency ) + { + Latency /= DownFactor; + } + } + } + } + else + { + fftoutBits = Filter -> getBlockLenBits() + 1; + DownShift = -1; + + if( !DoConsumeLatency && DownFactor > 1 ) + { + DownSkipInit = Latency % DownFactor; + Latency /= DownFactor; + } + } + + R8BASSERT( Latency >= 0 ); + + fftin = new CDSPRealFFTKeeper( fftinBits ); + + if( fftoutBits == fftinBits ) + { + fftout = fftin; + } + else + { + ffto2 = new CDSPRealFFTKeeper( fftoutBits ); + fftout = ffto2; + } + + WorkBlocks.alloc( BlockLen2 * 2 + PrevInputLen ); + CurInput = &WorkBlocks[ 0 ]; + CurOutput = &WorkBlocks[ BlockLen2 ]; // CurInput and + // CurOutput are address-aligned. + PrevInput = &WorkBlocks[ BlockLen2 * 2 ]; + + clear(); + + R8BCONSOLE( "CDSPBlockConvolver: flt_len=%i in_len=%i io=%i/%i " + "fft=%i/%i latency=%i\n", Filter -> getKernelLen(), InputLen, + UpFactor, DownFactor, (*fftin) -> getLen(), (*fftout) -> getLen(), + getLatency() ); + } + + virtual int getInLenBeforeOutPos( int ReqOutPos ) const + { + if( Next != R8B_NULL ) + { + ReqOutPos = Next -> getInLenBeforeOutPos( ReqOutPos ); + } + + return( (int) (( Latency + (double) ReqOutPos * DownFactor ) / + UpFactor + LatencyFrac * DownFactor / UpFactor )); + } + + virtual int getLatency() const + { + return( DoConsumeLatency ? 0 : Latency ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return(( MaxInLen * UpFactor + DownFactor - 1 ) / DownFactor ); + } + + virtual void clear() + { + memset( &PrevInput[ 0 ], 0, + (size_t) PrevInputLen * sizeof( PrevInput[ 0 ])); + + if( DoConsumeLatency ) + { + LatencyLeft = Latency; + } + else + { + LatencyLeft = 0; + + if( DownShift > 0 ) + { + memset( &CurOutput[ 0 ], 0, + (size_t) ( BlockLen2 >> DownShift ) * + sizeof( CurOutput[ 0 ])); + } + else + { + memset( &CurOutput[ BlockLen2 - OutOffset ], 0, + (size_t) OutOffset * sizeof( CurOutput[ 0 ])); + + memset( &CurOutput[ 0 ], 0, + (size_t) ( InputLen - OutOffset ) * + sizeof( CurOutput[ 0 ])); + } + } + + memset( CurInput, 0, (size_t) InputDelay * sizeof( CurInput[ 0 ])); + + InDataLeft = InputLen - InputDelay; + UpSkip = 0; + DownSkip = DownSkipInit; + } + + virtual int process( double* ip, int l0, double*& op0 ) + { + R8BASSERT( l0 >= 0 ); + R8BASSERT( UpFactor / DownFactor <= 1 || ip != op0 || l0 == 0 ); + + double* op = op0; + int l = l0 * UpFactor; + l0 = 0; + + while( l > 0 ) + { + const int Offs = InputLen - InDataLeft; + + if( l < InDataLeft ) + { + InDataLeft -= l; + + if( UpShift >= 0 ) + { + memcpy( &CurInput[ Offs >> UpShift ], ip, + (size_t) ( l >> UpShift ) * sizeof( CurInput[ 0 ])); + } + else + { + copyUpsample( ip, &CurInput[ Offs ], l ); + } + + copyToOutput( Offs - OutOffset, op, l, l0 ); + break; + } + + const int b = InDataLeft; + l -= b; + InDataLeft = InputLen; + int ilu; + + if( UpShift >= 0 ) + { + const int bu = b >> UpShift; + memcpy( &CurInput[ Offs >> UpShift ], ip, + (size_t) bu * sizeof( CurInput[ 0 ])); + + ip += bu; + ilu = InputLen >> UpShift; + } + else + { + copyUpsample( ip, &CurInput[ Offs ], b ); + ilu = InputLen; + } + + const size_t pil = (size_t) PrevInputLen * sizeof( CurInput[ 0 ]); + memcpy( &CurInput[ ilu ], PrevInput, pil ); + memcpy( PrevInput, &CurInput[ ilu - PrevInputLen ], pil ); + + realfft_t* CurInputFFT = (*fftin) -> forward( CurInput ); + realfft_t dsn = 0; + + if( UpShift > 0 || DownShift > 0 ) + { + CurInputFFT = (*fftin) -> reorderForward( CurInputFFT, + ( (*fftin) -> getLen() > (*fftout) -> getLen() ? + (*fftin) -> getWorkBuf() : (*fftout) -> getWorkBuf() )); + + if( UpShift > 0 ) + { + mirrorInputSpectrum( CurInputFFT ); + } + + if( DownShift > 0 ) + { + const int z = BlockLen2 >> DownShift; + dsn = Filter -> calcDownShiftNyquist( CurInputFFT[ z ], + CurInputFFT[ z + 1 ]); + } + + CurInputFFT = (*fftout) -> reorderInverse( CurInputFFT, + CurInput ); + } + + if( Filter -> isZeroPhase() ) + { + (*fftout) -> multiplyBlocksZP( Filter -> getKernelBlock(), + CurInputFFT ); + } + else + { + (*fftout) -> multiplyBlocks( Filter -> getKernelBlock(), + CurInputFFT ); + } + + if( DownShift > 0 ) + { + (*fftout) -> setBinNyquist( CurInputFFT, dsn ); + } + + (*fftout) -> inverse( CurInputFFT ); + + copyToOutput( Offs - OutOffset, op, b, l0 ); + + double* const tmp = CurInput; + CurInput = CurOutput; + CurOutput = tmp; + } + + return( l0 ); + } + +private: + CRefKeeper< CDSPFIRFilter > Filter; ///< Filter in use. + CPtrKeeper< CDSPRealFFTKeeper > fftin; ///< FFT object 1, used to produce + ///< the input spectrum (can embed the "power of 2" upsampling). + CPtrKeeper< CDSPRealFFTKeeper > ffto2; ///< FFT object 2 (can be + ///< `nullptr`). + CDSPRealFFTKeeper* fftout; ///< FFT object used to produce the output + ///< signal (can embed the "power of 2" downsampling), may point to + ///< either `fftin` or `ffto2`. + int UpFactor; ///< Upsampling factor. + int DownFactor; ///< Downsampling factor. + int BlockLen2; ///< Equals `block length * 2`. + int OutOffset; ///< Output offset, depends on filter's introduced latency. + int PrevInputLen; ///< The length of previous input data saved, used for + ///< overlap. + int InputLen; ///< The number of input samples that should be accumulated + ///< before the input block is processed. + double LatencyFrac; ///< Fractional latency, in samples, that is left in + ///< the output signal. + int Latency; ///< Processing latency, in samples. + int UpShift; ///< "Power of 2" upsampling shift. Equals -1 if `UpFactor` + ///< is not a "power of 2" value. Equals 0 if `UpFactor` equals 1. + int DownShift; ///< "Power of 2" downsampling shift. Equals -1 if + ///< `DownFactor` is not a "power of 2". Equals 0 if `DownFactor` + ///< equals 1. + int InputDelay; ///< Additional input delay, in samples. Used to make the + ///< output delay divisible by `DownShift`. Used only if `UpShift` is + ///< non-positive and `DownShift` is greater than 0. + double* PrevInput; ///< Previous input data buffer, capacity = `BlockLen`. + double* CurInput; ///< Input data buffer, capacity = `BlockLen2`. + double* CurOutput; ///< Output data buffer, capacity = `BlockLen2`. + int InDataLeft; ///< Samples left before processing input and output FFT + ///< blocks. Initialized to `InputLen` on clear. + int LatencyLeft; ///< Latency in samples left to skip. + int UpSkip; ///< The current upsampling sample skip (value in the range + ///< 0 to `UpFactor - 1`). + int DownSkip; ///< The current downsampling sample skip (value in the + ///< range 0 to `DownFactor - 1`). Not used if `DownShift` is + ///< positive. + int DownSkipInit; ///< The initial `DownSkip` value after clear(). + CFixedBuffer< double > WorkBlocks; ///< Previous input data, input and + ///< output data blocks, overall capacity = `BlockLen2 * 2 + + ///< PrevInputLen`. Used in the flip-flop manner. + bool DoConsumeLatency; ///< `true` if the output latency should be + ///< consumed. Does not apply to the fractional part of the latency + ///< (if such part is available). + + /** + * @brief Copies samples from the input buffer to the output buffer + * while inserting zeros inbetween them to perform the whole-numbered + * upsampling. + * + * @param[in,out] ip0 Input buffer. Will be advanced on function's return. + * @param[out] op Output buffer. + * @param l0 The number of samples to fill in the output buffer, including + * both input samples and interpolation (zero) samples. + */ + + void copyUpsample( double*& ip0, double* op, int l0 ) + { + int b = min( UpSkip, l0 ); + + if( b != 0 ) + { + UpSkip -= b; + l0 -= b; + + *op = 0.0; + op++; + + while( --b != 0 ) + { + *op = 0.0; + op++; + } + } + + double* ip = ip0; + const int upf = UpFactor; + int l = l0 / upf; + int lz = l0 - l * upf; + + if( upf == 3 ) + { + while( l != 0 ) + { + op[ 0 ] = *ip; + op[ 1 ] = 0.0; + op[ 2 ] = 0.0; + ip++; + op += upf; + l--; + } + } + else + if( upf == 5 ) + { + while( l != 0 ) + { + op[ 0 ] = *ip; + op[ 1 ] = 0.0; + op[ 2 ] = 0.0; + op[ 3 ] = 0.0; + op[ 4 ] = 0.0; + ip++; + op += upf; + l--; + } + } + else + { + const size_t zc = (size_t) ( upf - 1 ) * sizeof( op[ 0 ]); + + while( l != 0 ) + { + *op = *ip; + ip++; + + memset( op + 1, 0, zc ); + op += upf; + l--; + } + } + + if( lz != 0 ) + { + *op = *ip; + ip++; + op++; + + UpSkip = upf - lz; + + while( --lz != 0 ) + { + *op = 0.0; + op++; + } + } + + ip0 = ip; + } + + /** + * @brief Copies sample data from the CurOutput buffer to the specified + * output buffer and advances its position. + * + * If necessary, this function "consumes" latency and performs + * downsampling. + * + * @param Offs CurOutput buffer offset, can be negative. + * @param[out] op0 Output buffer pointer, will be advanced. + * @param b The number of output samples available, including those which + * are discarded during whole-number downsampling. + * @param l0 The overall output sample count, will be increased. + */ + + void copyToOutput( int Offs, double*& op0, int b, int& l0 ) + { + if( Offs < 0 ) + { + if( Offs + b <= 0 ) + { + Offs += BlockLen2; + } + else + { + copyToOutput( Offs + BlockLen2, op0, -Offs, l0 ); + b += Offs; + Offs = 0; + } + } + + if( LatencyLeft != 0 ) + { + if( LatencyLeft >= b ) + { + LatencyLeft -= b; + return; + } + + Offs += LatencyLeft; + b -= LatencyLeft; + LatencyLeft = 0; + } + + const int df = DownFactor; + + if( DownShift > 0 ) + { + int Skip = Offs & ( df - 1 ); + + if( Skip > 0 ) + { + Skip = df - Skip; + b -= Skip; + Offs += Skip; + } + + if( b > 0 ) + { + b = ( b + df - 1 ) >> DownShift; + memcpy( op0, &CurOutput[ Offs >> DownShift ], + (size_t) b * sizeof( op0[ 0 ])); + + op0 += b; + l0 += b; + } + } + else + { + if( df > 1 ) + { + const double* ip = &CurOutput[ Offs + DownSkip ]; + int l = ( b + df - 1 - DownSkip ) / df; + DownSkip += l * df - b; + + double* op = op0; + l0 += l; + op0 += l; + + while( l > 0 ) + { + *op = *ip; + ip += df; + op++; + l--; + } + } + else + { + memcpy( op0, &CurOutput[ Offs ], + (size_t) b * sizeof( op0[ 0 ])); + + op0 += b; + l0 += b; + } + } + } + + /** + * @brief Performs input spectrum mirroring which is used to perform a + * fast "power of 2" upsampling. + * + * Such mirroring is equivalent to insertion of zeros into the input + * signal. + * + * @param p Spectrum data block to mirror. + * @tparam T Buffer's element type. + */ + + template< typename T > + void mirrorInputSpectrum( T* const p ) + { + const int bl1 = BlockLen2 >> UpShift; + const int bl2 = bl1 * 2; + int i; + + for( i = bl1 + 2; i < bl2; i += 2 ) + { + p[ i ] = p[ bl2 - i ]; + p[ i + 1 ] = -p[ bl2 - i + 1 ]; + } + + p[ bl1 ] = p[ 1 ]; + p[ bl1 + 1 ] = (T) 0; + p[ 1 ] = p[ 0 ]; + + for( i = 1; i < UpShift; i++ ) + { + const int z = bl1 << i; + memcpy( &p[ z ], p, (size_t) z * sizeof( p[ 0 ])); + p[ z + 1 ] = (T) 0; + } + } +}; + +} // namespace r8b + +#endif // R8B_CDSPBLOCKCONVOLVER_INCLUDED diff --git a/native/vendor/r8brain/CDSPFIRFilter.h b/native/vendor/r8brain/CDSPFIRFilter.h new file mode 100644 index 0000000..35118d0 --- /dev/null +++ b/native/vendor/r8brain/CDSPFIRFilter.h @@ -0,0 +1,818 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPFIRFilter.h + * + * @brief FIR filter generator and filter cache classes. + * + * This file includes low-pass FIR filter generator and filter cache. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPFIRFILTER_INCLUDED +#define R8B_CDSPFIRFILTER_INCLUDED + +#include "CDSPSincFilterGen.h" +#include "CDSPRealFFT.h" + +namespace r8b { + +/** + * @brief Enumeration of filter's phase responses. + */ + +enum EDSPFilterPhaseResponse +{ + fprLinearPhase = 0, ///< Linear-phase response. Features a linear-phase, + ///< high-latency response, with the latency expressed as an integer + ///< value. + fprMinPhase ///< Minimum-phase response. Features a minimal-latency + ///< response, but the response's phase is non-linear. The latency is + ///< usually expressed as a non-integer value, and is usually small, + ///< but is never equal to zero. The minimum-phase filter is obtained + ///< from a linear-phase filter. Note that since in the context of + ///< r8brain-free-src other filters (interpolation, half-band) remain + ///< linear-phase, the resulting phase will be "intermediate". The + ///< minimum-phase transformation has precision limits: this may skew + ///< both the -3 dB point and attenuation of the filter being + ///< transformed: as it was measured, the skew happens purely at + ///< random, and in most cases is within tolerable range. In a small + ///< (1%) random subset of cases the skew is bigger and cannot be + ///< predicted. Minimum-phase transform requires 64-bit floating-point + ///< FFT; results with 32-bit float FFT are far from optimal. +}; + +/** + * @brief Calculation and storage class for FIR filters. + * + * Class that implements calculation and storing of a FIR filter (currently + * contains low-pass filter calculation routine designed for sample rate + * conversion). Objects of this class cannot be created directly, but can be + * obtained via the CDSPFilterCache::getLPFilter() static function. + */ + +class CDSPFIRFilter : public R8B_BASECLASS, + private CSinglyLinkedListItem< CDSPFIRFilter > +{ + R8BNOCTOR( CDSPFIRFilter ) + + friend class CSinglyLinkedListItem< CDSPFIRFilter >; + friend class CDSPFIRFilterCache; + +public: + /** + * @brief Returns the minimal allowed low-pass filter's transition band, + * in percent. + */ + + static double getLPMinTransBand() + { + return( 0.5 ); + } + + /** + * @brief Returns the maximal allowed low-pass filter's transition band, + * in percent. + */ + + static double getLPMaxTransBand() + { + return( 45.0 ); + } + + /** + * @brief Returns the minimal allowed low-pass filter's stop-band + * attenuation, in decibel. + */ + + static double getLPMinAtten() + { + return( 49.0 ); + } + + /** + * @brief Returns the maximal allowed low-pass filter's stop-band + * attenuation, in decibel. + */ + + static double getLPMaxAtten() + { + return( 218.0 ); + } + + /** + * @brief Returns `true` if kernel block of *this* filter has zero-phase + * response. + */ + + bool isZeroPhase() const + { + return( IsZeroPhase ); + } + + /** + * @brief Returns filter's latency, in samples (integer part). + */ + + int getLatency() const + { + return( Latency ); + } + + /** + * @brief Returns filter's latency, in samples (fractional part). Always + * zero for linear-phase filters. + */ + + double getLatencyFrac() const + { + return( LatencyFrac ); + } + + /** + * @brief Returns filter kernel length, in samples. Not to be confused + * with the block length. + */ + + int getKernelLen() const + { + return( KernelLen ); + } + + /** + * @brief Returns filter's block length, expressed as Nth power of 2. + * + * The actual length is twice as large due to zero-padding. Also it does + * not account for length reduction due to `ReqDownShift`. + */ + + int getBlockLenBits() const + { + return( BlockLenBits ); + } + + /** + * @brief Returns pointer to filter's kernel block, in complex-numbered + * form obtained via the CDSPRealFFT::forward() function call. + * + * Zero-padded, gain-adjusted with the CDSPRealFFT::getInvMulConst() + * multiplied by `ReqGain` constant, immediately suitable for convolution. + * Kernel block may have "zero-phase" response, depending on the + * isZeroPhase() function's result. + * + * If `ReqDownShift` is positive, the actual length of the kernel block is + * smaller by `2^ReqDownShift`. + */ + + const realfft_t* getKernelBlock() const + { + return( KernelBlockFFT ); + } + + /** + * @brief Calculates Nyquist bin value for downsampling using power-of-2 + * down-shifting. + * + * Multiplies the forward transform's complex component with filter's + * response at the edge bin. + * + * @param re Input value, real part. + * @param im Input value, imaginary part. + * @return The value of the Nyquist bin. + */ + + realfft_t calcDownShiftNyquist( const realfft_t re, + const realfft_t im ) const + { + return( dsnRe * re - dsnIm * im ); + } + + /** + * @brief Reduces reference count to *this* object. + * + * This function should be called when the filter obtained via the filter + * cache is no longer needed. + */ + + void unref(); + +private: + double ReqNormFreq; ///< Required normalized frequency, 0 to 1 inclusive. + double ReqTransBand; ///< Required transition band in percent, as passed + ///< by the user. + double ReqAtten; ///< Required stop-band attenuation in decibel, as passed + ///< by the user (positive value). + EDSPFilterPhaseResponse ReqPhase; ///< Required filter's phase response. + double ReqGain; ///< Required overall filter's gain. + int ReqDownShift; ///< Required downsampling shift, if a positive value. + int RefCount; ///< The number of references made to *this* FIR filter. + bool IsZeroPhase; ///< `true` if kernel block of *this* filter has + ///< zero-phase response. + int Latency; ///< Filter's latency in samples (integer part). + double LatencyFrac; ///< Filter's latency in samples (fractional part). + int KernelLen; ///< Filter kernel length, in samples. + int BlockLenBits; ///< Block length used to store *this* FIR filter, + ///< expressed as Nth power of 2. This value is used directly by the + ///< convolver. + CFixedBuffer< double > KernelBlock; ///< FIR filter buffer, capacity + ///< equals to `1 << ( BlockLenBits + 1 )`. Second half of the buffer + ///< contains zero-padding to allow alias-free convolution. + ///< Address-aligned. Reused to store the FFT of the filter. + realfft_t* KernelBlockFFT; ///< Pointer to FFT-transformed kernel block in + ///< native resolution. + realfft_t dsnRe; ///< Down-shift Nyquist bin's real value. + realfft_t dsnIm; ///< Down-shift Nyquist bin's imaginary value. + + CDSPFIRFilter() + : RefCount( 1 ) + { + } + + /** + * @brief Builds filter kernel based on the "Req" parameters. + * + * @param ExtAttenCorrs External attentuation correction table, for + * internal use. + */ + + void buildLPFilter( const double* const ExtAttenCorrs ) + { + const double tb = ReqTransBand * 0.01; + double pwr; + double fo1; + double hl; + double atten = -ReqAtten; + + if( tb >= 0.25 ) + { + if( ReqAtten >= 117.0 ) + { + atten -= 1.60; + } + else + if( ReqAtten >= 60.0 ) + { + atten -= 1.91; + } + else + { + atten -= 2.25; + } + } + else + if( tb >= 0.10 ) + { + if( ReqAtten >= 117.0 ) + { + atten -= 0.69; + } + else + if( ReqAtten >= 60.0 ) + { + atten -= 0.73; + } + else + { + atten -= 1.13; + } + } + else + { + if( ReqAtten >= 117.0 ) + { + atten -= 0.21; + } + else + if( ReqAtten >= 60.0 ) + { + atten -= 0.25; + } + else + { + atten -= 0.36; + } + } + + static const int AttenCorrCount = 264; + static const double AttenCorrMin = 49.0; + static const double AttenCorrDiff = 176.25; + int AttenCorr = (int) floor(( -atten - AttenCorrMin ) * + AttenCorrCount / AttenCorrDiff + 0.5 ); + + AttenCorr = min( AttenCorrCount, max( 0, AttenCorr )); + + if( ExtAttenCorrs != R8B_NULL ) + { + atten -= ExtAttenCorrs[ AttenCorr ]; + } + else + if( tb >= 0.25 ) + { + static const double AttenCorrScale = 101.0; + static const signed char AttenCorrs[] = { + -127, -127, -125, -125, -122, -119, -115, -110, -104, -97, + -91, -82, -75, -24, -16, -6, 4, 14, 24, 29, 30, 32, 37, 44, + 51, 57, 63, 67, 65, 50, 53, 56, 58, 60, 63, 64, 66, 68, 74, + 77, 78, 78, 78, 79, 79, 60, 60, 60, 61, 59, 52, 47, 41, 36, + 30, 24, 17, 9, 0, -8, -10, -11, -14, -13, -18, -25, -31, -38, + -44, -50, -57, -63, -68, -74, -81, -89, -96, -101, -104, -107, + -109, -110, -86, -84, -85, -82, -80, -77, -73, -67, -62, -55, + -48, -42, -35, -30, -20, -11, -2, 5, 6, 6, 7, 11, 16, 21, 26, + 34, 41, 46, 49, 52, 55, 56, 48, 49, 51, 51, 52, 52, 52, 52, + 52, 51, 51, 50, 47, 47, 50, 48, 46, 42, 38, 35, 31, 27, 24, + 20, 16, 12, 11, 12, 10, 8, 4, -1, -6, -11, -16, -19, -17, -21, + -24, -27, -32, -34, -37, -38, -40, -41, -40, -40, -42, -41, + -44, -45, -43, -41, -34, -31, -28, -24, -21, -18, -14, -10, + -5, -1, 2, 5, 8, 7, 4, 3, 2, 2, 4, 6, 8, 9, 9, 10, 10, 10, 10, + 9, 8, 9, 11, 14, 13, 12, 11, 10, 8, 7, 6, 5, 3, 2, 2, -1, -1, + -3, -3, -4, -4, -5, -4, -6, -7, -9, -5, -1, -1, 0, 1, 0, -2, + -3, -4, -5, -5, -8, -13, -13, -13, -12, -13, -12, -11, -11, + -9, -8, -7, -5, -3, -1, 2, 4, 6, 9, 10, 11, 14, 18, 21, 24, + 27, 30, 34, 37, 37, 39, 40 }; + + atten -= AttenCorrs[ AttenCorr ] / AttenCorrScale; + } + else + if( tb >= 0.10 ) + { + static const double AttenCorrScale = 210.0; + static const signed char AttenCorrs[] = { + -113, -118, -122, -125, -126, -97, -95, -92, -92, -89, -82, + -75, -69, -48, -42, -36, -30, -22, -14, -5, -2, 1, 6, 13, 22, + 28, 35, 41, 48, 55, 56, 56, 61, 65, 71, 77, 81, 83, 85, 85, + 74, 74, 73, 72, 71, 70, 68, 64, 59, 56, 49, 52, 46, 42, 36, + 32, 26, 20, 13, 7, -2, -6, -10, -15, -20, -27, -33, -38, -44, + -43, -48, -53, -57, -63, -69, -73, -75, -79, -81, -74, -76, + -77, -77, -78, -81, -80, -80, -78, -76, -65, -62, -59, -56, + -51, -48, -44, -38, -33, -25, -19, -13, -5, -1, 2, 7, 13, 17, + 21, 25, 30, 35, 40, 45, 50, 53, 56, 57, 55, 58, 59, 62, 64, + 67, 67, 68, 68, 62, 61, 61, 59, 59, 57, 57, 55, 52, 48, 42, + 38, 35, 31, 26, 20, 15, 13, 10, 7, 3, -2, -8, -13, -17, -23, + -28, -34, -37, -40, -41, -45, -48, -50, -53, -57, -59, -62, + -63, -63, -57, -57, -56, -56, -54, -54, -53, -49, -48, -41, + -38, -33, -31, -26, -23, -18, -12, -9, -7, -7, -3, 0, 5, 9, + 14, 16, 20, 22, 21, 23, 25, 27, 28, 29, 34, 33, 35, 33, 31, + 30, 29, 29, 26, 26, 25, 24, 20, 19, 15, 10, 8, 4, 1, -2, -6, + -10, -16, -19, -23, -26, -27, -30, -34, -39, -43, -47, -51, + -52, -54, -56, -58, -59, -62, -63, -66, -65, -65, -64, -59, + -57, -54, -52, -48, -44, -42, -37, -32, -22, -17, -10, -3, 5, + 13, 22, 30, 40, 50, 60, 72 }; + + atten -= AttenCorrs[ AttenCorr ] / AttenCorrScale; + } + else + { + static const double AttenCorrScale = 196.0; + static const signed char AttenCorrs[] = { + -15, -17, -20, -20, -20, -21, -20, -16, -17, -18, -17, -13, + -12, -11, -9, -7, -5, -4, -1, 1, 3, 4, 5, 6, 7, 9, 9, 10, 10, + 10, 11, 11, 11, 12, 12, 12, 10, 11, 10, 10, 8, 10, 11, 10, 11, + 11, 13, 14, 15, 19, 27, 26, 23, 18, 14, 8, 4, -2, -6, -12, + -17, -23, -28, -33, -37, -42, -46, -49, -53, -57, -60, -61, + -64, -65, -67, -66, -66, -66, -65, -64, -61, -59, -56, -52, + -48, -42, -38, -31, -27, -19, -13, -7, -1, 8, 14, 22, 29, 37, + 45, 52, 59, 66, 73, 80, 86, 91, 96, 100, 104, 108, 111, 114, + 115, 117, 118, 120, 120, 118, 117, 114, 113, 111, 107, 103, + 99, 95, 89, 84, 78, 72, 66, 60, 52, 44, 37, 30, 21, 14, 6, -3, + -11, -18, -26, -34, -43, -51, -58, -65, -73, -78, -85, -90, + -97, -102, -107, -113, -115, -118, -121, -125, -125, -126, + -126, -126, -125, -124, -121, -119, -115, -111, -109, -101, + -102, -95, -88, -81, -73, -67, -63, -54, -47, -40, -33, -26, + -18, -11, -5, 2, 8, 14, 19, 25, 31, 36, 37, 43, 47, 49, 51, + 52, 57, 57, 56, 57, 58, 58, 58, 57, 56, 52, 52, 50, 48, 44, + 41, 39, 37, 33, 31, 26, 24, 21, 18, 14, 11, 8, 4, 2, -2, -5, + -7, -9, -11, -13, -15, -16, -18, -19, -20, -23, -24, -24, -25, + -27, -26, -27, -29, -30, -31, -32, -35, -36, -39, -40, -44, + -46, -51, -54, -59, -63, -69, -76, -83, -91, -98 }; + + atten -= AttenCorrs[ AttenCorr ] / AttenCorrScale; + } + + pwr = 7.43932822146293e-8 * sqr( atten ) + 0.000102747434588003 * + cos( 0.00785021930010397 * atten ) * cos( 0.633854318781239 + + 0.103208573657699 * atten ) - 0.00798132247867036 - + 0.000903555213543865 * atten - 0.0969365532127236 * exp( + 0.0779275237937911 * atten ) - 1.37304948662012e-5 * atten * cos( + 0.00785021930010397 * atten ); + + if( pwr <= 0.067665322581 ) + { + if( tb >= 0.25 ) + { + hl = 2.6778150875894 / tb + 300.547590563091 * atan( atan( + 2.68959772209918 * pwr )) / ( 5.5099277187035 * tb - tb * + tanh( cos( asinh( atten )))); + + fo1 = 0.987205355829873 * tb + 1.00011788929851 * atan2( + -0.321432067051302 - 6.19131357321578 * sqrt( pwr ), + hl + -1.14861472207245 / ( hl - 14.1821147585957 ) + pow( + 0.9521145021664, pow( atan2( 1.12018764830637, tb ), + 2.10988901686912 * hl - 20.9691278378345 ))); + } + else + if( tb >= 0.10 ) + { + hl = ( 1.56688617018066 + 142.064321294568 * pwr + + 0.00419441117131136 * cos( 243.633511747297 * pwr ) - + 0.022953443903576 * atten - 0.026629568860284 * cos( + 127.715550622571 * pwr )) / tb; + + fo1 = 0.982299356642411 * tb + 0.999441744774215 * asinh(( + -0.361783054039583 - 5.80540593623676 * sqrt( pwr )) / + hl ); + } + else + { + hl = ( 2.45739657014937 + 269.183679500541 * pwr * cos( + 5.73225668178813 + atan2( cosh( 0.988861169868941 - + 17.2201556280744 * pwr ), 1.08340138240431 * pwr ))) / tb; + + fo1 = 2.291956939 * tb + 0.01942450693 * sqr( tb ) * hl - + 4.67538973161837 * pwr * tb - 1.668433124 * tb * + pow( pwr, pwr ); + } + } + else + { + if( tb >= 0.25 ) + { + hl = ( 1.50258368698213 + 158.556968859477 * asinh( pwr ) * + tanh( 57.9466246871383 * tanh( pwr )) - + 0.0105440479814834 * atten ) / tb; + + fo1 = 0.994024401639321 * tb + ( -0.236282717577215 - + 6.8724924545387 * sqrt( sin( pwr ))) / hl; + } + else + if( tb >= 0.10 ) + { + hl = ( 1.50277377248945 + 158.222625721046 * asinh( pwr ) * + tanh( 1.02875299001715 + 42.072277322604 * pwr ) - + 0.0108380943845632 * atten ) / tb; + + fo1 = 0.992539376734551 * tb + ( -0.251747813037178 - + 6.74159892452584 * sqrt( tanh( tanh( tan( pwr ))))) / hl; + } + else + { + hl = ( 1.15990238966306 * pwr - 5.02124037125213 * sqr( + pwr ) - 0.158676856669827 * atten * cos( 1.1609073390614 * + pwr - 6.33932586197475 * pwr * sqr( pwr ))) / tb; + + fo1 = 0.867344453126885 * tb + 0.052693817907757 * tb * log( + pwr ) + 0.0895511178735932 * tb * atan( 59.7538527741309 * + pwr ) - 0.0745653568081453 * pwr * tb; + } + } + + double WinParams[ 2 ]; + WinParams[ 0 ] = 125.0; + WinParams[ 1 ] = pwr; + + CDSPSincFilterGen sinc; + sinc.Len2 = 0.25 * hl / ReqNormFreq; + sinc.Freq1 = 0.0; + sinc.Freq2 = R8B_PI * ( 1.0 - fo1 ) * ReqNormFreq; + sinc.initBand( CDSPSincFilterGen :: wftKaiser, WinParams, true ); + + KernelLen = sinc.KernelLen; + BlockLenBits = getBitOccupancy( KernelLen - 1 ) + R8B_EXTFFT; + const int BlockLen = 1 << BlockLenBits; + + KernelBlock.alloc( BlockLen * 2 ); + sinc.generateBand( &KernelBlock[ 0 ], + &CDSPSincFilterGen :: calcWindowKaiser ); + + if( ReqPhase == fprLinearPhase ) + { + IsZeroPhase = true; + Latency = sinc.fl2; + LatencyFrac = 0.0; + } + else + { + IsZeroPhase = false; + double DCGroupDelay; + + calcMinPhaseTransform( &KernelBlock[ 0 ], KernelLen, 16, false, + &DCGroupDelay ); + + Latency = (int) DCGroupDelay; + LatencyFrac = DCGroupDelay - Latency; + } + + CDSPRealFFTKeeper ffto( BlockLenBits + 1 ); + + dsnRe = 0; + dsnIm = 0; + + if( IsZeroPhase ) + { + // Calculate DC gain. + + double s = 0.0; + int i; + + for( i = 0; i < KernelLen; i++ ) + { + s += KernelBlock[ i ]; + } + + s = ffto -> getInvMulConst() * ReqGain / s; + + // Time-shift the filter so that zero-phase response is produced. + // Simultaneously multiply by "s". + + for( i = 0; i <= sinc.fl2; i++ ) + { + KernelBlock[ i ] = KernelBlock[ sinc.fl2 + i ] * s; + } + + for( i = 1; i <= sinc.fl2; i++ ) + { + KernelBlock[ BlockLen * 2 - i ] = KernelBlock[ i ]; + } + + memset( &KernelBlock[ sinc.fl2 + 1 ], 0, + (size_t) ( BlockLen * 2 - KernelLen ) * + sizeof( KernelBlock[ 0 ])); + + KernelBlockFFT = ffto -> forward( KernelBlock ); + + if( ReqDownShift > 0 ) + { + KernelBlockFFT = ffto -> reorderForward( KernelBlockFFT, + ffto -> getWorkBuf() ); + + const int z = ( 2 << BlockLenBits ) >> ReqDownShift; + dsnRe = KernelBlockFFT[ z ]; + dsnIm = KernelBlockFFT[ z + 1 ]; + + CDSPRealFFTKeeper ffto2( BlockLenBits + 1 - ReqDownShift ); + CFixedBuffer< double > NewBlock( ffto2 -> getLen() * + (int) sizeof( realfft_t ) / (int) sizeof( double )); + + KernelBlockFFT = ffto2 -> reorderInverse( KernelBlockFFT, + NewBlock ); + + ffto2 -> convertToZP( KernelBlockFFT ); + + KernelBlock.moveFrom( NewBlock ); + } + else + { + ffto -> convertToZP( KernelBlockFFT ); + } + } + else + { + normalizeFIRFilter( &KernelBlock[ 0 ], KernelLen, + ffto -> getInvMulConst() * ReqGain ); + + memset( &KernelBlock[ KernelLen ], 0, + (size_t) ( BlockLen * 2 - KernelLen ) * + sizeof( KernelBlock[ 0 ])); + + KernelBlockFFT = ffto -> forward( KernelBlock ); + + if( ReqDownShift > 0 ) + { + KernelBlockFFT = ffto -> reorderForward( KernelBlockFFT, + ffto -> getWorkBuf() ); + + const int z = ( 2 << BlockLenBits ) >> ReqDownShift; + dsnRe = KernelBlockFFT[ z ]; + dsnIm = KernelBlockFFT[ z + 1 ]; + + CDSPRealFFTKeeper ffto2( BlockLenBits + 1 - ReqDownShift ); + CFixedBuffer< double > NewBlock( ffto2 -> getLen() * + (int) sizeof( realfft_t ) / (int) sizeof( double )); + + KernelBlockFFT = ffto2 -> reorderInverse( KernelBlockFFT, + NewBlock ); + + KernelBlock.moveFrom( NewBlock ); + } + } + + R8BCONSOLE( "CDSPFIRFilter: flt_len=%i latency=%i nfreq=%.4f " + "tb=%.1f att=%.1f gain=%.3f dsh=%i\n", KernelLen, Latency, + ReqNormFreq, ReqTransBand, ReqAtten, ReqGain, ReqDownShift ); + } +}; + +/** + * @brief FIR filter cache class. + * + * Class that implements cache for calculated FIR filters. The required FIR + * filter should be obtained via the getLPFilter() static function. + */ + +class CDSPFIRFilterCache : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPFIRFilterCache ) + + friend class CDSPFIRFilter; + +public: + /** + * @brief Returns the number of filters present in the cache now. This + * value can be monitored for debugging "forgotten" filters. + */ + + static int getObjCount() + { + R8BSYNC( getStateSync() ); + + return( getObjCountStatic() ); + } + + /** + * @brief Calculates or returns reference to a previously calculated + * (cached) low-pass FIR filter. + * + * Note that the real transition band and attenuation achieved by the + * filter varies with the magnitude of the required attenuation, and are + * never 100% exact. + * + * @param ReqNormFreq Required normalized frequency, in the range 0 to 1, + * inclusive. This is the point after which the stop-band spans. + * @param ReqTransBand Required transition band, in percent of the + * 0 to `ReqNormFreq` spectral bandwidth, in the range + * CDSPFIRFilter::getLPMinTransBand() to + * CDSPFIRFilter::getLPMaxTransBand(), inclusive. The transition band + * specifies the part of the spectrum between the -3 dB and `ReqNormFreq` + * points. The real resulting -3 dB point varies in the range from -3.00 + * to -3.05 dB, but is generally very close to -3 dB. + * @param ReqAtten Required stop-band attenuation in decibel, in the range + * CDSPFIRFilter::getLPMinAtten() to CDSPFIRFilter::getLPMaxAtten(), + * inclusive. Note that the actual stop-band attenuation of the resulting + * filter may be 0.40-4.46 dB higher. + * @param ReqPhase Required filter's phase response. + * @param ReqGain Required overall filter's gain (1.0 for unity gain). + * @param ReqDownFactor Required downsampling factor; it must be positive. + * @param AttenCorrs Attentuation correction table, to pass to the filter + * generation function. For internal use. + * @see EDSPFilterPhaseResponse + * @return A reference to a new or a previously calculated low-pass FIR + * filter object with the required characteristics. A reference count is + * incremented in the returned filter object which should be released + * after use via the CDSPFIRFilter::unref() function. + */ + + static CDSPFIRFilter& getLPFilter( const double ReqNormFreq, + const double ReqTransBand, const double ReqAtten, + const EDSPFilterPhaseResponse ReqPhase, const double ReqGain, + const int ReqDownFactor, const double* const AttenCorrs = R8B_NULL ) + { + R8BASSERT( ReqNormFreq > 0.0 && ReqNormFreq <= 1.0 ); + R8BASSERT( ReqTransBand >= CDSPFIRFilter :: getLPMinTransBand() ); + R8BASSERT( ReqTransBand <= CDSPFIRFilter :: getLPMaxTransBand() ); + R8BASSERT( ReqAtten >= CDSPFIRFilter :: getLPMinAtten() ); + R8BASSERT( ReqAtten <= CDSPFIRFilter :: getLPMaxAtten() ); + R8BASSERT( ReqGain > 0.0 ); + R8BASSERT( ReqDownFactor > 0 ); + + R8B_EXITDTOR static CPtrKeeper< CDSPFIRFilter > Objects; // The chain + // of cached objects. + + int ReqDownShift = getBitOccupancy( ReqDownFactor ) - 1; + + if(( 1 << ReqDownShift ) != ReqDownFactor ) + { + ReqDownShift = 0; + } + + R8BSYNC( getStateSync() ); + + int& ObjCount = getObjCountStatic(); + CDSPFIRFilter* PrevObj = R8B_NULL; + CDSPFIRFilter* CurObj = Objects; + + while( CurObj != R8B_NULL ) + { + if( CurObj -> ReqNormFreq == ReqNormFreq && + CurObj -> ReqTransBand == ReqTransBand && + CurObj -> ReqGain == ReqGain && + CurObj -> ReqDownShift == ReqDownShift && + CurObj -> ReqAtten == ReqAtten && + CurObj -> ReqPhase == ReqPhase ) + { + break; + } + + if( CurObj -> Next == R8B_NULL && + ObjCount >= R8B_FILTER_CACHE_MAX ) + { + if( CurObj -> RefCount == 0 ) + { + // Delete the last filter which is not used. + + PrevObj -> Next = R8B_NULL; + delete CurObj; + ObjCount--; + } + else + { + // Move the last filter to the top of the list since it + // seems to be in use for a long time. + + PrevObj -> Next = R8B_NULL; + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + } + + CurObj = R8B_NULL; + break; + } + + PrevObj = CurObj; + CurObj = CurObj -> Next; + } + + if( CurObj != R8B_NULL ) + { + CurObj -> RefCount++; + + if( PrevObj == R8B_NULL ) + { + return( *CurObj ); + } + + // Remove the filter from the list temporarily. + + PrevObj -> Next = CurObj -> Next; + } + else + { + // Create a new filter object (with RefCount == 1) and build the + // filter kernel. + + CPtrKeeper< CDSPFIRFilter > f( new CDSPFIRFilter() ); + + f -> ReqNormFreq = ReqNormFreq; + f -> ReqTransBand = ReqTransBand; + f -> ReqAtten = ReqAtten; + f -> ReqPhase = ReqPhase; + f -> ReqGain = ReqGain; + f -> ReqDownShift = ReqDownShift; + + f -> buildLPFilter( AttenCorrs ); + + CurObj = f.unkeep(); + ObjCount++; + } + + // Insert the filter at the start of the list. + + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + + return( *CurObj ); + } + +protected: + /** + * @brief Returns reference to filter cache sync object. + */ + + static CSyncObject& getStateSync() + { + R8B_EXITDTOR static CSyncObject StateSync; + + return( StateSync ); + } + + /** + * @brief Returns reference to variable containing cache object count. + */ + + static int& getObjCountStatic() + { + R8B_EXITDTOR static int ObjCount = 0; // The number of objects + // currently preset in the cache. + + return( ObjCount ); + } +}; + +// --------------------------------------------------------------------------- +// CDSPFIRFilter PUBLIC +// --------------------------------------------------------------------------- + +inline void CDSPFIRFilter :: unref() +{ + R8BSYNC( CDSPFIRFilterCache :: getStateSync() ); + + RefCount--; +} + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPFIRFILTER_INCLUDED diff --git a/native/vendor/r8brain/CDSPFracInterpolator.h b/native/vendor/r8brain/CDSPFracInterpolator.h new file mode 100644 index 0000000..24a54f9 --- /dev/null +++ b/native/vendor/r8brain/CDSPFracInterpolator.h @@ -0,0 +1,1162 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPFracInterpolator.h + * + * @brief Fractional delay interpolator and filter bank classes. + * + * This file includes fractional delay interpolator class. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPFRACINTERPOLATOR_INCLUDED +#define R8B_CDSPFRACINTERPOLATOR_INCLUDED + +#include "CDSPSincFilterGen.h" +#include "CDSPProcessor.h" + +namespace r8b { + +#if R8B_FLTTEST + extern int InterpFilterFracs; ///< Force this number of fractional filter + ///< positions. -1 - use default. +#endif // R8B_FLTTEST + +/** + * @brief Sinc function-based fractional delay filter bank class. + * + * Class implements storage and initialization of a bank of sinc-based + * fractional delay filters, expressed as 0th, 1st, 2nd or 3rd order + * polynomial interpolation coefficients. The filters are windowed by the + * "Kaiser" power-raised window function. + */ + +class CDSPFracDelayFilterBank : public R8B_BASECLASS, + private CSinglyLinkedListItem< CDSPFracDelayFilterBank > +{ + R8BNOCTOR( CDSPFracDelayFilterBank ) + + friend class CSinglyLinkedListItem< CDSPFracDelayFilterBank >; + friend class CDSPFracDelayFilterBankCache; + +public: + /** + * @brief Initializes the filter bank object. + * + * @param aFilterFracs The number of fractional delay positions to sample, + * -1 - use default. + * @param aElementSize The size of each filter's tap, in "double" values. + * This parameter corresponds to the complexity of interpolation. 4 should + * be set for 3rd order, 3 for 2nd order, 2 for linear interpolation, 1 + * for whole-numbered stepping. + * @param aInterpPoints The number of points the interpolation is based + * on. This value should not be confused with the `ElementSize`. Set to 2 + * for linear or no interpolation. + * @param aReqAtten Required filter attentuation. + * @param aIsThird `true` if one-third filter is required. + */ + + CDSPFracDelayFilterBank( const int aFilterFracs, const int aElementSize, + const int aInterpPoints, const double aReqAtten, const bool aIsThird ) + : InitFilterFracs( aFilterFracs ) + , ElementSize( aElementSize ) + , InterpPoints( aInterpPoints ) + , ReqAtten( aReqAtten ) + , IsThird( aIsThird ) + , RefCount( 1 ) + { + R8BASSERT( ElementSize >= 1 && ElementSize <= 4 ); + + // Kaiser window function Params, for half and third-band. + + const double* const Params = getWinParams( ReqAtten, IsThird, + FilterLen ); + + FilterSize = FilterLen * ElementSize; + + if( InitFilterFracs == -1 ) + { + FilterFracs = (int) ceil( pow( 6.4, ReqAtten / 50.0 )); + + #if R8B_FLTTEST + + if( InterpFilterFracs != -1 ) + { + FilterFracs = InterpFilterFracs; + } + + #endif // R8B_FLTTEST + } + else + { + FilterFracs = InitFilterFracs; + } + + Table.alloc( FilterSize * ( FilterFracs + InterpPoints )); + + CDSPSincFilterGen sinc; + sinc.Len2 = FilterLen / 2; + + double* p = Table; + const int pc2 = InterpPoints / 2; + int i; + + for( i = -pc2 + 1; i <= FilterFracs + pc2; i++ ) + { + sinc.FracDelay = (double) ( FilterFracs - i ) / FilterFracs; + sinc.initFrac( CDSPSincFilterGen :: wftKaiser, Params, true ); + sinc.generateFrac( p, &CDSPSincFilterGen :: calcWindowKaiser, + ElementSize ); + + normalizeFIRFilter( p, FilterLen, 1.0, ElementSize ); + p += FilterSize; + } + + const int TablePos2 = FilterSize; + const int TablePos3 = FilterSize * 2; + const int TablePos4 = FilterSize * 3; + const int TablePos5 = FilterSize * 4; + const int TablePos6 = FilterSize * 5; + const int TablePos7 = FilterSize * 6; + const int TablePos8 = FilterSize * 7; + double* const TableEnd = Table + ( FilterFracs + 1 ) * FilterSize; + p = Table; + + if( InterpPoints == 8 ) + { + if( ElementSize == 3 ) + { + // Calculate 2nd order spline (polynomial) interpolation + // coefficients using 8 points. + + while( p < TableEnd ) + { + calcSpline2p8Coeffs( p, p[ 0 ], p[ TablePos2 ], + p[ TablePos3 ], p[ TablePos4 ], p[ TablePos5 ], + p[ TablePos6 ], p[ TablePos7 ], p[ TablePos8 ]); + + p += ElementSize; + } + + #if defined( R8B_SIMD_ISH ) + shuffle2_3( Table, TableEnd ); + #endif // SIMD + } + else + if( ElementSize == 4 ) + { + // Calculate 3rd order spline (polynomial) interpolation + // coefficients using 8 points. + + while( p < TableEnd ) + { + calcSpline3p8Coeffs( p, p[ 0 ], p[ TablePos2 ], + p[ TablePos3 ], p[ TablePos4 ], p[ TablePos5 ], + p[ TablePos6 ], p[ TablePos7 ], p[ TablePos8 ]); + + p += ElementSize; + } + + #if defined( R8B_SIMD_ISH ) + shuffle2_4( Table, TableEnd ); + #endif // SIMD + } + } + else + { + if( ElementSize == 2 ) + { + // Calculate linear interpolation coefficients. + + while( p < TableEnd ) + { + p[ 1 ] = p[ TablePos2 ] - p[ 0 ]; + p += ElementSize; + } + + #if defined( R8B_SIMD_ISH ) + shuffle2_2( Table, TableEnd ); + #endif // SIMD + } + } + + R8BCONSOLE( "CDSPFracDelayFilterBank: fracs=%i order=%i taps=%i " + "att=%.1f third=%i\n", FilterFracs, ElementSize - 1, FilterLen, + ReqAtten, (int) IsThird ); + } + + /** + * @brief Rounds the specified attenuation to the nearest effective value. + * + * @param[in,out] att Required filter attentuation. Will be rounded to the + * nearest value. + * @param aIsThird `true` if one-third filter is required. + */ + + static void roundReqAtten( double& att, const bool aIsThird ) + { + int tmp; + getWinParams( att, aIsThird, tmp ); + } + + /** + * @brief Returns the length of the filter, in samples (taps). Always + * an even number, not less than 6. + */ + + int getFilterLen() const + { + return( FilterLen ); + } + + /** + * @brief Returns the number of fractional positions sampled by the bank. + */ + + int getFilterFracs() const + { + return( FilterFracs ); + } + + /** + * @brief Returns reference to the filter. + * + * @param i Filter index, in the range 0 to `FilterFracs`, inclusive. + */ + + const double& operator []( const int i ) const + { + R8BASSERT( i >= 0 && i <= FilterFracs ); + + return( Table[ i * FilterSize ]); + } + + /** + * @brief Reduces reference count to *this* object. + * + * This function should be called when the filter bank obtained via the + * filter bank cache is no longer needed. + */ + + void unref(); + +private: + int FilterLen; ///< Filter length. Always an even number, not less than 6. + int FilterFracs; ///< Fractional position count. + int InitFilterFracs; ///< Fractional position count as supplied to the + ///< constructor, may equal -1. + int ElementSize; ///< Filter element size. + int InterpPoints; ///< Interpolation points to use. + double ReqAtten; ///< Filter's attentuation. + bool IsThird; ///< `true` if one-third filter is in use. + int FilterSize; ///< This constant specifies the "size" of a single filter + ///< in "double" elements. + CFixedBuffer< double > Table; ///< The table of fractional delay filters + ///< for all discrete fractional x = 0..1 sample positions, and + ///< interpolation coefficients. + int RefCount; ///< The number of references made to *this* filter bank. + ///< Not considered for "static" filter bank objects. + + /** + * @brief Returns windowing function parameters for the specified + * attenuation and filter type. + * + * @param[in,out] att Required filter attentuation. Will be rounded to the + * nearest value. + * @param aIsThird `true` if one-third filter is required. + * @param[out] fltlen Resulting filter length. + */ + + static const double* getWinParams( double& att, const bool aIsThird, + int& fltlen ) + { + static const int Coeffs2Base = 8; + static const int Coeffs2Count = 12; + static const double Coeffs2[ Coeffs2Count ][ 3 ] = { + { 4.1308468534586913, 1.1752580009977263, 55.5446 }, // 0.0256 + { 4.4241520324148826, 1.8004881791443044, 81.4191 }, // 0.0886 + { 5.2615232289173663, 1.8133318236025469, 96.3392 }, // 0.0481 + { 5.9433751227216174, 1.8730186391986436, 111.1315 }, // 0.0264 + { 6.8308658290513815, 1.8549555110340281, 125.4653 }, // 0.0146 + { 7.6648458290312904, 1.8565766090828464, 139.7379 }, // 0.0081 + { 8.2038728664307605, 1.9269521820570166, 154.0532 }, // 0.0045 + { 8.7865150946655142, 1.9775307667441668, 168.2101 }, // 0.0025 + { 9.5945017884101773, 1.9718456992078597, 182.1076 }, // 0.0014 + { 10.5163141145985240, 1.9504067820201083, 195.5668 }, // 0.0008 + { 10.2382465206362470, 2.1608923446870087, 209.0610 }, // 0.0004 + { 10.9976060250714000, 2.1536533525688935, 222.5010 }, // 0.0003 + }; + + static const int Coeffs3Base = 6; + static const int Coeffs3Count = 10; + static const double Coeffs3[ Coeffs3Count ][ 3 ] = { + { 3.9888564562781847, 1.5869927184268915, 66.5701 }, // 0.0467 + { 4.6986694038145007, 1.8086068597928262, 86.4715 }, // 0.0136 + { 5.5995071329337822, 1.8930163360942349, 106.1195 }, // 0.0040 + { 6.3627287800257228, 1.9945748322093975, 125.2307 }, // 0.0012 + { 7.4299550711428308, 1.9893400572347544, 144.3469 }, // 0.0004 + { 8.0667715944075642, 2.0928201458699909, 163.4099 }, // 0.0001 + { 8.7469970226288822, 2.1640279784268355, 181.0694 }, // 0.0000 + { 10.0823430069835230, 2.0896678025321922, 199.2880 }, // 0.0000 + { 10.9222206090489510, 2.1221681162186004, 216.6865 }, // 0.0000 + { 21.2017743894772010, 1.1856768080118900, 233.9188 }, // 0.0000 + }; + + const double* Params; + int i = 0; + + if( aIsThird ) + { + while( i != Coeffs3Count - 1 && Coeffs3[ i ][ 2 ] < att ) + { + i++; + } + + Params = &Coeffs3[ i ][ 0 ]; + att = Coeffs3[ i ][ 2 ]; + fltlen = Coeffs3Base + i * 2; + } + else + { + while( i != Coeffs2Count - 1 && Coeffs2[ i ][ 2 ] < att ) + { + i++; + } + + Params = &Coeffs2[ i ][ 0 ]; + att = Coeffs2[ i ][ 2 ]; + fltlen = Coeffs2Base + i * 2; + } + + return( Params ); + } + + /** + * @brief Shuffles 2 order-2 filter points for SIMD operation. + * + * @param p Filter table start pointer. + * @param pe Filter table end pointer. + */ + + static void shuffle2_2( double* p, double* const pe ) + { + while( p != pe ) + { + const double t = p[ 2 ]; + p[ 2 ] = p[ 1 ]; + p[ 1 ] = t; + + p += 4; + } + } + + /** + * @brief Shuffles 2 order-3 filter points for SIMD operation. + * + * @param p Filter table start pointer. + * @param pe Filter table end pointer. + */ + + static void shuffle2_3( double* p, double* const pe ) + { + while( p != pe ) + { + const double t1 = p[ 1 ]; + const double t2 = p[ 2 ]; + const double t3 = p[ 3 ]; + const double t4 = p[ 4 ]; + p[ 1 ] = t3; + p[ 2 ] = t1; + p[ 3 ] = t4; + p[ 4 ] = t2; + + p += 6; + } + } + + /** + * @brief Shuffles 2 order-4 filter points for SIMD operation. + * + * @param p Filter table start pointer. + * @param pe Filter table end pointer. + */ + + static void shuffle2_4( double* p, double* const pe ) + { + while( p != pe ) + { + const double t1 = p[ 1 ]; + const double t2 = p[ 2 ]; + const double t3 = p[ 3 ]; + const double t4 = p[ 4 ]; + const double t5 = p[ 5 ]; + const double t6 = p[ 6 ]; + p[ 1 ] = t4; + p[ 2 ] = t1; + p[ 3 ] = t5; + p[ 4 ] = t2; + p[ 5 ] = t6; + p[ 6 ] = t3; + + p += 8; + } + } +}; + +/** + * @brief Fractional delay filter cache class. + * + * Class implements cache storage of fractional delay filter banks. + */ + +class CDSPFracDelayFilterBankCache : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPFracDelayFilterBankCache ) + + friend class CDSPFracDelayFilterBank; + +public: + /** + * @brief Calculates or returns reference to a previously calculated + * (cached) fractional delay filter bank. + * + * @param aFilterFracs The number of fractional delay positions to sample, + * -1 - use default. + * @param aElementSize The size of each filter's tap, in "double" values. + * @param aInterpPoints The number of points the interpolation is based + * on. + * @param ReqAtten Required filter attentuation. + * @param IsThird `true` if one-third filter is required. + * @param IsStatic `true` if a permanent static filter should be returned + * that is never removed from the cache until application terminates. + * @return Reference to a filter bank. + */ + + static CDSPFracDelayFilterBank& getFilterBank( const int aFilterFracs, + const int aElementSize, const int aInterpPoints, + double ReqAtten, const bool IsThird, const bool IsStatic ) + { + R8B_EXITDTOR static CPtrKeeper< CDSPFracDelayFilterBank > Objects; + // The chain of cached objects. + R8B_EXITDTOR static CPtrKeeper< CDSPFracDelayFilterBank > StaticObjects; + // The chain of static objects. + R8B_EXITDTOR static int ObjCount = 0; // The number of objects + // currently present in the Objects cache. + + CDSPFracDelayFilterBank :: roundReqAtten( ReqAtten, IsThird ); + + R8BSYNC( getStateSync() ); + + if( IsStatic ) + { + CDSPFracDelayFilterBank* PrevObj = R8B_NULL; + CDSPFracDelayFilterBank* CurObj = StaticObjects; + + while( CurObj != R8B_NULL ) + { + if( CurObj -> InitFilterFracs == aFilterFracs && + CurObj -> IsThird == IsThird && + CurObj -> ElementSize == aElementSize && + CurObj -> InterpPoints == aInterpPoints && + CurObj -> ReqAtten == ReqAtten ) + { + if( PrevObj != R8B_NULL ) + { + // Move the object to the top of the list. + + PrevObj -> Next = CurObj -> Next; + CurObj -> Next = StaticObjects.unkeep(); + StaticObjects = CurObj; + } + + return( *CurObj ); + } + + PrevObj = CurObj; + CurObj = CurObj -> Next; + } + + // Create a new filter bank and build it. + + CurObj = new CDSPFracDelayFilterBank( aFilterFracs, aElementSize, + aInterpPoints, ReqAtten, IsThird ); + + // Insert the bank at the start of the list. + + CurObj -> Next = StaticObjects.unkeep(); + StaticObjects = CurObj; + + return( *CurObj ); + } + + CDSPFracDelayFilterBank* PrevObj = R8B_NULL; + CDSPFracDelayFilterBank* CurObj = Objects; + + while( CurObj != R8B_NULL ) + { + if( CurObj -> InitFilterFracs == aFilterFracs && + CurObj -> IsThird == IsThird && + CurObj -> ElementSize == aElementSize && + CurObj -> InterpPoints == aInterpPoints && + CurObj -> ReqAtten == ReqAtten ) + { + break; + } + + if( CurObj -> Next == R8B_NULL && + ObjCount >= R8B_FRACBANK_CACHE_MAX ) + { + if( CurObj -> RefCount == 0 ) + { + // Delete the last bank which is not used. + + PrevObj -> Next = R8B_NULL; + delete CurObj; + ObjCount--; + } + else + { + // Move the last bank to the top of the list since it + // seems to be in use for a long time. + + PrevObj -> Next = R8B_NULL; + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + } + + CurObj = R8B_NULL; + break; + } + + PrevObj = CurObj; + CurObj = CurObj -> Next; + } + + if( CurObj != R8B_NULL ) + { + CurObj -> RefCount++; + + if( PrevObj == R8B_NULL ) + { + return( *CurObj ); + } + + // Remove the bank from the list temporarily. + + PrevObj -> Next = CurObj -> Next; + } + else + { + // Create a new filter bank (with RefCount == 1) and build it. + + CurObj = new CDSPFracDelayFilterBank( aFilterFracs, aElementSize, + aInterpPoints, ReqAtten, IsThird ); + + ObjCount++; + } + + // Insert the bank at the start of the list. + + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + + return( *CurObj ); + } + +protected: + /** + * @brief Returns reference to global filter bank cache sync object. + */ + + static CSyncObject& getStateSync() + { + R8B_EXITDTOR static CSyncObject StateSync; + + return( StateSync ); + } +}; + +// --------------------------------------------------------------------------- +// CDSPFracDelayFilterBank PUBLIC +// --------------------------------------------------------------------------- + +inline void CDSPFracDelayFilterBank :: unref() +{ + R8BSYNC( CDSPFracDelayFilterBankCache :: getStateSync() ); + + RefCount--; +} + +/** + * @brief Interatively searches for a greatest common denominator (GCD) of 2 + * numbers. + * + * @param l Number 1. + * @param s Number 2. + * @param[out] GCD Resulting GCD. + * @return `true` if the greatest common denominator of 2 numbers was found. + */ + +inline bool findGCD( double l, double s, double& GCD ) +{ + int it = 0; + + while( ++it < 150 ) + { + const double r = l - s; + + if( r == 0.0 ) + { + GCD = s; + return( s > 0.0 ); + } + + l = s; + s = fabs( r ); + } + + return( false ); +} + +/** + * @brief Evaluates source and destination sample rate ratio and returns + * the required input and output stepping. + * + * Function returns `false` if whole stepping cannot be used to perform + * interpolation using these sample rates. + * + * @param SSampleRate Source sample rate. + * @param DSampleRate Destination sample rate. + * @param[out] ResInStep Resulting input step. + * @param[out] ResOutStep Resulting output step. + * @return `true` if stepping was acquired. + */ + +inline bool getWholeStepping( const double SSampleRate, + const double DSampleRate, int& ResInStep, int& ResOutStep ) +{ + double GCD; + + if( !findGCD( SSampleRate, DSampleRate, GCD )) + { + return( false ); + } + + const double InStep0 = SSampleRate / GCD; + const double OutStep0 = DSampleRate / GCD; + + if( OutStep0 > 1500.0 ) + { + // Do not allow large output stepping due to low cache + // performance of large filter banks. + + return( false ); + } + + ResInStep = (int) InStep0; + ResOutStep = (int) OutStep0; + + // Check for double-to-int precision and overflows. + + if( InStep0 != ResInStep || OutStep0 != ResOutStep ) + { + return( false ); + } + + return( true ); +} + +/** + * @brief Fractional delay filter bank-based interpolator class. + * + * Class implements the fractional delay interpolator. This implementation at + * first puts the input signal into a ring buffer and then performs + * interpolation. The interpolation is performed using sinc-based fractional + * delay filters. These filters are contained in a bank, and for higher + * precision they are interpolated between adjacent filters. + * + * To increase the sample-timing precision, this class uses "resettable + * counter" approach. This gives zero overall sample-timing error. + */ + +class CDSPFracInterpolator : public CDSPProcessor +{ +public: + /** + * @brief Initalizes the interpolator. It is important to call the + * getMaxOutLen() function afterwards to obtain the optimal output buffer + * length. + * + * @param aSrcSampleRate Source sample rate. + * @param aDstSampleRate Destination sample rate. + * @param ReqAtten Required filter attentuation. + * @param IsThird `true` if one-third filter is required. + * @param PrevLatency Latency, in samples (any non-negative value), which + * was left in the output signal by a previous process. This latency will + * be consumed completely. + */ + + CDSPFracInterpolator( const double aSrcSampleRate, + const double aDstSampleRate, const double ReqAtten, + const bool IsThird, const double PrevLatency ) + : SrcSampleRate( aSrcSampleRate ) + , DstSampleRate( aDstSampleRate ) + , FracStep( aSrcSampleRate / aDstSampleRate ) + { + R8BASSERT( SrcSampleRate > 0.0 ); + R8BASSERT( DstSampleRate > 0.0 ); + R8BASSERT( PrevLatency >= 0.0 ); + R8BASSERT( BufLenBits >= 5 ); + + InitFracPos = PrevLatency; + Latency = (int) InitFracPos; + InitFracPos -= Latency; + + R8BASSERT( Latency >= 0 ); + + #if R8B_FLTTEST + + IsWhole = false; + LatencyFrac = 0.0; + FilterBank = new CDSPFracDelayFilterBank( -1, 3, 8, ReqAtten, + IsThird ); + + #else // R8B_FLTTEST + + IsWhole = getWholeStepping( SrcSampleRate, DstSampleRate, InStep, + OutStep ); + + if( IsWhole ) + { + const double spos = InitFracPos * OutStep; + InitFracPosW = (int) spos; + LatencyFrac = ( spos - InitFracPosW ) / InStep; + + FilterBank = &CDSPFracDelayFilterBankCache :: getFilterBank( + OutStep, 1, 2, ReqAtten, IsThird, false ); + } + else + { + LatencyFrac = 0.0; + FilterBank = &CDSPFracDelayFilterBankCache :: getFilterBank( + -1, 3, 8, ReqAtten, IsThird, true ); + } + + #endif // R8B_FLTTEST + + FilterLen = FilterBank -> getFilterLen(); + fl2 = FilterLen >> 1; + fll = fl2 - 1; + flo = fll + fl2; + flb = BufLen - fll; + + R8BASSERT(( 1 << BufLenBits ) >= FilterLen * 3 ); + + static const CConvolveFn FltConvFn0[ 13 ] = { + &CDSPFracInterpolator :: convolve0< 6 >, + &CDSPFracInterpolator :: convolve0< 8 >, + &CDSPFracInterpolator :: convolve0< 10 >, + &CDSPFracInterpolator :: convolve0< 12 >, + &CDSPFracInterpolator :: convolve0< 14 >, + &CDSPFracInterpolator :: convolve0< 16 >, + &CDSPFracInterpolator :: convolve0< 18 >, + &CDSPFracInterpolator :: convolve0< 20 >, + &CDSPFracInterpolator :: convolve0< 22 >, + &CDSPFracInterpolator :: convolve0< 24 >, + &CDSPFracInterpolator :: convolve0< 26 >, + &CDSPFracInterpolator :: convolve0< 28 >, + &CDSPFracInterpolator :: convolve0< 30 > + }; + + convfn = ( IsWhole ? FltConvFn0[ fl2 - 3 ] : + &CDSPFracInterpolator :: convolve2 ); + + R8BCONSOLE( "CDSPFracInterpolator: src=%.2f dst=%.2f taps=%i " + "fracs=%i whole=%i third=%i step=%.6f\n", SrcSampleRate, + DstSampleRate, FilterLen, ( IsWhole ? OutStep : + FilterBank -> getFilterFracs() ), (int) IsWhole, (int) IsThird, + aSrcSampleRate / aDstSampleRate ); + + clear(); + } + + virtual int getInLenBeforeOutPos( int ReqOutPos ) const + { + if( Next != R8B_NULL ) + { + ReqOutPos = Next -> getInLenBeforeOutPos( ReqOutPos ); + } + + const int ilat = fl2 + Latency; + + if( IsWhole ) + { + return( ilat + (int) (( InitFracPosW + + (double) ReqOutPos * InStep ) / OutStep + + LatencyFrac * InStep / OutStep )); + } + + return( ilat + (int) ( InitFracPos + ReqOutPos * SrcSampleRate / + DstSampleRate )); + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return( (int) ceil( MaxInLen * DstSampleRate / SrcSampleRate ) + 1 ); + } + + virtual void clear() + { + LatencyLeft = Latency; + BufLeft = 0; + WritePos = 0; + ReadPos = flb; // Set "read" position to account for filter's + // latency at zero fractional delay. + + memset( &Buf[ ReadPos ], 0, + (size_t) ( BufLen - flb ) * sizeof( Buf[ 0 ])); + + if( IsWhole ) + { + InPosFracW = InitFracPosW; + } + else + { + InPosFrac = InitFracPos; + InPosInt = 0; + InPosShift = InitFracPos / SrcSampleRate * DstSampleRate; + } + } + + virtual int process( double* ip, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + R8BASSERT( ip != op0 || l == 0 || SrcSampleRate > DstSampleRate ); + + if( LatencyLeft != 0 ) + { + if( LatencyLeft >= l ) + { + LatencyLeft -= l; + return( 0 ); + } + + l -= LatencyLeft; + ip += LatencyLeft; + LatencyLeft = 0; + } + + double* op = op0; + + while( l > 0 ) + { + // Copy new input samples to the ring buffer. + + const int b = min( l, min( BufLen - WritePos, flb - BufLeft )); + + double* const wp1 = Buf + WritePos; + memcpy( wp1, ip, (size_t) b * sizeof( wp1[ 0 ])); + const int ec = flo - WritePos; + + if( ec > 0 ) + { + memcpy( wp1 + BufLen, ip, + (size_t) min( b, ec ) * sizeof( wp1[ 0 ])); + } + + ip += b; + WritePos = ( WritePos + b ) & BufLenMask; + l -= b; + BufLeft += b; + + // Produce as many output samples as possible. + + op = ( *this.*convfn )( op ); + } + + if( !IsWhole && (int) InPosShift > 1000 ) + { + // Reset the interpolation position counter to achieve a higher + // sample-timing precision. + + InPosInt = 0; + InPosShift = InPosFrac / SrcSampleRate * DstSampleRate; + } + + return( (int) ( op - op0 )); + } + +private: + static const int BufLenBits = 8; ///< The length of the ring buffer, + ///< expressed as Nth power of 2. This value can be reduced if it is + ///< known that only short input buffers will be passed to the + ///< interpolator. The minimum value of this parameter is 5, and + ///< `1 << BufLenBits` should be at least 3 times larger than the + ///< FilterLen. However, this condition can be easily met if the input + ///< signal is suitably downsampled first before the interpolation is + ///< performed. + static const int BufLen = 1 << BufLenBits; ///< The length of the ring + ///< buffer. The actual length is longer, to permit "beyond bounds" + ///< positioning. + static const int BufLenMask = BufLen - 1; ///< Mask used for quick buffer + ///< position wrapping. + double Buf[ BufLen + 29 ]; ///< The ring buffer, including overrun + ///< protection for maximal filter length. + double SrcSampleRate; ///< Source sample rate. + double DstSampleRate; ///< Destination sample rate. + double InitFracPos; ///< Initial fractional position, in samples, in the + ///< range [0; 1). + int InitFracPosW; ///< Initial fractional position for whole-number + ///< stepping. + int Latency; ///< Initial latency that should be removed from the input. + double LatencyFrac; ///< Left-over fractional latency on output (always + ///< zero for non-whole stepping). + int FilterLen; ///< Filter length, in taps. Even value. + int fll; ///< Input latency (left-hand filter length). + int fl2; ///< Right-side (half) filter length. + int flo; ///< Overrun length. + int flb; ///< Initial buffer read position. + int InStep; ///< Input whole-number stepping. + int OutStep; ///< Output whole-number stepping (corresponds to filter bank + ///< size). + int LatencyLeft; ///< Input latency left to remove. + int BufLeft; ///< The number of samples left in the buffer to process. + int WritePos; ///< The current buffer write position. Incremented together + ///< with the `BufLeft` variable. + int ReadPos; ///< The current buffer read position. + int InPosFracW; ///< Interpolation position (fractional part) for + ///< whole-number stepping. Corresponds to the index into the filter + ///< bank. + int InPosInt; ///< Interpolation position (integer part). + double InPosFrac; ///< Interpolation position (fractional part). + double FracStep; ///< Fractional sample-timing step. + double InPosShift; ///< Interpolation position's shift. + + #if !R8B_FLTTEST + CRefKeeper< CDSPFracDelayFilterBank > FilterBank; ///< Filter bank in use, + ///< may be whole-number stepping filter bank or static bank. + #else // !R8B_FLTTEST + CPtrKeeper< CDSPFracDelayFilterBank > FilterBank; + #endif // !R8B_FLTTEST + + bool IsWhole; ///< `true` if whole-number stepping is in use. + + typedef double*( CDSPFracInterpolator ::* CConvolveFn )( double* op ); ///< + ///< Convolution function type. + CConvolveFn convfn; ///< Convolution function in use. + + /** + * @brief Convolution function for 0th order resampling. + * + * @param[out] op Output buffer. + * @tparam fltlen Filter length, in taps. + * @return Advanced "op" value. + */ + + template< int fltlen > + double* convolve0( double* op ) + { + const CDSPFracDelayFilterBank& fb = *FilterBank; + const int istep = InStep; + const int ostep = OutStep; + int fpos = InPosFracW; + int rpos = ReadPos; + int bl = BufLeft - fl2; + + while( bl > 0 ) + { + const double* const ftp = &fb[ fpos ]; + const double* const rp = Buf + rpos; + int i; + + #if defined( R8B_SSE2 ) && !defined( __INTEL_COMPILER ) + + __m128d s = _mm_setzero_pd(); + + for( i = 0; i < fltlen; i += 2 ) + { + const __m128d m = _mm_mul_pd( _mm_load_pd( ftp + i ), + _mm_loadu_pd( rp + i )); + + s = _mm_add_pd( s, m ); + } + + _mm_storel_pd( op, _mm_add_pd( s, _mm_shuffle_pd( s, s, 1 ))); + + #elif defined( R8B_NEON ) + + float64x2_t s = vdupq_n_f64( 0.0 ); + + for( i = 0; i < fltlen; i += 2 ) + { + s = vmlaq_f64( s, vld1q_f64( ftp + i ), vld1q_f64( rp + i )); + } + + *op = vaddvq_f64( s ); + + #else // SIMD + + double s = 0.0; + + for( i = 0; i < fltlen; i++ ) + { + s += ftp[ i ] * rp[ i ]; + } + + *op = s; + + #endif // SIMD + + op++; + + fpos += istep; + const int PosIncr = fpos / ostep; + fpos -= PosIncr * ostep; + + rpos = ( rpos + PosIncr ) & BufLenMask; + bl -= PosIncr; + } + + BufLeft = bl + fl2; + ReadPos = rpos; + InPosFracW = fpos; + + return( op ); + } + + /** + * @brief Convolution function for 2nd order resampling. + * + * @param[out] op Output buffer. + * @return Advanced "op" value. + */ + + double* convolve2( double* op ) + { + const CDSPFracDelayFilterBank& fb = *FilterBank; + const int fltlen = FilterLen; + const double ffracs = fb.getFilterFracs(); + const double fs = FracStep; + int ipos = InPosInt; + double fpos = InPosFrac; + double psh = InPosShift; + int rpos = ReadPos; + int bl = BufLeft - fl2; + + while( bl > 0 ) + { + double x = fpos * ffracs; + const int fti = (int) x; // Function table index. + const double* ftp = &fb[ fti ]; + x -= fti; // Coefficient for interpolation between adjacent + // fractional delay filters. + const double* const rp = Buf + rpos; + const double x2d = x * x; + int i; + + #if defined( R8B_SSE2 ) && defined( R8B_SIMD_ISH ) + + const __m128d x1 = _mm_set1_pd( x ); + const __m128d x2 = _mm_set1_pd( x2d ); + __m128d s = _mm_setzero_pd(); + + for( i = 0; i < fltlen; i += 2 ) + { + const __m128d ftp2 = _mm_load_pd( ftp + 2 ); + const __m128d xx1 = _mm_mul_pd( ftp2, x1 ); + const __m128d ftp4 = _mm_load_pd( ftp + 4 ); + const __m128d xx2 = _mm_mul_pd( ftp4, x2 ); + const __m128d ftp0 = _mm_load_pd( ftp ); + ftp += 6; + + const __m128d rpi = _mm_loadu_pd( rp + i ); + const __m128d xxs = _mm_add_pd( ftp0, _mm_add_pd( xx1, xx2 )); + + s = _mm_add_pd( s, _mm_mul_pd( rpi, xxs )); + } + + _mm_storel_pd( op, _mm_add_pd( s, _mm_shuffle_pd( s, s, 1 ))); + + #elif defined( R8B_NEON ) && defined( R8B_SIMD_ISH ) + + const float64x2_t x1 = vdupq_n_f64( x ); + const float64x2_t x2 = vdupq_n_f64( x2d ); + float64x2_t s = vdupq_n_f64( 0.0 ); + + for( i = 0; i < fltlen; i += 2 ) + { + const float64x2_t ftp2 = vld1q_f64( ftp + 2 ); + const float64x2_t xx1 = vmulq_f64( ftp2, x1 ); + const float64x2_t ftp4 = vld1q_f64( ftp + 4 ); + const float64x2_t xx2 = vmulq_f64( ftp4, x2 ); + const float64x2_t ftp0 = vld1q_f64( ftp ); + ftp += 6; + + const float64x2_t rpi = vld1q_f64( rp + i ); + const float64x2_t xxs = vaddq_f64( ftp0, + vaddq_f64( xx1, xx2 )); + + s = vmlaq_f64( s, rpi, xxs ); + } + + *op = vaddvq_f64( s ); + + #else // SIMD + + double s = 0.0; + + for( i = 0; i < fltlen; i++ ) + { + s += ( ftp[ 0 ] + ftp[ 1 ] * x + ftp[ 2 ] * x2d ) * rp[ i ]; + ftp += 3; + } + + *op = s; + + #endif // SIMD + + psh += 1.0; + op++; + + const double NextInPos = psh * fs; + const int NextInPosInt = (int) NextInPos; + const int PosIncr = NextInPosInt - ipos; + + fpos = NextInPos - NextInPosInt; + ipos = NextInPosInt; + + bl -= PosIncr; + rpos = ( rpos + PosIncr ) & BufLenMask; + } + + BufLeft = bl + fl2; + ReadPos = rpos; + InPosInt = ipos; + InPosFrac = fpos; + InPosShift = psh; + + return( op ); + } +}; + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPFRACINTERPOLATOR_INCLUDED diff --git a/native/vendor/r8brain/CDSPHBDownsampler.h b/native/vendor/r8brain/CDSPHBDownsampler.h new file mode 100644 index 0000000..140734d --- /dev/null +++ b/native/vendor/r8brain/CDSPHBDownsampler.h @@ -0,0 +1,309 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPHBDownsampler.h + * + * @brief Half-band downsampling convolver class. + * + * This file includes half-band downsampling convolver class. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPHBDOWNSAMPLER_INCLUDED +#define R8B_CDSPHBDOWNSAMPLER_INCLUDED + +#include "CDSPHBUpsampler.h" + +namespace r8b { + +/** + * @brief Half-band downsampler class. + * + * Class implements brute-force half-band 2X downsampling that uses small + * sparse symmetric FIR filters. The output has 2.0 gain. + */ + +class CDSPHBDownsampler : public CDSPProcessor +{ +public: + /** + * @brief Initalizes the half-band downsampler. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * downsampling ratio, e.g., at 4x downsampling 0 is used, at 8x + * downsampling 1 is used, etc. + * @param IsThird `true` if 1/3 resampling is performed. + * @param PrevLatency Latency, in samples (any non-negative value), which + * was left in the output signal by a previous process. Whole-number + * latency will be consumed by *this* object while remaining fractional + * latency can be obtained via the getLatencyFrac() function. + */ + + CDSPHBDownsampler( const double ReqAtten, const int SteepIndex, + const bool IsThird, const double PrevLatency ) + { + static const CConvolveFn FltConvFn[ 14 ] = { + &CDSPHBDownsampler :: convolve1, &CDSPHBDownsampler :: convolve2, + &CDSPHBDownsampler :: convolve3, &CDSPHBDownsampler :: convolve4, + &CDSPHBDownsampler :: convolve5, &CDSPHBDownsampler :: convolve6, + &CDSPHBDownsampler :: convolve7, &CDSPHBDownsampler :: convolve8, + &CDSPHBDownsampler :: convolve9, &CDSPHBDownsampler :: convolve10, + &CDSPHBDownsampler :: convolve11, &CDSPHBDownsampler :: convolve12, + &CDSPHBDownsampler :: convolve13, + &CDSPHBDownsampler :: convolve14 }; + + const double* fltp0; + int fltt; + double att; + + if( IsThird ) + { + CDSPHBUpsampler :: getHBFilterThird( ReqAtten, SteepIndex, fltp0, + fltt, att ); + } + else + { + CDSPHBUpsampler :: getHBFilter( ReqAtten, SteepIndex, fltp0, fltt, + att ); + } + + // Copy obtained filter to address-aligned buffer. + + fltp = align_ptr( FltBuf, 16 ); + memcpy( fltp, fltp0, (size_t) fltt * sizeof( fltp[ 0 ])); + + convfn = FltConvFn[ fltt - 1 ]; + fll = fltt; + fl2 = fltt - 1; + flo = fll + fl2; + flb = BufLen - fll; + BufRP = Buf + fll; + + LatencyFrac = PrevLatency * 0.5; + Latency = (int) LatencyFrac; + LatencyFrac -= Latency; + + R8BASSERT( Latency >= 0 ); + + R8BCONSOLE( "CDSPHBDownsampler: taps=%i third=%i att=%.1f io=1/2\n", + fltt, (int) IsThird, att ); + + clear(); + } + + virtual int getInLenBeforeOutPos( int ReqOutPos ) const + { + if( Next != R8B_NULL ) + { + ReqOutPos = Next -> getInLenBeforeOutPos( ReqOutPos ); + } + + return( flo + (int) (( Latency + LatencyFrac + ReqOutPos ) * 2.0 )); + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return(( MaxInLen + 1 ) >> 1 ); + } + + virtual void clear() + { + LatencyLeft = Latency; + BufLeft = 0; + WritePos1 = 0; + WritePos2 = 0; + ReadPos = flb; // Set "read" position to account for filter's latency. + + memset( &Buf[ ReadPos ], 0, + (size_t) ( BufLen - flb ) * sizeof( Buf[ 0 ])); + + memset( &Buf[ BufLenPad + ReadPos ], 0, + (size_t) ( BufLen - flb ) * sizeof( Buf[ 0 ])); + } + + virtual int process( double* ip, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + + double* op = op0; + + while( l > 0 ) + { + // Copy new input samples to 2 ring buffers. + + if( WritePos1 != WritePos2 ) + { + // If previous fill was asymmetrical, put a single sample to + // the second buffer. + + double* const wp2 = Buf + BufLenPad + WritePos2; + *wp2 = *ip; + + if( WritePos2 < flo ) + { + wp2[ BufLen ] = *ip; + } + + ip++; + WritePos2 = WritePos1; + l--; + BufLeft++; + } + + const int b1 = min(( l + 1 ) >> 1, + min( BufLen - WritePos1, flb - BufLeft )); + + const int b2 = b1 - ( b1 * 2 > l ); + + double* const wp1 = Buf + WritePos1; + double* const wp2 = Buf + BufLenPad + WritePos1; + int i; + + for( i = 0; i < b2; i++ ) + { + wp1[ i ] = ip[ 0 ]; + wp2[ i ] = ip[ 1 ]; + ip += 2; + } + + if( b1 != b2 ) + { + wp1[ b2 ] = *ip; + ip++; + } + + const int ec = flo - WritePos1; + + if( ec > 0 ) + { + memcpy( wp1 + BufLen, wp1, + (size_t) min( b1, ec ) * sizeof( wp1[ 0 ])); + + memcpy( wp2 + BufLen, wp2, + (size_t) min( b2, ec ) * sizeof( wp2[ 0 ])); + } + + WritePos1 = ( WritePos1 + b1 ) & BufLenMask; + WritePos2 = ( WritePos2 + b2 ) & BufLenMask; + l -= b1 + b2; + BufLeft += b2; + + // Produce output. + + int c = BufLeft - fl2; + + while( c > 0 ) + { + const int cb = min( c, BufLen - ReadPos ); + double* const opend = op + cb; + ( *convfn )( op, opend, fltp, BufRP + ReadPos ); + + op = opend; + ReadPos = ( ReadPos + cb ) & BufLenMask; + BufLeft -= cb; + c -= cb; + } + } + + int ol = (int) ( op - op0 ); + + if( LatencyLeft != 0 ) + { + if( LatencyLeft >= ol ) + { + LatencyLeft -= ol; + return( 0 ); + } + + ol -= LatencyLeft; + op0 += LatencyLeft; + LatencyLeft = 0; + } + + return( ol ); + } + +private: + static const int BufLenBits = 10; ///< The length of the ring buffer, + ///< expressed as Nth power of 2. This value can be reduced if it is + ///< known that only short input buffers will be passed to the + ///< interpolator. The minimum value of this parameter is 5, and + ///< `1 << BufLenBits` should be at least 3 times larger than the + ///< FilterLen. + static const int BufLen = 1 << BufLenBits; ///< The length of the ring + ///< buffer. The actual length is longer, to permit "beyond bounds" + ///< positioning. + static const int BufLenMask = BufLen - 1; ///< The mask used for quick + ///< buffer position wrapping. + static const int FltBufLen = 14; ///< The length of the `FltBuf` able to + ///< hold the largest filter. + static const int BufLenPad = BufLen + ( FltBufLen * 2 - 1 ); ///< The + ///< length of the ring buffer including overrun protection for the + ///< largest filter. + double Buf[ BufLenPad * 2 ]; ///< The ring buffers 1 and 2, spaced + ///< `BufLenPad` elements apart. + double FltBuf[ FltBufLen + 2 ]; ///< Holder for half-band filter taps, + ///< used with 16-byte address-aligning, for SIMD use. + const double* BufRP; ///< Offseted Buf pointer at `ReadPos` equal 0. + double* fltp; ///< Half-band filter taps, points to `FltBuf`. + double LatencyFrac; ///< Fractional latency left on the output. + int Latency; ///< Initial latency that should be removed from the output. + int fll; ///< Input latency (left-hand filter length). + int fl2; ///< Right-side filter length. + int flo; ///< Overrun length. + int flb; ///< Initial buffer read position. + int LatencyLeft; ///< Latency left to remove. + int BufLeft; ///< The number of samples left in the buffer to process. + ///< When this value is below `fl2`, the interpolation cycle ends. + int WritePos1; ///< The current buffer 1 write position. + int WritePos2; ///< The current buffer 2 write position. Incremented + ///< together with the `BufLeft` variable. + int ReadPos; ///< The current buffer read position. + + typedef void( *CConvolveFn )( double* op, double* const opend, + const double* const flt, const double* rp1 ); ///< + ///< Convolution function type. + CConvolveFn convfn; ///< Convolution function in use. + +#define R8BHBC1( fn ) \ + static void fn( double* op, double* const opend, const double* const flt, \ + const double* rp1 ) \ + { \ + while( op != opend ) \ + { \ + const double* const rp = rp1 + BufLenPad - 1; + +#define R8BHBC2 \ + op++; \ + rp1++; \ + } \ + } + +#include "CDSPHBDownsampler.inc" + +#undef R8BHBC1 +#undef R8BHBC2 +}; + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPHBDOWNSAMPLER_INCLUDED diff --git a/native/vendor/r8brain/CDSPHBDownsampler.inc b/native/vendor/r8brain/CDSPHBDownsampler.inc new file mode 100644 index 0000000..93aa511 --- /dev/null +++ b/native/vendor/r8brain/CDSPHBDownsampler.inc @@ -0,0 +1,718 @@ +// Auto-generated by `genhbc`, do not edit! + +#if defined( R8B_SSE2 ) + +R8BHBC1( convolve1 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); +R8BHBC2 + +R8BHBC1( convolve2 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve3 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ] + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); +R8BHBC2 + +R8BHBC1( convolve4 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve5 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ] + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); +R8BHBC2 + +R8BHBC1( convolve6 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve7 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ] + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); +R8BHBC2 + +R8BHBC1( convolve8 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve9 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ] + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); +R8BHBC2 + +R8BHBC1( convolve10 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve11 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ] + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); +R8BHBC2 + +R8BHBC1( convolve12 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 11 ); v3 = _mm_loadu_pd( rp + 11 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 10 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve13 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 11 ); v3 = _mm_loadu_pd( rp + 11 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 10 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ] + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); +R8BHBC2 + +R8BHBC1( convolve14 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 11 ); v3 = _mm_loadu_pd( rp + 11 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 10 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 13 ); v1 = _mm_loadu_pd( rp + 13 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 12 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 0 ] += rp1[ 0 ]; +R8BHBC2 + +#elif defined( R8B_NEON ) + +R8BHBC1( convolve1 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); +R8BHBC2 + +R8BHBC1( convolve2 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve3 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ] + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); +R8BHBC2 + +R8BHBC1( convolve4 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve5 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ] + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); +R8BHBC2 + +R8BHBC1( convolve6 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve7 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ] + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); +R8BHBC2 + +R8BHBC1( convolve8 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve9 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ] + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); +R8BHBC2 + +R8BHBC1( convolve10 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve11 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ] + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); +R8BHBC2 + +R8BHBC1( convolve12 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 11 ); v3 = vld1q_f64( rp + 11 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 10 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +R8BHBC1( convolve13 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 11 ); v3 = vld1q_f64( rp + 11 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 10 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ] + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); +R8BHBC2 + +R8BHBC1( convolve14 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 11 ); v3 = vld1q_f64( rp + 11 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 10 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 13 ); v1 = vld1q_f64( rp + 13 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 12 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 0 ] = vaddvq_f64( s1 ) + rp1[ 0 ]; +R8BHBC2 + +#else // SIMD + +R8BHBC1( convolve1 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); +R8BHBC2 + +R8BHBC1( convolve2 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]); +R8BHBC2 + +R8BHBC1( convolve3 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); +R8BHBC2 + +R8BHBC1( convolve4 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]); +R8BHBC2 + +R8BHBC1( convolve5 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); +R8BHBC2 + +R8BHBC1( convolve6 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]); +R8BHBC2 + +R8BHBC1( convolve7 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); +R8BHBC2 + +R8BHBC1( convolve8 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]); +R8BHBC2 + +R8BHBC1( convolve9 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); +R8BHBC2 + +R8BHBC1( convolve10 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]); +R8BHBC2 + +R8BHBC1( convolve11 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); +R8BHBC2 + +R8BHBC1( convolve12 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]); +R8BHBC2 + +R8BHBC1( convolve13 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]) + + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); +R8BHBC2 + +R8BHBC1( convolve14 ) +op[ 0 ] = rp1[ 0 ] + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]) + + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]) + + flt[ 13 ] * ( rp[ 14 ] + rp[ -13 ]); +R8BHBC2 + +#endif // SIMD diff --git a/native/vendor/r8brain/CDSPHBUpsampler.h b/native/vendor/r8brain/CDSPHBUpsampler.h new file mode 100644 index 0000000..fc33ff0 --- /dev/null +++ b/native/vendor/r8brain/CDSPHBUpsampler.h @@ -0,0 +1,843 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPHBUpsampler.h + * + * @brief Half-band upsampling class. + * + * This file includes half-band upsampling class. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPHBUPSAMPLER_INCLUDED +#define R8B_CDSPHBUPSAMPLER_INCLUDED + +#include "CDSPProcessor.h" + +namespace r8b { + +/** + * @brief Half-band upsampling class. + * + * Class implements brute-force half-band 2X upsampling that uses small + * sparse symmetric FIR filters. It is very efficient and should be used at + * latter upsampling steps after initial steep 2X upsampling. + */ + +class CDSPHBUpsampler : public CDSPProcessor +{ +public: + /** + * @brief Provides filter data for various steepness indices and + * attenuations. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * upsampling/downsampling ratio, e.g., at 4x 0 is used, at 8x 1 is used, + * etc. + * @param[out] flt Resulting pointer to filter taps. + * @param[out] fltt Resulting filter's half-length, in samples (taps). + * @param[out] att Resulting filter's attenuation (the closest found). + */ + + static void getHBFilter( const double ReqAtten, const int SteepIndex, + const double*& flt, int& fltt, double& att ) + { + static const double HBKernel_4A[ 4 ] = { // -54.5176 dB, 4 + 6.1729335650971517e-001, -1.5963945620743250e-001, + 5.5073370934086312e-002, -1.4603578989932850e-002,}; + static const double HBKernel_5A[ 5 ] = { // -66.3075 dB, 4 + 6.2068807424902472e-001, -1.6827573634467302e-001, + 6.5263016720721170e-002, -2.2483331611592005e-002, + 5.2917326684281110e-003,}; + static const double HBKernel_6A[ 6 ] = { // -89.5271 dB, 4 + 6.2187202340480707e-001, -1.7132842113816371e-001, + 6.9019169178765674e-002, -2.5799728312695277e-002, + 7.4880112525741666e-003, -1.2844465869952567e-003,}; + static const double HBKernel_7A[ 7 ] = { // -105.2842 dB, 4 + 6.2354494135775851e-001, -1.7571220703702045e-001, + 7.4529843603968457e-002, -3.0701736822442153e-002, + 1.0716755639039573e-002, -2.7833422930759735e-003, + 4.1118797093875510e-004,}; + static const double HBKernel_8A[ 8 ] = { // -121.0063 dB, 4 + 6.2488363107953926e-001, -1.7924942606514119e-001, + 7.9068155655640557e-002, -3.4907523415495731e-002, + 1.3710256799907897e-002, -4.3991142586987933e-003, + 1.0259190163889602e-003, -1.3278941979339359e-004,}; + static const double HBKernel_9A[ 9 ] = { // -136.6982 dB, 4 + 6.2597763804021977e-001, -1.8216414325139055e-001, + 8.2879104876726728e-002, -3.8563442248249404e-002, + 1.6471530499739394e-002, -6.0489108881335227e-003, + 1.7805283804140392e-003, -3.7533200112729561e-004, + 4.3172840558735476e-005,}; + static const double HBKernel_10A[ 10 ] = { // -152.3572 dB, 4 + 6.2688767582974092e-001, -1.8460766807559420e-001, + 8.6128943000481864e-002, -4.1774474147006607e-002, + 1.9014801985747346e-002, -7.6870397465866507e-003, + 2.6264590175341853e-003, -7.1106660285478562e-004, + 1.3645852036179345e-004, -1.4113888783332969e-005,}; + static const double HBKernel_11A[ 11 ] = { // -183.7962 dB, 4 + 6.2667167706948146e-001, -1.8407153342635879e-001, + 8.5529995610836046e-002, -4.1346831462361310e-002, + 1.8844831691322637e-002, -7.7125170365394992e-003, + 2.7268674860562087e-003, -7.9745028501057233e-004, + 1.8116344606360795e-004, -2.8569149754241848e-005, + 2.3667022010173616e-006,}; + static const double HBKernel_12A[ 12 ] = { // -199.4768 dB, 4 + 6.2747849730367999e-001, -1.8623616784506747e-001, + 8.8409755898467945e-002, -4.4207468821462342e-002, + 2.1149175945115381e-002, -9.2551508371115209e-003, + 3.5871562170822330e-003, -1.1923167653750219e-003, + 3.2627812189920129e-004, -6.9106902511490413e-005, + 1.0122897863125124e-005, -7.7531878906846174e-007,}; + static const double HBKernel_13A[ 13 ] = { // -215.1364 dB, 4 + 6.2816416252367324e-001, -1.8809076955230414e-001, + 9.0918539867353029e-002, -4.6765502683599310e-002, + 2.3287520498995663e-002, -1.0760627245014184e-002, + 4.4853922948425683e-003, -1.6438775426910800e-003, + 5.1441312354764978e-004, -1.3211725685765050e-004, + 2.6191319837779187e-005, -3.5802430606313093e-006, + 2.5491278270628601e-007,}; + static const double HBKernel_14A[ 14 ] = { // -230.7526 dB, 4 + 6.2875473120929948e-001, -1.8969941936903847e-001, + 9.3126094480960403e-002, -4.9067251179869126e-002, + 2.5273008851199916e-002, -1.2218646153393291e-002, + 5.4048942085580280e-003, -2.1409919546078581e-003, + 7.4250292812927973e-004, -2.1924542206832172e-004, + 5.3015808983125091e-005, -9.8743034923598196e-006, + 1.2650391141650221e-006, -8.4146674637474946e-008,}; + static const int FltCountA = 11; + static const int FlttBaseA = 4; + static const double FltAttensA[ FltCountA ] = { + 54.5176, 66.3075, 89.5271, 105.2842, 121.0063, 136.6982, 152.3572, 183.7962, 199.4768, 215.1364, 230.7526, }; + static const double* const FltPtrsA[ FltCountA ] = { + HBKernel_4A, HBKernel_5A, HBKernel_6A, HBKernel_7A, HBKernel_8A, HBKernel_9A, HBKernel_10A, HBKernel_11A, HBKernel_12A, HBKernel_13A, HBKernel_14A, }; + static const double HBKernel_2B[ 2 ] = { // -56.6007 dB, 8 + 5.7361525854329076e-001, -7.5092074924827903e-002,}; + static const double HBKernel_3B[ 3 ] = { // -83.0295 dB, 8 + 5.9277038608066912e-001, -1.0851340190268854e-001, + 1.5813570475513079e-002,}; + static const double HBKernel_4B[ 4 ] = { // -123.4724 dB, 8 + 6.0140277542879617e-001, -1.2564483854574138e-001, + 2.7446500598038322e-002, -3.2051079559057435e-003,}; + static const double HBKernel_5B[ 5 ] = { // -152.4411 dB, 8 + 6.0818642429088932e-001, -1.3981140187175697e-001, + 3.8489164054503623e-002, -7.6218861797853104e-003, + 7.5772358130952392e-004,}; + static const double HBKernel_6B[ 6 ] = { // -181.2501 dB, 8 + 6.1278392271464355e-001, -1.5000053762513338e-001, + 4.7575323511364960e-002, -1.2320702802243476e-002, + 2.1462442592348487e-003, -1.8425092381892940e-004,}; + static const double HBKernel_7B[ 7 ] = { // -209.9472 dB, 8 + 6.1610372263478952e-001, -1.5767891882524138e-001, + 5.5089691170294691e-002, -1.6895755656366061e-002, + 3.9416643438213977e-003, -6.0603623791604668e-004, + 4.5632602433393365e-005,}; + static const double HBKernel_8B[ 8 ] = { // -238.5616 dB, 8 + 6.1861282914465976e-001, -1.6367179451225150e-001, + 6.1369861342939716e-002, -2.1184466539006987e-002, + 5.9623357510842061e-003, -1.2483098507454090e-003, + 1.7099297537964702e-004, -1.1448313239478885e-005,}; + static const int FltCountB = 7; + static const int FlttBaseB = 2; + static const double FltAttensB[ FltCountB ] = { + 56.6007, 83.0295, 123.4724, 152.4411, 181.2501, 209.9472, 238.5616, }; + static const double* const FltPtrsB[ FltCountB ] = { + HBKernel_2B, HBKernel_3B, HBKernel_4B, HBKernel_5B, HBKernel_6B, HBKernel_7B, HBKernel_8B, }; + static const double HBKernel_2C[ 2 ] = { // -89.0473 dB, 16 + 5.6430278013478008e-001, -6.4338068855763375e-002,}; + static const double HBKernel_3C[ 3 ] = { // -130.8951 dB, 16 + 5.8706402915551448e-001, -9.9362380958670449e-002, + 1.2298637065869358e-002,}; + static const double HBKernel_4C[ 4 ] = { // -172.3192 dB, 16 + 5.9896586134984675e-001, -1.2111680603434927e-001, + 2.4763118076458895e-002, -2.6121758132212989e-003,}; + static const double HBKernel_5C[ 5 ] = { // -213.4984 dB, 16 + 6.0626808285230716e-001, -1.3588224032740795e-001, + 3.5544305238309003e-002, -6.5127022377289654e-003, + 5.8255449565950768e-004,}; + static const double HBKernel_6C[ 6 ] = { // -254.5186 dB, 16 + 6.1120171263351242e-001, -1.4654486853757870e-001, + 4.4582959299131253e-002, -1.0840543858123995e-002, + 1.7343706485509962e-003, -1.3363018567985596e-004,}; + static const int FltCountC = 5; + static const int FlttBaseC = 2; + static const double FltAttensC[ FltCountC ] = { + 89.0473, 130.8951, 172.3192, 213.4984, 254.5186, }; + static const double* const FltPtrsC[ FltCountC ] = { + HBKernel_2C, HBKernel_3C, HBKernel_4C, HBKernel_5C, HBKernel_6C, }; + static const double HBKernel_1D[ 1 ] = { // -54.4754 dB, 32 + 5.0188900022775451e-001,}; + static const double HBKernel_2D[ 2 ] = { // -113.2139 dB, 32 + 5.6295152180538044e-001, -6.2953706070191726e-002,}; + static const double HBKernel_3D[ 3 ] = { // -167.1447 dB, 32 + 5.8621968728755036e-001, -9.8080551656524531e-002, + 1.1860868761997080e-002,}; + static const double HBKernel_4D[ 4 ] = { // -220.6519 dB, 32 + 5.9835028657163591e-001, -1.1999986086623511e-001, + 2.4132530854004228e-002, -2.4829565686819706e-003,}; + static const int FltCountD = 4; + static const int FlttBaseD = 1; + static const double FltAttensD[ FltCountD ] = { + 54.4754, 113.2139, 167.1447, 220.6519, }; + static const double* const FltPtrsD[ FltCountD ] = { + HBKernel_1D, HBKernel_2D, HBKernel_3D, HBKernel_4D, }; + static const double HBKernel_1E[ 1 ] = { // -66.5391 dB, 64 + 5.0047102586416625e-001,}; + static const double HBKernel_2E[ 2 ] = { // -137.3173 dB, 64 + 5.6261293163933568e-001, -6.2613067826620017e-002,}; + static const double HBKernel_3E[ 3 ] = { // -203.2997 dB, 64 + 5.8600808139396787e-001, -9.7762185880067784e-002, + 1.1754104554493029e-002,}; + static const double HBKernel_4E[ 4 ] = { // -268.8550 dB, 64 + 5.9819599352772002e-001, -1.1972157555011861e-001, + 2.3977305567947922e-002, -2.4517235455853992e-003,}; + static const int FltCountE = 4; + static const int FlttBaseE = 1; + static const double FltAttensE[ FltCountE ] = { + 66.5391, 137.3173, 203.2997, 268.8550, }; + static const double* const FltPtrsE[ FltCountE ] = { + HBKernel_1E, HBKernel_2E, HBKernel_3E, HBKernel_4E, }; + static const double HBKernel_1F[ 1 ] = { // -82.4633 dB, 128 + 5.0007530666642896e-001,}; + static const double HBKernel_2F[ 2 ] = { // -161.4049 dB, 128 + 5.6252823610146030e-001, -6.2528244608044792e-002,}; + static const double HBKernel_3F[ 3 ] = { // -239.4313 dB, 128 + 5.8595514744674237e-001, -9.7682725156791952e-002, + 1.1727577711117231e-002,}; + static const int FltCountF = 3; + static const int FlttBaseF = 1; + static const double FltAttensF[ FltCountF ] = { + 82.4633, 161.4049, 239.4313, }; + static const double* const FltPtrsF[ FltCountF ] = { + HBKernel_1F, HBKernel_2F, HBKernel_3F, }; + static const double HBKernel_1G[ 1 ] = { // -94.5052 dB, 256 + 5.0001882524896712e-001,}; + static const double HBKernel_2G[ 2 ] = { // -185.4886 dB, 256 + 5.6250705922479682e-001, -6.2507059756378394e-002,}; + static const double HBKernel_3G[ 3 ] = { // -275.5501 dB, 256 + 5.8594191201187384e-001, -9.7662868266991207e-002, + 1.1720956255134043e-002,}; + static const int FltCountG = 3; + static const int FlttBaseG = 1; + static const double FltAttensG[ FltCountG ] = { + 94.5052, 185.4886, 275.5501, }; + static const double* const FltPtrsG[ FltCountG ] = { + HBKernel_1G, HBKernel_2G, HBKernel_3G, }; + + int k = 0; + + if( SteepIndex <= 0 ) + { + while( k != FltCountA - 1 && FltAttensA[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsA[ k ]; + fltt = FlttBaseA + k; + att = FltAttensA[ k ]; + } + else + if( SteepIndex == 1 ) + { + while( k != FltCountB - 1 && FltAttensB[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsB[ k ]; + fltt = FlttBaseB + k; + att = FltAttensB[ k ]; + } + else + if( SteepIndex == 2 ) + { + while( k != FltCountC - 1 && FltAttensC[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsC[ k ]; + fltt = FlttBaseC + k; + att = FltAttensC[ k ]; + } + else + if( SteepIndex == 3 ) + { + while( k != FltCountD - 1 && FltAttensD[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsD[ k ]; + fltt = FlttBaseD + k; + att = FltAttensD[ k ]; + } + else + if( SteepIndex == 4 ) + { + while( k != FltCountE - 1 && FltAttensE[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsE[ k ]; + fltt = FlttBaseE + k; + att = FltAttensE[ k ]; + } + else + if( SteepIndex == 5 ) + { + while( k != FltCountF - 1 && FltAttensF[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsF[ k ]; + fltt = FlttBaseF + k; + att = FltAttensF[ k ]; + } + else + { + while( k != FltCountG - 1 && FltAttensG[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsG[ k ]; + fltt = FlttBaseG + k; + att = FltAttensG[ k ]; + } + } + + /** + * @brief Provides filter data for various steepness indices and + * attenuations. For 1/3 resamplings. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * upsampling/downsampling ratio, e.g., at 4x 0 is used, at 8x 1 is used, + * etc. + * @param[out] flt Resulting pointer to filter taps. + * @param[out] fltt Resulting filter's half-length, in samples (taps). + * @param[out] att Resulting filter's attenuation (the closest found). + */ + + static void getHBFilterThird( const double ReqAtten, const int SteepIndex, + const double*& flt, int& fltt, double& att ) + { + static const double HBKernel_3A[ 3 ] = { // -66.3726 dB, 6 + 5.9811355069551475e-001, -1.1793396656733847e-001, + 2.0300557211946322e-002,}; + static const double HBKernel_4A[ 4 ] = { // -90.2546 dB, 6 + 6.0645499250612578e-001, -1.3555496505481171e-001, + 3.4022804962365975e-002, -4.9535418595798757e-003,}; + static const double HBKernel_5A[ 5 ] = { // -126.5507 dB, 6 + 6.1014115058940210e-001, -1.4393081816629907e-001, + 4.1760642892852244e-002, -8.9692183234056175e-003, + 9.9871340618342070e-004,}; + static const double HBKernel_6A[ 6 ] = { // -150.1839 dB, 6 + 6.1439563420546972e-001, -1.5360187826905250e-001, + 5.0840891345687034e-002, -1.4053648740561121e-002, + 2.6771286587305727e-003, -2.5815816044823123e-004,}; + static const double HBKernel_7A[ 7 ] = { // -173.7068 dB, 6 + 6.1747493476329918e-001, -1.6087373733313212e-001, + 5.8263075641409430e-002, -1.8872408173431318e-002, + 4.7421376543513687e-003, -8.0196529612267474e-004, + 6.7964807393798996e-005,}; + static const double HBKernel_8A[ 8 ] = { // -197.1454 dB, 6 + 6.1980610947775050e-001, -1.6654070578314714e-001, + 6.4416567441730327e-002, -2.3307744348719822e-002, + 6.9909157372312443e-003, -1.5871946293364403e-003, + 2.4017727382382763e-004, -1.8125308241541697e-005,}; + static const double HBKernel_9A[ 9 ] = { // -220.5199 dB, 6 + 6.2163188951899306e-001, -1.7108115323810941e-001, + 6.9588370095600260e-002, -2.7339625080613838e-002, + 9.2954469183791771e-003, -2.5537179959555429e-003, + 5.2572290897951021e-004, -7.1813356135154921e-005, + 4.8802382808892154e-006,}; + static const int FltCountA = 7; + static const int FlttBaseA = 3; + static const double FltAttensA[ FltCountA ] = { + 66.3726, 90.2546, 126.5507, 150.1839, 173.7068, 197.1454, 220.5199, }; + static const double* const FltPtrsA[ FltCountA ] = { + HBKernel_3A, HBKernel_4A, HBKernel_5A, HBKernel_6A, HBKernel_7A, HBKernel_8A, HBKernel_9A, }; + static const double HBKernel_2B[ 2 ] = { // -71.0965 dB, 12 + 5.6748544264806311e-001, -6.7764090509431732e-002,}; + static const double HBKernel_3B[ 3 ] = { // -115.7707 dB, 12 + 5.8793612182667199e-001, -1.0070583248877293e-001, + 1.2771337947163834e-002,}; + static const double HBKernel_4B[ 4 ] = { // -152.1535 dB, 12 + 5.9960155600862808e-001, -1.2228154335199336e-001, + 2.5433718917694709e-002, -2.7537562530837154e-003,}; + static const double HBKernel_5B[ 5 ] = { // -188.2914 dB, 12 + 6.0676859170554343e-001, -1.3689667009876413e-001, + 3.6288512631926818e-002, -6.7838855305035351e-003, + 6.2345167677087547e-004,}; + static const double HBKernel_6B[ 6 ] = { // -224.2705 dB, 12 + 6.1161456341904397e-001, -1.4743901958274458e-001, + 4.5344160157313275e-002, -1.1207371780924531e-002, + 1.8328497112594935e-003, -1.4518193006359589e-004,}; + static const int FltCountB = 5; + static const int FlttBaseB = 2; + static const double FltAttensB[ FltCountB ] = { + 71.0965, 115.7707, 152.1535, 188.2914, 224.2705, }; + static const double* const FltPtrsB[ FltCountB ] = { + HBKernel_2B, HBKernel_3B, HBKernel_4B, HBKernel_5B, HBKernel_6B, }; + static const double HBKernel_1C[ 1 ] = { // -49.4544 dB, 24 + 5.0336730531430562e-001,}; + static const double HBKernel_2C[ 2 ] = { // -103.1970 dB, 24 + 5.6330232648142819e-001, -6.3309247177420452e-002,}; + static const double HBKernel_3C[ 3 ] = { // -152.1195 dB, 24 + 5.8643891113580415e-001, -9.8411593011583087e-002, + 1.1972706651483846e-002,}; + static const double HBKernel_4C[ 4 ] = { // -200.6182 dB, 24 + 5.9851012363917222e-001, -1.2028885239978220e-001, + 2.4294521083140615e-002, -2.5157924156609776e-003,}; + static const double HBKernel_5C[ 5 ] = { // -248.8730 dB, 24 + 6.0590922882030196e-001, -1.3515953438018685e-001, + 3.5020857107815606e-002, -6.3256196990467053e-003, + 5.5506815147598793e-004,}; + static const int FltCountC = 5; + static const int FlttBaseC = 1; + static const double FltAttensC[ FltCountC ] = { + 49.4544, 103.1970, 152.1195, 200.6182, 248.8730, }; + static const double* const FltPtrsC[ FltCountC ] = { + HBKernel_1C, HBKernel_2C, HBKernel_3C, HBKernel_4C, HBKernel_5C, }; + static const double HBKernel_1D[ 1 ] = { // -61.5357 dB, 48 + 5.0083794231068057e-001,}; + static const double HBKernel_2D[ 2 ] = { // -127.3167 dB, 48 + 5.6270074379958690e-001, -6.2701174487726344e-002,}; + static const double HBKernel_3D[ 3 ] = { // -188.2990 dB, 48 + 5.8606296210323228e-001, -9.7844644765123029e-002, + 1.1781683046528768e-002,}; + static const double HBKernel_4D[ 4 ] = { // -248.8580 dB, 48 + 5.9823601243162516e-001, -1.1979368994739022e-001, + 2.4017458606412575e-002, -2.4597810910081913e-003,}; + static const int FltCountD = 4; + static const int FlttBaseD = 1; + static const double FltAttensD[ FltCountD ] = { + 61.5357, 127.3167, 188.2990, 248.8580, }; + static const double* const FltPtrsD[ FltCountD ] = { + HBKernel_1D, HBKernel_2D, HBKernel_3D, HBKernel_4D, }; + static const double HBKernel_1E[ 1 ] = { // -77.4651 dB, 96 + 5.0013388897382527e-001,}; + static const double HBKernel_2E[ 2 ] = { // -151.4084 dB, 96 + 5.6255019604317880e-001, -6.2550222932381064e-002,}; + static const double HBKernel_3E[ 3 ] = { // -224.4365 dB, 96 + 5.8596887234201078e-001, -9.7703321113080305e-002, + 1.1734448777069783e-002,}; + static const int FltCountE = 3; + static const int FlttBaseE = 1; + static const double FltAttensE[ FltCountE ] = { + 77.4651, 151.4084, 224.4365, }; + static const double* const FltPtrsE[ FltCountE ] = { + HBKernel_1E, HBKernel_2E, HBKernel_3E, }; + static const double HBKernel_1F[ 1 ] = { // -89.5075 dB, 192 + 5.0003346776264190e-001,}; + static const double HBKernel_2F[ 2 ] = { // -175.4932 dB, 192 + 5.6251254964097952e-001, -6.2512551321105267e-002,}; + static const double HBKernel_3F[ 3 ] = { // -260.5645 dB, 192 + 5.8594534336747051e-001, -9.7668015838639821e-002, + 1.1722672471262996e-002,}; + static const int FltCountF = 3; + static const int FlttBaseF = 1; + static const double FltAttensF[ FltCountF ] = { + 89.5075, 175.4932, 260.5645, }; + static const double* const FltPtrsF[ FltCountF ] = { + HBKernel_1F, HBKernel_2F, HBKernel_3F, }; + static const double HBKernel_1G[ 1 ] = { // -101.5490 dB, 384 + 5.0000836666064941e-001,}; + static const double HBKernel_2G[ 2 ] = { // -199.5761 dB, 384 + 5.6250313744943459e-001, -6.2503137554435345e-002,}; + static const double HBKernel_3G[ 3 ] = { // -296.5185 dB, 384 + 5.8593945786963764e-001, -9.7659186853499613e-002, + 1.1719728983863425e-002,}; + static const int FltCountG = 3; + static const int FlttBaseG = 1; + static const double FltAttensG[ FltCountG ] = { + 101.5490, 199.5761, 296.5185, }; + static const double* const FltPtrsG[ FltCountG ] = { + HBKernel_1G, HBKernel_2G, HBKernel_3G, }; + + int k = 0; + + if( SteepIndex <= 0 ) + { + while( k != FltCountA - 1 && FltAttensA[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsA[ k ]; + fltt = FlttBaseA + k; + att = FltAttensA[ k ]; + } + else + if( SteepIndex == 1 ) + { + while( k != FltCountB - 1 && FltAttensB[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsB[ k ]; + fltt = FlttBaseB + k; + att = FltAttensB[ k ]; + } + else + if( SteepIndex == 2 ) + { + while( k != FltCountC - 1 && FltAttensC[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsC[ k ]; + fltt = FlttBaseC + k; + att = FltAttensC[ k ]; + } + else + if( SteepIndex == 3 ) + { + while( k != FltCountD - 1 && FltAttensD[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsD[ k ]; + fltt = FlttBaseD + k; + att = FltAttensD[ k ]; + } + else + if( SteepIndex == 4 ) + { + while( k != FltCountE - 1 && FltAttensE[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsE[ k ]; + fltt = FlttBaseE + k; + att = FltAttensE[ k ]; + } + else + if( SteepIndex == 5 ) + { + while( k != FltCountF - 1 && FltAttensF[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsF[ k ]; + fltt = FlttBaseF + k; + att = FltAttensF[ k ]; + } + else + { + while( k != FltCountG - 1 && FltAttensG[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsG[ k ]; + fltt = FlttBaseG + k; + att = FltAttensG[ k ]; + } + } + + /** + * @brief Initalizes the half-band upsampler. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * upsampling ratio, e.g., at 4x upsampling 0 is used, at 8x upsampling 1 + * is used, etc. + * @param IsThird `true` if 1/3 of frequency response resampling is + * performed. + * @param PrevLatency Latency, in samples (any non-negative value), which + * was left in the output signal by a previous process. Whole-number + * latency will be consumed by *this* object while remaining fractional + * latency can be obtained via the getLatencyFrac() function. + * @param aDoConsumeLatency `true` if the output latency should be + * consumed. Does not apply to the fractional part of the latency (if such + * part is available). + */ + + CDSPHBUpsampler( const double ReqAtten, const int SteepIndex, + const bool IsThird, const double PrevLatency, + const bool aDoConsumeLatency = true ) + : DoConsumeLatency( aDoConsumeLatency ) + { + static const CConvolveFn FltConvFn[ 14 ] = { + &CDSPHBUpsampler :: convolve1, &CDSPHBUpsampler :: convolve2, + &CDSPHBUpsampler :: convolve3, &CDSPHBUpsampler :: convolve4, + &CDSPHBUpsampler :: convolve5, &CDSPHBUpsampler :: convolve6, + &CDSPHBUpsampler :: convolve7, &CDSPHBUpsampler :: convolve8, + &CDSPHBUpsampler :: convolve9, &CDSPHBUpsampler :: convolve10, + &CDSPHBUpsampler :: convolve11, &CDSPHBUpsampler :: convolve12, + &CDSPHBUpsampler :: convolve13, &CDSPHBUpsampler :: convolve14 }; + + const double* fltp0; + int fltt; + double att; + + if( IsThird ) + { + getHBFilterThird( ReqAtten, SteepIndex, fltp0, fltt, att ); + } + else + { + getHBFilter( ReqAtten, SteepIndex, fltp0, fltt, att ); + } + + // Copy obtained filter to address-aligned buffer. + + fltp = align_ptr( FltBuf, 16 ); + memcpy( fltp, fltp0, (size_t) fltt * sizeof( fltp[ 0 ])); + + convfn = FltConvFn[ fltt - 1 ]; + fll = fltt - 1; + fl2 = fltt; + flo = fll + fl2; + BufRP = Buf + fll; + + LatencyFrac = PrevLatency * 2.0; + Latency = (int) LatencyFrac; + LatencyFrac -= Latency; + + R8BASSERT( Latency >= 0 ); + + if( DoConsumeLatency ) + { + flb = BufLen - fll; + } + else + { + Latency += fl2 + fl2; + flb = BufLen - flo; + } + + R8BCONSOLE( "CDSPHBUpsampler: sti=%i third=%i taps=%i att=%.1f " + "io=2/1\n", SteepIndex, (int) IsThird, fltt, att ); + + clear(); + } + + virtual int getInLenBeforeOutPos( int ReqOutPos ) const + { + if( Next != R8B_NULL ) + { + ReqOutPos = Next -> getInLenBeforeOutPos( ReqOutPos ); + } + + return( fl2 + (int) (( Latency + LatencyFrac + ReqOutPos ) * 0.5 )); + } + + virtual int getLatency() const + { + return( DoConsumeLatency ? 0 : Latency ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return( MaxInLen * 2 ); + } + + virtual void clear() + { + if( DoConsumeLatency ) + { + LatencyLeft = Latency; + BufLeft = 0; + } + else + { + LatencyLeft = 0; + BufLeft = fl2; + } + + WritePos = 0; + ReadPos = flb & BufLenMask; // Set "read" position to account + // for filter's latency. The mask also normalizes the `fll` equal + // 0 case, where `flb` equals `BufLen`, thus preventing an overrun + // read past the ring buffer's mirror zone. + + memset( &Buf[ ReadPos ], 0, + (size_t) ( BufLen - flb ) * sizeof( Buf[ 0 ])); + } + + virtual int process( double* ip, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + + double* op = op0; + + // Copy at most `flo` new input samples to the ring buffer. This is + // sufficient to produce all output whose convolution window overlaps + // the input samples that preceded the current block. + + int ac = 0; // The number of samples copied to the ring buffer. + + while( l > 0 && ac < flo ) + { + // Copy new input samples to the ring buffer. + + const int b = min( min( l, flo - ac ), + min( BufLen - WritePos, flb - BufLeft )); + + double* const wp1 = Buf + WritePos; + memcpy( wp1, ip, (size_t) b * sizeof( wp1[ 0 ])); + const int ec = flo - WritePos; + + if( ec > 0 ) + { + memcpy( wp1 + BufLen, ip, + (size_t) min( b, ec ) * sizeof( wp1[ 0 ])); + } + + ip += b; + WritePos = ( WritePos + b ) & BufLenMask; + l -= b; + BufLeft += b; + ac += b; + + // Produce output. + + int c = BufLeft - fl2; + + while( c > 0 ) + { + const int cb = min( c, BufLen - ReadPos ); + double* const opend = op + cb * 2; + ( *convfn )( op, opend, fltp, BufRP + ReadPos ); + + op = opend; + ReadPos = ( ReadPos + cb ) & BufLenMask; + BufLeft -= cb; + c -= cb; + } + } + + if( l > 0 ) + { + R8BASSERT( ac == flo ); + + // The convolution window is now contained wholly within the + // input buffer: convolve it directly, without copying it to the + // ring buffer. The `ip` is offset by `fl2` samples to account + // for the input already processed via the ring buffer. + + double* const opend = op + l * 2; + ( *convfn )( op, opend, fltp, ip - fl2 ); + + ip += l; + op = opend; + + // Rebase the ring buffer to the last `flo` input samples, to + // provide a convolution window for the next call, with `fl2` + // samples left unprocessed. + + memcpy( Buf, ip - flo, (size_t) flo * sizeof( Buf[ 0 ])); + + ReadPos = 0; + WritePos = flo; + BufLeft = fl2; + } + + int ol = (int) ( op - op0 ); + + if( LatencyLeft != 0 ) + { + if( LatencyLeft >= ol ) + { + LatencyLeft -= ol; + return( 0 ); + } + + ol -= LatencyLeft; + op0 += LatencyLeft; + LatencyLeft = 0; + } + + return( ol ); + } + +private: + static const int BufLenBits = 7; ///< The length of the ring buffer, + ///< expressed as Nth power of 2. This value can be reduced if it is + ///< known that only short input buffers will be passed to the + ///< interpolator. The minimum value of this parameter is 5, and + ///< `1 << BufLenBits` should be at least 3 times larger than the + ///< FilterLen. + static const int BufLen = 1 << BufLenBits; ///< The length of the ring + ///< buffer. The actual length is longer, to permit "beyond bounds" + ///< positioning. + static const int BufLenMask = BufLen - 1; ///< Mask used for quick buffer + ///< position wrapping. + static const int FltBufLen = 14; ///< The length of the `FltBuf` able to + ///< hold the largest filter. + double Buf[ BufLen + ( FltBufLen * 2 - 1 )]; ///< The ring buffer, + ///< including overrun protection for the largest filter. + double FltBuf[ FltBufLen + 2 ]; ///< Holder for half-band filter taps, + ///< used with 16-byte address-aligning, for SIMD use. + const double* BufRP; ///< Offseted Buf pointer at `ReadPos` equal 0. + double* fltp; ///< Half-band filter taps, points to `FltBuf`. + int fll; ///< Input latency (left-hand filter length). + int fl2; ///< Right-side filter length. + int flo; ///< Overrrun length. + int flb; ///< Initial buffer read position. + double LatencyFrac; ///< Fractional latency left on the output. + int Latency; ///< Initial latency that should be removed from the output. + int LatencyLeft; ///< Latency left to remove. + int BufLeft; ///< The number of samples left in the buffer to process. + ///< When this value is below `fl2`, the interpolation cycle ends. + int WritePos; ///< The current buffer write position. Incremented together + ///< with the `BufLeft` variable. + int ReadPos; ///< The current buffer read position. + bool DoConsumeLatency; ///< `true` if the output latency should be + ///< consumed. Does not apply to the fractional part of the latency + ///< (if such part is available). + + typedef void( *CConvolveFn )( double* op, double* const opend, + const double* const flt, const double* rp ); ///< Convolution function + ///< type. + CConvolveFn convfn; ///< Convolution function in use. + +#define R8BHBC1( fn ) \ + static void fn( double* op, double* const opend, const double* const flt, \ + const double* rp ) \ + { \ + while( op != opend ) \ + { \ + op[ 0 ] = rp[ 0 ]; + +#define R8BHBC2 \ + op += 2; \ + rp++; \ + } \ + } + +#include "CDSPHBUpsampler.inc" + +#undef R8BHBC1 +#undef R8BHBC2 +}; + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPHBUPSAMPLER_INCLUDED diff --git a/native/vendor/r8brain/CDSPHBUpsampler.inc b/native/vendor/r8brain/CDSPHBUpsampler.inc new file mode 100644 index 0000000..f9a4114 --- /dev/null +++ b/native/vendor/r8brain/CDSPHBUpsampler.inc @@ -0,0 +1,711 @@ +// Auto-generated by `genhbc`, do not edit! + +#if defined( R8B_SSE2 ) + +R8BHBC1( convolve1 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); +R8BHBC2 + +R8BHBC1( convolve2 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +R8BHBC1( convolve3 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 1 ] += flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); +R8BHBC2 + +R8BHBC1( convolve4 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +R8BHBC1( convolve5 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 1 ] += flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); +R8BHBC2 + +R8BHBC1( convolve6 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +R8BHBC1( convolve7 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 1 ] += flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); +R8BHBC2 + +R8BHBC1( convolve8 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +R8BHBC1( convolve9 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 1 ] += flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); +R8BHBC2 + +R8BHBC1( convolve10 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +R8BHBC1( convolve11 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 1 ] += flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); +R8BHBC2 + +R8BHBC1( convolve12 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 11 ); v3 = _mm_loadu_pd( rp + 11 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 10 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +R8BHBC1( convolve13 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 11 ); v3 = _mm_loadu_pd( rp + 11 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 10 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +op[ 1 ] += flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); +R8BHBC2 + +R8BHBC1( convolve14 ) +__m128d v1, v2, m1, s1; +v2 = _mm_loadu_pd( rp - 1 ); v1 = _mm_loadu_pd( rp + 1 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 0 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = m1; +__m128d v3, v4, m3, s3; +v4 = _mm_loadu_pd( rp - 3 ); v3 = _mm_loadu_pd( rp + 3 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 2 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = m3; +v2 = _mm_loadu_pd( rp - 5 ); v1 = _mm_loadu_pd( rp + 5 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 4 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 7 ); v3 = _mm_loadu_pd( rp + 7 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 6 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 9 ); v1 = _mm_loadu_pd( rp + 9 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 8 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +v4 = _mm_loadu_pd( rp - 11 ); v3 = _mm_loadu_pd( rp + 11 ); +m3 = _mm_mul_pd( _mm_load_pd( flt + 10 ), + _mm_add_pd( v3, _mm_shuffle_pd( v4, v4, 1 ))); +s3 = _mm_add_pd( s3, m3 ); +v2 = _mm_loadu_pd( rp - 13 ); v1 = _mm_loadu_pd( rp + 13 ); +m1 = _mm_mul_pd( _mm_load_pd( flt + 12 ), + _mm_add_pd( v1, _mm_shuffle_pd( v2, v2, 1 ))); +s1 = _mm_add_pd( s1, m1 ); +s1 = _mm_add_pd( s1, s3 ); +_mm_storel_pd( op + 1, _mm_add_pd( s1, _mm_shuffle_pd( s1, s1, 1 ))); +R8BHBC2 + +#elif defined( R8B_NEON ) + +R8BHBC1( convolve1 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); +R8BHBC2 + +R8BHBC1( convolve2 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +R8BHBC1( convolve3 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +op[ 1 ] = vaddvq_f64( s1 ) + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); +R8BHBC2 + +R8BHBC1( convolve4 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +R8BHBC1( convolve5 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ) + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); +R8BHBC2 + +R8BHBC1( convolve6 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +R8BHBC1( convolve7 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ) + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); +R8BHBC2 + +R8BHBC1( convolve8 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +R8BHBC1( convolve9 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ) + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); +R8BHBC2 + +R8BHBC1( convolve10 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +R8BHBC1( convolve11 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ) + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); +R8BHBC2 + +R8BHBC1( convolve12 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 11 ); v3 = vld1q_f64( rp + 11 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 10 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +R8BHBC1( convolve13 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 11 ); v3 = vld1q_f64( rp + 11 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 10 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ) + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); +R8BHBC2 + +R8BHBC1( convolve14 ) +float64x2_t v1, v2, s1; +s1 = vdupq_n_f64( 0.0 ); +v2 = vld1q_f64( rp - 1 ); v1 = vld1q_f64( rp + 1 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 0 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +float64x2_t v3, v4, s3; +s3 = vdupq_n_f64( 0.0 ); +v4 = vld1q_f64( rp - 3 ); v3 = vld1q_f64( rp + 3 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 2 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 5 ); v1 = vld1q_f64( rp + 5 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 4 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 7 ); v3 = vld1q_f64( rp + 7 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 6 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 9 ); v1 = vld1q_f64( rp + 9 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 8 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +v4 = vld1q_f64( rp - 11 ); v3 = vld1q_f64( rp + 11 ); +s3 = vmlaq_f64( s3, vld1q_f64( flt + 10 ), + vaddq_f64( v3, vextq_f64( v4, v4, 1 ))); +v2 = vld1q_f64( rp - 13 ); v1 = vld1q_f64( rp + 13 ); +s1 = vmlaq_f64( s1, vld1q_f64( flt + 12 ), + vaddq_f64( v1, vextq_f64( v2, v2, 1 ))); +s1 = vaddq_f64( s1, s3 ); +op[ 1 ] = vaddvq_f64( s1 ); +R8BHBC2 + +#else // SIMD + +R8BHBC1( convolve1 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); +R8BHBC2 + +R8BHBC1( convolve2 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]); +R8BHBC2 + +R8BHBC1( convolve3 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); +R8BHBC2 + +R8BHBC1( convolve4 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]); +R8BHBC2 + +R8BHBC1( convolve5 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); +R8BHBC2 + +R8BHBC1( convolve6 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]); +R8BHBC2 + +R8BHBC1( convolve7 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); +R8BHBC2 + +R8BHBC1( convolve8 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]); +R8BHBC2 + +R8BHBC1( convolve9 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); +R8BHBC2 + +R8BHBC1( convolve10 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]); +R8BHBC2 + +R8BHBC1( convolve11 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); +R8BHBC2 + +R8BHBC1( convolve12 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]); +R8BHBC2 + +R8BHBC1( convolve13 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]) + + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); +R8BHBC2 + +R8BHBC1( convolve14 ) +op[ 1 ] = flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]) + + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]) + + flt[ 13 ] * ( rp[ 14 ] + rp[ -13 ]); +R8BHBC2 + +#endif // SIMD diff --git a/native/vendor/r8brain/CDSPProcessor.h b/native/vendor/r8brain/CDSPProcessor.h new file mode 100644 index 0000000..192f5cb --- /dev/null +++ b/native/vendor/r8brain/CDSPProcessor.h @@ -0,0 +1,138 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPProcessor.h + * + * @brief The base virtual class for DSP processing algorithms. + * + * This file includes the base virtual class for DSP processing algorithm + * classes like FIR filtering and interpolation. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPPROCESSOR_INCLUDED +#define R8B_CDSPPROCESSOR_INCLUDED + +#include "r8bbase.h" + +namespace r8b { + +/** + * @brief The base virtual class for DSP processing algorithms. + * + * This class can be used as a base class for various DSP processing + * algorithms (processors). DSP processors that are derived from this class + * can be seamlessly integrated into various DSP processing graphs. + * + * This class provides a singly-linked list functionality. + */ + +class CDSPProcessor : public R8B_DSPBASECLASS, + public CSinglyLinkedListItem< CDSPProcessor > +{ + R8BNOCTOR( CDSPProcessor ) + +public: + CDSPProcessor() + { + } + + virtual ~CDSPProcessor() + { + } + + /** + * @brief Returns the number of input samples required to advance to + * the specified output sample position (so that the next process() call + * passes this output position). + + * Assumes starting at the cleared or after-construction state of *this* + * object. + * + * Note that the implementation of this function assumes the caller only + * needs to estimate an initial buffering requirement; passing a full + * sample length value (e.g., greater than 100000) may overflow the + * calculation or cause rounding errors. + * + * The implementation should first call the same function of the `Next` + * processor. + * + * @param ReqOutPos The required output position. Set to 0 to obtain + * "input length before output start" latency. Must be a non-negative + * value. + * @return The number of input samples required. + */ + + virtual int getInLenBeforeOutPos( int ReqOutPos ) const = 0; + + /** + * @brief Return the latency, in samples, which is present in the output + * signal. + * + * This value is usually zero if the DSP processor "consumes" the latency + * automatically. + */ + + virtual int getLatency() const = 0; + + /** + * @brief Returns fractional latency, in samples, which is present in the + * output signal. + * + * This value is usually zero if a linear-phase filtering is used. With + * minimum-phase filters in use, this value can be non-zero even if + * the getLatency() function returns zero. + */ + + virtual double getLatencyFrac() const = 0; + + /** + * @brief Returns the maximal length of the output buffer required when + * processing the `MaxInLen` number of input samples. + * + * @param MaxInLen The number of samples planned to process at once, at + * most. + */ + + virtual int getMaxOutLen( const int MaxInLen ) const = 0; + + /** + * @brief Clears (resets) the state of *this* object and returns it to + * the state after construction. + * + * All input data accumulated in the internal buffer so far will be + * discarded. + */ + + virtual void clear() = 0; + + /** + * @brief Performs DSP processing. + * + * @param ip Input data pointer. + * @param l0 How many samples to process. + * @param[out] op0 Output data pointer. The capacity of this buffer should + * be equal to the value returned by the getMaxOutLen() function for the + * given `l0`. This buffer can be equal to `ip` only if the + * `getMaxOutLen( l0 )` call returned a value lesser than `l0`. This + * pointer can be incremented on function's return if latency compensation + * was performed by the processor. Note that on function's return, this + * pointer may point to some internal buffers, including the `ip` buffer, + * ignoring the originally passed value. + * @return The number of output samples written to the `op0` buffer and + * available after processing. This value can be smaller or larger in + * comparison to the original `l0` value due to processing and filter's + * latency compensation that took place, and due to resampling if it was + * performed. + */ + + virtual int process( double* ip, int l0, double*& op0 ) = 0; +}; + +} // namespace r8b + +#endif // R8B_CDSPPROCESSOR_INCLUDED diff --git a/native/vendor/r8brain/CDSPRealFFT.h b/native/vendor/r8brain/CDSPRealFFT.h new file mode 100644 index 0000000..d7f5a19 --- /dev/null +++ b/native/vendor/r8brain/CDSPRealFFT.h @@ -0,0 +1,1099 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPRealFFT.h + * + * @brief Real-valued FFT transform class. + * + * This file includes FFT object implementation. All created FFT objects are + * kept in a global list after use, for a future reusal. Such approach + * minimizes time necessary to initialize the FFT object of the required + * length. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPREALFFT_INCLUDED +#define R8B_CDSPREALFFT_INCLUDED + +#include "r8bbase.h" + +#if R8B_PFFFT_DOUBLE + #include "fft/pffft_double.h" +#elif R8B_PFFFT + #include "fft/pffft.h" +#elif !R8B_IPP + #include "fft/fft4g.h" +#endif // !R8B_IPP + +namespace r8b { + +#if R8B_PFFFT + typedef float realfft_t; ///< Forward transform's native type. +#else // R8B_PFFFT + typedef double realfft_t; ///< Forward transform's native type. +#endif // R8B_PFFFT + +/** + * @brief Real-valued FFT setup class. + * + * This class stores pre-calculated data for FFT functions of a specific + * length. Only a single object of this class is required for all FFT + * transform objects utilized concurrently. + */ + +class CDSPRealFFTSetup : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPRealFFTSetup ) + + friend class CPtrKeeper< CDSPRealFFTSetup >; + friend class CDSPRealFFT; + friend class CDSPRealFFTKeeper; + +public: + /** + * @brief Return a multiplication constant that should be used after + * inverse transform to obtain a correct value scale. + */ + + double getInvMulConst() const + { + return( InvMulConst ); + } + + /** + * @brief Returns the length (the number of real values in a transform) of + * *this* FFT setup object, expressed as Nth power of 2. + */ + + int getLenBits() const + { + return( LenBits ); + } + + /** + * @brief Returns the length (the number of real values in a transform) of + * *this* FFT setup object. + */ + + int getLen() const + { + return( Len ); + } + +private: + int LenBits; ///< Length of FFT block (expressed as Nth power of 2). + int Len; ///< Length of FFT block (number of real values). + double InvMulConst; ///< Inverse FFT multiply constant. + + #if R8B_IPP + IppsFFTSpec_R_64f* SpecPtr; ///< Pointer to the initialized + ///< specification buffer to be passed to IPP's FFT functions. + CFixedBuffer< unsigned char > SpecBuffer; ///< Working buffer. + int WorkBufferSize; ///< Size of the working buffer. + #elif R8B_PFFFT + PFFFT_Setup* setup; ///< PFFFT setup object. + #elif R8B_PFFFT_DOUBLE + PFFFTD_Setup* setup; ///< PFFFTD setup object. + #else // R8B_PFFFT_DOUBLE + CFixedBuffer< int > wi; ///< Coefficients buffer (ints). + CFixedBuffer< double > wd; ///< Coefficients buffer (doubles). + #endif // R8B_PFFFT_DOUBLE + + /** + * @brief Initializes *this* FFT setup object. + * + * @param aLenBits The length of FFT block (Nth power of 2), specifies the + * number of real values in a block. Values from 1 to 30 inclusive are + * supported. + */ + + CDSPRealFFTSetup( const int aLenBits ) + : LenBits( aLenBits ) + , Len( 1 << aLenBits ) + #if R8B_OOURA + , InvMulConst( 2.0 / Len ) + #else // R8B_OOURA + , InvMulConst( 1.0 / Len ) + #endif // R8B_OOURA + { + #if R8B_IPP + + int SpecBufferSize; + int InitBufferSize; + + ippsFFTGetSize_R_64f( LenBits, IPP_FFT_NODIV_BY_ANY, + ippAlgHintFast, &SpecBufferSize, &InitBufferSize, + &WorkBufferSize ); + + CFixedBuffer< unsigned char > InitBuffer( InitBufferSize ); + SpecBuffer.alloc( SpecBufferSize ); + + ippsFFTInit_R_64f( &SpecPtr, LenBits, IPP_FFT_NODIV_BY_ANY, + ippAlgHintFast, SpecBuffer, InitBuffer ); + + #elif R8B_PFFFT + + setup = pffft_new_setup( Len, PFFFT_REAL ); + + #elif R8B_PFFFT_DOUBLE + + setup = pffftd_new_setup( Len, PFFFT_REAL ); + + #else // R8B_PFFFT_DOUBLE + + wi.alloc( (int) ceil( 2.0 + sqrt( (double) ( Len >> 1 )))); + wd.alloc( Len >> 1 ); + + ooura_fft :: rdftinit( Len, wi, wd ); + + #endif // R8B_PFFFT_DOUBLE + } + + ~CDSPRealFFTSetup() + { + #if R8B_PFFFT + pffft_destroy_setup( setup ); + #elif R8B_PFFFT_DOUBLE + pffftd_destroy_setup( setup ); + #endif // R8B_PFFFT_DOUBLE + } +}; + +/** + * @brief Real-valued FFT transform class. + * + * Class implements a wrapper for real-valued discrete fast Fourier transform + * functions. The object of this class can only be obtained via the + * CDSPRealFFTKeeper class. + * + * As a general pre-condition, the buffers passed to the functions of this + * class should be aligned to 32 bytes. + * + * Uses functions from the FFT package: + * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html + * + * Also uses functions from the PFFFT and PFFFT DOUBLE packages. + * + * Also uses Intel IPP library functions if available (if the R8B_IPP=1 macro + * was defined). Note that IPP library's FFT functions are 2-3 times more + * efficient on the modern Intel Core i7-3770K processor than Ooura's + * functions. It may be worthwhile investing in IPP. Note, that FFT functions + * take less than 20% of the overall sample rate conversion time. However, + * when the "power of 2" resampling is used the performance of FFT functions + * dominates. + */ + +class CDSPRealFFT : public R8B_BASECLASS, + private CSinglyLinkedListItem< CDSPRealFFT > +{ + R8BNOCTOR( CDSPRealFFT ) + + friend class CSinglyLinkedListItem< CDSPRealFFT >; + friend class CPtrKeeper< CDSPRealFFT >; + friend class CDSPRealFFTKeeper; + +public: + /** + * @brief Return a multiplication constant that should be used after + * inverse transform to obtain a correct value scale. + */ + + double getInvMulConst() const + { + return( s -> InvMulConst ); + } + + /** + * @brief Returns the length (the number of real values in a transform) of + * *this* FFT object, expressed as Nth power of 2. + */ + + int getLenBits() const + { + return( s -> LenBits ); + } + + /** + * @brief Returns the length (the number of real values in a transform) of + * *this* FFT object. + */ + + int getLen() const + { + return( Len ); + } + + /** + * @brief Performs in-place forward FFT. + * + * If FFT's native type differs from `double`, the result will be placed + * to `p` using values of the native type. + * + * @param[in,out] p Pointer to data block to transform, length should be + * equal to *this* object's getLen(). + * @return The pointer `p` casted to transform's native type. + */ + + realfft_t* forward( double* const p ) const + { + #if R8B_IPP + + ippsFFTFwd_RToPerm_64f( p, p, s -> SpecPtr, WorkBuffer ); + + return( p ); + + #elif R8B_PFFFT + + float* const tp = &work[ Len ]; + int i; + + for( i = 0; i < Len; i++ ) + { + tp[ i ] = (float) p[ i ]; + } + + float* const op = construct_ptr< float >( (void*) p, (size_t) Len ); + + pffft_transform( s -> setup, tp, op, work, PFFFT_FORWARD ); + + return( op ); + + #elif R8B_PFFFT_DOUBLE + + pffftd_transform( s -> setup, p, p, work, PFFFT_FORWARD ); + + return( p ); + + #else // R8B_PFFFT_DOUBLE + + ooura_fft :: rdft( Len, 1, p, s -> wi, s -> wd ); + + return( p ); + + #endif // R8B_PFFFT_DOUBLE + } + + /** + * @brief Performs in-place inverse FFT. + * + * If FFT's native type differs from `double`, the result will be placed + * to `p` using `double` values. + * + * @param[in,out] p Pointer to data block to transform, length should be + * equal to *this* object's getLen(). + */ + + void inverse( realfft_t* const p ) const + { + #if R8B_IPP + + ippsFFTInv_PermToR_64f( p, p, s -> SpecPtr, WorkBuffer ); + + #elif R8B_PFFFT + + float* const tp = &work[ Len ]; + + pffft_transform( s -> setup, p, tp, work, PFFFT_BACKWARD ); + + double* const op = construct_ptr< double >( (void*) p, (size_t) Len ); + int i; + + for( i = 0; i < Len; i++ ) + { + op[ i ] = tp[ i ]; + } + + #elif R8B_PFFFT_DOUBLE + + pffftd_transform( s -> setup, p, p, work, PFFFT_BACKWARD ); + + #else // R8B_PFFFT_DOUBLE + + ooura_fft :: rdft( Len, -1, p, s -> wi, s -> wd ); + + #endif // R8B_PFFFT_DOUBLE + } + + /** + * @brief Returns pointer to the internal work buffer. + * + * This buffer can be used for FFT bin reordering. + * + * @return The work buffer pointer, or `nullptr` if FFT produces + * sequential-ordered FFT bins. This pointer should not be used beyond the + * calling context. + */ + + realfft_t* getWorkBuf() const + { + #if R8B_PFFFT || R8B_PFFFT_DOUBLE + + return( work ); + + #else // R8B_PFFFT || R8B_PFFFT_DOUBLE + + return( R8B_NULL ); + + #endif // R8B_PFFFT || R8B_PFFFT_DOUBLE + } + + /** + * @brief Reorders FFT bins from native ordering (after the forward + * transform) to sequential ordering. + * + * @param p Input FFT block. + * @param op Output FFT block. + * @return The pointer to the reordered FFT block. May return `p` if the + * transform has sequential ordering already. + */ + + realfft_t* reorderForward( realfft_t* const p, realfft_t* const op ) const + { + #if R8B_PFFFT + + pffft_zreorder( s -> setup, p, op, PFFFT_FORWARD ); + + return( op ); + + #elif R8B_PFFFT_DOUBLE + + pffftd_zreorder( s -> setup, p, op, PFFFT_FORWARD ); + + return( op ); + + #else // R8B_PFFFT_DOUBLE + + (void) op; + + return( p ); + + #endif // R8B_PFFFT_DOUBLE + } + + /** + * @brief Reorders FFT bins from sequential ordering (before the inverse + * transform) to native ordering. + * + * @param ip Pointer to FFT block (previously returned by the + * reorderForward() function). + * @param op Output FFT block (may equal `ip`). + * @return The pointer to the reordered FFT block (`op` casted to the + * `realfft_t` type). + */ + + realfft_t* reorderInverse( const realfft_t* const ip, + double* const op ) const + { + #if R8B_PFFFT + + float* const op2 = construct_ptr< float >( (void*) op, Len ); + + pffft_zreorder( s -> setup, ip, op2, PFFFT_BACKWARD ); + + return( op2 ); + + #elif R8B_PFFFT_DOUBLE + + pffftd_zreorder( s -> setup, ip, op, PFFFT_BACKWARD ); + + return( op ); + + #else // R8B_PFFFT_DOUBLE + + if( ip != op ) + { + memcpy( op, ip, (size_t) Len * sizeof( double )); + } + + return( op ); + + #endif // R8B_PFFFT_DOUBLE + } + + /** + * @brief Replaces the Nyquist FFT bin in the specified block. + * + * @param p Pointer to FFT block. + * @param v New Nyquist bin value. + */ + + static void setBinNyquist( realfft_t* const p, const realfft_t v ) + { + #if R8B_PFFFT || R8B_PFFFT_DOUBLE + + p[ 4 ] = v; + + #else // R8B_PFFFT || R8B_PFFFT_DOUBLE + + p[ 1 ] = v; + + #endif // R8B_PFFFT || R8B_PFFFT_DOUBLE + } + + /** + * @brief Multiplies two complex-valued data blocks and places result in + * a new data block. + * + * Length of all data blocks should be equal to *this* object's block + * length. Input blocks should have been produced with the forward() + * function of *this* object. + * + * @param ip1 Input FFT block 1. + * @param ip2 Input FFT block 2. + * @param[out] op Output FFT block, should not be equal to `ip1` nor + * `ip2`. + */ + + void multiplyBlocks( const realfft_t* const ip1, + const realfft_t* const ip2, realfft_t* const op ) const + { + #if R8B_IPP + + ippsMulPerm_64f( (Ipp64f*) ip1, (Ipp64f*) ip2, (Ipp64f*) op, Len ); + + #elif R8B_PFFFT + + pffft_zconvolve( s -> setup, ip1, ip2, op ); + + #elif R8B_PFFFT_DOUBLE + + pffftd_zconvolve( s -> setup, ip1, ip2, op ); + + #else // R8B_PFFFT_DOUBLE + + op[ 0 ] = ip1[ 0 ] * ip2[ 0 ]; + op[ 1 ] = ip1[ 1 ] * ip2[ 1 ]; + + int i = 2; + + while( i < Len ) + { + op[ i ] = ip1[ i ] * ip2[ i ] - ip1[ i + 1 ] * ip2[ i + 1 ]; + op[ i + 1 ] = ip1[ i ] * ip2[ i + 1 ] + ip1[ i + 1 ] * ip2[ i ]; + i += 2; + } + + #endif // R8B_PFFFT_DOUBLE + } + + /** + * @brief Multiplies two complex-valued FFT blocks in-place. + * + * Length of both blocks should be equal to *this* object's block length. + * Blocks should have been produced with the forward() function of *this* + * object. + * + * @param ip Input FFT block 1. + * @param[in,out] op Output/input FFT block 2. + */ + + void multiplyBlocks( const realfft_t* const ip, realfft_t* const op ) const + { + #if R8B_IPP + + ippsMulPerm_64f( (Ipp64f*) op, (Ipp64f*) ip, (Ipp64f*) op, Len ); + + #elif R8B_PFFFT + + pffft_zconvolve( s -> setup, ip, op, op ); + + #elif R8B_PFFFT_DOUBLE + + pffftd_zconvolve( s -> setup, ip, op, op ); + + #else // R8B_PFFFT_DOUBLE + + op[ 0 ] *= ip[ 0 ]; + op[ 1 ] *= ip[ 1 ]; + + realfft_t t; + int i = 2; + + while( i < Len ) + { + t = op[ i ] * ip[ i ] - op[ i + 1 ] * ip[ i + 1 ]; + op[ i + 1 ] = op[ i ] * ip[ i + 1 ] + op[ i + 1 ] * ip[ i ]; + op[ i ] = t; + i += 2; + } + + #endif // R8B_PFFFT_DOUBLE + } + + /** + * @brief Multiplies two complex-valued FFT blocks in-place, considering + * that the `ip` block contains "zero-phase" response. + * + * Length of both blocks should be equal to *this* object's block length. + * Blocks should have been produced with the forward() function of *this* + * object. + * + * @param ip Input FFT block 1, "zero-phase" response. This block should + * be first transformed via the convertToZP() function. + * @param[in,out] op Output/input FFT block 2. + */ + + void multiplyBlocksZP( const realfft_t* ip, realfft_t* op ) const + { + // SIMD implementations assume that pointers are address-aligned. + + #if R8B_PFFFT_DOUBLE + + pffftd_zconvolve_zp( s -> setup, op, ip, op ); + + #elif R8B_PFFFT && defined( R8B_SSE2 ) + + int c16 = Len >> 4; + + while( c16 != 0 ) + { + const __m128 iv1 = _mm_load_ps( ip ); + const __m128 iv2 = _mm_load_ps( ip + 4 ); + const __m128 ov1 = _mm_load_ps( op ); + const __m128 ov2 = _mm_load_ps( op + 4 ); + _mm_store_ps( op, _mm_mul_ps( iv1, ov1 )); + _mm_store_ps( op + 4, _mm_mul_ps( iv2, ov2 )); + + const __m128 iv3 = _mm_load_ps( ip + 8 ); + const __m128 iv4 = _mm_load_ps( ip + 12 ); + const __m128 ov3 = _mm_load_ps( op + 8 ); + const __m128 ov4 = _mm_load_ps( op + 12 ); + _mm_store_ps( op + 8, _mm_mul_ps( iv3, ov3 )); + _mm_store_ps( op + 12, _mm_mul_ps( iv4, ov4 )); + + ip += 16; + op += 16; + c16--; + } + + int c = Len & 15; + + while( c != 0 ) + { + *op *= *ip; + ip++; + op++; + c--; + } + + #elif R8B_PFFFT && defined( R8B_NEON ) + + int c16 = Len >> 4; + + while( c16 != 0 ) + { + const float32x4_t iv1 = vld1q_f32( ip ); + const float32x4_t iv2 = vld1q_f32( ip + 4 ); + const float32x4_t ov1 = vld1q_f32( op ); + const float32x4_t ov2 = vld1q_f32( op + 4 ); + vst1q_f32( op, vmulq_f32( iv1, ov1 )); + vst1q_f32( op + 4, vmulq_f32( iv2, ov2 )); + + const float32x4_t iv3 = vld1q_f32( ip + 8 ); + const float32x4_t iv4 = vld1q_f32( ip + 12 ); + const float32x4_t ov3 = vld1q_f32( op + 8 ); + const float32x4_t ov4 = vld1q_f32( op + 12 ); + vst1q_f32( op + 8, vmulq_f32( iv3, ov3 )); + vst1q_f32( op + 12, vmulq_f32( iv4, ov4 )); + + ip += 16; + op += 16; + c16--; + } + + int c = Len & 15; + + while( c != 0 ) + { + *op *= *ip; + ip++; + op++; + c--; + } + + #elif !R8B_PFFFT && defined( R8B_SSE2 ) + + int c8 = Len >> 3; + + while( c8 != 0 ) + { + const __m128d iv1 = _mm_load_pd( ip ); + const __m128d iv2 = _mm_load_pd( ip + 2 ); + const __m128d ov1 = _mm_load_pd( op ); + const __m128d ov2 = _mm_load_pd( op + 2 ); + _mm_store_pd( op, _mm_mul_pd( iv1, ov1 )); + _mm_store_pd( op + 2, _mm_mul_pd( iv2, ov2 )); + + const __m128d iv3 = _mm_load_pd( ip + 4 ); + const __m128d iv4 = _mm_load_pd( ip + 6 ); + const __m128d ov3 = _mm_load_pd( op + 4 ); + const __m128d ov4 = _mm_load_pd( op + 6 ); + _mm_store_pd( op + 4, _mm_mul_pd( iv3, ov3 )); + _mm_store_pd( op + 6, _mm_mul_pd( iv4, ov4 )); + + ip += 8; + op += 8; + c8--; + } + + int c = Len & 7; + + while( c != 0 ) + { + *op *= *ip; + ip++; + op++; + c--; + } + + #elif !R8B_PFFFT && defined( R8B_NEON ) + + int c8 = Len >> 3; + + while( c8 != 0 ) + { + const float64x2_t iv1 = vld1q_f64( ip ); + const float64x2_t iv2 = vld1q_f64( ip + 2 ); + const float64x2_t ov1 = vld1q_f64( op ); + const float64x2_t ov2 = vld1q_f64( op + 2 ); + vst1q_f64( op, vmulq_f64( iv1, ov1 )); + vst1q_f64( op + 2, vmulq_f64( iv2, ov2 )); + + const float64x2_t iv3 = vld1q_f64( ip + 4 ); + const float64x2_t iv4 = vld1q_f64( ip + 6 ); + const float64x2_t ov3 = vld1q_f64( op + 4 ); + const float64x2_t ov4 = vld1q_f64( op + 6 ); + vst1q_f64( op + 4, vmulq_f64( iv3, ov3 )); + vst1q_f64( op + 6, vmulq_f64( iv4, ov4 )); + + ip += 8; + op += 8; + c8--; + } + + int c = Len & 7; + + while( c != 0 ) + { + *op *= *ip; + ip++; + op++; + c--; + } + + #else // SIMD + + int i; + + for( i = 0; i < Len; i++ ) + { + op[ i ] *= ip[ i ]; + } + + #endif // SIMD + } + + /** + * @brief Converts the specified forward-transformed FFT block into + * "zero-phase" form, suitable for use with the multiplyBlocksZP() + * function. + * + * @param[in,out] p Block to transform. + */ + + void convertToZP( realfft_t* const p ) const + { + #if R8B_PFFFT + + pffft_zreorder( s -> setup, p, work, PFFFT_FORWARD ); + + int i = 2; + + while( i < Len ) + { + work[ i + 1 ] = work[ i ]; + i += 2; + } + + pffft_zreorder( s -> setup, work, p, PFFFT_BACKWARD ); + + #elif R8B_PFFFT_DOUBLE + + pffftd_zconvert_zp( s -> setup, p, p, (double) Len ); + + #else // R8B_PFFFT_DOUBLE + + int i = 2; + + while( i < Len ) + { + p[ i + 1 ] = p[ i ]; + i += 2; + } + + #endif // R8B_PFFFT_DOUBLE + } + +private: + const CDSPRealFFTSetup* s; ///< Pointer to static FFT transform setup + ///< object. + int Len; ///< Length of FFT block (number of real values). + + #if R8B_IPP + CFixedBuffer< unsigned char > WorkBuffer; ///< Working buffer. + #elif R8B_PFFFT + CFixedBuffer< float > work; ///< Working buffer, includes temporary + ///< buffer for double-to-float conversions. + #elif R8B_PFFFT_DOUBLE + CFixedBuffer< double > work; ///< Working buffer. + #endif // R8B_PFFFT_DOUBLE + + /** + * Constructor initializes FFT object. + * + * @param s0 Pointer to FFT setup object. + */ + + CDSPRealFFT( const CDSPRealFFTSetup* const s0 ) + : s( s0 ) + , Len( s -> Len ) + { + #if R8B_IPP + + WorkBuffer.alloc( s -> WorkBufferSize ); + + #elif R8B_PFFFT + + work.alloc( Len * 2 ); + + #elif R8B_PFFFT_DOUBLE + + work.alloc( Len ); + + #endif // R8B_PFFFT_DOUBLE + } +}; + +/** + * @brief A "keeper" class for real-valued FFT transform objects. + * + * Class implements "keeper" functionality for handling CDSPRealFFT objects. + * The allocated FFT objects are placed on the global static list of objects + * for future reuse instead of deallocation. + */ + +class CDSPRealFFTKeeper : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPRealFFTKeeper ) + +public: + CDSPRealFFTKeeper() + : Object( R8B_NULL ) + { + } + + /** + * @brief Acquires FFT object with the specified block length. + * + * @param LenBits The length of FFT block (Nth power of 2), in the range + * [1; 30] inclusive, specifies the number of real values in a FFT block. + */ + + CDSPRealFFTKeeper( const int LenBits ) + { + Object = acquire( LenBits ); + } + + ~CDSPRealFFTKeeper() + { + if( Object != R8B_NULL ) + { + release( Object ); + } + } + + /** + * @brief Returns pointer to the acquired FFT object. + */ + + const CDSPRealFFT* operator -> () const + { + R8BASSERT( Object != R8B_NULL ); + + return( Object ); + } + + /** + * @brief Acquires FFT object with the specified block length. This + * function can be called any number of times. + * + * @param LenBits The length of FFT block (Nth power of 2), in the range + * [1; 30] inclusive, specifies the number of real values in a FFT block. + */ + + void init( const int LenBits ) + { + if( Object != R8B_NULL ) + { + if( Object -> getLenBits() == LenBits ) + { + return; + } + + release( Object ); + } + + Object = acquire( LenBits ); + } + + /** + * @brief Releases a previously acquired FFT object. + */ + + void reset() + { + if( Object != R8B_NULL ) + { + release( Object ); + Object = R8B_NULL; + } + } + +private: + CDSPRealFFT* Object; ///< FFT object. + + /** + * @brief Acquires FFT object from the global pool. + * + * @param LenBits FFT block length (expressed as Nth power of 2). + */ + + CDSPRealFFT* acquire( const int LenBits ) + { + R8BASSERT( LenBits > 0 && LenBits <= 30 ); + + R8BSYNC( getStateSync() ); + + CDSPRealFFTSetup* s = getFFTSetupObjects()[ LenBits ]; + + if( s == R8B_NULL ) + { + s = new CDSPRealFFTSetup( LenBits ); + getFFTSetupObjects()[ LenBits ] = s; + } + + #if R8B_OOURA + + if( getFFTObjects()[ LenBits ] == R8B_NULL ) + { + getFFTObjects()[ LenBits ] = new CDSPRealFFT( s ); + } + + return( getFFTObjects()[ LenBits ]); + + #else // R8B_OOURA + + if( getFFTObjects()[ LenBits ] == R8B_NULL ) + { + return( new CDSPRealFFT( s )); + } + + CDSPRealFFT* const ffto = getFFTObjects()[ LenBits ].unkeep(); + getFFTObjects()[ LenBits ] = ffto -> Next; + ffto -> Next = R8B_NULL; + + return( ffto ); + + #endif // R8B_OOURA + } + + /** + * @brief Releases a previously acquired FFT object. + * + * @param ffto FFT object to release. + */ + + void release( CDSPRealFFT* const ffto ) + { + #if R8B_OOURA + + (void) ffto; + + #else // R8B_OOURA + + R8BSYNC( getStateSync() ); + + ffto -> Next = getFFTObjects()[ ffto -> getLenBits() ].unkeep(); + getFFTObjects()[ ffto -> getLenBits() ] = ffto; + + #endif // R8B_OOURA + } + + /** + * @brief Returns pointer to a pool of FFT setup objects. + */ + + static CPtrKeeper< CDSPRealFFTSetup >* getFFTSetupObjects() + { + R8B_EXITDTOR static CPtrKeeper< CDSPRealFFTSetup > FFTSetups[ 31 ]; + + return( FFTSetups ); + } + + /** + * @brief Returns pointer to a pool of free FFT objects. + */ + + static CPtrKeeper< CDSPRealFFT >* getFFTObjects() + { + R8B_EXITDTOR static CPtrKeeper< CDSPRealFFT > FFTObjects[ 31 ]; + + return( FFTObjects ); + } + + /** + * @brief Returns reference to FFT object pool sync object. + */ + + static CSyncObject& getStateSync() + { + R8B_EXITDTOR static CSyncObject StateSync; + + return( StateSync ); + } +}; + +/** + * @brief Calculates the minimum-phase transform of the filter kernel, using + * a discrete Hilbert transform in cepstrum domain. + * + * For more details, see part III.B of + * http://www.hpl.hp.com/personal/Niranjan_Damera-Venkata/files/ComplexMinPhase.pdf + * + * @param[in,out] Kernel Filter kernel buffer. + * @param KernelLen Filter kernel's length, in samples. + * @param LenMult Kernel length multiplier. Used as a coefficient of + * oversampling in the frequency domain. Such oversampling is needed to + * improve the precision of the minimum-phase transform. If the filter's + * attenuation is high, this multiplier should be increased or otherwise the + * required attenuation will not be reached due to "smoothing" effect of this + * transform. + * @param DoFinalMul `true` if the final multiplication after transform + * should be performed. Such multiplication returns the gain of the signal to + * its original value. This parameter can be set to `false` if normalization + * of the resulting filter kernel is planned to be used. + * @param[out] DCGroupDelay If not `nullptr`, this variable receives group + * delay at DC offset, in samples (can be a non-integer value). + */ + +inline void calcMinPhaseTransform( double* const Kernel, const int KernelLen, + const int LenMult = 2, const bool DoFinalMul = true, + double* const DCGroupDelay = R8B_NULL ) +{ + R8BASSERT( KernelLen > 0 ); + R8BASSERT( LenMult >= 2 ); + + const int LenBits = getBitOccupancy(( KernelLen * LenMult ) - 1 ); + const int Len = 1 << LenBits; + const int Len2 = Len >> 1; + int i; + + CFixedBuffer< double > ip( Len ); + CFixedBuffer< realfft_t > ip2( Len2 + 1 ); + + memcpy( &ip[ 0 ], Kernel, (size_t) KernelLen * sizeof( ip[ 0 ])); + memset( &ip[ KernelLen ], 0, + (size_t) ( Len - KernelLen ) * sizeof( ip[ 0 ])); + + CDSPRealFFTKeeper ffto( LenBits ); + + realfft_t* aip = ffto -> reorderForward( ffto -> forward( ip ), + ffto -> getWorkBuf() ); + + realfft_t* const aip2 = &ip2[ 0 ]; + + const realfft_t nzbias = ( sizeof( realfft_t ) < sizeof( double ) ? + 1e-35 : 1e-300 ); + + // Create the "log |c|" spectrum while saving the original power spectrum + // in the "ip2" buffer. + + aip2[ 0 ] = aip[ 0 ]; + aip[ 0 ] = log( fabs( aip[ 0 ]) + nzbias ); + aip2[ Len2 ] = aip[ 1 ]; + aip[ 1 ] = log( fabs( aip[ 1 ]) + nzbias ); + + for( i = 1; i < Len2; i++ ) + { + aip2[ i ] = sqrt( aip[ i * 2 ] * aip[ i * 2 ] + + aip[ i * 2 + 1 ] * aip[ i * 2 + 1 ]); + + aip[ i * 2 ] = log( aip2[ i ] + nzbias ); + aip[ i * 2 + 1 ] = 0.0; + } + + // Convert to cepstrum and apply discrete Hilbert transform. + + ffto -> inverse( ffto -> reorderInverse( aip, ip )); + + const double m1 = ffto -> getInvMulConst(); + const double m2 = -m1; + + ip[ 0 ] = 0.0; + + for( i = 1; i < Len2; i++ ) + { + ip[ i ] *= m1; + } + + ip[ Len2 ] = 0.0; + + for( i = Len2 + 1; i < Len; i++ ) + { + ip[ i ] *= m2; + } + + // Convert Hilbert-transformed cepstrum back to the "log |c|" spectrum and + // perform its exponentiation, multiplied by the power spectrum previously + // saved in the "ip2" buffer. + + aip = ffto -> reorderForward( ffto -> forward( ip ), + ffto -> getWorkBuf() ); + + aip[ 0 ] = aip2[ 0 ]; + aip[ 1 ] = aip2[ Len2 ]; + + for( i = 1; i < Len2; i++ ) + { + aip[ i * 2 + 0 ] = cos( aip[ i * 2 + 1 ]) * aip2[ i ]; + aip[ i * 2 + 1 ] = sin( aip[ i * 2 + 1 ]) * aip2[ i ]; + } + + ffto -> inverse( ffto -> reorderInverse( aip, ip )); + + if( DoFinalMul ) + { + for( i = 0; i < KernelLen; i++ ) + { + Kernel[ i ] = ip[ i ] * m1; + } + } + else + { + memcpy( &Kernel[ 0 ], &ip[ 0 ], + (size_t) KernelLen * sizeof( Kernel[ 0 ])); + } + + if( DCGroupDelay != R8B_NULL ) + { + *DCGroupDelay = calcFIRFilterGroupDelay( Kernel, KernelLen, 0.0 ); + } +} + +} // namespace r8b + +#endif // R8B_CDSPREALFFT_INCLUDED diff --git a/native/vendor/r8brain/CDSPResampler.h b/native/vendor/r8brain/CDSPResampler.h new file mode 100644 index 0000000..13bfdf7 --- /dev/null +++ b/native/vendor/r8brain/CDSPResampler.h @@ -0,0 +1,814 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPResampler.h + * + * @brief The master sample rate converter (resampler) class. + * + * This file includes the master sample rate converter (resampler) class that + * combines all elements of this library into a single front-end class. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPRESAMPLER_INCLUDED +#define R8B_CDSPRESAMPLER_INCLUDED + +#include "CDSPHBDownsampler.h" +#include "CDSPHBUpsampler.h" +#include "CDSPBlockConvolver.h" +#include "CDSPFracInterpolator.h" + +namespace r8b { + +/** + * @brief The master sample rate converter (resampler) class. + * + * This class can be considered the "master" sample rate converter (resampler) + * class since it combines all functionality of this library into a single + * front-end class that performs sample rate conversion to/from any sample + * rate, including non-integer sample rates. + * + * Note that objects of this class can be constructed on the stack as it has a + * small member data size. The default template parameters of this class are + * suited for 27-bit fixed point resampling. + * + * WARNING: Since FFT and filter object caches are global static variables, + * objects of the resampler class should not be declared as global variables + * to avoid causing crashes on exit; they should instead be allocated on the + * heap or stack. Additionally, under pre-C++11 standards only, the first-ever + * resampler object must be constructed in a non-concurrent context (i.e., + * not concurrently). + * + * Use the CDSPResampler16 class for 16-bit resampling. + * + * Use the CDSPResampler16IR class for 16-bit impulse response resampling. + * + * Use the CDSPResampler24 class for 24-bit resampling (including 32-bit + * floating point resampling). + */ + +class CDSPResampler : public CDSPProcessor +{ +public: + /** + * @brief Initalizes the resampler object. + * + * Note that increasing the transition band and decreasing attenuation + * reduces the filter length, this in turn reduces the "input before + * output" delay. However, the filter length has only a minor influence on + * the overall resampling speed. + * + * It should be noted that the `ReqAtten` specifies the minimal difference + * between the loudest input signal component and the produced aliasing + * artifacts during resampling. For example, if `ReqAtten=100` was + * specified when performing 2x upsampling, the analysis of the resulting + * signal may display high-frequency components which are quieter than + * the loudest part of the input signal by only 100 decibel meaning + * the high-frequency part did not become "magically" completely silent + * after resampling. You have to specify a higher `ReqAtten` value if you + * need a totally clean high-frequency content. On the other hand, it may + * not be reasonable to have a high-frequency content cleaner than + * the input signal itself: if the input signal is 16-bit, setting + * `ReqAtten` to 180 will make its high-frequency content 24-bit, but + * the original part of the signal will remain 16-bit. + * + * @param SrcSampleRate Source signal's sample rate. Both sample rates can + * be specified as a ratio: e.g., `SrcSampleRate = 1.0`, + * `DstSampleRate = 2.0`. + * @param DstSampleRate Destination signal's sample rate. The "power of 2" + * ratios between the source and destination sample rates force resampler + * to use several fast "power of 2" resampling steps, without using + * fractional interpolation at all. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. The resampler relies on + * this value as it allocates intermediate buffers. Input buffers longer + * than this value should never be supplied to the resampler. Note that + * upsampling produces more samples than was provided on input, so at + * higher upsampling ratios it is advisable to use smaller `aMaxInLen` + * values to reduce memory footprint. When downsampling, a larger + * `aMaxInLen` is suggested in order to increase downsampling performance. + * @param ReqTransBand Required transition band, in percent of the + * spectral space of the input signal (or the output signal if + * downsampling is performed) between filter's -3 dB point and the Nyquist + * frequency. The range is from CDSPFIRFilter::getLPMinTransBand() to + * CDSPFIRFilter::getLPMaxTransBand(), inclusive. When upsampling 88200 or + * 96000 audio to higher sample rates the ReqTransBand can be considerably + * increased, up to 30. The selection of ReqTransBand depends on the level + * of desire to preserve the high-frequency content. While values 0.5 to 2 + * are extremely "greedy" settings, not necessary in most cases, values 2 + * to 3 can be used in most cases. Values 3 to 4 are relaxed settings, but + * they still offer a flat frequency response up to 21kHz with 44.1k + * source or destination sample rate. + * @param ReqAtten Required stop-band attenuation in decibel, in the + * range CDSPFIRFilter::getLPMinAtten() to CDSPFIRFilter::getLPMaxAtten(), + * inclusive. The actual attenuation may be 0.40-4.46 dB higher. The + * general formula for selecting the `ReqAtten` is `6.02 * Bits + 40`, + * where `Bits` is the bit resolution (e.g., 16, 24), 40 is an added + * resolution for dynamic signals; this value can be decreased to 20 to + * 10, if the signal being resampled is non-dynamic (e.g., an impulse + * response or filter, with a non-steep frequency response). + * @param ReqPhase Required filter's phase response. Note that this + * setting does not affect interpolator's phase response which is always + * linear-phase. Also note that if the "power of 2" resampling was engaged + * by the resampler together with the minimum-phase response, the audio + * stream may become fractionally delayed, depending on the minimum-phase + * filter's actual fractional delay. Linear-phase filters do not have + * fractional delay. + * @see EDSPFilterPhaseResponse + */ + + CDSPResampler( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0, + const double ReqAtten = 206.91, + const EDSPFilterPhaseResponse ReqPhase = fprLinearPhase ) + : MaxInLen( aMaxInLen ) + , CurMaxOutLen( aMaxInLen ) + , LatencyFrac( 0.0 ) + { + R8BASSERT( SrcSampleRate > 0.0 ); + R8BASSERT( DstSampleRate > 0.0 ); + R8BASSERT( MaxInLen > 0 ); + R8BASSERT( ReqTransBand > 0.0 ); + R8BASSERT( ReqAtten > 0.0 ); + + R8BCONSOLE( "* CDSPResampler: src=%.1f dst=%.1f len=%i tb=%.1f " + "att=%.2f ph=%i\n", SrcSampleRate, DstSampleRate, aMaxInLen, + ReqTransBand, ReqAtten, (int) ReqPhase ); + + if( SrcSampleRate == DstSampleRate ) + { + return; + } + + TmpBufCapacities[ 0 ] = 0; + TmpBufCapacities[ 1 ] = 0; + CurTmpBuf = 0; + + // Try some common efficient ratios requiring only a single step. + + const int CommonRatioCount = 5; + const int CommonRatios[ CommonRatioCount ][ 2 ] = { + { 1, 2 }, + { 1, 3 }, + { 2, 3 }, + { 3, 2 }, + { 3, 4 } + }; + + int i; + + for( i = 0; i < CommonRatioCount; i++ ) + { + const int num = CommonRatios[ i ][ 0 ]; + const int den = CommonRatios[ i ][ 1 ]; + + if( SrcSampleRate * num == DstSampleRate * den ) + { + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( + 1.0 / ( num > den ? num : den ), ReqTransBand, + ReqAtten, ReqPhase, num, den ), num, den, LatencyFrac )); + + createTmpBuffers(); + return; + } + } + + // Try whole-number power-of-2 or 3*power-of-2 upsampling. + + for( i = 2; i <= 3; i++ ) + { + bool WasFound = false; + double sm = i; + int c = 0; + + while( true ) + { + const double NewSR = SrcSampleRate * sm; + + if( NewSR == DstSampleRate ) + { + WasFound = true; + break; + } + + if( NewSR > DstSampleRate ) + { + break; + } + + sm *= 2.0; + c++; + } + + if( WasFound ) + { + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( 1.0 / i, ReqTransBand, + ReqAtten, ReqPhase, i, 1 ), i, 1, LatencyFrac )); + + const bool IsThirdFirst = ( i == 3 ); + + for( i = 0; i < c; i++ ) + { + addProcessor( new CDSPHBUpsampler( ReqAtten, i, + ( IsThirdFirst || i > 1 ), LatencyFrac )); + } + + createTmpBuffers(); + return; + } + } + + if( DstSampleRate * 2.0 > SrcSampleRate ) + { + // Upsampling or fractional downsampling down to 2X. + + const double NormFreq = ( DstSampleRate > SrcSampleRate ? 0.5 : + 0.5 * DstSampleRate / SrcSampleRate ); + + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( NormFreq, ReqTransBand, + ReqAtten, ReqPhase, 2.0, 1 ), 2, 1, LatencyFrac )); + + // Try intermediate interpolated resampling with subsequent 2X + // or 3X upsampling. + + const double tbw = 0.0175; // Intermediate filter's transition + // band extension coefficient. + const double ThreshSampleRate = SrcSampleRate / + ( 1.0 - tbw * ReqTransBand ); // Make sure intermediate + // filter's transition band is not steeper than ReqTransBand + // (this keeps the latency under control). + + int c = 0; + double div = 1.0; + + while( true ) + { + const double ndiv = div * 2.0; + + if( DstSampleRate < ThreshSampleRate * ndiv ) + { + break; + } + + div = ndiv; + c++; + } + + const double SrcSampleRate2 = SrcSampleRate * 2.0; + int tmp1; + int tmp2; + + if( c == 1 && getWholeStepping( SrcSampleRate2, DstSampleRate, + tmp1, tmp2 )) + { + // Do not use intermediate interpolation if whole stepping is + // available as it performs very fast. + + c = 0; + } + + if( c > 0 ) + { + // Add steps using intermediate interpolation. + + const int num = 2; + + addProcessor( new CDSPFracInterpolator( SrcSampleRate2 * div, + DstSampleRate, ReqAtten, false, LatencyFrac )); + + double tb = ( 1.0 - SrcSampleRate * div / DstSampleRate ) / + tbw; // Divide TransBand by a constant that assures a + // linear response in the pass-band. + + if( tb > CDSPFIRFilter :: getLPMaxTransBand() ) + { + tb = CDSPFIRFilter :: getLPMaxTransBand(); + } + + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( 1.0 / num, tb, + ReqAtten, ReqPhase, num, 1 ), num, 1, LatencyFrac )); + + for( i = 1; i < c; i++ ) + { + addProcessor( new CDSPHBUpsampler( ReqAtten, i - 1, + ( i > 2 ), LatencyFrac )); + } + } + else + { + addProcessor( new CDSPFracInterpolator( SrcSampleRate2, + DstSampleRate, ReqAtten, false, LatencyFrac )); + } + + createTmpBuffers(); + return; + } + + // Use downsampling steps, including power-of-2 downsampling. + + double CheckSR = DstSampleRate * 4.0; + double SrcSRDiv = 1.0; + int c = 0; + + while( CheckSR <= SrcSampleRate ) + { + c++; + CheckSR *= 2.0; + SrcSRDiv *= 2.0; + } + + const double FinGain = 1.0 / SrcSRDiv; + + double NormFreq = 0.5; + bool UseInterp = true; + bool IsThirdFirst = false; + int downf; + + for( downf = 2; downf <= 3; downf++ ) + { + if( DstSampleRate * SrcSRDiv * downf == SrcSampleRate ) + { + NormFreq = 1.0 / downf; + UseInterp = false; + IsThirdFirst = ( downf == 3 ); + break; + } + } + + if( UseInterp ) + { + downf = 1; + NormFreq = DstSampleRate * SrcSRDiv / SrcSampleRate; + IsThirdFirst = ( NormFreq * 3.0 <= 1.0 ); + } + + for( i = 0; i < c; i++ ) + { + // Use fixed, very relaxed 2X downsampling filters, that at the + // final stage only guarantee stop-band between 0.75 and 1.0 of + // Nyquist. 0.5-0.75 range will be aliased to 0.25-0.5 range which + // will then be filtered out by the final filter. + + addProcessor( new CDSPHBDownsampler( ReqAtten, c - 1 - i, + ( IsThirdFirst || i < c - 2 ), LatencyFrac )); + } + + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( NormFreq, ReqTransBand, + ReqAtten, ReqPhase, FinGain, downf ), 1, downf, LatencyFrac )); + + if( UseInterp ) + { + addProcessor( new CDSPFracInterpolator( SrcSampleRate, + DstSampleRate * SrcSRDiv, ReqAtten, IsThirdFirst, + LatencyFrac )); + } + + createTmpBuffers(); + } + + virtual int getInLenBeforeOutPos( int ReqOutPos ) const + { + R8BASSERT( ReqOutPos >= 0 ); + + if( Next != R8B_NULL ) + { + ReqOutPos = Next -> getInLenBeforeOutPos( ReqOutPos ); + } + + if( StepFirst != R8B_NULL ) + { + ReqOutPos = StepFirst -> getInLenBeforeOutPos( ReqOutPos ); + } + + return( ReqOutPos ); + } + + /** + * @brief Returns the number of input samples required to advance to + * the specified output sample position (so that the next process() call + * passes this output position), starting at the cleared or + * after-construction state of *this* object. + * + * This function works by iteratively passing 1 sample at a time until the + * overall output length passes the specified value. This is a relatively + * CPU-consuming operation. This function should be called after the + * clear() function call or after object's construction. The function + * itself calls the clear() function before return. + * + * Note that this function can be considered a legacy function, and is now + * used for testing purposes. It is advised to use "instant" (much faster) + * getInLenBeforeOutPos() and getInputRequiredForOutput() functions + * instead. + * + * @param ReqOutPos The required output position. Must be a non-negative + * value. + * @return The number of input samples required. + */ + + int getInLenBeforeOutStart( const int ReqOutPos = 0 ) + { + R8BASSERT( ReqOutPos >= 0 ); + + int inc = 0; + int outc = 0; + + while( true ) + { + double ins = 0.0; + double* op; + outc += process( &ins, 1, op ); + + if( outc > ReqOutPos ) + { + clear(); + return( inc ); + } + + inc++; + } + } + + /** + * @brief Returns the number of input samples required to produce at + * least the specified number of output samples, starting at the cleared + * or after-construction state of *this* object. + * + * @param ReqOutSamples The number of output samples required. If a + * non-positive value was specified, the function returns 0. + * @return The number of input samples required. + */ + + int getInputRequiredForOutput( const int ReqOutSamples ) const + { + if( ReqOutSamples < 1 ) + { + return( 0 ); + } + + return( getInLenBeforeOutPos( ReqOutSamples - 1 ) + 1 ); + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + /** + * @brief This implementation ignores the supplied parameter and returns + * the maximal output buffer length that depends on the `MaxInLen` + * supplied to the constructor. + */ + + virtual int getMaxOutLen( const int/* MaxInLen */) const + { + return( CurMaxOutLen ); + } + + /** + * @brief Clears (resets) the state of *this* object and returns it to + * the state after construction. + * + * All input data accumulated in the internal buffer so far will be + * discarded. + * + * This function makes it possible to use *this* object for converting + * separate streams from the same source sample rate to the same + * destination sample rate without reconstructing the object. It is more + * efficient to clear the state of the resampler object than to destroy it + * and create a new object. + */ + + virtual void clear() + { + CDSPProcessor* ps = StepFirst; + + while( ps != R8B_NULL ) + { + ps -> clear(); + ps = ps -> Next; + } + } + + /** + * @brief Performs sample rate conversion. + * + * If the source and destination sample rates are equal, the resampler + * will do nothing and will simply return the input buffer unchanged. + * + * You do not need to allocate an intermediate output buffer for use with + * this function. If required, the resampler will allocate a suitable + * intermediate output buffer itself. + * + * @param ip0 Input buffer. This buffer is never used as output buffer by + * this function. This pointer may be returned in `op0` if no resampling + * is happening (source sample rate equals destination sample rate). + * @param l The number of samples available in the input buffer. Should + * not exceed the `MaxInLen` supplied in the constructor. + * @param[out] op0 This variable receives the pointer to the resampled + * data. On function's return, this pointer points to *this* object's + * internal buffer. In real-time applications it is suggested to pass this + * pointer to the next output audio block and consume any data left from + * the previous output audio block first before calling the process() + * function again. The buffer pointed to by the `op0` on return is owned + * by the resampler, so it should not be freed by the caller. + * @return The number of samples available in the `op0` output buffer. If + * the data from the output buffer `op0` is going to be written to a + * bigger output buffer, it is suggested to check the returned number of + * samples so that no overflow of the bigger output buffer happens. + */ + + virtual int process( double* ip0, int l, double*& op0 ) + { + R8BASSERT( l >= 0 && l <= MaxInLen ); + + double* ip = ip0; + int tp = 0; + CDSPProcessor* ps = StepFirst; + + while( ps != R8B_NULL ) + { + double* op = TmpBufs[ tp ]; + l = ps -> process( ip, l, op ); + ps = ps -> Next; + ip = op; + tp ^= 1; + } + + op0 = ip; + return( l ); + } + + /** + * @brief Performs resampling of an input sample buffer of the specified + * length in the "one-shot" mode. + * + * This function can be useful when impulse response or time-series + * resampling is required. + * + * @param ip Input buffer pointer. + * @param iplen Length of the input buffer in samples. + * @param[out] op Output buffer pointer. + * @param oplen Length of the output buffer in samples. + * @tparam Tin Input buffer's element type. + * @tparam Tout Output buffer's element type. + */ + + template< typename Tin, typename Tout > + void oneshot( Tin* ip, int iplen, Tout* op, int oplen ) + { + CFixedBuffer< double > Buf( MaxInLen ); + bool IsZero = false; + + while( oplen > 0 ) + { + int rc; + double* p; + int i; + + if( iplen == 0 ) + { + rc = MaxInLen; + p = &Buf[ 0 ]; + + if( !IsZero ) + { + IsZero = true; + memset( p, 0, MaxInLen * sizeof( p[ 0 ])); + } + } + else + { + rc = min( iplen, MaxInLen ); + p = getOneshotBuf( ip, Buf, rc ); + ip += rc; + iplen -= rc; + } + + double* op0; + int wc = process( p, rc, op0 ); + wc = min( oplen, wc ); + + for( i = 0; i < wc; i++ ) + { + op[ i ] = (Tout) op0[ i ]; + } + + op += wc; + oplen -= wc; + } + + clear(); + } + +private: + CPtrKeeper< CDSPProcessor > StepFirst; ///< The first processor in the + ///< singly-linked list. + CDSPProcessor* StepLast; ///< The last processor in the singly-linked + ///< list. + int MaxInLen; ///< Maximal input length. + CFixedBuffer< double > TmpBufAll; ///< Buffer containing both temporary + ///< buffers. + double* TmpBufs[ 2 ]; ///< Temporary output buffers. + int TmpBufCapacities[ 2 ]; ///< Capacities of temporary buffers, updated + ///< during processing steps building. + int CurTmpBuf; ///< Current temporary buffer. + int CurMaxOutLen; ///< Current maximal output length. + double LatencyFrac; ///< Current fractional latency. After object's + ///< construction, equals to the remaining fractional latency in the + ///< output. + + /** + * @brief Adds processor, updates `MaxOutLen` variable and adjusts length + * of temporary internal buffers. + * + * @param Proc Processor to add. This pointer is inherited and will be + * destroyed on *this* object's destruction. + */ + + void addProcessor( CDSPProcessor* const Proc ) + { + if( StepFirst != R8B_NULL ) + { + StepLast -> Next = Proc; + } + else + { + StepFirst = Proc; + } + + StepLast = Proc; + + LatencyFrac = Proc -> getLatencyFrac(); + CurMaxOutLen = Proc -> getMaxOutLen( CurMaxOutLen ); + + if( CurMaxOutLen > TmpBufCapacities[ CurTmpBuf ]) + { + TmpBufCapacities[ CurTmpBuf ] = CurMaxOutLen; + } + + CurTmpBuf ^= 1; + } + + /** + * @brief Creates temporary buffers. + */ + + void createTmpBuffers() + { + const int ol = TmpBufCapacities[ 0 ] + TmpBufCapacities[ 1 ]; + + if( ol > 0 ) + { + TmpBufAll.alloc( ol ); + TmpBufs[ 0 ] = &TmpBufAll[ 0 ]; + TmpBufs[ 1 ] = &TmpBufAll[ TmpBufCapacities[ 0 ]]; + } + + R8BCONSOLE( "* CDSPResampler: init done\n" ); + } + + /** + * @brief Auxiliary function for input buffer type convrsion in the + * oneshot() function. + * + * @param ip Input buffer pointer. + * @return `ip` verbatim, without type conversion. + */ + + static double* getOneshotBuf( double* const ip, double* const, const int ) + { + return( ip ); + } + + /** + * @brief Auxiliary function for input buffer type conversion in the + * oneshot() function. + * + * @param ip Input buffer pointer. + * @param Buf Temporary buffer. + * @param rc Input sample count. + * @return `Buf` filled with input samples converted from `float`. + */ + + static double* getOneshotBuf( float* const ip, double* const Buf, + const int rc ) + { + int i; + + for( i = 0; i < rc; i++ ) + { + Buf[ i ] = ip[ i ]; + } + + return( Buf ); + } +}; + +/** + * @brief The resampler class for 16-bit resampling. + * + * This class defines resampling parameters suitable for 16-bit resampling, + * using linear-phase low-pass filter. See the r8b::CDSPResampler class for + * details. + */ + +class CDSPResampler16 : public CDSPResampler +{ +public: + /** + * @brief Initializes the 16-bit resampler. See the r8b::CDSPResampler + * class for details. + * + * @param SrcSampleRate Source signal's sample rate. + * @param DstSampleRate Destination signal's sample rate. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. + * @param ReqTransBand Required transition band, in percent. + */ + + CDSPResampler16( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0 ) + : CDSPResampler( SrcSampleRate, DstSampleRate, aMaxInLen, ReqTransBand, + 136.45, fprLinearPhase ) + { + } +}; + +/** + * @brief The resampler class for 16-bit impulse response resampling. + * + * This class defines resampling parameters suitable for 16-bit impulse + * response resampling, using linear-phase low-pass filter. Impulse responses + * are non-dynamic signals, and thus need resampler with a lesser SNR. See the + * r8b::CDSPResampler class for details. + */ + +class CDSPResampler16IR : public CDSPResampler +{ +public: + /** + * @brief Initializes the 16-bit impulse response resampler. See the + * r8b::CDSPResampler class for details. + * + * @param SrcSampleRate Source signal's sample rate. + * @param DstSampleRate Destination signal's sample rate. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. + * @param ReqTransBand Required transition band, in percent. + */ + + CDSPResampler16IR( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0 ) + : CDSPResampler( SrcSampleRate, DstSampleRate, aMaxInLen, ReqTransBand, + 109.56, fprLinearPhase ) + { + } +}; + +/** + * @brief The resampler class for 24-bit resampling. + * + * This class defines resampling parameters suitable for 24-bit resampling + * (including 32-bit floating point resampling), using linear-phase low-pass + * filter. See the r8b::CDSPResampler class for details. + */ + +class CDSPResampler24 : public CDSPResampler +{ +public: + /** + * @brief Initializes the 24-bit resampler (including 32-bit floating + * point). See the r8b::CDSPResampler class for details. + * + * @param SrcSampleRate Source signal's sample rate. + * @param DstSampleRate Destination signal's sample rate. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. + * @param ReqTransBand Required transition band, in percent. + */ + + CDSPResampler24( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0 ) + : CDSPResampler( SrcSampleRate, DstSampleRate, aMaxInLen, ReqTransBand, + 180.15, fprLinearPhase ) + { + } +}; + +} // namespace r8b + +#endif // R8B_CDSPRESAMPLER_INCLUDED diff --git a/native/vendor/r8brain/CDSPSincFilterGen.h b/native/vendor/r8brain/CDSPSincFilterGen.h new file mode 100644 index 0000000..8e200a2 --- /dev/null +++ b/native/vendor/r8brain/CDSPSincFilterGen.h @@ -0,0 +1,702 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPSincFilterGen.h + * + * @brief Sinc function-based FIR filter generator class. + * + * This file includes the CDSPSincFilterGen class implementation that + * generates FIR filters. + * + * r8brain-free-src Copyright (c) 2013-2025 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8B_CDSPSINCFILTERGEN_INCLUDED +#define R8B_CDSPSINCFILTERGEN_INCLUDED + +#include "r8bbase.h" + +namespace r8b { + +/** + * @brief Sinc function-based FIR filter generator class. + * + * Structure that holds state used to perform generation of sinc functions of + * various types, windowed by the Blackman window by default (but the window + * function can be changed if necessary). + */ + +class CDSPSincFilterGen +{ +public: + double Len2; ///< Required half filter kernel's length in samples (can be + ///< a fractional value). Final physical kernel length will be + ///< provided in the `KernelLen` variable. `Len2` should be >= 2. + double Len2i; ///< Equals `1.0 / Len2`, initialized and used by some window + ///< functions for optimization (should not be initialized by the + ///< caller). + int KernelLen; ///< Resulting length of the filter kernel, this variable + ///< is set after the call to one of the "init" functions. + int fl2; ///< Internal "half kernel length" value. This value can be used + ///< as filter's latency in samples (taps), this variable is set after + ///< the call to one of the "init" functions. + + double Freq1; ///< Required corner circular frequency 1 [0; pi]. Used only + ///< in the generateBand() function. + double Freq2; ///< Required corner circular frequency 2 [0; pi]. Used only + ///< in the generateBand() function. The range [Freq1; Freq2] defines + ///< a pass band for the generateBand() function. + double FracDelay; ///< Fractional delay in the range [0; 1], used only in + ///< the generateFrac() function. Note that the `FracDelay` parameter + ///< is actually inversed. At 0.0 value it produces 1 sample delay + ///< (with the latency equal to fl2), at 1.0 value it produces 0 + ///< sample delay (with the latency equal to fl2 - 1). + + /** + * @brief Window function type. + */ + + enum EWindowFunctionType + { + wftCosine, ///< Generalized cosine window function. No parameters + ///< required. The "Power" parameter is optional. + wftKaiser, ///< Kaiser window function. Requires the "Beta" parameter. + ///< The "Power" parameter is optional. + wftGaussian ///< Gaussian window function. Requires the "Sigma" + ///< parameter. The "Power" parameter is optional. + }; + + typedef double( CDSPSincFilterGen ::* CWindowFunc )(); ///< Window + ///< calculation function pointer type. + + /** + * @brief Initializes *this* structure for generation of a window + * function, odd-sized. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If `nullptr`, the table + * values may be used. + * @param UsePower `true` if the power factor should be used to raise the + * window function. If `true`, the power factor should be specified as the + * last value in the `Params` array. If `Params` is `nullptr`, the table + * or default value of -1.0 (off) will be used. + */ + + void initWindow( const EWindowFunctionType WinType = wftCosine, + const double* const Params = R8B_NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) floor( Len2 ); + KernelLen = fl2 + fl2 + 1; + + setWindow( WinType, Params, UsePower, true ); + } + + /** + * @brief Initializes *this* structure for generation of band-limited + * sinc filter kernel. + * + * The generateBand() function should be used to calculate the filter. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If `nullptr`, the table + * values may be used. + * @param UsePower `true` if the power factor should be used to raise the + * window function. If `true`, the power factor should be specified as the + * last value in the `Params` array. If `Params` is `nullptr`, the table + * or default value of -1.0 (off) will be used. + */ + + void initBand( const EWindowFunctionType WinType = wftCosine, + const double* const Params = R8B_NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) floor( Len2 ); + KernelLen = fl2 + fl2 + 1; + + setWindow( WinType, Params, UsePower, true ); + } + + /** + * @brief Initializes *this* structure for Hilbert transformation filter + * calculation. + * + * `Freq1` and `Freq2` variables are not used. The generateHilbert() + * function should be used to calculate the filter. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If `nullptr`, the table + * values may be used. + * @param UsePower `true` if the power factor should be used to raise the + * window function. If `true`, the power factor should be specified as the + * last value in the `Params` array. If `Params` is `nullptr`, the table + * or default value of -1.0 (off) will be used. + */ + + void initHilbert( const EWindowFunctionType WinType = wftCosine, + const double* const Params = R8B_NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) floor( Len2 ); + KernelLen = fl2 + fl2 + 1; + + setWindow( WinType, Params, UsePower, true ); + } + + /** + * @brief Initializes *this* structure for generation of full-bandwidth + * fractional delay sinc filter kernel. + * + * `Freq1` and `Freq2` variables are not used. The generateFrac() function + * should be used to calculate the filter. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If `nullptr`, the table + * values may be used. + * @param UsePower `true` if the power factor should be used to raise the + * window function. If `true`, the power factor should be specified as the + * last value in the `Params` array. If `Params` is `nullptr`, the table + * or default value of -1.0 (off) will be used. + */ + + void initFrac( const EWindowFunctionType WinType = wftCosine, + const double* const Params = R8B_NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) ceil( Len2 ); + KernelLen = fl2 + fl2; + + setWindow( WinType, Params, UsePower, false, FracDelay ); + } + + /** + * @brief Returns the next "Hann" window function coefficient. + */ + + double calcWindowHann() + { + return( 0.5 + 0.5 * w1.generate() ); + } + + /** + * @brief Returns the next "Hamming" window function coefficient. + */ + + double calcWindowHamming() + { + return( 0.54 + 0.46 * w1.generate() ); + } + + /** + * @brief Returns the next "Blackman" window function coefficient. + */ + + double calcWindowBlackman() + { + return( 0.42 + 0.5 * w1.generate() + 0.08 * w2.generate() ); + } + + /** + * @brief Returns the next "Nuttall" window function coefficient. + */ + + double calcWindowNuttall() + { + return( 0.355768 + 0.487396 * w1.generate() + + 0.144232 * w2.generate() + 0.012604 * w3.generate() ); + } + + /** + * @brief Returns the next "Blackman-Nuttall" window function coefficient. + */ + + double calcWindowBlackmanNuttall() + { + return( 0.3635819 + 0.4891775 * w1.generate() + + 0.1365995 * w2.generate() + 0.0106411 * w3.generate() ); + } + + /** + * @brief Returns the next "Kaiser" window function coefficient. + */ + + double calcWindowKaiser() + { + const double n = 1.0 - sqr( wn * Len2i + KaiserLen2Frac ); + wn++; + + if( n <= 0.0 ) + { + return( 0.0 ); + } + + return( besselI0( KaiserBeta * sqrt( n )) * KaiserMul ); + } + + /** + * @brief Returns the next "Gaussian" window function coefficient. + */ + + double calcWindowGaussian() + { + const double f = exp( -0.5 * sqr( wn * GaussianSigmaI + + GaussianSigmaFrac )); + + wn++; + + return( f ); + } + + /** + * @brief Calculates the window function only. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + */ + + void generateWindow( double* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman ) + { + op += fl2; + double* op2 = op; + + int l = fl2; + + if( Power < 0.0 ) + { + *op = ( *this.*wfunc )(); + + while( l > 0 ) + { + const double v = ( *this.*wfunc )(); + + op++; + op2--; + *op = v; + *op2 = v; + l--; + } + } + else + { + *op = pow_a(( *this.*wfunc )(), Power ); + + while( l > 0 ) + { + const double v = pow_a(( *this.*wfunc )(), Power ); + + op++; + op2--; + *op = v; + *op2 = v; + l--; + } + } + } + + /** + * @brief Calculates band-limited windowed sinc function-based filter + * kernel. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + */ + + void generateBand( double* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman ) + { + CSineGen f2( Freq2, 0.0, 1.0 / R8B_PI ); + f2.generate(); + + op += fl2; + double* op2 = op; + const double pw = Power; + int t = 1; + + if( Freq1 < 2.3e-13 ) + { + if( pw < 0.0 ) + { + *op = Freq2 * ( *this.*wfunc )() / R8B_PI; + + while( t <= fl2 ) + { + const double v = f2.generate() * ( *this.*wfunc )() / t; + op++; + op2--; + *op = v; + *op2 = v; + t++; + } + } + else + { + *op = Freq2 * pow_a(( *this.*wfunc )(), pw ) / R8B_PI; + + while( t <= fl2 ) + { + const double v = f2.generate() * + pow_a(( *this.*wfunc )(), pw ) / t; + + op++; + op2--; + *op = v; + *op2 = v; + t++; + } + } + } + else + { + CSineGen f1( Freq1, 0.0, 1.0 / R8B_PI ); + f1.generate(); + + if( pw < 0.0 ) + { + *op = ( Freq2 - Freq1 ) * ( *this.*wfunc )() / R8B_PI; + + while( t <= fl2 ) + { + const double v = ( f2.generate() - f1.generate() ) * + ( *this.*wfunc )() / t; + + op++; + op2--; + *op = v; + *op2 = v; + t++; + } + } + else + { + *op = ( Freq2 - Freq1 ) * + pow_a(( *this.*wfunc )(), pw ) / R8B_PI; + + while( t <= fl2 ) + { + const double v = ( f2.generate() - f1.generate() ) * + pow_a(( *this.*wfunc )(), pw ) / t; + + op++; + op2--; + *op = v; + *op2 = v; + t++; + } + } + } + } + + /** + * @brief Calculates windowed Hilbert transformer filter kernel. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + */ + + void generateHilbert( double* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman ) + { + static const double fvalues[ 2 ] = { 0.0, 2.0 / R8B_PI }; + op += fl2; + double* op2 = op; + + ( *this.*wfunc )(); + *op = 0.0; + + int t = 1; + + if( Power < 0.0 ) + { + while( t <= fl2 ) + { + const double v = fvalues[ t & 1 ] * ( *this.*wfunc )() / t; + op++; + op2--; + *op = v; + *op2 = -v; + t++; + } + } + else + { + while( t <= fl2 ) + { + const double v = fvalues[ t & 1 ] * + pow_a(( *this.*wfunc )(), Power ) / t; + + op++; + op2--; + *op = v; + *op2 = -v; + t++; + } + } + } + + /** + * @brief Calculates windowed fractional delay filter kernel. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + * @param opinc Output buffer increment, in "op" elements. + */ + + void generateFrac( double* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman, + const int opinc = 1 ) + { + R8BASSERT( opinc != 0 ); + + const double pw = Power; + const double fd = FracDelay; + int t = -fl2; + + if( t + fd < -Len2 ) + { + ( *this.*wfunc )(); + *op = 0.0; + op += opinc; + t++; + } + + double f = sin( fd * R8B_PI ) / R8B_PI; + + if(( t & 1 ) != 0 ) + { + f = -f; + } + + int IsZeroX = ( fabs( fd - 1.0 ) < 2.3e-13 ); + int mt = 0 - IsZeroX; + IsZeroX = ( IsZeroX || fabs( fd ) < 2.3e-13 ); + + if( pw < 0.0 ) + { + while( t < mt ) + { + *op = f * ( *this.*wfunc )() / ( t + fd ); + op += opinc; + t++; + f = -f; + } + + if( IsZeroX ) // t+FracDelay==0 + { + *op = ( *this.*wfunc )(); + } + else + { + *op = f * ( *this.*wfunc )() / fd; // t==0 + } + + mt = fl2 - 2; + + while( t < mt ) + { + op += opinc; + t++; + f = -f; + *op = f * ( *this.*wfunc )() / ( t + fd ); + } + + op += opinc; + t++; + f = -f; + const double ut = t + fd; + *op = ( ut > Len2 ? 0.0 : f * ( *this.*wfunc )() / ut ); + } + else + { + while( t < mt ) + { + *op = f * pow_a(( *this.*wfunc )(), pw ) / ( t + fd ); + op += opinc; + t++; + f = -f; + } + + if( IsZeroX ) // t+FracDelay==0 + { + *op = pow_a(( *this.*wfunc )(), pw ); + } + else + { + *op = f * pow_a(( *this.*wfunc )(), pw ) / fd; // t==0 + } + + mt = fl2 - 2; + + while( t < mt ) + { + op += opinc; + t++; + f = -f; + *op = f * pow_a(( *this.*wfunc )(), pw ) / ( t + fd ); + } + + op += opinc; + t++; + f = -f; + const double ut = t + FracDelay; + *op = ( ut > Len2 ? 0.0 : + f * pow_a(( *this.*wfunc )(), pw ) / ut ); + } + } + +private: + double Power; ///< The power factor used to raise the window function. + ///< Equals a negative value if the power factor should not be used. + CSineGen w1; ///< Cosine wave 1 for window function. + CSineGen w2; ///< Cosine wave 2 for window function. + CSineGen w3; ///< Cosine wave 3 for window function. + + double KaiserBeta; ///< Kaiser window function's "Beta" coefficient. + double KaiserMul; ///< Kaiser window function's divisor, inverse. + double KaiserLen2Frac; ///< Equals `FracDelay / Len2`. + double GaussianSigmaI; ///< Gaussian window function's "Sigma" + ///< coefficient, inverse. + double GaussianSigmaFrac; ///< Equals `FracDelay / GaussianSigma`. + + int wn; ///< Window function integer position. 0 - center of the window + ///< function. This variable may not be used by some window functions. + + /** + * @brief Initializes Kaiser window function calculation. + * + * The `FracDelay` variable should be initialized when using this window + * function. + * + * @param Params Function parameters. If `nullptr`, the default values + * will be used; otherwise, the first parameter should specify the "Beta" + * value. + * @param UsePower `true` if the power factor should be used to raise the + * window function. + * @param IsCentered `true` if centered window should be used. This + * parameter usually equals to `false` for fractional delay filters only. + */ + + void setWindowKaiser( const double* Params, const bool UsePower, + const bool IsCentered ) + { + wn = ( IsCentered ? 0 : -fl2 ); + + if( Params == R8B_NULL ) + { + KaiserBeta = 9.5945013206755156; + Power = ( UsePower ? 1.9718457932433306 : -1.0 ); + } + else + { + KaiserBeta = clampr( Params[ 0 ], 1.0, 350.0 ); + Power = ( UsePower ? fabs( Params[ 1 ]) : -1.0 ); + } + + KaiserMul = 1.0 / besselI0( KaiserBeta ); + Len2i = 1.0 / Len2; + KaiserLen2Frac = FracDelay * Len2i; + } + + /** + * @brief Initializes Gaussian window function calculation. + * + * The `FracDelay` variable should be initialized when using this window + * function. + * + * @param Params Function parameters. If `nullptr`, the table values will + * be used; otherwise, the first parameter should specify the "Sigma" + * value. + * @param UsePower `true` if the power factor should be used to raise the + * window function. + * @param IsCentered `true` if centered window should be used. This + * parameter usually equals to `false` for fractional delay filters only. + */ + + void setWindowGaussian( const double* Params, const bool UsePower, + const bool IsCentered ) + { + wn = ( IsCentered ? 0 : -fl2 ); + + if( Params == R8B_NULL ) + { + GaussianSigmaI = 1.0; + Power = -1.0; + } + else + { + GaussianSigmaI = clampr( fabs( Params[ 0 ]), 1e-1, 100.0 ); + Power = ( UsePower ? fabs( Params[ 1 ]) : -1.0 ); + } + + GaussianSigmaI *= Len2; + GaussianSigmaI = 1.0 / GaussianSigmaI; + GaussianSigmaFrac = FracDelay * GaussianSigmaI; + } + + /** + * @brief Initializes calculation of window function of the specified + * type. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If `nullptr`, the table + * values may be used. + * @param UsePower `true` if the power factor should be used to raise the + * window function. If `true`, the power factor should be specified as the + * last value in the `Params` array. If `Params` is `nullptr`, the table + * or default value of -1.0 (off) will be used. + * @param IsCentered `true` if centered window should be used. This + * parameter usually equals to `false` for fractional delay filters only. + * @param UseFracDelay Fractional delay to use. + */ + + void setWindow( const EWindowFunctionType WinType, + const double* const Params, const bool UsePower, + const bool IsCentered, const double UseFracDelay = 0.0 ) + { + FracDelay = UseFracDelay; + + if( WinType == wftCosine ) + { + if( IsCentered ) + { + w1.init( R8B_PI / Len2, R8B_PId2 ); + w2.init( R8B_2PI / Len2, R8B_PId2 ); + w3.init( R8B_3PI / Len2, R8B_PId2 ); + } + else + { + const double step1 = R8B_PI / Len2; + w1.init( step1, R8B_PId2 - step1 * fl2 + step1 * FracDelay ); + + const double step2 = R8B_2PI / Len2; + w2.init( step2, R8B_PId2 - step2 * fl2 + step2 * FracDelay ); + + const double step3 = R8B_3PI / Len2; + w3.init( step3, R8B_PId2 - step3 * fl2 + step3 * FracDelay ); + } + + Power = ( UsePower && Params != R8B_NULL ? Params[ 0 ] : -1.0 ); + } + else + if( WinType == wftKaiser ) + { + setWindowKaiser( Params, UsePower, IsCentered ); + } + else + if( WinType == wftGaussian ) + { + setWindowGaussian( Params, UsePower, IsCentered ); + } + } +}; + +} // namespace r8b + +#endif // R8B_CDSPSINCFILTERGEN_INCLUDED diff --git a/native/vendor/r8brain/LICENSE b/native/vendor/r8brain/LICENSE new file mode 100644 index 0000000..20f5a9d --- /dev/null +++ b/native/vendor/r8brain/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Aleksey Vaneev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native/vendor/r8brain/example.cpp b/native/vendor/r8brain/example.cpp new file mode 100644 index 0000000..acef13f --- /dev/null +++ b/native/vendor/r8brain/example.cpp @@ -0,0 +1,79 @@ +/** + * @file example.cpp + * + * @brief An example C++ file that demonstrates resampler's usage. + * + * This is an example file which you won't be able to compile as it includes + * undisclosed WAVE audio file input-output code. Please consider this example + * as a pseudo-code demonstrating the use of the library. Here you can find an + * example implementation of the simplest sample rate converter utility. + * + * r8brain-free-src Copyright (c) 2013-2025 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +int main() +{ + const double OutSampleRate = 96000.0; + + CWaveFile inf; + inf.loadFile( "Audio.wav" ); + + CWaveFile outf; + outf.inheritFormat( inf ); + outf.SampleRate = OutSampleRate; + outf.saveFile( "AudioOut.wav" ); + + const int InBufCapacity = 1024; + CFixedBuffer< double > InBufs[ inf.ChannelCount ]; + CPtrKeeper< CDSPResampler24 > Resamps[ inf.ChannelCount ]; + + int i; + + for( i = 0; i < inf.ChannelCount; i++ ) + { + InBufs[ i ].alloc( InBufCapacity ); + + Resamps[ i ] = new CDSPResampler24( inf.SampleRate, OutSampleRate, + InBufCapacity ); + } + + long long int ol = inf.SampleCount * OutSampleRate / inf.SampleRate; + + while( ol > 0 ) + { + int ReadCount; + inf.readData( InBufs, InBufCapacity, ReadCount ); + + if( ReadCount == -1 ) + { + ReadCount = InBufCapacity; + + for( i = 0; i < inf.ChannelCount; i++ ) + { + memset( &InBufs[ i ][ 0 ], 0, ReadCount * sizeof( double )); + } + } + + double* opp[ inf.ChannelCount ]; + int WriteCount; // At initial steps this variable can be equal to 0 + // after resampler. Same number for all channels. + + for( i = 0; i < inf.ChannelCount; i++ ) + { + WriteCount = Resamps[ i ] -> process( + InBufs[ i ], ReadCount, opp[ i ]); + } + + if( WriteCount > ol ) + { + WriteCount = (int) ol; + } + + outf.writeData( opp, WriteCount ); + ol -= WriteCount; + } + + outf.finalize(); +} diff --git a/native/vendor/r8brain/fft/fft4g.h b/native/vendor/r8brain/fft/fft4g.h new file mode 100644 index 0000000..8544e28 --- /dev/null +++ b/native/vendor/r8brain/fft/fft4g.h @@ -0,0 +1,1373 @@ +//$ nobt +//$ nocpp + +/** + * @file fft4g.h + * + * @brief Wrapper class for Takuya OOURA's FFT functions. + * + * Functions from the FFT package by: Copyright(C) 1996-2001 Takuya OOURA + * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html + * + * Modified and used with permission granted by the license. Modifications + * include a one-time `ip` and `w` arrays initialization for the rdft() + * function, which are then passed to it as `const` arrays. + * + * Here, the original "fft4g.c" file was wrapped into the "ooura_fft" class. + */ + +#ifndef R8B_FFT4G_INCLUDED +#define R8B_FFT4G_INCLUDED + +#include + +namespace r8b { + +using std :: sin; +using std :: cos; +using std :: atan; + +/** + * @brief A wrapper class around Takuya OOURA's FFT functions. + * + * A wrapper class around fft4g.c file's FFT functions by Takuya OOURA. + * Provides static private functions for use by the CDSPRealFFT class. + */ + +class ooura_fft +{ +public: +/* +Fast Fourier/Cosine/Sine Transform + dimension :one + data length :power of 2 + decimation :frequency + radix :4, 2 + data :inplace + table :use +functions + cdft: Complex Discrete Fourier Transform + rdft: Real Discrete Fourier Transform + ddct: Discrete Cosine Transform + ddst: Discrete Sine Transform + dfct: Cosine Transform of RDFT (Real Symmetric DFT) + dfst: Sine Transform of RDFT (Real Anti-symmetric DFT) +function prototypes + void cdft(int, int, double *, int *, double *); + void rdft(int, int, double *, int *, double *); + void ddct(int, int, double *, int *, double *); + void ddst(int, int, double *, int *, double *); + void dfct(int, double *, double *, int *, double *); + void dfst(int, double *, double *, int *, double *); + + +-------- Complex DFT (Discrete Fourier Transform) -------- + [definition] + + X[k] = sum_j=0^n-1 x[j]*exp(2*pi*i*j*k/n), 0<=k + X[k] = sum_j=0^n-1 x[j]*exp(-2*pi*i*j*k/n), 0<=k + ip[0] = 0; // first time only + cdft(2*n, 1, a, ip, w); + + ip[0] = 0; // first time only + cdft(2*n, -1, a, ip, w); + [parameters] + 2*n :data length (int) + n >= 1, n = power of 2 + a[0...2*n-1] :input/output data (double *) + input data + a[2*j] = Re(x[j]), + a[2*j+1] = Im(x[j]), 0<=j= 2+sqrt(n) + strictly, + length of ip >= + 2+(1<<(int)(log(n+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n/2-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + cdft(2*n, -1, a, ip, w); + is + cdft(2*n, 1, a, ip, w); + for (j = 0; j <= 2 * n - 1; j++) { + a[j] *= 1.0 / n; + } + . + + +-------- Real DFT / Inverse of Real DFT -------- + [definition] + RDFT + R[k] = sum_j=0^n-1 a[j]*cos(2*pi*j*k/n), 0<=k<=n/2 + I[k] = sum_j=0^n-1 a[j]*sin(2*pi*j*k/n), 0 IRDFT (excluding scale) + a[k] = (R[0] + R[n/2]*cos(pi*k))/2 + + sum_j=1^n/2-1 R[j]*cos(2*pi*j*k/n) + + sum_j=1^n/2-1 I[j]*sin(2*pi*j*k/n), 0<=k + ip[0] = 0; // first time only + rdft(n, 1, a, ip, w); + + ip[0] = 0; // first time only + rdft(n, -1, a, ip, w); + [parameters] + n :data length (int) + n >= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + + output data + a[2*k] = R[k], 0<=k + input data + a[2*j] = R[j], 0<=j= 2+sqrt(n/2) + strictly, + length of ip >= + 2+(1<<(int)(log(n/2+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n/2-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + rdft(n, 1, a, ip, w); + is + rdft(n, -1, a, ip, w); + for (j = 0; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- DCT (Discrete Cosine Transform) / Inverse of DCT -------- + [definition] + IDCT (excluding scale) + C[k] = sum_j=0^n-1 a[j]*cos(pi*j*(k+1/2)/n), 0<=k DCT + C[k] = sum_j=0^n-1 a[j]*cos(pi*(j+1/2)*k/n), 0<=k + ip[0] = 0; // first time only + ddct(n, 1, a, ip, w); + + ip[0] = 0; // first time only + ddct(n, -1, a, ip, w); + [parameters] + n :data length (int) + n >= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + output data + a[k] = C[k], 0<=k= 2+sqrt(n/2) + strictly, + length of ip >= + 2+(1<<(int)(log(n/2+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/4-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + ddct(n, -1, a, ip, w); + is + a[0] *= 0.5; + ddct(n, 1, a, ip, w); + for (j = 0; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- DST (Discrete Sine Transform) / Inverse of DST -------- + [definition] + IDST (excluding scale) + S[k] = sum_j=1^n A[j]*sin(pi*j*(k+1/2)/n), 0<=k DST + S[k] = sum_j=0^n-1 a[j]*sin(pi*(j+1/2)*k/n), 0 + ip[0] = 0; // first time only + ddst(n, 1, a, ip, w); + + ip[0] = 0; // first time only + ddst(n, -1, a, ip, w); + [parameters] + n :data length (int) + n >= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + + input data + a[j] = A[j], 0 + output data + a[k] = S[k], 0= 2+sqrt(n/2) + strictly, + length of ip >= + 2+(1<<(int)(log(n/2+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/4-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + ddst(n, -1, a, ip, w); + is + a[0] *= 0.5; + ddst(n, 1, a, ip, w); + for (j = 0; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- Cosine Transform of RDFT (Real Symmetric DFT) -------- + [definition] + C[k] = sum_j=0^n a[j]*cos(pi*j*k/n), 0<=k<=n + [usage] + ip[0] = 0; // first time only + dfct(n, a, t, ip, w); + [parameters] + n :data length - 1 (int) + n >= 2, n = power of 2 + a[0...n] :input/output data (double *) + output data + a[k] = C[k], 0<=k<=n + t[0...n/2] :work area (double *) + ip[0...*] :work area for bit reversal (int *) + length of ip >= 2+sqrt(n/4) + strictly, + length of ip >= + 2+(1<<(int)(log(n/4+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/8-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + a[0] *= 0.5; + a[n] *= 0.5; + dfct(n, a, t, ip, w); + is + a[0] *= 0.5; + a[n] *= 0.5; + dfct(n, a, t, ip, w); + for (j = 0; j <= n; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- Sine Transform of RDFT (Real Anti-symmetric DFT) -------- + [definition] + S[k] = sum_j=1^n-1 a[j]*sin(pi*j*k/n), 0= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + output data + a[k] = S[k], 0= 2+sqrt(n/4) + strictly, + length of ip >= + 2+(1<<(int)(log(n/4+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/8-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + dfst(n, a, t, ip, w); + is + dfst(n, a, t, ip, w); + for (j = 1; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +Appendix : + The cos/sin table is recalculated when the larger table required. + w[] and ip[] are compatible with all routines. +*/ + + +static void cdft(int n, int isgn, double *a, int *ip, double *w) +{ + if (n > (ip[0] << 2)) { + makewt(n >> 2, ip, w); + } + if (n > 4) { + if (isgn >= 0) { + bitrv2init(n, ip + 2); + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + } else { + bitrv2conj(n, ip + 2, a); + cftbsub(n, a, w); + } + } else if (n == 4) { + cftfsub(n, a, w); + } +} + + +static void rdftinit(int n, int *ip, double *w) +{ + int nw, nc; + + nw = n >> 2; + makewt(nw, ip, w); + nc = ip[1]; + if (n > (nc << 2)) { + nc = n >> 2; + makect(nc, ip, w + nw); + } + if (n > 4) { + bitrv2init(n, ip + 2); + } +} + + +static void rdft(int n, int isgn, double *a, const int *ip, const double *w) +{ + int nw, nc; + double xi; + + nw = ip[0]; + nc = ip[1]; + if (isgn >= 0) { + if (n > 4) { + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + rftfsub(n, a, nc, w + nw); + } else if (n == 4) { + cftfsub(n, a, w); + } + xi = a[0] - a[1]; + a[0] += a[1]; + a[1] = xi; + } else { + a[1] = 0.5 * (a[0] - a[1]); + a[0] -= a[1]; + if (n > 4) { + rftbsub(n, a, nc, w + nw); + bitrv2(n, ip + 2, a); + cftbsub(n, a, w); + } else if (n == 4) { + cftfsub(n, a, w); + } + } +} + + +static void ddct(int n, int isgn, double *a, int *ip, double *w) +{ + int j, nw, nc; + double xr; + + nw = ip[0]; + if (n > (nw << 2)) { + nw = n >> 2; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > nc) { + nc = n; + makect(nc, ip, w + nw); + } + if (isgn < 0) { + xr = a[n - 1]; + for (j = n - 2; j >= 2; j -= 2) { + a[j + 1] = a[j] - a[j - 1]; + a[j] += a[j - 1]; + } + a[1] = a[0] - xr; + a[0] += xr; + if (n > 4) { + rftbsub(n, a, nc, w + nw); + bitrv2init(n, ip + 2); + bitrv2(n, ip + 2, a); + cftbsub(n, a, w); + } else if (n == 4) { + cftfsub(n, a, w); + } + } + dctsub(n, a, nc, w + nw); + if (isgn >= 0) { + if (n > 4) { + bitrv2init(n, ip + 2); + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + rftfsub(n, a, nc, w + nw); + } else if (n == 4) { + cftfsub(n, a, w); + } + xr = a[0] - a[1]; + a[0] += a[1]; + for (j = 2; j < n; j += 2) { + a[j - 1] = a[j] - a[j + 1]; + a[j] += a[j + 1]; + } + a[n - 1] = xr; + } +} + + +static void ddst(int n, int isgn, double *a, int *ip, double *w) +{ + int j, nw, nc; + double xr; + + nw = ip[0]; + if (n > (nw << 2)) { + nw = n >> 2; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > nc) { + nc = n; + makect(nc, ip, w + nw); + } + if (isgn < 0) { + xr = a[n - 1]; + for (j = n - 2; j >= 2; j -= 2) { + a[j + 1] = -a[j] - a[j - 1]; + a[j] -= a[j - 1]; + } + a[1] = a[0] + xr; + a[0] -= xr; + if (n > 4) { + rftbsub(n, a, nc, w + nw); + bitrv2init(n, ip + 2); + bitrv2(n, ip + 2, a); + cftbsub(n, a, w); + } else if (n == 4) { + cftfsub(n, a, w); + } + } + dstsub(n, a, nc, w + nw); + if (isgn >= 0) { + if (n > 4) { + bitrv2init(n, ip + 2); + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + rftfsub(n, a, nc, w + nw); + } else if (n == 4) { + cftfsub(n, a, w); + } + xr = a[0] - a[1]; + a[0] += a[1]; + for (j = 2; j < n; j += 2) { + a[j - 1] = -a[j] - a[j + 1]; + a[j] -= a[j + 1]; + } + a[n - 1] = -xr; + } +} + + +static void dfct(int n, double *a, double *t, int *ip, double *w) +{ + int j, k, l, m, mh, nw, nc; + double xr, xi, yr, yi; + + nw = ip[0]; + if (n > (nw << 3)) { + nw = n >> 3; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > (nc << 1)) { + nc = n >> 1; + makect(nc, ip, w + nw); + } + m = n >> 1; + yi = a[m]; + xi = a[0] + a[n]; + a[0] -= a[n]; + t[0] = xi - yi; + t[m] = xi + yi; + if (n > 2) { + mh = m >> 1; + for (j = 1; j < mh; j++) { + k = m - j; + xr = a[j] - a[n - j]; + xi = a[j] + a[n - j]; + yr = a[k] - a[n - k]; + yi = a[k] + a[n - k]; + a[j] = xr; + a[k] = yr; + t[j] = xi - yi; + t[k] = xi + yi; + } + t[mh] = a[mh] + a[n - mh]; + a[mh] -= a[n - mh]; + dctsub(m, a, nc, w + nw); + if (m > 4) { + bitrv2init(m, ip + 2); + bitrv2(m, ip + 2, a); + cftfsub(m, a, w); + rftfsub(m, a, nc, w + nw); + } else if (m == 4) { + cftfsub(m, a, w); + } + a[n - 1] = a[0] - a[1]; + a[1] = a[0] + a[1]; + for (j = m - 2; j >= 2; j -= 2) { + a[2 * j + 1] = a[j] + a[j + 1]; + a[2 * j - 1] = a[j] - a[j + 1]; + } + l = 2; + m = mh; + while (m >= 2) { + dctsub(m, t, nc, w + nw); + if (m > 4) { + bitrv2init(m, ip + 2); + bitrv2(m, ip + 2, t); + cftfsub(m, t, w); + rftfsub(m, t, nc, w + nw); + } else if (m == 4) { + cftfsub(m, t, w); + } + a[n - l] = t[0] - t[1]; + a[l] = t[0] + t[1]; + k = 0; + for (j = 2; j < m; j += 2) { + k += l << 2; + a[k - l] = t[j] - t[j + 1]; + a[k + l] = t[j] + t[j + 1]; + } + l <<= 1; + mh = m >> 1; + for (j = 0; j < mh; j++) { + k = m - j; + t[j] = t[m + k] - t[m + j]; + t[k] = t[m + k] + t[m + j]; + } + t[mh] = t[m + mh]; + m = mh; + } + a[l] = t[0]; + a[n] = t[2] - t[1]; + a[0] = t[2] + t[1]; + } else { + a[1] = a[0]; + a[2] = t[0]; + a[0] = t[1]; + } +} + + +static void dfst(int n, double *a, double *t, int *ip, double *w) +{ + int j, k, l, m, mh, nw, nc; + double xr, xi, yr, yi; + + nw = ip[0]; + if (n > (nw << 3)) { + nw = n >> 3; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > (nc << 1)) { + nc = n >> 1; + makect(nc, ip, w + nw); + } + if (n > 2) { + m = n >> 1; + mh = m >> 1; + for (j = 1; j < mh; j++) { + k = m - j; + xr = a[j] + a[n - j]; + xi = a[j] - a[n - j]; + yr = a[k] + a[n - k]; + yi = a[k] - a[n - k]; + a[j] = xr; + a[k] = yr; + t[j] = xi + yi; + t[k] = xi - yi; + } + t[0] = a[mh] - a[n - mh]; + a[mh] += a[n - mh]; + a[0] = a[m]; + dstsub(m, a, nc, w + nw); + if (m > 4) { + bitrv2init(m, ip + 2); + bitrv2(m, ip + 2, a); + cftfsub(m, a, w); + rftfsub(m, a, nc, w + nw); + } else if (m == 4) { + cftfsub(m, a, w); + } + a[n - 1] = a[1] - a[0]; + a[1] = a[0] + a[1]; + for (j = m - 2; j >= 2; j -= 2) { + a[2 * j + 1] = a[j] - a[j + 1]; + a[2 * j - 1] = -a[j] - a[j + 1]; + } + l = 2; + m = mh; + while (m >= 2) { + dstsub(m, t, nc, w + nw); + if (m > 4) { + bitrv2init(m, ip + 2); + bitrv2(m, ip + 2, t); + cftfsub(m, t, w); + rftfsub(m, t, nc, w + nw); + } else if (m == 4) { + cftfsub(m, t, w); + } + a[n - l] = t[1] - t[0]; + a[l] = t[0] + t[1]; + k = 0; + for (j = 2; j < m; j += 2) { + k += l << 2; + a[k - l] = -t[j] - t[j + 1]; + a[k + l] = t[j] - t[j + 1]; + } + l <<= 1; + mh = m >> 1; + for (j = 1; j < mh; j++) { + k = m - j; + t[j] = t[m + k] + t[m + j]; + t[k] = t[m + k] - t[m + j]; + } + t[0] = t[m + mh]; + m = mh; + } + a[l] = t[0]; + } + a[0] = 0; +} + + +/* -------- initializing routines -------- */ + + + +static void makewt(int nw, int *ip, double *w) +{ + int j, nwh; + double delta, x, y; + + ip[0] = nw; + ip[1] = 1; + if (nw > 2) { + nwh = nw >> 1; + delta = atan(1.0) / nwh; + w[0] = 1; + w[1] = 0; + w[nwh] = cos(delta * nwh); + w[nwh + 1] = w[nwh]; + if (nwh > 2) { + for (j = 2; j < nwh; j += 2) { + x = cos(delta * j); + y = sin(delta * j); + w[j] = x; + w[j + 1] = y; + w[nw - j] = y; + w[nw - j + 1] = x; + } + bitrv2init(nw, ip + 2); + bitrv2(nw, ip + 2, w); + } + } +} + + +static void makect(int nc, int *ip, double *c) +{ + int j, nch; + double delta; + + ip[1] = nc; + if (nc > 1) { + nch = nc >> 1; + delta = atan(1.0) / nch; + c[0] = cos(delta * nch); + c[nch] = 0.5 * c[0]; + for (j = 1; j < nch; j++) { + c[j] = 0.5 * cos(delta * j); + c[nc - j] = 0.5 * sin(delta * j); + } + } +} + + +/* -------- child routines -------- */ + + +static void bitrv2init(int n, int *ip) +{ + int j, l, m; + + ip[0] = 0; + l = n; + m = 1; + while ((m << 3) < l) { + l >>= 1; + for (j = 0; j < m; j++) { + ip[m + j] = ip[j] + l; + } + m <<= 1; + } +} + +static void bitrv2(int n, const int *ip, double *a) +{ + int j, j1, k, k1, l, m, m2; + double xr, xi, yr, yi; + + l = n; + m = 1; + while ((m << 3) < l) { + l >>= 1; + m <<= 1; + } + m2 = 2 * m; + if ((m << 3) == l) { + for (k = 0; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 -= m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + j1 = 2 * k + m2 + ip[k]; + k1 = j1 + m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + } else { + for (k = 1; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + } + } +} + + +static void bitrv2conj(int n, int *ip, double *a) +{ + int j, j1, k, k1, l, m, m2; + double xr, xi, yr, yi; + + ip[0] = 0; + l = n; + m = 1; + while ((m << 3) < l) { + l >>= 1; + for (j = 0; j < m; j++) { + ip[m + j] = ip[j] + l; + } + m <<= 1; + } + m2 = 2 * m; + if ((m << 3) == l) { + for (k = 0; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 -= m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + k1 = 2 * k + ip[k]; + a[k1 + 1] = -a[k1 + 1]; + j1 = k1 + m2; + k1 = j1 + m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + k1 += m2; + a[k1 + 1] = -a[k1 + 1]; + } + } else { + a[1] = -a[1]; + a[m2 + 1] = -a[m2 + 1]; + for (k = 1; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + k1 = 2 * k + ip[k]; + a[k1 + 1] = -a[k1 + 1]; + a[k1 + m2 + 1] = -a[k1 + m2 + 1]; + } + } +} + + +static void cftfsub(int n, double *a, const double *w) +{ + int j, j1, j2, j3, l; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; + + l = 2; + if (n > 8) { + cft1st(n, a, w); + l = 8; + while ((l << 2) < n) { + cftmdl(n, l, a, w); + l <<= 2; + } + } + if ((l << 2) == n) { + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + a[j2] = x0r - x2r; + a[j2 + 1] = x0i - x2i; + a[j1] = x1r - x3i; + a[j1 + 1] = x1i + x3r; + a[j3] = x1r + x3i; + a[j3 + 1] = x1i - x3r; + } + } else { + for (j = 0; j < l; j += 2) { + j1 = j + l; + x0r = a[j] - a[j1]; + x0i = a[j + 1] - a[j1 + 1]; + a[j] += a[j1]; + a[j + 1] += a[j1 + 1]; + a[j1] = x0r; + a[j1 + 1] = x0i; + } + } +} + + +static void cftbsub(int n, double *a, const double *w) +{ + int j, j1, j2, j3, l; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; + + l = 2; + if (n > 8) { + cft1st(n, a, w); + l = 8; + while ((l << 2) < n) { + cftmdl(n, l, a, w); + l <<= 2; + } + } + if ((l << 2) == n) { + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = -a[j + 1] - a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = -a[j + 1] + a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i - x2i; + a[j2] = x0r - x2r; + a[j2 + 1] = x0i + x2i; + a[j1] = x1r - x3i; + a[j1 + 1] = x1i - x3r; + a[j3] = x1r + x3i; + a[j3 + 1] = x1i + x3r; + } + } else { + for (j = 0; j < l; j += 2) { + j1 = j + l; + x0r = a[j] - a[j1]; + x0i = -a[j + 1] + a[j1 + 1]; + a[j] += a[j1]; + a[j + 1] = -a[j + 1] - a[j1 + 1]; + a[j1] = x0r; + a[j1 + 1] = x0i; + } + } +} + + +static void cft1st(int n, double *a, const double *w) +{ + int j, k1, k2; + double wk1r, wk1i, wk2r, wk2i, wk3r, wk3i; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; + + x0r = a[0] + a[2]; + x0i = a[1] + a[3]; + x1r = a[0] - a[2]; + x1i = a[1] - a[3]; + x2r = a[4] + a[6]; + x2i = a[5] + a[7]; + x3r = a[4] - a[6]; + x3i = a[5] - a[7]; + a[0] = x0r + x2r; + a[1] = x0i + x2i; + a[4] = x0r - x2r; + a[5] = x0i - x2i; + a[2] = x1r - x3i; + a[3] = x1i + x3r; + a[6] = x1r + x3i; + a[7] = x1i - x3r; + wk1r = w[2]; + x0r = a[8] + a[10]; + x0i = a[9] + a[11]; + x1r = a[8] - a[10]; + x1i = a[9] - a[11]; + x2r = a[12] + a[14]; + x2i = a[13] + a[15]; + x3r = a[12] - a[14]; + x3i = a[13] - a[15]; + a[8] = x0r + x2r; + a[9] = x0i + x2i; + a[12] = x2i - x0i; + a[13] = x0r - x2r; + x0r = x1r - x3i; + x0i = x1i + x3r; + a[10] = wk1r * (x0r - x0i); + a[11] = wk1r * (x0r + x0i); + x0r = x3i + x1r; + x0i = x3r - x1i; + a[14] = wk1r * (x0i - x0r); + a[15] = wk1r * (x0i + x0r); + k1 = 0; + for (j = 16; j < n; j += 16) { + k1 += 2; + k2 = 2 * k1; + wk2r = w[k1]; + wk2i = w[k1 + 1]; + wk1r = w[k2]; + wk1i = w[k2 + 1]; + wk3r = wk1r - 2 * wk2i * wk1i; + wk3i = 2 * wk2i * wk1r - wk1i; + x0r = a[j] + a[j + 2]; + x0i = a[j + 1] + a[j + 3]; + x1r = a[j] - a[j + 2]; + x1i = a[j + 1] - a[j + 3]; + x2r = a[j + 4] + a[j + 6]; + x2i = a[j + 5] + a[j + 7]; + x3r = a[j + 4] - a[j + 6]; + x3i = a[j + 5] - a[j + 7]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + x0r -= x2r; + x0i -= x2i; + a[j + 4] = wk2r * x0r - wk2i * x0i; + a[j + 5] = wk2r * x0i + wk2i * x0r; + x0r = x1r - x3i; + x0i = x1i + x3r; + a[j + 2] = wk1r * x0r - wk1i * x0i; + a[j + 3] = wk1r * x0i + wk1i * x0r; + x0r = x1r + x3i; + x0i = x1i - x3r; + a[j + 6] = wk3r * x0r - wk3i * x0i; + a[j + 7] = wk3r * x0i + wk3i * x0r; + wk1r = w[k2 + 2]; + wk1i = w[k2 + 3]; + wk3r = wk1r - 2 * wk2r * wk1i; + wk3i = 2 * wk2r * wk1r - wk1i; + x0r = a[j + 8] + a[j + 10]; + x0i = a[j + 9] + a[j + 11]; + x1r = a[j + 8] - a[j + 10]; + x1i = a[j + 9] - a[j + 11]; + x2r = a[j + 12] + a[j + 14]; + x2i = a[j + 13] + a[j + 15]; + x3r = a[j + 12] - a[j + 14]; + x3i = a[j + 13] - a[j + 15]; + a[j + 8] = x0r + x2r; + a[j + 9] = x0i + x2i; + x0r -= x2r; + x0i -= x2i; + a[j + 12] = -wk2i * x0r - wk2r * x0i; + a[j + 13] = -wk2i * x0i + wk2r * x0r; + x0r = x1r - x3i; + x0i = x1i + x3r; + a[j + 10] = wk1r * x0r - wk1i * x0i; + a[j + 11] = wk1r * x0i + wk1i * x0r; + x0r = x1r + x3i; + x0i = x1i - x3r; + a[j + 14] = wk3r * x0r - wk3i * x0i; + a[j + 15] = wk3r * x0i + wk3i * x0r; + } +} + + +static void cftmdl(int n, int l, double *a, const double *w) +{ + int j, j1, j2, j3, k, k1, k2, m, m2; + double wk1r, wk1i, wk2r, wk2i, wk3r, wk3i; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; + + m = l << 2; + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + a[j2] = x0r - x2r; + a[j2 + 1] = x0i - x2i; + a[j1] = x1r - x3i; + a[j1 + 1] = x1i + x3r; + a[j3] = x1r + x3i; + a[j3 + 1] = x1i - x3r; + } + wk1r = w[2]; + for (j = m; j < l + m; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + a[j2] = x2i - x0i; + a[j2 + 1] = x0r - x2r; + x0r = x1r - x3i; + x0i = x1i + x3r; + a[j1] = wk1r * (x0r - x0i); + a[j1 + 1] = wk1r * (x0r + x0i); + x0r = x3i + x1r; + x0i = x3r - x1i; + a[j3] = wk1r * (x0i - x0r); + a[j3 + 1] = wk1r * (x0i + x0r); + } + k1 = 0; + m2 = 2 * m; + for (k = m2; k < n; k += m2) { + k1 += 2; + k2 = 2 * k1; + wk2r = w[k1]; + wk2i = w[k1 + 1]; + wk1r = w[k2]; + wk1i = w[k2 + 1]; + wk3r = wk1r - 2 * wk2i * wk1i; + wk3i = 2 * wk2i * wk1r - wk1i; + for (j = k; j < l + k; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + x0r -= x2r; + x0i -= x2i; + a[j2] = wk2r * x0r - wk2i * x0i; + a[j2 + 1] = wk2r * x0i + wk2i * x0r; + x0r = x1r - x3i; + x0i = x1i + x3r; + a[j1] = wk1r * x0r - wk1i * x0i; + a[j1 + 1] = wk1r * x0i + wk1i * x0r; + x0r = x1r + x3i; + x0i = x1i - x3r; + a[j3] = wk3r * x0r - wk3i * x0i; + a[j3 + 1] = wk3r * x0i + wk3i * x0r; + } + wk1r = w[k2 + 2]; + wk1i = w[k2 + 3]; + wk3r = wk1r - 2 * wk2r * wk1i; + wk3i = 2 * wk2r * wk1r - wk1i; + for (j = k + m; j < l + (k + m); j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + x0r -= x2r; + x0i -= x2i; + a[j2] = -wk2i * x0r - wk2r * x0i; + a[j2 + 1] = -wk2i * x0i + wk2r * x0r; + x0r = x1r - x3i; + x0i = x1i + x3r; + a[j1] = wk1r * x0r - wk1i * x0i; + a[j1 + 1] = wk1r * x0i + wk1i * x0r; + x0r = x1r + x3i; + x0i = x1i - x3r; + a[j3] = wk3r * x0r - wk3i * x0i; + a[j3 + 1] = wk3r * x0i + wk3i * x0r; + } + } +} + + +static void rftfsub(int n, double *a, int nc, const double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr, xi, yr, yi; + + m = n >> 1; + ks = 2 * nc / m; + kk = 0; + for (j = 2; j < m; j += 2) { + k = n - j; + kk += ks; + wkr = 0.5 - c[nc - kk]; + wki = c[kk]; + xr = a[j] - a[k]; + xi = a[j + 1] + a[k + 1]; + yr = wkr * xr - wki * xi; + yi = wkr * xi + wki * xr; + a[j] -= yr; + a[j + 1] -= yi; + a[k] += yr; + a[k + 1] -= yi; + } +} + + +static void rftbsub(int n, double *a, int nc, const double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr, xi, yr, yi; + + a[1] = -a[1]; + m = n >> 1; + ks = 2 * nc / m; + kk = 0; + for (j = 2; j < m; j += 2) { + k = n - j; + kk += ks; + wkr = 0.5 - c[nc - kk]; + wki = c[kk]; + xr = a[j] - a[k]; + xi = a[j + 1] + a[k + 1]; + yr = wkr * xr + wki * xi; + yi = wkr * xi - wki * xr; + a[j] -= yr; + a[j + 1] = yi - a[j + 1]; + a[k] += yr; + a[k + 1] = yi - a[k + 1]; + } + a[m + 1] = -a[m + 1]; +} + + +static void dctsub(int n, double *a, int nc, const double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr; + + m = n >> 1; + ks = nc / n; + kk = 0; + for (j = 1; j < m; j++) { + k = n - j; + kk += ks; + wkr = c[kk] - c[nc - kk]; + wki = c[kk] + c[nc - kk]; + xr = wki * a[j] - wkr * a[k]; + a[j] = wkr * a[j] + wki * a[k]; + a[k] = xr; + } + a[m] *= c[0]; +} + + +static void dstsub(int n, double *a, int nc, const double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr; + + m = n >> 1; + ks = nc / n; + kk = 0; + for (j = 1; j < m; j++) { + k = n - j; + kk += ks; + wkr = c[kk] - c[nc - kk]; + wki = c[kk] + c[nc - kk]; + xr = wki * a[k] - wkr * a[j]; + a[k] = wkr * a[k] + wki * a[j]; + a[j] = xr; + } + a[m] *= c[0]; +} + +}; + +} // namespace r8b + +#endif // R8B_FFT4G_INCLUDED diff --git a/native/vendor/r8brain/fft/pffft.c b/native/vendor/r8brain/fft/pffft.c new file mode 100644 index 0000000..e10d6cf --- /dev/null +++ b/native/vendor/r8brain/fft/pffft.c @@ -0,0 +1,1942 @@ +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB + (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber + of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. + + + PFFFT : a Pretty Fast FFT. + + This file is largerly based on the original FFTPACK implementation, modified in + order to take advantage of SIMD instructions of modern CPUs. +*/ + +/* + ChangeLog: + - 2011/10/02, version 1: This is the very first release of this file. +*/ + +#ifndef _USE_MATH_DEFINES +# define _USE_MATH_DEFINES // ask gently MSVC to define M_PI, M_SQRT2 etc. +#endif + +#include "pffft.h" +#include +#include +#include +#include + +/* detect compiler flavour */ +#if defined(_MSC_VER) +# define COMPILER_MSVC +#elif defined(__GNUC__) +# define COMPILER_GCC +#endif + +#if defined(COMPILER_GCC) +# define ALWAYS_INLINE(return_type) inline return_type __attribute__ ((always_inline)) +# define NEVER_INLINE(return_type) return_type __attribute__ ((noinline)) +# define RESTRICT __restrict +# define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ varname__[size__]; +#elif defined(COMPILER_MSVC) +# define ALWAYS_INLINE(return_type) __forceinline return_type +# define NEVER_INLINE(return_type) __declspec(noinline) return_type +# define RESTRICT __restrict +# define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ *varname__ = (type__*)_alloca(size__ * sizeof(type__)) +#endif + + +/* + vector support macros: the rest of the code is independant of + SSE/Altivec/NEON -- adding support for other platforms with 4-element + vectors should be limited to these macros +*/ + + +// define PFFFT_SIMD_DISABLE if you want to use scalar code instead of simd code +//#define PFFFT_SIMD_DISABLE + +/* select which SIMD intrinsics will be used */ +#if !defined(PFFFT_SIMD_DISABLE) +# if (defined(__ppc__) || defined(__ppc64__) || defined(__powerpc__) || defined(__powerpc64__)) \ + && (defined(__VEC__) || defined(__ALTIVEC__)) +# define PFFFT_SIMD_ALTIVEC +# elif defined(__ARM_NEON) || defined(__aarch64__) || defined(__arm64) \ + || defined(_M_ARM64) || defined(_M_ARM64EC) || defined(__wasm_simd128__) + // we test _M_ARM64EC before _M_X64 because when _M_ARM64EC is defined, the microsoft compiler also defines _M_X64 +# define PFFFT_SIMD_NEON +# elif defined(__x86_64__) || defined(__SSE__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 1) +# define PFFFT_SIMD_SSE +# endif +#endif // PFFFT_SIMD_DISABLE + +/* + Altivec support macros +*/ +#ifdef PFFFT_SIMD_ALTIVEC +#include +typedef vector float v4sf; +# define SIMD_SZ 4 +# define VZERO() ((vector float) vec_splat_u8(0)) +# define VMUL(a,b) vec_madd(a,b, VZERO()) +# define VADD(a,b) vec_add(a,b) +# define VMADD(a,b,c) vec_madd(a,b,c) +# define VSUB(a,b) vec_sub(a,b) +inline v4sf ld_ps1(const float *p) { v4sf v=vec_lde(0,p); return vec_splat(vec_perm(v, v, vec_lvsl(0, p)), 0); } +# define LD_PS1(p) ld_ps1(&p) +# define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = vec_mergeh(in1, in2); out2 = vec_mergel(in1, in2); out1 = tmp__; } +# define UNINTERLEAVE2(in1, in2, out1, out2) { \ + vector unsigned char vperm1 = (vector unsigned char){0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27}; \ + vector unsigned char vperm2 = (vector unsigned char){4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31}; \ + v4sf tmp__ = vec_perm(in1, in2, vperm1); out2 = vec_perm(in1, in2, vperm2); out1 = tmp__; \ + } +# define VTRANSPOSE4(x0,x1,x2,x3) { \ + v4sf y0 = vec_mergeh(x0, x2); \ + v4sf y1 = vec_mergel(x0, x2); \ + v4sf y2 = vec_mergeh(x1, x3); \ + v4sf y3 = vec_mergel(x1, x3); \ + x0 = vec_mergeh(y0, y2); \ + x1 = vec_mergel(y0, y2); \ + x2 = vec_mergeh(y1, y3); \ + x3 = vec_mergel(y1, y3); \ + } +# define VSWAPHL(a,b) vec_perm(a,b, (vector unsigned char){16,17,18,19,20,21,22,23,8,9,10,11,12,13,14,15}) +# define VALIGNED(ptr) ((((size_t)(ptr)) & 0xF) == 0) + +/* + SSE1 support macros +*/ +#elif defined(PFFFT_SIMD_SSE) + +#include +typedef __m128 v4sf; +# define SIMD_SZ 4 // 4 floats by simd vector -- this is pretty much hardcoded in the preprocess/finalize functions anyway so you will have to work if you want to enable AVX with its 256-bit vectors. +# define VZERO() _mm_setzero_ps() +# define VMUL(a,b) _mm_mul_ps(a,b) +# define VADD(a,b) _mm_add_ps(a,b) +# define VMADD(a,b,c) _mm_add_ps(_mm_mul_ps(a,b), c) +# define VSUB(a,b) _mm_sub_ps(a,b) +# define LD_PS1(p) _mm_set1_ps(p) +# define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_unpacklo_ps(in1, in2); out2 = _mm_unpackhi_ps(in1, in2); out1 = tmp__; } +# define UNINTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(2,0,2,0)); out2 = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(3,1,3,1)); out1 = tmp__; } +# define VTRANSPOSE4(x0,x1,x2,x3) _MM_TRANSPOSE4_PS(x0,x1,x2,x3) +# define VSWAPHL(a,b) _mm_shuffle_ps(b, a, _MM_SHUFFLE(3,2,1,0)) +# define VALIGNED(ptr) ((((size_t)(ptr)) & 0xF) == 0) + +/* + ARM NEON support macros +*/ +#elif defined(PFFFT_SIMD_NEON) +# include +typedef float32x4_t v4sf; +# define SIMD_SZ 4 +# define VZERO() vdupq_n_f32(0) +# define VMUL(a,b) vmulq_f32(a,b) +# define VADD(a,b) vaddq_f32(a,b) +# define VMADD(a,b,c) vmlaq_f32(c,a,b) +# define VSUB(a,b) vsubq_f32(a,b) +# define LD_PS1(p) vld1q_dup_f32(&(p)) +# define INTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vzipq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; } +# define UNINTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vuzpq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; } +# define VTRANSPOSE4(x0,x1,x2,x3) { \ + float32x4x2_t t0_ = vzipq_f32(x0, x2); \ + float32x4x2_t t1_ = vzipq_f32(x1, x3); \ + float32x4x2_t u0_ = vzipq_f32(t0_.val[0], t1_.val[0]); \ + float32x4x2_t u1_ = vzipq_f32(t0_.val[1], t1_.val[1]); \ + x0 = u0_.val[0]; x1 = u0_.val[1]; x2 = u1_.val[0]; x3 = u1_.val[1]; \ + } +// marginally faster version +//# define VTRANSPOSE4(x0,x1,x2,x3) { asm("vtrn.32 %q0, %q1;\n vtrn.32 %q2,%q3\n vswp %f0,%e2\n vswp %f1,%e3" : "+w"(x0), "+w"(x1), "+w"(x2), "+w"(x3)::); } +# define VSWAPHL(a,b) vcombine_f32(vget_low_f32(b), vget_high_f32(a)) +# define VALIGNED(ptr) ((((size_t)(ptr)) & 0x3) == 0) +#else +# if !defined(PFFFT_SIMD_DISABLE) +# warning "building with simd disabled !\n"; +# define PFFFT_SIMD_DISABLE // fallback to scalar code +# endif +#endif + +// fallback mode for situations where SSE/Altivec are not available, use scalar mode instead +#ifdef PFFFT_SIMD_DISABLE +typedef float v4sf; +# define SIMD_SZ 1 +# define VZERO() 0.f +# define VMUL(a,b) ((a)*(b)) +# define VADD(a,b) ((a)+(b)) +# define VMADD(a,b,c) ((a)*(b)+(c)) +# define VSUB(a,b) ((a)-(b)) +# define LD_PS1(p) (p) +# define VALIGNED(ptr) ((((size_t)(ptr)) & 0x3) == 0) +#endif + +// shortcuts for complex multiplcations +#define VCPLXMUL(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VSUB(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VADD(ai,tmp); } +#define VCPLXMULCONJ(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VADD(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VSUB(ai,tmp); } +#ifndef SVMUL +// multiply a scalar with a vector +#define SVMUL(f,v) VMUL(LD_PS1(f),v) +#endif + +#if !defined(PFFFT_SIMD_DISABLE) +typedef union v4sf_union { + v4sf v; + float f[4]; +} v4sf_union; + +#include + +#define assertv4(v,f0,f1,f2,f3) assert(v.f[0] == (f0) && v.f[1] == (f1) && v.f[2] == (f2) && v.f[3] == (f3)) + +/* detect bugs with the vector support macros */ +void validate_pffft_simd(void) { + float f[16] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 }; + v4sf_union a0, a1, a2, a3, t, u; + memcpy(a0.f, f, 4*sizeof(float)); + memcpy(a1.f, f+4, 4*sizeof(float)); + memcpy(a2.f, f+8, 4*sizeof(float)); + memcpy(a3.f, f+12, 4*sizeof(float)); + + t = a0; u = a1; t.v = VZERO(); + printf("VZERO=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 0, 0, 0, 0); + t.v = VADD(a1.v, a2.v); + printf("VADD(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 12, 14, 16, 18); + t.v = VMUL(a1.v, a2.v); + printf("VMUL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 32, 45, 60, 77); + t.v = VMADD(a1.v, a2.v,a0.v); + printf("VMADD(4:7,8:11,0:3)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 32, 46, 62, 80); + + INTERLEAVE2(a1.v,a2.v,t.v,u.v); + printf("INTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]); + assertv4(t, 4, 8, 5, 9); assertv4(u, 6, 10, 7, 11); + UNINTERLEAVE2(a1.v,a2.v,t.v,u.v); + printf("UNINTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]); + assertv4(t, 4, 6, 8, 10); assertv4(u, 5, 7, 9, 11); + + t.v=LD_PS1(f[15]); + printf("LD_PS1(15)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); + assertv4(t, 15, 15, 15, 15); + t.v = VSWAPHL(a1.v, a2.v); + printf("VSWAPHL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); + assertv4(t, 8, 9, 6, 7); + VTRANSPOSE4(a0.v, a1.v, a2.v, a3.v); + printf("VTRANSPOSE4(0:3,4:7,8:11,12:15)=[%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", + a0.f[0], a0.f[1], a0.f[2], a0.f[3], a1.f[0], a1.f[1], a1.f[2], a1.f[3], + a2.f[0], a2.f[1], a2.f[2], a2.f[3], a3.f[0], a3.f[1], a3.f[2], a3.f[3]); + assertv4(a0, 0, 4, 8, 12); assertv4(a1, 1, 5, 9, 13); assertv4(a2, 2, 6, 10, 14); assertv4(a3, 3, 7, 11, 15); +} +#else +void validate_pffft_simd() {} // allow test_pffft.c to call this function even when simd is not available.. +#endif //!PFFFT_SIMD_DISABLE + +/* SSE and co like 16-bytes aligned pointers */ +#define MALLOC_V4SF_ALIGNMENT 64 // with a 64-byte alignment, we are even aligned on L2 cache lines... +void *pffft_aligned_malloc(size_t nb_bytes) { + void *p, *p0 = malloc(nb_bytes + MALLOC_V4SF_ALIGNMENT); + if (!p0) return (void *) 0; + // enforce alignment -- formula works even on weird platforms were sizeof(size_t) < sizeof(void*) + p = (void *) (((char*) p0 + MALLOC_V4SF_ALIGNMENT) - (((size_t) p0) & (MALLOC_V4SF_ALIGNMENT-1))); + *((void **) p - 1) = p0; + return p; +} + +void pffft_aligned_free(void *p) { + if (p) free(*((void **) p - 1)); +} + +int pffft_simd_size(void) { return SIMD_SZ; } + +/* + passf2 and passb2 has been merged here, fsign = -1 for passf2, +1 for passb2 +*/ +static NEVER_INLINE(void) passf2_ps(int ido, int l1, const v4sf *cc, v4sf *ch, const float *wa1, float fsign) { + int k, i; + int l1ido = l1*ido; + if (ido <= 2) { + for (k=0; k < l1ido; k += ido, ch += ido, cc+= 2*ido) { + ch[0] = VADD(cc[0], cc[ido+0]); + ch[l1ido] = VSUB(cc[0], cc[ido+0]); + ch[1] = VADD(cc[1], cc[ido+1]); + ch[l1ido + 1] = VSUB(cc[1], cc[ido+1]); + } + } else { + for (k=0; k < l1ido; k += ido, ch += ido, cc += 2*ido) { + for (i=0; i 2); + for (k=0; k< l1ido; k += ido, cc+= 3*ido, ch +=ido) { + for (i=0; i 2); + for (k = 0; k < l1; ++k, cc += 5*ido, ch += ido) { + for (i = 0; i < ido-1; i += 2) { + ti5 = VSUB(cc_ref(i , 2), cc_ref(i , 5)); + ti2 = VADD(cc_ref(i , 2), cc_ref(i , 5)); + ti4 = VSUB(cc_ref(i , 3), cc_ref(i , 4)); + ti3 = VADD(cc_ref(i , 3), cc_ref(i , 4)); + tr5 = VSUB(cc_ref(i-1, 2), cc_ref(i-1, 5)); + tr2 = VADD(cc_ref(i-1, 2), cc_ref(i-1, 5)); + tr4 = VSUB(cc_ref(i-1, 3), cc_ref(i-1, 4)); + tr3 = VADD(cc_ref(i-1, 3), cc_ref(i-1, 4)); + ch_ref(i-1, 1) = VADD(cc_ref(i-1, 1), VADD(tr2, tr3)); + ch_ref(i , 1) = VADD(cc_ref(i , 1), VADD(ti2, ti3)); + cr2 = VADD(cc_ref(i-1, 1), VADD(SVMUL(tr11, tr2),SVMUL(tr12, tr3))); + ci2 = VADD(cc_ref(i , 1), VADD(SVMUL(tr11, ti2),SVMUL(tr12, ti3))); + cr3 = VADD(cc_ref(i-1, 1), VADD(SVMUL(tr12, tr2),SVMUL(tr11, tr3))); + ci3 = VADD(cc_ref(i , 1), VADD(SVMUL(tr12, ti2),SVMUL(tr11, ti3))); + cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4)); + ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4)); + cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4)); + ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4)); + dr3 = VSUB(cr3, ci4); + dr4 = VADD(cr3, ci4); + di3 = VADD(ci3, cr4); + di4 = VSUB(ci3, cr4); + dr5 = VADD(cr2, ci5); + dr2 = VSUB(cr2, ci5); + di5 = VSUB(ci2, cr5); + di2 = VADD(ci2, cr5); + wr1=wa1[i]; wi1=fsign*wa1[i+1]; wr2=wa2[i]; wi2=fsign*wa2[i+1]; + wr3=wa3[i]; wi3=fsign*wa3[i+1]; wr4=wa4[i]; wi4=fsign*wa4[i+1]; + VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1)); + ch_ref(i - 1, 2) = dr2; + ch_ref(i, 2) = di2; + VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2)); + ch_ref(i - 1, 3) = dr3; + ch_ref(i, 3) = di3; + VCPLXMUL(dr4, di4, LD_PS1(wr3), LD_PS1(wi3)); + ch_ref(i - 1, 4) = dr4; + ch_ref(i, 4) = di4; + VCPLXMUL(dr5, di5, LD_PS1(wr4), LD_PS1(wi4)); + ch_ref(i - 1, 5) = dr5; + ch_ref(i, 5) = di5; + } + } +#undef ch_ref +#undef cc_ref +} + +static NEVER_INLINE(void) radf2_ps(int ido, int l1, const v4sf * RESTRICT cc, v4sf * RESTRICT ch, const float *wa1) { + static const float minus_one = -1.f; + int i, k, l1ido = l1*ido; + for (k=0; k < l1ido; k += ido) { + v4sf a = cc[k], b = cc[k + l1ido]; + ch[2*k] = VADD(a, b); + ch[2*(k+ido)-1] = VSUB(a, b); + } + if (ido < 2) return; + if (ido != 2) { + for (k=0; k < l1ido; k += ido) { + for (i=2; i 5) { + wa[i1-1] = wa[i-1]; + wa[i1] = wa[i]; + } + } + l1 = l2; + } +} /* cffti1 */ + + +v4sf *cfftf1_ps(int n, const v4sf *input_readonly, v4sf *work1, v4sf *work2, const float *wa, const int *ifac, int isign) { + v4sf *in = (v4sf*)input_readonly; + v4sf *out = (in == work2 ? work1 : work2); + int nf = ifac[1], k1; + int l1 = 1; + int iw = 0; + assert(in != out && work1 != work2); + for (k1=2; k1<=nf+1; k1++) { + int ip = ifac[k1]; + int l2 = ip*l1; + int ido = n / l2; + int idot = ido + ido; + switch (ip) { + case 5: { + int ix2 = iw + idot; + int ix3 = ix2 + idot; + int ix4 = ix3 + idot; + passf5_ps(idot, l1, in, out, &wa[iw], &wa[ix2], &wa[ix3], &wa[ix4], (float)isign); + } break; + case 4: { + int ix2 = iw + idot; + int ix3 = ix2 + idot; + passf4_ps(idot, l1, in, out, &wa[iw], &wa[ix2], &wa[ix3], (float)isign); + } break; + case 2: { + passf2_ps(idot, l1, in, out, &wa[iw], (float)isign); + } break; + case 3: { + int ix2 = iw + idot; + passf3_ps(idot, l1, in, out, &wa[iw], &wa[ix2], (float)isign); + } break; + default: + assert(0); + } + l1 = l2; + iw += (ip - 1)*idot; + if (out == work2) { + out = work1; in = work2; + } else { + out = work2; in = work1; + } + } + + return in; /* this is in fact the output .. */ +} + + +struct PFFFT_Setup { + int N; + int Ncvec; // nb of complex simd vectors (N/4 if PFFFT_COMPLEX, N/8 if PFFFT_REAL) + // hold the decomposition into small integers of N + int ifac[IFAC_MAX_SIZE]; // N , number of factors, factors (admitted values: 2, 3, 4 ou 5) + pffft_transform_t transform; + v4sf *data; // allocated room for twiddle coefs + float *e; // points into 'data' , N/4*3 elements + float *twiddle; // points into 'data', N/4 elements +}; + +PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform) { + PFFFT_Setup *s = NULL; + int k, m; + + // validate N for negative values or potential int overflow + if (N < 0) { + return 0; + } + if (N > (1<<26)) { + // higher values of N will make you enter in the integer overflow world... + assert(0); + return 0; + } + s = (PFFFT_Setup*)malloc(sizeof(PFFFT_Setup)); + /* unfortunately, the fft size must be a multiple of 16 for complex FFTs + and 32 for real FFTs -- a lot of stuff would need to be rewritten to + handle other cases (or maybe just switch to a scalar fft, I don't know..) */ + if (transform == PFFFT_REAL) { assert((N%(2*SIMD_SZ*SIMD_SZ))==0 && N>0); } + if (transform == PFFFT_COMPLEX) { assert((N%(SIMD_SZ*SIMD_SZ))==0 && N>0); } + //assert((N % 32) == 0); + s->N = N; + s->transform = transform; + /* nb of complex simd vectors */ + s->Ncvec = (transform == PFFFT_REAL ? N/2 : N)/SIMD_SZ; + s->data = (v4sf*)pffft_aligned_malloc(2*(size_t)s->Ncvec * sizeof(v4sf)); + s->e = (float*)s->data; + s->twiddle = (float*)(s->data + (2*s->Ncvec*(SIMD_SZ-1))/SIMD_SZ); + + for (k=0; k < s->Ncvec; ++k) { + int i = k/SIMD_SZ; + int j = k%SIMD_SZ; + for (m=0; m < SIMD_SZ-1; ++m) { + float A = -2*(float)M_PI*(m+1)*k / N; + s->e[(2*(i*3 + m) + 0) * SIMD_SZ + j] = cosf(A); + s->e[(2*(i*3 + m) + 1) * SIMD_SZ + j] = sinf(A); + } + } + + if (transform == PFFFT_REAL) { + rffti1_ps(N/SIMD_SZ, s->twiddle, s->ifac); + } else { + cffti1_ps(N/SIMD_SZ, s->twiddle, s->ifac); + } + + /* check that N is decomposable with allowed prime factors */ + for (k=0, m=1; k < s->ifac[1]; ++k) { m *= s->ifac[2+k]; } + if (m != N/SIMD_SZ) { + pffft_destroy_setup(s); s = 0; + } + + return s; +} + + +void pffft_destroy_setup(PFFFT_Setup *s) { + pffft_aligned_free(s->data); + free(s); +} + +#if !defined(PFFFT_SIMD_DISABLE) + +/* [0 0 1 2 3 4 5 6 7 8] -> [0 8 7 6 5 4 3 2 1] */ +static void reversed_copy(int N, const v4sf *in, int in_stride, v4sf *out) { + v4sf g0, g1; + int k; + INTERLEAVE2(in[0], in[1], g0, g1); in += in_stride; + + *--out = VSWAPHL(g0, g1); // [g0l, g0h], [g1l g1h] -> [g1l, g0h] + for (k=1; k < N; ++k) { + v4sf h0, h1; + INTERLEAVE2(in[0], in[1], h0, h1); in += in_stride; + *--out = VSWAPHL(g1, h0); + *--out = VSWAPHL(h0, h1); + g1 = h1; + } + *--out = VSWAPHL(g1, g0); +} + +static void unreversed_copy(int N, const v4sf *in, v4sf *out, int out_stride) { + v4sf g0, g1, h0, h1; + int k; + g0 = g1 = in[0]; ++in; + for (k=1; k < N; ++k) { + h0 = *in++; h1 = *in++; + g1 = VSWAPHL(g1, h0); + h0 = VSWAPHL(h0, h1); + UNINTERLEAVE2(h0, g1, out[0], out[1]); out += out_stride; + g1 = h1; + } + h0 = *in++; h1 = g0; + g1 = VSWAPHL(g1, h0); + h0 = VSWAPHL(h0, h1); + UNINTERLEAVE2(h0, g1, out[0], out[1]); +} + +void pffft_zreorder(const PFFFT_Setup *setup, const float *in, float *out, pffft_direction_t direction) { + int k, N = setup->N, Ncvec = setup->Ncvec; + const v4sf *vin = (const v4sf*)in; + v4sf *vout = (v4sf*)out; + assert(in != out); + if (setup->transform == PFFFT_REAL) { + int k, dk = N/32; + if (direction == PFFFT_FORWARD) { + for (k=0; k < dk; ++k) { + INTERLEAVE2(vin[k*8 + 0], vin[k*8 + 1], vout[2*(0*dk + k) + 0], vout[2*(0*dk + k) + 1]); + INTERLEAVE2(vin[k*8 + 4], vin[k*8 + 5], vout[2*(2*dk + k) + 0], vout[2*(2*dk + k) + 1]); + } + reversed_copy(dk, vin+2, 8, (v4sf*)(out + N/2)); + reversed_copy(dk, vin+6, 8, (v4sf*)(out + N)); + } else { + for (k=0; k < dk; ++k) { + UNINTERLEAVE2(vin[2*(0*dk + k) + 0], vin[2*(0*dk + k) + 1], vout[k*8 + 0], vout[k*8 + 1]); + UNINTERLEAVE2(vin[2*(2*dk + k) + 0], vin[2*(2*dk + k) + 1], vout[k*8 + 4], vout[k*8 + 5]); + } + unreversed_copy(dk, (v4sf*)(in + N/4), (v4sf*)(out + N - 6*SIMD_SZ), -8); + unreversed_copy(dk, (v4sf*)(in + 3*N/4), (v4sf*)(out + N - 2*SIMD_SZ), -8); + } + } else { + if (direction == PFFFT_FORWARD) { + for (k=0; k < Ncvec; ++k) { + int kk = (k/4) + (k%4)*(Ncvec/4); + INTERLEAVE2(vin[k*2], vin[k*2+1], vout[kk*2], vout[kk*2+1]); + } + } else { + for (k=0; k < Ncvec; ++k) { + int kk = (k/4) + (k%4)*(Ncvec/4); + UNINTERLEAVE2(vin[kk*2], vin[kk*2+1], vout[k*2], vout[k*2+1]); + } + } + } +} + +void pffft_cplx_finalize(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + assert(in != out); + for (k=0; k < dk; ++k) { + r0 = in[8*k+0]; i0 = in[8*k+1]; + r1 = in[8*k+2]; i1 = in[8*k+3]; + r2 = in[8*k+4]; i2 = in[8*k+5]; + r3 = in[8*k+6]; i3 = in[8*k+7]; + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + VCPLXMUL(r1,i1,e[k*6+0],e[k*6+1]); + VCPLXMUL(r2,i2,e[k*6+2],e[k*6+3]); + VCPLXMUL(r3,i3,e[k*6+4],e[k*6+5]); + + sr0 = VADD(r0,r2); dr0 = VSUB(r0, r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r1, r3); + si0 = VADD(i0,i2); di0 = VSUB(i0, i2); + si1 = VADD(i1,i3); di1 = VSUB(i1, i3); + + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 -1 0 0 -1 0 1] [r1] + [1 -1 1 -1 0 0 0 0] [r2] + [1 0 -1 0 0 1 0 -1] [r3] + [0 0 0 0 1 1 1 1] * [i0] + [0 1 0 -1 1 0 -1 0] [i1] + [0 0 0 0 1 -1 1 -1] [i2] + [0 -1 0 1 1 0 -1 0] [i3] + */ + + r0 = VADD(sr0, sr1); i0 = VADD(si0, si1); + r1 = VADD(dr0, di1); i1 = VSUB(di0, dr1); + r2 = VSUB(sr0, sr1); i2 = VSUB(si0, si1); + r3 = VSUB(dr0, di1); i3 = VADD(di0, dr1); + + *out++ = r0; *out++ = i0; *out++ = r1; *out++ = i1; + *out++ = r2; *out++ = i2; *out++ = r3; *out++ = i3; + } +} + +void pffft_cplx_preprocess(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + assert(in != out); + for (k=0; k < dk; ++k) { + r0 = in[8*k+0]; i0 = in[8*k+1]; + r1 = in[8*k+2]; i1 = in[8*k+3]; + r2 = in[8*k+4]; i2 = in[8*k+5]; + r3 = in[8*k+6]; i3 = in[8*k+7]; + + sr0 = VADD(r0,r2); dr0 = VSUB(r0, r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r1, r3); + si0 = VADD(i0,i2); di0 = VSUB(i0, i2); + si1 = VADD(i1,i3); di1 = VSUB(i1, i3); + + r0 = VADD(sr0, sr1); i0 = VADD(si0, si1); + r1 = VSUB(dr0, di1); i1 = VADD(di0, dr1); + r2 = VSUB(sr0, sr1); i2 = VSUB(si0, si1); + r3 = VADD(dr0, di1); i3 = VSUB(di0, dr1); + + VCPLXMULCONJ(r1,i1,e[k*6+0],e[k*6+1]); + VCPLXMULCONJ(r2,i2,e[k*6+2],e[k*6+3]); + VCPLXMULCONJ(r3,i3,e[k*6+4],e[k*6+5]); + + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + *out++ = r0; *out++ = i0; *out++ = r1; *out++ = i1; + *out++ = r2; *out++ = i2; *out++ = r3; *out++ = i3; + } +} + + +static ALWAYS_INLINE(void) pffft_real_finalize_4x4(const v4sf *in0, const v4sf *in1, const v4sf *in, + const v4sf *e, v4sf *out) { + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + r0 = *in0; i0 = *in1; + r1 = *in++; i1 = *in++; r2 = *in++; i2 = *in++; r3 = *in++; i3 = *in++; + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 -1 0 0 -1 0 1] [r1] + [1 0 -1 0 0 1 0 -1] [r2] + [1 -1 1 -1 0 0 0 0] [r3] + [0 0 0 0 1 1 1 1] * [i0] + [0 -1 0 1 -1 0 1 0] [i1] + [0 -1 0 1 1 0 -1 0] [i2] + [0 0 0 0 -1 1 -1 1] [i3] + */ + + //cerr << "matrix initial, before e , REAL:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n"; + //cerr << "matrix initial, before e, IMAG :\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n"; + + VCPLXMUL(r1,i1,e[0],e[1]); + VCPLXMUL(r2,i2,e[2],e[3]); + VCPLXMUL(r3,i3,e[4],e[5]); + + //cerr << "matrix initial, real part:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n"; + //cerr << "matrix initial, imag part:\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n"; + + sr0 = VADD(r0,r2); dr0 = VSUB(r0,r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r3,r1); + si0 = VADD(i0,i2); di0 = VSUB(i0,i2); + si1 = VADD(i1,i3); di1 = VSUB(i3,i1); + + r0 = VADD(sr0, sr1); + r3 = VSUB(sr0, sr1); + i0 = VADD(si0, si1); + i3 = VSUB(si1, si0); + r1 = VADD(dr0, di1); + r2 = VSUB(dr0, di1); + i1 = VSUB(dr1, di0); + i2 = VADD(dr1, di0); + + *out++ = r0; + *out++ = i0; + *out++ = r1; + *out++ = i1; + *out++ = r2; + *out++ = i2; + *out++ = r3; + *out++ = i3; + +} + +static NEVER_INLINE(void) pffft_real_finalize(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */ + + v4sf_union cr, ci, *uout = (v4sf_union*)out; + v4sf save = in[7], zero=VZERO(); + float xr0, xi0, xr1, xi1, xr2, xi2, xr3, xi3; + static const float s = (float)(M_SQRT1_2); + + cr.v = in[0]; ci.v = in[Ncvec*2-1]; + assert(in != out); + pffft_real_finalize_4x4(&zero, &zero, in+1, e, out); + + /* + [cr0 cr1 cr2 cr3 ci0 ci1 ci2 ci3] + + [Xr(1)] ] [1 1 1 1 0 0 0 0] + [Xr(N/4) ] [0 0 0 0 1 s 0 -s] + [Xr(N/2) ] [1 0 -1 0 0 0 0 0] + [Xr(3N/4)] [0 0 0 0 1 -s 0 s] + [Xi(1) ] [1 -1 1 -1 0 0 0 0] + [Xi(N/4) ] [0 0 0 0 0 -s -1 -s] + [Xi(N/2) ] [0 -1 0 1 0 0 0 0] + [Xi(3N/4)] [0 0 0 0 0 -s 1 -s] + */ + + xr0=(cr.f[0]+cr.f[2]) + (cr.f[1]+cr.f[3]); uout[0].f[0] = xr0; + xi0=(cr.f[0]+cr.f[2]) - (cr.f[1]+cr.f[3]); uout[1].f[0] = xi0; + xr2=(cr.f[0]-cr.f[2]); uout[4].f[0] = xr2; + xi2=(cr.f[3]-cr.f[1]); uout[5].f[0] = xi2; + xr1= ci.f[0] + s*(ci.f[1]-ci.f[3]); uout[2].f[0] = xr1; + xi1=-ci.f[2] - s*(ci.f[1]+ci.f[3]); uout[3].f[0] = xi1; + xr3= ci.f[0] - s*(ci.f[1]-ci.f[3]); uout[6].f[0] = xr3; + xi3= ci.f[2] - s*(ci.f[1]+ci.f[3]); uout[7].f[0] = xi3; + + for (k=1; k < dk; ++k) { + v4sf save_next = in[8*k+7]; + pffft_real_finalize_4x4(&save, &in[8*k+0], in + 8*k+1, + e + k*6, out + k*8); + save = save_next; + } + +} + +static ALWAYS_INLINE(void) pffft_real_preprocess_4x4(const v4sf *in, + const v4sf *e, v4sf *out, int first) { + v4sf r0=in[0], i0=in[1], r1=in[2], i1=in[3], r2=in[4], i2=in[5], r3=in[6], i3=in[7]; + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 0 -1 0 -1 -1 0] [r1] + [1 -1 -1 1 0 0 0 0] [r2] + [1 0 0 -1 0 1 1 0] [r3] + [0 0 0 0 1 -1 1 -1] * [i0] + [0 -1 1 0 1 0 0 1] [i1] + [0 0 0 0 1 1 -1 -1] [i2] + [0 1 -1 0 1 0 0 1] [i3] + */ + + v4sf sr0 = VADD(r0,r3), dr0 = VSUB(r0,r3); + v4sf sr1 = VADD(r1,r2), dr1 = VSUB(r1,r2); + v4sf si0 = VADD(i0,i3), di0 = VSUB(i0,i3); + v4sf si1 = VADD(i1,i2), di1 = VSUB(i1,i2); + + r0 = VADD(sr0, sr1); + r2 = VSUB(sr0, sr1); + r1 = VSUB(dr0, si1); + r3 = VADD(dr0, si1); + i0 = VSUB(di0, di1); + i2 = VADD(di0, di1); + i1 = VSUB(si0, dr1); + i3 = VADD(si0, dr1); + + VCPLXMULCONJ(r1,i1,e[0],e[1]); + VCPLXMULCONJ(r2,i2,e[2],e[3]); + VCPLXMULCONJ(r3,i3,e[4],e[5]); + + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + if (!first) { + *out++ = r0; + *out++ = i0; + } + *out++ = r1; + *out++ = i1; + *out++ = r2; + *out++ = i2; + *out++ = r3; + *out++ = i3; +} + +static NEVER_INLINE(void) pffft_real_preprocess(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */ + + v4sf_union Xr, Xi, *uout = (v4sf_union*)out; + float cr0, ci0, cr1, ci1, cr2, ci2, cr3, ci3; + static const float s = (float)M_SQRT2; + assert(in != out); + for (k=0; k < 4; ++k) { + Xr.f[k] = ((float*)in)[8*k]; + Xi.f[k] = ((float*)in)[8*k+4]; + } + + pffft_real_preprocess_4x4(in, e, out+1, 1); // will write only 6 values + + /* + [Xr0 Xr1 Xr2 Xr3 Xi0 Xi1 Xi2 Xi3] + + [cr0] [1 0 2 0 1 0 0 0] + [cr1] [1 0 0 0 -1 0 -2 0] + [cr2] [1 0 -2 0 1 0 0 0] + [cr3] [1 0 0 0 -1 0 2 0] + [ci0] [0 2 0 2 0 0 0 0] + [ci1] [0 s 0 -s 0 -s 0 -s] + [ci2] [0 0 0 0 0 -2 0 2] + [ci3] [0 -s 0 s 0 -s 0 -s] + */ + for (k=1; k < dk; ++k) { + pffft_real_preprocess_4x4(in+8*k, e + k*6, out-1+k*8, 0); + } + + cr0=(Xr.f[0]+Xi.f[0]) + 2*Xr.f[2]; uout[0].f[0] = cr0; + cr1=(Xr.f[0]-Xi.f[0]) - 2*Xi.f[2]; uout[0].f[1] = cr1; + cr2=(Xr.f[0]+Xi.f[0]) - 2*Xr.f[2]; uout[0].f[2] = cr2; + cr3=(Xr.f[0]-Xi.f[0]) + 2*Xi.f[2]; uout[0].f[3] = cr3; + ci0= 2*(Xr.f[1]+Xr.f[3]); uout[2*Ncvec-1].f[0] = ci0; + ci1= s*(Xr.f[1]-Xr.f[3]) - s*(Xi.f[1]+Xi.f[3]); uout[2*Ncvec-1].f[1] = ci1; + ci2= 2*(Xi.f[3]-Xi.f[1]); uout[2*Ncvec-1].f[2] = ci2; + ci3=-s*(Xr.f[1]-Xr.f[3]) - s*(Xi.f[1]+Xi.f[3]); uout[2*Ncvec-1].f[3] = ci3; +} + + +void pffft_transform_internal(const PFFFT_Setup *setup, const float *finput, float *foutput, v4sf *scratch, + pffft_direction_t direction, int ordered) { + int k, Ncvec = setup->Ncvec; + int nf_odd = (setup->ifac[1] & 1); + + // temporary buffer is allocated on the stack if the scratch pointer is NULL + int stack_allocate = (scratch == 0 ? Ncvec*2 : 1); + VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate); + + const v4sf *vinput = (const v4sf*)finput; + v4sf *voutput = (v4sf*)foutput; + v4sf *buff[2] = { voutput, scratch ? scratch : scratch_on_stack }; + int ib = (nf_odd ^ ordered ? 1 : 0); + + assert(VALIGNED(finput) && VALIGNED(foutput)); + + //assert(finput != foutput); + if (direction == PFFFT_FORWARD) { + ib = !ib; + if (setup->transform == PFFFT_REAL) { + ib = (rfftf1_ps(Ncvec*2, vinput, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + pffft_real_finalize(Ncvec, buff[ib], buff[!ib], (v4sf*)setup->e); + } else { + v4sf *tmp = buff[ib]; + for (k=0; k < Ncvec; ++k) { + UNINTERLEAVE2(vinput[k*2], vinput[k*2+1], tmp[k*2], tmp[k*2+1]); + } + ib = (cfftf1_ps(Ncvec, buff[ib], buff[!ib], buff[ib], + setup->twiddle, &setup->ifac[0], -1) == buff[0] ? 0 : 1); + pffft_cplx_finalize(Ncvec, buff[ib], buff[!ib], (v4sf*)setup->e); + } + if (ordered) { + pffft_zreorder(setup, (float*)buff[!ib], (float*)buff[ib], PFFFT_FORWARD); + } else ib = !ib; + } else { + if (vinput == buff[ib]) { + ib = !ib; // may happen when finput == foutput + } + if (ordered) { + pffft_zreorder(setup, (float*)vinput, (float*)buff[ib], PFFFT_BACKWARD); + vinput = buff[ib]; ib = !ib; + } + if (setup->transform == PFFFT_REAL) { + pffft_real_preprocess(Ncvec, vinput, buff[ib], (v4sf*)setup->e); + ib = (rfftb1_ps(Ncvec*2, buff[ib], buff[0], buff[1], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + pffft_cplx_preprocess(Ncvec, vinput, buff[ib], (v4sf*)setup->e); + ib = (cfftf1_ps(Ncvec, buff[ib], buff[0], buff[1], + setup->twiddle, &setup->ifac[0], +1) == buff[0] ? 0 : 1); + for (k=0; k < Ncvec; ++k) { + INTERLEAVE2(buff[ib][k*2], buff[ib][k*2+1], buff[ib][k*2], buff[ib][k*2+1]); + } + } + } + + if (buff[ib] != voutput) { + /* extra copy required -- this situation should only happen when finput == foutput */ + assert(finput==foutput); + for (k=0; k < Ncvec; ++k) { + v4sf a = buff[ib][2*k], b = buff[ib][2*k+1]; + voutput[2*k] = a; voutput[2*k+1] = b; + } + ib = !ib; + } + assert(buff[ib] == voutput); +} + +void pffft_zconvolve_accumulate(const PFFFT_Setup *s, const float *a, const float *b, float *ab, float scaling) { + int Ncvec = s->Ncvec; + const v4sf * RESTRICT va = (const v4sf*)a; + const v4sf * RESTRICT vb = (const v4sf*)b; + v4sf * RESTRICT vab = (v4sf*)ab; + +#ifdef __arm__ + __builtin_prefetch(va); + __builtin_prefetch(vb); + __builtin_prefetch(vab); + __builtin_prefetch(va+2); + __builtin_prefetch(vb+2); + __builtin_prefetch(vab+2); + __builtin_prefetch(va+4); + __builtin_prefetch(vb+4); + __builtin_prefetch(vab+4); + __builtin_prefetch(va+6); + __builtin_prefetch(vb+6); + __builtin_prefetch(vab+6); +# ifndef __clang__ +# define ZCONVOLVE_USING_INLINE_NEON_ASM +# endif +#endif + + float ar0, ai0, br0, bi0, abr0, abi0; +#ifndef ZCONVOLVE_USING_INLINE_ASM + v4sf vscal = LD_PS1(scaling); + int i; +#endif + + assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab)); + ar0 = ((v4sf_union*)va)[0].f[0]; + ai0 = ((v4sf_union*)va)[1].f[0]; + br0 = ((v4sf_union*)vb)[0].f[0]; + bi0 = ((v4sf_union*)vb)[1].f[0]; + abr0 = ((v4sf_union*)vab)[0].f[0]; + abi0 = ((v4sf_union*)vab)[1].f[0]; + +#ifdef ZCONVOLVE_USING_INLINE_ASM // inline asm version, unfortunately miscompiled by clang 3.2, at least on ubuntu.. so this will be restricted to gcc + const float *a_ = a, *b_ = b; float *ab_ = ab; + int N = Ncvec; + asm volatile("mov r8, %2 \n" + "vdup.f32 q15, %4 \n" + "1: \n" + "pld [%0,#64] \n" + "pld [%1,#64] \n" + "pld [%2,#64] \n" + "pld [%0,#96] \n" + "pld [%1,#96] \n" + "pld [%2,#96] \n" + "vld1.f32 {q0,q1}, [%0,:128]! \n" + "vld1.f32 {q4,q5}, [%1,:128]! \n" + "vld1.f32 {q2,q3}, [%0,:128]! \n" + "vld1.f32 {q6,q7}, [%1,:128]! \n" + "vld1.f32 {q8,q9}, [r8,:128]! \n" + + "vmul.f32 q10, q0, q4 \n" + "vmul.f32 q11, q0, q5 \n" + "vmul.f32 q12, q2, q6 \n" + "vmul.f32 q13, q2, q7 \n" + "vmls.f32 q10, q1, q5 \n" + "vmla.f32 q11, q1, q4 \n" + "vld1.f32 {q0,q1}, [r8,:128]! \n" + "vmls.f32 q12, q3, q7 \n" + "vmla.f32 q13, q3, q6 \n" + "vmla.f32 q8, q10, q15 \n" + "vmla.f32 q9, q11, q15 \n" + "vmla.f32 q0, q12, q15 \n" + "vmla.f32 q1, q13, q15 \n" + "vst1.f32 {q8,q9},[%2,:128]! \n" + "vst1.f32 {q0,q1},[%2,:128]! \n" + "subs %3, #2 \n" + "bne 1b \n" + : "+r"(a_), "+r"(b_), "+r"(ab_), "+r"(N) : "r"(scaling) : "r8", "q0","q1","q2","q3","q4","q5","q6","q7","q8","q9", "q10","q11","q12","q13","q15","memory"); +#else // default routine, works fine for non-arm cpus with current compilers + for (i=0; i < Ncvec; i += 2) { + v4sf ar, ai, br, bi; + ar = va[2*i+0]; ai = va[2*i+1]; + br = vb[2*i+0]; bi = vb[2*i+1]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+0] = VMADD(ar, vscal, vab[2*i+0]); + vab[2*i+1] = VMADD(ai, vscal, vab[2*i+1]); + ar = va[2*i+2]; ai = va[2*i+3]; + br = vb[2*i+2]; bi = vb[2*i+3]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+2] = VMADD(ar, vscal, vab[2*i+2]); + vab[2*i+3] = VMADD(ai, vscal, vab[2*i+3]); + } +#endif + if (s->transform == PFFFT_REAL) { + ((v4sf_union*)vab)[0].f[0] = abr0 + ar0*br0*scaling; + ((v4sf_union*)vab)[1].f[0] = abi0 + ai0*bi0*scaling; + } +} + +void pffft_zconvolve(const PFFFT_Setup *s, const float *a, const float *b, float *ab) { + int Ncvec = s->Ncvec; + const v4sf * RESTRICT va = (const v4sf*)a; + const v4sf * RESTRICT vb = (const v4sf*)b; + v4sf * RESTRICT vab = (v4sf*)ab; + + float ar0, ai0, br0, bi0; + int i; + + assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab)); + ar0 = ((v4sf_union*)va)[0].f[0]; + ai0 = ((v4sf_union*)va)[1].f[0]; + br0 = ((v4sf_union*)vb)[0].f[0]; + bi0 = ((v4sf_union*)vb)[1].f[0]; + + for (i=0; i < Ncvec; i += 2) { + v4sf ar, ai, br, bi; + ar = va[2*i+0]; ai = va[2*i+1]; + br = vb[2*i+0]; bi = vb[2*i+1]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+0] = ar; + vab[2*i+1] = ai; + ar = va[2*i+2]; ai = va[2*i+3]; + br = vb[2*i+2]; bi = vb[2*i+3]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+2] = ar; + vab[2*i+3] = ai; + } + if (s->transform == PFFFT_REAL) { + ((v4sf_union*)vab)[0].f[0] = ar0*br0; + ((v4sf_union*)vab)[1].f[0] = ai0*bi0; + } +} + +#else // defined(PFFFT_SIMD_DISABLE) + +// standard routine using scalar floats, without SIMD stuff. + +#define pffft_zreorder_nosimd pffft_zreorder +void pffft_zreorder_nosimd(const PFFFT_Setup *setup, const float *in, float *out, pffft_direction_t direction) { + int k, N = setup->N; + if (setup->transform == PFFFT_COMPLEX) { + for (k=0; k < 2*N; ++k) out[k] = in[k]; + return; + } + else if (direction == PFFFT_FORWARD) { + float x_N = in[N-1]; + for (k=N-1; k > 1; --k) out[k] = in[k-1]; + out[0] = in[0]; + out[1] = x_N; + } else { + float x_N = in[1]; + for (k=1; k < N-1; ++k) out[k] = in[k+1]; + out[0] = in[0]; + out[N-1] = x_N; + } +} + +#define pffft_transform_internal_nosimd pffft_transform_internal +void pffft_transform_internal_nosimd(const PFFFT_Setup *setup, const float *input, float *output, float *scratch, + pffft_direction_t direction, int ordered) { + int Ncvec = setup->Ncvec; + int nf_odd = (setup->ifac[1] & 1); + + // temporary buffer is allocated on the stack if the scratch pointer is NULL + int stack_allocate = (scratch == 0 ? Ncvec*2 : 1); + VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate); + float *buff[2]; + int ib; + if (scratch == 0) scratch = scratch_on_stack; + buff[0] = output; buff[1] = scratch; + + if (setup->transform == PFFFT_COMPLEX) ordered = 0; // it is always ordered. + ib = (nf_odd ^ ordered ? 1 : 0); + + if (direction == PFFFT_FORWARD) { + if (setup->transform == PFFFT_REAL) { + ib = (rfftf1_ps(Ncvec*2, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0], -1) == buff[0] ? 0 : 1); + } + if (ordered) { + pffft_zreorder(setup, buff[ib], buff[!ib], PFFFT_FORWARD); ib = !ib; + } + } else { + if (input == buff[ib]) { + ib = !ib; // may happen when finput == foutput + } + if (ordered) { + pffft_zreorder(setup, input, buff[!ib], PFFFT_BACKWARD); + input = buff[!ib]; + } + if (setup->transform == PFFFT_REAL) { + ib = (rfftb1_ps(Ncvec*2, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0], +1) == buff[0] ? 0 : 1); + } + } + if (buff[ib] != output) { + int k; + // extra copy required -- this situation should happens only when finput == foutput + assert(input==output); + for (k=0; k < Ncvec; ++k) { + float a = buff[ib][2*k], b = buff[ib][2*k+1]; + output[2*k] = a; output[2*k+1] = b; + } + ib = !ib; + } + assert(buff[ib] == output); +} + +#define pffft_zconvolve_accumulate_nosimd pffft_zconvolve_accumulate +void pffft_zconvolve_accumulate_nosimd(const PFFFT_Setup *s, const float *a, const float *b, + float *ab, float scaling) { + int i, Ncvec = s->Ncvec; + + if (s->transform == PFFFT_REAL) { + // take care of the fftpack ordering + ab[0] += a[0]*b[0]*scaling; + ab[2*Ncvec-1] += a[2*Ncvec-1]*b[2*Ncvec-1]*scaling; + ++ab; ++a; ++b; --Ncvec; + } + for (i=0; i < Ncvec; ++i) { + float ar, ai, br, bi; + ar = a[2*i+0]; ai = a[2*i+1]; + br = b[2*i+0]; bi = b[2*i+1]; + VCPLXMUL(ar, ai, br, bi); + ab[2*i+0] += ar*scaling; + ab[2*i+1] += ai*scaling; + } +} + +#endif // defined(PFFFT_SIMD_DISABLE) + +void pffft_transform(const PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction) { + pffft_transform_internal(setup, input, output, (v4sf*)work, direction, 0); +} + +void pffft_transform_ordered(const PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction) { + pffft_transform_internal(setup, input, output, (v4sf*)work, direction, 1); +} diff --git a/native/vendor/r8brain/fft/pffft.h b/native/vendor/r8brain/fft/pffft.h new file mode 100644 index 0000000..111ed02 --- /dev/null +++ b/native/vendor/r8brain/fft/pffft.h @@ -0,0 +1,186 @@ +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB, + authored by Dr Paul Swarztrauber of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +/* + PFFFT : a Pretty Fast FFT. + + This is basically an adaptation of the single precision fftpack + (v4) as found on netlib taking advantage of SIMD instruction found + on cpus such as intel x86 (SSE1), powerpc (Altivec), and arm (NEON). + + For architectures where no SIMD instruction is available, the code + falls back to a scalar version. + + Restrictions: + + - 1D transforms only, with 32-bit single precision. + + - supports only transforms for inputs of length N of the form + N=(2^a)*(3^b)*(5^c), a >= 5, b >=0, c >= 0 (32, 48, 64, 96, 128, + 144, 160, etc are all acceptable lengths). Performance is best for + 128<=N<=8192. + + - all (float*) pointers in the functions below are expected to + have an "simd-compatible" alignment, that is 16 bytes on x86 and + powerpc CPUs. + + You can allocate such buffers with the functions + pffft_aligned_malloc / pffft_aligned_free (or with stuff like + posix_memalign..) + +*/ + +#ifndef PFFFT_H +#define PFFFT_H + +#include // for size_t + +#ifdef __cplusplus +extern "C" { +#endif + + /** + Opaque struct holding internal stuff (precomputed twiddle factors) + this struct can be shared by many threads as it contains only + read-only data. + */ + typedef struct PFFFT_Setup PFFFT_Setup; + + /** Direction of the transform */ + typedef enum { PFFFT_FORWARD, PFFFT_BACKWARD } pffft_direction_t; + + /** Type of transform */ + typedef enum { PFFFT_REAL, PFFFT_COMPLEX } pffft_transform_t; + + /** + Prepare for performing transforms of size N -- the returned + PFFFT_Setup structure is read-only so it can safely be shared by + multiple concurrent threads. + + Will return NULL if N is not suitable (too large / no decomposable with simple integer + factors..) + */ + PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform); + void pffft_destroy_setup(PFFFT_Setup *); + /** + Perform a Fourier transform , The z-domain data is stored in the + most efficient order for transforming it back, or using it for + convolution. If you need to have its content sorted in the + "usual" way, that is as an array of interleaved complex numbers, + either use pffft_transform_ordered , or call pffft_zreorder after + the forward fft, and before the backward fft. + + Transforms are not scaled: PFFFT_BACKWARD(PFFFT_FORWARD(x)) = N*x. + Typically you will want to scale the backward transform by 1/N. + + The 'work' pointer should point to an area of N (2*N for complex + fft) floats, properly aligned. If 'work' is NULL, then stack will + be used instead (this is probably the best strategy for small + FFTs, say for N < 16384). + + input and output may alias. + */ + void pffft_transform(const PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction); + + /** + Similar to pffft_transform, but makes sure that the output is + ordered as expected (interleaved complex numbers). This is + similar to calling pffft_transform and then pffft_zreorder. + + input and output may alias. + */ + void pffft_transform_ordered(const PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction); + + /** + call pffft_zreorder(.., PFFFT_FORWARD) after pffft_transform(..., + PFFFT_FORWARD) if you want to have the frequency components in + the correct "canonical" order, as interleaved complex numbers. + + (for real transforms, both 0-frequency and half frequency + components, which are real, are assembled in the first entry as + F(0)+i*F(n/2+1). Note that the original fftpack did place + F(n/2+1) at the end of the arrays). + + input and output should not alias. + */ + void pffft_zreorder(const PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction); + + /** + Perform a multiplication of the frequency components of dft_a and + dft_b and accumulate them into dft_ab. The arrays should have + been obtained with pffft_transform(.., PFFFT_FORWARD) and should + *not* have been reordered with pffft_zreorder (otherwise just + perform the operation yourself as the dft coefs are stored as + interleaved complex numbers). + + the operation performed is: dft_ab += (dft_a * fdt_b)*scaling + + The dft_a, dft_b and dft_ab pointers may alias. + */ + void pffft_zconvolve_accumulate(const PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling); + + /** + * r8brain-free-src specific implementation. + */ + void pffft_zconvolve(const PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab); + + /** + the float buffers must have the correct alignment (16-byte boundary + on intel and powerpc). This function may be used to obtain such + correctly aligned buffers. + */ + void *pffft_aligned_malloc(size_t nb_bytes); + void pffft_aligned_free(void *); + + /** return 4 or 1 wether support SSE/Altivec instructions was enable when building pffft.c */ + int pffft_simd_size(void); + +#ifdef __cplusplus +} +#endif + +#endif // PFFFT_H diff --git a/native/vendor/r8brain/fft/pffft_double.c b/native/vendor/r8brain/fft/pffft_double.c new file mode 100644 index 0000000..da83bb9 --- /dev/null +++ b/native/vendor/r8brain/fft/pffft_double.c @@ -0,0 +1,209 @@ +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + Copyright (c) 2020 Hayati Ayguen ( h_ayguen@web.de ) + Copyright (c) 2020 Dario Mambro ( dario.mambro@gmail.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB + (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber + of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. + + + PFFFT : a Pretty Fast FFT. + + This file is largerly based on the original FFTPACK implementation, modified in + order to take advantage of SIMD instructions of modern CPUs. +*/ + +/* + NOTE: This file is adapted from Julien Pommier's original PFFFT, + which works on 32 bit floating point precision using SSE instructions, + to work with 64 bit floating point precision using AVX instructions. + Author: Dario Mambro @ https://github.com/unevens/pffft +*/ + +#include "pffft_double.h" + +/* detect compiler flavour */ +#if defined(_MSC_VER) +# define COMPILER_MSVC +#elif defined(__GNUC__) +# define COMPILER_GCC +#endif + +#ifdef COMPILER_MSVC +# ifndef _USE_MATH_DEFINES +# define _USE_MATH_DEFINES +# endif +# include +#elif defined(__MINGW32__) || defined(__MINGW64__) +# include +#else +# include +#endif + +#include +#include +#include +#include +#include + +#if defined(COMPILER_GCC) +# define ALWAYS_INLINE(return_type) inline return_type __attribute__ ((always_inline)) +# define NEVER_INLINE(return_type) return_type __attribute__ ((noinline)) +# define RESTRICT __restrict +# define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ varname__[size__]; +#elif defined(COMPILER_MSVC) +# define ALWAYS_INLINE(return_type) __forceinline return_type +# define NEVER_INLINE(return_type) __declspec(noinline) return_type +# define RESTRICT __restrict +# define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ *varname__ = (type__*)_alloca(size__ * sizeof(type__)) +#endif + + +#ifdef COMPILER_MSVC +#pragma warning( disable : 4244 4305 4204 4456 ) +#endif + +/* + vector support macros: the rest of the code is independant of + AVX -- adding support for other platforms with 4-element + vectors should be limited to these macros +*/ +#include "simd/pf_double.h" + +/* have code comparable with this definition */ +#define float double +#define SETUP_STRUCT PFFFTD_Setup +#define FUNC_NEW_SETUP pffftd_new_setup +#define FUNC_DESTROY pffftd_destroy_setup +#define FUNC_TRANSFORM_UNORDRD pffftd_transform +#define FUNC_TRANSFORM_ORDERED pffftd_transform_ordered +#define FUNC_ZREORDER pffftd_zreorder +#define FUNC_ZCONVOLVE_ACCUMULATE pffftd_zconvolve_accumulate +#define FUNC_ZCONVOLVE pffftd_zconvolve +#define FUNC_ZCONVERT_ZP pffftd_zconvert_zp +#define FUNC_ZCONVOLVE_ZP pffftd_zconvolve_zp +#define FUNC_ZCONVOLVE_SCALE pffftd_zconvolve_scale + +#define FUNC_ALIGNED_MALLOC pffftd_aligned_malloc +#define FUNC_ALIGNED_FREE pffftd_aligned_free +#define FUNC_SIMD_SIZE pffftd_simd_size +#define FUNC_MIN_FFT_SIZE pffftd_min_fft_size +#define FUNC_IS_VALID_SIZE pffftd_is_valid_size +#define FUNC_NEAREST_SIZE pffftd_nearest_transform_size +#define FUNC_SIMD_ARCH pffftd_simd_arch +#define FUNC_VALIDATE_SIMD_A validate_pffftd_simd +#define FUNC_VALIDATE_SIMD_EX validate_pffftd_simd_ex + +#define FUNC_CPLX_FINALIZE pffftd_cplx_finalize +#define FUNC_CPLX_PREPROCESS pffftd_cplx_preprocess +#define FUNC_REAL_PREPROCESS_4X4 pffftd_real_preprocess_4x4 +#define FUNC_REAL_PREPROCESS pffftd_real_preprocess +#define FUNC_REAL_FINALIZE_4X4 pffftd_real_finalize_4x4 +#define FUNC_REAL_FINALIZE pffftd_real_finalize +#define FUNC_TRANSFORM_INTERNAL pffftd_transform_internal + +#define FUNC_COS cos +#define FUNC_SIN sin + + +#include "pffft_priv_impl.h" + + + +/* deprecated synonym for pffftd_zconvolve_scale() -- kept for ABI compatibility */ +void pffftd_zconvolve_no_accu(const PFFFTD_Setup *setup, const double *dft_a, const double *dft_b, double *dft_ab, double scaling) { + pffftd_zconvolve_scale(setup, dft_a, dft_b, dft_ab, scaling); +} + +/* moved from "pffft_common.c" */ + +/* SSE and co like 16-bytes aligned pointers + * with a 64-byte alignment, we are even aligned on L2 cache lines... */ +#define MALLOC_V4SF_ALIGNMENT 64 +#define EXTRA_BYTES_FOR_ALIGNMENT_AND_A_POINTER (MALLOC_V4SF_ALIGNMENT - 1 + sizeof(void*)) + +static void * Valigned_malloc(size_t nb_bytes) { + void *p, *p0 = malloc(nb_bytes + EXTRA_BYTES_FOR_ALIGNMENT_AND_A_POINTER); + if (!p0) return (void *) 0; + p = (void *) (((uintptr_t) p0 + EXTRA_BYTES_FOR_ALIGNMENT_AND_A_POINTER) & (~((uintptr_t) (MALLOC_V4SF_ALIGNMENT-1)))); + *((void **) p - 1) = p0; + return p; +} + +static void Valigned_free(void *p) { + if (p) free(*((void **) p - 1)); +} + + +static int next_power_of_two(int N) { + /* https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 */ + /* compute the next highest power of 2 of 32-bit v */ + unsigned v = N; + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; +} + +static int is_power_of_two(int N) { + /* https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 */ + int f = N && !(N & (N - 1)); + return f; +} + + + +//PFFFT_EXPORT void *pffft_aligned_malloc(size_t nb_bytes) { return Valigned_malloc(nb_bytes); } +//PFFFT_EXPORT void pffft_aligned_free(void *p) { Valigned_free(p); } +//PFFFT_EXPORT int pffft_next_power_of_two(int N) { return next_power_of_two(N); } +//PFFFT_EXPORT int pffft_is_power_of_two(int N) { return is_power_of_two(N); } + +PFFFT_EXPORT void *pffftd_aligned_malloc(size_t nb_bytes) { return Valigned_malloc(nb_bytes); } +PFFFT_EXPORT void pffftd_aligned_free(void *p) { Valigned_free(p); } +PFFFT_EXPORT int pffftd_next_power_of_two(int N) { return next_power_of_two(N); } +PFFFT_EXPORT int pffftd_is_power_of_two(int N) { return is_power_of_two(N); } diff --git a/native/vendor/r8brain/fft/pffft_double.h b/native/vendor/r8brain/fft/pffft_double.h new file mode 100644 index 0000000..c8b2720 --- /dev/null +++ b/native/vendor/r8brain/fft/pffft_double.h @@ -0,0 +1,324 @@ +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB, + authored by Dr Paul Swarztrauber of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ +/* + NOTE: This file is adapted from Julien Pommier's original PFFFT, + which works on 32 bit floating point precision using SSE instructions, + to work with 64 bit floating point precision using AVX instructions. + Author: Dario Mambro @ https://github.com/unevens/pffft +*/ +/* + PFFFT : a Pretty Fast FFT. + + This is basically an adaptation of the single precision fftpack + (v4) as found on netlib taking advantage of SIMD instruction found + on cpus such as intel x86 (SSE1), powerpc (Altivec), and arm (NEON). + + For architectures where no SIMD instruction is available, the code + falls back to a scalar version. + + Restrictions: + + - 1D transforms only, with 64-bit double precision. + + - supports only transforms for inputs of length N of the form + N=(2^a)*(3^b)*(5^c), a >= 5, b >=0, c >= 0 (32, 48, 64, 96, 128, + 144, 160, etc are all acceptable lengths). Performance is best for + 128<=N<=8192. + + - all (double*) pointers in the functions below are expected to + have an "simd-compatible" alignment, that is 32 bytes on x86 and + powerpc CPUs. + + You can allocate such buffers with the functions + pffft_aligned_malloc / pffft_aligned_free (or with stuff like + posix_memalign..) + +*/ + +#ifndef PFFFT_DOUBLE_H +#define PFFFT_DOUBLE_H + +#include /* for size_t */ + +#define PFFFT_EXPORT + +#ifndef PFFFT_EXPORT + #ifdef PFFFT_STATIC_DEFINE + #define PFFFT_EXPORT + #elif defined(_WIN32) || defined(__CYGWIN__) + #ifdef PFFFT_EXPORTS + #define PFFFT_EXPORT __declspec(dllexport) + #else + #define PFFFT_EXPORT __declspec(dllimport) + #endif + #elif defined(PFFFT_EXPORTS) && (defined(__GNUC__) || defined(__clang__)) + #define PFFFT_EXPORT __attribute__((visibility("default"))) + #else + #define PFFFT_EXPORT + #endif +#endif + +#ifndef PFFFT_DEPRECATED + #if defined(__GNUC__) || defined(__clang__) + #define PFFFT_DEPRECATED __attribute__((deprecated)) + #elif defined(_MSC_VER) + #define PFFFT_DEPRECATED __declspec(deprecated) + #else + #define PFFFT_DEPRECATED + #endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + + /* opaque struct holding internal stuff (precomputed twiddle factors) + this struct can be shared by many threads as it contains only + read-only data. + */ + typedef struct PFFFTD_Setup PFFFTD_Setup; + +#ifndef PFFFT_COMMON_ENUMS +#define PFFFT_COMMON_ENUMS + + /* direction of the transform */ + typedef enum { PFFFT_FORWARD, PFFFT_BACKWARD } pffft_direction_t; + + /* type of transform */ + typedef enum { PFFFT_REAL, PFFFT_COMPLEX } pffft_transform_t; + +#endif + + /* + prepare for performing transforms of size N -- the returned + PFFFTD_Setup structure is read-only so it can safely be shared by + multiple concurrent threads. + */ + PFFFT_EXPORT PFFFTD_Setup *pffftd_new_setup(int N, pffft_transform_t transform); + PFFFT_EXPORT void pffftd_destroy_setup(PFFFTD_Setup *); + /* + Perform a Fourier transform , The z-domain data is stored in the + most efficient order for transforming it back, or using it for + convolution. If you need to have its content sorted in the + "usual" way, that is as an array of interleaved complex numbers, + either use pffft_transform_ordered , or call pffft_zreorder after + the forward fft, and before the backward fft. + + Transforms are not scaled: PFFFT_BACKWARD(PFFFT_FORWARD(x)) = N*x. + Typically you will want to scale the backward transform by 1/N. + + The 'work' pointer should point to an area of N (2*N for complex + fft) doubles, properly aligned. If 'work' is NULL, then stack will + be used instead (this is probably the best strategy for small + FFTs, say for N < 16384). Threads usually have a small stack, that + there's no sufficient amount of memory, usually leading to a crash! + Use the heap with pffft_aligned_malloc() in this case. + + input and output may alias. + */ + PFFFT_EXPORT void pffftd_transform(const PFFFTD_Setup *setup, const double *input, double *output, double *work, pffft_direction_t direction); + + /* + Similar to pffft_transform, but makes sure that the output is + ordered as expected (interleaved complex numbers). This is + similar to calling pffft_transform and then pffft_zreorder. + + input and output may alias. + */ + PFFFT_EXPORT void pffftd_transform_ordered(const PFFFTD_Setup *setup, const double *input, double *output, double *work, pffft_direction_t direction); + + /* + call pffft_zreorder(.., PFFFT_FORWARD) after pffft_transform(..., + PFFFT_FORWARD) if you want to have the frequency components in + the correct "canonical" order, as interleaved complex numbers. + + (for real transforms, both 0-frequency and half frequency + components, which are real, are assembled in the first entry as + F(0)+i*F(n/2+1). Note that the original fftpack did place + F(n/2+1) at the end of the arrays). + + input and output should not alias. + */ + PFFFT_EXPORT void pffftd_zreorder(const PFFFTD_Setup *setup, const double *input, double *output, pffft_direction_t direction); + + /* + Perform a multiplication of the frequency components of dft_a and + dft_b and accumulate them into dft_ab. The arrays should have + been obtained with pffft_transform(.., PFFFT_FORWARD) and should + *not* have been reordered with pffft_zreorder (otherwise just + perform the operation yourself as the dft coefs are stored as + interleaved complex numbers). + + the operation performed is: dft_ab += (dft_a * fdt_b)*scaling + + The dft_a, dft_b and dft_ab pointers may alias. + */ + PFFFT_EXPORT void pffftd_zconvolve_accumulate(const PFFFTD_Setup *setup, const double *dft_a, const double *dft_b, double *dft_ab, double scaling); + + /* + Perform a multiplication of the frequency components of dft_a and + dft_b and put result in dft_ab. The arrays should have + been obtained with pffft_transform(.., PFFFT_FORWARD) and should + *not* have been reordered with pffft_zreorder (otherwise just + perform the operation yourself as the dft coefs are stored as + interleaved complex numbers). + + the operation performed is: dft_ab = (dft_a * fdt_b)*scaling + + The dft_a, dft_b and dft_ab pointers may alias. + */ + PFFFT_EXPORT void pffftd_zconvolve_scale(const PFFFTD_Setup *setup, const double *dft_a, const double *dft_b, double *dft_ab, double scaling); + + /* + Perform a multiplication of the frequency components of dft_a and + dft_b and put result in dft_ab, without accumulation and without + scaling. The arrays should have been obtained with + pffft_transform(.., PFFFT_FORWARD) and should *not* have been + reordered with pffft_zreorder (otherwise just perform the operation + yourself as the dft coefs are stored as interleaved complex numbers). + + the operation performed is: dft_ab = dft_a * dft_b + + The dft_a, dft_b and dft_ab pointers may alias. + */ + PFFFT_EXPORT void pffftd_zconvolve(const PFFFTD_Setup *setup, const double *dft_a, const double *dft_b, double *dft_ab); + + /* + Convert an unordered PFFFT_REAL forward-transform result (as + produced by pffftd_transform(.., PFFFT_FORWARD)) into "zero-phase" + form: imaginary vector lanes are zeroed, real lanes are multiplied + by 'scaling' and normalized by 1/N. Useful for linear-phase FIR + filters whose impulse response is centered at t = 0 with the + left-hand taps wrapped around to the end of the transform block: + their spectrum has no imaginary components. 'in' may alias 'out'. + + With scaling == 1 the pipeline pffftd_transform(x, FORWARD) -> + pffftd_zconvolve_zp() -> pffftd_transform(.., BACKWARD) yields + the circular convolution of x with the filter at unit gain. + 'scaling' specifies an additional gain on top of that. + + Layout caveat: the unordered REAL layout packs the two real-valued + boundary coefficients F(0) and F(N/2) into the first lanes of the + first two vectors, where F(N/2) occupies an otherwise-imaginary + lane. pffftd_zconvert_zp() keeps that lane (scaled), so the output + is NOT a plain spectrum: multiply it only with pffftd_zconvolve_zp(), + never with pffftd_zconvolve_scale() or pffftd_zconvolve(), which + would cross-multiply the DC and Nyquist lanes. + + Returns 0 on success, nonzero if setup was not created for + PFFFT_REAL. + */ + PFFFT_EXPORT int pffftd_zconvert_zp(const PFFFTD_Setup *setup, const double *in, double *out, double scaling); + + /* + Frequency-domain multiply of an unordered PFFFT_REAL forward + spectrum dft_x with a zero-phase filter spectrum dft_hzp produced + by pffftd_zconvert_zp(): dft_ab = dft_x * dft_hzp. No accumulation. + With a filter converted using scaling == 1, the pipeline + pffftd_transform(x, FORWARD) -> zconvolve_zp() -> + pffftd_transform(.., BACKWARD) yields the circular convolution at + unit gain. All pointers may alias. + + Returns 0 on success, nonzero if setup was not created for + PFFFT_REAL. + */ + PFFFT_EXPORT int pffftd_zconvolve_zp(const PFFFTD_Setup *setup, const double *dft_x, const double *dft_hzp, double *dft_ab); + + /* + deprecated synonym for pffftd_zconvolve_scale() + */ + PFFFT_EXPORT void PFFFT_DEPRECATED pffftd_zconvolve_no_accu(const PFFFTD_Setup *setup, const double *dft_a, const double *dft_b, double *dft_ab, double scaling); + + /* return 4 or 1 wether support AVX instructions was enabled when building pffft-double.c */ + PFFFT_EXPORT int pffftd_simd_size(); + + /* return string identifier of used architecture (AVX/..) */ + PFFFT_EXPORT const char * pffftd_simd_arch(); + + /* simple helper to get minimum possible fft size */ + PFFFT_EXPORT int pffftd_min_fft_size(pffft_transform_t transform); + + /* simple helper to determine size N is valid + - factorizable to pffft_min_fft_size() with factors 2, 3, 5 + */ + PFFFT_EXPORT int pffftd_is_valid_size(int N, pffft_transform_t cplx); + + /* determine nearest valid transform size (by brute-force testing) + - factorizable to pffft_min_fft_size() with factors 2, 3, 5. + higher: bool-flag to find nearest higher value; else lower. + */ + PFFFT_EXPORT int pffftd_nearest_transform_size(int N, pffft_transform_t cplx, int higher); + + + /* following functions are identical to the pffft_ functions - both declared */ + + /* simple helper to determine next power of 2 + - without inexact/rounding floating point operations + */ + PFFFT_EXPORT int pffftd_next_power_of_two(int N); + PFFFT_EXPORT int pffft_next_power_of_two(int N); + + /* simple helper to determine if power of 2 - returns bool */ + PFFFT_EXPORT int pffftd_is_power_of_two(int N); + PFFFT_EXPORT int pffft_is_power_of_two(int N); + + /* + the double buffers must have the correct alignment (32-byte boundary + on intel and powerpc). This function may be used to obtain such + correctly aligned buffers. + */ + PFFFT_EXPORT void *pffftd_aligned_malloc(size_t nb_bytes); + PFFFT_EXPORT void *pffft_aligned_malloc(size_t nb_bytes); + PFFFT_EXPORT void pffftd_aligned_free(void *); + PFFFT_EXPORT void pffft_aligned_free(void *); + +#ifdef __cplusplus +} +#endif + +#endif /* PFFFT_DOUBLE_H */ + diff --git a/native/vendor/r8brain/fft/pffft_priv_impl.h b/native/vendor/r8brain/fft/pffft_priv_impl.h new file mode 100644 index 0000000..48b873c --- /dev/null +++ b/native/vendor/r8brain/fft/pffft_priv_impl.h @@ -0,0 +1,2413 @@ +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + Copyright (c) 2020 Hayati Ayguen ( h_ayguen@web.de ) + Copyright (c) 2020 Dario Mambro ( dario.mambro@gmail.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB + (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber + of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. + + + PFFFT : a Pretty Fast FFT. + + This file is largerly based on the original FFTPACK implementation, modified in + order to take advantage of SIMD instructions of modern CPUs. +*/ + +/* this file requires architecture specific preprocessor definitions + * it's only for library internal use + */ + + +/* define own constants required to turn off g++ extensions .. */ +#ifndef M_PI + #define M_PI 3.14159265358979323846 /* pi */ +#endif + +#ifndef M_SQRT2 + #define M_SQRT2 1.41421356237309504880 /* sqrt(2) */ +#endif + +#ifndef M_SQRT1_2 + #define M_SQRT1_2 0.707106781186547524400844362104849039 /* 1/sqrt(2) */ +#endif + + +int FUNC_SIMD_SIZE(void) { return SIMD_SZ; } + +int FUNC_MIN_FFT_SIZE(pffft_transform_t transform) { + /* unfortunately, the fft size must be a multiple of 16 for complex FFTs + and 32 for real FFTs -- a lot of stuff would need to be rewritten to + handle other cases (or maybe just switch to a scalar fft, I don't know..) */ + int simdSz = FUNC_SIMD_SIZE(); + if (transform == PFFFT_REAL) + return ( 2 * simdSz * simdSz ); + else if (transform == PFFFT_COMPLEX) + return ( simdSz * simdSz ); + else + return 1; +} + +int FUNC_IS_VALID_SIZE(int N, pffft_transform_t cplx) { + const int N_min = FUNC_MIN_FFT_SIZE(cplx); + int R = N; + while (R >= 5*N_min && (R % 5) == 0) R /= 5; + while (R >= 3*N_min && (R % 3) == 0) R /= 3; + while (R >= 2*N_min && (R % 2) == 0) R /= 2; + return (R == N_min) ? 1 : 0; +} + +int FUNC_NEAREST_SIZE(int N, pffft_transform_t cplx, int higher) { + int d; + const int N_min = FUNC_MIN_FFT_SIZE(cplx); + if (N < N_min) + N = N_min; + d = (higher) ? N_min : -N_min; + if (d > 0) + N = N_min * ((N+N_min-1) / N_min); /* round up */ + else + N = N_min * (N / N_min); /* round down */ + + for (; ; N += d) + if (FUNC_IS_VALID_SIZE(N, cplx)) + return N; +} + +const char * FUNC_SIMD_ARCH(void) { return VARCH; } + + +/* + passf2 and passb2 has been merged here, fsign = -1 for passf2, +1 for passb2 +*/ +static NEVER_INLINE(void) passf2_ps(int ido, int l1, const v4sf *cc, v4sf *ch, const float *wa1, float fsign) { + int k, i; + int l1ido = l1*ido; + if (ido <= 2) { + for (k=0; k < l1ido; k += ido, ch += ido, cc+= 2*ido) { + ch[0] = VADD(cc[0], cc[ido+0]); + ch[l1ido] = VSUB(cc[0], cc[ido+0]); + ch[1] = VADD(cc[1], cc[ido+1]); + ch[l1ido + 1] = VSUB(cc[1], cc[ido+1]); + } + } else { + for (k=0; k < l1ido; k += ido, ch += ido, cc += 2*ido) { + for (i=0; i 2); + for (k=0; k< l1ido; k += ido, cc+= 3*ido, ch +=ido) { + for (i=0; i 2); + for (k = 0; k < l1; ++k, cc += 5*ido, ch += ido) { + for (i = 0; i < ido-1; i += 2) { + ti5 = VSUB(cc_ref(i , 2), cc_ref(i , 5)); + ti2 = VADD(cc_ref(i , 2), cc_ref(i , 5)); + ti4 = VSUB(cc_ref(i , 3), cc_ref(i , 4)); + ti3 = VADD(cc_ref(i , 3), cc_ref(i , 4)); + tr5 = VSUB(cc_ref(i-1, 2), cc_ref(i-1, 5)); + tr2 = VADD(cc_ref(i-1, 2), cc_ref(i-1, 5)); + tr4 = VSUB(cc_ref(i-1, 3), cc_ref(i-1, 4)); + tr3 = VADD(cc_ref(i-1, 3), cc_ref(i-1, 4)); + ch_ref(i-1, 1) = VADD(cc_ref(i-1, 1), VADD(tr2, tr3)); + ch_ref(i , 1) = VADD(cc_ref(i , 1), VADD(ti2, ti3)); + cr2 = VADD(cc_ref(i-1, 1), VADD(SVMUL(tr11, tr2),SVMUL(tr12, tr3))); + ci2 = VADD(cc_ref(i , 1), VADD(SVMUL(tr11, ti2),SVMUL(tr12, ti3))); + cr3 = VADD(cc_ref(i-1, 1), VADD(SVMUL(tr12, tr2),SVMUL(tr11, tr3))); + ci3 = VADD(cc_ref(i , 1), VADD(SVMUL(tr12, ti2),SVMUL(tr11, ti3))); + cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4)); + ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4)); + cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4)); + ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4)); + dr3 = VSUB(cr3, ci4); + dr4 = VADD(cr3, ci4); + di3 = VADD(ci3, cr4); + di4 = VSUB(ci3, cr4); + dr5 = VADD(cr2, ci5); + dr2 = VSUB(cr2, ci5); + di5 = VSUB(ci2, cr5); + di2 = VADD(ci2, cr5); + wr1=wa1[i]; wi1=fsign*wa1[i+1]; wr2=wa2[i]; wi2=fsign*wa2[i+1]; + wr3=wa3[i]; wi3=fsign*wa3[i+1]; wr4=wa4[i]; wi4=fsign*wa4[i+1]; + VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1)); + ch_ref(i - 1, 2) = dr2; + ch_ref(i, 2) = di2; + VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2)); + ch_ref(i - 1, 3) = dr3; + ch_ref(i, 3) = di3; + VCPLXMUL(dr4, di4, LD_PS1(wr3), LD_PS1(wi3)); + ch_ref(i - 1, 4) = dr4; + ch_ref(i, 4) = di4; + VCPLXMUL(dr5, di5, LD_PS1(wr4), LD_PS1(wi4)); + ch_ref(i - 1, 5) = dr5; + ch_ref(i, 5) = di5; + } + } +#undef ch_ref +#undef cc_ref +} + +static NEVER_INLINE(void) radf2_ps(int ido, int l1, const v4sf * RESTRICT cc, v4sf * RESTRICT ch, const float *wa1) { + static const float minus_one = -1.f; + int i, k, l1ido = l1*ido; + for (k=0; k < l1ido; k += ido) { + v4sf a = cc[k], b = cc[k + l1ido]; + ch[2*k] = VADD(a, b); + ch[2*(k+ido)-1] = VSUB(a, b); + } + if (ido < 2) return; + if (ido != 2) { + for (k=0; k < l1ido; k += ido) { + for (i=2; i 5) { + wa[i1-1] = wa[i-1]; + wa[i1] = wa[i]; + } + } + l1 = l2; + } +} /* cffti1 */ + + +static v4sf *cfftf1_ps(int n, const v4sf *input_readonly, v4sf *work1, v4sf *work2, const float *wa, const int *ifac, int isign) { + v4sf *in = (v4sf*)input_readonly; + v4sf *out = (in == work2 ? work1 : work2); + int nf = ifac[1], k1; + int l1 = 1; + int iw = 0; + assert(in != out && work1 != work2); + for (k1=2; k1<=nf+1; k1++) { + int ip = ifac[k1]; + int l2 = ip*l1; + int ido = n / l2; + int idot = ido + ido; + switch (ip) { + case 5: { + int ix2 = iw + idot; + int ix3 = ix2 + idot; + int ix4 = ix3 + idot; + passf5_ps(idot, l1, in, out, &wa[iw], &wa[ix2], &wa[ix3], &wa[ix4], isign); + } break; + case 4: { + int ix2 = iw + idot; + int ix3 = ix2 + idot; + passf4_ps(idot, l1, in, out, &wa[iw], &wa[ix2], &wa[ix3], isign); + } break; + case 2: { + passf2_ps(idot, l1, in, out, &wa[iw], isign); + } break; + case 3: { + int ix2 = iw + idot; + passf3_ps(idot, l1, in, out, &wa[iw], &wa[ix2], isign); + } break; + default: + assert(0); + } + l1 = l2; + iw += (ip - 1)*idot; + if (out == work2) { + out = work1; in = work2; + } else { + out = work2; in = work1; + } + } + + return in; /* this is in fact the output .. */ +} + + +struct SETUP_STRUCT { + int N; + int Ncvec; /* nb of complex simd vectors (N/4 if PFFFT_COMPLEX, N/8 if PFFFT_REAL) */ + /* hold the decomposition into small integers of N */ + int ifac[IFAC_MAX_SIZE]; /* N, number of factors, factors (admitted values: 2, 3, 4 or 5) */ + pffft_transform_t transform; + v4sf *data; /* allocated room for twiddle coefs */ + float *e; /* points into 'data', N/4*3 elements */ + float *twiddle; /* points into 'data', N/4 elements */ +}; + +SETUP_STRUCT *FUNC_NEW_SETUP(int N, pffft_transform_t transform) { + SETUP_STRUCT *s = 0; + int k, m; + /* validate N for negative values or potential int overflow */ + if (N < 0) { + return s; + } + if (N > (1<<26)) { + /* higher values of N will make you enter in the integer overflow world... */ + assert(0); + return s; + } + /* unfortunately, the fft size must be a multiple of 16 for complex FFTs + and 32 for real FFTs -- a lot of stuff would need to be rewritten to + handle other cases (or maybe just switch to a scalar fft, I don't know..) */ + if (transform == PFFFT_REAL) { if ((N%(2*SIMD_SZ*SIMD_SZ)) || N<=0) return s; } + if (transform == PFFFT_COMPLEX) { if ((N%( SIMD_SZ*SIMD_SZ)) || N<=0) return s; } + s = (SETUP_STRUCT*)malloc(sizeof(SETUP_STRUCT)); + /* assert((N % 32) == 0); */ + s->N = N; + s->transform = transform; + /* nb of complex simd vectors */ + s->Ncvec = (transform == PFFFT_REAL ? N/2 : N)/SIMD_SZ; + s->data = (v4sf*)FUNC_ALIGNED_MALLOC(2*s->Ncvec * sizeof(v4sf)); + s->e = (float*)s->data; + s->twiddle = (float*)(s->data + (2*s->Ncvec*(SIMD_SZ-1))/SIMD_SZ); + + for (k=0; k < s->Ncvec; ++k) { + int i = k/SIMD_SZ; + int j = k%SIMD_SZ; + for (m=0; m < SIMD_SZ-1; ++m) { + float A = -2*(float)M_PI*(m+1)*k / N; + s->e[(2*(i*3 + m) + 0) * SIMD_SZ + j] = FUNC_COS(A); + s->e[(2*(i*3 + m) + 1) * SIMD_SZ + j] = FUNC_SIN(A); + } + } + + if (transform == PFFFT_REAL) { + rffti1_ps(N/SIMD_SZ, s->twiddle, s->ifac); + } else { + cffti1_ps(N/SIMD_SZ, s->twiddle, s->ifac); + } + + /* check that N is decomposable with allowed prime factors */ + for (k=0, m=1; k < s->ifac[1]; ++k) { m *= s->ifac[2+k]; } + if (m != N/SIMD_SZ) { + FUNC_DESTROY(s); s = 0; + } + + return s; +} + + +void FUNC_DESTROY(SETUP_STRUCT *s) { + if (!s) + return; + FUNC_ALIGNED_FREE(s->data); + free(s); +} + +#if ( SIMD_SZ == 4 ) /* !defined(PFFFT_SIMD_DISABLE) */ + +/* [0 0 1 2 3 4 5 6 7 8] -> [0 8 7 6 5 4 3 2 1] */ +static void reversed_copy(int N, const v4sf *in, int in_stride, v4sf *out) { + v4sf g0, g1; + int k; + INTERLEAVE2(in[0], in[1], g0, g1); in += in_stride; + + *--out = VSWAPHL(g0, g1); /* [g0l, g0h], [g1l g1h] -> [g1l, g0h] */ + for (k=1; k < N; ++k) { + v4sf h0, h1; + INTERLEAVE2(in[0], in[1], h0, h1); in += in_stride; + *--out = VSWAPHL(g1, h0); + *--out = VSWAPHL(h0, h1); + g1 = h1; + } + *--out = VSWAPHL(g1, g0); +} + +static void unreversed_copy(int N, const v4sf *in, v4sf *out, int out_stride) { + v4sf g0, g1, h0, h1; + int k; + g0 = g1 = in[0]; ++in; + for (k=1; k < N; ++k) { + h0 = *in++; h1 = *in++; + g1 = VSWAPHL(g1, h0); + h0 = VSWAPHL(h0, h1); + UNINTERLEAVE2(h0, g1, out[0], out[1]); out += out_stride; + g1 = h1; + } + h0 = *in++; h1 = g0; + g1 = VSWAPHL(g1, h0); + h0 = VSWAPHL(h0, h1); + UNINTERLEAVE2(h0, g1, out[0], out[1]); +} + +void FUNC_ZREORDER(const SETUP_STRUCT *setup, const float *in, float *out, pffft_direction_t direction) { + int k, N = setup->N, Ncvec = setup->Ncvec; + const v4sf *vin = (const v4sf*)in; + v4sf *vout = (v4sf*)out; + assert(in != out); + if (setup->transform == PFFFT_REAL) { + int dk = N/32; + if (direction == PFFFT_FORWARD) { + for (k=0; k < dk; ++k) { + INTERLEAVE2(vin[k*8 + 0], vin[k*8 + 1], vout[2*(0*dk + k) + 0], vout[2*(0*dk + k) + 1]); + INTERLEAVE2(vin[k*8 + 4], vin[k*8 + 5], vout[2*(2*dk + k) + 0], vout[2*(2*dk + k) + 1]); + } + reversed_copy(dk, vin+2, 8, (v4sf*)(out + N/2)); + reversed_copy(dk, vin+6, 8, (v4sf*)(out + N)); + } else { + for (k=0; k < dk; ++k) { + UNINTERLEAVE2(vin[2*(0*dk + k) + 0], vin[2*(0*dk + k) + 1], vout[k*8 + 0], vout[k*8 + 1]); + UNINTERLEAVE2(vin[2*(2*dk + k) + 0], vin[2*(2*dk + k) + 1], vout[k*8 + 4], vout[k*8 + 5]); + } + unreversed_copy(dk, (v4sf*)(in + N/4), (v4sf*)(out + N - 6*SIMD_SZ), -8); + unreversed_copy(dk, (v4sf*)(in + 3*N/4), (v4sf*)(out + N - 2*SIMD_SZ), -8); + } + } else { + if (direction == PFFFT_FORWARD) { + for (k=0; k < Ncvec; ++k) { + int kk = (k/4) + (k%4)*(Ncvec/4); + INTERLEAVE2(vin[k*2], vin[k*2+1], vout[kk*2], vout[kk*2+1]); + } + } else { + for (k=0; k < Ncvec; ++k) { + int kk = (k/4) + (k%4)*(Ncvec/4); + UNINTERLEAVE2(vin[kk*2], vin[kk*2+1], vout[k*2], vout[k*2+1]); + } + } + } +} + +void FUNC_CPLX_FINALIZE(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; /* number of 4x4 matrix blocks */ + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + assert(in != out); + for (k=0; k < dk; ++k) { + r0 = in[8*k+0]; i0 = in[8*k+1]; + r1 = in[8*k+2]; i1 = in[8*k+3]; + r2 = in[8*k+4]; i2 = in[8*k+5]; + r3 = in[8*k+6]; i3 = in[8*k+7]; + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + VCPLXMUL(r1,i1,e[k*6+0],e[k*6+1]); + VCPLXMUL(r2,i2,e[k*6+2],e[k*6+3]); + VCPLXMUL(r3,i3,e[k*6+4],e[k*6+5]); + + sr0 = VADD(r0,r2); dr0 = VSUB(r0, r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r1, r3); + si0 = VADD(i0,i2); di0 = VSUB(i0, i2); + si1 = VADD(i1,i3); di1 = VSUB(i1, i3); + + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 -1 0 0 -1 0 1] [r1] + [1 -1 1 -1 0 0 0 0] [r2] + [1 0 -1 0 0 1 0 -1] [r3] + [0 0 0 0 1 1 1 1] * [i0] + [0 1 0 -1 1 0 -1 0] [i1] + [0 0 0 0 1 -1 1 -1] [i2] + [0 -1 0 1 1 0 -1 0] [i3] + */ + + r0 = VADD(sr0, sr1); i0 = VADD(si0, si1); + r1 = VADD(dr0, di1); i1 = VSUB(di0, dr1); + r2 = VSUB(sr0, sr1); i2 = VSUB(si0, si1); + r3 = VSUB(dr0, di1); i3 = VADD(di0, dr1); + + *out++ = r0; *out++ = i0; *out++ = r1; *out++ = i1; + *out++ = r2; *out++ = i2; *out++ = r3; *out++ = i3; + } +} + +void FUNC_CPLX_PREPROCESS(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; /* number of 4x4 matrix blocks */ + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + assert(in != out); + for (k=0; k < dk; ++k) { + r0 = in[8*k+0]; i0 = in[8*k+1]; + r1 = in[8*k+2]; i1 = in[8*k+3]; + r2 = in[8*k+4]; i2 = in[8*k+5]; + r3 = in[8*k+6]; i3 = in[8*k+7]; + + sr0 = VADD(r0,r2); dr0 = VSUB(r0, r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r1, r3); + si0 = VADD(i0,i2); di0 = VSUB(i0, i2); + si1 = VADD(i1,i3); di1 = VSUB(i1, i3); + + r0 = VADD(sr0, sr1); i0 = VADD(si0, si1); + r1 = VSUB(dr0, di1); i1 = VADD(di0, dr1); + r2 = VSUB(sr0, sr1); i2 = VSUB(si0, si1); + r3 = VADD(dr0, di1); i3 = VSUB(di0, dr1); + + VCPLXMULCONJ(r1,i1,e[k*6+0],e[k*6+1]); + VCPLXMULCONJ(r2,i2,e[k*6+2],e[k*6+3]); + VCPLXMULCONJ(r3,i3,e[k*6+4],e[k*6+5]); + + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + *out++ = r0; *out++ = i0; *out++ = r1; *out++ = i1; + *out++ = r2; *out++ = i2; *out++ = r3; *out++ = i3; + } +} + + +static ALWAYS_INLINE(void) FUNC_REAL_FINALIZE_4X4(const v4sf *in0, const v4sf *in1, const v4sf *in, + const v4sf *e, v4sf *out) { + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + r0 = *in0; i0 = *in1; + r1 = *in++; i1 = *in++; r2 = *in++; i2 = *in++; r3 = *in++; i3 = *in++; + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 -1 0 0 -1 0 1] [r1] + [1 0 -1 0 0 1 0 -1] [r2] + [1 -1 1 -1 0 0 0 0] [r3] + [0 0 0 0 1 1 1 1] * [i0] + [0 -1 0 1 -1 0 1 0] [i1] + [0 -1 0 1 1 0 -1 0] [i2] + [0 0 0 0 -1 1 -1 1] [i3] + */ + + /* cerr << "matrix initial, before e , REAL:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n"; */ + /* cerr << "matrix initial, before e, IMAG :\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n"; */ + + VCPLXMUL(r1,i1,e[0],e[1]); + VCPLXMUL(r2,i2,e[2],e[3]); + VCPLXMUL(r3,i3,e[4],e[5]); + + /* cerr << "matrix initial, real part:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n"; */ + /* cerr << "matrix initial, imag part:\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n"; */ + + sr0 = VADD(r0,r2); dr0 = VSUB(r0,r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r3,r1); + si0 = VADD(i0,i2); di0 = VSUB(i0,i2); + si1 = VADD(i1,i3); di1 = VSUB(i3,i1); + + r0 = VADD(sr0, sr1); + r3 = VSUB(sr0, sr1); + i0 = VADD(si0, si1); + i3 = VSUB(si1, si0); + r1 = VADD(dr0, di1); + r2 = VSUB(dr0, di1); + i1 = VSUB(dr1, di0); + i2 = VADD(dr1, di0); + + *out++ = r0; + *out++ = i0; + *out++ = r1; + *out++ = i1; + *out++ = r2; + *out++ = i2; + *out++ = r3; + *out++ = i3; + +} + +static NEVER_INLINE(void) FUNC_REAL_FINALIZE(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; /* number of 4x4 matrix blocks */ + /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */ + + v4sf_union cr, ci, *uout = (v4sf_union*)out; + v4sf save = in[7], zero=VZERO(); + float xr0, xi0, xr1, xi1, xr2, xi2, xr3, xi3; + static const float s = (float)(M_SQRT1_2); + + cr.v = in[0]; ci.v = in[Ncvec*2-1]; + assert(in != out); + FUNC_REAL_FINALIZE_4X4(&zero, &zero, in+1, e, out); + + /* + [cr0 cr1 cr2 cr3 ci0 ci1 ci2 ci3] + + [Xr(1)] ] [1 1 1 1 0 0 0 0] + [Xr(N/4) ] [0 0 0 0 1 s 0 -s] + [Xr(N/2) ] [1 0 -1 0 0 0 0 0] + [Xr(3N/4)] [0 0 0 0 1 -s 0 s] + [Xi(1) ] [1 -1 1 -1 0 0 0 0] + [Xi(N/4) ] [0 0 0 0 0 -s -1 -s] + [Xi(N/2) ] [0 -1 0 1 0 0 0 0] + [Xi(3N/4)] [0 0 0 0 0 -s 1 -s] + */ + + xr0=(cr.f[0]+cr.f[2]) + (cr.f[1]+cr.f[3]); uout[0].f[0] = xr0; + xi0=(cr.f[0]+cr.f[2]) - (cr.f[1]+cr.f[3]); uout[1].f[0] = xi0; + xr2=(cr.f[0]-cr.f[2]); uout[4].f[0] = xr2; + xi2=(cr.f[3]-cr.f[1]); uout[5].f[0] = xi2; + xr1= ci.f[0] + s*(ci.f[1]-ci.f[3]); uout[2].f[0] = xr1; + xi1=-ci.f[2] - s*(ci.f[1]+ci.f[3]); uout[3].f[0] = xi1; + xr3= ci.f[0] - s*(ci.f[1]-ci.f[3]); uout[6].f[0] = xr3; + xi3= ci.f[2] - s*(ci.f[1]+ci.f[3]); uout[7].f[0] = xi3; + + for (k=1; k < dk; ++k) { + v4sf save_next = in[8*k+7]; + FUNC_REAL_FINALIZE_4X4(&save, &in[8*k+0], in + 8*k+1, + e + k*6, out + k*8); + save = save_next; + } + +} + +static ALWAYS_INLINE(void) FUNC_REAL_PREPROCESS_4X4(const v4sf *in, + const v4sf *e, v4sf *out, int first) { + v4sf r0=in[0], i0=in[1], r1=in[2], i1=in[3], r2=in[4], i2=in[5], r3=in[6], i3=in[7]; + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 0 -1 0 -1 -1 0] [r1] + [1 -1 -1 1 0 0 0 0] [r2] + [1 0 0 -1 0 1 1 0] [r3] + [0 0 0 0 1 -1 1 -1] * [i0] + [0 -1 1 0 1 0 0 1] [i1] + [0 0 0 0 1 1 -1 -1] [i2] + [0 1 -1 0 1 0 0 1] [i3] + */ + + v4sf sr0 = VADD(r0,r3), dr0 = VSUB(r0,r3); + v4sf sr1 = VADD(r1,r2), dr1 = VSUB(r1,r2); + v4sf si0 = VADD(i0,i3), di0 = VSUB(i0,i3); + v4sf si1 = VADD(i1,i2), di1 = VSUB(i1,i2); + + r0 = VADD(sr0, sr1); + r2 = VSUB(sr0, sr1); + r1 = VSUB(dr0, si1); + r3 = VADD(dr0, si1); + i0 = VSUB(di0, di1); + i2 = VADD(di0, di1); + i1 = VSUB(si0, dr1); + i3 = VADD(si0, dr1); + + VCPLXMULCONJ(r1,i1,e[0],e[1]); + VCPLXMULCONJ(r2,i2,e[2],e[3]); + VCPLXMULCONJ(r3,i3,e[4],e[5]); + + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + if (!first) { + *out++ = r0; + *out++ = i0; + } + *out++ = r1; + *out++ = i1; + *out++ = r2; + *out++ = i2; + *out++ = r3; + *out++ = i3; +} + +static NEVER_INLINE(void) FUNC_REAL_PREPROCESS(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; /* number of 4x4 matrix blocks */ + /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */ + + v4sf_union Xr, Xi, *uout = (v4sf_union*)out; + float cr0, ci0, cr1, ci1, cr2, ci2, cr3, ci3; + static const float s = (float)M_SQRT2; + assert(in != out); + for (k=0; k < 4; ++k) { + Xr.f[k] = ((float*)in)[8*k]; + Xi.f[k] = ((float*)in)[8*k+4]; + } + + FUNC_REAL_PREPROCESS_4X4(in, e, out+1, 1); /* will write only 6 values */ + + /* + [Xr0 Xr1 Xr2 Xr3 Xi0 Xi1 Xi2 Xi3] + + [cr0] [1 0 2 0 1 0 0 0] + [cr1] [1 0 0 0 -1 0 -2 0] + [cr2] [1 0 -2 0 1 0 0 0] + [cr3] [1 0 0 0 -1 0 2 0] + [ci0] [0 2 0 2 0 0 0 0] + [ci1] [0 s 0 -s 0 -s 0 -s] + [ci2] [0 0 0 0 0 -2 0 2] + [ci3] [0 -s 0 s 0 -s 0 -s] + */ + for (k=1; k < dk; ++k) { + FUNC_REAL_PREPROCESS_4X4(in+8*k, e + k*6, out-1+k*8, 0); + } + + cr0=(Xr.f[0]+Xi.f[0]) + 2*Xr.f[2]; uout[0].f[0] = cr0; + cr1=(Xr.f[0]-Xi.f[0]) - 2*Xi.f[2]; uout[0].f[1] = cr1; + cr2=(Xr.f[0]+Xi.f[0]) - 2*Xr.f[2]; uout[0].f[2] = cr2; + cr3=(Xr.f[0]-Xi.f[0]) + 2*Xi.f[2]; uout[0].f[3] = cr3; + ci0= 2*(Xr.f[1]+Xr.f[3]); uout[2*Ncvec-1].f[0] = ci0; + ci1= s*(Xr.f[1]-Xr.f[3]) - s*(Xi.f[1]+Xi.f[3]); uout[2*Ncvec-1].f[1] = ci1; + ci2= 2*(Xi.f[3]-Xi.f[1]); uout[2*Ncvec-1].f[2] = ci2; + ci3=-s*(Xr.f[1]-Xr.f[3]) - s*(Xi.f[1]+Xi.f[3]); uout[2*Ncvec-1].f[3] = ci3; +} + + +void FUNC_TRANSFORM_INTERNAL(const SETUP_STRUCT *setup, const float *finput, float *foutput, v4sf *scratch, + pffft_direction_t direction, int ordered) { + int k, Ncvec = setup->Ncvec; + int nf_odd = (setup->ifac[1] & 1); + + /* temporary buffer is allocated on the stack if the scratch pointer is NULL */ + int stack_allocate = (scratch == 0 ? Ncvec*2 : 1); + VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate); + + const v4sf *vinput = (const v4sf*)finput; + v4sf *voutput = (v4sf*)foutput; + v4sf *buff[2] = { voutput, scratch ? scratch : scratch_on_stack }; + int ib = (nf_odd ^ ordered ? 1 : 0); + + assert(VALIGNED(finput) && VALIGNED(foutput)); + + /* assert(finput != foutput); */ + if (direction == PFFFT_FORWARD) { + ib = !ib; + if (setup->transform == PFFFT_REAL) { + ib = (rfftf1_ps(Ncvec*2, vinput, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + FUNC_REAL_FINALIZE(Ncvec, buff[ib], buff[!ib], (v4sf*)setup->e); + } else { + v4sf *tmp = buff[ib]; + for (k=0; k < Ncvec; ++k) { + UNINTERLEAVE2(vinput[k*2], vinput[k*2+1], tmp[k*2], tmp[k*2+1]); + } + ib = (cfftf1_ps(Ncvec, buff[ib], buff[!ib], buff[ib], + setup->twiddle, &setup->ifac[0], -1) == buff[0] ? 0 : 1); + FUNC_CPLX_FINALIZE(Ncvec, buff[ib], buff[!ib], (v4sf*)setup->e); + } + if (ordered) { + FUNC_ZREORDER(setup, (float*)buff[!ib], (float*)buff[ib], PFFFT_FORWARD); + } else ib = !ib; + } else { + if (vinput == buff[ib]) { + ib = !ib; /* may happen when finput == foutput */ + } + if (ordered) { + FUNC_ZREORDER(setup, (float*)vinput, (float*)buff[ib], PFFFT_BACKWARD); + vinput = buff[ib]; ib = !ib; + } + if (setup->transform == PFFFT_REAL) { + FUNC_REAL_PREPROCESS(Ncvec, vinput, buff[ib], (v4sf*)setup->e); + ib = (rfftb1_ps(Ncvec*2, buff[ib], buff[0], buff[1], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + FUNC_CPLX_PREPROCESS(Ncvec, vinput, buff[ib], (v4sf*)setup->e); + ib = (cfftf1_ps(Ncvec, buff[ib], buff[0], buff[1], + setup->twiddle, &setup->ifac[0], +1) == buff[0] ? 0 : 1); + for (k=0; k < Ncvec; ++k) { + INTERLEAVE2(buff[ib][k*2], buff[ib][k*2+1], buff[ib][k*2], buff[ib][k*2+1]); + } + } + } + + if (buff[ib] != voutput) { + /* extra copy required -- this situation should only happen when finput == foutput */ + assert(finput==foutput); + for (k=0; k < Ncvec; ++k) { + v4sf a = buff[ib][2*k], b = buff[ib][2*k+1]; + voutput[2*k] = a; voutput[2*k+1] = b; + } + ib = !ib; + } + assert(buff[ib] == voutput); +} + +void FUNC_ZCONVOLVE_ACCUMULATE(const SETUP_STRUCT *s, const float *a, const float *b, float *ab, float scaling) { + int Ncvec = s->Ncvec; + const v4sf * RESTRICT va = (const v4sf*)a; + const v4sf * RESTRICT vb = (const v4sf*)b; + v4sf * RESTRICT vab = (v4sf*)ab; + +#ifdef __arm__ + __builtin_prefetch(va); + __builtin_prefetch(vb); + __builtin_prefetch(vab); + __builtin_prefetch(va+2); + __builtin_prefetch(vb+2); + __builtin_prefetch(vab+2); + __builtin_prefetch(va+4); + __builtin_prefetch(vb+4); + __builtin_prefetch(vab+4); + __builtin_prefetch(va+6); + __builtin_prefetch(vb+6); + __builtin_prefetch(vab+6); +# ifndef __clang__ +# define ZCONVOLVE_USING_INLINE_NEON_ASM +# endif +#endif + + float ar0, ai0, br0, bi0, abr0, abi0; +#ifndef ZCONVOLVE_USING_INLINE_ASM + v4sf vscal = LD_PS1(scaling); + int i; +#endif + + assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab)); + ar0 = ((v4sf_union*)va)[0].f[0]; + ai0 = ((v4sf_union*)va)[1].f[0]; + br0 = ((v4sf_union*)vb)[0].f[0]; + bi0 = ((v4sf_union*)vb)[1].f[0]; + abr0 = ((v4sf_union*)vab)[0].f[0]; + abi0 = ((v4sf_union*)vab)[1].f[0]; + +#ifdef ZCONVOLVE_USING_INLINE_ASM + /* inline asm version, unfortunately miscompiled by clang 3.2, + * at least on ubuntu.. so this will be restricted to gcc */ + const float *a_ = a, *b_ = b; float *ab_ = ab; + int N = Ncvec; + asm volatile("mov r8, %2 \n" + "vdup.f32 q15, %4 \n" + "1: \n" + "pld [%0,#64] \n" + "pld [%1,#64] \n" + "pld [%2,#64] \n" + "pld [%0,#96] \n" + "pld [%1,#96] \n" + "pld [%2,#96] \n" + "vld1.f32 {q0,q1}, [%0,:128]! \n" + "vld1.f32 {q4,q5}, [%1,:128]! \n" + "vld1.f32 {q2,q3}, [%0,:128]! \n" + "vld1.f32 {q6,q7}, [%1,:128]! \n" + "vld1.f32 {q8,q9}, [r8,:128]! \n" + + "vmul.f32 q10, q0, q4 \n" + "vmul.f32 q11, q0, q5 \n" + "vmul.f32 q12, q2, q6 \n" + "vmul.f32 q13, q2, q7 \n" + "vmls.f32 q10, q1, q5 \n" + "vmla.f32 q11, q1, q4 \n" + "vld1.f32 {q0,q1}, [r8,:128]! \n" + "vmls.f32 q12, q3, q7 \n" + "vmla.f32 q13, q3, q6 \n" + "vmla.f32 q8, q10, q15 \n" + "vmla.f32 q9, q11, q15 \n" + "vmla.f32 q0, q12, q15 \n" + "vmla.f32 q1, q13, q15 \n" + "vst1.f32 {q8,q9},[%2,:128]! \n" + "vst1.f32 {q0,q1},[%2,:128]! \n" + "subs %3, #2 \n" + "bne 1b \n" + : "+r"(a_), "+r"(b_), "+r"(ab_), "+r"(N) : "r"(scaling) : "r8", "q0","q1","q2","q3","q4","q5","q6","q7","q8","q9", "q10","q11","q12","q13","q15","memory"); +#else + /* default routine, works fine for non-arm cpus with current compilers */ + for (i=0; i < Ncvec; i += 2) { + v4sf ar, ai, br, bi; + ar = va[2*i+0]; ai = va[2*i+1]; + br = vb[2*i+0]; bi = vb[2*i+1]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+0] = VMADD(ar, vscal, vab[2*i+0]); + vab[2*i+1] = VMADD(ai, vscal, vab[2*i+1]); + ar = va[2*i+2]; ai = va[2*i+3]; + br = vb[2*i+2]; bi = vb[2*i+3]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+2] = VMADD(ar, vscal, vab[2*i+2]); + vab[2*i+3] = VMADD(ai, vscal, vab[2*i+3]); + } +#endif + if (s->transform == PFFFT_REAL) { + ((v4sf_union*)vab)[0].f[0] = abr0 + ar0*br0*scaling; + ((v4sf_union*)vab)[1].f[0] = abi0 + ai0*bi0*scaling; + } +} + +void FUNC_ZCONVOLVE_SCALE(const SETUP_STRUCT *s, const float *a, const float *b, float *ab, float scaling) { + v4sf vscal = LD_PS1(scaling); + const v4sf * RESTRICT va = (const v4sf*)a; + const v4sf * RESTRICT vb = (const v4sf*)b; + v4sf * RESTRICT vab = (v4sf*)ab; + float sar, sai, sbr, sbi; + const int NcvecMulTwo = 2*s->Ncvec; /* int Ncvec = s->Ncvec; */ + int k; /* was i -- but always used "2*i" - except at for() */ + +#ifdef __arm__ + __builtin_prefetch(va); + __builtin_prefetch(vb); + __builtin_prefetch(vab); + __builtin_prefetch(va+2); + __builtin_prefetch(vb+2); + __builtin_prefetch(vab+2); + __builtin_prefetch(va+4); + __builtin_prefetch(vb+4); + __builtin_prefetch(vab+4); + __builtin_prefetch(va+6); + __builtin_prefetch(vb+6); + __builtin_prefetch(vab+6); +# ifndef __clang__ +# define ZCONVOLVE_USING_INLINE_NEON_ASM +# endif +#endif + + assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab)); + sar = ((v4sf_union*)va)[0].f[0]; + sai = ((v4sf_union*)va)[1].f[0]; + sbr = ((v4sf_union*)vb)[0].f[0]; + sbi = ((v4sf_union*)vb)[1].f[0]; + + /* default routine, works fine for non-arm cpus with current compilers */ + for (k=0; k < NcvecMulTwo; k += 4) { + v4sf var, vai, vbr, vbi; + var = va[k+0]; vai = va[k+1]; + vbr = vb[k+0]; vbi = vb[k+1]; + VCPLXMUL(var, vai, vbr, vbi); + vab[k+0] = VMUL(var, vscal); + vab[k+1] = VMUL(vai, vscal); + var = va[k+2]; vai = va[k+3]; + vbr = vb[k+2]; vbi = vb[k+3]; + VCPLXMUL(var, vai, vbr, vbi); + vab[k+2] = VMUL(var, vscal); + vab[k+3] = VMUL(vai, vscal); + } + + if (s->transform == PFFFT_REAL) { + ((v4sf_union*)vab)[0].f[0] = sar*sbr*scaling; + ((v4sf_union*)vab)[1].f[0] = sai*sbi*scaling; + } +} + + +void FUNC_ZCONVOLVE(const SETUP_STRUCT *s, const float *a, const float *b, float *ab) { + const v4sf * RESTRICT va = (const v4sf*)a; + const v4sf * RESTRICT vb = (const v4sf*)b; + v4sf * RESTRICT vab = (v4sf*)ab; + float sar, sai, sbr, sbi; + const int NcvecMulTwo = 2*s->Ncvec; /* int Ncvec = s->Ncvec; */ + int k; /* was i -- but always used "2*i" - except at for() */ + +#ifdef __arm__ + __builtin_prefetch(va); + __builtin_prefetch(vb); + __builtin_prefetch(vab); + __builtin_prefetch(va+2); + __builtin_prefetch(vb+2); + __builtin_prefetch(vab+2); + __builtin_prefetch(va+4); + __builtin_prefetch(vb+4); + __builtin_prefetch(vab+4); + __builtin_prefetch(va+6); + __builtin_prefetch(vb+6); + __builtin_prefetch(vab+6); +#endif + + assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab)); + sar = ((v4sf_union*)va)[0].f[0]; + sai = ((v4sf_union*)va)[1].f[0]; + sbr = ((v4sf_union*)vb)[0].f[0]; + sbi = ((v4sf_union*)vb)[1].f[0]; + + /* default routine, works fine for non-arm cpus with current compilers */ + for (k=0; k < NcvecMulTwo; k += 4) { + v4sf var, vai, vbr, vbi; + var = va[k+0]; vai = va[k+1]; + vbr = vb[k+0]; vbi = vb[k+1]; + VCPLXMUL(var, vai, vbr, vbi); + vab[k+0] = var; + vab[k+1] = vai; + var = va[k+2]; vai = va[k+3]; + vbr = vb[k+2]; vbi = vb[k+3]; + VCPLXMUL(var, vai, vbr, vbi); + vab[k+2] = var; + vab[k+3] = vai; + } + + if (s->transform == PFFFT_REAL) { + ((v4sf_union*)vab)[0].f[0] = sar*sbr; + ((v4sf_union*)vab)[1].f[0] = sai*sbi; + } +} + + +int FUNC_ZCONVERT_ZP(const SETUP_STRUCT *s, const float *in, float *out, float scaling) { + int j, nv = s->Ncvec * 2; /* number of simd vectors: alternating real/imag vector pairs */ + /* no RESTRICT: in may alias out by contract */ + const v4sf *vin = (const v4sf*)in; + v4sf *vout = (v4sf*)out; + const float nscaling = scaling / (float)s->N; /* unit-gain normalization */ + v4sf vs = LD_PS1(nscaling); + if (s->transform != PFFFT_REAL) return -1; + assert(VALIGNED(in) && VALIGNED(out)); + /* + unordered forward REAL layout: even-indexed vectors hold the real + parts of 4 coefficients each, odd-indexed vectors their imaginary + parts -- except the very first odd lane, which packs the real + Nyquist coefficient F(N/2) next to F(0) in vector 0 (same packing + that pffft_zconvolve_accumulate() treats component-wise). + Zero-phase form: keep/scale the real vectors, zero the imag + vectors, keep/scale the Nyquist lane. + + The conversion applies a 1/N factor (times 'scaling'): with + scaling == 1, transform(x, FORWARD) -> zconvolve_zp() -> + transform(.., BACKWARD) yields the circular convolution of x + with the filter at unit gain. */ + vout[0] = VMUL(vin[0], vs); + { + /* latch the scaled Nyquist coefficient before zeroing: with + in == out the vector store below would destroy vin[1].f[0] */ + const float nyquist = ((const v4sf_union*)vin)[1].f[0] * nscaling; + const v4sf vz = VZERO(); + vout[1] = vz; + ((v4sf_union*)vout)[1].f[0] = nyquist; + for (j = 2; j < nv; j += 2) { + vout[j] = VMUL(vin[j], vs); + vout[j+1] = vz; + } + } + return 0; +} + +int FUNC_ZCONVOLVE_ZP(const SETUP_STRUCT *s, const float *x, const float *hzp, float *ab) { + int j, np = s->Ncvec; /* number of real/imag vector pairs */ + /* no RESTRICT: any pair of pointers may alias by contract */ + const v4sf *vx = (const v4sf*)x; + const v4sf *vh = (const v4sf*)hzp; + v4sf *vab = (v4sf*)ab; + if (s->transform != PFFFT_REAL) return -1; + assert(VALIGNED(x) && VALIGNED(hzp) && VALIGNED(ab)); + /* + with a zero-phase filter (imag vectors all zero): + Y_re = X_re * H_re and Y_im = X_im * H_re. + The first two vectors additionally carry the real DC and Nyquist + coefficients in their f[0] lanes (see FUNC_ZCONVERT_ZP), so that + pair is finished scalar-wise, component-wise, exactly like + FUNC_ZCONVOLVE_ACCUMULATE() does. */ + { + /* note: the Nyquist product reads hv1.f[0], not hv0.f[0] */ + v4sf_union xv1, hv0, hv1; + xv1.v = vx[1]; + hv0.v = vh[0]; hv1.v = vh[1]; + vab[0] = VMUL(vx[0], vh[0]); + ((v4sf_union*)vab)[1].f[0] = xv1.f[0] * hv1.f[0]; + ((v4sf_union*)vab)[1].f[1] = xv1.f[1] * hv0.f[1]; + ((v4sf_union*)vab)[1].f[2] = xv1.f[2] * hv0.f[2]; + ((v4sf_union*)vab)[1].f[3] = xv1.f[3] * hv0.f[3]; + } + for (j = 1; j < np; ++j) { + v4sf hr = vh[2*j]; + vab[2*j] = VMUL(vx[2*j], hr); + vab[2*j+1] = VMUL(vx[2*j+1], hr); + } + return 0; +} + +#else /* #if ( SIMD_SZ == 4 ) * !defined(PFFFT_SIMD_DISABLE) */ + +/* standard routine using scalar floats, without SIMD stuff. */ + +#define pffft_zreorder_nosimd FUNC_ZREORDER +void pffft_zreorder_nosimd(const SETUP_STRUCT *setup, const float *in, float *out, pffft_direction_t direction) { + int k, N = setup->N; + if (setup->transform == PFFFT_COMPLEX) { + for (k=0; k < 2*N; ++k) out[k] = in[k]; + return; + } + else if (direction == PFFFT_FORWARD) { + float x_N = in[N-1]; + for (k=N-1; k > 1; --k) out[k] = in[k-1]; + out[0] = in[0]; + out[1] = x_N; + } else { + float x_N = in[1]; + for (k=1; k < N-1; ++k) out[k] = in[k+1]; + out[0] = in[0]; + out[N-1] = x_N; + } +} + +#define pffft_transform_internal_nosimd FUNC_TRANSFORM_INTERNAL +void pffft_transform_internal_nosimd(const SETUP_STRUCT *setup, const float *input, float *output, float *scratch, + pffft_direction_t direction, int ordered) { + int Ncvec = setup->Ncvec; + int nf_odd = (setup->ifac[1] & 1); + + /* temporary buffer is allocated on the stack if the scratch pointer is NULL */ + int stack_allocate = (scratch == 0 ? Ncvec*2 : 1); + VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate); + float *buff[2]; + int ib; + if (scratch == 0) scratch = scratch_on_stack; + buff[0] = output; buff[1] = scratch; + + if (setup->transform == PFFFT_COMPLEX) ordered = 0; /* it is always ordered. */ + ib = (nf_odd ^ ordered ? 1 : 0); + + if (direction == PFFFT_FORWARD) { + if (setup->transform == PFFFT_REAL) { + ib = (rfftf1_ps(Ncvec*2, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0], -1) == buff[0] ? 0 : 1); + } + if (ordered) { + FUNC_ZREORDER(setup, buff[ib], buff[!ib], PFFFT_FORWARD); ib = !ib; + } + } else { + if (input == buff[ib]) { + ib = !ib; /* may happen when finput == foutput */ + } + if (ordered) { + FUNC_ZREORDER(setup, input, buff[!ib], PFFFT_BACKWARD); + input = buff[!ib]; + } + if (setup->transform == PFFFT_REAL) { + ib = (rfftb1_ps(Ncvec*2, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0], +1) == buff[0] ? 0 : 1); + } + } + if (buff[ib] != output) { + int k; + /* extra copy required -- this situation should happens only when finput == foutput */ + assert(input==output); + for (k=0; k < Ncvec; ++k) { + float a = buff[ib][2*k], b = buff[ib][2*k+1]; + output[2*k] = a; output[2*k+1] = b; + } + ib = !ib; + } + assert(buff[ib] == output); +} + +#define pffft_zconvolve_accumulate_nosimd FUNC_ZCONVOLVE_ACCUMULATE +void pffft_zconvolve_accumulate_nosimd(const SETUP_STRUCT *s, const float *a, const float *b, + float *ab, float scaling) { + int NcvecMulTwo = 2*s->Ncvec; /* int Ncvec = s->Ncvec; */ + int k; /* was i -- but always used "2*i" - except at for() */ + + if (s->transform == PFFFT_REAL) { + /* take care of the fftpack ordering */ + ab[0] += a[0]*b[0]*scaling; + ab[NcvecMulTwo-1] += a[NcvecMulTwo-1]*b[NcvecMulTwo-1]*scaling; + ++ab; ++a; ++b; NcvecMulTwo -= 2; + } + for (k=0; k < NcvecMulTwo; k += 2) { + float ar, ai, br, bi; + ar = a[k+0]; ai = a[k+1]; + br = b[k+0]; bi = b[k+1]; + VCPLXMUL(ar, ai, br, bi); + ab[k+0] += ar*scaling; + ab[k+1] += ai*scaling; + } +} + +#define pffft_zconvolve_scale_nosimd FUNC_ZCONVOLVE_SCALE +void pffft_zconvolve_scale_nosimd(const SETUP_STRUCT *s, const float *a, const float *b, + float *ab, float scaling) { + int NcvecMulTwo = 2*s->Ncvec; /* int Ncvec = s->Ncvec; */ + int k; /* was i -- but always used "2*i" - except at for() */ + + if (s->transform == PFFFT_REAL) { + /* take care of the fftpack ordering */ + ab[0] = a[0]*b[0]*scaling; + ab[NcvecMulTwo-1] = a[NcvecMulTwo-1]*b[NcvecMulTwo-1]*scaling; + ++ab; ++a; ++b; NcvecMulTwo -= 2; + } + for (k=0; k < NcvecMulTwo; k += 2) { + float ar, ai, br, bi; + ar = a[k+0]; ai = a[k+1]; + br = b[k+0]; bi = b[k+1]; + VCPLXMUL(ar, ai, br, bi); + ab[k+0] = ar*scaling; + ab[k+1] = ai*scaling; + } +} + + +#define pffft_zconvolve_nosimd FUNC_ZCONVOLVE +void pffft_zconvolve_nosimd(const SETUP_STRUCT *s, const float *a, const float *b, + float *ab) { + int NcvecMulTwo = 2*s->Ncvec; /* int Ncvec = s->Ncvec; */ + int k; /* was i -- but always used "2*i" - except at for() */ + + if (s->transform == PFFFT_REAL) { + /* take care of the fftpack ordering */ + ab[0] = a[0]*b[0]; + ab[NcvecMulTwo-1] = a[NcvecMulTwo-1]*b[NcvecMulTwo-1]; + ++ab; ++a; ++b; NcvecMulTwo -= 2; + } + for (k=0; k < NcvecMulTwo; k += 2) { + float ar, ai, br, bi; + ar = a[k+0]; ai = a[k+1]; + br = b[k+0]; bi = b[k+1]; + VCPLXMUL(ar, ai, br, bi); + ab[k+0] = ar; + ab[k+1] = ai; + } +} + + +#define pffft_zconvert_zp_nosimd FUNC_ZCONVERT_ZP +int pffft_zconvert_zp_nosimd(const SETUP_STRUCT *s, const float *in, float *out, float scaling) { + int k, N2 = s->Ncvec * 2; + float nscaling = scaling / (float)s->N; /* unit-gain normalization */ + if (s->transform != PFFFT_REAL) return -1; + out[0] = in[0] * nscaling; + out[N2-1] = in[N2-1] * nscaling; + for (k = 1; k < N2-1; k += 2) { + out[k] = in[k] * nscaling; + out[k+1] = 0.f; + } + return 0; +} + +#define pffft_zconvolve_zp_nosimd FUNC_ZCONVOLVE_ZP +int pffft_zconvolve_zp_nosimd(const SETUP_STRUCT *s, const float *x, const float *hzp, float *ab) { + int k, N2 = s->Ncvec * 2; + if (s->transform != PFFFT_REAL) return -1; + ab[0] = x[0] * hzp[0]; + ab[N2-1] = x[N2-1] * hzp[N2-1]; + for (k = 1; k < N2-1; k += 2) { + float hr = hzp[k]; + ab[k] = x[k] * hr; + ab[k+1] = x[k+1] * hr; + } + return 0; +} + +#endif /* #if ( SIMD_SZ == 4 ) * !defined(PFFFT_SIMD_DISABLE) */ + + +void FUNC_TRANSFORM_UNORDRD(const SETUP_STRUCT *setup, const float *input, float *output, float *work, pffft_direction_t direction) { + FUNC_TRANSFORM_INTERNAL(setup, input, output, (v4sf*)work, direction, 0); +} + +void FUNC_TRANSFORM_ORDERED(const SETUP_STRUCT *setup, const float *input, float *output, float *work, pffft_direction_t direction) { + FUNC_TRANSFORM_INTERNAL(setup, input, output, (v4sf*)work, direction, 1); +} + + +#if ( SIMD_SZ == 4 ) + +#define assertv4(v,f0,f1,f2,f3) assert(v.f[0] == (f0) && v.f[1] == (f1) && v.f[2] == (f2) && v.f[3] == (f3)) + +/* detect bugs with the vector support macros */ +PFFFT_EXPORT void FUNC_VALIDATE_SIMD_A(void) { + float f[16] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 }; + v4sf_union a0, a1, a2, a3, t, u; + memcpy(a0.f, f, 4*sizeof(float)); + memcpy(a1.f, f+4, 4*sizeof(float)); + memcpy(a2.f, f+8, 4*sizeof(float)); + memcpy(a3.f, f+12, 4*sizeof(float)); + + t = a0; u = a1; t.v = VZERO(); + printf("VZERO=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 0, 0, 0, 0); + t.v = VADD(a1.v, a2.v); + printf("VADD(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 12, 14, 16, 18); + t.v = VMUL(a1.v, a2.v); + printf("VMUL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 32, 45, 60, 77); + t.v = VMADD(a1.v, a2.v,a0.v); + printf("VMADD(4:7,8:11,0:3)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 32, 46, 62, 80); + + INTERLEAVE2(a1.v,a2.v,t.v,u.v); + printf("INTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]); + assertv4(t, 4, 8, 5, 9); assertv4(u, 6, 10, 7, 11); + UNINTERLEAVE2(a1.v,a2.v,t.v,u.v); + printf("UNINTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]); + assertv4(t, 4, 6, 8, 10); assertv4(u, 5, 7, 9, 11); + + t.v=LD_PS1(f[15]); + printf("LD_PS1(15)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); + assertv4(t, 15, 15, 15, 15); + t.v = VSWAPHL(a1.v, a2.v); + printf("VSWAPHL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); + assertv4(t, 8, 9, 6, 7); + VTRANSPOSE4(a0.v, a1.v, a2.v, a3.v); + printf("VTRANSPOSE4(0:3,4:7,8:11,12:15)=[%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", + a0.f[0], a0.f[1], a0.f[2], a0.f[3], a1.f[0], a1.f[1], a1.f[2], a1.f[3], + a2.f[0], a2.f[1], a2.f[2], a2.f[3], a3.f[0], a3.f[1], a3.f[2], a3.f[3]); + assertv4(a0, 0, 4, 8, 12); assertv4(a1, 1, 5, 9, 13); assertv4(a2, 2, 6, 10, 14); assertv4(a3, 3, 7, 11, 15); +} + + +static void pffft_assert1( float result, float ref, const char * vartxt, const char * functxt, int * numErrs, const char * f, int lineNo ) +{ + if ( !( fabs( result - ref ) < 0.01F ) ) + { + fprintf(stderr, "%s: assert for %s at %s(%d)\n expected %f value %f\n", functxt, vartxt, f, lineNo, ref, result); + ++(*numErrs); + } +} + +static void pffft_assert4( vsfscalar v0, vsfscalar v1, vsfscalar v2, vsfscalar v3, + float a, float b, float c, float d, const char * functxt, int * numErrs, const char * f, int lineNo ) +{ + pffft_assert1( v0, a, "[0]", functxt, numErrs, f, lineNo ); + pffft_assert1( v1, b, "[1]", functxt, numErrs, f, lineNo ); + pffft_assert1( v2, c, "[2]", functxt, numErrs, f, lineNo ); + pffft_assert1( v3, d, "[3]", functxt, numErrs, f, lineNo ); +} + +#define PFFFT_ASSERT4( V, a, b, c, d, FUNCTXT ) pffft_assert4( (V).f[0], (V).f[1], (V).f[2], (V).f[3], a, b, c, d, FUNCTXT, &numErrs, __FILE__, __LINE__ ) + + +PFFFT_EXPORT int FUNC_VALIDATE_SIMD_EX(FILE * DbgOut) +{ + int numErrs = 0; + + { + v4sf_union C; + int k; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: { }\n" ); + } + C.v = VZERO(); + if (DbgOut) { + fprintf(DbgOut, "VZERO(a) => C) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( C, 0.0F, 0.0F, 0.0F, 0.0F, "VZERO() Out C" ); + } + + { + v4sf_union C; + float a = 42.0F; + int k; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: a = {\n" ); + fprintf(DbgOut, " Inp a: %f\n", a ); + fprintf(DbgOut, "}\n" ); + } + C.v = LD_PS1(a); + if (DbgOut) { + fprintf(DbgOut, "LD_PS1(a) => C) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( C, 42.0F, 42.0F, 42.0F, 42.0F, "LD_PS1() Out C" ); + } + + { + v4sf_union C; + float a[16]; + int numAligned = 0, numUnaligned = 0; + int k; + const char * pUn; + for ( k = 0; k < 16; ++k ) a[k] = k+1; + + for ( k = 0; k + 3 < 16; ++k ) + { + const float * ptr = &a[k]; + if (DbgOut) + fprintf(DbgOut, "\ninput: a = [ %f, %f, %f, %f ]\n", ptr[0], ptr[1], ptr[2], ptr[3] ); + if ( VALIGNED(ptr) ) + { + C.v = VLOAD_ALIGNED( ptr ); + pUn = ""; + ++numAligned; + } + else + { + C.v = VLOAD_UNALIGNED( ptr ); + pUn = "UN"; + ++numUnaligned; + } + if (DbgOut) { + fprintf(DbgOut, "C = VLOAD_%sALIGNED(&a[%d]) => {\n", pUn, k ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + //PFFFT_ASSERT4( C, 32.0F, 34.0F, 36.0F, 38.0F, "VADD(): Out C" ); + + if ( numAligned >= 1 && numUnaligned >= 4 ) + break; + } + if ( numAligned < 1 ) { + fprintf(stderr, "VALIGNED() should have found at least 1 occurence!"); + ++numErrs; + } + if ( numUnaligned < 4 ) { + fprintf(stderr, "!VALIGNED() should have found at least 4 occurences!"); + ++numErrs; + } + } + + { + v4sf_union A, B, C; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = 20 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, "}\n" ); + } + C.v = VADD(A.v, B.v); + if (DbgOut) { + fprintf(DbgOut, "C = VADD(A,B) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "VADD(): Inp A" ); + PFFFT_ASSERT4( B, 21.0F, 22.0F, 23.0F, 24.0F, "VADD(): Inp B" ); + PFFFT_ASSERT4( C, 32.0F, 34.0F, 36.0F, 38.0F, "VADD(): Out C" ); + } + + { + v4sf_union A, B, C; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 20 + 2*k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, "}\n" ); + } + C.v = VSUB(A.v, B.v); + if (DbgOut) { + fprintf(DbgOut, "C = VSUB(A,B) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 21.0F, 23.0F, 25.0F, 27.0F, "VSUB(): Inp A" ); + PFFFT_ASSERT4( B, 11.0F, 12.0F, 13.0F, 14.0F, "VSUB(): Inp B" ); + PFFFT_ASSERT4( C, 10.0F, 11.0F, 12.0F, 13.0F, "VSUB(): Out C" ); + } + + { + v4sf_union A, B, C; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, "}\n" ); + } + C.v = VMUL(A.v, B.v); + if (DbgOut) { + fprintf(DbgOut, "C = VMUL(A,B) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "VMUL(): Inp A" ); + PFFFT_ASSERT4( B, 1.0F, 2.0F, 3.0F, 4.0F, "VMUL(): Inp B" ); + PFFFT_ASSERT4( C, 11.0F, 24.0F, 39.0F, 56.0F, "VMUL(): Out C" ); + } + + { + v4sf_union A, B, C, D; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 10 + k; + for ( k = 0; k < 4; ++k ) D.f[k] = 40 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B,C = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, " Inp C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + D.v = VMADD(A.v, B.v, C.v); + if (DbgOut) { + fprintf(DbgOut, "D = VMADD(A,B,C) => {\n" ); + fprintf(DbgOut, " Out D: %f, %f, %f, %f\n", D.f[0], D.f[1], D.f[2], D.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "VMADD(): Inp A" ); + PFFFT_ASSERT4( B, 1.0F, 2.0F, 3.0F, 4.0F, "VMADD(): Inp B" ); + PFFFT_ASSERT4( C, 10.0F, 11.0F, 12.0F, 13.0F, "VMADD(): Inp C" ); + PFFFT_ASSERT4( D, 21.0F, 35.0F, 51.0F, 69.0F, "VMADD(): Out D" ); + } + + { + v4sf_union A, B, C, D; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = 20 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + for ( k = 0; k < 4; ++k ) D.f[k] = 40 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, "}\n" ); + } + INTERLEAVE2(A.v, B.v, C.v, D.v); + if (DbgOut) { + fprintf(DbgOut, "INTERLEAVE2(A,B, => C,D) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, " Out D: %f, %f, %f, %f\n", D.f[0], D.f[1], D.f[2], D.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "INTERLEAVE2() Inp A" ); + PFFFT_ASSERT4( B, 21.0F, 22.0F, 23.0F, 24.0F, "INTERLEAVE2() Inp B" ); + PFFFT_ASSERT4( C, 11.0F, 21.0F, 12.0F, 22.0F, "INTERLEAVE2() Out C" ); + PFFFT_ASSERT4( D, 13.0F, 23.0F, 14.0F, 24.0F, "INTERLEAVE2() Out D" ); + } + + { + v4sf_union A, B, C, D; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = 20 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + for ( k = 0; k < 4; ++k ) D.f[k] = 40 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, "}\n" ); + } + UNINTERLEAVE2(A.v, B.v, C.v, D.v); + if (DbgOut) { + fprintf(DbgOut, "UNINTERLEAVE2(A,B, => C,D) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, " Out D: %f, %f, %f, %f\n", D.f[0], D.f[1], D.f[2], D.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "UNINTERLEAVE2() Inp A" ); + PFFFT_ASSERT4( B, 21.0F, 22.0F, 23.0F, 24.0F, "UNINTERLEAVE2() Inp B" ); + PFFFT_ASSERT4( C, 11.0F, 13.0F, 21.0F, 23.0F, "UNINTERLEAVE2() Out C" ); + PFFFT_ASSERT4( D, 12.0F, 14.0F, 22.0F, 24.0F, "UNINTERLEAVE2() Out D" ); + } + + { + v4sf_union A, B, C, D; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = 20 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + for ( k = 0; k < 4; ++k ) D.f[k] = 40 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B,C,D = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, " Inp C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, " Inp D: %f, %f, %f, %f\n", D.f[0], D.f[1], D.f[2], D.f[3] ); + fprintf(DbgOut, "}\n" ); + } + VTRANSPOSE4(A.v, B.v, C.v, D.v); + if (DbgOut) { + fprintf(DbgOut, "VTRANSPOSE4(A,B,C,D) => {\n" ); + fprintf(DbgOut, " Out A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Out B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, " Out D: %f, %f, %f, %f\n", D.f[0], D.f[1], D.f[2], D.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 21.0F, 31.0F, 41.0F, "VTRANSPOSE4(): Out A" ); + PFFFT_ASSERT4( B, 12.0F, 22.0F, 32.0F, 42.0F, "VTRANSPOSE4(): Out B" ); + PFFFT_ASSERT4( C, 13.0F, 23.0F, 33.0F, 43.0F, "VTRANSPOSE4(): Out C" ); + PFFFT_ASSERT4( D, 14.0F, 24.0F, 34.0F, 44.0F, "VTRANSPOSE4(): Out D" ); + } + + { + v4sf_union A, B, C; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) B.f[k] = 20 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A,B = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, " Inp B: %f, %f, %f, %f\n", B.f[0], B.f[1], B.f[2], B.f[3] ); + fprintf(DbgOut, "}\n" ); + } + C.v = VSWAPHL(A.v, B.v); + if (DbgOut) { + fprintf(DbgOut, "C = VSWAPHL(A,B) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "VSWAPHL(): Inp A" ); + PFFFT_ASSERT4( B, 21.0F, 22.0F, 23.0F, 24.0F, "VSWAPHL(): Inp B" ); + PFFFT_ASSERT4( C, 21.0F, 22.0F, 13.0F, 14.0F, "VSWAPHL(): Out C" ); + } + + { + v4sf_union A, C; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + for ( k = 0; k < 4; ++k ) C.f[k] = 30 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, "}\n" ); + } + C.v = VREV_S(A.v); + if (DbgOut) { + fprintf(DbgOut, "C = VREV_S(A) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "VREV_S(): Inp A" ); + PFFFT_ASSERT4( C, 14.0F, 13.0F, 12.0F, 11.0F, "VREV_S(): Out C" ); + } + + { + v4sf_union A, C; + int k; + for ( k = 0; k < 4; ++k ) A.f[k] = 10 + k+1; + + if (DbgOut) { + fprintf(DbgOut, "\ninput: A = {\n" ); + fprintf(DbgOut, " Inp A: %f, %f, %f, %f\n", A.f[0], A.f[1], A.f[2], A.f[3] ); + fprintf(DbgOut, "}\n" ); + } + C.v = VREV_C(A.v); + if (DbgOut) { + fprintf(DbgOut, "C = VREV_C(A) => {\n" ); + fprintf(DbgOut, " Out C: %f, %f, %f, %f\n", C.f[0], C.f[1], C.f[2], C.f[3] ); + fprintf(DbgOut, "}\n" ); + } + PFFFT_ASSERT4( A, 11.0F, 12.0F, 13.0F, 14.0F, "VREV_C(): Inp A" ); + PFFFT_ASSERT4( C, 13.0F, 14.0F, 11.0F, 12.0F, "VREV_C(): Out A" ); + } + + return numErrs; +} + +#else /* if ( SIMD_SZ == 4 ) */ + +PFFFT_EXPORT void FUNC_VALIDATE_SIMD_A() +{ +} + +PFFFT_EXPORT int FUNC_VALIDATE_SIMD_EX(FILE * DbgOut) +{ + return -1; +} + +#endif /* end if ( SIMD_SZ == 4 ) */ + diff --git a/native/vendor/r8brain/fft/simd/pf_avx_double.h b/native/vendor/r8brain/fft/simd/pf_avx_double.h new file mode 100644 index 0000000..fe0efa8 --- /dev/null +++ b/native/vendor/r8brain/fft/simd/pf_avx_double.h @@ -0,0 +1,145 @@ +/* + Copyright (c) 2020 Dario Mambro ( dario.mambro@gmail.com ) +*/ + +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +#ifndef PF_AVX_DBL_H +#define PF_AVX_DBL_H + +/* + vector support macros: the rest of the code is independant of + AVX -- adding support for other platforms with 4-element + vectors should be limited to these macros +*/ + + +/* + AVX support macros +*/ +#if !defined(SIMD_SZ) && !defined(PFFFT_SIMD_DISABLE) && defined(__AVX__) +#pragma message( __FILE__ ": AVX macros are defined" ) + +#include +typedef __m256d v4sf; + +/* 4 doubles by simd vector */ +# define SIMD_SZ 4 + +typedef union v4sf_union { + v4sf v; + double f[SIMD_SZ]; +} v4sf_union; + +# define VARCH "AVX" +# define VREQUIRES_ALIGN 1 +# define VZERO() _mm256_setzero_pd() +# define VMUL(a,b) _mm256_mul_pd(a,b) +# define VADD(a,b) _mm256_add_pd(a,b) +# define VMADD(a,b,c) _mm256_add_pd(_mm256_mul_pd(a,b), c) +# define VSUB(a,b) _mm256_sub_pd(a,b) +# define LD_PS1(p) _mm256_set1_pd(p) +# define VLOAD_UNALIGNED(ptr) _mm256_loadu_pd(ptr) +# define VLOAD_ALIGNED(ptr) _mm256_load_pd(ptr) + +/* INTERLEAVE2 (in1, in2, out1, out2) pseudo code: +out1 = [ in1[0], in2[0], in1[1], in2[1] ] +out2 = [ in1[2], in2[2], in1[3], in2[3] ] +*/ +# define INTERLEAVE2(in1, in2, out1, out2) { \ + __m128d low1__ = _mm256_castpd256_pd128(in1); \ + __m128d low2__ = _mm256_castpd256_pd128(in2); \ + __m128d high1__ = _mm256_extractf128_pd(in1, 1); \ + __m128d high2__ = _mm256_extractf128_pd(in2, 1); \ + __m256d tmp__ = _mm256_insertf128_pd( \ + _mm256_castpd128_pd256(_mm_shuffle_pd(low1__, low2__, 0)), \ + _mm_shuffle_pd(low1__, low2__, 3), \ + 1); \ + out2 = _mm256_insertf128_pd( \ + _mm256_castpd128_pd256(_mm_shuffle_pd(high1__, high2__, 0)), \ + _mm_shuffle_pd(high1__, high2__, 3), \ + 1); \ + out1 = tmp__; \ +} + +/*UNINTERLEAVE2(in1, in2, out1, out2) pseudo code: +out1 = [ in1[0], in1[2], in2[0], in2[2] ] +out2 = [ in1[1], in1[3], in2[1], in2[3] ] +*/ +# define UNINTERLEAVE2(in1, in2, out1, out2) { \ + __m128d low1__ = _mm256_castpd256_pd128(in1); \ + __m128d low2__ = _mm256_castpd256_pd128(in2); \ + __m128d high1__ = _mm256_extractf128_pd(in1, 1); \ + __m128d high2__ = _mm256_extractf128_pd(in2, 1); \ + __m256d tmp__ = _mm256_insertf128_pd( \ + _mm256_castpd128_pd256(_mm_shuffle_pd(low1__, high1__, 0)), \ + _mm_shuffle_pd(low2__, high2__, 0), \ + 1); \ + out2 = _mm256_insertf128_pd( \ + _mm256_castpd128_pd256(_mm_shuffle_pd(low1__, high1__, 3)), \ + _mm_shuffle_pd(low2__, high2__, 3), \ + 1); \ + out1 = tmp__; \ +} + +# define VTRANSPOSE4(row0, row1, row2, row3) { \ + __m256d tmp3, tmp2, tmp1, tmp0; \ + \ + tmp0 = _mm256_shuffle_pd((row0),(row1), 0x0); \ + tmp2 = _mm256_shuffle_pd((row0),(row1), 0xF); \ + tmp1 = _mm256_shuffle_pd((row2),(row3), 0x0); \ + tmp3 = _mm256_shuffle_pd((row2),(row3), 0xF); \ + \ + (row0) = _mm256_permute2f128_pd(tmp0, tmp1, 0x20); \ + (row1) = _mm256_permute2f128_pd(tmp2, tmp3, 0x20); \ + (row2) = _mm256_permute2f128_pd(tmp0, tmp1, 0x31); \ + (row3) = _mm256_permute2f128_pd(tmp2, tmp3, 0x31); \ + } + +/*VSWAPHL(a, b) pseudo code: +return [ b[0], b[1], a[2], a[3] ] +*/ +# define VSWAPHL(a,b) \ + _mm256_insertf128_pd(_mm256_castpd128_pd256(_mm256_castpd256_pd128(b)), _mm256_extractf128_pd(a, 1), 1) + +/* reverse/flip all floats */ +# define VREV_S(a) _mm256_insertf128_pd(_mm256_castpd128_pd256(_mm_permute_pd(_mm256_extractf128_pd(a, 1),1)), _mm_permute_pd(_mm256_castpd256_pd128(a), 1), 1) + +/* reverse/flip complex floats */ +# define VREV_C(a) _mm256_insertf128_pd(_mm256_castpd128_pd256(_mm256_extractf128_pd(a, 1)), _mm256_castpd256_pd128(a), 1) + +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x1F) == 0) + +#endif + +#endif /* PF_AVX_DBL_H */ + diff --git a/native/vendor/r8brain/fft/simd/pf_double.h b/native/vendor/r8brain/fft/simd/pf_double.h new file mode 100644 index 0000000..1025827 --- /dev/null +++ b/native/vendor/r8brain/fft/simd/pf_double.h @@ -0,0 +1,84 @@ + +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +#ifndef PF_DBL_H +#define PF_DBL_H + +#include +#include +#include + + +/* + * SIMD reference material: + * + * general SIMD introduction: + * https://www.linuxjournal.com/content/introduction-gcc-compiler-intrinsics-vector-processing + * + * SSE 1: + * https://software.intel.com/sites/landingpage/IntrinsicsGuide/ + * + * ARM NEON: + * https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics + * + * Altivec: + * https://www.nxp.com/docs/en/reference-manual/ALTIVECPIM.pdf + * https://gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/PowerPC-AltiVec_002fVSX-Built-in-Functions.html + * better one? + * + */ + +typedef double vsfscalar; + +#include "pf_avx_double.h" +#include "pf_sse2_double.h" +#include "pf_neon_double.h" + +#ifndef SIMD_SZ +# if !defined(PFFFT_SIMD_DISABLE) +# pragma message( "building double with simd disabled !" ) +# define PFFFT_SIMD_DISABLE /* fallback to scalar code */ +# endif +#endif + +#include "pf_scalar_double.h" + +/* shortcuts for complex multiplcations */ +#define VCPLXMUL(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VSUB(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VADD(ai,tmp); } +#define VCPLXMULCONJ(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VADD(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VSUB(ai,tmp); } +#ifndef SVMUL +/* multiply a scalar with a vector */ +#define SVMUL(f,v) VMUL(LD_PS1(f),v) +#endif + +#endif /* PF_DBL_H */ + diff --git a/native/vendor/r8brain/fft/simd/pf_neon_double.h b/native/vendor/r8brain/fft/simd/pf_neon_double.h new file mode 100644 index 0000000..c35d59d --- /dev/null +++ b/native/vendor/r8brain/fft/simd/pf_neon_double.h @@ -0,0 +1,203 @@ +/* + Copyright (c) 2020 Dario Mambro ( dario.mambro@gmail.com ) +*/ + +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +#ifndef PF_NEON_DBL_H +#define PF_NEON_DBL_H + +/* + NEON 64bit support macros +*/ +#if !defined(PFFFT_SIMD_DISABLE) && defined(PFFFT_ENABLE_NEON) && (defined(__aarch64__) || defined(__arm64__) || defined(__wasm_simd128__)) + +#pragma message (__FILE__ ": NEON (from AVX) macros are defined" ) + +#include "pf_neon_double_from_avx.h" +typedef __m256d v4sf; + +/* 4 doubles by simd vector */ +# define SIMD_SZ 4 + +typedef union v4sf_union { + v4sf v; + double f[SIMD_SZ]; +} v4sf_union; + +# define VARCH "NEON" +# define VREQUIRES_ALIGN 1 +# define VZERO() _mm256_setzero_pd() +# define VMUL(a,b) _mm256_mul_pd(a,b) +# define VADD(a,b) _mm256_add_pd(a,b) +# define VMADD(a,b,c) _mm256_add_pd(_mm256_mul_pd(a,b), c) +# define VSUB(a,b) _mm256_sub_pd(a,b) +# define LD_PS1(p) _mm256_set1_pd(p) +# define VLOAD_UNALIGNED(ptr) _mm256_loadu_pd(ptr) +# define VLOAD_ALIGNED(ptr) _mm256_load_pd(ptr) + +FORCE_INLINE __m256d _mm256_insertf128_pd_1(__m256d a, __m128d b) +{ + __m256d res; + res.vect_f64[0] = a.vect_f64[0]; + res.vect_f64[1] = b; + return res; +} + +FORCE_INLINE __m128d _mm_shuffle_pd_00(__m128d a, __m128d b) +{ + float64x1_t al = vget_low_f64(a); + float64x1_t bl = vget_low_f64(b); + return vcombine_f64(al, bl); +} + +FORCE_INLINE __m128d _mm_shuffle_pd_11(__m128d a, __m128d b) +{ + float64x1_t ah = vget_high_f64(a); + float64x1_t bh = vget_high_f64(b); + return vcombine_f64(ah, bh); +} + +FORCE_INLINE __m256d _mm256_shuffle_pd_00(__m256d a, __m256d b) +{ + __m256d res; + res.vect_f64[0] = _mm_shuffle_pd_00(a.vect_f64[0],b.vect_f64[0]); + res.vect_f64[1] = _mm_shuffle_pd_00(a.vect_f64[1],b.vect_f64[1]); + return res; +} + +FORCE_INLINE __m256d _mm256_shuffle_pd_11(__m256d a, __m256d b) +{ + __m256d res; + res.vect_f64[0] = _mm_shuffle_pd_11(a.vect_f64[0],b.vect_f64[0]); + res.vect_f64[1] = _mm_shuffle_pd_11(a.vect_f64[1],b.vect_f64[1]); + return res; +} + +FORCE_INLINE __m256d _mm256_permute2f128_pd_0x20(__m256d a, __m256d b) { + __m256d res; + res.vect_f64[0] = a.vect_f64[0]; + res.vect_f64[1] = b.vect_f64[0]; + return res; +} + + +FORCE_INLINE __m256d _mm256_permute2f128_pd_0x31(__m256d a, __m256d b) +{ + __m256d res; + res.vect_f64[0] = a.vect_f64[1]; + res.vect_f64[1] = b.vect_f64[1]; + return res; +} + +FORCE_INLINE __m256d _mm256_reverse(__m256d x) +{ + __m256d res; + float64x2_t low = x.vect_f64[0]; + float64x2_t high = x.vect_f64[1]; + float64x1_t a = vget_low_f64(low); + float64x1_t b = vget_high_f64(low); + float64x1_t c = vget_low_f64(high); + float64x1_t d = vget_high_f64(high); + res.vect_f64[0] = vcombine_f64(d, c); + res.vect_f64[1] = vcombine_f64(b, a); + return res; +} + +/* INTERLEAVE2 (in1, in2, out1, out2) pseudo code: +out1 = [ in1[0], in2[0], in1[1], in2[1] ] +out2 = [ in1[2], in2[2], in1[3], in2[3] ] +*/ +# define INTERLEAVE2(in1, in2, out1, out2) { \ + __m128d low1__ = _mm256_castpd256_pd128(in1); \ + __m128d low2__ = _mm256_castpd256_pd128(in2); \ + __m128d high1__ = _mm256_extractf128_pd(in1, 1); \ + __m128d high2__ = _mm256_extractf128_pd(in2, 1); \ + __m256d tmp__ = _mm256_insertf128_pd_1( \ + _mm256_castpd128_pd256(_mm_shuffle_pd_00(low1__, low2__)), \ + _mm_shuffle_pd_11(low1__, low2__)); \ + out2 = _mm256_insertf128_pd_1( \ + _mm256_castpd128_pd256(_mm_shuffle_pd_00(high1__, high2__)), \ + _mm_shuffle_pd_11(high1__, high2__)); \ + out1 = tmp__; \ +} + +/*UNINTERLEAVE2(in1, in2, out1, out2) pseudo code: +out1 = [ in1[0], in1[2], in2[0], in2[2] ] +out2 = [ in1[1], in1[3], in2[1], in2[3] ] +*/ +# define UNINTERLEAVE2(in1, in2, out1, out2) { \ + __m128d low1__ = _mm256_castpd256_pd128(in1); \ + __m128d low2__ = _mm256_castpd256_pd128(in2); \ + __m128d high1__ = _mm256_extractf128_pd(in1, 1); \ + __m128d high2__ = _mm256_extractf128_pd(in2, 1); \ + __m256d tmp__ = _mm256_insertf128_pd_1( \ + _mm256_castpd128_pd256(_mm_shuffle_pd_00(low1__, high1__)), \ + _mm_shuffle_pd_00(low2__, high2__)); \ + out2 = _mm256_insertf128_pd_1( \ + _mm256_castpd128_pd256(_mm_shuffle_pd_11(low1__, high1__)), \ + _mm_shuffle_pd_11(low2__, high2__)); \ + out1 = tmp__; \ +} + +# define VTRANSPOSE4(row0, row1, row2, row3) { \ + __m256d tmp3, tmp2, tmp1, tmp0; \ + \ + tmp0 = _mm256_shuffle_pd_00((row0),(row1)); \ + tmp2 = _mm256_shuffle_pd_11((row0),(row1)); \ + tmp1 = _mm256_shuffle_pd_00((row2),(row3)); \ + tmp3 = _mm256_shuffle_pd_11((row2),(row3)); \ + \ + (row0) = _mm256_permute2f128_pd_0x20(tmp0, tmp1); \ + (row1) = _mm256_permute2f128_pd_0x20(tmp2, tmp3); \ + (row2) = _mm256_permute2f128_pd_0x31(tmp0, tmp1); \ + (row3) = _mm256_permute2f128_pd_0x31(tmp2, tmp3); \ + } + +/*VSWAPHL(a, b) pseudo code: +return [ b[0], b[1], a[2], a[3] ] +*/ +# define VSWAPHL(a,b) \ + _mm256_insertf128_pd_1(_mm256_castpd128_pd256(_mm256_castpd256_pd128(b)), _mm256_extractf128_pd(a, 1)) + +/* reverse/flip all floats */ +# define VREV_S(a) _mm256_reverse(a) + +/* reverse/flip complex floats */ +# define VREV_C(a) _mm256_insertf128_pd_1(_mm256_castpd128_pd256(_mm256_extractf128_pd(a, 1)), _mm256_castpd256_pd128(a)) + +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x1F) == 0) + +#endif + +#endif /* PF_AVX_DBL_H */ + diff --git a/native/vendor/r8brain/fft/simd/pf_neon_double_from_avx.h b/native/vendor/r8brain/fft/simd/pf_neon_double_from_avx.h new file mode 100644 index 0000000..5cce17e --- /dev/null +++ b/native/vendor/r8brain/fft/simd/pf_neon_double_from_avx.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2020. Huawei Technologies Co., Ltd. All rights reserved. + + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ + +//see https://github.com/kunpengcompute/AvxToNeon + +#ifndef PF_NEON_DBL_FROM_AVX_H +#define PF_NEON_DBL_FROM_AVX_H +#include + + +#if defined(__GNUC__) || defined(__clang__) + +#pragma push_macro("FORCE_INLINE") +#define FORCE_INLINE static inline __attribute__((always_inline)) + +#else + +#error "Macro name collisions may happens with unknown compiler" +#ifdef FORCE_INLINE +#undef FORCE_INLINE +#endif + +#define FORCE_INLINE static inline + +#endif + +typedef struct { + float32x4_t vect_f32[2]; +} __m256; + +typedef struct { + float64x2_t vect_f64[2]; +} __m256d; + +typedef float64x2_t __m128d; + +FORCE_INLINE __m256d _mm256_setzero_pd(void) +{ + __m256d ret; + ret.vect_f64[0] = ret.vect_f64[1] = vdupq_n_f64(0.0); + return ret; +} + +FORCE_INLINE __m256d _mm256_mul_pd(__m256d a, __m256d b) +{ + __m256d res_m256d; + res_m256d.vect_f64[0] = vmulq_f64(a.vect_f64[0], b.vect_f64[0]); + res_m256d.vect_f64[1] = vmulq_f64(a.vect_f64[1], b.vect_f64[1]); + return res_m256d; +} + +FORCE_INLINE __m256d _mm256_add_pd(__m256d a, __m256d b) +{ + __m256d res_m256d; + res_m256d.vect_f64[0] = vaddq_f64(a.vect_f64[0], b.vect_f64[0]); + res_m256d.vect_f64[1] = vaddq_f64(a.vect_f64[1], b.vect_f64[1]); + return res_m256d; +} + +FORCE_INLINE __m256d _mm256_sub_pd(__m256d a, __m256d b) +{ + __m256d res_m256d; + res_m256d.vect_f64[0] = vsubq_f64(a.vect_f64[0], b.vect_f64[0]); + res_m256d.vect_f64[1] = vsubq_f64(a.vect_f64[1], b.vect_f64[1]); + return res_m256d; +} + +FORCE_INLINE __m256d _mm256_set1_pd(double a) +{ + __m256d ret; + ret.vect_f64[0] = ret.vect_f64[1] = vdupq_n_f64(a); + return ret; +} + +FORCE_INLINE __m256d _mm256_load_pd (double const * mem_addr) +{ + __m256d res; + res.vect_f64[0] = vld1q_f64((const double *)mem_addr); + res.vect_f64[1] = vld1q_f64((const double *)mem_addr + 2); + return res; +} +FORCE_INLINE __m256d _mm256_loadu_pd (double const * mem_addr) +{ + __m256d res; + res.vect_f64[0] = vld1q_f64((const double *)mem_addr); + res.vect_f64[1] = vld1q_f64((const double *)mem_addr + 2); + return res; +} + +FORCE_INLINE __m128d _mm256_castpd256_pd128(__m256d a) +{ + return a.vect_f64[0]; +} + +FORCE_INLINE __m128d _mm256_extractf128_pd (__m256d a, const int imm8) +{ + assert(imm8 >= 0 && imm8 <= 1); + return a.vect_f64[imm8]; +} + +FORCE_INLINE __m256d _mm256_castpd128_pd256(__m128d a) +{ + __m256d res; + res.vect_f64[0] = a; + return res; +} + +#endif /* PF_AVX_DBL_H */ + diff --git a/native/vendor/r8brain/fft/simd/pf_scalar_double.h b/native/vendor/r8brain/fft/simd/pf_scalar_double.h new file mode 100644 index 0000000..6c54938 --- /dev/null +++ b/native/vendor/r8brain/fft/simd/pf_scalar_double.h @@ -0,0 +1,189 @@ + +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + Copyright (c) 2020 Hayati Ayguen ( h_ayguen@web.de ) + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +#ifndef PF_SCAL_DBL_H +#define PF_SCAL_DBL_H + +/* + fallback mode(s) for situations where SSE/AVX/NEON/Altivec are not available, use scalar mode instead +*/ + +#ifndef PFFFT_SCALVEC_ENABLED +#define PFFFT_SCALVEC_ENABLED +#endif + +#if !defined(SIMD_SZ) && defined(PFFFT_SCALVEC_ENABLED) +#pragma message( __FILE__ ": double SCALAR4 macros are defined" ) + +typedef struct { + vsfscalar a; + vsfscalar b; + vsfscalar c; + vsfscalar d; +} v4sf; + +# define SIMD_SZ 4 + +typedef union v4sf_union { + v4sf v; + vsfscalar f[SIMD_SZ]; +} v4sf_union; + +# define VARCH "4xScalar" +# define VREQUIRES_ALIGN 0 + + static ALWAYS_INLINE(v4sf) VZERO() { + v4sf r = { 0.f, 0.f, 0.f, 0.f }; + return r; + } + + static ALWAYS_INLINE(v4sf) VMUL(v4sf A, v4sf B) { + v4sf r = { A.a * B.a, A.b * B.b, A.c * B.c, A.d * B.d }; + return r; + } + + static ALWAYS_INLINE(v4sf) VADD(v4sf A, v4sf B) { + v4sf r = { A.a + B.a, A.b + B.b, A.c + B.c, A.d + B.d }; + return r; + } + + static ALWAYS_INLINE(v4sf) VMADD(v4sf A, v4sf B, v4sf C) { + v4sf r = { A.a * B.a + C.a, A.b * B.b + C.b, A.c * B.c + C.c, A.d * B.d + C.d }; + return r; + } + + static ALWAYS_INLINE(v4sf) VSUB(v4sf A, v4sf B) { + v4sf r = { A.a - B.a, A.b - B.b, A.c - B.c, A.d - B.d }; + return r; + } + + static ALWAYS_INLINE(v4sf) LD_PS1(vsfscalar v) { + v4sf r = { v, v, v, v }; + return r; + } + +# define VLOAD_UNALIGNED(ptr) (*((v4sf*)(ptr))) + +# define VLOAD_ALIGNED(ptr) (*((v4sf*)(ptr))) + +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & (sizeof(v4sf)-1) ) == 0) + + + /* INTERLEAVE2() */ + #define INTERLEAVE2( A, B, C, D) \ + do { \ + v4sf Cr = { A.a, B.a, A.b, B.b }; \ + v4sf Dr = { A.c, B.c, A.d, B.d }; \ + C = Cr; \ + D = Dr; \ + } while (0) + + + /* UNINTERLEAVE2() */ + #define UNINTERLEAVE2(A, B, C, D) \ + do { \ + v4sf Cr = { A.a, A.c, B.a, B.c }; \ + v4sf Dr = { A.b, A.d, B.b, B.d }; \ + C = Cr; \ + D = Dr; \ + } while (0) + + + /* VTRANSPOSE4() */ + #define VTRANSPOSE4(A, B, C, D) \ + do { \ + v4sf Ar = { A.a, B.a, C.a, D.a }; \ + v4sf Br = { A.b, B.b, C.b, D.b }; \ + v4sf Cr = { A.c, B.c, C.c, D.c }; \ + v4sf Dr = { A.d, B.d, C.d, D.d }; \ + A = Ar; \ + B = Br; \ + C = Cr; \ + D = Dr; \ + } while (0) + + + /* VSWAPHL() */ + static ALWAYS_INLINE(v4sf) VSWAPHL(v4sf A, v4sf B) { + v4sf r = { B.a, B.b, A.c, A.d }; + return r; + } + + + /* reverse/flip all floats */ + static ALWAYS_INLINE(v4sf) VREV_S(v4sf A) { + v4sf r = { A.d, A.c, A.b, A.a }; + return r; + } + + /* reverse/flip complex floats */ + static ALWAYS_INLINE(v4sf) VREV_C(v4sf A) { + v4sf r = { A.c, A.d, A.a, A.b }; + return r; + } + +#else +/* #pragma message( __FILE__ ": double SCALAR4 macros are not defined" ) */ +#endif + + +#if !defined(SIMD_SZ) +#pragma message( __FILE__ ": float SCALAR1 macros are defined" ) +typedef vsfscalar v4sf; + +# define SIMD_SZ 1 + +typedef union v4sf_union { + v4sf v; + vsfscalar f[SIMD_SZ]; +} v4sf_union; + +# define VARCH "Scalar" +# define VREQUIRES_ALIGN 0 +# define VZERO() 0.0 +# define VMUL(a,b) ((a)*(b)) +# define VADD(a,b) ((a)+(b)) +# define VMADD(a,b,c) ((a)*(b)+(c)) +# define VSUB(a,b) ((a)-(b)) +# define LD_PS1(p) (p) +# define VLOAD_UNALIGNED(ptr) (*(ptr)) +# define VLOAD_ALIGNED(ptr) (*(ptr)) +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & (sizeof(vsfscalar)-1) ) == 0) + +#else +/* #pragma message( __FILE__ ": double SCALAR1 macros are not defined" ) */ +#endif + + +#endif /* PF_SCAL_DBL_H */ + diff --git a/native/vendor/r8brain/fft/simd/pf_sse2_double.h b/native/vendor/r8brain/fft/simd/pf_sse2_double.h new file mode 100644 index 0000000..cca4f52 --- /dev/null +++ b/native/vendor/r8brain/fft/simd/pf_sse2_double.h @@ -0,0 +1,282 @@ +/* + Copyright (c) 2020 Dario Mambro ( dario.mambro@gmail.com ) +*/ + +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +#ifndef PF_SSE2_DBL_H +#define PF_SSE2_DBL_H + +//detect sse2 support under MSVC +#if defined ( _M_IX86_FP ) +# if _M_IX86_FP == 2 +# if !defined(__SSE2__) +# define __SSE2__ +# endif +# endif +#endif + +/* + SSE2 64bit support macros +*/ +/* we test _M_ARM64EC before _M_X64 because when _M_ARM64EC is defined, the microsoft compiler also defines _M_X64 */ +#if !defined(SIMD_SZ) && !defined(PFFFT_SIMD_DISABLE) && !defined(_M_ARM64EC) && (defined( __SSE4_2__ ) | defined( __SSE4_1__ ) || defined( __SSE3__ ) || defined( __SSE2__ ) || defined ( __x86_64__ ) || defined( _M_AMD64 ) || defined( _M_X64 ) || defined( __amd64 )) +#pragma message (__FILE__ ": SSE2 double macros are defined" ) + +#include + +typedef struct { + __m128d d128[2]; +} m256d; + +typedef m256d v4sf; + +# define SIMD_SZ 4 + +typedef union v4sf_union { + v4sf v; + double f[SIMD_SZ]; +} v4sf_union; + + +#if defined(__GNUC__) || defined(__clang__) + +#pragma push_macro("FORCE_INLINE") +#define FORCE_INLINE static inline __attribute__((always_inline)) + +#elif defined (_MSC_VER) +#define FORCE_INLINE static __forceinline + +#else +#error "Macro name collisions may happens with unknown compiler" +#ifdef FORCE_INLINE +#undef FORCE_INLINE +#endif +#define FORCE_INLINE static inline +#endif + +FORCE_INLINE m256d mm256_setzero_pd(void) +{ + m256d ret; + ret.d128[0] = ret.d128[1] = _mm_setzero_pd(); + return ret; +} + +FORCE_INLINE m256d mm256_mul_pd(m256d a, m256d b) +{ + m256d ret; + ret.d128[0] = _mm_mul_pd(a.d128[0], b.d128[0]); + ret.d128[1] = _mm_mul_pd(a.d128[1], b.d128[1]); + return ret; +} + +FORCE_INLINE m256d mm256_add_pd(m256d a, m256d b) +{ + m256d ret; + ret.d128[0] = _mm_add_pd(a.d128[0], b.d128[0]); + ret.d128[1] = _mm_add_pd(a.d128[1], b.d128[1]); + return ret; +} + +FORCE_INLINE m256d mm256_sub_pd(m256d a, m256d b) +{ + m256d ret; + ret.d128[0] = _mm_sub_pd(a.d128[0], b.d128[0]); + ret.d128[1] = _mm_sub_pd(a.d128[1], b.d128[1]); + return ret; +} + +FORCE_INLINE m256d mm256_set1_pd(double a) +{ + m256d ret; + ret.d128[0] = ret.d128[1] = _mm_set1_pd(a); + return ret; +} + +FORCE_INLINE m256d mm256_load_pd (double const * mem_addr) +{ + m256d res; + res.d128[0] = _mm_load_pd((const double *)mem_addr); + res.d128[1] = _mm_load_pd((const double *)mem_addr + 2); + return res; +} +FORCE_INLINE m256d mm256_loadu_pd (double const * mem_addr) +{ + m256d res; + res.d128[0] = _mm_loadu_pd((const double *)mem_addr); + res.d128[1] = _mm_loadu_pd((const double *)mem_addr + 2); + return res; +} + + +# define VARCH "SSE2" +# define VREQUIRES_ALIGN 1 +# define VZERO() mm256_setzero_pd() +# define VMUL(a,b) mm256_mul_pd(a,b) +# define VADD(a,b) mm256_add_pd(a,b) +# define VMADD(a,b,c) mm256_add_pd(mm256_mul_pd(a,b), c) +# define VSUB(a,b) mm256_sub_pd(a,b) +# define LD_PS1(p) mm256_set1_pd(p) +# define VLOAD_UNALIGNED(ptr) mm256_loadu_pd(ptr) +# define VLOAD_ALIGNED(ptr) mm256_load_pd(ptr) + + +FORCE_INLINE __m128d mm256_castpd256_pd128(m256d a) +{ + return a.d128[0]; +} + +FORCE_INLINE __m128d mm256_extractf128_pd (m256d a, const int imm8) +{ + assert(imm8 >= 0 && imm8 <= 1); + return a.d128[imm8]; +} +FORCE_INLINE m256d mm256_insertf128_pd_1(m256d a, __m128d b) +{ + m256d res; + res.d128[0] = a.d128[0]; + res.d128[1] = b; + return res; +} +FORCE_INLINE m256d mm256_castpd128_pd256(__m128d a) +{ + m256d res; + res.d128[0] = a; + return res; +} + +FORCE_INLINE m256d mm256_shuffle_pd_00(m256d a, m256d b) +{ + m256d res; + res.d128[0] = _mm_shuffle_pd(a.d128[0],b.d128[0],0); + res.d128[1] = _mm_shuffle_pd(a.d128[1],b.d128[1],0); + return res; +} + +FORCE_INLINE m256d mm256_shuffle_pd_11(m256d a, m256d b) +{ + m256d res; + res.d128[0] = _mm_shuffle_pd(a.d128[0],b.d128[0], 3); + res.d128[1] = _mm_shuffle_pd(a.d128[1],b.d128[1], 3); + return res; +} + +FORCE_INLINE m256d mm256_permute2f128_pd_0x20(m256d a, m256d b) { + m256d res; + res.d128[0] = a.d128[0]; + res.d128[1] = b.d128[0]; + return res; +} + + +FORCE_INLINE m256d mm256_permute2f128_pd_0x31(m256d a, m256d b) +{ + m256d res; + res.d128[0] = a.d128[1]; + res.d128[1] = b.d128[1]; + return res; +} + +FORCE_INLINE m256d mm256_reverse(m256d x) +{ + m256d res; + res.d128[0] = _mm_shuffle_pd(x.d128[1],x.d128[1],1); + res.d128[1] = _mm_shuffle_pd(x.d128[0],x.d128[0],1); + return res; +} + +/* INTERLEAVE2 (in1, in2, out1, out2) pseudo code: +out1 = [ in1[0], in2[0], in1[1], in2[1] ] +out2 = [ in1[2], in2[2], in1[3], in2[3] ] +*/ +# define INTERLEAVE2(in1, in2, out1, out2) { \ + __m128d low1__ = mm256_castpd256_pd128(in1); \ + __m128d low2__ = mm256_castpd256_pd128(in2); \ + __m128d high1__ = mm256_extractf128_pd(in1, 1); \ + __m128d high2__ = mm256_extractf128_pd(in2, 1); \ + m256d tmp__ = mm256_insertf128_pd_1( \ + mm256_castpd128_pd256(_mm_shuffle_pd(low1__, low2__, 0)), \ + _mm_shuffle_pd(low1__, low2__, 3)); \ + out2 = mm256_insertf128_pd_1( \ + mm256_castpd128_pd256(_mm_shuffle_pd(high1__, high2__, 0)), \ + _mm_shuffle_pd(high1__, high2__, 3)); \ + out1 = tmp__; \ +} + +/*UNINTERLEAVE2(in1, in2, out1, out2) pseudo code: +out1 = [ in1[0], in1[2], in2[0], in2[2] ] +out2 = [ in1[1], in1[3], in2[1], in2[3] ] +*/ +# define UNINTERLEAVE2(in1, in2, out1, out2) { \ + __m128d low1__ = mm256_castpd256_pd128(in1); \ + __m128d low2__ = mm256_castpd256_pd128(in2); \ + __m128d high1__ = mm256_extractf128_pd(in1, 1); \ + __m128d high2__ = mm256_extractf128_pd(in2, 1); \ + m256d tmp__ = mm256_insertf128_pd_1( \ + mm256_castpd128_pd256(_mm_shuffle_pd(low1__, high1__, 0)), \ + _mm_shuffle_pd(low2__, high2__, 0)); \ + out2 = mm256_insertf128_pd_1( \ + mm256_castpd128_pd256(_mm_shuffle_pd(low1__, high1__, 3)), \ + _mm_shuffle_pd(low2__, high2__, 3)); \ + out1 = tmp__; \ +} + +# define VTRANSPOSE4(row0, row1, row2, row3) { \ + m256d tmp3, tmp2, tmp1, tmp0; \ + \ + tmp0 = mm256_shuffle_pd_00((row0),(row1)); \ + tmp2 = mm256_shuffle_pd_11((row0),(row1)); \ + tmp1 = mm256_shuffle_pd_00((row2),(row3)); \ + tmp3 = mm256_shuffle_pd_11((row2),(row3)); \ + \ + (row0) = mm256_permute2f128_pd_0x20(tmp0, tmp1); \ + (row1) = mm256_permute2f128_pd_0x20(tmp2, tmp3); \ + (row2) = mm256_permute2f128_pd_0x31(tmp0, tmp1); \ + (row3) = mm256_permute2f128_pd_0x31(tmp2, tmp3); \ + } + +/*VSWAPHL(a, b) pseudo code: +return [ b[0], b[1], a[2], a[3] ] +*/ +# define VSWAPHL(a,b) \ + mm256_insertf128_pd_1(mm256_castpd128_pd256(mm256_castpd256_pd128(b)), mm256_extractf128_pd(a, 1)) + +/* reverse/flip all floats */ +# define VREV_S(a) mm256_reverse(a) + +/* reverse/flip complex floats */ +# define VREV_C(a) mm256_insertf128_pd_1(mm256_castpd128_pd256(mm256_extractf128_pd(a, 1)), mm256_castpd256_pd128(a)) + +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x1F) == 0) + +#endif +#endif diff --git a/native/vendor/r8brain/r8bbase.h b/native/vendor/r8brain/r8bbase.h new file mode 100644 index 0000000..99ca50b --- /dev/null +++ b/native/vendor/r8brain/r8bbase.h @@ -0,0 +1,1394 @@ +//$ nobt + +/** + * @file r8bbase.h + * + * @version 7.5 + * + * @brief The "base" header file with basic classes and functions. + * + * This is the "base" header file for the "r8brain-free-src" sample rate + * converter. This file contains implementations of several small utility + * classes and functions used by the library. + * + * @mainpage + * + * @section intro_sec Introduction + * + * Open source (under the MIT license) high-quality professional audio sample + * rate converter (SRC) / resampler C++ library. Features routines for SRC, + * both up- and downsampling, to/from any sample rate, including non-integer + * sample rates: it can be also used for conversion to/from SACD/DSD sample + * rates, and even go beyond that. SRC routines were implemented in a + * multi-platform C++ code, and have a high level of optimality. Also suitable + * for fast general-purpose 1D time-series resampling / interpolation (with + * relaxed filter parameters). + * + * For more information, please visit + * https://github.com/avaneev/r8brain-free-src + * + * Email: aleksey.vaneev@gmail.com or info@voxengo.com + * + * @section license License + * + * The MIT License (MIT) + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Please credit the creator of this library in your documentation in the + * following way: "Sample rate converter designed by Aleksey Vaneev of + * Voxengo" + */ + +#ifndef R8BBASE_INCLUDED +#define R8BBASE_INCLUDED + +#define R8B_VERSION "7.5" ///< Macro defines r8brain-free-src version string. + +/** + * @def R8B_CONST + * @brief The prefix for constant definitions, portable between C++11 and + * earlier C++ versions. + */ + +/** + * @def R8B_NULL + * @brief The "null pointer" value, portable between C++11 and earlier C++ + * versions. + */ + +#include +#include +#include + +#if __cplusplus >= 201103L + + #include + #include + + #define R8B_CONST constexpr + #define R8B_NULL nullptr + +#else // __cplusplus >= 201103L + + #include // A C99 fallback, as C++98 has no cstdint header. + + #if defined( _WIN32 ) + #include + #else // defined( _WIN32 ) + #include + #endif // defined( _WIN32 ) + + #define R8B_CONST static const + #define R8B_NULL NULL + +#endif // __cplusplus >= 201103L + +#include "r8bconf.h" + +#if defined( __aarch64__ ) || defined( __arm64__ ) || \ + defined( _M_ARM64 ) || defined( _M_ARM64EC ) + + #if defined( _MSC_VER ) + #include + #else // defined( _MSC_VER ) + #include + #endif // defined( _MSC_VER ) + + #define R8B_NEON + + #if !defined( __APPLE__ ) + #define R8B_SIMD_ISH // Shuffled interpolation is inefficient on M1. + #endif // !defined( __APPLE__ ) + +#elif defined( __SSE2__ ) || defined( _M_AMD64 ) || \ + ( defined( _M_IX86_FP ) && _M_IX86_FP == 2 ) + + #if defined( _MSC_VER ) + #include + #else // defined( _MSC_VER ) + #include + #endif // defined( _MSC_VER ) + + #define R8B_SSE2 + #define R8B_SIMD_ISH + +#endif // SSE2 + +/** + * @def R8B_EXITDTOR + * @brief Macro that defines the attribute specifying that the exit-time + * destructor should be called for a static variable (suppresses a compiler + * warning). + */ + +#if defined( __clang__ ) + + #define R8B_EXITDTOR __attribute__((always_destroy)) + +#else // defined( __clang__ ) + + #define R8B_EXITDTOR + +#endif // defined( __clang__ ) + +/** + * @brief The "r8brain-free-src" library namespace. + * + * The "r8brain-free-src" sample rate converter library namespace. + */ + +namespace r8b { + +using std :: memcpy; +using std :: memset; +using std :: floor; +using std :: ceil; +using std :: fabs; +using std :: sqrt; +using std :: log; +using std :: exp; +using std :: pow; +using std :: sin; +using std :: cos; +using std :: tan; +using std :: atan; +using std :: atan2; +using std :: tanh; +using std :: cosh; +using std :: size_t; + +#if __cplusplus >= 201103L + + using std :: uintptr_t; + +#endif // __cplusplus >= 201103L + +R8B_CONST double R8B_PI = 3.14159265358979324; ///< Equals `pi`. +R8B_CONST double R8B_2PI = 6.28318530717958648; ///< Equals `2*pi`. +R8B_CONST double R8B_3PI = 9.42477796076937972; ///< Equals `3*pi`. +R8B_CONST double R8B_PId2 = 1.57079632679489662; ///< Equals `0.5*pi`. + +/** + * @def R8BNOCTOR( ClassName ) + * @brief Macro that defines empty copy-constructor and copy operator. + * + * This macro should be used in classes that cannot be copied in a standard + * C++ way. It is also assumed that objects of such classes are + * non-relocatable. + * + * This macro does not need to be defined in classes derived from a class + * where such macro was already used. + * + * @param ClassName The name of the class which uses this macro. + */ + +#if __cplusplus >= 201103L + + #define R8BNOCTOR( ClassName ) \ + public: \ + ClassName( const ClassName& ) = delete; \ + ClassName& operator = ( const ClassName& ) = delete; + +#else // __cplusplus >= 201103L + + #define R8BNOCTOR( ClassName ) \ + private: \ + ClassName( const ClassName& ) { } \ + ClassName& operator = ( const ClassName& ) { return( *this ); } + +#endif // __cplusplus >= 201103L + +/** + * @brief The default base class for objects created on heap. + */ + +class CStdClassAllocator +{ +}; + +/** + * @brief The default base class for objects that allocate blocks of memory. + * + * Memory buffer allocator that uses standard C++ "new" to allocate memory. + */ + +class CStdMemAllocator : public CStdClassAllocator +{ +public: + /** + * @brief Allocates a memory block. + * + * @param Size The size of the block, in bytes. + * @return The pointer to the allocated block. + */ + + static void* allocmem( const size_t Size ) + { + return( new char[ Size ]); + } + + /** + * @brief Frees a previously allocated memory block. + * + * @param p Pointer to the allocated block, can be `nullptr`. + */ + + static void freemem( void* const p ) + { + delete[] (char*) p; + } +}; + +/** + * @brief Forces the provided `ptr` pointer to be aligned to `align` bytes. + * + * Works with power-of-2 alignments only. + * + * @param ptr Pointer to align. + * @param align Alignment, in bytes, power-of-2. + * @tparam T Pointer's element type. + * @return Aligned pointer. + */ + +template< typename T > +inline T* align_ptr( T* const ptr, const uintptr_t align ) +{ + return( (T*) (( (uintptr_t) ptr + align - 1 ) & ~( align - 1 ))); +} + +/** + * @brief Performs placement `new` to turn a block of unoccupied memory into + * a "constructed" array of elements of type `T`. + * + * @param ptr Pointer for placement of elements. + * @param c The number of elements to "construct". + * @tparam T Required element type. + * @return The pointer to a formally-constructed array of elements. + */ + +template< typename T > +inline T* construct_ptr( void* const ptr, const size_t c ) +{ + return( :: new( ptr ) T[ c ]); +} + +/** + * @brief Templated memory buffer class for element buffers of fixed capacity. + * + * Fixed memory buffer object. Supports allocation of a fixed amount of + * memory. Does not store buffer's capacity - the user should know the actual + * capacity of the buffer. Does not feature "internal" storage, memory is + * always allocated via the `R8B_MEMALLOCCLASS` class's functions. Thus the + * object of this class can be moved in memory. + * + * This class manages memory space only - it does not perform element class + * construction nor destruction operations. + * + * This class applies 64-byte memory address alignment to the allocated data + * block. + * + * @tparam T The type of the stored elements (e.g., `double`). + */ + +template< typename T > +class CFixedBuffer : protected R8B_MEMALLOCCLASS +{ + R8BNOCTOR( CFixedBuffer ) + +public: + CFixedBuffer() + : Data0( R8B_NULL ) + , Data( R8B_NULL ) + { + } + + /** + * @brief Constructor allocates memory so that the specified number of + * elements of type T can be stored in *this* buffer object. + * + * @param Capacity Storage for this number of elements to allocate. + */ + + CFixedBuffer( const int Capacity ) + { + R8BASSERT( Capacity >= 0 ); + + Data0 = allocmem( (size_t) Capacity * sizeof( T ) + Alignment ); + Data = construct_ptr< T >( align_ptr( Data0, Alignment ), + (size_t) Capacity ); + + R8BASSERT( Data0 != R8B_NULL || Capacity == 0 ); + } + + ~CFixedBuffer() + { + freemem( Data0 ); + } + + /** + * @brief Allocates memory so that the specified number of elements of + * type T can be stored in *this* buffer object. + * + * @param Capacity Storage for this number of elements to allocate. + */ + + void alloc( const int Capacity ) + { + R8BASSERT( Capacity >= 0 ); + + freemem( Data0 ); + Data0 = allocmem( (size_t) Capacity * sizeof( T ) + Alignment ); + Data = construct_ptr< T >( align_ptr( Data0, Alignment ), + (size_t) Capacity ); + + R8BASSERT( Data0 != R8B_NULL || Capacity == 0 ); + } + + /** + * @brief Reallocates memory so that the specified number of elements of + * type T can be stored in *this* buffer object. Previously allocated data + * is copied to the new memory buffer. + * + * @param PrevCapacity Previous capacity of *this* buffer. + * @param NewCapacity Storage for this number of elements to allocate. + */ + + void realloc( const int PrevCapacity, const int NewCapacity ) + { + R8BASSERT( PrevCapacity >= 0 ); + R8BASSERT( NewCapacity >= 0 ); + + void* const NewData0 = allocmem( (size_t) NewCapacity * sizeof( T ) + + Alignment ); + + T* const NewData = construct_ptr< T >( align_ptr( NewData0, + Alignment ), (size_t) NewCapacity ); + + const size_t CopySize = ( PrevCapacity > NewCapacity ? + (size_t) NewCapacity : (size_t) PrevCapacity ) * sizeof( T ); + + if( CopySize > 0 ) + { + memcpy( NewData, Data, CopySize ); + } + + freemem( Data0 ); + Data0 = NewData0; + Data = NewData; + + R8BASSERT( Data0 != R8B_NULL || NewCapacity == 0 ); + } + + /** + * @brief Moves buffer from another object to *this* object. + * + * @param s Source buffer. + */ + + void moveFrom( CFixedBuffer& s ) + { + freemem( Data0 ); + Data0 = s.Data0; + Data = s.Data; + s.Data0 = R8B_NULL; + s.Data = R8B_NULL; + } + + /** + * @brief Deallocates a previously allocated buffer. + */ + + void free() + { + freemem( Data0 ); + Data0 = R8B_NULL; + Data = R8B_NULL; + } + + /** + * @brief Returns pointer to the first element of the allocated buffer, + * `nullptr`, if not allocated. + */ + + operator T* () const + { + return( Data ); + } + +private: + static const size_t Alignment = 64; ///< Buffer address alignment, in + ///< bytes. + void* Data0; ///< Buffer pointer, original unaligned. + T* Data; ///< Element buffer pointer, aligned. +}; + +/** + * @brief Pointer-to-object "keeper" class with automatic deletion. + * + * An auxiliary class that can be used for keeping a pointer to object that + * should be deleted together with the "keeper" by calling object's "delete" + * operator. + * + * @tparam T Type of the keeped object, must not include an additional + * asterisk. + */ + +template< typename T > +class CPtrKeeper +{ + R8BNOCTOR( CPtrKeeper ) + +public: + CPtrKeeper() + : Object( R8B_NULL ) + { + } + + /** + * @brief Constructor assigns a pointer to object to *this* keeper. + * + * @param aObject Pointer to object to keep, can be `nullptr`. + * @tparam T2 Object's pointer type. + */ + + template< typename T2 > + CPtrKeeper( T2 const aObject ) + : Object( aObject ) + { + } + + ~CPtrKeeper() + { + delete Object; + } + + /** + * @brief Assigns a pointer to object to *this* keeper. A previously + * keeped pointer will be reset and object deleted. + * + * @param aObject Pointer to object to keep, can be `nullptr`. + * @tparam T2 Object's pointer type. + */ + + template< typename T2 > + void operator = ( T2 const aObject ) + { + reset(); + Object = aObject; + } + + /** + * @brief Returns pointer to keeped object, or `nullptr`, if no object is + * being kept. + */ + + T* operator -> () const + { + return( Object ); + } + + /** + * @brief Returns pointer to keeped object, or `nullptr`, if no object is + * kept. + */ + + operator T* () const + { + return( Object ); + } + + /** + * @brief Resets the keeped pointer and deletes the keeped object. + */ + + void reset() + { + T* const DelObj = Object; + Object = R8B_NULL; + delete DelObj; + } + + /** + * @brief Returns the keeped pointer and resets it in *this* keeper + * without object deletion. + */ + + T* unkeep() + { + T* const ResObject = Object; + Object = R8B_NULL; + return( ResObject ); + } + +private: + T* Object; ///< Pointer to keeped object. +}; + +/** + * @brief Reference "keeper" class with automatic unref() call. + * + * An auxiliary class that can be used for keeping a reference to object that + * should be unreferenced when the "keeper" is deleted. + * + * @tparam T Type of the referenced object, must have the unref() function. + */ + +template< typename T > +class CRefKeeper +{ + R8BNOCTOR( CRefKeeper ) + +public: + CRefKeeper() + : Object( R8B_NULL ) + { + } + + /** + * @brief Constructor assigns a pointer to object to *this* keeper. + * + * @param aObject Pointer to object to keep, can be `nullptr`. + * @tparam T2 Object's pointer type. + */ + + template< typename T2 > + CRefKeeper( T2 const aObject ) + : Object( aObject ) + { + } + + ~CRefKeeper() + { + if( Object != R8B_NULL ) + { + Object -> unref(); + } + } + + /** + * @brief Assigns a pointer to object to *this* keeper. A previously + * keeped pointer will be reset and object unreferenced. + * + * @param aObject Pointer to object to keep; it can be `nullptr`. + * @tparam T2 Object's pointer type. + */ + + template< typename T2 > + void operator = ( T2 const aObject ) + { + reset(); + Object = aObject; + } + + /** + * @brief Returns pointer to keeped object, or `nullptr`, if no object is + * being kept. + */ + + T* operator -> () const + { + return( Object ); + } + + /** + * @brief Returns pointer to keeped object, or `nullptr`, if no object is + * kept. + */ + + operator T* () const + { + return( Object ); + } + + /** + * @brief Resets the keeped pointer and unreferences the keeped object. + */ + + void reset() + { + if( Object != R8B_NULL ) + { + T* const RefObj = Object; + Object = R8B_NULL; + RefObj -> unref(); + } + } + +private: + T* Object; ///< Pointer to keeped object. +}; + +/** + * @brief Class for items of a singly-linked list. + * + * Implements automatic deletion of linked objects. + * + * @tparam T Name of the list item class. + */ + +template< class T > +class CSinglyLinkedListItem +{ +public: + T* Next; ///< Next object of type `T` in a singly-linked list. + +protected: + CSinglyLinkedListItem() + : Next( R8B_NULL ) + { + } + + ~CSinglyLinkedListItem() + { + while( Next != R8B_NULL ) + { + T* const nn = Next -> Next; + Next -> Next = R8B_NULL; + delete Next; + Next = nn; + } + } +}; + +#if __cplusplus >= 201103L + +typedef std :: mutex CSyncObject; ///< Mutex class. +typedef std :: lock_guard< std :: mutex > CSyncKeeper; ///< Mutex keeper class. + +#else // __cplusplus >= 201103L + +/** + * @brief Multi-threaded synchronization object class. + * + * This class uses standard OS thread-locking (mutex) mechanism which is + * fairly efficient in most cases. + * + * The acquire() function is re-enterant, and can be called recursively, in + * the same thread, for this kind of thread-locking mechanism. This will not + * produce a dead-lock. + */ + +class CSyncObject +{ + R8BNOCTOR( CSyncObject ) + +public: + CSyncObject() + { + #if defined( _WIN32 ) + InitializeCriticalSectionAndSpinCount( &CritSec, 2000 ); + #else // defined( _WIN32 ) + pthread_mutexattr_t MutexAttrs; + pthread_mutexattr_init( &MutexAttrs ); + pthread_mutexattr_settype( &MutexAttrs, PTHREAD_MUTEX_RECURSIVE ); + pthread_mutex_init( &Mutex, &MutexAttrs ); + pthread_mutexattr_destroy( &MutexAttrs ); + #endif // defined( _WIN32 ) + } + + ~CSyncObject() + { + #if defined( _WIN32 ) + DeleteCriticalSection( &CritSec ); + #else // defined( _WIN32 ) + pthread_mutex_destroy( &Mutex ); + #endif // defined( _WIN32 ) + } + + /** + * @brief Acquires *this* thread synchronizer object immediately or + * waits until another thread releases it. + */ + + void acquire() + { + #if defined( _WIN32 ) + EnterCriticalSection( &CritSec ); + #else // defined( _WIN32 ) + pthread_mutex_lock( &Mutex ); + #endif // defined( _WIN32 ) + } + + /** + * @brief Releases *this*, previously acquired, thread synchronizer + * object. + */ + + void release() + { + #if defined( _WIN32 ) + LeaveCriticalSection( &CritSec ); + #else // defined( _WIN32 ) + pthread_mutex_unlock( &Mutex ); + #endif // defined( _WIN32 ) + } + +private: + #if defined( _WIN32 ) + CRITICAL_SECTION CritSec; ///< Standard Windows critical section + ///< structure. + #else // defined( _WIN32 ) + pthread_mutex_t Mutex; ///< pthread.h mutex object. + #endif // defined( _WIN32 ) +}; + +/** + * @brief A "keeper" class for CSyncObject-based synchronization. + * + * Sync keeper class. The object of this class can be used as auto-init and + * auto-deinit object for calling the acquire() and release() functions of an + * object of the CSyncObject class. This "keeper" object is best used in + * functions as an "automatic" object allocated on the stack, possibly via the + * R8BSYNC() macro. + */ + +class CSyncKeeper +{ + R8BNOCTOR( CSyncKeeper ) + +public: + CSyncKeeper() + : SyncObj( R8B_NULL ) + { + } + + /** + * @brief Constructor acquires a specified synchronization object. + * + * @param aSyncObj Pointer to the sync object which should be used for + * sync'ing, can be `nullptr`. + */ + + CSyncKeeper( CSyncObject* const aSyncObj ) + : SyncObj( aSyncObj ) + { + if( SyncObj != R8B_NULL ) + { + SyncObj -> acquire(); + } + } + + /** + * @brief Constructor acquires a specified synchronization object. + * + * @param aSyncObj Reference to the sync object which should be used for + * sync'ing. + */ + + CSyncKeeper( CSyncObject& aSyncObj ) + : SyncObj( &aSyncObj ) + { + SyncObj -> acquire(); + } + + ~CSyncKeeper() + { + if( SyncObj != R8B_NULL ) + { + SyncObj -> release(); + } + } + +private: + CSyncObject* SyncObj; ///< Sync object in use (can be `nullptr`). +}; + +#endif // __cplusplus >= 201103L + +/** + * @brief Thread synchronization macro. + * + * The R8BSYNC() macro, which creates and object of the r8b::CSyncKeeper class + * on stack, should be put before sections of the code that may potentially + * change data asynchronously with other threads at the same time. + * The R8BSYNC() macro "acquires" the synchronization object thus blocking + * execution of other threads that also use the same R8BSYNC() macro. + * The blocked section begins with the R8BSYNC() macro and finishes at the end + * of the current C++ code block. R8BSYNC() is re-enterant within the same + * thread. + * + * @param SyncObject An object of the CSyncObject type that is used for + * synchronization. + */ + +#define R8BSYNC( SyncObject ) R8BSYNC1( SyncObject, __LINE__ ) +#define R8BSYNC1( SyncObject, id ) R8BSYNC2( SyncObject, id ) ///< Aux macro. +#define R8BSYNC2( SyncObject, id ) \ + const CSyncKeeper SyncKeeper##id( SyncObject ) ///< Aux macro. + +/** + * @brief Sine signal generator class. + * + * Class implements sine signal generator without biasing. + */ + +class CSineGen +{ +public: + CSineGen() + { + } + + /** + * @brief Constructor initializes *this* sine signal generator, with unity + * gain output. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add R8B_PId2 for cosine function. + */ + + CSineGen( const double si, const double ph ) + : svalue1( sin( ph )) + , svalue2( sin( ph - si )) + , sincr( 2.0 * cos( si )) + { + } + + /** + * @brief Constructor initializes *this* sine signal generator. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add R8B_PId2 for cosine function. + * @param g The overall gain factor, 1.0 for unity gain (-1.0 to 1.0 + * amplitude). + */ + + CSineGen( const double si, const double ph, const double g ) + : svalue1( sin( ph ) * g ) + , svalue2( sin( ph - si ) * g ) + , sincr( 2.0 * cos( si )) + { + } + + /** + * @brief Function initializes *this* sine signal generator, with unity + * gain output. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add R8B_PId2 for cosine function. + */ + + void init( const double si, const double ph ) + { + svalue1 = sin( ph ); + svalue2 = sin( ph - si ); + sincr = 2.0 * cos( si ); + } + + /** + * @brief Function initializes *this* sine signal generator. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add R8B_PId2 for cosine function. + * @param g The overall gain factor, 1.0 for unity gain (-1.0 to 1.0 + * amplitude). + */ + + void init( const double si, const double ph, const double g ) + { + svalue1 = sin( ph ) * g; + svalue2 = sin( ph - si ) * g; + sincr = 2.0 * cos( si ); + } + + /** + * @brief Generates the next sample. + * + * @return Next value of the sine function, without biasing. + */ + + double generate() + { + const double res = svalue1; + + svalue1 = sincr * res - svalue2; + svalue2 = res; + + return( res ); + } + +private: + double svalue1; ///< Current sine value. + double svalue2; ///< Previous sine value. + double sincr; ///< Sine value increment. +}; + +/** + * @brief Calculate the exact number of bits a value needs for representation. + * + * @param v Input value. + * @return Calculated bit occupancy of the specified input value. Bit + * occupancy means how many significant lower bits are necessary to store a + * specified value. Function treats the input value as unsigned. + */ + +inline int getBitOccupancy( const int v ) +{ + static const unsigned char OccupancyTable[] = + { + 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 + }; + + const int tt = v >> 16; + + if( tt != 0 ) + { + const int t = v >> 24; + + return( t != 0 ? 24 + OccupancyTable[ t & 0xFF ] : + 16 + OccupancyTable[ tt ]); + } + else + { + const int t = v >> 8; + + return( t != 0 ? 8 + OccupancyTable[ t ] : OccupancyTable[ v ]); + } +} + +/** + * @brief FIR filter's frequency response calculation. + * + * Function calculates frequency response of the specified FIR filter at the + * specified circular frequency. Phase can be calculated as atan2( im, re ). + * + * @param flt FIR filter's coefficients. + * @param fltlen Number of coefficients (taps) in the filter. + * @param th Circular frequency [0; pi]. + * @param[out] re0 Resulting real part of the complex frequency response. + * @param[out] im0 Resulting imaginary part of the complex frequency response. + * @param fltlat Filter's latency, in samples. + */ + +inline void calcFIRFilterResponse( const double* flt, int fltlen, + const double th, double& re0, double& im0, const int fltlat = 0 ) +{ + const double sincr = 2.0 * cos( th ); + double cvalue1; + double svalue1; + + if( fltlat == 0 ) + { + cvalue1 = 1.0; + svalue1 = 0.0; + } + else + { + cvalue1 = cos( -fltlat * th ); + svalue1 = sin( -fltlat * th ); + } + + double cvalue2 = cos( -( fltlat + 1 ) * th ); + double svalue2 = sin( -( fltlat + 1 ) * th ); + + double re = 0.0; + double im = 0.0; + + while( fltlen > 0 ) + { + re += cvalue1 * flt[ 0 ]; + im += svalue1 * flt[ 0 ]; + flt++; + fltlen--; + + double tmp = cvalue1; + cvalue1 = sincr * cvalue1 - cvalue2; + cvalue2 = tmp; + + tmp = svalue1; + svalue1 = sincr * svalue1 - svalue2; + svalue2 = tmp; + } + + re0 = re; + im0 = im; +} + +/** + * @brief FIR filter's group delay calculation function. + * + * Function calculates group delay of the specified FIR filter at the + * specified circular frequency. The group delay is calculated by evaluating + * the filter's response at close side-band frequencies of "th". + * + * @param flt FIR filter's coefficients. + * @param fltlen Number of coefficients (taps) in the filter. + * @param th Circular frequency [0; pi]. + * @return Resulting group delay at the specified frequency, in samples. + */ + +inline double calcFIRFilterGroupDelay( const double* const flt, + const int fltlen, const double th ) +{ + const int Count = 2; + const double thd2 = 1e-9; + double ths[ Count ] = { th - thd2, th + thd2 }; // Side-band frequencies. + + if( ths[ 0 ] < 0.0 ) + { + ths[ 0 ] = 0.0; + } + + if( ths[ 1 ] > R8B_PI ) + { + ths[ 1 ] = R8B_PI; + } + + double ph1[ Count ]; + int i; + + for( i = 0; i < Count; i++ ) + { + double re1; + double im1; + + calcFIRFilterResponse( flt, fltlen, ths[ i ], re1, im1 ); + ph1[ i ] = atan2( im1, re1 ); + } + + if( fabs( ph1[ 1 ] - ph1[ 0 ]) > R8B_PI ) + { + if( ph1[ 1 ] > ph1[ 0 ]) + { + ph1[ 1 ] -= R8B_2PI; + } + else + { + ph1[ 1 ] += R8B_2PI; + } + } + + const double thd = ths[ 1 ] - ths[ 0 ]; + + return(( ph1[ 1 ] - ph1[ 0 ]) / thd ); +} + +/** + * @brief FIR filter's gain normalization. + * + * Function normalizes FIR filter so that its frequency response at DC is + * equal to DCGain. + * + * @param[in,out] p Filter coefficients. + * @param l Filter length. + * @param DCGain Filter's gain at DC (linear, non-decibel value). + * @param pstep "p" array step. + */ + +inline void normalizeFIRFilter( double* const p, const int l, + const double DCGain, const int pstep = 1 ) +{ + R8BASSERT( l > 0 ); + R8BASSERT( pstep != 0 ); + + double s = 0.0; + double* pp = p; + int i = l; + + while( i > 0 ) + { + s += *pp; + pp += pstep; + i--; + } + + s = DCGain / s; + pp = p; + i = l; + + while( i > 0 ) + { + *pp *= s; + pp += pstep; + i--; + } +} + +/** + * @brief Calculates 3rd order spline coefficients, using 8 points. + * + * Function calculates coefficients used to calculate 3rd order spline + * (polynomial) on the equidistant lattice, using 8 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param xm3 Point at x-3 position. + * @param xm2 Point at x-2 position. + * @param xm1 Point at x-1 position. + * @param x0 Point at x position. + * @param x1 Point at x+1 position. + * @param x2 Point at x+2 position. + * @param x3 Point at x+3 position. + * @param x4 Point at x+4 position. + */ + +inline void calcSpline3p8Coeffs( double* const c, const double xm3, + const double xm2, const double xm1, const double x0, const double x1, + const double x2, const double x3, const double x4 ) +{ + c[ 0 ] = x0; + c[ 1 ] = ( 61.0 * ( x1 - xm1 ) + 16.0 * ( xm2 - x2 ) + + 3.0 * ( x3 - xm3 )) * 1.31578947368421052e-2; + + c[ 2 ] = ( 106.0 * ( xm1 + x1 ) + 10.0 * x3 + 6.0 * xm3 - 3.0 * x4 - + 29.0 * ( xm2 + x2 ) - 167.0 * x0 ) * 1.31578947368421052e-2; + + c[ 3 ] = ( 91.0 * ( x0 - x1 ) + 45.0 * ( x2 - xm1 ) + + 13.0 * ( xm2 - x3 ) + 3.0 * ( x4 - xm3 )) * 1.31578947368421052e-2; +} + +/** + * @brief Calculates 2nd order spline coefficients, using 8 points. + * + * Function calculates coefficients used to calculate 2nd order spline + * (polynomial) on the equidistant lattice, using 8 points. This function is + * based on the calcSpline3p8Coeffs() function, but without the 3rd order + * coefficient. + * + * @param[out] c Output coefficients buffer, length = 3. + * @param xm3 Point at x-3 position. + * @param xm2 Point at x-2 position. + * @param xm1 Point at x-1 position. + * @param x0 Point at x position. + * @param x1 Point at x+1 position. + * @param x2 Point at x+2 position. + * @param x3 Point at x+3 position. + * @param x4 Point at x+4 position. + */ + +inline void calcSpline2p8Coeffs( double* const c, const double xm3, + const double xm2, const double xm1, const double x0, const double x1, + const double x2, const double x3, const double x4 ) +{ + c[ 0 ] = x0; + c[ 1 ] = ( 61.0 * ( x1 - xm1 ) + 16.0 * ( xm2 - x2 ) + + 3.0 * ( x3 - xm3 )) * 1.31578947368421052e-2; + + c[ 2 ] = ( 106.0 * ( xm1 + x1 ) + 10.0 * x3 + 6.0 * xm3 - 3.0 * x4 - + 29.0 * ( xm2 + x2 ) - 167.0 * x0 ) * 1.31578947368421052e-2; +} + +/** + * @brief Calculates 3rd order spline coefficients, using 4 points. + * + * Function calculates coefficients used to calculate 3rd order segment + * interpolation polynomial on the equidistant lattice, using 4 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param[in] y Equidistant point values. Value at offset 1 corresponds to + * x=0 point. + */ + +inline void calcSpline3p4Coeffs( double* const c, const double* const y ) +{ + c[ 0 ] = y[ 1 ]; + c[ 1 ] = 0.5 * ( y[ 2 ] - y[ 0 ]); + c[ 2 ] = y[ 0 ] - 2.5 * y[ 1 ] + y[ 2 ] + y[ 2 ] - 0.5 * y[ 3 ]; + c[ 3 ] = 0.5 * ( y[ 3 ] - y[ 0 ] ) + 1.5 * ( y[ 1 ] - y[ 2 ]); +} + +/** + * @brief Calculates 3rd order spline coefficients, using 6 points. + * + * Function calculates coefficients used to calculate 3rd order segment + * interpolation polynomial on the equidistant lattice, using 6 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param[in] y Equidistant point values. Value at offset 2 corresponds to + * x=0 point. + */ + +inline void calcSpline3p6Coeffs( double* const c, const double* const y ) +{ + c[ 0 ] = y[ 2 ]; + c[ 1 ] = ( 11.0 * ( y[ 3 ] - y[ 1 ]) + 2.0 * ( y[ 0 ] - y[ 4 ])) / 14.0; + c[ 2 ] = ( 20.0 * ( y[ 1 ] + y[ 3 ]) + 2.0 * y[ 5 ] - 4.0 * y[ 0 ] - + 7.0 * y[ 4 ] - 31.0 * y[ 2 ]) / 14.0; + + c[ 3 ] = ( 17.0 * ( y[ 2 ] - y[ 3 ]) + 9.0 * ( y[ 4 ] - y[ 1 ]) + + 2.0 * ( y[ 0 ] - y[ 5 ])) / 14.0; +} + +#if !defined( min ) + +/** + * @brief Returns minimum of two values. + * + * @param v1 Value 1. + * @param v2 Value 2. + * @tparam T Values' type. + * @return The minimum of 2 values. + */ + +template< typename T > +inline T min( const T& v1, const T& v2 ) +{ + return( v1 < v2 ? v1 : v2 ); +} + +#endif // !defined( min ) + +#if !defined( max ) + +/** + * @brief Returns maximum of two values. + * + * @param v1 Value 1. + * @param v2 Value 2. + * @tparam T Values' type. + * @return The maximum of 2 values. + */ + +template< typename T > +inline T max( const T& v1, const T& v2 ) +{ + return( v1 > v2 ? v1 : v2 ); +} + +#endif // !defined( max ) + +/** + * @brief Clamps a value to be within the specified min-max range. + * + * Function "clamps" (clips) the specified value so that it is not lesser than + * "minv", and not greater than "maxv". + * + * @param Value Value to clamp. + * @param minv Minimal allowed value. + * @param maxv Maximal allowed value. + * @return "Clamped" value. + */ + +inline double clampr( const double Value, const double minv, + const double maxv ) +{ + if( Value < minv ) + { + return( minv ); + } + + if( Value > maxv ) + { + return( maxv ); + } + + return( Value ); +} + +/** + * @brief Returns square ot a value. + * + * @param x Value to square. + * @return Squared value of the argument. + */ + +inline double sqr( const double x ) +{ + return( x * x ); +} + +/** + * @brief Power of an absolute value. + * + * @param v Input value. + * @param p Power factor. + * @return Returns a precise, but generally approximate pow() function's value + * of absolute of input value. + */ + +inline double pow_a( const double v, const double p ) +{ + return( exp( p * log( fabs( v ) + 1e-300 ))); +} + +/** + * @brief Single-argument Gaussian function of a value. + * + * @param v Input value. + * @return Calculated single-argument Gaussian function of the input value. + */ + +inline double gauss( const double v ) +{ + return( exp( -( v * v ))); +} + +/** + * @brief Hyperbolic sine of a value. + * + * @param v Input value. + * @return Calculated inverse hyperbolic sine of the input value. + */ + +inline double asinh( const double v ) +{ + return( log( v + sqrt( v * v + 1.0 ))); +} + +/** + * @brief 1st kind, 0th order modified Bessel function of a value. + * + * @param x Input value. + * @return Calculated zero-th order modified Bessel function of the first kind + * of the input value. Approximate value. Coefficients by Abramowitz and + * Stegun. + */ + +inline double besselI0( const double x ) +{ + const double ax = fabs( x ); + double y; + + if( ax < 3.75 ) + { + y = x / 3.75; + y *= y; + + return( 1.0 + y * ( 3.5156229 + y * ( 3.0899424 + y * ( 1.2067492 + + y * ( 0.2659732 + y * ( 0.360768e-1 + y * 0.45813e-2 )))))); + } + + y = 3.75 / ax; + + return( exp( ax ) / sqrt( ax ) * ( 0.39894228 + y * ( 0.1328592e-1 + + y * ( 0.225319e-2 + y * ( -0.157565e-2 + y * ( 0.916281e-2 + + y * ( -0.2057706e-1 + y * ( 0.2635537e-1 + y * ( -0.1647633e-1 + + y * 0.392377e-2 ))))))))); +} + +} // namespace r8b + +#endif // R8BBASE_INCLUDED diff --git a/native/vendor/r8brain/r8bconf.h b/native/vendor/r8brain/r8bconf.h new file mode 100644 index 0000000..082fe3d --- /dev/null +++ b/native/vendor/r8brain/r8bconf.h @@ -0,0 +1,200 @@ +//$ nobt +//$ nocpp + +/** + * @file r8bconf.h + * + * @brief The "configuration" inclusion file you can modify. + * + * This is the "configuration" inclusion file for the "r8brain-free-src" + * sample rate converter. You may redefine the macros here as you see fit. + * + * r8brain-free-src Copyright (c) 2013-2026 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8BCONF_INCLUDED +#define R8BCONF_INCLUDED + +#if !defined( R8BASSERT ) + /** + * @brief Assertion macro used to check for certain run-time conditions. + * By default, no action is taken if assertion fails. + * + * @param e Expression to check. + */ + + #define R8BASSERT( e ) (void) 0 +#endif // !defined( R8BASSERT ) + +#if !defined( R8BCONSOLE ) + /** + * @brief Console output macro, used to output various resampler status + * strings, including filter design parameters, convolver parameters. + * + * @param ... Expression sent to the console. Usually consists of a + * standard printf() format string followed by several parameters + * (`__VA_ARGS__`). + */ + + #define R8BCONSOLE( ... ) (void) 0 +#endif // !defined( R8BCONSOLE ) + +#if !defined( R8B_BASECLASS ) + /** + * @brief Macro defines the name of the class from which all classes that + * are designed to be created on heap are derived. + * + * The default r8b::CStdClassAllocator class uses "stdlib" memory + * allocation functions. + * + * The classes that are best placed on stack or used as class members are + * not derived from any class. + */ + + #define R8B_BASECLASS :: r8b :: CStdClassAllocator +#endif // !defined( R8B_BASECLASS ) + +#if !defined( R8B_MEMALLOCCLASS ) + /** + * @brief Macro defines the name of the class that implements raw memory + * allocation functions, see the r8b::CStdMemAllocator class for details. + */ + + #define R8B_MEMALLOCCLASS :: r8b :: CStdMemAllocator +#endif // !defined( R8B_MEMALLOCCLASS ) + +#if !defined( R8B_DSPBASECLASS ) + /** + * @brief Macro defines the name of the class from which all CDSPProcessor + * objects are derived. + * + * The default value is R8B_BASECLASS. + * + * These objects are dynamically allocated, but are not cached in the + * global static variables. + */ + + #define R8B_DSPBASECLASS R8B_BASECLASS +#endif // !defined( R8B_DSPBASECLASS ) + +#if !defined( R8B_FILTER_CACHE_MAX ) + /** + * @brief Macro specifies the number of filters kept in the cache at most. + * + * The actual number can be higher, if many different filters are in use + * at the same time. + */ + + #define R8B_FILTER_CACHE_MAX 96 +#endif // !defined( R8B_FILTER_CACHE_MAX ) + +#if !defined( R8B_FRACBANK_CACHE_MAX ) + /** + * @brief Macro specifies the number of whole-number stepping fractional + * delay filter banks kept in the cache at most. + * + * The actual number can be higher, if many different filter banks are in + * use at the same time. As filter banks are usually big objects, it is + * advisable to keep this cache size small. + */ + + #define R8B_FRACBANK_CACHE_MAX 12 +#endif // !defined( R8B_FRACBANK_CACHE_MAX ) + +#if !defined( R8B_FLTTEST ) + /** + * @brief This macro, when equal to 1, enables fractional delay filter + * bank testing. + * + * In this mode, the filter bank becomes a dynamic member of the + * CDSPFracInterpolator object instead of being a global static object. + */ + + #define R8B_FLTTEST 0 +#endif // !defined( R8B_FLTTEST ) + +#if !defined( R8B_EXTFFT ) + /** + * @brief This macro, when equal to 1, extends length of low-pass filters' + * FFT block by a factor of 2, by zero-padding it. + * + * This usually improves the overall time performance of the resampler at + * the expense of a higher overall latency (initial processing delay). + * If such delay is not an issue, setting this macro to 1 is preferrable. + * This macro can only have a value of 0 or 1. + */ + + #define R8B_EXTFFT 0 +#endif // !defined( R8B_EXTFFT ) + +#if !defined( R8B_IPP ) + /** + * @brief Set this macro definition to 1 to enable the use of Intel IPP's + * fast Fourier transform functions. Also uncomment and correct the IPP + * header inclusion macros. + * + * Do not forget to call the ippInit() function at the start of the + * application, before using this library's functions. + */ + + #define R8B_IPP 0 + +// #include +// #include +#endif // !defined( R8B_IPP ) + +#if !defined( R8B_PFFFT_DOUBLE ) + /** + * @brief When defined as 1, enables PFFFT "double" routines which are + * fast, and which provide the highest precision. + */ + + #define R8B_PFFFT_DOUBLE 0 +#endif // !defined( R8B_PFFFT_DOUBLE ) + +#if !defined( R8B_PFFFT ) + /** + * @brief When defined as 1, enables PFFFT routines which are fast, but + * which are limited to 24-bit precision. + * + * May be a good choice for time-series interpolation, when stop-band + * attenuation higher than 120 dB is not required. + */ + + #define R8B_PFFFT 0 +#else // !defined( R8B_PFFFT ) + #if R8B_PFFFT && R8B_PFFFT_DOUBLE + // Handle the case when both `R8B_PFFFT` and `R8B_PFFFT_DOUBLE` were + // enabled together by mistake. + + #error R8B_PFFFT and R8B_PFFFT_DOUBLE collision. + #endif // R8B_PFFFT && R8B_PFFFT_DOUBLE +#endif // !defined( R8B_PFFFT ) + +#if defined( R8B_OOURA ) + #error R8B_OOURA should not be pre-defined. +#endif // defined( R8B_OOURA ) + +#if !R8B_IPP && !R8B_PFFFT && !R8B_PFFFT_DOUBLE + /** + * @brief This macro is set to 1 if the default Ooura FFT is in use. + * + * This macro should not be defined externally. + */ + + #define R8B_OOURA 1 +#else // Non-Ooura FFT + #define R8B_OOURA 0 +#endif // Non-Ooura FFT + +#if R8B_FILTER_CACHE_MAX < 2 + #error R8B_FILTER_CACHE_MAX must be greater than 1. +#endif // R8B_FILTER_CACHE_MAX < 2 + +#if R8B_FRACBANK_CACHE_MAX < 2 + #error R8B_FRACBANK_CACHE_MAX must be greater than 1. +#endif // R8B_FRACBANK_CACHE_MAX < 2 + +#endif // R8BCONF_INCLUDED diff --git a/native/vendor/r8brain/r8butil.h b/native/vendor/r8brain/r8butil.h new file mode 100644 index 0000000..f5348c7 --- /dev/null +++ b/native/vendor/r8brain/r8butil.h @@ -0,0 +1,315 @@ +//$ nobt +//$ nocpp + +/** + * @file r8butil.h + * + * @brief The inclusion file with several utility functions. + * + * This file includes several utility functions used by various utility + * programs like "calcErrorTable.cpp". + * + * r8brain-free-src Copyright (c) 2013-2025 Aleksey Vaneev + * + * See the "LICENSE" file for license. + */ + +#ifndef R8BUTIL_INCLUDED +#define R8BUTIL_INCLUDED + +#include "r8bbase.h" + +namespace r8b { + +/** + * @brief Converts complex response into magnitude in log scale. + * + * @param re Real part of the frequency response. + * @param im Imaginary part of the frequency response. + * @return A magnitude response value converted from the linear scale to the + * logarithmic scale. + */ + +inline double convertResponseToLog( const double re, const double im ) +{ + return( 4.34294481903251828 * log( re * re + im * im + 1e-100 )); +} + +/** + * @brief An utility function that performs frequency response scanning step + * update based on the current magnitude response's slope. + * + * @param[in,out] step The current scanning step. Will be updated on + * function's return. Must be a positive value. + * @param curg Squared magnitude response at the current frequency point. + * @param[in,out] prevg_log Previous magnitude response, log scale. Will be + * updated on function's return. + * @param prec Precision multiplier, affects the size of the step. + * @param maxstep The maximal allowed step. + * @param minstep The minimal allowed step. + */ + +inline void updateScanStep( double& step, const double curg, + double& prevg_log, const double prec, const double maxstep, + const double minstep = 1e-11 ) +{ + double curg_log = 4.34294481903251828 * log( curg + 1e-100 ); + curg_log += ( prevg_log - curg_log ) * 0.7; + + const double slope = fabs( curg_log - prevg_log ); + prevg_log = curg_log; + + if( slope > 0.0 ) + { + step /= prec * slope; + step = max( min( step, maxstep ), minstep ); + } +} + +/** + * @brief Locates normalized frequency at which the minimum filter gain is + * reached. + * + * The scanning is performed from lower (left) to higher (right) frequencies, + * the whole range is scanned. + * + * Function expects that the magnitude response is always reducing from lower + * to high frequencies, starting at "minth". + * + * @param flt Filter response. + * @param fltlen Filter response's length in samples (taps). + * @param[out] ming The current minimal gain (squared). On function's return + * will contain the minimal gain value found (squared). + * @param[out] minth The normalized frequency where the minimal gain is + * currently at. On function's return will point to the normalized frequency + * where the new minimum was found. + * @param thend The ending frequency, inclusive. + */ + +inline void findFIRFilterResponseMinLtoR( const double* const flt, + const int fltlen, double& ming, double& minth, const double thend ) +{ + const double maxstep = minth * 2e-3; + double curth = minth; + double re; + double im; + calcFIRFilterResponse( flt, fltlen, R8B_PI * curth, re, im ); + double prevg_log = convertResponseToLog( re, im ); + double step = 1e-11; + + while( true ) + { + curth += step; + + if( curth > thend ) + { + break; + } + + calcFIRFilterResponse( flt, fltlen, R8B_PI * curth, re, im ); + const double curg = re * re + im * im; + + if( curg > ming ) + { + ming = curg; + minth = curth; + break; + } + + ming = curg; + minth = curth; + + updateScanStep( step, curg, prevg_log, 0.31, maxstep ); + } +} + +/** + * @brief Locates normalized frequency at which the maximal filter gain is + * reached. + * + * The scanning is performed from lower (left) to higher (right) frequencies, + * the whole range is scanned. + * + * Note: this function may "stall" in very rare cases if the magnitude + * response happens to be "saw-tooth" like, requiring a very small stepping to + * be used. If this happens, it may take dozens of seconds to complete. + * + * @param flt Filter response. + * @param fltlen Filter response's length in samples (taps). + * @param[out] maxg The current maximal gain (squared). On function's return + * will contain the maximal gain value (squared). + * @param[out] maxth The normalized frequency where the maximal gain is + * currently at. On function's return will point to the normalized frequency + * where the maximum was reached. + * @param thend The ending frequency, inclusive. + */ + +inline void findFIRFilterResponseMaxLtoR( const double* const flt, + const int fltlen, double& maxg, double& maxth, const double thend ) +{ + const double maxstep = maxth * 1e-4; + double premaxth = maxth; + double premaxg = maxg; + double postmaxth = maxth; + double postmaxg = maxg; + + double prevth = maxth; + double prevg = maxg; + double curth = maxth; + double re; + double im; + calcFIRFilterResponse( flt, fltlen, R8B_PI * curth, re, im ); + double prevg_log = convertResponseToLog( re, im ); + double step = 1e-11; + + bool WasPeak = false; + int AfterPeakCount = 0; + + while( true ) + { + curth += step; + + if( curth > thend ) + { + break; + } + + calcFIRFilterResponse( flt, fltlen, R8B_PI * curth, re, im ); + const double curg = re * re + im * im; + + if( curg > maxg ) + { + premaxth = prevth; + premaxg = prevg; + maxg = curg; + maxth = curth; + WasPeak = true; + AfterPeakCount = 0; + } + else + if( WasPeak ) + { + if( AfterPeakCount == 0 ) + { + postmaxth = curth; + postmaxg = curg; + } + + if( AfterPeakCount == 5 ) + { + // Perform 2 approximate binary searches. + + int k; + + for( k = 0; k < 2; k++ ) + { + double l = ( k == 0 ? premaxth : maxth ); + double curgl = ( k == 0 ? premaxg : maxg ); + double r = ( k == 0 ? maxth : postmaxth ); + double curgr = ( k == 0 ? maxg : postmaxg ); + + while( true ) + { + const double c = ( l + r ) * 0.5; + calcFIRFilterResponse( flt, fltlen, R8B_PI * c, + re, im ); + + const double curg = re * re + im * im; + + if( curgl > curgr ) + { + r = c; + curgr = curg; + } + else + { + l = c; + curgl = curg; + } + + if( r - l < 1e-11 ) + { + if( curgl > curgr ) + { + maxth = l; + maxg = curgl; + } + else + { + maxth = r; + maxg = curgr; + } + + break; + } + } + } + + break; + } + + AfterPeakCount++; + } + + prevth = curth; + prevg = curg; + + updateScanStep( step, curg, prevg_log, 1.0, maxstep ); + } +} + +/** + * @brief Locates normalized frequency at which the specified maximum filter + * gain is reached. + * + * The scanning is performed from higher (right) to lower (left) frequencies, + * scanning stops when the required gain value was crossed. Function uses an + * extremely efficient binary search and thus expects that the magnitude + * response has the "main lobe" form produced by windowing, with a minimal + * pass-band ripple. + * + * @param flt Filter response. + * @param fltlen Filter response's length in samples (taps). + * @param maxg Maximal gain (squared). + * @param[out] th The current normalized frequency. On function's return will + * point to the normalized frequency where "maxg" is reached. + * @param thend The leftmost frequency to scan, inclusive. + */ + +inline void findFIRFilterResponseLevelRtoL( const double* const flt, + const int fltlen, const double maxg, double& th, const double thend ) +{ + // Perform exact binary search. + + double l = thend; + double r = th; + + while( true ) + { + const double c = ( l + r ) * 0.5; + + if( r - l < 1e-14 ) + { + th = c; + break; + } + + double re; + double im; + calcFIRFilterResponse( flt, fltlen, R8B_PI * c, re, im ); + const double curg = re * re + im * im; + + if( curg > maxg ) + { + l = c; + } + else + { + r = c; + } + } +} + +} // namespace r8b + +#endif // R8BUTIL_INCLUDED diff --git a/package-lock.json b/package-lock.json index bd7b464..ef1820c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prism", - "version": "0.3.0-beta", + "version": "0.4.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prism", - "version": "0.3.0-beta", + "version": "0.4.0-beta", "hasInstallScript": true, "license": "GPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index be920d7..928648b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prism", - "version": "0.3.0-beta", + "version": "0.4.0-beta", "description": "Open-source audio metering and visualization tool", "main": "./out/main/index.js", "scripts": { @@ -12,12 +12,19 @@ "build:icons": "node scripts/build/generate-prism-icons.mjs", "build:clip-drag-icons": "node scripts/build/generate-audio-clip-drag-icons.mjs", "test": "node scripts/run-tests.mjs", + "test:linux": "node scripts/linux-test.mjs", + "test:linux-launcher": "node --test test/linux-test.test.mjs", "test:audio-router": "node scripts/run-audio-router-tests.mjs", "test:audio-clips": "node scripts/run-audio-clip-tests.mjs", "test:audio-store": "node scripts/run-audio-store-tests.mjs", "test:astra": "node scripts/run-astra-integration-tests.mjs", + "test:linux-capture": "node --test test/linux-capture-integration.test.mjs", "test:capture-support": "node scripts/run-capture-support-tests.mjs", + "test:capture-channel-selection-native": "node --test test/capture-channel-selection-native.test.mjs", + "test:daw-bridge": "node scripts/run-daw-bridge-tests.mjs", "test:spotify-provider": "node scripts/run-mac-spotify-provider-tests.mjs", + "test:tidal-provider": "node scripts/run-tidal-provider-tests.mjs", + "test:now-playing-ui": "node scripts/run-now-playing-ui-tests.mjs", "test:now-playing": "node scripts/run-now-playing-manager-tests.mjs", "test:profiles": "node scripts/run-profile-library-tests.mjs", "test:themes": "node scripts/run-theme-library-tests.mjs", @@ -25,6 +32,8 @@ "test:window-state": "node scripts/run-window-state-tests.mjs", "test:desktop-integration": "node scripts/run-desktop-integration-tests.mjs", "test:renderer-helpers": "node scripts/run-renderer-helper-tests.mjs", + "test:waterfall-native": "node --test test/waterfall-native.test.mjs", + "test:reference-tracks": "node scripts/run-reference-track-tests.mjs", "test:spectrum-native": "node --test test/spectrum-native.test.mjs", "test:spectrogram-native": "node --test test/spectrogram-native.test.mjs", "test:vectorscope-native": "node --test test/vectorscope-native.test.mjs", @@ -33,20 +42,30 @@ "test:tui": "node scripts/build/build-tui.cjs --test", "test:build-metadata": "node scripts/run-build-metadata-tests.mjs", "test:updates": "node scripts/run-update-tests.mjs", + "test:plugin-packaging": "node --test test/plugin-packaging.test.mjs", + "test:plugin-build": "node --test test/plugin-build.test.mjs", + "test:plugin-install": "node --test test/plugin-install.test.mjs", + "configure:plugins": "node scripts/build/build-plugins.cjs --configure-only", + "build:plugins": "node scripts/build/build-plugins.cjs", + "install:plugins": "node scripts/build/install-plugins.mjs", + "prepare:plugins": "node scripts/build/build-plugins.cjs --stage", + "test:plugins": "node scripts/build/build-plugins.cjs --test", + "stage:plugins": "node scripts/build/stage-plugins.mjs", + "test:clap": "node scripts/validate-clap.mjs", "plugin-ui:dev": "vite --config vite.plugin-ui.config.ts", "plugin-ui:build": "vite build --config vite.plugin-ui.config.ts", - "build:native": "cd native && node-gyp rebuild", + "build:native": "node scripts/build/build-native.cjs", "configure:tui": "node scripts/build/build-tui.cjs --configure-only", "build:tui": "node scripts/build/build-tui.cjs", "stage:tui": "node scripts/build/build-tui.cjs --stage", - "rebuild:native": "node -e \"const e=require('electron/package.json').version;const a=process.arch;const{execSync}=require('child_process');execSync('node-gyp rebuild --target='+e+' --arch='+a+' --dist-url=https://electronjs.org/headers',{stdio:'inherit',cwd:'native'})\"", + "rebuild:native": "node scripts/build/build-native.cjs --electron", "postinstall": "node scripts/build/postinstall-native.cjs", "dist": "node scripts/build/dist-current.cjs", - "dist:mac": "npm run stage:tui && npm run build && electron-builder --mac --publish never", - "dist:win": "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File scripts/build/check-windows-dist-output.ps1 && npm run stage:tui && npm run build && electron-builder --win --publish never", - "dist:linux": "npm run dist:linux:appimage && npm run stage:tui && electron-builder --linux deb rpm tar.gz --publish never", + "dist:mac": "npm run prepare:plugins && npm run stage:tui && npm run build && electron-builder --mac --publish never", + "dist:win": "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File scripts/build/check-windows-dist-output.ps1 && npm run prepare:plugins && npm run stage:tui && npm run build && electron-builder --win --publish never", + "dist:linux": "npm run dist:linux:appimage && npm run dist:linux:packages", "dist:linux:appimage": "npm run build && electron-builder --config electron-builder.appimage.cjs --linux AppImage --publish never", - "dist:linux:packages": "npm run stage:tui && electron-builder --linux deb rpm tar.gz --publish never" + "dist:linux:packages": "npm run prepare:plugins && npm run stage:tui && npm run build && electron-builder --linux deb rpm tar.gz --publish never" }, "repository": { "type": "git", @@ -133,6 +152,10 @@ "target": ["pkg", "zip"], "icon": "resources/icon.icns", "category": "public.app-category.music", + "extendInfo": { + "NSMicrophoneUsageDescription": "Prism uses audio inputs so you can meter microphones, interfaces, and line inputs.", + "NSAudioCaptureUsageDescription": "Prism captures system audio so you can meter output devices." + }, "hardenedRuntime": true, "entitlements": "resources/entitlements.mac.plist", "entitlementsInherit": "resources/entitlements.mac.inherit.plist" diff --git a/plugin/.gitignore b/plugin/.gitignore index 563db30..05c7f1e 100644 --- a/plugin/.gitignore +++ b/plugin/.gitignore @@ -1,2 +1,6 @@ build/ webview-dist/ + +# Local WebView2 SDK download and extraction +/sdk/ +/webview2.zip diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 27a061b..cae5cc3 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -11,6 +11,8 @@ endif() project(PrismPlugins VERSION 0.1.0 LANGUAGES C CXX) +include(CTest) + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -27,6 +29,11 @@ else() FetchContent_MakeAvailable(JUCE) endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + include(cmake/PrismLinuxWebView.cmake) + prism_isolate_linux_webview() +endif() + # Reused, unmodified Prism DSP (no N-API dependency). set(PRISM_NATIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../native/src) @@ -35,19 +42,23 @@ option(PRISM_DEV_SERVER "Load the webview UI from the Vite dev server instead of # Build controls. Defaults preserve the full local build; CI narrows these to # the formats that are actually packaged. AU is macOS-only, so Linux/Windows -# default to the portable VST3 + Standalone set. +# default to the portable VST3 + CLAP + Standalone set. if(APPLE) - set(PRISM_DEFAULT_PLUGIN_FORMATS "AU;VST3;Standalone") + set(PRISM_DEFAULT_PLUGIN_FORMATS "AU;VST3;CLAP;Standalone") else() - set(PRISM_DEFAULT_PLUGIN_FORMATS "VST3;Standalone") + set(PRISM_DEFAULT_PLUGIN_FORMATS "VST3;CLAP;Standalone") endif() -set(PRISM_PLUGIN_FORMATS "${PRISM_DEFAULT_PLUGIN_FORMATS}" CACHE STRING "JUCE plugin formats to build") +set(PRISM_PLUGIN_FORMATS "${PRISM_DEFAULT_PLUGIN_FORMATS}" CACHE STRING "Plugin formats to build (including CLAP)") option(PRISM_COPY_PLUGIN_AFTER_BUILD "Copy built plugins into user plugin folders after build" ON) +set(PRISM_JUCE_PLUGIN_FORMATS ${PRISM_PLUGIN_FORMATS}) +list(REMOVE_ITEM PRISM_JUCE_PLUGIN_FORMATS CLAP) +include(cmake/PrismClap.cmake) + set(PRISM_LINUX_UI_MODE "webview" CACHE STRING "Linux-only UI diagnostic mode: webview, web_smoke, native_smoke, or floating_webview") set_property(CACHE PRISM_LINUX_UI_MODE PROPERTY STRINGS webview web_smoke native_smoke floating_webview) set(PRISM_VALID_LINUX_UI_MODES webview web_smoke native_smoke floating_webview) -option(PRISM_LINUX_UI_DIAGNOSTICS "Write Linux VST UI diagnostic logs" OFF) +option(PRISM_LINUX_UI_DIAGNOSTICS "Write Linux plugin UI diagnostic logs" OFF) if(UNIX AND NOT APPLE) list(FIND PRISM_VALID_LINUX_UI_MODES "${PRISM_LINUX_UI_MODE}" PRISM_LINUX_UI_MODE_INDEX) @@ -105,7 +116,7 @@ function(add_prism_scope TARGET PRODUCT PLUGIN_CODE SCOPE_DEFINE) BUNDLE_ID com.astra.prism.${TARGET} PLUGIN_MANUFACTURER_CODE Prsm PLUGIN_CODE ${PLUGIN_CODE} - FORMATS ${PRISM_PLUGIN_FORMATS} + FORMATS ${PRISM_JUCE_PLUGIN_FORMATS} IS_SYNTH FALSE NEEDS_MIDI_INPUT FALSE NEEDS_MIDI_OUTPUT FALSE @@ -115,8 +126,11 @@ function(add_prism_scope TARGET PRODUCT PLUGIN_CODE SCOPE_DEFINE) target_sources(${TARGET} PRIVATE Source/PluginProcessor.cpp + Source/ReferenceTrackManager.cpp Source/PluginEditor.cpp ${PRISM_NATIVE_DIR}/spectrum.cpp + ${PRISM_NATIVE_DIR}/reference_analysis.cpp + ${PRISM_NATIVE_DIR}/reference_decode.cpp ${PRISM_NATIVE_DIR}/oscilloscope.cpp ${PRISM_NATIVE_DIR}/vumeter.cpp ${PRISM_NATIVE_DIR}/lufsmeter.cpp @@ -124,6 +138,7 @@ function(add_prism_scope TARGET PRODUCT PLUGIN_CODE SCOPE_DEFINE) ${PRISM_NATIVE_DIR}/multiband.cpp ${PRISM_NATIVE_DIR}/spectrogram.cpp ${PRISM_NATIVE_DIR}/waveform.cpp + ${PRISM_NATIVE_DIR}/waterfall.cpp ${PRISM_NATIVE_DIR}/dsp_utils.cpp) target_include_directories(${TARGET} PRIVATE Source ${PRISM_NATIVE_DIR}) @@ -136,6 +151,8 @@ function(add_prism_scope TARGET PRODUCT PLUGIN_CODE SCOPE_DEFINE) PRISM_LINUX_UI_DIAGNOSTICS=$ ${PRISM_LINUX_UI_MODE_DEFINITIONS}) if(WIN32) + # r8brain includes Windows.h; keep its min/max macros out of the DSP sources. + target_compile_definitions(${TARGET} PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) # NEEDS_WEBVIEW2 links the WebView2 SDK but doesn't turn on the JUCE compile # paths that gate withResourceProvider. JUCE_USE_WIN_WEBVIEW2=1 enables the # WebView2-aware code (incl. the resource provider); _WITH_STATIC_LINKING=1 @@ -186,7 +203,13 @@ function(add_prism_scope TARGET PRODUCT PLUGIN_CODE SCOPE_DEFINE) # directly after fork(), which is unsafe in multi-threaded hosts and has # crashed during pluginval/DAW editor creation. juce_link_with_embedded_linux_subprocess(${TARGET}) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_property(TARGET juce_linux_subprocess_helper PROPERTY SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/Source/LinuxWebViewHelper.cpp") + endif() endif() + + prism_add_clap(${TARGET} analyzer) endfunction() add_prism_scope(PrismSpectrum "Prism Spectrum" Pspc "") @@ -196,6 +219,110 @@ add_prism_scope(PrismLUFSMeter "Prism Loudness Meter" Pluf "PRISM_SCOPE_LUFSM add_prism_scope(PrismVectorscope "Prism Vectorscope" Pvct "PRISM_SCOPE_VECTORSCOPE=1") add_prism_scope(PrismSpectrogram "Prism Spectrogram" Pspg "PRISM_SCOPE_SPECTROGRAM=1") add_prism_scope(PrismWaveform "Prism Waveform" Pwav "PRISM_SCOPE_WAVEFORM=1") +add_prism_scope(PrismWaterfall "Prism Waterfall" Pwfl "PRISM_SCOPE_WATERFALL=1") + +if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(PrismLinuxWebViewHelperProbe SHARED Tests/LinuxWebViewHelperProbe.cpp) + target_link_libraries(PrismLinuxWebViewHelperProbe PRIVATE ${CMAKE_DL_LIBS}) + set(host_override_dir "${CMAKE_CURRENT_BINARY_DIR}/host-library-override") + set(host_override_library "${host_override_dir}/libprism_host_override.so") + add_custom_command(TARGET PrismLinuxWebViewHelperProbe POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${host_override_dir}" + COMMAND ${CMAKE_COMMAND} -E copy $ "${host_override_library}" + VERBATIM) + add_test(NAME PrismLinuxWebViewHelperTests COMMAND juce_linux_subprocess_helper + $ prismWebViewHelperProbe + "argument with spaces" second "${host_override_library}") + set_tests_properties(PrismLinuxWebViewHelperTests PROPERTIES + ENVIRONMENT "LD_LIBRARY_PATH=${host_override_dir};LD_PRELOAD=${host_override_library}") +endif() + +# Prism Bridge is intentionally a small native-only pass-through plug-in. It has +# no standalone target and never creates a webview or performs networking on the +# audio thread. +set(PRISM_BRIDGE_FORMATS ${PRISM_JUCE_PLUGIN_FORMATS}) +list(REMOVE_ITEM PRISM_BRIDGE_FORMATS Standalone) + +juce_add_plugin(PrismBridge + PRODUCT_NAME "Prism Bridge" + COMPANY_NAME "Prism" + BUNDLE_ID com.astra.prism.PrismBridge + PLUGIN_MANUFACTURER_CODE Prsm + PLUGIN_CODE Pbrg + FORMATS ${PRISM_BRIDGE_FORMATS} + IS_SYNTH FALSE + NEEDS_MIDI_INPUT FALSE + NEEDS_MIDI_OUTPUT FALSE + IS_MIDI_EFFECT FALSE + AU_SANDBOX_SAFE FALSE + COPY_PLUGIN_AFTER_BUILD ${PRISM_COPY_PLUGIN_AFTER_BUILD}) + +target_sources(PrismBridge PRIVATE + Source/BridgeProcessor.cpp + Source/BridgeEditor.cpp) + +target_include_directories(PrismBridge PRIVATE Source) +target_compile_definitions(PrismBridge PRIVATE + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + JUCE_VST3_CAN_REPLACE_VST2=0) +target_link_libraries(PrismBridge PRIVATE + juce::juce_audio_utils + juce::juce_recommended_config_flags + juce::juce_recommended_lto_flags + juce::juce_recommended_warning_flags) + +prism_add_clap(PrismBridge utility) + +if(APPLE) + # Keep local/CI AU bundles structurally valid before the release pipeline + # replaces this ad-hoc signature with the distribution identity. + foreach(au_target IN ITEMS PrismBridge_AU PrismWaterfall_AU) + if(TARGET ${au_target}) + add_custom_command(TARGET ${au_target} POST_BUILD + COMMAND codesign --force --deep --sign - "$" + VERBATIM) + endif() + endforeach() +endif() + +if(BUILD_TESTING) + juce_add_console_app(PrismReferenceTests PRODUCT_NAME "Prism Reference Tests") + target_sources(PrismReferenceTests PRIVATE Tests/ReferenceTrackTests.cpp) + target_include_directories(PrismReferenceTests PRIVATE Source ${PRISM_NATIVE_DIR} ${JUCE_SOURCE_DIR}/modules) + target_compile_definitions(PrismReferenceTests PRIVATE + JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1 JUCE_WEB_BROWSER=1 JucePlugin_Name="Prism Spectrum") + target_link_libraries(PrismReferenceTests PRIVATE PrismSpectrum) + add_test(NAME PrismReferenceTests COMMAND PrismReferenceTests) + + juce_add_console_app(PrismWaterfallTests PRODUCT_NAME "Prism Waterfall Tests") + target_sources(PrismWaterfallTests PRIVATE Tests/WaterfallTests.cpp) + target_include_directories(PrismWaterfallTests PRIVATE Source ${PRISM_NATIVE_DIR} ${JUCE_SOURCE_DIR}/modules) + target_compile_definitions(PrismWaterfallTests PRIVATE + JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1 + JUCE_WEB_BROWSER=1 + JucePlugin_Name="Prism Waterfall") + target_link_libraries(PrismWaterfallTests PRIVATE PrismWaterfall) + add_test(NAME PrismWaterfallTests COMMAND PrismWaterfallTests) + + juce_add_console_app(PrismBridgeTests PRODUCT_NAME "Prism Bridge Tests") + target_sources(PrismBridgeTests PRIVATE + Tests/BridgeProcessorTests.cpp + Tests/BridgeEditorTests.cpp + Source/BridgeProcessor.cpp + Source/BridgeEditor.cpp) + target_include_directories(PrismBridgeTests PRIVATE Source) + target_compile_definitions(PrismBridgeTests PRIVATE + PRISM_BRIDGE_TESTING=1 + JucePlugin_Name="Prism Bridge" + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0) + target_link_libraries(PrismBridgeTests PRIVATE + juce::juce_audio_utils + juce::juce_recommended_config_flags + juce::juce_recommended_warning_flags) + add_test(NAME PrismBridgeProcessorTests COMMAND PrismBridgeTests) +endif() set(PRISM_SCOPE_TARGETS PrismSpectrum @@ -204,10 +331,16 @@ set(PRISM_SCOPE_TARGETS PrismLUFSMeter PrismVectorscope PrismSpectrogram - PrismWaveform) + PrismWaveform + PrismWaterfall + PrismBridge) add_custom_target(PrismInstallerPlugins) foreach(PRISM_SCOPE_TARGET IN LISTS PRISM_SCOPE_TARGETS) + if("CLAP" IN_LIST PRISM_PLUGIN_FORMATS) + add_dependencies(PrismInstallerPlugins ${PRISM_SCOPE_TARGET}_CLAP) + endif() + if("VST3" IN_LIST PRISM_PLUGIN_FORMATS) add_dependencies(PrismInstallerPlugins ${PRISM_SCOPE_TARGET}_VST3) endif() diff --git a/plugin/README.md b/plugin/README.md index d6efe80..8e190e0 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,24 +1,127 @@ # Prism — DAW plugins -JUCE 8 plugins (VST3 / AU / Standalone) that render Prism's scopes inside a DAW — +JUCE 8 plugins (VST3 / AU / CLAP / Standalone) that render Prism's scopes inside a DAW — one plugin per scope: **spectrum, oscilloscope, vectorscope, spectrogram, VU meter, -loudness meter, waveform**. They **reuse Prism's existing C++ DSP** (`native/src/*.cpp`) +loudness meter, waveform, waterfall** — plus the native **Prism Bridge** pass-through plugin. +The analyzers **reuse Prism's existing C++ DSP** (`native/src/*.cpp`) and the **existing React canvas UI** (`src/plugin-ui`, importing the unchanged -visualizers from `src/renderer/visualizers/`). VST3 is built for macOS, Windows, +visualizers from `src/renderer/visualizers/`). VST3 and CLAP are built for macOS, Windows, and Linux; AU is macOS-only. ## How it fits together ``` DAW track audio - → processBlock (RT thread): mix to mono, write to lock-free FIFO [Source/PluginProcessor.cpp] - → 60 Hz timer (message thread): drain FIFO → Visualizer::Spectrum [Source/PluginEditor.cpp + native/src/spectrum.cpp] - → emit "spectrumFrame" (base64 Float32 magnitudes) to the webview - → juceBridge.ts decodes → BridgeSpectrumAnalyzer (a SpectrumNativeAnalyzer shim) - → SpectrumAnalyzer.ts renders to canvas (unchanged Electron code) [src/plugin-ui] + → processBlock (RT thread): copy stereo into a lock-free FIFO [Source/PluginProcessor.cpp] + → display callback (message thread): drain FIFO → native ScopeEngine [Source/PluginEditor.cpp] + → emit scope frames (compact base64 Float32 arrays) to the webview + → juceBridge.ts decodes → the scope's Bridge*Analyzer adapter + → the existing desktop visualizer renders to canvas [src/plugin-ui] ``` No DSP or allocation runs on the realtime audio thread; audio passes through unmodified. +Waterfall records history at 60 slices per second of audio, independently of the +display rate, and repaints when a native plot frame arrives. + +Spectrum accepts one reference track through **Settings → Reference** or a file +drop onto its graph (mono/stereo WAV, AIFF, FLAC, or MP3). Overlay shows its average +curve; Difference shows live Mid minus reference minus trim on a ±24 dB scale. +Match level sets trim once from the latest three seconds of live Mid power. +Import workers belong to the processor and continue when the editor closes. +Analyzed curves are stored per instance in DAW state; the source file is not +needed for recall, and desktop profile references are never inherited. +Pinned decoder/resampler sources and licenses are in `native/vendor`. + +Reference validation: build `PrismReferenceTests` and run +`ctest --test-dir plugin/build -R PrismReferenceTests --output-on-failure`. +`PrismReferenceTests --ui [audio-file]` opens a native editor with a quiet test +tone for file-drop, picker, loading, and narrow-window checks. + +## Build with npm (all platforms) + +After installing the platform prerequisites below and running `npm install`, use +these commands from the repository root: + +```sh +npm run build:plugins # Build the web UI, configure CMake, compile in Release +npm run test:plugins # Also enable and run all native plugin tests +npm run configure:plugins # Build the web UI and configure, without compiling +npm run install:plugins # Install already-built plugin formats; no compilation +npm run prepare:plugins # Build and stage all formats required by installers +``` + +The commands reuse `plugin/build`, including its existing generator and selected +formats. Fresh builds use CMake's default generator and include VST3, CLAP, and +Standalone on Windows/Linux, plus AU on macOS. Bridge has no Standalone target. +Pinned JUCE and CLAP sources are downloaded by CMake when needed. Build products +are under `plugin/build/Prism*_artefacts/Release/`. + +Compilation always disables CMake's copy-after-build step, including when an +older cache enabled it. `--install` runs the separate installation helper only +after a successful build (and tests, when requested). If installation fails or +is cancelled, the compiled files remain available and can be installed later +with `npm run install:plugins` without rebuilding. The direct CMake commands +below retain CMake's default copy-after-build behavior. + +```sh +npm run build:plugins -- --install +npm run build:plugins -- --jobs 2 +npm run build:plugins -- "-DPRISM_PLUGIN_FORMATS=VST3;CLAP" +npm run build:plugins -- "-DCMAKE_BUILD_TYPE=Debug" +npm run build:plugins -- "-DJUCE_PATH=/path/to/JUCE" +npm run install:plugins -- --dry-run +npm run install:plugins -- --formats VST3 +npm run install:plugins -- --config Debug +``` + +Installation checks the complete set of selected products before copying or +requesting elevation. By default, it reads the formats selected in the CMake +cache and skips Standalone apps. `--formats VST3,CLAP` selects an explicit subset. + +- **Windows:** installs to `Common Files/VST3` and `Common Files/CLAP`, requesting + UAC only for a PowerShell copy helper. Run npm from a normal terminal. For a + user-only copy, use `npm run install:plugins -- --user`; this writes under + `%LOCALAPPDATA%/Programs/Common/` (your DAW may need those scan paths added). +- **macOS:** installs to `~/Library/Audio/Plug-Ins/{VST3,CLAP,Components}`. +- **Linux:** installs to `~/.vst3` and `~/.clap`. + +On macOS/Linux, `--system` uses `/Library/Audio/Plug-Ins/` or `/usr/lib/{vst3,clap}` +and requests `sudo` for the copy helper only. Close DAWs using Prism before +updating installed plugins. + +`npm run dist` now prepares plugins before packaging: it builds the embedded UI, +adds missing VST3/CLAP formats (plus AU on macOS) to the existing CMake selection, +builds the Release installer targets, then validates and stages every product +in `plugin/dist-installer`. Missing plugins stop packaging. Other cached formats, +such as Standalone, are preserved. Use the matching host OS for native packaging. +The Linux AppImage stays app-only; `.deb`, `.rpm`, and `.tar.gz` include plugins. + +Release CI sets `PRISM_PLUGINS_PREBUILT=1` after its separate native build and +validation steps. In this mode preparation still checks and stages every release +artifact, but does not rebuild outside the CI compiler environment. Local builds +should leave this variable unset. + +Parallelism defaults to `CMAKE_BUILD_PARALLEL_LEVEL`, or 4 when unset. CMake +definitions (`-DNAME=VALUE`) are forwarded as individual arguments, including +paths with spaces and semicolon-separated formats. Generator/toolchain options +`-G`, `-A`, and `-T` are also accepted; an existing build directory must retain +its original generator. Run `npm run build:plugins -- --help` for all options. + +On Windows, the helper detects an SDK extracted to +`plugin/sdk/Microsoft.Web.WebView2/`. An explicit +`-DJUCE_WEBVIEW2_PACKAGE_LOCATION=...` takes precedence over the environment +variable of the same name, an existing CMake cache entry, and that local SDK. +Otherwise, JUCE searches its standard user NuGet package location. The value +must name the **parent directory** containing `Microsoft.Web.WebView2`. +With the NuGet CLI installed, you can prepare the local SDK using the same +version as CI: + +```sh +nuget install Microsoft.Web.WebView2 -Version 1.0.1901.177 -OutputDirectory plugin/sdk -ExcludeVersion +``` + +The SDK download is separate from the WebView2 Runtime. SDK files and build +outputs are ignored by Git. The npm helper does not install system prerequisites. ## Build & run (macOS) @@ -35,13 +138,15 @@ cmake -B plugin/build -S plugin -DCMAKE_BUILD_TYPE=Release # embeds the bundle ( cmake --build plugin/build --config Release ``` -`COPY_PLUGIN_AFTER_BUILD` installs all seven plugins into your user plugin folders: +`PRISM_COPY_PLUGIN_AFTER_BUILD` installs all eight analyzers plus Prism Bridge into your user plugin folders: - AU: `~/Library/Audio/Plug-Ins/Components/Prism *.component` - VST3: `~/Library/Audio/Plug-Ins/VST3/Prism *.vst3` +- CLAP: `~/Library/Audio/Plug-Ins/CLAP/Prism *.clap` -Load any `Prism *` AU / VST3 on a track in Ableton / FL / Logic / Reaper (or run the +Load a `Prism *` plugin in a DAW that supports its format (or run the matching **Standalone** from `plugin/build/Prism*_artefacts/Release/Standalone/`), play audio, and the scope animates. +(Prism Bridge has AU/VST3/CLAP targets and no Standalone target.) (To use a local JUCE checkout instead of fetching: add `-DJUCE_PATH=/path/to/JUCE`.) ### UI development: dev-server mode (hot reload) @@ -70,17 +175,46 @@ cmake -B plugin\build -S plugin -G "Visual Studio 17 2022" -A x64 cmake --build plugin\build --config Release ``` -`COPY_PLUGIN_AFTER_BUILD` targets the system VST3 folder -(`C:\Program Files\Common Files\VST3\Prism *.vst3`), which **needs admin -privileges**. Either run the `cmake --build` step from an elevated shell, or copy -the built bundles from `plugin\build\Prism*_artefacts\Release\VST3\` into your -user-local VST3 folder (`%LOCALAPPDATA%\Programs\Common\VST3\`) by hand — most -DAWs scan both. +`PRISM_COPY_PLUGIN_AFTER_BUILD` installs VST3 bundles into +`C:\Program Files\Common Files\VST3` and CLAP files into +`C:\Program Files\Common Files\CLAP`. These locations need admin privileges. +To build without installing, configure with `-DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF`. +Then copy the products from `plugin\build\Prism*_artefacts\Release\VST3\` +and `...\CLAP\` to the system directories, or the user directories +`%LOCALAPPDATA%\Programs\Common\VST3\` and `...\CLAP\`. + +The Windows installer offers independent VST3 and CLAP checkboxes, both checked +by default. Silent installations install both. For scripted installation, use +`/VST3=0|1` and `/CLAP=0|1` before any final `/D=...` install-directory argument. +Uninstall removes only the nine named Prism products from each system directory. Dev-server mode works the same as macOS: `-DPRISM_DEV_SERVER=ON` + `npm run plugin-ui:dev`. ## Build & run (Linux) +Release and build-smoke CI use Ubuntu 24.04 as the Linux binary baseline. For +plugins to share across distributions or load in a Flatpak DAW, build on that +baseline rather than a newer distribution. A Fedora 44 build can require +`GLIBC_2.43` even when the DAW's Flatpak runtime only provides glibc 2.42; +that fails during scanning, before Prism's processor or editor starts. +`readelf --version-info "Prism Oscilloscope.clap"` shows required symbol versions. +The [Windows Linux test launcher](../docs/linux-testing.md) can build in Ubuntu +WSL and preserve the build logs. + +Scope editors need WebKitGTK 4.1 (or JUCE's 4.0 fallback) in the environment +running the DAW. A Flatpak runtime must provide it inside the sandbox; installing +WebKitGTK only on the host does not satisfy that requirement. Successful plugin +scanning alone does not verify editor support. Prism Bridge uses a native editor +and does not require WebKitGTK. + +The Linux browser runs in a helper process with `LD_LIBRARY_PATH` and +`LD_PRELOAD` cleared before a clean exec, so a DAW's bundled GLib cannot prevent +the system WebKitGTK from loading. The DAW's own environment stays unchanged. +`cmake/PrismLinuxWebView.cmake` patches a build-local copy of JUCE 8.0.4 to defer +its WebKit probe to that helper. The configure step fails if JUCE's patch anchors +change; review the patch when updating JUCE. The isolation regression runs as +`PrismLinuxWebViewHelperTests` in CTest and Linux CI. + Prereqs: CMake >= 3.22, Ninja, GCC/Clang, Node.js, and JUCE's Linux GUI/WebView dependencies. On Ubuntu 24.04+: @@ -95,25 +229,118 @@ sudo apt-get install build-essential cmake ninja-build pkg-config \ ```sh npm install npm run plugin-ui:build -cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release -DPRISM_PLUGIN_FORMATS=VST3 +cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release -DPRISM_PLUGIN_FORMATS="VST3;CLAP" cmake --build plugin/build --target PrismInstallerPlugins --parallel ``` Built bundles land under `plugin/build/Prism*_artefacts/Release/VST3/`. Copy the -seven `Prism *.vst3` directories to one of the standard Linux VST3 scan paths: +nine `Prism *.vst3` directories to one of the standard Linux VST3 scan paths: - User-local: `$HOME/.vst3` - System-wide: `/usr/lib/vst3` - System-wide local: `/usr/local/lib/vst3` -The Linux `.deb` and `.rpm` release packages install Prism's VST3 bundles to -`/usr/lib/vst3` and remove only those seven bundles on package removal. The Linux -`tar.gz` release includes `resources/plugins/install-vst3.sh`, which installs to -`$HOME/.vst3` by default or `/usr/lib/vst3` with `--system`. The AppImage is -portable app-only and does not install DAW plugins. +CLAP products are single `.clap` files under +`plugin/build/Prism*_artefacts/Release/CLAP/`. Copy the nine `Prism *.clap` +files to `$HOME/.clap` or `/usr/lib/clap`. + +The Linux `.deb` and `.rpm` release packages install VST3 bundles to `/usr/lib/vst3` +and CLAP files to `/usr/lib/clap`. Package upgrades retain the plugins; uninstall +removes only the nine named Prism products per format. The `tar.gz` release includes +`resources/plugins/install-vst3.sh` and `resources/plugins/install-clap.sh`: + +```sh +sh resources/plugins/install-vst3.sh +sh resources/plugins/install-clap.sh +# Use --system for global installation, or --dest PATH for a custom location. +``` + +The CLAP helper defaults to `$HOME/.clap`; `--system` selects `/usr/lib/clap`. +`--source PATH` overrides the bundled source directory. Environment overrides are +`PRISM_CLAP_SOURCE_DIR` and `PRISM_CLAP_DEST_DIR`; explicit command-line options win. +The AppImage is portable app-only and does not install DAW plugins. + +## CLAP build configuration and validation + +Fresh builds include CLAP. Existing CMake caches keep their selected formats; +reconfigure with `-DPRISM_PLUGIN_FORMATS="AU;VST3;CLAP"` on macOS or +`-DPRISM_PLUGIN_FORMATS="VST3;CLAP"` on Windows/Linux. Use +`-DPRISM_PLUGIN_FORMATS=CLAP` for CLAP only, or omit CLAP from the format list to +avoid fetching or linking its dependencies. Build `PrismInstallerPlugins` for all +selected installable formats, or an individual target such as `PrismSpectrum_CLAP`. + +CLAP uses [clap-juce-extensions](https://github.com/free-audio/clap-juce-extensions) +at `9fbefae3d9c3d130aafb558c1ec15427a4bd24be`, including its pinned CLAP and +clap-helpers submodules. CMake fetches it automatically. An existing recursive +checkout can be supplied with `-DCLAP_JUCE_EXTENSIONS_PATH=/path/to/checkout`. +JUCE remains at 8.0.4. See the root third-party notices for dependency licenses. + +CLAP IDs are stable `com.astra.prism.` identifiers, for example +`com.astra.prism.PrismSpectrum` and `com.astra.prism.PrismBridge`. Settings use the +same JSON as VST3/AU, including references; a fresh analyzer saves `{}` before +its editor opens. DAWs treat each format as a separate plugin instance, so existing +VST3/AU instances are not automatically converted. + +After building, `npm run stage:plugins` checks and stages all nine products in the +release formats (VST3/CLAP plus AU on macOS). For macOS ZIP installations, copy the +bundles from `Prism.app/Contents/Resources/plugins/{CLAP,VST3,AU}` into the matching +user plugin folders listed above. The macOS PKG installs them into the corresponding +system folders under `/Library/Audio/Plug-Ins`; CLAP bundles are ad-hoc signed during +building. For a manually extracted Windows portable build, copy the `.clap` files from +`resources/plugins/CLAP` into a standard CLAP folder. + +CI builds [clap-validator](https://github.com/free-audio/clap-validator) at +`b2f1d9b79b1d264a5747f46707d72b1aa40a02ef`. With that binary on PATH, run +`npm run test:clap`; alternatively set `CLAP_VALIDATOR_PATH` to its executable. +Per-plugin results are saved under `plugin/build/clap-validation/` and uploaded by CI. +The validator's `param-conversions` test divides by zero when there are no parameters; +only that test is excluded for Prism's nine parameterless products. The wrapper also +reports successful loading of random state bytes because JUCE's state-load callback +cannot return failure; the validator reports this as a warning. All processing, +state round-trip, and lifecycle checks remain enabled. + +`node scripts/check-plugin-formats.mjs` checks CLAP-only and VST3-only configurations +after the full build is configured. `npm run test:plugin-packaging` tests staging, +Unix installation, repeated installation, upgrades, and removal using temporary +paths. CI also tests Windows silent defaults and every format-selection combination. + +Before releasing, exercise CLAP in a compatible DAW: rescan all nine products; +verify mono/stereo pass-through, editor animation and resizing, repeated reopen, +project save/reload, Spectrum references, Waterfall history, and Bridge connection, +naming, transport, and reconnection. Host-provided track names depend on the host's +CLAP track-info support; Bridge retains its custom-name and instance-tag fallback. ## Notes +Waterfall's native engine/processor tests run with `ctest --test-dir plugin/build +-C Release -R PrismWaterfallTests --output-on-failure` after building the +`PrismWaterfallTests` target. Run that executable with `--ui` for a silent native +editor smoke test of live rendering, color controls, and resizing; it saves canvas +snapshots in the test application's temporary directory. + +- **Waterfall:** reuses the desktop Canvas renderer and the native 60-slice/second + stereo analyzer. The webview requests only the visible ridge count and frequency + resolution. History duration, density, Theme/Heat colors, guides, FFT, frequency + scale/range, smoothing, and tilt are saved per instance in DAW state. Audio gaps, + editor reopen, sample-rate changes, and FFT-size changes start fresh history; + duration and appearance changes preserve available history. Freeze/inspect and + angled 3D remain future work. + +- **Prism Bridge:** start the standalone Prism application, insert Bridge on a + mono or stereo track/bus, then select that instance in Prism's **DAW Bridges** + input group. Bridge is bit-transparent, ignores offline rendering, and sends + audio and transport metadata only over `127.0.0.1:51789`. Its audio callback + only packetizes into a bounded lock-free queue; a background thread owns the + socket. The source UUID and custom name are saved in DAW state. + Its compact native nameplate follows the DAW track name automatically. Click + the name to override it; Enter or clicking away saves, Escape cancels, and + clearing restores the track name. The small instance tag matches duplicate + names in Prism. The editor can stay closed while Bridge is in use. +- **Logic/AU validation:** `AU_SANDBOX_SAFE FALSE` is explicit because Bridge needs + loopback networking. Apple may host Audio Units out of process; AU loopback in + Logic is therefore a release-blocking host validation, not something the build + alone can prove. + - **Refresh rate (macOS):** frames are emitted on `juce::VBlankAttachment` (synced to the display, adapts to 60/120/144 Hz) and the webview renders via the `display-sync` FrameScheduler. WKWebView otherwise throttles `requestAnimationFrame` to 60 fps regardless diff --git a/plugin/Source/BridgeEditor.cpp b/plugin/Source/BridgeEditor.cpp new file mode 100644 index 0000000..96ef669 --- /dev/null +++ b/plugin/Source/BridgeEditor.cpp @@ -0,0 +1,107 @@ +#include "BridgeEditor.h" +#include "BridgeProcessor.h" + +PrismBridgeEditor::PrismBridgeEditor(PrismBridgeProcessor& p) + : juce::AudioProcessorEditor(&p), bridgeProcessor(p) +{ + title.setText("PRISM BRIDGE", juce::dontSendNotification); + title.setFont(juce::Font(juce::FontOptions(11.0f, juce::Font::bold))); + title.setColour(juce::Label::textColourId, juce::Colour(0xff94a3b8)); + title.setBorderSize(juce::BorderSize(0)); + addAndMakeVisible(title); + + nameLabel.setName("Source name"); + nameLabel.setFont(juce::Font(juce::FontOptions(23.0f, juce::Font::bold))); + nameLabel.setColour(juce::Label::textColourId, juce::Colour(0xfff1f5f9)); + nameLabel.setColour(juce::Label::textWhenEditingColourId, juce::Colour(0xfff1f5f9)); + nameLabel.setColour(juce::Label::backgroundWhenEditingColourId, juce::Colour(0xff111827)); + nameLabel.setColour(juce::Label::outlineWhenEditingColourId, juce::Colour(0xff38bdf8)); + nameLabel.setBorderSize(juce::BorderSize(4, 0, 4, 0)); + nameLabel.setMinimumHorizontalScale(1.0f); + nameLabel.setEditable(true, false, false); + nameLabel.setMouseCursor(juce::MouseCursor::IBeamCursor); + nameLabel.onEditorShow = [this] + { + if (auto* editor = nameLabel.getCurrentTextEditor()) + { + editor->setInputRestrictions(160); + editor->setSelectAllWhenFocused(true); + editor->selectAll(); + } + }; + nameLabel.onTextChange = [this] + { + bridgeProcessor.setCustomName(nameLabel.getText()); + timerCallback(); + }; + addAndMakeVisible(nameLabel); + + instanceTag.setName("Bridge instance tag"); + instanceTag.setText(bridgeProcessor.getInstanceTag(), juce::dontSendNotification); + instanceTag.setFont(juce::Font(juce::FontOptions(11.0f))); + instanceTag.setColour(juce::Label::textColourId, juce::Colour(0xff64748b)); + instanceTag.setBorderSize(juce::BorderSize(0)); + instanceTag.setTooltip("Match this tag in Prism when sources have the same name.\nInstance: " + + bridgeProcessor.getInstanceId()); + addAndMakeVisible(instanceTag); + addAndMakeVisible(connectionDot); + + // A text editor is created only on click or keyboard navigation. + // Merely opening the plug-in must not start a rename. + setResizable(false, false); + setSize(320, 104); + timerCallback(); + startTimerHz(4); +} + +void PrismBridgeEditor::paint(juce::Graphics& g) +{ + g.fillAll(juce::Colour(0xff080d18)); + g.setColour(juce::Colour(0xff1e293b)); + g.drawRoundedRectangle(getLocalBounds().toFloat().reduced(6.0f), 7.0f, 1.0f); +} + +void PrismBridgeEditor::resized() +{ + auto bounds = getLocalBounds().reduced(20, 16); + auto header = bounds.removeFromTop(14); + connectionDot.setBounds(header.removeFromRight(16)); + title.setBounds(header); + bounds.removeFromTop(4); + nameLabel.setBounds(bounds.removeFromTop(36)); + bounds.removeFromTop(4); + instanceTag.setBounds(bounds); +} + +void PrismBridgeEditor::timerCallback() +{ + // Host track/state changes must not replace an in-progress edit. + if (!nameLabel.isBeingEdited()) + nameLabel.setText(bridgeProcessor.getDisplayName(), juce::dontSendNotification); + + nameLabel.setTooltip(bridgeProcessor.getDisplayName() + + "\nClick to rename. Clear the name to use the DAW track name automatically."); + connectionDot.setStatus(bridgeProcessor.isConnectedToPrism(), + bridgeProcessor.isSelectedInPrism(), + bridgeProcessor.getConnectionDescription()); +} + +void PrismBridgeEditor::ConnectionDot::setStatus(bool connected, bool selected, + const juce::String& description) +{ + const auto nextColour = !connected ? juce::Colour(0xff64748b) + : selected ? juce::Colour(0xff4ade80) : juce::Colour(0xff38bdf8); + setTitle(description); + setTooltip(description); + if (colour != nextColour) + { + colour = nextColour; + repaint(); + } +} + +void PrismBridgeEditor::ConnectionDot::paint(juce::Graphics& g) +{ + g.setColour(colour); + g.fillEllipse(getLocalBounds().toFloat().withSizeKeepingCentre(6.0f, 6.0f)); +} diff --git a/plugin/Source/BridgeEditor.h b/plugin/Source/BridgeEditor.h new file mode 100644 index 0000000..e7003ac --- /dev/null +++ b/plugin/Source/BridgeEditor.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +class PrismBridgeProcessor; + +class PrismBridgeEditor final : public juce::AudioProcessorEditor, + private juce::Timer +{ +public: + explicit PrismBridgeEditor(PrismBridgeProcessor&); + ~PrismBridgeEditor() override = default; + void paint(juce::Graphics&) override; + void resized() override; + +private: + void timerCallback() override; + + class ConnectionDot final : public juce::Component, + public juce::SettableTooltipClient + { + public: + void setStatus(bool connected, bool selected, const juce::String& description); + void paint(juce::Graphics&) override; + + private: + juce::Colour colour { 0xff64748b }; + }; + + PrismBridgeProcessor& bridgeProcessor; + juce::Label title; + juce::Label nameLabel; + juce::Label instanceTag; + ConnectionDot connectionDot; + juce::TooltipWindow tooltip { this, 500 }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PrismBridgeEditor) +}; diff --git a/plugin/Source/BridgeProcessor.cpp b/plugin/Source/BridgeProcessor.cpp new file mode 100644 index 0000000..012a5dd --- /dev/null +++ b/plugin/Source/BridgeProcessor.cpp @@ -0,0 +1,593 @@ +#include "BridgeProcessor.h" +#include "BridgeEditor.h" +#include +#include +#include +#include + +namespace +{ +constexpr uint32_t frameMagic = 0x4d535250; +constexpr uint16_t protocolVersion = 1; +constexpr uint16_t messageHello = 1; +constexpr uint16_t messageHeartbeat = 2; +constexpr uint16_t messageSourceUpdate = 3; +constexpr uint16_t messageSubscribe = 10; +constexpr uint16_t messageAudio = 20; +constexpr size_t frameHeaderBytes = 12; +constexpr size_t audioHeaderBytes = 76; +constexpr size_t maxPayloadBytes = 256 * 1024; +constexpr int serverPort = 51789; + +template +void writeLittleEndian(uint8_t* destination, Integer value) noexcept +{ + const auto little = juce::ByteOrder::swapIfBigEndian(value); + std::memcpy(destination, &little, sizeof(little)); +} + +void writeDoubleLittleEndian(uint8_t* destination, double value) noexcept +{ + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + writeLittleEndian(destination, bits); +} + +uint16_t readU16(const uint8_t* source) noexcept +{ + uint16_t value = 0; + std::memcpy(&value, source, sizeof(value)); + return juce::ByteOrder::swapIfBigEndian(value); +} + +uint32_t readU32(const uint8_t* source) noexcept +{ + uint32_t value = 0; + std::memcpy(&value, source, sizeof(value)); + return juce::ByteOrder::swapIfBigEndian(value); +} +} + +PrismBridgeProcessor::PrismBridgeProcessor() + : juce::AudioProcessor(BusesProperties() + .withInput("Input", juce::AudioChannelSet::stereo(), true) + .withOutput("Output", juce::AudioChannelSet::stereo(), true)), + juce::Thread("Prism Bridge network"), + sourceId(juce::Uuid().toString()), + instanceId(juce::Uuid().toString()), + hostName(juce::PluginHostType().getHostDescription()) +{ +} + +PrismBridgeProcessor::~PrismBridgeProcessor() +{ + signalThreadShouldExit(); + stopThread(1500); + closeSocket(); +} + +void PrismBridgeProcessor::prepareToPlay(double sampleRate, int) +{ + currentSampleRate.store(sampleRate > 0.0 ? sampleRate : 48000.0); + nextSequence.store(0); + fifo.reset(); +#if ! defined(PRISM_BRIDGE_TESTING) || ! PRISM_BRIDGE_TESTING + if (!isThreadRunning()) + startThread(juce::Thread::Priority::normal); +#endif + identityDirty.store(true); +} + +void PrismBridgeProcessor::releaseResources() +{ + signalThreadShouldExit(); + stopThread(1000); + closeSocket(); + connected.store(false); + selected.store(false); + discardQueuedPackets(); +} + +bool PrismBridgeProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const +{ + const auto input = layouts.getMainInputChannelSet(); + const auto output = layouts.getMainOutputChannelSet(); + return input == output + && (input == juce::AudioChannelSet::mono() || input == juce::AudioChannelSet::stereo()); +} + +PrismBridgeProcessor::TransportData PrismBridgeProcessor::readTransport() const noexcept +{ + TransportData result; + if (const auto* playHead = getPlayHead()) + { + if (const auto position = playHead->getPosition()) + { + result.isPlaying = position->getIsPlaying(); + result.isRecording = position->getIsRecording(); + result.isLooping = position->getIsLooping(); + if (const auto value = position->getTimeInSamples()) + { + result.timeInSamples = *value; + result.hasTimeInSamples = true; + } + if (const auto value = position->getTimeInSeconds()) + { + result.timeInSeconds = *value; + result.hasTimeInSeconds = true; + } + if (const auto value = position->getPpqPosition()) + { + result.ppqPosition = *value; + result.hasPpq = true; + } + if (const auto value = position->getBpm()) + { + result.bpm = *value; + result.hasBpm = std::isfinite(result.bpm) && result.bpm > 0.0; + } + if (const auto value = position->getPpqPositionOfLastBarStart()) + result.lastBarPpq = *value; + else + result.lastBarPpq = std::numeric_limits::quiet_NaN(); + if (const auto value = position->getTimeSignature()) + { + result.numerator = static_cast(value->numerator); + result.denominator = static_cast(value->denominator); + result.hasTimeSignature = value->numerator > 0 && value->denominator > 0; + } + if (const auto value = position->getLoopPoints()) + { + result.loopStartPpq = value->ppqStart; + result.loopEndPpq = value->ppqEnd; + } + else + { + result.loopStartPpq = std::numeric_limits::quiet_NaN(); + result.loopEndPpq = std::numeric_limits::quiet_NaN(); + } + } + } + return result; +} + +PrismBridgeProcessor::TransportData PrismBridgeProcessor::offsetTransport( + const TransportData& base, + int sampleOffset) const noexcept +{ + auto result = base; + const double sampleRate = std::max(1.0, currentSampleRate.load()); + const double seconds = static_cast(sampleOffset) / sampleRate; + if (result.hasTimeInSamples) result.timeInSamples += sampleOffset; + if (result.hasTimeInSeconds) result.timeInSeconds += seconds; + if (result.hasPpq && result.hasBpm) result.ppqPosition += seconds * result.bpm / 60.0; + return result; +} + +void PrismBridgeProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer&) +{ + juce::ScopedNoDenormals noDenormals; + if (isNonRealtime() || !selected.load(std::memory_order_relaxed)) + return; + + const int frameCount = buffer.getNumSamples(); + const int channels = buffer.getNumChannels(); + if (frameCount <= 0 || channels <= 0) + return; + + const auto transport = readTransport(); + const float* left = buffer.getReadPointer(0); + const float* right = channels > 1 ? buffer.getReadPointer(1) : left; + for (int offset = 0; offset < frameCount; offset += packetFrames) + { + const int count = std::min(packetFrames, frameCount - offset); + enqueuePacket(left + offset, right + offset, count, channels, offsetTransport(transport, offset)); + } +} + +void PrismBridgeProcessor::enqueuePacket( + const float* left, + const float* right, + int frameCount, + int channelCount, + const TransportData& transport) noexcept +{ + int start1 = 0, size1 = 0, start2 = 0, size2 = 0; + fifo.prepareToWrite(1, start1, size1, start2, size2); + if (size1 == 0) + { + droppedFrames.fetch_add(static_cast(frameCount), std::memory_order_relaxed); + return; + } + + auto& packet = packets[static_cast(start1)]; + std::memcpy(packet.left.data(), left, static_cast(frameCount) * sizeof(float)); + std::memcpy(packet.right.data(), right, static_cast(frameCount) * sizeof(float)); + packet.transport = transport; + packet.sequence = nextSequence.fetch_add(1, std::memory_order_relaxed) + 1; + packet.frameCount = static_cast(frameCount); + packet.channelCount = static_cast(channelCount > 1 ? 2 : 1); + packet.sampleRate = currentSampleRate.load(std::memory_order_relaxed); + fifo.finishedWrite(1); +} + +bool PrismBridgeProcessor::popPacket(AudioPacket& destination) noexcept +{ + int start1 = 0, size1 = 0, start2 = 0, size2 = 0; + fifo.prepareToRead(1, start1, size1, start2, size2); + if (size1 == 0) return false; + destination = packets[static_cast(start1)]; + fifo.finishedRead(1); + return true; +} + +void PrismBridgeProcessor::discardQueuedPackets() noexcept +{ + AudioPacket scratch; + while (popPacket(scratch)) {} +} + +void PrismBridgeProcessor::run() +{ + int reconnectDelayMs = 250; + int64_t lastHeartbeat = 0; + AudioPacket packet; + + while (!threadShouldExit()) + { + if (!socket || !socket->isConnected()) + { + connected.store(false); + selected.store(false); + discardQueuedPackets(); + if (!connectSocket()) + { + wait(reconnectDelayMs); + reconnectDelayMs = std::min(5000, reconnectDelayMs * 2); + continue; + } + reconnectDelayMs = 250; + lastHeartbeat = static_cast(juce::Time::getMillisecondCounterHiRes()); + } + + if (!readControlMessages()) + { + closeSocket(); + continue; + } + + const auto currentDroppedFrames = droppedFrames.load(); + if ((identityDirty.exchange(false) || currentDroppedFrames != reportedDroppedFrames) + && !sendHello(messageSourceUpdate)) + { + closeSocket(); + continue; + } + reportedDroppedFrames = currentDroppedFrames; + + const auto now = static_cast(juce::Time::getMillisecondCounterHiRes()); + if (now - lastHeartbeat >= 1000) + { + if (!sendHeartbeat()) + { + closeSocket(); + continue; + } + lastHeartbeat = now; + } + + if (!selected.load()) + { + discardQueuedPackets(); + wait(5); + continue; + } + + const auto maximumLivePackets = std::max( + 1, + static_cast(std::floor(currentSampleRate.load() * 0.05 / packetFrames))); + if (fifo.getNumReady() > maximumLivePackets) + { + while (fifo.getNumReady() > maximumLivePackets && popPacket(packet)) + droppedFrames.fetch_add(packet.frameCount, std::memory_order_relaxed); + } + + if (popPacket(packet)) + { + if (!sendAudioPacket(packet)) + closeSocket(); + } + else + { + wait(2); + } + } + + closeSocket(); +} + +bool PrismBridgeProcessor::connectSocket() +{ + socket = std::make_unique(); + if (!socket->connect("127.0.0.1", serverPort, 250)) + { + socket.reset(); + return false; + } + connected.store(true); + controlBuffer.setSize(0); + if (!sendHello(messageHello)) + { + closeSocket(); + return false; + } + reportedDroppedFrames = droppedFrames.load(); + return true; +} + +void PrismBridgeProcessor::closeSocket() +{ + connected.store(false); + selected.store(false); + if (socket) socket->close(); + socket.reset(); +} + +juce::String PrismBridgeProcessor::buildIdentityJson() const +{ + const juce::ScopedLock lock(identityLock); + auto object = new juce::DynamicObject(); + object->setProperty("sourceId", sourceId); + object->setProperty("instanceId", instanceId); + object->setProperty("customName", customName); + object->setProperty("trackName", trackName); + object->setProperty("hostName", hostName); + object->setProperty("sampleRate", currentSampleRate.load()); + object->setProperty("channelCount", getTotalNumInputChannels()); + object->setProperty("droppedFrames", static_cast(droppedFrames.load())); + return juce::JSON::toString(juce::var(object), true); +} + +bool PrismBridgeProcessor::sendHello(uint16_t messageType) +{ + const auto json = buildIdentityJson(); + return sendFrame(messageType, json.toRawUTF8(), static_cast(json.getNumBytesAsUTF8())); +} + +bool PrismBridgeProcessor::sendHeartbeat() +{ + return sendFrame(messageHeartbeat, nullptr, 0); +} + +bool PrismBridgeProcessor::sendAudioPacket(const AudioPacket& packet) +{ + std::array payload {}; + const auto payloadSize = encodeAudioPayload(packet, payload.data(), payload.size()); + return payloadSize > 0 && sendFrame(messageAudio, payload.data(), payloadSize); +} + +size_t PrismBridgeProcessor::encodeAudioPayload( + const AudioPacket& packet, + uint8_t* payload, + size_t capacity) const noexcept +{ + const auto requiredSize = audioHeaderBytes + + static_cast(packet.frameCount) * packet.channelCount * sizeof(float); + if (!payload || packet.frameCount == 0 || packet.frameCount > packetFrames + || (packet.channelCount != 1 && packet.channelCount != 2) || capacity < requiredSize) + return 0; + + std::memset(payload, 0, requiredSize); + writeLittleEndian(payload + 0, packet.sequence); + writeLittleEndian(payload + 4, packet.frameCount); + payload[6] = packet.channelCount; + uint8_t flags = 0; + if (packet.transport.isPlaying) flags |= 1 << 0; + if (packet.transport.isRecording) flags |= 1 << 1; + if (packet.transport.isLooping) flags |= 1 << 2; + if (packet.transport.hasTimeInSamples) flags |= 1 << 3; + if (packet.transport.hasTimeInSeconds) flags |= 1 << 4; + if (packet.transport.hasPpq) flags |= 1 << 5; + if (packet.transport.hasBpm) flags |= 1 << 6; + if (packet.transport.hasTimeSignature) flags |= 1 << 7; + payload[7] = flags; + writeDoubleLittleEndian(payload + 8, packet.sampleRate); + writeLittleEndian(payload + 16, packet.transport.timeInSamples); + writeDoubleLittleEndian(payload + 24, packet.transport.timeInSeconds); + writeDoubleLittleEndian(payload + 32, packet.transport.ppqPosition); + writeDoubleLittleEndian(payload + 40, packet.transport.bpm); + writeDoubleLittleEndian(payload + 48, packet.transport.lastBarPpq); + writeDoubleLittleEndian(payload + 56, packet.transport.loopStartPpq); + writeDoubleLittleEndian(payload + 64, packet.transport.loopEndPpq); + writeLittleEndian(payload + 72, packet.transport.numerator); + writeLittleEndian(payload + 74, packet.transport.denominator); + + size_t offset = audioHeaderBytes; + for (size_t index = 0; index < packet.frameCount; ++index) + { + uint32_t bits = 0; + std::memcpy(&bits, &packet.left[index], sizeof(bits)); + writeLittleEndian(payload + offset, bits); + offset += sizeof(float); + } + if (packet.channelCount > 1) + { + for (size_t index = 0; index < packet.frameCount; ++index) + { + uint32_t bits = 0; + std::memcpy(&bits, &packet.right[index], sizeof(bits)); + writeLittleEndian(payload + offset, bits); + offset += sizeof(float); + } + } + return offset; +} + +bool PrismBridgeProcessor::sendFrame(uint16_t messageType, const void* payload, size_t payloadSize) +{ + if (payloadSize > maxPayloadBytes) return false; + std::array header {}; + writeLittleEndian(header.data() + 0, frameMagic); + writeLittleEndian(header.data() + 4, protocolVersion); + writeLittleEndian(header.data() + 6, messageType); + writeLittleEndian(header.data() + 8, static_cast(payloadSize)); + return writeAll(header.data(), header.size()) + && (payloadSize == 0 || writeAll(payload, payloadSize)); +} + +bool PrismBridgeProcessor::writeAll(const void* source, size_t byteCount) +{ + if (!socket || !socket->isConnected()) return false; + const auto* bytes = static_cast(source); + size_t written = 0; + while (written < byteCount && !threadShouldExit()) + { + if (socket->waitUntilReady(false, 50) < 1) return false; + const int result = socket->write(bytes + written, static_cast(byteCount - written)); + if (result <= 0) return false; + written += static_cast(result); + } + return written == byteCount; +} + +bool PrismBridgeProcessor::readControlMessages() +{ + if (!socket || !socket->isConnected()) return false; + while (socket->waitUntilReady(true, 0) > 0) + { + std::array buffer {}; + const int bytesRead = socket->read(buffer.data(), static_cast(buffer.size()), false); + if (bytesRead <= 0) return false; + controlBuffer.append(buffer.data(), static_cast(bytesRead)); + if (controlBuffer.getSize() > maxPayloadBytes + frameHeaderBytes) return false; + parseControlBuffer(); + } + return true; +} + +void PrismBridgeProcessor::parseControlBuffer() +{ + while (controlBuffer.getSize() >= frameHeaderBytes) + { + const auto* data = static_cast(controlBuffer.getData()); + const uint32_t magic = readU32(data + 0); + const uint16_t version = readU16(data + 4); + const uint16_t messageType = readU16(data + 6); + const uint32_t payloadSize = readU32(data + 8); + if (magic != frameMagic || version != protocolVersion || payloadSize > maxPayloadBytes) + { + closeSocket(); + return; + } + const size_t frameSize = frameHeaderBytes + payloadSize; + if (controlBuffer.getSize() < frameSize) return; + + if (messageType == messageSubscribe) + { + const auto json = juce::String::fromUTF8( + reinterpret_cast(data + frameHeaderBytes), + static_cast(payloadSize)); + const auto parsed = juce::JSON::parse(json); + if (const auto* object = parsed.getDynamicObject()) + selected.store(static_cast(object->getProperty("selected"))); + } + + juce::MemoryBlock remainder; + if (controlBuffer.getSize() > frameSize) + remainder.append(data + frameSize, controlBuffer.getSize() - frameSize); + controlBuffer.swapWith(remainder); + } +} + +void PrismBridgeProcessor::updateTrackProperties(const TrackProperties& properties) +{ + { + const juce::ScopedLock lock(identityLock); + trackName = properties.name.trim().substring(0, 160); + } + identityDirty.store(true); +} + +void PrismBridgeProcessor::setCustomName(const juce::String& value) +{ + { + const juce::ScopedLock lock(identityLock); + const auto nextName = value.trim().substring(0, 160); + if (customName == nextName) return; + customName = nextName; + } + identityDirty.store(true); + updateHostDisplay(ChangeDetails().withNonParameterStateChanged(true)); +} + +juce::String PrismBridgeProcessor::getCustomName() const +{ + const juce::ScopedLock lock(identityLock); + return customName; +} + +juce::String PrismBridgeProcessor::getDisplayName() const +{ + const juce::ScopedLock lock(identityLock); + if (customName.isNotEmpty()) return customName; + if (trackName.isNotEmpty()) return trackName; + return "Bridge " + getInstanceTag(); +} + +juce::String PrismBridgeProcessor::getInstanceTag() const +{ + return instanceId.substring(0, 8).toUpperCase(); +} + +juce::String PrismBridgeProcessor::getHostName() const +{ + const juce::ScopedLock lock(identityLock); + return hostName; +} + +juce::String PrismBridgeProcessor::getTrackName() const +{ + const juce::ScopedLock lock(identityLock); + return trackName; +} + +juce::String PrismBridgeProcessor::getConnectionDescription() const +{ + if (!connected.load()) return "Waiting for Prism"; + return selected.load() ? "Selected in Prism" : "Available in Prism"; +} + +void PrismBridgeProcessor::getStateInformation(juce::MemoryBlock& destination) +{ + const juce::ScopedLock lock(identityLock); + auto object = new juce::DynamicObject(); + object->setProperty("sourceId", sourceId); + object->setProperty("customName", customName); + const auto json = juce::JSON::toString(juce::var(object)); + destination.setSize(0); + destination.append(json.toRawUTF8(), static_cast(json.getNumBytesAsUTF8())); +} + +void PrismBridgeProcessor::setStateInformation(const void* data, int sizeInBytes) +{ + if (!data || sizeInBytes <= 0) return; + const auto parsed = juce::JSON::parse(juce::String::fromUTF8(static_cast(data), sizeInBytes)); + if (const auto* object = parsed.getDynamicObject()) + { + const juce::ScopedLock lock(identityLock); + const auto restoredId = object->getProperty("sourceId").toString(); + if (restoredId.isNotEmpty()) sourceId = restoredId; + customName = object->getProperty("customName").toString().trim().substring(0, 160); + } + identityDirty.store(true); +} + +juce::AudioProcessorEditor* PrismBridgeProcessor::createEditor() +{ + return new PrismBridgeEditor(*this); +} + +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() +{ + return new PrismBridgeProcessor(); +} diff --git a/plugin/Source/BridgeProcessor.h b/plugin/Source/BridgeProcessor.h new file mode 100644 index 0000000..b51995c --- /dev/null +++ b/plugin/Source/BridgeProcessor.h @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include +#include + +class PrismBridgeEditor; + +class PrismBridgeProcessor final : public juce::AudioProcessor, + private juce::Thread +{ +public: + static constexpr int packetFrames = 512; + static constexpr int packetCapacity = 128; + + PrismBridgeProcessor(); + ~PrismBridgeProcessor() override; + + void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + bool isBusesLayoutSupported(const BusesLayout& layouts) const override; + void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; + void updateTrackProperties(const TrackProperties&) override; + + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override { return true; } + const juce::String getName() const override { return JucePlugin_Name; } + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + bool isMidiEffect() const override { return false; } + double getTailLengthSeconds() const override { return 0.0; } + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram(int) override {} + const juce::String getProgramName(int) override { return {}; } + void changeProgramName(int, const juce::String&) override {} + + void getStateInformation(juce::MemoryBlock&) override; + void setStateInformation(const void*, int) override; + + void setCustomName(const juce::String&); + juce::String getCustomName() const; + juce::String getDisplayName() const; + juce::String getInstanceTag() const; + juce::String getInstanceId() const { return instanceId; } + bool isConnectedToPrism() const noexcept { return connected.load(); } + bool isSelectedInPrism() const noexcept { return selected.load(); } + juce::String getHostName() const; + juce::String getTrackName() const; + juce::String getConnectionDescription() const; + uint64_t getDroppedFrameCount() const noexcept { return droppedFrames.load(); } + +#if defined(PRISM_BRIDGE_TESTING) && PRISM_BRIDGE_TESTING + void setSelectedForTesting(bool value) noexcept { selected.store(value); } + int getQueuedPacketCountForTesting() const noexcept { return fifo.getNumReady(); } + void clearQueuedPacketsForTesting() noexcept { discardQueuedPackets(); } +#endif + +private: +#if defined(PRISM_BRIDGE_TESTING) && PRISM_BRIDGE_TESTING +public: +#endif + struct TransportData + { + int64_t timeInSamples = 0; + double timeInSeconds = 0.0; + double ppqPosition = 0.0; + double bpm = 0.0; + double lastBarPpq = 0.0; + double loopStartPpq = 0.0; + double loopEndPpq = 0.0; + int16_t numerator = 4; + int16_t denominator = 4; + bool hasTimeInSamples = false; + bool hasTimeInSeconds = false; + bool hasPpq = false; + bool hasBpm = false; + bool hasTimeSignature = false; + bool isPlaying = false; + bool isRecording = false; + bool isLooping = false; + }; + + struct AudioPacket + { + std::array left {}; + std::array right {}; + TransportData transport; + uint32_t sequence = 0; + uint16_t frameCount = 0; + uint8_t channelCount = 2; + double sampleRate = 48000.0; + }; + +#if defined(PRISM_BRIDGE_TESTING) && PRISM_BRIDGE_TESTING + TransportData offsetTransportForTesting(const TransportData& value, int offset) const noexcept + { + return offsetTransport(value, offset); + } + size_t encodeAudioPayloadForTesting(const AudioPacket& packet, uint8_t* destination, size_t capacity) const noexcept + { + return encodeAudioPayload(packet, destination, capacity); + } +#endif + +private: + + void run() override; + void enqueuePacket(const float*, const float*, int, int, const TransportData&) noexcept; + bool popPacket(AudioPacket&) noexcept; + void discardQueuedPackets() noexcept; + TransportData readTransport() const noexcept; + TransportData offsetTransport(const TransportData&, int sampleOffset) const noexcept; + bool connectSocket(); + void closeSocket(); + bool sendHello(uint16_t messageType = 1); + bool sendHeartbeat(); + bool sendAudioPacket(const AudioPacket&); + size_t encodeAudioPayload(const AudioPacket&, uint8_t*, size_t) const noexcept; + bool readControlMessages(); + bool sendFrame(uint16_t messageType, const void*, size_t); + bool writeAll(const void*, size_t); + void parseControlBuffer(); + juce::String buildIdentityJson() const; + + // AbstractFifo reserves one sentinel slot, so allocate one extra element to + // make packetCapacity the actual writable packet count. + juce::AbstractFifo fifo { packetCapacity + 1 }; + std::array packets; + std::atomic currentSampleRate { 48000.0 }; + std::atomic nextSequence { 0 }; + std::atomic droppedFrames { 0 }; + std::atomic connected { false }; + std::atomic selected { false }; + std::atomic identityDirty { true }; + + mutable juce::CriticalSection identityLock; + juce::String sourceId; + const juce::String instanceId; + juce::String customName; + juce::String trackName; + juce::String hostName; + + std::unique_ptr socket; + juce::MemoryBlock controlBuffer; + uint64_t reportedDroppedFrames = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PrismBridgeProcessor) +}; diff --git a/plugin/Source/LinuxWebViewHelper.cpp b/plugin/Source/LinuxWebViewHelper.cpp new file mode 100644 index 0000000..a643b67 --- /dev/null +++ b/plugin/Source/LinuxWebViewHelper.cpp @@ -0,0 +1,39 @@ +// Loaded as a separate executable, before the plugin or any GTK/GLib libraries. +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + // The dynamic linker caches LD_LIBRARY_PATH at process startup. Unsetting + // it alone is insufficient; re-exec before dlopen to discard host overrides. + // This changes only the browser helper, never the DAW's environment. + if (std::getenv("LD_LIBRARY_PATH") != nullptr || std::getenv("LD_PRELOAD") != nullptr) + { + unsetenv("LD_LIBRARY_PATH"); + unsetenv("LD_PRELOAD"); + execv("/proc/self/exe", argv); + std::perror("Prism WebView: re-exec failed"); + return 1; + } + + if (argc < 3) + return 1; + + auto* library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (library == nullptr) + { + std::fprintf(stderr, "Prism WebView: %s\n", dlerror()); + return 1; + } + + auto* entry = reinterpret_cast(dlsym(library, argv[2])); + if (entry == nullptr) + { + std::fprintf(stderr, "Prism WebView: %s\n", dlerror()); + return 1; + } + + return entry(argc - 3, argv + 3); +} diff --git a/plugin/Source/PluginEditor.cpp b/plugin/Source/PluginEditor.cpp index f63424b..f6d9695 100644 --- a/plugin/Source/PluginEditor.cpp +++ b/plugin/Source/PluginEditor.cpp @@ -1,4 +1,5 @@ #include "PluginEditor.h" +#include #include "SpectrumEngine.h" #include "OscilloscopeEngine.h" #include "VUMeterEngine.h" @@ -6,6 +7,7 @@ #include "VectorscopeEngine.h" #include "SpectrogramEngine.h" #include "WaveformEngine.h" +#include "WaterfallEngine.h" #include #include #include @@ -88,9 +90,13 @@ namespace "\n"); } - juce::String wrapperTypeName() + juce::String wrapperTypeName(const PrismSpectrumProcessor& processor) { - switch (juce::PluginHostType::getPluginLoadedAs()) +#if defined(HAS_CLAP_JUCE_EXTENSIONS) && HAS_CLAP_JUCE_EXTENSIONS + if (processor.is_clap) + return "CLAP"; +#endif + switch (processor.wrapperType) { case juce::AudioProcessor::wrapperType_VST3: return "VST3"; case juce::AudioProcessor::wrapperType_VST: return "VST2"; @@ -104,13 +110,13 @@ namespace } } - juce::String linuxHostDisplayContext() + juce::String linuxHostDisplayContext(const PrismSpectrumProcessor& processor) { const juce::PluginHostType host; juce::String context; context << "mode=" << PRISM_LINUX_UI_MODE_NAME << " host=\"" << host.getHostDescription() << "\"" - << " wrapper=" << wrapperTypeName() + << " wrapper=" << wrapperTypeName(processor) << " display=" << envValue("DISPLAY") << " waylandDisplay=" << envValue("WAYLAND_DISPLAY") << " sessionType=" << envValue("XDG_SESSION_TYPE") @@ -131,7 +137,9 @@ namespace std::unique_ptr makeEngine() { -#if defined(PRISM_SCOPE_WAVEFORM) && PRISM_SCOPE_WAVEFORM +#if defined(PRISM_SCOPE_WATERFALL) && PRISM_SCOPE_WATERFALL + return std::make_unique(); +#elif defined(PRISM_SCOPE_WAVEFORM) && PRISM_SCOPE_WAVEFORM return std::make_unique(); #elif defined(PRISM_SCOPE_SPECTROGRAM) && PRISM_SCOPE_SPECTROGRAM return std::make_unique(); @@ -269,16 +277,25 @@ namespace }; #endif - juce::WebBrowserComponent::Options makeWebOptions(PrismSpectrumEditor& editor, const char* scopeId) + juce::WebBrowserComponent::Options makeWebOptions(PrismSpectrumEditor& editor, PrismSpectrumProcessor& processor, + const char* scopeId, std::shared_ptr browser) { + const juce::Component::SafePointer safe(&editor); auto options = juce::WebBrowserComponent::Options{} .withNativeIntegrationEnabled() .withInitialisationData("prismScope", juce::String(scopeId)) - .withEventListener("prismConfig", [&editor](juce::var v) { editor.onPrismConfig(std::move(v)); }) - .withEventListener("prismReady", [&editor](juce::var) { editor.onPrismReady(); }) - .withEventListener("prismSpectrogramConfig", [&editor](juce::var v) { editor.onScopeNativeConfig(std::move(v)); }) - .withEventListener("prismScopeMeasurement", [&editor](juce::var v) { editor.onScopeMeasurement(std::move(v)); }) - .withEventListener("prismSettingsPanel", [&editor](juce::var v) { editor.onSettingsPanel(std::move(v)); }); + .withEventListener("prismReferenceCommand", [safe, &processor, browser](juce::var v) { + if (safe) safe->onReferenceCommand(std::move(v)); + else processor.handleReferenceTransfer(std::move(v), *browser); + }) + .withEventListener("prismSpectrumReferenceConfig", [safe](juce::var v) { if (safe) safe->onScopeNativeConfig(std::move(v)); }) + .withEventListener("prismConfig", [safe](juce::var v) { if (safe) safe->onPrismConfig(std::move(v)); }) + .withEventListener("prismReady", [safe](juce::var) { if (safe) safe->onPrismReady(); }) + .withEventListener("prismSpectrogramConfig", [safe](juce::var v) { if (safe) safe->onScopeNativeConfig(std::move(v)); }) + .withEventListener("prismWaterfallConfig", [safe](juce::var v) { if (safe) safe->onScopeNativeConfig(std::move(v)); }) + .withEventListener("prismScopeMeasurement", [safe](juce::var v) { if (safe) safe->onScopeMeasurement(std::move(v)); }) + .withEventListener("prismSettingsPanel", [safe](juce::var v) { if (safe) safe->onSettingsPanel(std::move(v)); }); + if (juce::String(scopeId) == "spectrum") options = options.withKeepPageLoadedWhenBrowserIsHidden(); #if JUCE_WINDOWS options = options.withBackend(juce::WebBrowserComponent::Options::Backend::webview2); @@ -318,7 +335,7 @@ PrismSpectrumEditor::PrismSpectrumEditor(PrismSpectrumProcessor& p) #if JUCE_LINUX #if PRISM_LINUX_UI_DIAGNOSTICS logLinuxDiagnostic("editor constructed scope=" + juce::String(engine->scopeId()) - + " " + linuxHostDisplayContext() + + " " + linuxHostDisplayContext(processorRef) + " logFile=" + linuxDiagnosticLogFile().getFullPathName()); #endif #endif @@ -334,6 +351,11 @@ PrismSpectrumEditor::PrismSpectrumEditor(PrismSpectrumProcessor& p) PrismSpectrumEditor::~PrismSpectrumEditor() { + webViewReady = false; + if (webView && processorRef.referenceTracks.activeUploadId().isNotEmpty()) { + if (auto* parent = webView->getParentComponent()) parent->removeChildComponent(webView.get()); + processorRef.retainReferenceTransfer(std::move(webView)); + } #if JUCE_WINDOWS || JUCE_LINUX stopTimer(); #endif @@ -367,7 +389,7 @@ void PrismSpectrumEditor::loadUi() { #if JUCE_LINUX #if PRISM_LINUX_UI_DIAGNOSTICS - logLinuxDiagnostic("loadUi " + linuxHostDisplayContext()); + logLinuxDiagnostic("loadUi " + linuxHostDisplayContext(processorRef)); #endif #if defined(PRISM_LINUX_UI_MODE_NATIVE_SMOKE) && PRISM_LINUX_UI_MODE_NATIVE_SMOKE @@ -387,7 +409,7 @@ void PrismSpectrumEditor::loadUi() bool PrismSpectrumEditor::createWebView() { - auto options = makeWebOptions(*this, engine->scopeId()); + auto options = makeWebOptions(*this, processorRef, engine->scopeId(), referenceBrowserLink); #if JUCE_WINDOWS if (! juce::WebBrowserComponent::areOptionsSupported(options)) @@ -404,6 +426,7 @@ bool PrismSpectrumEditor::createWebView() #endif webView = std::make_unique(options); + *referenceBrowserLink = webView.get(); return true; } @@ -596,7 +619,12 @@ void PrismSpectrumEditor::onPrismReady() #endif #endif webViewReady = true; +#if defined(PRISM_SCOPE_WATERFALL) && PRISM_SCOPE_WATERFALL + // Samples buffered while the editor was closed are not current history. + processorRef.restartAudioHistory(); +#endif pushRestoreSettings(); + pushReferenceState(); sendAppDefaults(); startFrameDriver(); } @@ -686,6 +714,8 @@ void PrismSpectrumEditor::sendAppDefaults() void PrismSpectrumEditor::renderFrame() { + if (processorRef.syncReferenceResult()) pushRestoreSettings(); + pushReferenceState(); if (webView == nullptr || ! webViewReady) return; @@ -706,8 +736,19 @@ void PrismSpectrumEditor::renderFrame() } const int drained = processorRef.drainStereo(drainLeft.data(), drainRight.data(), (int) drainLeft.size()); + if (juce::String(engine->scopeId()) == "spectrum" && processorRef.consumeAudioDiscontinuity()) + engine->resetAudioHistory(); -#if JUCE_LINUX +#if defined(PRISM_SCOPE_WATERFALL) && PRISM_SCOPE_WATERFALL + if (processorRef.consumeAudioDiscontinuity()) + { + engine->resetAudioHistory(); + webView->emitEventIfBrowserIsVisible(engine->frameEventId(), engine->buildFrame(sampleRate)); + return; + } +#endif + +#if JUCE_LINUX && !defined(PRISM_SCOPE_WATERFALL) if (drained <= 0) return; #endif @@ -716,3 +757,44 @@ void PrismSpectrumEditor::renderFrame() webView->emitEventIfBrowserIsVisible(engine->frameEventId(), engine->buildFrame(sampleRate)); } + +void PrismSpectrumEditor::pushReferenceState() +{ + if (webView == nullptr || !webViewReady || juce::String(engine->scopeId()) != "spectrum") return; + const auto revision = processorRef.referenceTracks.revision(); + if (revision == referenceStateRevision) return; + referenceStateRevision = revision; + const auto state = processorRef.referenceTracks.snapshot(); + webView->emitEventIfBrowserIsVisible("prismReferenceState", state); +} + +void PrismSpectrumEditor::onReferenceCommand(juce::var payload) +{ + if (juce::String(engine->scopeId()) != "spectrum") return; + if (payload.getProperty("action", "").toString() != "choose") { + processorRef.handleReferenceTransfer(std::move(payload), webView.get()); + pushReferenceState(); + return; + } + auto* response = new juce::DynamicObject(); + response->setProperty("requestId", payload.getProperty("requestId", juce::var())); + try { + auto& tracks = processorRef.referenceTracks; + { + const auto generation = tracks.generation(); + referenceChooser = std::make_unique("Load spectrum reference", juce::File(), "*.wav;*.wave;*.aif;*.aiff;*.aifc;*.flac;*.mp3"); + referenceChooser->launchAsync(juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles, + [safe = juce::Component::SafePointer(this), generation](const juce::FileChooser& chooser) { + if (!safe || !chooser.getResult().existsAsFile() || generation != safe->processorRef.referenceTracks.generation()) return; + safe->processorRef.referenceTracks.start(chooser.getResult()); + safe->pushReferenceState(); + }); + } + response->setProperty("ok", true); + } catch (const std::exception& error) { + response->setProperty("ok", false); response->setProperty("error", juce::String::fromUTF8(error.what())); + processorRef.referenceTracks.cancelUpload(payload.getProperty("uploadId", "").toString()); + } + if (webView != nullptr) webView->emitEventIfBrowserIsVisible("prismReferenceResponse", juce::var(response)); + pushReferenceState(); +} diff --git a/plugin/Source/PluginEditor.h b/plugin/Source/PluginEditor.h index a3980c5..a7dee7b 100644 --- a/plugin/Source/PluginEditor.h +++ b/plugin/Source/PluginEditor.h @@ -32,6 +32,8 @@ class PrismSpectrumEditor : public juce::AudioProcessorEditor // Called by the webview event listeners (message thread). void onPrismConfig(juce::var payload); void onPrismReady(); + void onReferenceCommand(juce::var); + void pushReferenceState(); // Scope-specific native config (e.g. the spectrogram's canvas-derived rowCount). void onScopeNativeConfig(juce::var payload); @@ -70,6 +72,8 @@ class PrismSpectrumEditor : public juce::AudioProcessorEditor PrismSpectrumProcessor& processorRef; std::unique_ptr engine; + std::unique_ptr referenceChooser; + uint64_t referenceStateRevision = UINT64_MAX; std::vector drainLeft, drainRight; double lastSampleRate = 0.0; @@ -83,6 +87,7 @@ class PrismSpectrumEditor : public juce::AudioProcessorEditor int uncapAttempts = 0; std::unique_ptr webView; + std::shared_ptr referenceBrowserLink = std::make_shared(nullptr); #if JUCE_LINUX std::unique_ptr floatingWebViewWindow; #endif diff --git a/plugin/Source/PluginProcessor.cpp b/plugin/Source/PluginProcessor.cpp index 1a8c299..7a5a742 100644 --- a/plugin/Source/PluginProcessor.cpp +++ b/plugin/Source/PluginProcessor.cpp @@ -1,6 +1,52 @@ #include "PluginProcessor.h" #include "PluginEditor.h" #include +#include + +PrismSpectrumProcessor::~PrismSpectrumProcessor() { + stopTimer(); + referenceTracks.cancel(); + detachedTransfers.clear(); +} + +void PrismSpectrumProcessor::retainReferenceTransfer(std::unique_ptr browser) { + const auto id = referenceTracks.activeUploadId(); + if (id.isEmpty() || !browser) return; + // Keep the File/Blob and its acknowledged reads alive until the native temp + // file is complete, even if the DAW destroys the visible editor meanwhile. + auto window = std::make_unique("Prism reference import", juce::Colours::black, 0); + window->setContentOwned(browser.release(), false); + window->setSize(1, 1); + detachedTransfers.push_back({ id, std::move(window) }); + startTimer(100); +} + +void PrismSpectrumProcessor::timerCallback() { + const auto active = referenceTracks.activeUploadId(); + detachedTransfers.erase(std::remove_if(detachedTransfers.begin(), detachedTransfers.end(), + [&active](const auto& transfer) { return transfer.id != active; }), detachedTransfers.end()); + if (detachedTransfers.empty()) stopTimer(); +} + +void PrismSpectrumProcessor::handleReferenceTransfer(juce::var payload, juce::WebBrowserComponent* browser) { + auto* response = new juce::DynamicObject(); + response->setProperty("requestId", payload.getProperty("requestId", juce::var())); + try { + const auto action = payload.getProperty("action", "").toString(); + if (action == "getState") response->setProperty("state", referenceTracks.snapshot()); + else if (action == "cancel") referenceTracks.cancel(); + else if (action == "cancelUpload") referenceTracks.cancelUpload(payload.getProperty("uploadId", "").toString()); + else if (action == "beginUpload") response->setProperty("uploadId", referenceTracks.beginUpload(payload.getProperty("name", "audio").toString(), static_cast(payload.getProperty("size", 0)))); + else if (action == "appendUpload") referenceTracks.appendUpload(payload.getProperty("uploadId", "").toString(), static_cast(payload.getProperty("offset", 0)), payload.getProperty("data", "").toString()); + else if (action == "finishUpload") referenceTracks.finishUpload(payload.getProperty("uploadId", "").toString()); + else throw std::runtime_error("Unknown reference command."); + response->setProperty("ok", true); + } catch (const std::exception& error) { + response->setProperty("ok", false); response->setProperty("error", juce::String::fromUTF8(error.what())); + referenceTracks.cancelUpload(payload.getProperty("uploadId", "").toString()); + } + if (browser) browser->emitEventIfBrowserIsVisible("prismReferenceResponse", juce::var(response)); +} PrismSpectrumProcessor::PrismSpectrumProcessor() : juce::AudioProcessor(BusesProperties() @@ -15,6 +61,7 @@ void PrismSpectrumProcessor::prepareToPlay(double sampleRate, int) { currentSampleRate.store(sampleRate); fifo.reset(); + audioDiscontinuity.store(true); } bool PrismSpectrumProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const @@ -42,6 +89,7 @@ void PrismSpectrumProcessor::pushStereoToFifo(const float* left, const float* ri std::memcpy(rightBuffer.data() + start2, right + size1, (size_t) size2 * sizeof(float)); } fifo.finishedWrite(size1 + size2); + if (size1 + size2 < num) audioDiscontinuity.store(true); } int PrismSpectrumProcessor::drainStereo(float* destLeft, float* destRight, int maxSamples) noexcept @@ -82,18 +130,42 @@ void PrismSpectrumProcessor::processBlock(juce::AudioBuffer& buffer, juce void PrismSpectrumProcessor::setSettingsJson(const juce::String& json) { const juce::ScopedLock sl(settingsLock); - settingsJson = json; + auto settings = juce::JSON::parse(json); + if (settings.isObject() && settings.hasProperty("reference")) { + settings.getDynamicObject()->setProperty("reference", normalizeSpectrumReference(settings.getProperty("reference", juce::var()))); + settingsJson = juce::JSON::toString(settings); + } else settingsJson = json; } -juce::String PrismSpectrumProcessor::getSettingsJson() const +juce::String PrismSpectrumProcessor::getSettingsJson() { + syncReferenceResult(); const juce::ScopedLock sl(settingsLock); return settingsJson; } +bool PrismSpectrumProcessor::syncReferenceResult() +{ + const juce::ScopedLock sl(settingsLock); + auto asset = referenceTracks.takeCompletedAsset(); + if (!asset.isObject()) return false; + auto settings = juce::JSON::parse(settingsJson); + if (!settings.isObject()) settings = juce::var(new juce::DynamicObject()); + const auto previous = settings.getProperty("reference", juce::var()); + auto* reference = new juce::DynamicObject(); + reference->setProperty("asset", asset); reference->setProperty("trimDb", 0.0); + reference->setProperty("view", previous.getProperty("view", "overlay")); + settings.getDynamicObject()->setProperty("reference", juce::var(reference)); + settingsJson = juce::JSON::toString(settings); + return true; +} + void PrismSpectrumProcessor::getStateInformation(juce::MemoryBlock& destData) { - const juce::String json = getSettingsJson(); + // CLAP's state loader requires a nonempty stream, including before the + // editor has supplied any settings. Keep the existing JSON state format. + const auto settings = getSettingsJson(); + const juce::String json = settings.isEmpty() ? "{}" : settings; destData.setSize(0); destData.append(json.toRawUTF8(), json.getNumBytesAsUTF8()); } @@ -102,6 +174,8 @@ void PrismSpectrumProcessor::setStateInformation(const void* data, int sizeInByt { if (data == nullptr || sizeInBytes <= 0) return; + referenceTracks.cancel(); + audioDiscontinuity.store(true); setSettingsJson(juce::String::fromUTF8(static_cast(data), sizeInBytes)); // If the editor is already open (host restored state after opening it), push diff --git a/plugin/Source/PluginProcessor.h b/plugin/Source/PluginProcessor.h index bc234ab..7a15e15 100644 --- a/plugin/Source/PluginProcessor.h +++ b/plugin/Source/PluginProcessor.h @@ -1,9 +1,15 @@ #pragma once #include +#include +#include "ReferenceTrackManager.h" #include #include +#if defined(HAS_CLAP_JUCE_EXTENSIONS) && HAS_CLAP_JUCE_EXTENSIONS + #include +#endif + /** * Prism Spectrum — analyzer plugin. * @@ -15,11 +21,14 @@ * UI settings (JSON) are owned here so they survive editor open/close and DAW * session save/restore. */ -class PrismSpectrumProcessor : public juce::AudioProcessor +class PrismSpectrumProcessor : public juce::AudioProcessor, private juce::Timer +#if defined(HAS_CLAP_JUCE_EXTENSIONS) && HAS_CLAP_JUCE_EXTENSIONS + , public clap_juce_extensions::clap_properties +#endif { public: PrismSpectrumProcessor(); - ~PrismSpectrumProcessor() override = default; + ~PrismSpectrumProcessor() override; void prepareToPlay(double sampleRate, int samplesPerBlock) override; void releaseResources() override {} @@ -49,16 +58,27 @@ class PrismSpectrumProcessor : public juce::AudioProcessor /** Copy up to `maxSamples` of buffered L/R audio into the destinations; returns count. */ int drainStereo(float* destLeft, float* destRight, int maxSamples) noexcept; + bool consumeAudioDiscontinuity() noexcept { return audioDiscontinuity.exchange(false); } + void restartAudioHistory() noexcept { audioDiscontinuity.store(true); } + /** Persisted UI settings as a JSON string (set from the editor, read on save). */ void setSettingsJson(const juce::String& json); - juce::String getSettingsJson() const; + juce::String getSettingsJson(); + bool syncReferenceResult(); + ReferenceTrackManager referenceTracks; + void handleReferenceTransfer(juce::var payload, juce::WebBrowserComponent* browser); + void retainReferenceTransfer(std::unique_ptr browser); private: + void timerCallback() override; + struct DetachedTransfer { juce::String id; std::unique_ptr window; }; + std::vector detachedTransfers; void pushStereoToFifo(const float* left, const float* right, int num) noexcept; juce::AbstractFifo fifo { 1 << 16 }; std::vector leftBuffer, rightBuffer; // backing storage for `fifo` std::atomic currentSampleRate { 48000.0 }; + std::atomic audioDiscontinuity { true }; juce::CriticalSection settingsLock; juce::String settingsJson; diff --git a/plugin/Source/ReferenceTrackManager.cpp b/plugin/Source/ReferenceTrackManager.cpp new file mode 100644 index 0000000..1e623bd --- /dev/null +++ b/plugin/Source/ReferenceTrackManager.cpp @@ -0,0 +1,181 @@ +#include "ReferenceTrackManager.h" +#include +#include +#include + +juce::var normalizeSpectrumReference(const juce::var& reference) { + const auto value = reference.getProperty("asset", juce::var()); + const auto numeric = [&value](const char* key, double min, double max) { + const auto number = value.getProperty(key, juce::var()); + const auto n = static_cast(number); + return (number.isDouble() || number.isInt() || number.isInt64()) && std::isfinite(n) && n >= min && n <= max; + }; + const auto id = value.getProperty("id", "").toString(), name = value.getProperty("name", "").toString(); + if (!value.isObject() || !numeric("version", 1, 1) || !numeric("sampleRate", 48000, 48000) + || !numeric("sourceNyquistHz", 4000, 192000) || !numeric("durationSeconds", 1.0 / 48000, 1e12) + || !numeric("meanSquare", 0, 1e12) || id.isEmpty() || id.length() > 128 || name.isEmpty() || name.length() > 1024) + return {}; + const auto curves = value.getProperty("curves", juce::var()); + auto* cleanCurves = new juce::DynamicObject(); + juce::var curveOwner(cleanCurves); + for (const int size : Visualizer::referenceFFTSizes) { + const juce::Identifier key { juce::String(size) }; + const auto encoded = curves.getProperty(key, juce::var()).toString(); + juce::MemoryOutputStream bytes; + if (encoded.length() != ((size * 2 + 2) / 3) * 4 || !juce::Base64::convertFromBase64(bytes, encoded) + || bytes.getDataSize() != static_cast(size * 2)) return {}; + for (size_t i = 0; i < bytes.getDataSize(); i += 4) { + const auto bits = juce::ByteOrder::littleEndianInt(static_cast(bytes.getData()) + i); + float power; std::memcpy(&power, &bits, 4); + if (!std::isfinite(power) || power < 0 || power > 1e12f) return {}; + } + cleanCurves->setProperty(key, encoded); + } + auto* cleanAsset = new juce::DynamicObject(); + for (const auto* key : { "version", "id", "name", "sampleRate", "sourceNyquistHz", "durationSeconds", "meanSquare" }) + cleanAsset->setProperty(key, value.getProperty(key, juce::var())); + cleanAsset->setProperty("curves", curveOwner); + auto* result = new juce::DynamicObject(); + result->setProperty("asset", juce::var(cleanAsset)); + const double trim = static_cast(reference.getProperty("trimDb", 0.0)); + result->setProperty("trimDb", std::isfinite(trim) ? std::round(juce::jlimit(-24.0, 24.0, trim) * 10) / 10 : 0.0); + result->setProperty("view", reference.getProperty("view", "overlay").toString() == "difference" ? "difference" : "overlay"); + return juce::var(result); +} + +namespace { +juce::var state(const juce::String& phase, const juce::String& id = {}, const juce::String& name = {}) { + auto* value = new juce::DynamicObject(); + value->setProperty("phase", phase); + value->setProperty("jobId", id.isEmpty() ? juce::var() : juce::var(id)); + value->setProperty("name", name); + value->setProperty("progress", juce::var()); value->setProperty("preview", juce::var()); + value->setProperty("result", juce::var()); value->setProperty("error", juce::var()); + return juce::var(value); +} +juce::var asset(const Visualizer::ReferenceCurve& curve, const juce::String& id, const juce::String& name) { + auto* value = new juce::DynamicObject(); + value->setProperty("version", 1); value->setProperty("id", id); value->setProperty("name", name.substring(0, 1024)); + value->setProperty("sampleRate", Visualizer::referenceSampleRate); + value->setProperty("sourceNyquistHz", curve.sourceNyquistHz); + value->setProperty("durationSeconds", curve.durationSeconds); value->setProperty("meanSquare", curve.meanSquare); + auto* curves = new juce::DynamicObject(); + for (size_t i = 0; i < curve.powers.size(); ++i) + curves->setProperty(juce::Identifier(juce::String(Visualizer::referenceFFTSizes[i])), + juce::Base64::toBase64(curve.powers[i].data(), curve.powers[i].size() * sizeof(float))); + value->setProperty("curves", juce::var(curves)); + return juce::var(value); +} +} + +ReferenceTrackManager::~ReferenceTrackManager() { + cancel(); + for (auto& worker : workers) worker.job->cancelled.store(true); + for (auto& worker : workers) if (worker.thread.joinable()) worker.thread.join(); +} +void ReferenceTrackManager::cleanUpload() { + uploadStream.reset(); + if (uploadFile != juce::File()) uploadFile.deleteFile(); + uploadFile = juce::File(); uploadName = {}; uploadId = {}; uploadWritten = uploadSize = 0; uploadState = juce::var(); +} +void ReferenceTrackManager::cancel() { + std::lock_guard ownerLock(ownerMutex); + ++ownerGeneration; + if (current) current->cancelled.store(true); + current.reset(); cleanUpload(); + ++*workerRevision; +} +void ReferenceTrackManager::start(const juce::File& file, const juce::String& name, bool temporary) { + std::lock_guard ownerLock(ownerMutex); + cancel(); + for (auto it = workers.begin(); it != workers.end();) { + if (it->job->done.load()) { it->thread.join(); it = workers.erase(it); } else ++it; + } + auto job = std::make_shared(); + const auto id = juce::Uuid().toString(), label = name.isEmpty() ? file.getFileName() : name; + job->state = state("opening", id, label); job->revision = workerRevision; + current = job; ++*workerRevision; + workers.push_back({ job, std::thread([job, file, temporary, id, label] { + const auto publish = [job](juce::var next) { + if (job->cancelled.load()) return; + { std::lock_guard lock(job->mutex); job->state = std::move(next); } + ++*job->revision; + }; + try { + const auto result = Visualizer::analyzeReferenceFile(file.getFullPathName().toStdString(), job->cancelled, + [&](double fraction, const Visualizer::ReferenceCurve& curve) { + auto next = state("analyzing", id, label); + next.getDynamicObject()->setProperty("progress", fraction >= 0 ? juce::var(fraction) : juce::var()); + if (curve.durationSeconds > 0) next.getDynamicObject()->setProperty("preview", asset(curve, id, label)); + publish(next); + }); + auto next = state("ready", id, label); + next.getDynamicObject()->setProperty("progress", 1.0); + next.getDynamicObject()->setProperty("result", asset(result, id, label)); + publish(next); + } catch (const std::exception& error) { + auto next = state("error", id, label); next.getDynamicObject()->setProperty("error", juce::String::fromUTF8(error.what())); + publish(next); + } + if (temporary) file.deleteFile(); + job->done.store(true); + }) }); +} +juce::var ReferenceTrackManager::snapshot() const { + std::lock_guard ownerLock(ownerMutex); + if (uploadState.isObject()) return uploadState; + if (!current) return state("idle"); + std::lock_guard lock(current->mutex); + return current->state; +} +juce::var ReferenceTrackManager::takeCompletedAsset() { + std::lock_guard ownerLock(ownerMutex); + if (!current) return {}; + std::lock_guard lock(current->mutex); + if (current->committed || current->state.getProperty("phase", {}).toString() != "ready") return {}; + current->committed = true; + return current->state.getProperty("result", {}); +} +juce::String ReferenceTrackManager::beginUpload(const juce::String& name, int64_t bytes) { + std::lock_guard ownerLock(ownerMutex); + cancel(); + if (bytes <= 0) throw std::runtime_error("Choose a non-empty audio file."); + uploadFile = juce::File::getSpecialLocation(juce::File::tempDirectory) + .getNonexistentChildFile("prism-reference-" + juce::Uuid().toString(), ".audio", false); + uploadStream = uploadFile.createOutputStream(); + if (!uploadStream || !uploadStream->openedOk()) { cleanUpload(); throw std::runtime_error("Could not create a temporary audio file."); } + uploadName = name.substring(0, 1024); uploadSize = bytes; uploadWritten = 0; + uploadId = juce::Uuid().toString(); + uploadState = state("opening", uploadId, uploadName); ++*workerRevision; + return uploadId; +} +void ReferenceTrackManager::cancelUpload(const juce::String& id) { + std::lock_guard ownerLock(ownerMutex); + if (id.isNotEmpty() && id == uploadId) cancel(); +} +juce::String ReferenceTrackManager::activeUploadId() const { + std::lock_guard ownerLock(ownerMutex); + return uploadStream ? uploadId : juce::String(); +} +void ReferenceTrackManager::appendUpload(const juce::String& id, int64_t offset, const juce::String& base64) { + std::lock_guard ownerLock(ownerMutex); + if (id.isEmpty() || id != uploadId) throw std::runtime_error("This audio transfer was superseded."); + if (!uploadStream || offset != uploadWritten || base64.length() > 349528) throw std::runtime_error("The audio transfer was interrupted."); + juce::MemoryOutputStream bytes; + if (!juce::Base64::convertFromBase64(bytes, base64) || bytes.getDataSize() > 262144 + || bytes.getDataSize() == 0 || uploadWritten + static_cast(bytes.getDataSize()) > uploadSize) + throw std::runtime_error("Invalid audio transfer chunk."); + if (!uploadStream->write(bytes.getData(), bytes.getDataSize())) throw std::runtime_error("Could not write the temporary audio file."); + uploadWritten += static_cast(bytes.getDataSize()); +} +void ReferenceTrackManager::finishUpload(const juce::String& id) { + std::lock_guard ownerLock(ownerMutex); + if (id.isEmpty() || id != uploadId) throw std::runtime_error("This audio transfer was superseded."); + if (!uploadStream || uploadWritten != uploadSize) throw std::runtime_error("The audio transfer is incomplete."); + uploadStream->flush(); + if (uploadStream->getStatus().failed()) throw std::runtime_error("Could not finish the temporary audio file."); + uploadStream.reset(); + const auto file = uploadFile; const auto name = uploadName; + uploadFile = juce::File(); uploadState = juce::var(); + start(file, name, true); +} diff --git a/plugin/Source/ReferenceTrackManager.h b/plugin/Source/ReferenceTrackManager.h new file mode 100644 index 0000000..2e46b64 --- /dev/null +++ b/plugin/Source/ReferenceTrackManager.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include "reference_analysis.h" +#include +#include +#include +#include + +juce::var normalizeSpectrumReference(const juce::var& reference); + +/** Processor-owned jobs. Workers touch only their own state, never an editor or audio callback. */ +class ReferenceTrackManager { +public: + ReferenceTrackManager() = default; + ~ReferenceTrackManager(); + void start(const juce::File& file, const juce::String& name = {}, bool temporary = false); + void cancel(); + juce::var snapshot() const; + juce::var takeCompletedAsset(); + uint64_t revision() const { return workerRevision->load(); } + uint64_t generation() const { return ownerGeneration.load(); } + juce::String beginUpload(const juce::String& name, int64_t bytes); + void appendUpload(const juce::String& id, int64_t offset, const juce::String& base64); + void finishUpload(const juce::String& id); + void cancelUpload(const juce::String& id); + juce::String activeUploadId() const; +private: + mutable std::recursive_mutex ownerMutex; + std::atomic ownerGeneration { 0 }; + struct Job { + std::atomic cancelled { false }, done { false }; + mutable std::mutex mutex; + juce::var state; + bool committed = false; + std::shared_ptr> revision; + }; + struct Worker { std::shared_ptr job; std::thread thread; }; + std::vector workers; + std::shared_ptr current; + std::shared_ptr> workerRevision = std::make_shared>(0); + juce::File uploadFile; + std::unique_ptr uploadStream; + juce::String uploadName, uploadId; + int64_t uploadSize = 0, uploadWritten = 0; + juce::var uploadState; + void cleanUpload(); +}; diff --git a/plugin/Source/ScopeEngine.h b/plugin/Source/ScopeEngine.h index 3558067..7f6a307 100644 --- a/plugin/Source/ScopeEngine.h +++ b/plugin/Source/ScopeEngine.h @@ -44,6 +44,9 @@ class ScopeEngine /** Temporary UI interaction state. Most scopes do not need DSP changes. */ virtual void setMeasurementActive(bool) {} + /** Audio was interrupted or discarded. History scopes must start a new span. */ + virtual void resetAudioHistory() {} + /** Feed audio (called off the realtime thread). numSamples may be 0. */ virtual void process(const float* left, const float* right, int numSamples) = 0; diff --git a/plugin/Source/SpectrumEngine.h b/plugin/Source/SpectrumEngine.h index 5c1db0b..4f83613 100644 --- a/plugin/Source/SpectrumEngine.h +++ b/plugin/Source/SpectrumEngine.h @@ -27,6 +27,12 @@ class SpectrumEngine : public ScopeEngine applySmoothing(); } + void configureNative(const juce::var& config) override + { + spectrum.setReferenceEnabled((bool) config.getProperty("referenceEnabled", false)); + } + void resetAudioHistory() override { spectrum.reset(); } + void setMeasurementActive(bool active) override { if (measurementActive == active) @@ -48,6 +54,10 @@ class SpectrumEngine : public ScopeEngine { auto* obj = new juce::DynamicObject(); obj->setProperty("sampleRate", sampleRate); + obj->setProperty("hasSpectrumData", spectrum.hasSpectrumData()); + obj->setProperty("referenceEnabled", spectrum.isReferenceEnabled()); + obj->setProperty("referenceMeanSquare", spectrum.getReferenceMeanSquare()); + obj->setProperty("referenceSeconds", spectrum.getReferenceSeconds()); obj->setProperty("magnitudes", toBase64(spectrum.getMagnitudes())); obj->setProperty("side", toBase64(spectrum.getSideMagnitudes())); obj->setProperty("channelMax", toBase64(spectrum.getChannelMaxMagnitudes())); diff --git a/plugin/Source/WaterfallEngine.h b/plugin/Source/WaterfallEngine.h new file mode 100644 index 0000000..847919d --- /dev/null +++ b/plugin/Source/WaterfallEngine.h @@ -0,0 +1,102 @@ +#pragma once + +#include "ScopeEngine.h" +#include "waterfall.h" +#include +#include + +// The native analyzer keeps the 60 Hz audio timeline. Only the visible ridges +// and frequency columns cross into the webview; paint rate never samples history. +class WaterfallEngine : public ScopeEngine +{ +public: + const char* scopeId() const override { return "waterfall"; } + juce::Identifier frameEventId() const override { return { "waterfallFrame" }; } + PreferredSize preferredSize() const override { return { 760, 360, 360, 180 }; } + + void setSampleRate(double rate) override + { + if (std::isfinite(rate) && rate > 0 && (float) rate != config.sampleRate) + { + config.sampleRate = (float) rate; + analyzer.configure(config); + } + } + + void configure(const juce::var& settings) override + { + if (! settings.isObject()) return; + applyAnalysisSettings(settings); + const bool audible = settings.getProperty("frequencyRangeMode", "extended").toString() == "audible"; + config.minFrequency = audible ? 20.0f : 10.0f; + config.maxFrequency = audible ? 20000.0f : 24000.0f; + analyzer.configure(config); + } + + void configureNative(const juce::var& payload) override + { + if (! payload.isObject()) return; + revision = (int) payload.getProperty("revision", revision); + if ((bool) payload.getProperty("reset", false)) analyzer.reset(); + const auto options = payload.getProperty("config", juce::var()); + if (options.isObject()) + { + applyAnalysisSettings(options); + // Host sample rate always wins over the webview's initial estimate. + config.minFrequency = number(options, "minFrequency", config.minFrequency); + config.maxFrequency = number(options, "maxFrequency", config.maxFrequency); + analyzer.configure(config); + } + ridges = (size_t) juce::jlimit(2, 64, (int) payload.getProperty("ridges", (int) ridges)); + columns = (size_t) juce::jlimit(2, 512, (int) payload.getProperty("columns", (int) columns)); + } + + void resetAudioHistory() override { analyzer.reset(); } + + void process(const float* left, const float* right, int count) override + { + if (count > 0) analyzer.processStereo(left, right, (size_t) count); + } + + juce::var buildFrame(double) override + { + const auto frame = analyzer.getFrame(ridges, columns); + auto* object = new juce::DynamicObject(); + object->setProperty("sampleRate", config.sampleRate); + object->setProperty("revision", revision); + object->setProperty("columns", (int) frame.columns); + object->setProperty("audioSeconds", frame.audioSeconds); + object->setProperty("levels", encode(frame.levels)); + object->setProperty("ages", encode(frame.ages)); + object->setProperty("frequencies", encode(frame.frequencies)); + return juce::var(object); + } + +private: + static float number(const juce::var& object, const char* key, float fallback) + { + const auto value = object.getProperty(key, fallback); + if (! (value.isInt() || value.isInt64() || value.isDouble())) return fallback; + const auto result = (float) value; + return std::isfinite(result) ? result : fallback; + } + + void applyAnalysisSettings(const juce::var& settings) + { + config.fftSize = (size_t) std::clamp(number(settings, "fftSize", (float) config.fftSize), 0.0f, 16384.0f); + config.historySeconds = number(settings, "historySeconds", config.historySeconds); + config.smoothing = number(settings, "smoothing", config.smoothing); + config.tiltDbPerOctave = number(settings, "tiltDbPerOctave", config.tiltDbPerOctave); + config.scaleMode = settings.getProperty("scaleMode", juce::String(config.scaleMode)).toString().toStdString(); + } + + static juce::String encode(const std::vector& values) + { + return values.empty() ? juce::String() : juce::Base64::toBase64(values.data(), values.size() * sizeof(float)); + } + + Visualizer::WaterfallAnalyzer analyzer; + Visualizer::WaterfallConfig config; + size_t ridges = 32, columns = 256; + int revision = 0; +}; diff --git a/plugin/Tests/BridgeEditorTests.cpp b/plugin/Tests/BridgeEditorTests.cpp new file mode 100644 index 0000000..e6a7c4d --- /dev/null +++ b/plugin/Tests/BridgeEditorTests.cpp @@ -0,0 +1,137 @@ +#include "BridgeProcessor.h" +#include + +int runBridgeEditorTests(bool interactive); + +// Exercise the real native label/editor with the message loop running. --ui +// keeps it open for manual checks; --ui-test checks edits and saves snapshots. +int runBridgeEditorTests(bool interactive) +{ + juce::ScopedJuceInitialiser_GUI initialise; + juce::Process::makeForegroundProcess(); + struct Preview final : juce::DocumentWindow, private juce::Timer + { + PrismBridgeProcessor processor; + std::unique_ptr editor; + juce::Label* name = nullptr; + int step = 0; + int failures = 0; + + explicit Preview(bool manual) + : DocumentWindow("Prism Bridge - nameplate preview", juce::Colour(0xff080d18), closeButton) + { + setTrack("Drums"); + editor.reset(processor.createEditorIfNeeded()); + for (auto* child : editor->getChildren()) + if (child->getName() == "Source name") name = dynamic_cast(child); + setUsingNativeTitleBar(true); + setContentNonOwned(editor.get(), true); + centreWithSize(editor->getWidth(), editor->getHeight()); + setVisible(true); + toFront(true); + if (!manual) startTimer(350); + } + + ~Preview() override { stopTimer(); clearContentComponent(); } + void closeButtonPressed() override { juce::MessageManager::getInstance()->stopDispatchLoop(); } + void expect(bool value, const char* message) + { + if (!value) { std::cerr << "FAIL: " << message << '\n'; ++failures; } + } + void setTrack(const juce::String& value) + { + juce::AudioProcessor::TrackProperties track; + track.name = value; + processor.updateTrackProperties(track); + } + void edit(const juce::String& text, int key) + { + name->showEditor(); + auto* input = name->getCurrentTextEditor(); + expect(input != nullptr, "nameplate must open its text editor on demand"); + if (!input) return; + input->setText(text, false); + input->keyPressed(juce::KeyPress(key)); + } + void snapshot(const juce::String& suffix) + { + const auto directory = juce::File::getSpecialLocation(juce::File::tempDirectory) + .getChildFile("prism-bridge-nameplate-qa"); + directory.createDirectory(); + const auto file = directory.getChildFile(suffix + ".png"); + juce::FileOutputStream output(file); + expect(output.openedOk(), "snapshot file must open"); + output.setPosition(0); + output.truncate(); + expect(juce::PNGImageFormat().writeImageToStream( + editor->createComponentSnapshot(editor->getLocalBounds(), true, 2.0f), output), + "nameplate snapshot must save"); + std::cout << file.getFullPathName() << '\n'; + } + void timerCallback() override + { + if (!name) { expect(false, "source-name label must exist"); closeButtonPressed(); return; } + switch (step++) + { + case 0: + expect(!name->isBeingEdited(), "opening the editor must not start editing"); + expect(name->getText() == "Drums", "nameplate must show the host name"); + snapshot("automatic"); + setTrack("Percussion"); + break; + case 1: + expect(name->getText() == "Percussion", "host renames must refresh the visible name"); + edit(" Drum Bus ", juce::KeyPress::returnKey); + break; + case 2: + expect(processor.getCustomName() == "Drum Bus", "Enter must save and trim a name"); + expect(name->getText() == "Drum Bus", "the displayed name must reflect the committed value"); + setTrack("Rhythm"); + break; + case 3: + expect(name->getText() == "Drum Bus", "host changes must preserve a custom name"); + edit("", juce::KeyPress::returnKey); + break; + case 4: + expect(processor.getCustomName().isEmpty(), "clearing must restore automatic naming"); + expect(name->getText() == "Rhythm", "clearing must display the latest host name"); + edit("Cancelled", juce::KeyPress::escapeKey); + break; + case 5: + expect(processor.getCustomName().isEmpty() && name->getText() == "Rhythm", + "Escape must leave automatic naming intact"); + name->showEditor(); + name->getCurrentTextEditor()->setText("Focus saved", false); + editor->setWantsKeyboardFocus(true); + editor->grabKeyboardFocus(); + break; + case 6: + expect(processor.getCustomName() == "Focus saved", "losing focus must commit the name"); + name->showEditor(); + name->getCurrentTextEditor()->setText("Still typing", false); + setTrack("Updated during edit"); + break; + case 7: + expect(name->isBeingEdited() && name->getCurrentTextEditor()->getText() == "Still typing", + "timer updates must not interrupt typing"); + name->getCurrentTextEditor()->keyPressed(juce::KeyPress(juce::KeyPress::escapeKey)); + processor.setCustomName("An exceptionally long source name that must stay readable without shrinking the text"); + break; + case 8: + snapshot("long-name"); + processor.setCustomName(""); + setTrack(""); + break; + case 9: + expect(name->getText() == "Bridge " + processor.getInstanceTag(), "unnamed bridges must show their tag"); + snapshot("unnamed"); + std::cout << "Bridge nameplate UI checks: " << (failures ? "FAILED" : "passed") << '\n'; + closeButtonPressed(); + break; + default: closeButtonPressed(); break; + } + } + } preview(interactive); + juce::MessageManager::getInstance()->runDispatchLoop(); + return preview.failures ? 1 : 0; +} diff --git a/plugin/Tests/BridgeProcessorTests.cpp b/plugin/Tests/BridgeProcessorTests.cpp new file mode 100644 index 0000000..59f851d --- /dev/null +++ b/plugin/Tests/BridgeProcessorTests.cpp @@ -0,0 +1,170 @@ +#include "BridgeProcessor.h" +#include +#include +#include +#include + +namespace +{ +int failures = 0; + +void expect(bool condition, const char* message) +{ + if (condition) return; + std::cerr << "FAIL: " << message << '\n'; + ++failures; +} + +void fillTestSignal(juce::AudioBuffer& buffer) +{ + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) + for (int sample = 0; sample < buffer.getNumSamples(); ++sample) + buffer.setSample(channel, sample, std::sin(static_cast(sample + channel * 7) * 0.013f)); +} + +void expectBitIdentical(const juce::AudioBuffer& actual, + const juce::AudioBuffer& expected, + const char* message) +{ + bool equal = actual.getNumChannels() == expected.getNumChannels() + && actual.getNumSamples() == expected.getNumSamples(); + for (int channel = 0; equal && channel < actual.getNumChannels(); ++channel) + equal = std::memcmp(actual.getReadPointer(channel), + expected.getReadPointer(channel), + static_cast(actual.getNumSamples()) * sizeof(float)) == 0; + expect(equal, message); +} +} + +int runBridgeEditorTests(bool interactive); + +int main(int argc, char** argv) +{ + if (argc > 1 && juce::String(argv[1]) == "--ui") return runBridgeEditorTests(true); + if (argc > 1 && juce::String(argv[1]) == "--ui-test") return runBridgeEditorTests(false); + // Two processor packet queues exceed the default Windows stack size. + const auto processorOwner = std::make_unique(); + auto& processor = *processorOwner; + processor.prepareToPlay(48000.0, 1024); + processor.setSelectedForTesting(true); + juce::MidiBuffer midi; + + juce::AudioBuffer stereo(2, 1025); + fillTestSignal(stereo); + juce::AudioBuffer stereoCopy; + stereoCopy.makeCopyOf(stereo); + processor.processBlock(stereo, midi); + expectBitIdentical(stereo, stereoCopy, "stereo processing must be bit-identical"); + expect(processor.getQueuedPacketCountForTesting() == 3, "1025 stereo frames must produce three packets"); + + processor.clearQueuedPacketsForTesting(); + juce::AudioBuffer mono(1, PrismBridgeProcessor::packetFrames); + fillTestSignal(mono); + juce::AudioBuffer monoCopy; + monoCopy.makeCopyOf(mono); + processor.processBlock(mono, midi); + expectBitIdentical(mono, monoCopy, "mono processing must be bit-identical"); + expect(processor.getQueuedPacketCountForTesting() == 1, "one mono packet must be queued"); + + processor.clearQueuedPacketsForTesting(); + processor.setNonRealtime(true); + processor.processBlock(mono, midi); + expect(processor.getQueuedPacketCountForTesting() == 0, "offline rendering must not enqueue audio"); + processor.setNonRealtime(false); + + for (int index = 0; index < PrismBridgeProcessor::packetCapacity + 2; ++index) + processor.processBlock(mono, midi); + expect(processor.getQueuedPacketCountForTesting() == PrismBridgeProcessor::packetCapacity, + "the audio queue must remain bounded"); + expect(processor.getDroppedFrameCount() >= 2 * PrismBridgeProcessor::packetFrames, + "overflow must report dropped frames"); + + PrismBridgeProcessor::TransportData transport; + transport.timeInSamples = 24000; + transport.timeInSeconds = 0.5; + transport.ppqPosition = 4.0; + transport.bpm = 120.0; + transport.hasTimeInSamples = true; + transport.hasTimeInSeconds = true; + transport.hasPpq = true; + transport.hasBpm = true; + const auto interpolated = processor.offsetTransportForTesting(transport, 24000); + expect(interpolated.timeInSamples == 48000, "sample transport must interpolate by packet offset"); + expect(std::abs(interpolated.timeInSeconds - 1.0) < 1.0e-9, + "seconds transport must interpolate by packet offset"); + expect(std::abs(interpolated.ppqPosition - 5.0) < 1.0e-9, + "PPQ transport must interpolate using tempo"); + + PrismBridgeProcessor::AudioPacket encodedPacket; + encodedPacket.sequence = 0x01020304; + encodedPacket.frameCount = 2; + encodedPacket.channelCount = 2; + encodedPacket.sampleRate = 48000.0; + encodedPacket.transport = transport; + encodedPacket.transport.isPlaying = true; + encodedPacket.transport.hasTimeSignature = true; + encodedPacket.transport.numerator = 7; + encodedPacket.transport.denominator = 8; + encodedPacket.left[0] = 0.25f; + encodedPacket.left[1] = -0.5f; + encodedPacket.right[0] = 0.75f; + encodedPacket.right[1] = -1.0f; + std::array encoded {}; + const auto encodedBytes = processor.encodeAudioPayloadForTesting( + encodedPacket, encoded.data(), encoded.size()); + expect(encodedBytes == 92, "protocol payload must contain the fixed header and planar samples"); + expect(encoded[0] == 0x04 && encoded[1] == 0x03 && encoded[2] == 0x02 && encoded[3] == 0x01, + "protocol sequence must be little-endian"); + expect(encoded[4] == 2 && encoded[5] == 0 && encoded[6] == 2, + "protocol frame and channel counts must be encoded"); + expect(encoded[72] == 7 && encoded[74] == 8, + "protocol time signature must be little-endian"); + float decodedLeft = 0.0f; + std::memcpy(&decodedLeft, encoded.data() + 76, sizeof(decodedLeft)); + decodedLeft = juce::ByteOrder::swapIfBigEndian(decodedLeft); + expect(decodedLeft == 0.25f, "protocol audio must be planar Float32"); + + expect(processor.getDisplayName() == "Bridge " + processor.getInstanceTag(), + "an unnamed bridge must show its live instance tag"); + juce::AudioProcessor::TrackProperties track; + track.name = " Drums "; + processor.updateTrackProperties(track); + expect(processor.getDisplayName() == "Drums", "automatic names must use the trimmed host track name"); + track.name = "Percussion"; + processor.updateTrackProperties(track); + expect(processor.getDisplayName() == "Percussion", "automatic names must follow track renames"); + processor.setCustomName(" Drum Bus "); + track.name = "Rhythm"; + processor.updateTrackProperties(track); + expect(processor.getDisplayName() == "Drum Bus", "a custom name must override later host track renames"); + juce::MemoryBlock state; + processor.getStateInformation(state); + const auto restoredOwner = std::make_unique(); + auto& restored = *restoredOwner; + restored.setStateInformation(state.getData(), static_cast(state.getSize())); + expect(restored.getCustomName() == "Drum Bus", "custom source names must restore from DAW state"); + expect(restored.getInstanceId() != processor.getInstanceId(), "restoring state must not copy the live instance ID"); + juce::MemoryBlock restoredState; + restored.getStateInformation(restoredState); + expect(state == restoredState, "existing saved source IDs and custom names must round-trip unchanged"); + processor.setCustomName(" "); + expect(processor.getDisplayName() == "Rhythm", "clearing a custom name must restore the current track name"); + track.name = {}; + processor.updateTrackProperties(track); + expect(processor.getDisplayName() == "Bridge " + processor.getInstanceTag(), "missing host names must restore the generated name"); + restored.setCustomName(juce::String::repeatedString("x", 180)); + expect(restored.getCustomName().length() == 160, "custom names must remain bounded"); + + processor.clearQueuedPacketsForTesting(); + processor.setSelectedForTesting(false); + processor.processBlock(stereo, midi); + expectBitIdentical(stereo, stereoCopy, "unselected processing must remain bit-identical"); + expect(processor.getQueuedPacketCountForTesting() == 0, "unselected instances must not enqueue audio"); + processor.processBlockBypassed(stereo, midi); + expectBitIdentical(stereo, stereoCopy, "host bypass must remain bit-identical"); + + processor.releaseResources(); + if (failures == 0) + std::cout << "Prism Bridge processor tests passed\n"; + return failures == 0 ? 0 : 1; +} diff --git a/plugin/Tests/LinuxWebViewHelperProbe.cpp b/plugin/Tests/LinuxWebViewHelperProbe.cpp new file mode 100644 index 0000000..222a404 --- /dev/null +++ b/plugin/Tests/LinuxWebViewHelperProbe.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include + +extern "C" int prismWebViewHelperProbe(int argc, const char* const* argv) +{ + if (std::getenv("LD_LIBRARY_PATH") != nullptr || std::getenv("LD_PRELOAD") != nullptr) + return 1; + if (argc != 3 || std::strcmp(argv[0], "argument with spaces") != 0 + || std::strcmp(argv[1], "second") != 0) + return 2; + if (access(argv[2], R_OK) != 0) + return 3; + // Merely unsetting the variables leaves both the preloaded library and the + // linker's startup search path in place. Only a clean exec passes this. + if (dlopen("libprism_host_override.so", RTLD_NOW | RTLD_LOCAL) != nullptr) + return 4; + return 0; +} diff --git a/plugin/Tests/ReferenceTrackTests.cpp b/plugin/Tests/ReferenceTrackTests.cpp new file mode 100644 index 0000000..0c3a415 --- /dev/null +++ b/plugin/Tests/ReferenceTrackTests.cpp @@ -0,0 +1,191 @@ +#include "PluginProcessor.h" +#include "SpectrumEngine.h" +#include +#include +#include +#include +#include + +namespace { +int failures = 0; +void expect(bool value, const char* message) { + if (!value) { std::cerr << "FAIL: " << message << '\n'; ++failures; } +} +bool finish(ReferenceTrackManager& manager) { + for (int i = 0; i < 1000; ++i) { + const auto phase = manager.snapshot()["phase"].toString(); + if (phase == "ready") return true; + if (phase == "error" || phase == "idle") return false; + juce::Thread::sleep(5); + } + return false; +} +juce::File makeAudio(const juce::File& directory, int repeats = 1) { + const auto file = directory.getChildFile(juce::String::fromUTF8("référence 日本語.wav")); + juce::WavAudioFormat format; + std::unique_ptr writer(format.createWriterFor(file.createOutputStream().release(), 44100, 2, 24, {}, 0)); + juce::AudioBuffer data(2, 44100 * 5); + for (int i = 0; i < data.getNumSamples(); ++i) { + const auto sample = static_cast(0.1 * std::sin(2 * juce::MathConstants::pi * 1500 * i / 44100)); + data.setSample(0, i, sample); data.setSample(1, i, sample); + } + for (int i = 0; i < repeats; ++i) + expect(writer && writer->writeFromAudioSampleBuffer(data, 0, data.getNumSamples()), "test audio writes"); + return file; +} +} + +// Optional native-editor preview for OS file-drop, picker, and narrow-layout QA. +int showEditor(const juce::File& initialFile) { + juce::ScopedJuceInitialiser_GUI initialise; + juce::Process::makeForegroundProcess(); + struct Preview : juce::DocumentWindow, juce::Timer { + PrismSpectrumProcessor processor; + struct TestHost : juce::Component, juce::ComponentListener { + struct DragFile : juce::Label { + juce::File file; + explicit DragFile(juce::File path) : file(std::move(path)) { setText("Drag reference fixture into graph", juce::dontSendNotification); } + void mouseDrag(const juce::MouseEvent&) override { juce::DragAndDropContainer::performExternalDragDropOfFiles({file.getFullPathName()}, false, this); } + } dragFile; + PrismSpectrumProcessor& processor; + std::unique_ptr editor; + juce::TextButton toggle { "Close / reopen editor" }; + bool layingOut = false; + TestHost(PrismSpectrumProcessor& p, const juce::File& file) : dragFile(file), processor(p) { + addAndMakeVisible(dragFile); addAndMakeVisible(toggle); + toggle.onClick = [this] { + if (editor) { editor->removeComponentListener(this); editor.reset(); } + else open(); + }; + open(); + } + ~TestHost() override { if (editor) editor->removeComponentListener(this); } + void open() { + editor.reset(processor.createEditorIfNeeded()); + addAndMakeVisible(*editor); editor->addComponentListener(this); + setSize(editor->getWidth(), editor->getHeight() + 30); resized(); + } + void resized() override { + layingOut = true; + dragFile.setBounds(8, 0, juce::jmax(120, getWidth() - 190), 30); + toggle.setBounds(getWidth() - 180, 2, 172, 26); + if (editor) editor->setBounds(0, 30, getWidth(), juce::jmax(100, getHeight() - 30)); + layingOut = false; + } + void componentMovedOrResized(juce::Component&, bool, bool resizedFlag) override { + if (!layingOut && resizedFlag && editor) setSize(editor->getWidth(), editor->getHeight() + 30); + } + }; + juce::File fixtureDirectory; + std::unique_ptr host; + int64_t samples = 0; + Preview(const juce::File& file) : DocumentWindow("Prism Spectrum reference preview", juce::Colours::black, DocumentWindow::closeButton) { + processor.prepareToPlay(48000, 800); + processor.setSettingsJson(R"({"fftSize":4096,"smoothing":0.7,"peakInfoMode":"on"})"); + fixtureDirectory = juce::File::getSpecialLocation(juce::File::tempDirectory).getChildFile("prism-reference-ui-" + juce::Uuid().toString()); + fixtureDirectory.createDirectory(); + host = std::make_unique(processor, makeAudio(fixtureDirectory, 120)); + setUsingNativeTitleBar(true); setContentNonOwned(host.get(), true); setResizable(true, false); + setResizeLimits(360, 210, 4096, 4096); + centreWithSize(host->getWidth(), host->getHeight()); setVisible(true); toFront(true); + if (file.existsAsFile()) processor.referenceTracks.start(file); + startTimerHz(60); + } + ~Preview() override { stopTimer(); clearContentComponent(); host.reset(); processor.referenceTracks.cancel(); fixtureDirectory.deleteRecursively(); } + void closeButtonPressed() override { juce::MessageManager::getInstance()->stopDispatchLoop(); } + void timerCallback() override { + juce::AudioBuffer audio(2, 800); juce::MidiBuffer midi; + for (int i = 0; i < 800; ++i, ++samples) { + const auto value = static_cast(0.05 * std::sin(2 * juce::MathConstants::pi * 1500 * samples / 48000)); + audio.setSample(0, i, value); audio.setSample(1, i, value); + } + processor.processBlock(audio, midi); + } + } preview(initialFile); + juce::MessageManager::getInstance()->runDispatchLoop(); + return 0; +} + +int main(int argc, char** argv) { +#if JUCE_MAC + if (argc == 1 && juce::File::getSpecialLocation(juce::File::currentApplicationFile).hasFileExtension("app")) return showEditor({}); +#endif + if (argc > 1 && juce::String(argv[1]) == "--ui") return showEditor(argc > 2 ? juce::File(argv[2]) : juce::File()); + { + PrismSpectrumProcessor untouched, restored; + juce::MemoryBlock initial, roundTrip; + untouched.getStateInformation(initial); + expect(initial.getSize() == 2 && std::memcmp(initial.getData(), "{}", 2) == 0, + "unopened analyzer saves a nonempty JSON state for CLAP"); + restored.setStateInformation(initial.getData(), static_cast(initial.getSize())); + restored.getStateInformation(roundTrip); + expect(initial == roundTrip, "initial state round trips without opening an editor"); + const juce::String populated = R"({"fftSize":4096,"smoothing":0.75,"colorMode":"theme"})"; + untouched.setSettingsJson(populated); + untouched.getStateInformation(initial); + restored.setStateInformation(initial.getData(), static_cast(initial.getSize())); + expect(restored.getSettingsJson() == populated, "populated analyzer settings round trip exactly"); + } + const auto directory = juce::File::getSpecialLocation(juce::File::tempDirectory).getChildFile("prism-reference-test-" + juce::Uuid().toString()); + directory.createDirectory(); + const auto file = makeAudio(directory); + PrismSpectrumProcessor first, second; + first.prepareToPlay(48000, 512); + first.referenceTracks.start(file); + juce::AudioBuffer audio(2, 512); juce::MidiBuffer midi; + for (int i = 0; i < 512; ++i) { audio.setSample(0, i, i / 512.0f); audio.setSample(1, i, -i / 512.0f); } + juce::AudioBuffer original; original.makeCopyOf(audio); + for (int i = 0; i < 200; ++i) first.processBlock(audio, midi); + for (int channel = 0; channel < 2; ++channel) + expect(std::memcmp(original.getReadPointer(channel), audio.getReadPointer(channel), 512 * sizeof(float)) == 0, "audio passes through unchanged during import"); + expect(finish(first.referenceTracks), "processor-owned job completes with no editor"); + if (failures) return 1; + expect(first.syncReferenceResult(), "completed asset commits once"); + expect(!first.syncReferenceResult(), "completion is not applied twice"); + auto settings = juce::JSON::parse(first.getSettingsJson()); + expect(settings["reference"]["asset"]["name"].toString() == file.getFileName(), "Unicode filename survives import"); + expect(std::abs(static_cast(settings["reference"]["asset"]["meanSquare"]) - 0.005) < 0.00001, "native plugin analysis matches Mid power"); + expect(second.getSettingsJson().isEmpty(), "references remain per plugin instance"); + + settings["reference"].getDynamicObject()->setProperty("trimDb", -6.0); + settings["reference"].getDynamicObject()->setProperty("view", "difference"); + first.setSettingsJson(juce::JSON::toString(settings)); + juce::MemoryBlock saved; first.getStateInformation(saved); + file.deleteFile(); + second.setStateInformation(saved.getData(), static_cast(saved.getSize())); + expect(second.getSettingsJson() == first.getSettingsJson(), "DAW state recalls every curve after source deletion"); + auto invalid = juce::JSON::parse(first.getSettingsJson()); + invalid["reference"]["asset"]["curves"].getDynamicObject()->setProperty("2048", "bad"); + second.setSettingsJson(juce::JSON::toString(invalid)); + expect(juce::JSON::parse(second.getSettingsJson())["reference"].isVoid(), "invalid saved array is rejected"); + const juce::String legacy = R"({"fftSize":2048})"; + second.setStateInformation(legacy.toRawUTF8(), static_cast(legacy.getNumBytesAsUTF8())); + expect(!juce::JSON::parse(second.getSettingsJson()).hasProperty("reference"), "legacy state has no reference"); + + const auto nextFile = makeAudio(directory); + juce::MemoryBlock fileBytes; nextFile.loadFileAsData(fileBytes); + auto& manager = second.referenceTracks; + const auto stale = manager.beginUpload("old.wav", static_cast(fileBytes.getSize())); + const auto id = manager.beginUpload("new.wav", static_cast(fileBytes.getSize())); + bool rejected = false; + try { manager.appendUpload(stale, 0, "AAAA"); } catch (...) { rejected = true; } + manager.cancelUpload(stale); + expect(rejected && manager.snapshot()["jobId"].toString() == id, "stale upload cannot write or cancel its replacement"); + for (size_t offset = 0; offset < fileBytes.getSize(); offset += 262144) { + const auto count = std::min(262144, fileBytes.getSize() - offset); + manager.appendUpload(id, static_cast(offset), juce::Base64::toBase64(static_cast(fileBytes.getData()) + offset, count)); + } + manager.finishUpload(id); + expect(finish(manager) && manager.snapshot()["name"].toString() == "new.wav", "bounded byte upload decodes successfully"); + const auto cancelled = manager.beginUpload("cancel.wav", 100); + manager.cancelUpload(cancelled); + expect(manager.snapshot()["phase"].toString() == "idle", "cancel clears upload state"); + manager.start(nextFile); + second.setStateInformation(saved.getData(), static_cast(saved.getSize())); + expect(manager.snapshot()["phase"].toString() == "idle", "project restore cancels active analysis"); + expect(second.getSettingsJson() == first.getSettingsJson(), "canceled job cannot replace restored reference"); + manager.cancel(); + directory.deleteRecursively(); + std::cout << (failures ? "Reference plugin tests failed" : "Reference plugin tests passed") << '\n'; + return failures ? 1 : 0; +} diff --git a/plugin/Tests/WaterfallTests.cpp b/plugin/Tests/WaterfallTests.cpp new file mode 100644 index 0000000..5d6e3d4 --- /dev/null +++ b/plugin/Tests/WaterfallTests.cpp @@ -0,0 +1,185 @@ +#include "WaterfallEngine.h" +#include "PluginProcessor.h" +#include +#include +#include +#include + +namespace { +int failures = 0; +void expect(bool value, const char* message) { + if (!value) { std::cerr << "FAIL: " << message << '\n'; ++failures; } +} +std::vector floats(const juce::var& frame, const char* key) { + juce::MemoryOutputStream bytes; + expect(juce::Base64::convertFromBase64(bytes, frame.getProperty(key, "").toString()), "frame base64 decodes"); + std::vector result(bytes.getDataSize() / sizeof(float)); + if (!result.empty()) std::memcpy(result.data(), bytes.getData(), bytes.getDataSize()); + return result; +} +} + +// Optional, silent editor smoke test: exercise the real processor, DSP, native +// bridge and embedded WKWebView without requiring a DAW or an audio device. +int showEditor() { + juce::ScopedJuceInitialiser_GUI initialise; + juce::Process::makeForegroundProcess(); + struct Preview : juce::DocumentWindow, juce::Timer { + PrismSpectrumProcessor processor; + std::unique_ptr editor; + int64_t samples = 0; + int stage = 0; + bool waiting = false; + Preview() : DocumentWindow("Prism Waterfall — plugin preview", juce::Colours::black, DocumentWindow::closeButton) { + processor.prepareToPlay(48000, 800); + processor.setSettingsJson(R"({"historySeconds":5,"colorMode":"heat"})"); + editor.reset(processor.createEditorIfNeeded()); + setUsingNativeTitleBar(true); + setContentNonOwned(editor.get(), true); + setResizable(true, false); + centreWithSize(editor->getWidth(), editor->getHeight()); + setVisible(true); + toFront(true); + startTimerHz(60); + } + ~Preview() override { stopTimer(); clearContentComponent(); } + void closeButtonPressed() override { juce::MessageManager::getInstance()->stopDispatchLoop(); } + void inspect() { + juce::WebBrowserComponent* browser = nullptr; + for (auto* child : editor->getChildren()) + if (auto* web = dynamic_cast(child)) browser = web; + expect(browser != nullptr, "native plugin editor creates its webview"); + if (!browser) { closeButtonPressed(); return; } + waiting = true; + const int step = stage++; + const juce::String actions = step == 0 + ? "document.querySelector('button[aria-label=Settings]').click();" + : step == 1 + ? "const select=[...document.querySelectorAll('select')].find(s=>[...s.options].some(o=>o.value==='heat'));if(!select)throw Error('Missing Waterfall color control');select.value='theme';select.dispatchEvent(new Event('change',{bubbles:true}));document.querySelector('button[aria-label=Settings]').click();" + : ""; + browser->evaluateJavascript("(()=>{if(!window.__waterfallQA){window.__waterfallQA={frames:0,raf:0,errors:[]};window.__JUCE__.backend.addEventListener('waterfallFrame',f=>{window.__waterfallQA.frames++;window.__waterfallQA.frame={revision:f.revision,columns:f.columns,audioSeconds:f.audioSeconds,ageBytes:f.ages.length};});window.addEventListener('error',e=>window.__waterfallQA.errors.push(e.message));const tick=()=>{window.__waterfallQA.raf++;requestAnimationFrame(tick)};requestAnimationFrame(tick);}const canvas=document.querySelector('canvas');if(!canvas)throw Error('No scope canvas');const result={stats:window.__waterfallQA,scope:window.__JUCE__.initialisationData.prismScope[0],png:canvas.toDataURL(),width:canvas.width,height:canvas.height,ink:canvas.getContext('2d').getImageData(0,0,canvas.width,canvas.height).data.some((v,i)=>i%4===3&&v>0)};" + actions + "return JSON.stringify(result)})()", + [this, step](auto result) { + const auto* value = result.getResult(); + expect(value != nullptr, "native webview script evaluates"); + if (value) { + const auto data = juce::JSON::parse(value->toString()); + std::cout << "UI diagnostics: " << juce::JSON::toString(data["stats"]) << " ink=" << data["ink"].toString() << '\n'; + expect((bool) data["ink"], "native Waterfall canvas actually paints"); + expect(data["scope"].toString() == "waterfall", "native editor selects Waterfall UI"); + expect((int) data["width"] > 300 && (int) data["height"] > 100, "scope canvas has a usable viewport"); + const auto directory = juce::File::getSpecialLocation(juce::File::tempDirectory).getChildFile("prism-waterfall-plugin-qa"); + directory.createDirectory(); + juce::MemoryOutputStream png; + juce::Base64::convertFromBase64(png, data["png"].toString().fromFirstOccurrenceOf(",", false, false)); + const auto file = directory.getChildFile("native-editor-" + juce::String(step) + ".png"); + expect(file.replaceWithData(png.getData(), png.getDataSize()), "native editor snapshot saves"); + std::cout << file.getFullPathName() << '\n'; + } + if (step == 2) { + expect(juce::JSON::parse(processor.getSettingsJson())["colorMode"].toString() == "theme", + "native editor settings control persists to the processor"); + setSize(440, 240); + } + waiting = false; + if (step == 3) closeButtonPressed(); + }); + } + void timerCallback() override { + juce::AudioBuffer audio(2, 800); + juce::MidiBuffer midi; + for (int i = 0; i < 800; ++i, ++samples) { + const double t = samples / 48000.0; + const double beat = std::exp(-std::fmod(t * 2, 1.0) * 10); + const auto tone = [&](double hz) { return std::sin(t * 2 * juce::MathConstants::pi * hz); }; + const float value = (float) (0.3 * tone(65) * (0.3 + beat) + 0.12 * tone(330) + + 0.13 * tone(1320) * (0.6 + 0.4 * std::sin(t * 2)) + 0.08 * tone(4200) * beat); + audio.setSample(0, i, value); + audio.setSample(1, i, -value); + } + processor.processBlock(audio, midi); + if (!waiting && samples >= 48000 * (5 + stage * 3)) inspect(); + if (samples >= 48000 * 120) closeButtonPressed(); + } + } preview; + juce::MessageManager::getInstance()->runDispatchLoop(); + return failures ? 1 : 0; +} + +int main(int argc, char** argv) { + if (argc > 1 && juce::String(argv[1]) == "--ui") return showEditor(); + WaterfallEngine engine; + engine.configure(juce::JSON::parse(R"({"fftSize":2048,"historySeconds":3,"smoothing":0,"tiltDbPerOctave":0})")); + engine.configureNative(juce::JSON::parse(R"({"ridges":32,"columns":256,"revision":1})")); + std::vector left(48000 * 6), right(left.size()); + for (size_t i = 0; i < left.size(); ++i) { + left[i] = 0.5f * std::sin((double) i * 2 * juce::MathConstants::pi * 1007.8125 / 48000); + right[i] = -left[i]; + } + engine.process(left.data(), right.data(), (int) left.size()); + auto frame = engine.buildFrame(48000); + auto levels = floats(frame, "levels"); + auto ages = floats(frame, "ages"); + expect(!levels.empty() && levels.size() == ages.size() * 256, "compact plot payload dimensions match"); + const float peak = *std::max_element(levels.begin(), levels.end()); + expect(peak > -6.4f && peak < -5.8f, "anti-phase stereo remains calibrated and visible"); + expect((double) frame["audioSeconds"] == 6 && ages.back() > 2.8f, "history uses audio duration"); + expect((int) frame["revision"] == 1, "responses echo the viewport revision"); + engine.process(nullptr, nullptr, 0); + expect(juce::JSON::toString(engine.buildFrame(48000)) == juce::JSON::toString(frame), "suspended input holds its plot"); + + engine.configure(juce::JSON::parse(R"({"historySeconds":3,"colorMode":"heat","density":"dense"})")); + expect(floats(engine.buildFrame(48000), "levels") == levels, "appearance preserves history"); + engine.configureNative(juce::JSON::parse(R"({"ridges":8,"columns":40,"revision":2})")); + frame = engine.buildFrame(48000); + expect((int) frame["columns"] == 40 && floats(frame, "levels").size() <= 8 * 40, "resizing only requests visible data"); + expect((double) frame["audioSeconds"] == 6 && floats(frame, "ages").back() > 2.5f, "small plots retain the full selected timespan"); + engine.configure(juce::JSON::parse(R"({"historySeconds":30})")); + expect((double) engine.buildFrame(48000)["audioSeconds"] == 6, "extending duration preserves available history"); + engine.resetAudioHistory(); + expect(floats(engine.buildFrame(48000), "ages").empty(), "audio discontinuity clears history"); + engine.process(left.data(), right.data(), 48000); + engine.setSampleRate(44100); + expect((double) engine.buildFrame(44100)["audioSeconds"] == 0, "sample-rate changes reset history"); + engine.configureNative(juce::JSON::parse(R"({"config":{"sampleRate":96000},"revision":3})")); + expect((double) engine.buildFrame(96000)["sampleRate"] == 44100, "host sample rate wins over webview config"); + engine.process(left.data(), right.data(), 44100); + engine.configure(juce::JSON::parse(R"({"fftSize":4096})")); + expect(floats(engine.buildFrame(44100), "ages").empty(), "FFT-size changes reset history"); + engine.process(left.data(), right.data(), 44100); + engine.configureNative(juce::JSON::parse(R"({"reset":true,"revision":4})")); + expect((double) engine.buildFrame(44100)["audioSeconds"] == 0, "webview reset starts a new history span"); + + PrismSpectrumProcessor processor; + processor.prepareToPlay(48000, 512); + expect(processor.consumeAudioDiscontinuity(), "prepare marks a fresh audio span"); + juce::MidiBuffer midi; + for (int channels : {1, 2}) { + juce::AudioBuffer buffer(channels, 512), original; + for (int channel = 0; channel < channels; ++channel) + std::memcpy(buffer.getWritePointer(channel), channel == 0 ? left.data() : right.data(), 512 * sizeof(float)); + original.makeCopyOf(buffer); + processor.processBlock(buffer, midi); + for (int channel = 0; channel < channels; ++channel) + expect(std::memcmp(buffer.getReadPointer(channel), original.getReadPointer(channel), 512 * sizeof(float)) == 0, + "mono and stereo host audio pass through bit-identically"); + float drainedLeft[512], drainedRight[512]; + expect(processor.drainStereo(drainedLeft, drainedRight, 512) == 512, "FIFO preserves sample count"); + expect(std::memcmp(drainedRight, channels == 1 ? left.data() : right.data(), 512 * sizeof(float)) == 0, + "mono duplicates correctly and stereo preserves phase"); + } + juce::AudioBuffer overflow(2, 70000); + overflow.clear(); + processor.processBlock(overflow, midi); + expect(processor.consumeAudioDiscontinuity() && !processor.consumeAudioDiscontinuity(), "FIFO overrun is reported once"); + processor.restartAudioHistory(); + expect(processor.consumeAudioDiscontinuity(), "editor reopen discards stale buffered history"); + const juce::String settings = R"({"historySeconds":17,"colorMode":"heat","density":"dense","fftSize":8192,"scaleMode":"mel","smoothing":0.7,"tiltDbPerOctave":4,"showGrid":false})"; + processor.setSettingsJson(settings); + juce::MemoryBlock state; + processor.getStateInformation(state); + PrismSpectrumProcessor restored; + restored.setStateInformation(state.getData(), (int) state.getSize()); + expect(restored.getSettingsJson() == settings, "Waterfall settings round-trip through DAW project state"); + std::cout << (failures ? "Waterfall plugin tests failed\n" : "Waterfall plugin tests passed\n"); + return failures ? 1 : 0; +} diff --git a/plugin/cmake/PrismClap.cmake b/plugin/cmake/PrismClap.cmake new file mode 100644 index 0000000..7a321dd --- /dev/null +++ b/plugin/cmake/PrismClap.cmake @@ -0,0 +1,68 @@ +# CLAP wraps the existing JUCE shared-code targets; it is not a JUCE FORMATS entry. +if("CLAP" IN_LIST PRISM_PLUGIN_FORMATS) + set(CLAP_JUCE_EXTENSIONS_PATH "" CACHE PATH "Optional local clap-juce-extensions checkout (including submodules)") + set(CLAP_JUCE_EXTENSIONS_BUILD_EXAMPLES OFF CACHE BOOL "Build CLAP extension examples" FORCE) + if(CLAP_JUCE_EXTENSIONS_PATH) + add_subdirectory("${CLAP_JUCE_EXTENSIONS_PATH}" clap-juce-extensions EXCLUDE_FROM_ALL) + else() + include(FetchContent) + FetchContent_Declare(clap_juce_extensions + GIT_REPOSITORY https://github.com/free-audio/clap-juce-extensions.git + GIT_TAG 9fbefae3d9c3d130aafb558c1ec15427a4bd24be + GIT_SUBMODULES_RECURSE TRUE) + FetchContent_MakeAvailable(clap_juce_extensions) + endif() +endif() + +function(prism_add_clap target category) + if(NOT "CLAP" IN_LIST PRISM_PLUGIN_FORMATS) + return() + endif() + + # Own the copy step so macOS signing happens after the wrapper assembles its + # resources and before installation. The upstream helper has no Windows copy. + get_target_property(copy_after_build ${target} JUCE_COPY_PLUGIN_AFTER_BUILD) + set_target_properties(${target} PROPERTIES JUCE_COPY_PLUGIN_AFTER_BUILD FALSE) + clap_juce_extensions_plugin(TARGET ${target} + CLAP_ID "com.astra.prism.${target}" + CLAP_FEATURES audio-effect ${category} mono stereo + CLAP_MANUAL_URL "https://github.com/Boof2015/prism/blob/main/plugin/README.md" + CLAP_SUPPORT_URL "https://github.com/Boof2015/prism/issues") + set_target_properties(${target} PROPERTIES JUCE_COPY_PLUGIN_AFTER_BUILD ${copy_after_build}) + + if(APPLE) + add_custom_command(TARGET ${target}_CLAP POST_BUILD + COMMAND codesign --force --deep --sign - "$" + VERBATIM) + endif() + + if(PRISM_COPY_PLUGIN_AFTER_BUILD) + get_target_property(product ${target} JUCE_PRODUCT_NAME) + if(APPLE) + set(destination "$ENV{HOME}/Library/Audio/Plug-Ins/CLAP") + add_custom_command(TARGET ${target}_CLAP POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${destination}" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "$" "${destination}/${product}.clap" + VERBATIM) + else() + if(WIN32) + file(TO_CMAKE_PATH "$ENV{CommonProgramW6432}" common_files) + if(NOT common_files) + file(TO_CMAKE_PATH "$ENV{CommonProgramFiles}" common_files) + endif() + if(NOT common_files) + message(FATAL_ERROR "CLAP installation needs CommonProgramFiles; set PRISM_COPY_PLUGIN_AFTER_BUILD=OFF to build without installing.") + endif() + set(destination "${common_files}/CLAP") + else() + set(destination "$ENV{HOME}/.clap") + endif() + add_custom_command(TARGET ${target}_CLAP POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${destination}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" "${destination}/${product}.clap" + VERBATIM) + endif() + endif() +endfunction() diff --git a/plugin/cmake/PrismLinuxWebView.cmake b/plugin/cmake/PrismLinuxWebView.cmake new file mode 100644 index 0000000..076dd1f --- /dev/null +++ b/plugin/cmake/PrismLinuxWebView.cmake @@ -0,0 +1,50 @@ +# Keep WebKit and host-bundled GTK/GLib in separate processes. JUCE 8.0.4 +# probes WebKit inside the host even when using its external browser helper. +# Patch a build-local module copy so an external JUCE checkout is never edited. +function(prism_isolate_linux_webview) + get_target_property(module_base juce_gui_extra INTERFACE_JUCE_MODULE_PATH) + set(module_dir "${module_base}/juce_gui_extra") + set(browser_file "native/juce_WebBrowserComponent_linux.cpp") + file(READ "${module_dir}/${browser_file}" browser_source) + string(REPLACE "\r\n" "\n" browser_source "${browser_source}") + + set(host_probe " webKitIsAvailable = WebKitSymbols::getInstance()->isWebKitAvailable();") + set(child_probe " auto& wk = *WebKitSymbols::getInstance();\n\n // webkit2gtk crashes") + set(failed_launch " killChild();\n return;") + foreach(anchor IN ITEMS host_probe child_probe failed_launch) + string(FIND "${browser_source}" "${${anchor}}" location) + if(location EQUAL -1) + message(FATAL_ERROR "JUCE Linux WebView changed (${anchor}); review PrismLinuxWebView.cmake before building.") + endif() + endforeach() + + string(REPLACE "${host_probe}" [=[ + #if JUCE_USE_EXTERNAL_TEMPORARY_SUBPROCESS + // The helper checks WebKit with system libraries. Loading it here can + // fail against a DAW's bundled GLib before the helper even starts. + webKitIsAvailable = ! JUCEApplicationBase::isStandaloneApp() + || WebKitSymbols::getInstance()->isWebKitAvailable(); + #else + webKitIsAvailable = WebKitSymbols::getInstance()->isWebKitAvailable(); + #endif]=] browser_source "${browser_source}") + string(REPLACE "${child_probe}" [=[ + auto& wk = *WebKitSymbols::getInstance(); + if (! wk.isWebKitAvailable()) + { + std::cerr << "Prism WebView: WebKitGTK could not be loaded in the browser helper." << std::endl; + return 1; + } + + // webkit2gtk crashes]=] browser_source "${browser_source}") + string(REPLACE "${failed_launch}" " killChild();\n webKitIsAvailable = false;\n return;" browser_source "${browser_source}") + + set(copy_base "${CMAKE_CURRENT_BINARY_DIR}/prism-juce") + set(copy_dir "${copy_base}/juce_gui_extra") + file(COPY "${module_dir}" DESTINATION "${copy_base}" PATTERN "juce_WebBrowserComponent_linux.cpp" EXCLUDE) + file(CONFIGURE OUTPUT "${copy_dir}/${browser_file}" CONTENT "${browser_source}" @ONLY) + foreach(property IN ITEMS INTERFACE_SOURCES INTERFACE_JUCE_MODULE_SOURCES) + get_target_property(sources juce_gui_extra ${property}) + string(REPLACE "${module_dir}/" "${copy_dir}/" sources "${sources}") + set_property(TARGET juce_gui_extra PROPERTY ${property} "${sources}") + endforeach() +endfunction() diff --git a/resources/installer/linux/install-clap.sh b/resources/installer/linux/install-clap.sh new file mode 100755 index 0000000..192e587 --- /dev/null +++ b/resources/installer/linux/install-clap.sh @@ -0,0 +1,116 @@ +#!/bin/sh +# Helper bundled in the Linux tarball under resources/plugins/. By default it +# installs Prism's native Linux CLAP bundles to the current user's CLAP folder. + +set -eu + +usage() { + cat <<'EOF' +Usage: install-clap.sh [--system] [--dest PATH] [--source PATH] + +Installs the bundled Prism CLAP plugins. + +Options: + --system Install to /usr/lib/clap instead of $HOME/.clap. + --dest PATH Install to a custom CLAP directory. + --source PATH Read Prism *.clap bundles from a custom source directory. + -h, --help Show this help. +EOF +} + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +SOURCE_DIR="${PRISM_CLAP_SOURCE_DIR:-}" +DEST_DIR="${PRISM_CLAP_DEST_DIR:-$HOME/.clap}" + +while [ "$#" -gt 0 ]; do + case "$1" in + --system) + DEST_DIR="/usr/lib/clap" + ;; + --dest) + shift + if [ "$#" -eq 0 ]; then + echo "install-clap.sh: --dest requires a path" >&2 + exit 2 + fi + DEST_DIR="$1" + ;; + --source) + shift + if [ "$#" -eq 0 ]; then + echo "install-clap.sh: --source requires a path" >&2 + exit 2 + fi + SOURCE_DIR="$1" + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "install-clap.sh: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [ -z "$SOURCE_DIR" ]; then + if [ -d "$SCRIPT_DIR/CLAP" ]; then + SOURCE_DIR="$SCRIPT_DIR/CLAP" + elif [ -d "$SCRIPT_DIR/plugins/CLAP" ]; then + SOURCE_DIR="$SCRIPT_DIR/plugins/CLAP" + else + SOURCE_DIR="$SCRIPT_DIR" + fi +fi + +if [ ! -d "$SOURCE_DIR" ]; then + echo "install-clap.sh: source directory not found: $SOURCE_DIR" >&2 + exit 1 +fi + +install_plugin() { + plugin_name="$1" + source_plugin="$SOURCE_DIR/$plugin_name" + dest_plugin="$DEST_DIR/$plugin_name" + + if [ ! -f "$source_plugin" ]; then + echo "install-clap.sh: missing bundled plugin: $source_plugin" >&2 + return 1 + fi + + cp -p "$source_plugin" "$dest_plugin" +} + +# Check the complete set before replacing any installed files. +for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + if [ ! -f "$SOURCE_DIR/Prism $name.clap" ]; then + echo "install-clap.sh: missing bundled plugin: $SOURCE_DIR/Prism $name.clap" >&2 + exit 1 + fi +done + +mkdir -p "$DEST_DIR" + +install_plugin "Prism Spectrum.clap" +install_plugin "Prism Oscilloscope.clap" +install_plugin "Prism VU Meter.clap" +install_plugin "Prism Loudness Meter.clap" +install_plugin "Prism Vectorscope.clap" +install_plugin "Prism Spectrogram.clap" +install_plugin "Prism Waveform.clap" +install_plugin "Prism Waterfall.clap" +install_plugin "Prism Bridge.clap" + +for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + chmod u+rw,go+r "$DEST_DIR/Prism $name.clap" +done + +cat <&2 return 0 fi - for dir in \ - /opt/Prism/resources/plugins/VST3 \ - /opt/prism/resources/plugins/VST3 - do - if [ -d "$dir" ]; then - printf '%s\n' "$dir" - return 0 + # Preflight the entire format before replacing any installed products. + for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + source_plugin="$source_dir/Prism $name.$extension" + if { [ "$format" = VST3 ] && [ ! -d "$source_plugin" ]; } || + { [ "$format" = CLAP ] && [ ! -f "$source_plugin" ]; }; then + echo "Prism $format install: missing bundled plugin: $source_plugin" >&2 + return 1 fi done - return 1 -} - -install_plugin() { - plugin_name="$1" - source_plugin="$SOURCE_DIR/$plugin_name" - dest_plugin="$DEST_DIR/$plugin_name" - - if [ ! -d "$source_plugin" ]; then - echo "Prism VST3 install: missing bundled plugin: $source_plugin" >&2 - return 1 - fi - - rm -rf "$dest_plugin" - cp -a "$source_plugin" "$DEST_DIR/" + mkdir -p "$dest_dir" + for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + plugin="Prism $name.$extension" + if [ "$format" = VST3 ]; then + rm -rf "$dest_dir/$plugin" + cp -a "$source_dir/$plugin" "$dest_dir/" + else + cp -p "$source_dir/$plugin" "$dest_dir/$plugin" + fi + chmod -R a+rX "$dest_dir/$plugin" + done + echo "Prism $format plugins installed to $dest_dir" } install_tui - -SOURCE_DIR="$(find_source_dir || true)" -if [ -z "$SOURCE_DIR" ]; then - echo "Prism VST3 install: bundled VST3 directory not found; skipping plugin install." >&2 - exit 0 -fi - -mkdir -p "$DEST_DIR" - -install_plugin "Prism Spectrum.vst3" -install_plugin "Prism Oscilloscope.vst3" -install_plugin "Prism VU Meter.vst3" -install_plugin "Prism Loudness Meter.vst3" -install_plugin "Prism Vectorscope.vst3" -install_plugin "Prism Spectrogram.vst3" -install_plugin "Prism Waveform.vst3" - -chmod -R a+rX "$DEST_DIR"/Prism*.vst3 2>/dev/null || true -echo "Prism VST3 plugins installed to $DEST_DIR" +install_format VST3 vst3 "${PRISM_VST3_SOURCE_DIR:-}" "${PRISM_VST3_DEST_DIR:-/usr/lib/vst3}" +install_format CLAP clap "${PRISM_CLAP_SOURCE_DIR:-}" "${PRISM_CLAP_DEST_DIR:-/usr/lib/clap}" diff --git a/resources/installer/linux/install-vst3.sh b/resources/installer/linux/install-vst3.sh index 4509ccd..df5a674 100755 --- a/resources/installer/linux/install-vst3.sh +++ b/resources/installer/linux/install-vst3.sh @@ -94,6 +94,8 @@ install_plugin "Prism Loudness Meter.vst3" install_plugin "Prism Vectorscope.vst3" install_plugin "Prism Spectrogram.vst3" install_plugin "Prism Waveform.vst3" +install_plugin "Prism Waterfall.vst3" +install_plugin "Prism Bridge.vst3" chmod -R u+rwX,go+rX "$DEST_DIR"/Prism*.vst3 2>/dev/null || true diff --git a/resources/installer/linux/remove-vst3-from-system.sh b/resources/installer/linux/remove-vst3-from-system.sh index c751394..c5d2338 100755 --- a/resources/installer/linux/remove-vst3-from-system.sh +++ b/resources/installer/linux/remove-vst3-from-system.sh @@ -1,6 +1,6 @@ #!/bin/sh # Package removal hook for Linux .deb/.rpm builds. Remove only Prism's known -# VST3 bundles from the global VST3 scan path. +# VST3 bundles and CLAP files from their global scan paths. set -eu @@ -21,7 +21,8 @@ case "$remove_action" in ;; esac -DEST_DIR="${PRISM_VST3_DEST_DIR:-/usr/lib/vst3}" +VST3_DEST="${PRISM_VST3_DEST_DIR:-/usr/lib/vst3}" +CLAP_DEST="${PRISM_CLAP_DEST_DIR:-/usr/lib/clap}" TUI_LINK="${PRISM_TUI_LINK_PATH:-/usr/bin/prism-tui}" remove_tui_link() { @@ -41,17 +42,10 @@ remove_tui_link() { esac } -remove_plugin() { - rm -rf "$DEST_DIR/$1" -} - -remove_plugin "Prism Spectrum.vst3" -remove_plugin "Prism Oscilloscope.vst3" -remove_plugin "Prism VU Meter.vst3" -remove_plugin "Prism Loudness Meter.vst3" -remove_plugin "Prism Vectorscope.vst3" -remove_plugin "Prism Spectrogram.vst3" -remove_plugin "Prism Waveform.vst3" +for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + rm -rf "$VST3_DEST/Prism $name.vst3" + rm -f "$CLAP_DEST/Prism $name.clap" +done remove_tui_link -echo "Prism VST3 plugins removed from $DEST_DIR" +echo "Prism VST3 and CLAP plugins removed" diff --git a/resources/installer/macos-scripts/postinstall b/resources/installer/macos-scripts/postinstall index 1e68b2a..be477a4 100755 --- a/resources/installer/macos-scripts/postinstall +++ b/resources/installer/macos-scripts/postinstall @@ -1,6 +1,6 @@ #!/bin/bash # Postinstall: after the macOS Installer drops Prism.app into /Applications, copy -# the bundled VST3 / AU plugins out of the app's Resources into the system +# the bundled VST3 / AU / CLAP plugins out of the app's Resources into the system # Audio Plug-Ins folders so DAWs find them, and expose prism-tui on PATH. # # Standard pkg postinstall args: @@ -19,6 +19,7 @@ APP_PATH="${2:-/Applications}/Prism.app" PLUGINS_SRC="$APP_PATH/Contents/Resources/plugins" VST3_DEST="${PRISM_VST3_DEST_DIR:-/Library/Audio/Plug-Ins/VST3}" AU_DEST="${PRISM_AU_DEST_DIR:-/Library/Audio/Plug-Ins/Components}" +CLAP_DEST="${PRISM_CLAP_DEST_DIR:-/Library/Audio/Plug-Ins/CLAP}" TUI_SOURCE="$APP_PATH/Contents/Resources/tui/prism-tui" TUI_LINK="${PRISM_TUI_LINK_PATH:-/usr/local/bin/prism-tui}" @@ -31,6 +32,19 @@ if [ -d "$PLUGINS_SRC/AU" ]; then cp -R "$PLUGINS_SRC/AU/"*.component "$AU_DEST/" 2>/dev/null || true fi +if [ -d "$PLUGINS_SRC/CLAP" ]; then + for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + if [ ! -d "$PLUGINS_SRC/CLAP/Prism $name.clap" ]; then + echo "Prism postinstall: missing CLAP bundle: Prism $name.clap" >&2 + exit 1 + fi + done + mkdir -p "$CLAP_DEST" + for name in Spectrum Oscilloscope "VU Meter" "Loudness Meter" Vectorscope Spectrogram Waveform Waterfall Bridge; do + cp -R "$PLUGINS_SRC/CLAP/Prism $name.clap" "$CLAP_DEST/" + done +fi + if [ -f "$TUI_SOURCE" ]; then chmod 755 "$TUI_SOURCE" mkdir -p "$(dirname "$TUI_LINK")" diff --git a/resources/installer/windows-vst3.nsh b/resources/installer/windows-vst3.nsh index fb5b587..ca099de 100644 --- a/resources/installer/windows-vst3.nsh +++ b/resources/installer/windows-vst3.nsh @@ -1,24 +1,18 @@ -; Custom NSIS include for electron-builder. -; -; Wizard installer (oneClick=false, perMachine=true) with an optional VST3 -; install step. After the directory page, we add a custom page with a checkbox: -; -; [x] Install Prism VST3 plugins (recommended) -; -; If checked, customInstall xcopies the bundled .vst3 bundles from -; $INSTDIR\resources\plugins\VST3\ into $COMMONFILES64\VST3\. The whole -; installer already runs elevated (perMachine=true → UAC at launch), so writes -; to Common Files succeed without a second elevation. On uninstall, the bundles -; are removed unconditionally (harmless if the user opted out at install time). +; Custom NSIS include for electron-builder. Retained at its original path. +; Independent VST3/CLAP options default on, including silent installs. +; /VST3=0|1 and /CLAP=0|1 allow scripted selection and installer smoke tests. !include "LogicLib.nsh" !include "WinMessages.nsh" !ifndef BUILD_UNINSTALLER !include "nsDialogs.nsh" + !include "FileFunc.nsh" Var PRISM_VST_CHECKBOX Var PRISM_VST_STATE + Var PRISM_CLAP_CHECKBOX + Var PRISM_CLAP_STATE !macro customHeader ; electron-builder disables the standard NSIS details view in common.nsh. @@ -40,6 +34,30 @@ !macro customInit ; Silent installs skip the options page, so default to installing plugins. StrCpy $PRISM_VST_STATE ${BST_CHECKED} + StrCpy $PRISM_CLAP_STATE ${BST_CHECKED} + ${GetParameters} $0 + ClearErrors + ${GetOptions} $0 "/VST3=" $1 + ${IfNot} ${Errors} + ${If} $1 == "0" + StrCpy $PRISM_VST_STATE ${BST_UNCHECKED} + ${ElseIf} $1 != "1" + MessageBox MB_OK|MB_ICONSTOP "Expected /VST3=0 or /VST3=1." /SD IDOK + SetErrorLevel 2 + Abort + ${EndIf} + ${EndIf} + ClearErrors + ${GetOptions} $0 "/CLAP=" $1 + ${IfNot} ${Errors} + ${If} $1 == "0" + StrCpy $PRISM_CLAP_STATE ${BST_UNCHECKED} + ${ElseIf} $1 != "1" + MessageBox MB_OK|MB_ICONSTOP "Expected /CLAP=0 or /CLAP=1." /SD IDOK + SetErrorLevel 2 + Abort + ${EndIf} + ${EndIf} !macroend ; NOTE on parse order: this file is `!include`d before electron-builder's main @@ -48,7 +66,7 @@ Page custom prismVstOptionsPage prismVstOptionsPageLeave Function prismVstOptionsPage - !insertmacro MUI_HEADER_TEXT "VST3 Plugins" "Choose whether to install the Prism scope plugins." + !insertmacro MUI_HEADER_TEXT "Audio Plugins" "Choose the Prism plugin formats to install." nsDialogs::Create 1018 Pop $0 @@ -58,9 +76,13 @@ ${NSD_CreateCheckBox} 0 0u 100% 12u "Install Prism VST3 plugins (recommended)" Pop $PRISM_VST_CHECKBOX - ${NSD_Check} $PRISM_VST_CHECKBOX + ${NSD_SetState} $PRISM_VST_CHECKBOX $PRISM_VST_STATE - ${NSD_CreateLabel} 0 22u 100% 80u "Installs the seven Prism scope plugins (Spectrum, Oscilloscope, Vectorscope, Spectrogram, VU Meter, Loudness Meter, Waveform) to:$\r$\n$\r$\n $COMMONFILES64\VST3$\r$\n$\r$\nDAWs (FL Studio, Ableton, Logic, Reaper, etc.) will find them on the next plugin rescan.$\r$\n$\r$\nUncheck to install only the Prism desktop app. You can re-run this installer at any time to add the plugins later." + ${NSD_CreateCheckBox} 0 20u 100% 12u "Install Prism CLAP plugins (recommended)" + Pop $PRISM_CLAP_CHECKBOX + ${NSD_SetState} $PRISM_CLAP_CHECKBOX $PRISM_CLAP_STATE + + ${NSD_CreateLabel} 0 42u 100% 90u "Each format includes eight analyzers plus Prism Bridge.$\r$\n$\r$\nVST3: $COMMONFILES64\VST3$\r$\nCLAP: $COMMONFILES64\CLAP$\r$\n$\r$\nRescan plugins in a DAW that supports the selected format.$\r$\nPrism Bridge sends a track or bus to the Prism app.$\r$\nUncheck both to install only the app and prism-tui." Pop $0 nsDialogs::Show @@ -68,6 +90,7 @@ Function prismVstOptionsPageLeave ${NSD_GetState} $PRISM_VST_CHECKBOX $PRISM_VST_STATE + ${NSD_GetState} $PRISM_CLAP_CHECKBOX $PRISM_CLAP_STATE FunctionEnd !macroend @@ -130,6 +153,80 @@ DetailPrint "Step 3 of 4: Prism VST3 plugins skipped (opted out)." ${EndIf} + ${If} $PRISM_CLAP_STATE == ${BST_CHECKED} + DetailPrint "Step 3 of 4: Installing Prism CLAP plugins to $COMMONFILES64\CLAP..." + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\*.clap" + DetailPrint "WARNING: bundled Prism CLAP plugins were not found; skipping CLAP installation" + MessageBox MB_OK|MB_ICONEXCLAMATION "Prism was installed, but this package does not contain the optional CLAP plugins." /SD IDOK + ${Else} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Spectrum.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Spectrum.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Oscilloscope.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Oscilloscope.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism VU Meter.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism VU Meter.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Loudness Meter.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Loudness Meter.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Vectorscope.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Vectorscope.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Spectrogram.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Spectrogram.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Waveform.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Waveform.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Waterfall.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Waterfall.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + ${IfNot} ${FileExists} "$INSTDIR\resources\plugins\CLAP\Prism Bridge.clap" + MessageBox MB_OK|MB_ICONSTOP "Missing bundled CLAP plugin: Prism Bridge.clap" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + CreateDirectory "$COMMONFILES64\CLAP" + ClearErrors + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Spectrum.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Oscilloscope.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism VU Meter.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Loudness Meter.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Vectorscope.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Spectrogram.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Waveform.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Waterfall.clap" "$COMMONFILES64\CLAP" + CopyFiles /SILENT "$INSTDIR\resources\plugins\CLAP\Prism Bridge.clap" "$COMMONFILES64\CLAP" + ${If} ${Errors} + DetailPrint "ERROR: Prism CLAP plugin copy failed" + MessageBox MB_OK|MB_ICONSTOP "Prism CLAP plugin installation failed while copying files to:$\r$\n$COMMONFILES64\CLAP" /SD IDOK + SetErrorLevel 1 + Abort + ${EndIf} + DetailPrint "Installed Prism CLAP plugins." + ${EndIf} + ${Else} + DetailPrint "Step 3 of 4: Prism CLAP plugins skipped (opted out)." + ${EndIf} + DetailPrint "Step 4 of 4: Finishing Prism installation..." !macroend !endif @@ -159,4 +256,15 @@ RMDir /r "$COMMONFILES64\VST3\Prism Vectorscope.vst3" RMDir /r "$COMMONFILES64\VST3\Prism Spectrogram.vst3" RMDir /r "$COMMONFILES64\VST3\Prism Waveform.vst3" + RMDir /r "$COMMONFILES64\VST3\Prism Waterfall.vst3" + RMDir /r "$COMMONFILES64\VST3\Prism Bridge.vst3" + Delete "$COMMONFILES64\CLAP\Prism Spectrum.clap" + Delete "$COMMONFILES64\CLAP\Prism Oscilloscope.clap" + Delete "$COMMONFILES64\CLAP\Prism VU Meter.clap" + Delete "$COMMONFILES64\CLAP\Prism Loudness Meter.clap" + Delete "$COMMONFILES64\CLAP\Prism Vectorscope.clap" + Delete "$COMMONFILES64\CLAP\Prism Spectrogram.clap" + Delete "$COMMONFILES64\CLAP\Prism Waveform.clap" + Delete "$COMMONFILES64\CLAP\Prism Waterfall.clap" + Delete "$COMMONFILES64\CLAP\Prism Bridge.clap" !macroend diff --git a/scripts/build/build-native.cjs b/scripts/build/build-native.cjs new file mode 100644 index 0000000..fe9fdfc --- /dev/null +++ b/scripts/build/build-native.cjs @@ -0,0 +1,24 @@ +const { spawnSync } = require('node:child_process') +const { join } = require('node:path') + +const options = process.argv.slice(2) +const args = ['rebuild'] +if (options.includes('--electron')) { + args.push( + `--target=${require('electron/package.json').version}`, + `--arch=${process.arch}`, + '--dist-url=https://electronjs.org/headers', + ) +} +args.push(...options.filter(option => option !== '--electron')) + +// Both node-gyp and @electron/node-gyp provide the same command shim. Resolve +// our direct dependency explicitly: the older Electron fork can inherit Node's +// clang setting on Windows and request ClangCL instead of the installed MSVC. +const result = spawnSync(process.execPath, [require.resolve('node-gyp/bin/node-gyp.js'), ...args], { + cwd: join(__dirname, '..', '..', 'native'), + stdio: 'inherit', + shell: false, +}) +if (result.error) console.error(`Could not start native build: ${result.error.message}`) +process.exitCode = result.status ?? 1 diff --git a/scripts/build/build-plugins.cjs b/scripts/build/build-plugins.cjs new file mode 100644 index 0000000..53d5a87 --- /dev/null +++ b/scripts/build/build-plugins.cjs @@ -0,0 +1,150 @@ +const { existsSync, readFileSync } = require('node:fs') +const { join } = require('node:path') +const { spawnSync } = require('node:child_process') + +const help = `Usage: npm run build:plugins -- [options] [CMake options] + +Builds the web UI, configures plugin/build, and compiles the plugins. + + --configure-only Build the UI and configure CMake without compiling + --test Also enable and run the native plugin tests + --install Copy plugins into the platform's plugin folders after building + --stage Build all release formats and stage them for packaging + --jobs N Parallel build jobs (default: CMAKE_BUILD_PARALLEL_LEVEL or 4) + --help Show this help + +CMake options: -DNAME=VALUE, -G GENERATOR, -A PLATFORM, -T TOOLSET. +Existing generator and format selections are preserved unless overridden. +Defaults: Release, embedded UI, no installation. Use -DCMAKE_BUILD_TYPE=Debug +for a debug build. AU is available only on macOS; CMake selects native formats. +` + +function parseOptions(argv, env) { + const options = { cmakeArgs: [], definitions: new Map(), jobs: env.CMAKE_BUILD_PARALLEL_LEVEL || '4' } + const valueAfter = (index, flag) => { + const value = argv[index + 1] + if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value.`) + return value + } + for (let i = 0; i < argv.length; ++i) { + const arg = argv[i] + if (arg === '--help') options.help = true + else if (arg === '--configure-only') options.configureOnly = true + else if (arg === '--test') options.test = true + else if (arg === '--install') options.install = true + else if (arg === '--stage') options.stage = true + else if (arg === '--jobs') options.jobs = valueAfter(i++, arg) + else if (['-G', '-A', '-T'].includes(arg)) options.cmakeArgs.push(arg, valueAfter(i++, arg)) + else if (arg.startsWith('-D')) { + const definition = arg === '-D' ? valueAfter(i++, arg) : arg.slice(2) + const match = /^([^:=]+)(?::[^=]+)?=(.*)$/.exec(definition) + if (!match) throw new Error(`Invalid CMake definition: ${arg}. Use -DNAME=VALUE.`) + options.definitions.set(match[1], match[2]) + options.cmakeArgs.push(`-D${definition}`) + } else throw new Error(`Unknown option: ${arg}. Run with --help for usage.`) + } + if (!/^[1-9]\d*$/.test(options.jobs)) throw new Error('--jobs must be a positive integer.') + if (options.test && options.configureOnly) throw new Error('--test cannot be combined with --configure-only.') + if (options.configureOnly && (options.install || options.stage)) throw new Error('--configure-only cannot install or stage plugins.') + if (options.stage && options.install) throw new Error('--stage cannot be combined with --install.') + if (options.definitions.has('PRISM_COPY_PLUGIN_AFTER_BUILD') && + !/^(OFF|FALSE|NO|0)$/i.test(options.definitions.get('PRISM_COPY_PLUGIN_AFTER_BUILD'))) { + throw new Error('CMake copy-after-build is disabled. Use --install or npm run install:plugins for the separate copy step.') + } + return options +} + +function buildPlugins({ + argv = process.argv.slice(2), + platform = process.platform, + env = process.env, + rootDir = join(__dirname, '..', '..'), + spawn = spawnSync, +} = {}) { + const options = parseOptions(argv, env) + if (options.help) { + console.log(help) + return + } + if (!['win32', 'darwin', 'linux'].includes(platform)) throw new Error(`Plugin builds are not configured for ${platform}.`) + if (!env.npm_execpath) throw new Error('Could not locate npm. Run this script through npm run build:plugins.') + + function run(label, command, args) { + const result = spawn(command, args, { cwd: rootDir, env, stdio: 'inherit', shell: false }) + if (result.error) { + const hint = command === 'cmake' || command === 'ctest' ? ' Install CMake 3.22+ and ensure it is on PATH.' : '' + throw new Error(`${label} could not start: ${result.error.message}.${hint}`) + } + if (result.status !== 0) { + const error = new Error(`${label} failed (${result.signal || `exit ${result.status ?? 1}`}).`) + error.exitCode = result.status > 0 ? result.status : 1 + throw error + } + } + + const config = options.definitions.get('CMAKE_BUILD_TYPE') ?? 'Release' + if (!config.trim()) throw new Error('CMAKE_BUILD_TYPE must name a configuration, such as Release or Debug.') + if (options.stage && config !== 'Release') throw new Error('Installer staging requires a Release build.') + const stage = () => run('Plugin staging', process.execPath, [join(rootDir, 'scripts/build/stage-plugins.mjs')]) + if (options.stage && !options.test && env.PRISM_PLUGINS_PREBUILT === '1') { + // Release CI has already built and tested with its compiler environment. + // Still validate every required artifact before packaging. + stage() + return + } + const cachePath = join(rootDir, 'plugin/build/CMakeCache.txt') + const cache = existsSync(cachePath) ? readFileSync(cachePath, 'utf8') : '' + const configureArgs = [ + '-S', 'plugin', '-B', 'plugin/build', + `-DCMAKE_BUILD_TYPE=${config}`, + '-DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF', + '-DPRISM_DEV_SERVER=OFF', + ] + + // JUCE needs the parent of Microsoft.Web.WebView2, not the package itself. + // Explicit CMake arguments take priority over the environment, cache, and local SDK. + if (platform === 'win32' && !options.definitions.has('JUCE_WEBVIEW2_PACKAGE_LOCATION')) { + const cachedSdk = /^JUCE_WEBVIEW2_PACKAGE_LOCATION:[^=]+=(.+)$/m.exec(cache)?.[1].trim() + const localSdk = join(rootDir, 'plugin/sdk') + if (env.JUCE_WEBVIEW2_PACKAGE_LOCATION) { + configureArgs.push(`-DJUCE_WEBVIEW2_PACKAGE_LOCATION:PATH=${env.JUCE_WEBVIEW2_PACKAGE_LOCATION}`) + } else if (!cachedSdk && existsSync(join(localSdk, 'Microsoft.Web.WebView2/build/native/include/WebView2.h'))) { + configureArgs.push(`-DJUCE_WEBVIEW2_PACKAGE_LOCATION:PATH=${localSdk}`) + } + } + configureArgs.push(...options.cmakeArgs) + if (options.stage) { + const requiredFormats = platform === 'darwin' ? ['AU', 'VST3', 'CLAP'] : ['VST3', 'CLAP'] + const previousFormats = options.definitions.get('PRISM_PLUGIN_FORMATS') ?? + /^PRISM_PLUGIN_FORMATS:[^=]+=(.*)$/m.exec(cache)?.[1].trim() ?? '' + const formats = [...new Set([...previousFormats.split(';').filter(Boolean), ...requiredFormats])] + configureArgs.push(`-DPRISM_PLUGIN_FORMATS=${formats.join(';')}`, '-DPRISM_DEV_SERVER=OFF', '-DPRISM_LINUX_UI_MODE=webview') + } + if (options.test) configureArgs.push('-DBUILD_TESTING=ON') + + // Launch npm's JS entry point through Node: Windows .cmd shims cannot be + // spawned directly on recent Node versions, and shell quoting breaks paths. + run('Plugin UI build', process.execPath, [env.npm_execpath, 'run', 'plugin-ui:build']) + run('Plugin configuration', 'cmake', configureArgs) + if (options.configureOnly) return + const targets = options.stage && !options.test ? ['--target', 'PrismInstallerPlugins'] : [] + run('Plugin compilation', 'cmake', ['--build', 'plugin/build', '--config', config, '--parallel', options.jobs, ...targets]) + if (options.test) run('Plugin tests', 'ctest', ['--test-dir', 'plugin/build', '-C', config, '--output-on-failure']) + if (options.stage) stage() + if (options.install) { + console.log('Compilation succeeded. Installing the built plugins separately...') + run('Plugin installation (compiled files are still available in plugin/build)', process.execPath, + [join(rootDir, 'scripts/build/install-plugins.mjs'), '--config', config]) + } +} + +module.exports = { buildPlugins } + +if (require.main === module) { + try { + buildPlugins() + } catch (error) { + console.error(`[plugins] ${error.message}`) + process.exitCode = error.exitCode || 1 + } +} diff --git a/scripts/build/install-plugins-windows.ps1 b/scripts/build/install-plugins-windows.ps1 new file mode 100644 index 0000000..372ebb8 --- /dev/null +++ b/scripts/build/install-plugins-windows.ps1 @@ -0,0 +1,99 @@ +param( + [string]$BuildDir, + [string]$Configuration = 'Release', + [string]$Formats = 'VST3,CLAP', + [switch]$Elevated +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Get-PrismWindowsInstallPlan { + param([string]$SourceRoot, [string]$Config, [string]$SelectedFormats, [string]$DestinationRoot) + if ($Config -notmatch '^[A-Za-z0-9_-]+$') { throw 'Invalid build configuration.' } + $formatList = @($SelectedFormats -split ',' | Select-Object -Unique) + if ($formatList.Count -eq 0 -or @($formatList | Where-Object { $_ -notin @('VST3', 'CLAP') }).Count -gt 0) { + throw 'Expected VST3 and/or CLAP formats.' + } + $products = @( + @('PrismSpectrum', 'Prism Spectrum'), @('PrismOscilloscope', 'Prism Oscilloscope'), + @('PrismVUMeter', 'Prism VU Meter'), @('PrismLUFSMeter', 'Prism Loudness Meter'), + @('PrismVectorscope', 'Prism Vectorscope'), @('PrismSpectrogram', 'Prism Spectrogram'), + @('PrismWaveform', 'Prism Waveform'), @('PrismWaterfall', 'Prism Waterfall'), + @('PrismBridge', 'Prism Bridge') + ) + foreach ($format in $formatList) { + $destinationDirectory = [IO.Path]::GetFullPath((Join-Path $DestinationRoot $format)) + foreach ($product in $products) { + $name = $product[1] + '.' + $format.ToLowerInvariant() + $source = [IO.Path]::GetFullPath((Join-Path $SourceRoot ($product[0] + "_artefacts\$Config\$format\$name"))) + $destination = [IO.Path]::GetFullPath((Join-Path $destinationDirectory $name)) + if ([IO.Path]::GetDirectoryName($destination) -ne $destinationDirectory) { throw "Invalid destination: $destination" } + $pathType = if ($format -eq 'VST3') { 'Container' } else { 'Leaf' } + if (-not (Test-Path -LiteralPath $source -PathType $pathType)) { throw "Missing built plugin: $source" } + if ($format -eq 'VST3') { + $contents = Join-Path $source 'Contents' + $binaries = @(Get-ChildItem -LiteralPath $contents -Directory | Where-Object Name -Like '*-win' | + ForEach-Object { Get-ChildItem -LiteralPath $_.FullName -Filter '*.vst3' -File }) + if ($binaries.Count -eq 0) { throw "Missing plugin binary: $source" } + } + if ((Test-Path -LiteralPath $destination) -and + ((Get-Item -LiteralPath $destination -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { + throw "Cannot install over a linked plugin: $destination" + } + [PSCustomObject]@{ Source = $source; Directory = $destinationDirectory; Destination = $destination } + } + } +} + +function Copy-PrismWindowsPlugins { + param([object[]]$Plan) + foreach ($entry in $Plan) { + New-Item -ItemType Directory -Path $entry.Directory -Force | Out-Null + Copy-Item -LiteralPath $entry.Source -Destination $entry.Directory -Recurse -Force + Write-Host "Installed $($entry.Destination)" + } +} + +function Get-PrismElevationArguments { + param([string]$Script, [string]$SourceRoot, [string]$Config, [string]$SelectedFormats) + # Encode the complete invocation so Start-Process cannot lose quotes around + # paths containing spaces, apostrophes, or PowerShell metacharacters. + $invocation = "& '" + $Script.Replace("'", "''") + "' -BuildDir '" + $SourceRoot.Replace("'", "''") + + "' -Configuration '" + $Config.Replace("'", "''") + "' -Formats '" + $SelectedFormats.Replace("'", "''") + "' -Elevated" + $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($invocation)) + return @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encoded) +} + +# Dot-sourcing exposes only the copy/planning functions for isolated tests. +if ($MyInvocation.InvocationName -ne '.') { + try { + $commonFiles = $env:CommonProgramW6432 + if (-not $commonFiles) { $commonFiles = [Environment]::GetFolderPath('CommonProgramFiles') } + $plan = @(Get-PrismWindowsInstallPlan $BuildDir $Configuration $Formats $commonFiles) + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + if ($Elevated) { throw 'Windows did not grant administrator permissions.' } + Write-Host 'Requesting administrator permission to copy the built Prism plugins...' + $arguments = Get-PrismElevationArguments $PSCommandPath $BuildDir $Configuration $Formats + try { + $child = Start-Process -FilePath (Join-Path $PSHOME 'powershell.exe') -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden -Wait -PassThru + } catch { + $exception = $_.Exception + while ($exception) { + if ($exception -is [ComponentModel.Win32Exception] -and $exception.NativeErrorCode -eq 1223) { exit 1223 } + $exception = $exception.InnerException + } + throw + } + if ($child.ExitCode -ne 0) { Write-Host 'The elevated copy failed. Close DAWs using Prism plugins and check permissions in Common Files.' } + else { Write-Host "Installed $($plan.Count) plugins into $commonFiles." } + exit $child.ExitCode + } + Copy-PrismWindowsPlugins $plan + } catch { + [Console]::Error.WriteLine($_.Exception.Message) + exit 1 + } +} diff --git a/scripts/build/install-plugins.mjs b/scripts/build/install-plugins.mjs new file mode 100644 index 0000000..919a9e9 --- /dev/null +++ b/scripts/build/install-plugins.mjs @@ -0,0 +1,112 @@ +import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawnSync } from 'node:child_process' +import { collectPluginArtifacts, releasePluginFormats } from './stage-plugins.mjs' + +const scriptPath = fileURLToPath(import.meta.url) +const repoRoot = resolve(dirname(scriptPath), '../..') + +export function pluginInstallDirectories({ platform = process.platform, system = platform === 'win32', home = homedir(), env = process.env } = {}) { + releasePluginFormats(platform) + if (platform === 'win32') { + const base = system ? env.CommonProgramW6432 || env.CommonProgramFiles : env.LOCALAPPDATA && join(env.LOCALAPPDATA, 'Programs/Common') + if (!base) throw new Error(`Cannot locate Windows ${system ? 'Common Files' : 'Local AppData'} directory.`) + return { VST3: join(base, 'VST3'), CLAP: join(base, 'CLAP') } + } + if (platform === 'darwin') { + const base = system ? '/Library/Audio/Plug-Ins' : join(home, 'Library/Audio/Plug-Ins') + return { VST3: join(base, 'VST3'), CLAP: join(base, 'CLAP'), AU: join(base, 'Components') } + } + return system ? { VST3: '/usr/lib/vst3', CLAP: '/usr/lib/clap' } : { VST3: join(home, '.vst3'), CLAP: join(home, '.clap') } +} + +export function copyPluginArtifacts(products, directories) { + // Check all destinations before copying anything. Only named Prism products + // are updated, and an existing link must not redirect a privileged copy. + const plan = products.map(product => { + const root = resolve(directories[product.format]) + const destination = resolve(root, product.name) + if (dirname(destination) !== root || lstatSync(destination, { throwIfNoEntry: false })?.isSymbolicLink()) { + throw new Error(`Invalid plugin destination: ${destination}`) + } + return { ...product, root, destination } + }) + for (const { source, root, destination } of plan) { + mkdirSync(root, { recursive: true }) + cpSync(source, destination, { recursive: true, force: true, verbatimSymlinks: true }) + console.log(`Installed ${destination}`) + } +} + +export function installPlugins({ + argv = process.argv.slice(2), platform = process.platform, env = process.env, + rootDir = repoRoot, home = homedir(), spawn = spawnSync, getuid = process.getuid, +} = {}) { + let config = 'Release' + let formats + let scope + let dryRun = false + for (let i = 0; i < argv.length; ++i) { + const arg = argv[i] + if (arg === '--help') { + console.log('Usage: npm run install:plugins -- [--config Release] [--formats VST3,CLAP] [--user | --system] [--dry-run]\nInstalls already-built plugins. Windows defaults to system folders with UAC; macOS/Linux default to user folders. No compilation runs elevated.') + return + } + if (arg === '--user' || arg === '--system') { + if (scope && scope !== arg) throw new Error('Choose either --user or --system.') + scope = arg + } else if (arg === '--dry-run') dryRun = true + else if (arg === '--config' || arg === '--formats') { + const value = argv[++i] + if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value.`) + if (arg === '--config') config = value + else formats = value.split(',') + } else throw new Error(`Unknown option: ${arg}`) + } + if (!/^[A-Za-z0-9_-]+$/.test(config)) throw new Error('Invalid build configuration.') + const supported = releasePluginFormats(platform) + const buildDir = join(rootDir, 'plugin/build') + if (!formats) { + const cachePath = join(buildDir, 'CMakeCache.txt') + const cache = existsSync(cachePath) ? readFileSync(cachePath, 'utf8') : '' + const selected = /^PRISM_PLUGIN_FORMATS:[^=]+=(.*)$/m.exec(cache)?.[1].trim() + if (selected === undefined) throw new Error('No configured plugin build found. Run npm run build:plugins first.') + formats = selected.split(';').filter(format => supported.includes(format)) + } + formats = [...new Set(formats)] + const products = collectPluginArtifacts({ buildDir, platform, config, formats }) + const system = scope ? scope === '--system' : platform === 'win32' + const directories = pluginInstallDirectories({ platform, system, home, env }) + if (dryRun) { + for (const { source, format, name } of products) console.log(`${source} -> ${join(directories[format], name)}`) + return + } + + function run(command, args) { + const result = spawn(command, args, { cwd: rootDir, env, stdio: 'inherit', shell: false }) + if (result.error) throw new Error(`Could not start plugin installation: ${result.error.message}`) + if (result.status !== 0) { + const error = new Error(result.status === 1223 ? 'Plugin installation cancelled at the UAC prompt. Built plugins are unchanged.' : + `Plugin installation failed (exit ${result.status ?? 1}). Built plugins remain in plugin/build; close any DAW using them and check the copy error above.`) + error.exitCode = result.status > 0 ? result.status : 1 + throw error + } + } + if (platform === 'win32' && system) { + run('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', + join(rootDir, 'scripts/build/install-plugins-windows.ps1'), '-BuildDir', buildDir, + '-Configuration', config, '-Formats', formats.join(',')]) + } else if (system && getuid?.() !== 0) { + // Elevate only this copy helper, never npm, CMake, or dependency scripts. + run('sudo', ['--', process.execPath, scriptPath, '--system', '--config', config, '--formats', formats.join(',')]) + } else { + copyPluginArtifacts(products, directories) + } +} + +if (process.argv[1] && resolve(process.argv[1]) === scriptPath) { + try { installPlugins() } + catch (error) { console.error(`[plugins] ${error.message}`); process.exitCode = error.exitCode || 1 } +} diff --git a/scripts/build/postinstall-native.cjs b/scripts/build/postinstall-native.cjs index 51bdb79..8417162 100644 --- a/scripts/build/postinstall-native.cjs +++ b/scripts/build/postinstall-native.cjs @@ -1,4 +1,5 @@ const { spawnSync } = require('node:child_process') +const { join } = require('node:path') const skipNativePostinstall = /^(1|true|yes)$/i.test(process.env.PRISM_SKIP_NATIVE_POSTINSTALL || '') @@ -7,12 +8,16 @@ if (skipNativePostinstall) { process.exit(0) } -const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm' -const result = spawnSync(npmCommand, ['run', 'rebuild:native'], { stdio: 'inherit' }) +// Launch JavaScript through Node; Windows cannot spawn npm.cmd without a shell. +const result = spawnSync(process.execPath, [join(__dirname, 'build-native.cjs'), '--electron'], { + stdio: 'inherit', + shell: false, +}) if (result.status === 0) { process.exit(0) } +if (result.error) console.warn(`Could not start native build: ${result.error.message}`) console.warn('Native build failed, will use JS fallback') process.exit(0) diff --git a/scripts/build/stage-plugins.mjs b/scripts/build/stage-plugins.mjs new file mode 100644 index 0000000..7c144cd --- /dev/null +++ b/scripts/build/stage-plugins.mjs @@ -0,0 +1,96 @@ +import { cpSync, mkdirSync, readdirSync, rmSync, statSync, chmodSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +export const pluginProducts = [ + ['PrismSpectrum', 'Prism Spectrum'], + ['PrismOscilloscope', 'Prism Oscilloscope'], + ['PrismVUMeter', 'Prism VU Meter'], + ['PrismLUFSMeter', 'Prism Loudness Meter'], + ['PrismVectorscope', 'Prism Vectorscope'], + ['PrismSpectrogram', 'Prism Spectrogram'], + ['PrismWaveform', 'Prism Waveform'], + ['PrismWaterfall', 'Prism Waterfall'], + ['PrismBridge', 'Prism Bridge'], +] + +const repoRoot = fileURLToPath(new URL('../../', import.meta.url)) + +export function releasePluginFormats(platform) { + if (!['darwin', 'linux', 'win32'].includes(platform)) throw new Error(`Unsupported platform: ${platform}`) + return platform === 'darwin' ? ['VST3', 'CLAP', 'AU'] : ['VST3', 'CLAP'] +} + +export function collectPluginArtifacts({ + buildDir = join(repoRoot, 'plugin/build'), + platform = process.platform, + config = 'Release', + formats = releasePluginFormats(platform), +} = {}) { + const supported = releasePluginFormats(platform) + if (!formats.length || formats.some(format => !supported.includes(format))) throw new Error(`Invalid plugin formats for ${platform}: ${formats}`) + const products = [] + const requirePath = (path, directory) => { + const stat = statSync(path, { throwIfNoEntry: false }) + if (!(directory ? stat?.isDirectory() : stat?.isFile())) { + throw new Error(`Missing plugin ${directory ? 'bundle/directory' : 'file'}: ${path}`) + } + } + + // Check everything before replacing a previously staged, working release. + for (const format of formats) { + const extension = { VST3: 'vst3', CLAP: 'clap', AU: 'component' }[format] + for (const [target, product] of pluginProducts) { + const name = `${product}.${extension}` + const source = join(buildDir, `${target}_artefacts`, config, format, name) + requirePath(source, platform === 'darwin' || format !== 'CLAP') + if (platform === 'darwin') { + requirePath(join(source, 'Contents/Info.plist'), false) + requirePath(join(source, 'Contents/MacOS', product), false) + } else if (format === 'VST3') { + // JUCE also supports ARM builds; do not assume an x86_64 bundle. + const contents = join(source, 'Contents') + requirePath(contents, true) + const suffix = platform === 'win32' ? '-win' : '-linux' + const extension = platform === 'win32' ? '.vst3' : '.so' + const architectures = readdirSync(contents, { withFileTypes: true }).filter(entry => entry.isDirectory() && entry.name.endsWith(suffix)) + if (!architectures.some(entry => readdirSync(join(contents, entry.name), { withFileTypes: true }) + .some(binary => binary.isFile() && binary.name.endsWith(extension)))) { + throw new Error(`Missing plugin binary: ${source}`) + } + } + products.push({ source, format, name }) + } + } + return products +} + +export function stagePlugins({ + buildDir = join(repoRoot, 'plugin/build'), + destination = join(repoRoot, 'plugin/dist-installer'), + platform = process.platform, +} = {}) { + const formats = releasePluginFormats(platform) + const products = collectPluginArtifacts({ buildDir, platform, formats }) + + rmSync(destination, { recursive: true, force: true }) + for (const { source, format, name } of products) { + mkdirSync(join(destination, format), { recursive: true }) + cpSync(source, join(destination, format, name), { recursive: true, preserveTimestamps: true }) + } + for (const format of formats) { + const extension = { VST3: 'vst3', CLAP: 'clap', AU: 'component' }[format] + const count = readdirSync(join(destination, format)).filter((name) => name.endsWith(`.${extension}`)).length + if (count !== pluginProducts.length) throw new Error(`Expected 9 ${format} plugins, found ${count}`) + console.log(`Staged ${count} ${format} plugins`) + } + if (platform === 'linux') { + for (const format of ['vst3', 'clap']) { + const name = `install-${format}.sh` + cpSync(join(repoRoot, 'resources/installer/linux', name), join(destination, name)) + chmodSync(join(destination, name), 0o755) + } + } +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) stagePlugins() diff --git a/scripts/check-plugin-formats.mjs b/scripts/check-plugin-formats.mjs new file mode 100644 index 0000000..dc905fd --- /dev/null +++ b/scripts/check-plugin-formats.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict' +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' +import { pluginProducts } from './build/stage-plugins.mjs' + +const buildDir = resolve('plugin/build') +const cache = readFileSync(join(buildDir, 'CMakeCache.txt'), 'utf8') +const value = (name) => cache.split('\n').find((line) => line.startsWith(`${name}:`))?.split('=').slice(1).join('=').trim() +const juce = value('JUCE_SOURCE_DIR') +const clap = value('clap-juce-extensions_SOURCE_DIR') +assert.ok(juce && clap, 'Configure the full CLAP-enabled build first') + +for (const formats of ['CLAP', 'VST3']) { + const dir = join(buildDir, 'format-checks', formats.toLowerCase()) + const query = join(dir, '.cmake/api/v1/query') + mkdirSync(query, { recursive: true }) + writeFileSync(join(query, 'codemodel-v2'), '') + const args = ['-S', resolve('plugin'), '-B', dir, '-G', value('CMAKE_GENERATOR'), + `-DJUCE_PATH=${juce}`, `-DPRISM_PLUGIN_FORMATS=${formats}`, + '-DPRISM_COPY_PLUGIN_AFTER_BUILD=OFF', '-DPRISM_DEV_SERVER=ON', + '-DBUILD_TESTING=OFF', '-DCMAKE_BUILD_TYPE=Release', + // A deliberately invalid dependency path proves VST3 builds never load it. + `-DCLAP_JUCE_EXTENSIONS_PATH=${formats === 'CLAP' ? clap : join(dir, 'must-not-be-read')}`] + for (const name of ['JUCE_WEBVIEW2_PACKAGE_LOCATION', 'CMAKE_OSX_ARCHITECTURES', 'CMAKE_OSX_DEPLOYMENT_TARGET']) { + if (value(name)) args.push(`-D${name}=${value(name)}`) + } + const result = spawnSync('cmake', args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }) + writeFileSync(join(dir, 'configure.log'), `${result.stdout || ''}${result.stderr || ''}`) + assert.equal(result.status, 0, result.error?.message || result.stdout + result.stderr) + const reply = join(dir, '.cmake/api/v1/reply') + const indexName = readdirSync(reply).filter((name) => name.startsWith('index-')).sort().at(-1) + const index = JSON.parse(readFileSync(join(reply, indexName), 'utf8')) + const model = JSON.parse(readFileSync(join(reply, index.reply['codemodel-v2'].jsonFile), 'utf8')) + const targets = model.configurations[0].targets.map(({ name }) => name) + for (const [target] of pluginProducts) { + assert.ok(targets.includes(`${target}_${formats}`), `${target}_${formats} must exist`) + for (const excluded of ['CLAP', 'VST3', 'AU', 'Standalone'].filter((format) => format !== formats)) { + assert.ok(!targets.includes(`${target}_${excluded}`), `${target}_${excluded} must not exist`) + } + } + if (formats === 'VST3') { + assert.ok(!readFileSync(join(dir, 'CMakeCache.txt'), 'utf8').includes('CLAP_SOURCE_DIR:'), 'CLAP SDK was not loaded') + } + console.log(`${formats}-only configuration has exactly the nine requested plugin targets`) +} diff --git a/scripts/linux-test.mjs b/scripts/linux-test.mjs new file mode 100644 index 0000000..f8fd118 --- /dev/null +++ b/scripts/linux-test.mjs @@ -0,0 +1,158 @@ +import { createHash, randomUUID } from 'node:crypto' +import { spawn, execFileSync } from 'node:child_process' +import { createWriteStream, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const help = `Usage: npm run test:linux -- [options] + --target wsl|fedora Default: wsl + --distro NAME WSL distribution (default: Ubuntu) + --host USER@HOST SSH destination or alias (default: prism-fedora) + --suite routine|full Default: routine; full adds plugin tests and packages + --dry-run Preview source selection without transferring or running + --help Show this help + +Runs in a fresh ~/.cache/prism-linux-tests/runs//source directory. +Local logs: artifacts/linux-tests//. See docs/linux-testing.md for setup. +` + +export function parseOptions(args) { + const options = { target: 'wsl', distro: 'Ubuntu', host: 'prism-fedora', suite: 'routine' } + for (let i = 0; i < args.length; i++) { + const key = args[i].replace(/^--/, '') + if ((key === 'help' || key === 'dry-run') && args[i].startsWith('--')) options[key] = true + else if (['target', 'distro', 'host', 'suite'].includes(key) && args[i].startsWith('--')) { + const value = args[++i] + if (!value || value.startsWith('-')) throw new Error(`--${key} requires a value`) + options[key] = value + } else throw new Error(`Unknown option: ${args[i]}`) + } + if (!['wsl', 'fedora'].includes(options.target)) throw new Error('Target must be wsl or fedora') + if (!['routine', 'full'].includes(options.suite)) throw new Error('Suite must be routine or full') + if (!/^[\w.@:[\]-]+$/.test(options.host) || options.host.startsWith('-')) throw new Error('Invalid SSH host') + return options +} + +export function shellQuote(value) { + return `'${String(value).replaceAll("'", "'\\''")}'` +} + +export function selectSourceFiles(repo) { + const paths = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], { + cwd: repo, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, + }).split('\0').filter(Boolean) + return [...new Set(paths)].filter((path) => { + // Defense in depth if a generated file has accidentally become tracked. + if (/(^|\/)(\.git|node_modules)(\/|$)/.test(path) || + /^(artifacts|out|dist|native\/build|tui\/build|tui\/dist-installer|plugin\/build|plugin\/sdk|plugin\/webview-dist|plugin\/dist-installer)(\/|$)/.test(path)) return false + const fullPath = join(repo, path) + if (!existsSync(fullPath)) return false // A tracked deletion must stay deleted. + const stat = lstatSync(fullPath) + if (stat.isSymbolicLink()) throw new Error(`Source symlinks need explicit handling: ${path}`) + return stat.isFile() + }).sort() +} + +export async function main(args = process.argv.slice(2)) { + const options = parseOptions(args) + if (options.help) { console.log(help); return 0 } + if (options.target === 'wsl' && process.platform !== 'win32') throw new Error('The WSL target must be launched from Windows') + const files = selectSourceFiles(rootDir) + if (options['dry-run']) { + console.log(JSON.stringify({ ...options, sourceRoot: rootDir, fileCount: files.length, files }, null, 2)) + return 0 + } + const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${options.target}-${randomUUID().slice(0, 8)}` + const logDir = join(rootDir, 'artifacts', 'linux-tests', runId) + mkdirSync(logDir, { recursive: true }) + const log = createWriteStream(join(logDir, 'runner.log')) + const result = { runId, ...options, startedAt: new Date().toISOString(), status: 'running', exitCode: null, steps: [] } + const gitModes = new Map(execFileSync('git', ['ls-files', '--stage', '-z'], { cwd: rootDir, encoding: 'utf8' }) + .split('\0').filter(Boolean).map((entry) => [entry.slice(entry.indexOf('\t') + 1), entry.slice(0, 6)])) + const source = { + revision: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: rootDir, encoding: 'utf8' }).trim(), + gitStatus: execFileSync('git', ['status', '--porcelain=v1'], { cwd: rootDir, encoding: 'utf8' }), + files: files.map((path) => ({ path, mode: gitModes.get(path) ?? '100644', sha256: createHash('sha256').update(readFileSync(join(rootDir, path))).digest('hex') })), + } + source.isDirty = source.gitStatus.length > 0 + source.fingerprint = createHash('sha256').update(JSON.stringify(source.files)).digest('hex') + writeFileSync(join(logDir, 'source-manifest.json'), JSON.stringify(source, null, 2) + '\n') + writeFileSync(join(logDir, 'files.list'), files.join('\0') + '\0') + const archive = join(logDir, 'source.tar.gz') + let remotePath + + async function run(label, command, argv, input) { + const step = { label, startedAt: new Date().toISOString(), exitCode: null } + result.steps.push(step) + const heading = `\n[linux-test] ${label}\n` + process.stdout.write(heading); log.write(heading) + step.exitCode = await new Promise((resolveExit) => { + const child = spawn(command, argv, { cwd: rootDir, shell: false, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }) + for (const stream of [child.stdout, child.stderr]) stream.on('data', (chunk) => { + process.stdout.write(chunk); log.write(chunk) + }) + child.on('error', (error) => { log.write(error.message + '\n'); console.error(error.message); resolveExit(1) }) + child.on('close', (code) => resolveExit(code ?? 1)) + child.stdin.on('error', () => {}) // Connection failure can close stdin early. + child.stdin.end(input) + }) + step.finishedAt = new Date().toISOString() + log.write(`[linux-test] ${label}: exit ${step.exitCode}\n`) + if (step.exitCode !== 0) throw Object.assign(new Error(`${label} failed (exit ${step.exitCode})`), { exitCode: step.exitCode }) + } + + function remote(label, script) { + const command = options.target === 'wsl' ? 'wsl.exe' : 'ssh' + const argv = options.target === 'wsl' + ? ['-d', options.distro, '--exec', 'bash', '-s'] + : ['-T', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', options.host, 'bash -s'] + return run(label, command, argv, `set -euo pipefail\n${script}\n`) + } + + try { + console.log(`Linux test logs: ${logDir}`) + await run('Archive current working files', 'tar', ['-czf', archive, '--null', '-T', join(logDir, 'files.list'), '--no-recursion']) + // The fresh directory makes deleted/renamed files and old build outputs unambiguous. + remotePath = `.cache/prism-linux-tests/runs/${runId}` + await remote('Create isolated Linux run', `umask 077\nmkdir -p "$HOME/${remotePath}"\ntest ! -e "$HOME/${remotePath}/source"\nmkdir "$HOME/${remotePath}/source"`) + if (options.target === 'wsl') { + const linuxArchive = execFileSync('wsl.exe', ['-d', options.distro, '--exec', 'wslpath', '-a', archive], { encoding: 'utf8' }).trim() + const linuxManifest = execFileSync('wsl.exe', ['-d', options.distro, '--exec', 'wslpath', '-a', join(logDir, 'source-manifest.json')], { encoding: 'utf8' }).trim() + await remote('Transfer source to WSL', `cp -- ${shellQuote(linuxArchive)} "$HOME/${remotePath}/source.tar.gz"\ncp -- ${shellQuote(linuxManifest)} "$HOME/${remotePath}/source-manifest.json"`) + } else { + await run('Transfer source over SSH', 'scp', ['-B', '-o', 'ConnectTimeout=10', archive, join(logDir, 'source-manifest.json'), `${options.host}:${remotePath}/`]) + } + await remote('Run Linux checks', `cd "$HOME/${remotePath}"\ntar -xzf source.tar.gz -C source\npython3 source/scripts/linux/prepare-source.py\nexport PRISM_GIT_COMMIT=${shellQuote(source.revision)}\nexport PRISM_GIT_DIRTY=${shellQuote(source.isDirty ? '1' : '0')}\nbash source/scripts/linux/run.sh ${shellQuote(options.suite)}`) + result.status = 'passed'; result.exitCode = 0 + } catch (error) { + console.error(error.message) + result.status = 'failed'; result.exitCode = error.exitCode || 1; result.error = error.message + } finally { + // Retrieve partial results even if compilation or a test failed. + if (remotePath) { + try { + if (options.target === 'wsl') { + const linuxLogs = execFileSync('wsl.exe', ['-d', options.distro, '--exec', 'wslpath', '-a', logDir], { encoding: 'utf8' }).trim() + await remote('Collect Linux logs', `if [[ -d "$HOME/${remotePath}/logs" ]]; then cp -r "$HOME/${remotePath}/logs" ${shellQuote(linuxLogs)}; fi`) + } else { + await run('Collect Linux logs', 'scp', ['-B', '-r', '-o', 'ConnectTimeout=10', `${options.host}:${remotePath}/logs`, join(logDir, 'logs')]) + } + } catch (error) { + result.collectionError = error.message + if (result.exitCode === 0) { result.exitCode = 1; result.status = 'failed' } + } + } + result.remoteDirectory = remotePath ? `~/${remotePath}` : null + result.sourceFingerprint = source.fingerprint + result.finishedAt = new Date().toISOString() + writeFileSync(join(logDir, 'result.json'), JSON.stringify(result, null, 2) + '\n') + await new Promise((resolveEnd) => log.end(resolveEnd)) + console.log(`\n${result.status.toUpperCase()}: ${logDir}\nLinux files: ${result.remoteDirectory}`) + } + return result.exitCode +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + main().then((code) => { process.exitCode = code }).catch((error) => { console.error(error.message); process.exitCode = 1 }) +} diff --git a/scripts/linux/prepare-source.py b/scripts/linux/prepare-source.py new file mode 100644 index 0000000..896b029 --- /dev/null +++ b/scripts/linux/prepare-source.py @@ -0,0 +1,26 @@ +"""Verify transferred bytes, then restore Linux shell endings and executable modes.""" +import hashlib +import json +import re +from pathlib import Path + +run_dir = Path.cwd() +source_dir = (run_dir / 'source').resolve() +manifest = json.loads((run_dir / 'source-manifest.json').read_text()) +prepared = [] +for entry in manifest['files']: + path = source_dir / entry['path'] + if not path.resolve().is_relative_to(source_dir) or path.is_symlink(): + raise RuntimeError(f"Invalid source path: {entry['path']}") + content = path.read_bytes() + if hashlib.sha256(content).hexdigest() != entry['sha256']: + raise RuntimeError(f"Source changed while archiving or transferring: {entry['path']}. Run again.") + is_shell = path.suffix == '.sh' or re.match(rb'^#![^\n]*\b(?:ba|da|z|k)?sh\b', content[:100]) + if is_shell and b'\r\n' in content: + content = content.replace(b'\r\n', b'\n') + path.write_bytes(content) + prepared.append({'path': entry['path'], 'change': 'CRLF to LF', 'sha256': hashlib.sha256(content).hexdigest()}) + path.chmod(0o755 if is_shell or entry['mode'] == '100755' else 0o644) +(run_dir / 'logs').mkdir(exist_ok=True) +(run_dir / 'logs' / 'source-preparation.json').write_text(json.dumps(prepared, indent=2) + '\n') +print(f"Verified {len(manifest['files'])} source files; normalized {len(prepared)} shell scripts.") diff --git a/scripts/linux/profile-diagnostics.cjs b/scripts/linux/profile-diagnostics.cjs new file mode 100644 index 0000000..1ba19ce --- /dev/null +++ b/scripts/linux/profile-diagnostics.cjs @@ -0,0 +1,98 @@ +// Opt-in, development-only hook. It can instrument an existing checkout without edits: +// NODE_OPTIONS='--require=/absolute/path/profile-diagnostics.cjs' npm run dev +if (process.versions.electron && process.type === 'browser') { + // NODE_OPTIONS runs before Electron registers its built-in module. Instrument + // the first normal import, then immediately restore Node's loader. + const Module = require('node:module') + const originalLoad = Module._load + Module._load = function (...args) { + const result = originalLoad.apply(this, args) + if (args[0] === 'electron' && result.app && result.ipcMain) { + Module._load = originalLoad + install(result) + } + return result + } +} + +function install({ app, ipcMain, Menu }) { + const { statSync, accessSync, constants } = require('node:fs') + const { join } = require('node:path') + const trace = (stage, details = {}) => console.error('[profile-trace]', JSON.stringify({ time: new Date().toISOString(), stage, ...details })) + trace('installed', { electron: process.versions.electron, platform: process.platform }) + + const originalHandle = ipcMain.handle.bind(ipcMain) + ipcMain.handle = (channel, listener) => originalHandle(channel, /^(profiles|dialog):/.test(channel) + ? async (event, ...args) => { + trace('invoke', { channel, sender: event.sender.id }) + try { + const result = await listener(event, ...args) + trace('resolved', { channel }) + return result + } catch (error) { + trace('rejected', { channel, error: String(error), stack: error?.stack }) + throw error + } + } : listener) + + const originalTemplate = Menu.buildFromTemplate.bind(Menu) + Menu.buildFromTemplate = (template) => { + const isProfileMenu = template.some((item) => item.label === 'Profiles') + if (!isProfileMenu) return originalTemplate(template) + trace('menu-built', { items: template.map((item) => item.label).filter(Boolean) }) + // Do not retain Menu instances: that could mask a native menu lifetime bug. + return originalTemplate(template.map((item) => { + if (!item.click) return item + const click = item.click + return { ...item, click: (...args) => { trace('menu-click', { label: item.label }); return click(...args) } } + })) + } + + app.on('web-contents-created', (_event, contents) => { + contents.on('console-message', (event) => { + const details = event + if (details && typeof details.message === 'string') { + trace('renderer-console', { sender: contents.id, level: details.level, message: details.message, source: details.sourceId, line: details.lineNumber }) + } + }) + contents.on('preload-error', (_event, preloadPath, error) => trace('preload-error', { preloadPath, error: String(error) })) + contents.on('render-process-gone', (_event, details) => trace('renderer-gone', details)) + contents.on('ipc-message', (_event, channel) => { + if (/^(profile-menu|dialog):/.test(channel)) trace('renderer-send', { channel, sender: contents.id }) + }) + contents.on('did-finish-load', () => { + void contents.executeJavaScript(`(() => { + const api = window.electronAPI; + if (!api) return 'no electronAPI'; + for (const name of ['onProfileMenuClosed', 'onProfileMenuLoad', 'onProfileMenuSaveNew', 'onProfileMenuSaveOverwrite', 'onProfileMenuRenameActive', 'onProfileMenuDeleteActive', 'onProfileMenuImport', 'onProfileMenuShowFolder']) { + if (typeof api[name] === 'function') api[name](() => console.warn('[profile-trace] renderer-received ' + name)); + } + console.warn('[profile-trace] document visibility=' + document.visibilityState + ' fonts=' + document.fonts.status); + if (location.search.includes('dialog')) { + requestAnimationFrame(() => console.warn('[profile-trace] dialog animation frame ran')); + } + return 'profile listeners attached'; + })()`).then((result) => trace('renderer-probe', { sender: contents.id, result })) + .catch((error) => trace('renderer-probe-error', { error: String(error) })) + }) + }) + + app.on('browser-window-created', (_event, win) => { + setTimeout(() => { + if (!win.isDestroyed()) trace('window-state', { id: win.id, visible: win.isVisible(), bounds: win.getBounds(), url: win.webContents.getURL() }) + }, 2000).unref() + }) + + app.whenReady().then(() => { + const paths = { documents: app.getPath('documents'), userData: app.getPath('userData') } + paths.profiles = join(paths.documents, 'Prism Profiles') + trace('environment', { ...paths, desktop: process.env.XDG_CURRENT_DESKTOP, session: process.env.XDG_SESSION_TYPE, display: process.env.DISPLAY, wayland: process.env.WAYLAND_DISPLAY, argv: process.argv }) + for (const [name, path] of Object.entries(paths)) { + try { + const stat = statSync(path) + accessSync(path, constants.R_OK | constants.W_OK) + trace('path-access', { name, path, writable: true, uid: stat.uid, mode: stat.mode.toString(8) }) + } catch (error) { trace('path-access', { name, path, error: String(error) }) } + } + }) +} diff --git a/scripts/linux/run.sh b/scripts/linux/run.sh new file mode 100644 index 0000000..8734c7d --- /dev/null +++ b/scripts/linux/run.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail +suite=${1:-routine} +[[ "$suite" == routine || "$suite" == full ]] || { echo 'Expected routine or full' >&2; exit 2; } +source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +run_dir=$(dirname "$source_dir") +mkdir -p "$run_dir/logs" +exec > >(tee "$run_dir/logs/linux.log") 2>&1 +trap 'code=$?; printf "exit_code=%s\nfinished_at=%s\n" "$code" "$(date -u +%FT%TZ)" > "$run_dir/logs/status.txt"' EXIT +export PATH="$HOME/.local/share/prism-linux/node/bin:/usr/local/bin:/usr/bin:/bin" +unset ELECTRON_RUN_AS_NODE NODE_OPTIONS +export CMAKE_BUILD_PARALLEL_LEVEL=4 +export npm_config_loglevel=warn +cd "$source_dir" + +step() { + local label=$1 + shift + printf '\n[linux-test] %s: %s\n' "$label" "$(date -u +%FT%TZ)" + local code=0 + "$@" || code=$? + printf '%s\t%s\n' "$label" "$code" >> "$run_dir/logs/steps.tsv" + if [[ $code != 0 ]]; then return "$code"; fi +} + +{ + cat /etc/os-release + uname -a + id + printf 'suite=%s\nsource=%s\ncommit=%s\ndirty=%s\n' "$suite" "$source_dir" "${PRISM_GIT_COMMIT:-unknown}" "${PRISM_GIT_DIRTY:-unknown}" + for tool in node npm git cmake g++ python3 pkg-config; do + command -v "$tool" || { echo "Missing $tool; run scripts/linux/setup.sh system" >&2; exit 1; } + done + node --version + npm --version + cmake --version + g++ --version + python3 --version + pkg-config --modversion libpulse + df -h . + printf 'desktop=%s\nsession=%s\ndisplay=%s\nwayland_display=%s\n' "${XDG_CURRENT_DESKTOP:-unset}" "${XDG_SESSION_TYPE:-unset}" "${DISPLAY:-unset}" "${WAYLAND_DISPLAY:-unset}" +} | tee "$run_dir/logs/environment.txt" + +step 'Verify Linux Node 22' node -e 'const [major,minor]=process.versions.node.split(".").map(Number);if(process.platform!=="linux" || major!==22 || minor<12)throw Error("Run scripts/linux/setup.sh node: Linux Node 22.12+ is required")' +step 'Install dependencies' env PRISM_SKIP_NATIVE_POSTINSTALL=1 npm ci --prefer-offline --no-audit --no-fund +step 'Build native addon' npm run rebuild:native +step 'Verify native addon' node -e 'const n=require("./native/build/Release/visualizer_dsp.node");if(!n.spectrum || !n.linuxCapture)throw Error("Missing Linux native exports")' +step 'Typecheck' npm run typecheck +step 'Automated tests' npm test +# An optional clean checkout of the pinned dependency permits offline VM runs. +ftxui_revision=$(sed -nE 's/^[[:space:]]*GIT_TAG ([[:xdigit:]]{40}).*/\1/p' tui/CMakeLists.txt) +ftxui_cache="$HOME/.cache/prism-linux-tests/dependencies/ftxui-$ftxui_revision" +if [[ -n "$ftxui_revision" && -d "$ftxui_cache/.git" ]]; then + [[ "$(git -C "$ftxui_cache" rev-parse HEAD)" == "$ftxui_revision" && -z "$(git -C "$ftxui_cache" status --porcelain)" ]] || { + echo "FTXUI cache does not match the clean pinned revision: $ftxui_cache" >&2; exit 1; + } + step 'Configure cached FTXUI' cmake -S tui -B tui/build "-DFETCHCONTENT_SOURCE_DIR_FTXUI=$ftxui_cache" +fi +step 'TUI build and tests' npm run test:tui +step 'Desktop build' npm run build +step 'Plugin UI build' npm run plugin-ui:build +if [[ "$suite" == full ]]; then + step 'Plugin build and tests' xvfb-run -a npm run test:plugins -- '-DPRISM_PLUGIN_FORMATS=VST3;CLAP' + step 'Linux packages' env PRISM_PLUGINS_PREBUILT=1 npm run dist:linux +fi +echo 'Automated checks passed. Fedora desktop/menu/audio checks are recorded separately.' diff --git a/scripts/linux/setup.sh b/scripts/linux/setup.sh new file mode 100644 index 0000000..9684c10 --- /dev/null +++ b/scripts/linux/setup.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +# System packages and the user-local Node installation are deliberately separate. +mode=${1:-node} +if [[ "$mode" == system ]]; then + if [[ $EUID != 0 ]]; then + echo 'Run the system step with sudo (or wsl -u root).' >&2 + exit 1 + fi + . /etc/os-release + case "$ID" in + ubuntu|debian) + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + ca-certificates curl xz-utils git rsync build-essential cmake ninja-build \ + pkg-config python3 libpulse-dev libasound2-dev libjack-jackd2-dev \ + ladspa-sdk libcurl4-openssl-dev libfreetype-dev libfontconfig1-dev \ + libgtk-3-dev libx11-dev libxcomposite-dev libxcursor-dev libxext-dev \ + libxinerama-dev libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev \ + xvfb xauth libglu1-mesa-dev mesa-common-dev libnss3 libxss1 \ + libxtst6 libsecret-1-0 libnotify4 xdg-utils pulseaudio-utils rpm + ;; + fedora) + dnf install -y \ + ca-certificates curl xz git rsync gcc gcc-c++ make cmake ninja-build \ + pkgconf-pkg-config python3 pulseaudio-libs-devel alsa-lib-devel \ + jack-audio-connection-kit-devel ladspa-devel libcurl-devel freetype-devel \ + fontconfig-devel gtk3-devel libX11-devel libXcomposite-devel \ + libXcursor-devel libXext-devel libXinerama-devel libXrandr-devel \ + libXrender-devel webkit2gtk4.1-devel xorg-x11-server-Xvfb xorg-x11-xauth \ + mesa-libGLU-devel mesa-libGL-devel nss libXScrnSaver libXtst \ + libsecret libnotify xdg-utils pulseaudio-utils rpm-build + ;; + *) echo "Unsupported distribution: $ID. Install the dependencies in docs/linux-testing.md." >&2; exit 1 ;; + esac +elif [[ "$mode" == node ]]; then + if [[ $EUID == 0 ]]; then + echo 'Run the Node step as the Linux desktop/test user, not root.' >&2 + exit 1 + fi + node_dir="$HOME/.local/share/prism-linux/node" + if [[ -x "$node_dir/bin/node" ]] && "$node_dir/bin/node" -e 'const [major,minor]=process.versions.node.split(".").map(Number);process.exit(major===22 && minor>=12 ? 0 : 1)'; then + "$node_dir/bin/node" --version + exit 0 + fi + case "$(uname -m)" in + x86_64) node_arch=x64 ;; + aarch64) node_arch=arm64 ;; + *) echo 'Node setup supports x86_64 and aarch64.' >&2; exit 1 ;; + esac + setup_dir=$(mktemp -d) + trap 'rm -rf -- "$setup_dir"' EXIT + base_url=https://nodejs.org/dist/latest-v22.x + curl --fail --location --retry 3 "$base_url/SHASUMS256.txt" -o "$setup_dir/SHASUMS256.txt" + checksum=$(grep -E " node-v22\.[0-9]+\.[0-9]+-linux-$node_arch\.tar\.xz$" "$setup_dir/SHASUMS256.txt") + archive=${checksum##* } + [[ -n "$checksum" && "$archive" != */* ]] + curl --fail --location --retry 3 "$base_url/$archive" -o "$setup_dir/$archive" + (cd "$setup_dir" && printf '%s\n' "$checksum" | sha256sum --check -) + mkdir -p "$node_dir" + tar -xJf "$setup_dir/$archive" --strip-components=1 -C "$node_dir" + "$node_dir/bin/node" --version + echo "Installed Linux Node in $node_dir. The test runner adds it to PATH automatically." +else + echo 'Usage: bash scripts/linux/setup.sh [system|node]' >&2 + exit 2 +fi diff --git a/scripts/run-daw-bridge-tests.mjs b/scripts/run-daw-bridge-tests.mjs new file mode 100644 index 0000000..07366d3 --- /dev/null +++ b/scripts/run-daw-bridge-tests.mjs @@ -0,0 +1,31 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawn } from 'node:child_process' +import { build } from 'esbuild' + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))) +const tempDir = await mkdtemp(join(tmpdir(), 'prism-daw-bridge-tests-')) +const bundledTestPath = join(tempDir, 'daw-bridge-service.test.mjs') + +let exitCode = 1 +try { + await build({ + entryPoints: [join(rootDir, 'test', 'daw-bridge-service.test.ts')], + outfile: bundledTestPath, + bundle: true, + platform: 'node', + format: 'esm', + target: 'node23', + sourcemap: 'inline', + }) + exitCode = await new Promise((resolve) => { + const child = spawn(process.execPath, ['--test', bundledTestPath], { stdio: 'inherit', cwd: rootDir }) + child.on('exit', (code) => resolve(code ?? 1)) + child.on('error', () => resolve(1)) + }) +} finally { + await rm(tempDir, { recursive: true, force: true }) +} +process.exit(exitCode) diff --git a/scripts/run-now-playing-ui-tests.mjs b/scripts/run-now-playing-ui-tests.mjs new file mode 100644 index 0000000..7445335 --- /dev/null +++ b/scripts/run-now-playing-ui-tests.mjs @@ -0,0 +1,45 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawn } from 'node:child_process' +import { build } from 'esbuild' + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))) +const tempDir = await mkdtemp(join(tmpdir(), 'prism-now-playing-ui-tests-')) +const bundledTestPath = join(tempDir, 'now-playing-ui.test.mjs') +const entryPoint = join(rootDir, 'test', 'now-playing-ui.test.tsx') + +let exitCode = 1 + +try { + await build({ + entryPoints: [entryPoint], + outfile: bundledTestPath, + bundle: true, + platform: 'node', + format: 'esm', + banner: { js: "import { createRequire as createTestRequire } from 'node:module'; const require = createTestRequire(import.meta.url);" }, + target: 'node23', + sourcemap: 'inline', + }) + + exitCode = await new Promise((resolve) => { + const child = spawn(process.execPath, ['--test', bundledTestPath], { + stdio: 'inherit', + cwd: rootDir, + }) + + child.on('exit', (code) => { + resolve(code ?? 1) + }) + + child.on('error', () => { + resolve(1) + }) + }) +} finally { + await rm(tempDir, { recursive: true, force: true }) +} + +process.exit(exitCode) diff --git a/scripts/run-reference-track-tests.mjs b/scripts/run-reference-track-tests.mjs new file mode 100644 index 0000000..be94397 --- /dev/null +++ b/scripts/run-reference-track-tests.mjs @@ -0,0 +1,44 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawn } from 'node:child_process' +import { build } from 'esbuild' + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))) +const tempDir = await mkdtemp(join(tmpdir(), 'prism-reference-track-tests-')) +const bundledTestPath = join(tempDir, 'reference-tracks.test.mjs') +const entryPoint = join(rootDir, 'test', 'reference-tracks.test.ts') + +let exitCode = 1 + +try { + await build({ + entryPoints: [entryPoint], + outfile: bundledTestPath, + bundle: true, + platform: 'node', + format: 'esm', + target: 'node23', + sourcemap: 'inline', + }) + + exitCode = await new Promise((resolve) => { + const child = spawn(process.execPath, ['--test', bundledTestPath, join(rootDir, 'test', 'reference-tracks-native.test.mjs')], { + stdio: 'inherit', + cwd: rootDir, + }) + + child.on('exit', (code) => { + resolve(code ?? 1) + }) + + child.on('error', () => { + resolve(1) + }) + }) +} finally { + await rm(tempDir, { recursive: true, force: true }) +} + +process.exit(exitCode) diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs index 1c77f16..6c3cf2b 100644 --- a/scripts/run-tests.mjs +++ b/scripts/run-tests.mjs @@ -1,13 +1,18 @@ import { spawnSync } from 'node:child_process' const testScripts = [ + 'test:linux-launcher', 'test:audio-router', 'test:audio-clips', 'test:audio-store', 'test:astra', 'test:capture-support', + 'test:capture-channel-selection-native', + 'test:daw-bridge', 'test:spotify-provider', + 'test:tidal-provider', 'test:now-playing', + 'test:now-playing-ui', 'test:profiles', 'test:themes', 'test:secret-vault', @@ -16,16 +21,26 @@ const testScripts = [ 'test:renderer-helpers', 'test:build-metadata', 'test:updates', + 'test:plugin-packaging', + 'test:plugin-build', + 'test:plugin-install', + 'test:waterfall-native', 'test:spectrum-native', + 'test:reference-tracks', 'test:spectrogram-native', 'test:vectorscope-native', 'test:lufsmeter-native', ] -const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm' +const npmCli = process.env.npm_execpath +if (!npmCli) { + console.error('[test] Could not locate the npm CLI. Run this command through npm test.') + process.exit(1) +} for (const testScript of testScripts) { - const result = spawnSync(npmCommand, ['run', testScript], { + // Use npm's JS entry point so Windows does not have to spawn a .cmd shim. + const result = spawnSync(process.execPath, [npmCli, 'run', testScript], { cwd: process.cwd(), stdio: 'inherit', }) diff --git a/scripts/run-tidal-provider-tests.mjs b/scripts/run-tidal-provider-tests.mjs new file mode 100644 index 0000000..d50be0e --- /dev/null +++ b/scripts/run-tidal-provider-tests.mjs @@ -0,0 +1,44 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawn } from 'node:child_process' +import { build } from 'esbuild' + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))) +const tempDir = await mkdtemp(join(tmpdir(), 'prism-tidal-provider-tests-')) +const bundledTestPath = join(tempDir, 'tidal-provider.test.mjs') +const entryPoint = join(rootDir, 'test', 'tidal-provider.test.ts') + +let exitCode = 1 + +try { + await build({ + entryPoints: [entryPoint], + outfile: bundledTestPath, + bundle: true, + platform: 'node', + format: 'esm', + target: 'node23', + sourcemap: 'inline', + }) + + exitCode = await new Promise((resolve) => { + const child = spawn(process.execPath, ['--test', bundledTestPath], { + stdio: 'inherit', + cwd: rootDir, + }) + + child.on('exit', (code) => { + resolve(code ?? 1) + }) + + child.on('error', () => { + resolve(1) + }) + }) +} finally { + await rm(tempDir, { recursive: true, force: true }) +} + +process.exit(exitCode) diff --git a/scripts/test-windows-installer.ps1 b/scripts/test-windows-installer.ps1 index d099278..54e2343 100644 --- a/scripts/test-windows-installer.ps1 +++ b/scripts/test-windows-installer.ps1 @@ -48,6 +48,35 @@ $machinePathBefore = [Environment]::GetEnvironmentVariable('Path', 'Machine') $uninstalled = $false $legacyDirectoryCreated = $false +$pluginNames = @('Spectrum', 'Oscilloscope', 'VU Meter', 'Loudness Meter', 'Vectorscope', 'Spectrogram', 'Waveform', 'Waterfall', 'Bridge') +$commonFiles = if ($env:CommonProgramW6432) { $env:CommonProgramW6432 } else { $env:CommonProgramFiles } +$clapRoot = Join-Path $commonFiles 'CLAP' +$vstRoot = Join-Path $commonFiles 'VST3' +$unrelatedClap = Join-Path $clapRoot "Prism Unrelated-$testId.clap" + +function Assert-PluginFormats { + param([bool]$Vst3, [bool]$Clap) + foreach ($name in $pluginNames) { + $vstPath = Join-Path $vstRoot "Prism $name.vst3" + $clapPath = Join-Path $clapRoot "Prism $name.clap" + if ((Test-Path -LiteralPath $vstPath -PathType Container) -ne $Vst3) { + throw "Unexpected VST3 install state for $vstPath (expected $Vst3)." + } + if ((Test-Path -LiteralPath $clapPath -PathType Leaf) -ne $Clap) { + throw "Unexpected CLAP install state for $clapPath (expected $Clap)." + } + if ($Clap) { + $source = Join-Path $installRoot "resources\plugins\CLAP\Prism $name.clap" + if ((Get-FileHash -LiteralPath $source).Hash -ne (Get-FileHash -LiteralPath $clapPath).Hash) { + throw "Installed CLAP differs from bundled file: $clapPath" + } + } + } + if (-not (Test-Path -LiteralPath $unrelatedClap)) { + throw 'The installer removed an unrelated CLAP plugin.' + } +} + function Get-PathEntries { param([AllowNull()][string]$Value) @@ -156,6 +185,15 @@ try { } } + foreach ($name in $pluginNames) { + if ((Test-Path -LiteralPath (Join-Path $vstRoot "Prism $name.vst3")) -or + (Test-Path -LiteralPath (Join-Path $clapRoot "Prism $name.clap"))) { + throw "Refusing to overwrite a preexisting Prism plugin during installer smoke testing: $name" + } + } + New-Item -ItemType Directory -Force -Path $clapRoot | Out-Null + Set-Content -LiteralPath $unrelatedClap -Value 'Unrelated plugin fixture' + $seededEntries = @($originalEntries) + $legacyEntry [Environment]::SetEnvironmentVariable('Path', ($seededEntries -join ';'), 'Machine') @@ -170,6 +208,8 @@ try { throw "The installer did not create the expected uninstaller at $uninstallerPath." } + Assert-PluginFormats -Vst3 $true -Clap $true + $installedEntries = @(Get-PathEntries ([Environment]::GetEnvironmentVariable('Path', 'Machine'))) $expectedInstalledEntries = @($originalEntries) + $tuiDirectory Assert-PathEntriesEqual -Expected $expectedInstalledEntries -Actual $installedEntries -Context 'Installed machine PATH' @@ -204,6 +244,8 @@ try { [Environment]::SetEnvironmentVariable('Path', ((@($installedEntries) + $legacyEntry) -join ';'), 'Machine') Invoke-CheckedProcess -FilePath $InstallerPath -Arguments "/S /D=$installRoot" | Out-Null + Assert-PluginFormats -Vst3 $true -Clap $true + $reinstalledEntries = @(Get-PathEntries ([Environment]::GetEnvironmentVariable('Path', 'Machine'))) $expectedReinstalledEntries = @($originalEntries) + $legacyEntry + $tuiDirectory Assert-PathEntriesEqual -Expected $expectedReinstalledEntries -Actual $reinstalledEntries -Context 'Reinstalled machine PATH' @@ -213,6 +255,7 @@ try { # NSIS requires this to be the final argument and forbids quoting its value. Invoke-CheckedProcess -FilePath $uninstallerPath -Arguments "/S _?=$installRoot" | Out-Null $uninstalled = $true + Assert-PluginFormats -Vst3 $false -Clap $false $uninstalledEntries = @(Get-PathEntries ([Environment]::GetEnvironmentVariable('Path', 'Machine'))) $expectedUninstalledEntries = @($originalEntries) + $legacyEntry @@ -221,7 +264,15 @@ try { throw "The uninstaller left the Prism TUI PATH entry in place: $tuiDirectory" } - Write-Host "Windows installer PATH smoke test passed for $InstallerPath" + foreach ($selection in @(@(0, 0), @(1, 0), @(0, 1), @(1, 1))) { + $uninstalled = $false + Invoke-CheckedProcess -FilePath $InstallerPath -Arguments "/S /VST3=$($selection[0]) /CLAP=$($selection[1]) /D=$installRoot" | Out-Null + Assert-PluginFormats -Vst3 ([bool]$selection[0]) -Clap ([bool]$selection[1]) + Invoke-CheckedProcess -FilePath $uninstallerPath -Arguments "/S _?=$installRoot" | Out-Null + $uninstalled = $true + Assert-PluginFormats -Vst3 $false -Clap $false + } + Write-Host "Windows installer plugin formats and PATH smoke tests passed for $InstallerPath" } finally { if (-not $uninstalled -and (Test-Path -LiteralPath $uninstallerPath -PathType Leaf)) { try { @@ -231,6 +282,7 @@ try { } } + Remove-Item -LiteralPath $unrelatedClap -Force -ErrorAction SilentlyContinue [Environment]::SetEnvironmentVariable('Path', $machinePathBefore, 'Machine') if (Test-Path -LiteralPath $installRoot) { diff --git a/scripts/validate-clap.mjs b/scripts/validate-clap.mjs new file mode 100644 index 0000000..45ec0c0 --- /dev/null +++ b/scripts/validate-clap.mjs @@ -0,0 +1,28 @@ +import { mkdirSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' +import { pluginProducts } from './build/stage-plugins.mjs' + +const validator = process.env.CLAP_VALIDATOR_PATH || 'clap-validator' +const buildDir = resolve('plugin/build') +const logDir = join(buildDir, 'clap-validation') +mkdirSync(logDir, { recursive: true }) +let failed = false +for (const [target, product] of pluginProducts) { + const plugin = join(buildDir, `${target}_artefacts/Release/CLAP`, `${product}.clap`) + // Validator b2f1d9b divides by zero in param-conversions for plugins with + // zero parameters. All nine Prism products have no automatable parameters; + // skip only that inapplicable test, retaining every processing/state check. + const result = spawnSync(validator, ['validate', plugin, '--only-failed', '--exclude', '^param-conversions$'], { + encoding: 'utf8', timeout: 180_000, maxBuffer: 16 * 1024 * 1024, + }) + const output = `${result.stdout || ''}${result.stderr || ''}${result.error ? `${result.error}\n` : ''}` + writeFileSync(join(logDir, `${target}.log`), output) + if (result.status !== 0) { + failed = true + console.error(`${product}: CLAP validation failed\n${output}`) + } else { + console.log(`${product}: CLAP validation passed\n${output}`) + } +} +process.exitCode = failed ? 1 : 0 diff --git a/src/main/audioClipLibrary.ts b/src/main/audioClipLibrary.ts index a7f694a..7cb694e 100644 --- a/src/main/audioClipLibrary.ts +++ b/src/main/audioClipLibrary.ts @@ -1,9 +1,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' -import type { AudioClipDragPayload } from '../types/audioClip' +import { isAudioClipFormat, type AudioClipDragPayload } from '../types/audioClip' -const WAV_HEADER_BYTES = 44 -const PCM_BITS_PER_SAMPLE = 16 const MAX_SAMPLE_RATE = 384000 const MAX_CLIP_SECONDS = 60 @@ -13,6 +11,9 @@ export function validateAudioClipDragPayload(raw: unknown): AudioClipDragPayload } const candidate = raw as Partial + if (!isAudioClipFormat(candidate.format)) { + throw new Error('The audio clip format is invalid.') + } if (!(candidate.pcmBytes instanceof Uint8Array)) { throw new Error('The audio clip is missing PCM sample data.') } @@ -34,12 +35,14 @@ export function validateAudioClipDragPayload(raw: unknown): AudioClipDragPayload throw new Error('The audio clip duration is invalid.') } - const expectedBytes = candidate.frameCount! * candidate.channelCount * (PCM_BITS_PER_SAMPLE / 8) + const bytesPerSample = candidate.format === 'float32' ? 4 : 2 + const expectedBytes = candidate.frameCount! * candidate.channelCount * bytesPerSample if (candidate.pcmBytes.byteLength !== expectedBytes) { throw new Error('The audio clip PCM data length is invalid.') } return { + format: candidate.format, pcmBytes: candidate.pcmBytes, sampleRate: candidate.sampleRate!, channelCount: candidate.channelCount, @@ -47,26 +50,35 @@ export function validateAudioClipDragPayload(raw: unknown): AudioClipDragPayload } } -export function encodePcm16Wav(payload: AudioClipDragPayload): Buffer { +export function encodeAudioClipWav(payload: AudioClipDragPayload): Buffer { const validated = validateAudioClipDragPayload(payload) + const isFloat = validated.format === 'float32' + const bitsPerSample = isFloat ? 32 : 16 + const headerBytes = isFloat ? 58 : 44 const dataBytes = validated.pcmBytes.byteLength - const blockAlign = validated.channelCount * (PCM_BITS_PER_SAMPLE / 8) - const wav = Buffer.allocUnsafe(WAV_HEADER_BYTES + dataBytes) + const blockAlign = validated.channelCount * (bitsPerSample / 8) + const wav = Buffer.allocUnsafe(headerBytes + dataBytes) wav.write('RIFF', 0, 'ascii') - wav.writeUInt32LE(36 + dataBytes, 4) + wav.writeUInt32LE(headerBytes - 8 + dataBytes, 4) wav.write('WAVE', 8, 'ascii') wav.write('fmt ', 12, 'ascii') - wav.writeUInt32LE(16, 16) - wav.writeUInt16LE(1, 20) + wav.writeUInt32LE(isFloat ? 18 : 16, 16) + wav.writeUInt16LE(isFloat ? 3 : 1, 20) wav.writeUInt16LE(validated.channelCount, 22) wav.writeUInt32LE(validated.sampleRate, 24) wav.writeUInt32LE(validated.sampleRate * blockAlign, 28) wav.writeUInt16LE(blockAlign, 32) - wav.writeUInt16LE(PCM_BITS_PER_SAMPLE, 34) - wav.write('data', 36, 'ascii') - wav.writeUInt32LE(dataBytes, 40) - wav.set(validated.pcmBytes, WAV_HEADER_BYTES) + wav.writeUInt16LE(bitsPerSample, 34) + if (isFloat) { + wav.writeUInt16LE(0, 36) // No format extension. + wav.write('fact', 38, 'ascii') + wav.writeUInt32LE(4, 42) + wav.writeUInt32LE(validated.frameCount, 46) + } + wav.write('data', headerBytes - 8, 'ascii') + wav.writeUInt32LE(dataBytes, headerBytes - 4) + wav.set(validated.pcmBytes, headerBytes) return wav } @@ -96,7 +108,7 @@ export class AudioClipLibrary { writeClip(raw: unknown): string { const payload = validateAudioClipDragPayload(raw) - const wav = encodePcm16Wav(payload) + const wav = encodeAudioClipWav(payload) this.ensureDirectory() const baseName = buildAudioClipBaseName(this.now()) diff --git a/src/main/dockingGeometryGuard.ts b/src/main/dockingGeometryGuard.ts new file mode 100644 index 0000000..933d52e --- /dev/null +++ b/src/main/dockingGeometryGuard.ts @@ -0,0 +1,23 @@ +/** Windows can fit ordinary windows to the new work area without user input. */ +export class DockingGeometryGuard { + private settlingUntil = 0 + private userChanges = new Map() + constructor(private now: () => number = Date.now) {} + + systemChange(): void { + this.settlingUntil = this.now() + 600 + this.userChanges.clear() + } + + userChange(windowId: number): void { + this.userChanges.set(windowId, this.now() + 600) + } + + forget(windowId: number): void { this.userChanges.delete(windowId) } + + shouldPersist(windowId: number, dockingEnabled: boolean): boolean { + const now = this.now() + return (!dockingEnabled && now >= this.settlingUntil) + || now < (this.userChanges.get(windowId) ?? 0) + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 8308df4..105db5b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,5 @@ -import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, nativeTheme, safeStorage, screen, session, shell, Tray } from 'electron' +import { registerReferenceTracks, referenceTrackJobs } from './referenceTracks' +import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, nativeTheme, safeStorage, screen, session, shell, systemPreferences, Tray } from 'electron' import type { BrowserWindowConstructorOptions, MenuItemConstructorOptions, OpenDialogOptions, WebContents } from 'electron' import { execFileSync } from 'child_process' import { existsSync, readFileSync } from 'fs' @@ -16,15 +17,21 @@ import type { import type { ProfileMenuRequest } from '../types/profileMenu' import type { LegacyProfileMigrationPayload, Profile } from '../types/profile' import { SCOPE_KINDS, SCOPE_LABELS, type ScopeKind } from '../types/scope' +import { + isLinkedAnalysisCompatibleScopeKind, + normalizeLinkedAnalysisMessage, + type LinkedAnalysisMessage, + type LinkedAnalysisProbe, +} from '../types/analysis' import type { LegacyThemeMigrationPayload, ThemeLibrarySnapshot, } from '../types/theme' import { RESIZE_DIRECTIONS, type ResizeDirection } from '../types/windowResize' -import type { DialogOptions, DialogResult } from '../types/dialog' +import type { DialogConfig, DialogLayout, DialogOptions, DialogResult } from '../types/dialog' import { normalizeProfile } from '../shared/profileState' import { getScopePopoutMinWidth } from '../shared/scopeSizing' -import { resolveNativeThemeSource } from '../shared/themeState' +import { createDefaultTheme, resolveNativeThemeSource, resolveTheme } from '../shared/themeState' import { resolveMacWindowBlurMaterial, resolveWindowCapabilities, @@ -40,14 +47,20 @@ import { FileBackedProfileLibrary } from './profileLibrary' import { AudioClipLibrary } from './audioClipLibrary' import { loadNativeWindowsMediaApi } from './nativeWindowsMedia' import { loadNativeWindowChromeApi } from './nativeWindowChrome' +import { WindowDockingService } from './windowDocking' +import { DockingGeometryGuard } from './dockingGeometryGuard' +import { normalizeWindowDocking } from '../shared/windowDocking' import { NowPlayingManager } from './services/nowPlayingManager' +import { bindNowPlayingWindowConsumer } from './services/nowPlayingWindowConsumer' import { AstraIntegrationService } from './services/astraIntegration' +import { TidalProvider } from './services/tidalProvider' import { MacSpotifyProvider } from './services/macSpotifyProvider' import { SecretVault } from './services/secretVault' import { checkForUpdates, resolveSafeReleaseUrl } from './services/updates' import { FileBackedThemeLibrary } from './themeLibrary' import { normalizeWindowBackgroundState } from '../shared/windowState' import { FileBackedWindowStateStore } from './windowStateStore' +import { showReadyScopePopout } from './scopePopoutReady' import type { WindowBackgroundSnapshot, WindowBackgroundState } from '../types/windowState' import type { NativeWindowsMediaAPI } from '../types/nativeWindowsMedia' import type { NativeWindowChromeAPI } from '../types/nativeWindowChrome' @@ -76,8 +89,12 @@ import { } from './services/trayMenu' import { TrayRendererCommandQueue } from './services/trayRendererCommandQueue' import { resolveTrayAssetPath } from './services/trayAssets' +import { DawBridgeService } from './services/dawBridgeService' +import type { DawBridgeAudioBatch, DawBridgeSnapshot } from '../types/dawBridge' let mainWindow: BrowserWindow | null = null +let windowDocking: WindowDockingService | null = null +const dockingGeometryGuard = new DockingGeometryGuard() let moveInterval: ReturnType | null = null let moveStartCursor: { x: number; y: number } | null = null let moveStartBounds: WindowBounds | null = null @@ -95,18 +112,22 @@ let suppressMainWindowSyncUntil = 0 let mainWindowLogicalBounds: WindowBounds | null = null let windowRecreationPending = false let isAppQuitting = false +let dockingQuitFlushed = false let appHiddenToTray = false let appTray: Tray | null = null let latestTrayMenuStateKey: string | null = null let trayRendererReady = false let latestTrayRendererState: TrayRendererState = { ...DEFAULT_TRAY_RENDERER_STATE } const pendingTrayRendererCommands = new TrayRendererCommandQueue() -const customDialogWindows = new Set() +const customDialogWindows = new Map() +let activeDialogTheme = resolveTheme(createDefaultTheme()).interface const scopePopoutWindows = new Map() const scopePopoutCloseAllowed = new Set() const popoutBoundsTimers = new Map>() const suppressNextPopoutBoundsEvents = new Set() +const linkedAnalysisSources = new Map() +const linkedAnalysisCleanupRegistered = new Set() let nowPlayingConfigWindow: BrowserWindow | null = null let nowPlayingConfigBoundsTimer: ReturnType | null = null const windowSettingsHeights = new Map() @@ -122,6 +143,7 @@ let nativeWindowsMediaApi: NativeWindowsMediaAPI | null | undefined let nativeWindowChromeApi: NativeWindowChromeAPI | null | undefined let loginItemService: LoginItemService | null = null let audioClipLibrary: AudioClipLibrary | null = null +let dawBridgeService: DawBridgeService | null = null let desktopIntegrationPreferences: DesktopIntegrationPreferences = { ...DEFAULT_DESKTOP_INTEGRATION_PREFERENCES, } @@ -525,7 +547,8 @@ function showPrismWindows(): void { nowPlayingConfigWindow.show() } - const visibleDialogs = Array.from(customDialogWindows).filter((window) => !window.isDestroyed()) + const visibleDialogs = Array.from(customDialogWindows.keys()) + .filter((window) => !window.isDestroyed() && customDialogWindows.get(window)?.ready) for (const window of visibleDialogs) { window.show() } @@ -602,6 +625,11 @@ function flushPendingTrayRendererCommands(): void { function repositionWindowToEdge(targetWindow: BrowserWindow, position: 'top' | 'bottom'): void { if (!supportsProgrammaticReposition() || targetWindow.isDestroyed()) return + if (position !== 'top' && position !== 'bottom') return + if (isMainRendererWindow(targetWindow)) { + windowDocking?.controller.setEdge(position) + if (windowDocking?.controller.ownsGeometry) return + } const display = screen.getDisplayMatching(targetWindow.getBounds()) const workArea = display.workArea @@ -678,6 +706,19 @@ function createNativeTrayMenu(model: ReturnType): Ele deviceId: source.id || null, }), })), + { type: 'separator' }, + { label: 'DAW Bridges', enabled: false }, + ...model.rendererState.dawSources.map((source): MenuItemConstructorOptions => ({ + label: source.label, + type: 'radio', + checked: model.rendererState.captureMode === 'daw' + && source.id === model.rendererState.selectedDawSourceId, + enabled: model.rendererReady, + click: () => sendTrayRendererCommand({ + type: 'select-daw-source', + sourceId: source.id, + }), + })), ] if (model.rendererState.systemSources.length === 0) { audioSourceItems.splice(1, 0, { label: 'No outputs available', enabled: false }) @@ -685,6 +726,9 @@ function createNativeTrayMenu(model: ReturnType): Ele if (model.rendererState.inputSources.length === 0) { audioSourceItems.push({ label: 'No inputs available', enabled: false }) } + if (model.rendererState.dawSources.length === 0) { + audioSourceItems.push({ label: 'No DAW bridges connected', enabled: false }) + } const loginStatus = loginItemStatusLabel(model.desktopIntegration) const rollingCaptureItems: MenuItemConstructorOptions[] = [ @@ -714,6 +758,7 @@ function createNativeTrayMenu(model: ReturnType): Ele label: 'Always on Top', type: 'checkbox', checked: model.alwaysOnTop, + enabled: !windowDocking?.controller.ownsGeometry, click: () => { if (mainWindow && !mainWindow.isDestroyed()) { setWindowAlwaysOnTop(mainWindow, !mainWindow.isAlwaysOnTop()) @@ -737,6 +782,19 @@ function createNativeTrayMenu(model: ReturnType): Ele if (mainWindow && !mainWindow.isDestroyed()) repositionWindowToEdge(mainWindow, 'bottom') }, }, + ...(windowDocking?.controller.snapshot.supported ? [ + { type: 'separator' as const }, + { + label: 'Reserve screen space', type: 'checkbox' as const, + checked: windowDocking.controller.ownsGeometry, + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + flushMainWindowBoundsChanged(mainWindow) + windowDocking?.controller.setEnabled(!windowDocking.controller.ownsGeometry) + } + }, + }, + ] : []), ], }, { type: 'separator' }, @@ -823,7 +881,7 @@ function refreshTrayMenu(): void { alwaysOnTop: Boolean(mainWindow && !mainWindow.isDestroyed() && mainWindow.isAlwaysOnTop()), supportsReposition: supportsProgrammaticReposition(), }) - const stateKey = createTrayMenuStateKey(model) + const stateKey = createTrayMenuStateKey(model) + JSON.stringify(windowDocking?.controller.snapshot) appTray!.setToolTip(model.tooltip) if (stateKey !== latestTrayMenuStateKey) { appTray!.setContextMenu(createNativeTrayMenu(model)) @@ -950,6 +1008,9 @@ function getNowPlayingManager(): NowPlayingManager { new MacSpotifyProvider({ windowsMediaApi: getNativeWindowsMediaApi(), }), + new TidalProvider({ + windowsMediaApi: getNativeWindowsMediaApi(), + }), ], }) nowPlayingManager.subscribe((state) => { @@ -1021,6 +1082,12 @@ function applyNativeThemeSnapshot(snapshot: ThemeLibrarySnapshot): void { : null nativeTheme.themeSource = resolveNativeThemeSource(activeTheme) refreshMacBlurVibrancy() + activeDialogTheme = resolveTheme(activeTheme ?? createDefaultTheme()).interface + for (const window of customDialogWindows.keys()) { + if (!window.isDestroyed() && !window.webContents.isDestroyed()) { + window.webContents.send('dialog:theme-changed', activeDialogTheme) + } + } } async function syncNativeThemeAppearance(): Promise { @@ -1035,6 +1102,7 @@ function clearPendingMainWindowBoundsSave(): void { } function sendMainWindowBoundsChanged(window: BrowserWindow): void { + if (windowDocking?.controller.ownsGeometry) return if (!isMainRendererWindow(window) || !mainRendererReady || !supportsGeometryPersistence()) return if (window.isDestroyed() || window.webContents.isDestroyed()) return @@ -1088,6 +1156,36 @@ function getScopeKindForWindow(window: BrowserWindow | null): ScopeKind | null { return null } +function broadcastLinkedAnalysisMessage(message: LinkedAnalysisMessage): void { + const targets = new Set() + if (mainWindow) targets.add(mainWindow) + for (const popoutWindow of scopePopoutWindows.values()) targets.add(popoutWindow) + + for (const target of targets) { + if (!target.isDestroyed() && !target.webContents.isDestroyed()) { + target.webContents.send('linked-analysis:update', message) + } + } +} + +function registerLinkedAnalysisSourceCleanup(sender: WebContents): void { + if (linkedAnalysisCleanupRegistered.has(sender.id)) return + const senderId = sender.id + linkedAnalysisCleanupRegistered.add(senderId) + sender.once('destroyed', () => { + linkedAnalysisCleanupRegistered.delete(senderId) + const activeProbe = linkedAnalysisSources.get(senderId) + linkedAnalysisSources.delete(senderId) + if (activeProbe) { + broadcastLinkedAnalysisMessage({ + active: false, + interactionId: activeProbe.interactionId, + sourceKind: activeProbe.sourceKind, + }) + } + }) +} + function describeWindow(window: BrowserWindow): string { if (isMainRendererWindow(window)) { return 'main window' @@ -1111,6 +1209,7 @@ function normalizeMainWindowBounds(bounds: WindowBounds): WindowBounds { } function getDisplayWorkAreas(): WindowBounds[] { + if (windowDocking) return windowDocking.workAreas() return screen.getAllDisplays().map((display) => ({ x: display.workArea.x, y: display.workArea.y, @@ -1163,6 +1262,7 @@ function toLogicalBounds(window: BrowserWindow, bounds = window.getBounds()): Wi } function syncMainWindowLogicalBounds(window: BrowserWindow, bounds = window.getBounds()): void { + if (windowDocking?.controller.ownsGeometry) return if (!isMainRendererWindow(window)) { return } @@ -1183,6 +1283,7 @@ function syncMainWindowLogicalBounds(window: BrowserWindow, bounds = window.getB } function applyMainWindowLogicalBounds(window: BrowserWindow, bounds: WindowBounds): void { + if (windowDocking?.controller.ownsGeometry) return const logicalBounds = clampRestoredWindowBounds( normalizeMainWindowBounds(bounds), getDisplayWorkAreas(), @@ -1233,6 +1334,7 @@ function setWindowHeight(window: BrowserWindow, bounds: WindowBounds, height: nu } function applySettingsHeight(window: BrowserWindow, rawNextHeight: number): void { + if (isMainRendererWindow(window) && windowDocking?.controller.ownsGeometry) return const currentSettingsHeight = getSettingsHeight(window) const nextSettingsHeight = Math.max(0, Math.round(rawNextHeight)) const baseMinHeight = getBaseMinHeight(window) @@ -1465,7 +1567,7 @@ function recreateWindowsForBackgroundChange(): void { mainWindow.once('closed', () => { try { createMainWindow(restoreBounds) - if (wasMaximized) { + if (wasMaximized && !windowDocking?.controller.ownsGeometry) { mainWindow?.maximize() } } finally { @@ -1608,6 +1710,7 @@ async function persistAlwaysOnTopPreference(window: BrowserWindow, next: boolean } function setWindowAlwaysOnTop(window: BrowserWindow, next: boolean): void { + if (isMainRendererWindow(window) && windowDocking?.controller.ownsGeometry) return if (window.isDestroyed()) { return } @@ -1634,10 +1737,9 @@ function loadRendererTarget(window: BrowserWindow, query: Record async function showCustomDialog(options: DialogOptions): Promise { return new Promise((resolve) => { - const height = options.type === 'prompt' ? 200 : 160 const win = new BrowserWindow({ width: 380, - height, + height: 200, frame: false, transparent: true, backgroundColor: '#00000000', @@ -1654,10 +1756,9 @@ async function showCustomDialog(options: DialogOptions): Promise { nodeIntegration: false, }, }) - customDialogWindows.add(win) + customDialogWindows.set(win, { options, ready: false }) win.center() - loadRendererTarget(win, { mode: 'dialog' }) const onResult = (_event: Electron.IpcMainEvent, result: DialogResult) => { if (_event.sender !== win.webContents) return @@ -1667,16 +1768,13 @@ async function showCustomDialog(options: DialogOptions): Promise { ipcMain.on('dialog:result', onResult) - win.webContents.once('did-finish-load', () => { - win.webContents.send('dialog:config', options) - if (!appHiddenToTray) win.show() - }) - win.once('closed', () => { customDialogWindows.delete(win) ipcMain.removeListener('dialog:result', onResult) resolve({ buttonIndex: options.cancelId ?? options.buttons.length - 1 }) }) + + loadRendererTarget(win, { mode: 'dialog' }) }) } @@ -1789,10 +1887,12 @@ function createMainWindow(restoreBounds?: WindowBounds): void { refreshTrayMenu() }) + windowDocking?.attach(mainWindow) loadRendererTarget(mainWindow, { window: 'main', ...getWindowBackgroundQuery(background) }) } function sendScopePopoutBoundsChanged(kind: ScopeKind, window: BrowserWindow): void { + if (!dockingGeometryGuard.shouldPersist(window.id, Boolean(windowDocking?.controller.ownsGeometry))) return if ( !mainWindow || mainWindow.isDestroyed() @@ -1844,6 +1944,7 @@ function flushScopePopoutBoundsChanged(kind: ScopeKind, window: BrowserWindow): } function flushRepositionedWindowBounds(window: BrowserWindow): void { + dockingGeometryGuard.userChange(window.id) if (isMainRendererWindow(window)) { flushMainWindowBoundsChanged(window) return @@ -1944,13 +2045,6 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro setSettingsHeightForWindow(popoutWindow, 0) scopePopoutWindows.set(kind, popoutWindow) - popoutWindow.once('ready-to-show', () => { - if (!popoutWindow.isDestroyed() && !appHiddenToTray) { - popoutWindow.show() - raiseMainWindowAboveNormalPopouts() - } - }) - popoutWindow.on('close', (event) => { if (scopePopoutCloseAllowed.has(kind) || !mainWindow || mainWindow.isDestroyed()) { return @@ -1961,6 +2055,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro }) popoutWindow.on('closed', () => { + dockingGeometryGuard.forget(popoutWindow.id) if (resizeWindow === popoutWindow) { stopWindowResizeController() } @@ -1976,6 +2071,8 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro } }) + popoutWindow.on('will-move', () => dockingGeometryGuard.userChange(popoutWindow.id)) + popoutWindow.on('will-resize', () => dockingGeometryGuard.userChange(popoutWindow.id)) popoutWindow.on('move', () => emitPopoutBoundsChanged(kind, popoutWindow)) popoutWindow.on('resize', () => emitPopoutBoundsChanged(kind, popoutWindow)) @@ -2113,6 +2210,10 @@ function createNowPlayingConfigWindow(): BrowserWindow { const configWindow = nowPlayingConfigWindow + bindNowPlayingWindowConsumer(configWindow.webContents, getNowPlayingManager(), (error) => { + console.warn('Could not update now-playing config window activity:', error) + }) + configWindow.once('ready-to-show', () => { if (!configWindow.isDestroyed() && !appHiddenToTray) { configWindow.show() @@ -2163,6 +2264,21 @@ function setupPermissions(): void { } function setupIPC(): void { + ipcMain.handle('window:docking-get', () => windowDocking?.controller.snapshot + ?? { ...normalizeWindowDocking(undefined), supported: false, active: false, error: null }) + ipcMain.handle('window:docking-set', (event, enabled: unknown) => { + if (!isMainRendererWindow(getWindowFromSender(event.sender)) || typeof enabled !== 'boolean') return null + if (mainWindow) flushMainWindowBoundsChanged(mainWindow) + windowDocking?.controller.setEnabled(enabled) + return windowDocking?.controller.snapshot ?? null + }) + ipcMain.handle('window:docked-settings-show', (event, height: unknown) => { + if (!isMainRendererWindow(getWindowFromSender(event.sender)) || typeof height !== 'number') return false + return windowDocking?.showSettings(height) ?? false + }) + ipcMain.on('window:docked-settings-close', event => { + if (isMainRendererWindow(getWindowFromSender(event.sender))) windowDocking?.closeSettings() + }) ipcMain.on('window:minimize', (event) => { getWindowFromSender(event.sender)?.minimize() }) @@ -2186,6 +2302,12 @@ function setupIPC(): void { const current = screen.getCursorScreenPoint() const dx = current.x - moveStartCursor.x const dy = current.y - moveStartCursor.y + dockingGeometryGuard.userChange(targetWindow.id) + + if (isMainRendererWindow(targetWindow) && windowDocking?.controller.ownsGeometry) { + if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return + moveStartBounds = windowDocking.controller.undockForDrag(moveStartCursor) ?? targetWindow.getBounds() + } if (isMainRendererWindow(targetWindow)) { const nextBounds = clampDraggedMainWindowBounds({ @@ -2212,6 +2334,11 @@ function setupIPC(): void { const targetWindow = getWindowFromSender(event.sender) if (!targetWindow || targetWindow.isDestroyed()) return + if (isMainRendererWindow(targetWindow) && windowDocking?.controller.ownsGeometry) { + const inwardEdge = windowDocking.controller.snapshot.edge === 'top' ? 's' : 'n' + if (rawEdge !== inwardEdge) return + } + stopWindowMoveController() stopWindowResizeController() @@ -2233,7 +2360,10 @@ function setupIPC(): void { } const currentCursor = screen.getCursorScreenPoint() - const [minWidth, minHeight] = resizeWindow.getMinimumSize() + dockingGeometryGuard.userChange(resizeWindow.id) + // A non-resizable Electron window reports its current size as its minimum. + const [minWidth, minHeight] = isMainRendererWindow(resizeWindow) && windowDocking?.controller.ownsGeometry + ? [WINDOW_DEFAULTS.minWidth, WINDOW_DEFAULTS.minHeight] : resizeWindow.getMinimumSize() const nextBounds = calculateResizedWindowBounds({ edge: resizeEdge, startBounds: resizeStartBounds, @@ -2243,7 +2373,11 @@ function setupIPC(): void { minHeight, }) - resizeWindow.setBounds(nextBounds) + if (isMainRendererWindow(resizeWindow) && windowDocking?.controller.ownsGeometry) { + windowDocking.controller.resize(nextBounds.height) + } else { + resizeWindow.setBounds(nextBounds) + } }, 16) }) @@ -2308,6 +2442,32 @@ function setupIPC(): void { return getAppBuildInfo() }) + ipcMain.handle('audio:request-microphone-access', async () => { + if (process.platform !== 'darwin') return true + const status = systemPreferences.getMediaAccessStatus('microphone') + if (status === 'granted') return true + if (status === 'denied' || status === 'restricted') return false + return systemPreferences.askForMediaAccess('microphone') + }) + + ipcMain.handle('daw-bridge:get-snapshot', () => { + return dawBridgeService?.getSnapshot() ?? { + available: false, + reason: 'The DAW bridge listener is not initialized.', + selectedSourceId: null, + sources: [], + } satisfies DawBridgeSnapshot + }) + + ipcMain.handle('daw-bridge:select-source', (_event, sourceId: unknown) => { + return dawBridgeService?.selectSource(typeof sourceId === 'string' ? sourceId : null) ?? { + available: false, + reason: 'The DAW bridge listener is not initialized.', + selectedSourceId: null, + sources: [], + } satisfies DawBridgeSnapshot + }) + ipcMain.on('audio-clips:start-drag', (event, rawPayload: unknown) => { const targetWindow = getWindowFromSender(event.sender) if (!targetWindow || !isMainRendererWindow(targetWindow)) return @@ -2326,6 +2486,8 @@ function setupIPC(): void { } }) + registerReferenceTracks() + ipcMain.handle('audio-clips:reveal-folder', async (event) => { const targetWindow = getWindowFromSender(event.sender) if (!targetWindow || !isMainRendererWindow(targetWindow)) { @@ -2394,6 +2556,7 @@ function setupIPC(): void { }) ipcMain.handle('profiles:load', async (_event, id: string) => { + referenceTrackJobs.cancel() return getProfileLibrary().loadProfile(id) }) @@ -2453,6 +2616,35 @@ function setupIPC(): void { return showCustomDialog(options) }) + ipcMain.handle('dialog:get-config', (event): DialogConfig => { + const win = getWindowFromSender(event.sender) + const state = win && customDialogWindows.get(win) + if (!state) throw new Error('Dialog configuration is only available to dialog windows.') + return { options: state.options, theme: activeDialogTheme } + }) + + ipcMain.on('dialog:layout-ready', (event, layout: DialogLayout) => { + const win = getWindowFromSender(event.sender) + const state = win && customDialogWindows.get(win) + if (!win || !state || !Number.isFinite(layout?.height) || layout.height <= 0) return + + const bounds = win.getBounds() + const { workArea } = screen.getDisplayMatching(bounds) + const maxHeight = Math.max(1, workArea.height - 32) + const height = Math.min(maxHeight, Math.max(120, Math.ceil(layout.height))) + const y = Math.round(Math.max(workArea.y + 16, Math.min( + bounds.y + (bounds.height - height) / 2, + workArea.y + workArea.height - height - 16, + ))) + if (bounds.height !== height || bounds.y !== y) { + win.setBounds({ ...bounds, height, y }) + } + if (!state.ready) { + state.ready = true + if (!appHiddenToTray) win.show() + } + }) + ipcMain.handle('profiles:reveal-folder', async () => { const folderPath = getProfileLibrary().getProfilesDirectory() const openResult = await shell.openPath(folderPath) @@ -2570,6 +2762,7 @@ function setupIPC(): void { const targetWindow = getWindowFromSender(event.sender) if (!targetWindow) return null + if (isMainRendererWindow(targetWindow) && windowDocking?.controller.ownsGeometry) return null return toLogicalBounds(targetWindow) }) @@ -2646,6 +2839,7 @@ function setupIPC(): void { if (!isMainRendererWindow(targetWindow)) return mainRendererReady = true + windowDocking?.ready() void processPendingProfileOpenPaths() }) @@ -2684,8 +2878,11 @@ function setupIPC(): void { ipcMain.on('scope-popout:ready', (event, kind: ScopeKind) => { const targetWindow = getWindowFromSender(event.sender) - if (!targetWindow || isMainRendererWindow(targetWindow) || !isScopeKind(kind)) return + if (!isScopeKind(kind) || !targetWindow || targetWindow !== scopePopoutWindows.get(kind)) return mainWindow?.webContents.send('scope-popout:ready', kind) + if (showReadyScopePopout(targetWindow, appHiddenToTray)) { + raiseMainWindowAboveNormalPopouts() + } }) ipcMain.on('scope-popout:request-pop-in', (event, kind: ScopeKind) => { @@ -2699,6 +2896,32 @@ function setupIPC(): void { if (!targetWindow || isMainRendererWindow(targetWindow) || !isScopeKind(kind)) return mainWindow?.webContents.send('scope-popout:settings-update', kind, partial) }) + + ipcMain.on('linked-analysis:update', (event, rawMessage: unknown) => { + const targetWindow = getWindowFromSender(event.sender) + const message = normalizeLinkedAnalysisMessage(rawMessage) + if (!targetWindow || !message) return + + const popoutKind = getScopeKindForWindow(targetWindow) + if (!isMainRendererWindow(targetWindow)) { + if (!popoutKind || !isLinkedAnalysisCompatibleScopeKind(popoutKind) || message.sourceKind !== popoutKind) { + return + } + } + + const senderId = event.sender.id + registerLinkedAnalysisSourceCleanup(event.sender) + if (message.active) { + linkedAnalysisSources.set(senderId, message) + broadcastLinkedAnalysisMessage(message) + return + } + + const activeProbe = linkedAnalysisSources.get(senderId) + if (!activeProbe || activeProbe.interactionId !== message.interactionId) return + linkedAnalysisSources.delete(senderId) + broadcastLinkedAnalysisMessage(message) + }) } app.commandLine.appendSwitch('enable-features', 'Metal') @@ -2714,7 +2937,35 @@ if (!hasSingleInstanceLock) { setupPermissions() void getNowPlayingManager().initialize() setupIPC() + dawBridgeService = new DawBridgeService({ + onSnapshot: (snapshot: DawBridgeSnapshot) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('daw-bridge:snapshot', snapshot) + } + }, + onAudioBatch: (batch: DawBridgeAudioBatch) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('daw-bridge:audio-batch', batch) + } + }, + }) + await dawBridgeService.start() await getWindowStateStore().initialize() + windowDocking = new WindowDockingService({ + store: getWindowStateStore(), + geometryChanging: () => dockingGeometryGuard.systemChange(), + logicalBounds: toLogicalBounds, + prepare: window => { + setSettingsHeightForWindow(window, 0) + window.setMinimumSize(WINDOW_DEFAULTS.minWidth, WINDOW_DEFAULTS.minHeight) + }, + restore: (window, bounds) => { + setSettingsHeightForWindow(window, 0) + window.setMinimumSize(WINDOW_DEFAULTS.minWidth, WINDOW_DEFAULTS.minHeight) + applyMainWindowLogicalBounds(window, bounds) + }, + changed: () => refreshTrayMenu(), + }) desktopIntegrationPreferences = await loadDesktopIntegrationPreferences( getDesktopIntegrationPreferencesPath(), ) @@ -2771,8 +3022,17 @@ app.on('window-all-closed', () => { app.quit() }) -app.on('before-quit', () => { +app.on('before-quit', (event) => { isAppQuitting = true + if (windowDocking && !dockingQuitFlushed) { + event.preventDefault() + void windowDocking.shutdown().finally(() => { + dockingQuitFlushed = true + app.quit() + }) + } + dawBridgeService?.stop() + dawBridgeService = null destroyAppTray() pendingTrayRendererCommands.clear() }) diff --git a/src/main/nativeWindowDocking.ts b/src/main/nativeWindowDocking.ts new file mode 100644 index 0000000..19ea0a1 --- /dev/null +++ b/src/main/nativeWindowDocking.ts @@ -0,0 +1,19 @@ +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { NativeWindowDockingAPI } from '../types/windowDocking' + +const require = createRequire(import.meta.url) +export function loadNativeWindowDockingApi(): NativeWindowDockingAPI | null { + if (process.platform !== 'win32') return null + for (const path of [ + join(dirname(fileURLToPath(import.meta.url)), '../../native/build/Release/visualizer_dsp.node'), + join(process.resourcesPath, 'native/visualizer_dsp.node'), + ]) { + try { + const api = require(path).windowDocking as NativeWindowDockingAPI | undefined + if (api && ['register', 'remove', 'lower', 'position', 'messages', 'workArea', 'fullscreen'].every(key => typeof api[key as keyof NativeWindowDockingAPI] === 'function')) return api + } catch { /* An older/missing addon simply does not expose docking. */ } + } + return null +} diff --git a/src/main/profileLibrary.ts b/src/main/profileLibrary.ts index e1f976f..a9fad90 100644 --- a/src/main/profileLibrary.ts +++ b/src/main/profileLibrary.ts @@ -355,6 +355,9 @@ export class FileBackedProfileLibrary { candidate.version !== 1 && candidate.version !== 2 && candidate.version !== 3 + && candidate.version !== 4 + && candidate.version !== 5 + && candidate.version !== 6 && candidate.version !== PROFILE_FILE_VERSION ) { throw new Error(`Unsupported profile version in ${basename(filePath)}.`) diff --git a/src/main/referenceTrackJobs.ts b/src/main/referenceTrackJobs.ts new file mode 100644 index 0000000..e3b75ec --- /dev/null +++ b/src/main/referenceTrackJobs.ts @@ -0,0 +1,62 @@ +import { basename } from 'node:path' +import { randomUUID } from 'node:crypto' +import { EMPTY_REFERENCE_IMPORT, REFERENCE_FFT_SIZES, REFERENCE_SAMPLE_RATE, encodeReferencePower, normalizeReferenceAsset, type SpectrumReferenceAsset, type SpectrumReferenceImportState } from '../types/spectrumReference' + +export interface NativeReferenceCurve { + sourceNyquistHz: number + durationSeconds: number + meanSquare: number + curves: Record +} +export interface NativeReferenceAnalysis { + start: (path: string, progress: (fraction: number, curve: NativeReferenceCurve) => void, + complete: (error: unknown, curve: NativeReferenceCurve | null) => void) => number + cancel: (id: number) => void +} + +export class ReferenceTrackJobs { + private state: SpectrumReferenceImportState = { ...EMPTY_REFERENCE_IMPORT } + private nativeJob: number | null = null + generation = 0 + constructor(private readonly native: () => NativeReferenceAnalysis, private readonly publish: (state: SpectrumReferenceImportState) => void) {} + getState(): SpectrumReferenceImportState { return this.state } + private setState(state: SpectrumReferenceImportState): void { this.state = state; this.publish(state) } + cancel(): void { + this.generation++ + if (this.nativeJob !== null) this.native().cancel(this.nativeJob) + this.nativeJob = null + this.setState({ ...EMPTY_REFERENCE_IMPORT }) + } + start(path: string): void { + this.cancel() + const jobId = randomUUID(), name = basename(path).slice(0, 1024) + this.setState({ ...EMPTY_REFERENCE_IMPORT, jobId, name, phase: 'opening' }) + const toAsset = (curve: NativeReferenceCurve): SpectrumReferenceAsset | null => normalizeReferenceAsset({ + version: 1, id: jobId, name, sampleRate: REFERENCE_SAMPLE_RATE, + sourceNyquistHz: curve.sourceNyquistHz, durationSeconds: curve.durationSeconds, + meanSquare: curve.meanSquare, + curves: Object.fromEntries(REFERENCE_FFT_SIZES.map(size => [size, encodeReferencePower(curve.curves[size])])), + }) + try { + this.nativeJob = this.native().start(path, (fraction, curve) => { + if (this.state.jobId !== jobId || this.state.phase === 'ready' || this.state.phase === 'error') return + try { + this.setState({ ...this.state, phase: 'analyzing', progress: fraction >= 0 ? Math.min(1, fraction) : null, preview: toAsset(curve) }) + } catch { /* A malformed preview must not disrupt the import or live metering. */ } + }, (error, curve) => { + if (this.state.jobId !== jobId) return + this.nativeJob = null + try { + if (error) throw error + const asset = curve ? toAsset(curve) : null + if (!asset) throw new Error('The reference analysis result is invalid.') + this.setState({ ...this.state, phase: 'ready', progress: 1, preview: null, result: asset }) + } catch (failure) { + this.setState({ ...this.state, phase: 'error', preview: null, error: failure instanceof Error ? failure.message : String(failure) }) + } + }) + } catch (error) { + this.setState({ ...this.state, phase: 'error', error: error instanceof Error ? error.message : String(error) }) + } + } +} diff --git a/src/main/referenceTracks.ts b/src/main/referenceTracks.ts new file mode 100644 index 0000000..54aecd1 --- /dev/null +++ b/src/main/referenceTracks.ts @@ -0,0 +1,40 @@ +import { app, BrowserWindow, dialog, ipcMain } from 'electron' +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { ReferenceTrackJobs, type NativeReferenceAnalysis } from './referenceTrackJobs' + +let nativeAPI: NativeReferenceAnalysis | null = null +function loadNative(): NativeReferenceAnalysis { + if (nativeAPI) return nativeAPI + const require = createRequire(import.meta.url) + const here = dirname(fileURLToPath(import.meta.url)) + for (const path of [join(here, '../../native/build/Release/visualizer_dsp.node'), join(process.resourcesPath, 'native/visualizer_dsp.node')]) { + try { nativeAPI = require(path).referenceAnalysis as NativeReferenceAnalysis; if (nativeAPI) return nativeAPI } catch { /* try packaged path */ } + } + throw new Error('Reference analysis is unavailable. Rebuild or reinstall Prism’s native audio module.') +} + +export const referenceTrackJobs = new ReferenceTrackJobs(loadNative, state => { + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) window.webContents.send('reference-tracks:state', state) + } +}) + +export function registerReferenceTracks(): void { + ipcMain.handle('reference-tracks:get-state', () => referenceTrackJobs.getState()) + ipcMain.handle('reference-tracks:cancel', () => referenceTrackJobs.cancel()) + ipcMain.handle('reference-tracks:import', (_event, path: unknown) => { + if (typeof path !== 'string' || !path || path.includes('\0')) throw new Error('Choose an audio file on disk.') + referenceTrackJobs.start(path) + }) + ipcMain.handle('reference-tracks:choose', async event => { + const generation = referenceTrackJobs.generation + const parent = BrowserWindow.fromWebContents(event.sender) + const options = { title: 'Load spectrum reference', properties: ['openFile'] as ['openFile'], + filters: [{ name: 'Audio tracks', extensions: ['wav', 'wave', 'aif', 'aiff', 'aifc', 'flac', 'mp3'] }] } + const result = parent ? await dialog.showOpenDialog(parent, options) : await dialog.showOpenDialog(options) + if (!result.canceled && result.filePaths[0] && generation === referenceTrackJobs.generation) referenceTrackJobs.start(result.filePaths[0]) + }) + app.on('before-quit', () => referenceTrackJobs.cancel()) +} diff --git a/src/main/scopePopoutReady.ts b/src/main/scopePopoutReady.ts new file mode 100644 index 0000000..0408ec2 --- /dev/null +++ b/src/main/scopePopoutReady.ts @@ -0,0 +1,12 @@ +interface ReadyPopoutWindow { + isDestroyed(): boolean + isVisible(): boolean + show(): void +} + +/** Renderer readiness does not depend on a hidden Wayland window being painted. */ +export function showReadyScopePopout(window: ReadyPopoutWindow, appHiddenToTray: boolean): boolean { + if (window.isDestroyed() || appHiddenToTray || window.isVisible()) return false + window.show() + return true +} diff --git a/src/main/services/dawBridgeService.ts b/src/main/services/dawBridgeService.ts new file mode 100644 index 0000000..ce0b406 --- /dev/null +++ b/src/main/services/dawBridgeService.ts @@ -0,0 +1,492 @@ +import { createServer, type Server, type Socket } from 'net' +import { + DAW_BRIDGE_AUDIO_HEADER_BYTES, + DAW_BRIDGE_FRAME_HEADER_BYTES, + DAW_BRIDGE_FRAME_MAGIC, + DAW_BRIDGE_HOST, + DAW_BRIDGE_MAX_PAYLOAD_BYTES, + DAW_BRIDGE_MESSAGE, + DAW_BRIDGE_PORT, + DAW_BRIDGE_PROTOCOL_VERSION, + type DawBridgeAudioBatch, + type DawBridgeAudioPacket, + type DawBridgeHelloPayload, + type DawBridgeSnapshot, + type DawBridgeSourceDescriptor, + type DawBridgeSubscribePayload, + type DawTransportSnapshot, +} from '../../types/dawBridge' + +const HEARTBEAT_INTERVAL_MS = 1000 +const HEARTBEAT_TIMEOUT_MS = 3000 +const AUDIO_BATCH_INTERVAL_MS = 10 +const MAX_CONNECTIONS = 64 +const MAX_PENDING_PACKETS = 128 +const SAFE_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/ + +interface ClientState { + socket: Socket + receiveBuffer: Buffer + source: DawBridgeSourceDescriptor | null + lastHeartbeatAt: number +} + +interface DawBridgeServiceOptions { + host?: string + port?: number + onSnapshot?: (snapshot: DawBridgeSnapshot) => void + onAudioBatch?: (batch: DawBridgeAudioBatch) => void + heartbeatIntervalMs?: number + heartbeatTimeoutMs?: number + audioBatchIntervalMs?: number +} + +function finiteNumber(value: unknown, fallback: number): number { + return typeof value === 'number' && Number.isFinite(value) ? value : fallback +} + +function optionalString(value: unknown): string | null { + if (typeof value !== 'string') return null + const trimmed = value.trim() + // JUCE String limits names by Unicode code point, not UTF-16 code unit. + return trimmed ? Array.from(trimmed).slice(0, 160).join('') : null +} + +function isHelloPayload(value: unknown): value is DawBridgeHelloPayload { + if (typeof value !== 'object' || value === null) return false + const candidate = value as Partial + return typeof candidate.sourceId === 'string' + && SAFE_ID_PATTERN.test(candidate.sourceId) + && typeof candidate.instanceId === 'string' + && SAFE_ID_PATTERN.test(candidate.instanceId) +} + +function sourceLabel(payload: DawBridgeHelloPayload): string { + return optionalString(payload.customName) + ?? optionalString(payload.trackName) + ?? `Bridge ${instanceTag(payload.instanceId)}` +} + +// Keep in sync with PrismBridgeProcessor::getInstanceTag(). Tags identify a +// live plug-in, including copies which share the same saved source UUID. +function instanceTag(instanceId: string): string { + return instanceId.slice(0, 8).toUpperCase() +} + +function displaySources(sources: DawBridgeSourceDescriptor[]): DawBridgeSourceDescriptor[] { + return sources.map((source) => { + const matching = sources.filter((other) => ( + other.label === source.label && other.hostName === source.hostName + )) + let label = source.label + if (matching.length > 1) { + let tag = instanceTag(source.instanceId) + // Rare short-tag collisions can be matched against the full instance ID + // in the nameplate tooltip. Do not change either source's selection key. + if (matching.some((other) => other.id !== source.id && instanceTag(other.instanceId) === tag)) { + tag = source.instanceId + if (matching.some((other) => other.id !== source.id && other.instanceId === source.instanceId)) { + tag = source.id + } + } + label = `${label} · ${tag}` + } + return { ...source, label: source.hostName ? `${label} — ${source.hostName}` : label } + }).sort((left, right) => left.label.localeCompare(right.label)) +} + +function isLoopbackAddress(address: string | undefined): boolean { + return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1' +} + +export class DawBridgeService { + private readonly host: string + private readonly port: number + private readonly onSnapshot?: (snapshot: DawBridgeSnapshot) => void + private readonly onAudioBatch?: (batch: DawBridgeAudioBatch) => void + private readonly heartbeatIntervalMs: number + private readonly heartbeatTimeoutMs: number + private readonly audioBatchIntervalMs: number + private server: Server | null = null + private clients = new Set() + private sources = new Map() + private selectedSourceId: string | null = null + private available = false + private unavailableReason: string | null = 'The DAW bridge listener has not started.' + private heartbeatTimer: ReturnType | null = null + private audioBatchTimer: ReturnType | null = null + private pendingAudioPackets: DawBridgeAudioPacket[] = [] + + constructor(options: DawBridgeServiceOptions = {}) { + this.host = options.host ?? DAW_BRIDGE_HOST + this.port = options.port ?? DAW_BRIDGE_PORT + this.onSnapshot = options.onSnapshot + this.onAudioBatch = options.onAudioBatch + this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS + this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? HEARTBEAT_TIMEOUT_MS + this.audioBatchIntervalMs = options.audioBatchIntervalMs ?? AUDIO_BATCH_INTERVAL_MS + } + + async start(): Promise { + if (this.server) return this.getSnapshot() + + const server = createServer((socket) => this.acceptClient(socket)) + this.server = server + server.maxConnections = MAX_CONNECTIONS + + await new Promise((resolve) => { + const handleError = (error: Error): void => { + server.removeListener('listening', handleListening) + this.available = false + this.unavailableReason = `Could not listen on ${this.host}:${this.port}: ${error.message}` + this.server = null + try { + server.close() + } catch { + // The server may never have entered the listening state. + } + resolve() + } + const handleListening = (): void => { + server.removeListener('error', handleError) + this.available = true + this.unavailableReason = null + this.heartbeatTimer = setInterval(() => this.expireSilentClients(), this.heartbeatIntervalMs) + resolve() + } + server.once('error', handleError) + server.once('listening', handleListening) + server.listen(this.port, this.host) + }) + + this.emitSnapshot() + return this.getSnapshot() + } + + stop(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer) + this.heartbeatTimer = null + } + if (this.audioBatchTimer) { + clearTimeout(this.audioBatchTimer) + this.audioBatchTimer = null + } + this.pendingAudioPackets = [] + for (const client of this.clients) { + client.socket.destroy() + } + this.clients.clear() + this.sources.clear() + this.selectedSourceId = null + this.server?.close() + this.server = null + this.available = false + this.unavailableReason = 'The DAW bridge listener is stopped.' + } + + getSnapshot(): DawBridgeSnapshot { + return { + available: this.available, + reason: this.unavailableReason, + selectedSourceId: this.selectedSourceId, + sources: displaySources([...this.sources.values()] + .flatMap((client) => client.source ? [client.source] : [])), + } + } + + getListeningPort(): number | null { + const address = this.server?.address() + return typeof address === 'object' && address ? address.port : null + } + + selectSource(sourceId: string | null): DawBridgeSnapshot { + this.selectedSourceId = typeof sourceId === 'string' && this.sources.has(sourceId) ? sourceId : null + for (const client of this.clients) { + this.sendSubscription(client) + if (client.source) { + client.source = { + ...client.source, + connectionState: client.source.id === this.selectedSourceId ? 'selected' : 'connected', + } + } + } + this.emitSnapshot() + return this.getSnapshot() + } + + private acceptClient(socket: Socket): void { + if (!isLoopbackAddress(socket.remoteAddress) || this.clients.size >= MAX_CONNECTIONS) { + socket.destroy() + return + } + + socket.setNoDelay(true) + socket.setKeepAlive(true, this.heartbeatIntervalMs) + const client: ClientState = { + socket, + receiveBuffer: Buffer.alloc(0), + source: null, + lastHeartbeatAt: Date.now(), + } + this.clients.add(client) + + socket.on('data', (chunk) => this.handleData(client, chunk)) + socket.on('error', () => socket.destroy()) + socket.on('close', () => this.removeClient(client)) + } + + private handleData(client: ClientState, chunk: Buffer): void { + client.lastHeartbeatAt = Date.now() + client.receiveBuffer = client.receiveBuffer.length === 0 + ? chunk + : Buffer.concat([client.receiveBuffer, chunk]) + + while (client.receiveBuffer.length >= DAW_BRIDGE_FRAME_HEADER_BYTES) { + const magic = client.receiveBuffer.readUInt32LE(0) + const version = client.receiveBuffer.readUInt16LE(4) + const messageType = client.receiveBuffer.readUInt16LE(6) + const payloadLength = client.receiveBuffer.readUInt32LE(8) + if ( + magic !== DAW_BRIDGE_FRAME_MAGIC + || version !== DAW_BRIDGE_PROTOCOL_VERSION + || payloadLength > DAW_BRIDGE_MAX_PAYLOAD_BYTES + ) { + client.socket.destroy() + return + } + + const frameLength = DAW_BRIDGE_FRAME_HEADER_BYTES + payloadLength + if (client.receiveBuffer.length < frameLength) return + const payload = client.receiveBuffer.subarray(DAW_BRIDGE_FRAME_HEADER_BYTES, frameLength) + client.receiveBuffer = client.receiveBuffer.subarray(frameLength) + if (!this.handleFrame(client, messageType, payload)) { + client.socket.destroy() + return + } + } + } + + private handleFrame(client: ClientState, messageType: number, payload: Buffer): boolean { + switch (messageType) { + case DAW_BRIDGE_MESSAGE.hello: + case DAW_BRIDGE_MESSAGE.sourceUpdate: + return this.handleHello(client, payload) + case DAW_BRIDGE_MESSAGE.heartbeat: + return true + case DAW_BRIDGE_MESSAGE.audio: + return this.handleAudio(client, payload) + default: + return false + } + } + + private handleHello(client: ClientState, payloadBuffer: Buffer): boolean { + let payload: unknown + try { + payload = JSON.parse(payloadBuffer.toString('utf8')) + } catch { + return false + } + if (!isHelloPayload(payload)) return false + + const previousSource = client.source + if (previousSource) { + this.sources.delete(previousSource.id) + } + + // A metadata update must retain the explicitly chosen live key, even if + // another copy has disconnected since that key was allocated. + let liveId = previousSource?.persistentId === payload.sourceId + && previousSource.instanceId === payload.instanceId + ? previousSource.id + : payload.sourceId + const conflictingClient = [...this.sources.values()].find((candidate) => ( + candidate !== client && candidate.source?.persistentId === payload.sourceId + )) + if (conflictingClient?.source) { + const existingSource = conflictingClient.source + if (existingSource.id === payload.sourceId) { + this.sources.delete(existingSource.id) + const distinctExistingId = this.allocateLiveId( + payload.sourceId, + existingSource.instanceId, + conflictingClient, + ) + conflictingClient.source = { ...existingSource, id: distinctExistingId, connectionState: 'connected' } + this.sources.set(distinctExistingId, conflictingClient) + this.pendingAudioPackets = this.pendingAudioPackets.filter((packet) => packet.sourceId !== payload.sourceId) + } + if (this.selectedSourceId === payload.sourceId) { + this.selectedSourceId = null + } + this.sendSubscription(conflictingClient) + liveId = this.allocateLiveId(payload.sourceId, payload.instanceId, client) + } + + const descriptor: DawBridgeSourceDescriptor = { + id: liveId, + persistentId: payload.sourceId, + instanceId: payload.instanceId, + label: sourceLabel(payload), + customName: optionalString(payload.customName), + trackName: optionalString(payload.trackName), + hostName: optionalString(payload.hostName), + sampleRate: Math.max(1, finiteNumber(payload.sampleRate, 48000)), + channelCount: Math.max(1, Math.min(2, Math.floor(finiteNumber(payload.channelCount, 2)))), + droppedFrames: Math.max(0, Math.floor(finiteNumber(payload.droppedFrames, 0))), + connectionState: liveId === this.selectedSourceId ? 'selected' : 'connected', + } + client.source = descriptor + this.sources.set(descriptor.id, client) + this.sendSubscription(client) + this.emitSnapshot() + return true + } + + private handleAudio(client: ClientState, payload: Buffer): boolean { + const source = client.source + if (!source || source.id !== this.selectedSourceId || payload.length < DAW_BRIDGE_AUDIO_HEADER_BYTES) { + return Boolean(source) + } + + const sequence = payload.readUInt32LE(0) + const frameCount = payload.readUInt16LE(4) + const channelCount = payload.readUInt8(6) + const flags = payload.readUInt8(7) + const sampleRate = payload.readDoubleLE(8) + const expectedBytes = DAW_BRIDGE_AUDIO_HEADER_BYTES + frameCount * channelCount * Float32Array.BYTES_PER_ELEMENT + if ( + frameCount === 0 + || (channelCount !== 1 && channelCount !== 2) + || !Number.isFinite(sampleRate) + || sampleRate <= 0 + || payload.length !== expectedBytes + ) { + return false + } + + const transport: DawTransportSnapshot = { + sequence, + isPlaying: Boolean(flags & (1 << 0)), + isRecording: Boolean(flags & (1 << 1)), + isLooping: Boolean(flags & (1 << 2)), + } + if (flags & (1 << 3)) transport.timeInSamples = Number(payload.readBigInt64LE(16)) + if (flags & (1 << 4)) transport.timeInSeconds = payload.readDoubleLE(24) + if (flags & (1 << 5)) { + transport.ppqPosition = payload.readDoubleLE(32) + const lastBar = payload.readDoubleLE(48) + if (Number.isFinite(lastBar)) transport.ppqPositionOfLastBarStart = lastBar + } + if (flags & (1 << 6)) transport.bpm = payload.readDoubleLE(40) + if (flags & (1 << 7)) { + transport.timeSignature = { + numerator: payload.readInt16LE(72), + denominator: payload.readInt16LE(74), + } + } + const loopStart = payload.readDoubleLE(56) + const loopEnd = payload.readDoubleLE(64) + if (transport.isLooping && Number.isFinite(loopStart) && Number.isFinite(loopEnd)) { + transport.loopPoints = { ppqStart: loopStart, ppqEnd: loopEnd } + } + + const audioOffset = DAW_BRIDGE_AUDIO_HEADER_BYTES + const left = new Float32Array(frameCount) + const right = new Float32Array(frameCount) + for (let index = 0; index < frameCount; index += 1) { + left[index] = payload.readFloatLE(audioOffset + index * 4) + } + if (channelCount === 2) { + const rightOffset = audioOffset + frameCount * 4 + for (let index = 0; index < frameCount; index += 1) { + right[index] = payload.readFloatLE(rightOffset + index * 4) + } + } else { + right.set(left) + } + + this.pendingAudioPackets.push({ + sourceId: source.id, + sequence, + sampleRate, + channelCount, + left, + right, + transport, + }) + if (this.pendingAudioPackets.length > MAX_PENDING_PACKETS) { + this.pendingAudioPackets.splice(0, this.pendingAudioPackets.length - MAX_PENDING_PACKETS) + } + this.scheduleAudioBatch() + return true + } + + private scheduleAudioBatch(): void { + if (this.audioBatchTimer) return + this.audioBatchTimer = setTimeout(() => { + this.audioBatchTimer = null + const sourceId = this.selectedSourceId + if (!sourceId) { + this.pendingAudioPackets = [] + return + } + const packets = this.pendingAudioPackets.filter((packet) => packet.sourceId === sourceId) + this.pendingAudioPackets = [] + if (packets.length > 0) { + this.onAudioBatch?.({ sourceId, packets }) + } + }, this.audioBatchIntervalMs) + } + + private sendSubscription(client: ClientState): void { + if (!client.source || client.socket.destroyed) return + const payload: DawBridgeSubscribePayload = { + selected: client.source.id === this.selectedSourceId, + } + this.sendFrame(client.socket, DAW_BRIDGE_MESSAGE.subscribe, Buffer.from(JSON.stringify(payload), 'utf8')) + } + + private allocateLiveId(sourceId: string, instanceId: string, owner: ClientState): string { + const base = `${sourceId}:${instanceId.slice(0, 16)}` + let candidate = base + let suffix = 2 + while (this.sources.has(candidate) && this.sources.get(candidate) !== owner) { + candidate = `${base}:${suffix}` + suffix += 1 + } + return candidate + } + + private sendFrame(socket: Socket, messageType: number, payload: Buffer): void { + const header = Buffer.allocUnsafe(DAW_BRIDGE_FRAME_HEADER_BYTES) + header.writeUInt32LE(DAW_BRIDGE_FRAME_MAGIC, 0) + header.writeUInt16LE(DAW_BRIDGE_PROTOCOL_VERSION, 4) + header.writeUInt16LE(messageType, 6) + header.writeUInt32LE(payload.length, 8) + socket.write(Buffer.concat([header, payload])) + } + + private expireSilentClients(): void { + const cutoff = Date.now() - this.heartbeatTimeoutMs + for (const client of this.clients) { + if (client.lastHeartbeatAt < cutoff) { + client.socket.destroy() + } + } + } + + private removeClient(client: ClientState): void { + this.clients.delete(client) + if (client.source && this.sources.get(client.source.id) === client) { + this.sources.delete(client.source.id) + this.pendingAudioPackets = this.pendingAudioPackets.filter((packet) => packet.sourceId !== client.source!.id) + this.emitSnapshot() + } + } + + private emitSnapshot(): void { + this.onSnapshot?.(this.getSnapshot()) + } +} diff --git a/src/main/services/localMediaMpris.ts b/src/main/services/localMediaMpris.ts new file mode 100644 index 0000000..3eb052f --- /dev/null +++ b/src/main/services/localMediaMpris.ts @@ -0,0 +1,112 @@ +import type { NowPlayingControlCommand } from '../../types/nowPlaying' + +const MPRIS_PLAYER_INTERFACE = 'org.mpris.MediaPlayer2.Player' +const MPRIS_PLAYER_OBJECT_PATH = '/org/mpris/MediaPlayer2' +const SESSION_DBUS_INTERFACE = 'org.freedesktop.DBus' +const SESSION_DBUS_OBJECT_PATH = '/org/freedesktop/DBus' + +function normalizeString(value: string | undefined): string { + return (value ?? '').trim() +} + +function escapeRegexLiteral(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +// g_variant_print uses either quote style depending on the text being encoded. +const GVARIANT_STRING = String.raw`(?:'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)")` +function decodeGVariantString(value: string): string { + return value.replace(/\\([\\'"nrtbf])/g, (_match, escaped: string) => ({ + n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', + }[escaped] ?? escaped)) +} +function decodedMatch(match: RegExpMatchArray | null): string { + return normalizeString(decodeGVariantString(match?.[1] ?? match?.[2] ?? '')) +} +export function extractGdbusStringVariant(output: string, key: string): string { + return decodedMatch(output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:@s )?${GVARIANT_STRING}>`))) +} +export function extractGdbusObjectPathVariant(output: string, key: string): string { + return decodedMatch(output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:objectpath |@o )${GVARIANT_STRING}>`))) +} + +export function extractGdbusInt64Variant(output: string, key: string): number { + const match = output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:@x |@t |int64 |uint64 )?(-?\\d+)>`)) + if (!match) { + return 0 + } + + const numeric = Number.parseInt(match[1], 10) + if (!Number.isFinite(numeric)) { + return 0 + } + + return Math.max(0, numeric) +} + +export function extractGdbusStringArrayVariant(output: string, key: string): string[] { + const match = output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:@as )?\\[([\\s\\S]*?)\\]>`)) + if (!match?.[1]) { + return [] + } + + return parseLinuxBusNames(match[1]) +} + +export function parseLinuxBusNames(output: string): string[] { + return Array.from(output.matchAll(new RegExp(GVARIANT_STRING, 'g')), decodedMatch).filter(Boolean) +} + +export function buildLinuxListNamesArgs(): string[] { + return [ + 'call', + '--session', + '--dest', + SESSION_DBUS_INTERFACE, + '--object-path', + SESSION_DBUS_OBJECT_PATH, + '--method', + `${SESSION_DBUS_INTERFACE}.ListNames`, + ] +} + +export function buildLinuxPropertiesArgs(busName: string, interfaceName = MPRIS_PLAYER_INTERFACE): string[] { + return [ + 'call', + '--session', + '--dest', + busName, + '--object-path', + MPRIS_PLAYER_OBJECT_PATH, + '--method', + 'org.freedesktop.DBus.Properties.GetAll', + interfaceName, + ] +} + +export function buildLinuxCommandArgs(busName: string, command: NowPlayingControlCommand): string[] { + const methodName = (() => { + switch (command) { + case 'play': + return 'Play' + case 'pause': + return 'Pause' + case 'next': + return 'Next' + case 'previous': + return 'Previous' + } + })() + + return [ + 'call', + '--session', + '--dest', + busName, + '--object-path', + MPRIS_PLAYER_OBJECT_PATH, + '--method', + `${MPRIS_PLAYER_INTERFACE}.${methodName}`, + ] +} + diff --git a/src/main/services/macSpotifyProvider.ts b/src/main/services/macSpotifyProvider.ts index f1c06f7..cf1ebe7 100644 --- a/src/main/services/macSpotifyProvider.ts +++ b/src/main/services/macSpotifyProvider.ts @@ -1,3 +1,13 @@ +import { + extractGdbusStringVariant, + extractGdbusObjectPathVariant, + extractGdbusInt64Variant, + extractGdbusStringArrayVariant, + parseLinuxBusNames, + buildLinuxListNamesArgs, + buildLinuxPropertiesArgs, + buildLinuxCommandArgs, +} from './localMediaMpris' import { access } from 'node:fs/promises' import { execFile } from 'node:child_process' import { homedir } from 'node:os' @@ -53,10 +63,6 @@ const SLOW_POLL_MS = 5000 const SPOTIFY_ARTWORK_COLOR = '#1ed760' const SPOTIFY_DELIMITER = '\u001f' const SPOTIFY_APP_BUNDLE_ID = 'com.spotify.client' -const MPRIS_PLAYER_INTERFACE = 'org.mpris.MediaPlayer2.Player' -const MPRIS_PLAYER_OBJECT_PATH = '/org/mpris/MediaPlayer2' -const SESSION_DBUS_INTERFACE = 'org.freedesktop.DBus' -const SESSION_DBUS_OBJECT_PATH = '/org/freedesktop/DBus' const SPOTIFY_MPRIS_NAME_PATTERN = /^org\.mpris\.MediaPlayer2\.spotify(?:\..+)?$/i const execFileAsync = promisify(execFile) @@ -194,54 +200,6 @@ function createTrackId(title: string, artist: string, album: string): string { return `spotify-local:${title}\n${artist}\n${album}` } -function escapeRegexLiteral(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -} - -function decodeGVariantString(value: string): string { - return value - .replace(/\\\\/g, '\\') - .replace(/\\'/g, '\'') - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\t/g, '\t') -} - -function extractGdbusStringVariant(output: string, key: string): string { - const match = output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:@s )?'((?:\\\\.|[^'])*)'>`)) - return normalizeString(match?.[1] ? decodeGVariantString(match[1]) : '') -} - -function extractGdbusObjectPathVariant(output: string, key: string): string { - const match = output.match(new RegExp(`'${escapeRegexLiteral(key)}': `)) - return normalizeString(match?.[1] ? decodeGVariantString(match[1]) : '') -} - -function extractGdbusInt64Variant(output: string, key: string): number { - const match = output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:@x |@t |int64 |uint64 )?(-?\\d+)>`)) - if (!match) { - return 0 - } - - const numeric = Number.parseInt(match[1], 10) - if (!Number.isFinite(numeric)) { - return 0 - } - - return Math.max(0, numeric) -} - -function extractGdbusStringArrayVariant(output: string, key: string): string[] { - const match = output.match(new RegExp(`'${escapeRegexLiteral(key)}': <(?:@as )?\\[([\\s\\S]*?)\\]>`)) - if (!match?.[1]) { - return [] - } - - return Array.from(match[1].matchAll(/'((?:\\.|[^'])*)'/g), (entry) => { - return normalizeString(decodeGVariantString(entry[1] ?? '')) - }).filter(Boolean) -} - function parseLinuxPlaybackState(value: string): LocalSpotifySnapshot['playbackState'] { switch (value.toLowerCase()) { case 'playing': @@ -389,12 +347,6 @@ function parseWindowsSpotifyStatusPayload( } } -function parseLinuxBusNames(output: string): string[] { - return Array.from(output.matchAll(/'((?:\\.|[^'])*)'/g), (entry) => { - return normalizeString(decodeGVariantString(entry[1] ?? '')) - }).filter(Boolean) -} - function getLinuxSpotifyBusName(output: string): string | null { return parseLinuxBusNames(output).find((name) => SPOTIFY_MPRIS_NAME_PATTERN.test(name)) ?? null } @@ -531,59 +483,6 @@ async function defaultCommandRunner(command: string, args: string[]): Promise { - switch (command) { - case 'play': - return 'Play' - case 'pause': - return 'Pause' - case 'next': - return 'Next' - case 'previous': - return 'Previous' - } - })() - - return [ - 'call', - '--session', - '--dest', - busName, - '--object-path', - MPRIS_PLAYER_OBJECT_PATH, - '--method', - `${MPRIS_PLAYER_INTERFACE}.${methodName}`, - ] -} - export class SpotifyProvider implements NowPlayingProviderService<'spotify'> { readonly providerId = 'spotify' diff --git a/src/main/services/nowPlayingManager.ts b/src/main/services/nowPlayingManager.ts index a0e4f0e..d790214 100644 --- a/src/main/services/nowPlayingManager.ts +++ b/src/main/services/nowPlayingManager.ts @@ -21,6 +21,7 @@ interface NowPlayingManagerOptions { type ProviderServiceMap = Partial<{ astra: NowPlayingProviderService<'astra'> spotify: NowPlayingProviderService<'spotify'> + tidal: NowPlayingProviderService<'tidal'> }> interface NowPlayingLocalState { @@ -96,8 +97,7 @@ function normalizeLocalState(raw: unknown): NowPlayingLocalState { } } -function createPlaceholderProviderState(providerId: Exclude): NowPlayingProviderState { - const definition = NOW_PLAYING_PROVIDER_DEFINITIONS[providerId] +function createUnavailableProviderState(providerId: NowPlayingProviderId): NowPlayingProviderState { return { providerId, connectionState: 'unavailable', @@ -105,8 +105,8 @@ function createPlaceholderProviderState(providerId: Exclude } else if (providerService.providerId === 'spotify') { acc.spotify = providerService as NowPlayingProviderService<'spotify'> + } else if (providerService.providerId === 'tidal') { + acc.tidal = providerService as NowPlayingProviderService<'tidal'> } providerService.subscribe(() => { if (!this.initialized) { @@ -220,6 +222,9 @@ export class NowPlayingManager { throw new Error(`${NOW_PLAYING_PROVIDER_DEFINITIONS[activeProviderId].label} controls are not available yet.`) } + if (!providerService.getProviderState().supportsTransportControls) { + throw new Error(`${NOW_PLAYING_PROVIDER_DEFINITIONS[activeProviderId].label} controls are unavailable on this platform.`) + } await providerService.sendControl(command) return this.getState() } @@ -231,7 +236,7 @@ export class NowPlayingManager { hasToken: false, }, spotify: this.providerServices.spotify?.getPublicConfig() ?? {}, - tidal: {}, + tidal: this.providerServices.tidal?.getPublicConfig() ?? {}, } const providers: NowPlayingProviderStateMap = { astra: this.providerServices.astra?.getProviderState() ?? { @@ -254,7 +259,7 @@ export class NowPlayingManager { available: false, supportsTransportControls: true, }, - tidal: createPlaceholderProviderState('tidal'), + tidal: this.providerServices.tidal?.getProviderState() ?? createUnavailableProviderState('tidal'), } const hasConfiguredProvider = this.providerPriority.some((providerId) => { diff --git a/src/main/services/nowPlayingProvider.ts b/src/main/services/nowPlayingProvider.ts index 6a500ef..60fd218 100644 --- a/src/main/services/nowPlayingProvider.ts +++ b/src/main/services/nowPlayingProvider.ts @@ -6,7 +6,7 @@ import type { NowPlayingProviderState, } from '../../types/nowPlaying' -export type ManagedNowPlayingProviderId = Exclude +export type ManagedNowPlayingProviderId = NowPlayingProviderId export interface NowPlayingProviderService { readonly providerId: K diff --git a/src/main/services/nowPlayingWindowConsumer.ts b/src/main/services/nowPlayingWindowConsumer.ts new file mode 100644 index 0000000..32cc0b5 --- /dev/null +++ b/src/main/services/nowPlayingWindowConsumer.ts @@ -0,0 +1,37 @@ +import type { NowPlayingManager } from './nowPlayingManager' + +interface ConsumerWebContents { + readonly id: number + isDestroyed(): boolean + once(event: 'destroyed', listener: () => void): unknown +} + +/** Keep providers active for the native window's lifetime, including renderer reloads. */ +export function bindNowPlayingWindowConsumer( + contents: ConsumerWebContents, + manager: Pick, + onError: (error: unknown) => void, +): void { + if (contents.isDestroyed()) return + + const consumerId = contents.id + let destroyed = false + const setActive = async (active: boolean): Promise => { + try { + await manager.setConsumerActive(consumerId, active) + } catch (error) { + onError(error) + } + } + + contents.once('destroyed', () => { + destroyed = true + void setActive(false) + }) + + void (async () => { + await setActive(true) + // Initialization may have added this consumer after the destruction cleanup ran. + if (destroyed) await setActive(false) + })() +} diff --git a/src/main/services/tidalProvider.ts b/src/main/services/tidalProvider.ts new file mode 100644 index 0000000..ddbfd5f --- /dev/null +++ b/src/main/services/tidalProvider.ts @@ -0,0 +1,406 @@ +import { execFile } from 'node:child_process' +import { access, open } from 'node:fs/promises' +import { homedir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { setTimeout as delay } from 'node:timers/promises' +import type { NowPlayingControlCommand, NowPlayingProviderState, NowPlayingSnapshot } from '../../types/nowPlaying' +import type { NativeWindowsMediaAPI } from '../../types/nativeWindowsMedia' +import type { NowPlayingProviderService } from './nowPlayingProvider' +import { + buildLinuxCommandArgs, buildLinuxListNamesArgs, buildLinuxPropertiesArgs, + extractGdbusInt64Variant, extractGdbusObjectPathVariant, extractGdbusStringArrayVariant, + extractGdbusStringVariant, parseLinuxBusNames, +} from './localMediaMpris' + +const execFileAsync = promisify(execFile) +const MAX_ARTWORK_BYTES = 4 * 1024 * 1024 +const COMMAND_TIMEOUT_MS = 5000 +const TIDAL_BUNDLE_ID = 'com.tidal.desktop' +type CommandRunner = (command: string, args: string[], signal: AbortSignal) => Promise + +interface TidalProviderOptions { + platform?: NodeJS.Platform + now?: () => number + commandRunner?: CommandRunner + accessImpl?: typeof access + appPathCandidates?: string[] + fetchImpl?: typeof fetch + windowsMediaApi?: NativeWindowsMediaAPI | null +} + +// Executed by the system JXA host, without loading any bundled framework or helper. +// Keep reads optional: MediaRemote is private and fields vary by app and OS version. +export const TIDAL_MAC_STATUS_SCRIPT = ` +ObjC.import('Foundation'); +function optional(read, fallback) { + try { var value = read(); return value == null ? fallback : value; } catch (_) { return fallback; } +} +function run() { + var framework = $.NSBundle.bundleWithPath('/System/Library/PrivateFrameworks/MediaRemote.framework'); + if (!framework.load) throw new Error('TIDAL_MEDIA_UNAVAILABLE: macOS media information cannot be loaded.'); + var request = $.NSClassFromString('MRNowPlayingRequest'); + if (!request || !request.respondsToSelector('localNowPlayingItem') || + !request.respondsToSelector('localNowPlayingPlayerPath') || !request.respondsToSelector('localIsPlaying')) { + throw new Error('TIDAL_MEDIA_UNAVAILABLE: This macOS version does not expose local media information.'); + } + var item = request.localNowPlayingItem; + if (!item || !item.js) return 'null'; + var client = request.localNowPlayingPlayerPath.client; + var bundleIdentifier = optional(function () { return ObjC.unwrap(client.bundleIdentifier); }, ''); + var parentApplicationBundleIdentifier = optional(function () { return ObjC.unwrap(client.parentApplicationBundleIdentifier); }, ''); + if (bundleIdentifier !== 'com.tidal.desktop' && parentApplicationBundleIdentifier !== 'com.tidal.desktop') return 'null'; + var info = item.nowPlayingInfo; + function field(name) { return optional(function () { return ObjC.unwrap(info.objectForKey('kMRMediaRemoteNowPlayingInfo' + name)); }, null); } + var timestampMs = optional(function () { return Number(info.objectForKey('kMRMediaRemoteNowPlayingInfoTimestamp').timeIntervalSince1970) * 1000; }, null); + var artworkData = optional(function () { + var data = info.objectForKey('kMRMediaRemoteNowPlayingInfoArtworkData'); + return data.length > 0 && data.length <= 4194304 ? ObjC.unwrap(data.base64EncodedStringWithOptions(0)) : null; + }, null); + return JSON.stringify({ + bundleIdentifier: bundleIdentifier, parentApplicationBundleIdentifier: parentApplicationBundleIdentifier, + title: field('Title'), artist: field('Artist'), album: field('Album'), + id: field('ContentItemIdentifier'), playing: !!request.localIsPlaying, + duration: field('Duration'), elapsedTime: field('ElapsedTime'), timestampMs: timestampMs, + artworkData: artworkData, artworkMimeType: field('ArtworkMIMEType') + }); +} +` + +function string(value: unknown): string { return typeof value === 'string' ? value.trim() : '' } +function number(value: unknown): number { + return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, value) : 0 +} +function errorMessage(error: unknown): string { + if (error && typeof error === 'object') { + const details = error as { killed?: boolean; stderr?: string } + if (details.killed) return 'The local media query timed out.' + if (details.stderr?.trim()) return details.stderr.trim() + } + return error instanceof Error ? error.message : String(error) +} +function isMissingSession(error: unknown): boolean { + return /ServiceUnknown|NameHasNoOwner|UnknownObject|was not provided by any .service/i.test(errorMessage(error)) +} +function isTidalIdentity(value: string): boolean { + return /^(tidal|tidal[- ]hi[- ]?fi|com\.mastermindzh\.tidal-hifi)(?:\.desktop)?$/i.test(value) +} +export function isTidalWindowsSession(sourceId: string): boolean { + return /^(com\.squirrel\.tidal\.tidal|tidal(?:\.exe)?|com\.tidal\.desktop)$/i.test(sourceId) + || /^TIDALMusicAS\.TIDAL_[a-z0-9]+!TIDAL$/i.test(sourceId) +} +function dataArtwork(value: unknown): string | null { + const data = string(value) + return data.length <= MAX_ARTWORK_BYTES * 4 / 3 + 100 + && /^data:image\/(png|jpeg|jpg|webp|gif);base64,[a-z0-9+/=]+$/i.test(data) ? data : null +} +function snapshot(fields: { + title: unknown; artist?: unknown; album?: unknown; id?: unknown + playbackState: NowPlayingSnapshot['playbackState']; currentTime: number; duration: number + artworkDataUrl?: string | null +}, now: number): NowPlayingSnapshot | null { + const title = string(fields.title) + if (!title) return null + const artist = string(fields.artist) + const album = string(fields.album) + return { + playbackState: fields.playbackState, + currentTime: fields.duration > 0 ? Math.min(fields.currentTime, fields.duration) : fields.currentTime, + duration: fields.duration, updatedAt: now, queueLength: 0, outputDeviceLabel: null, + visualizerLineColor: '#f4f6f8', + currentTrack: { + id: `tidal:${string(fields.id) || `${title}\n${artist}\n${album}`}`, + title, artist, album, isFavorite: false, artworkDataUrl: fields.artworkDataUrl ?? null, + }, + } +} + +export function parseTidalMacStatus(output: string, now: number): NowPlayingSnapshot | null { + const raw: unknown = JSON.parse(output) + if (raw === null) return null + if (typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Invalid TIDAL macOS media response.') + const data = raw as Record + if (data.bundleIdentifier !== TIDAL_BUNDLE_ID && data.parentApplicationBundleIdentifier !== TIDAL_BUNDLE_ID) return null + const elapsed = number(data.elapsedTime) + const timestamp = number(data.timestampMs) + // Without a reported position there is nothing to extrapolate. + const advance = data.playing === true && typeof data.elapsedTime === 'number' && timestamp > 0 + ? Math.max(0, now - timestamp) / 1000 : 0 + return snapshot({ + ...data, title: data.title, playbackState: data.playing === true ? 'playing' : 'paused', + currentTime: elapsed + advance, duration: number(data.duration), + artworkDataUrl: dataArtwork(`data:${string(data.artworkMimeType)};base64,${string(data.artworkData)}`), + }, now) +} + +export class TidalProvider implements NowPlayingProviderService<'tidal'> { + readonly providerId = 'tidal' + private readonly platform: NodeJS.Platform + private readonly now: () => number + private readonly run: CommandRunner + private readonly options: TidalProviderOptions + private readonly listeners = new Set<() => void>() + private readonly consumers = new Set() + private readonly lifetime = new AbortController() + private poll: AbortController | null = null + private chain: Promise = Promise.resolve() + private initialization: Promise | null = null + private selectedBus: string | null = null + private artworkCache: { key: string; data: string | null; retryAt: number } | null = null + private state: NowPlayingProviderState = { + providerId: 'tidal', connectionState: 'unavailable', available: false, isConfigured: false, + supportsTransportControls: false, snapshot: null, lastError: null, lastControlError: null, + } + + constructor(options: TidalProviderOptions = {}) { + this.options = options + this.platform = options.platform ?? process.platform + this.now = options.now ?? Date.now + this.run = options.commandRunner ?? (async (command, args, signal) => { + const { stdout } = await execFileAsync(command, args, { + signal, timeout: COMMAND_TIMEOUT_MS, maxBuffer: 6 * 1024 * 1024, + }) + return stdout.trim() + }) + } + + getPublicConfig(): Record { return {} } + getProviderState(): NowPlayingProviderState { return structuredClone(this.state) } + subscribe(listener: () => void): () => void { + this.listeners.add(listener) + return () => { this.listeners.delete(listener) } + } + private publish(update: Partial, signal = this.lifetime.signal): void { + if (signal.aborted || this.lifetime.signal.aborted) return + this.state = { ...this.state, ...update } + for (const listener of this.listeners) listener() + } + private enqueue(work: () => Promise): Promise { + const next = this.chain.catch(() => undefined).then(work) + this.chain = next + return next + } + initialize(): Promise { + this.initialization ??= this.enqueue(() => this.probe()).then(() => { this.startPolling() }) + return this.initialization + } + async dispose(): Promise { + this.lifetime.abort() + this.stopPolling() + this.listeners.clear() + this.consumers.clear() + await this.chain.catch(() => undefined) + } + async saveConfig(): Promise { throw new Error('TIDAL does not require configuration.') } + async setConsumerActive(consumerId: number, active: boolean): Promise { + if (this.lifetime.signal.aborted) return + if (active) this.consumers.add(consumerId) + else this.consumers.delete(consumerId) + if (!this.consumers.size) { + this.stopPolling() + this.selectedBus = null + this.artworkCache = null + this.publish({ snapshot: null, connectionState: this.state.available ? 'disabled' : 'unavailable' }) + } else if (this.initialization) { + await this.initialization + this.startPolling() + } + } + async retry(): Promise { + if (this.lifetime.signal.aborted) return + await this.initialize() + this.stopPolling() + await this.enqueue(() => this.probe()) + if (!this.state.available) throw new Error(this.state.lastError ?? 'TIDAL integration is unavailable.') + this.artworkCache = null + const refresh = this.startPolling() + await refresh + } + private async probe(): Promise { + const signal = this.lifetime.signal + signal.throwIfAborted() + try { + if (this.platform === 'darwin') { + const candidates = this.options.appPathCandidates ?? ['/Applications/TIDAL.app', join(homedir(), 'Applications/TIDAL.app')] + const installed = await Promise.all(candidates.map(path => (this.options.accessImpl ?? access)(path).then(() => true, () => false))) + if (!installed.some(Boolean)) throw new Error('Install TIDAL.app, then click Retry to enable local track information.') + parseTidalMacStatus(await this.run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', TIDAL_MAC_STATUS_SCRIPT], signal), this.now()) + } else if (this.platform === 'linux') { + await this.run('gdbus', buildLinuxListNamesArgs(), signal) + } else if (this.platform === 'win32') { + const api = this.options.windowsMediaApi + if (!api?.getTidalPlaybackState || !api.sendTidalControl) throw new Error('TIDAL Windows media integration is missing. Rebuild or update Prism, then click Retry.') + const support = api.getSupport() + if (!support.available) throw new Error(support.reason ?? 'Windows system media controls are unavailable. Click Retry after restoring access.') + } else { + throw new Error('Local TIDAL integration is supported on macOS, Windows, and Linux.') + } + this.publish({ available: true, isConfigured: true, supportsTransportControls: this.platform !== 'darwin', + connectionState: 'disabled', snapshot: null, lastError: null, lastControlError: null }, signal) + } catch (error) { + this.publish({ available: false, isConfigured: false, supportsTransportControls: false, + connectionState: 'unavailable', snapshot: null, lastError: this.describeError(error) }, signal) + } + } + private describeError(error: unknown): string { + const message = errorMessage(error) + if (this.platform === 'darwin') return `TIDAL track information is unavailable. ${message} Retry after checking TIDAL and macOS compatibility.` + if (this.platform === 'linux') return `Could not access TIDAL through the Linux session media bus. ${message} Check gdbus and your client's MPRIS support, then click Retry.` + return message + } + private stopPolling(): void { + this.poll?.abort() + this.poll = null + } + private startPolling(): Promise | undefined { + if (this.poll || !this.consumers.size || !this.state.available || this.lifetime.signal.aborted) return + const controller = new AbortController() + this.poll = controller + this.publish({ connectionState: 'connecting' }, controller.signal) + const first = this.enqueue(() => this.refresh(controller.signal)) + void (async () => { + await first + while (!controller.signal.aborted && this.state.available) { + await delay(this.state.snapshot?.playbackState === 'playing' ? 1500 : 5000, undefined, { signal: controller.signal }) + await this.enqueue(() => this.refresh(controller.signal)) + } + })().catch(() => undefined).finally(() => { if (this.poll === controller) this.poll = null }) + return first + } + private async refresh(signal: AbortSignal): Promise { + if (signal.aborted || this.lifetime.signal.aborted) return + try { + const { state, artworkUrl } = await this.readSnapshot(signal) + if (!state) this.artworkCache = null + if (state?.currentTrack && this.artworkCache?.key === `${state.currentTrack.id}\n${artworkUrl}`) { + state.currentTrack.artworkDataUrl = this.artworkCache.data + } + this.publish({ snapshot: state, connectionState: state ? 'connected' : 'disabled', lastError: null }, signal) + if (state?.currentTrack && artworkUrl && !signal.aborted) { + const artwork = await this.readArtwork(`${state.currentTrack.id}\n${artworkUrl}`, artworkUrl, signal) + if (artwork) this.publish({ snapshot: { ...state, currentTrack: { ...state.currentTrack, artworkDataUrl: artwork } } }, signal) + } + } catch (error) { + if (signal.aborted || this.lifetime.signal.aborted) return + this.selectedBus = null + this.artworkCache = null + const unavailable = this.platform === 'darwin' && /TIDAL_MEDIA_UNAVAILABLE|selector|not a function/i.test(errorMessage(error)) + this.publish({ snapshot: null, connectionState: unavailable ? 'unavailable' : 'error', lastError: this.describeError(error), + ...(unavailable ? { available: false, isConfigured: false, supportsTransportControls: false } : {}) }, signal) + } + } + private async readSnapshot(signal: AbortSignal): Promise<{ state: NowPlayingSnapshot | null; artworkUrl?: string }> { + if (this.platform === 'darwin') { + return { state: parseTidalMacStatus(await this.run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', TIDAL_MAC_STATUS_SCRIPT], signal), this.now()) } + } + if (this.platform === 'win32') { + const data = this.options.windowsMediaApi!.getTidalPlaybackState!() + if (!data || !isTidalWindowsSession(data.sourceAppUserModelId)) return { state: null } + const status = string(data.playbackStatus).toLowerCase() + return { state: snapshot({ ...data, id: `${data.sourceAppUserModelId}\n${data.title}\n${data.artist}\n${data.album}`, + playbackState: status === 'playing' ? 'playing' : status === 'paused' ? 'paused' : 'stopped', + duration: number(data.durationMs) / 1000, currentTime: number(data.positionMs) / 1000, + artworkDataUrl: dataArtwork(data.artworkDataUrl) }, this.now()) } + } + const names = parseLinuxBusNames(await this.run('gdbus', buildLinuxListNamesArgs(), signal)) + .filter(name => name.startsWith('org.mpris.MediaPlayer2.')).sort() + const candidates: { bus: string; state: NowPlayingSnapshot | null; artworkUrl: string }[] = [] + for (const bus of names) { + signal.throwIfAborted() + let identified = false + try { + const identity = await this.run('gdbus', buildLinuxPropertiesArgs(bus, 'org.mpris.MediaPlayer2'), signal) + if (!this.matchesLinuxIdentity(identity)) continue + identified = true + const data = await this.run('gdbus', buildLinuxPropertiesArgs(bus), signal) + const status = extractGdbusStringVariant(data, 'PlaybackStatus') + candidates.push({ bus, artworkUrl: extractGdbusStringVariant(data, 'mpris:artUrl'), state: snapshot({ + title: extractGdbusStringVariant(data, 'xesam:title'), artist: extractGdbusStringArrayVariant(data, 'xesam:artist').join(', '), + album: extractGdbusStringVariant(data, 'xesam:album'), + id: `${bus}\n${extractGdbusObjectPathVariant(data, 'mpris:trackid')}\n${extractGdbusStringVariant(data, 'xesam:title')}\n${extractGdbusStringArrayVariant(data, 'xesam:artist').join(', ')}\n${extractGdbusStringVariant(data, 'xesam:album')}`, + playbackState: status === 'Playing' ? 'playing' : status === 'Paused' ? 'paused' : 'stopped', + duration: extractGdbusInt64Variant(data, 'mpris:length') / 1_000_000, + currentTime: extractGdbusInt64Variant(data, 'Position') / 1_000_000, + }, this.now()) }) + } catch (error) { + if (signal.aborted) throw error + if ((identified || bus === this.selectedBus) && !isMissingSession(error)) throw error + // Unrelated or disappearing media clients must not break TIDAL discovery. + } + } + candidates.sort((a, b) => Number(b.state?.playbackState === 'playing') - Number(a.state?.playbackState === 'playing') + || Number(b.bus === this.selectedBus) - Number(a.bus === this.selectedBus)) + const selected = candidates.find(candidate => candidate.state) ?? candidates[0] + if (!signal.aborted) this.selectedBus = selected?.bus ?? null + return selected ?? { state: null } + } + private matchesLinuxIdentity(output: string): boolean { + return isTidalIdentity(extractGdbusStringVariant(output, 'Identity')) + || isTidalIdentity(extractGdbusStringVariant(output, 'DesktopEntry')) + } + async sendControl(command: NowPlayingControlCommand): Promise { + return this.enqueue(async () => { + const signal = this.lifetime.signal + signal.throwIfAborted() + try { + if (!['play', 'pause', 'next', 'previous'].includes(command)) throw new Error('Unsupported TIDAL control command.') + if (!this.state.supportsTransportControls) throw new Error('TIDAL transport controls are unavailable on this platform. Use the TIDAL app.') + if (this.platform === 'linux') { + const bus = this.selectedBus + if (!bus) throw new Error('No active TIDAL media session. Start playback in TIDAL, then click Retry.') + const identity = await this.run('gdbus', buildLinuxPropertiesArgs(bus, 'org.mpris.MediaPlayer2'), signal) + if (!this.matchesLinuxIdentity(identity)) throw new Error('The selected media session no longer belongs to TIDAL.') + await this.run('gdbus', buildLinuxCommandArgs(bus, command), signal) + } else { + if (!this.options.windowsMediaApi!.sendTidalControl!(command)) throw new Error('TIDAL did not accept the playback command.') + } + this.publish({ lastControlError: null }, signal) + } catch (error) { + this.publish({ lastControlError: errorMessage(error) }, signal) + throw error + } + }).then(async () => { if (this.poll) await this.enqueue(() => this.refresh(this.poll?.signal ?? this.lifetime.signal)) }) + } + private async readArtwork(key: string, url: string, signal: AbortSignal): Promise { + if (this.artworkCache?.key === key && this.artworkCache.retryAt > this.now()) return this.artworkCache.data + let data: string | null = null + try { + const parsed = new URL(url) + const artworkSignal = AbortSignal.any([signal, AbortSignal.timeout(COMMAND_TIMEOUT_MS)]) + if (parsed.protocol === 'file:' && !parsed.host) { + const handle = await open(fileURLToPath(parsed), 'r') + try { + artworkSignal.throwIfAborted() + const info = await handle.stat() + if (info.isFile() && info.size > 0 && info.size <= MAX_ARTWORK_BYTES) { + const buffer = Buffer.alloc(info.size) + const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0) + const mime = /\.png$/i.test(parsed.pathname) ? 'image/png' : /\.webp$/i.test(parsed.pathname) ? 'image/webp' : 'image/jpeg' + data = bytesRead ? `data:${mime};base64,${buffer.subarray(0, bytesRead).toString('base64')}` : null + } + } finally { await handle.close() } + } else if (parsed.protocol === 'https:' || parsed.protocol === 'http:') { + const response = await (this.options.fetchImpl ?? fetch)(url, { signal: artworkSignal }) + const mime = response.headers.get('content-type')?.split(';')[0].trim() ?? '' + if (response.ok && /^image\/(png|jpeg|jpg|webp|gif)$/i.test(mime) && response.body) { + const reader = response.body.getReader() + const chunks: Uint8Array[] = [] + let total = 0 + try { + while (true) { + const next = await reader.read() + if (next.done) break + total += next.value.length + if (total > MAX_ARTWORK_BYTES) throw new Error('Artwork is too large.') + chunks.push(next.value) + } + data = total ? `data:${mime};base64,${Buffer.concat(chunks).toString('base64')}` : null + } finally { await reader.cancel().catch(() => undefined) } + } else { await response.body?.cancel() } + } + } catch { /* Artwork is optional; never discard a valid track when loading it fails. */ } + if (!signal.aborted) this.artworkCache = { key, data, retryAt: data ? Infinity : this.now() + 30_000 } + return data + } +} diff --git a/src/main/services/trayMenu.ts b/src/main/services/trayMenu.ts index 37445a5..1bc3388 100644 --- a/src/main/services/trayMenu.ts +++ b/src/main/services/trayMenu.ts @@ -68,6 +68,7 @@ export function normalizeTrayRendererState(value: unknown): TrayRendererState { const candidate = value as Partial const captureStatus = candidate.captureStatus === 'connecting' + || candidate.captureStatus === 'waiting' || candidate.captureStatus === 'capturing' || candidate.captureStatus === 'error' ? candidate.captureStatus @@ -81,18 +82,24 @@ export function normalizeTrayRendererState(value: unknown): TrayRendererState { activeSourceLabel: typeof candidate.activeSourceLabel === 'string' ? normalizeText(candidate.activeSourceLabel, 'Audio Source') : null, - captureMode: candidate.captureMode === 'device' ? 'device' : 'system', + captureMode: candidate.captureMode === 'device' || candidate.captureMode === 'daw' + ? candidate.captureMode + : 'system', selectedSystemSourceId: typeof candidate.selectedSystemSourceId === 'string' ? candidate.selectedSystemSourceId : null, selectedDeviceId: typeof candidate.selectedDeviceId === 'string' ? candidate.selectedDeviceId : null, + selectedDawSourceId: typeof candidate.selectedDawSourceId === 'string' + ? candidate.selectedDawSourceId + : null, rollingCaptureSeconds: isRollingCaptureDuration(candidate.rollingCaptureSeconds) ? candidate.rollingCaptureSeconds : null, systemSources: normalizeSources(candidate.systemSources), inputSources: normalizeSources(candidate.inputSources), + dawSources: normalizeSources(candidate.dawSources), } } @@ -102,6 +109,8 @@ export function buildTrayMenuModel(state: TrayMenuState): TrayMenuModel { ? sourceLabel ? `Capturing · ${sourceLabel}` : 'Capturing' : state.rendererState.captureStatus === 'connecting' ? 'Connecting to audio…' + : state.rendererState.captureStatus === 'waiting' + ? 'Waiting for DAW bridge…' : state.rendererState.captureStatus === 'error' ? 'Audio capture error' : 'Capture stopped' @@ -115,6 +124,7 @@ export function buildTrayMenuModel(state: TrayMenuState): TrayMenuModel { mainWindowActionLabel: state.mainWindowVisible ? 'Hide Prism' : 'Show Prism', captureActionLabel: state.rendererState.captureStatus === 'capturing' || state.rendererState.captureStatus === 'connecting' + || state.rendererState.captureStatus === 'waiting' ? 'Stop Capture' : 'Start Capture', captureActionEnabled: state.rendererReady, diff --git a/src/main/windowDocking.ts b/src/main/windowDocking.ts new file mode 100644 index 0000000..40dc480 --- /dev/null +++ b/src/main/windowDocking.ts @@ -0,0 +1,306 @@ +import { BrowserWindow, screen } from 'electron' +import type { FileBackedWindowStateStore } from './windowStateStore' +import { loadNativeWindowDockingApi } from './nativeWindowDocking' +import { WindowDockingController } from './windowDockingController' +import { dockSettingsBounds } from '../shared/windowDocking' +import type { WindowBounds } from '../types/popout' +import type { WindowDockingPreferences, WindowDockingSnapshot } from '../types/windowDocking' + +interface Dependencies { + store: FileBackedWindowStateStore + logicalBounds(window: BrowserWindow): WindowBounds + prepare(window: BrowserWindow): void + restore(window: BrowserWindow, bounds: WindowBounds): void + changed(snapshot: WindowDockingSnapshot): void + geometryChanging(): void +} + +export class WindowDockingService { + readonly controller: WindowDockingController + private api = loadNativeWindowDockingApi() + private window: BrowserWindow | null = null + private panel: BrowserWindow | null = null + private panelHeight = 400 + private rendererReady = false + private fullscreen = false + private fullscreenTimer: ReturnType | null = null + private timer: ReturnType | null = null + private saveTimer: ReturnType | null = null + private restoreTimer: ReturnType | null = null + private pendingSave: WindowDockingPreferences | null = null + private saving: Promise = Promise.resolve() + private lastSnapshot = '' + private messages = this.api?.messages() + private managed = false + + constructor(private deps: Dependencies) { + this.controller = new WindowDockingController({ + supported: Boolean(this.api), + bounds: () => this.controller.ownsGeometry ? this.window!.getBounds() : deps.logicalBounds(this.window!), + displays: () => screen.getAllDisplays().map(display => this.currentDisplay(display)), + primaryDisplay: () => this.currentDisplay(screen.getPrimaryDisplay()), + matchingDisplay: bounds => this.currentDisplay(screen.getDisplayMatching(bounds)), + presented: () => Boolean(this.window && !this.window.isDestroyed() && this.rendererReady + && this.window.isVisible() && !this.window.isMinimized()), + reserve: (display, edge, height) => { + if (!this.window || !this.api) throw new Error('Windows docking is unavailable.') + if (this.restoreTimer) clearTimeout(this.restoreTimer) + this.restoreTimer = null + deps.geometryChanging() + const nativeHandle = this.window.getNativeWindowHandle() + if (!this.api.register(nativeHandle)) throw new Error('Windows could not register Prism as a desktop bar.') + const target = screen.getAllDisplays().find(item => item.id === display.id)! + const rectangle = this.api.position(nativeHandle, edge, + screen.dipToScreenRect(null, display.bounds), Math.round(height * target.scaleFactor)) + if (!rectangle) throw new Error('Windows could not reserve space for Prism.') + const bounds = screen.screenToDipRect(null, rectangle) + const current = this.window.getBounds() + if (current.x !== bounds.x || current.y !== bounds.y + || current.width !== bounds.width || current.height !== bounds.height) { + this.window.setBounds(bounds) + } + this.positionPanel() + this.updateFullscreen() + }, + release: () => { + if (this.controller.ownsGeometry) deps.geometryChanging() + if (this.window && !this.window.isDestroyed()) this.api?.remove(this.window.getNativeWindowHandle()) + }, + managed: enabled => this.setManaged(enabled), + restore: bounds => { + const window = this.window + if (this.restoreTimer) clearTimeout(this.restoreTimer) + // Chromium may consume the first SetBounds while handling ABM_REMOVE. + // Verify the actual HWND bounds and retry after the shell update settles. + let attempts = 0 + const restore = (): void => { + this.restoreTimer = null + if (window && window === this.window && !window.isDestroyed() && !this.controller.ownsGeometry) { + deps.restore(window, bounds) + const actual = window.getBounds() + if (++attempts < 4 && (actual.x !== bounds.x || actual.y !== bounds.y + || actual.width !== bounds.width || actual.height !== bounds.height)) { + this.restoreTimer = setTimeout(restore, 30) + } + } + } + this.restoreTimer = setTimeout(restore, 0) + }, + closeSettings: () => this.closeSettings(), + changed: snapshot => { + const serialized = JSON.stringify(snapshot) + if (serialized === this.lastSnapshot) return + this.lastSnapshot = serialized + this.send('window:docking-changed', snapshot) + deps.changed(snapshot) + }, + persist: preferences => { + this.pendingSave = preferences + if (this.saveTimer) clearTimeout(this.saveTimer) + this.saveTimer = setTimeout(() => { void this.flush() }, 100) + }, + }, deps.store.getDocking()) + screen.on('display-added', this.schedule) + screen.on('display-removed', this.schedule) + screen.on('display-metrics-changed', this.schedule) + } + + private send(channel: string, ...args: unknown[]): void { + if (this.window && !this.window.isDestroyed() && !this.window.webContents.isDestroyed()) { + this.window.webContents.send(channel, ...args) + } + } + + private setManaged(enabled: boolean): void { + const window = this.window + if (!window || window.isDestroyed()) return + if (enabled !== this.managed) { + this.managed = enabled + if (enabled) { + this.deps.prepare(window) + if (window.isFullScreen()) window.setFullScreen(false) + if (window.isMaximized()) window.unmaximize() + } + window.setMovable(!enabled) + // The native subclass blocks resize hit-testing/system commands. Keep + // Electron's programmatic size constraints free for resizing and restore. + window.setMaximizable(!enabled) + window.setFullScreenable(!enabled) + } + const top = enabled ? !this.fullscreen : this.deps.store.getMainAlwaysOnTop() + if (window.isAlwaysOnTop() !== top) window.setAlwaysOnTop(top) + this.send('window:always-on-top-changed', top) + if (this.panel && !this.panel.isDestroyed()) this.panel.setAlwaysOnTop(top) + } + + private schedule = (): void => { + if (this.timer) return + this.timer = setTimeout(() => { + this.timer = null + this.controller.resume() + this.positionPanel() + }, 30) + } + + private updateFullscreen = (): void => { + const window = this.window + if (!window || window.isDestroyed() || !this.controller.ownsGeometry) return + const fullscreen = Boolean(this.api?.fullscreen(window.getNativeWindowHandle())) + if (fullscreen === this.fullscreen) return + this.fullscreen = fullscreen + this.setManaged(true) + if (fullscreen) { + if (this.panel && !this.panel.isDestroyed()) this.api?.lower(this.panel.getNativeWindowHandle()) + this.api?.lower(window.getNativeWindowHandle()) + } + } + + private currentDisplay(display: Electron.Display): Electron.Display { + // Electron's display cache updates asynchronously after ABM_REMOVE. + const rectangle = this.api?.workArea(screen.dipToScreenRect(null, display.bounds)) + return rectangle ? { ...display, workArea: screen.screenToDipRect(null, rectangle) } : display + } + + workAreas(): WindowBounds[] { + return screen.getAllDisplays().map(display => this.currentDisplay(display).workArea) + } + + attach(window: BrowserWindow): void { + this.window = window + this.rendererReady = false + this.managed = false + this.fullscreen = false + if (this.fullscreenTimer) clearInterval(this.fullscreenTimer) + // Foreground borderless fullscreen is not reliably announced by Explorer. + if (this.api) this.fullscreenTimer = setInterval(this.updateFullscreen, 250) + if (this.messages) { + window.hookWindowMessage(this.messages.callback, (wParam, lParam) => { + const code = wParam.readUInt32LE(0) + if (code === 1) this.schedule() // ABN_POSCHANGED + if (code === 2) { // ABN_FULLSCREENAPP + // Read current state as well: some shell versions omit the matching + // exit notification when a fullscreen window is destroyed. + this.updateFullscreen() + } + if (code === 3) { // ABN_WINDOWARRANGE: preserve the reservation while excluded from tiling. + if (lParam.readUInt32LE(0)) this.closeSettings() + this.schedule() + } + }) + window.hookWindowMessage(this.messages.taskbarCreated, () => { + if (this.controller.ownsGeometry) this.controller.shellRestarted() + }) + window.hookWindowMessage(0x02e0, this.schedule) // WM_DPICHANGED + } + window.on('hide', () => this.controller.suspend()) + window.on('minimize', () => this.controller.suspend()) + window.on('show', this.schedule) + window.on('restore', this.schedule) + // Windows can fit the HWND into the new work area after a shell change. + // Reapply only if the actual bounds differ from the negotiated rectangle. + window.on('move', () => { if (this.controller.ownsGeometry) this.schedule() }) + window.on('resize', () => { if (this.controller.ownsGeometry) this.schedule() }) + window.on('will-move', event => { if (this.controller.ownsGeometry) event.preventDefault() }) + window.on('will-resize', event => { if (this.controller.ownsGeometry) event.preventDefault() }) + window.webContents.on('did-start-loading', () => { + this.rendererReady = false + this.controller.suspend() + }) + window.webContents.on('render-process-gone', () => { + this.rendererReady = false + this.controller.suspend() + }) + window.once('closed', () => { + this.controller.suspend() + this.window = null + this.managed = false + if (this.fullscreenTimer) clearInterval(this.fullscreenTimer) + this.fullscreenTimer = null + }) + this.installSettingsWindow(window) + this.controller.resume() + } + + ready(): void { + this.rendererReady = true + this.controller.resume() + this.send('window:docking-changed', this.controller.snapshot) + } + + closeSettings(): void { + if (this.panel && !this.panel.isDestroyed()) this.panel.close() + this.panel = null + this.send('window:docked-settings-closed') + } + + private installSettingsWindow(window: BrowserWindow): void { + window.webContents.setWindowOpenHandler(({ url, frameName }) => { + if (url !== 'about:blank' || frameName !== 'prism-docked-settings' + || !this.controller.snapshot.active || this.panel) return { action: 'deny' } + return { action: 'allow', overrideBrowserWindowOptions: { + parent: window, frame: false, show: false, skipTaskbar: true, + resizable: false, movable: false, minimizable: false, maximizable: false, + fullscreenable: false, alwaysOnTop: !this.fullscreen, + backgroundColor: '#080a0e', title: 'Prism settings', + } } + }) + window.webContents.on('did-create-window', (panel, details) => { + if (details.frameName !== 'prism-docked-settings') return + this.panel = panel + panel.webContents.on('will-navigate', event => event.preventDefault()) + panel.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) + panel.webContents.on('before-input-event', (event, input) => { + if (input.type === 'keyDown' && input.key === 'Escape') { event.preventDefault(); this.closeSettings() } + }) + panel.once('closed', () => { + if (this.panel === panel) this.panel = null + this.send('window:docked-settings-closed') + }) + this.positionPanel() + }) + } + + showSettings(height: number): boolean { + if (!this.panel || this.panel.isDestroyed() || !this.controller.snapshot.active || !Number.isFinite(height)) return false + this.panelHeight = Math.max(1, height) + this.positionPanel() + if (!this.panel.isVisible()) { this.panel.show(); this.panel.focus() } + return true + } + + private positionPanel(): void { + if (!this.panel || this.panel.isDestroyed() || !this.window || this.window.isDestroyed()) return + const snapshot = this.controller.snapshot + const selected = screen.getAllDisplays().find(item => item.id === snapshot.displayId) + const display = selected && this.currentDisplay(selected) + if (!display) return + this.panel.setBounds(dockSettingsBounds(this.window.getBounds(), display.workArea, snapshot.edge, this.panelHeight)) + } + + async flush(): Promise { + if (this.saveTimer) clearTimeout(this.saveTimer) + this.saveTimer = null + const preferences = this.pendingSave + this.pendingSave = null + if (preferences) { + this.saving = this.saving.then(() => this.deps.store.setDocking(preferences)) + .catch(error => { console.warn('Could not save window docking:', error) }) + } + await this.saving + } + + shutdown(): Promise { + this.rendererReady = false + if (this.timer) clearTimeout(this.timer) + this.timer = null + if (this.restoreTimer) clearTimeout(this.restoreTimer) + this.restoreTimer = null + if (this.fullscreenTimer) clearInterval(this.fullscreenTimer) + this.fullscreenTimer = null + screen.removeListener('display-added', this.schedule) + screen.removeListener('display-removed', this.schedule) + screen.removeListener('display-metrics-changed', this.schedule) + this.controller.suspend() + return this.flush() + } +} diff --git a/src/main/windowDockingController.ts b/src/main/windowDockingController.ts new file mode 100644 index 0000000..61bf347 --- /dev/null +++ b/src/main/windowDockingController.ts @@ -0,0 +1,142 @@ +import type { WindowBounds } from '../types/popout' +import type { DockEdge, WindowDockingPreferences, WindowDockingSnapshot } from '../types/windowDocking' +import { fitFloatingBounds, normalizeWindowDocking } from '../shared/windowDocking' + +export interface DockDisplay { id: number; bounds: WindowBounds; workArea: WindowBounds } +export interface DockingHost { + supported: boolean + bounds(): WindowBounds + displays(): DockDisplay[] + primaryDisplay(): DockDisplay + matchingDisplay(bounds: WindowBounds): DockDisplay + presented(): boolean + reserve(display: DockDisplay, edge: DockEdge, height: number): void + release(): void + managed(enabled: boolean): void + restore(bounds: WindowBounds): void + closeSettings(): void + changed(snapshot: WindowDockingSnapshot): void + persist(preferences: WindowDockingPreferences): void +} + +/** Owns requested docking independently of whether a visible HWND reserves space. */ +export class WindowDockingController { + private preferences: WindowDockingPreferences + private active = false + private error: string | null = null + private updating = false + + constructor(private host: DockingHost, initial: unknown) { + this.preferences = normalizeWindowDocking(initial) + } + + get snapshot(): WindowDockingSnapshot { + return { ...structuredClone(this.preferences), enabled: this.ownsGeometry, + supported: this.host.supported, active: this.active, error: this.error } + } + + /** Also guards geometry persistence during hidden/restoring/recreated states. */ + get ownsGeometry(): boolean { return this.host.supported && this.preferences.enabled } + + private publish(persist = false): void { + if (persist) this.host.persist(structuredClone(this.preferences)) + this.host.changed(this.snapshot) + } + + setEnabled(enabled: boolean): void { + if (!this.host.supported || enabled === this.preferences.enabled) return + this.error = null + this.host.closeSettings() + if (enabled) { + const bounds = this.host.bounds() + this.preferences = { ...this.preferences, enabled: true, + floatingBounds: bounds, height: bounds.height, + displayId: this.host.matchingDisplay(bounds).id } + this.host.managed(true) + this.resume() + } else { + this.suspend() + this.preferences.enabled = false + this.host.managed(false) + this.restoreFloating() + } + this.publish(true) + } + + setEdge(edge: DockEdge): void { + this.preferences.edge = edge + if (this.ownsGeometry) this.resume() + this.publish(true) + } + + resize(height: number): void { + if (!this.ownsGeometry || !Number.isFinite(height)) return + const display = this.host.displays().find(item => item.id === this.preferences.displayId) + if (!display) { this.resume(); return } + // The shell may reduce this further to accommodate other bars. + this.preferences.height = Math.min(Math.max(100, Math.round(height)), display.bounds.height) + this.resume() + this.publish(true) + } + + suspend(): void { + this.host.closeSettings() + this.host.release() + this.active = false + this.publish() + } + + resume(): void { + if (!this.ownsGeometry || this.updating) return + const display = this.host.displays().find(item => item.id === this.preferences.displayId) + if (!display) { + this.suspend() + this.preferences.enabled = false + this.host.managed(false) + this.restoreFloating(this.host.primaryDisplay()) + this.publish(true) + return + } + this.host.managed(true) + if (!this.host.presented()) return + this.updating = true + try { + this.host.reserve(display, this.preferences.edge, this.preferences.height) + this.active = true + this.error = null + } catch (error) { + this.host.release() + this.active = false + this.preferences.enabled = false + this.host.managed(false) + this.restoreFloating() + this.error = error instanceof Error ? error.message : 'Prism could not reserve screen space.' + this.host.persist(structuredClone(this.preferences)) + } finally { + this.updating = false + this.publish() + } + } + + shellRestarted(): void { this.suspend(); this.resume() } + + undockForDrag(cursor: { x: number; y: number }): WindowBounds | undefined { + if (!this.ownsGeometry) return + const dock = this.host.bounds() + const floating = this.preferences.floatingBounds ?? dock + const grabFraction = Math.max(0, Math.min(1, (cursor.x - dock.x) / dock.width)) + this.setEnabled(false) + const display = this.host.matchingDisplay({ x: cursor.x, y: cursor.y, width: 1, height: 1 }) + const restored = fitFloatingBounds({ ...floating, + x: Math.round(cursor.x - floating.width * grabFraction), + y: Math.round(cursor.y - Math.min(24, Math.max(0, cursor.y - dock.y))), + }, display.workArea) + this.host.restore(restored) + return restored + } + + private restoreFloating(display?: DockDisplay): void { + const bounds = this.preferences.floatingBounds ?? this.host.bounds() + this.host.restore(fitFloatingBounds(bounds, (display ?? this.host.matchingDisplay(bounds)).workArea)) + } +} diff --git a/src/main/windowStateStore.ts b/src/main/windowStateStore.ts index e860c13..2cb8130 100644 --- a/src/main/windowStateStore.ts +++ b/src/main/windowStateStore.ts @@ -3,6 +3,7 @@ import { dirname } from 'node:path' import { createEmptyWindowLocalState, normalizeWindowLocalState } from '../shared/windowState' import type { WindowBounds } from '../types/popout' import type { ScopeKind } from '../types/scope' +import type { WindowDockingPreferences } from '../types/windowDocking' import type { PrismWindowLocalStateV1, WindowBackgroundState } from '../types/windowState' export class FileBackedWindowStateStore { @@ -22,6 +23,16 @@ export class FileBackedWindowStateStore { return this.state.mainAlwaysOnTop } + getDocking(): WindowDockingPreferences { + return structuredClone(this.state.docking) + } + + async setDocking(docking: WindowDockingPreferences): Promise { + await this.ensureInitialized() + this.state = normalizeWindowLocalState({ ...this.state, docking }) + await this.persistState() + } + getPopoutAlwaysOnTop(kind: ScopeKind): boolean { return this.state.popoutAlwaysOnTop[kind] === true } diff --git a/src/plugin-ui/BridgeSpectrumAnalyzer.ts b/src/plugin-ui/BridgeSpectrumAnalyzer.ts index e85acc9..e4f1d78 100644 --- a/src/plugin-ui/BridgeSpectrumAnalyzer.ts +++ b/src/plugin-ui/BridgeSpectrumAnalyzer.ts @@ -1,3 +1,5 @@ +import { emitToHost } from './juceBridge' +import type { SpectrumReferenceLevel } from '../types/spectrumReference' import type { SpectrumNativeAnalyzer } from '../renderer/audio/native' const FFT_SILENCE_DB = -100 @@ -16,6 +18,26 @@ const FFT_SILENCE_DB = -100 * through the webview. */ export class BridgeSpectrumAnalyzer implements SpectrumNativeAnalyzer { + private spectrumDataReady = false + + hasSpectrumData(): boolean { + return this.spectrumDataReady + } + + private referenceEnabled = false + private referenceLevel: SpectrumReferenceLevel = { meanSquare: 0, seconds: 0 } + private referenceFrameTime = 0 + setReferenceEnabled(enabled: boolean): void { + if (enabled === this.referenceEnabled) return + this.referenceEnabled = enabled + emitToHost('prismSpectrumReferenceConfig', { referenceEnabled: enabled }) + } + setReferenceLevel(meanSquare: number, seconds: number): void { + this.referenceLevel = { meanSquare, seconds }; this.referenceFrameTime = performance.now() + } + getReferenceLevel(): SpectrumReferenceLevel { + return performance.now() - this.referenceFrameTime < 300 ? this.referenceLevel : { meanSquare: 0, seconds: 0 } + } private fftSize = 2048 private sampleRate = 48000 private magnitudes: Float32Array @@ -30,7 +52,8 @@ export class BridgeSpectrumAnalyzer implements SpectrumNativeAnalyzer { } /** Called by the bridge whenever the host emits a new frame. */ - setMagnitudes(magnitudes: Float32Array, side?: Float32Array, channelMax?: Float32Array): void { + setMagnitudes(magnitudes: Float32Array, side?: Float32Array, channelMax?: Float32Array, hasSpectrumData = true): void { + this.spectrumDataReady = hasSpectrumData && magnitudes.length > 0 if (magnitudes.length !== this.magnitudes.length) { this.magnitudes = new Float32Array(magnitudes.length) } @@ -56,6 +79,7 @@ export class BridgeSpectrumAnalyzer implements SpectrumNativeAnalyzer { setFFTSize(size: number): void { if (size > 0 && size !== this.fftSize) { + this.spectrumDataReady = false this.fftSize = size this.magnitudes = new Float32Array(size / 2).fill(FFT_SILENCE_DB) this.sideMagnitudes = new Float32Array(size / 2).fill(FFT_SILENCE_DB) @@ -129,10 +153,12 @@ export class BridgeSpectrumAnalyzer implements SpectrumNativeAnalyzer { } binToFrequency(bin: number): number { - return (bin * this.sampleRate) / this.fftSize + return (bin * (this.referenceEnabled ? 48000 : this.sampleRate)) / this.fftSize } reset(): void { + this.spectrumDataReady = false + this.referenceLevel = { meanSquare: 0, seconds: 0 } this.magnitudes.fill(FFT_SILENCE_DB) this.sideMagnitudes.fill(FFT_SILENCE_DB) this.channelMaxMagnitudes.fill(FFT_SILENCE_DB) diff --git a/src/plugin-ui/BridgeWaterfallAnalyzer.ts b/src/plugin-ui/BridgeWaterfallAnalyzer.ts new file mode 100644 index 0000000..af257b3 --- /dev/null +++ b/src/plugin-ui/BridgeWaterfallAnalyzer.ts @@ -0,0 +1,60 @@ +import type { WaterfallFrame, WaterfallNativeAnalyzer, WaterfallNativeConfig } from '../types/waterfall' +import { DEFAULT_WATERFALL_SETTINGS } from '../types/waterfall' +import { emitToHost, type WaterfallPluginFrame } from './juceBridge' + +export interface WaterfallFrameRequest { + revision: number + ridges: number + columns: number + config: WaterfallNativeConfig +} + +/** Native history stays in the plugin; the webview retains only its latest plot. */ +export class BridgeWaterfallAnalyzer implements WaterfallNativeAnalyzer { + private revision = 0 + private ridges = 32 + private columns = 256 + private config: WaterfallNativeConfig = { + ...DEFAULT_WATERFALL_SETTINGS, sampleRate: 48000, minFrequency: 10, maxFrequency: 24000, + } + private frame: WaterfallFrame | null = null + + constructor(private readonly send = (payload: unknown): void => emitToHost('prismWaterfallConfig', payload)) {} + + configure(config: WaterfallNativeConfig): void { + this.config = { ...config } + this.send({ revision: ++this.revision, config: this.config }) + } + + processStereo(_left: Float32Array, _right: Float32Array): void {} + isAvailable(): boolean { return true } + + pushFrame(frame: WaterfallPluginFrame): void { + // Ignore in-flight responses to earlier settings, viewport sizes or resets. + if (frame.revision === this.revision) this.frame = frame + } + + getFrame(ridges: number, columns: number): WaterfallFrame { + const nextRidges = Math.max(2, Math.min(64, Math.round(ridges))) + const nextColumns = Math.max(2, Math.min(512, Math.round(columns))) + if (nextRidges !== this.ridges || nextColumns !== this.columns) { + this.ridges = nextRidges + this.columns = nextColumns + this.send({ revision: ++this.revision, ridges: this.ridges, columns: this.columns }) + } + // Retain the previous plot during a resize, including while audio is held. + return this.frame ?? { + columns: this.columns, audioSeconds: 0, + levels: new Float32Array(0), ages: new Float32Array(0), frequencies: new Float32Array(0), + } + } + + reset(): void { + this.frame = null + this.send({ revision: ++this.revision, reset: true }) + } + + getRequest(): WaterfallFrameRequest { + return { revision: this.revision, ridges: this.ridges, columns: this.columns, config: { ...this.config } } + } +} diff --git a/src/plugin-ui/NativeFrameScheduler.ts b/src/plugin-ui/NativeFrameScheduler.ts new file mode 100644 index 0000000..c638bbc --- /dev/null +++ b/src/plugin-ui/NativeFrameScheduler.ts @@ -0,0 +1,15 @@ +import { FrameScheduler, type FrameSchedulerCallback } from '../renderer/visualizers/frameScheduler' + +/** JUCE supplies the display clock; embedded WebKit may suspend browser RAF. */ +export class NativeFrameScheduler extends FrameScheduler { + private readonly nativeCallbacks = new Set() + + override subscribe(callback: FrameSchedulerCallback): () => void { + this.nativeCallbacks.add(callback) + return () => { this.nativeCallbacks.delete(callback) } + } + + dispatchFrame(): void { + for (const callback of [...this.nativeCallbacks]) callback() + } +} diff --git a/src/plugin-ui/PluginWebViewDataSource.ts b/src/plugin-ui/PluginWebViewDataSource.ts index 11f3eae..d506c3a 100644 --- a/src/plugin-ui/PluginWebViewDataSource.ts +++ b/src/plugin-ui/PluginWebViewDataSource.ts @@ -20,6 +20,7 @@ export class PluginWebViewDataSource implements SpectrumAnalyzerDataSource { sampleRate: 48000, channelCount: 2, capturing: false, + suspended: false, backendKind: null, } @@ -84,10 +85,18 @@ export class PluginWebViewDataSource implements SpectrumAnalyzerDataSource { return [] } + getPendingWaterfallSamples(): SpectrumStereoChunk[] { + return [] + } + getSampleRate(): number { return this.sessionState.sampleRate } + getBackendKind(): null { + return null + } + isPlaying(): boolean { return this.sessionState.capturing } diff --git a/src/plugin-ui/ScopeApp.tsx b/src/plugin-ui/ScopeApp.tsx index 8540427..408729f 100644 --- a/src/plugin-ui/ScopeApp.tsx +++ b/src/plugin-ui/ScopeApp.tsx @@ -1,3 +1,5 @@ +import { SpectrumReferenceProvider } from '../renderer/components/SpectrumReference' +import { pluginReferenceTransport } from './referenceTransport' import { useEffect, useRef, useState, type CSSProperties, type JSX, type ReactNode } from 'react' import type { ScopeKind } from '../types/scope' import type { ScopeSettings } from '../types/settings' @@ -49,7 +51,7 @@ export default function ScopeApp({ kind, renderScope }: Sco ? undefined : ({ '--spectrum-viewport-height': `${lockedViewportHeight}px` } as CSSProperties) - return ( + const content = (
{renderScope(settings, resolvedTheme)} @@ -76,4 +78,7 @@ export default function ScopeApp({ kind, renderScope }: Sco )}
) + return kind === 'spectrum' ? handleUpdate({ reference } as unknown as Partial)}>{content} : content } diff --git a/src/plugin-ui/SpectrumScope.tsx b/src/plugin-ui/SpectrumScope.tsx index dc914f2..3a82fd5 100644 --- a/src/plugin-ui/SpectrumScope.tsx +++ b/src/plugin-ui/SpectrumScope.tsx @@ -1,3 +1,4 @@ +import { ReferenceImportStatus, useReferenceDrop, useSpectrumReference } from '../renderer/components/SpectrumReference' import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type JSX } from 'react' import { SpectrumAnalyzer } from '../renderer/visualizers/SpectrumAnalyzer' import type { ScopeSettings } from '../types/settings' @@ -34,6 +35,8 @@ export default function SpectrumScope({ settings, theme, }: SpectrumScopeProps): JSX.Element { + const referenceController = useSpectrumReference() + const referenceDrop = useReferenceDrop() const containerRef = useRef(null) const canvasRef = useRef(null) const analyzerRef = useRef(null) @@ -106,6 +109,11 @@ export default function SpectrumScope({ }) }, [settings, theme]) + useEffect(() => { + analyzerRef.current?.setOptions({ referencePreview: referenceController?.state.preview ?? null, + referenceImporting: referenceController?.busy ?? false, onReferenceLevel: referenceController?.reportLevel ?? (() => {}) }) + }, [referenceController?.state.preview, referenceController?.busy, referenceController?.reportLevel]) + useLayoutEffect(() => { applySizeRef.current?.() }, [settings.rotation]) @@ -142,14 +150,17 @@ export default function SpectrumScope({ return (
+ + {referenceDrop.dragging &&
Drop audio to use as reference
} - {formatSpectrumPeakDbfs(peak.dbfs)} + {peak.deltaDb !== undefined ? `${peak.deltaDb >= 0 ? '+' : ''}${peak.deltaDb.toFixed(1)} dB relative` : formatSpectrumPeakDbfs(peak.dbfs)} / {formatSpectrumPeakFrequency(peak.frequencyHz)} / diff --git a/src/plugin-ui/WaterfallScope.tsx b/src/plugin-ui/WaterfallScope.tsx new file mode 100644 index 0000000..5eb9772 --- /dev/null +++ b/src/plugin-ui/WaterfallScope.tsx @@ -0,0 +1,54 @@ +import { useEffect, useRef, type JSX } from 'react' +import { Waterfall } from '../renderer/visualizers/Waterfall' +import type { WaterfallSettings } from '../types/waterfall' +import type { ResolvedWaterfallTheme } from '../types/theme' +import type { BridgeWaterfallAnalyzer } from './BridgeWaterfallAnalyzer' +import type { PluginWebViewDataSource } from './PluginWebViewDataSource' +import { waterfallSettingsToOptions } from './waterfallOptions' +import { applyPluginScopeCanvasLayout } from './scopeCanvasLayout' +import type { NativeFrameScheduler } from './NativeFrameScheduler' + +interface Props { + dataSource: PluginWebViewDataSource + nativeAnalyzer: BridgeWaterfallAnalyzer + frameScheduler: NativeFrameScheduler + settings: WaterfallSettings + theme: ResolvedWaterfallTheme +} + +export default function WaterfallScope({ dataSource, nativeAnalyzer, frameScheduler, settings, theme }: Props): JSX.Element { + const containerRef = useRef(null) + const canvasRef = useRef(null) + const vizRef = useRef(null) + + useEffect(() => { + const container = containerRef.current, canvas = canvasRef.current + if (!container || !canvas) return + const viz = new Waterfall(canvas, { + ...waterfallSettingsToOptions(settings, theme), dataSource, nativeAnalyzer, frameScheduler, + }) + vizRef.current = viz + const resize = (): void => { + if (applyPluginScopeCanvasLayout(container, canvas, 0).changed) viz.resize() + } + resize() + viz.start() + const observer = new ResizeObserver(resize) + observer.observe(container) + return () => { + observer.disconnect() + viz.dispose() + vizRef.current = null + } + // Settings and theme updates use setOptions and preserve native history. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataSource, nativeAnalyzer, frameScheduler]) + + useEffect(() => { + vizRef.current?.setOptions(waterfallSettingsToOptions(settings, theme)) + }, [settings, theme]) + + return
+ +
+} diff --git a/src/plugin-ui/juceBridge.ts b/src/plugin-ui/juceBridge.ts index af4dd2b..dfaceaa 100644 --- a/src/plugin-ui/juceBridge.ts +++ b/src/plugin-ui/juceBridge.ts @@ -1,3 +1,7 @@ +import type { WaterfallFrame } from '../types/waterfall' +import type { WaterfallFrameRequest } from './BridgeWaterfallAnalyzer' +import { frequencyAtNormalizedPosition } from '../types/frequencyScale' + /** * Bridge between the JUCE 8 plugin host (C++) and this webview UI. * @@ -10,6 +14,10 @@ */ export interface SpectrumFrame { + /** Absent in legacy frames, which are treated as ready on receipt. */ + hasSpectrumData?: boolean + referenceMeanSquare?: number + referenceSeconds?: number /** Host sample rate in Hz. */ sampleRate: number /** Mid (mono) magnitudes in dB, length = fftSize/2. */ @@ -21,6 +29,9 @@ export interface SpectrumFrame { } interface SpectrumFramePayload { + hasSpectrumData?: boolean + referenceMeanSquare?: number + referenceSeconds?: number sampleRate?: number magnitudes?: string side?: string @@ -103,15 +114,101 @@ export function base64ToFloat32Array(b64: string): Float32Array { return new Float32Array(bytes.buffer, 0, byteLength >> 2) } +export interface WaterfallPluginFrame extends WaterfallFrame { + sampleRate: number + revision: number +} + +export function decodeWaterfallFrame(payload: unknown): WaterfallPluginFrame | null { + if (!payload || typeof payload !== 'object') return null + const p = payload as Record + const { columns, audioSeconds, sampleRate, revision } = p + if (typeof columns !== 'number' || !Number.isInteger(columns) || columns < 2 || columns > 512 + || typeof audioSeconds !== 'number' || !Number.isFinite(audioSeconds) || audioSeconds < 0 + || typeof sampleRate !== 'number' || !Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 384000 + || typeof revision !== 'number' || !Number.isSafeInteger(revision) || revision < 0) return null + const decode = (value: unknown, limit: number): Float32Array | null => { + if (typeof value !== 'string' || value.length > Math.ceil(limit * 4 / 3) * 4) return null + try { + const binary = atob(value) + if (binary.length % 4 !== 0) return null + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)) + const result = new Float32Array(bytes.buffer) + return result.length <= limit && result.every(Number.isFinite) ? result : null + } catch { return null } + } + const ages = decode(p.ages, 64), frequencies = decode(p.frequencies, columns) + if (!ages || !frequencies || frequencies.length !== columns + || ages.some((age, i) => age < 0 || age > 30.1 || (i > 0 && age < ages[i - 1])) + || frequencies.some((hz, i) => hz <= 0 || (i > 0 && hz < frequencies[i - 1]))) return null + const levels = decode(p.levels, ages.length * columns) + if (!levels || levels.length !== ages.length * columns) return null + return { levels, ages, frequencies, columns, audioSeconds, sampleRate, revision } +} + +export function connectWaterfallBridge(handlers: { + onFrame: (frame: WaterfallPluginFrame) => void + getRequest: () => WaterfallFrameRequest +}): () => void { + let disposed = false + let listenerId: number | null = null + let mockRaf: number | null = null + void ensureBackend().then((backend) => { + if (disposed) return + if (backend) { + listenerId = backend.addEventListener('waterfallFrame', (payload) => { + const frame = decodeWaterfallFrame(payload) + if (frame) handlers.onFrame(frame) + }) + return + } + console.warn('[prism-plugin] no JUCE host — using synthetic waterfall (browser dev mode)') + const started = performance.now() + const tick = (): void => { + if (disposed) return + const { config, columns, ridges, revision } = handlers.getRequest() + const audioSeconds = (performance.now() - started) / 1000 + const spacing = config.historySeconds / (ridges - 1) + const anchor = Math.floor(audioSeconds / spacing) * spacing + const ages = new Float32Array(Array.from({ length: ridges }, (_, i) => + i === 0 ? 0 : audioSeconds - anchor + (i - 1) * spacing).filter((age) => age <= audioSeconds && age <= config.historySeconds)) + const frequencies = Float32Array.from({ length: columns }, (_, x) => + frequencyAtNormalizedPosition(x / (columns - 1), config.minFrequency, config.maxFrequency, config.scaleMode)) + const levels = new Float32Array(ages.length * columns) + for (let row = 0; row < ages.length; row++) { + const time = audioSeconds - ages[row] + for (let x = 0; x < columns; x++) { + const octave = Math.log2(frequencies[x] / 80) + levels[row * columns + x] = -85 + + 46 * Math.exp(-octave * octave * 1.8) * (0.8 + 0.2 * Math.sin(time * 3)) + + 30 * Math.exp(-Math.pow((octave - 3.8 - Math.sin(time) * 0.15) * 3, 2)) + + 22 * Math.exp(-Math.pow((octave - 6) * 2, 2)) * (0.6 + 0.4 * Math.sin(time * 2)) + } + } + handlers.onFrame({ levels, ages, frequencies, columns, audioSeconds, sampleRate: config.sampleRate, revision }) + mockRaf = requestAnimationFrame(tick) + } + mockRaf = requestAnimationFrame(tick) + }) + return () => { + disposed = true + if (mockRaf !== null) cancelAnimationFrame(mockRaf) + if (listenerId !== null) window.__JUCE__?.backend?.removeEventListener?.(listenerId) + } +} + export function decodeSpectrumFrame(payload: unknown): SpectrumFrame | null { if (typeof payload !== 'object' || payload === null) return null - const { sampleRate, magnitudes, side, channelMax } = payload as SpectrumFramePayload + const { sampleRate, magnitudes, side, channelMax, referenceMeanSquare, referenceSeconds, hasSpectrumData } = payload as SpectrumFramePayload if (typeof magnitudes !== 'string' || magnitudes.length === 0) return null const decodedMagnitudes = base64ToFloat32Array(magnitudes) const decodedChannelMax = typeof channelMax === 'string' ? base64ToFloat32Array(channelMax) : new Float32Array(0) return { + hasSpectrumData: typeof hasSpectrumData === 'boolean' ? hasSpectrumData : undefined, + referenceMeanSquare: referenceMeanSquare ?? 0, + referenceSeconds: referenceSeconds ?? 0, sampleRate: typeof sampleRate === 'number' && sampleRate > 0 ? sampleRate : 48000, magnitudes: decodedMagnitudes, side: typeof side === 'string' ? base64ToFloat32Array(side) : new Float32Array(0), diff --git a/src/plugin-ui/main.tsx b/src/plugin-ui/main.tsx index 7b2cb3d..e65942f 100644 --- a/src/plugin-ui/main.tsx +++ b/src/plugin-ui/main.tsx @@ -13,6 +13,7 @@ import LUFSMeterScope from './LUFSMeterScope' import VectorscopeScope from './VectorscopeScope' import SpectrogramScope from './SpectrogramScope' import WaveformScope from './WaveformScope' +import WaterfallScope from './WaterfallScope' import { BridgeSpectrumAnalyzer } from './BridgeSpectrumAnalyzer' import { BridgeOscilloscopeAnalyzer } from './BridgeOscilloscopeAnalyzer' import { BridgeVUMeterAnalyzer } from './BridgeVUMeterAnalyzer' @@ -20,8 +21,10 @@ import { BridgeLUFSMeterAnalyzer } from './BridgeLUFSMeterAnalyzer' import { BridgeVectorscopeAnalyzer } from './BridgeVectorscopeAnalyzer' import { BridgeSpectrogramAnalyzer } from './BridgeSpectrogramAnalyzer' import { BridgeWaveformAnalyzer } from './BridgeWaveformAnalyzer' +import { BridgeWaterfallAnalyzer } from './BridgeWaterfallAnalyzer' import { PluginWebViewDataSource } from './PluginWebViewDataSource' -import { connectOscilloscopeBridge, connectSpectrumBridge, connectVUMeterBridge, connectLUFSMeterBridge, connectVectorscopeBridge, connectSpectrogramBridge, connectWaveformBridge } from './juceBridge' +import { NativeFrameScheduler } from './NativeFrameScheduler' +import { connectOscilloscopeBridge, connectSpectrumBridge, connectVUMeterBridge, connectLUFSMeterBridge, connectVectorscopeBridge, connectSpectrogramBridge, connectWaveformBridge, connectWaterfallBridge } from './juceBridge' // The C++ plugin tells us which scope it is via JUCE initialisation data. // JUCE stores each value as an array (e.g. prismScope = ["oscilloscope"]). @@ -36,6 +39,23 @@ function getScopeKind(): string { const dataSource = new PluginWebViewDataSource() function buildApp(): JSX.Element { + if (getScopeKind() === 'waterfall') { + const analyzer = new BridgeWaterfallAnalyzer() + const frameScheduler = new NativeFrameScheduler() + connectWaterfallBridge({ + onFrame: (frame) => { + dataSource.setSampleRate(frame.sampleRate) + analyzer.pushFrame(frame) + dataSource.setPlaying(true) + frameScheduler.dispatchFrame() + }, + getRequest: () => analyzer.getRequest(), + }) + return ( + + )} /> + } + if (getScopeKind() === 'waveform') { const analyzer = new BridgeWaveformAnalyzer() connectWaveformBridge({ @@ -188,7 +208,8 @@ function buildApp(): JSX.Element { const analyzer = new BridgeSpectrumAnalyzer(2048) connectSpectrumBridge({ onFrame: (frame) => { - analyzer.setMagnitudes(frame.magnitudes, frame.side, frame.channelMax) + analyzer.setMagnitudes(frame.magnitudes, frame.side, frame.channelMax, frame.hasSpectrumData) + analyzer.setReferenceLevel(frame.referenceMeanSquare ?? 0, frame.referenceSeconds ?? 0) dataSource.setSampleRate(frame.sampleRate) dataSource.setPlaying(true) }, diff --git a/src/plugin-ui/referenceTransport.ts b/src/plugin-ui/referenceTransport.ts new file mode 100644 index 0000000..e9cfd08 --- /dev/null +++ b/src/plugin-ui/referenceTransport.ts @@ -0,0 +1,49 @@ +import { emitToHost, onHostEvent } from './juceBridge' +import { EMPTY_REFERENCE_IMPORT, type SpectrumReferenceImportState, type SpectrumReferenceTransport } from '../types/spectrumReference' + +let uploadGeneration = 0 +async function request(action: string, payload: Record = {}): Promise> { + if (!window.__JUCE__?.backend) throw new Error('Reference tracks require the native Spectrum plugin.') + const requestId = crypto.randomUUID() + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { unsubscribe(); reject(new Error('The plugin did not respond to the reference request.')) }, 15000) + const unsubscribe = onHostEvent('prismReferenceResponse', raw => { + const response = raw as Record + if (response.requestId !== requestId) return + clearTimeout(timeout); unsubscribe() + if (response.ok) resolve(response) + else reject(new Error(String(response.error ?? 'Reference import failed.'))) + }) + emitToHost('prismReferenceCommand', { ...payload, action, requestId }) + }) +} + +export const pluginReferenceTransport: SpectrumReferenceTransport = { + subscribe: callback => onHostEvent('prismReferenceState', value => callback(value as SpectrumReferenceImportState)), + getState: async () => { + if (!window.__JUCE__?.backend) return { ...EMPTY_REFERENCE_IMPORT } + return (await request('getState')).state as SpectrumReferenceImportState + }, + choose: async () => { uploadGeneration++; await request('choose') }, + cancel: async () => { uploadGeneration++; await request('cancel') }, + importFile: async file => { + const generation = ++uploadGeneration + let uploadId: unknown + try { + uploadId = (await request('beginUpload', { name: file.name, size: file.size })).uploadId + for (let offset = 0; offset < file.size; offset += 262144) { + if (generation !== uploadGeneration) return + const bytes = new Uint8Array(await file.slice(offset, offset + 262144).arrayBuffer()) + if (generation !== uploadGeneration) return + let binary = '' + for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode(...bytes.subarray(i, i + 8192)) + await request('appendUpload', { uploadId, offset, data: btoa(binary) }) + } + if (generation === uploadGeneration) await request('finishUpload', { uploadId }) + } catch (error) { + if (generation !== uploadGeneration) return + if (uploadId) await request('cancelUpload', { uploadId }).catch(() => {}) + throw error + } + }, +} diff --git a/src/plugin-ui/spectrumOptions.ts b/src/plugin-ui/spectrumOptions.ts index b520092..13c37d1 100644 --- a/src/plugin-ui/spectrumOptions.ts +++ b/src/plugin-ui/spectrumOptions.ts @@ -14,8 +14,10 @@ export function spectrumSettingsToOptions( ): SpectrumAnalyzerOptions { const range = nominalFrequencyBoundsForRange(settings.frequencyRangeMode) return { + reference: settings.reference ?? null, lineColor: theme.line, secondaryLineColor: theme.sideLine, + referenceLineColor: theme.referenceLine, gradientColors: theme.fillGradient, heatColors: theme.heatColors, heatBaseColor: theme.heatBase, diff --git a/src/plugin-ui/styles.css b/src/plugin-ui/styles.css index 8632e62..aea6275 100644 --- a/src/plugin-ui/styles.css +++ b/src/plugin-ui/styles.css @@ -1,3 +1,4 @@ +@import "../renderer/styles/spectrumReference.css"; :root { color-scheme: dark; } diff --git a/src/plugin-ui/useScopeHostSync.ts b/src/plugin-ui/useScopeHostSync.ts index 2cf9776..a7d786b 100644 --- a/src/plugin-ui/useScopeHostSync.ts +++ b/src/plugin-ui/useScopeHostSync.ts @@ -29,7 +29,11 @@ function resolveAppScopeSettings(kind: K, profileJson: stri try { const parsed = JSON.parse(profileJson) as { scopeSettings?: Record } const scoped = parsed?.scopeSettings?.[kind] - if (scoped) return mergeScopeSettings(kind, scoped) + if (scoped) { + const normalized = mergeScopeSettings(kind, scoped) + if (kind === 'spectrum') (normalized as ScopeSettings['spectrum']).reference = null + return normalized + } } catch { // fall through } diff --git a/src/plugin-ui/waterfallOptions.ts b/src/plugin-ui/waterfallOptions.ts new file mode 100644 index 0000000..9c0b323 --- /dev/null +++ b/src/plugin-ui/waterfallOptions.ts @@ -0,0 +1,10 @@ +import type { WaterfallSettings } from '../types/waterfall' +import type { ResolvedWaterfallTheme } from '../types/theme' +import type { WaterfallOptions } from '../renderer/visualizers/Waterfall' + +export function waterfallSettingsToOptions(settings: WaterfallSettings, theme: ResolvedWaterfallTheme): WaterfallOptions { + return { + ...settings, lineColor: theme.line, backgroundColor: theme.background, + gridColor: theme.guides, labelColor: theme.labels, heatColors: theme.heatColors, + } +} diff --git a/src/preload/captureSupport.ts b/src/preload/captureSupport.ts index 7c6d7e1..a1cdcf2 100644 --- a/src/preload/captureSupport.ts +++ b/src/preload/captureSupport.ts @@ -25,6 +25,7 @@ export function resolveNativeCaptureSupport( kind: 'native-macos', available: support.available, reason: support.reason, + channelRoutingAvailable: typeof macosCapture.setChannelRouting === 'function', } } @@ -39,6 +40,7 @@ export function resolveNativeCaptureSupport( kind: 'native-windows', available: support.available, reason: support.reason, + channelRoutingAvailable: typeof windowsCapture.setChannelRouting === 'function', } } @@ -52,6 +54,7 @@ export function resolveNativeCaptureSupport( kind: 'native-linux', available: support.available, reason: support.reason, + channelRoutingAvailable: typeof linuxCapture.setChannelRouting === 'function', } } @@ -59,12 +62,19 @@ export function getCaptureBackendSupport( platform: string, nativeCaptureAPI: NativeCaptureAPI | null, ): CaptureBackendSupport { + const nativeDeviceInputSupport = nativeCaptureAPI?.deviceInputCapture?.getSupport() return { nativeBackend: resolveNativeCaptureSupport(platform, nativeCaptureAPI), deviceInput: { kind: 'device-input', available: true, reason: null, + channelRoutingAvailable: nativeDeviceInputSupport?.available === true, + }, + dawBridge: { + kind: 'daw-bridge', + available: false, + reason: 'The DAW bridge listener is still starting.', }, } } diff --git a/src/preload/index.ts b/src/preload/index.ts index aea8c8f..4d2ee44 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,4 +1,6 @@ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer, webUtils } from 'electron' +import type { WindowDockingSnapshot } from '../types/windowDocking' +import type { SpectrumReferenceImportState } from '../types/spectrumReference' import type { AppBuildInfo } from '../types/appBuildInfo' import type { AudioClipDragPayload } from '../types/audioClip' import type { CaptureBackendSupport } from '../types/capture' @@ -24,12 +26,14 @@ import type { ProfileLibrarySnapshot, } from '../types/profile' import type { ScopeKind } from '../types/scope' +import type { LinkedAnalysisMessage } from '../types/analysis' import type { LegacyThemeMigrationPayload, LegacyThemeMigrationResult, + ResolvedInterfaceTheme, ThemeLibrarySnapshot, } from '../types/theme' -import type { DialogOptions, DialogResult } from '../types/dialog' +import type { DialogConfig, DialogLayout, DialogOptions, DialogResult } from '../types/dialog' import type { UpdateCheckResult } from '../types/updates' import type { WindowCapabilities } from '../types/windowCapabilities' import type { ResizeDirection } from '../types/windowResize' @@ -43,6 +47,10 @@ import type { import type { VisualizerDSP } from '../renderer/audio/native/visualizer-dsp' import { resolveWindowCapabilities } from '../shared/windowCapabilities' import { getCaptureBackendSupport } from './captureSupport' +import type { + DawBridgeAudioBatch, + DawBridgeSnapshot, +} from '../types/dawBridge' type NativeAddonModule = VisualizerDSP & NativeCaptureAPI const windowCapabilities: WindowCapabilities = resolveWindowCapabilities({ @@ -54,9 +62,36 @@ const windowCapabilities: WindowCapabilities = resolveWindowCapabilities({ // Expose Electron API to renderer contextBridge.exposeInMainWorld('electronAPI', { + docking: { + get: (): Promise => ipcRenderer.invoke('window:docking-get'), + setEnabled: (enabled: boolean): Promise => ipcRenderer.invoke('window:docking-set', enabled), + onChanged: (callback: (snapshot: WindowDockingSnapshot) => void) => { + const listener = (_event: unknown, snapshot: WindowDockingSnapshot): void => callback(snapshot) + ipcRenderer.on('window:docking-changed', listener) + return () => ipcRenderer.removeListener('window:docking-changed', listener) + }, + showSettings: (height: number): Promise => ipcRenderer.invoke('window:docked-settings-show', height), + closeSettings: (): void => ipcRenderer.send('window:docked-settings-close'), + onSettingsClosed: (callback: () => void) => { + ipcRenderer.on('window:docked-settings-closed', callback) + return () => ipcRenderer.removeListener('window:docked-settings-closed', callback) + }, + }, + referenceTracks: { + getState: () => ipcRenderer.invoke('reference-tracks:get-state') as Promise, + choose: () => ipcRenderer.invoke('reference-tracks:choose') as Promise, + importFile: (file: File) => ipcRenderer.invoke('reference-tracks:import', webUtils.getPathForFile(file)) as Promise, + cancel: () => ipcRenderer.invoke('reference-tracks:cancel') as Promise, + subscribe: (callback: (state: SpectrumReferenceImportState) => void) => { + const handler = (_event: Electron.IpcRendererEvent, state: SpectrumReferenceImportState): void => callback(state) + ipcRenderer.on('reference-tracks:state', handler) + return () => ipcRenderer.removeListener('reference-tracks:state', handler) + }, + }, platform: process.platform, windowCapabilities, getAppBuildInfo: () => ipcRenderer.invoke('app:get-build-info') as Promise, + requestMicrophoneAccess: () => ipcRenderer.invoke('audio:request-microphone-access') as Promise, minimize: () => ipcRenderer.send('window:minimize'), close: () => ipcRenderer.send('window:close'), desktopIntegration: { @@ -103,7 +138,35 @@ contextBridge.exposeInMainWorld('electronAPI', { getWindowBackground: () => ipcRenderer.invoke('window:get-background') as Promise, setWindowBackground: (state: WindowBackgroundState) => ipcRenderer.invoke('window:set-background', state) as Promise, isCursorInsideWindow: () => ipcRenderer.invoke('window:is-cursor-inside') as Promise, - getCaptureBackendSupport: async () => getCaptureBackendSupport(process.platform, nativeCaptureAPI) as CaptureBackendSupport, + getCaptureBackendSupport: async () => { + const nativeSupport = getCaptureBackendSupport(process.platform, nativeCaptureAPI) + const bridgeSnapshot = await ipcRenderer.invoke('daw-bridge:get-snapshot') as DawBridgeSnapshot + return { + ...nativeSupport, + dawBridge: { + kind: 'daw-bridge', + available: bridgeSnapshot.available, + reason: bridgeSnapshot.reason, + }, + } as CaptureBackendSupport + }, + dawBridge: { + getSnapshot: () => ipcRenderer.invoke('daw-bridge:get-snapshot') as Promise, + selectSource: (sourceId: string | null) => ipcRenderer.invoke( + 'daw-bridge:select-source', + sourceId, + ) as Promise, + onSnapshot: (callback: (snapshot: DawBridgeSnapshot) => void) => { + const handler = (_event: Electron.IpcRendererEvent, snapshot: DawBridgeSnapshot): void => callback(snapshot) + ipcRenderer.on('daw-bridge:snapshot', handler) + return () => ipcRenderer.removeListener('daw-bridge:snapshot', handler) + }, + onAudioBatch: (callback: (batch: DawBridgeAudioBatch) => void) => { + const handler = (_event: Electron.IpcRendererEvent, batch: DawBridgeAudioBatch): void => callback(batch) + ipcRenderer.on('daw-bridge:audio-batch', handler) + return () => ipcRenderer.removeListener('daw-bridge:audio-batch', handler) + }, + }, audioClips: { startDrag: (payload: AudioClipDragPayload) => ipcRenderer.send('audio-clips:start-drag', payload), revealFolder: () => ipcRenderer.invoke('audio-clips:reveal-folder') as Promise, @@ -167,6 +230,12 @@ contextBridge.exposeInMainWorld('electronAPI', { notifyScopePopoutReady: (kind: ScopeKind) => ipcRenderer.send('scope-popout:ready', kind), requestScopePopIn: (kind: ScopeKind) => ipcRenderer.send('scope-popout:request-pop-in', kind), sendScopePopoutSettingsUpdate: (kind: ScopeKind, partial: unknown) => ipcRenderer.send('scope-popout:settings-update', kind, partial), + sendLinkedAnalysisMessage: (message: LinkedAnalysisMessage) => ipcRenderer.send('linked-analysis:update', message), + onLinkedAnalysisMessage: (callback: (message: LinkedAnalysisMessage) => void) => { + const handler = (_event: Electron.IpcRendererEvent, message: LinkedAnalysisMessage): void => callback(message) + ipcRenderer.on('linked-analysis:update', handler) + return () => ipcRenderer.removeListener('linked-analysis:update', handler) + }, onAlwaysOnTopChanged: (callback: (isOnTop: boolean) => void) => { const handler = (_event: Electron.IpcRendererEvent, isOnTop: boolean): void => callback(isOnTop) ipcRenderer.on('window:always-on-top-changed', handler) @@ -278,11 +347,13 @@ contextBridge.exposeInMainWorld('electronAPI', { return () => ipcRenderer.removeListener('scope-popout:session', handler) }, showDialog: (options: DialogOptions) => ipcRenderer.invoke('dialog:show', options) as Promise, - onDialogConfig: (callback: (options: DialogOptions) => void) => { - const handler = (_event: Electron.IpcRendererEvent, options: DialogOptions): void => callback(options) - ipcRenderer.on('dialog:config', handler) - return () => ipcRenderer.removeListener('dialog:config', handler) + getDialogConfig: () => ipcRenderer.invoke('dialog:get-config') as Promise, + onDialogThemeChanged: (callback: (theme: ResolvedInterfaceTheme) => void) => { + const handler = (_event: Electron.IpcRendererEvent, theme: ResolvedInterfaceTheme): void => callback(theme) + ipcRenderer.on('dialog:theme-changed', handler) + return () => ipcRenderer.removeListener('dialog:theme-changed', handler) }, + reportDialogLayout: (layout: DialogLayout) => ipcRenderer.send('dialog:layout-ready', layout), sendDialogResult: (result: DialogResult) => ipcRenderer.send('dialog:result', result), }) @@ -301,6 +372,7 @@ try { const visualizerAPI = nativeAddonModule ? { oscilloscope: nativeAddonModule.oscilloscope, + waterfall: nativeAddonModule.waterfall, spectrum: nativeAddonModule.spectrum, spectrogram: nativeAddonModule.spectrogram, vectorscope: nativeAddonModule.vectorscope, @@ -315,6 +387,7 @@ const nativeCaptureAPI = nativeAddonModule macosCapture: nativeAddonModule.macosCapture, windowsCapture: nativeAddonModule.windowsCapture, linuxCapture: nativeAddonModule.linuxCapture, + deviceInputCapture: nativeAddonModule.deviceInputCapture, } : null diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 66ab258..d5285ab 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -6,6 +6,8 @@ import BottomBar from './components/BottomBar' import ScopePopoutBridge from './components/ScopePopoutBridge' import AppBanner from './components/AppBanner' import WindowResizeOverlay from './components/WindowResizeOverlay' +import DockedSettingsPortal from './components/DockedSettingsPortal' +import { useWindowDockingStore } from './stores/windowDockingStore' import TrayControlBridge from './components/TrayControlBridge' import { resolveMainWindowSettingsHeight } from './mainWindowSettings' import { useSettingsStore } from './stores/settingsStore' @@ -17,6 +19,7 @@ import { useWindowBackgroundStore } from './stores/windowBackgroundStore' import { useUpdateStore } from './stores/updateStore' import { useDesktopIntegrationStore } from './stores/desktopIntegrationStore' import { getRendererWindowCapabilities } from './windowCapabilities' +import { useLinkedAnalysis } from './useLinkedAnalysis' export default function App(): JSX.Element { const [toolbarVisible, setToolbarVisible] = useState(false) @@ -40,18 +43,36 @@ export default function App(): JSX.Element { const scopeOrder = useSettingsStore((s) => s.scopeOrder) const hiddenScopes = useSettingsStore((s) => s.hiddenScopes) const scopePopouts = useSettingsStore((s) => s.scopePopouts) + const linkedAnalysisEnabled = useSettingsStore((s) => s.analysisSettings.linkedAnalysis) const settingsOpen = useUiStore((s) => s.settingsOpen) const toggleSettings = useUiStore((s) => s.toggleSettings) const setSettingsOpen = useUiStore((s) => s.setSettingsOpen) const showBanner = useUiStore((s) => s.showBanner) const initializeWindowBackground = useWindowBackgroundStore((s) => s.initialize) const windowBackgroundMode = useWindowBackgroundStore((s) => s.effective.mode) - const useNativeDragRegions = getRendererWindowCapabilities().useNativeDragRegions + const docking = useWindowDockingStore() + const useNativeDragRegions = getRendererWindowCapabilities().useNativeDragRegions && !docking.enabled const initializeDesktopIntegration = useDesktopIntegrationStore((s) => s.initialize) const applyDesktopIntegrationSnapshot = useDesktopIntegrationStore((s) => s.applySnapshot) const isNowPlayingVisible = !hiddenScopes.has('nowPlaying') && (scopeOrder.includes('nowPlaying') || scopePopouts.nowPlaying?.poppedOut === true) + const linkedAnalysis = useLinkedAnalysis(linkedAnalysisEnabled) + + useEffect(() => { + let changed = false + let disposed = false + const unsubscribe = window.electronAPI.docking.onChanged(snapshot => { + changed = true + useWindowDockingStore.setState(snapshot) + if (snapshot.error) showBanner({ tone: 'error', message: snapshot.error, actions: [] }) + }) + void window.electronAPI.docking.get().then(snapshot => { + if (!disposed && !changed) useWindowDockingStore.setState(snapshot) + }) + const unsubscribeClosed = window.electronAPI.docking.onSettingsClosed(() => setSettingsOpen(false)) + return () => { disposed = true; unsubscribe(); unsubscribeClosed() } + }, [setSettingsOpen, showBanner]) // Auto-capture on launch useEffect(() => { @@ -184,8 +205,8 @@ export default function App(): JSX.Element { const settingsVisible = settingsOpen && settingsHeight > 0 useLayoutEffect(() => { - window.electronAPI.setSettingsHeight(settingsHeight) - }, [settingsHeight]) + window.electronAPI.setSettingsHeight(docking.enabled ? 0 : settingsHeight) + }, [docking.enabled, settingsHeight]) const showToolbar = useCallback(() => { if (hideTimeoutRef.current) { @@ -277,12 +298,8 @@ export default function App(): JSX.Element { }, [useNativeDragRegions]) const handleAltDragEnd = useCallback(() => { - if (useNativeDragRegions) { - return - } - window.electronAPI.stopWindowMove() - }, [useNativeDragRegions]) + }, []) useEffect(() => { return () => { @@ -300,7 +317,7 @@ export default function App(): JSX.Element { onMouseMove={showToolbar} onMouseLeave={handleToolbarHoverLeave} onMouseDown={useNativeDragRegions ? undefined : handleAltDragStart} - onMouseUp={useNativeDragRegions ? undefined : handleAltDragEnd} + onMouseUp={handleAltDragEnd} >
- +
@@ -317,16 +339,23 @@ export default function App(): JSX.Element { -
+ + + + ) : null) :
-
+
} - {windowBackgroundMode !== 'solid' && } + {(docking.enabled || windowBackgroundMode !== 'solid') && ( + + )}
) } diff --git a/src/renderer/audio/AudioCapture.ts b/src/renderer/audio/AudioCapture.ts index 1ad5267..a1c02b8 100644 --- a/src/renderer/audio/AudioCapture.ts +++ b/src/renderer/audio/AudioCapture.ts @@ -1,12 +1,14 @@ /** * AudioCapture — backend manager for Prism's live capture pipeline. * System output capture flows through native platform backends while - * input-device capture continues to use browser media devices. + * input-device capture uses native multichannel capture where available and + * falls back to browser media devices elsewhere. */ import { audioRouter } from './AudioRouter' import { nativeVisualizerTransport } from './NativeVisualizerTransport' import { RollingAudioBuffer } from './RollingAudioBuffer' +import { ChannelActivity, type ChannelActivitySnapshot } from './ChannelActivity' import { applyInputGainToStereoSamples, inputGainDbToLinear } from './inputGain' import type { RollingAudioSnapshot, @@ -17,15 +19,28 @@ import type { CaptureBackendKind, CaptureBackendSupport, CaptureBackendSupportEntry, + CaptureChannelRouting, CaptureMode, CaptureSourceDescriptor, } from '../../types/capture' -import { resolveNativeBackendKind } from '../../types/capture' +import { + createDefaultCaptureChannelRouting, + getCaptureRoutingStorageKey, + normalizeCaptureChannelRouting, + resolveNativeBackendKind, +} from '../../types/capture' import type { NativeCaptureDrainResult, + NativeCaptureSource, NativeCaptureStartResult, + NativeDeviceInputCaptureAPI, NativeSystemCaptureAPI, } from '../../types/nativeCapture' +import type { + DawBridgeAudioBatch, + DawBridgeSnapshot, + DawTransportSnapshot, +} from '../../types/dawBridge' export type { CaptureMode } from '../../types/capture' @@ -35,6 +50,8 @@ interface CaptureChunk { channelCount: number capturedAt: number sequence: number + sourceChannelPeaks?: Float32Array + transport?: DawTransportSnapshot } interface CaptureBackendStatus { @@ -44,13 +61,19 @@ interface CaptureBackendStatus { reason: string | null sampleRate: number channelCount: number + sourceChannelCount: number + channelRoutingAvailable: boolean + channelRouting: CaptureChannelRouting activeSourceId: string | null activeSourceLabel: string | null + waiting?: boolean } interface CaptureBackendStartRequest { deviceId?: string + persistentSourceId?: string forceRestart?: boolean + channelRouting?: CaptureChannelRouting } interface CaptureBackend { @@ -60,6 +83,7 @@ interface CaptureBackend { listSources(): Promise subscribe(listener: (chunk: CaptureChunk) => void): () => void getStatus(): CaptureBackendStatus + setChannelRouting(routing: CaptureChannelRouting): CaptureChannelRouting } const NATIVE_BACKLOG_CATCH_UP_CHUNK_THRESHOLD = 4 @@ -134,11 +158,18 @@ export function createDefaultBackendSupport(platform = resolvePlatform()): Captu kind: resolveNativeBackendKind(platform), available: false, reason: 'Native system audio capture is not available in this build.', + channelRoutingAvailable: false, }, deviceInput: { kind: 'device-input', available: true, reason: null, + channelRoutingAvailable: false, + }, + dawBridge: { + kind: 'daw-bridge', + available: false, + reason: 'The DAW bridge listener is still starting.', }, } } @@ -149,9 +180,13 @@ export interface CaptureManagerStatus { backendSupport: CaptureBackendSupport | null sampleRate: number channelCount: number + sourceChannelCount: number + channelRoutingAvailable: boolean + channelRouting: CaptureChannelRouting isCapturing: boolean activeSourceId: string | null activeSourceLabel: string | null + waiting: boolean } type StatusListener = (status: CaptureManagerStatus) => void @@ -164,6 +199,8 @@ function toDeviceSourceDescriptor(device: MediaDeviceInfo): CaptureSourceDescrip id: device.deviceId, label: device.label || `Input ${device.deviceId.slice(0, 8)}`, kind: 'device', + isDefault: device.deviceId === 'default', + channelRoutingAvailable: false, } } @@ -237,6 +274,7 @@ class DeviceInputCaptureRuntime { } getStatus(kind: CaptureBackendKind): CaptureBackendStatus { + const routing = createDefaultCaptureChannelRouting(this.channelCount) return { kind, active: this.active, @@ -244,6 +282,9 @@ class DeviceInputCaptureRuntime { reason: null, sampleRate: this.sampleRate, channelCount: this.channelCount, + sourceChannelCount: this.channelCount, + channelRoutingAvailable: false, + channelRouting: routing, activeSourceId: this.active ? this.activeSourceId : null, activeSourceLabel: this.active ? this.activeSourceLabel : null, } @@ -392,6 +433,19 @@ class DeviceInputCaptureBackend implements CaptureBackend { getStatus(): CaptureBackendStatus { return this.runtime.getStatus(this.kind) } + + setChannelRouting(): CaptureChannelRouting { + return createDefaultCaptureChannelRouting(this.runtime.getStatus(this.kind).channelCount) + } +} + +interface NativePolledCaptureModule { + getSupport: () => { available: boolean; reason: string | null } + start: (deviceId?: string, routing?: CaptureChannelRouting) => NativeCaptureStartResult + setChannelRouting?: (left: number, right: number) => CaptureChannelRouting + stop: () => void + drain: (maxChunks?: number) => NativeCaptureDrainResult + nowMilliseconds: () => number } export abstract class NativePolledCaptureBackend implements CaptureBackend { @@ -402,6 +456,8 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { private active = false private sampleRate = 48000 private channelCount = 2 + private sourceChannelCount = 2 + private channelRouting: CaptureChannelRouting = createDefaultCaptureChannelRouting(2) private supportReason: string | null private performanceOffsetMilliseconds = 0 private activeSourceId: string | null = null @@ -429,10 +485,25 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { request?.deviceId && request.deviceId !== DEFAULT_SYSTEM_SOURCE_ID ? request.deviceId : undefined, + request?.channelRouting, ) as NativeCaptureStartResult this.sampleRate = Math.max(1, Math.floor(startResult.sampleRate) || 48000) - this.channelCount = Math.max(1, Math.floor(startResult.channelCount) || 2) + this.channelCount = Math.max(1, Math.min(2, Math.floor(startResult.channelCount) || 2)) + this.sourceChannelCount = Math.max( + 1, + Math.floor(startResult.sourceChannelCount ?? startResult.channelCount) || 1, + ) + this.channelRouting = normalizeCaptureChannelRouting( + request?.channelRouting, + this.sourceChannelCount, + ) + if (nativeCapture.setChannelRouting) { + this.channelRouting = nativeCapture.setChannelRouting( + this.channelRouting.left, + this.channelRouting.right, + ) + } this.activeSourceId = startResult.deviceId || null this.activeSourceLabel = startResult.deviceLabel || null this.supportReason = null @@ -459,13 +530,16 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { return [] } - return nativeCapture.listOutputDevices().map((source) => ({ + return this.listNativeSources(nativeCapture).map((source) => ({ id: source.id, label: source.label, - kind: 'system', + kind: source.kind, isDefault: source.isDefault, sampleRate: source.sampleRate, channelCount: source.channelCount, + channels: source.channels, + channelRoutingAvailable: source.channelRoutingAvailable + && typeof nativeCapture.setChannelRouting === 'function', })) } @@ -484,11 +558,23 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { reason: this.supportReason, sampleRate: this.sampleRate, channelCount: this.channelCount, + sourceChannelCount: this.sourceChannelCount, + channelRoutingAvailable: typeof this.getNativeCaptureModule()?.setChannelRouting === 'function', + channelRouting: this.channelRouting, activeSourceId: this.active ? this.activeSourceId : null, activeSourceLabel: this.active ? this.activeSourceLabel : null, } } + setChannelRouting(routing: CaptureChannelRouting): CaptureChannelRouting { + const normalized = normalizeCaptureChannelRouting(routing, this.sourceChannelCount) + const nativeCapture = this.getNativeCaptureModule() + this.channelRouting = nativeCapture?.setChannelRouting + ? nativeCapture.setChannelRouting(normalized.left, normalized.right) + : normalized + return this.channelRouting + } + protected shouldTrimBacklogForLiveCapture(): boolean { return false } @@ -517,6 +603,7 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { channelCount: Math.max(1, Math.floor(chunk.channelCount) || 1), capturedAt: chunk.capturedAtMilliseconds + this.performanceOffsetMilliseconds, sequence: Math.max(1, Math.floor(chunk.sequence) || 1), + sourceChannelPeaks: chunk.sourceChannelPeaks, } for (const listener of this.chunkListeners) { @@ -544,7 +631,8 @@ export abstract class NativePolledCaptureBackend implements CaptureBackend { } } - protected abstract getNativeCaptureModule(): NativeSystemCaptureAPI | null + protected abstract getNativeCaptureModule(): NativePolledCaptureModule | null + protected abstract listNativeSources(module: NativePolledCaptureModule): NativeCaptureSource[] protected abstract getBackendLabel(): string } @@ -555,6 +643,10 @@ class NativeMacOSCaptureBackend extends NativePolledCaptureBackend { return typeof window !== 'undefined' ? window.nativeCaptureAPI?.macosCapture ?? null : null } + protected listNativeSources(module: NativePolledCaptureModule): NativeCaptureSource[] { + return (module as NativeSystemCaptureAPI).listOutputDevices() + } + protected getBackendLabel(): string { return 'Native macOS' } @@ -567,6 +659,10 @@ class NativeWindowsCaptureBackend extends NativePolledCaptureBackend { return typeof window !== 'undefined' ? window.nativeCaptureAPI?.windowsCapture ?? null : null } + protected listNativeSources(module: NativePolledCaptureModule): NativeCaptureSource[] { + return (module as NativeSystemCaptureAPI).listOutputDevices() + } + protected getBackendLabel(): string { return 'Native Windows' } @@ -579,6 +675,10 @@ class NativeLinuxCaptureBackend extends NativePolledCaptureBackend { return typeof window !== 'undefined' ? window.nativeCaptureAPI?.linuxCapture ?? null : null } + protected listNativeSources(module: NativePolledCaptureModule): NativeCaptureSource[] { + return (module as NativeSystemCaptureAPI).listOutputDevices() + } + protected getBackendLabel(): string { return 'Native Linux' } @@ -588,6 +688,36 @@ class NativeLinuxCaptureBackend extends NativePolledCaptureBackend { } } +export class NativeDeviceInputCaptureBackend extends NativePolledCaptureBackend { + readonly kind = 'device-input' as const + + async start(request?: CaptureBackendStartRequest): Promise { + if (typeof window !== 'undefined' && window.electronAPI?.requestMicrophoneAccess) { + const granted = await window.electronAPI.requestMicrophoneAccess() + if (!granted) { + throw new Error( + 'Microphone access is required for input-device capture. Enable Prism in System Settings → Privacy & Security → Microphone, then restart Prism.', + ) + } + } + await super.start(request) + } + + protected getNativeCaptureModule(): NativeDeviceInputCaptureAPI | null { + return typeof window !== 'undefined' + ? window.nativeCaptureAPI?.deviceInputCapture ?? null + : null + } + + protected listNativeSources(module: NativePolledCaptureModule): NativeCaptureSource[] { + return (module as NativeDeviceInputCaptureAPI).listInputDevices() + } + + protected getBackendLabel(): string { + return 'Native device input' + } +} + class NativeUnavailableCaptureBackend implements CaptureBackend { readonly kind: CaptureBackendKind private readonly reason: string | null @@ -613,6 +743,10 @@ class NativeUnavailableCaptureBackend implements CaptureBackend { return () => {} } + setChannelRouting(): CaptureChannelRouting { + return createDefaultCaptureChannelRouting(2) + } + getStatus(): CaptureBackendStatus { return { kind: this.kind, @@ -621,19 +755,183 @@ class NativeUnavailableCaptureBackend implements CaptureBackend { reason: this.reason, sampleRate: 48000, channelCount: 2, + sourceChannelCount: 2, + channelRoutingAvailable: false, + channelRouting: createDefaultCaptureChannelRouting(2), activeSourceId: null, activeSourceLabel: null, } } } +class DawBridgeCaptureBackend implements CaptureBackend { + readonly kind = 'daw-bridge' as const + + private snapshot: DawBridgeSnapshot = { + available: false, + reason: 'The DAW bridge listener is not initialized.', + selectedSourceId: null, + sources: [], + } + private active = false + private selectedSourceId: string | null = null + private preferredPersistentId: string | null = null + private readonly chunkListeners = new Set<(chunk: CaptureChunk) => void>() + private readonly statusListeners = new Set<() => void>() + private readonly sourceListeners = new Set<() => void>() + + constructor() { + if (typeof window === 'undefined' || !window.electronAPI?.dawBridge) return + window.electronAPI.dawBridge.onSnapshot((snapshot) => { + this.snapshot = snapshot + void this.reconcileSelection() + for (const listener of this.sourceListeners) listener() + }) + window.electronAPI.dawBridge.onAudioBatch((batch) => this.handleAudioBatch(batch)) + void window.electronAPI.dawBridge.getSnapshot().then((snapshot) => { + this.snapshot = snapshot + this.emitStatus() + for (const listener of this.sourceListeners) listener() + }) + } + + async start(request?: CaptureBackendStartRequest): Promise { + this.active = true + this.preferredPersistentId = request?.persistentSourceId ?? request?.deviceId ?? this.preferredPersistentId + this.selectedSourceId = this.resolveRequestedSource(request?.deviceId ?? this.selectedSourceId) + if (typeof window === 'undefined' || !window.electronAPI?.dawBridge) { + throw new Error('The DAW bridge API is unavailable in this build.') + } + this.snapshot = await window.electronAPI.dawBridge.selectSource(this.selectedSourceId) + if (!this.snapshot.available) { + this.active = false + throw new Error(this.snapshot.reason ?? 'The DAW bridge listener is unavailable.') + } + this.emitStatus() + } + + async stop(): Promise { + this.active = false + if (typeof window !== 'undefined' && window.electronAPI?.dawBridge) { + this.snapshot = await window.electronAPI.dawBridge.selectSource(null) + } + this.emitStatus() + } + + async listSources(): Promise { + if (typeof window !== 'undefined' && window.electronAPI?.dawBridge) { + this.snapshot = await window.electronAPI.dawBridge.getSnapshot() + } + return this.snapshot.sources.map((source) => ({ + id: source.id, + persistentId: source.persistentId, + label: source.label, + kind: 'daw', + sampleRate: source.sampleRate, + channelCount: source.channelCount, + })) + } + + subscribe(listener: (chunk: CaptureChunk) => void): () => void { + this.chunkListeners.add(listener) + return () => this.chunkListeners.delete(listener) + } + + subscribeStatus(listener: () => void): () => void { + this.statusListeners.add(listener) + return () => this.statusListeners.delete(listener) + } + + subscribeSources(listener: () => void): () => void { + this.sourceListeners.add(listener) + return () => this.sourceListeners.delete(listener) + } + + setSelectedSourceId(persistentSourceId: string | null, liveSourceId: string | null = null): void { + this.preferredPersistentId = persistentSourceId + this.selectedSourceId = liveSourceId ?? this.resolveRequestedSource(persistentSourceId) + } + + getSelectedSourceId(): string | null { + return this.selectedSourceId + } + + getStatus(): CaptureBackendStatus { + const source = this.snapshot.sources.find((candidate) => candidate.id === this.selectedSourceId) ?? null + return { + kind: this.kind, + active: this.active, + available: this.snapshot.available, + reason: this.snapshot.reason, + sampleRate: source?.sampleRate ?? 48000, + channelCount: source?.channelCount ?? 2, + sourceChannelCount: source?.channelCount ?? 2, + channelRoutingAvailable: false, + channelRouting: createDefaultCaptureChannelRouting(source?.channelCount ?? 2), + activeSourceId: this.active ? this.selectedSourceId : null, + activeSourceLabel: this.active ? source?.label ?? null : null, + waiting: this.active && !source, + } + } + + setChannelRouting(): CaptureChannelRouting { + return createDefaultCaptureChannelRouting(this.getStatus().channelCount) + } + + private handleAudioBatch(batch: DawBridgeAudioBatch): void { + if (!this.active || batch.sourceId !== this.selectedSourceId) return + for (const packet of batch.packets) { + const chunk: CaptureChunk = { + left: packet.left, + right: packet.right, + channelCount: packet.channelCount, + capturedAt: performance.now(), + sequence: packet.sequence, + transport: packet.transport, + } + for (const listener of this.chunkListeners) listener(chunk) + } + } + + private emitStatus(): void { + for (const listener of this.statusListeners) listener() + } + + private resolveRequestedSource(sourceId: string | null | undefined): string | null { + if (!sourceId) return null + const exact = this.snapshot.sources.find((source) => source.id === sourceId) + if (exact) return exact.id + const stableMatches = this.snapshot.sources.filter((source) => source.persistentId === sourceId) + return stableMatches.length === 1 ? stableMatches[0]!.id : null + } + + private async reconcileSelection(): Promise { + if (!this.active) { + this.emitStatus() + return + } + if (this.selectedSourceId && this.snapshot.sources.some((source) => source.id === this.selectedSourceId)) { + this.emitStatus() + return + } + + this.selectedSourceId = this.resolveRequestedSource(this.preferredPersistentId) + if (typeof window !== 'undefined' && window.electronAPI?.dawBridge) { + this.snapshot = await window.electronAPI.dawBridge.selectSource(this.selectedSourceId) + } + this.emitStatus() + } +} + interface CaptureStartOptions { forceDeviceRestart?: boolean + channelRouting?: CaptureChannelRouting } class AudioCapture { private readonly deviceInputRuntime = new DeviceInputCaptureRuntime() private readonly deviceInputBackend: CaptureBackend + private readonly dawBridgeBackend = new DawBridgeCaptureBackend() private backendSupport: CaptureBackendSupport | null = null private backendSupportPromise: Promise | null = null @@ -643,8 +941,12 @@ class AudioCapture { private selectedDeviceId: string | null = null private selectedSystemSourceId: string | null = DEFAULT_SYSTEM_SOURCE_ID + private selectedDawSourceId: string | null = null + private selectedDawConnectionId: string | null = null private captureMode: CaptureMode = 'system' + private selectedChannelRouting: CaptureChannelRouting = createDefaultCaptureChannelRouting(2) private sessionId: number | null = null + private readonly channelActivity = new ChannelActivity() private inputGainDb = 0 private inputGainLinear = 1 private statusListeners = new Set() @@ -653,8 +955,21 @@ class AudioCapture { private rollingCaptureStatusListeners = new Set() constructor() { - this.deviceInputBackend = new DeviceInputCaptureBackend(this.deviceInputRuntime) + const nativeInputModule = typeof window !== 'undefined' + ? window.nativeCaptureAPI?.deviceInputCapture ?? null + : null + const nativeInputSupport = nativeInputModule?.getSupport() + this.deviceInputBackend = nativeInputSupport?.available + ? new NativeDeviceInputCaptureBackend({ + kind: 'device-input', + available: true, + reason: null, + channelRoutingAvailable: true, + }) + : new DeviceInputCaptureBackend(this.deviceInputRuntime) this.deviceInputBackend.subscribe((chunk) => this.handleChunk(this.deviceInputBackend.kind, chunk)) + this.dawBridgeBackend.subscribe((chunk) => this.handleChunk(this.dawBridgeBackend.kind, chunk)) + this.dawBridgeBackend.subscribeStatus(() => this.handleDawBridgeStatus()) this.nativeBackend = this.createNativeBackend(createDefaultBackendSupport().nativeBackend) this.bindNativeBackend(this.nativeBackend) @@ -687,12 +1002,12 @@ class AudioCapture { return this.ensureBackendSupport() } - async startSystemAudio(sourceId?: string): Promise { + async startSystemAudio(sourceId?: string, options: CaptureStartOptions = {}): Promise { this.captureMode = 'system' if (sourceId) { this.selectedSystemSourceId = sourceId } - await this.start() + await this.start(undefined, options) } async startDevice(deviceId?: string, options: CaptureStartOptions = {}): Promise { @@ -701,6 +1016,11 @@ class AudioCapture { await this.start(undefined, options) } + async startDawBridge(): Promise { + this.captureMode = 'daw' + await this.start() + } + async start(deviceId?: string, options: CaptureStartOptions = {}): Promise { if (deviceId) { this.selectedDeviceId = deviceId @@ -712,28 +1032,48 @@ class AudioCapture { const requestedBackend = this.captureMode === 'device' ? this.deviceInputBackend - : this.nativeBackend + : this.captureMode === 'daw' + ? this.dawBridgeBackend + : this.nativeBackend const requestedDeviceId = this.captureMode === 'device' ? this.selectedDeviceId ?? undefined - : this.selectedSystemSourceId ?? DEFAULT_SYSTEM_SOURCE_ID + : this.captureMode === 'daw' + ? this.selectedDawConnectionId ?? this.selectedDawSourceId ?? undefined + : this.selectedSystemSourceId ?? DEFAULT_SYSTEM_SOURCE_ID await requestedBackend.start({ deviceId: requestedDeviceId, + persistentSourceId: this.captureMode === 'daw' ? this.selectedDawSourceId ?? undefined : undefined, forceRestart: this.captureMode === 'device' && options.forceDeviceRestart === true, + channelRouting: options.channelRouting ?? this.selectedChannelRouting, }) this.activeBackend = requestedBackend const backendStatus = requestedBackend.getStatus() + this.selectedChannelRouting = backendStatus.channelRouting this.sessionId = audioRouter.beginSession( backendStatus.sampleRate, backendStatus.channelCount, backendStatus.kind, ) + if (backendStatus.channelRoutingAvailable && backendStatus.activeSourceId) { + this.channelActivity.beginSession( + this.sessionId, + getCaptureRoutingStorageKey( + backendStatus.kind === 'device-input' ? 'device' : 'system', + backendStatus.activeSourceId, + ), + backendStatus.sourceChannelCount, + ) + } this.beginRollingCaptureSession( backendStatus.sampleRate, backendStatus.channelCount, ) nativeVisualizerTransport.reset(audioRouter.getSessionState()) + if (requestedBackend.kind === 'daw-bridge') { + this.handleDawBridgeStatus() + } this.emitStatus() } @@ -747,6 +1087,9 @@ class AudioCapture { if (mode === 'device') { return this.deviceInputBackend.listSources() } + if (mode === 'daw') { + return this.dawBridgeBackend.listSources() + } const sources = await this.nativeBackend.listSources() if (!sources.length) { @@ -757,9 +1100,8 @@ class AudioCapture { return [getDefaultSystemSourceDescriptor(), ...dedupedSources] } - async listDevices(): Promise { - const devices = await navigator.mediaDevices.enumerateDevices() - return devices.filter((device) => device.kind === 'audioinput') + async listDevices(): Promise { + return this.listSources('device') } getSelectedDeviceId(): string | null { @@ -780,6 +1122,21 @@ class AudioCapture { this.emitStatus() } + getSelectedDawSourceId(): string | null { + return this.selectedDawSourceId + } + + setSelectedDawSourceId(id: string | null, liveConnectionId: string | null = null): void { + this.selectedDawSourceId = id + this.selectedDawConnectionId = liveConnectionId + this.dawBridgeBackend.setSelectedSourceId(id, liveConnectionId) + this.emitStatus() + } + + subscribeDawSources(listener: () => void): () => void { + return this.dawBridgeBackend.subscribeSources(listener) + } + getCaptureMode(): CaptureMode { return this.captureMode } @@ -789,10 +1146,31 @@ class AudioCapture { this.emitStatus() } + setChannelRouting(routing: CaptureChannelRouting): CaptureChannelRouting { + const sourceChannelCount = this.activeBackend?.getStatus().sourceChannelCount ?? 2 + this.selectedChannelRouting = normalizeCaptureChannelRouting(routing, sourceChannelCount) + if ( + this.activeBackend + && this.activeBackend.kind !== 'daw-bridge' + && this.activeBackend.getStatus().channelRoutingAvailable + ) { + this.selectedChannelRouting = this.activeBackend.setChannelRouting( + this.selectedChannelRouting, + ) + } + this.emitStatus() + return this.selectedChannelRouting + } + getSampleRate(): number { return this.activeBackend?.getStatus().sampleRate ?? 48000 } + getChannelActivity(now = performance.now()): ChannelActivitySnapshot | null { + if (!this.activeBackend?.getStatus().active) return null + return this.channelActivity.getSnapshot(now) + } + getStatus(): CaptureManagerStatus { const backendStatus = this.activeBackend?.getStatus() const isCapturing = Boolean(backendStatus?.active && this.sessionId !== null) @@ -802,9 +1180,13 @@ class AudioCapture { backendSupport: this.backendSupport, sampleRate: backendStatus?.sampleRate ?? 48000, channelCount: backendStatus?.channelCount ?? 2, + sourceChannelCount: backendStatus?.sourceChannelCount ?? 2, + channelRoutingAvailable: backendStatus?.channelRoutingAvailable ?? false, + channelRouting: backendStatus?.channelRouting ?? this.selectedChannelRouting, isCapturing, activeSourceId: isCapturing ? backendStatus?.activeSourceId ?? null : null, activeSourceLabel: isCapturing ? backendStatus?.activeSourceLabel ?? null : null, + waiting: Boolean(backendStatus?.waiting), } } @@ -895,6 +1277,7 @@ class AudioCapture { } private async stopActiveCapture(): Promise { + this.channelActivity.reset() if (this.rollingAudioBuffer) { this.rollingAudioBuffer = null this.emitRollingCaptureStatus() @@ -918,15 +1301,37 @@ class AudioCapture { return } - if (originKind.startsWith('native-') && this.inputGainLinear !== 1) { + this.channelActivity.ingest(chunk.sourceChannelPeaks, chunk.capturedAt) + + const nativeDeviceInput = originKind === 'device-input' + && this.activeBackend instanceof NativeDeviceInputCaptureBackend + if ( + (originKind.startsWith('native-') || originKind === 'daw-bridge' || nativeDeviceInput) + && this.inputGainLinear !== 1 + ) { applyInputGainToStereoSamples(chunk.left, chunk.right, this.inputGainLinear) } + if ( + originKind === 'daw-bridge' + && this.activeBackend + && ( + this.activeBackend.getStatus().sampleRate !== audioRouter.getSampleRate() + || chunk.channelCount !== audioRouter.getChannelCount() + ) + ) { + const status = this.activeBackend.getStatus() + this.sessionId = audioRouter.beginSession(status.sampleRate, chunk.channelCount, originKind) + this.beginRollingCaptureSession(status.sampleRate, chunk.channelCount) + nativeVisualizerTransport.reset(audioRouter.getSessionState()) + } + audioRouter.ingestChunk(chunk.left, chunk.right, { sessionId: this.sessionId, channelCount: chunk.channelCount, capturedAt: chunk.capturedAt, sequence: chunk.sequence, + transport: chunk.transport, }) let rollingAudioBuffer = this.rollingAudioBuffer @@ -955,6 +1360,32 @@ class AudioCapture { } } + private handleDawBridgeStatus(): void { + if (this.activeBackend?.kind !== 'daw-bridge' || this.sessionId === null) { + this.emitStatus() + return + } + + const status = this.activeBackend.getStatus() + if (status.waiting) { + audioRouter.suspendSession() + } else if ( + status.sampleRate !== audioRouter.getSampleRate() + || status.channelCount !== audioRouter.getChannelCount() + ) { + this.sessionId = audioRouter.beginSession( + status.sampleRate, + status.channelCount, + 'daw-bridge', + ) + this.beginRollingCaptureSession(status.sampleRate, status.channelCount) + nativeVisualizerTransport.reset(audioRouter.getSessionState()) + } else { + audioRouter.resumeSession() + } + this.emitStatus() + } + setInputGain(db: number): void { this.inputGainDb = db this.inputGainLinear = inputGainDbToLinear(this.inputGainDb) diff --git a/src/renderer/audio/AudioRouter.ts b/src/renderer/audio/AudioRouter.ts index 3ae0928..9850e4e 100644 --- a/src/renderer/audio/AudioRouter.ts +++ b/src/renderer/audio/AudioRouter.ts @@ -6,6 +6,7 @@ import { AUDIO_SCOPE_KINDS, type AudioScopeKind } from '../../types/scope' import type { CaptureBackendKind } from '../../types/capture' +import type { DawTransportSnapshot } from '../../types/dawBridge' const MAX_PENDING_CHUNKS = 20 const MAX_PENDING_SPECTRUM_CHUNKS = 96 @@ -13,6 +14,7 @@ const MAX_PENDING_VECTORSCOPE_CHUNKS = 20 const LATENCY_SAMPLE_WINDOW = 240 const SCOPE_RING_CAPACITY: Record = { + waterfall: MAX_PENDING_SPECTRUM_CHUNKS, spectrum: MAX_PENDING_SPECTRUM_CHUNKS, oscilloscope: MAX_PENDING_CHUNKS, vectorscope: MAX_PENDING_VECTORSCOPE_CHUNKS, @@ -27,10 +29,12 @@ export interface AudioSessionState { sampleRate: number channelCount: number capturing: boolean + suspended: boolean backendKind: CaptureBackendKind | null } export interface VisualizerConsumerDemand { + waterfall?: boolean spectrum?: boolean oscilloscope?: boolean vectorscope?: boolean @@ -67,12 +71,14 @@ interface AudioChunkMeta { channelCount?: number capturedAt?: number sequence?: number + transport?: DawTransportSnapshot } interface MonoChunkRecord { samples: Float32Array capturedAt: number sequence: number + transport?: DawTransportSnapshot } interface StereoChunkRecord { @@ -80,6 +86,7 @@ interface StereoChunkRecord { right: Float32Array capturedAt: number sequence: number + transport?: DawTransportSnapshot } interface ScopeLatencyTracker { @@ -90,6 +97,7 @@ interface ScopeLatencyTracker { } type ScopeRingMap = { + waterfall: FixedChunkRing spectrum: FixedChunkRing oscilloscope: FixedChunkRing vectorscope: FixedChunkRing @@ -191,6 +199,7 @@ class RollingLatencyWindow { function createEmptyDemand(): NormalizedVisualizerConsumerDemand { return { + waterfall: false, spectrum: false, oscilloscope: false, vectorscope: false, @@ -212,6 +221,7 @@ function createScopeLatencyTracker(): ScopeLatencyTracker { export class AudioRouter { private readonly rings: ScopeRingMap = { + waterfall: new FixedChunkRing(SCOPE_RING_CAPACITY.waterfall), spectrum: new FixedChunkRing(SCOPE_RING_CAPACITY.spectrum), oscilloscope: new FixedChunkRing(SCOPE_RING_CAPACITY.oscilloscope), vectorscope: new FixedChunkRing(SCOPE_RING_CAPACITY.vectorscope), @@ -222,6 +232,7 @@ export class AudioRouter { } private readonly scopeLatency: Record = { + waterfall: createScopeLatencyTracker(), spectrum: createScopeLatencyTracker(), oscilloscope: createScopeLatencyTracker(), vectorscope: createScopeLatencyTracker(), @@ -235,6 +246,7 @@ export class AudioRouter { private _sampleRate = 48000 private _capturing = false + private _suspended = false private _channelCount = 2 private _sessionId = 0 private _backendKind: CaptureBackendKind | null = null @@ -271,6 +283,7 @@ export class AudioRouter { sampleRate: this._sampleRate, channelCount: this._channelCount, capturing: this._capturing, + suspended: this._suspended, backendKind: this._backendKind, } } @@ -280,6 +293,7 @@ export class AudioRouter { this._sampleRate = sampleRate this._channelCount = Math.max(1, Math.floor(channelCount) || 1) this._capturing = true + this._suspended = false this._backendKind = backendKind this.reset() this.emitSessionState() @@ -289,11 +303,24 @@ export class AudioRouter { endSession(): void { this._sessionId += 1 this._capturing = false + this._suspended = false this._backendKind = null this.reset() this.emitSessionState() } + suspendSession(): void { + if (!this._capturing || this._suspended) return + this._suspended = true + this.emitSessionState() + } + + resumeSession(): void { + if (!this._capturing || !this._suspended) return + this._suspended = false + this.emitSessionState() + } + subscribeToSessionChanges(listener: (state: AudioSessionState) => void): () => void { this.sessionListeners.add(listener) listener(this.getSessionState()) @@ -319,6 +346,7 @@ export class AudioRouter { spectrum: Boolean(demand.spectrum), oscilloscope: Boolean(demand.oscilloscope), vectorscope: Boolean(demand.vectorscope), + waterfall: Boolean(demand.waterfall), spectrogram: Boolean(demand.spectrogram), vumeter: Boolean(demand.vumeter), lufsmeter: Boolean(demand.lufsmeter), @@ -344,7 +372,7 @@ export class AudioRouter { } ingestChunk(left: Float32Array, right: Float32Array, meta: AudioChunkMeta = {}): void { - if (!this._capturing) { + if (!this._capturing || this._suspended) { this.notCapturingDrops += 1 return } @@ -363,7 +391,7 @@ export class AudioRouter { const activeDemand = this.getActiveDemand() const needsSpectrum = Boolean(activeDemand.spectrum) - const needsStereo = Boolean(activeDemand.spectrogram || activeDemand.vectorscope || activeDemand.vumeter || activeDemand.lufsmeter || activeDemand.waveform) + const needsStereo = Boolean(activeDemand.waterfall || activeDemand.spectrogram || activeDemand.vectorscope || activeDemand.vumeter || activeDemand.lufsmeter || activeDemand.waveform) const needsLeft = Boolean(activeDemand.oscilloscope) if (!needsSpectrum && !needsStereo && !needsLeft) { @@ -377,34 +405,44 @@ export class AudioRouter { const rightSamples = resolvedRight.length === len ? resolvedRight : resolvedRight.subarray(0, len) if (activeDemand.oscilloscope) { - this.rings.oscilloscope.push({ samples: leftSamples, capturedAt, sequence }) + this.rings.oscilloscope.push({ samples: leftSamples, capturedAt, sequence, transport: meta.transport }) } if (activeDemand.spectrum) { - this.rings.spectrum.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) + this.rings.spectrum.push({ left: leftSamples, right: rightSamples, capturedAt, sequence, transport: meta.transport }) + } + + if (activeDemand.waterfall) { + this.rings.waterfall.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) } if (activeDemand.spectrogram) { - this.rings.spectrogram.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) + this.rings.spectrogram.push({ left: leftSamples, right: rightSamples, capturedAt, sequence, transport: meta.transport }) } if (activeDemand.vectorscope) { - this.rings.vectorscope.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) + this.rings.vectorscope.push({ left: leftSamples, right: rightSamples, capturedAt, sequence, transport: meta.transport }) } if (activeDemand.vumeter) { - this.rings.vumeter.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) + this.rings.vumeter.push({ left: leftSamples, right: rightSamples, capturedAt, sequence, transport: meta.transport }) } if (activeDemand.lufsmeter) { - this.rings.lufsmeter.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) + this.rings.lufsmeter.push({ left: leftSamples, right: rightSamples, capturedAt, sequence, transport: meta.transport }) } if (activeDemand.waveform) { - this.rings.waveform.push({ left: leftSamples, right: rightSamples, capturedAt, sequence }) + this.rings.waveform.push({ left: leftSamples, right: rightSamples, capturedAt, sequence, transport: meta.transport }) } } + flushPendingWaterfallSamples(): { left: Float32Array; right: Float32Array; sequence: number }[] { + const records = this.rings.waterfall.drain() + this.recordScopeDrain('waterfall', records) + return records.map(({ left, right, sequence }) => ({ left, right, sequence })) + } + flushPendingOscilloscopeSamples(): Float32Array[] { const records = this.rings.oscilloscope.drain() this.recordScopeDrain('oscilloscope', records) @@ -443,10 +481,10 @@ export class AudioRouter { }) } - flushPendingSpectrogramStereoSamples(): { left: Float32Array; right: Float32Array }[] { + flushPendingSpectrogramStereoSamples(): { left: Float32Array; right: Float32Array; transport?: DawTransportSnapshot }[] { const records = this.rings.spectrogram.drain() this.recordScopeDrain('spectrogram', records) - return records.map((record) => ({ left: record.left, right: record.right })) + return records.map((record) => ({ left: record.left, right: record.right, transport: record.transport })) } flushPendingVectorscopeSamples(): { left: Float32Array; right: Float32Array }[] { @@ -473,10 +511,16 @@ export class AudioRouter { return records.map((record) => record.left) } - flushPendingWaveformStereoSamples(): { left: Float32Array; right: Float32Array }[] { + flushPendingWaveformStereoSamples(): { left: Float32Array; right: Float32Array; transport?: DawTransportSnapshot }[] { const records = this.rings.waveform.drain() this.recordScopeDrain('waveform', records) - return records.map((record) => ({ left: record.left, right: record.right })) + return records.map((record) => ({ left: record.left, right: record.right, transport: record.transport })) + } + + flushPendingWaveformAnnotatedSamples(): { left: Float32Array; right: Float32Array; transport?: DawTransportSnapshot }[] { + const records = this.rings.waveform.drain() + this.recordScopeDrain('waveform', records) + return records.map((record) => ({ left: record.left, right: record.right, transport: record.transport })) } getDiagnosticsSnapshot(): AudioRouterDiagnostics { diff --git a/src/renderer/audio/ChannelActivity.ts b/src/renderer/audio/ChannelActivity.ts new file mode 100644 index 0000000..0eb380e --- /dev/null +++ b/src/renderer/audio/ChannelActivity.ts @@ -0,0 +1,76 @@ +const ACTIVITY_RELEASE_MS = 300 +const ACTIVITY_STALE_MS = 1000 + +export interface ChannelActivitySnapshot { + readonly sessionId: number + readonly sourceKey: string + readonly channelCount: number + readonly updatedAt: number + readonly opacities: readonly number[] +} + +export function sourcePeakToOpacity(peak: number): number { + if (!Number.isFinite(peak) || peak <= 0.001) return 0 + return Math.min(1, Math.max(0, (20 * Math.log10(peak) + 60) / 60)) * 0.5 +} + +/** Holds brief signals between paints; all times use the renderer performance clock. */ +export class ChannelActivity { + private sessionId: number | null = null + private sourceKey: string | null = null + private opacities = new Float32Array(0) + private updatedAt: number | null = null + + beginSession(sessionId: number, sourceKey: string, channelCount: number): void { + this.sessionId = sessionId + this.sourceKey = sourceKey + this.opacities = new Float32Array(channelCount) + this.updatedAt = null + } + + reset(): void { + this.sessionId = null + this.sourceKey = null + this.opacities = new Float32Array(0) + this.updatedAt = null + } + + ingest(peaks: Float32Array | undefined, capturedAt: number): void { + if (this.sessionId === null || !Number.isFinite(capturedAt)) return + if (this.updatedAt !== null && capturedAt < this.updatedAt) return + if (!peaks?.length) { + this.opacities.fill(0) + this.updatedAt = null + return + } + if (peaks.length !== this.opacities.length) { + this.opacities = new Float32Array(peaks.length) + this.updatedAt = null + } + const elapsed = this.updatedAt === null ? Infinity : capturedAt - this.updatedAt + const release = elapsed >= ACTIVITY_STALE_MS ? 0 : Math.exp(-elapsed / ACTIVITY_RELEASE_MS) + for (let index = 0; index < peaks.length; index += 1) { + this.opacities[index] = Math.max( + sourcePeakToOpacity(peaks[index]), + this.opacities[index] * release, + ) + } + this.updatedAt = capturedAt + } + + getSnapshot(now: number): ChannelActivitySnapshot | null { + if (this.sessionId === null || this.sourceKey === null || this.updatedAt === null + || !Number.isFinite(now) || now - this.updatedAt >= ACTIVITY_STALE_MS) return null + const release = Math.exp(-Math.max(0, now - this.updatedAt) / ACTIVITY_RELEASE_MS) + return { + sessionId: this.sessionId, + sourceKey: this.sourceKey, + channelCount: this.opacities.length, + updatedAt: this.updatedAt, + opacities: Array.from(this.opacities, (opacity) => { + const decayed = opacity * release + return decayed < 0.001 ? 0 : decayed + }), + } + } +} diff --git a/src/renderer/audio/NativeVisualizerTransport.ts b/src/renderer/audio/NativeVisualizerTransport.ts index 09ed069..977e87a 100644 --- a/src/renderer/audio/NativeVisualizerTransport.ts +++ b/src/renderer/audio/NativeVisualizerTransport.ts @@ -29,6 +29,7 @@ export interface NativeVisualizerTransportBridge { } const EMPTY_DEMAND: Required = { + waterfall: false, spectrum: false, oscilloscope: false, vectorscope: false, @@ -60,6 +61,7 @@ const defaultBridge: NativeVisualizerTransportBridge = { function normalizeDemand(demand: VisualizerConsumerDemand): Required { return { + waterfall: Boolean(demand.waterfall), spectrum: Boolean(demand.spectrum), oscilloscope: Boolean(demand.oscilloscope), vectorscope: Boolean(demand.vectorscope), @@ -84,7 +86,8 @@ export class NativeVisualizerTransport { setDemand(demand: VisualizerConsumerDemand): void { const nextDemand = normalizeDemand(demand) if ( - nextDemand.spectrum === this.demand.spectrum + nextDemand.waterfall === this.demand.waterfall + && nextDemand.spectrum === this.demand.spectrum && nextDemand.oscilloscope === this.demand.oscilloscope && nextDemand.vectorscope === this.demand.vectorscope && nextDemand.spectrogram === this.demand.spectrogram diff --git a/src/renderer/audio/RollingAudioBuffer.ts b/src/renderer/audio/RollingAudioBuffer.ts index 289b54f..c505f28 100644 --- a/src/renderer/audio/RollingAudioBuffer.ts +++ b/src/renderer/audio/RollingAudioBuffer.ts @@ -3,12 +3,8 @@ import type { RollingCaptureDurationSeconds, } from '../../types/audioClip' -function floatToPcm16(sample: number): number { - if (!Number.isFinite(sample)) return 0 - const clamped = Math.max(-1, Math.min(1, sample)) - return clamped < 0 - ? Math.round(clamped * 32768) - : Math.round(clamped * 32767) +function finiteSample(sample: number): number { + return Number.isFinite(sample) ? sample : 0 } function normalizeChannelCount(channelCount: number): 1 | 2 { @@ -16,7 +12,7 @@ function normalizeChannelCount(channelCount: number): 1 | 2 { } export class RollingAudioBuffer { - private samples: Int16Array + private samples: Float32Array private capacityFrames: number private writeFrameIndex = 0 private bufferedFrames = 0 @@ -34,7 +30,7 @@ export class RollingAudioBuffer { this.sampleRate = Math.max(1, Math.floor(sampleRate) || 1) this.channelCount = normalizeChannelCount(channelCount) this.capacityFrames = Math.max(1, Math.floor(durationSeconds * this.sampleRate)) - this.samples = new Int16Array(this.capacityFrames * this.channelCount) + this.samples = new Float32Array(this.capacityFrames * this.channelCount) } get durationSeconds(): RollingCaptureDurationSeconds { @@ -75,9 +71,9 @@ export class RollingAudioBuffer { for (let offset = 0; offset < contiguousFrames; offset += 1) { const sourceIndex = sourceFrameIndex + offset const destinationIndex = (this.writeFrameIndex + offset) * this.channelCount - this.samples[destinationIndex] = floatToPcm16(left[sourceIndex] ?? 0) + this.samples[destinationIndex] = finiteSample(left[sourceIndex] ?? 0) if (this.channelCount === 2) { - this.samples[destinationIndex + 1] = floatToPcm16(right[sourceIndex] ?? 0) + this.samples[destinationIndex + 1] = finiteSample(right[sourceIndex] ?? 0) } } @@ -96,7 +92,7 @@ export class RollingAudioBuffer { const nextCapacityFrames = Math.max(1, Math.floor(durationSeconds * this.sampleRate)) const framesToKeep = Math.min(this.bufferedFrames, nextCapacityFrames) - const nextSamples = new Int16Array(nextCapacityFrames * this.channelCount) + const nextSamples = new Float32Array(nextCapacityFrames * this.channelCount) if (framesToKeep > 0) { const startFrame = ( @@ -115,7 +111,7 @@ export class RollingAudioBuffer { snapshot(): RollingAudioSnapshot | null { if (this.bufferedFrames <= 0) return null - const pcmSamples = new Int16Array(this.bufferedFrames * this.channelCount) + const pcmSamples = new Float32Array(this.bufferedFrames * this.channelCount) const startFrame = ( this.writeFrameIndex - this.bufferedFrames + this.capacityFrames ) % this.capacityFrames @@ -130,7 +126,7 @@ export class RollingAudioBuffer { } private copyFramesTo( - destination: Int16Array, + destination: Float32Array, startFrame: number, frameCount: number, ): void { diff --git a/src/renderer/audio/native/index.ts b/src/renderer/audio/native/index.ts index 55051ac..0d0fb4f 100644 --- a/src/renderer/audio/native/index.ts +++ b/src/renderer/audio/native/index.ts @@ -1,3 +1,5 @@ +import type { WaterfallNativeAnalyzer } from '../../../types/waterfall' +import type { SpectrumReferenceLevel } from '../../../types/spectrumReference' // Native visualizer DSP module loader // This loads the native C++ addon for high-performance audio visualization @@ -40,6 +42,9 @@ export function getNativeLoadError(): Error | null { export const OSCILLOSCOPE_BUFFER_SIZE = 32768 export interface SpectrumNativeAnalyzer { + hasSpectrumData(): boolean + setReferenceEnabled?: (enabled: boolean) => void + getReferenceLevel?: () => SpectrumReferenceLevel setFFTSize(size: number): void getFFTSize(): number setSampleRate(sampleRate: number): void @@ -137,6 +142,9 @@ export const oscilloscope = { } export const spectrum: SpectrumNativeAnalyzer = { + hasSpectrumData: (): boolean => nativeModule?.spectrum.hasSpectrumData() ?? false, + setReferenceEnabled: (enabled: boolean): void => { nativeModule?.spectrum.setReferenceEnabled?.(enabled) }, + getReferenceLevel: (): SpectrumReferenceLevel => nativeModule?.spectrum.getReferenceLevel?.() ?? { meanSquare: 0, seconds: 0 }, isAvailable: (): boolean => { return Boolean(nativeModule?.spectrum) }, @@ -462,3 +470,11 @@ export type { VectorscopeMultibandPointsResult, VUMeterNativeSnapshot, } + +export const waterfall: WaterfallNativeAnalyzer = { + isAvailable: () => Boolean(nativeModule?.waterfall), + configure: (options) => nativeModule?.waterfall?.configure(options), + processStereo: (left, right) => nativeModule?.waterfall?.processStereo(left, right), + getFrame: (ridges, columns) => nativeModule?.waterfall?.getFrame(ridges, columns) ?? null, + reset: () => nativeModule?.waterfall?.reset(), +} diff --git a/src/renderer/audio/native/visualizer-dsp.d.ts b/src/renderer/audio/native/visualizer-dsp.d.ts index f82707c..7d042a0 100644 --- a/src/renderer/audio/native/visualizer-dsp.d.ts +++ b/src/renderer/audio/native/visualizer-dsp.d.ts @@ -1,3 +1,5 @@ +import type { SpectrumReferenceLevel } from '../../../types/spectrumReference' +import type { WaterfallNativeAnalyzer } from '../../../types/waterfall' // Type definitions for visualizer_dsp native addon export interface OscilloscopeResult { @@ -100,6 +102,9 @@ export interface OscilloscopeModule { } export interface SpectrumModule { + hasSpectrumData(): boolean; + setReferenceEnabled(enabled: boolean): void; + getReferenceLevel(): SpectrumReferenceLevel; setFFTSize(size: number): void; getFFTSize(): number; setSampleRate(sampleRate: number): void; @@ -162,6 +167,7 @@ export interface VUMeterModule { export interface VisualizerDSP { oscilloscope: OscilloscopeModule; + waterfall: WaterfallNativeAnalyzer; spectrum: SpectrumModule; spectrogram: SpectrogramModule; vectorscope: VectorscopeModule; diff --git a/src/renderer/components/AstraScopeModule.tsx b/src/renderer/components/AstraScopeModule.tsx index c3457f8..e2206f8 100644 --- a/src/renderer/components/AstraScopeModule.tsx +++ b/src/renderer/components/AstraScopeModule.tsx @@ -1,3 +1,4 @@ +import { getLocalAvailabilityDetail } from '../utils/localNowPlaying' import { useEffect, useMemo, useState, type CSSProperties, type JSX } from 'react' import type { NowPlayingProviderId } from '../../types/nowPlaying' import type { ScopeSettings } from '../../types/settings' @@ -56,6 +57,16 @@ function getFallbackTitle( return 'Nothing playing' } + if (providerId === 'tidal') { + switch (connectionState) { + case 'disabled': return 'TIDAL is idle' + case 'connecting': return 'Checking TIDAL' + case 'error': return 'TIDAL connection failed' + case 'unavailable': return 'TIDAL unavailable' + case 'connected': return 'Nothing playing' + } + } + if (providerId === 'spotify') { switch (connectionState) { case 'disabled': @@ -110,6 +121,14 @@ function getFallbackDetail( return '' } + if (providerId === 'tidal') { + if (connectionState === 'connected') return '' + if (connectionState === 'error' || connectionState === 'unavailable') return getLocalAvailabilityDetail(platform, 'tidal') + return platform === 'darwin' + ? 'Start playback in TIDAL so it appears in macOS Now Playing. Track information only.' + : 'Start playback in a compatible local TIDAL app.' + } + if (providerId === 'spotify') { switch (connectionState) { case 'disabled': @@ -216,9 +235,6 @@ export default function AstraScopeModule({ const providerState = displayProviderId ? nowPlayingState.providers[displayProviderId] : null - const providerDefinition = displayProviderId - ? nowPlayingState.definitions[displayProviderId] - : null const platform = window.electronAPI.platform useEffect(() => { @@ -296,6 +312,7 @@ export default function AstraScopeModule({ ) } + const showControls = settings.showControls && Boolean(providerState?.supportsTransportControls) const toggleCommand = snapshot?.playbackState === 'playing' ? 'pause' : 'play' const toggleLabel = snapshot?.playbackState === 'playing' ? 'Pause' : 'Play' const toggleIcon = snapshot?.playbackState === 'playing' ? '\u23F8' : '\u25B6' @@ -303,7 +320,7 @@ export default function AstraScopeModule({ const shouldShowTransport = settings.showProgress || settings.showTime const shouldShowBody = shouldShowMeta || shouldShowTransport - || settings.showControls + || showControls || Boolean(errorMessage) const isCoverArtOnly = settings.showCoverArt && !shouldShowBody const cardClassName = [ @@ -368,12 +385,12 @@ export default function AstraScopeModule({
)} - {settings.showControls && ( + {showControls && (
- ) - })} +
+
+ {SCOPE_KINDS.map((kind) => { + const active = scopeOrder.includes(kind) && !hiddenScopes.has(kind) + return ( + + ) + })} +
+
-
-
-
Theme
- {themeCredit.credit ? ( - - {themeCredit.url ? ( - { - event.preventDefault() - void handleOpenThemeWebsite(themeCredit.url!) - }} - > - By {themeCredit.credit} - - ) : ( - By {themeCredit.credit} - )} - {themeCredit.description ? ( - - - {themeCredit.description} - - ) : null} - - ) : null} +
+
+
+ Audio Source +
+
+
+ { + void handleSourceChange(event.target.value) + }} + className="bottom-bar__select" + > + + {visibleSystemSources.map((source) => ( + + ))} + + + + {devices.filter((device) => device.id !== 'default').map((device) => ( + + ))} + + + {captureMode === 'daw' + && selectedDawSourceId + && !selectedDawLiveSourceId ? ( + + ) : null} + {dawSources.length === 0 && captureMode !== 'daw' ? ( + + ) : null} + {dawSources.map((source) => ( + + ))} + + + +
+ + {indicatorLabel} +
+
+ + {captureMessage ? ( + <> +
+ {captureMessage} +
+
+ + {canUseDefaultSource ? ( + + ) : null} + {!captureError && captureNotice ? ( + + ) : null} +
+ + ) : null} +
-
-
- { - void handleThemeChange(event.target.value) - }} - className="bottom-bar__select" + + {routingChannels.length > 0 ? ( + <> +
+ +
+ +
+
Rolling Capture
+
+
+
+ + {ROLLING_CAPTURE_DURATIONS.map((duration) => ( + ))} - -
+ +
- {isRefreshingThemes ? 'Refreshing...' : 'Refresh'} - + + {rollingCaptureStatusLabel} +
+ + +
+ WAV format +
+ {(['pcm16', 'float32'] as const).map((format) => ( + + ))} +
+
-
-
-
Window
- {windowBackground.mode !== 'solid' || loginItemStatusMessage ? ( - - {windowBackground.mode !== 'solid' ? ( - - Window snapping is disabled in this mode - - ) : null} - {windowBackground.mode !== 'solid' && loginItemStatusMessage ? ( - - ) : null} - {loginItemStatusMessage ? ( - - {loginItemStatusMessage} - - ) : null} - - ) : null} -
-
-
-
- {WINDOW_BACKGROUND_MODES.map((mode) => { - const unsupported = mode === 'blurred' && !supportsBlurredBackground - return ( - - ) - })} + By {themeCredit.credit} + + ) : ( + By {themeCredit.credit} + )} + {themeCredit.description ? ( + + + {themeCredit.description} + + ) : null} + + ) : null} +
+
+
+ { + void handleThemeChange(event.target.value) + }} + className="bottom-bar__select" + > + {themeEntries.map(([id, theme]) => ( + + ))} + + + +
+
+
+ +
-
+
Now Playing
-
-
Audio Source
+
+
+
Startup & Tray
+ {loginItemStatusMessage ? ( + + {loginItemStatusMessage} + + ) : null} +
-
+
+ { - void handleSourceChange(event.target.value) + void setLoginLaunchMode(event.target.value === 'tray' ? 'tray' : 'show') }} - className="bottom-bar__select" + className="bottom-bar__login-select" + title="What Prism should show when opened automatically at login" > - - {visibleSystemSources.map((source) => ( - - ))} - - - - {devices.map((device) => ( - - ))} - + + - -
- - {indicatorLabel} -
-
- - {captureMessage ? ( - <> -
- {captureMessage} -
-
- - {canUseDefaultSource ? ( - - ) : null} - {!captureError && captureNotice ? ( - - ) : null} -
- - ) : null} -
-
- -
- -
-
Rolling Capture
-
-
-
- - {ROLLING_CAPTURE_DURATIONS.map((duration) => ( - - ))} -
- -
- - {rollingCaptureStatusLabel} -
- +
@@ -797,7 +991,7 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
-
+
Performance
@@ -821,30 +1015,6 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
- -
- -
-
Trim
-
-
- - {inputGainDb > 0 ? '+' : ''}{inputGainDb.toFixed(1)}dB - - setInputGain(Number(event.target.value))} - onDoubleClick={() => setInputGain(0)} - /> -
-
-
diff --git a/src/renderer/components/ChannelRoutingMatrix.tsx b/src/renderer/components/ChannelRoutingMatrix.tsx new file mode 100644 index 0000000..3edafaa --- /dev/null +++ b/src/renderer/components/ChannelRoutingMatrix.tsx @@ -0,0 +1,135 @@ +import { useLayoutEffect, useRef, type CSSProperties, type JSX } from 'react' +import { audioCapture } from '../audio/AudioCapture' +import { getHorizontalWheelScrollResult } from '../utils/horizontalWheelScroll' +import type { + CaptureChannelDescriptor, + CaptureChannelRouting, +} from '../../types/capture' + +interface ChannelRoutingMatrixProps { + channels: CaptureChannelDescriptor[] + routing: CaptureChannelRouting + onChange: (routing: CaptureChannelRouting) => void + sourceKey: string | null + activityEnabled?: boolean +} + +export default function ChannelRoutingMatrix({ + channels, + routing, + onChange, + sourceKey, + activityEnabled = true, +}: ChannelRoutingMatrixProps): JSX.Element | null { + const rootRef = useRef(null) + const scrollRef = useRef(null) + useLayoutEffect(() => { + const scroller = scrollRef.current + if (!scroller) return + const onWheel = (event: WheelEvent): void => { + if (event.ctrlKey) return + const result = getHorizontalWheelScrollResult({ + clientWidth: scroller.clientWidth, + scrollWidth: scroller.scrollWidth, + scrollLeft: scroller.scrollLeft, + deltaMode: event.deltaMode, + deltaX: event.deltaX, + deltaY: event.deltaY, + }) + if (!result) return + scroller.scrollLeft = result.nextScrollLeft + event.preventDefault() + event.stopPropagation() + } + scroller.addEventListener('wheel', onWheel, { passive: false }) + return () => scroller.removeEventListener('wheel', onWheel) + }, [channels.length]) + + useLayoutEffect(() => { + const cells = Array.from(rootRef.current?.querySelectorAll('[data-channel-index]') ?? []) + if (!cells.length) return + for (const cell of cells) cell.style.setProperty('--channel-activity-opacity', '0') + if (!activityEnabled) return + let frameId: number | null = null + let lastPaint = -Infinity + + const paint = (now: number): void => { + const snapshot = audioCapture.getChannelActivity(now) + const opacities = snapshot?.sourceKey === sourceKey && snapshot.channelCount === channels.length + ? snapshot.opacities + : null + for (const cell of cells) { + const opacity = opacities?.[Number(cell.dataset.channelIndex)] ?? 0 + cell.style.setProperty('--channel-activity-opacity', opacity.toFixed(4)) + } + } + const tick = (now: number): void => { + if (now - lastPaint >= 1000 / 30) { + paint(now) + lastPaint = now + } + frameId = requestAnimationFrame(tick) + } + const onVisibilityChange = (): void => { + if (frameId !== null) cancelAnimationFrame(frameId) + frameId = null + if (!document.hidden) { + paint(performance.now()) + lastPaint = performance.now() + frameId = requestAnimationFrame(tick) + } + } + onVisibilityChange() + document.addEventListener('visibilitychange', onVisibilityChange) + return () => { + if (frameId !== null) cancelAnimationFrame(frameId) + document.removeEventListener('visibilitychange', onVisibilityChange) + } + }, [activityEnabled, channels, sourceKey]) + + if (channels.length === 0) return null + + const renderRow = ( + side: 'left' | 'right', + rowLabel: 'L' | 'R', + ): JSX.Element => { + const selectedIndex = routing[side] + return ( +
+ + {channels.map((channel) => { + const selected = channel.index === selectedIndex + return ( + + ) + })} +
+ ) + } + + return ( +
+
+
+ {renderRow('left', 'L')} + {renderRow('right', 'R')} +
+
+
+ ) +} diff --git a/src/renderer/components/DesktopReferenceProvider.tsx b/src/renderer/components/DesktopReferenceProvider.tsx new file mode 100644 index 0000000..d729a2f --- /dev/null +++ b/src/renderer/components/DesktopReferenceProvider.tsx @@ -0,0 +1,17 @@ +import { useMemo, type JSX, type ReactNode } from 'react' +import { useSettingsStore } from '../stores/settingsStore' +import { SpectrumReferenceProvider } from './SpectrumReference' +import type { SpectrumReferenceTransport } from '../../types/spectrumReference' + +export function desktopReferenceTransport(): SpectrumReferenceTransport { + return window.electronAPI.referenceTracks +} + +export default function DesktopReferenceProvider({ children }: { children: ReactNode }): JSX.Element { + const reference = useSettingsStore(state => state.scopeSettings.spectrum.reference) + const ownerKey = useSettingsStore(state => state.activeProfileId) + const update = useSettingsStore(state => state.updateScopeSettings) + const transport = useMemo(desktopReferenceTransport, []) + return update('spectrum', { reference: next })}>{children} +} diff --git a/src/renderer/components/DialogApp.tsx b/src/renderer/components/DialogApp.tsx index bed3c3a..265ff5e 100644 --- a/src/renderer/components/DialogApp.tsx +++ b/src/renderer/components/DialogApp.tsx @@ -1,226 +1,154 @@ -import { useState, useEffect, useRef, useCallback, type JSX, type KeyboardEvent } from 'react' +import { useState, useEffect, useLayoutEffect, useRef, useCallback, type JSX, type KeyboardEvent } from 'react' import type { DialogOptions, DialogResult } from '../../types/dialog' +import { applyResolvedThemeToDocument } from '../../shared/themeState' +import { observeDialogLayout } from '../utils/dialogLayout' +import '../styles/dialog.css' export default function DialogApp(): JSX.Element { const [config, setConfig] = useState(null) const [inputValue, setInputValue] = useState('') const inputRef = useRef(null) + const defaultButtonRef = useRef(null) + const copyRef = useRef(null) + const buttonsRef = useRef(null) + const submittedRef = useRef(false) useEffect(() => { - const unsubscribe = window.electronAPI.onDialogConfig((options) => { - setConfig(options) + let disposed = false + let themeChanged = false + const unsubscribe = window.electronAPI.onDialogThemeChanged((theme) => { + themeChanged = true + applyResolvedThemeToDocument({ interface: theme }, document.documentElement.style) + }) + + void window.electronAPI.getDialogConfig().then(({ options, theme }) => { + if (disposed) return + // A theme event received during bootstrap is newer than the requested snapshot. + if (!themeChanged) { + applyResolvedThemeToDocument({ interface: theme }, document.documentElement.style) + } setInputValue(options.defaultValue ?? '') + setConfig(options) + }).catch((error: unknown) => { + if (disposed) return + console.error('Could not initialize profile dialog:', error) + window.electronAPI.close() }) - return unsubscribe + + return () => { + disposed = true + unsubscribe() + } }, []) + useLayoutEffect(() => { + if (!config) return + if (config.type === 'prompt') { + inputRef.current?.focus() + inputRef.current?.select() + } else { + defaultButtonRef.current?.focus() + } + }, [config]) + useEffect(() => { - if (config?.type === 'prompt' && inputRef.current) { - inputRef.current.focus() - inputRef.current.select() + if (!config || !copyRef.current || !buttonsRef.current) return + let lastHeight = 0 + + const reportLayout = (): void => { + if (!copyRef.current || !buttonsRef.current) return + // Measure the unconstrained copy, so a capped window can still scroll its body. + // Include the card border (2px) and transparent window margins (16px). + const height = Math.ceil( + copyRef.current.getBoundingClientRect().height + + buttonsRef.current.getBoundingClientRect().height + 18, + ) + if (height !== lastHeight) { + lastHeight = height + window.electronAPI.reportDialogLayout({ height }) + } } + + return observeDialogLayout([copyRef.current, buttonsRef.current], reportLayout) }, [config]) const submit = useCallback((buttonIndex: number) => { - if (!config) return + if (!config || submittedRef.current) return const isPrimaryPromptSubmit = config.type === 'prompt' && buttonIndex === (config.defaultId ?? 0) - if (isPrimaryPromptSubmit && !inputValue.trim()) { - return - } + if (isPrimaryPromptSubmit && !inputValue.trim()) return const result: DialogResult = { buttonIndex } - if (config.type === 'prompt') { - result.value = inputValue - } + if (config.type === 'prompt') result.value = inputValue + submittedRef.current = true window.electronAPI.sendDialogResult(result) }, [config, inputValue]) - const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (!config) return - if (e.key === 'Enter') { - const defaultId = config.defaultId ?? 0 - submit(defaultId) - } else if (e.key === 'Escape') { - const cancelId = config.cancelId ?? config.buttons.length - 1 - submit(cancelId) + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (!config || event.nativeEvent.isComposing) return + if (event.key === 'Escape') { + event.preventDefault() + submit(config.cancelId ?? config.buttons.length - 1) + } else if (event.key === 'Enter' && !(event.target instanceof HTMLButtonElement)) { + event.preventDefault() + submit(config.defaultId ?? 0) } }, [config, submit]) - if (!config) { - return
- } + if (!config) return
const primaryIndex = config.defaultId ?? 0 - const cancelId = config.cancelId ?? config.buttons.length - 1 const isPromptPrimaryDisabled = config.type === 'prompt' && !inputValue.trim() + // Reverse the DOM order as well as the visual order, keeping original result indices. + const buttons = config.buttons.map((label, index) => ({ label, index })).reverse() return ( -
-
+
+
-
{config.title}
-
{config.message}
- {config.detail && ( -
{config.detail}
- )} - {config.type === 'prompt' && ( - setInputValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.stopPropagation() - submit(primaryIndex) - } - }} - /> - )} +
+

{config.title}

+

{config.message}

+ {config.detail &&

{config.detail}

} + {config.type === 'prompt' && ( + setInputValue(event.target.value)} + /> + )} +
-
- {config.buttons.map((label, i) => ( +
+ {buttons.map(({ label, index }) => ( ))}
-
) } diff --git a/src/renderer/components/DockedSettingsPortal.tsx b/src/renderer/components/DockedSettingsPortal.tsx new file mode 100644 index 0000000..47d9134 --- /dev/null +++ b/src/renderer/components/DockedSettingsPortal.tsx @@ -0,0 +1,94 @@ +import { useLayoutEffect, useRef, useState, type ReactNode } from 'react' +import { createPortal } from 'react-dom' +import { useUiStore } from '../stores/uiStore' + +/** The parent owns these controls and stores; the child is only a DOM surface. */ +export default function DockedSettingsPortal({ children }: { children: ReactNode }): ReactNode { + const childRef = useRef(null) + const lifetime = useRef(0) + const [container, setContainer] = useState(null) + + useLayoutEffect(() => { + const generation = ++lifetime.current + let child = childRef.current + if (!child || child.closed) { + child = window.open('about:blank', 'prism-docked-settings') + childRef.current = child + } + if (!child) { + useUiStore.getState().setSettingsOpen(false) + useUiStore.getState().showBanner({ tone: 'error', message: 'Prism could not open docked settings.', actions: [] }) + return + } + const panelWindow = child + const panelDocument = child.document + panelDocument.title = 'Prism settings' + let root = panelDocument.getElementById('docked-settings') + if (!root) { + const base = panelDocument.createElement('base') + base.href = document.baseURI + panelDocument.head.appendChild(base) + root = panelDocument.createElement('div') + root.id = 'docked-settings' + root.className = 'docked-settings' + panelDocument.body.appendChild(root) + } + const copyStyles = (): void => { + panelDocument.querySelectorAll('[data-prism-copied-style]').forEach(node => node.remove()) + document.querySelectorAll('style, link[rel="stylesheet"]').forEach(node => { + const copy = node.cloneNode(true) as HTMLElement + copy.setAttribute('data-prism-copied-style', '') + panelDocument.head.appendChild(copy) + }) + } + const copyTheme = (): void => { + panelDocument.documentElement.style.cssText = document.documentElement.style.cssText + panelDocument.documentElement.dataset.windowBg = 'solid' + } + copyStyles() + copyTheme() + const styles = new MutationObserver(copyStyles) + styles.observe(document.head, { childList: true, subtree: true, characterData: true }) + const theme = new MutationObserver(copyTheme) + theme.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] }) + let disposed = false + let frame = 0 + const fail = (): void => { + if (disposed) return + useUiStore.getState().setSettingsOpen(false) + useUiStore.getState().showBanner({ tone: 'error', message: 'Prism could not show docked settings.', actions: [] }) + } + const resize = (): void => { + cancelAnimationFrame(frame) + frame = requestAnimationFrame(() => { + if (!disposed && !panelWindow.closed && root) { + // Measure natural content, not the clamped/scrolled viewport. + const content = root.firstElementChild as HTMLElement | null + const height = content?.scrollHeight ?? root.scrollHeight + void window.electronAPI.docking.showSettings(height).then(shown => { if (!shown) fail() }).catch(fail) + } + }) + } + const resizeObserver = new ResizeObserver(resize) + resizeObserver.observe(root) + const contentObserver = new MutationObserver(resize) + contentObserver.observe(root, { subtree: true, childList: true, attributes: true }) + setContainer(root) + void panelDocument.fonts.ready.then(resize) + resize() + return () => { + disposed = true + cancelAnimationFrame(frame) + styles.disconnect() + theme.disconnect() + resizeObserver.disconnect() + contentObserver.disconnect() + // React StrictMode immediately remounts effects. Reuse its child window. + queueMicrotask(() => { + if (lifetime.current === generation && !panelWindow.closed) panelWindow.close() + }) + } + }, []) + + return container ? createPortal(
{children}
, container) : null +} diff --git a/src/renderer/components/LocalNowPlayingProviderDetails.tsx b/src/renderer/components/LocalNowPlayingProviderDetails.tsx new file mode 100644 index 0000000..0a51c0c --- /dev/null +++ b/src/renderer/components/LocalNowPlayingProviderDetails.tsx @@ -0,0 +1,42 @@ +import type { JSX } from 'react' +import { NOW_PLAYING_PROVIDER_DEFINITIONS, type NowPlayingProviderId, type NowPlayingProviderState } from '../../types/nowPlaying' +import { getLocalAvailabilityDetail, getLocalProviderCopy } from '../utils/localNowPlaying' + +interface Props { + providerId: NowPlayingProviderId + provider: NowPlayingProviderState + platform: string + retryProvider: (providerId: NowPlayingProviderId) => Promise + onError: (message: string) => void +} + +export function LocalNowPlayingProviderDetails({ providerId, provider, platform, retryProvider, onError }: Props): JSX.Element { + const definition = NOW_PLAYING_PROVIDER_DEFINITIONS[providerId] + return ( +
+
{definition.description}
+
{getLocalProviderCopy(platform, providerId)}
+
+ +
+ {!provider.available && ( +
{getLocalAvailabilityDetail(platform, providerId)}
+ )} + {(provider.lastError || provider.lastControlError) && ( +
+ {provider.lastError ?? provider.lastControlError} +
+ )} +
+ ) +} diff --git a/src/renderer/components/NowPlayingConfigWindow.tsx b/src/renderer/components/NowPlayingConfigWindow.tsx index 698749e..30aeaad 100644 --- a/src/renderer/components/NowPlayingConfigWindow.tsx +++ b/src/renderer/components/NowPlayingConfigWindow.tsx @@ -1,3 +1,5 @@ +import { LocalNowPlayingProviderDetails } from './LocalNowPlayingProviderDetails' +import { getLocalIntegrationLabel, getLocalUnavailableMetaText } from '../utils/localNowPlaying' import { useCallback, useEffect, @@ -95,82 +97,6 @@ function getErrorMessage(error: unknown, fallback: string): string { : fallback } -function isMacOSPlatform(platform: string): boolean { - return platform === 'darwin' -} - -function isLinuxPlatform(platform: string): boolean { - return platform === 'linux' -} - -function isWindowsPlatform(platform: string): boolean { - return platform === 'win32' -} - -function getSpotifyIntegrationLabel(platform: string): string { - if (isMacOSPlatform(platform)) { - return 'Local macOS app' - } - - if (isLinuxPlatform(platform)) { - return 'Local Linux MPRIS' - } - - if (isWindowsPlatform(platform)) { - return 'Local Windows media session' - } - - return 'Local Spotify integration' -} - -function getSpotifyUnavailableMetaText(platform: string): string { - if (isMacOSPlatform(platform)) { - return 'Local macOS app unavailable' - } - - if (isLinuxPlatform(platform)) { - return 'Local Linux MPRIS unavailable' - } - - if (isWindowsPlatform(platform)) { - return 'Local Windows media session unavailable' - } - - return 'Local Spotify integration unavailable' -} - -function getSpotifyAvailabilityDetail(platform: string): string { - if (isMacOSPlatform(platform)) { - return 'Install Spotify.app in /Applications to enable this provider.' - } - - if (isLinuxPlatform(platform)) { - return 'This provider needs a Linux desktop session with Spotify MPRIS access.' - } - - if (isWindowsPlatform(platform)) { - return 'This provider needs Windows system media controls to expose a Spotify session.' - } - - return 'This provider is currently available on macOS, Linux, and Windows.' -} - -function getSpotifyProviderCopy(platform: string): string { - if (isMacOSPlatform(platform)) { - return 'No Spotify developer account or API setup is required. Prism reads the local Spotify macOS app directly.' - } - - if (isLinuxPlatform(platform)) { - return 'No Spotify developer account or API setup is required. Prism reads Spotify through the local Linux MPRIS session.' - } - - if (isWindowsPlatform(platform)) { - return 'No Spotify developer account or API setup is required. Prism reads Spotify through the local Windows media session.' - } - - return 'No Spotify developer account or API setup is required. On supported systems, Prism reads the local Spotify app directly.' -} - function getProviderStatusLabel( definition: NowPlayingProviderDefinition, provider: NowPlayingProviderState, @@ -211,8 +137,8 @@ function getProviderMetaText( } if (!provider.available) { - return provider.providerId === 'spotify' - ? getSpotifyUnavailableMetaText(platform) + return definition.authMode === 'local' + ? getLocalUnavailableMetaText(platform, provider.providerId) : 'Unavailable on this device' } @@ -221,10 +147,10 @@ function getProviderMetaText( } if (definition.authMode === 'local') { - const integrationLabel = getSpotifyIntegrationLabel(platform) + const integrationLabel = getLocalIntegrationLabel(platform, provider.providerId) switch (provider.connectionState) { case 'disabled': - return `${integrationLabel} · Waiting for Spotify` + return `${integrationLabel} · Waiting for ${definition.label}` case 'connecting': return `${integrationLabel} · Checking playback` case 'connected': @@ -234,7 +160,7 @@ function getProviderMetaText( case 'error': return `${integrationLabel} · Needs attention` case 'unavailable': - return getSpotifyUnavailableMetaText(platform) + return getLocalUnavailableMetaText(platform, provider.providerId) } } @@ -475,7 +401,7 @@ export default function NowPlayingConfigWindow(): JSX.Element {
{nowPlayingState.onboardingRequired - ? 'Start with Astra or the local Spotify integration. TIDAL stays visible here for future priority.' + ? 'Connect Astra or use local Spotify or TIDAL playback. Drag providers to set their priority.' : 'The highest configured provider that starts playing takes over immediately.'}
@@ -651,42 +577,13 @@ export default function NowPlayingConfigWindow(): JSX.Element {
) : ( -
-
- {definition.description} -
-
- {getSpotifyProviderCopy(platform)} -
-
- -
- {!provider.available ? ( -
- {getSpotifyAvailabilityDetail(platform)} -
- ) : null} - {provider.lastError || provider.lastControlError ? ( -
- {provider.lastError ?? provider.lastControlError} -
- ) : null} -
+ showBanner({ tone: 'error', message, actions: [] })} + /> ) ) : null}
diff --git a/src/renderer/components/ScopeMeasurementOverlay.tsx b/src/renderer/components/ScopeMeasurementOverlay.tsx index 6b1afa5..c6fec03 100644 --- a/src/renderer/components/ScopeMeasurementOverlay.tsx +++ b/src/renderer/components/ScopeMeasurementOverlay.tsx @@ -10,12 +10,14 @@ import { type RefObject, } from 'react' import type { ScopeDisplayRotation } from '../../types/scopeTransform' +import type { LinkedAnalysisProjection } from '../../types/analysis' import { resolveMeasurementReadoutPosition, resolveMeasurementSourcePoint, type ActiveScopeMeasurement, type ScopeMeasurementSource, } from '../scopeMeasurement' +import { transformNormalizedScopePoint } from '../scopeCanvasTransform' interface ScopeMeasurementControllerOptions { containerRef: RefObject @@ -24,6 +26,12 @@ interface ScopeMeasurementControllerOptions { rotation: ScopeDisplayRotation mirrorHorizontal: boolean onActiveChange?: (active: boolean) => void + onMeasurementChange?: (change: ScopeMeasurementChange) => void +} + +export interface ScopeMeasurementChange { + interactionId: string + measurement: ActiveScopeMeasurement | null } interface ScopeMeasurementPointerBindings { @@ -47,14 +55,18 @@ export function useScopeMeasurement({ rotation, mirrorHorizontal, onActiveChange, + onMeasurementChange, }: ScopeMeasurementControllerOptions): ScopeMeasurementController { const [measurement, setMeasurement] = useState(null) const activePointerIdRef = useRef(null) const getSourceRef = useRef(getSource) const onActiveChangeRef = useRef(onActiveChange) + const onMeasurementChangeRef = useRef(onMeasurementChange) + const interactionIdRef = useRef(null) const transformRef = useRef({ rotation, mirrorHorizontal }) getSourceRef.current = getSource onActiveChangeRef.current = onActiveChange + onMeasurementChangeRef.current = onMeasurementChange transformRef.current = { rotation, mirrorHorizontal } const setActive = useCallback((active: boolean): void => { @@ -66,11 +78,16 @@ export function useScopeMeasurement({ const pointerId = activePointerIdRef.current if (pointerId === null) return activePointerIdRef.current = null + const interactionId = interactionIdRef.current + interactionIdRef.current = null const container = containerRef.current if (container?.hasPointerCapture(pointerId)) { container.releasePointerCapture(pointerId) } setMeasurement(null) + if (interactionId) { + onMeasurementChangeRef.current?.({ interactionId, measurement: null }) + } setActive(false) }, [containerRef, setActive]) @@ -89,18 +106,25 @@ export function useScopeMeasurement({ transformRef.current.rotation, transformRef.current.mirrorHorizontal, ) - setMeasurement({ + const interactionId = interactionIdRef.current + if (!interactionId) return + const nextMeasurement: ActiveScopeMeasurement = { pointerId: event.pointerId, viewportPoint, sourcePoint, measurement: source.getMeasurementAt(sourcePoint), - }) + } + setMeasurement(nextMeasurement) + onMeasurementChangeRef.current?.({ interactionId, measurement: nextMeasurement }) }, [containerRef]) const handlePointerDown = useCallback((event: ReactPointerEvent): void => { if (!enabled || event.altKey || event.button !== 0 || !event.isPrimary || activePointerIdRef.current !== null) return if (!getSourceRef.current()) return event.preventDefault() + interactionIdRef.current = typeof globalThis.crypto?.randomUUID === 'function' + ? globalThis.crypto.randomUUID() + : `${Date.now().toString(36)}-${event.pointerId.toString(36)}-${Math.random().toString(36).slice(2)}` activePointerIdRef.current = event.pointerId event.currentTarget.setPointerCapture(event.pointerId) setActive(true) @@ -219,3 +243,103 @@ export function ScopeMeasurementOverlay({
) } + +interface ScopeLinkedAnalysisOverlayProps { + containerRef: RefObject + projection: LinkedAnalysisProjection | null + rotation: ScopeDisplayRotation + mirrorHorizontal: boolean +} + +interface ViewportGuide { + axis: 'horizontal' | 'vertical' + position: number +} + +function transformLinkedGuide( + guide: LinkedAnalysisProjection['guides'][number], + rotation: ScopeDisplayRotation, + mirrorHorizontal: boolean, +): ViewportGuide { + const from = transformNormalizedScopePoint(guide.from, rotation, mirrorHorizontal) + const to = transformNormalizedScopePoint(guide.to, rotation, mirrorHorizontal) + if (Math.abs(from.x - to.x) <= Math.abs(from.y - to.y)) { + return { axis: 'vertical', position: (from.x + to.x) / 2 } + } + return { axis: 'horizontal', position: (from.y + to.y) / 2 } +} + +export function ScopeLinkedAnalysisOverlay({ + containerRef, + projection, + rotation, + mirrorHorizontal, +}: ScopeLinkedAnalysisOverlayProps): JSX.Element | null { + const labelRef = useRef(null) + const [labelSize, setLabelSize] = useState({ width: 90, height: 26 }) + const [viewportSize, setViewportSize] = useState({ width: 1, height: 1 }) + const active = projection !== null + + useLayoutEffect(() => { + if (!active) return + const label = labelRef.current + const container = containerRef.current + if (!label || !container) return + const measure = (): void => { + const nextLabelSize = { width: label.offsetWidth, height: label.offsetHeight } + setLabelSize((previous) => previous.width === nextLabelSize.width + && previous.height === nextLabelSize.height ? previous : nextLabelSize) + const rect = container.getBoundingClientRect() + const nextViewportSize = { width: rect.width, height: rect.height } + setViewportSize((previous) => previous.width === nextViewportSize.width + && previous.height === nextViewportSize.height ? previous : nextViewportSize) + } + measure() + const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure) + observer?.observe(label) + observer?.observe(container) + return () => observer?.disconnect() + }, [active, containerRef]) + + if (!projection) return null + const guides = projection.guides.map((guide) => transformLinkedGuide( + guide, + rotation, + mirrorHorizontal, + )) + const anchor = guides.reduce((sum, guide) => { + if (guide.axis === 'vertical') return { x: sum.x + guide.position, y: sum.y + 0.5 } + return { x: sum.x + 0.5, y: sum.y + guide.position } + }, { x: 0, y: 0 }) + const divisor = Math.max(1, guides.length) + const pointer = { + x: (anchor.x / divisor) * viewportSize.width, + y: (anchor.y / divisor) * viewportSize.height, + } + const labelPosition = resolveMeasurementReadoutPosition( + pointer, + viewportSize, + labelSize, + ) + + return ( + + ) +} diff --git a/src/renderer/components/ScopeModule.tsx b/src/renderer/components/ScopeModule.tsx index ccd9796..fc78e16 100644 --- a/src/renderer/components/ScopeModule.tsx +++ b/src/renderer/components/ScopeModule.tsx @@ -1,5 +1,13 @@ +import { ReferenceImportStatus, useReferenceDrop, useSpectrumReference } from './SpectrumReference' +import { Waterfall, type WaterfallDataSource } from '../visualizers/Waterfall' import { useCallback, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type JSX } from 'react' import { isTransformableScopeKind, type ScopeKind } from '../../types/scope' +import { + isLinkedAnalysisCompatibleScopeKind, + type LinkedAnalysisMessage, + type LinkedAnalysisProbe, + type LinkedAnalysisProjection, +} from '../../types/analysis' import type { ScopeSettings } from '../../types/settings' import { nominalFrequencyBoundsForRange } from '../../types/frequencyScale' import type { ScopeDisplayRotation } from '../../types/scopeTransform' @@ -12,6 +20,7 @@ import type { ResolvedSpectrumTheme, ResolvedVectorscopeTheme, ResolvedVUMeterTheme, + ResolvedWaterfallTheme, ResolvedWaveformTheme, } from '../../types/theme' import type { SpectrumPeakInfo } from '../../types/spectrum' @@ -28,10 +37,11 @@ import { LUFSMeter, type LUFSMeterDataSource } from '../visualizers/LUFSMeter' import { Waveform, type WaveformDataSource } from '../visualizers/Waveform' import type { FrameScheduler } from '../visualizers/frameScheduler' import { + ScopeLinkedAnalysisOverlay, ScopeMeasurementOverlay, useScopeMeasurement, } from './ScopeMeasurementOverlay' -import type { ScopeMeasurementSource } from '../scopeMeasurement' +import type { ActiveScopeMeasurement, ScopeMeasurementSource } from '../scopeMeasurement' import { getScopeCanvasTransformStyle, isSameScopeCanvasLayout, @@ -47,6 +57,7 @@ type ScopeModuleTheme = | ResolvedSpectrogramTheme | ResolvedVUMeterTheme | ResolvedLUFSMeterTheme + | ResolvedWaterfallTheme | ResolvedWaveformTheme | ResolvedAstraTheme @@ -56,7 +67,11 @@ interface ScopeModuleProps { settings?: ScopeSettings[ScopeKind] frameScheduler?: FrameScheduler onMeasurementActiveChange?: (active: boolean) => void + linkedAnalysisEnabled?: boolean + linkedAnalysisProbe?: LinkedAnalysisProbe | null + onLinkedAnalysisMessage?: (message: LinkedAnalysisMessage) => void dataSource?: + | WaterfallDataSource | SpectrumAnalyzerDataSource | OscilloscopeDataSource | VectorscopeDataSource @@ -74,6 +89,7 @@ interface Visualizer { setOptions(options: Record): void getMeasurementAt?: ScopeMeasurementSource['getMeasurementAt'] setMeasurementActive?: ScopeMeasurementSource['setMeasurementActive'] + getLinkedAnalysisProjection?: (probe: LinkedAnalysisProbe) => LinkedAnalysisProjection | null } const SPECTRUM_PEAK_OVERLAY_MARGIN_PX = 10 @@ -167,13 +183,20 @@ export function scopeSettingsToOptions( theme: ScopeModuleTheme, ): Record { switch (kind) { + case 'waterfall': { + const s = settings as ScopeSettings['waterfall'] + const t = theme as ResolvedWaterfallTheme + return { ...s, lineColor: t.line, heatColors: t.heatColors, backgroundColor: t.background, gridColor: t.guides, labelColor: t.labels } + } case 'spectrum': { const s = settings as ScopeSettings['spectrum'] const t = theme as ResolvedSpectrumTheme const range = nominalFrequencyBoundsForRange(s.frequencyRangeMode) return { + reference: s.reference ?? null, lineColor: t.line, secondaryLineColor: t.sideLine, + referenceLineColor: t.referenceLine, gradientColors: t.fillGradient, heatColors: t.heatColors, heatBaseColor: t.heatBase, @@ -253,6 +276,7 @@ export function scopeSettingsToOptions( showGrid: s.showGrid, orientation: 'horizontal', colorScheme: s.colorScheme, + timelineUnit: s.timelineUnit, } } case 'vumeter': { @@ -297,6 +321,7 @@ export function scopeSettingsToOptions( lineColor: t.line, gridMajorColor: t.guides, gridMinorColor: t.guidesSecondary, + labelColor: t.line, bandColors: { low: t.bandLow, mid: t.bandMid, @@ -305,6 +330,7 @@ export function scopeSettingsToOptions( mode: s.mode, scrollSpeed: s.scrollSpeed, multiband: s.multiband, + timelineUnit: s.timelineUnit, } } case 'nowPlaying': @@ -354,6 +380,8 @@ function createVisualizer( frameScheduler, } switch (scopeKind) { + case 'waterfall': + return new Waterfall(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as WaterfallDataSource } : {}) }) case 'spectrum': return new SpectrumAnalyzer(canvas, { ...opts, @@ -404,9 +432,14 @@ export default function ScopeModule({ settings, frameScheduler, onMeasurementActiveChange, + linkedAnalysisEnabled = false, + linkedAnalysisProbe = null, + onLinkedAnalysisMessage, dataSource, }: ScopeModuleProps): JSX.Element { const containerRef = useRef(null) + const referenceController = useSpectrumReference() + const referenceDrop = useReferenceDrop(scopeKind === 'spectrum') const canvasRef = useRef(null) const visualizerRef = useRef(null) const initializedRef = useRef(false) @@ -417,6 +450,12 @@ export default function ScopeModule({ const peakOverlayRef = useRef(null) const [spectrumPeakInfo, setSpectrumPeakInfo] = useState(null) const [peakOverlaySize, setPeakOverlaySize] = useState(null) + const [linkedProjection, setLinkedProjection] = useState(null) + const linkedAnalysisProbeRef = useRef(linkedAnalysisProbe) + const refreshLinkedProjectionRef = useRef<() => void>(() => {}) + const onMeasurementActiveChangeRef = useRef(onMeasurementActiveChange) + linkedAnalysisProbeRef.current = linkedAnalysisProbe + onMeasurementActiveChangeRef.current = onMeasurementActiveChange const storeSettings = useSettingsStore((s) => s.scopeSettings[scopeKind]) const activeTheme = useThemeStore((s) => s.activeTheme) @@ -440,6 +479,26 @@ export default function ScopeModule({ || scopeKind === 'spectrogram' || scopeKind === 'oscilloscope' || scopeKind === 'waveform' + const handleMeasurementChange = useCallback((change: { + interactionId: string + measurement: ActiveScopeMeasurement | null + }): void => { + if (!linkedAnalysisEnabled || !isLinkedAnalysisCompatibleScopeKind(scopeKind)) return + if (change.measurement) { + onLinkedAnalysisMessage?.({ + active: true, + interactionId: change.interactionId, + sourceKind: scopeKind, + dimensions: change.measurement.measurement.dimensions, + }) + return + } + onLinkedAnalysisMessage?.({ + active: false, + interactionId: change.interactionId, + sourceKind: scopeKind, + }) + }, [linkedAnalysisEnabled, onLinkedAnalysisMessage, scopeKind]) const measurementController = useScopeMeasurement({ containerRef, enabled: measurementEnabled, @@ -451,8 +510,28 @@ export default function ScopeModule({ ? visualizer as ScopeMeasurementSource : null }, - onActiveChange: onMeasurementActiveChange, + onMeasurementChange: handleMeasurementChange, }) + const refreshLinkedProjection = useCallback((): void => { + const probe = linkedAnalysisProbeRef.current + const visualizer = visualizerRef.current + const nextProjection = linkedAnalysisEnabled + && probe + && probe.sourceKind !== scopeKind + && visualizer?.getLinkedAnalysisProjection + ? visualizer.getLinkedAnalysisProjection(probe) + : null + setLinkedProjection(nextProjection) + }, [linkedAnalysisEnabled, scopeKind]) + refreshLinkedProjectionRef.current = refreshLinkedProjection + const analysisActive = measurementController.active || linkedProjection !== null + + useEffect(() => { + onMeasurementActiveChangeRef.current?.(analysisActive) + return () => { + if (analysisActive) onMeasurementActiveChangeRef.current?.(false) + } + }, [analysisActive]) useEffect(() => { if (!captureSpectrumPeakInfo) { @@ -527,6 +606,7 @@ export default function ScopeModule({ visualizerRef.current = viz viz.start() + refreshLinkedProjectionRef.current() requestAnimationFrame(() => { initializedRef.current = true @@ -543,7 +623,7 @@ export default function ScopeModule({ }, [dataSource, frameScheduler, handleSpectrumPeakInfo, scopeKind]) useEffect(() => { - if (!visualizerRef.current || !initializedRef.current) return + if (!visualizerRef.current) return const opts = { ...applyWindowBackgroundAlphaToOptions( scopeSettingsToOptions(scopeKind, mySettings, myTheme), @@ -561,6 +641,10 @@ export default function ScopeModule({ visualizerRef.current.setOptions(opts) }, [captureSpectrumPeakInfo, dataSource, frameScheduler, handleSpectrumPeakInfo, mySettings, myTheme, scopeKind, windowBgAlpha]) + useEffect(() => { + refreshLinkedProjection() + }, [linkedAnalysisProbe, mySettings, refreshLinkedProjection]) + useLayoutEffect(() => { const container = containerRef.current const canvas = canvasRef.current @@ -614,6 +698,7 @@ export default function ScopeModule({ appliedResizeRef.current = nextResize visualizerRef.current?.resize() + refreshLinkedProjectionRef.current() } const scheduleResize = (): void => { @@ -647,6 +732,14 @@ export default function ScopeModule({ } }, [rotation]) + useEffect(() => { + if (scopeKind !== 'spectrum') return + const analyzer = visualizerRef.current as SpectrumAnalyzer | null + analyzer?.setOptions({ referencePreview: referenceController?.state.preview ?? null, + referenceImporting: referenceController?.busy ?? false, + onReferenceLevel: referenceController?.reportLevel ?? (() => {}) }) + }, [scopeKind, referenceController?.state.preview, referenceController?.busy, referenceController?.reportLevel]) + const spectrumPeakOverlayStyle = scopeKind === 'spectrum' && spectrumPeakMode === 'following' && spectrumPeakInfo @@ -663,11 +756,13 @@ export default function ScopeModule({
+ {scopeKind === 'spectrum' && } + {referenceDrop.dragging &&
Drop audio to use as reference
} - {!measurementController.active && scopeKind === 'spectrum' && spectrumPeakMode !== 'off' && spectrumPeakInfo && ( + {!measurementController.active && ( + + )} + {!analysisActive && scopeKind === 'spectrum' && spectrumPeakMode !== 'off' && spectrumPeakInfo && (
- {formatSpectrumPeakDbfs(spectrumPeakInfo.dbfs)} + {spectrumPeakInfo.deltaDb !== undefined ? `${spectrumPeakInfo.deltaDb >= 0 ? '+' : ''}${spectrumPeakInfo.deltaDb.toFixed(1)} dB relative` : formatSpectrumPeakDbfs(spectrumPeakInfo.dbfs)} / {formatSpectrumPeakFrequency(spectrumPeakInfo.frequencyHz)} / diff --git a/src/renderer/components/ScopePopoutBridge.tsx b/src/renderer/components/ScopePopoutBridge.tsx index 6385fcc..1c3b701 100644 --- a/src/renderer/components/ScopePopoutBridge.tsx +++ b/src/renderer/components/ScopePopoutBridge.tsx @@ -30,6 +30,8 @@ function buildConsumerDemand(kind: AudioScopeKind): Record s.hiddenScopes) const scopePopouts = useSettingsStore((s) => s.scopePopouts) const scopeSettings = useSettingsStore((s) => s.scopeSettings) + const analysisSettings = useSettingsStore((s) => s.analysisSettings) const popInScope = useSettingsStore((s) => s.popInScope) const updatePopoutBounds = useSettingsStore((s) => s.updatePopoutBounds) const updateScopeSettings = useSettingsStore((s) => s.updateScopeSettings) @@ -114,10 +116,11 @@ export default function ScopePopoutBridge(): null { interfaceTheme: activeTheme.interface, scopeTheme: activeTheme[kind], settings: scopeSettings[kind], + analysisSettings, } window.electronAPI.sendScopePopoutSnapshot(snapshot) } - }, [activePopoutKinds, activeTheme, scopeSettings]) + }, [activePopoutKinds, activeTheme, analysisSettings, scopeSettings]) useEffect(() => { const sessionState = toPopoutSessionState(audioRouter.getSessionState()) @@ -150,6 +153,7 @@ export default function ScopePopoutBridge(): null { interfaceTheme: useThemeStore.getState().activeTheme.interface, scopeTheme: useThemeStore.getState().activeTheme[kind], settings: useSettingsStore.getState().scopeSettings[kind], + analysisSettings: useSettingsStore.getState().analysisSettings, }) if (isAudioScopeKind(kind)) { window.electronAPI.sendScopePopoutSession(kind, toPopoutSessionState(audioRouter.getSessionState())) diff --git a/src/renderer/components/ScopeSettingsSection.tsx b/src/renderer/components/ScopeSettingsSection.tsx index 52780ae..f5de086 100644 --- a/src/renderer/components/ScopeSettingsSection.tsx +++ b/src/renderer/components/ScopeSettingsSection.tsx @@ -1,3 +1,4 @@ +import { SpectrumReferenceControls } from './SpectrumReference' import { useState, type CSSProperties, type JSX, type ReactNode } from 'react' import type { ScopeKind } from '../../types/scope' import { SCOPE_LABELS, isTransformableScopeKind } from '../../types/scope' @@ -82,6 +83,10 @@ function appendTransformSummary( export function scopeSummary(kind: ScopeKind, settings: ScopeSettings[ScopeKind]): string { switch (kind) { + case 'waterfall': { + const s = settings as ScopeSettings['waterfall'] + return `Spectrum history · ${s.historySeconds}s · ${s.density} · ${s.colorMode === 'heat' ? 'Heat' : 'Theme'}` + } case 'spectrum': { const scopeSettings = settings as ScopeSettings['spectrum'] const summary = `${scopeSettings.scaleMode.toUpperCase()} · ${frequencyRangeLabel(scopeSettings.frequencyRangeMode)} · ${scopeSettings.heatmap ? 'Heat' : 'Fill'} · FFT ${scopeSettings.fftSize}` @@ -314,11 +319,14 @@ interface ScopeSettingsSectionProps { onUpdate: (kind: K, partial: Partial) => void } +let spectrumSettingsTab: 'general' | 'reference' = 'general' + export default function ScopeSettingsSection({ kind, settings, onUpdate, }: ScopeSettingsSectionProps): JSX.Element { + const [tab, setTab] = useState(spectrumSettingsTab) return (
@@ -326,7 +334,49 @@ export default function ScopeSettingsSection({
{scopeSummary(kind, settings)}
-
+ {kind === 'spectrum' &&
+ {(['general', 'reference'] as const).map(value => )} +
} + {kind === 'spectrum' && tab === 'reference' &&
} +