From ecf13c28577ef3851597fb2e94276c7a0a019dc5 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:08:26 -0400 Subject: [PATCH] initial concept --- .gitignore | 27 + electron-vite.config.ts | 14 + native/binding.gyp | 42 + native/src/dsp_utils.cpp | 433 + native/src/dsp_utils.h | 91 + native/src/main.cpp | 300 + native/src/oscilloscope.cpp | 331 + native/src/oscilloscope.h | 85 + native/src/spectrum.cpp | 132 + native/src/spectrum.h | 45 + native/src/vectorscope.cpp | 110 + native/src/vectorscope.h | 66 + package-lock.json | 7938 +++++++++++++++++ package.json | 86 + src/main/index.ts | 108 + src/preload/index.ts | 32 + src/renderer/App.tsx | 122 + src/renderer/audio/AudioCapture.ts | 176 + src/renderer/audio/AudioRouter.ts | 165 + src/renderer/audio/native/index.ts | 142 + .../native/oscilloscopeDisplaySamples.ts | 25 + src/renderer/audio/native/visualizer-dsp.d.ts | 74 + src/renderer/components/ScopeModule.tsx | 105 + src/renderer/components/Strip.tsx | 21 + src/renderer/env.d.ts | 20 + src/renderer/index.html | 12 + src/renderer/main.tsx | 14 + src/renderer/public/capture-worklet.js | 20 + src/renderer/stores/audioStore.ts | 66 + src/renderer/styles/globals.css | 54 + src/renderer/visualizers/LUFSMeter.ts | 467 + src/renderer/visualizers/Oscilloscope.ts | 344 + src/renderer/visualizers/Spectrogram.ts | 691 ++ src/renderer/visualizers/SpectrumAnalyzer.ts | 514 ++ src/renderer/visualizers/VUMeter.ts | 610 ++ src/renderer/visualizers/Vectorscope.ts | 336 + src/renderer/visualizers/Waveform.ts | 416 + src/renderer/visualizers/multibandSplitter.ts | 279 + src/renderer/visualizers/vectorscopeGrids.ts | 321 + src/types/lufsmeter.ts | 9 + src/types/scope.ts | 11 + src/types/spectrogram.ts | 38 + src/types/spectrum.ts | 37 + src/types/vumeter.ts | 16 + src/types/waveform.ts | 29 + tsconfig.json | 24 + tsconfig.node.json | 11 + 47 files changed, 15009 insertions(+) create mode 100644 .gitignore create mode 100644 electron-vite.config.ts create mode 100644 native/binding.gyp create mode 100644 native/src/dsp_utils.cpp create mode 100644 native/src/dsp_utils.h create mode 100644 native/src/main.cpp create mode 100644 native/src/oscilloscope.cpp create mode 100644 native/src/oscilloscope.h create mode 100644 native/src/spectrum.cpp create mode 100644 native/src/spectrum.h create mode 100644 native/src/vectorscope.cpp create mode 100644 native/src/vectorscope.h create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/main/index.ts create mode 100644 src/preload/index.ts create mode 100644 src/renderer/App.tsx create mode 100644 src/renderer/audio/AudioCapture.ts create mode 100644 src/renderer/audio/AudioRouter.ts create mode 100644 src/renderer/audio/native/index.ts create mode 100644 src/renderer/audio/native/oscilloscopeDisplaySamples.ts create mode 100644 src/renderer/audio/native/visualizer-dsp.d.ts create mode 100644 src/renderer/components/ScopeModule.tsx create mode 100644 src/renderer/components/Strip.tsx create mode 100644 src/renderer/env.d.ts create mode 100644 src/renderer/index.html create mode 100644 src/renderer/main.tsx create mode 100644 src/renderer/public/capture-worklet.js create mode 100644 src/renderer/stores/audioStore.ts create mode 100644 src/renderer/styles/globals.css create mode 100644 src/renderer/visualizers/LUFSMeter.ts create mode 100644 src/renderer/visualizers/Oscilloscope.ts create mode 100644 src/renderer/visualizers/Spectrogram.ts create mode 100644 src/renderer/visualizers/SpectrumAnalyzer.ts create mode 100644 src/renderer/visualizers/VUMeter.ts create mode 100644 src/renderer/visualizers/Vectorscope.ts create mode 100644 src/renderer/visualizers/Waveform.ts create mode 100644 src/renderer/visualizers/multibandSplitter.ts create mode 100644 src/renderer/visualizers/vectorscopeGrids.ts create mode 100644 src/types/lufsmeter.ts create mode 100644 src/types/scope.ts create mode 100644 src/types/spectrogram.ts create mode 100644 src/types/spectrum.ts create mode 100644 src/types/vumeter.ts create mode 100644 src/types/waveform.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f95701 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Build outputs +out/ +dist/ +native/build/ + +# Electron +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Environment +.env +.env.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/electron-vite.config.ts b/electron-vite.config.ts new file mode 100644 index 0000000..9bf7950 --- /dev/null +++ b/electron-vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()] + }, + preload: { + plugins: [externalizeDepsPlugin()] + }, + renderer: { + plugins: [react()] + } +}) diff --git a/native/binding.gyp b/native/binding.gyp new file mode 100644 index 0000000..27b2860 --- /dev/null +++ b/native/binding.gyp @@ -0,0 +1,42 @@ +{ + "targets": [ + { + "target_name": "visualizer_dsp", + "cflags!": ["-fno-exceptions"], + "cflags_cc!": ["-fno-exceptions"], + "cflags_cc": ["-std=c++17", "-O3", "-ffast-math"], + "sources": [ + "src/main.cpp", + "src/oscilloscope.cpp", + "src/spectrum.cpp", + "src/vectorscope.cpp", + "src/dsp_utils.cpp" + ], + "include_dirs": [ + " +#include +#include + +namespace DSP { + +// FFT Implementation +FFT::FFT(size_t size) : size_(size) { + // Precompute twiddle factors + twiddles_.resize(size / 2); + for (size_t i = 0; i < size / 2; i++) { + float angle = -2.0f * M_PI * i / size; + twiddles_[i] = std::complex(cosf(angle), sinf(angle)); + } + buffer_.resize(size); + scratch_.resize(size); +} + +void FFT::bitReverse(std::complex* data) { + size_t n = size_; + for (size_t i = 1, j = 0; i < n; i++) { + size_t bit = n >> 1; + while (j & bit) { + j ^= bit; + bit >>= 1; + } + j ^= bit; + if (i < j) { + std::swap(data[i], data[j]); + } + } +} + +void FFT::forward(const float* input, std::complex* output) { + // Copy input to internal buffer + for (size_t i = 0; i < size_; i++) { + buffer_[i] = std::complex(input[i], 0.0f); + } + + bitReverse(buffer_.data()); + + // Cooley-Tukey FFT + for (size_t len = 2; len <= size_; len *= 2) { + size_t halfLen = len / 2; + size_t step = size_ / len; + for (size_t i = 0; i < size_; i += len) { + for (size_t j = 0; j < halfLen; j++) { + std::complex t = twiddles_[j * step] * buffer_[i + j + halfLen]; + buffer_[i + j + halfLen] = buffer_[i + j] - t; + buffer_[i + j] = buffer_[i + j] + t; + } + } + } + + memcpy(output, buffer_.data(), size_ * sizeof(std::complex)); +} + +void FFT::forward(const float* input, float* magnitudes) { + // Use scratch buffer for complex output to avoid allocation + forward(input, scratch_.data()); + + // Calculate magnitudes (only first half is useful) + // Scale by 2/N for correct magnitude + float scale = 2.0f / size_; + for (size_t i = 0; i < size_ / 2; i++) { + magnitudes[i] = std::abs(scratch_[i]) * scale; + } +} + +// BiquadFilter Implementation +BiquadFilter::BiquadFilter() + : b0_(1), b1_(0), b2_(0), a1_(0), a2_(0) + , x1_(0), x2_(0), y1_(0), y2_(0) {} + +void BiquadFilter::setLowpass(float frequency, float sampleRate, float Q) { + float omega = 2.0f * M_PI * frequency / sampleRate; + float sinOmega = sinf(omega); + float cosOmega = cosf(omega); + float alpha = sinOmega / (2.0f * Q); + + float a0 = 1.0f + alpha; + b0_ = (1.0f - cosOmega) / 2.0f / a0; + b1_ = (1.0f - cosOmega) / a0; + b2_ = (1.0f - cosOmega) / 2.0f / a0; + a1_ = -2.0f * cosOmega / a0; + a2_ = (1.0f - alpha) / a0; +} + +void BiquadFilter::setBandpass(float frequency, float sampleRate, float Q) { + float omega = 2.0f * M_PI * frequency / sampleRate; + float sinOmega = sinf(omega); + float cosOmega = cosf(omega); + float alpha = sinOmega / (2.0f * Q); + + float a0 = 1.0f + alpha; + b0_ = alpha / a0; + b1_ = 0.0f; + b2_ = -alpha / a0; + a1_ = -2.0f * cosOmega / a0; + a2_ = (1.0f - alpha) / a0; +} + +void BiquadFilter::setHighShelf(float frequency, float sampleRate, float gainDB, float Q) { + float A = powf(10.0f, gainDB / 40.0f); // sqrt(10^(dB/20)) + float omega = 2.0f * M_PI * frequency / sampleRate; + float sinOmega = sinf(omega); + float cosOmega = cosf(omega); + float alpha = sinOmega / (2.0f * Q); + + float a0 = (A + 1.0f) - (A - 1.0f) * cosOmega + 2.0f * sqrtf(A) * alpha; + b0_ = A * ((A + 1.0f) + (A - 1.0f) * cosOmega + 2.0f * sqrtf(A) * alpha) / a0; + b1_ = -2.0f * A * ((A - 1.0f) + (A + 1.0f) * cosOmega) / a0; + b2_ = A * ((A + 1.0f) + (A - 1.0f) * cosOmega - 2.0f * sqrtf(A) * alpha) / a0; + a1_ = 2.0f * ((A - 1.0f) - (A + 1.0f) * cosOmega) / a0; + a2_ = ((A + 1.0f) - (A - 1.0f) * cosOmega - 2.0f * sqrtf(A) * alpha) / a0; +} + +float BiquadFilter::process(float input) { + float output = b0_ * input + b1_ * x1_ + b2_ * x2_ - a1_ * y1_ - a2_ * y2_; + x2_ = x1_; + x1_ = input; + y2_ = y1_; + y1_ = output; + + // Denormal protection + if (std::abs(y1_) < 1e-20f) y1_ = 0.0f; + if (std::abs(y2_) < 1e-20f) y2_ = 0.0f; + + return output; +} + +void BiquadFilter::reset() { + x1_ = x2_ = y1_ = y2_ = 0.0f; +} + +void BiquadFilter::processBuffer(const float* input, float* output, size_t length, bool bidirectional) { + reset(); + + // Forward pass + for (size_t i = 0; i < length; i++) { + output[i] = process(input[i]); + } + + if (bidirectional) { + // Backward pass for zero phase delay + reset(); + for (int i = length - 1; i >= 0; i--) { + output[i] = process(output[i]); + } + } +} + +// FIRFilter Implementation +FIRFilter::FIRFilter() : idx_(0), order_(0) {} + +// Modified Bessel function of the first kind, order 0 (I0) +// Approximation from Abramowitz and Stegun +double FIRFilter::besselI0(double x) { + double ax = std::abs(x); + if (ax <= 3.75) { + double y = (x / 3.75); + y *= y; + return 1.0 + y * (3.5156229 + y * (3.0899424 + y * (1.2067492 + + y * (0.2659732 + y * (0.0360768 + y * 0.0045813))))); + } else { + double y = 3.75 / ax; + return (std::exp(ax) / std::sqrt(ax)) * (0.39894228 + + y * (0.01328592 + y * (0.00225319 + y * (-0.00157565 + + y * (0.00916281 + y * (-0.02057706 + y * (0.02635537 + + y * (-0.01647633 + y * 0.00392377)))))))); + } +} + +std::vector FIRFilter::kaiserWindow(size_t length, float beta) { + std::vector window(length); + if (length == 0) return window; + const double denom = besselI0(static_cast(beta)); + const double M = static_cast(length - 1); + for (size_t n = 0; n < length; ++n) { + double ratio = (M == 0.0) ? 0.0 : (2.0 * static_cast(n) / M - 1.0); + double val = besselI0(static_cast(beta) * + std::sqrt(std::max(0.0, 1.0 - ratio * ratio))) / denom; + window[n] = static_cast(val); + } + return window; +} + +void FIRFilter::designBandpass(float centerFreq, float bandwidth, float sampleRate, float sidelobeAtten) { + // Kaiser beta from sidelobe attenuation + float beta = sidelobeAtten < 21.0f ? 0.0f + : sidelobeAtten < 50.0f ? 0.5842f * powf(sidelobeAtten - 21.0f, 0.4f) + + 0.07886f * (sidelobeAtten - 21.0f) + : 0.1102f * (sidelobeAtten - 8.7f); + + // Normalized frequencies + float wc1 = 2.0f * static_cast(M_PI) * (centerFreq - bandwidth / 2.0f) / sampleRate; + float wc2 = 2.0f * static_cast(M_PI) * (centerFreq + bandwidth / 2.0f) / sampleRate; + wc1 = std::max(wc1, 0.001f); + wc2 = std::min(wc2, static_cast(M_PI) - 0.001f); + + // Calculate filter order + float deltaF = (wc2 - wc1) / static_cast(M_PI); + int order = static_cast((sidelobeAtten - 8) / (2.285 * deltaF * M_PI)); + order = std::clamp(order, 1, 512); + order_ = static_cast(order); + + size_t len = order + 1; + size_t centerTap = len / 2; + + // Ideal bandpass impulse response + std::vector ideal(len); + for (size_t i = 0; i < len; ++i) { + if (i == centerTap) { + ideal[i] = (wc2 - wc1) / static_cast(M_PI); + } else { + float n = static_cast(static_cast(i) - static_cast(centerTap)); + ideal[i] = (sinf(wc2 * n) - sinf(wc1 * n)) / (static_cast(M_PI) * n); + } + } + + // Apply Kaiser window + std::vector window = kaiserWindow(len, beta); + coeffs_.resize(len); + for (size_t i = 0; i < len; ++i) { + coeffs_[i] = ideal[i] * window[i]; + } + + // Normalize to unity gain at center frequency + float centerOmega = 2.0f * static_cast(M_PI) * centerFreq / sampleRate; + float response = 0.0f; + for (size_t i = 0; i < len; ++i) { + response += coeffs_[i] * cosf(centerOmega * + (static_cast(i) - static_cast(centerTap))); + } + if (std::abs(response) > 1e-6f) { + float scale = 1.0f / response; + for (float& coeff : coeffs_) { + coeff *= scale; + } + } + + // Reset delay line + delay_.resize(len, 0.0f); + idx_ = 0; +} + +float FIRFilter::process(float input) { + if (coeffs_.empty()) return input; + + size_t nTaps = coeffs_.size(); + idx_ %= nTaps; + delay_[idx_] = input; + + float out = 0.0f; + size_t firstLen = nTaps - idx_; + + // Process first segment [idx_ .. end] + for (size_t i = 0; i < firstLen; ++i) { + out += coeffs_[i] * delay_[idx_ + i]; + } + // Process second segment [0 .. idx_-1] + for (size_t i = 0; i < idx_; ++i) { + out += coeffs_[firstLen + i] * delay_[i]; + } + + idx_ = (idx_ + 1) % nTaps; + return out; +} + +void FIRFilter::reset() { + std::fill(delay_.begin(), delay_.end(), 0.0f); + idx_ = 0; +} + +// Pitch detection using autocorrelation +float detectPitch(const float* data, size_t length, float sampleRate, float minFreq, float maxFreq) { + int minPeriod = static_cast(sampleRate / maxFreq); + int maxPeriod = static_cast(sampleRate / minFreq); + + maxPeriod = std::min(maxPeriod, static_cast(length / 2)); + if (maxPeriod <= minPeriod) return 0.0f; + + float bestCorrelation = -1.0f; + int bestPeriod = 0; + + // Use a simplified autocorrelation: only compute for lags in range + for (int period = minPeriod; period < maxPeriod; period++) { + float correlation = 0.0f; + float energy1 = 0.0f; + float energy2 = 0.0f; + + // Use fewer samples for performance, but enough for accuracy + int samples = std::min(static_cast(length) - period, 512); + + for (int i = 0; i < samples; i++) { + correlation += data[i] * data[i + period]; + energy1 += data[i] * data[i]; + energy2 += data[i + period] * data[i + period]; + } + + // Normalized correlation + if (energy1 > 1e-9f && energy2 > 1e-9f) { + float norm = sqrtf(energy1 * energy2); + correlation /= norm; + + if (correlation > bestCorrelation) { + bestCorrelation = correlation; + bestPeriod = period; + } + } + } + + // Threshold for valid pitch + if (bestCorrelation < 0.5f || bestPeriod == 0) { + return 0.0f; // No confident pitch found + } + + // Parabolic interpolation for sub-sample accuracy could be added here + // but basic integer period is often enough for visual stabilization + + return sampleRate / bestPeriod; +} + +// FFT-based pitch detection (more stable than autocorrelation) +float detectPitchFFT(const float* data, size_t length, float sampleRate, float minFreq, float maxFreq) { + // Use power-of-2 FFT size + size_t fftSize = 2048; + if (length < fftSize) { + fftSize = 1024; + if (length < fftSize) { + fftSize = 512; + } + } + + FFT fft(fftSize); + std::vector magnitudes(fftSize / 2); + + // Apply Hann window and run FFT + std::vector windowed(fftSize, 0.0f); + size_t copyLen = std::min(length, fftSize); + for (size_t i = 0; i < copyLen; i++) { + float win = 0.5f * (1.0f - cosf(2.0f * static_cast(M_PI) * i / fftSize)); + windowed[i] = data[i] * win; + } + fft.forward(windowed.data(), magnitudes.data()); + + // Find peak in frequency range + int minBin = std::max(1, static_cast(minFreq * fftSize / sampleRate)); + int maxBin = std::min(static_cast(fftSize / 2 - 1), static_cast(maxFreq * fftSize / sampleRate)); + + if (minBin >= maxBin) { + return 0.0f; + } + + float peakMag = 0.0f; + int peakBin = minBin; + for (int i = minBin; i <= maxBin; i++) { + if (magnitudes[i] > peakMag) { + peakMag = magnitudes[i]; + peakBin = i; + } + } + + // Check if peak is significant (avoid noise) + if (peakMag < 1e-6f) { + return 0.0f; + } + + // Quadratic interpolation for sub-bin accuracy + if (peakBin > 0 && peakBin < static_cast(fftSize / 2) - 1) { + float y1 = magnitudes[peakBin - 1]; + float y2 = magnitudes[peakBin]; + float y3 = magnitudes[peakBin + 1]; + float denom = y1 - 2.0f * y2 + y3; + if (std::abs(denom) > 1e-9f) { + float offset = 0.5f * (y1 - y3) / denom; + offset = std::clamp(offset, -0.5f, 0.5f); + return (static_cast(peakBin) + offset) * sampleRate / static_cast(fftSize); + } + } + + return static_cast(peakBin) * sampleRate / static_cast(fftSize); +} + +// Find zero-crossing trigger point (sub-sample precision) +// searches in [searchStart, searchEnd) +// Finds the STRONGEST (steepest slope) rising zero crossing for consistency +float findTriggerPoint(const float* data, size_t length, int searchStart, int searchEnd) { + searchStart = std::max(1, searchStart); // Need i-1 + searchEnd = std::min(static_cast(length), searchEnd); + + if (searchStart >= searchEnd) return -1.0f; + + // Find the zero crossing with the steepest positive slope + float bestSlope = 0.0f; + int bestIdx = -1; + + for (int i = searchStart; i < searchEnd; i++) { + float prev = data[i - 1]; + float curr = data[i]; + + // Rising zero crossing: prev < 0 and curr >= 0 + if (prev < 0.0f && curr >= 0.0f) { + float slope = curr - prev; // Always positive for rising crossing + if (slope > bestSlope) { + bestSlope = slope; + bestIdx = i; + } + } + } + + if (bestIdx < 0) return -1.0f; + + // Linear interpolation for sub-sample precision + float prev = data[bestIdx - 1]; + float curr = data[bestIdx]; + float t = -prev / (curr - prev); + return static_cast(bestIdx - 1) + t; +} + +// Calculate RMS +float calculateRMS(const float* data, size_t length) { + if (length == 0) return 0.0f; + float sum = 0.0f; + for (size_t i = 0; i < length; i++) { + sum += data[i] * data[i]; + } + return sqrtf(sum / length); +} + +} // namespace DSP diff --git a/native/src/dsp_utils.h b/native/src/dsp_utils.h new file mode 100644 index 0000000..92136bd --- /dev/null +++ b/native/src/dsp_utils.h @@ -0,0 +1,91 @@ +#pragma once +#define _USE_MATH_DEFINES +#include +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#include +#include +#include + +namespace DSP { + +// Simple FFT implementation (Cooley-Tukey radix-2) +class FFT { +public: + explicit FFT(size_t size); + void forward(const float* input, float* magnitudes); + void forward(const float* input, std::complex* output); + size_t getSize() const { return size_; } + +private: + size_t size_; + std::vector> twiddles_; + std::vector> buffer_; // Reuse buffer to avoid allocations + std::vector> scratch_; // Scratch buffer if needed + void bitReverse(std::complex* data); +}; + +// Biquad filter for lowpass/bandpass +class BiquadFilter { +public: + BiquadFilter(); + void setLowpass(float frequency, float sampleRate, float Q = 0.707f); + void setBandpass(float frequency, float sampleRate, float Q = 2.0f); + void setHighShelf(float frequency, float sampleRate, float gainDB, float Q = 0.707f); + float process(float input); + void reset(); + + // Process entire buffer (bidirectional for zero phase) + void processBuffer(const float* input, float* output, size_t length, bool bidirectional = true); + +private: + float b0_, b1_, b2_; + float a1_, a2_; + float x1_, x2_; + float y1_, y2_; +}; + +// Linear-phase FIR filter for stable trigger detection +// Uses Kaiser-windowed bandpass design for consistent zero crossings +class FIRFilter { +public: + FIRFilter(); + + // Design Kaiser-windowed bandpass filter centered on frequency + void designBandpass(float centerFreq, float bandwidth, float sampleRate, float sidelobeAtten = 60.0f); + + // Process single sample + float process(float input); + + // Get filter delay (for phase compensation) + size_t getDelay() const { return order_ / 2; } + + // Reset filter state + void reset(); + +private: + std::vector coeffs_; + std::vector delay_; + size_t idx_; + size_t order_; + + // Kaiser window helpers + static std::vector kaiserWindow(size_t length, float beta); + static double besselI0(double x); +}; + +// Pitch detection using autocorrelation +float detectPitch(const float* data, size_t length, float sampleRate, float minFreq = 40.0f, float maxFreq = 2000.0f); + +// FFT-based pitch detection (more stable than autocorrelation) +float detectPitchFFT(const float* data, size_t length, float sampleRate, float minFreq = 40.0f, float maxFreq = 2000.0f); + +// Find zero-crossing trigger point with hysteresis/hold-off (sub-sample precision) +float findTriggerPoint(const float* data, size_t length, int searchStart, int searchEnd); + +// Calculate RMS +float calculateRMS(const float* data, size_t length); + +} // namespace DSP diff --git a/native/src/main.cpp b/native/src/main.cpp new file mode 100644 index 0000000..1bd288e --- /dev/null +++ b/native/src/main.cpp @@ -0,0 +1,300 @@ +#include +#include +#include "oscilloscope.h" +#include "spectrum.h" +#include "vectorscope.h" + +// Global instances +static Visualizer::Oscilloscope oscilloscope; +static Visualizer::Spectrum spectrum(2048); +static Visualizer::Vectorscope vectorscope; + +// ============== Oscilloscope ============== + +Napi::Value OscilloscopeSetSampleRate(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected sample rate").ThrowAsJavaScriptException(); + return env.Null(); + } + oscilloscope.setSampleRate(info[0].As().FloatValue()); + return env.Undefined(); +} + +Napi::Value OscilloscopeSetPitchLock(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsBoolean()) { + Napi::TypeError::New(env, "Expected boolean").ThrowAsJavaScriptException(); + return env.Null(); + } + oscilloscope.setPitchLock(info[0].As().Value()); + return env.Undefined(); +} + +Napi::Value OscilloscopeSetDisplaySamples(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected number").ThrowAsJavaScriptException(); + return env.Null(); + } + oscilloscope.setDisplaySamples(info[0].As().Int32Value()); + return env.Undefined(); +} + +Napi::Value OscilloscopePushSamples(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsTypedArray()) { + Napi::TypeError::New(env, "Expected Float32Array").ThrowAsJavaScriptException(); + return env.Null(); + } + Napi::Float32Array audioData = info[0].As(); + oscilloscope.pushSamples(audioData.Data(), audioData.ElementLength()); + return env.Undefined(); +} + +Napi::Value OscilloscopeProcessContinuous(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + auto result = oscilloscope.process(); + Napi::Object obj = Napi::Object::New(env); + obj.Set("triggerIndex", Napi::Number::New(env, result.triggerIndex)); + obj.Set("samplesToShow", Napi::Number::New(env, result.samplesToShow)); + obj.Set("detectedPitch", Napi::Number::New(env, result.detectedPitch)); + obj.Set("writePos", Napi::Number::New(env, static_cast(oscilloscope.getWritePos()))); + return obj; +} + +Napi::Value OscilloscopeProcess(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsTypedArray()) { + Napi::TypeError::New(env, "Expected Float32Array").ThrowAsJavaScriptException(); + return env.Null(); + } + Napi::Float32Array audioData = info[0].As(); + auto result = oscilloscope.processSnapshot(audioData.Data(), audioData.ElementLength()); + Napi::Object obj = Napi::Object::New(env); + obj.Set("triggerIndex", Napi::Number::New(env, result.triggerIndex)); + obj.Set("samplesToShow", Napi::Number::New(env, result.samplesToShow)); + obj.Set("detectedPitch", Napi::Number::New(env, result.detectedPitch)); + obj.Set("writePos", Napi::Number::New(env, static_cast(oscilloscope.getWritePos()))); + return obj; +} + +Napi::Value OscilloscopeGetWritePos(const Napi::CallbackInfo& info) { + return Napi::Number::New(info.Env(), static_cast(oscilloscope.getWritePos())); +} + +Napi::Value OscilloscopeGetSamples(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) { + Napi::TypeError::New(env, "Expected startPos (float) and count").ThrowAsJavaScriptException(); + return env.Null(); + } + float startPos = info[0].As().FloatValue(); + size_t count = static_cast(info[1].As().Uint32Value()); + Napi::Float32Array output = Napi::Float32Array::New(env, count); + oscilloscope.getSamplesInterpolated(output.Data(), startPos, count); + return output; +} + +Napi::Value OscilloscopeReset(const Napi::CallbackInfo& info) { + oscilloscope.reset(); + return info.Env().Undefined(); +} + +// ============== Spectrum ============== + +Napi::Value SpectrumSetFFTSize(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected FFT size").ThrowAsJavaScriptException(); + return env.Null(); + } + spectrum.setFFTSize(info[0].As().Uint32Value()); + return env.Undefined(); +} + +Napi::Value SpectrumGetFFTSize(const Napi::CallbackInfo& info) { + return Napi::Number::New(info.Env(), spectrum.getFFTSize()); +} + +Napi::Value SpectrumSetSampleRate(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected sample rate").ThrowAsJavaScriptException(); + return env.Null(); + } + spectrum.setSampleRate(info[0].As().FloatValue()); + return env.Undefined(); +} + +Napi::Value SpectrumSetSmoothing(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected smoothing value").ThrowAsJavaScriptException(); + return env.Null(); + } + spectrum.setSmoothing(info[0].As().FloatValue()); + return env.Undefined(); +} + +Napi::Value SpectrumProcess(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsTypedArray()) { + Napi::TypeError::New(env, "Expected Float32Array").ThrowAsJavaScriptException(); + return env.Null(); + } + Napi::Float32Array audioData = info[0].As(); + const auto& magnitudes = spectrum.process(audioData.Data(), audioData.ElementLength()); + Napi::Float32Array result = Napi::Float32Array::New(env, magnitudes.size()); + memcpy(result.Data(), magnitudes.data(), magnitudes.size() * sizeof(float)); + return result; +} + +Napi::Value SpectrumBinToFrequency(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected bin number").ThrowAsJavaScriptException(); + return env.Null(); + } + return Napi::Number::New(env, spectrum.binToFrequency(info[0].As().Int32Value())); +} + +Napi::Value SpectrumReset(const Napi::CallbackInfo& info) { + spectrum.reset(); + return info.Env().Undefined(); +} + +// ============== Vectorscope ============== + +Napi::Value VectorscopeSetSampleRate(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected sample rate").ThrowAsJavaScriptException(); + return env.Null(); + } + vectorscope.setSampleRate(info[0].As().FloatValue()); + return env.Undefined(); +} + +Napi::Value VectorscopePushSamples(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray()) { + Napi::TypeError::New(env, "Expected two Float32Arrays (left, right)").ThrowAsJavaScriptException(); + return env.Null(); + } + Napi::Float32Array leftData = info[0].As(); + Napi::Float32Array rightData = info[1].As(); + size_t length = std::min(leftData.ElementLength(), rightData.ElementLength()); + vectorscope.pushSamples(leftData.Data(), rightData.Data(), length); + return env.Undefined(); +} + +Napi::Value VectorscopeGetPoints(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected max points count").ThrowAsJavaScriptException(); + return env.Null(); + } + size_t maxPoints = static_cast(info[0].As().Uint32Value()); + Napi::Float32Array xArray = Napi::Float32Array::New(env, maxPoints); + Napi::Float32Array yArray = Napi::Float32Array::New(env, maxPoints); + size_t actual = vectorscope.getPoints(xArray.Data(), yArray.Data(), maxPoints); + Napi::Object result = Napi::Object::New(env); + if (actual < maxPoints) { + Napi::Float32Array xTrimmed = Napi::Float32Array::New(env, actual); + Napi::Float32Array yTrimmed = Napi::Float32Array::New(env, actual); + memcpy(xTrimmed.Data(), xArray.Data(), actual * sizeof(float)); + memcpy(yTrimmed.Data(), yArray.Data(), actual * sizeof(float)); + result.Set("x", xTrimmed); + result.Set("y", yTrimmed); + } else { + result.Set("x", xArray); + result.Set("y", yArray); + } + result.Set("count", Napi::Number::New(env, static_cast(actual))); + return result; +} + +Napi::Value VectorscopeSetBufferSize(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Expected buffer size").ThrowAsJavaScriptException(); + return env.Null(); + } + vectorscope.setBufferSize(info[0].As().Uint32Value()); + return env.Undefined(); +} + +Napi::Value VectorscopeGetBufferSize(const Napi::CallbackInfo& info) { + return Napi::Number::New(info.Env(), vectorscope.getBufferSize()); +} + +Napi::Value VectorscopeProcess(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray()) { + Napi::TypeError::New(env, "Expected two Float32Arrays (left, right)").ThrowAsJavaScriptException(); + return env.Null(); + } + Napi::Float32Array leftData = info[0].As(); + Napi::Float32Array rightData = info[1].As(); + size_t length = std::min(leftData.ElementLength(), rightData.ElementLength()); + const auto& points = vectorscope.process(leftData.Data(), rightData.Data(), length); + Napi::Float32Array xArray = Napi::Float32Array::New(env, points.size()); + Napi::Float32Array yArray = Napi::Float32Array::New(env, points.size()); + for (size_t i = 0; i < points.size(); i++) { + xArray[i] = points[i].x; + yArray[i] = points[i].y; + } + Napi::Object result = Napi::Object::New(env); + result.Set("x", xArray); + result.Set("y", yArray); + return result; +} + +Napi::Value VectorscopeReset(const Napi::CallbackInfo& info) { + vectorscope.reset(); + return info.Env().Undefined(); +} + +// ============== Module Init ============== + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + // Oscilloscope + Napi::Object oscExports = Napi::Object::New(env); + oscExports.Set("setSampleRate", Napi::Function::New(env, OscilloscopeSetSampleRate)); + oscExports.Set("setPitchLock", Napi::Function::New(env, OscilloscopeSetPitchLock)); + oscExports.Set("setDisplaySamples", Napi::Function::New(env, OscilloscopeSetDisplaySamples)); + oscExports.Set("process", Napi::Function::New(env, OscilloscopeProcess)); + oscExports.Set("pushSamples", Napi::Function::New(env, OscilloscopePushSamples)); + oscExports.Set("processContinuous", Napi::Function::New(env, OscilloscopeProcessContinuous)); + oscExports.Set("getWritePos", Napi::Function::New(env, OscilloscopeGetWritePos)); + oscExports.Set("getSamples", Napi::Function::New(env, OscilloscopeGetSamples)); + oscExports.Set("reset", Napi::Function::New(env, OscilloscopeReset)); + exports.Set("oscilloscope", oscExports); + + // Spectrum + Napi::Object specExports = Napi::Object::New(env); + specExports.Set("setFFTSize", Napi::Function::New(env, SpectrumSetFFTSize)); + specExports.Set("getFFTSize", Napi::Function::New(env, SpectrumGetFFTSize)); + specExports.Set("setSampleRate", Napi::Function::New(env, SpectrumSetSampleRate)); + specExports.Set("setSmoothing", Napi::Function::New(env, SpectrumSetSmoothing)); + specExports.Set("process", Napi::Function::New(env, SpectrumProcess)); + specExports.Set("binToFrequency", Napi::Function::New(env, SpectrumBinToFrequency)); + specExports.Set("reset", Napi::Function::New(env, SpectrumReset)); + exports.Set("spectrum", specExports); + + // Vectorscope + Napi::Object vecExports = Napi::Object::New(env); + vecExports.Set("setSampleRate", Napi::Function::New(env, VectorscopeSetSampleRate)); + vecExports.Set("pushSamples", Napi::Function::New(env, VectorscopePushSamples)); + vecExports.Set("getPoints", Napi::Function::New(env, VectorscopeGetPoints)); + vecExports.Set("setBufferSize", Napi::Function::New(env, VectorscopeSetBufferSize)); + vecExports.Set("getBufferSize", Napi::Function::New(env, VectorscopeGetBufferSize)); + vecExports.Set("process", Napi::Function::New(env, VectorscopeProcess)); + vecExports.Set("reset", Napi::Function::New(env, VectorscopeReset)); + exports.Set("vectorscope", vecExports); + + return exports; +} + +NODE_API_MODULE(visualizer_dsp, Init) diff --git a/native/src/oscilloscope.cpp b/native/src/oscilloscope.cpp new file mode 100644 index 0000000..7d2a158 --- /dev/null +++ b/native/src/oscilloscope.cpp @@ -0,0 +1,331 @@ +#include "oscilloscope.h" +#include +#include + +namespace Visualizer { + +Oscilloscope::Oscilloscope() + : sampleRate_(48000.0f) + , pitchLock_(true) + , displaySamples_(2048) + , writePos_(0) + , lastFilterPitch_(200.0f) + , lastTrigger_(0) + , smoothedPitch_(200.0f) + , pitchSamplesProcessed_(0) { + + // Initialize circular buffers + circularBuffer_.resize(OSCILLOSCOPE_BUFFER_SIZE, 0.0f); + filteredBuffer_.resize(OSCILLOSCOPE_BUFFER_SIZE, 0.0f); + + // Initialize FIR bandpass filter centered at 200Hz with 10% bandwidth (20Hz) + // Tight bandwidth removes harmonics, leaving only ONE rising zero crossing per period + bandpassFilter_.designBandpass(200.0f, 20.0f, sampleRate_, 60.0f); + + // Initialize high shelf for pitch analysis (-3dB at 400Hz, Q=0.71) + // Reduces high frequency interference with pitch detection + pitchAnalysisShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f); + + // Initialize analysis and render buffers + displayBuffer_.resize(OSCILLOSCOPE_BUFFER_SIZE, 0.0f); + visualBuffer_.resize(OSCILLOSCOPE_BUFFER_SIZE, 0.0f); + + // Initialize display filters (high shelf + cascaded lowpass for steep rolloff) + displayShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f); + displayLowpass1_.setLowpass(18000.0f, sampleRate_, 0.707f); + displayLowpass2_.setLowpass(18000.0f, sampleRate_, 0.707f); + + // Initialize pitch detection lowpass (cascaded for steep slope) + pitchLowpass1_.setLowpass(18000.0f, sampleRate_, 0.707f); + pitchLowpass2_.setLowpass(18000.0f, sampleRate_, 0.707f); +} + +void Oscilloscope::setSampleRate(float sampleRate) { + sampleRate_ = sampleRate; + // Redesign filter with new sample rate (10% bandwidth) + float bandwidth = lastFilterPitch_ * 0.1f; + bandpassFilter_.designBandpass(lastFilterPitch_, bandwidth, sampleRate_, 60.0f); + // Update high shelf for new sample rate + pitchAnalysisShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f); + + // Update display filters + displayShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f); + displayLowpass1_.setLowpass(18000.0f, sampleRate_, 0.707f); + displayLowpass2_.setLowpass(18000.0f, sampleRate_, 0.707f); + + // Update pitch detection lowpass + pitchLowpass1_.setLowpass(18000.0f, sampleRate_, 0.707f); + pitchLowpass2_.setLowpass(18000.0f, sampleRate_, 0.707f); +} + +void Oscilloscope::setPitchLock(bool enabled) { + pitchLock_ = enabled; +} + +void Oscilloscope::setDisplaySamples(int samples) { + displaySamples_ = std::clamp(samples, 64, static_cast(OSCILLOSCOPE_BUFFER_SIZE - 1)); +} + +// Push samples into circular buffer (called from AudioWorklet) +void Oscilloscope::pushSamples(const float* samples, size_t count) { + for (size_t i = 0; i < count; i++) { + // Store raw sample + circularBuffer_[writePos_] = samples[i]; + + // Apply FIR bandpass filter and store filtered sample + // Linear-phase filter provides consistent zero crossings + filteredBuffer_[writePos_] = bandpassFilter_.process(samples[i]); + + // Tracking path: cascaded lowpass only + float displaySample = displayLowpass1_.process(samples[i]); + displaySample = displayLowpass2_.process(displaySample); + displayBuffer_[writePos_] = displaySample; + + // Visual path: high shelf on top of tracking sample + float visualSample = displayShelf_.process(displaySample); + visualBuffer_[writePos_] = visualSample; + + writePos_ = (writePos_ + 1) % OSCILLOSCOPE_BUFFER_SIZE; + } +} + +// Update filtered buffer from circular buffer (for backwards compatibility) +void Oscilloscope::updateFiltered() { + // This is called when using snapshot mode - filter is applied in pushSamples for continuous mode +} + +// Find trigger by searching BACKWARDS from target position +// With tight bandpass filter (10% bandwidth), there's only ONE rising zero crossing per period +// So we simply take the FIRST valid crossing found - no phase tracking needed +float Oscilloscope::findTriggerBackwards(size_t target, size_t range) { + float periodSamples = sampleRate_ / smoothedPitch_; + + // Search backwards from target to find FIRST rising zero crossing + for (size_t i = 0; i < range && i < OSCILLOSCOPE_BUFFER_SIZE; i++) { + size_t pos = (target + OSCILLOSCOPE_BUFFER_SIZE - i) % OSCILLOSCOPE_BUFFER_SIZE; + size_t prev = (pos + OSCILLOSCOPE_BUFFER_SIZE - 1) % OSCILLOSCOPE_BUFFER_SIZE; + + float prevVal = filteredBuffer_[prev]; + float currVal = filteredBuffer_[pos]; + + // Rising zero crossing + if (prevVal < 0.0f && currVal >= 0.0f) { + // Check signal amplitude (look ahead ~1/4 period) + size_t lookAhead = std::clamp( + static_cast(periodSamples / 4.0f), + static_cast(4), + static_cast(256) + ); + + float peakAfter = 0.0f; + for (size_t j = 0; j < lookAhead; j++) { + size_t checkPos = (pos + j) % OSCILLOSCOPE_BUFFER_SIZE; + float val = std::abs(filteredBuffer_[checkPos]); + if (val > peakAfter) peakAfter = val; + } + + // Only accept if signal has significant amplitude + if (peakAfter > 0.01f) { + // Sub-sample interpolation for smooth rendering + float t = -prevVal / (currVal - prevVal); + return static_cast(prev) + t; + } + } + } + + return -1.0f; // No crossing found +} + +// Process using circular buffer (continuous capture mode) +OscilloscopeResult Oscilloscope::process() { + OscilloscopeResult result; + result.triggerIndex = 0; + result.samplesToShow = displaySamples_; + result.detectedPitch = smoothedPitch_; + + if (!pitchLock_) { + return result; + } + + // Detect pitch from recent samples in circular buffer + // Use RAW buffer for pitch detection (filtered buffer may attenuate the fundamental) + // Use last 2048 samples for pitch detection + std::vector recentSamples(2048); + for (size_t i = 0; i < 2048; i++) { + size_t idx = (writePos_ + OSCILLOSCOPE_BUFFER_SIZE - 2048 + i) % OSCILLOSCOPE_BUFFER_SIZE; + recentSamples[i] = displayBuffer_[idx]; // Use RAW samples, not filtered + } + + // Apply high shelf filter to reduce HF interference with pitch detection + pitchAnalysisShelf_.reset(); + for (size_t i = 0; i < 2048; i++) { + recentSamples[i] = pitchAnalysisShelf_.process(recentSamples[i]); + } + + // Apply cascaded lowpass for steep HF rejection + pitchLowpass1_.reset(); + pitchLowpass2_.reset(); + for (size_t i = 0; i < 2048; i++) { + recentSamples[i] = pitchLowpass1_.process(recentSamples[i]); + recentSamples[i] = pitchLowpass2_.process(recentSamples[i]); + } + + float newPitch = DSP::detectPitchFFT(recentSamples.data(), 2048, sampleRate_, 40.0f, 1000.0f); + if (newPitch > 0.0f) { + pitchSamplesProcessed_++; + + // Adaptive smoothing: fast convergence initially, then conservative + // First ~20 frames: use 0.5/0.5 for quick lock-on + // After warmup: use 0.95/0.05 for stable tracking + float smoothingOld = (pitchSamplesProcessed_ < 20) ? 0.5f : 0.95f; + float smoothingNew = 1.0f - smoothingOld; + smoothedPitch_ = smoothedPitch_ * smoothingOld + newPitch * smoothingNew; + + // Redesign FIR bandpass filter if pitch changed significantly (>10%) + // This keeps the filter centered on the fundamental for stable trigger + if (std::abs(smoothedPitch_ - lastFilterPitch_) / lastFilterPitch_ > 0.1f) { + float bandwidth = smoothedPitch_ * 0.1f; // 10% of center freq (tight = single zero crossing) + bandpassFilter_.designBandpass(smoothedPitch_, bandwidth, sampleRate_, 60.0f); + lastFilterPitch_ = smoothedPitch_; + } + } + result.detectedPitch = smoothedPitch_; + + // Calculate target position for trigger search + // We search backwards from (writePos - displaySamples - firDelay) to find a rising zero crossing + float periodSamples = sampleRate_ / smoothedPitch_; + size_t samples = static_cast(displaySamples_); + size_t firDelay = bandpassFilter_.getDelay(); + + // Target: look back from current write position by display window size AND FIR delay + // This ensures we're searching in the correct region where filtered data is valid + size_t target = (writePos_ + OSCILLOSCOPE_BUFFER_SIZE - samples - firDelay) % OSCILLOSCOPE_BUFFER_SIZE; + + // Search range: 4 periods for robust detection + size_t range = static_cast(periodSamples * 4.0f); + + // Find zero crossing by searching backwards from target + float zeroCross = findTriggerBackwards(target, range); + + // LEFT-ANCHORED TRIGGER (MiniMeters style): + // The zero crossing IS the left edge of display + // Waveform starts at rising edge and extends rightward + if (zeroCross >= 0.0f) { + // Apply FIR filter delay compensation + // The filtered signal is delayed by order/2 samples relative to raw signal + size_t firDelay = bandpassFilter_.getDelay(); + + // The trigger index is where we start reading raw samples for display + // Compensate for filter delay so trigger aligns with raw audio + result.triggerIndex = zeroCross - static_cast(firDelay); + + // Wrap if negative + while (result.triggerIndex < 0) { + result.triggerIndex += OSCILLOSCOPE_BUFFER_SIZE; + } + } else { + // No crossing found - use target as fallback + result.triggerIndex = static_cast(target); + } + + return result; +} + +// Legacy snapshot processing (for backwards compatibility) +OscilloscopeResult Oscilloscope::processSnapshot(const float* audioData, size_t length) { + OscilloscopeResult result; + result.triggerIndex = 0; + result.samplesToShow = std::min(displaySamples_, static_cast(length)); + result.detectedPitch = smoothedPitch_; + + if (!pitchLock_ || length == 0) { + return result; + } + + // Push samples to circular buffer + pushSamples(audioData, length); + + // Use the new continuous process method + return process(); +} + +// Get samples from circular buffer starting at position (integer version) +// Returns filtered samples for display (high shelf + lowpass applied) +void Oscilloscope::getSamples(float* output, size_t startPos, size_t count) const { + for (size_t i = 0; i < count; i++) { + size_t idx = (startPos + i) % OSCILLOSCOPE_BUFFER_SIZE; + output[i] = visualBuffer_[idx]; // Visual-only filtered signal + } +} + +// Get samples with sub-sample interpolation (float start position) +// Uses Catmull-Rom spline for smooth rendering at sub-pixel precision +// This preserves the high-precision trigger position from zero-crossing detection +void Oscilloscope::getSamplesInterpolated(float* output, float startPos, size_t count) const { + for (size_t i = 0; i < count; i++) { + float pos = startPos + static_cast(i); + + // Wrap position to buffer bounds + while (pos < 0) pos += OSCILLOSCOPE_BUFFER_SIZE; + while (pos >= OSCILLOSCOPE_BUFFER_SIZE) pos -= OSCILLOSCOPE_BUFFER_SIZE; + + size_t idx = static_cast(pos) % OSCILLOSCOPE_BUFFER_SIZE; + float frac = pos - std::floor(pos); + + if (frac < 0.0001f) { + // No interpolation needed - exact sample position + output[i] = visualBuffer_[idx]; + } else { + // Cubic (Catmull-Rom) interpolation for smooth sub-sample rendering + // This eliminates pixel-level ghosting/jitter from truncated trigger positions + size_t i0 = (idx + OSCILLOSCOPE_BUFFER_SIZE - 1) % OSCILLOSCOPE_BUFFER_SIZE; + size_t i1 = idx; + size_t i2 = (idx + 1) % OSCILLOSCOPE_BUFFER_SIZE; + size_t i3 = (idx + 2) % OSCILLOSCOPE_BUFFER_SIZE; + + float y0 = visualBuffer_[i0]; + float y1 = visualBuffer_[i1]; + float y2 = visualBuffer_[i2]; + float y3 = visualBuffer_[i3]; + + // Catmull-Rom spline coefficients + float t = frac; + float t2 = t * t; + float t3 = t2 * t; + + output[i] = 0.5f * ( + (2.0f * y1) + + (-y0 + y2) * t + + (2.0f * y0 - 5.0f * y1 + 4.0f * y2 - y3) * t2 + + (-y0 + 3.0f * y1 - 3.0f * y2 + y3) * t3 + ); + } + } +} + +void Oscilloscope::reset() { + writePos_ = 0; + lastTrigger_ = 0.0f; + smoothedPitch_ = 200.0f; + lastFilterPitch_ = 200.0f; + pitchSamplesProcessed_ = 0; // Reset warmup counter for fast convergence on next use + + // Redesign filter to default 200Hz (reset() only clears delay line, not coefficients) + bandpassFilter_.designBandpass(200.0f, 20.0f, sampleRate_, 60.0f); + pitchAnalysisShelf_.reset(); + + // Reset display and pitch detection filters + displayShelf_.reset(); + displayLowpass1_.reset(); + displayLowpass2_.reset(); + pitchLowpass1_.reset(); + pitchLowpass2_.reset(); + + // Clear buffers + std::fill(circularBuffer_.begin(), circularBuffer_.end(), 0.0f); + std::fill(filteredBuffer_.begin(), filteredBuffer_.end(), 0.0f); + std::fill(displayBuffer_.begin(), displayBuffer_.end(), 0.0f); + std::fill(visualBuffer_.begin(), visualBuffer_.end(), 0.0f); +} + +} // namespace Visualizer diff --git a/native/src/oscilloscope.h b/native/src/oscilloscope.h new file mode 100644 index 0000000..2d57acf --- /dev/null +++ b/native/src/oscilloscope.h @@ -0,0 +1,85 @@ +#pragma once + +#include "dsp_utils.h" +#include +#include + +namespace Visualizer { + +struct OscilloscopeResult { + float triggerIndex; + int samplesToShow; + float detectedPitch; +}; + +// Circular buffer size (same as pulse-visualizer) +constexpr size_t OSCILLOSCOPE_BUFFER_SIZE = 32768; + +class Oscilloscope { +public: + Oscilloscope(); + + // Configuration + void setSampleRate(float sampleRate); + void setPitchLock(bool enabled); + void setDisplaySamples(int samples); + + // Push samples into circular buffer (continuous capture) + void pushSamples(const float* samples, size_t count); + + // Process and find trigger point (uses circular buffer) + OscilloscopeResult process(); + + // Legacy: Process snapshot (for backwards compatibility) + OscilloscopeResult processSnapshot(const float* audioData, size_t length); + + // Get current write position + size_t getWritePos() const { return writePos_; } + + // Get samples from circular buffer (for rendering) + void getSamples(float* output, size_t startPos, size_t count) const; + + // Get samples with sub-sample interpolation (preserves trigger precision) + void getSamplesInterpolated(float* output, float startPos, size_t count) const; + + // Reset state + void reset(); + +private: + float sampleRate_; + bool pitchLock_; + int displaySamples_; + + // Circular buffer for continuous audio + std::vector circularBuffer_; + std::vector filteredBuffer_; + size_t writePos_; + + // Linear-phase FIR bandpass filter for stable trigger detection + DSP::FIRFilter bandpassFilter_; + float lastFilterPitch_; // Track pitch for filter redesign + + // High shelf filter to reduce HF before pitch detection + DSP::BiquadFilter pitchAnalysisShelf_; + + // Display filtering (high shelf + steep lowpass) + DSP::BiquadFilter displayShelf_; // High shelf for display + DSP::BiquadFilter displayLowpass1_; // First stage of cascaded lowpass + DSP::BiquadFilter displayLowpass2_; // Second stage (4th order total = 24dB/oct) + std::vector displayBuffer_; // Lowpass filtered samples for tracking + std::vector visualBuffer_; // Visual-only samples (display shelf applied) + + // Pitch detection lowpass (after existing high shelf) + DSP::BiquadFilter pitchLowpass1_; // First stage + DSP::BiquadFilter pitchLowpass2_; // Second stage + + float lastTrigger_; + float smoothedPitch_; + int pitchSamplesProcessed_; // Track samples for adaptive smoothing + + // Internal helpers + void updateFiltered(); + float findTriggerBackwards(size_t target, size_t range); +}; + +} // namespace Visualizer diff --git a/native/src/spectrum.cpp b/native/src/spectrum.cpp new file mode 100644 index 0000000..c3d0475 --- /dev/null +++ b/native/src/spectrum.cpp @@ -0,0 +1,132 @@ +#define _USE_MATH_DEFINES +#include "spectrum.h" +#include +#include +#include + +namespace Visualizer { + +Spectrum::Spectrum(size_t fftSize) + : fftSize_(fftSize) + , sampleRate_(44100.0f) + , smoothing_(0.9f) + , bufferedSamples_(0) { + fft_ = std::make_unique(fftSize); + historyBuffer_.resize(fftSize, 0.0f); + windowedInput_.resize(fftSize); + magnitudes_.resize(fftSize / 2); + // Initialize to silence (-100.0f dB) + smoothedMagnitudes_.resize(fftSize / 2, -100.0f); +} + +void Spectrum::setFFTSize(size_t size) { + if (size != fftSize_) { + fftSize_ = size; + fft_ = std::make_unique(size); + historyBuffer_.assign(size, 0.0f); + windowedInput_.resize(size); + magnitudes_.resize(size / 2); + // Initialize to silence (-100.0f dB) + smoothedMagnitudes_.resize(size / 2, -100.0f); + bufferedSamples_ = 0; + } +} + +void Spectrum::setSampleRate(float sampleRate) { + sampleRate_ = sampleRate; +} + +void Spectrum::setSmoothing(float smoothing) { + smoothing_ = std::clamp(smoothing, 0.0f, 0.99f); +} + +void Spectrum::applyWindow(const float* input, float* output, size_t length) { + if (length <= 1) { + if (length == 1) { + output[0] = input[0]; + } + return; + } + + // Hann window + for (size_t i = 0; i < length; i++) { + float window = 0.5f * (1.0f - cosf(2.0f * M_PI * i / (length - 1))); + output[i] = input[i] * window; + } +} + +void Spectrum::pushSamples(const float* input, size_t length) { + if (length == 0 || fftSize_ == 0) { + return; + } + + // Keep only the most recent fftSize_ samples. + if (length >= fftSize_) { + std::memcpy(historyBuffer_.data(), input + (length - fftSize_), fftSize_ * sizeof(float)); + bufferedSamples_ = fftSize_; + return; + } + + const size_t keep = fftSize_ - length; + std::move(historyBuffer_.begin() + length, historyBuffer_.end(), historyBuffer_.begin()); + std::memcpy(historyBuffer_.data() + keep, input, length * sizeof(float)); + bufferedSamples_ = std::min(fftSize_, bufferedSamples_ + length); +} + +const std::vector& Spectrum::process(const float* audioData, size_t length) { + if (audioData != nullptr && length > 0) { + pushSamples(audioData, length); + } + + if (historyBuffer_.empty() || magnitudes_.empty()) { + return smoothedMagnitudes_; + } + + // Always analyze a full FFT frame from the rolling buffer. + applyWindow(historyBuffer_.data(), windowedInput_.data(), fftSize_); + + // Perform FFT + fft_->forward(windowedInput_.data(), magnitudes_.data()); + + // Convert to dB and apply smoothing + for (size_t i = 0; i < magnitudes_.size(); i++) { + float mag = magnitudes_[i]; + + // Convert to dB + // Add epsilon to avoid log(0) + float db = 20.0f * log10f(std::max(mag, 1e-10f)); + + // Compensate Hann window coherent gain (about -6 dB). + db += 6.0f; + + // Clamp to a stable display range. + db = std::clamp(db, -120.0f, 12.0f); + + if (bufferedSamples_ < fftSize_) { + smoothedMagnitudes_[i] = db; + continue; + } + + // Apply temporal smoothing only (no bin-to-bin averaging). + smoothedMagnitudes_[i] = smoothing_ * smoothedMagnitudes_[i] + (1.0f - smoothing_) * db; + + // Safety check + if (!std::isfinite(smoothedMagnitudes_[i])) { + smoothedMagnitudes_[i] = -100.0f; + } + } + + return smoothedMagnitudes_; +} + +float Spectrum::binToFrequency(int bin) const { + return bin * sampleRate_ / fftSize_; +} + +void Spectrum::reset() { + std::fill(historyBuffer_.begin(), historyBuffer_.end(), 0.0f); + std::fill(smoothedMagnitudes_.begin(), smoothedMagnitudes_.end(), -100.0f); + bufferedSamples_ = 0; +} + +} // namespace Visualizer diff --git a/native/src/spectrum.h b/native/src/spectrum.h new file mode 100644 index 0000000..8d7322b --- /dev/null +++ b/native/src/spectrum.h @@ -0,0 +1,45 @@ +#pragma once + +#include "dsp_utils.h" +#include +#include + +namespace Visualizer { + +class Spectrum { +public: + explicit Spectrum(size_t fftSize = 2048); + + // Configuration + void setFFTSize(size_t size); + size_t getFFTSize() const { return fftSize_; } + void setSampleRate(float sampleRate); + void setSmoothing(float smoothing); // 0.0 - 1.0 + + // Process audio and get spectrum data + // Returns magnitude data (size = fftSize / 2) + const std::vector& process(const float* audioData, size_t length); + + // Get frequency for a given bin + float binToFrequency(int bin) const; + + // Reset state + void reset(); + +private: + size_t fftSize_; + float sampleRate_; + float smoothing_; + + std::unique_ptr fft_; + std::vector historyBuffer_; + std::vector windowedInput_; + std::vector magnitudes_; + std::vector smoothedMagnitudes_; + size_t bufferedSamples_; + + void applyWindow(const float* input, float* output, size_t length); + void pushSamples(const float* input, size_t length); +}; + +} // namespace Visualizer diff --git a/native/src/vectorscope.cpp b/native/src/vectorscope.cpp new file mode 100644 index 0000000..ff1c5a7 --- /dev/null +++ b/native/src/vectorscope.cpp @@ -0,0 +1,110 @@ +#include "vectorscope.h" +#include +#include + +namespace Visualizer { + +Vectorscope::Vectorscope() + : sampleRate_(48000.0f) + , bufferSize_(1024) + , writePos_(0) + , validSamples_(0) { + + leftBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f); + rightBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f); + points_.reserve(1024); + + // Cascaded lowpass at 8kHz, Butterworth (Q=0.707) + // Two stages per channel = 4th order = 24 dB/oct rolloff + // Removes HF noise that causes erratic Lissajous motion + leftLowpass1_.setLowpass(8000.0f, sampleRate_, 0.707f); + leftLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f); + rightLowpass1_.setLowpass(8000.0f, sampleRate_, 0.707f); + rightLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f); +} + +void Vectorscope::setSampleRate(float sampleRate) { + sampleRate_ = sampleRate; + // Redesign all filters with new sample rate + leftLowpass1_.setLowpass(8000.0f, sampleRate_, 0.707f); + leftLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f); + rightLowpass1_.setLowpass(8000.0f, sampleRate_, 0.707f); + rightLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f); +} + +void Vectorscope::setBufferSize(size_t size) { + bufferSize_ = size; + points_.reserve(size); +} + +void Vectorscope::pushSamples( + const float* leftChannel, + const float* rightChannel, + size_t length +) { + for (size_t i = 0; i < length; i++) { + // Apply cascaded lowpass filtering + float filteredL = leftLowpass1_.process(leftChannel[i]); + filteredL = leftLowpass2_.process(filteredL); + + float filteredR = rightLowpass1_.process(rightChannel[i]); + filteredR = rightLowpass2_.process(filteredR); + + leftBuffer_[writePos_] = filteredL; + rightBuffer_[writePos_] = filteredR; + + writePos_ = (writePos_ + 1) % VECTORSCOPE_BUFFER_SIZE; + if (validSamples_ < VECTORSCOPE_BUFFER_SIZE) { + validSamples_++; + } + } +} + +size_t Vectorscope::getPoints(float* xOut, float* yOut, size_t maxPoints) const { + size_t count = std::min(maxPoints, validSamples_); + + // Read the most recent `count` samples from the circular buffer + for (size_t i = 0; i < count; i++) { + size_t idx = (writePos_ + VECTORSCOPE_BUFFER_SIZE - count + i) % VECTORSCOPE_BUFFER_SIZE; + xOut[i] = rightBuffer_[idx]; // X = Right (standard Lissajous) + yOut[i] = leftBuffer_[idx]; // Y = Left + } + + return count; +} + +// Legacy process method (routes through new pipeline) +const std::vector& Vectorscope::process( + const float* leftChannel, + const float* rightChannel, + size_t length +) { + // Push through the filtering pipeline + pushSamples(leftChannel, rightChannel, length); + + // Build legacy output from buffer + points_.clear(); + size_t count = std::min(length, validSamples_); + for (size_t i = 0; i < count; i++) { + size_t idx = (writePos_ + VECTORSCOPE_BUFFER_SIZE - count + i) % VECTORSCOPE_BUFFER_SIZE; + VectorscopePoint p; + p.x = rightBuffer_[idx]; + p.y = leftBuffer_[idx]; + points_.push_back(p); + } + return points_; +} + +void Vectorscope::reset() { + writePos_ = 0; + validSamples_ = 0; + std::fill(leftBuffer_.begin(), leftBuffer_.end(), 0.0f); + std::fill(rightBuffer_.begin(), rightBuffer_.end(), 0.0f); + leftLowpass1_.reset(); + leftLowpass2_.reset(); + rightLowpass1_.reset(); + rightLowpass2_.reset(); + points_.clear(); +} + +} // namespace Visualizer diff --git a/native/src/vectorscope.h b/native/src/vectorscope.h new file mode 100644 index 0000000..0042c09 --- /dev/null +++ b/native/src/vectorscope.h @@ -0,0 +1,66 @@ +#pragma once + +#include "dsp_utils.h" +#include +#include + +namespace Visualizer { + +struct VectorscopePoint { + float x; // Right channel + float y; // Left channel +}; + +// Circular buffer size (~170ms at 48kHz) +constexpr size_t VECTORSCOPE_BUFFER_SIZE = 8192; + +class Vectorscope { +public: + Vectorscope(); + + // Configuration + void setSampleRate(float sampleRate); + void setBufferSize(size_t size); // Legacy, kept for compat + size_t getBufferSize() const { return bufferSize_; } + + // Push stereo samples into circular buffer (called per worklet chunk) + void pushSamples(const float* leftChannel, const float* rightChannel, size_t length); + + // Get the most recent N points for rendering (from circular buffer) + // Returns count of valid points written to output arrays + size_t getPoints(float* xOut, float* yOut, size_t maxPoints) const; + + // Get number of valid samples in buffer + size_t getValidSamples() const { return validSamples_; } + + // Legacy process (kept for backwards compatibility) + const std::vector& process( + const float* leftChannel, + const float* rightChannel, + size_t length + ); + + // Reset state + void reset(); + +private: + float sampleRate_; + size_t bufferSize_; // Legacy + size_t writePos_; + size_t validSamples_; + + // Circular buffers for filtered L/R + std::vector leftBuffer_; + std::vector rightBuffer_; + + // Cascaded lowpass filters (4th order Butterworth at 8kHz per channel) + DSP::BiquadFilter leftLowpass1_; + DSP::BiquadFilter leftLowpass2_; + DSP::BiquadFilter rightLowpass1_; + DSP::BiquadFilter rightLowpass2_; + + // Legacy + std::vector points_; +}; + +} // namespace Visualizer diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..970eb1f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7938 @@ +{ + "name": "prism", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prism", + "version": "0.1.0", + "hasInstallScript": true, + "license": "GPL-3.0-only", + "dependencies": { + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "node-addon-api": "^8.5.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "zustand": "^5.0.10" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.1", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "autoprefixer": "^10.4.23", + "electron": "^40.0.0", + "electron-builder": "^26.0.0", + "electron-vite": "^5.0.0", + "node-gyp": "^10.3.1", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-lBSgDMQqt7QWMuIjS8zNAq5FI5o5RVBAcJUGWGI6GgoQITJt3msAkUrHp8YHj3RTVE+h70ndqMGqURjp3IfRyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz", + "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/app-builder-lib/node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/app-builder-lib/node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/app-builder-lib/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/app-builder-lib/node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/app-builder-lib/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/app-builder-lib/node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/app-builder-lib/node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/app-builder-lib/node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "40.8.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.8.3.tgz", + "integrity": "sha512-MH6LK4xM6VVmmtz0nRE0Fe8l2jTKSYTvH1t0ZfbNLw3o6dlBCVTRqQha6uL8ZQVoMy74JyLguGwK7dU7rCKIhw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", + "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "cac": "^6.7.14", + "esbuild": "^0.25.11", + "magic-string": "^0.30.19", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-corefoundation/node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/node-gyp/node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/temp/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bd5635b --- /dev/null +++ b/package.json @@ -0,0 +1,86 @@ +{ + "name": "prism", + "version": "0.1.0", + "description": "Open-source audio metering and visualization tool", + "main": "./out/main/index.js", + "scripts": { + "dev": "env -u ELECTRON_RUN_AS_NODE electron-vite dev", + "build": "electron-vite build", + "preview": "electron-vite preview", + "typecheck": "tsc --noEmit", + "build:native": "cd native && node-gyp rebuild", + "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'})\"", + "postinstall": "npm run rebuild:native || echo 'Native build failed, will use JS fallback'", + "dist": "npm run build && electron-builder --publish never", + "dist:mac": "npm run build && electron-builder --mac --publish never", + "dist:win": "npm run build && electron-builder --win --publish never", + "dist:linux": "npm run build && electron-builder --linux --publish never" + }, + "repository": { + "type": "git", + "url": "https://github.com/Boof2015/prism" + }, + "author": { + "name": "Boof2015", + "email": "contact@novaml.ai" + }, + "license": "GPL-3.0-only", + "dependencies": { + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "node-addon-api": "^8.5.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "zustand": "^5.0.10" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.1", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "autoprefixer": "^10.4.23", + "electron": "^40.0.0", + "electron-builder": "^26.0.0", + "electron-vite": "^5.0.0", + "node-gyp": "^10.3.1", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.3.1" + }, + "build": { + "appId": "com.astra.prism", + "productName": "Prism", + "directories": { + "output": "dist" + }, + "files": [ + "out/**/*", + "native/build/Release/*.node" + ], + "extraResources": [ + { + "from": "native/build/Release/", + "to": "native/", + "filter": ["*.node"] + } + ], + "mac": { + "target": ["dmg", "zip"], + "category": "public.app-category.music", + "hardenedRuntime": true, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.inherit.plist" + }, + "win": { + "target": [ + { "target": "nsis", "arch": ["x64"] }, + { "target": "portable", "arch": ["x64"] } + ] + }, + "linux": { + "target": ["AppImage", "deb"], + "category": "Audio" + } + } +} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..7c68eef --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,108 @@ +import { app, BrowserWindow, desktopCapturer, ipcMain, session } from 'electron' +import { join } from 'path' + +let mainWindow: BrowserWindow | null = null + +const WINDOW_DEFAULTS = { + width: 900, + height: 180, + minWidth: 400, + minHeight: 100, +} + +function createWindow(): void { + mainWindow = new BrowserWindow({ + ...WINDOW_DEFAULTS, + frame: false, + transparent: false, + backgroundColor: '#000000', + alwaysOnTop: true, + autoHideMenuBar: true, + resizable: true, + maximizable: false, + fullscreenable: false, + title: 'Prism', + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + contextIsolation: true, + nodeIntegration: false, + backgroundThrottling: false, + }, + }) + + mainWindow.on('closed', () => { + mainWindow = null + }) + + // Load the renderer + if (process.env.ELECTRON_RENDERER_URL) { + mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +// Auto-grant media (microphone) permission for audio capture +function setupPermissions(): void { + session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => { + if (permission === 'media' || permission === 'screen') { + callback(true) + } else { + callback(false) + } + }) +} + +// IPC handlers +function setupIPC(): void { + ipcMain.on('window:minimize', () => { + mainWindow?.minimize() + }) + + ipcMain.on('window:close', () => { + mainWindow?.close() + }) + + ipcMain.on('window:toggle-always-on-top', () => { + if (!mainWindow) return + const current = mainWindow.isAlwaysOnTop() + mainWindow.setAlwaysOnTop(!current) + mainWindow.webContents.send('window:always-on-top-changed', !current) + }) + + ipcMain.handle('window:is-always-on-top', () => { + return mainWindow?.isAlwaysOnTop() ?? true + }) + + ipcMain.handle('audio:get-desktop-sources', async () => { + const sources = await desktopCapturer.getSources({ types: ['screen'] }) + return sources.map((s) => ({ id: s.id, name: s.name })) + }) + + ipcMain.on('window:expand-settings', (_event, panelHeight: number) => { + if (!mainWindow) return + const [width, height] = mainWindow.getSize() + const [minW] = mainWindow.getMinimumSize() + mainWindow.setMinimumSize(minW, WINDOW_DEFAULTS.minHeight + panelHeight) + mainWindow.setSize(width, height + panelHeight, true) + }) + + ipcMain.on('window:collapse-settings', (_event, panelHeight: number) => { + if (!mainWindow) return + const [width, height] = mainWindow.getSize() + const [minW] = mainWindow.getMinimumSize() + mainWindow.setMinimumSize(minW, WINDOW_DEFAULTS.minHeight) + mainWindow.setSize(width, Math.max(WINDOW_DEFAULTS.minHeight, height - panelHeight), true) + }) +} + +app.whenReady().then(() => { + setupPermissions() + setupIPC() + createWindow() +}) + +app.on('window-all-closed', () => { + app.quit() +}) diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..9586949 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,32 @@ +import { contextBridge, ipcRenderer } from 'electron' + +// Expose Electron API to renderer +contextBridge.exposeInMainWorld('electronAPI', { + platform: process.platform, + minimize: () => ipcRenderer.send('window:minimize'), + close: () => ipcRenderer.send('window:close'), + toggleAlwaysOnTop: () => ipcRenderer.send('window:toggle-always-on-top'), + isAlwaysOnTop: () => ipcRenderer.invoke('window:is-always-on-top'), + getDesktopSources: () => ipcRenderer.invoke('audio:get-desktop-sources') as Promise<{ id: string; name: string }[]>, + expandSettings: (panelHeight: number) => ipcRenderer.send('window:expand-settings', panelHeight), + collapseSettings: (panelHeight: number) => ipcRenderer.send('window:collapse-settings', panelHeight), + onAlwaysOnTopChanged: (callback: (isOnTop: boolean) => void) => { + const handler = (_event: Electron.IpcRendererEvent, isOnTop: boolean): void => callback(isOnTop) + ipcRenderer.on('window:always-on-top-changed', handler) + return () => ipcRenderer.removeListener('window:always-on-top-changed', handler) + }, +}) + +// Native DSP module — load if available, gracefully degrade if not +let visualizerDSP: unknown = null +try { + const isDev = process.env.NODE_ENV === 'development' + const modulePath = isDev + ? require('path').join(__dirname, '../../native/build/Release/visualizer_dsp.node') + : require('path').join(process.resourcesPath!, 'native/visualizer_dsp.node') + visualizerDSP = require(modulePath) +} catch { + console.warn('Native DSP module not available — using JS fallback') +} + +contextBridge.exposeInMainWorld('visualizerAPI', visualizerDSP) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 0000000..5b1136a --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,122 @@ +import { useEffect } from 'react' +import { useAudioStore } from './stores/audioStore' +import Strip from './components/Strip' + +export default function App(): JSX.Element { + const { + devices, + selectedDeviceId, + captureMode, + isCapturing, + refreshDevices, + selectDevice, + setCaptureMode, + startCapture, + stopCapture, + } = useAudioStore() + + // Enumerate devices on mount + useEffect(() => { + refreshDevices() + navigator.mediaDevices.addEventListener('devicechange', refreshDevices) + return () => navigator.mediaDevices.removeEventListener('devicechange', refreshDevices) + }, [refreshDevices]) + + const handleSourceChange = (e: React.ChangeEvent): void => { + const value = e.target.value + if (value === '__system__') { + setCaptureMode('system') + } else { + selectDevice(value) + } + } + + const handleToggleCapture = (): void => { + if (isCapturing) { + stopCapture() + } else { + startCapture() + } + } + + return ( +
+ {/* Scope strip — fills all available space */} +
+ +
+ + {/* Temporary source picker bar — will be replaced by Toolbar + Settings in Phase 6 */} +
+ {/* Signal indicator */} +
+ + + + +
+
+ ) +} diff --git a/src/renderer/audio/AudioCapture.ts b/src/renderer/audio/AudioCapture.ts new file mode 100644 index 0000000..ef4af6f --- /dev/null +++ b/src/renderer/audio/AudioCapture.ts @@ -0,0 +1,176 @@ +/** + * AudioCapture — captures system audio output via Electron's desktopCapturer, + * with fallback to getUserMedia for virtual audio devices (BlackHole, etc). + * Feeds captured samples into AudioRouter for distribution to visualizers. + */ + +import { audioRouter } from './AudioRouter' + +export type CaptureMode = 'system' | 'device' + +class AudioCapture { + private audioContext: AudioContext | null = null + private stream: MediaStream | null = null + private sourceNode: MediaStreamAudioSourceNode | null = null + private workletNode: AudioWorkletNode | null = null + private selectedDeviceId: string | null = null + private captureMode: CaptureMode = 'system' + + /** + * Start capturing system audio output via desktopCapturer (ScreenCaptureKit on macOS 13+). + * This captures all system audio without needing BlackHole or any virtual device. + */ + async startSystemAudio(): Promise { + this.stop() + + // Get a screen source ID from the main process + const sources = await window.electronAPI.getDesktopSources() + if (!sources.length) { + throw new Error('No desktop sources available for system audio capture') + } + + // Create AudioContext + this.audioContext = new AudioContext() + await this.audioContext.audioWorklet.addModule('./capture-worklet.js') + + // Request system audio via desktop capturer — must include video (Chromium requirement), + // but we immediately discard the video track + this.stream = await navigator.mediaDevices.getUserMedia({ + audio: { + mandatory: { + chromeMediaSource: 'desktop', + }, + } as unknown as MediaTrackConstraints, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sources[0].id, + }, + } as unknown as MediaTrackConstraints, + }) + + // Drop the video track immediately — we only need audio + this.stream.getVideoTracks().forEach((track) => track.stop()) + + this.wireUpStream() + this.captureMode = 'system' + } + + /** + * Start capturing from a specific audio input device (e.g. BlackHole, microphone). + * Fallback for when system audio capture isn't available. + */ + async startDevice(deviceId?: string): Promise { + this.stop() + + const targetDeviceId = deviceId ?? this.selectedDeviceId + + this.audioContext = new AudioContext() + await this.audioContext.audioWorklet.addModule('./capture-worklet.js') + + const constraints: MediaStreamConstraints = { + audio: { + ...(targetDeviceId ? { deviceId: { exact: targetDeviceId } } : {}), + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false, + channelCount: 2, + } as MediaTrackConstraints, + } + + this.stream = await navigator.mediaDevices.getUserMedia(constraints) + + this.wireUpStream() + this.captureMode = 'device' + + if (targetDeviceId) { + this.selectedDeviceId = targetDeviceId + } + } + + /** + * Start capture — uses system audio by default, falls back to device capture. + */ + async start(deviceId?: string): Promise { + if (deviceId) { + return this.startDevice(deviceId) + } + + try { + await this.startSystemAudio() + } catch (err) { + console.warn('System audio capture failed, falling back to device capture:', err) + await this.startDevice(deviceId) + } + } + + private wireUpStream(): void { + if (!this.audioContext || !this.stream) return + + this.sourceNode = this.audioContext.createMediaStreamSource(this.stream) + + this.workletNode = new AudioWorkletNode(this.audioContext, 'capture-processor', { + numberOfInputs: 1, + numberOfOutputs: 0, + channelCount: 2, + }) + + this.workletNode.port.onmessage = (event: MessageEvent<{ left: Float32Array; right: Float32Array }>) => { + audioRouter.ingestChunk(event.data.left, event.data.right) + } + + this.sourceNode.connect(this.workletNode) + + audioRouter.setSampleRate(this.audioContext.sampleRate) + audioRouter.setCapturing(true) + } + + stop(): void { + audioRouter.setCapturing(false) + audioRouter.reset() + + if (this.workletNode) { + this.workletNode.disconnect() + this.workletNode.port.onmessage = null + this.workletNode = null + } + + if (this.sourceNode) { + this.sourceNode.disconnect() + this.sourceNode = null + } + + if (this.stream) { + this.stream.getTracks().forEach((track) => track.stop()) + this.stream = null + } + + if (this.audioContext) { + this.audioContext.close() + this.audioContext = null + } + } + + async listDevices(): Promise { + const devices = await navigator.mediaDevices.enumerateDevices() + return devices.filter((d) => d.kind === 'audioinput') + } + + getSelectedDeviceId(): string | null { + return this.selectedDeviceId + } + + setSelectedDeviceId(id: string | null): void { + this.selectedDeviceId = id + } + + getCaptureMode(): CaptureMode { + return this.captureMode + } + + getSampleRate(): number { + return this.audioContext?.sampleRate ?? 48000 + } +} + +export const audioCapture = new AudioCapture() diff --git a/src/renderer/audio/AudioRouter.ts b/src/renderer/audio/AudioRouter.ts new file mode 100644 index 0000000..3c01b95 --- /dev/null +++ b/src/renderer/audio/AudioRouter.ts @@ -0,0 +1,165 @@ +/** + * AudioRouter — distributes captured audio samples to per-scope pending buffers. + * Pattern extracted from Astra's AudioEngine (lines 196-202, 470-561, 2997-3043). + */ + +const MAX_PENDING_CHUNKS = 20 +const MAX_PENDING_SPECTRUM_CHUNKS = 96 +const MAX_PENDING_VECTORSCOPE_CHUNKS = 20 + +class AudioRouter { + private pendingOscilloscopeSamples: Float32Array[] = [] + private pendingSpectrumSamples: Float32Array[] = [] + private pendingSpectrogramSamples: Float32Array[] = [] + private pendingVectorscopeSamples: { left: Float32Array; right: Float32Array }[] = [] + private pendingVUMeterSamples: { left: Float32Array; right: Float32Array }[] = [] + private pendingLUFSMeterSamples: { left: Float32Array; right: Float32Array }[] = [] + private pendingWaveformSamples: Float32Array[] = [] + + private _sampleRate = 48000 + private _capturing = false + + setSampleRate(rate: number): void { + this._sampleRate = rate + } + + getSampleRate(): number { + return this._sampleRate + } + + setCapturing(capturing: boolean): void { + this._capturing = capturing + } + + isCapturing(): boolean { + return this._capturing + } + + ingestChunk(left: Float32Array, right: Float32Array): void { + // Compute mono + const len = Math.min(left.length, right.length) + const mono = new Float32Array(len) + for (let i = 0; i < len; i++) { + mono[i] = (left[i] + right[i]) / 2 + } + + // Oscilloscope — uses left channel + if (this.pendingOscilloscopeSamples.length >= MAX_PENDING_CHUNKS) { + this.pendingOscilloscopeSamples = this.pendingOscilloscopeSamples.slice( + -Math.floor(MAX_PENDING_CHUNKS / 2) + ) + } + this.pendingOscilloscopeSamples.push(new Float32Array(left)) + + // Spectrum — uses mono + if (this.pendingSpectrumSamples.length >= MAX_PENDING_SPECTRUM_CHUNKS) { + this.pendingSpectrumSamples = this.pendingSpectrumSamples.slice( + -Math.floor(MAX_PENDING_SPECTRUM_CHUNKS / 2) + ) + } + this.pendingSpectrumSamples.push(mono) + + // Spectrogram — uses mono + if (this.pendingSpectrogramSamples.length >= MAX_PENDING_SPECTRUM_CHUNKS) { + this.pendingSpectrogramSamples = this.pendingSpectrogramSamples.slice( + -Math.floor(MAX_PENDING_SPECTRUM_CHUNKS / 2) + ) + } + this.pendingSpectrogramSamples.push(mono) + + // Vectorscope — uses stereo + if (this.pendingVectorscopeSamples.length >= MAX_PENDING_VECTORSCOPE_CHUNKS) { + this.pendingVectorscopeSamples = this.pendingVectorscopeSamples.slice( + -Math.floor(MAX_PENDING_VECTORSCOPE_CHUNKS / 2) + ) + } + this.pendingVectorscopeSamples.push({ + left: new Float32Array(left), + right: new Float32Array(right), + }) + + // VU Meter — uses stereo + if (this.pendingVUMeterSamples.length >= MAX_PENDING_VECTORSCOPE_CHUNKS) { + this.pendingVUMeterSamples = this.pendingVUMeterSamples.slice( + -Math.floor(MAX_PENDING_VECTORSCOPE_CHUNKS / 2) + ) + } + this.pendingVUMeterSamples.push({ + left: new Float32Array(left), + right: new Float32Array(right), + }) + + // LUFS Meter — uses stereo + if (this.pendingLUFSMeterSamples.length >= MAX_PENDING_SPECTRUM_CHUNKS) { + this.pendingLUFSMeterSamples = this.pendingLUFSMeterSamples.slice( + -Math.floor(MAX_PENDING_SPECTRUM_CHUNKS / 2) + ) + } + this.pendingLUFSMeterSamples.push({ + left: new Float32Array(left), + right: new Float32Array(right), + }) + + // Waveform — uses left channel + if (this.pendingWaveformSamples.length >= MAX_PENDING_SPECTRUM_CHUNKS) { + this.pendingWaveformSamples = this.pendingWaveformSamples.slice( + -Math.floor(MAX_PENDING_SPECTRUM_CHUNKS / 2) + ) + } + this.pendingWaveformSamples.push(new Float32Array(left)) + } + + flushPendingOscilloscopeSamples(): Float32Array[] { + const samples = this.pendingOscilloscopeSamples + this.pendingOscilloscopeSamples = [] + return samples + } + + flushPendingSpectrumSamples(): Float32Array[] { + const samples = this.pendingSpectrumSamples + this.pendingSpectrumSamples = [] + return samples + } + + flushPendingSpectrogramSamples(): Float32Array[] { + const samples = this.pendingSpectrogramSamples + this.pendingSpectrogramSamples = [] + return samples + } + + flushPendingVectorscopeSamples(): { left: Float32Array; right: Float32Array }[] { + const samples = this.pendingVectorscopeSamples + this.pendingVectorscopeSamples = [] + return samples + } + + flushPendingVUMeterSamples(): { left: Float32Array; right: Float32Array }[] { + const samples = this.pendingVUMeterSamples + this.pendingVUMeterSamples = [] + return samples + } + + flushPendingLUFSMeterSamples(): { left: Float32Array; right: Float32Array }[] { + const samples = this.pendingLUFSMeterSamples + this.pendingLUFSMeterSamples = [] + return samples + } + + flushPendingWaveformSamples(): Float32Array[] { + const samples = this.pendingWaveformSamples + this.pendingWaveformSamples = [] + return samples + } + + reset(): void { + this.pendingOscilloscopeSamples = [] + this.pendingSpectrumSamples = [] + this.pendingSpectrogramSamples = [] + this.pendingVectorscopeSamples = [] + this.pendingVUMeterSamples = [] + this.pendingLUFSMeterSamples = [] + this.pendingWaveformSamples = [] + } +} + +export const audioRouter = new AudioRouter() diff --git a/src/renderer/audio/native/index.ts b/src/renderer/audio/native/index.ts new file mode 100644 index 0000000..e97c379 --- /dev/null +++ b/src/renderer/audio/native/index.ts @@ -0,0 +1,142 @@ +// Native visualizer DSP module loader +// This loads the native C++ addon for high-performance audio visualization + +import type { VisualizerDSP, OscilloscopeResult, VectorscopeResult, VectorscopePointsResult } from './visualizer-dsp' + +let nativeModule: VisualizerDSP | null = null +let loadError: Error | null = null + +// Try to load the native module +// Try to load the native module from the exposed API +if (typeof window !== 'undefined' && window.visualizerAPI) { + nativeModule = window.visualizerAPI + console.log('Native visualizer DSP module loaded via preload') +} else { + console.warn('Native visualizer DSP module not available (not found in window.visualizerAPI)') + console.warn('Falling back to JavaScript implementation') + loadError = new Error('Native module not found in window.visualizerAPI') +} + +// Check if native module is available +export function isNativeAvailable(): boolean { + return nativeModule !== null +} + +export function getNativeLoadError(): Error | null { + return loadError +} + +// Circular buffer size (must match native code) +export const OSCILLOSCOPE_BUFFER_SIZE = 32768 + +// Export the native module functions with type safety +export const oscilloscope = { + setSampleRate: (sampleRate: number): void => { + nativeModule?.oscilloscope.setSampleRate(sampleRate) + }, + + setPitchLock: (enabled: boolean): void => { + nativeModule?.oscilloscope.setPitchLock(enabled) + }, + + setDisplaySamples: (samples: number): void => { + nativeModule?.oscilloscope.setDisplaySamples(samples) + }, + + // Push samples to circular buffer (for continuous capture) + pushSamples: (samples: Float32Array): void => { + nativeModule?.oscilloscope.pushSamples(samples) + }, + + // Process using circular buffer (continuous mode) + processContinuous: (): OscilloscopeResult | null => { + if (!nativeModule) return null + return nativeModule.oscilloscope.processContinuous() + }, + + // Legacy: process snapshot (pushes to buffer and processes) + process: (audioData: Float32Array): OscilloscopeResult | null => { + if (!nativeModule) return null + return nativeModule.oscilloscope.process(audioData) + }, + + // Get current write position + getWritePos: (): number => { + return nativeModule?.oscilloscope.getWritePos() ?? 0 + }, + + // Get samples from circular buffer for rendering + getSamples: (startPos: number, count: number): Float32Array | null => { + if (!nativeModule) return null + return nativeModule.oscilloscope.getSamples(startPos, count) + }, + + reset: (): void => { + nativeModule?.oscilloscope.reset() + } +} + +export const spectrum = { + setFFTSize: (size: number): void => { + nativeModule?.spectrum.setFFTSize(size) + }, + + getFFTSize: (): number => { + return nativeModule?.spectrum.getFFTSize() ?? 2048 + }, + + setSampleRate: (sampleRate: number): void => { + nativeModule?.spectrum.setSampleRate(sampleRate) + }, + + setSmoothing: (smoothing: number): void => { + nativeModule?.spectrum.setSmoothing(smoothing) + }, + + process: (audioData: Float32Array): Float32Array | null => { + if (!nativeModule) return null + return nativeModule.spectrum.process(audioData) + }, + + binToFrequency: (bin: number): number => { + return nativeModule?.spectrum.binToFrequency(bin) ?? 0 + }, + + reset: (): void => { + nativeModule?.spectrum.reset() + } +} + +export const vectorscope = { + setSampleRate: (sampleRate: number): void => { + nativeModule?.vectorscope.setSampleRate(sampleRate) + }, + + pushSamples: (leftChannel: Float32Array, rightChannel: Float32Array): void => { + nativeModule?.vectorscope.pushSamples(leftChannel, rightChannel) + }, + + getPoints: (maxPoints: number): VectorscopePointsResult | null => { + if (!nativeModule) return null + return nativeModule.vectorscope.getPoints(maxPoints) + }, + + setBufferSize: (size: number): void => { + nativeModule?.vectorscope.setBufferSize(size) + }, + + getBufferSize: (): number => { + return nativeModule?.vectorscope.getBufferSize() ?? 1024 + }, + + process: (leftChannel: Float32Array, rightChannel: Float32Array): VectorscopeResult | null => { + if (!nativeModule) return null + return nativeModule.vectorscope.process(leftChannel, rightChannel) + }, + + reset: (): void => { + nativeModule?.vectorscope.reset() + } +} + +export type { OscilloscopeResult, VectorscopeResult, VectorscopePointsResult } diff --git a/src/renderer/audio/native/oscilloscopeDisplaySamples.ts b/src/renderer/audio/native/oscilloscopeDisplaySamples.ts new file mode 100644 index 0000000..9248b18 --- /dev/null +++ b/src/renderer/audio/native/oscilloscopeDisplaySamples.ts @@ -0,0 +1,25 @@ +export const BASE_DISPLAY_SAMPLES = 2048 +export const BASE_RATE_MIN = 44100 +export const BASE_RATE_MAX = 48000 +export const MIN_DISPLAY_SAMPLES = 64 +export const MAX_DISPLAY_SAMPLES = 32767 + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)) +} + +export function getNormalizedOscilloscopeDisplaySamples(sampleRate: number): number { + const safeSampleRate = Number.isFinite(sampleRate) && sampleRate > 0 + ? sampleRate + : BASE_RATE_MAX + + let samples = BASE_DISPLAY_SAMPLES + + if (safeSampleRate < BASE_RATE_MIN) { + samples = Math.round(BASE_DISPLAY_SAMPLES * (safeSampleRate / BASE_RATE_MIN)) + } else if (safeSampleRate > BASE_RATE_MAX) { + samples = Math.round(BASE_DISPLAY_SAMPLES * (safeSampleRate / BASE_RATE_MAX)) + } + + return clamp(samples, MIN_DISPLAY_SAMPLES, MAX_DISPLAY_SAMPLES) +} diff --git a/src/renderer/audio/native/visualizer-dsp.d.ts b/src/renderer/audio/native/visualizer-dsp.d.ts new file mode 100644 index 0000000..2dda2f2 --- /dev/null +++ b/src/renderer/audio/native/visualizer-dsp.d.ts @@ -0,0 +1,74 @@ +// Type definitions for visualizer_dsp native addon + +export interface OscilloscopeResult { + triggerIndex: number; // float for sub-sample precision (position in circular buffer) + samplesToShow: number; + detectedPitch: number; + writePos: number; // current write position in circular buffer +} + +export interface VectorscopeResult { + x: Float32Array; + y: Float32Array; +} + +export interface VectorscopePointsResult { + x: Float32Array; + y: Float32Array; + count: number; +} + +// Circular buffer size (must match native code) +export const OSCILLOSCOPE_BUFFER_SIZE = 32768; + +export interface OscilloscopeModule { + setSampleRate(sampleRate: number): void; + setPitchLock(enabled: boolean): void; + setDisplaySamples(samples: number): void; + + // Push samples to circular buffer (for continuous capture) + pushSamples(samples: Float32Array): void; + + // Process using circular buffer (continuous mode) + processContinuous(): OscilloscopeResult; + + // Legacy: process snapshot (pushes to buffer and processes) + process(audioData: Float32Array): OscilloscopeResult; + + // Get current write position in circular buffer + getWritePos(): number; + + // Get samples from circular buffer for rendering + getSamples(startPos: number, count: number): Float32Array; + + reset(): void; +} + +export interface SpectrumModule { + setFFTSize(size: number): void; + getFFTSize(): number; + setSampleRate(sampleRate: number): void; + setSmoothing(smoothing: number): void; + process(audioData: Float32Array): Float32Array; + binToFrequency(bin: number): number; + reset(): void; +} + +export interface VectorscopeModule { + setSampleRate(sampleRate: number): void; + pushSamples(leftChannel: Float32Array, rightChannel: Float32Array): void; + getPoints(maxPoints: number): VectorscopePointsResult; + setBufferSize(size: number): void; + getBufferSize(): number; + process(leftChannel: Float32Array, rightChannel: Float32Array): VectorscopeResult; + reset(): void; +} + +export interface VisualizerDSP { + oscilloscope: OscilloscopeModule; + spectrum: SpectrumModule; + vectorscope: VectorscopeModule; +} + +declare const visualizerDSP: VisualizerDSP; +export default visualizerDSP; diff --git a/src/renderer/components/ScopeModule.tsx b/src/renderer/components/ScopeModule.tsx new file mode 100644 index 0000000..ef4ab60 --- /dev/null +++ b/src/renderer/components/ScopeModule.tsx @@ -0,0 +1,105 @@ +import { useEffect, useRef } from 'react' +import type { ScopeKind } from '../../types/scope' +import { SpectrumAnalyzer } from '../visualizers/SpectrumAnalyzer' +import { Oscilloscope } from '../visualizers/Oscilloscope' +import { Vectorscope } from '../visualizers/Vectorscope' + +interface ScopeModuleProps { + scopeKind: ScopeKind + lineColor?: string +} + +type Visualizer = SpectrumAnalyzer | Oscilloscope | Vectorscope + +function createVisualizer(scopeKind: ScopeKind, canvas: HTMLCanvasElement, lineColor: string): Visualizer | null { + switch (scopeKind) { + case 'spectrum': + return new SpectrumAnalyzer(canvas, { lineColor }) + case 'oscilloscope': + return new Oscilloscope(canvas, { lineColor }) + case 'vectorscope': + return new Vectorscope(canvas, { lineColor }) + default: + console.warn(`Scope type "${scopeKind}" not yet implemented`) + return null + } +} + +export default function ScopeModule({ scopeKind, lineColor = '#38bdf8' }: ScopeModuleProps): JSX.Element { + const containerRef = useRef(null) + const canvasRef = useRef(null) + const visualizerRef = useRef(null) + + // Initialize and manage visualizer lifecycle + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + + const viz = createVisualizer(scopeKind, canvas, lineColor) + if (!viz) return + + visualizerRef.current = viz + viz.start() + + return () => { + viz.dispose() + visualizerRef.current = null + } + }, [scopeKind]) // Only recreate when scope type changes + + // Update lineColor without recreating + useEffect(() => { + visualizerRef.current?.setOptions({ lineColor }) + }, [lineColor]) + + // ResizeObserver for DPI-aware canvas sizing + useEffect(() => { + const container = containerRef.current + const canvas = canvasRef.current + if (!container || !canvas) return + + const resizeCanvas = (): void => { + const rect = container.getBoundingClientRect() + const width = Math.max(1, Math.floor(rect.width)) + const height = Math.max(1, Math.floor(rect.height)) + const dpr = window.devicePixelRatio || 1 + + canvas.style.width = `${width}px` + canvas.style.height = `${height}px` + canvas.width = Math.max(1, Math.floor(width * dpr)) + canvas.height = Math.max(1, Math.floor(height * dpr)) + + visualizerRef.current?.resize() + } + + const observer = new ResizeObserver(resizeCanvas) + observer.observe(container) + resizeCanvas() // Initial size + + return () => observer.disconnect() + }, []) + + return ( +
+ +
+ ) +} diff --git a/src/renderer/components/Strip.tsx b/src/renderer/components/Strip.tsx new file mode 100644 index 0000000..8da8f96 --- /dev/null +++ b/src/renderer/components/Strip.tsx @@ -0,0 +1,21 @@ +import ScopeModule from './ScopeModule' + +export default function Strip(): JSX.Element { + return ( +
+ +
+ +
+ +
+ ) +} diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts new file mode 100644 index 0000000..36698ec --- /dev/null +++ b/src/renderer/env.d.ts @@ -0,0 +1,20 @@ +/// + +import type { VisualizerDSP } from './audio/native/visualizer-dsp' + +declare global { + interface Window { + visualizerAPI: VisualizerDSP | null + electronAPI: { + platform: string + minimize: () => void + close: () => void + toggleAlwaysOnTop: () => void + isAlwaysOnTop: () => Promise + getDesktopSources: () => Promise<{ id: string; name: string }[]> + expandSettings: (panelHeight: number) => void + collapseSettings: (panelHeight: number) => void + onAlwaysOnTopChanged: (callback: (isOnTop: boolean) => void) => () => void + } + } +} diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..6d09f9a --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + + Prism + + +
+ + + diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx new file mode 100644 index 0000000..729d5a5 --- /dev/null +++ b/src/renderer/main.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles/globals.css' +import '@fontsource/inter/400.css' +import '@fontsource/inter/500.css' +import '@fontsource/inter/600.css' +import '@fontsource/jetbrains-mono/400.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/src/renderer/public/capture-worklet.js b/src/renderer/public/capture-worklet.js new file mode 100644 index 0000000..f922412 --- /dev/null +++ b/src/renderer/public/capture-worklet.js @@ -0,0 +1,20 @@ +class CaptureProcessor extends AudioWorkletProcessor { + process(inputs) { + const input = inputs[0] + if (!input || input.length === 0) return true + + const left = input[0] + if (!left || left.length === 0) return true + + const right = input.length > 1 ? input[1] : left + + this.port.postMessage({ + left: left.slice(), + right: right.slice(), + }) + + return true + } +} + +registerProcessor('capture-processor', CaptureProcessor) diff --git a/src/renderer/stores/audioStore.ts b/src/renderer/stores/audioStore.ts new file mode 100644 index 0000000..6c19b41 --- /dev/null +++ b/src/renderer/stores/audioStore.ts @@ -0,0 +1,66 @@ +import { create } from 'zustand' +import { audioCapture, type CaptureMode } from '../audio/AudioCapture' + +interface AudioState { + devices: MediaDeviceInfo[] + selectedDeviceId: string | null + captureMode: CaptureMode + isCapturing: boolean + sampleRate: number + refreshDevices: () => Promise + selectDevice: (deviceId: string) => Promise + setCaptureMode: (mode: CaptureMode) => void + startCapture: () => Promise + stopCapture: () => void +} + +export const useAudioStore = create((set, get) => ({ + devices: [], + selectedDeviceId: null, + captureMode: 'system', + isCapturing: false, + sampleRate: 48000, + + refreshDevices: async () => { + const devices = await audioCapture.listDevices() + set({ devices }) + }, + + selectDevice: async (deviceId: string) => { + set({ selectedDeviceId: deviceId, captureMode: 'device' }) + audioCapture.setSelectedDeviceId(deviceId) + + // If currently capturing, restart with new device + if (get().isCapturing) { + await get().startCapture() + } + }, + + setCaptureMode: (mode: CaptureMode) => { + set({ captureMode: mode }) + }, + + startCapture: async () => { + try { + const { captureMode, selectedDeviceId } = get() + if (captureMode === 'system') { + await audioCapture.startSystemAudio() + } else { + await audioCapture.startDevice(selectedDeviceId ?? undefined) + } + set({ + isCapturing: true, + sampleRate: audioCapture.getSampleRate(), + captureMode: audioCapture.getCaptureMode(), + }) + } catch (err) { + console.error('Failed to start audio capture:', err) + set({ isCapturing: false }) + } + }, + + stopCapture: () => { + audioCapture.stop() + set({ isCapturing: false }) + }, +})) diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css new file mode 100644 index 0000000..2f94600 --- /dev/null +++ b/src/renderer/styles/globals.css @@ -0,0 +1,54 @@ +@import 'tailwindcss'; + +:root { + --bg-primary: #000000; + --bg-secondary: #050505; + --bg-tertiary: #0a0a0a; + + --glass-bg: rgba(255, 255, 255, 0.03); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-highlight: rgba(255, 255, 255, 0.045); + + --text-primary: rgba(255, 255, 255, 0.95); + --text-secondary: rgba(255, 255, 255, 0.6); + --text-tertiary: rgba(255, 255, 255, 0.4); + + --accent: #38bdf8; + --accent-hover: #7dd3fc; + --accent-glow: rgba(56, 189, 248, 0.3); + --accent-rgb: 56, 189, 248; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #root { + width: 100%; + height: 100%; + overflow: hidden; + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); +} diff --git a/src/renderer/visualizers/LUFSMeter.ts b/src/renderer/visualizers/LUFSMeter.ts new file mode 100644 index 0000000..83a2046 --- /dev/null +++ b/src/renderer/visualizers/LUFSMeter.ts @@ -0,0 +1,467 @@ +import { audioRouter } from '../audio/AudioRouter' +import type { LUFSMeterMode } from '../../types/lufsmeter' + +export interface LUFSMeterDataSource { + getPendingLUFSMeterSamples: () => Array<{ left: Float32Array; right: Float32Array }> + getSampleRate: () => number + isPlaying: () => boolean +} + +export interface LUFSMeterOptions { + mode?: LUFSMeterMode + lineColor?: string + dataSource?: LUFSMeterDataSource +} + +type ResolvedLUFSMeterOptions = Required> + +const defaultOptions: ResolvedLUFSMeterOptions = { + mode: 'bar', + lineColor: '#38bdf8', +} + +const defaultLUFSMeterDataSource: LUFSMeterDataSource = { + getPendingLUFSMeterSamples: () => audioRouter.flushPendingLUFSMeterSamples(), + getSampleRate: () => audioRouter.getSampleRate(), + isPlaying: () => audioRouter.isCapturing(), +} + +// ---- Constants ---- + +const METER_MIN_LUFS = -60 +const METER_MAX_LUFS = 0 +const MOMENTARY_WINDOW_S = 0.4 +const SHORT_TERM_WINDOW_S = 3.0 +const INTEGRATED_BLOCK_S = 0.4 +const INTEGRATED_HOP_S = 0.1 +const ABSOLUTE_GATE_LUFS = -70 +const RELATIVE_GATE_OFFSET = -10 +const TARGET_LUFS = -14 +const SMOOTHING = 0.7 + +// ---- K-weighting filter coefficients (ITU-R BS.1770) ---- + +interface BiquadCoeffs { + b0: number; b1: number; b2: number + a1: number; a2: number +} + +// Pre-filter (high shelf) — 48kHz +const PRE_FILTER_48K: BiquadCoeffs = { + b0: 1.53512485958697, b1: -2.69169618940638, b2: 1.19839281085285, + a1: -1.69065929318241, a2: 0.73248077421585, +} + +// RLB weighting (high pass) — 48kHz +const RLB_FILTER_48K: BiquadCoeffs = { + b0: 1.0, b1: -2.0, b2: 1.0, + a1: -1.99004745483398, a2: 0.99007225036621, +} + +// Pre-filter — 44.1kHz +const PRE_FILTER_44K: BiquadCoeffs = { + b0: 1.5308412300498355, b1: -2.6509799951536985, b2: 1.1690790799210956, + a1: -1.6636551132560204, a2: 0.7125954280732254, +} + +// RLB — 44.1kHz +const RLB_FILTER_44K: BiquadCoeffs = { + b0: 1.0, b1: -2.0, b2: 1.0, + a1: -1.9891696736297957, a2: 0.9891990357870394, +} + +function getKWeightingCoeffs(sampleRate: number): { pre: BiquadCoeffs; rlb: BiquadCoeffs } { + if (Math.abs(sampleRate - 44100) < 100) { + return { pre: PRE_FILTER_44K, rlb: RLB_FILTER_44K } + } + // Default to 48kHz (also reasonable approximation for 96kHz, etc.) + return { pre: PRE_FILTER_48K, rlb: RLB_FILTER_48K } +} + +// ---- Biquad filter state ---- + +interface BiquadState { + x1: number; x2: number + y1: number; y2: number +} + +function createBiquadState(): BiquadState { + return { x1: 0, x2: 0, y1: 0, y2: 0 } +} + +function applyBiquad(coeffs: BiquadCoeffs, state: BiquadState, input: number): number { + const output = coeffs.b0 * input + coeffs.b1 * state.x1 + coeffs.b2 * state.x2 + - coeffs.a1 * state.y1 - coeffs.a2 * state.y2 + state.x2 = state.x1 + state.x1 = input + state.y2 = state.y1 + state.y1 = output + return output +} + +// ---- Color utilities ---- + +function parseHexColor(hex: string): [number, number, number] { + const h = hex.replace('#', '') + return [ + parseInt(h.substring(0, 2), 16) || 56, + parseInt(h.substring(2, 4), 16) || 189, + parseInt(h.substring(4, 6), 16) || 248, + ] +} + +// ---- LUFS Meter class ---- + +export class LUFSMeter { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private options: ResolvedLUFSMeterOptions + private dataSource: LUFSMeterDataSource + private animationId: number | null = null + private isRunning = false + + // K-weighting filter state (per channel, two stages) + private preFilterL = createBiquadState() + private preFilterR = createBiquadState() + private rlbFilterL = createBiquadState() + private rlbFilterR = createBiquadState() + private currentSampleRate = 48000 + private kWeightingCoeffs = getKWeightingCoeffs(48000) + + // Ring buffer for K-weighted squared samples (sized for SHORT_TERM_WINDOW_S) + private ringBufferL = new Float32Array(0) + private ringBufferR = new Float32Array(0) + private ringBufferPos = 0 + private ringBufferFilled = 0 // how many samples have been written total (capped at buffer size) + + // Integrated loudness: accumulate 400ms block mean-squares with 100ms hop + private integratedBlockSumL = 0 + private integratedBlockSumR = 0 + private integratedBlockSamples = 0 + private integratedHopCounter = 0 + private integratedBlockLoudness: number[] = [] // LUFS per block + + // Smoothed display values + private momentaryLUFS = METER_MIN_LUFS + private shortTermLUFS = METER_MIN_LUFS + private integratedLUFS = METER_MIN_LUFS + + constructor(canvas: HTMLCanvasElement, options: LUFSMeterOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + + const { dataSource, ...optionOverrides } = options + this.options = { ...defaultOptions, ...optionOverrides } + this.dataSource = dataSource ?? defaultLUFSMeterDataSource + + this.initRingBuffer(this.dataSource.getSampleRate()) + } + + private initRingBuffer(sampleRate: number): void { + this.currentSampleRate = Math.max(1, sampleRate) + this.kWeightingCoeffs = getKWeightingCoeffs(this.currentSampleRate) + const bufferSize = Math.ceil(this.currentSampleRate * SHORT_TERM_WINDOW_S) + this.ringBufferL = new Float32Array(bufferSize) + this.ringBufferR = new Float32Array(bufferSize) + this.ringBufferPos = 0 + this.ringBufferFilled = 0 + } + + private resetMeters(): void { + this.momentaryLUFS = METER_MIN_LUFS + this.shortTermLUFS = METER_MIN_LUFS + this.integratedLUFS = METER_MIN_LUFS + this.ringBufferL.fill(0) + this.ringBufferR.fill(0) + this.ringBufferPos = 0 + this.ringBufferFilled = 0 + this.integratedBlockSumL = 0 + this.integratedBlockSumR = 0 + this.integratedBlockSamples = 0 + this.integratedHopCounter = 0 + this.integratedBlockLoudness = [] + this.preFilterL = createBiquadState() + this.preFilterR = createBiquadState() + this.rlbFilterL = createBiquadState() + this.rlbFilterR = createBiquadState() + } + + setOptions(options: Partial): void { + const { dataSource, ...optionUpdates } = options + this.options = { ...this.options, ...optionUpdates } + if (dataSource) { + this.dataSource = dataSource + } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { + // Canvas resize handled externally + } + + private processAudio(): void { + const chunks = this.dataSource.getPendingLUFSMeterSamples() + + // Check if sample rate changed + const sr = this.dataSource.getSampleRate() + if (Math.abs(sr - this.currentSampleRate) > 100) { + this.initRingBuffer(sr) + this.resetMeters() + } + + const playing = this.dataSource.isPlaying() + + if (!playing && chunks.length === 0) { + // Decay toward silence only when truly stopped + this.momentaryLUFS = this.momentaryLUFS * SMOOTHING + METER_MIN_LUFS * (1 - SMOOTHING) + this.shortTermLUFS = this.shortTermLUFS * SMOOTHING + METER_MIN_LUFS * (1 - SMOOTHING) + return + } + + // Process any new audio chunks into the ring buffer + if (chunks.length > 0) { + const { pre, rlb } = this.kWeightingCoeffs + const bufLen = this.ringBufferL.length + const hopSamples = Math.round(this.currentSampleRate * INTEGRATED_HOP_S) + const blockSamples = Math.round(this.currentSampleRate * INTEGRATED_BLOCK_S) + + for (const chunk of chunks) { + const len = Math.min(chunk.left.length, chunk.right.length) + for (let i = 0; i < len; i++) { + // Apply K-weighting: pre-filter then RLB, per channel + const kwL = applyBiquad(rlb, this.rlbFilterL, applyBiquad(pre, this.preFilterL, chunk.left[i])) + const kwR = applyBiquad(rlb, this.rlbFilterR, applyBiquad(pre, this.preFilterR, chunk.right[i])) + + // Store squared K-weighted samples in ring buffer + const sqL = kwL * kwL + const sqR = kwR * kwR + this.ringBufferL[this.ringBufferPos] = sqL + this.ringBufferR[this.ringBufferPos] = sqR + this.ringBufferPos = (this.ringBufferPos + 1) % bufLen + if (this.ringBufferFilled < bufLen) this.ringBufferFilled++ + + // Accumulate for integrated measurement + this.integratedBlockSumL += sqL + this.integratedBlockSumR += sqR + this.integratedBlockSamples++ + this.integratedHopCounter++ + + // Every hop interval, store a block loudness value + if (this.integratedHopCounter >= hopSamples && this.integratedBlockSamples >= blockSamples) { + const meanSqL = this.integratedBlockSumL / this.integratedBlockSamples + const meanSqR = this.integratedBlockSumR / this.integratedBlockSamples + const blockLUFS = -0.691 + 10 * Math.log10(Math.max(meanSqL + meanSqR, 1e-10)) + this.integratedBlockLoudness.push(blockLUFS) + + // Slide the block window: remove oldest hop worth of samples + // Approximate by keeping a running sum and subtracting the hop fraction + const hopFraction = hopSamples / this.integratedBlockSamples + this.integratedBlockSumL *= (1 - hopFraction) + this.integratedBlockSumR *= (1 - hopFraction) + this.integratedBlockSamples = Math.round(this.integratedBlockSamples * (1 - hopFraction)) + this.integratedHopCounter = 0 + } + } + } + } + + // Always compute M/S from the ring buffer (it persists across frames) + const bufLen = this.ringBufferL.length + + // Compute momentary loudness (last 400ms) + const momentarySamples = Math.min( + Math.round(this.currentSampleRate * MOMENTARY_WINDOW_S), + this.ringBufferFilled + ) + if (momentarySamples > 0) { + let sumL = 0, sumR = 0 + for (let i = 0; i < momentarySamples; i++) { + const idx = (this.ringBufferPos - 1 - i + bufLen) % bufLen + sumL += this.ringBufferL[idx] + sumR += this.ringBufferR[idx] + } + const rawM = -0.691 + 10 * Math.log10(Math.max(sumL / momentarySamples + sumR / momentarySamples, 1e-10)) + this.momentaryLUFS = this.momentaryLUFS * SMOOTHING + Math.max(METER_MIN_LUFS, rawM) * (1 - SMOOTHING) + } + + // Compute short-term loudness (last 3s) + const shortTermSamples = Math.min( + Math.round(this.currentSampleRate * SHORT_TERM_WINDOW_S), + this.ringBufferFilled + ) + if (shortTermSamples > 0) { + let sumL = 0, sumR = 0 + for (let i = 0; i < shortTermSamples; i++) { + const idx = (this.ringBufferPos - 1 - i + bufLen) % bufLen + sumL += this.ringBufferL[idx] + sumR += this.ringBufferR[idx] + } + const rawS = -0.691 + 10 * Math.log10(Math.max(sumL / shortTermSamples + sumR / shortTermSamples, 1e-10)) + this.shortTermLUFS = this.shortTermLUFS * SMOOTHING + Math.max(METER_MIN_LUFS, rawS) * (1 - SMOOTHING) + } + + // Compute integrated loudness with gating + this.integratedLUFS = this.computeGatedIntegratedLoudness() + } + + private computeGatedIntegratedLoudness(): number { + const blocks = this.integratedBlockLoudness + if (blocks.length === 0) return METER_MIN_LUFS + + // Absolute gate: remove blocks below -70 LUFS + const afterAbsolute = blocks.filter(l => l > ABSOLUTE_GATE_LUFS) + if (afterAbsolute.length === 0) return METER_MIN_LUFS + + // Compute mean of blocks passing absolute gate + let sum = 0 + for (const l of afterAbsolute) sum += Math.pow(10, l / 10) + const ungatedMean = 10 * Math.log10(sum / afterAbsolute.length) + + // Relative gate: remove blocks below (ungatedMean - 10) LUFS + const relativeThreshold = ungatedMean + RELATIVE_GATE_OFFSET + const afterRelative = afterAbsolute.filter(l => l > relativeThreshold) + if (afterRelative.length === 0) return METER_MIN_LUFS + + // Final integrated loudness + let finalSum = 0 + for (const l of afterRelative) finalSum += Math.pow(10, l / 10) + return Math.max(METER_MIN_LUFS, 10 * Math.log10(finalSum / afterRelative.length)) + } + + private draw = (): void => { + if (!this.isRunning) return + + this.processAudio() + + const { canvas, ctx } = this + const width = canvas.width + const height = canvas.height + + if (width <= 0 || height <= 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + ctx.clearRect(0, 0, width, height) + + this.drawBars(width, height) + + this.animationId = requestAnimationFrame(this.draw) + } + + private drawBars(width: number, height: number): void { + const ctx = this.ctx + const [tintR, tintG, tintB] = parseHexColor(this.options.lineColor) + const dpr = window.devicePixelRatio || 1 + + const padding = Math.round(8 * dpr) + const labelHeight = Math.round(20 * dpr) + const readoutHeight = Math.round(18 * dpr) + const scaleWidth = Math.round(32 * dpr) + const barAreaTop = padding + labelHeight + const barAreaBottom = height - padding - readoutHeight + const barAreaHeight = Math.max(1, barAreaBottom - barAreaTop) + const barAreaWidth = width - scaleWidth - padding + + const barCount = 3 + const barGap = Math.round(4 * dpr) + const totalGaps = (barCount - 1) * barGap + const barWidth = Math.max(4, Math.floor((barAreaWidth - totalGaps) / barCount)) + + const values = [this.momentaryLUFS, this.shortTermLUFS, this.integratedLUFS] + const labels = ['M', 'S', 'I'] + const dbRange = METER_MAX_LUFS - METER_MIN_LUFS + + const fontSize = Math.min(Math.round(13 * dpr), Math.max(Math.round(9 * dpr), Math.round(barWidth * 0.4))) + ctx.textAlign = 'center' + ctx.textBaseline = 'top' + + for (let i = 0; i < barCount; i++) { + const x = scaleWidth + i * (barWidth + barGap) + const lufs = values[i] + const normalized = Math.max(0, Math.min(1, (lufs - METER_MIN_LUFS) / dbRange)) + const barH = Math.round(normalized * barAreaHeight) + + // Bar label + ctx.font = `600 ${fontSize}px "Inter", system-ui, sans-serif` + ctx.fillStyle = `rgba(${tintR}, ${tintG}, ${tintB}, 0.7)` + ctx.fillText(labels[i], x + barWidth / 2, padding) + + // Bar background + ctx.fillStyle = `rgba(${tintR}, ${tintG}, ${tintB}, 0.08)` + ctx.fillRect(x, barAreaTop, barWidth, barAreaHeight) + + // Bar fill — gradient from dim at bottom to bright at top + if (barH > 0) { + const gradient = ctx.createLinearGradient(0, barAreaBottom, 0, barAreaBottom - barH) + gradient.addColorStop(0, `rgba(${tintR}, ${tintG}, ${tintB}, 0.3)`) + gradient.addColorStop(0.5, `rgba(${tintR}, ${tintG}, ${tintB}, 0.6)`) + gradient.addColorStop(1, `rgba(${tintR}, ${tintG}, ${tintB}, 0.9)`) + ctx.fillStyle = gradient + ctx.fillRect(x, barAreaBottom - barH, barWidth, barH) + } + + // Bright cap line at top of bar + if (barH > 1) { + ctx.fillStyle = `rgb(${tintR}, ${tintG}, ${tintB})` + ctx.fillRect(x, barAreaBottom - barH, barWidth, Math.max(1, Math.round(2 * dpr))) + } + + // LUFS readout below bar + const displayLufs = lufs <= METER_MIN_LUFS + 1 ? '-∞' : lufs.toFixed(1) + ctx.font = `500 ${Math.max(Math.round(8 * dpr), fontSize - Math.round(2 * dpr))}px "JetBrains Mono", "SF Mono", monospace` + ctx.fillStyle = `rgba(${tintR}, ${tintG}, ${tintB}, 0.8)` + ctx.fillText(displayLufs, x + barWidth / 2, barAreaBottom + Math.round(4 * dpr)) + } + + // Target reference line (-14 LUFS) + const targetNorm = Math.max(0, Math.min(1, (TARGET_LUFS - METER_MIN_LUFS) / dbRange)) + const targetY = Math.round(barAreaBottom - targetNorm * barAreaHeight) + ctx.strokeStyle = `rgba(${tintR}, ${tintG}, ${tintB}, 0.25)` + ctx.lineWidth = Math.max(1, dpr) + ctx.setLineDash([Math.round(4 * dpr), Math.round(3 * dpr)]) + ctx.beginPath() + ctx.moveTo(scaleWidth, targetY) + ctx.lineTo(scaleWidth + barCount * barWidth + (barCount - 1) * barGap, targetY) + ctx.stroke() + ctx.setLineDash([]) + + // Scale markings on left + const scaleFont = Math.max(Math.round(7 * dpr), Math.round(9 * dpr)) + ctx.font = `400 ${scaleFont}px "JetBrains Mono", "SF Mono", monospace` + ctx.textAlign = 'right' + ctx.textBaseline = 'middle' + ctx.fillStyle = `rgba(${tintR}, ${tintG}, ${tintB}, 0.35)` + + const tickValues = [-60, -48, -36, -24, -18, -14, -9, -6, -3, 0] + for (const tick of tickValues) { + const norm = (tick - METER_MIN_LUFS) / dbRange + if (norm < 0 || norm > 1) continue + const y = Math.round(barAreaBottom - norm * barAreaHeight) + ctx.fillText(`${tick}`, scaleWidth - Math.round(4 * dpr), y) + + // Tick mark + ctx.fillRect(scaleWidth - Math.round(3 * dpr), y, Math.round(2 * dpr), Math.max(1, dpr)) + } + + } + + dispose(): void { + this.stop() + } +} diff --git a/src/renderer/visualizers/Oscilloscope.ts b/src/renderer/visualizers/Oscilloscope.ts new file mode 100644 index 0000000..9276b9f --- /dev/null +++ b/src/renderer/visualizers/Oscilloscope.ts @@ -0,0 +1,344 @@ +import { audioRouter } from '../audio/AudioRouter' +import { + oscilloscope as nativeOscilloscope, + OSCILLOSCOPE_BUFFER_SIZE, + isNativeAvailable +} from '../audio/native' +import { getNormalizedOscilloscopeDisplaySamples } from '../audio/native/oscilloscopeDisplaySamples' + +export interface OscilloscopeOptions { + lineColor?: string + lineWidth?: number + backgroundColor?: string + showGrid?: boolean + gridColor?: string + pitchLock?: boolean + underfillEnabled?: boolean +} + +const defaultOptions: Required = { + lineColor: '#00ffff', + lineWidth: 2, + backgroundColor: 'transparent', + showGrid: true, + gridColor: 'rgba(255, 255, 255, 0.1)', + pitchLock: true, + underfillEnabled: false +} + +function parseRgbChannels(color: string): string | null { + const normalized = color.trim() + + if (normalized.startsWith('#')) { + const hex = normalized.slice(1) + const expanded = hex.length === 3 + ? hex.split('').map((ch) => `${ch}${ch}`).join('') + : hex + + if (expanded.length === 6) { + const r = Number.parseInt(expanded.slice(0, 2), 16) + const g = Number.parseInt(expanded.slice(2, 4), 16) + const b = Number.parseInt(expanded.slice(4, 6), 16) + if (!Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b)) { + return `${r}, ${g}, ${b}` + } + } + } + + const rgbMatch = /^rgba?\((.*)\)$/i.exec(normalized) + if (!rgbMatch) return null + + const tokens = rgbMatch[1] + ?.split(',') + .map((token) => token.trim()) + .filter(Boolean) ?? [] + if (tokens.length < 3) return null + + const r = Number.parseFloat(tokens[0]) + const g = Number.parseFloat(tokens[1]) + const b = Number.parseFloat(tokens[2]) + if (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b)) return null + + return `${Math.max(0, Math.min(255, Math.round(r)))}, ${Math.max(0, Math.min(255, Math.round(g)))}, ${Math.max(0, Math.min(255, Math.round(b)))}` +} + +function highContrastUnderfillColor(accentColor: string, alpha: number): string { + const safeAlpha = Math.max(0, Math.min(1, alpha)) + const channels = parseRgbChannels(accentColor) + const nearWhite = { r: 245, g: 248, b: 252 } + const tintAmount = 0.18 + + if (!channels) { + return `rgba(${nearWhite.r}, ${nearWhite.g}, ${nearWhite.b}, ${safeAlpha})` + } + + const [accentR, accentG, accentB] = channels + .split(',') + .map((token) => Number.parseFloat(token.trim())) + + if (!Number.isFinite(accentR) || !Number.isFinite(accentG) || !Number.isFinite(accentB)) { + return `rgba(${nearWhite.r}, ${nearWhite.g}, ${nearWhite.b}, ${safeAlpha})` + } + + const mix = (base: number, tint: number): number => Math.round((base * (1 - tintAmount)) + (tint * tintAmount)) + const r = mix(nearWhite.r, accentR) + const g = mix(nearWhite.g, accentG) + const b = mix(nearWhite.b, accentB) + return `rgba(${r}, ${g}, ${b}, ${safeAlpha})` +} + +export class Oscilloscope { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private options: Required + private animationId: number | null = null + private isRunning: boolean = false + private nativeInitialized: boolean = false + private samplesReceived: number = 0 + private lastSampleRate: number = 0 + private static readonly WARMUP_SAMPLES = 4096 // Need ~4K samples before pitch detection is reliable + + constructor(canvas: HTMLCanvasElement, options: OscilloscopeOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + this.options = { ...defaultOptions, ...options } + + // Initialize native module + this.initNative() + } + + private initNative(): void { + if (isNativeAvailable() && !this.nativeInitialized) { + // Get actual sample rate from AudioEngine (defaults to 48000 if context not ready) + const sampleRate = audioRouter.getSampleRate() + this.lastSampleRate = sampleRate + nativeOscilloscope.setSampleRate(sampleRate) + nativeOscilloscope.setPitchLock(this.options.pitchLock) + nativeOscilloscope.setDisplaySamples(getNormalizedOscilloscopeDisplaySamples(sampleRate)) + // Note: Filter is now pitch-adaptive FIR bandpass (auto-configured in native code) + this.nativeInitialized = true + console.log(`Oscilloscope: Using native DSP with AudioWorklet (${sampleRate}Hz)`) + } else if (!isNativeAvailable()) { + console.error('Oscilloscope: Native DSP not available!') + } + } + + // Update sample rate if AudioContext changes (called from draw loop) + private updateSampleRateIfNeeded(): void { + if (!isNativeAvailable()) return + const currentRate = audioRouter.getSampleRate() + if (currentRate !== this.lastSampleRate && currentRate > 0) { + this.lastSampleRate = currentRate + nativeOscilloscope.setSampleRate(currentRate) + nativeOscilloscope.setDisplaySamples(getNormalizedOscilloscopeDisplaySamples(currentRate)) + console.log(`Oscilloscope: Sample rate updated to ${currentRate}Hz`) + } + } + + setOptions(options: Partial): void { + this.options = { ...this.options, ...options } + + // Update native module settings + if (isNativeAvailable() && options.pitchLock !== undefined) { + nativeOscilloscope.setPitchLock(options.pitchLock) + } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { } + + private draw = (): void => { + if (!this.isRunning) return + + const { canvas, ctx, options } = this + const width = canvas.width + const height = canvas.height + const dpr = window.devicePixelRatio || 1 + + ctx.clearRect(0, 0, width, height) + + if (options.backgroundColor !== 'transparent') { + ctx.fillStyle = options.backgroundColor + ctx.fillRect(0, 0, width, height) + } + + if (options.showGrid) { + this.drawGrid() + } + + // Native C++ is being fed continuously by AudioWorklet via AudioEngine + if (!isNativeAvailable()) { + console.error('Oscilloscope: Native DSP required') + this.animationId = requestAnimationFrame(this.draw) + return + } + + // Check if sample rate needs updating (AudioContext may have initialized after us) + this.updateSampleRateIfNeeded() + + // Flush ALL pending samples to native C++ (prevents sample loss) + const pendingSamples = audioRouter.flushPendingOscilloscopeSamples() + for (const chunk of pendingSamples) { + nativeOscilloscope.pushSamples(chunk) + this.samplesReceived += chunk.length + } + + // Skip pitch-locked processing during warmup period. + // Bypass mode (pitchLock=false) should render immediately using a moving window. + if (options.pitchLock && this.samplesReceived < Oscilloscope.WARMUP_SAMPLES) { + // During warmup, just show a static waveform or grid + this.animationId = requestAnimationFrame(this.draw) + return + } + + // Process using circular buffer - searches backwards from writePos + const result = nativeOscilloscope.processContinuous() + if (!result) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + const samplesToShow = result.samplesToShow + let triggerIndex = result.triggerIndex + + // In bypass mode, ignore trigger locking and follow the live write head. + // This produces free-running oscilloscope motion without touching pitch-lock behavior. + if (!options.pitchLock) { + const writePos = result.writePos + triggerIndex = writePos - samplesToShow + while (triggerIndex < 0) triggerIndex += OSCILLOSCOPE_BUFFER_SIZE + } + + // Get samples from circular buffer for rendering + const renderData = nativeOscilloscope.getSamples(Math.floor(triggerIndex), samplesToShow) + if (!renderData || renderData.length === 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + // Draw waveform (data already starts at trigger point) + const sliceWidth = width / samplesToShow + const centerY = height / 2 + const VISUAL_GAIN = 1.8 + const points: Array<{ x: number; y: number }> = [] + + for (let i = 0; i < samplesToShow && i < renderData.length; i++) { + const sample = renderData[i] + const y = ((1 - sample * VISUAL_GAIN) / 2) * height + const x = i * sliceWidth + points.push({ x, y }) + } + + if (points.length < 2) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + if (options.underfillEnabled) { + ctx.beginPath() + ctx.moveTo(points[0].x, centerY) + for (const point of points) { + ctx.lineTo(point.x, point.y) + } + ctx.lineTo(points[points.length - 1].x, centerY) + ctx.closePath() + const peakAlpha = 0.28 + const shoulderAlpha = peakAlpha * 0.74 + const centerlineAlpha = 0.09 + const fillGradient = ctx.createLinearGradient(0, 0, 0, height) + fillGradient.addColorStop(0, highContrastUnderfillColor(options.lineColor, peakAlpha)) + fillGradient.addColorStop(0.44, highContrastUnderfillColor(options.lineColor, peakAlpha * 0.94)) + fillGradient.addColorStop(0.48, highContrastUnderfillColor(options.lineColor, shoulderAlpha)) + fillGradient.addColorStop(0.5, highContrastUnderfillColor(options.lineColor, centerlineAlpha)) + fillGradient.addColorStop(0.52, highContrastUnderfillColor(options.lineColor, shoulderAlpha)) + fillGradient.addColorStop(0.56, highContrastUnderfillColor(options.lineColor, peakAlpha * 0.94)) + fillGradient.addColorStop(1, highContrastUnderfillColor(options.lineColor, peakAlpha)) + ctx.fillStyle = fillGradient + ctx.fill() + } + + ctx.lineWidth = options.lineWidth * dpr + ctx.strokeStyle = options.lineColor + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + ctx.beginPath() + ctx.moveTo(points[0].x, points[0].y) + for (let i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y) + } + ctx.stroke() + this.animationId = requestAnimationFrame(this.draw) + } + + private drawGrid(): void { + const { ctx, canvas, options } = this + const width = canvas.width + const height = canvas.height + const dpr = window.devicePixelRatio || 1 + + ctx.strokeStyle = options.gridColor + ctx.lineWidth = dpr + + ctx.beginPath() + ctx.moveTo(0, height / 2) + ctx.lineTo(width, height / 2) + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(width / 2, 0) + ctx.lineTo(width / 2, height) + ctx.stroke() + + ctx.strokeStyle = options.gridColor.replace('0.1', '0.05') + for (let i = 1; i < 4; i++) { + if (i === 2) continue + ctx.beginPath() + ctx.moveTo(0, (height / 4) * i) + ctx.lineTo(width, (height / 4) * i) + ctx.stroke() + ctx.beginPath() + ctx.moveTo((width / 4) * i, 0) + ctx.lineTo((width / 4) * i, height) + ctx.stroke() + } + } + + // Reset state for new track (call on track change to re-enable fast pitch convergence) + reset(): void { + // Reset JS warmup state + this.samplesReceived = 0 + + // Reset native state (clears buffers, resets pitch tracking, re-enables fast smoothing) + if (isNativeAvailable()) { + nativeOscilloscope.reset() + } + } + + dispose(): void { + this.stop() + + // Reset native module state + if (isNativeAvailable()) { + nativeOscilloscope.reset() + } + + // Reset warmup state + this.samplesReceived = 0 + this.lastSampleRate = 0 + } +} diff --git a/src/renderer/visualizers/Spectrogram.ts b/src/renderer/visualizers/Spectrogram.ts new file mode 100644 index 0000000..c711667 --- /dev/null +++ b/src/renderer/visualizers/Spectrogram.ts @@ -0,0 +1,691 @@ +import { audioRouter } from '../audio/AudioRouter' +import { + DEFAULT_SPECTROGRAM_CLARITY_MODE, + DEFAULT_SPECTROGRAM_SCALE_MODE, + DEFAULT_SPECTROGRAM_SCROLL_SPEED, + clampSpectrogramScrollSpeed, + isSpectrogramClarityMode, + isSpectrogramScaleMode, + type SpectrogramClarityMode, + type SpectrogramScaleMode, +} from '../../types/spectrogram' + +export interface SpectrogramDataSource { + getPendingSpectrogramSamples: () => Float32Array[] + getSampleRate: () => number + isPlaying: () => boolean +} + +export interface SpectrogramOptions { + fftSize?: number + minFrequency?: number + maxFrequency?: number + minDecibels?: number + maxDecibels?: number + scrollSpeed?: number + clarityMode?: SpectrogramClarityMode + scaleMode?: SpectrogramScaleMode + colorScheme?: 'heat' | 'mono' + lineColor?: string + dataSource?: SpectrogramDataSource +} + +type ResolvedSpectrogramOptions = Required> + +interface SpectrogramClarityProfile { + gamma: number // contrast curve exponent + sharpness: number // local peak suppression exponent (0 = off, higher = thinner lines) + tiltDb: number // dB/octave frequency compensation +} + +const defaultOptions: ResolvedSpectrogramOptions = { + fftSize: 4096, + minFrequency: 20, + maxFrequency: 20000, + minDecibels: -90, + maxDecibels: -12, + scrollSpeed: DEFAULT_SPECTROGRAM_SCROLL_SPEED, + clarityMode: DEFAULT_SPECTROGRAM_CLARITY_MODE, + scaleMode: DEFAULT_SPECTROGRAM_SCALE_MODE, + colorScheme: 'heat', + lineColor: '#38bdf8', +} + +const defaultSpectrogramDataSource: SpectrogramDataSource = { + getPendingSpectrogramSamples: () => audioRouter.flushPendingSpectrogramSamples(), + getSampleRate: () => audioRouter.getSampleRate(), + isPlaying: () => audioRouter.isCapturing(), +} + +function getClarityProfile(mode: SpectrogramClarityMode): SpectrogramClarityProfile { + switch (mode) { + case 'classic': + return { gamma: 1.4, sharpness: 0, tiltDb: 2.0 } + case 'sharp': + return { gamma: 1.5, sharpness: 2.5, tiltDb: 2.0 } + case 'sharper': + return { gamma: 1.6, sharpness: 5.0, tiltDb: 2.0 } + } +} + +function resolveClarityMode(value: unknown, fallback: SpectrogramClarityMode): SpectrogramClarityMode { + return isSpectrogramClarityMode(value) ? value : fallback +} + +function resolveScaleMode(value: unknown, fallback: SpectrogramScaleMode): SpectrogramScaleMode { + return isSpectrogramScaleMode(value) ? value : fallback +} + +function resolveOptions(base: ResolvedSpectrogramOptions, overrides: Partial): ResolvedSpectrogramOptions { + return { + fftSize: typeof overrides.fftSize === 'number' ? overrides.fftSize : base.fftSize, + minFrequency: typeof overrides.minFrequency === 'number' ? overrides.minFrequency : base.minFrequency, + maxFrequency: typeof overrides.maxFrequency === 'number' ? overrides.maxFrequency : base.maxFrequency, + minDecibels: typeof overrides.minDecibels === 'number' ? overrides.minDecibels : base.minDecibels, + maxDecibels: typeof overrides.maxDecibels === 'number' ? overrides.maxDecibels : base.maxDecibels, + scrollSpeed: overrides.scrollSpeed === undefined + ? base.scrollSpeed + : clampSpectrogramScrollSpeed(overrides.scrollSpeed), + clarityMode: resolveClarityMode(overrides.clarityMode, base.clarityMode), + scaleMode: resolveScaleMode(overrides.scaleMode, base.scaleMode), + colorScheme: overrides.colorScheme ?? base.colorScheme, + lineColor: overrides.lineColor ?? base.lineColor, + } +} + +const SLANEY_F_SP = 200 / 3 +const SLANEY_MIN_LOG_HZ = 1000 +const SLANEY_MIN_LOG_MEL = SLANEY_MIN_LOG_HZ / SLANEY_F_SP +const SLANEY_LOG_STEP = Math.log(6.4) / 27 + +function hzToMelSlaney(frequencyHz: number): number { + if (frequencyHz < SLANEY_MIN_LOG_HZ) { + return frequencyHz / SLANEY_F_SP + } + return SLANEY_MIN_LOG_MEL + (Math.log(frequencyHz / SLANEY_MIN_LOG_HZ) / SLANEY_LOG_STEP) +} + +function melToHzSlaney(mel: number): number { + if (mel < SLANEY_MIN_LOG_MEL) { + return mel * SLANEY_F_SP + } + return SLANEY_MIN_LOG_HZ * Math.exp(SLANEY_LOG_STEP * (mel - SLANEY_MIN_LOG_MEL)) +} + +function frequencyFromScale( + scaleMode: SpectrogramScaleMode, + minFrequency: number, + maxFrequency: number, + normalizedPosition: number +): number { + switch (scaleMode) { + case 'linear': + return minFrequency + (normalizedPosition * (maxFrequency - minFrequency)) + case 'log': { + const logMin = Math.log10(minFrequency) + const logMax = Math.log10(maxFrequency) + return 10 ** (logMin + (normalizedPosition * (logMax - logMin))) + } + case 'mel': { + const melMin = hzToMelSlaney(minFrequency) + const melMax = hzToMelSlaney(maxFrequency) + return melToHzSlaney(melMin + (normalizedPosition * (melMax - melMin))) + } + } +} + +function clamp01(value: number): number { + return Math.max(0, Math.min(1, value)) +} + +function fft(re: Float32Array, im: Float32Array): void { + const n = re.length + if (n <= 1) return + + let j = 0 + for (let i = 1; i < n; i += 1) { + let bit = n >> 1 + while (j & bit) { + j ^= bit + bit >>= 1 + } + j ^= bit + + if (i < j) { + let tmp = re[i] + re[i] = re[j] + re[j] = tmp + tmp = im[i] + im[i] = im[j] + im[j] = tmp + } + } + + for (let len = 2; len <= n; len <<= 1) { + const halfLen = len >> 1 + const angle = -2 * Math.PI / len + const wRe = Math.cos(angle) + const wIm = Math.sin(angle) + + for (let i = 0; i < n; i += len) { + let curRe = 1 + let curIm = 0 + + for (let k = 0; k < halfLen; k += 1) { + const evenIdx = i + k + const oddIdx = i + k + halfLen + + const tRe = curRe * re[oddIdx] - curIm * im[oddIdx] + const tIm = curRe * im[oddIdx] + curIm * re[oddIdx] + + re[oddIdx] = re[evenIdx] - tRe + im[oddIdx] = im[evenIdx] - tIm + re[evenIdx] += tRe + im[evenIdx] += tIm + + const nextRe = curRe * wRe - curIm * wIm + curIm = curRe * wIm + curIm * wRe + curRe = nextRe + } + } + } +} + +const hannWindowCache = new Map() + +function getHannWindow(size: number): Float32Array { + let window = hannWindowCache.get(size) + if (window) return window + + window = new Float32Array(size) + for (let i = 0; i < size; i += 1) { + window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (size - 1))) + } + hannWindowCache.set(size, window) + return window +} + +type ColorStop = { + at: number + color: [number, number, number] +} + +const HEAT_STOPS: readonly ColorStop[] = [ + { at: 0, color: [0, 0, 0] }, + { at: 0.14, color: [15, 7, 33] }, + { at: 0.32, color: [61, 11, 94] }, + { at: 0.54, color: [163, 26, 121] }, + { at: 0.74, color: [255, 82, 87] }, + { at: 0.9, color: [255, 166, 63] }, + { at: 1, color: [255, 241, 209] }, +] + +function lerpChannel(start: number, end: number, amount: number): number { + return Math.round(start + ((end - start) * amount)) +} + +function buildHeatLUT(): Uint8Array { + const lut = new Uint8Array(256 * 3) + + for (let index = 0; index < 256; index += 1) { + const t = index / 255 + let start = HEAT_STOPS[0] + let end = HEAT_STOPS[HEAT_STOPS.length - 1] + + for (let stopIndex = 0; stopIndex < HEAT_STOPS.length - 1; stopIndex += 1) { + const nextStop = HEAT_STOPS[stopIndex + 1] + if (t <= nextStop.at) { + start = HEAT_STOPS[stopIndex] + end = nextStop + break + } + } + + const span = Math.max(1e-6, end.at - start.at) + const amount = Math.max(0, Math.min(1, (t - start.at) / span)) + lut[index * 3] = lerpChannel(start.color[0], end.color[0], amount) + lut[index * 3 + 1] = lerpChannel(start.color[1], end.color[1], amount) + lut[index * 3 + 2] = lerpChannel(start.color[2], end.color[2], amount) + } + + return lut +} + +const HEAT_LUT = buildHeatLUT() + +function parseHexColor(hex: string): [number, number, number] { + const normalized = hex.replace('#', '') + return [ + Number.parseInt(normalized.substring(0, 2), 16) || 56, + Number.parseInt(normalized.substring(2, 4), 16) || 189, + Number.parseInt(normalized.substring(4, 6), 16) || 248, + ] +} + +// Zero-pad FFT for finer frequency resolution (visual interpolation) +const FFT_PAD_FACTOR = 4 + +export class Spectrogram { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private options: ResolvedSpectrogramOptions + private dataSource: SpectrogramDataSource + private animationId: number | null = null + private isRunning = false + + private fftRe: Float32Array + private fftIm: Float32Array + private sampleBuffer: Float32Array + private sampleBufferPos = 0 + + private waterfallCanvas: HTMLCanvasElement + private waterfallCtx: CanvasRenderingContext2D + + private rowCenterBins = new Float32Array(0) + private rowBandStartBins = new Float32Array(0) + private rowBandEndBins = new Float32Array(0) + private columnValues = new Float32Array(0) + private rawColumnValues = new Float32Array(0) + private columnImageData: ImageData | null = null + + private lastWidth = 0 + private lastHeight = 0 + private lastFftSize = 0 + private lastSampleRate = 0 + private lastMinFrequency = 0 + private lastMaxFrequency = 0 + private lastScaleMode: SpectrogramScaleMode | null = null + + constructor(canvas: HTMLCanvasElement, options: SpectrogramOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + + const { dataSource, ...optionOverrides } = options + this.options = resolveOptions(defaultOptions, optionOverrides) + this.dataSource = dataSource ?? defaultSpectrogramDataSource + + const windowSize = this.options.fftSize + const paddedSize = windowSize * FFT_PAD_FACTOR + this.fftRe = new Float32Array(paddedSize) + this.fftIm = new Float32Array(paddedSize) + this.sampleBuffer = new Float32Array(windowSize) + + this.waterfallCanvas = document.createElement('canvas') + this.waterfallCanvas.width = canvas.width + this.waterfallCanvas.height = canvas.height + const waterfallCtx = this.waterfallCanvas.getContext('2d') + if (!waterfallCtx) throw new Error('Could not get waterfall 2D context') + this.waterfallCtx = waterfallCtx + + this.ctx.imageSmoothingEnabled = false + this.waterfallCtx.imageSmoothingEnabled = false + } + + private resetDisplay(): void { + this.sampleBufferPos = 0 + this.waterfallCtx.clearRect(0, 0, this.waterfallCanvas.width, this.waterfallCanvas.height) + } + + setOptions(options: Partial): void { + const { dataSource, ...optionUpdates } = options + const previousOptions = this.options + this.options = resolveOptions(previousOptions, optionUpdates) + + if (dataSource) { + this.dataSource = dataSource + } + + if (this.options.fftSize !== previousOptions.fftSize) { + const windowSize = this.options.fftSize + const paddedSize = windowSize * FFT_PAD_FACTOR + this.fftRe = new Float32Array(paddedSize) + this.fftIm = new Float32Array(paddedSize) + this.sampleBuffer = new Float32Array(windowSize) + this.sampleBufferPos = 0 + this.lastFftSize = 0 + this.resetDisplay() + } else if (this.options.scaleMode !== previousOptions.scaleMode) { + this.resetDisplay() + } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { + this.lastWidth = 0 + this.lastHeight = 0 + } + + private ensureColumnBuffers(height: number): void { + if (height <= 0) return + if (this.columnValues.length === height && this.columnImageData && this.columnImageData.height === height) { + return + } + + this.columnValues = new Float32Array(height) + this.rawColumnValues = new Float32Array(height) + this.columnImageData = new ImageData(1, height) + } + + private shiftAndPaintColumn(values: Float32Array): void { + const width = this.waterfallCanvas.width + const height = this.waterfallCanvas.height + if (width <= 0 || height <= 0 || !this.columnImageData) return + + this.paintColumnImage(values) + + // Shift existing content left by 1 pixel + this.waterfallCtx.drawImage(this.waterfallCanvas, -1, 0) + + // Paint new column at right edge + this.waterfallCtx.putImageData(this.columnImageData, width - 1, 0) + } + + private ensureBandMapping(): void { + const { canvas, options } = this + const width = canvas.width + const height = canvas.height + const fftSize = options.fftSize + const sampleRate = Math.max(1, this.dataSource.getSampleRate()) + const nyquist = sampleRate / 2 + const minFrequency = Math.max(1, Math.min(options.minFrequency, nyquist)) + const maxFrequency = Math.max(minFrequency + 1, Math.min(options.maxFrequency, nyquist)) + + if ( + width === this.lastWidth + && height === this.lastHeight + && fftSize === this.lastFftSize + && sampleRate === this.lastSampleRate + && minFrequency === this.lastMinFrequency + && maxFrequency === this.lastMaxFrequency + && options.scaleMode === this.lastScaleMode + ) { + return + } + + this.lastWidth = width + this.lastHeight = height + this.lastFftSize = fftSize + this.lastSampleRate = sampleRate + this.lastMinFrequency = minFrequency + this.lastMaxFrequency = maxFrequency + this.lastScaleMode = options.scaleMode + + const numBins = (fftSize * FFT_PAD_FACTOR) / 2 + const rowSpan = Math.max(1, height - 1) + const binWidth = nyquist / numBins + + this.rowCenterBins = new Float32Array(height) + this.rowBandStartBins = new Float32Array(height) + this.rowBandEndBins = new Float32Array(height) + for (let row = 0; row < height; row += 1) { + const normalizedPosition = 1 - (row / rowSpan) + const centerFrequency = frequencyFromScale( + options.scaleMode, + minFrequency, + maxFrequency, + normalizedPosition + ) + const upperEdgeNormalized = row === 0 + ? 1 + : 1 - ((row - 0.5) / rowSpan) + const lowerEdgeNormalized = row === height - 1 + ? 0 + : 1 - ((row + 0.5) / rowSpan) + const upperEdgeFrequency = frequencyFromScale( + options.scaleMode, + minFrequency, + maxFrequency, + upperEdgeNormalized + ) + const lowerEdgeFrequency = frequencyFromScale( + options.scaleMode, + minFrequency, + maxFrequency, + lowerEdgeNormalized + ) + + this.rowCenterBins[row] = Math.max(0, Math.min(numBins - 1, centerFrequency / binWidth)) + this.rowBandStartBins[row] = Math.max(0, Math.min(numBins, lowerEdgeFrequency / binWidth)) + this.rowBandEndBins[row] = Math.max(0, Math.min(numBins, upperEdgeFrequency / binWidth)) + } + + this.ensureColumnBuffers(height) + } + + private processFFT(samples: Float32Array): Float32Array { + const windowSize = samples.length + const paddedSize = windowSize * FFT_PAD_FACTOR + const window = getHannWindow(windowSize) + + // Apply window to audio samples + for (let index = 0; index < windowSize; index += 1) { + this.fftRe[index] = samples[index] * window[index] + } + // Zero-pad the rest for finer frequency interpolation + for (let index = windowSize; index < paddedSize; index += 1) { + this.fftRe[index] = 0 + } + this.fftIm.fill(0) + + fft(this.fftRe, this.fftIm) + + const numBins = paddedSize / 2 + const magnitudes = new Float32Array(numBins) + const scale = 2 / windowSize // normalize by window size, not padded size + + for (let index = 0; index < numBins; index += 1) { + const re = this.fftRe[index] + const im = this.fftIm[index] + const magnitude = Math.sqrt((re * re) + (im * im)) * scale + magnitudes[index] = 20 * Math.log10(Math.max(magnitude, 1e-10)) + } + + return magnitudes + } + + private paintColumnImage(values: Float32Array): void { + if (!this.columnImageData) return + + const imageData = this.columnImageData.data + const [tintR, tintG, tintB] = this.options.colorScheme === 'mono' + ? parseHexColor(this.options.lineColor) + : [0, 0, 0] + + for (let row = 0; row < values.length; row += 1) { + const intensity = Math.max(0, Math.min(1, values[row])) + const lutIndex = Math.round(intensity * 255) + const dataIndex = row * 4 + + if (this.options.colorScheme === 'heat') { + imageData[dataIndex] = HEAT_LUT[lutIndex * 3] + imageData[dataIndex + 1] = HEAT_LUT[(lutIndex * 3) + 1] + imageData[dataIndex + 2] = HEAT_LUT[(lutIndex * 3) + 2] + } else { + imageData[dataIndex] = Math.round(tintR * intensity) + imageData[dataIndex + 1] = Math.round(tintG * intensity) + imageData[dataIndex + 2] = Math.round(tintB * intensity) + } + + imageData[dataIndex + 3] = 255 + } + } + + private drawColumn(magnitudes: Float32Array): Float32Array { + const height = this.waterfallCanvas.height + if (height <= 0) return this.columnValues + + this.ensureColumnBuffers(height) + const values = this.columnValues + const raw = this.rawColumnValues + const numBins = magnitudes.length + + const clarity = getClarityProfile(this.options.clarityMode) + const minDecibels = this.options.minDecibels + const dbRange = Math.max(1e-6, this.options.maxDecibels - minDecibels) + + // Compute bin width for frequency-based tilt + const sampleRate = Math.max(1, this.dataSource.getSampleRate()) + const binWidth = (sampleRate / 2) / numBins + const TILT_REFERENCE_HZ = 1000 + + // Pass 1: sub-bin interpolation + tilt → raw normalized values (no gamma yet) + for (let row = 0; row < height; row += 1) { + const centerBin = this.rowCenterBins[row] + + // Sub-bin interpolation in dB domain + const binLo = Math.floor(centerBin) + const binHi = Math.min(binLo + 1, numBins - 1) + const frac = centerBin - binLo + const db = magnitudes[binLo] * (1 - frac) + magnitudes[binHi] * frac + + // Frequency-based tilt — dB per octave from reference, scale-mode independent + const centerFreq = Math.max(1, centerBin * binWidth) + const tiltAmount = clarity.tiltDb * Math.log2(centerFreq / TILT_REFERENCE_HZ) + raw[row] = clamp01(((db + tiltAmount) - minDecibels) / dbRange) + } + + // Pass 2: local peak suppression — thin spectral lines for sharp/sharper modes + const sharpness = clarity.sharpness + if (sharpness > 0) { + // Hann mainlobe = 4 original bins = 4 * FFT_PAD_FACTOR padded bins + const mainlobePaddedBins = 4 * FFT_PAD_FACTOR + // Target visual line width in pixels — suppression scales to achieve this + const TARGET_LINE_WIDTH = 3 + + for (let row = 0; row < height; row += 1) { + // Adaptive window: mainlobe width in pixel rows at this frequency + const bandWidthPerRow = Math.max(0.1, this.rowBandEndBins[row] - this.rowBandStartBins[row]) + const mainlobePixels = mainlobePaddedBins / bandWidthPerRow + const halfWin = Math.max(2, Math.min(50, Math.round(mainlobePixels / 2))) + + // Scale suppression by how wide the mainlobe is vs target width + // At low freqs (mainlobe=26px, target=3px): 8.7x stronger suppression + // At high freqs (mainlobe=2px, target=3px): 1x base suppression + const scaleFactor = Math.max(1, mainlobePixels / TARGET_LINE_WIDTH) + const effectiveSharpness = sharpness * scaleFactor + + // Find local peak in neighborhood + let localMax = raw[row] + for (let d = 1; d <= halfWin; d += 1) { + if (row - d >= 0 && raw[row - d] > localMax) localMax = raw[row - d] + if (row + d < height && raw[row + d] > localMax) localMax = raw[row + d] + } + + // Suppress off-peak values: peak stays bright, slopes get crushed + if (localMax > 1e-6) { + const ratio = raw[row] / localMax + raw[row] *= Math.pow(ratio, effectiveSharpness) + } + } + } + + // Pass 3: apply gamma + for (let row = 0; row < height; row += 1) { + values[row] = Math.pow(raw[row], clarity.gamma) + } + + return values + } + + private draw = (): void => { + if (!this.isRunning) return + + const width = this.canvas.width + const height = this.canvas.height + if (width <= 0 || height <= 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + // Re-set after external resize resets context state + this.ctx.imageSmoothingEnabled = false + + if (this.waterfallCanvas.width !== width || this.waterfallCanvas.height !== height) { + const previousCanvas = document.createElement('canvas') + previousCanvas.width = this.waterfallCanvas.width + previousCanvas.height = this.waterfallCanvas.height + const previousCtx = previousCanvas.getContext('2d') + if (previousCtx) { + previousCtx.drawImage(this.waterfallCanvas, 0, 0) + } + + this.waterfallCanvas.width = width + this.waterfallCanvas.height = height + this.waterfallCtx.imageSmoothingEnabled = false + + // Anchor right edge — newest columns stay, old data crops naturally + if (previousCtx && previousCanvas.width > 0 && previousCanvas.height > 0) { + const srcX = Math.max(0, previousCanvas.width - width) + const srcW = Math.min(previousCanvas.width, width) + const dstX = Math.max(0, width - previousCanvas.width) + this.waterfallCtx.drawImage( + previousCanvas, + srcX, 0, srcW, previousCanvas.height, + dstX, 0, srcW, height + ) + } + + this.lastWidth = 0 + } + + this.ensureBandMapping() + + if (!this.dataSource.isPlaying()) { + this.dataSource.getPendingSpectrogramSamples() + // Freeze waterfall in place instead of blanking + this.ctx.clearRect(0, 0, width, height) + this.ctx.drawImage(this.waterfallCanvas, 0, 0) + this.animationId = requestAnimationFrame(this.draw) + return + } + + const pendingSamples = this.dataSource.getPendingSpectrogramSamples() + const fftSize = this.options.fftSize + + // Scroll speed solely controls temporal resolution (hop divisor) + const BASE_HOP_DIVISOR = 8 + const effectiveHopDivisor = Math.max(2, Math.min(64, Math.round(BASE_HOP_DIVISOR * this.options.scrollSpeed))) + const hopSize = Math.max(1, Math.floor(fftSize / effectiveHopDivisor)) + const overlapSamples = fftSize - hopSize + + for (const chunk of pendingSamples) { + for (let index = 0; index < chunk.length; index += 1) { + this.sampleBuffer[this.sampleBufferPos] = chunk[index] + this.sampleBufferPos += 1 + + if (this.sampleBufferPos >= fftSize) { + const magnitudes = this.processFFT(this.sampleBuffer) + const values = this.drawColumn(magnitudes) + // Each FFT hop = exactly 1 pixel column. No accumulation, no duplication. + this.shiftAndPaintColumn(values) + + this.sampleBuffer.copyWithin(0, hopSize) + this.sampleBufferPos = overlapSamples + } + } + } + + this.ctx.clearRect(0, 0, width, height) + this.ctx.drawImage(this.waterfallCanvas, 0, 0) + this.animationId = requestAnimationFrame(this.draw) + } + + dispose(): void { + this.stop() + } +} diff --git a/src/renderer/visualizers/SpectrumAnalyzer.ts b/src/renderer/visualizers/SpectrumAnalyzer.ts new file mode 100644 index 0000000..ffbdfd1 --- /dev/null +++ b/src/renderer/visualizers/SpectrumAnalyzer.ts @@ -0,0 +1,514 @@ +import { audioRouter } from '../audio/AudioRouter' +import { spectrum as nativeSpectrum, isNativeAvailable } from '../audio/native' +import { + DEFAULT_SPECTRUM_TILT_DB_PER_OCTAVE, + DEFAULT_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE, + clampSpectrumTiltDbPerOctave, + clampSpectrumHeatmapTiltDbPerOctave, +} from '../../types/spectrum' + +export interface SpectrumAnalyzerDataSource { + getPendingSpectrumSamples: () => Float32Array[] + getSampleRate: () => number + isPlaying: () => boolean +} + +export interface SpectrumAnalyzerOptions { + lineColor?: string + lineWidth?: number + fillGradient?: boolean + heatmapFill?: boolean + gradientColors?: string[] // Bottom to top + backgroundColor?: string + showGrid?: boolean + gridColor?: string + scaleType?: 'linear' | 'log' + smoothing?: number + minDecibels?: number + maxDecibels?: number + minFrequency?: number + maxFrequency?: number + tiltDbPerOctave?: number + heatmapTiltDbPerOctave?: number + tiltReferenceHz?: number + fftSize?: number + dataSource?: SpectrumAnalyzerDataSource +} + +type ResolvedSpectrumAnalyzerOptions = Required> + +// ---- Heat LUT for heatmap fill (same palette as Spectrogram) ---- +type HeatStop = { at: number; color: [number, number, number] } +const HEAT_STOPS: readonly HeatStop[] = [ + { at: 0, color: [0, 0, 0] }, + { at: 0.14, color: [15, 7, 33] }, + { at: 0.32, color: [61, 11, 94] }, + { at: 0.54, color: [163, 26, 121] }, + { at: 0.74, color: [255, 82, 87] }, + { at: 0.9, color: [255, 166, 63] }, + { at: 1, color: [255, 241, 209] }, +] + +function buildHeatLUT(): Uint8Array { + const lut = new Uint8Array(256 * 3) + for (let i = 0; i < 256; i++) { + const t = i / 255 + let s = HEAT_STOPS[0], e = HEAT_STOPS[HEAT_STOPS.length - 1] + for (let si = 0; si < HEAT_STOPS.length - 1; si++) { + if (t <= HEAT_STOPS[si + 1].at) { s = HEAT_STOPS[si]; e = HEAT_STOPS[si + 1]; break } + } + const a = Math.max(0, Math.min(1, (t - s.at) / Math.max(1e-6, e.at - s.at))) + lut[i * 3] = Math.round(s.color[0] + (e.color[0] - s.color[0]) * a) + lut[i * 3 + 1] = Math.round(s.color[1] + (e.color[1] - s.color[1]) * a) + lut[i * 3 + 2] = Math.round(s.color[2] + (e.color[2] - s.color[2]) * a) + } + return lut +} +const HEAT_LUT = buildHeatLUT() +const HEATMAP_GAMMA = 1.4 + +const defaultOptions: ResolvedSpectrumAnalyzerOptions = { + lineColor: '#00ffff', + lineWidth: 2, + fillGradient: true, + heatmapFill: false, + gradientColors: ['rgba(0, 255, 255, 0)', 'rgba(0, 255, 255, 0.3)', 'rgba(138, 43, 226, 0.5)'], + backgroundColor: 'transparent', + showGrid: true, + gridColor: 'rgba(255, 255, 255, 0.1)', + scaleType: 'log', + smoothing: 0.9, + minDecibels: -90, + maxDecibels: -10, + minFrequency: 20, + maxFrequency: 20000, + tiltDbPerOctave: DEFAULT_SPECTRUM_TILT_DB_PER_OCTAVE, + heatmapTiltDbPerOctave: DEFAULT_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE, + tiltReferenceHz: 1000, + fftSize: 2048 +} + +const defaultSpectrumDataSource: SpectrumAnalyzerDataSource = { + getPendingSpectrumSamples: () => audioRouter.flushPendingSpectrumSamples(), + getSampleRate: () => audioRouter.getSampleRate(), + isPlaying: () => audioRouter.isCapturing(), +} + +export class SpectrumAnalyzer { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private options: ResolvedSpectrumAnalyzerOptions + private dataSource: SpectrumAnalyzerDataSource + private animationId: number | null = null + private isRunning: boolean = false + private nativeInitialized: boolean = false + private sampleRate: number = 48000 + private lastSampleRate: number = 0 + + constructor(canvas: HTMLCanvasElement, options: SpectrumAnalyzerOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + const { dataSource, ...optionOverrides } = options + this.options = { + ...defaultOptions, + ...optionOverrides, + tiltDbPerOctave: clampSpectrumTiltDbPerOctave( + optionOverrides.tiltDbPerOctave ?? defaultOptions.tiltDbPerOctave + ), + heatmapTiltDbPerOctave: clampSpectrumHeatmapTiltDbPerOctave( + optionOverrides.heatmapTiltDbPerOctave ?? defaultOptions.heatmapTiltDbPerOctave + ), + } + this.dataSource = dataSource ?? defaultSpectrumDataSource + + // Initialize native module + this.initNative() + } + + private initNative(): void { + if (isNativeAvailable() && !this.nativeInitialized) { + this.sampleRate = Math.max(1, this.dataSource.getSampleRate()) + this.lastSampleRate = this.sampleRate + nativeSpectrum.setFFTSize(this.options.fftSize) + nativeSpectrum.setSampleRate(this.sampleRate) + nativeSpectrum.setSmoothing(this.getNativeSmoothing()) + this.nativeInitialized = true + console.log(`SpectrumAnalyzer: Using native DSP (${this.sampleRate}Hz)`) + } else if (!isNativeAvailable()) { + console.error('SpectrumAnalyzer: Native DSP not available!') + } + } + + private updateSampleRateIfNeeded(): void { + if (!isNativeAvailable()) return + const currentRate = Math.max(1, this.dataSource.getSampleRate()) + if (currentRate !== this.lastSampleRate && currentRate > 0) { + this.sampleRate = currentRate + this.lastSampleRate = currentRate + nativeSpectrum.setSampleRate(currentRate) + console.log(`SpectrumAnalyzer: Sample rate updated to ${currentRate}Hz`) + } + } + + private getNativeSmoothing(): number { + const base = Math.min(0.99, Math.max(0, this.options.smoothing)) + const fftRatio = Math.max(0.5, this.options.fftSize / 2048) + return Math.min(0.99, Math.max(0, Math.pow(base, fftRatio))) + } + + setOptions(options: Partial): void { + const { dataSource, ...optionUpdates } = options + const nextOptions = { ...this.options, ...optionUpdates } + if (optionUpdates.tiltDbPerOctave !== undefined) { + nextOptions.tiltDbPerOctave = clampSpectrumTiltDbPerOctave(optionUpdates.tiltDbPerOctave) + } + if (optionUpdates.heatmapTiltDbPerOctave !== undefined) { + nextOptions.heatmapTiltDbPerOctave = clampSpectrumHeatmapTiltDbPerOctave(optionUpdates.heatmapTiltDbPerOctave) + } + this.options = nextOptions + if (dataSource) { + this.dataSource = dataSource + } + + // Update native module settings + if (isNativeAvailable()) { + if (options.fftSize !== undefined) { + nativeSpectrum.setFFTSize(options.fftSize) + } + if (options.smoothing !== undefined || options.fftSize !== undefined) { + nativeSpectrum.setSmoothing(this.getNativeSmoothing()) + } + } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { + // Canvas resize is handled externally + } + + // Linear interpolation helper + private lerp(a: number, b: number, t: number): number { + return a + (b - a) * t + } + + // Get interpolated value from frequency data + private getInterpolatedValue(data: Float32Array, index: number): number { + const i0 = Math.floor(index) + const i1 = Math.min(i0 + 1, data.length - 1) + const t = index - i0 + return this.lerp(data[i0], data[i1], t) + } + + private frequencyAtPosition(t: number, minFrequency: number, maxFrequency: number): number { + if (this.options.scaleType === 'log') { + const logMin = Math.log10(minFrequency) + const logMax = Math.log10(maxFrequency) + return Math.pow(10, logMin + t * (logMax - logMin)) + } + return minFrequency + t * (maxFrequency - minFrequency) + } + + private getPeakInRange(data: Float32Array, startIndex: number, endIndex: number): number { + const clampedStart = Math.max(0, Math.min(data.length - 1, startIndex)) + const clampedEnd = Math.max(0, Math.min(data.length - 1, endIndex)) + const lo = Math.floor(Math.min(clampedStart, clampedEnd)) + const hi = Math.ceil(Math.max(clampedStart, clampedEnd)) + + if (hi <= lo) { + return this.getInterpolatedValue(data, clampedStart) + } + + let peak = -Infinity + for (let i = lo; i <= hi; i++) { + peak = Math.max(peak, data[i]) + } + + return Math.max( + peak, + this.getInterpolatedValue(data, clampedStart), + this.getInterpolatedValue(data, clampedEnd) + ) + } + + private applyTilt(db: number, frequency: number, tiltDbPerOctave = this.options.tiltDbPerOctave): number { + const safeFreq = Math.max(1, frequency) + const reference = Math.max(1, this.options.tiltReferenceHz) + const octaves = Math.log2(safeFreq / reference) + return db + tiltDbPerOctave * octaves + } + + private mergePendingSpectrumChunks(pendingSpectrum: Float32Array[]): Float32Array | null { + if (pendingSpectrum.length === 0) return null + if (pendingSpectrum.length === 1) return pendingSpectrum[0] + + let totalLength = 0 + for (const chunk of pendingSpectrum) totalLength += chunk.length + + const monoData = new Float32Array(totalLength) + let offset = 0 + for (const chunk of pendingSpectrum) { + monoData.set(chunk, offset) + offset += chunk.length + } + + return monoData + } + + private draw = (): void => { + if (!this.isRunning) return + + const { canvas, ctx, options } = this + const width = canvas.width + const height = canvas.height + const dpr = window.devicePixelRatio || 1 + if (width <= 0 || height <= 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + // Get frequency data from native FFT + if (!isNativeAvailable()) { + console.error('SpectrumAnalyzer: Native DSP required') + this.animationId = requestAnimationFrame(this.draw) + return + } + + this.updateSampleRateIfNeeded() + + if (!this.dataSource.isPlaying()) { + this.dataSource.getPendingSpectrumSamples() + nativeSpectrum.reset() + + ctx.clearRect(0, 0, width, height) + if (options.backgroundColor !== 'transparent') { + ctx.fillStyle = options.backgroundColor + ctx.fillRect(0, 0, width, height) + } + + const nyquist = this.sampleRate / 2 + const minFrequency = Math.max(1, Math.min(options.minFrequency, nyquist)) + const maxFrequency = Math.max(minFrequency + 1, Math.min(options.maxFrequency, nyquist)) + if (options.showGrid) { + this.drawGrid(minFrequency, maxFrequency) + } + + this.animationId = requestAnimationFrame(this.draw) + return + } + + const pendingSpectrum = this.dataSource.getPendingSpectrumSamples() + const monoData = this.mergePendingSpectrumChunks(pendingSpectrum) + if (!monoData) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + const nativeResult = nativeSpectrum.process(monoData) + if (!nativeResult) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + let frequencyData = nativeResult + const bufferLength = frequencyData.length + + if (bufferLength === 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + // Clear canvas + ctx.clearRect(0, 0, width, height) + + // Draw background if not transparent + if (options.backgroundColor !== 'transparent') { + ctx.fillStyle = options.backgroundColor + ctx.fillRect(0, 0, width, height) + } + + // Draw grid + const nyquist = this.sampleRate / 2 + const minFrequency = Math.max(1, Math.min(options.minFrequency, nyquist)) + const maxFrequency = Math.max(minFrequency + 1, Math.min(options.maxFrequency, nyquist)) + if (options.showGrid) { + this.drawGrid(minFrequency, maxFrequency) + } + + // Calculate frequency mapping + const binWidth = nyquist / bufferLength + + // Build one point per horizontal pixel and preserve local peaks. + const points: { x: number; y: number; heatmapIntensity: number }[] = [] + const numPoints = Math.max(2, Math.floor(width)) + + for (let i = 0; i < numPoints; i++) { + const t0 = i / (numPoints - 1) + const t1 = Math.min(1, (i + 1) / (numPoints - 1)) + const x = t0 * width + + const frequency0 = this.frequencyAtPosition(t0, minFrequency, maxFrequency) + const frequency1 = this.frequencyAtPosition(t1, minFrequency, maxFrequency) + const centerFrequency = (frequency0 + frequency1) * 0.5 + const bin0 = frequency0 / binWidth + const bin1 = frequency1 / binWidth + + const centerBin = (bin0 + bin1) * 0.5 + const binSpan = Math.abs(bin1 - bin0) + + // Low frequencies can look stepped because each pixel maps to <1 FFT bin. + // Use sub-bin interpolation there, and keep peak-hold for wider spans. + const rawDb = binSpan <= 1 + ? this.getInterpolatedValue(frequencyData, Math.min(centerBin, bufferLength - 1)) + : this.getPeakInRange(frequencyData, bin0, bin1) + const db = this.applyTilt(rawDb, centerFrequency) + const heatmapDb = this.applyTilt(rawDb, centerFrequency, options.heatmapTiltDbPerOctave) + + // Normalize to 0-1 range + const normalized = (db - options.minDecibels) / (options.maxDecibels - options.minDecibels) + const heatmapNormalized = (heatmapDb - options.minDecibels) / (options.maxDecibels - options.minDecibels) + const y = height - Math.max(0, Math.min(1, normalized)) * height + const heatmapIntensity = Math.pow(Math.max(0, Math.min(1, heatmapNormalized)), HEATMAP_GAMMA) + + points.push({ x, y, heatmapIntensity }) + } + + // Draw filled area + if (options.heatmapFill && points.length > 0) { + // Per-column heat-colored fill — each frequency colored by its intensity + for (let i = 0; i < points.length; i++) { + const x = Math.floor(points[i].x) + const y = points[i].y + const nextX = i < points.length - 1 ? Math.floor(points[i + 1].x) : width + const colWidth = Math.max(1, nextX - x) + const fillHeight = height - y + if (fillHeight <= 0) continue + + const intensity = points[i].heatmapIntensity + const li = Math.round(intensity * 255) + const r = HEAT_LUT[li * 3] + const g = HEAT_LUT[li * 3 + 1] + const b = HEAT_LUT[li * 3 + 2] + + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.85)` + ctx.fillRect(x, Math.floor(y), colWidth, Math.ceil(fillHeight)) + } + } else if (options.fillGradient && points.length > 0) { + ctx.beginPath() + ctx.moveTo(points[0].x, points[0].y) + + for (let i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y) + } + + // Complete path for fill + ctx.lineTo(width, height) + ctx.lineTo(0, height) + ctx.closePath() + + // Create gradient + const gradient = ctx.createLinearGradient(0, height, 0, 0) + const colors = options.gradientColors + for (let i = 0; i < colors.length; i++) { + gradient.addColorStop(i / (colors.length - 1), colors[i]) + } + + ctx.fillStyle = gradient + ctx.fill() + } + + // Draw the line on top + ctx.beginPath() + ctx.moveTo(points[0].x, points[0].y) + + for (let i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y) + } + + ctx.lineWidth = options.lineWidth * dpr + ctx.strokeStyle = options.lineColor + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + ctx.stroke() + + this.animationId = requestAnimationFrame(this.draw) + } + + private drawGrid(minFrequency: number, maxFrequency: number): void { + const { ctx, canvas, options } = this + const width = canvas.width + const height = canvas.height + const dpr = window.devicePixelRatio || 1 + + ctx.strokeStyle = options.gridColor + ctx.lineWidth = dpr + + // Horizontal dB lines + const dbSteps = [-80, -60, -40, -20, 0] + ctx.fillStyle = options.gridColor + ctx.font = `${10 * dpr}px monospace` + ctx.textAlign = 'left' + + for (const db of dbSteps) { + const normalized = (db - options.minDecibels) / (options.maxDecibels - options.minDecibels) + const y = height - normalized * height + + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(width, y) + ctx.stroke() + + ctx.fillText(`${db}dB`, 4 * dpr, y - 2 * dpr) + } + + // Vertical frequency lines (log scale) + const freqSteps = [50, 100, 200, 500, 1000, 2000, 5000, 10000] + ctx.textAlign = 'center' + + for (const freq of freqSteps) { + if (freq < minFrequency || freq > maxFrequency) continue + + let x: number + if (options.scaleType === 'log') { + const logMin = Math.log10(minFrequency) + const logMax = Math.log10(maxFrequency) + const logFreq = Math.log10(freq) + x = ((logFreq - logMin) / (logMax - logMin)) * width + } else { + x = ((freq - minFrequency) / (maxFrequency - minFrequency)) * width + } + + ctx.beginPath() + ctx.moveTo(x, 0) + ctx.lineTo(x, height) + ctx.stroke() + + const label = freq >= 1000 ? `${freq / 1000}k` : `${freq}` + ctx.fillText(label, x, height - 4 * dpr) + } + } + + dispose(): void { + this.stop() + + // Reset native module state + if (isNativeAvailable()) { + nativeSpectrum.reset() + } + this.lastSampleRate = 0 + } +} diff --git a/src/renderer/visualizers/VUMeter.ts b/src/renderer/visualizers/VUMeter.ts new file mode 100644 index 0000000..1ab86fd --- /dev/null +++ b/src/renderer/visualizers/VUMeter.ts @@ -0,0 +1,610 @@ +import { audioRouter } from '../audio/AudioRouter' +import { + DEFAULT_VU_METER_ORIENTATION, + type VUMeterMode, + type VUMeterOrientation, +} from '../../types/vumeter' + +export interface VUMeterDataSource { + getPendingVUMeterSamples: () => Array<{ left: Float32Array; right: Float32Array }> + getSampleRate: () => number + isPlaying: () => boolean +} + +export interface VUMeterOptions { + mode?: VUMeterMode + orientation?: VUMeterOrientation + lineColor?: string + dataSource?: VUMeterDataSource +} + +type ResolvedVUMeterOptions = Required> + +const defaultOptions: ResolvedVUMeterOptions = { + mode: 'bar', + orientation: DEFAULT_VU_METER_ORIENTATION, + lineColor: '#38bdf8', +} + +const defaultVUMeterDataSource: VUMeterDataSource = { + getPendingVUMeterSamples: () => audioRouter.flushPendingVUMeterSamples(), + getSampleRate: () => audioRouter.getSampleRate(), + isPlaying: () => audioRouter.isCapturing(), +} + +// ---- Meter constants ---- + +const METER_MIN_DB = -60 +const METER_MAX_DB = 0 +const PEAK_HOLD_FRAMES = 45 // ~0.75s at 60fps +const PEAK_DECAY_DB_PER_FRAME = 0.3 +const RMS_SMOOTHING = 0.85 // exponential smoothing factor +const CORRELATION_SMOOTHING = 0.88 + +// ---- Color utilities ---- + +function parseHexColor(hex: string): [number, number, number] { + const h = hex.replace('#', '') + return [ + parseInt(h.substring(0, 2), 16) || 56, + parseInt(h.substring(2, 4), 16) || 189, + parseInt(h.substring(4, 6), 16) || 248, + ] +} + +function colorWithAlpha(r: number, g: number, b: number, a: number): string { + return `rgba(${r}, ${g}, ${b}, ${a})` +} + +// ---- VU Meter class ---- + +export class VUMeter { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private options: ResolvedVUMeterOptions + private dataSource: VUMeterDataSource + private animationId: number | null = null + private isRunning = false + + // Meter state + private rmsL = METER_MIN_DB + private rmsR = METER_MIN_DB + private peakL = METER_MIN_DB + private peakR = METER_MIN_DB + private peakHoldL = 0 + private peakHoldR = 0 + private correlation = 0 + + constructor(canvas: HTMLCanvasElement, options: VUMeterOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + + const { dataSource, ...optionOverrides } = options + this.options = { ...defaultOptions, ...optionOverrides } + this.dataSource = dataSource ?? defaultVUMeterDataSource + } + + private resetMeters(): void { + this.rmsL = METER_MIN_DB + this.rmsR = METER_MIN_DB + this.peakL = METER_MIN_DB + this.peakR = METER_MIN_DB + this.peakHoldL = 0 + this.peakHoldR = 0 + this.correlation = 0 + } + + setOptions(options: Partial): void { + const { dataSource, ...optionUpdates } = options + this.options = { ...this.options, ...optionUpdates } + if (dataSource) { + this.dataSource = dataSource + } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { + // Canvas resize handled externally + } + + private processAudio(): void { + const chunks = this.dataSource.getPendingVUMeterSamples() + + if (!this.dataSource.isPlaying() || chunks.length === 0) { + // Decay toward silence + this.rmsL = this.rmsL * RMS_SMOOTHING + METER_MIN_DB * (1 - RMS_SMOOTHING) + this.rmsR = this.rmsR * RMS_SMOOTHING + METER_MIN_DB * (1 - RMS_SMOOTHING) + this.correlation = this.correlation * CORRELATION_SMOOTHING + this.updatePeaks() + return + } + + // Compute RMS and correlation across all chunks + let sumSqL = 0 + let sumSqR = 0 + let sumLR = 0 + let totalSamples = 0 + + for (const chunk of chunks) { + const len = Math.min(chunk.left.length, chunk.right.length) + for (let i = 0; i < len; i++) { + const l = chunk.left[i] + const r = chunk.right[i] + sumSqL += l * l + sumSqR += r * r + sumLR += l * r + } + totalSamples += len + } + + if (totalSamples === 0) return + + const rawRmsL = Math.sqrt(sumSqL / totalSamples) + const rawRmsR = Math.sqrt(sumSqR / totalSamples) + const dbL = 20 * Math.log10(Math.max(rawRmsL, 1e-10)) + const dbR = 20 * Math.log10(Math.max(rawRmsR, 1e-10)) + + // Smooth RMS values + this.rmsL = this.rmsL * RMS_SMOOTHING + dbL * (1 - RMS_SMOOTHING) + this.rmsR = this.rmsR * RMS_SMOOTHING + dbR * (1 - RMS_SMOOTHING) + + // Compute correlation coefficient: sum(L*R) / sqrt(sum(L^2) * sum(R^2)) + const denominator = Math.sqrt(sumSqL * sumSqR) + const rawCorrelation = denominator > 1e-10 ? sumLR / denominator : 0 + this.correlation = this.correlation * CORRELATION_SMOOTHING + rawCorrelation * (1 - CORRELATION_SMOOTHING) + + this.updatePeaks() + } + + private updatePeaks(): void { + // Update peak hold for L + if (this.rmsL > this.peakL) { + this.peakL = this.rmsL + this.peakHoldL = PEAK_HOLD_FRAMES + } else if (this.peakHoldL > 0) { + this.peakHoldL-- + } else { + this.peakL = Math.max(this.peakL - PEAK_DECAY_DB_PER_FRAME, METER_MIN_DB) + } + + // Update peak hold for R + if (this.rmsR > this.peakR) { + this.peakR = this.rmsR + this.peakHoldR = PEAK_HOLD_FRAMES + } else if (this.peakHoldR > 0) { + this.peakHoldR-- + } else { + this.peakR = Math.max(this.peakR - PEAK_DECAY_DB_PER_FRAME, METER_MIN_DB) + } + } + + private dbToNormalized(db: number): number { + return Math.max(0, Math.min(1, (db - METER_MIN_DB) / (METER_MAX_DB - METER_MIN_DB))) + } + + private drawBarMode(width: number, height: number): void { + if (this.options.orientation === 'vertical') { + this.drawVerticalBarMode(width, height) + return + } + + this.drawHorizontalBarMode(width, height) + } + + private drawHorizontalBarMode(width: number, height: number): void { + const ctx = this.ctx + const [cr, cg, cb] = parseHexColor(this.options.lineColor) + + const meterHeight = Math.max(1, Math.floor(height * 0.28)) + const corrHeight = Math.max(1, Math.floor(height * 0.16)) + const gap = Math.max(2, Math.floor(height * 0.04)) + const labelWidth = Math.max(24, Math.floor(width * 0.07)) + const dbLabelWidth = Math.max(52, Math.floor(width * 0.1)) + const barLeft = labelWidth + 4 + const barRight = width - dbLabelWidth - 4 + const barWidth = Math.max(1, barRight - barLeft) + + // Total content height + const totalHeight = meterHeight * 2 + corrHeight + gap * 2 + const topOffset = Math.max(0, Math.floor((height - totalHeight) / 2)) + + // ---- L meter ---- + const lY = topOffset + this.drawHorizontalMeterBar(ctx, barLeft, lY, barWidth, meterHeight, this.rmsL, this.peakL, cr, cg, cb) + this.drawMeterLabel(ctx, 0, lY, labelWidth, meterHeight, 'L') + this.drawDbLabel(ctx, barRight + 4, lY, dbLabelWidth, meterHeight, this.rmsL) + + // ---- R meter ---- + const rY = lY + meterHeight + gap + this.drawHorizontalMeterBar(ctx, barLeft, rY, barWidth, meterHeight, this.rmsR, this.peakR, cr, cg, cb) + this.drawMeterLabel(ctx, 0, rY, labelWidth, meterHeight, 'R') + this.drawDbLabel(ctx, barRight + 4, rY, dbLabelWidth, meterHeight, this.rmsR) + + // ---- Correlation meter ---- + const corrY = rY + meterHeight + gap + this.drawCorrelationBar(ctx, barLeft, corrY, barWidth, corrHeight, cr, cg, cb) + } + + private drawVerticalBarMode(width: number, height: number): void { + const ctx = this.ctx + const [cr, cg, cb] = parseHexColor(this.options.lineColor) + + const sidePadding = Math.max(4, Math.floor(width * 0.08)) + const channelGap = Math.max(4, Math.floor(width * 0.08)) + const labelHeight = Math.max(14, Math.floor(height * 0.08)) + const dbHeight = Math.max(14, Math.floor(height * 0.1)) + const corrHeight = Math.max(10, Math.floor(height * 0.11)) + const gapY = Math.max(4, Math.floor(height * 0.03)) + const maxMeterWidth = Math.max(4, Math.floor((width - channelGap) / 2)) + const availableMeterWidth = Math.max(8, width - sidePadding * 2 - channelGap) + const meterWidth = Math.min(Math.max(6, Math.floor(availableMeterWidth / 2)), maxMeterWidth) + const totalMeterWidth = meterWidth * 2 + channelGap + const meterLeft = Math.max(0, Math.floor((width - totalMeterWidth) / 2)) + const meterTop = gapY + labelHeight + const meterHeight = Math.max(1, height - labelHeight - dbHeight - corrHeight - gapY * 4) + const dbY = meterTop + meterHeight + gapY + const corrY = dbY + dbHeight + gapY + const corrX = Math.max(4, Math.floor(width * 0.06)) + const corrWidth = Math.max(1, width - corrX * 2) + + const lX = meterLeft + const rX = meterLeft + meterWidth + channelGap + + this.drawMeterLabel(ctx, lX, 0, meterWidth, labelHeight, 'L') + this.drawVerticalMeterBar(ctx, lX, meterTop, meterWidth, meterHeight, this.rmsL, this.peakL, cr, cg, cb) + this.drawCenteredDbLabel(ctx, lX, dbY, meterWidth, dbHeight, this.rmsL) + + this.drawMeterLabel(ctx, rX, 0, meterWidth, labelHeight, 'R') + this.drawVerticalMeterBar(ctx, rX, meterTop, meterWidth, meterHeight, this.rmsR, this.peakR, cr, cg, cb) + this.drawCenteredDbLabel(ctx, rX, dbY, meterWidth, dbHeight, this.rmsR) + + this.drawCorrelationBar(ctx, corrX, corrY, corrWidth, corrHeight, cr, cg, cb) + } + + private drawHorizontalMeterBar( + ctx: CanvasRenderingContext2D, + x: number, y: number, w: number, h: number, + rmsDb: number, peakDb: number, + cr: number, cg: number, cb: number + ): void { + const rmsNorm = this.dbToNormalized(rmsDb) + const peakNorm = this.dbToNormalized(peakDb) + const rmsWidth = rmsNorm * w + const hotThreshold = this.dbToNormalized(-6) * w + + // Background track + ctx.fillStyle = 'rgba(255, 255, 255, 0.04)' + ctx.fillRect(x, y, w, h) + + // RMS bar + if (rmsWidth > 0) { + const safeWidth = Math.min(rmsWidth, hotThreshold) + if (safeWidth > 0) { + ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82) + ctx.fillRect(x, y, safeWidth, h) + } + if (rmsWidth > hotThreshold) { + // Hot zone: transition to warm/red + const hotWidth = rmsWidth - hotThreshold + const hotProgress = Math.min(1, hotWidth / Math.max(1, w - hotThreshold)) + const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7) + const hotG = Math.round(cg * (1 - hotProgress * 0.6)) + const hotB = Math.round(cb * (1 - hotProgress * 0.7)) + ctx.fillStyle = colorWithAlpha(hotR, hotG, hotB, 0.82) + ctx.fillRect(x + hotThreshold, y, hotWidth, h) + } + } + + // Peak indicator line + if (peakNorm > 0.001) { + const peakX = x + peakNorm * w + const peakInHot = peakDb > -6 + ctx.fillStyle = peakInHot + ? 'rgba(255, 120, 80, 0.9)' + : colorWithAlpha(cr, cg, cb, 0.9) + ctx.fillRect(peakX - 1, y, 2, h) + } + + // Scale ticks + ctx.fillStyle = 'rgba(255, 255, 255, 0.12)' + const tickDbs = [-48, -36, -24, -18, -12, -6, -3, 0] + for (const db of tickDbs) { + const tickX = x + this.dbToNormalized(db) * w + ctx.fillRect(tickX, y + h - 3, 1, 3) + } + } + + private drawVerticalMeterBar( + ctx: CanvasRenderingContext2D, + x: number, y: number, w: number, h: number, + rmsDb: number, peakDb: number, + cr: number, cg: number, cb: number + ): void { + const rmsNorm = this.dbToNormalized(rmsDb) + const peakNorm = this.dbToNormalized(peakDb) + const rmsHeight = rmsNorm * h + const hotThreshold = this.dbToNormalized(-6) * h + + ctx.fillStyle = 'rgba(255, 255, 255, 0.04)' + ctx.fillRect(x, y, w, h) + + if (rmsHeight > 0) { + const safeHeight = Math.min(rmsHeight, hotThreshold) + if (safeHeight > 0) { + ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82) + ctx.fillRect(x, y + h - safeHeight, w, safeHeight) + } + if (rmsHeight > hotThreshold) { + const hotHeight = rmsHeight - hotThreshold + const hotProgress = Math.min(1, hotHeight / Math.max(1, h - hotThreshold)) + const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7) + const hotG = Math.round(cg * (1 - hotProgress * 0.6)) + const hotB = Math.round(cb * (1 - hotProgress * 0.7)) + ctx.fillStyle = colorWithAlpha(hotR, hotG, hotB, 0.82) + ctx.fillRect(x, y + h - rmsHeight, w, hotHeight) + } + } + + if (peakNorm > 0.001) { + const peakY = y + h - peakNorm * h + const peakInHot = peakDb > -6 + ctx.fillStyle = peakInHot + ? 'rgba(255, 120, 80, 0.9)' + : colorWithAlpha(cr, cg, cb, 0.9) + ctx.fillRect(x, peakY - 1, w, 2) + } + + ctx.fillStyle = 'rgba(255, 255, 255, 0.1)' + const tickDbs = [-48, -36, -24, -18, -12, -6, -3, 0] + for (const db of tickDbs) { + const tickY = y + h - this.dbToNormalized(db) * h + ctx.fillRect(x, tickY, w, 1) + } + } + + private drawMeterLabel( + ctx: CanvasRenderingContext2D, + x: number, y: number, w: number, h: number, + label: string + ): void { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)' + ctx.font = `${Math.min(22, Math.max(10, h * 0.65))}px "JetBrains Mono", monospace` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(label, x + w / 2, y + h / 2) + } + + private drawDbLabel( + ctx: CanvasRenderingContext2D, + x: number, y: number, _w: number, h: number, + db: number + ): void { + const displayDb = Math.max(METER_MIN_DB, Math.min(0, db)) + const text = displayDb <= METER_MIN_DB + 1 ? '-∞' : `${displayDb.toFixed(1)}` + ctx.fillStyle = 'rgba(255, 255, 255, 0.4)' + ctx.font = `${Math.min(20, Math.max(9, h * 0.55))}px "JetBrains Mono", monospace` + ctx.textAlign = 'left' + ctx.textBaseline = 'middle' + ctx.fillText(text, x, y + h / 2) + } + + private drawCenteredDbLabel( + ctx: CanvasRenderingContext2D, + x: number, y: number, w: number, h: number, + db: number + ): void { + const displayDb = Math.max(METER_MIN_DB, Math.min(0, db)) + const text = displayDb <= METER_MIN_DB + 1 ? '-∞' : `${displayDb.toFixed(1)}` + ctx.fillStyle = 'rgba(255, 255, 255, 0.4)' + ctx.font = `${Math.min(16, Math.max(8, h * 0.5))}px "JetBrains Mono", monospace` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(text, x + w / 2, y + h / 2) + } + + private drawCorrelationBar( + ctx: CanvasRenderingContext2D, + x: number, y: number, w: number, h: number, + cr: number, cg: number, cb: number + ): void { + const centerX = x + w / 2 + const corr = Math.max(-1, Math.min(1, this.correlation)) + + // Background track + ctx.fillStyle = 'rgba(255, 255, 255, 0.04)' + ctx.fillRect(x, y, w, h) + + // Center line + ctx.fillStyle = 'rgba(255, 255, 255, 0.12)' + ctx.fillRect(centerX - 0.5, y, 1, h) + + // Correlation indicator + const indicatorWidth = Math.abs(corr) * (w / 2) + if (indicatorWidth > 0.5) { + if (corr >= 0) { + // Positive correlation: draw rightward from center (good) + ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.6) + ctx.fillRect(centerX, y, indicatorWidth, h) + } else { + // Negative correlation: draw leftward from center (out of phase) + ctx.fillStyle = 'rgba(255, 120, 80, 0.6)' + ctx.fillRect(centerX - indicatorWidth, y, indicatorWidth, h) + } + } + + // Labels + const fontSize = Math.min(18, Math.max(8, h * 0.55)) + ctx.font = `${fontSize}px "JetBrains Mono", monospace` + ctx.textBaseline = 'middle' + ctx.fillStyle = 'rgba(255, 255, 255, 0.3)' + ctx.textAlign = 'left' + ctx.fillText('-1', x + 2, y + h / 2) + ctx.textAlign = 'center' + ctx.fillText('Ø', centerX, y + h / 2) + ctx.textAlign = 'right' + ctx.fillText('+1', x + w - 2, y + h / 2) + } + + private drawNeedleMode(width: number, height: number): void { + const ctx = this.ctx + const [cr, cg, cb] = parseHexColor(this.options.lineColor) + + // Layout: two meters side by side, correlation bar below + const corrHeight = Math.max(1, Math.floor(height * 0.12)) + const gap = Math.max(2, Math.floor(height * 0.03)) + const meterAreaHeight = height - corrHeight - gap + const meterWidth = Math.floor(width / 2) - 2 + const barLeft = Math.max(16, Math.floor(width * 0.06)) + 4 + const barRight = width - Math.max(36, Math.floor(width * 0.08)) - 4 + const barWidth = Math.max(1, barRight - barLeft) + + // L needle + this.drawNeedleMeter(ctx, 0, 0, meterWidth, meterAreaHeight, this.rmsL, this.peakL, 'L', cr, cg, cb) + // R needle + this.drawNeedleMeter(ctx, meterWidth + 4, 0, meterWidth, meterAreaHeight, this.rmsR, this.peakR, 'R', cr, cg, cb) + + // Correlation bar at bottom + this.drawCorrelationBar(ctx, barLeft, meterAreaHeight + gap, barWidth, corrHeight, cr, cg, cb) + } + + private drawNeedleMeter( + ctx: CanvasRenderingContext2D, + x: number, y: number, w: number, h: number, + rmsDb: number, peakDb: number, + label: string, + cr: number, cg: number, cb: number + ): void { + const centerX = x + w / 2 + const arcRadius = Math.min(w * 0.42, h * 0.65) + const arcCenterY = y + h * 0.78 + + // Arc background (sweep from -135° to -45°, top half) + const startAngle = Math.PI * 1.25 // 225° (bottom-left) + const endAngle = Math.PI * 1.75 // 315° (bottom-right) + + // Scale arc + ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.arc(centerX, arcCenterY, arcRadius, startAngle, endAngle) + ctx.stroke() + + // Scale ticks + const tickDbs = [-48, -36, -24, -18, -12, -6, -3, 0] + for (const db of tickDbs) { + const norm = this.dbToNormalized(db) + const angle = startAngle + norm * (endAngle - startAngle) + const innerR = arcRadius - 6 + const outerR = arcRadius + 2 + + ctx.strokeStyle = db >= -6 + ? 'rgba(255, 120, 80, 0.3)' + : 'rgba(255, 255, 255, 0.15)' + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(centerX + Math.cos(angle) * innerR, arcCenterY + Math.sin(angle) * innerR) + ctx.lineTo(centerX + Math.cos(angle) * outerR, arcCenterY + Math.sin(angle) * outerR) + ctx.stroke() + } + + // Needle + const rmsNorm = this.dbToNormalized(rmsDb) + const needleAngle = startAngle + rmsNorm * (endAngle - startAngle) + const needleLength = arcRadius * 0.88 + + ctx.strokeStyle = colorWithAlpha(cr, cg, cb, 0.9) + ctx.lineWidth = 1.5 + ctx.lineCap = 'round' + ctx.beginPath() + ctx.moveTo(centerX, arcCenterY) + ctx.lineTo( + centerX + Math.cos(needleAngle) * needleLength, + arcCenterY + Math.sin(needleAngle) * needleLength + ) + ctx.stroke() + + // Needle pivot dot + ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.7) + ctx.beginPath() + ctx.arc(centerX, arcCenterY, 2.5, 0, Math.PI * 2) + ctx.fill() + + // Peak indicator (small dot on the arc) + const peakNorm = this.dbToNormalized(peakDb) + if (peakNorm > 0.001) { + const peakAngle = startAngle + peakNorm * (endAngle - startAngle) + const peakInHot = peakDb > -6 + ctx.fillStyle = peakInHot + ? 'rgba(255, 120, 80, 0.8)' + : colorWithAlpha(cr, cg, cb, 0.8) + ctx.beginPath() + ctx.arc( + centerX + Math.cos(peakAngle) * arcRadius, + arcCenterY + Math.sin(peakAngle) * arcRadius, + 2.5, 0, Math.PI * 2 + ) + ctx.fill() + } + + // Channel label + const fontSize = Math.min(22, Math.max(10, h * 0.1)) + ctx.fillStyle = 'rgba(255, 255, 255, 0.45)' + ctx.font = `${fontSize}px "JetBrains Mono", monospace` + ctx.textAlign = 'center' + ctx.textBaseline = 'top' + ctx.fillText(label, centerX, y + 4) + + // dB readout + const displayDb = Math.max(METER_MIN_DB, Math.min(0, rmsDb)) + const dbText = displayDb <= METER_MIN_DB + 1 ? '-∞ dB' : `${displayDb.toFixed(1)} dB` + ctx.fillStyle = 'rgba(255, 255, 255, 0.35)' + ctx.font = `${Math.max(9, fontSize - 1)}px "JetBrains Mono", monospace` + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.fillText(dbText, centerX, y + h - 2) + } + + private draw = (): void => { + if (!this.isRunning) return + + const { canvas, ctx, options } = this + const width = canvas.width + const height = canvas.height + + if (width <= 0 || height <= 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + this.processAudio() + + ctx.clearRect(0, 0, width, height) + + if (options.mode === 'needle') { + this.drawNeedleMode(width, height) + } else { + this.drawBarMode(width, height) + } + + this.animationId = requestAnimationFrame(this.draw) + } + + dispose(): void { + this.stop() + } +} diff --git a/src/renderer/visualizers/Vectorscope.ts b/src/renderer/visualizers/Vectorscope.ts new file mode 100644 index 0000000..e21b743 --- /dev/null +++ b/src/renderer/visualizers/Vectorscope.ts @@ -0,0 +1,336 @@ +import { audioRouter } from '../audio/AudioRouter' +import { vectorscope as nativeVectorscope, isNativeAvailable } from '../audio/native' +import { transformPoint, drawVectorscopeGridForMode, getVectorscopeLayout } from './vectorscopeGrids' +import { MultibandSplitter, MultibandBuffer, BAND_COLORS } from './multibandSplitter' + +export type VectorscopeMode = 'lissajous' | 'polar-unipolar' | 'polar-bipolar' | 'linear-unipolar' | 'linear-bipolar' + +export interface VectorscopeOptions { + lineColor?: string + lineWidth?: number + backgroundColor?: string + showGrid?: boolean + gridColor?: string + persistence?: number // 0.0 (no trail) to 1.0 (infinite trail), default 0.10 + displayPoints?: number // how many points to request from native, default 4096 + mode?: VectorscopeMode + multiband?: boolean +} + +const defaultOptions: Required = { + lineColor: '#00ffff', + lineWidth: 1.5, + backgroundColor: 'transparent', + showGrid: true, + gridColor: 'rgba(255, 255, 255, 0.1)', + persistence: 0.10, + displayPoints: 4096, + mode: 'lissajous', + multiband: false, +} + +const BAND_ORDER = ['low', 'mid', 'high'] as const + +export class Vectorscope { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private offscreenCanvas: HTMLCanvasElement + private offscreenCtx: CanvasRenderingContext2D + private options: Required + private animationId: number | null = null + private isRunning: boolean = false + private nativeInitialized: boolean = false + private lastSampleRate: number = 0 + private splitter: MultibandSplitter = new MultibandSplitter() + private multibandBuffer: MultibandBuffer = new MultibandBuffer() + + constructor(canvas: HTMLCanvasElement, options: VectorscopeOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + this.options = { ...defaultOptions, ...options } + + // Create offscreen canvas for persistence/fade + this.offscreenCanvas = document.createElement('canvas') + this.offscreenCanvas.width = canvas.width + this.offscreenCanvas.height = canvas.height + const offCtx = this.offscreenCanvas.getContext('2d') + if (!offCtx) throw new Error('Could not get offscreen 2D context') + this.offscreenCtx = offCtx + + // Initialize native module if available + this.initNative() + } + + private initNative(): void { + if (isNativeAvailable() && !this.nativeInitialized) { + const sampleRate = audioRouter.getSampleRate() + this.lastSampleRate = sampleRate + nativeVectorscope.setSampleRate(sampleRate) + this.nativeInitialized = true + console.log(`Vectorscope: Using native DSP (${sampleRate}Hz)`) + } else if (!isNativeAvailable()) { + console.log('Vectorscope: Using JavaScript fallback') + } + } + + private updateSampleRateIfNeeded(): void { + const currentRate = audioRouter.getSampleRate() + if (currentRate !== this.lastSampleRate && currentRate > 0) { + this.lastSampleRate = currentRate + if (isNativeAvailable()) { + nativeVectorscope.setSampleRate(currentRate) + } + this.splitter.configure(currentRate) + } + } + + private resetDisplay(): void { + // Clear the offscreen canvas and reset native state + if (isNativeAvailable()) { + nativeVectorscope.reset() + } + this.splitter.reset() + this.multibandBuffer.reset() + this.offscreenCtx.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height) + } + + setOptions(options: Partial): void { + this.options = { ...this.options, ...options } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { + // Canvas resize is handled externally; offscreen will sync in draw() + } + + private draw = (): void => { + if (!this.isRunning) return + + const { canvas, ctx, offscreenCanvas, offscreenCtx, options } = this + const width = canvas.width + const height = canvas.height + const isPolar = options.mode === 'polar-unipolar' || options.mode === 'polar-bipolar' + const VISUAL_GAIN = isPolar ? 1.2 : 1.5 + const layout = getVectorscopeLayout(width, height, options.mode) + const centerX = layout.centerX + const centerY = layout.centerY + const scale = layout.radius * VISUAL_GAIN + + // Sync offscreen canvas size + if (offscreenCanvas.width !== width || offscreenCanvas.height !== height) { + offscreenCanvas.width = width + offscreenCanvas.height = height + } + + // Update sample rate if changed + this.updateSampleRateIfNeeded() + + // ---- PERSISTENCE FADE ---- + offscreenCtx.globalCompositeOperation = 'destination-in' + offscreenCtx.fillStyle = `rgba(255, 255, 255, ${options.persistence})` + offscreenCtx.fillRect(0, 0, width, height) + offscreenCtx.globalCompositeOperation = 'source-over' + + // ---- FLUSH SAMPLES ---- + const pendingSamples = audioRouter.flushPendingVectorscopeSamples() + + if (options.multiband) { + // Multiband path: split into 3 bands, render each with its own color + this.drawMultibandPoints(offscreenCtx, pendingSamples, centerX, centerY, scale) + } else if (isNativeAvailable()) { + // Push all accumulated stereo chunks to native circular buffer + for (const chunk of pendingSamples) { + nativeVectorscope.pushSamples(chunk.left, chunk.right) + } + + // Get filtered points from native circular buffer + const pointsResult = nativeVectorscope.getPoints(options.displayPoints) + + if (pointsResult && pointsResult.count > 0) { + this.drawPoints(offscreenCtx, pointsResult.x, pointsResult.y, pointsResult.count, centerX, centerY, scale) + } + } else { + // JavaScript fallback: draw raw samples from pending chunks + this.drawFallbackPoints(offscreenCtx, pendingSamples, centerX, centerY, scale) + } + + // ---- COMPOSITE TO VISIBLE CANVAS ---- + ctx.clearRect(0, 0, width, height) + + // Draw background + if (options.backgroundColor !== 'transparent') { + ctx.fillStyle = options.backgroundColor + ctx.fillRect(0, 0, width, height) + } + + // Draw grid underneath + if (options.showGrid) { + const dpr = window.devicePixelRatio || 1 + drawVectorscopeGridForMode(ctx, width, height, options.gridColor, options.mode, dpr) + } + + // Draw the accumulated vectorscope image on top + ctx.drawImage(offscreenCanvas, 0, 0) + + this.animationId = requestAnimationFrame(this.draw) + } + + private drawPoints( + ctx: CanvasRenderingContext2D, + x: Float32Array, + y: Float32Array, + count: number, + centerX: number, + centerY: number, + scale: number + ): void { + const { options } = this + const mode = options.mode + const dpr = window.devicePixelRatio || 1 + const dotSize = options.lineWidth * dpr + + // Draw dots with age-based opacity: oldest dimmer, newest brighter + const segments = 8 + const pointsPerSegment = Math.ceil(count / segments) + + for (let seg = 0; seg < segments; seg++) { + const startIdx = seg * pointsPerSegment + const endIdx = Math.min((seg + 1) * pointsPerSegment, count) + if (startIdx >= count) break + + // Older segments (lower seg) are dimmer + const alpha = 0.15 + 0.85 * (seg / Math.max(segments - 1, 1)) + + ctx.fillStyle = options.lineColor + ctx.globalAlpha = alpha + + for (let i = startIdx; i < endIdx; i++) { + // Native returns x=Right, y=Left + const point = transformPoint(y[i], x[i], mode) + if (!point) continue + + const px = centerX + point.dx * scale + const py = centerY - point.dy * scale + ctx.fillRect(px - dotSize / 2, py - dotSize / 2, dotSize, dotSize) + } + } + ctx.globalAlpha = 1.0 + } + + private drawFallbackPoints( + ctx: CanvasRenderingContext2D, + pendingSamples: { left: Float32Array; right: Float32Array }[], + centerX: number, + centerY: number, + scale: number + ): void { + if (pendingSamples.length === 0) return + + const { options } = this + const mode = options.mode + const dpr = window.devicePixelRatio || 1 + const dotSize = options.lineWidth * dpr + + ctx.fillStyle = options.lineColor + ctx.globalAlpha = 0.8 + + for (const chunk of pendingSamples) { + for (let i = 0; i < chunk.left.length; i++) { + const point = transformPoint(chunk.left[i], chunk.right[i], mode) + if (!point) continue + + const px = centerX + point.dx * scale + const py = centerY - point.dy * scale + ctx.fillRect(px - dotSize / 2, py - dotSize / 2, dotSize, dotSize) + } + } + ctx.globalAlpha = 1.0 + } + + private drawMultibandPoints( + ctx: CanvasRenderingContext2D, + pendingSamples: { left: Float32Array; right: Float32Array }[], + centerX: number, + centerY: number, + scale: number + ): void { + const { options } = this + const mode = options.mode + const dpr = window.devicePixelRatio || 1 + const dotSize = options.lineWidth * dpr + + // Ensure splitter is configured + const sampleRate = audioRouter.getSampleRate() + if (sampleRate > 0) { + this.splitter.configure(sampleRate) + } + + // Also push to native so switching back to single-color is seamless + if (isNativeAvailable()) { + for (const chunk of pendingSamples) { + nativeVectorscope.pushSamples(chunk.left, chunk.right) + } + } + + // Split new samples into bands and push into circular buffer + for (const chunk of pendingSamples) { + const bands = this.splitter.split(chunk.left, chunk.right) + this.multibandBuffer.push(bands) + } + + // Read all buffered points and draw with age-based opacity (same as native path) + const result = this.multibandBuffer.getPoints(options.displayPoints) + if (result.count === 0) return + + const segments = 8 + const pointsPerSegment = Math.ceil(result.count / segments) + + for (let seg = 0; seg < segments; seg++) { + const startIdx = seg * pointsPerSegment + const endIdx = Math.min((seg + 1) * pointsPerSegment, result.count) + if (startIdx >= result.count) break + + const alpha = 0.15 + 0.85 * (seg / Math.max(segments - 1, 1)) + ctx.globalAlpha = alpha + + for (const band of BAND_ORDER) { + const bandData = result.bands[band] + ctx.fillStyle = BAND_COLORS[band] + + for (let i = startIdx; i < endIdx; i++) { + const point = transformPoint(bandData.left[i], bandData.right[i], mode) + if (!point) continue + + const px = centerX + point.dx * scale + const py = centerY - point.dy * scale + ctx.fillRect(px - dotSize / 2, py - dotSize / 2, dotSize, dotSize) + } + } + } + ctx.globalAlpha = 1.0 + } + + dispose(): void { + this.stop() + + // Reset native module state + if (isNativeAvailable()) { + nativeVectorscope.reset() + } + } +} diff --git a/src/renderer/visualizers/Waveform.ts b/src/renderer/visualizers/Waveform.ts new file mode 100644 index 0000000..78b65a2 --- /dev/null +++ b/src/renderer/visualizers/Waveform.ts @@ -0,0 +1,416 @@ +import { audioRouter } from '../audio/AudioRouter' +import { + DEFAULT_WAVEFORM_GAIN_DB, + DEFAULT_WAVEFORM_SCROLL_SPEED, + clampWaveformGainDb, + clampWaveformScrollSpeed, +} from '../../types/waveform' +import { MultibandSplitter } from './multibandSplitter' + +export interface WaveformDataSource { + getPendingWaveformSamples: () => Float32Array[] + getSampleRate: () => number + isPlaying: () => boolean +} + +export interface WaveformOptions { + lineColor?: string + scrollSpeed?: number + gainDb?: number + multiband?: boolean + dataSource?: WaveformDataSource +} + +type ResolvedWaveformOptions = Required> + +const defaultOptions: ResolvedWaveformOptions = { + lineColor: '#38bdf8', + scrollSpeed: DEFAULT_WAVEFORM_SCROLL_SPEED, + gainDb: DEFAULT_WAVEFORM_GAIN_DB, + multiband: false, +} + +// Band colors for multiband mode — same hues as vectorscope RGB +const BAND_LOW: [number, number, number] = [255, 68, 68] // red — bass +const BAND_MID: [number, number, number] = [68, 221, 68] // green — mids +const BAND_HIGH: [number, number, number] = [68, 136, 255] // blue — highs +const MULTIBAND_WEIGHT_EMPHASIS = 2.6 +const MULTIBAND_DOMINANCE_SENSITIVITY = 5 +const MULTIBAND_FOCUSED_BLEND = 0.68 +const MULTIBAND_FILL_ALPHA = 0.72 +const MULTIBAND_EDGE_ALPHA = 1.0 + +const defaultWaveformDataSource: WaveformDataSource = { + getPendingWaveformSamples: () => audioRouter.flushPendingWaveformSamples(), + getSampleRate: () => audioRouter.getSampleRate(), + isPlaying: () => audioRouter.isCapturing(), +} + +// Calibrate 1.0x to the prior 8s window at roughly 512px wide, +// while keeping scroll speed independent from panel width. +const BASE_PIXELS_PER_SECOND = 64 + +function parseHexColor(hex: string): [number, number, number] { + const h = hex.replace('#', '') + return [ + parseInt(h.substring(0, 2), 16) || 56, + parseInt(h.substring(2, 4), 16) || 189, + parseInt(h.substring(4, 6), 16) || 248, + ] +} + +export class Waveform { + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private options: ResolvedWaveformOptions + private dataSource: WaveformDataSource + private animationId: number | null = null + private isRunning = false + + // Offscreen canvas for scrolling content + private waterfallCanvas: HTMLCanvasElement + private waterfallCtx: CanvasRenderingContext2D + + // Sample accumulator for current pixel column + private columnAccumulator: Float32Array = new Float32Array(0) + private columnAccumulatorPos = 0 + private samplesPerColumn = 0 + private lastSampleRate = 0 + + // Multiband analysis + private splitter = new MultibandSplitter() + private bandLowAcc: Float32Array = new Float32Array(0) + private bandMidAcc: Float32Array = new Float32Array(0) + private bandHighAcc: Float32Array = new Float32Array(0) + + constructor(canvas: HTMLCanvasElement, options: WaveformOptions = {}) { + this.canvas = canvas + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2D context') + this.ctx = ctx + this.ctx.imageSmoothingEnabled = false + + const { dataSource, ...optionOverrides } = options + this.options = { + ...defaultOptions, + ...optionOverrides, + scrollSpeed: clampWaveformScrollSpeed(optionOverrides.scrollSpeed ?? defaultOptions.scrollSpeed), + gainDb: clampWaveformGainDb(optionOverrides.gainDb ?? defaultOptions.gainDb), + multiband: optionOverrides.multiband ?? defaultOptions.multiband, + } + this.dataSource = dataSource ?? defaultWaveformDataSource + + this.waterfallCanvas = document.createElement('canvas') + this.waterfallCanvas.width = canvas.width + this.waterfallCanvas.height = canvas.height + const waterfallCtx = this.waterfallCanvas.getContext('2d') + if (!waterfallCtx) throw new Error('Could not get waterfall 2D context') + this.waterfallCtx = waterfallCtx + this.waterfallCtx.imageSmoothingEnabled = false + + this.recomputeSamplesPerColumn() + } + + private resetDisplay(): void { + this.waterfallCtx.clearRect(0, 0, this.waterfallCanvas.width, this.waterfallCanvas.height) + this.columnAccumulatorPos = 0 + this.splitter.reset() + } + + private recomputeSamplesPerColumn(): void { + const sampleRate = Math.max(1, this.dataSource.getSampleRate()) + const pixelsPerSecond = BASE_PIXELS_PER_SECOND * this.options.scrollSpeed + const next = Math.max(1, Math.round(sampleRate / pixelsPerSecond)) + if (next !== this.samplesPerColumn) { + this.samplesPerColumn = next + this.columnAccumulator = new Float32Array(next) + this.bandLowAcc = new Float32Array(next) + this.bandMidAcc = new Float32Array(next) + this.bandHighAcc = new Float32Array(next) + this.columnAccumulatorPos = 0 + } + this.lastSampleRate = sampleRate + this.splitter.configure(sampleRate) + } + + setOptions(options: Partial): void { + const { dataSource, ...optionUpdates } = options + const nextOptions: ResolvedWaveformOptions = { + ...this.options, + ...optionUpdates, + lineColor: optionUpdates.lineColor ?? this.options.lineColor, + scrollSpeed: clampWaveformScrollSpeed(optionUpdates.scrollSpeed ?? this.options.scrollSpeed), + gainDb: clampWaveformGainDb(optionUpdates.gainDb ?? this.options.gainDb), + multiband: optionUpdates.multiband ?? this.options.multiband, + } + const speedChanged = nextOptions.scrollSpeed !== this.options.scrollSpeed + const multibandChanged = nextOptions.multiband !== this.options.multiband + + this.options = nextOptions + if (dataSource) { + this.dataSource = dataSource + } + if (speedChanged) { + this.recomputeSamplesPerColumn() + this.resetDisplay() + } + if (multibandChanged) { + this.splitter.reset() + this.resetDisplay() + } + } + + start(): void { + if (this.isRunning) return + this.isRunning = true + this.draw() + } + + stop(): void { + this.isRunning = false + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId) + this.animationId = null + } + } + + resize(): void { + // Resize handled in draw loop + } + + private computeMinMax(): { min: number; max: number } { + let min = this.columnAccumulator[0] + let max = this.columnAccumulator[0] + for (let i = 1; i < this.columnAccumulatorPos; i++) { + const s = this.columnAccumulator[i] + if (s < min) min = s + if (s > max) max = s + } + return { min, max } + } + + private computeBandColor(): [number, number, number] { + const n = this.columnAccumulatorPos + if (n === 0) return BAND_MID + + // Compute RMS energy for each band + let lowSum = 0 + let midSum = 0 + let highSum = 0 + for (let i = 0; i < n; i++) { + const l = this.bandLowAcc[i] + const m = this.bandMidAcc[i] + const h = this.bandHighAcc[i] + lowSum += l * l + midSum += m * m + highSum += h * h + } + + const lowRms = Math.sqrt(lowSum / n) + const midRms = Math.sqrt(midSum / n) + const highRms = Math.sqrt(highSum / n) + const total = lowRms + midRms + highRms + + if (total < 1e-10) return BAND_MID + + const emphasizedWeights = [ + Math.pow(lowRms / total, MULTIBAND_WEIGHT_EMPHASIS), + Math.pow(midRms / total, MULTIBAND_WEIGHT_EMPHASIS), + Math.pow(highRms / total, MULTIBAND_WEIGHT_EMPHASIS), + ] as const + const emphasizedTotal = emphasizedWeights[0] + emphasizedWeights[1] + emphasizedWeights[2] + if (emphasizedTotal < 1e-10) return BAND_MID + + const normalizedBands = [ + { color: BAND_LOW, weight: emphasizedWeights[0] / emphasizedTotal }, + { color: BAND_MID, weight: emphasizedWeights[1] / emphasizedTotal }, + { color: BAND_HIGH, weight: emphasizedWeights[2] / emphasizedTotal }, + ] as const + + const blended: [number, number, number] = [ + Math.round(normalizedBands[0].color[0] * normalizedBands[0].weight + normalizedBands[1].color[0] * normalizedBands[1].weight + normalizedBands[2].color[0] * normalizedBands[2].weight), + Math.round(normalizedBands[0].color[1] * normalizedBands[0].weight + normalizedBands[1].color[1] * normalizedBands[1].weight + normalizedBands[2].color[1] * normalizedBands[2].weight), + Math.round(normalizedBands[0].color[2] * normalizedBands[0].weight + normalizedBands[1].color[2] * normalizedBands[1].weight + normalizedBands[2].color[2] * normalizedBands[2].weight), + ] + + const sortedBands = [...normalizedBands].sort((left, right) => right.weight - left.weight) + const dominance = Math.max(0, Math.min(1, (sortedBands[0].weight - sortedBands[1].weight) * MULTIBAND_DOMINANCE_SENSITIVITY)) + const dominantMix = 0.78 + (0.14 * dominance) + const secondaryMix = 1 - dominantMix + const focused: [number, number, number] = [ + Math.round(sortedBands[0].color[0] * dominantMix + sortedBands[1].color[0] * secondaryMix), + Math.round(sortedBands[0].color[1] * dominantMix + sortedBands[1].color[1] * secondaryMix), + Math.round(sortedBands[0].color[2] * dominantMix + sortedBands[1].color[2] * secondaryMix), + ] + + const focusBlend = MULTIBAND_FOCUSED_BLEND + ((1 - MULTIBAND_FOCUSED_BLEND) * dominance) + return [ + Math.round(blended[0] * (1 - focusBlend) + focused[0] * focusBlend), + Math.round(blended[1] * (1 - focusBlend) + focused[1] * focusBlend), + Math.round(blended[2] * (1 - focusBlend) + focused[2] * focusBlend), + ] + } + + private shiftAndPaintColumn(min: number, max: number, width: number, height: number): void { + // Shift existing content left by 1 pixel — use 'copy' to avoid + // alpha accumulation from source-over compositing on semi-transparent pixels + this.waterfallCtx.globalCompositeOperation = 'copy' + this.waterfallCtx.drawImage(this.waterfallCanvas, -1, 0) + this.waterfallCtx.globalCompositeOperation = 'source-over' + + const centerY = height / 2 + const amplitudeGain = Math.pow(10, this.options.gainDb / 20) + const scaledMin = Math.max(-1, Math.min(1, min * amplitudeGain)) + const scaledMax = Math.max(-1, Math.min(1, max * amplitudeGain)) + const displayMargin = 0.95 // slight margin so full-scale doesn't clip at edge + const yTop = Math.round(centerY - scaledMax * centerY * displayMargin) + const yBottom = Math.round(centerY - scaledMin * centerY * displayMargin) + const lineHeight = Math.max(1, yBottom - yTop) + + let r: number, g: number, b: number + if (this.options.multiband) { + ;[r, g, b] = this.computeBandColor() + } else { + ;[r, g, b] = parseHexColor(this.options.lineColor) + } + + const fillAlpha = this.options.multiband ? MULTIBAND_FILL_ALPHA : 0.55 + const edgeAlpha = this.options.multiband ? MULTIBAND_EDGE_ALPHA : 0.9 + + // Draw the amplitude column — brighter at the edges, dimmer in the middle + this.waterfallCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${fillAlpha})` + this.waterfallCtx.fillRect(width - 1, yTop, 1, lineHeight) + + // Bright edge pixels at min/max + this.waterfallCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${edgeAlpha})` + this.waterfallCtx.fillRect(width - 1, yTop, 1, 1) + if (lineHeight > 1) { + this.waterfallCtx.fillRect(width - 1, yBottom - 1, 1, 1) + } + } + + private drawGrid(width: number, height: number): void { + const ctx = this.ctx + const centerY = height / 2 + + // Center line (zero crossing) + ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)' + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(0, centerY) + ctx.lineTo(width, centerY) + ctx.stroke() + + // ±0.5 guide lines + ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)' + const quarterY = centerY * 0.5 + ctx.beginPath() + ctx.moveTo(0, quarterY) + ctx.lineTo(width, quarterY) + ctx.moveTo(0, height - quarterY) + ctx.lineTo(width, height - quarterY) + ctx.stroke() + } + + private draw = (): void => { + if (!this.isRunning) return + + const width = this.canvas.width + const height = this.canvas.height + + if (width <= 0 || height <= 0) { + this.animationId = requestAnimationFrame(this.draw) + return + } + + this.ctx.imageSmoothingEnabled = false + + // Handle resize: preserve existing content anchored to right edge + if (this.waterfallCanvas.width !== width || this.waterfallCanvas.height !== height) { + const previousCanvas = document.createElement('canvas') + previousCanvas.width = this.waterfallCanvas.width + previousCanvas.height = this.waterfallCanvas.height + const previousCtx = previousCanvas.getContext('2d') + if (previousCtx) { + previousCtx.drawImage(this.waterfallCanvas, 0, 0) + } + + this.waterfallCanvas.width = width + this.waterfallCanvas.height = height + this.waterfallCtx.imageSmoothingEnabled = false + + if (previousCtx && previousCanvas.width > 0 && previousCanvas.height > 0) { + const srcX = Math.max(0, previousCanvas.width - width) + const srcW = Math.min(previousCanvas.width, width) + const dstX = Math.max(0, width - previousCanvas.width) + this.waterfallCtx.drawImage( + previousCanvas, + srcX, 0, srcW, previousCanvas.height, + dstX, 0, srcW, height + ) + } + + this.recomputeSamplesPerColumn() + } + + // Handle sample rate changes + const sampleRate = this.dataSource.getSampleRate() + if (Math.abs(sampleRate - this.lastSampleRate) > 100) { + this.recomputeSamplesPerColumn() + } + + if (!this.dataSource.isPlaying()) { + this.dataSource.getPendingWaveformSamples() // drain + // Freeze display — show last waveform + this.ctx.clearRect(0, 0, width, height) + this.drawGrid(width, height) + this.ctx.drawImage(this.waterfallCanvas, 0, 0) + this.animationId = requestAnimationFrame(this.draw) + return + } + + const pending = this.dataSource.getPendingWaveformSamples() + const samplesPerCol = this.samplesPerColumn + const multiband = this.options.multiband + + if (samplesPerCol > 0) { + for (const chunk of pending) { + // When multiband is enabled, split each chunk through the crossover filters + let lowBand: Float32Array | null = null + let midBand: Float32Array | null = null + let highBand: Float32Array | null = null + if (multiband) { + const bands = this.splitter.split(chunk, chunk) + lowBand = bands.low.left + midBand = bands.mid.left + highBand = bands.high.left + } + + for (let i = 0; i < chunk.length; i++) { + this.columnAccumulator[this.columnAccumulatorPos] = chunk[i] + if (multiband && lowBand && midBand && highBand) { + this.bandLowAcc[this.columnAccumulatorPos] = lowBand[i] + this.bandMidAcc[this.columnAccumulatorPos] = midBand[i] + this.bandHighAcc[this.columnAccumulatorPos] = highBand[i] + } + this.columnAccumulatorPos++ + + if (this.columnAccumulatorPos >= samplesPerCol) { + const { min, max } = this.computeMinMax() + this.shiftAndPaintColumn(min, max, width, height) + this.columnAccumulatorPos = 0 + } + } + } + } + + this.ctx.clearRect(0, 0, width, height) + this.drawGrid(width, height) + this.ctx.drawImage(this.waterfallCanvas, 0, 0) + this.animationId = requestAnimationFrame(this.draw) + } + + dispose(): void { + this.stop() + } +} diff --git a/src/renderer/visualizers/multibandSplitter.ts b/src/renderer/visualizers/multibandSplitter.ts new file mode 100644 index 0000000..c0059ae --- /dev/null +++ b/src/renderer/visualizers/multibandSplitter.ts @@ -0,0 +1,279 @@ +/** + * 3-band crossover filter for multiband vectorscope coloring. + * + * Splits stereo audio into Low / Mid / High frequency bands using + * 2nd-order Butterworth biquad filters (Linkwitz-Riley style crossover). + * + * Crossover frequencies: + * Low ↔ Mid : 250 Hz + * Mid ↔ High: 4000 Hz + * + * Each band carries independent left/right channels so the vectorscope + * can render each band's stereo image in a distinct color. + */ + +const LOW_MID_CROSSOVER = 250 +const MID_HIGH_CROSSOVER = 4000 + +export const BAND_COLORS = { + low: '#ff4444', // Red — bass + mid: '#44dd44', // Green — mids + high: '#4488ff', // Blue — highs +} as const + +export interface MultibandChunk { + low: { left: Float32Array; right: Float32Array } + mid: { left: Float32Array; right: Float32Array } + high: { left: Float32Array; right: Float32Array } +} + +// ---------- Biquad filter ---------- + +class BiquadFilter { + private b0 = 1 + private b1 = 0 + private b2 = 0 + private a1 = 0 + private a2 = 0 + + private x1 = 0 + private x2 = 0 + private y1 = 0 + private y2 = 0 + + setLowpass(freq: number, sampleRate: number): void { + const w0 = (2 * Math.PI * freq) / sampleRate + const cosw0 = Math.cos(w0) + const sinw0 = Math.sin(w0) + const alpha = sinw0 / (2 * Math.SQRT2) // Q = 1/sqrt(2) for Butterworth + + const a0 = 1 + alpha + this.b0 = ((1 - cosw0) / 2) / a0 + this.b1 = (1 - cosw0) / a0 + this.b2 = ((1 - cosw0) / 2) / a0 + this.a1 = (-2 * cosw0) / a0 + this.a2 = (1 - alpha) / a0 + } + + setHighpass(freq: number, sampleRate: number): void { + const w0 = (2 * Math.PI * freq) / sampleRate + const cosw0 = Math.cos(w0) + const sinw0 = Math.sin(w0) + const alpha = sinw0 / (2 * Math.SQRT2) + + const a0 = 1 + alpha + this.b0 = ((1 + cosw0) / 2) / a0 + this.b1 = (-(1 + cosw0)) / a0 + this.b2 = ((1 + cosw0) / 2) / a0 + this.a1 = (-2 * cosw0) / a0 + this.a2 = (1 - alpha) / a0 + } + + process(input: Float32Array, output: Float32Array): void { + for (let i = 0; i < input.length; i++) { + const x0 = input[i] + const y0 = this.b0 * x0 + this.b1 * this.x1 + this.b2 * this.x2 + - this.a1 * this.y1 - this.a2 * this.y2 + this.x2 = this.x1 + this.x1 = x0 + this.y2 = this.y1 + this.y1 = y0 + output[i] = y0 + } + } + + reset(): void { + this.x1 = 0 + this.x2 = 0 + this.y1 = 0 + this.y2 = 0 + } +} + +// ---------- MultibandSplitter ---------- + +/** + * Stateful 3-band stereo crossover filter. + * + * Call `configure(sampleRate)` whenever the sample rate changes, + * then `split(left, right)` for each stereo chunk. + */ +export class MultibandSplitter { + // Low band: lowpass at LOW_MID_CROSSOVER (L + R) + private lowLpL = new BiquadFilter() + private lowLpR = new BiquadFilter() + + // Mid band: highpass at LOW_MID_CROSSOVER → lowpass at MID_HIGH_CROSSOVER (L + R) + private midHpL = new BiquadFilter() + private midHpR = new BiquadFilter() + private midLpL = new BiquadFilter() + private midLpR = new BiquadFilter() + + // High band: highpass at MID_HIGH_CROSSOVER (L + R) + private highHpL = new BiquadFilter() + private highHpR = new BiquadFilter() + + private configuredSampleRate = 0 + + configure(sampleRate: number): void { + if (sampleRate === this.configuredSampleRate) return + this.configuredSampleRate = sampleRate + + this.lowLpL.setLowpass(LOW_MID_CROSSOVER, sampleRate) + this.lowLpR.setLowpass(LOW_MID_CROSSOVER, sampleRate) + + this.midHpL.setHighpass(LOW_MID_CROSSOVER, sampleRate) + this.midHpR.setHighpass(LOW_MID_CROSSOVER, sampleRate) + this.midLpL.setLowpass(MID_HIGH_CROSSOVER, sampleRate) + this.midLpR.setLowpass(MID_HIGH_CROSSOVER, sampleRate) + + this.highHpL.setHighpass(MID_HIGH_CROSSOVER, sampleRate) + this.highHpR.setHighpass(MID_HIGH_CROSSOVER, sampleRate) + + this.reset() + } + + split(left: Float32Array, right: Float32Array): MultibandChunk { + const n = left.length + + const lowL = new Float32Array(n) + const lowR = new Float32Array(n) + const midL = new Float32Array(n) + const midR = new Float32Array(n) + const highL = new Float32Array(n) + const highR = new Float32Array(n) + + // Temp buffers for mid band (highpass then lowpass) + const midTmpL = new Float32Array(n) + const midTmpR = new Float32Array(n) + + // Low band + this.lowLpL.process(left, lowL) + this.lowLpR.process(right, lowR) + + // Mid band (highpass → lowpass) + this.midHpL.process(left, midTmpL) + this.midHpR.process(right, midTmpR) + this.midLpL.process(midTmpL, midL) + this.midLpR.process(midTmpR, midR) + + // High band + this.highHpL.process(left, highL) + this.highHpR.process(right, highR) + + return { + low: { left: lowL, right: lowR }, + mid: { left: midL, right: midR }, + high: { left: highL, right: highR }, + } + } + + reset(): void { + this.lowLpL.reset() + this.lowLpR.reset() + this.midHpL.reset() + this.midHpR.reset() + this.midLpL.reset() + this.midLpR.reset() + this.highHpL.reset() + this.highHpR.reset() + } +} + +// ---------- MultibandBuffer ---------- + +const MULTIBAND_BUFFER_SIZE = 4096 + +interface BandRingBuffer { + left: Float32Array + right: Float32Array +} + +/** + * Circular buffer that accumulates band-split stereo samples. + * + * Mirrors the native vectorscope's circular buffer so that each frame + * can re-draw ALL buffered points (not just newly-arrived ones), + * producing the same dense persistence effect as the single-color path. + */ +export class MultibandBuffer { + private buffers: Record<'low' | 'mid' | 'high', BandRingBuffer> + private writePos = 0 + private validSamples = 0 + private readonly capacity: number + + constructor(capacity = MULTIBAND_BUFFER_SIZE) { + this.capacity = capacity + this.buffers = { + low: { left: new Float32Array(capacity), right: new Float32Array(capacity) }, + mid: { left: new Float32Array(capacity), right: new Float32Array(capacity) }, + high: { left: new Float32Array(capacity), right: new Float32Array(capacity) }, + } + } + + push(bands: MultibandChunk): void { + const n = bands.low.left.length + for (let i = 0; i < n; i++) { + const pos = this.writePos + this.buffers.low.left[pos] = bands.low.left[i] + this.buffers.low.right[pos] = bands.low.right[i] + this.buffers.mid.left[pos] = bands.mid.left[i] + this.buffers.mid.right[pos] = bands.mid.right[i] + this.buffers.high.left[pos] = bands.high.left[i] + this.buffers.high.right[pos] = bands.high.right[i] + + this.writePos = (pos + 1) % this.capacity + if (this.validSamples < this.capacity) { + this.validSamples++ + } + } + } + + /** + * Returns the most recent `maxPoints` samples for each band, + * ordered oldest-first (matching the native getPoints() convention). + */ + getPoints(maxPoints: number): { + bands: Record<'low' | 'mid' | 'high', { left: Float32Array; right: Float32Array }> + count: number + } { + const count = Math.min(maxPoints, this.validSamples) + if (count === 0) { + return { + bands: { + low: { left: new Float32Array(0), right: new Float32Array(0) }, + mid: { left: new Float32Array(0), right: new Float32Array(0) }, + high: { left: new Float32Array(0), right: new Float32Array(0) }, + }, + count: 0, + } + } + + const out = { + low: { left: new Float32Array(count), right: new Float32Array(count) }, + mid: { left: new Float32Array(count), right: new Float32Array(count) }, + high: { left: new Float32Array(count), right: new Float32Array(count) }, + } + + for (let i = 0; i < count; i++) { + const idx = (this.writePos + this.capacity - count + i) % this.capacity + out.low.left[i] = this.buffers.low.left[idx] + out.low.right[i] = this.buffers.low.right[idx] + out.mid.left[i] = this.buffers.mid.left[idx] + out.mid.right[i] = this.buffers.mid.right[idx] + out.high.left[i] = this.buffers.high.left[idx] + out.high.right[i] = this.buffers.high.right[idx] + } + + return { bands: out, count } + } + + reset(): void { + for (const band of ['low', 'mid', 'high'] as const) { + this.buffers[band].left.fill(0) + this.buffers[band].right.fill(0) + } + this.writePos = 0 + this.validSamples = 0 + } +} diff --git a/src/renderer/visualizers/vectorscopeGrids.ts b/src/renderer/visualizers/vectorscopeGrids.ts new file mode 100644 index 0000000..dd44404 --- /dev/null +++ b/src/renderer/visualizers/vectorscopeGrids.ts @@ -0,0 +1,321 @@ +import type { VectorscopeMode } from './Vectorscope' + +const INV_SQRT2 = 1 / Math.sqrt(2) +const COS45 = Math.SQRT2 / 2 // 0.7071... + +export interface VectorscopeLayout { + centerX: number + centerY: number + radius: number +} + +/** + * Compute the center point and radius for a vectorscope mode. + * + * Unipolar modes place the center at the bottom of the canvas so the + * semicircle/triangle fills the full vertical space. + * Bipolar and Lissajous center in the canvas. + */ +export function getVectorscopeLayout( + width: number, + height: number, + mode: VectorscopeMode +): VectorscopeLayout { + const centerX = width / 2 + const isUnipolar = mode === 'polar-unipolar' || mode === 'linear-unipolar' + + if (isUnipolar) { + // Center near the bottom; radius fills upward + const margin = height * 0.04 + const centerY = height - margin + const radius = Math.min(width / 2, height - margin) * 0.88 + return { centerX, centerY, radius } + } + + // Bipolar / Lissajous: centered + const radius = Math.min(width, height) / 2 * 0.9 + return { centerX, centerY: height / 2, radius } +} + +/** + * Transform raw L/R sample values into display coordinates based on mode. + * Returns null for points filtered out by unipolar modes (mid < 0). + * + * dx/dy are in normalized space: positive dx = right, positive dy = up. + * Caller maps to canvas: canvasX = centerX + dx * scale, canvasY = centerY - dy * scale. + * + * Polar modes apply sqrt amplitude scaling so points follow the circular + * contours instead of forming diamond/linear patterns. + */ +export function transformPoint( + L: number, + R: number, + mode: VectorscopeMode +): { dx: number; dy: number } | null { + if (mode === 'lissajous') { + return { dx: R, dy: L } + } + + // M/S transform (45° rotation), normalized to preserve amplitude range + const mid = (L + R) * INV_SQRT2 + const side = (R - L) * INV_SQRT2 + + // Unipolar: filter out negative mid (anti-phase / lower half) + const isUnipolar = mode === 'polar-unipolar' || mode === 'linear-unipolar' + if (isUnipolar && mid < 0) { + return null + } + + const isPolar = mode === 'polar-unipolar' || mode === 'polar-bipolar' + if (isPolar) { + // Amplitude-compressed radial scaling: pushes points toward circular contours. + // Power < 1 compresses dynamic range — lower = more circular. + // 0.5 = sqrt (mild), 0.33 = cube root (moderate), 0.25 = fourth root (strong) + const ampSq = mid * mid + side * side + if (ampSq < 1e-12) { + return { dx: 0, dy: 0 } + } + const amp = Math.sqrt(ampSq) + const scaledAmp = Math.pow(amp, 0.35) + const factor = scaledAmp / amp + return { dx: side * factor, dy: mid * factor } + } + + // Linear modes: direct M/S Cartesian mapping + return { dx: side, dy: mid } +} + +/** + * Draw the Lissajous grid: crosshairs + box boundary. + */ +export function drawLissajousGrid( + ctx: CanvasRenderingContext2D, + layout: VectorscopeLayout, + gridColor: string, + dpr: number +): void { + const { centerX, centerY, radius } = layout + + ctx.strokeStyle = gridColor + ctx.lineWidth = dpr + + // Outer box + ctx.strokeRect( + centerX - radius, + centerY - radius, + radius * 2, + radius * 2 + ) + + // Vertical crosshair + ctx.beginPath() + ctx.moveTo(centerX, centerY - radius) + ctx.lineTo(centerX, centerY + radius) + ctx.stroke() + + // Horizontal crosshair + ctx.beginPath() + ctx.moveTo(centerX - radius, centerY) + ctx.lineTo(centerX + radius, centerY) + ctx.stroke() + + // Diagonal guides (dimmer) + const dimColor = gridColor.replace(/[\d.]+\)$/, (m) => `${parseFloat(m) * 0.5})`) + ctx.strokeStyle = dimColor + + ctx.beginPath() + ctx.moveTo(centerX - radius, centerY - radius) + ctx.lineTo(centerX + radius, centerY + radius) + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(centerX + radius, centerY - radius) + ctx.lineTo(centerX - radius, centerY + radius) + ctx.stroke() + + // Labels + ctx.fillStyle = gridColor + ctx.font = `${10 * dpr}px monospace` + ctx.textAlign = 'center' + ctx.fillText('L', centerX, centerY - radius - 6 * dpr) + ctx.fillText('R', centerX + radius + 12 * dpr, centerY + 4 * dpr) +} + +/** + * Draw the Polar (Scaled) grid: concentric circles + crosshairs. + */ +export function drawPolarGrid( + ctx: CanvasRenderingContext2D, + layout: VectorscopeLayout, + gridColor: string, + unipolar: boolean, + dpr: number +): void { + const { centerX, centerY, radius } = layout + + ctx.strokeStyle = gridColor + ctx.lineWidth = dpr + + // Concentric circles (or semicircles for unipolar) + const rings = [0.25, 0.5, 0.75, 1.0] + for (const scale of rings) { + ctx.beginPath() + if (unipolar) { + ctx.arc(centerX, centerY, radius * scale, Math.PI, 0, false) + } else { + ctx.arc(centerX, centerY, radius * scale, 0, Math.PI * 2) + } + ctx.stroke() + } + + // Vertical crosshair (mono axis) + ctx.beginPath() + ctx.moveTo(centerX, centerY - radius) + if (unipolar) { + ctx.lineTo(centerX, centerY) + } else { + ctx.lineTo(centerX, centerY + radius) + } + ctx.stroke() + + // Horizontal crosshair (side axis) + ctx.beginPath() + ctx.moveTo(centerX - radius, centerY) + ctx.lineTo(centerX + radius, centerY) + ctx.stroke() + + // Diagonal guides (L and R channel axes) — dimmer + const dimColor = gridColor.replace(/[\d.]+\)$/, (m) => `${parseFloat(m) * 0.5})`) + ctx.strokeStyle = dimColor + + if (unipolar) { + ctx.beginPath() + ctx.moveTo(centerX, centerY) + ctx.lineTo(centerX - radius * COS45, centerY - radius * COS45) + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(centerX, centerY) + ctx.lineTo(centerX + radius * COS45, centerY - radius * COS45) + ctx.stroke() + } else { + ctx.beginPath() + ctx.moveTo(centerX - radius * COS45, centerY - radius * COS45) + ctx.lineTo(centerX + radius * COS45, centerY + radius * COS45) + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(centerX + radius * COS45, centerY - radius * COS45) + ctx.lineTo(centerX - radius * COS45, centerY + radius * COS45) + ctx.stroke() + } + + // Labels + ctx.fillStyle = gridColor + ctx.font = `${10 * dpr}px monospace` + ctx.textAlign = 'center' + + ctx.fillText('+', centerX, centerY - radius - 6 * dpr) + ctx.fillText('L', centerX - radius * COS45 - 10 * dpr, centerY - radius * COS45 - 4 * dpr) + ctx.fillText('R', centerX + radius * COS45 + 10 * dpr, centerY - radius * COS45 - 4 * dpr) + + if (!unipolar) { + ctx.fillText('-', centerX, centerY + radius + 14 * dpr) + } +} + +/** + * Draw the Linear grid: diamond/triangle guides. + */ +export function drawLinearGrid( + ctx: CanvasRenderingContext2D, + layout: VectorscopeLayout, + gridColor: string, + unipolar: boolean, + dpr: number +): void { + const { centerX, centerY, radius } = layout + + ctx.strokeStyle = gridColor + ctx.lineWidth = dpr + + const scales = [0.25, 0.5, 0.75, 1.0] + for (const scale of scales) { + const r = radius * scale + ctx.beginPath() + if (unipolar) { + ctx.moveTo(centerX, centerY - r) // top (mono) + ctx.lineTo(centerX - r, centerY) // left (L) + ctx.lineTo(centerX + r, centerY) // right (R) + ctx.closePath() + } else { + ctx.moveTo(centerX, centerY - r) // top (mono) + ctx.lineTo(centerX + r, centerY) // right (R) + ctx.lineTo(centerX, centerY + r) // bottom (anti-phase) + ctx.lineTo(centerX - r, centerY) // left (L) + ctx.closePath() + } + ctx.stroke() + } + + // Vertical crosshair + ctx.beginPath() + ctx.moveTo(centerX, centerY - radius) + if (unipolar) { + ctx.lineTo(centerX, centerY) + } else { + ctx.lineTo(centerX, centerY + radius) + } + ctx.stroke() + + // Horizontal crosshair + ctx.beginPath() + ctx.moveTo(centerX - radius, centerY) + ctx.lineTo(centerX + radius, centerY) + ctx.stroke() + + // Labels + ctx.fillStyle = gridColor + ctx.font = `${10 * dpr}px monospace` + ctx.textAlign = 'center' + + ctx.fillText('+', centerX, centerY - radius - 6 * dpr) + ctx.fillText('L', centerX - radius - 12 * dpr, centerY + 4 * dpr) + ctx.fillText('R', centerX + radius + 12 * dpr, centerY + 4 * dpr) + + if (!unipolar) { + ctx.fillText('-', centerX, centerY + radius + 14 * dpr) + } +} + +/** + * Draw the appropriate grid for a given vectorscope mode. + */ +export function drawVectorscopeGridForMode( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + gridColor: string, + mode: VectorscopeMode, + dpr: number = 1 +): void { + const layout = getVectorscopeLayout(width, height, mode) + + switch (mode) { + case 'lissajous': + drawLissajousGrid(ctx, layout, gridColor, dpr) + break + case 'polar-unipolar': + drawPolarGrid(ctx, layout, gridColor, true, dpr) + break + case 'polar-bipolar': + drawPolarGrid(ctx, layout, gridColor, false, dpr) + break + case 'linear-unipolar': + drawLinearGrid(ctx, layout, gridColor, true, dpr) + break + case 'linear-bipolar': + drawLinearGrid(ctx, layout, gridColor, false, dpr) + break + } +} diff --git a/src/types/lufsmeter.ts b/src/types/lufsmeter.ts new file mode 100644 index 0000000..3bc31a3 --- /dev/null +++ b/src/types/lufsmeter.ts @@ -0,0 +1,9 @@ +export type LUFSMeterMode = 'bar' + +export const LUFS_METER_MODES: readonly LUFSMeterMode[] = ['bar'] + +export const DEFAULT_LUFS_METER_MODE: LUFSMeterMode = 'bar' + +export function isLUFSMeterMode(value: unknown): value is LUFSMeterMode { + return typeof value === 'string' && LUFS_METER_MODES.includes(value as LUFSMeterMode) +} diff --git a/src/types/scope.ts b/src/types/scope.ts new file mode 100644 index 0000000..f563f3d --- /dev/null +++ b/src/types/scope.ts @@ -0,0 +1,11 @@ +export type ScopeKind = 'spectrum' | 'oscilloscope' | 'vectorscope' | 'spectrogram' | 'vumeter' | 'lufsmeter' | 'waveform' + +export const SCOPE_KINDS: ScopeKind[] = [ + 'spectrum', + 'oscilloscope', + 'vectorscope', + 'spectrogram', + 'vumeter', + 'lufsmeter', + 'waveform', +] diff --git a/src/types/spectrogram.ts b/src/types/spectrogram.ts new file mode 100644 index 0000000..f5c8873 --- /dev/null +++ b/src/types/spectrogram.ts @@ -0,0 +1,38 @@ +export type SpectrogramClarityMode = 'classic' | 'sharp' | 'sharper' +export type SpectrogramScaleMode = 'mel' | 'log' | 'linear' + +export const SPECTROGRAM_CLARITY_MODES: readonly SpectrogramClarityMode[] = [ + 'classic', + 'sharp', + 'sharper', +] +export const SPECTROGRAM_SCALE_MODES: readonly SpectrogramScaleMode[] = [ + 'mel', + 'log', + 'linear', +] + +export const DEFAULT_SPECTROGRAM_CLARITY_MODE: SpectrogramClarityMode = 'sharper' +export const DEFAULT_SPECTROGRAM_SCALE_MODE: SpectrogramScaleMode = 'log' +export const MIN_SPECTROGRAM_SCROLL_SPEED = 0.5 +export const MAX_SPECTROGRAM_SCROLL_SPEED = 4 +export const SPECTROGRAM_SCROLL_SPEED_STEP = 0.5 +export const DEFAULT_SPECTROGRAM_SCROLL_SPEED = 2 + +export function isSpectrogramClarityMode(value: unknown): value is SpectrogramClarityMode { + return typeof value === 'string' && SPECTROGRAM_CLARITY_MODES.includes(value as SpectrogramClarityMode) +} + +export function isSpectrogramScaleMode(value: unknown): value is SpectrogramScaleMode { + return typeof value === 'string' && SPECTROGRAM_SCALE_MODES.includes(value as SpectrogramScaleMode) +} + +export function clampSpectrogramScrollSpeed(value: unknown): number { + const numeric = Number(value) + if (!Number.isFinite(numeric)) { + return DEFAULT_SPECTROGRAM_SCROLL_SPEED + } + + const snapped = Math.round(numeric / SPECTROGRAM_SCROLL_SPEED_STEP) * SPECTROGRAM_SCROLL_SPEED_STEP + return Math.min(MAX_SPECTROGRAM_SCROLL_SPEED, Math.max(MIN_SPECTROGRAM_SCROLL_SPEED, snapped)) +} diff --git a/src/types/spectrum.ts b/src/types/spectrum.ts new file mode 100644 index 0000000..0745c71 --- /dev/null +++ b/src/types/spectrum.ts @@ -0,0 +1,37 @@ +export const DEFAULT_SPECTRUM_TILT_DB_PER_OCTAVE = 2.0 +export const MIN_SPECTRUM_TILT_DB_PER_OCTAVE = -2.0 +export const MAX_SPECTRUM_TILT_DB_PER_OCTAVE = 8.0 +export const SPECTRUM_TILT_STEP = 0.1 + +export const DEFAULT_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE = 2.0 +export const MIN_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE = -2.0 +export const MAX_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE = 8.0 +export const SPECTRUM_HEATMAP_TILT_STEP = 0.1 + +export function clampSpectrumTiltDbPerOctave(value: unknown): number { + const numeric = Number(value) + if (!Number.isFinite(numeric)) { + return DEFAULT_SPECTRUM_TILT_DB_PER_OCTAVE + } + + const snapped = Math.round(numeric / SPECTRUM_TILT_STEP) * SPECTRUM_TILT_STEP + const rounded = Math.round(snapped * 10) / 10 + return Math.min( + MAX_SPECTRUM_TILT_DB_PER_OCTAVE, + Math.max(MIN_SPECTRUM_TILT_DB_PER_OCTAVE, rounded) + ) +} + +export function clampSpectrumHeatmapTiltDbPerOctave(value: unknown): number { + const numeric = Number(value) + if (!Number.isFinite(numeric)) { + return DEFAULT_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE + } + + const snapped = Math.round(numeric / SPECTRUM_HEATMAP_TILT_STEP) * SPECTRUM_HEATMAP_TILT_STEP + const rounded = Math.round(snapped * 10) / 10 + return Math.min( + MAX_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE, + Math.max(MIN_SPECTRUM_HEATMAP_TILT_DB_PER_OCTAVE, rounded) + ) +} diff --git a/src/types/vumeter.ts b/src/types/vumeter.ts new file mode 100644 index 0000000..3be54d2 --- /dev/null +++ b/src/types/vumeter.ts @@ -0,0 +1,16 @@ +export type VUMeterMode = 'needle' | 'bar' +export type VUMeterOrientation = 'horizontal' | 'vertical' + +export const VU_METER_MODES: readonly VUMeterMode[] = ['needle', 'bar'] +export const VU_METER_ORIENTATIONS: readonly VUMeterOrientation[] = ['horizontal', 'vertical'] + +export const DEFAULT_VU_METER_MODE: VUMeterMode = 'bar' +export const DEFAULT_VU_METER_ORIENTATION: VUMeterOrientation = 'horizontal' + +export function isVUMeterMode(value: unknown): value is VUMeterMode { + return typeof value === 'string' && VU_METER_MODES.includes(value as VUMeterMode) +} + +export function isVUMeterOrientation(value: unknown): value is VUMeterOrientation { + return typeof value === 'string' && VU_METER_ORIENTATIONS.includes(value as VUMeterOrientation) +} diff --git a/src/types/waveform.ts b/src/types/waveform.ts new file mode 100644 index 0000000..252832b --- /dev/null +++ b/src/types/waveform.ts @@ -0,0 +1,29 @@ +export const MIN_WAVEFORM_SCROLL_SPEED = 0.5 +export const MAX_WAVEFORM_SCROLL_SPEED = 8 +export const WAVEFORM_SCROLL_SPEED_STEP = 0.5 +export const DEFAULT_WAVEFORM_SCROLL_SPEED = 1 +export const MIN_WAVEFORM_GAIN_DB = -12 +export const MAX_WAVEFORM_GAIN_DB = 18 +export const WAVEFORM_GAIN_DB_STEP = 0.5 +export const DEFAULT_WAVEFORM_GAIN_DB = 0 + +export function clampWaveformScrollSpeed(value: unknown): number { + const numeric = Number(value) + if (!Number.isFinite(numeric)) { + return DEFAULT_WAVEFORM_SCROLL_SPEED + } + + const snapped = Math.round(numeric / WAVEFORM_SCROLL_SPEED_STEP) * WAVEFORM_SCROLL_SPEED_STEP + return Math.min(MAX_WAVEFORM_SCROLL_SPEED, Math.max(MIN_WAVEFORM_SCROLL_SPEED, snapped)) +} + +export function clampWaveformGainDb(value: unknown): number { + const numeric = Number(value) + if (!Number.isFinite(numeric)) { + return DEFAULT_WAVEFORM_GAIN_DB + } + + const snapped = Math.round(numeric / WAVEFORM_GAIN_DB_STEP) * WAVEFORM_GAIN_DB_STEP + const rounded = Math.round(snapped * 10) / 10 + return Math.min(MAX_WAVEFORM_GAIN_DB, Math.max(MIN_WAVEFORM_GAIN_DB, rounded)) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..88b53a7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..9075b13 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["electron-vite.config.ts"] +}