From bc687dfe871f5dbe07c9f0369ee2cf59da8d7321 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:19:37 -0400 Subject: [PATCH] m4 --- .gitignore | 4 + .../android/build.gradle | 7 + .../AstraLibraryScannerModule.kt | 382 ++++++++++++++++- modules/astra-library-scanner/index.ts | 20 + .../modules/astrascope/AstraScopeModule.kt | 47 ++ .../java/expo/modules/astrascope/EqBridge.kt | 29 ++ .../expo/modules/astrascope/GainBridge.kt | 37 ++ .../expo/modules/astrascope/ScopeBridge.kt | 17 + modules/astra-scope/cpp/scope_jni.cpp | 34 ++ modules/astra-scope/cpp/scope_ring.h | 71 ++- modules/astra-scope/index.ts | 31 ++ src/app/(tabs)/eq.tsx | 405 +++++++++++++++--- src/app/(tabs)/settings.tsx | 226 ++++++++-- src/app/_layout.tsx | 21 +- src/app/now-playing.tsx | 103 +++-- src/audio/autoEQParser.ts | 68 +++ src/audio/eq.ts | 266 ++++++++++++ src/audio/eqNative.ts | 90 ++++ src/audio/eqPresets.ts | 111 +++++ src/audio/normalization.ts | 133 ++++++ src/audio/trackAnalysis.ts | 100 +++++ src/audio/useNormalizationSync.ts | 142 ++++++ src/components/FormatBadge.tsx | 7 +- src/components/MarqueeText.tsx | 137 ++++++ src/components/OscilloscopeWave.tsx | 29 +- src/components/SpectrumCurve.tsx | 10 +- src/components/eq/BandDetailPanel.tsx | 125 ++++++ src/components/eq/BandStrip.tsx | 87 ++++ src/components/eq/EQGraph.tsx | 330 ++++++++++++++ src/components/eq/EQSlider.tsx | 196 +++++++++ src/components/eq/EQValueEditSheet.tsx | 156 +++++++ src/components/eq/EqSheet.tsx | 132 ++++++ src/components/eq/PresetSheet.tsx | 102 +++++ src/components/eq/SavePresetSheet.tsx | 105 +++++ src/components/eq/eqGraphMath.ts | 83 ++++ src/components/eq/format.ts | 31 ++ src/components/library/ScanProgress.tsx | 14 +- src/db/queries.ts | 68 +++ src/db/schema.ts | 30 +- src/library/scanner.ts | 6 +- src/scope/oscilloscopeGain.ts | 32 ++ src/scope/scopeStore.ts | 10 + src/stores/audioSettingsStore.ts | 105 +++++ src/stores/eqStore.ts | 292 +++++++++++-- src/stores/libraryStore.ts | 2 +- src/types/audio.ts | 7 +- src/types/library.ts | 6 + .../classes/classes_dex/classes.dex | Bin 184264 -> 192948 bytes .../kotlinaudio/players/BaseAudioPlayer$2.dex | Bin 5732 -> 5732 bytes .../players/BaseAudioPlayer$Companion.dex | Bin 1360 -> 1360 bytes .../BaseAudioPlayer$PlayerListener.dex | Bin 7968 -> 9244 bytes ...seAudioPlayer$createForwardingPlayer$1.dex | Bin 3548 -> 3548 bytes .../players/BaseAudioPlayer$ratingType$1.dex | Bin 2804 -> 2804 bytes .../kotlinaudio/players/BaseAudioPlayer.dex | Bin 25604 -> 25604 bytes ...FactoryKt$buildScopeRenderersFactory$1.dex | Bin 2608 -> 2900 bytes .../scope/ScopeRenderersFactoryKt.dex | Bin 1468 -> 1468 bytes .../bundleLibCompileToJarDebug/classes.jar | Bin 446201 -> 464005 bytes .../compile-file-map.properties | 2 +- .../players/BaseAudioPlayer$2.class | Bin 6841 -> 6841 bytes .../players/BaseAudioPlayer$Companion.class | Bin 933 -> 933 bytes .../BaseAudioPlayer$PlayerListener.class | Bin 8879 -> 10700 bytes ...AudioPlayer$createForwardingPlayer$1.class | Bin 4036 -> 4036 bytes .../BaseAudioPlayer$ratingType$1.class | Bin 2988 -> 2988 bytes .../kotlinaudio/players/BaseAudioPlayer.class | Bin 34091 -> 34091 bytes ...ctoryKt$buildScopeRenderersFactory$1.class | Bin 2411 -> 2681 bytes .../scope/ScopeRenderersFactoryKt.class | Bin 1197 -> 1197 bytes .../bundleLibRuntimeToJarDebug/classes.jar | Bin 444639 -> 462443 bytes .../caches-jvm/inputs/source-to-output.tab | Bin 4096 -> 4096 bytes .../inputs/source-to-output.tab.keystream | Bin 12288 -> 12288 bytes .../inputs/source-to-output.tab.keystream.len | Bin 8 -> 8 bytes .../inputs/source-to-output.tab.len | Bin 8 -> 8 bytes .../inputs/source-to-output.tab.values.at | Bin 26882 -> 27849 bytes .../caches-jvm/inputs/source-to-output.tab_i | Bin 32768 -> 32768 bytes .../jvm/kotlin/class-attributes.tab | Bin 4096 -> 4096 bytes .../jvm/kotlin/class-attributes.tab.keystream | Bin 12288 -> 12288 bytes .../kotlin/class-attributes.tab.keystream.len | Bin 8 -> 8 bytes .../jvm/kotlin/class-attributes.tab.len | Bin 8 -> 8 bytes .../jvm/kotlin/class-attributes.tab.values.at | Bin 253 -> 262 bytes .../jvm/kotlin/class-attributes.tab_i | Bin 32768 -> 32768 bytes .../jvm/kotlin/class-fq-name-to-source.tab | Bin 4096 -> 4096 bytes .../class-fq-name-to-source.tab.keystream | Bin 12288 -> 12288 bytes .../class-fq-name-to-source.tab.keystream.len | Bin 8 -> 8 bytes .../kotlin/class-fq-name-to-source.tab.len | Bin 8 -> 8 bytes .../class-fq-name-to-source.tab.values.at | Bin 15179 -> 15864 bytes .../jvm/kotlin/class-fq-name-to-source.tab_i | Bin 32768 -> 32768 bytes .../jvm/kotlin/internal-name-to-source.tab | Bin 4096 -> 4096 bytes .../internal-name-to-source.tab.keystream | Bin 20480 -> 20480 bytes .../internal-name-to-source.tab.keystream.len | Bin 8 -> 8 bytes .../kotlin/internal-name-to-source.tab.len | Bin 8 -> 8 bytes .../internal-name-to-source.tab.values.at | Bin 25018 -> 25703 bytes .../jvm/kotlin/internal-name-to-source.tab_i | Bin 32768 -> 32768 bytes .../cacheable/caches-jvm/jvm/kotlin/proto.tab | Bin 4096 -> 4096 bytes .../caches-jvm/jvm/kotlin/proto.tab.keystream | Bin 12288 -> 12288 bytes .../jvm/kotlin/proto.tab.keystream.len | Bin 8 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.len | Bin 8 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.values.at | Bin 15710 -> 17601 bytes .../caches-jvm/jvm/kotlin/proto.tab_i | Bin 32768 -> 32768 bytes .../jvm/kotlin/source-to-classes.tab | Bin 4096 -> 4096 bytes .../kotlin/source-to-classes.tab.keystream | Bin 12288 -> 12288 bytes .../source-to-classes.tab.keystream.len | Bin 8 -> 8 bytes .../jvm/kotlin/source-to-classes.tab.len | Bin 8 -> 8 bytes .../kotlin/source-to-classes.tab.values.at | Bin 16758 -> 17224 bytes .../jvm/kotlin/source-to-classes.tab_i | Bin 32768 -> 32768 bytes .../jvm/kotlin/subtypes.tab.values.at | Bin 2936 -> 3116 bytes .../caches-jvm/jvm/kotlin/supertypes.tab | Bin 4096 -> 4096 bytes .../jvm/kotlin/supertypes.tab.keystream | Bin 4096 -> 4096 bytes .../jvm/kotlin/supertypes.tab.keystream.len | Bin 8 -> 8 bytes .../caches-jvm/jvm/kotlin/supertypes.tab.len | Bin 8 -> 8 bytes .../jvm/kotlin/supertypes.tab.values.at | Bin 2436 -> 2607 bytes .../caches-jvm/jvm/kotlin/supertypes.tab_i | Bin 32768 -> 32768 bytes .../cacheable/caches-jvm/lookups/counters.tab | 2 +- .../caches-jvm/lookups/file-to-id.tab | Bin 4096 -> 4096 bytes .../lookups/file-to-id.tab.keystream | Bin 12288 -> 12288 bytes .../lookups/file-to-id.tab.keystream.len | Bin 8 -> 8 bytes .../caches-jvm/lookups/file-to-id.tab.len | Bin 8 -> 8 bytes .../lookups/file-to-id.tab.values.at | Bin 235 -> 253 bytes .../caches-jvm/lookups/file-to-id.tab_i | Bin 32768 -> 32768 bytes .../caches-jvm/lookups/id-to-file.tab | Bin 4096 -> 4096 bytes .../lookups/id-to-file.tab.keystream | Bin 4096 -> 4096 bytes .../lookups/id-to-file.tab.keystream.len | Bin 8 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab.len | Bin 8 -> 8 bytes .../lookups/id-to-file.tab.values.at | Bin 3440 -> 3781 bytes .../caches-jvm/lookups/id-to-file.tab_i | Bin 32768 -> 32768 bytes .../cacheable/caches-jvm/lookups/lookups.tab | Bin 16384 -> 28672 bytes .../caches-jvm/lookups/lookups.tab.keystream | Bin 167936 -> 167936 bytes .../lookups/lookups.tab.keystream.len | Bin 8 -> 8 bytes .../caches-jvm/lookups/lookups.tab.len | Bin 8 -> 8 bytes .../caches-jvm/lookups/lookups.tab.values.at | Bin 12391 -> 13995 bytes .../caches-jvm/lookups/lookups.tab_i | Bin 32768 -> 32768 bytes .../cacheable/last-build.bin | Bin 18 -> 18 bytes .../shrunk-classpath-snapshot.bin | Bin 71362 -> 74352 bytes .../local-state/build-history.bin | Bin 31 -> 31 bytes .../players/BaseAudioPlayer$2.class | Bin 6841 -> 6841 bytes .../players/BaseAudioPlayer$Companion.class | Bin 933 -> 933 bytes .../BaseAudioPlayer$PlayerListener.class | Bin 8879 -> 10700 bytes ...AudioPlayer$createForwardingPlayer$1.class | Bin 4036 -> 4036 bytes .../BaseAudioPlayer$ratingType$1.class | Bin 2988 -> 2988 bytes .../kotlinaudio/players/BaseAudioPlayer.class | Bin 34091 -> 34091 bytes ...ctoryKt$buildScopeRenderersFactory$1.class | Bin 2411 -> 2681 bytes .../scope/ScopeRenderersFactoryKt.class | Bin 1197 -> 1197 bytes .../kotlinaudio/players/BaseAudioPlayer.kt | 7 + .../kotlinaudio/scope/EqAudioProcessor.kt | 258 +++++++++++ .../scope/NormalizationGainProcessor.kt | 80 ++++ .../scope/PostEqTapAudioProcessor.kt | 66 +++ .../scope/ScopeRenderersFactory.kt | 21 +- 145 files changed, 4685 insertions(+), 197 deletions(-) create mode 100644 modules/astra-scope/android/src/main/java/expo/modules/astrascope/EqBridge.kt create mode 100644 modules/astra-scope/android/src/main/java/expo/modules/astrascope/GainBridge.kt create mode 100644 src/audio/autoEQParser.ts create mode 100644 src/audio/eq.ts create mode 100644 src/audio/eqNative.ts create mode 100644 src/audio/eqPresets.ts create mode 100644 src/audio/normalization.ts create mode 100644 src/audio/trackAnalysis.ts create mode 100644 src/audio/useNormalizationSync.ts create mode 100644 src/components/MarqueeText.tsx create mode 100644 src/components/eq/BandDetailPanel.tsx create mode 100644 src/components/eq/BandStrip.tsx create mode 100644 src/components/eq/EQGraph.tsx create mode 100644 src/components/eq/EQSlider.tsx create mode 100644 src/components/eq/EQValueEditSheet.tsx create mode 100644 src/components/eq/EqSheet.tsx create mode 100644 src/components/eq/PresetSheet.tsx create mode 100644 src/components/eq/SavePresetSheet.tsx create mode 100644 src/components/eq/eqGraphMath.ts create mode 100644 src/components/eq/format.ts create mode 100644 src/scope/oscilloscopeGain.ts create mode 100644 src/stores/audioSettingsStore.ts create mode 100644 vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/EqAudioProcessor.kt create mode 100644 vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/NormalizationGainProcessor.kt create mode 100644 vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/PostEqTapAudioProcessor.kt diff --git a/.gitignore b/.gitignore index 3bc4011..2bf3da5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,11 @@ example modules/*/android/build/ modules/*/android/.gradle/ modules/*/android/.cxx/ + +# vendored kotlin-audio fork — built from source by the root Gradle build vendor/kotlinaudio/kotlin-audio/build/ +vendor/kotlinaudio/kotlin-audio/.gradle/ +vendor/kotlinaudio/kotlin-audio/.cxx/ HANDOFF.md DESIGN.md diff --git a/modules/astra-library-scanner/android/build.gradle b/modules/astra-library-scanner/android/build.gradle index 4498825..165986b 100644 --- a/modules/astra-library-scanner/android/build.gradle +++ b/modules/astra-library-scanner/android/build.gradle @@ -16,3 +16,10 @@ android { abortOnError false } } + +dependencies { + // ExoPlayer 2.19.0 (same version the vendored kotlin-audio fork pulls in) for + // MetadataRetriever — parses ID3/Vorbis/MP4 container tags (ReplayGain) without + // decoding audio. Transitively brings exoplayer-extractor (the frame classes). + implementation 'com.google.android.exoplayer:exoplayer-core:2.19.0' +} diff --git a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/AstraLibraryScannerModule.kt b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/AstraLibraryScannerModule.kt index c40576e..7aedcb6 100644 --- a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/AstraLibraryScannerModule.kt +++ b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/AstraLibraryScannerModule.kt @@ -12,9 +12,19 @@ import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build import android.provider.DocumentsContract +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.MetadataRetriever +import com.google.android.exoplayer2.metadata.id3.InternalFrame +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame +import com.google.android.exoplayer2.metadata.flac.VorbisComment import java.nio.ByteOrder +import java.util.concurrent.TimeUnit +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.log10 import kotlin.math.max import kotlin.math.sqrt +import kotlin.math.tan import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.functions.Coroutine import expo.modules.kotlin.modules.Module @@ -38,6 +48,21 @@ class FileRequest : Record { @Field val coverUri: String? = null } +/** Result of one scan-time decode: waveform peaks + integrated loudness + sample peak. */ +class AudioAnalysis : Record { + @Field var peaks: FloatArray = FloatArray(0) + @Field var lufs: Double? = null // integrated LUFS (negative dB); null if unmeasured + @Field var peak: Double? = null // absolute sample peak, linear [0,1]; null if unmeasured +} + +/** ReplayGain tags read from the container (no audio decode). Null = tag absent. */ +class ReplayGainTags : Record { + @Field var trackGainDb: Double? = null // REPLAYGAIN_TRACK_GAIN (dB) + @Field var albumGainDb: Double? = null // REPLAYGAIN_ALBUM_GAIN (dB) + @Field var trackPeak: Double? = null // REPLAYGAIN_TRACK_PEAK (linear, ~[0,1+]) + @Field var albumPeak: Double? = null // REPLAYGAIN_ALBUM_PEAK (linear, ~[0,1+]) +} + class AstraLibraryScannerModule : Module() { private val artworkThumbSize = 128 @@ -72,10 +97,25 @@ class AstraLibraryScannerModule : Module() { // run lazily per track on the JS side; results are cached in SQLite there. AsyncFunction("extractWaveform") Coroutine { uri: String, bins: Int -> waveformSemaphore.withPermit { - withContext(Dispatchers.IO) { extractWaveform(uri, if (bins > 0) bins else 512) } + withContext(Dispatchers.IO) { decodeAndAnalyze(uri, if (bins > 0) bins else 512).peaks } } } + // Fast loudness (M4): decodes only a few short windows spread across the track + // (not the whole file) + gated K-weighting -> integrated LUFS + sample peak. + // Waveform peaks stay lazy/full-decode (extractWaveform), decoupled from this. + AsyncFunction("measureLoudness") Coroutine { uri: String -> + waveformSemaphore.withPermit { + withContext(Dispatchers.IO) { measureLoudness(uri) } + } + } + + // ReplayGain tags (M4): reads container metadata only (no PCM decode), so it is + // cheap and lets us normalize a tagged library without the slow loudness decode. + AsyncFunction("readReplayGain") Coroutine { uri: String -> + withContext(Dispatchers.IO) { readReplayGain(uri) } + } + Function("getArtworkDirPath") { artworkDir().absolutePath } @@ -127,6 +167,100 @@ class AstraLibraryScannerModule : Module() { private fun artworkThumbDir(): File = File(requireContext().filesDir, "artwork-thumbs").apply { if (!exists()) mkdirs() } + // --------------------------------------------------------------------------- + // ReplayGain tags (container metadata only — no audio decode) + // --------------------------------------------------------------------------- + + // Cap MetadataRetriever per file so a malformed/huge container can't hang a worker. + private val metadataTimeoutMs = 12_000L + + /** + * Read ReplayGain track/album gain (dB) + peak (linear) from container tags via + * ExoPlayer's MetadataRetriever (parses ID3 TXXX, Vorbis comments, MP4 freeform + * atoms without decoding PCM). Mirrors the desktop's extractReplayGainDb fuzzy + * matching. Returns all-null on any failure (unsupported container, IO, timeout). + */ + private fun readReplayGain(uriStr: String): ReplayGainTags { + val result = ReplayGainTags() + try { + val mediaItem = MediaItem.fromUri(Uri.parse(uriStr)) + val trackGroups = MetadataRetriever.retrieveMetadata(requireContext(), mediaItem) + .get(metadataTimeoutMs, TimeUnit.MILLISECONDS) + + // R128 (Opus / EBU) is a fallback used only when no REPLAYGAIN_* tag is present. + var r128Track: Double? = null + var r128Album: Double? = null + + fun consider(rawKey: String?, rawValue: String?) { + if (rawKey == null || rawValue == null) return + val key = normalizeRgKey(rawKey) + when { + result.trackGainDb == null && (key.contains("replaygain_track_gain") || key.contains("rg_track_gain")) -> + result.trackGainDb = parseRgDb(rawValue) + result.albumGainDb == null && (key.contains("replaygain_album_gain") || key.contains("rg_album_gain")) -> + result.albumGainDb = parseRgDb(rawValue) + result.trackPeak == null && (key.contains("replaygain_track_peak") || key.contains("rg_track_peak")) -> + result.trackPeak = parsePeak(rawValue) + result.albumPeak == null && (key.contains("replaygain_album_peak") || key.contains("rg_album_peak")) -> + result.albumPeak = parsePeak(rawValue) + r128Track == null && key.contains("r128_track_gain") -> r128Track = parseR128(rawValue) + r128Album == null && key.contains("r128_album_gain") -> r128Album = parseR128(rawValue) + } + } + + for (g in 0 until trackGroups.length) { + val group = trackGroups.get(g) + for (f in 0 until group.length) { + val metadata = group.getFormat(f).metadata ?: continue + for (i in 0 until metadata.length()) { + when (val entry = metadata.get(i)) { + // ID3 user-defined text frame: description is the key, value the text. + is TextInformationFrame -> if (entry.id == "TXXX") consider(entry.description, entry.value) + // FLAC/Ogg/Opus Vorbis comments (vorbis.VorbisComment extends this). + is VorbisComment -> consider(entry.key, entry.value) + // MP4 iTunes freeform "----:com.apple.iTunes:replaygain_*" atoms. + is InternalFrame -> consider(entry.description, entry.text) + } + } + } + } + + if (result.trackGainDb == null) result.trackGainDb = r128Track + if (result.albumGainDb == null) result.albumGainDb = r128Album + } catch (_: Throwable) { + // Unsupported container, IO error, or timeout -> no tags. + } + return result + } + + private fun normalizeRgKey(id: String): String = + id.trim().lowercase().replace(Regex("[\\s-]+"), "_") + + /** Parse a ReplayGain dB value like "-6.54 dB", "-6,54", or "+3.2". */ + private fun parseRgDb(raw: String): Double? { + val trimmed = raw.trim() + if (trimmed.isEmpty()) return null + trimmed.toDoubleOrNull()?.let { return it } + trimmed.replace(Regex("(?i)\\s*dB\\s*$"), "").trim().toDoubleOrNull()?.let { return it } + val m = Regex("[+-]?\\d+(?:[.,]\\d+)?").find(trimmed) ?: return null + return m.value.replace(',', '.').toDoubleOrNull() + } + + /** Parse a ReplayGain peak (linear amplitude, > 0). */ + private fun parsePeak(raw: String): Double? { + val trimmed = raw.trim() + trimmed.toDoubleOrNull()?.let { return if (it > 0.0) it else null } + val m = Regex("[+-]?\\d+(?:[.,]\\d+)?").find(trimmed) ?: return null + val v = m.value.replace(',', '.').toDoubleOrNull() ?: return null + return if (v > 0.0) v else null + } + + /** R128_*_GAIN is Q7.8 dB relative to -23 LUFS; +5 dB realigns to the RG reference. */ + private fun parseR128(raw: String): Double? { + val v = raw.trim().toIntOrNull() ?: return null + return v / 256.0 + 5.0 + } + // --------------------------------------------------------------------------- // Directory walk // --------------------------------------------------------------------------- @@ -303,11 +437,12 @@ class AstraLibraryScannerModule : Module() { // Waveform peaks (offline RMS bins) // --------------------------------------------------------------------------- - // Decodes the whole file to PCM and accumulates RMS energy per bin (mirrors - // desktop waveformExtractor.extractWaveformPeaks), normalized to [0,1]. Returns - // an empty array on any failure (caller falls back to a flat seek bar). - private fun extractWaveform(uriStr: String, bins: Int): FloatArray { + // One whole-file PCM decode -> per-bin RMS waveform peaks (normalized [0,1]) for the + // seek bar. Returns empty peaks on any failure (caller falls back to a flat seek + // bar). Loudness is measured separately by measureLoudness. + private fun decodeAndAnalyze(uriStr: String, bins: Int): AudioAnalysis { val context = requireContext() + val result = AudioAnalysis() val uri = Uri.parse(uriStr) val extractor = MediaExtractor() var codec: MediaCodec? = null @@ -322,7 +457,7 @@ class AstraLibraryScannerModule : Module() { trackFormat = f; trackIndex = i; break } } - val format = trackFormat ?: return FloatArray(0) + val format = trackFormat ?: return result extractor.selectTrack(trackIndex) val sampleRate = @@ -370,7 +505,7 @@ class AstraLibraryScannerModule : Module() { out.position(info.offset) out.limit(info.offset + info.size) out.order(ByteOrder.nativeOrder()) - frame = accumulate(out, pcmFloat, channelCount, bins, totalFrames, frame, sumSquares, counts) + frame = accumulateAnalyze(out, pcmFloat, channelCount, bins, totalFrames, frame, sumSquares, counts) } codec.releaseOutputBuffer(outIndex, false) } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { @@ -394,9 +529,10 @@ class AstraLibraryScannerModule : Module() { if (globalMax > 0) { for (i in 0 until bins) peaks[i] = (peaks[i] / globalMax).toFloat() } - return peaks + result.peaks = peaks + return result } catch (_: Throwable) { - return FloatArray(0) + return result } finally { try { codec?.stop() } catch (_: Throwable) {} try { codec?.release() } catch (_: Throwable) {} @@ -404,9 +540,128 @@ class AstraLibraryScannerModule : Module() { } } - // Folds one decoded PCM buffer into the per-bin RMS accumulators. Handles + // Integrated gated loudness over the WHOLE file (accurate — subset sampling caused + // too much loudness inconsistency). Decodes the full track and feeds the gated + // K-weighting meter. Measured on the fly per track (current + queue lookahead) and + // cached, so the cost is paid once per track, never in a bulk background pass. + private fun measureLoudness(uriStr: String): AudioAnalysis { + val context = requireContext() + val result = AudioAnalysis() + val uri = Uri.parse(uriStr) + val extractor = MediaExtractor() + var codec: MediaCodec? = null + try { + extractor.setDataSource(context, uri, null) + + var trackFormat: MediaFormat? = null + var trackIndex = -1 + for (i in 0 until extractor.trackCount) { + val f = extractor.getTrackFormat(i) + if (f.getString(MediaFormat.KEY_MIME)?.startsWith("audio/") == true) { + trackFormat = f; trackIndex = i; break + } + } + val format = trackFormat ?: return result + extractor.selectTrack(trackIndex) + + val sampleRate = + if (format.containsKey(MediaFormat.KEY_SAMPLE_RATE)) format.getInteger(MediaFormat.KEY_SAMPLE_RATE) else 44100 + var channelCount = + if (format.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) else 2 + var pcmFloat = false + + codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!) + codec.configure(format, null, null, 0) + codec.start() + val info = MediaCodec.BufferInfo() + + var meter: LoudnessMeter? = null + var sawInputEOS = false + var sawOutputEOS = false + while (!sawOutputEOS) { + if (!sawInputEOS) { + val inIndex = codec.dequeueInputBuffer(10_000) + if (inIndex >= 0) { + val inBuf = codec.getInputBuffer(inIndex)!! + val size = extractor.readSampleData(inBuf, 0) + if (size < 0) { + codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + sawInputEOS = true + } else { + codec.queueInputBuffer(inIndex, 0, size, extractor.sampleTime, 0) + extractor.advance() + } + } + } + val outIndex = codec.dequeueOutputBuffer(info, 10_000) + if (outIndex >= 0) { + if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) sawOutputEOS = true + if (info.size > 0) { + val out = codec.getOutputBuffer(outIndex)!! + out.position(info.offset) + out.limit(info.offset + info.size) + out.order(ByteOrder.nativeOrder()) + val m = meter ?: LoudnessMeter(channelCount, sampleRate).also { meter = it } + feedMeter(out, pcmFloat, channelCount, m) + } + codec.releaseOutputBuffer(outIndex, false) + } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + val nf = codec.outputFormat + if (nf.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) channelCount = nf.getInteger(MediaFormat.KEY_CHANNEL_COUNT) + if (nf.containsKey(MediaFormat.KEY_PCM_ENCODING)) { + pcmFloat = nf.getInteger(MediaFormat.KEY_PCM_ENCODING) == AudioFormat.ENCODING_PCM_FLOAT + } + } + } + + meter?.let { + result.lufs = it.lufs() + result.peak = it.peak + } + return result + } catch (_: Throwable) { + return result + } finally { + try { codec?.stop() } catch (_: Throwable) {} + try { codec?.release() } catch (_: Throwable) {} + try { extractor.release() } catch (_: Throwable) {} + } + } + + // Feeds one decoded PCM buffer (16-bit or float) to the loudness meter. + private fun feedMeter( + out: java.nio.ByteBuffer, + pcmFloat: Boolean, + channelCount: Int, + meter: LoudnessMeter + ) { + if (pcmFloat) { + val fb = out.asFloatBuffer() + val n = fb.remaining() + var k = 0 + while (k < n) { + var c = 0 + while (c < channelCount && k < n) { + meter.process(fb.get(k).toDouble(), c); k++; c++ + } + } + } else { + val sb = out.asShortBuffer() + val n = sb.remaining() + var k = 0 + while (k < n) { + var c = 0 + while (c < channelCount && k < n) { + meter.process(sb.get(k) / 32768.0, c); k++; c++ + } + } + } + } + + // Folds one decoded PCM buffer into the per-bin RMS accumulators and, when a + // loudness meter is provided, the K-weighted loudness + sample peak. Handles // 16-bit (default) and float PCM. Returns the updated running frame index. - private fun accumulate( + private fun accumulateAnalyze( out: java.nio.ByteBuffer, pcmFloat: Boolean, channelCount: Int, @@ -451,6 +706,111 @@ class AstraLibraryScannerModule : Module() { return frame } + // Gated integrated K-weighted loudness per ITU-R BS.1770 + absolute sample peak. + // Two cascaded biquads (high-shelf pre-filter + RLB high-pass) per channel with + // pyloudnorm-reference coefficients (so the -0.691 offset holds), accumulated into + // 400 ms blocks, then a two-stage gate (-70 LUFS absolute, -10 LU relative). Unity + // channel weights (fine for mono/stereo). Non-overlapping blocks (vs the spec's 75% + // overlap) — within ~0.1 LU and much cheaper. + private class LoudnessMeter(private val channels: Int, sampleRate: Int) { + private val b0a: Double; private val b1a: Double; private val b2a: Double + private val a1a: Double; private val a2a: Double + private val a1b: Double; private val a2b: Double + + private val s1a = DoubleArray(channels) + private val s2a = DoubleArray(channels) + private val s1b = DoubleArray(channels) + private val s2b = DoubleArray(channels) + + private val blockSumSq = DoubleArray(channels) + private val blockFrames: Int + private var framesInBlock = 0 + // Per-block summed-channel mean-square energy (z), for gating. + private val blockEnergies = ArrayList() + + var peak: Double = 0.0 + private set + + init { + val fs = sampleRate.coerceAtLeast(1).toDouble() + // Stage 1: high-shelf pre-filter. + val f0a = 1681.974450955533 + val ga = 3.999843853973347 + val qa = 0.7071752369554196 + val ka = tan(PI * f0a / fs) + val vh = Math.pow(10.0, ga / 20.0) + val vb = Math.pow(vh, 0.4996667741545416) + val a0a = 1.0 + ka / qa + ka * ka + b0a = (vh + vb * ka / qa + ka * ka) / a0a + b1a = 2.0 * (ka * ka - vh) / a0a + b2a = (vh - vb * ka / qa + ka * ka) / a0a + a1a = 2.0 * (ka * ka - 1.0) / a0a + a2a = (1.0 - ka / qa + ka * ka) / a0a + // Stage 2: RLB high-pass (b = [1, -2, 1]). + val f0b = 38.13547087602444 + val qb = 0.5003270373238773 + val kb = tan(PI * f0b / fs) + val a0b = 1.0 + kb / qb + kb * kb + a1b = 2.0 * (kb * kb - 1.0) / a0b + a2b = (1.0 - kb / qb + kb * kb) / a0b + + blockFrames = max(1L, (0.4 * fs).toLong()).toInt() // 400 ms gating block + } + + fun process(sample: Double, ch: Int) { + if (ch >= channels) return + val a = abs(sample) + if (a > peak) peak = a + // Stage 1 (transposed direct form II). + val y1 = b0a * sample + s1a[ch] + s1a[ch] = b1a * sample - a1a * y1 + s2a[ch] + s2a[ch] = b2a * sample - a2a * y1 + // Stage 2: b0=1, b1=-2, b2=1. + val y2 = y1 + s1b[ch] + s1b[ch] = -2.0 * y1 - a1b * y2 + s2b[ch] + s2b[ch] = y1 - a2b * y2 + blockSumSq[ch] += y2 * y2 + // One frame completes when the last channel of the frame is processed. + if (ch == channels - 1) { + framesInBlock++ + if (framesInBlock >= blockFrames) finalizeBlock() + } + } + + private fun finalizeBlock() { + if (framesInBlock <= 0) return + var energy = 0.0 + for (c in 0 until channels) { + energy += blockSumSq[c] / framesInBlock + blockSumSq[c] = 0.0 + } + framesInBlock = 0 + if (energy > 0.0) blockEnergies.add(energy) + } + + fun lufs(): Double { + finalizeBlock() // flush the trailing partial block + if (blockEnergies.isEmpty()) return -70.0 + + // Absolute gate at -70 LUFS (energy terms). + val absThresh = Math.pow(10.0, (-70.0 + 0.691) / 10.0) + var sum = 0.0 + var cnt = 0 + for (e in blockEnergies) if (e >= absThresh) { sum += e; cnt++ } + if (cnt == 0) return -70.0 + + // Relative gate: -10 LU below the abs-gated mean. + val relLoudness = -0.691 + 10.0 * log10(sum / cnt) + val relThresh = Math.pow(10.0, (relLoudness - 10.0 + 0.691) / 10.0) + sum = 0.0 + cnt = 0 + for (e in blockEnergies) if (e >= absThresh && e >= relThresh) { sum += e; cnt++ } + if (cnt == 0) return -70.0 + + return -0.691 + 10.0 * log10(sum / cnt) + } + } + private fun readBitsPerSample(format: MediaFormat): Int? { // The framework FLAC/WAV extractors expose "bits-per-sample"; other codecs // may expose a PCM encoding instead. Both are best-effort. diff --git a/modules/astra-library-scanner/index.ts b/modules/astra-library-scanner/index.ts index 7905e4a..ce330c4 100644 --- a/modules/astra-library-scanner/index.ts +++ b/modules/astra-library-scanner/index.ts @@ -43,6 +43,14 @@ export interface ExtractedMetadata { artworkHash?: string | null; } +/** ReplayGain tags read from a file's container (null = tag absent). */ +export interface ReplayGainTags { + trackGainDb: number | null; + albumGainDb: number | null; + trackPeak: number | null; + albumPeak: number | null; +} + export interface ScanProgressEvent { phase: 'discovering'; found: number; @@ -60,6 +68,18 @@ declare class AstraLibraryScannerModuleType extends NativeModule; + /** + * Fast integrated loudness (M4): decodes only a few short windows across the + * track + gated K-weighting -> integrated LUFS + absolute sample peak. Null on + * failure / unmeasurable audio. Waveform peaks are separate (extractWaveform). + */ + measureLoudness(uri: string): Promise<{ lufs: number | null; peak: number | null }>; + /** + * Read ReplayGain track/album gain (dB) + peak (linear) from container tags + * (ID3 TXXX / Vorbis comments / MP4 freeform) without decoding audio. All fields + * are null when the tag is absent; the whole call is cheap (metadata only). + */ + readReplayGain(uri: string): Promise; getArtworkDirPath(): string; getArtworkThumbDirPath(): string; ensureArtworkThumbnails(hashes: string[]): Promise; diff --git a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt index eac67fc..0e2c329 100644 --- a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt +++ b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt @@ -29,5 +29,52 @@ class AstraScopeModule : Module() { Function("getOscilloscopeFrame") { out: Float32Array -> ScopeBridge.nativeFillOscilloscope(out.toDirectBuffer(), out.length) } + + // --- M4: post-EQ spectrum (EQ screen overlay) --- + // Gate the post-EQ tap (true only while the EQ screen is open). + Function("setActivePostEq") { active: Boolean -> + ScopeBridge.postEqActive = active + } + + Function("getSpectrumFramePostEq") { out: Float32Array -> + ScopeBridge.nativeFillSpectrumPostEq(out.toDirectBuffer(), out.length) + } + + // --- M4: EQ params + per-track gain (consumed by the kotlin-audio processors) --- + Function("setEqEnabled") { enabled: Boolean -> + EqBridge.enabled = enabled + EqBridge.revision += 1 + } + + Function("setEqPreamp") { linear: Double -> + EqBridge.preampLinear = linear.toFloat() + EqBridge.revision += 1 + } + + // Flat band params: 5 floats per band [typeOrdinal, frequency, gain, Q, enabled?1:0]. + Function("setEqBands") { params: FloatArray -> + EqBridge.bands = params + EqBridge.revision += 1 + } + + Function("setNormalizationGain") { linear: Double -> + GainBridge.linearGain = linear.toFloat() + } + + // Register a queued track's gain by URL so the player can switch to it natively at + // the exact media-item transition (no JS round-trip on track change). + Function("setTrackGain") { url: String, linear: Double -> + GainBridge.putGain(url, linear.toFloat()) + } + + // Make the registered gain for this URL active now (used for the current track on + // mount / settings change, where no transition fires). + Function("activateTrackGain") { url: String -> + GainBridge.activateFor(url) + } + + Function("clearTrackGains") { + GainBridge.clearGains() + } } } diff --git a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/EqBridge.kt b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/EqBridge.kt new file mode 100644 index 0000000..49348e2 --- /dev/null +++ b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/EqBridge.kt @@ -0,0 +1,29 @@ +package expo.modules.astrascope + +/** + * Holds the current parametric-EQ configuration, set from JS (raw band params, NOT + * biquad coefficients). The vendored kotlin-audio `EqAudioProcessor` reads these + * fields lock-free on the ExoPlayer audio thread and recomputes coefficients at the + * real stream sample rate whenever [revision] changes. + * + * Single writer (JS thread via [AstraScopeModule]); single reader (audio thread). + * `bands` is published before `revision` is bumped, so a reader that observes a new + * revision also observes the matching band array. + */ +object EqBridge { + /** Master bypass. When false the processor is passthrough. */ + @Volatile + var enabled: Boolean = false + + /** EQ preamp as a linear amplitude (1 = unity). */ + @Volatile + var preampLinear: Float = 1f + + /** 5 floats per band: [typeOrdinal, frequency, gain, Q, enabled?1:0]. */ + @Volatile + var bands: FloatArray = FloatArray(0) + + /** Bumped on every enabled/preamp/bands change so the processor recomputes. */ + @Volatile + var revision: Int = 0 +} diff --git a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/GainBridge.kt b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/GainBridge.kt new file mode 100644 index 0000000..1a86334 --- /dev/null +++ b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/GainBridge.kt @@ -0,0 +1,37 @@ +package expo.modules.astrascope + +import java.util.concurrent.ConcurrentHashMap + +/** + * Per-track normalization / ReplayGain gain, read lock-free by the vendored + * kotlin-audio `NormalizationGainProcessor` on the audio thread. Applied BEFORE the + * scope taps so the scopes see normalized levels. + * + * JS pre-registers each queued track's gain by URL ([putGain]); the player then swaps + * the active gain to the matching one natively at the real media-item transition + * ([activateFor], called from BaseAudioPlayer.onMediaItemTransition). That lands the + * gain at the actual audio boundary instead of after a JS round-trip on track change. + */ +object GainBridge { + /** Active linear amplitude multiplier (1 = unity). Read on the audio thread. */ + @Volatile + var linearGain: Float = 1f + + // url -> linear gain, seeded from JS ahead of playback. + private val gains = ConcurrentHashMap() + + /** Register (or update) the gain for a track URL. */ + fun putGain(url: String, gain: Float) { + gains[url] = gain + } + + /** Make the gain registered for [url] active (unity if unknown/null). */ + fun activateFor(url: String?) { + linearGain = if (url != null) gains[url] ?: 1f else 1f + } + + /** Drop all registered gains (e.g. on full queue reset). */ + fun clearGains() { + gains.clear() + } +} diff --git a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt index b885e2a..aadccb9 100644 --- a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt +++ b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt @@ -21,6 +21,14 @@ object ScopeBridge { @Volatile var active: Boolean = false + /** + * Gates the POST-EQ tap specifically (true only while the EQ screen is open). + * The post-EQ tap runs when [active] && [postEqActive], so the second downmix + * costs nothing unless the EQ overlay is actually being viewed. + */ + @Volatile + var postEqActive: Boolean = false + /** Audio thread. Tell the analyzer the stream's sample rate / channels. */ external fun nativeConfigure(sampleRate: Int, channelCount: Int) @@ -41,4 +49,13 @@ object ScopeBridge { * writes straight into JS memory. */ external fun nativeFillOscilloscope(buffer: java.nio.ByteBuffer, capacityFloats: Int): Int + + /** Audio thread. Push POST-EQ interleaved float PCM (the M4 second tap). */ + external fun nativePushFramesPostEq(frames: FloatArray, frameCount: Int, channelCount: Int) + + /** + * Render thread. Fill `buffer` with the latest POST-EQ dB spectrum (feeds the + * EQ screen's response-curve overlay). Returns bins written. Zero-copy. + */ + external fun nativeFillSpectrumPostEq(buffer: java.nio.ByteBuffer, capacityFloats: Int): Int } diff --git a/modules/astra-scope/cpp/scope_jni.cpp b/modules/astra-scope/cpp/scope_jni.cpp index 5a7261f..33f6d8a 100644 --- a/modules/astra-scope/cpp/scope_jni.cpp +++ b/modules/astra-scope/cpp/scope_jni.cpp @@ -73,4 +73,38 @@ Java_expo_modules_astrascope_ScopeBridge_nativeFillOscilloscope( return static_cast(n); } +// --- POST-EQ source (M4) ---------------------------------------------------- +// The post-EQ tap pushes here; the EQ screen pulls the post-EQ spectrum. + +JNIEXPORT void JNICALL +Java_expo_modules_astrascope_ScopeBridge_nativePushFramesPostEq( + JNIEnv* env, jobject /*thiz*/, jfloatArray frames, jint frameCount, + jint channelCount) { + if (frames == nullptr || frameCount <= 0 || channelCount <= 0) { + return; + } + auto* data = static_cast( + env->GetPrimitiveArrayCritical(frames, nullptr)); + if (data == nullptr) { + return; + } + driver().pushInterleavedPostEq(data, static_cast(frameCount), + static_cast(channelCount)); + env->ReleasePrimitiveArrayCritical(frames, data, JNI_ABORT); +} + +JNIEXPORT jint JNICALL +Java_expo_modules_astrascope_ScopeBridge_nativeFillSpectrumPostEq( + JNIEnv* env, jobject /*thiz*/, jobject buffer, jint capacityFloats) { + if (buffer == nullptr || capacityFloats <= 0) { + return 0; + } + auto* dst = static_cast(env->GetDirectBufferAddress(buffer)); + if (dst == nullptr) { + return 0; + } + const size_t n = driver().fillSpectrumPostEq(dst, static_cast(capacityFloats)); + return static_cast(n); +} + } // extern "C" diff --git a/modules/astra-scope/cpp/scope_ring.h b/modules/astra-scope/cpp/scope_ring.h index d40cbed..b20a1f3 100644 --- a/modules/astra-scope/cpp/scope_ring.h +++ b/modules/astra-scope/cpp/scope_ring.h @@ -184,10 +184,70 @@ class ScopeDriver { return count; } + // ---- POST-EQ source (M4) ------------------------------------------------- + // A second, spectrum-only SPSC source fed by the post-EQ tap. Mirrors the + // pre-EQ spectrum path exactly; used only by the EQ screen's response-curve + // overlay, so there is no post-EQ oscilloscope. + + // Audio thread. Downmix interleaved float frames to mono into the post-EQ ring. + void pushInterleavedPostEq(const float* data, size_t frames, int channels) { + if (data == nullptr || frames == 0 || channels <= 0) { + return; + } + size_t w = postEqWritePos_.load(std::memory_order_relaxed); + const float inv = 1.0f / static_cast(channels); + for (size_t f = 0; f < frames; ++f) { + float sum = 0.0f; + const float* frame = data + f * channels; + for (int c = 0; c < channels; ++c) { + sum += frame[c]; + } + postEqRing_[w & kMask] = sum * inv; + ++w; + } + postEqWritePos_.store(w, std::memory_order_release); + } + + // Render thread. Latest post-EQ spectrum window -> `out` (dB magnitudes). + size_t fillSpectrumPostEq(float* out, size_t cap) { + if (out == nullptr || cap == 0) { + return 0; + } + + const int sr = pendingSampleRate_.load(std::memory_order_acquire); + if (sr != postEqAppliedSampleRate_) { + postEqSpectrum_.setSampleRate(static_cast(sr)); + postEqAppliedSampleRate_ = sr; + } + + const size_t fftSize = postEqSpectrum_.getFFTSize(); + const size_t w = postEqWritePos_.load(std::memory_order_acquire); + const size_t sampleRate = sr > 0 ? static_cast(sr) : static_cast(48000); + const size_t delaySamples = scopeOutputDelaySamples(sampleRate); + const size_t readHead = w > delaySamples ? w - delaySamples : 0; + + const std::vector* mags; + if (readHead >= fftSize) { + postEqScratch_.resize(fftSize); + const size_t start = readHead - fftSize; + for (size_t i = 0; i < fftSize; ++i) { + postEqScratch_[i] = postEqRing_[(start + i) & kMask]; + } + mags = &postEqSpectrum_.process(postEqScratch_.data(), fftSize); + } else { + mags = &postEqSpectrum_.process(nullptr, 0); + } + + const size_t n = std::min(cap, mags->size()); + std::memcpy(out, mags->data(), n * sizeof(float)); + return n; + } + size_t binCount() const { return spectrum_.getFFTSize() / 2; } void reset() { spectrum_.reset(); + postEqSpectrum_.reset(); osc_.reset(); oscReadPos_ = writePos_.load(std::memory_order_acquire); oscSamplesSeen_ = 0; @@ -196,9 +256,11 @@ class ScopeDriver { } private: - ScopeDriver() : spectrum_(kFftSize) { + ScopeDriver() : spectrum_(kFftSize), postEqSpectrum_(kFftSize) { spectrum_.setSmoothing(0.92f); + postEqSpectrum_.setSmoothing(0.92f); ring_.assign(kSize, 0.0f); + postEqRing_.assign(kSize, 0.0f); } static constexpr size_t kFftSize = 2048; // -> 1024 dB bins @@ -277,6 +339,13 @@ class ScopeDriver { Visualizer::Spectrum spectrum_; int appliedSampleRate_{0}; + // Post-EQ source (M4) — second SPSC ring + spectrum-only analyzer. + std::vector postEqRing_; + std::atomic postEqWritePos_{0}; + std::vector postEqScratch_; + Visualizer::Spectrum postEqSpectrum_; + int postEqAppliedSampleRate_{0}; + Visualizer::Oscilloscope osc_; size_t oscReadPos_{0}; size_t oscSamplesSeen_{0}; diff --git a/modules/astra-scope/index.ts b/modules/astra-scope/index.ts index 25c794e..42ae706 100644 --- a/modules/astra-scope/index.ts +++ b/modules/astra-scope/index.ts @@ -25,6 +25,37 @@ declare class AstraScopeModuleType extends NativeModule { * ~[-1, 1]. Returns the number of points written (0 before warmup). */ getOscilloscopeFrame(out: Float32Array): number; + /** Gate the post-EQ tap (true only while the EQ screen is visible). */ + setActivePostEq(active: boolean): void; + /** + * Like {@link getSpectrumFrame} but for the POST-EQ tap (ring #2) — feeds the + * EQ screen's spectrum behind the response curve. Returns bins written. + */ + getSpectrumFramePostEq(out: Float32Array): number; + + // --- M4 EQ + per-track gain (params pushed from JS; biquad coeffs computed + // natively at the real stream sample rate) --- + + /** Master EQ bypass. When false the EqAudioProcessor is passthrough. */ + setEqEnabled(enabled: boolean): void; + /** EQ preamp as a linear amplitude (1 = unity). */ + setEqPreamp(linear: number): void; + /** + * Flat band params: 5 values per band — [typeOrdinal, frequency, gain, Q, + * enabled?1:0]. Native recomputes biquad coefficients at the stream rate. + */ + setEqBands(params: number[]): void; + /** Set the active per-track normalization/ReplayGain gain (linear; 1 = unity). */ + setNormalizationGain(linear: number): void; + /** + * Register a queued track's gain by URL. The player switches the active gain to the + * matching entry natively at the media-item transition (no JS round-trip). + */ + setTrackGain(url: string, linear: number): void; + /** Activate the registered gain for this URL now (current track on mount/settings). */ + activateTrackGain(url: string): void; + /** Drop all registered per-track gains. */ + clearTrackGains(): void; } export const AstraScope = requireNativeModule('AstraScope'); diff --git a/src/app/(tabs)/eq.tsx b/src/app/(tabs)/eq.tsx index 60b454b..53a88f8 100644 --- a/src/app/(tabs)/eq.tsx +++ b/src/app/(tabs)/eq.tsx @@ -1,76 +1,381 @@ -import { View, StyleSheet } from 'react-native'; +import { useCallback, useState } from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useFocusEffect } from 'expo-router'; +import * as DocumentPicker from 'expo-document-picker'; +import { readAsStringAsync } from 'expo-file-system/legacy'; import { Screen } from '@/components/Screen'; import { Text } from '@/components/Text'; +import { EQGraph } from '@/components/eq/EQGraph'; +import { BandStrip } from '@/components/eq/BandStrip'; +import { BandDetailPanel, type EQEditableValue } from '@/components/eq/BandDetailPanel'; +import { EQSlider } from '@/components/eq/EQSlider'; +import { EqSheet, EqSheetItem } from '@/components/eq/EqSheet'; +import { EQValueEditSheet } from '@/components/eq/EQValueEditSheet'; +import { PresetSheet } from '@/components/eq/PresetSheet'; +import { SavePresetSheet } from '@/components/eq/SavePresetSheet'; import { colors, radius, spacing } from '@/theme'; import { useEQStore } from '@/stores/eqStore'; +import { useScopeActive } from '@/scope/scopeStore'; +import { setActivePostEqNative } from '@/audio/eqNative'; +import { + EQ_MAX_BANDS, + EQ_MAX_FREQUENCY, + EQ_MAX_GAIN_DB, + EQ_MAX_PREAMP_DB, + EQ_MAX_Q, + EQ_MIN_FREQUENCY, + EQ_MIN_PREAMP_DB, + EQ_MIN_Q, + isPassEQBandType, +} from '@/audio/eq'; +import { parseAutoEQ } from '@/audio/autoEQParser'; +import { BAND_TYPE_LABEL, formatGain } from '@/components/eq/format'; +import type { EQBand, EQBandType } from '@/types/audio'; -function formatFreq(hz: number): string { - return hz >= 1000 ? `${hz / 1000}k` : `${hz}`; -} +type SheetKind = 'none' | 'preset' | 'save' | 'overflow' | 'type'; + +const BAND_TYPES: EQBandType[] = ['lowshelf', 'peaking', 'highshelf', 'highpass', 'lowpass']; export default function EQScreen() { - const bands = useEQStore((s) => s.bands); + const eq = useEQStore(); + const scopeActive = useScopeActive(); + const [focused, setFocused] = useState(false); + const [sheet, setSheet] = useState('none'); + const [editingValue, setEditingValue] = useState(null); + const closeSheet = useCallback(() => setSheet('none'), []); + + // Gate the post-EQ tap to while this screen is visible. + useFocusEffect( + useCallback(() => { + setFocused(true); + setActivePostEqNative(true); + return () => { + setFocused(false); + setActivePostEqNative(false); + }; + }, []) + ); + + const activeBand = eq.bands.find((b) => b.id === eq.activeBandId) ?? null; + const activeBandNumber = eq.bands.findIndex((b) => b.id === eq.activeBandId) + 1; + const presetName = eq.presets.find((p) => p.id === eq.activePresetId)?.name ?? 'Custom'; + const defaultPresetName = `Preset ${eq.presets.filter((p) => p.isCustom).length + 1}`; + const valueEditConfig = activeBand && editingValue ? getValueEditConfig(editingValue, activeBand) : null; + + const handleImportAutoEQ = async () => { + closeSheet(); + try { + const res = await DocumentPicker.getDocumentAsync({ type: '*/*', copyToCacheDirectory: true }); + if (res.canceled || !res.assets?.[0]) return; + const asset = res.assets[0]; + const content = await readAsStringAsync(asset.uri); + const preset = parseAutoEQ(content, asset.name); + if (preset.bands.length > 0) eq.importPreset(preset); + } catch { + /* invalid file — ignore */ + } + }; return ( - - - Equalizer - - - The band model is in place. The Media3 biquad chain that makes these - sliders live arrives in M4. - - - - {bands.map((band) => ( - - - - - - {formatFreq(band.frequency)} - - - ))} + + + Equalizer + + setSheet('save')} hitSlop={8}> + + + setSheet('overflow')} hitSlop={8}> + + + + + setSheet('preset')}> + + {presetName} + + + + + + eq.updateBand(id, updates)} + /> + + + + eq.addBand()} + /> + + + + 0 ? activeBandNumber : 1} + onUpdate={(updates) => activeBand && eq.updateBand(activeBand.id, updates)} + onEditType={() => setSheet('type')} + onEditValue={setEditingValue} + /> + + + + + `${formatGain(v)} dB`} + onChange={eq.setPreamp} + /> + + + + + {eq.enabled ? 'EQ on' : 'EQ off'} + + + + + {sheet === 'preset' ? ( + setSheet('save')} + onClose={closeSheet} + /> + ) : null} + + {sheet === 'save' ? ( + eq.saveCustomPreset(name)} + onClose={closeSheet} + /> + ) : null} + + {sheet === 'overflow' ? ( + + + {eq.bands.length > 1 && activeBand ? ( + { + eq.removeBand(activeBand.id); + closeSheet(); + }} + /> + ) : null} + { + eq.resetToFlat(); + closeSheet(); + }} + /> + + ) : null} + + {sheet === 'type' && activeBand ? ( + + + Filter type + + {BAND_TYPES.map((type) => ( + { + eq.updateBand(activeBand.id, { type }); + closeSheet(); + }} + /> + ))} + + ) : null} + + {valueEditConfig && activeBand && editingValue ? ( + eq.updateBand(activeBand.id, createValueUpdate(editingValue, value))} + onClose={() => setEditingValue(null)} + /> + ) : null} ); } +function getValueEditConfig(kind: EQEditableValue, band: EQBand) { + switch (kind) { + case 'frequency': + return { + title: 'Edit frequency', + initialValue: String(Math.round(band.frequency)), + unit: 'Hz', + rangeLabel: `${EQ_MIN_FREQUENCY}-${EQ_MAX_FREQUENCY} Hz`, + placeholder: '1000 or 1k', + keyboardType: 'default' as const, + parseValue: parseFrequency, + }; + case 'gain': + if (isPassEQBandType(band.type)) return null; + return { + title: 'Edit gain', + initialValue: band.gain.toFixed(1), + unit: 'dB', + rangeLabel: `${-EQ_MAX_GAIN_DB} to +${EQ_MAX_GAIN_DB} dB`, + placeholder: '0.0', + keyboardType: 'numbers-and-punctuation' as const, + parseValue: parseDb, + }; + case 'Q': + return { + title: 'Edit Q', + initialValue: band.Q.toFixed(2), + unit: 'Q', + rangeLabel: `${EQ_MIN_Q}-${EQ_MAX_Q}`, + placeholder: '1.00', + keyboardType: 'numbers-and-punctuation' as const, + parseValue: parsePlainNumber, + }; + } +} + +function createValueUpdate(kind: EQEditableValue, value: number): Partial { + switch (kind) { + case 'frequency': + return { frequency: value }; + case 'gain': + return { gain: value }; + case 'Q': + return { Q: value }; + } +} + +function parseFrequency(value: string): number | null { + const match = value + .trim() + .toLowerCase() + .replace(/,/g, '') + .replace(/\s+/g, '') + .match(/^([+-]?(?:\d+\.?\d*|\.\d+))(khz|hz|k)?$/); + if (!match) return null; + const parsed = Number(match[1]); + if (!Number.isFinite(parsed)) return null; + return match[2] === 'k' || match[2] === 'khz' ? parsed * 1000 : parsed; +} + +function parseDb(value: string): number | null { + const normalized = value.trim().toLowerCase().replace(/\s+/g, ''); + const raw = normalized.endsWith('db') ? normalized.slice(0, -2) : normalized; + return parsePlainNumber(raw); +} + +function parsePlainNumber(value: string): number | null { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + const styles = StyleSheet.create({ - heading: { - marginTop: spacing.xl, - }, - note: { - marginTop: spacing.sm, - marginBottom: spacing.xxl, - lineHeight: 20, - }, - bands: { + header: { flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-end', - }, - band: { - flex: 1, alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.lg, + paddingTop: spacing.md, + paddingBottom: spacing.sm, + }, + headerActions: { + flexDirection: 'row', gap: spacing.sm, }, - track: { - width: 4, - height: 140, - borderRadius: radius.pill, - backgroundColor: colors.glassBorder, + iconButton: { + width: 40, + height: 40, + borderRadius: radius.md, alignItems: 'center', justifyContent: 'center', + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, }, - knob: { - width: 14, - height: 14, + presetRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginHorizontal: spacing.lg, + marginBottom: spacing.md, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + borderRadius: radius.md, + backgroundColor: colors.bgTertiary, + }, + graphWrap: { + flex: 1, + minHeight: 180, + marginHorizontal: spacing.lg, + }, + section: { + marginHorizontal: spacing.lg, + marginTop: spacing.md, + }, + bottomBar: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + paddingHorizontal: spacing.lg, + paddingTop: spacing.md, + paddingBottom: spacing.sm, + }, + preamp: { + flex: 1, + }, + eqToggle: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, borderRadius: radius.pill, - backgroundColor: colors.accent, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, }, - freq: { - color: colors.textSecondary, + eqToggleOn: { + borderColor: colors.accent, + backgroundColor: colors.accentGlow, + }, + sheetTitle: { + marginTop: spacing.xs, + marginBottom: spacing.sm, }, }); diff --git a/src/app/(tabs)/settings.tsx b/src/app/(tabs)/settings.tsx index ef46d8f..a3f84e1 100644 --- a/src/app/(tabs)/settings.tsx +++ b/src/app/(tabs)/settings.tsx @@ -1,9 +1,12 @@ -import { View, Pressable, StyleSheet } from 'react-native'; +import { View, Pressable, ScrollView, StyleSheet, Switch } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Screen } from '@/components/Screen'; import { Text } from '@/components/Text'; +import { EQSlider } from '@/components/eq/EQSlider'; import { colors, radius, spacing } from '@/theme'; import { useSettingsStore } from '@/stores/settingsStore'; +import { useAudioSettingsStore } from '@/stores/audioSettingsStore'; +import type { ReplayGainMode } from '@/audio/normalization'; import type { ArtistGroupingMode } from '@/library/artistGrouping'; const ARTIST_GROUPING_OPTIONS: { mode: ArtistGroupingMode; title: string; description: string }[] = [ @@ -19,59 +22,159 @@ const ARTIST_GROUPING_OPTIONS: { mode: ArtistGroupingMode; title: string; descri }, ]; +const REPLAYGAIN_MODES: { mode: ReplayGainMode; label: string }[] = [ + { mode: 'auto', label: 'Auto' }, + { mode: 'track', label: 'Track' }, + { mode: 'album', label: 'Album' }, +]; + +function ToggleRow({ + title, + description, + value, + onValueChange, +}: { + title: string; + description: string; + value: boolean; + onValueChange: (v: boolean) => void; +}) { + return ( + + + {title} + + {description} + + + + + ); +} + export default function SettingsScreen() { const groupingMode = useSettingsStore((s) => s.artistGroupingMode); const setArtistGroupingMode = useSettingsStore((s) => s.setArtistGroupingMode); + const normalizationEnabled = useAudioSettingsStore((s) => s.normalizationEnabled); + const normalizationTargetLufs = useAudioSettingsStore((s) => s.normalizationTargetLufs); + const replayGainEnabled = useAudioSettingsStore((s) => s.replayGainEnabled); + const replayGainMode = useAudioSettingsStore((s) => s.replayGainMode); + const setNormalizationEnabled = useAudioSettingsStore((s) => s.setNormalizationEnabled); + const setNormalizationTargetLufs = useAudioSettingsStore((s) => s.setNormalizationTargetLufs); + const setReplayGainEnabled = useAudioSettingsStore((s) => s.setReplayGainEnabled); + const setReplayGainMode = useAudioSettingsStore((s) => s.setReplayGainMode); + return ( - - Settings - + + + Settings + - - LIBRARY - - - Artist grouping - - - How tracks are organized into artists in the library. - + + AUDIO + + + void setNormalizationEnabled(v)} + /> + {normalizationEnabled ? ( + + `${Math.round(v)} LUFS`} + onChange={(v) => void setNormalizationTargetLufs(Math.round(v))} + /> + + ) : null} + - - {ARTIST_GROUPING_OPTIONS.map((option) => { - const selected = option.mode === groupingMode; - return ( - void setArtistGroupingMode(option.mode)} - accessibilityRole="radio" - accessibilityState={{ selected }} - > - - - {option.title} - - - {option.description} - - - {selected ? ( - - ) : ( - - )} - - ); - })} - + + void setReplayGainEnabled(v)} + /> + {replayGainEnabled ? ( + + {REPLAYGAIN_MODES.map((m) => { + const selected = m.mode === replayGainMode; + return ( + void setReplayGainMode(m.mode)} + > + + {m.label} + + + ); + })} + + ) : null} + + + + LIBRARY + + + Artist grouping + + + How tracks are organized into artists in the library. + + + + {ARTIST_GROUPING_OPTIONS.map((option) => { + const selected = option.mode === groupingMode; + return ( + void setArtistGroupingMode(option.mode)} + accessibilityRole="radio" + accessibilityState={{ selected }} + > + + + {option.title} + + + {option.description} + + + {selected ? ( + + ) : ( + + )} + + ); + })} + + ); } const styles = StyleSheet.create({ + content: { + paddingBottom: spacing.xxl, + }, heading: { marginTop: spacing.xl, marginBottom: spacing.xxl, @@ -80,6 +183,47 @@ const styles = StyleSheet.create({ letterSpacing: 1, marginBottom: spacing.sm, }, + sectionSpacing: { + marginTop: spacing.xxl, + }, + card: { + borderRadius: radius.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + padding: spacing.lg, + }, + cardSpacing: { + marginTop: spacing.sm, + }, + toggleRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + }, + toggleText: { + flex: 1, + gap: 2, + }, + indent: { + marginTop: spacing.sm, + }, + modeRow: { + flexDirection: 'row', + gap: spacing.sm, + marginTop: spacing.md, + }, + modePill: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, + borderRadius: radius.pill, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + }, + modePillSelected: { + borderColor: colors.accent, + backgroundColor: colors.accentGlow, + }, settingTitle: { marginBottom: spacing.xs, }, diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 3713150..51514cc 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -18,6 +18,9 @@ import { import { usePlaybackSync } from '@/audio/usePlaybackSync'; import { useScopeLifecycle } from '@/scope/useScopeLifecycle'; import { useLibraryStore } from '@/stores/libraryStore'; +import { useEQStore } from '@/stores/eqStore'; +import { useAudioSettingsStore } from '@/stores/audioSettingsStore'; +import { useNormalizationSync } from '@/audio/useNormalizationSync'; import { colors } from '@/theme'; SplashScreen.preventAutoHideAsync(); @@ -34,6 +37,12 @@ function ScopeLifecycle() { return null; } +/** Pushes per-track normalization gain to native on track/settings change. */ +function NormalizationSync() { + useNormalizationSync(); + return null; +} + export default function RootLayout() { const [fontsLoaded] = useFonts({ Inter_400Regular, @@ -51,12 +60,21 @@ export default function RootLayout() { }, [fontsLoaded]); // Eager library init: SQLite open + initial reads are tens of ms, and the - // Library tab + playback adapters get data immediately. + // Library tab + playback adapters get data immediately. EQ + audio settings load + // alongside so the native EQ/gain reflect persisted prefs from the first play. useEffect(() => { useLibraryStore .getState() .initialize() .catch((err) => console.error('[library] init failed', err)); + useEQStore + .getState() + .load() + .catch((err) => console.error('[eq] load failed', err)); + useAudioSettingsStore + .getState() + .load() + .catch((err) => console.error('[audioSettings] load failed', err)); }, []); if (!fontsLoaded) return null; @@ -67,6 +85,7 @@ export default function RootLayout() { + s.scopeMode); const scopeStageVisible = useSettingsStore((s) => s.scopeStageVisible); const setScopeStageVisible = useSettingsStore((s) => s.setScopeStageVisible); + const artistGroupingMode = useSettingsStore((s) => s.artistGroupingMode); const libraryTracks = useLibraryStore((s) => s.tracks); const track = usePlayerStore((s) => s.currentTrack); const playbackState = usePlayerStore((s) => s.playbackState); @@ -194,11 +197,15 @@ export default function NowPlayingScreen() { const source = track?.album?.trim() ? track.album : 'Library'; const shellRight = Math.max(layout.contentPadding, (windowWidth - layout.contentWidth) / 2); const menuTop = insets.top + CONTENT_TOP_PADDING + HEADER_HEIGHT + spacing.xs; - const artistName = track?.artist.trim() ?? ''; const libraryTrack = useMemo( () => (track ? libraryTracks.find((entry) => entry.path === track.path) ?? null : null), [libraryTracks, track] ); + const artistName = track + ? artistGroupingMode === 'fileTags' + ? resolveStrictBrowseArtist(libraryTrack ?? { artist: track.artist, album_artist: track.albumArtist ?? null }) + : resolveCanonicalBrowseArtist(libraryTrack ?? { artist: track.artist, album_artist: track.albumArtist ?? null }) + : ''; const albumKey = track?.albumIdentityKey ?? libraryTrack?.album_identity_key; const navigateToArtist = () => { @@ -454,9 +461,13 @@ export default function NowPlayingScreen() { - + {track.title} - + - + {track.artist} - + - - - - void setScopeStageVisible(!scopeStageVisible)} - accessibilityLabel={scopeStageVisible ? 'Hide visualizer' : 'Show visualizer'} - accessibilityState={{ selected: scopeStageVisible }} - > - - - setQueueOpen(true)} - accessibilityLabel="Queue" - > - - + + + + + void setScopeStageVisible(!scopeStageVisible)} + accessibilityLabel={scopeStageVisible ? 'Hide visualizer' : 'Show visualizer'} + accessibilityState={{ selected: scopeStageVisible }} + > + + + setQueueOpen(true)} + accessibilityLabel="Queue" + > + + + @@ -778,6 +791,8 @@ const styles = StyleSheet.create({ }, trackTitle: { alignSelf: 'stretch', + }, + trackTitleText: { textAlign: 'left', }, inlineActionBtn: { @@ -789,13 +804,13 @@ const styles = StyleSheet.create({ trackMetaRow: { alignSelf: 'stretch', flexDirection: 'row', - flexWrap: 'wrap', + flexWrap: 'nowrap', alignItems: 'center', gap: spacing.sm, marginTop: spacing.xs, }, artistButton: { - flexShrink: 1, + flex: 1, minWidth: 0, }, centered: { @@ -804,9 +819,6 @@ const styles = StyleSheet.create({ artist: { color: colors.accentText, }, - badges: { - flexShrink: 0, - }, spacer: { flex: 1, minHeight: MIN_FLOATING_SPACE, @@ -843,11 +855,22 @@ const styles = StyleSheet.create({ subRow: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-end', - gap: spacing.lg, + justifyContent: 'space-between', + gap: spacing.md, marginTop: SUB_TOP_MARGIN, paddingHorizontal: spacing.sm, }, + subBadges: { + flex: 1, + minWidth: 0, + overflow: 'hidden', + }, + subActions: { + flexDirection: 'row', + alignItems: 'center', + flexShrink: 0, + gap: spacing.lg, + }, subBtn: { width: SUB_BUTTON_SIZE, height: SUB_BUTTON_SIZE, diff --git a/src/audio/autoEQParser.ts b/src/audio/autoEQParser.ts new file mode 100644 index 0000000..8991c91 --- /dev/null +++ b/src/audio/autoEQParser.ts @@ -0,0 +1,68 @@ +// AutoEQ ParametricEQ.txt parser — ported from desktop `src/renderer/utils/autoEQParser.ts`. + +import type { EQBand, EQBandType, EQPreset } from '@/types/audio'; +import { EQ_MAX_BANDS, clampEQFrequency, clampEQGain, clampEQQ, clampPreamp } from './eq'; +import { genEqId } from './eqPresets'; + +const TYPE_MAP: Record = { + PK: 'peaking', + LS: 'lowshelf', + HS: 'highshelf', +}; + +/** + * Parse an AutoEQ ParametricEQ.txt file into an EQPreset. + * + * Format: + * Preamp: -6.2 dB + * Filter 1: ON PK Fc 31 Hz Gain 4.5 dB Q 1.41 + * Filter 2: ON LS Fc 105 Hz Gain -2.1 dB Q 0.71 + */ +export function parseAutoEQ(content: string, filename?: string): EQPreset { + const lines = content + .split('\n') + .map((l) => l.trim()) + .filter(Boolean); + + let preamp = 0; + const bands: EQBand[] = []; + + for (const line of lines) { + const preampMatch = line.match(/^Preamp:\s*([-\d.]+)\s*dB/i); + if (preampMatch) { + preamp = clampPreamp(parseFloat(preampMatch[1])); + continue; + } + + const filterMatch = line.match( + /^Filter\s+\d+:\s*(ON|OFF)\s+(PK|LS|HS)\s+Fc\s+([\d.]+)\s*Hz\s+Gain\s+([-\d.]+)\s*dB\s+Q\s+([\d.]+)/i + ); + if (filterMatch) { + const [, onOff, typeCode, fc, gain, q] = filterMatch; + if (onOff.toUpperCase() === 'OFF') continue; + + bands.push({ + id: genEqId(), + type: TYPE_MAP[typeCode.toUpperCase()] || 'peaking', + frequency: clampEQFrequency(parseFloat(fc)), + gain: clampEQGain(parseFloat(gain)), + Q: clampEQQ(parseFloat(q)), + enabled: true, + }); + } + } + + bands.sort((a, b) => a.frequency - b.frequency); + + const name = filename + ? filename.replace(/^.*[\\/]/, '').replace(/\.[^.]+$/, '') + : 'Imported AutoEQ'; + + return { + id: genEqId(), + name, + preamp, + bands: bands.slice(0, EQ_MAX_BANDS), + isCustom: true, + }; +} diff --git a/src/audio/eq.ts b/src/audio/eq.ts new file mode 100644 index 0000000..e74c553 --- /dev/null +++ b/src/audio/eq.ts @@ -0,0 +1,266 @@ +// Parametric EQ math + helpers — ported from desktop `src/renderer/utils/eq.ts`. +// The biquad cookbook (Audio EQ Cookbook) magnitude math drives the response curve +// in the EQ screen. Coefficients themselves are computed natively (Kotlin) at the +// real stream sample rate — here we only flatten band params for the native bridge. + +import type { EQBand, EQBandType, EQPreset } from '@/types/audio'; + +export const EQ_MIN_GAIN_DB = -12; +export const EQ_MAX_GAIN_DB = 12; +export const EQ_MIN_FREQUENCY = 20; +export const EQ_MAX_FREQUENCY = 20000; +export const EQ_MIN_Q = 0.1; +export const EQ_MAX_Q = 18; +export const EQ_PASS_FILTER_DEFAULT_Q = 0.707; +export const EQ_MAX_BANDS = 10; +export const EQ_MIN_PREAMP_DB = -12; +export const EQ_MAX_PREAMP_DB = 12; +export const EQ_PRESET_VERSION = 1; + +// Ordinals MUST match the Kotlin `EqBandType` enum order in EqBridge.kt. +export const EQ_BAND_TYPE_ORDINAL: Record = { + lowshelf: 0, + peaking: 1, + highshelf: 2, + highpass: 3, + lowpass: 4, +}; + +interface RawEQBand { + type?: unknown; + frequency?: unknown; + gain?: unknown; + Q?: unknown; + enabled?: unknown; +} + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +function coerceFiniteNumber(value: unknown, fallback: number): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +export function clampEQGain(value: number): number { + return clamp(value, EQ_MIN_GAIN_DB, EQ_MAX_GAIN_DB); +} + +export function clampEQFrequency(value: number): number { + return clamp(value, EQ_MIN_FREQUENCY, EQ_MAX_FREQUENCY); +} + +export function clampEQQ(value: number): number { + return clamp(value, EQ_MIN_Q, EQ_MAX_Q); +} + +export function clampPreamp(value: number): number { + return clamp(value, EQ_MIN_PREAMP_DB, EQ_MAX_PREAMP_DB); +} + +export function normalizeEQBandType(value: unknown): EQBandType { + switch (value) { + case 'lowshelf': + case 'peaking': + case 'highshelf': + case 'highpass': + case 'lowpass': + return value; + default: + return 'peaking'; + } +} + +export function isPassEQBandType(type: EQBandType): boolean { + return type === 'highpass' || type === 'lowpass'; +} + +/** Pass filters carry no gain — force it to 0. */ +export function normalizeEQBand(band: T): T { + if (!isPassEQBandType(band.type) || band.gain === 0) { + return band; + } + return { ...band, gain: 0 }; +} + +export function createNormalizedEQBand(rawBand: RawEQBand, id: string): EQBand { + const band: EQBand = { + id, + type: normalizeEQBandType(rawBand.type), + frequency: clampEQFrequency(coerceFiniteNumber(rawBand.frequency, 1000)), + gain: clampEQGain(coerceFiniteNumber(rawBand.gain, 0)), + Q: clampEQQ(coerceFiniteNumber(rawBand.Q, 1.0)), + enabled: rawBand.enabled === undefined ? true : rawBand.enabled !== false, + }; + return normalizeEQBand(band); +} + +export function parseEQPresetData(value: unknown, createId: () => string): EQPreset { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Invalid preset file'); + } + const raw = value as { name?: unknown; preamp?: unknown; bands?: unknown }; + if (typeof raw.name !== 'string' || raw.name.trim().length === 0 || !Array.isArray(raw.bands)) { + throw new Error('Invalid preset file'); + } + return { + id: createId(), + name: raw.name.trim(), + preamp: clampPreamp(coerceFiniteNumber(raw.preamp, 0)), + bands: raw.bands + .slice(0, EQ_MAX_BANDS) + .map((band) => + createNormalizedEQBand( + band && typeof band === 'object' && !Array.isArray(band) ? (band as RawEQBand) : {}, + createId() + ) + ), + isCustom: true, + }; +} + +export function serializeEQPresetData(preset: Pick): { + version: number; + name: string; + preamp: number; + bands: Pick[]; +} { + return { + version: EQ_PRESET_VERSION, + name: preset.name, + preamp: clampPreamp(coerceFiniteNumber(preset.preamp, 0)), + bands: preset.bands.map((b) => { + const n = normalizeEQBand(b); + return { type: n.type, frequency: n.frequency, gain: n.gain, Q: n.Q, enabled: n.enabled }; + }), + }; +} + +// --------------------------------------------------------------------------- +// Response curve magnitude (Audio EQ Cookbook) — for the Skia response curve. +// --------------------------------------------------------------------------- + +export function computeEQFilterMagnitude(band: EQBand, testFreq: number, sampleRate: number): number { + if (sampleRate <= 0) return 0; + + const w0 = (2 * Math.PI * band.frequency) / sampleRate; + const w = (2 * Math.PI * testFreq) / sampleRate; + const A = Math.pow(10, band.gain / 40); + const sinW0 = Math.sin(w0); + const cosW0 = Math.cos(w0); + const alpha = sinW0 / (2 * band.Q); + + let b0 = 1; + let b1 = 0; + let b2 = 0; + let a0 = 1; + let a1 = 0; + let a2 = 0; + + switch (band.type) { + case 'peaking': + b0 = 1 + alpha * A; + b1 = -2 * cosW0; + b2 = 1 - alpha * A; + a0 = 1 + alpha / A; + a1 = -2 * cosW0; + a2 = 1 - alpha / A; + break; + case 'lowshelf': { + const sqrtA = Math.sqrt(A); + b0 = A * (A + 1 - (A - 1) * cosW0 + 2 * sqrtA * alpha); + b1 = 2 * A * (A - 1 - (A + 1) * cosW0); + b2 = A * (A + 1 - (A - 1) * cosW0 - 2 * sqrtA * alpha); + a0 = A + 1 + (A - 1) * cosW0 + 2 * sqrtA * alpha; + a1 = -2 * (A - 1 + (A + 1) * cosW0); + a2 = A + 1 + (A - 1) * cosW0 - 2 * sqrtA * alpha; + break; + } + case 'highshelf': { + const sqrtA = Math.sqrt(A); + b0 = A * (A + 1 + (A - 1) * cosW0 + 2 * sqrtA * alpha); + b1 = -2 * A * (A - 1 + (A + 1) * cosW0); + b2 = A * (A + 1 + (A - 1) * cosW0 - 2 * sqrtA * alpha); + a0 = A + 1 - (A - 1) * cosW0 + 2 * sqrtA * alpha; + a1 = 2 * (A - 1 - (A + 1) * cosW0); + a2 = A + 1 - (A - 1) * cosW0 - 2 * sqrtA * alpha; + break; + } + case 'lowpass': + b0 = (1 - cosW0) / 2; + b1 = 1 - cosW0; + b2 = (1 - cosW0) / 2; + a0 = 1 + alpha; + a1 = -2 * cosW0; + a2 = 1 - alpha; + break; + case 'highpass': + b0 = (1 + cosW0) / 2; + b1 = -(1 + cosW0); + b2 = (1 + cosW0) / 2; + a0 = 1 + alpha; + a1 = -2 * cosW0; + a2 = 1 - alpha; + break; + } + + const cosW = Math.cos(w); + const sinW = Math.sin(w); + const cos2W = Math.cos(2 * w); + const sin2W = Math.sin(2 * w); + + const numReal = b0 / a0 + (b1 / a0) * cosW + (b2 / a0) * cos2W; + const numImag = -(b1 / a0) * sinW - (b2 / a0) * sin2W; + const denReal = 1 + (a1 / a0) * cosW + (a2 / a0) * cos2W; + const denImag = -(a1 / a0) * sinW - (a2 / a0) * sin2W; + + const numMag = Math.sqrt(numReal * numReal + numImag * numImag); + const denMag = Math.sqrt(denReal * denReal + denImag * denImag); + + return 20 * Math.log10(numMag / (denMag + 1e-20)); +} + +/** Sum of per-band magnitudes (dB) at a frequency, skipping disabled bands. */ +export function computeCombinedEQMagnitude( + bands: readonly EQBand[], + testFreq: number, + sampleRate: number +): number { + let totalDb = 0; + for (const band of bands) { + if (band.enabled === false) continue; + totalDb += computeEQFilterMagnitude(band, testFreq, sampleRate); + } + return totalDb; +} + +// --------------------------------------------------------------------------- +// Native bridge encoding. +// --------------------------------------------------------------------------- + +/** dB → linear amplitude (for the preamp gain pushed to native). */ +export function dbToLinear(db: number): number { + return Math.pow(10, db / 20); +} + +/** + * Flatten bands into the flat number[] the native EqBridge consumes: + * 5 values per band — [typeOrdinal, frequency, gain, Q, enabled?1:0]. + * Disabled and pass-normalized bands are encoded as-is; Kotlin computes the + * biquad coefficients at the actual stream sample rate. + */ +export function flattenBandsForNative(bands: readonly EQBand[]): number[] { + const out: number[] = []; + for (const band of bands) { + const n = normalizeEQBand(band); + out.push( + EQ_BAND_TYPE_ORDINAL[n.type], + n.frequency, + n.gain, + n.Q, + n.enabled === false ? 0 : 1 + ); + } + return out; +} diff --git a/src/audio/eqNative.ts b/src/audio/eqNative.ts new file mode 100644 index 0000000..3f40384 --- /dev/null +++ b/src/audio/eqNative.ts @@ -0,0 +1,90 @@ +// Thin, defensive wrapper over the native EQ/gain setters on the AstraScope module. +// Guards every call so a JS bundle running against an older native binary (before the +// M4 native rebuild) degrades to a no-op instead of crashing. + +import { AstraScope } from '../../modules/astra-scope'; + +type NativeEq = { + setEqEnabled?: (enabled: boolean) => void; + setEqPreamp?: (linear: number) => void; + setEqBands?: (params: number[]) => void; + setNormalizationGain?: (linear: number) => void; + setTrackGain?: (url: string, linear: number) => void; + activateTrackGain?: (url: string) => void; + clearTrackGains?: () => void; + setActivePostEq?: (active: boolean) => void; +}; + +const native = AstraScope as unknown as NativeEq; + +export function setEqEnabledNative(enabled: boolean): void { + try { + native.setEqEnabled?.(enabled); + } catch { + /* older native binary — no-op */ + } +} + +export function setEqPreampNative(linear: number): void { + try { + native.setEqPreamp?.(linear); + } catch { + /* no-op */ + } +} + +export function setEqBandsNative(params: number[]): void { + try { + native.setEqBands?.(params); + } catch { + /* no-op */ + } +} + +/** Set the active normalization/ReplayGain gain directly (linear). 1 = unity. */ +export function setNormalizationGainNative(linear: number): void { + try { + native.setNormalizationGain?.(linear); + } catch { + /* no-op */ + } +} + +/** + * Register a queued track's gain by URL so the player switches to it natively at the + * media-item transition (no JS round-trip on track change). + */ +export function setTrackGainNative(url: string, linear: number): void { + try { + native.setTrackGain?.(url, linear); + } catch { + /* no-op */ + } +} + +/** Activate the registered gain for this URL now (current track on mount/settings). */ +export function activateTrackGainNative(url: string): void { + try { + native.activateTrackGain?.(url); + } catch { + /* no-op */ + } +} + +/** Drop all registered per-track gains. */ +export function clearTrackGainsNative(): void { + try { + native.clearTrackGains?.(); + } catch { + /* no-op */ + } +} + +/** Gate the post-EQ tap (true only while the EQ screen is visible). */ +export function setActivePostEqNative(active: boolean): void { + try { + native.setActivePostEq?.(active); + } catch { + /* no-op */ + } +} diff --git a/src/audio/eqPresets.ts b/src/audio/eqPresets.ts new file mode 100644 index 0000000..e07f2fa --- /dev/null +++ b/src/audio/eqPresets.ts @@ -0,0 +1,111 @@ +// Default bands + built-in presets — ported from desktop `src/renderer/stores/eqStore.ts`. +// Each band carries the mobile `enabled` flag (default on). + +import type { EQBand, EQPreset } from '@/types/audio'; + +let idCounter = 0; +/** Monotonic, collision-free id for bands/presets (RN-safe, no crypto needed). */ +export function genEqId(): string { + idCounter += 1; + return `eq-${Date.now().toString(36)}-${idCounter.toString(36)}`; +} + +type BandSeed = Omit & { enabled?: boolean }; + +function mkBand(seed: BandSeed): EQBand { + return { + id: genEqId(), + type: seed.type, + frequency: seed.frequency, + gain: seed.gain, + Q: seed.Q, + enabled: seed.enabled ?? true, + }; +} + +// 5-band default (shelves at the extremes, 3 peaking in between). More bands can +// be added from the EQ screen up to EQ_MAX_BANDS. +export const DEFAULT_BAND_SEEDS: BandSeed[] = [ + { type: 'lowshelf', frequency: 60, gain: 0, Q: 0.707 }, + { type: 'peaking', frequency: 250, gain: 0, Q: 1.0 }, + { type: 'peaking', frequency: 1000, gain: 0, Q: 1.0 }, + { type: 'peaking', frequency: 4000, gain: 0, Q: 1.0 }, + { type: 'highshelf', frequency: 12000, gain: 0, Q: 0.707 }, +]; + +/** Fresh default (flat) bands with new ids. */ +export function createDefaultBands(): EQBand[] { + return DEFAULT_BAND_SEEDS.map(mkBand); +} + +interface PresetSeed { + id: string; + name: string; + preamp: number; + bands: BandSeed[]; +} + +const BUILT_IN_SEEDS: PresetSeed[] = [ + { id: 'flat', name: 'Flat', preamp: 0, bands: DEFAULT_BAND_SEEDS }, + { + id: 'bass-boost', + name: 'Bass Boost', + preamp: -2, + bands: [ + { type: 'lowshelf', frequency: 60, gain: 6, Q: 0.707 }, + { type: 'peaking', frequency: 150, gain: 4, Q: 1.0 }, + { type: 'peaking', frequency: 400, gain: 1, Q: 1.0 }, + { type: 'peaking', frequency: 1000, gain: 0, Q: 1.0 }, + { type: 'highshelf', frequency: 12000, gain: 0, Q: 0.707 }, + ], + }, + { + id: 'treble-boost', + name: 'Treble Boost', + preamp: -2, + bands: [ + { type: 'lowshelf', frequency: 60, gain: 0, Q: 0.707 }, + { type: 'peaking', frequency: 1000, gain: 0, Q: 1.0 }, + { type: 'peaking', frequency: 4000, gain: 3, Q: 1.0 }, + { type: 'peaking', frequency: 8000, gain: 5, Q: 1.0 }, + { type: 'highshelf', frequency: 12000, gain: 6, Q: 0.707 }, + ], + }, + { + id: 'vocal', + name: 'Vocal', + preamp: -1, + bands: [ + { type: 'lowshelf', frequency: 80, gain: -2, Q: 0.707 }, + { type: 'peaking', frequency: 250, gain: 1, Q: 1.0 }, + { type: 'peaking', frequency: 1500, gain: 4, Q: 1.2 }, + { type: 'peaking', frequency: 4000, gain: 3, Q: 1.0 }, + { type: 'highshelf', frequency: 12000, gain: 1, Q: 0.707 }, + ], + }, + { + id: 'loudness', + name: 'Loudness', + preamp: -3, + bands: [ + { type: 'lowshelf', frequency: 60, gain: 5, Q: 0.707 }, + { type: 'peaking', frequency: 400, gain: 2, Q: 1.0 }, + { type: 'peaking', frequency: 1000, gain: -1, Q: 1.0 }, + { type: 'peaking', frequency: 4000, gain: 2, Q: 1.0 }, + { type: 'highshelf', frequency: 12000, gain: 5, Q: 0.707 }, + ], + }, +]; + +/** Built-in presets with fresh band ids (call once at store init). */ +export function createBuiltInPresets(): EQPreset[] { + return BUILT_IN_SEEDS.map((seed) => ({ + id: seed.id, + name: seed.name, + preamp: seed.preamp, + bands: seed.bands.map(mkBand), + isCustom: false, + })); +} + +export const FLAT_PRESET_ID = 'flat'; diff --git a/src/audio/normalization.ts b/src/audio/normalization.ts new file mode 100644 index 0000000..358bf07 --- /dev/null +++ b/src/audio/normalization.ts @@ -0,0 +1,133 @@ +// Per-track normalization gain — ported from desktop AudioEngine +// `resolveStaticNormalizationGain` / `resolveGainStateForAnalysis`. +// +// Precedence (desktop-match, locked with the user): +// normalization off -> unity +// ReplayGain on + tag present -> use the tag (clamped) +// else -> targetLufs - scannedLUFS (clamped) +// then back off so peak * linearGain <= 0.98 (peak limiter). + +export const NORM_MIN_GAIN_DB = -18; +export const NORM_MAX_GAIN_DB = 6; +export const NORM_PEAK_CEILING_LINEAR = 0.98; +export const DEFAULT_TARGET_LUFS = -12; + +export type ReplayGainMode = 'auto' | 'track' | 'album'; +export type NormalizationMode = 'off' | 'replaygain' | 'normalization'; + +export interface LoudnessFacts { + loudnessLufs: number | null; + samplePeak: number | null; + replayGainTrackDb: number | null; + replayGainAlbumDb: number | null; + replayGainTrackPeak: number | null; + replayGainAlbumPeak: number | null; +} + +export interface NormalizationSettings { + enabled: boolean; + targetLufs: number; + replayGainEnabled: boolean; + replayGainMode: ReplayGainMode; +} + +export interface ResolvedGain { + gainDb: number; + linearGain: number; + mode: NormalizationMode; + peakLimited: boolean; +} + +const UNITY: ResolvedGain = { gainDb: 0, linearGain: 1, mode: 'off', peakLimited: false }; + +function clamp(v: number, min: number, max: number): number { + return Math.max(min, Math.min(max, v)); +} + +function dbToLinear(db: number): number { + return Math.pow(10, db / 20); +} + +interface PickedReplayGain { + gainDb: number; + /** The peak matching the chosen gain (track gain -> track peak), for clip-limiting. */ + peak: number | null; +} + +/** Pick the ReplayGain gain+peak to use for the given mode, or null if unavailable. */ +function pickReplayGain(facts: LoudnessFacts, mode: ReplayGainMode): PickedReplayGain | null { + const useTrack: PickedReplayGain | null = + facts.replayGainTrackDb != null && Number.isFinite(facts.replayGainTrackDb) + ? { gainDb: facts.replayGainTrackDb, peak: facts.replayGainTrackPeak } + : null; + const useAlbum: PickedReplayGain | null = + facts.replayGainAlbumDb != null && Number.isFinite(facts.replayGainAlbumDb) + ? { gainDb: facts.replayGainAlbumDb, peak: facts.replayGainAlbumPeak } + : null; + switch (mode) { + case 'track': + return useTrack ?? useAlbum; + case 'album': + return useAlbum ?? useTrack; + case 'auto': + default: + // Album gain keeps relative loudness within an album; prefer it when present. + return useAlbum ?? useTrack; + } +} + +/** + * Whether ReplayGain alone can normalize this track (RG on + a usable tag present), + * so callers can skip the expensive loudness decode for tagged libraries. + */ +export function hasUsableReplayGain(facts: LoudnessFacts, settings: NormalizationSettings): boolean { + return settings.replayGainEnabled && pickReplayGain(facts, settings.replayGainMode) != null; +} + +/** Apply the peak ceiling to a candidate gain. */ +function applyPeakLimit( + gainDb: number, + samplePeak: number | null +): { gainDb: number; peakLimited: boolean } { + if (samplePeak == null || samplePeak <= 0) return { gainDb, peakLimited: false }; + const linear = dbToLinear(gainDb); + if (samplePeak * linear <= NORM_PEAK_CEILING_LINEAR) return { gainDb, peakLimited: false }; + const maxLinear = NORM_PEAK_CEILING_LINEAR / samplePeak; + const limitedDb = 20 * Math.log10(maxLinear); + return { gainDb: limitedDb, peakLimited: true }; +} + +export function resolveNormalizationGain( + facts: LoudnessFacts, + settings: NormalizationSettings +): ResolvedGain { + if (!settings.enabled) return UNITY; + + let gainDb: number; + let mode: NormalizationMode; + // Peak used for clip-limiting: the RG tag's own peak in RG mode (falling back to the + // measured sample peak), or the measured peak for loudness normalization. + let peak: number | null; + + const rg = settings.replayGainEnabled ? pickReplayGain(facts, settings.replayGainMode) : null; + if (rg != null) { + gainDb = clamp(rg.gainDb, NORM_MIN_GAIN_DB, NORM_MAX_GAIN_DB); + mode = 'replaygain'; + peak = rg.peak ?? facts.samplePeak; + } else if (facts.loudnessLufs != null && Number.isFinite(facts.loudnessLufs)) { + gainDb = clamp(settings.targetLufs - facts.loudnessLufs, NORM_MIN_GAIN_DB, NORM_MAX_GAIN_DB); + mode = 'normalization'; + peak = facts.samplePeak; + } else { + // Enabled but nothing measured yet — unity until analysis backfills. + return { gainDb: 0, linearGain: 1, mode: 'normalization', peakLimited: false }; + } + + const limited = applyPeakLimit(gainDb, peak); + return { + gainDb: limited.gainDb, + linearGain: dbToLinear(limited.gainDb), + mode, + peakLimited: limited.peakLimited, + }; +} diff --git a/src/audio/trackAnalysis.ts b/src/audio/trackAnalysis.ts new file mode 100644 index 0000000..e764f1b --- /dev/null +++ b/src/audio/trackAnalysis.ts @@ -0,0 +1,100 @@ +// Per-track normalization facts: ReplayGain tags (cheap, container-only) + measured +// integrated LUFS / sample peak (a decode, only when ReplayGain can't cover the track). +// +// ensureTrackLoudness is the single deduped entry point used by the normalization sync +// (current track + queue prefetch). It reads ReplayGain tags once per track, and only +// falls back to the expensive loudness decode when ReplayGain is off or absent — so a +// fully tagged library normalizes with no decoding at all. + +import { AstraLibraryScanner } from '../../modules/astra-library-scanner'; +import type { LibraryDatabase } from '@/db/database'; +import { openLibraryDb } from '@/db/database'; +import { getTrackLoudness, setTrackLoudness, setTrackReplayGain, type TrackLoudness } from '@/db/queries'; +import { hasUsableReplayGain, type LoudnessFacts } from '@/audio/normalization'; +import { useAudioSettingsStore } from '@/stores/audioSettingsStore'; + +function factsFromRow(row: TrackLoudness | null): LoudnessFacts { + return { + loudnessLufs: row?.loudness_lufs ?? null, + samplePeak: row?.sample_peak ?? null, + replayGainTrackDb: row?.replay_gain_track_db ?? null, + replayGainAlbumDb: row?.replay_gain_album_db ?? null, + replayGainTrackPeak: row?.replay_gain_track_peak ?? null, + replayGainAlbumPeak: row?.replay_gain_album_peak ?? null, + }; +} + +/** + * Measure + store integrated loudness + sample peak for one track (always + * re-measures). The decode is the expensive part; failures leave loudness NULL. + */ +export async function measureAndStoreLoudness( + db: LibraryDatabase, + path: string +): Promise<{ lufs: number | null; peak: number | null }> { + try { + const res = await AstraLibraryScanner.measureLoudness(path); + const lufs = res?.lufs ?? null; + const peak = res?.peak ?? null; + await setTrackLoudness(db, path, lufs, peak).catch(() => {}); + return { lufs, peak }; + } catch { + return { lufs: null, peak: null }; + } +} + +const inflight = new Map>(); + +/** + * Loudness facts for a track, reading ReplayGain tags and decoding only as needed + * (deduped by path). Cheap when already analyzed (single DB read). The normalization + * sync uses this so tracks from a pre-M4 library still normalize before a full rescan. + */ +export function ensureTrackLoudness(path: string): Promise { + const existing = inflight.get(path); + if (existing) return existing; + const task = run(path).finally(() => inflight.delete(path)); + inflight.set(path, task); + return task; +} + +async function run(path: string): Promise { + const db = await openLibraryDb(); + const row = await getTrackLoudness(db, path); + let facts = factsFromRow(row); + + // 1. Read ReplayGain tags once per track (container-only, no decode). Decoupled + // from loudness so a track measured before ReplayGain was enabled still picks + // up its tags; rg_scanned stays unset on failure so it retries next touch. + if (!row || row.rg_scanned !== 1) { + try { + const rg = await AstraLibraryScanner.readReplayGain(path); + await setTrackReplayGain(db, path, { + trackGainDb: rg.trackGainDb, + albumGainDb: rg.albumGainDb, + trackPeak: rg.trackPeak, + albumPeak: rg.albumPeak, + }).catch(() => {}); + facts = { + ...facts, + replayGainTrackDb: rg.trackGainDb, + replayGainAlbumDb: rg.albumGainDb, + replayGainTrackPeak: rg.trackPeak, + replayGainAlbumPeak: rg.albumPeak, + }; + } catch { + /* tag read failed — fall through to a loudness measure */ + } + } + + // 2. Loudness already measured — nothing more to do. + if (facts.loudnessLufs != null) return facts; + + // 3. ReplayGain alone can normalize this track — skip the expensive decode. + const settings = useAudioSettingsStore.getState().asNormalizationSettings(); + if (hasUsableReplayGain(facts, settings)) return facts; + + // 4. Otherwise measure loudness now (decode) and merge it in. + const measured = await measureAndStoreLoudness(db, path); + return { ...facts, loudnessLufs: measured.lufs, samplePeak: measured.peak }; +} diff --git a/src/audio/useNormalizationSync.ts b/src/audio/useNormalizationSync.ts new file mode 100644 index 0000000..bd1abcf --- /dev/null +++ b/src/audio/useNormalizationSync.ts @@ -0,0 +1,142 @@ +// Owns per-track normalization gain. It reads each track's loudness facts from +// SQLite, resolves the gain, and registers it natively keyed by URL — for the current +// track AND the next few queued tracks. The player then swaps to the matching gain +// natively at the real media-item transition (no JS round-trip on track change). The +// current track is also activated directly here, since on mount / settings change no +// transition fires. Renders nothing — mount once near the root. + +import { useEffect } from 'react'; +import { usePlayerStore } from '@/stores/playerStore'; +import { useQueueStore } from '@/stores/queueStore'; +import { useAudioSettingsStore } from '@/stores/audioSettingsStore'; +import { resolveNormalizationGain, type LoudnessFacts } from '@/audio/normalization'; +import { ensureTrackLoudness } from '@/audio/trackAnalysis'; +import { + setNormalizationGainNative, + setTrackGainNative, + activateTrackGainNative, +} from '@/audio/eqNative'; +import { useScopeStore } from '@/scope/scopeStore'; +import { computeOscilloscopeGain, DEFAULT_OSC_GAIN } from '@/scope/oscilloscopeGain'; + +const EMPTY_FACTS: LoudnessFacts = { + loudnessLufs: null, + samplePeak: null, + replayGainTrackDb: null, + replayGainAlbumDb: null, + replayGainTrackPeak: null, + replayGainAlbumPeak: null, +}; + +// How many upcoming queue tracks to pre-measure. Bounded work (native decode +// concurrency is capped at 2); covers songs queued a few positions ahead. +const PREFETCH_AHEAD = 5; + +export function useNormalizationSync(): void { + useEffect(() => { + let cancelled = false; + + async function recompute(): Promise { + const path = usePlayerStore.getState().currentTrack?.path ?? null; + const settings = useAudioSettingsStore.getState().asNormalizationSettings(); + if (!path) { + setNormalizationGainNative(1); + useScopeStore.getState().setOscGain(DEFAULT_OSC_GAIN); + return; + } + + // ensureTrackLoudness is cheap when already analyzed (single DB read) and + // decodes+stores on a miss (lazy backfill for pre-scan tracks). + let facts = EMPTY_FACTS; + try { + facts = await ensureTrackLoudness(path); + if (cancelled) return; + // Track changed during the await — let the newer recompute win. + if (usePlayerStore.getState().currentTrack?.path !== path) return; + } catch { + /* fall back to unity via EMPTY_FACTS */ + } + + const resolved = resolveNormalizationGain(facts, settings); + // Seed the native map (so transitioning back to this track picks it up) and make + // it active now (mount / settings change fire no media-item transition). + setTrackGainNative(path, resolved.linearGain); + activateTrackGainNative(path); + + // Pick the oscilloscope's per-track display gain from the track's peak and the + // gain we just applied (the scope tap is post-normalization). Held constant for + // the whole track, so dynamics within the song are preserved. + const basePeak = + facts.samplePeak ?? facts.replayGainTrackPeak ?? facts.replayGainAlbumPeak ?? null; + useScopeStore.getState().setOscGain(computeOscilloscopeGain(basePeak, resolved.linearGain)); + } + + // Warm the next several upcoming tracks' loudness while the current one plays, and + // register each one's resolved gain natively by URL — so when the player advances, + // the gain is already in the map and gets applied at the transition with no JS in + // the loop. Looking a few ahead (not just the immediate next) means a song added + // several positions back is still measured + registered with plenty of lead time. + // Derived from the queue mirror, so it re-runs on reorder / add-next / advance. + // Deduped + DB-cached + native-semaphore-capped, so it stays cheap and gentle. + function prefetchUpcoming(): void { + const { tracks, activeIndex } = useQueueStore.getState(); + if (activeIndex < 0) return; + const settings = useAudioSettingsStore.getState().asNormalizationSettings(); + for (let i = 1; i <= PREFETCH_AHEAD; i++) { + const url = tracks[activeIndex + i]?.url; + if (typeof url !== 'string' || url.length === 0) continue; + void ensureTrackLoudness(url) + .then((facts) => { + if (cancelled) return; + const resolved = resolveNormalizationGain(facts, settings); + setTrackGainNative(url, resolved.linearGain); + }) + .catch(() => { + /* leave unregistered — defaults to unity at the transition */ + }); + } + } + + // The queue can change rapidly (drag-reorder); coalesce re-warms. + let prefetchTimer: ReturnType | null = null; + function schedulePrefetch(): void { + if (prefetchTimer) clearTimeout(prefetchTimer); + prefetchTimer = setTimeout(() => { + prefetchTimer = null; + prefetchUpcoming(); + }, 250); + } + + const unsubTrack = usePlayerStore.subscribe((state, prev) => { + if (state.currentTrack?.path !== prev.currentTrack?.path) void recompute(); + }); + const unsubQueue = useQueueStore.subscribe((state, prev) => { + // Re-warm when the upcoming order changes (reorder, add-next, remove, advance). + if (state.tracks !== prev.tracks || state.activeIndex !== prev.activeIndex) { + schedulePrefetch(); + } + }); + const unsubSettings = useAudioSettingsStore.subscribe((state, prev) => { + if ( + state.normalizationEnabled !== prev.normalizationEnabled || + state.normalizationTargetLufs !== prev.normalizationTargetLufs || + state.replayGainEnabled !== prev.replayGainEnabled || + state.replayGainMode !== prev.replayGainMode + ) { + void recompute(); + // Upcoming tracks' gains depend on the same settings — re-register them. + schedulePrefetch(); + } + }); + void recompute(); + prefetchUpcoming(); + + return () => { + cancelled = true; + if (prefetchTimer) clearTimeout(prefetchTimer); + unsubTrack(); + unsubQueue(); + unsubSettings(); + }; + }, []); +} diff --git a/src/components/FormatBadge.tsx b/src/components/FormatBadge.tsx index 192536a..ab611e6 100644 --- a/src/components/FormatBadge.tsx +++ b/src/components/FormatBadge.tsx @@ -20,8 +20,10 @@ export function Badge({ label }: { label: string }) { */ export function FormatBadges({ track, + wrap = true, }: { track: Pick; + wrap?: boolean; }) { const labels: string[] = []; if (track.format) labels.push(track.format.toUpperCase()); @@ -31,7 +33,7 @@ export function FormatBadges({ if (labels.length === 0) return null; return ( - + {labels.map((label) => ( ))} @@ -45,6 +47,9 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', gap: spacing.xs, }, + rowNoWrap: { + flexWrap: 'nowrap', + }, badge: { backgroundColor: colors.glassBg, borderColor: colors.glassBorder, diff --git a/src/components/MarqueeText.tsx b/src/components/MarqueeText.tsx new file mode 100644 index 0000000..a146f2c --- /dev/null +++ b/src/components/MarqueeText.tsx @@ -0,0 +1,137 @@ +import { useEffect, useState } from 'react'; +import { + StyleSheet, + View, + type LayoutChangeEvent, + type StyleProp, + type TextStyle, + type ViewStyle, +} from 'react-native'; +import type { TextLayoutEvent } from 'react-native/Libraries/Types/CoreEventTypes'; +import Animated, { + Easing, + cancelAnimation, + useAnimatedStyle, + useSharedValue, + withDelay, + withRepeat, + withSequence, + withTiming, +} from 'react-native-reanimated'; +import { Text } from './Text'; + +type TextVariant = 'title' | 'heading' | 'body' | 'label' | 'caption' | 'mono'; + +const DEFAULT_DELAY_MS = 900; +const DEFAULT_HOLD_MS = 900; +const DEFAULT_SPEED_PX_PER_SECOND = 28; +const MIN_DURATION_MS = 1600; +const MEASURE_WIDTH = 10000; + +interface MarqueeTextProps { + children: string; + variant?: TextVariant; + color?: string; + style?: StyleProp; + containerStyle?: StyleProp; + delayMs?: number; + holdMs?: number; + speedPxPerSecond?: number; +} + +export function MarqueeText({ + children, + variant = 'body', + color, + style, + containerStyle, + delayMs = DEFAULT_DELAY_MS, + holdMs = DEFAULT_HOLD_MS, + speedPxPerSecond = DEFAULT_SPEED_PX_PER_SECOND, +}: MarqueeTextProps) { + const [containerWidth, setContainerWidth] = useState(0); + const [textWidth, setTextWidth] = useState(0); + const offset = useSharedValue(0); + const overflowDistance = Math.max(0, Math.ceil(textWidth - containerWidth)); + + useEffect(() => { + cancelAnimation(offset); + offset.value = 0; + + if (overflowDistance <= 1) return; + + const duration = Math.max( + MIN_DURATION_MS, + Math.round((overflowDistance / speedPxPerSecond) * 1000) + ); + offset.value = withDelay( + delayMs, + withRepeat( + withSequence( + withTiming(-overflowDistance, { duration, easing: Easing.linear }), + withDelay(holdMs, withTiming(-overflowDistance, { duration: 0 })), + withTiming(0, { duration, easing: Easing.linear }), + withDelay(holdMs, withTiming(0, { duration: 0 })) + ), + -1, + false + ) + ); + }, [delayMs, holdMs, offset, overflowDistance, speedPxPerSecond]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: offset.value }], + })); + + const handleContainerLayout = (event: LayoutChangeEvent) => { + setContainerWidth(event.nativeEvent.layout.width); + }; + + const handleTextLayout = (event: TextLayoutEvent) => { + const measuredWidth = Math.ceil(event.nativeEvent.lines[0]?.width ?? 0); + setTextWidth((current) => (Math.abs(current - measuredWidth) > 1 ? measuredWidth : current)); + }; + + return ( + + 0 ? { width: textWidth } : null, animatedStyle]} + > + + {children} + + + + {children} + + + ); +} + +const styles = StyleSheet.create({ + container: { + overflow: 'hidden', + }, + content: { + alignSelf: 'flex-start', + }, + measure: { + position: 'absolute', + width: MEASURE_WIDTH, + opacity: 0, + }, +}); + +export default MarqueeText; diff --git a/src/components/OscilloscopeWave.tsx b/src/components/OscilloscopeWave.tsx index f29242c..6f60686 100644 --- a/src/components/OscilloscopeWave.tsx +++ b/src/components/OscilloscopeWave.tsx @@ -8,6 +8,8 @@ import { type SkPicture, } from '@shopify/react-native-skia'; import { AstraScope, OSCILLOSCOPE_POINTS } from '../../modules/astra-scope'; +import { useScopeStore } from '@/scope/scopeStore'; +import { DEFAULT_OSC_GAIN } from '@/scope/oscilloscopeGain'; import { colors } from '@/theme'; interface OscilloscopeWaveProps { @@ -25,7 +27,6 @@ type SkiaViewApiShape = { requestRedraw: (nativeId: number) => void; }; -const VISUAL_GAIN = 1.8; const values = new Float32Array(OSCILLOSCOPE_POINTS); function skiaViewApi(): SkiaViewApiShape | null { @@ -58,7 +59,8 @@ function buildPicture( height: number, color: string, lineWidth: number, - glow: boolean + glow: boolean, + gain: number ): SkPicture { const recorder = Skia.PictureRecorder(); const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, width, height)); @@ -70,7 +72,9 @@ function buildPicture( const amp = mid - lineWidth; const xAt = (i: number) => (i / (n - 1)) * width; const yAt = (i: number) => { - let v = samples[i] * VISUAL_GAIN; + let v = samples[i] * gain; + // Per-track gain targets ~85% of full scale, so this only catches the rare + // intra-track peak that runs a touch hotter than the analyzed sample peak. if (v < -1) v = -1; else if (v > 1) v = 1; return mid - v * amp; @@ -94,6 +98,10 @@ function buildPicture( * Imperative oscilloscope renderer. This mirrors desktop/prism's hot path: * a frame loop pulls native scope data and draws directly into a canvas-like * surface instead of routing each frame through React reconciliation. + * + * Amplitude uses a per-track display gain (scopeStore.oscGain, set once per track by + * useNormalizationSync) — read fresh each frame so it tracks song changes, but held + * constant within a track so the music's own dynamics are preserved. */ export function OscilloscopeWave({ active, @@ -106,7 +114,17 @@ export function OscilloscopeWave({ }: OscilloscopeWaveProps) { const viewRef = useRef(null); const initialPicture = useMemo( - () => buildPicture(values, values.length, Math.max(1, width), Math.max(1, height), color, lineWidth, glow), + () => + buildPicture( + values, + values.length, + Math.max(1, width), + Math.max(1, height), + color, + lineWidth, + glow, + DEFAULT_OSC_GAIN + ), [color, glow, height, lineWidth, width] ); @@ -119,7 +137,8 @@ export function OscilloscopeWave({ let raf = 0; const draw = (sampleCount: number) => { - const picture = buildPicture(values, sampleCount, width, height, color, lineWidth, glow); + const gain = useScopeStore.getState().oscGain; + const picture = buildPicture(values, sampleCount, width, height, color, lineWidth, glow, gain); api.setJsiProperty(view.nativeId, 'picture', picture); api.requestRedraw(view.nativeId); }; diff --git a/src/components/SpectrumCurve.tsx b/src/components/SpectrumCurve.tsx index 1073696..9e7fbc2 100644 --- a/src/components/SpectrumCurve.tsx +++ b/src/components/SpectrumCurve.tsx @@ -18,6 +18,8 @@ interface SpectrumCurveProps { height: number; /** Pull native spectrum frames while active, bypassing React per-frame state. */ active?: boolean; + /** Which native tap to pull from. 'post' is the post-EQ ring (EQ screen). */ + source?: 'pre' | 'post'; /** Number of render points when active. Defaults to one point per rendered pixel. */ pointCount?: number; /** Active render cadence. 0 means display-sync; 32 keeps the mini-player battery-friendly. */ @@ -292,6 +294,7 @@ export function SpectrumCurve({ width, height, active = false, + source = 'pre', pointCount, frameMs = MINI_FRAME_MS, analysisFrameMs, @@ -391,7 +394,11 @@ export function SpectrumCurve({ raf = requestAnimationFrame(tick); if (analysisThreshold <= 0 || t - lastAnalysis >= analysisThreshold) { lastAnalysis = t; - if (AstraScope.getSpectrumFrame(spectrumBins) > 0) { + const got = + source === 'post' + ? AstraScope.getSpectrumFramePostEq(spectrumBins) + : AstraScope.getSpectrumFrame(spectrumBins); + if (got > 0) { writeSpectrumPoints(spectrumBins, renderValues, pointOptions); hasNewFrame = true; } @@ -425,6 +432,7 @@ export function SpectrumCurve({ lineOpacity, lineWidth, resolvedPointCount, + source, tiltDbPerOctave, width, ]); diff --git a/src/components/eq/BandDetailPanel.tsx b/src/components/eq/BandDetailPanel.tsx new file mode 100644 index 0000000..d9c774f --- /dev/null +++ b/src/components/eq/BandDetailPanel.tsx @@ -0,0 +1,125 @@ +import { Pressable, StyleSheet, Switch, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { Text } from '@/components/Text'; +import { colors, radius, spacing } from '@/theme'; +import type { EQBand } from '@/types/audio'; +import { EQ_MAX_FREQUENCY, EQ_MAX_GAIN_DB, EQ_MAX_Q, EQ_MIN_FREQUENCY, EQ_MIN_Q, isPassEQBandType } from '@/audio/eq'; +import { EQSlider } from './EQSlider'; +import { BAND_TYPE_LABEL, formatFreq, formatGain } from './format'; + +interface BandDetailPanelProps { + band: EQBand | null; + bandNumber: number; + onUpdate: (updates: Partial) => void; + /** Open the filter-type picker (the sheet lives at the screen root). */ + onEditType: () => void; + /** Open the exact value editor (the sheet lives at the screen root). */ + onEditValue: (value: EQEditableValue) => void; +} + +export type EQEditableValue = 'frequency' | 'gain' | 'Q'; + +/** "Band N" + type dropdown + On toggle + Frequency / Gain / Q sliders. */ +export function BandDetailPanel({ band, bandNumber, onUpdate, onEditType, onEditValue }: BandDetailPanelProps) { + if (!band) { + return ( + + + Select a band to edit. + + + ); + } + + const isPass = isPassEQBandType(band.type); + + return ( + + + Band {bandNumber} + + + {BAND_TYPE_LABEL[band.type]} + + + + + {band.enabled ? 'On' : 'Off'} + onUpdate({ enabled })} + trackColor={{ false: colors.glassBorder, true: colors.accent }} + thumbColor={colors.textPrimary} + /> + + + + `${formatFreq(v)} Hz`} + onChange={(v) => onUpdate({ frequency: v })} + onValuePress={() => onEditValue('frequency')} + /> + `${formatGain(v)} dB`} + onChange={(v) => onUpdate({ gain: v })} + onValuePress={() => onEditValue('gain')} + disabled={isPass} + /> + v.toFixed(2)} + onChange={(v) => onUpdate({ Q: v })} + onValuePress={() => onEditValue('Q')} + /> + + ); +} + +const styles = StyleSheet.create({ + card: { + borderRadius: radius.lg, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + padding: spacing.lg, + gap: spacing.xs, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + marginBottom: spacing.sm, + }, + typeButton: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: radius.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + }, + toggle: { + marginLeft: 'auto', + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, +}); + +export default BandDetailPanel; diff --git a/src/components/eq/BandStrip.tsx b/src/components/eq/BandStrip.tsx new file mode 100644 index 0000000..48d6444 --- /dev/null +++ b/src/components/eq/BandStrip.tsx @@ -0,0 +1,87 @@ +import { Pressable, ScrollView, StyleSheet } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { Text } from '@/components/Text'; +import { colors, radius, spacing } from '@/theme'; +import type { EQBand } from '@/types/audio'; +import { formatFreq, formatGain, gainColor } from './format'; + +interface BandStripProps { + bands: EQBand[]; + activeBandId: string | null; + canAdd: boolean; + onSelect: (id: string) => void; + onAdd: () => void; +} + +/** Horizontal strip of per-band cells (freq + gain) + a trailing "+" add cell. */ +export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: BandStripProps) { + return ( + + {bands.map((band) => { + const isActive = band.id === activeBandId; + return ( + onSelect(band.id)} + style={[styles.cell, isActive && styles.cellActive]} + > + + {formatFreq(band.frequency)} + + + {formatGain(band.gain)} + + + ); + })} + {canAdd ? ( + + + + ) : null} + + ); +} + +const styles = StyleSheet.create({ + content: { + gap: spacing.sm, + paddingVertical: spacing.xs, + }, + cell: { + minWidth: 66, + alignItems: 'center', + gap: 2, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + borderRadius: radius.md, + backgroundColor: colors.glassBg, + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'transparent', + }, + cellActive: { + borderColor: colors.accent, + backgroundColor: colors.glassHighlight, + }, + addCell: { + justifyContent: 'center', + borderColor: colors.glassBorder, + borderStyle: 'dashed', + minWidth: 52, + }, + freq: { + color: colors.textSecondary, + }, + gain: { + fontSize: 15, + }, +}); + +export default BandStrip; diff --git a/src/components/eq/EQGraph.tsx b/src/components/eq/EQGraph.tsx new file mode 100644 index 0000000..8bbcae0 --- /dev/null +++ b/src/components/eq/EQGraph.tsx @@ -0,0 +1,330 @@ +import { useMemo, useRef, useState } from 'react'; +import { + View, + StyleSheet, + type GestureResponderEvent, + type LayoutChangeEvent, +} from 'react-native'; +import { + Canvas, + Circle, + DashPathEffect, + Group, + Path, + Skia, + type SkPath, +} from '@shopify/react-native-skia'; +import { Text } from '@/components/Text'; +import { SpectrumCurve } from '@/components/SpectrumCurve'; +import { colors } from '@/theme'; +import type { EQBand } from '@/types/audio'; +import { + FREQ_TICKS, + buildResponseFill, + buildResponsePath, + freqToX, + gainToY, + xToFreq, + yToGain, +} from './eqGraphMath'; + +const HIT_RADIUS = 34; +const NODE_R = 13; + +interface EQGraphProps { + bands: EQBand[]; + activeBandId: string | null; + enabled: boolean; + /** Pull the live post-EQ spectrum behind the curve. */ + spectrumActive: boolean; + onSelectBand: (id: string) => void; + onChangeBand: (id: string, updates: { frequency: number; gain: number }) => void; +} + +/** + * The EQ response graph: a live post-EQ spectrum behind a draggable response curve + * with one numbered node per band. Skia draws the curve/grid/nodes; a transparent + * RN responder maps touches to the nearest node and drags it (x → frequency, y → + * gain). Q is edited from the detail panel, not the curve. + */ +export function EQGraph({ + bands, + activeBandId, + enabled, + spectrumActive, + onSelectBand, + onChangeBand, +}: EQGraphProps) { + const [size, setSize] = useStableSize(); + const width = size.width; + const height = size.height; + + // Anchor the grabbed node + grant page coords; move by absolute page deltas + // (clamped to the graph) so veering off-bounds can't snap to a corner. + const dragRef = useRef<{ + id: string; + pageX: number; + pageY: number; + startX: number; + startY: number; + } | null>(null); + + const linePath = useMemo( + () => buildResponsePath(bands, width, height), + [bands, width, height] + ); + const fillPath = useMemo( + () => buildResponseFill(linePath, width, height), + [linePath, width, height] + ); + + const onLayout = (e: LayoutChangeEvent) => { + setSize({ width: e.nativeEvent.layout.width, height: e.nativeEvent.layout.height }); + }; + + const nearestBandId = (x: number, y: number): string | null => { + let best: string | null = null; + let bestDist = HIT_RADIUS * HIT_RADIUS; + for (const band of bands) { + const bx = freqToX(band.frequency, width); + const by = gainToY(band.gain, height); + const d = (bx - x) ** 2 + (by - y) ** 2; + if (d <= bestDist) { + bestDist = d; + best = band.id; + } + } + return best; + }; + + const handleGrant = (e: GestureResponderEvent) => { + const { locationX, locationY, pageX, pageY } = e.nativeEvent; + const id = nearestBandId(locationX, locationY); + if (!id) { + dragRef.current = null; + return; + } + const band = bands.find((b) => b.id === id); + if (!band) return; + dragRef.current = { + id, + pageX, + pageY, + startX: freqToX(band.frequency, width), + startY: gainToY(band.gain, height), + }; + onSelectBand(id); + }; + + const handleMove = (e: GestureResponderEvent) => { + const d = dragRef.current; + if (!d) return; + const { pageX, pageY } = e.nativeEvent; + const nx = Math.max(0, Math.min(width, d.startX + (pageX - d.pageX))); + const ny = Math.max(0, Math.min(height, d.startY + (pageY - d.pageY))); + onChangeBand(d.id, { frequency: xToFreq(nx, width), gain: yToGain(ny, height) }); + }; + + const endDrag = () => { + dragRef.current = null; + }; + + const curveColor = enabled ? colors.accent : colors.textTertiary; + const centerY = height / 2; + const yPlus6 = gainToY(6, height); + const yMinus6 = gainToY(-6, height); + + return ( + + {width > 0 && height > 0 ? ( + <> + {/* Live post-EQ spectrum behind the curve. */} + + + + + + {/* Grid: ±6 dB lines + dashed 0 dB centerline. */} + + + + + + + + + {/* Response curve + soft fill. */} + + + + {/* Band nodes. */} + {bands.map((band) => { + const cx = freqToX(band.frequency, width); + const cy = gainToY(band.gain, height); + const isActive = band.id === activeBandId; + const dim = !band.enabled || !enabled; + return ( + + + + + ); + })} + + + {/* Node numbers (RN text over the canvas). */} + {bands.map((band, i) => { + const cx = freqToX(band.frequency, width); + const cy = gainToY(band.gain, height); + const isActive = band.id === activeBandId; + return ( + + {i + 1} + + ); + })} + + {/* dB labels (right edge). */} + + +6 + + + -6 + + + {/* Frequency labels (bottom axis). */} + {FREQ_TICKS.map((tick) => ( + + {tick.label} + + ))} + + {/* Gesture overlay. */} + true} + onMoveShouldSetResponder={() => true} + onResponderTerminationRequest={() => false} + onResponderGrant={handleGrant} + onResponderMove={handleMove} + onResponderRelease={endDrag} + onResponderTerminate={endDrag} + /> + + ) : null} + + ); +} + +// --- helpers --------------------------------------------------------------- + +function hLine(x0: number, y: number, width: number): SkPath { + const p = Skia.Path.Make(); + p.moveTo(x0, y); + p.lineTo(width, y); + return p; +} + +function withAlpha(hex: string, alpha: number): string { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +function useStableSize(): [ + { width: number; height: number }, + (s: { width: number; height: number }) => void, +] { + const [size, setSize] = useState({ width: 0, height: 0 }); + const set = (s: { width: number; height: number }) => { + setSize((prev) => (prev.width === s.width && prev.height === s.height ? prev : s)); + }; + return [size, set]; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + borderRadius: 16, + overflow: 'hidden', + backgroundColor: colors.bgSecondary, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + }, + nodeLabel: { + position: 'absolute', + width: NODE_R * 2, + textAlign: 'center', + fontSize: 12, + }, + dbLabel: { + position: 'absolute', + right: 8, + color: colors.textTertiary, + }, + freqLabel: { + position: 'absolute', + bottom: 4, + width: 20, + textAlign: 'center', + color: colors.textTertiary, + }, +}); + +export default EQGraph; diff --git a/src/components/eq/EQSlider.tsx b/src/components/eq/EQSlider.tsx new file mode 100644 index 0000000..fe52cf1 --- /dev/null +++ b/src/components/eq/EQSlider.tsx @@ -0,0 +1,196 @@ +import { useRef, useState } from 'react'; +import { + Pressable, + View, + StyleSheet, + type GestureResponderEvent, + type LayoutChangeEvent, +} from 'react-native'; +import { Text } from '@/components/Text'; +import { colors, radius, spacing } from '@/theme'; + +const THUMB = 16; + +interface EQSliderProps { + label: string; + value: number; + min: number; + max: number; + /** Logarithmic mapping (for frequency). */ + log?: boolean; + format: (v: number) => string; + onChange: (v: number) => void; + disabled?: boolean; + onValuePress?: () => void; +} + +const clamp01 = (f: number) => Math.min(1, Math.max(0, f)); + +/** Labeled horizontal slider following the SeekBar gesture/derivation pattern. */ +export function EQSlider({ + label, + value, + min, + max, + log, + format, + onChange, + disabled, + onValuePress, +}: EQSliderProps) { + const [width, setWidth] = useState(0); + const [active, setActive] = useState(false); + const widthRef = useRef(0); + // Anchor on grant, then track absolute pageX deltas so veering off the row + // vertically can't corrupt the value (the SeekBar pattern). + const grantRef = useRef({ fraction: 0, pageX: 0 }); + + const valueToFraction = (v: number): number => { + if (log) { + const lo = Math.log10(min); + const hi = Math.log10(max); + return clamp01((Math.log10(Math.max(min, v)) - lo) / (hi - lo)); + } + return clamp01((v - min) / (max - min)); + }; + + const fractionToValue = (f: number): number => { + if (log) { + const lo = Math.log10(min); + const hi = Math.log10(max); + return 10 ** (lo + clamp01(f) * (hi - lo)); + } + return min + clamp01(f) * (max - min); + }; + + const onLayout = (e: LayoutChangeEvent) => { + widthRef.current = e.nativeEvent.layout.width; + setWidth(e.nativeEvent.layout.width); + }; + + const handleGrant = (e: GestureResponderEvent) => { + setActive(true); + const f = clamp01(e.nativeEvent.locationX / Math.max(1, widthRef.current)); + grantRef.current = { fraction: f, pageX: e.nativeEvent.pageX }; + onChange(fractionToValue(f)); + }; + + const handleMove = (e: GestureResponderEvent) => { + const delta = (e.nativeEvent.pageX - grantRef.current.pageX) / Math.max(1, widthRef.current); + onChange(fractionToValue(clamp01(grantRef.current.fraction + delta))); + }; + + const fraction = valueToFraction(value); + + return ( + + + {label} + + !disabled} + onMoveShouldSetResponder={() => !disabled} + onResponderTerminationRequest={() => false} + onResponderGrant={handleGrant} + onResponderMove={handleMove} + onResponderRelease={() => setActive(false)} + onResponderTerminate={() => setActive(false)} + accessibilityRole="adjustable" + accessibilityLabel={label} + > + + + + + + {onValuePress && !disabled ? ( + [styles.valueButton, pressed && styles.valueButtonPressed]} + onPress={onValuePress} + accessibilityRole="button" + accessibilityLabel={`Edit ${label}`} + > + + {format(value)} + + + ) : ( + + {format(value)} + + )} + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + paddingVertical: spacing.xs, + }, + disabled: { + opacity: 0.4, + }, + label: { + width: 78, + }, + touch: { + flex: 1, + justifyContent: 'center', + paddingVertical: spacing.sm, + }, + track: { + height: 4, + borderRadius: radius.pill, + backgroundColor: colors.glassBorder, + overflow: 'hidden', + }, + fill: { + height: 4, + borderRadius: radius.pill, + backgroundColor: colors.accent, + }, + thumb: { + position: 'absolute', + width: THUMB, + height: THUMB, + borderRadius: THUMB / 2, + backgroundColor: colors.accent, + }, + thumbActive: { + transform: [{ scale: 1.3 }], + backgroundColor: colors.accentHover, + }, + valueButton: { + minWidth: 68, + alignItems: 'flex-end', + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: radius.pill, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + }, + valueButtonPressed: { + borderColor: colors.accent, + backgroundColor: colors.glassHighlight, + }, + value: { + width: 64, + textAlign: 'right', + color: colors.textPrimary, + }, +}); + +export default EQSlider; diff --git a/src/components/eq/EQValueEditSheet.tsx b/src/components/eq/EQValueEditSheet.tsx new file mode 100644 index 0000000..76d4e53 --- /dev/null +++ b/src/components/eq/EQValueEditSheet.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react'; +import { + Pressable, + StyleSheet, + View, + type KeyboardTypeOptions, +} from 'react-native'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; +import { Text } from '@/components/Text'; +import { colors, fonts, radius, spacing } from '@/theme'; +import { EqSheet } from './EqSheet'; + +interface EQValueEditSheetProps { + title: string; + initialValue: string; + unit: string; + rangeLabel: string; + placeholder?: string; + keyboardType?: KeyboardTypeOptions; + parseValue: (value: string) => number | null; + onApply: (value: number) => void; + onClose: () => void; +} + +/** Focused numeric editor for exact EQ band values. */ +export function EQValueEditSheet({ + title, + initialValue, + unit, + rangeLabel, + placeholder, + keyboardType = 'numbers-and-punctuation', + parseValue, + onApply, + onClose, +}: EQValueEditSheetProps) { + const [value, setValue] = useState(initialValue); + const trimmed = value.trim(); + const parsed = trimmed.length > 0 ? parseValue(trimmed) : null; + const valid = parsed !== null; + + const apply = () => { + if (parsed === null) return; + onApply(parsed); + onClose(); + }; + + return ( + + + {title} + + + 0 && !valid && styles.inputInvalid]} + autoFocus + selectTextOnFocus + maxLength={16} + returnKeyType="done" + onSubmitEditing={apply} + selectionColor={colors.accent} + /> + + {unit} + + + 0 && !valid && styles.invalidText]}> + {valid || trimmed.length === 0 ? rangeLabel : 'Enter a valid number'} + + + + + Cancel + + + + + Apply + + + + + ); +} + +const styles = StyleSheet.create({ + title: { + marginTop: spacing.xs, + marginBottom: spacing.md, + }, + inputRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, + input: { + flex: 1, + color: colors.textPrimary, + fontFamily: fonts.mono.regular, + fontSize: 18, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + borderRadius: radius.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + }, + inputInvalid: { + borderColor: colors.warning, + }, + unit: { + minWidth: 34, + color: colors.textSecondary, + }, + range: { + marginTop: spacing.sm, + color: colors.textTertiary, + }, + invalidText: { + color: colors.warning, + }, + actions: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: spacing.sm, + marginTop: spacing.lg, + }, + btn: { + paddingHorizontal: spacing.xl, + paddingVertical: spacing.md, + borderRadius: radius.pill, + }, + cancel: { + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + }, + apply: { + backgroundColor: colors.accentGlow, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.accent, + }, + applyDisabled: { + opacity: 0.4, + }, +}); + +export default EQValueEditSheet; diff --git a/src/components/eq/EqSheet.tsx b/src/components/eq/EqSheet.tsx new file mode 100644 index 0000000..a60aa7a --- /dev/null +++ b/src/components/eq/EqSheet.tsx @@ -0,0 +1,132 @@ +import { useCallback, type ReactNode } from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import BottomSheet, { + BottomSheetBackdrop, + BottomSheetView, + type BottomSheetBackdropProps, +} from '@gorhom/bottom-sheet'; +import { Text } from '@/components/Text'; +import { colors, radius, spacing } from '@/theme'; + +/** + * Bottom sheet for the EQ screen's menus — same chrome/behaviour as the now-playing + * QueueTray (inline gorhom BottomSheet, dimmed backdrop, grab handle, pan-to-close) + * so trays stay consistent across the app. Dynamically sized to its content; render + * it conditionally ({open && }). + */ +export function EqSheet({ onClose, children }: { onClose: () => void; children: ReactNode }) { + const insets = useSafeAreaInsets(); + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [] + ); + + return ( + + + {children} + + + ); +} + +/** Section label inside a sheet. */ +export function EqSheetSection({ label }: { label: string }) { + return ( + + {label} + + ); +} + +interface EqSheetItemProps { + label: string; + icon?: keyof typeof Ionicons.glyphMap; + selected?: boolean; + destructive?: boolean; + onPress: () => void; + /** Optional trailing control (e.g. a delete button). */ + trailing?: ReactNode; +} + +/** One tappable row, styled like the ActionSheet items it replaces. */ +export function EqSheetItem({ label, icon, selected, destructive, onPress, trailing }: EqSheetItemProps) { + const tint = destructive ? colors.warning : selected ? colors.accentTextStrong : colors.textPrimary; + return ( + + [styles.item, pressed && styles.itemPressed]} + onPress={onPress} + accessibilityRole="button" + > + {icon ? ( + + ) : null} + + {label} + + {selected ? : null} + + {trailing} + + ); +} + +const styles = StyleSheet.create({ + sheetBg: { + backgroundColor: colors.bgSecondary, + borderTopLeftRadius: radius.lg, + borderTopRightRadius: radius.lg, + }, + handle: { + backgroundColor: colors.glassBorder, + width: 38, + }, + content: { + paddingHorizontal: spacing.lg, + paddingTop: spacing.xs, + }, + section: { + color: colors.textTertiary, + letterSpacing: 1, + marginTop: spacing.md, + marginBottom: spacing.xs, + }, + itemRow: { + flexDirection: 'row', + alignItems: 'center', + }, + item: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + paddingVertical: spacing.md, + }, + itemPressed: { + opacity: 0.6, + }, + itemLabel: { + flex: 1, + }, +}); + +export default EqSheet; diff --git a/src/components/eq/PresetSheet.tsx b/src/components/eq/PresetSheet.tsx new file mode 100644 index 0000000..9198682 --- /dev/null +++ b/src/components/eq/PresetSheet.tsx @@ -0,0 +1,102 @@ +import { Pressable, StyleSheet } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { Text } from '@/components/Text'; +import { colors, spacing } from '@/theme'; +import type { EQPreset } from '@/types/audio'; +import { EqSheet, EqSheetItem, EqSheetSection } from './EqSheet'; + +interface PresetSheetProps { + presets: EQPreset[]; + activePresetId: string | null; + onApply: (id: string) => void; + onDelete: (id: string) => void; + onSaveNew: () => void; + onClose: () => void; +} + +/** Preset hub: pick a built-in or custom preset, delete custom ones, or save a new one. */ +export function PresetSheet({ + presets, + activePresetId, + onApply, + onDelete, + onSaveNew, + onClose, +}: PresetSheetProps) { + const builtIn = presets.filter((p) => !p.isCustom); + const custom = presets.filter((p) => p.isCustom); + + return ( + + + Presets + + + + {builtIn.map((p) => ( + { + onApply(p.id); + onClose(); + }} + /> + ))} + + + {custom.length === 0 ? ( + + No saved presets yet. + + ) : ( + custom.map((p) => ( + { + onApply(p.id); + onClose(); + }} + trailing={ + onDelete(p.id)} + style={styles.delete} + accessibilityLabel={`Delete preset ${p.name}`} + > + + + } + /> + )) + )} + + { + onClose(); + onSaveNew(); + }} + /> + + ); +} + +const styles = StyleSheet.create({ + title: { + marginTop: spacing.xs, + }, + empty: { + paddingVertical: spacing.sm, + }, + delete: { + paddingHorizontal: spacing.sm, + paddingVertical: spacing.sm, + }, +}); + +export default PresetSheet; diff --git a/src/components/eq/SavePresetSheet.tsx b/src/components/eq/SavePresetSheet.tsx new file mode 100644 index 0000000..a5c7408 --- /dev/null +++ b/src/components/eq/SavePresetSheet.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; +import { Text } from '@/components/Text'; +import { colors, fonts, radius, spacing } from '@/theme'; +import { EqSheet } from './EqSheet'; + +interface SavePresetSheetProps { + defaultName: string; + onSave: (name: string) => void; + onClose: () => void; +} + +/** Name + save a custom preset from the current bands/preamp. */ +export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetSheetProps) { + const [name, setName] = useState(defaultName); + const trimmed = name.trim(); + + return ( + + + Save preset + + { + if (trimmed) { + onSave(trimmed); + onClose(); + } + }} + /> + + + + Cancel + + + { + onSave(trimmed); + onClose(); + }} + > + + Save + + + + + ); +} + +const styles = StyleSheet.create({ + title: { + marginTop: spacing.xs, + marginBottom: spacing.md, + }, + input: { + color: colors.textPrimary, + fontFamily: fonts.sans.regular, + fontSize: 16, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + borderRadius: radius.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + }, + actions: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: spacing.sm, + marginTop: spacing.lg, + }, + btn: { + paddingHorizontal: spacing.xl, + paddingVertical: spacing.md, + borderRadius: radius.pill, + }, + cancel: { + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + }, + save: { + backgroundColor: colors.accentGlow, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.accent, + }, + saveDisabled: { + opacity: 0.4, + }, +}); + +export default SavePresetSheet; diff --git a/src/components/eq/eqGraphMath.ts b/src/components/eq/eqGraphMath.ts new file mode 100644 index 0000000..dd2593a --- /dev/null +++ b/src/components/eq/eqGraphMath.ts @@ -0,0 +1,83 @@ +// Coordinate mapping + response-curve sampling for the EQ graph. Frequency is on a +// log axis (20 Hz–20 kHz); gain is linear (±12 dB) centered vertically. + +import { Skia, type SkPath } from '@shopify/react-native-skia'; +import type { EQBand } from '@/types/audio'; +import { + EQ_MAX_FREQUENCY, + EQ_MAX_GAIN_DB, + EQ_MIN_FREQUENCY, + computeCombinedEQMagnitude, +} from '@/audio/eq'; + +export const GRAPH_SAMPLE_RATE = 48000; +export const GRAPH_PAD_Y = 14; // px headroom so ±12 dB nodes aren't clipped +const LOG_MIN = Math.log10(EQ_MIN_FREQUENCY); +const LOG_MAX = Math.log10(EQ_MAX_FREQUENCY); +const LOG_SPAN = LOG_MAX - LOG_MIN; + +function clamp(v: number, min: number, max: number): number { + return Math.max(min, Math.min(max, v)); +} + +export function freqToX(freq: number, width: number): number { + const f = clamp(freq, EQ_MIN_FREQUENCY, EQ_MAX_FREQUENCY); + return ((Math.log10(f) - LOG_MIN) / LOG_SPAN) * width; +} + +export function xToFreq(x: number, width: number): number { + const t = clamp(width > 0 ? x / width : 0, 0, 1); + return 10 ** (LOG_MIN + t * LOG_SPAN); +} + +export function gainToY(gainDb: number, height: number): number { + const center = height / 2; + const usable = center - GRAPH_PAD_Y; + return center - (clamp(gainDb, -EQ_MAX_GAIN_DB, EQ_MAX_GAIN_DB) / EQ_MAX_GAIN_DB) * usable; +} + +export function yToGain(y: number, height: number): number { + const center = height / 2; + const usable = center - GRAPH_PAD_Y; + if (usable <= 0) return 0; + return clamp(((center - y) / usable) * EQ_MAX_GAIN_DB, -EQ_MAX_GAIN_DB, EQ_MAX_GAIN_DB); +} + +/** Combined response curve as a stroked SkPath sampled across the width. */ +export function buildResponsePath( + bands: readonly EQBand[], + width: number, + height: number, + samples = 96 +): SkPath { + const path = Skia.Path.Make(); + if (width <= 0 || height <= 0) return path; + for (let i = 0; i <= samples; i++) { + const x = (i / samples) * width; + const freq = xToFreq(x, width); + const db = computeCombinedEQMagnitude(bands, freq, GRAPH_SAMPLE_RATE); + const y = gainToY(db, height); + if (i === 0) path.moveTo(x, y); + else path.lineTo(x, y); + } + return path; +} + +/** Closes a copy of the response path down to the baseline for a soft fill. */ +export function buildResponseFill(line: SkPath, width: number, height: number): SkPath { + const fill = line.copy(); + fill.lineTo(width, height / 2); + fill.lineTo(0, height / 2); + fill.close(); + return fill; +} + +/** Frequency gridline positions + labels shown along the bottom axis. */ +export const FREQ_TICKS: { freq: number; label: string }[] = [ + { freq: 30, label: '30' }, + { freq: 100, label: '100' }, + { freq: 500, label: '500' }, + { freq: 1000, label: '1k' }, + { freq: 5000, label: '5k' }, + { freq: 15000, label: '15k' }, +]; diff --git a/src/components/eq/format.ts b/src/components/eq/format.ts new file mode 100644 index 0000000..a337660 --- /dev/null +++ b/src/components/eq/format.ts @@ -0,0 +1,31 @@ +// Shared EQ value formatting for the band strip + detail panel. + +import { colors } from '@/theme'; +import type { EQBandType } from '@/types/audio'; + +export function formatFreq(hz: number): string { + if (hz >= 1000) { + const k = hz / 1000; + return `${Number.isInteger(k) ? k : k.toFixed(1)}k`; + } + return `${Math.round(hz)}`; +} + +export function formatGain(db: number): string { + if (Math.abs(db) < 0.05) return '0'; + return `${db > 0 ? '+' : ''}${db.toFixed(1)}`; +} + +export function gainColor(db: number): string { + if (db > 0.05) return colors.accentText; + if (db < -0.05) return colors.warning; + return colors.textTertiary; +} + +export const BAND_TYPE_LABEL: Record = { + lowshelf: 'Low Shelf', + peaking: 'Peaking', + highshelf: 'High Shelf', + highpass: 'High Pass', + lowpass: 'Low Pass', +}; diff --git a/src/components/library/ScanProgress.tsx b/src/components/library/ScanProgress.tsx index 5fda644..0a3d2e7 100644 --- a/src/components/library/ScanProgress.tsx +++ b/src/components/library/ScanProgress.tsx @@ -11,14 +11,16 @@ export function ScanProgress() { if (!isScanning) return null; const label = - progress.phase === 'extracting' - ? `Scanning ${progress.folderName ?? ''}… ${progress.processed}/${progress.total}` - : progress.total > 0 - ? `Found ${progress.total} files in ${progress.folderName ?? ''}…` - : `Looking for music${progress.folderName ? ` in ${progress.folderName}` : ''}…`; + progress.phase === 'analyzing' + ? `Analyzing audio… ${progress.processed}/${progress.total}` + : progress.phase === 'extracting' + ? `Scanning ${progress.folderName ?? ''}… ${progress.processed}/${progress.total}` + : progress.total > 0 + ? `Found ${progress.total} files in ${progress.folderName ?? ''}…` + : `Looking for music${progress.folderName ? ` in ${progress.folderName}` : ''}…`; const fraction = - progress.phase === 'extracting' && progress.total > 0 + (progress.phase === 'extracting' || progress.phase === 'analyzing') && progress.total > 0 ? progress.processed / progress.total : 0; diff --git a/src/db/queries.ts b/src/db/queries.ts index dd040a0..cc288b1 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -141,6 +141,74 @@ export async function getTrackCount(db: LibraryDatabase): Promise { return row?.count ?? 0; } +// --- Loudness (M4 normalization facts) --------------------------------------- + +export interface TrackLoudness { + loudness_lufs: number | null; + sample_peak: number | null; + replay_gain_track_db: number | null; + replay_gain_album_db: number | null; + replay_gain_track_peak: number | null; + replay_gain_album_peak: number | null; + /** 1 once ReplayGain tags have been read (whether or not any were present). */ + rg_scanned: number | null; +} + +/** Loudness facts for one track path (NULL fields = not yet analyzed). */ +export async function getTrackLoudness( + db: LibraryDatabase, + path: string +): Promise { + return ( + (await db.get( + `SELECT loudness_lufs, sample_peak, + replay_gain_track_db, replay_gain_album_db, + replay_gain_track_peak, replay_gain_album_peak, rg_scanned + FROM tracks WHERE path = ?`, + [path] + )) ?? null + ); +} + +/** Persist measured loudness + sample peak for a track (scan analyze pass). */ +export async function setTrackLoudness( + db: LibraryDatabase, + path: string, + lufs: number | null, + samplePeak: number | null +): Promise { + await db.run('UPDATE tracks SET loudness_lufs = ?, sample_peak = ? WHERE path = ?', [ + lufs, + samplePeak, + path, + ]); +} + +export interface ReplayGainColumns { + trackGainDb: number | null; + albumGainDb: number | null; + trackPeak: number | null; + albumPeak: number | null; +} + +/** + * Persist ReplayGain tags read from the container + mark the track as scanned, so + * we read tags once per track (independent of loudness, which may re-measure). + */ +export async function setTrackReplayGain( + db: LibraryDatabase, + path: string, + rg: ReplayGainColumns +): Promise { + await db.run( + `UPDATE tracks SET + replay_gain_track_db = ?, replay_gain_album_db = ?, + replay_gain_track_peak = ?, replay_gain_album_peak = ?, rg_scanned = 1 + WHERE path = ?`, + [rg.trackGainDb, rg.albumGainDb, rg.trackPeak, rg.albumPeak, path] + ); +} + // --- Settings (key-value preferences) ---------------------------------------- export async function getSetting(db: LibraryDatabase, key: string): Promise { diff --git a/src/db/schema.ts b/src/db/schema.ts index 4214bc8..12eec69 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -4,11 +4,16 @@ // non-ASCII tags were truncated by the pre-fix op-sqlite binding (see database.ts); // v4 adds a key-value settings table (artist grouping mode, future prefs); // v5 caches offline waveform peaks for the M3 waveform seek bar; v6 repairs DBs -// that an abandoned earlier M3 spike left at v5 with a stale `waveform_cache`. +// that an abandoned earlier M3 spike left at v5 with a stale `waveform_cache`; +// v7 (M4) adds per-track loudness facts (integrated LUFS + sample peak + ReplayGain +// tags) measured for normalization; v8 clears any loudness measured by the earlier +// ungated whole-file method so it re-measures with the fast gated subset method; +// v9 adds ReplayGain peak columns + an `rg_scanned` sentinel so tag reading runs +// once per track (and is retried if it ever failed), independent of loudness. import type { LibraryDatabase } from './database'; -export const SCHEMA_VERSION = 6; +export const SCHEMA_VERSION = 9; // One statement per entry — op-sqlite executes single statements. const MIGRATIONS: readonly (readonly string[])[] = [ @@ -115,6 +120,27 @@ const MIGRATIONS: readonly (readonly string[])[] = [ )`, `DROP TABLE IF EXISTS waveform_cache`, ], + // v6 -> v7 — per-track loudness facts for normalization (M4). NULL = not yet + // analyzed (the scan analyze pass / lazy fallback backfills them). loudness_lufs + // is integrated LUFS (negative dB); sample_peak is linear [0,1]; replay_gain_* + // are tag dB values when present. + [ + `ALTER TABLE tracks ADD COLUMN loudness_lufs REAL`, + `ALTER TABLE tracks ADD COLUMN sample_peak REAL`, + `ALTER TABLE tracks ADD COLUMN replay_gain_track_db REAL`, + `ALTER TABLE tracks ADD COLUMN replay_gain_album_db REAL`, + ], + // v7 -> v8 — re-measure loudness with the gated subset method (the earlier values + // were ungated whole-file). NULL forces the background pass to recompute them. + [`UPDATE tracks SET loudness_lufs = NULL, sample_peak = NULL`], + // v8 -> v9 — ReplayGain peaks (linear, for clip-limiting in RG mode) + an + // `rg_scanned` flag (0 = tags not yet read). Tag reading is decoupled from the + // loudness decode so it runs once per track and survives loudness re-measures. + [ + `ALTER TABLE tracks ADD COLUMN replay_gain_track_peak REAL`, + `ALTER TABLE tracks ADD COLUMN replay_gain_album_peak REAL`, + `ALTER TABLE tracks ADD COLUMN rg_scanned INTEGER NOT NULL DEFAULT 0`, + ], ]; export async function migrate(db: LibraryDatabase): Promise { diff --git a/src/library/scanner.ts b/src/library/scanner.ts index 57f2778..f6dce26 100644 --- a/src/library/scanner.ts +++ b/src/library/scanner.ts @@ -23,7 +23,7 @@ import { metadataToUpsertRow } from './trackAdapter'; const EXTRACT_BATCH_SIZE = 24; export interface ScanProgress { - phase: 'discovering' | 'extracting'; + phase: 'discovering' | 'extracting' | 'analyzing'; processed: number; total: number; folderName: string; @@ -157,6 +157,10 @@ export async function scanFolder( } await markFolderScanned(db, folder.id); + + // Loudness + waveform are measured on the fly: the first time a track is played, + // useNormalizationSync (loudness) and the seek bar (waveform) decode + cache it. + // No bulk background decoding — gentle on low-end devices. return result; } diff --git a/src/scope/oscilloscopeGain.ts b/src/scope/oscilloscopeGain.ts new file mode 100644 index 0000000..63280e5 --- /dev/null +++ b/src/scope/oscilloscopeGain.ts @@ -0,0 +1,32 @@ +// Per-track oscilloscope display gain. +// +// The oscilloscope tap sits right after the normalization processor, so each track +// arrives at a different level (loudness normalization scales quiet and loud masters +// by different amounts). A single fixed display gain therefore buries quiet tracks +// and clips loud ones. Instead we pick ONE gain per track that maps the track's peak +// to a consistent display height, and hold it for the whole track — so the song's own +// quiet/loud dynamics are preserved (a constant multiplier doesn't change them) while +// every track lines up to the same reference. It only changes when the track changes. + +/** Fallback when the track's peak is unknown (not yet analyzed / untagged). */ +export const DEFAULT_OSC_GAIN = 1.8; + +// Map the track's (post-normalization) peak to this fraction of the half-height, +// leaving a little headroom so the line doesn't kiss the edges. +const TARGET_LEVEL = 0.85; +const MIN_OSC_GAIN = 0.5; +const MAX_OSC_GAIN = 8; // cap so a very quiet master doesn't blow up to noise + +/** + * Display gain for the oscilloscope given the track's pre-normalization linear peak + * and the normalization gain currently applied (the tap is post-normalization, so the + * level it sees is `basePeak * normGain`). Returns {@link DEFAULT_OSC_GAIN} when the + * peak is unknown. + */ +export function computeOscilloscopeGain(basePeak: number | null, normGain: number): number { + if (basePeak == null || !(basePeak > 0)) return DEFAULT_OSC_GAIN; + const postNormPeak = basePeak * (normGain > 0 ? normGain : 1); + if (!(postNormPeak > 0)) return DEFAULT_OSC_GAIN; + const gain = TARGET_LEVEL / postNormPeak; + return Math.max(MIN_OSC_GAIN, Math.min(MAX_OSC_GAIN, gain)); +} diff --git a/src/scope/scopeStore.ts b/src/scope/scopeStore.ts index 44462ed..85486aa 100644 --- a/src/scope/scopeStore.ts +++ b/src/scope/scopeStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { DEFAULT_OSC_GAIN } from './oscilloscopeGain'; /** * Whether the visualizers should run. Set by useScopeLifecycle (foreground + @@ -8,11 +9,20 @@ import { create } from 'zustand'; interface ScopeStore { active: boolean; setActive: (active: boolean) => void; + /** + * Per-track oscilloscope display gain. Set once per track by useNormalizationSync + * (from the track's peak + the normalization gain) and read each frame by the + * oscilloscope so the level is consistent across tracks but constant within one. + */ + oscGain: number; + setOscGain: (gain: number) => void; } export const useScopeStore = create((set) => ({ active: false, setActive: (active) => set({ active }), + oscGain: DEFAULT_OSC_GAIN, + setOscGain: (oscGain) => set({ oscGain }), })); export const useScopeActive = (): boolean => useScopeStore((s) => s.active); diff --git a/src/stores/audioSettingsStore.ts b/src/stores/audioSettingsStore.ts new file mode 100644 index 0000000..4016c32 --- /dev/null +++ b/src/stores/audioSettingsStore.ts @@ -0,0 +1,105 @@ +import { create } from 'zustand'; +import { openLibraryDb } from '@/db/database'; +import { getSetting, setSetting } from '@/db/queries'; +import { + DEFAULT_TARGET_LUFS, + type NormalizationSettings, + type ReplayGainMode, +} from '@/audio/normalization'; + +/** + * Loudness/normalization + ReplayGain preferences. SQLite (settings table) is the + * source of truth, mirrored in memory (settingsStore pattern; no zustand-persist). + * The EQ's own state lives in eqStore — this store is the normalization half. + */ +const NORMALIZATION_ENABLED_KEY = 'normalization_enabled'; +const NORMALIZATION_TARGET_KEY = 'normalization_target_lufs'; +const REPLAYGAIN_ENABLED_KEY = 'replaygain_enabled'; +const REPLAYGAIN_MODE_KEY = 'replaygain_mode'; + +function parseMode(value: string | null): ReplayGainMode { + return value === 'track' || value === 'album' ? value : 'auto'; +} + +interface AudioSettingsStore { + normalizationEnabled: boolean; + normalizationTargetLufs: number; + replayGainEnabled: boolean; + replayGainMode: ReplayGainMode; + loaded: boolean; + + load: () => Promise; + setNormalizationEnabled: (enabled: boolean) => Promise; + setNormalizationTargetLufs: (lufs: number) => Promise; + setReplayGainEnabled: (enabled: boolean) => Promise; + setReplayGainMode: (mode: ReplayGainMode) => Promise; + /** Current settings as the plain shape the gain resolver consumes. */ + asNormalizationSettings: () => NormalizationSettings; +} + +export const useAudioSettingsStore = create((set, get) => ({ + normalizationEnabled: true, + normalizationTargetLufs: DEFAULT_TARGET_LUFS, + replayGainEnabled: false, + replayGainMode: 'auto', + loaded: false, + + load: async () => { + if (get().loaded) return; + const db = await openLibraryDb(); + const [enabled, target, rgEnabled, rgMode] = await Promise.all([ + getSetting(db, NORMALIZATION_ENABLED_KEY), + getSetting(db, NORMALIZATION_TARGET_KEY), + getSetting(db, REPLAYGAIN_ENABLED_KEY), + getSetting(db, REPLAYGAIN_MODE_KEY), + ]); + const targetNum = Number(target); + set({ + // Defaults to ON when never set. + normalizationEnabled: enabled === null ? true : enabled === 'true', + normalizationTargetLufs: Number.isFinite(targetNum) && target !== null ? targetNum : DEFAULT_TARGET_LUFS, + replayGainEnabled: rgEnabled === 'true', + replayGainMode: parseMode(rgMode), + loaded: true, + }); + }, + + setNormalizationEnabled: async (enabled) => { + if (get().normalizationEnabled === enabled) return; + set({ normalizationEnabled: enabled }); + const db = await openLibraryDb(); + await setSetting(db, NORMALIZATION_ENABLED_KEY, enabled ? 'true' : 'false'); + }, + + setNormalizationTargetLufs: async (lufs) => { + const clamped = Math.max(-30, Math.min(-5, lufs)); + if (get().normalizationTargetLufs === clamped) return; + set({ normalizationTargetLufs: clamped }); + const db = await openLibraryDb(); + await setSetting(db, NORMALIZATION_TARGET_KEY, String(clamped)); + }, + + setReplayGainEnabled: async (enabled) => { + if (get().replayGainEnabled === enabled) return; + set({ replayGainEnabled: enabled }); + const db = await openLibraryDb(); + await setSetting(db, REPLAYGAIN_ENABLED_KEY, enabled ? 'true' : 'false'); + }, + + setReplayGainMode: async (mode) => { + if (get().replayGainMode === mode) return; + set({ replayGainMode: mode }); + const db = await openLibraryDb(); + await setSetting(db, REPLAYGAIN_MODE_KEY, mode); + }, + + asNormalizationSettings: () => { + const s = get(); + return { + enabled: s.normalizationEnabled, + targetLufs: s.normalizationTargetLufs, + replayGainEnabled: s.replayGainEnabled, + replayGainMode: s.replayGainMode, + }; + }, +})); diff --git a/src/stores/eqStore.ts b/src/stores/eqStore.ts index 1441a70..4e92e71 100644 --- a/src/stores/eqStore.ts +++ b/src/stores/eqStore.ts @@ -1,44 +1,280 @@ import { create } from 'zustand'; -import type { EQBand } from '@/types/audio'; +import type { EQBand, EQPreset } from '@/types/audio'; +import { openLibraryDb } from '@/db/database'; +import { getSetting, setSetting } from '@/db/queries'; +import { + EQ_MAX_BANDS, + clampEQFrequency, + clampEQGain, + clampEQQ, + clampPreamp, + createNormalizedEQBand, + dbToLinear, + flattenBandsForNative, +} from '@/audio/eq'; +import { createBuiltInPresets, createDefaultBands, FLAT_PRESET_ID, genEqId } from '@/audio/eqPresets'; +import { setEqBandsNative, setEqEnabledNative, setEqPreampNative } from '@/audio/eqNative'; /** - * EQ state — M0 stub. The biquad chain is implemented as a Media3 AudioProcessor - * at M4; for now this just holds the band model (ported `EQBand`) so the EQ - * screen and later DSP wiring share one shape. + * Parametric EQ state — SQLite (settings table) is the source of truth, mirrored + * in memory (mirrors the settingsStore pattern; no zustand-persist). Every band/ + * preamp/enable change pushes params to the native EqAudioProcessor via _syncToNative + * (immediate, for live audio) and debounce-persists to SQLite. */ -const DEFAULT_FREQUENCIES = [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]; +const ENABLED_KEY = 'eq_enabled'; +const PREAMP_KEY = 'eq_preamp'; +const BANDS_KEY = 'eq_bands'; +const ACTIVE_PRESET_KEY = 'eq_active_preset'; +const CUSTOM_PRESETS_KEY = 'eq_custom_presets'; -function makeDefaultBands(): EQBand[] { - return DEFAULT_FREQUENCIES.map((frequency) => ({ - id: `band-${frequency}`, - type: 'peaking', - frequency, - gain: 0, - Q: 1.0, - })); +const PERSIST_DEBOUNCE_MS = 250; + +function parseBands(json: string | null): EQBand[] | null { + if (!json) return null; + try { + const arr = JSON.parse(json); + if (!Array.isArray(arr) || arr.length === 0) return null; + return arr.slice(0, EQ_MAX_BANDS).map((raw) => createNormalizedEQBand(raw, genEqId())); + } catch { + return null; + } +} + +function parseCustomPresets(json: string | null): EQPreset[] { + if (!json) return []; + try { + const arr = JSON.parse(json); + if (!Array.isArray(arr)) return []; + return arr + .filter((p): p is { id?: string; name: string; preamp?: number; bands?: unknown[] } => !!p && typeof p.name === 'string') + .map((p) => ({ + // Keep the stored id so a persisted activePresetId still matches on reload. + id: typeof p.id === 'string' && p.id.length > 0 ? p.id : genEqId(), + name: p.name, + preamp: clampPreamp(typeof p.preamp === 'number' ? p.preamp : 0), + bands: Array.isArray(p.bands) + ? p.bands.slice(0, EQ_MAX_BANDS).map((b) => createNormalizedEQBand(b as object, genEqId())) + : createDefaultBands(), + isCustom: true, + })); + } catch { + return []; + } } interface EQStore { enabled: boolean; preamp: number; // dB bands: EQBand[]; + presets: EQPreset[]; // built-in + custom + activePresetId: string | null; // null = manually edited ("Custom") + activeBandId: string | null; // UI selection shared by curve / strip / panel + loaded: boolean; + load: () => Promise; setEnabled: (enabled: boolean) => void; - setPreamp: (preamp: number) => void; - setBandGain: (id: string, gain: number) => void; - reset: () => void; + toggleEnabled: () => void; + setPreamp: (db: number) => void; + addBand: (band?: Partial) => void; + removeBand: (id: string) => void; + updateBand: (id: string, updates: Partial) => void; + selectBand: (id: string | null) => void; + applyPreset: (presetId: string) => void; + resetToFlat: () => void; + saveCustomPreset: (name: string) => void; + deleteCustomPreset: (presetId: string) => void; + importPreset: (preset: EQPreset) => void; + + _syncToNative: () => void; } -export const useEQStore = create((set) => ({ - enabled: false, - preamp: 0, - bands: makeDefaultBands(), +let persistTimer: ReturnType | null = null; - setEnabled: (enabled) => set({ enabled }), - setPreamp: (preamp) => set({ preamp }), - setBandGain: (id, gain) => - set((state) => ({ - bands: state.bands.map((b) => (b.id === id ? { ...b, gain } : b)), - })), - reset: () => set({ enabled: false, preamp: 0, bands: makeDefaultBands() }), -})); +export const useEQStore = create((set, get) => { + function syncToNative(): void { + const { enabled, preamp, bands } = get(); + setEqEnabledNative(enabled); + setEqPreampNative(enabled ? dbToLinear(preamp) : 1); + setEqBandsNative(flattenBandsForNative(bands)); + } + + function schedulePersist(): void { + if (persistTimer) clearTimeout(persistTimer); + persistTimer = setTimeout(() => { + persistTimer = null; + void persistNow(); + }, PERSIST_DEBOUNCE_MS); + } + + async function persistNow(): Promise { + const { enabled, preamp, bands, activePresetId, presets } = get(); + const custom = presets.filter((p) => p.isCustom); + try { + const db = await openLibraryDb(); + await Promise.all([ + setSetting(db, ENABLED_KEY, enabled ? 'true' : 'false'), + setSetting(db, PREAMP_KEY, String(preamp)), + setSetting(db, BANDS_KEY, JSON.stringify(bands)), + setSetting(db, ACTIVE_PRESET_KEY, activePresetId ?? ''), + setSetting( + db, + CUSTOM_PRESETS_KEY, + JSON.stringify( + custom.map((p) => ({ id: p.id, name: p.name, preamp: p.preamp, bands: p.bands })) + ) + ), + ]); + } catch { + /* persistence failure is non-fatal */ + } + } + + /** Mark the current band set as a manual edit (no longer matches a preset). */ + function markEdited(patch: Partial): void { + set({ ...patch, activePresetId: null }); + syncToNative(); + schedulePersist(); + } + + return { + enabled: false, + preamp: 0, + bands: createDefaultBands(), + presets: createBuiltInPresets(), + activePresetId: FLAT_PRESET_ID, + activeBandId: null, + loaded: false, + + load: async () => { + if (get().loaded) return; + const db = await openLibraryDb(); + const [enabledRaw, preampRaw, bandsRaw, activeRaw, customRaw] = await Promise.all([ + getSetting(db, ENABLED_KEY), + getSetting(db, PREAMP_KEY), + getSetting(db, BANDS_KEY), + getSetting(db, ACTIVE_PRESET_KEY), + getSetting(db, CUSTOM_PRESETS_KEY), + ]); + + const bands = parseBands(bandsRaw) ?? createDefaultBands(); + const presets = [...createBuiltInPresets(), ...parseCustomPresets(customRaw)]; + const storedActive = activeRaw && activeRaw.length > 0 ? activeRaw : null; + + set({ + enabled: enabledRaw === 'true', + preamp: clampPreamp(Number(preampRaw) || 0), + bands, + presets, + // Stored active preset ids are regenerated on load (parseCustomPresets makes + // new ids), so only built-in ids survive a reload; fall back to "Custom". + activePresetId: presets.some((p) => p.id === storedActive) ? storedActive : null, + activeBandId: bands[0]?.id ?? null, + loaded: true, + }); + syncToNative(); + }, + + setEnabled: (enabled) => { + set({ enabled }); + syncToNative(); + schedulePersist(); + }, + + toggleEnabled: () => { + set({ enabled: !get().enabled }); + syncToNative(); + schedulePersist(); + }, + + setPreamp: (db) => markEdited({ preamp: clampPreamp(db) }), + + addBand: (partial) => { + const { bands } = get(); + if (bands.length >= EQ_MAX_BANDS) return; + const band = createNormalizedEQBand( + { + type: partial?.type ?? 'peaking', + frequency: partial?.frequency ?? 1000, + gain: partial?.gain ?? 0, + Q: partial?.Q ?? 1.0, + enabled: partial?.enabled ?? true, + }, + genEqId() + ); + const next = [...bands, band].sort((a, b) => a.frequency - b.frequency); + markEdited({ bands: next, activeBandId: band.id }); + }, + + removeBand: (id) => { + const { bands, activeBandId } = get(); + if (bands.length <= 1) return; + const next = bands.filter((b) => b.id !== id); + markEdited({ + bands: next, + activeBandId: activeBandId === id ? (next[0]?.id ?? null) : activeBandId, + }); + }, + + updateBand: (id, updates) => { + const next = get().bands.map((b) => { + if (b.id !== id) return b; + const merged: EQBand = { ...b, ...updates }; + if (updates.frequency !== undefined) merged.frequency = clampEQFrequency(updates.frequency); + if (updates.gain !== undefined) merged.gain = clampEQGain(updates.gain); + if (updates.Q !== undefined) merged.Q = clampEQQ(updates.Q); + return merged; + }); + markEdited({ + bands: updates.frequency !== undefined ? next.sort((a, b) => a.frequency - b.frequency) : next, + }); + }, + + selectBand: (id) => set({ activeBandId: id }), + + applyPreset: (presetId) => { + const preset = get().presets.find((p) => p.id === presetId); + if (!preset) return; + const bands = preset.bands.map((b) => createNormalizedEQBand(b, genEqId())); + set({ + bands, + preamp: clampPreamp(preset.preamp), + activePresetId: presetId, + activeBandId: bands[0]?.id ?? null, + }); + syncToNative(); + schedulePersist(); + }, + + resetToFlat: () => get().applyPreset(FLAT_PRESET_ID), + + saveCustomPreset: (name) => { + const { bands, preamp, presets } = get(); + const preset: EQPreset = { + id: genEqId(), + name: name.trim() || 'Custom Preset', + preamp, + bands: bands.map((b) => ({ ...b, id: genEqId() })), + isCustom: true, + }; + set({ presets: [...presets, preset], activePresetId: preset.id }); + schedulePersist(); + }, + + deleteCustomPreset: (presetId) => { + const { presets, activePresetId } = get(); + set({ + presets: presets.filter((p) => p.id !== presetId), + activePresetId: activePresetId === presetId ? null : activePresetId, + }); + schedulePersist(); + }, + + importPreset: (preset) => { + const stored: EQPreset = { ...preset, id: genEqId(), isCustom: true }; + set({ presets: [...get().presets, stored] }); + get().applyPreset(stored.id); + }, + + _syncToNative: syncToNative, + }; +}); diff --git a/src/stores/libraryStore.ts b/src/stores/libraryStore.ts index 7b60294..df3e9c2 100644 --- a/src/stores/libraryStore.ts +++ b/src/stores/libraryStore.ts @@ -25,7 +25,7 @@ type ViewMode = 'tracks' | 'albums' | 'artists' | 'playlists' | 'folders'; export type FolderWithCount = LibraryFolder & { track_count: number }; interface ScanProgressState { - phase: 'idle' | 'discovering' | 'extracting'; + phase: 'idle' | 'discovering' | 'extracting' | 'analyzing'; processed: number; total: number; folderName?: string; diff --git a/src/types/audio.ts b/src/types/audio.ts index d5ebb55..9cecbe6 100644 --- a/src/types/audio.ts +++ b/src/types/audio.ts @@ -52,12 +52,17 @@ export interface PlayerState { } // EQ Band +export type EQBandType = 'lowshelf' | 'peaking' | 'highshelf' | 'highpass' | 'lowpass'; + export interface EQBand { id: string; - type: 'lowshelf' | 'peaking' | 'highshelf' | 'highpass' | 'lowpass'; + type: EQBandType; frequency: number; gain: number; Q: number; + // Per-band bypass (mobile addition vs desktop — the EQ screen's per-band On toggle). + // A disabled band is passthrough and is skipped in the response curve. + enabled: boolean; } // EQ Preset diff --git a/src/types/library.ts b/src/types/library.ts index c21cc61..03dd6d8 100644 --- a/src/types/library.ts +++ b/src/types/library.ts @@ -30,6 +30,12 @@ export interface DbTrack { mtime: number; added_at: number; modified_at: number; + // M4 loudness facts (NULL until analyzed). loudness_lufs: integrated LUFS (dB, + // negative); sample_peak: linear [0,1]; replay_gain_*: tag dB when present. + loudness_lufs: number | null; + sample_peak: number | null; + replay_gain_track_db: number | null; + replay_gain_album_db: number | null; } export interface LibraryFolder { diff --git a/vendor/kotlinaudio/kotlin-audio/build/.transforms/7a308bbc4fe84868415859cf0cc55834/transformed/classes/classes_dex/classes.dex b/vendor/kotlinaudio/kotlin-audio/build/.transforms/7a308bbc4fe84868415859cf0cc55834/transformed/classes/classes_dex/classes.dex index e8ea8d58848c5c45af1d19cb21582929ff7275e5..32849379eb0d0aff7751d4f7d29f0fa394ff19cb 100644 GIT binary patch literal 192948 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 Km`HPHEb=^Sy)VR$XqF^&2; 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%0D=%uA*@Iet0ZKBC~QI!ZrW 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 JT|7~d{R>SsL?!?L diff --git a/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$2.dex b/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$2.dex index 2edd38aa93201bd3c7fbfb88e97822eb99d8cf3f..7688c4c2c1d03af916bb00daafd08d2f28d339b5 100644 GIT binary patch delta 69 zcmV-L0J{I=EaWU8WMy~?Ff%s*d-oztm&%Rf-Tw36v;X;!9BvnVv{^=_u^`I_2$TW> babXSsg0u4nJ`4ykH#jq6WjHspITR}bLr)v9 delta 69 zcmV-L0J{I=EaWU8WMy~?Ff%s*JNR#I&&SkwT9n}U%gWW5s>K|V`*}&ryvsZ@@riE}IMddyam%){bu^?Ol0o}8G U0jL59WM*SxIA%0rvn>Q80rSEZ?EnA( delta 62 zcmV-E0Kxyz3eXB5WMy~?Ff%s*uM&KU$tgH$TGMS{nhL|*=#4_Z_NY!Gu^?Ol0o=2F U0jL59Vlg*nF*#&8vn>Q80si+F0{{R3 diff --git a/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$PlayerListener.dex b/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$PlayerListener.dex index 8ff4f461bde2a05bc8a52dd09474aa995b19ca6f..a7d930d7fe526f81745ebe7a140fc49a4702ce19 100644 GIT binary patch literal 9244 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 U*j)Y!(}%xH)|`v3p{ literal 7968 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 diff --git a/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$createForwardingPlayer$1.dex b/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$createForwardingPlayer$1.dex index 5a7e4915f057ecfa1807df2bd8df6b8fb4c02f6a..9f46c814c5ca500efc08d128dfa81c846472c04c 100644 GIT binary patch delta 128 zcmca3eMed$CAEUfz}TE2oAswUSBGefz5k0JuFGzAU%7a_t9oKu4Q^C zyNi)UPeomE3BzVaMrIv`eT+YF7Ft(VSFJR8A*;gV|E#K< zN>)mVCPv0dhLcxurtvc{Ff%YPJm+L!cnid}UPdN8{Uw>Ya?&15nHd-uUUD)pya(d7lV!OU^R5N57!(*77=hIH$!EFL0ZeWz AS^xk5 diff --git a/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.dex b/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.dex index a026324fe825721cd46c8db04c6295c2d5e0921d..7e9bb2a3a390c4cfead212ed41b7a0d2af2bdcd5 100644 GIT binary patch delta 510 zcmZoU!Ps(wQ6VL@g3G|zoIz*GA7~MGz6p$jir&0%W8EaT*XO0&zSLUz%*A?w}nBWX)j$65p7a zb})ToxM zs}6K96YnHuJ_evHUlhYuAZcS06;)xA$?%Gal>r3!7``wuu49VyW@gyUWEU01@S2Hf z9n)#1$S50zqmvCZwZx`^^>%>B%xH#wAPuDEOfJw=so&4c^pUZPIWm)(;R+LT6vGi_ zrh`mC{v{}X0V5;O4QrVhPcQ+kuVMr`C5j;pBsP~3!pQ_t-nvokM?D=GHh=~7Dwt#r zF|;s46if$;wKFUM)4CN5^T7^@V)(_xz|Yt2W6N-wi3Q@3Gn4}1rQO$$jir&3}mDMaViie0C5};Uz}{C?w}n3WX)y*5?`5_ zb})To2R}6B%wYGxG2;^iS5)kW-rlvTX{8 zSPLThfy9S&hB^j5Mj`%mhE51KXmXK8a`aXf2EM3e%a|FqF|lPXlTv18yv@Xv$q>ZE zs}6K96YnHuK8B@0zeF)?0g^U0QBf5(nG7$PSQ$WokKr>D<2t5DZ)S$wOm;EVLez-uYyVD z5JNLFM8Pz$SUbaFFs)m`Fc0jID2AU*4E%iUKDG>}m{=elIlWm)%i4q^&CJ{|E!og~ Jb86gtRsg)Pe@XxV diff --git a/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt$buildScopeRenderersFactory$1.dex b/vendor/kotlinaudio/kotlin-audio/build/.transforms/efb325046c0dd0f6a51032165627c60b/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt$buildScopeRenderersFactory$1.dex index 1f8f79cf29ab6c82d4ae18210a16442ffa457a6d..375de2fac885a2957d22b2121d399ce1595f9fae 100644 GIT binary patch delta 1268 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 YIqR>mrbgI;gFIYzOZZW&A24J0zc6qkDF6Tf diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/vendor/kotlinaudio/kotlin-audio/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties index 3a496aa..df8e87f 100644 --- a/vendor/kotlinaudio/kotlin-audio/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +++ b/vendor/kotlinaudio/kotlin-audio/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties @@ -1 +1 @@ -#Wed Jun 17 14:38:54 EDT 2026 +#Fri Jun 19 12:51:01 EDT 2026 diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$2.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$2.class index 8d57ddc9333d4670ade1ac85304f27e3f8fbf7f6..26b55d7682c3dcd42fd9ab7f3d03e217185fd910 100644 GIT binary patch delta 89 zcmdmKy3=%nhlDE=g90NfgE}J{gEk`vLkuG)LjofgLkc4|Lk1%cLk=S^Ljh2{f{~q} rno)qE1*o=_QIKIKqY%S#Mq!3sjA9J?7@0QrN^mnWifo=KslW&Tbx{tA delta 89 zcmdmKy3=%nhlDF5g90N9gE}KCgEk{OLkuGaLjofwLkc4oLk1%^Lk=SkLjh2{f{~4( rnvtKO1*o=_QGj74qaed_Mj?h>jG_$t7#TPBN^mnW3U8h%slW&Tas3W= diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$Companion.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$Companion.class index 125b5cbf69c179b22fcd84f7d75a64d8eb097158..89e98a492a9a9334c9b9139312eda2449e8a4075 100644 GIT binary patch delta 13 VcmZ3=zLb5#cP7Tz$v>FZ0stk^1)2Z= delta 13 VcmZ3=zLb5#cP7S|$v>FZ0stk<1(^T< diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$PlayerListener.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$PlayerListener.class index 8780c9a0201afdad7eaf6777eef1fd09910b2504..7dc00fa3a4de5ad4b659f14d5a087720ce345473 100644 GIT binary patch delta 4181 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 delta 2716 zcmZWr4Oo;_7JknihWTNJpBWH_pBY44K~OZ!)YNbR*-2C$Xzc;p)e%QT5puo}l}$(8 zto%V&Z`#&|nn}22TOy96t-4uSnVG9@nOdoBwe7n8WNX%@d%o`zczTxS8Qyc>bM8Io zo}WAOUim9U_IGdX*#}@GjU*Jwfl(fLJ0?-CjE&h*mTQp?dolTBrM)t>3e&7`!%v9F zx<7lO4JK4uF%>fi_wlwodE4%wnbHwGKdKh9t;j?jVcZ=}tUMVVYkAm;Ija4bE;cvD zVo|IJs%&K!yD%m#vcZaZm?1aE>^Cn!qw4uL@>c9{>5ua{mk^9um6duBzqMj19+ju# z8qAO3anN5E+8U zdXu-J#e(PY0>@ZZG`g_DhHX0YM>!_GH1ra-tCxA1)kS4x6UuCOMOVK{FqJx2Yf zRh3^+G8OF>G9h8Hc^~$xF&vQ138e{d;GnvAlMu}(7t9R=IA`)}>xj@H99A`Nb6_PC zMi+X=7u)cze(n*1rAAS+Qd3v0<+IX;qj+DnKgRZ@C8H}e`{TO(U%S)z;0%9Vncp{U zfek0|zM95UT&dF0zbsed{!q97hVvg4b$LPm{9s;rFu>PR#s*EeF7uOu9{dM4toRn+vD2Bp24CLXU`=ga zNqv>C)^Ec1@%htE;|qpoxU+;Tkq3f{a!&P$ZiK>r3kA z`2z($#h=x`fB$SVnF%!6Ojd?|X0j2iTD=v;SM9e^w01d)A@q?~ZBE&knGuuB=Zgvl#!Sc8}#eO#reI$noF@=W6 zo9>jjdzcZhFz0|1g6(>Hdc~=BR^}sxA8}$j+g5l|dJJXO#4#eC1wuCZY8zJkWJ3*a zn8_RV{r`r6xJ%uz^RmZ6p<#SR6ih!L->v?okm|FMTgfIGkSOLOUo1eeXha3$Wbp{5 zi-nlaxJZPsOf1H7u>?(GDb}+56m!jD8McbYuuVLU?cxc%%5s>w*TqU46svGpG~tL? zjbkjI63&%4Bi7=AScmJ3H^h35c}34W8Y=hnOtlZ@3-*E$1`cYoEY8RdO|9tL4toct z??%l5%nGBv6_q-bu@`f?wSi9Uh%RlgTRXQ?>+aId>(?dXC|a>ZY{NRn^zTq?ET7QxgM*i{SSFcyQjNis9jLrW`C(spB6 z7|Xk&8GtJQwt zQ?k@vWdCDx9KY*8YOZ5l2jWu=qjJOe{g8;XgorIPC^s#PXXS^Px%8Zj=$#R2ZAC#F zUTnokHJxGnsSP_?5ZkqAe}S916`nTiZiOr7HS8gNmn?1gb1SSG89~F1F5@Arb{X&N zYQa)fav3}DmsT{WGKKM$u6^earm0dJ-eYx0CqH-^r8@Z{le%<5+dYh3{*CYq2I!U? zUwaFB=_aQ&lOAmHG4Yp^y?f)M25P-^v^(Q2#QunQqCvfkMwh`DZOk{}dH0Bnki)2z zA%IjKsr^V62k;B=25QAY1jL&>N{9LLHjmOf+!;r(K^(=i;uu~MA7F<#jyAS!7bozR zIEmxpBb*keaZ!AXOX3Vq%UL3Ej-tf{$`luAp!kx8i?66q?5F$1C9Twi?SGl zjMie?)%6ZEi-H$#=M8SmhW5809Ay@`_Fl;xT4b0`io>y!k+$8jz3XAb5m@L7mjF|adQGjKE7Fz^6rAx0|(QAYd8 cxqOMDPCyxFpo|Mp#u+H%1e9@~{Fv_{02wnN1ONa4 delta 159 zcmX>ie?)%6ZEi;C$#=M8SY;Um8097x@`_Fl;xT4b1oD)?y!k+$Dv+lJ=Dh*(G=V%V zFwc_Lm{k|Z(*yE6SPg+JBe3{optvcJX9nge@L7mjGO#mRF>o_lGw=XuAx29EQAWGT cxqOMDjzAeFpo}w6#tA6n2$XS~{Fv_{01>ty=>Px# diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$ratingType$1.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$ratingType$1.class index e6a287551d246b07839b58358567d6b1d852e526..777d6f46c1e88748985f0fbf4035383d520972f7 100644 GIT binary patch delta 79 zcmZ1@zD9gQAt&SJ$wi$0jJqZu;%t)K#~{qGpFy1A0D}a>K?X@6Tb^MbgA&7$$+28{ dlE;CvCxEgifwHH7vL}GD$APkECqLmj0RXs@7c~F? delta 79 zcmZ1@zD9gQAt&Re$wi$0j5{YE;%t)K%OK3Kk3pPaKZ69r0R~ARTb^MrgA&8x$+28{ dlE;9u$APjZfU+lnvd4k4$AGeDCO_di0RXm37bO4y diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.class index 101498768c27fa5431d40e398c543dae0ee4a83d..2397e2ee772d900164f2004d53c96fa45c559693 100644 GIT binary patch delta 1329 zcmXX_dra146n>tQU-2mniZ;D*J0Q!MqGjt0( z^OEJY)HTgnOf$=Ao7u$7m`%)Vn2Bbamt5Xbx6YVh;)Qt1|pDwzLUi#9AqJ67WYHsT^S;Wuo< zC47Mn9L1mb0awt9POG|(hxiLmaD%|#?1t+Mz%>TpCL?f*LvfqK4JWyAhi{;RZ{jXJ zxW^p)%RD?_F&=V09r;bU4ok_JjgH}V>pk)tIfGPn69q;J~}mT zAZK-<&4%V0iq_7&ILqs7DlhI-n?pja`+# zB9F7l@@p*Lr00sVoz1pdXRBr%Rh+{0T~%5<)Ugpgut{5sC-EZcK9-OTG=2z3x^5Ohlr95&sr zF@Q&G(r+-F-)dcTw)2?j97iO-)6VKO&J*a(@6n$pTb3?bBfe8cwjzwD5zQYF&oj2I z6^Z;AITrbBLnY6l-f%t7Vg=7*CEKxy7tF9<(ZY)s=W&#maDu<1otN!O{9)JN3hwhN z1K7z>Ub8>?DtlW*@j74RW%jj*;SIhV!ShVuO{Vh}vv`|@yu%9q!#dv7ElaOD@0*_w z5X<}8RFm&~Y%V`BM;_~=nsR>uLWPJG7&%Zst{oHNhi>9TfcPU={Lo7R5G{Usul8_s zAo@!X21ziIq$kEn2-2h%yb_9Z%gvO)mY?dnb3!EiBnlzY2mK@(@e+e!@)BN?Sd5nb zNR>E@m3|m618v(NWLQn6#3N4$gZ`ncP;FgJ)B5Bs&WMs({EU@=NnQDhj z!!q$=m84^>OvgHzfen(0Z8BR+>vMTg+v-QrtKA!t?b+54syPiq4C%AAo8FeN6~WK_ E4~9o*6#xJL delta 1335 zcmXX_YfzO{6kY4&6+Yt%guF!o^Oc~JB{=3SF}y-oQ7+&ElbDdp82RukdD^jrqY4`v$u^n@891Cy?%g}*!IF0rAMJvu= z7tUfo&f^=LLpv_w1TNt=F5^C~5cr)vaEX5CVj!+E0@pYa*EvdYq7yeb4rlp1{-g{4 zFb{t-AGcVHe_4*(tiT;s<1SypJ=Wts8X0DY)*(o*G?WeEs7zv$9$>t!8NImVp`9-KYp}Qs0bGu7u8_p4bn6Z6Yo%sZ z%j&8m+ZyHHl#VL5eY56Ub?3Jc!7Y+dJ=wNRxp!q*b((F5YVK6cQ2DF6Eb0?I;cobH zkE(pCx_dE%pXo{WVH7`?`kGwp3)MM*zC0*hH7l$y5y?Xs#IM?F=dUo{Z2pBhJL& zDKh|L%;R|048(Xd2op>!Qp^xtHWbsfCff`{zKKJ%8IBso^Uaf3Vn)iqhVNoVsak@n zjaIcW7;c_{!#GrF44yONw1K3yAg>=MC&O+gAj~-Nw3&#}CI!hRRX1|NXmAs}A Qisa$kNq1Y=;-E+V2Mz~pKmY&$ diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt$buildScopeRenderersFactory$1.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt$buildScopeRenderersFactory$1.class index 77d428771afe453ba0ae99e7d9762865b65dbaf9..c8373c3d8b8291d354d74578f0dfe78cc93a68cf 100644 GIT binary patch delta 629 zcmZ9JOHY$Q6o#K^zh3wR(Mm{*w5cGq6f9c7OA#qx#Uf%A6iWpuph**?!obE&m#&*x zxb_z)niOKAxb_#ggtdtacW#WQb!R4-_c=4?I`hN#yRGHhpHE+a5mpts%296j`A>%$ ztsM27oGER+D6AJ>6}F3|jmbiB!@PEe)Nth}X3EY}O;MX(B8nmL^`|$-cGile>{e;@ znbxH(4}CWMM9i`~8qAjTc6xKJ@bZ73xXplh>#TS$lCbe}$t10`e~8O<c+=k);nd*DmqL^b(jP9y!#=K6ocl_6*7mG+oU`` z_1MgMKJ+}1DUfjic>hj&-V!D8J;$==42iFv!rbR6>vFgoXzcVZZ_buYV-=m1YI@y1B^rPgVV(vYv|NliT`JKtLWlC$RyR#j;qOW1fPbgFGvu(~|P0luGAuX1JAM qG9#^IM48HPhv_^@OH(s>j_Eqy)3j)^npRD-CZ)Nr8P`l`9{dGn4SEIu delta 469 zcmXAkOG_J36vuxznTccKi&5evtrC2oCK_L9HL=>Kw&VM)V|F59p}ULS`Uwik1vh?( zHZ4S{v%9>ZCO{Yqm)4HA5B1?*bsl+->!tvCK>2W^yZpaEHB4QBB<}Pt6f+B2&93_7X zTjr0`eG*n)U`rH=Ik)AC2~QHOrAvY= zv29_lWW#F^QIA#D#P0~RSYVl5K@rvo#4n2Cl#F^(Ch9ZoGvo7=XX1J8^MaRUl;#z) bWfD3Q;yF#D=C!6-)1aBqywMajC5`_N^Po)a diff --git a/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt.class b/vendor/kotlinaudio/kotlin-audio/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt.class index 33360a17d0aa37453c6ff10ac056ce80f199cf60..a7c0419f9bc182781b3544d5e93ab39a17e3253b 100644 GIT binary patch delta 17 YcmZ3>xt4Q-G7GB+gCK+YWDS;904DGQ*#H0l delta 17 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℞uF-_-2GWoiqm z{DSIw6%3|KFKEG#N^x`Bf+{gE<76{748fu20@!3Y;z3jJGGqxLf-9tT!q)X}IK<-6 z9RnBqxTdlF4COZT|&D`>6%gi4A&y0J*9vb zgfvS4FWj(zU~=I9jlE`xO{1GM3f3q_+95PRPsVA~snN~9u^?{#=c=|qT#50SM7!S{ zohUkYbBpTL+To4(aWzk1w}{VfRtKF+p0tKvx%-CHbBlKWcMY%j|1t>%ONaMhlZA$e z*VppVW&NkU4X1sf{H+xHWQNA!oM zF_TJckhxi6W+zX!mQlHEa*lDF&oz#7{fwW7GhVujS>%#)3=Rii0KJt(hOyzM z#j;3^VQkAKc^*G=-kA4)B{ zxscmR%VeYQKmLKa&~69b*l#Y>(+Wvg+3noG4>Q-U-yV)JMu*~)jeN20&I;&10QZQe zHgPAOmgy&+fS?;EUMP00H1$Iv(tmW~U5A_tpLntzd8~4rb+Y~+op^iv<>dam;escg z?BfA!UwJZ3yzq&4gGByu^bNlOp0KoT8YNYBZdF)*4~?Id0o7hu2|%>iudi=JLr|_# zUq=EFG<;$5WDX!aq6mR*9R%$#4E!Gn9g>IC1n5;c3FBu0qd+Z@A`NXLJ%7+F(7J{x zAY*Z$$*1t=XqGU!4M4!b~q10FjPRNu~jco!Kya%ZYp>U#YNw)4{n6XyHuwe6rwh$c7_fI9=w$k+T6u&Ndu5SL4XJ9Yyzp(T8yKJdUI1 z8NA+=_jAc|iOo&`4Zke~--=OlEts#Oewlnd1KKtm1O{zCI_g9&$1q;PX(JcOg#&Jb zf05quFA|QmxBRPQ!g*+@{25T=yCDQOBdq{%+l%mGEZIhMwn^9cmzUD!Qk*zj5z@9& zoGDl88Xs?K7D(j|jlbg@jeqs|8Xrk${HYp^&|+xk`C5PbfQmMl;4s6*YmK~x)pSp2 z;?qpRu?$&5rUDTrIi=Ebp7p;`rEF*Ju`lbM}`6q7)ucnYryaI zGUO|{6ftla=WIgv&}IC7hDU_%<-E-*K~+4|7$r+2iQ8Iv2o9Z##TJbp9wHIKe6U}N zJeND!D&4;_^%8ybOqS=Uc(WpLEaf~MF&T73o>D1vcx`VKB<&Tphp$}4ZJ1YUA zI|w4006PosI}uI;NHUXrLvb_^Bov?rJZ$oMA^+y%c3?O;=c7WQKDYTe+zWNZF(0-@ zA*9$0Kb-WC8*?WJq6?n7`3S2)Hv^%ngojS@VgW5iI0K?tgr16Z(JU#YDW$@*PH~M2 z(4-YITcbdY%5)KQ=y*oCMw2v}tkInFW$%{bFHY@#u!Ao%s}a!k!@7s9=BBCoPvS-9 z)4X2)^}4w6X*{Go=U3|y&+u)^_SGWfIi4%-J)j1PQTuqL{v!o(<{2KTG_5!Cjy_X6Fl4W_b2*E0lHQY>hqwV udD%}twG|IL8=nWKetN@CdSV9;5NEb?9$7YBm757l)kjz75gmV9CjS=)|41_c delta 5511 zcmZ8l2Yi%87XQE5o!OF2C%uQ3kdRQM1OW*ofsjB#0%C$fLI@DRB;*Sy5m~B=bWq1a z6)+Y!BtkZT0%t+7Ab4j1Q4Ya!cTy}^(9?V0uE2T8Z}-)C^XAQaGjI0a>toNXiJ8Bc z7K_D}#nwG?Sa<80K2gtj3cJg-k-3CKvZ6Cdq>&A`lN}Z0z$9{FD*0gsxlqsiyGh|5 z3dCa8T|z;4jDpcjA=po0I7sAaKV13|g?cgzWCmGJM!9!;Dhp-4g>Ki+70Cn67l%w( zxGNdhm8|QFWjPJdL0Ph&-ej`{ctS_XbcZ%>p`lc%W5&z!s47fyA|BNQYs&D{TV1eW ziW3ttRVOWxsp}GLQObsCg!Fawy`%Nz@p7Df2JUpC18VgxL#Ra6J2BJL9!doc!-+;- z3(9r(92%r=sgSX1wi9=;ys|P^Ii=39qYNH)>O;?mtl$gfAe-l=5<) z2aB9o$m{1xcdv_H`{h`t3-{rEbJ7QN@dOdAdsN9q{;LS8dUkbfqhZGyeaLN1(%o*h zD80E#+Pd4Yj+bQAH1WzTDXIA`Jd8)2cnF&5Db8V|d(TKcSN*$<+7_gdmm{ z�U;v9fAY0(0$pTlb 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 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab index 74dca0f1b2cb0d0259c55a67eefe519f6e0912b8..f73e3cc69b726f3f0009bd5f2fc5a4d08b99bda9 100644 GIT binary patch delta 109 zcmZorXi(6d?X#PY0SJ^p1Plm37!w#77#e`g|3Cl|QJSbL%?eWJywO*hQDce(14H_( t|NrYG85m?{{{NpO#lUcV`v3n<(hLmqrvCr0BE!HCJ>~!Z&5eu&d;ss59!~%O delta 89 zcmZorXi(6d?X#PY0SM$l1Plm37!`~R3=Kf$e;@#f$WPRjW(6s<-RLXLC@@cgfgyF~ Y|Njk=3=B#${{P=Rkujfd@&ZOl0Ni&J*8l(j diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream index bfab74da75b521eeb104d5766d5341172d2533ee..ddf2197c2a0646eac53b30c7488c5fab4cd8e65f 100644 GIT binary patch delta 133 zcmZojXh_&_P<(PE$0Gh>hGd3(h609E27LxshQi5llB$zig%;T&i~9jp6fxv7Br@bM zWHM9%=@KBF&ydI94rFHn=>VX7KG398hGHPhpLk@gII;l&K*?gDCXfXYlG>XOiq94W E02Ndr$^ZZW delta 16 XcmZojXh_&_P<(T$)NIjBvP+l%J)Q=j diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len index 7acc4520c5e7c359709141b7a368472b7d524f1e..87807f129f4c0e671c78fb23c110e5f14110e283 100644 GIT binary patch literal 8 McmZQz00EgN00ATc0RR91 literal 8 McmZQz00Ain004XdTL1t6 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len index 3085af4de91a1242522803287919159fba2f3c58..1b7c1f8b1c5305091abf1576128bd7f449f9f1fd 100644 GIT binary patch literal 8 McmZQz00G7c005c*k^lez literal 8 McmZQz00G7d004pjdH?_b diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at index 5d2f2380b6a4f0a336f3cde770df3769f165476b..2f82378bc7058f53b1004ed54e156ca262e7c016 100644 GIT binary patch delta 106 zcmZp=#CY;1eG yLluxN0n+&lc?|AAb|ypKWb^c8Oovz}CoqCFa0dV-i-C$jW~rxd+nkuPkOcslDk59} delta 14 VcmX?kldoDjshTOhN}FTje;8(H%};f!7@pNg~hrqNO|)?=>`q}pNkuL delta 63 zcmV-F0Kor%fC7Mk0t5gb00Xm81v~|_Km~vfksv?;V6y=fuLqOB69lug8$?nFZ~*`S VJ!?+vu@R62vygJ=1hdc^hzK!E6U_hs diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab index f81f9ba11b251bd6995661670ef274ad9ba7fb9d..455f5abaf04f8fbc02fd3fc9bee5d1cca2ba69c6 100644 GIT binary patch delta 93 zcmZorXi(6d?X#PY0SMed1Plm37(1947#e`g|3Cl|ai6Fw%?eW3yU{n8N#dUZ0|OIK XszDK`yf7#e`g|3Cl|aha$q%?eW3zR@?BY4brQd)~@~ delta 17 YcmZojXh_&FSz+^kMMl2O6O?io0Yuvd*Z=?k diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len index 952c5d60217eb77f9c37c0e13e3378a4bcebf485..18051b3170558196149b44b8918bd9603f1c8fb2 100644 GIT binary patch literal 8 McmZQz00E^k006=OnE(I) literal 8 McmZQz00BiV002|~EC2ui diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len index 74768ff5c01d5a10fc74c28ae3b1faf8c09e17a0..04a2552d7c9c281f3e4c5e6bf437b571248e3a00 100644 GIT binary patch literal 8 McmZQz00E{Q006`Qy8r+H literal 8 McmZQz00E{200682qW}N^ diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at index ebbcfcf36e48db80c774141ed130d334bfa056fd..664da0c2d80788f7a4eb5cb321492f40edc4cc8a 100644 GIT binary patch delta 9 Qcmey%*v2&B??eYq02QnQ?f?J) delta 6 NcmZo;`pY=sF8~Qh0~7!N diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i index ea783070937b66ede47020a48c884e81fd6cfd65..32926fd82e0e533f5008a019cc950329e55bb6b2 100644 GIT binary patch delta 81 zcmZo@U}|V!VqtJ+V4lp#T(hy^hQZ_y_AD&?3*&EXKFIfjX>)|h3~t5^n?Kt73vsMq kVqoC;<*y7>x?uB*pa5}>1wfJ512da|A{Co=WO;}J0DoT`eEfC7Mk0t5g=00WZ&1AMW7*f68e4g<4TG?)tkn6vRePZE;}1E{m;QcxF@ L5Ced-xN1lh-FXty diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab index 27e38800f9105eba41cadb07a22a2dc4cb5ab3fe..e1301dfdd49081ef3db347625b2a49c2288eb624 100644 GIT binary patch delta 93 zcmZorXi(6d?X#PY0SMed1Plm37(1947#e`g|3Cl|ai6Fw%?eW3yU{n8N#dUZ1H*wM c|Nl2AGB8{|{Qv(SApPdh|Nom8GI{U<0Ht6XSO5S3 delta 72 zcmZorXi(6d?X#PY0SH_`1Plm37z>yf7#e`g|3Cl|aha$q%?eW3zR@?BY4brQd)~@~ delta 17 YcmZojXh_&FSz+^kMMl2O6O?io0Yuvd*Z=?k diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len index 952c5d60217eb77f9c37c0e13e3378a4bcebf485..18051b3170558196149b44b8918bd9603f1c8fb2 100644 GIT binary patch literal 8 McmZQz00E^k006=OnE(I) literal 8 McmZQz00BiV002|~EC2ui diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len index 74768ff5c01d5a10fc74c28ae3b1faf8c09e17a0..04a2552d7c9c281f3e4c5e6bf437b571248e3a00 100644 GIT binary patch literal 8 McmZQz00E{Q006`Qy8r+H literal 8 McmZQz00E{200682qW}N^ diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at index 482516888beace5cf20f024ccdfcc867e8650554..e4d2e09bcce2ba2f01000f15204cdc45f8aae474 100644 GIT binary patch delta 112 zcmX?I_M>`3n$_f9{uPt?ta+GS844$BS}RRX04P`tR17ju*;-?=yDrE;TL43q9`*nL delta 19 bcmexSeY$Kzn$=`?-4&Dhta&CMw>1O+U{MHU diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i index ea783070937b66ede47020a48c884e81fd6cfd65..32926fd82e0e533f5008a019cc950329e55bb6b2 100644 GIT binary patch delta 81 zcmZo@U}|V!VqtJ+V4lp#T(hy^hQZ_y_AD&?3*&EXKFIfjX>)|h3~t5^n?Kt73vsMq kVqoC;<*y7>x?uB*pa5}>1wfJ512da|A{Co=WO;}J0DoT`eEfC7Mk0t5g=00WZ&1AMW7*f68e4g<4TG?)tkn6vRePZE;}1E{m;QcxF@ L5Ced-xN1lh-FXty diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab index 5582096b8142d091056a48cc808b442cff9d3900..8346f5d18364a14d1b831ae4864bd258bc6df0e7 100644 GIT binary patch delta 117 zcmZorXi(6d?X#PY0SJmf1Plm37y>K|3=Kf$e;@#f6iw8XW(6sHywP_uvq6J31H-Ka y|Nk@DFfhEG|Nnm-kY)qYzksyzy#N0<*fKCU%>Dmg%Z`B|dd~mE&plazMG^ot92?63 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream index cecf6e2a00cdd4636a764c58cdddb71c131c44f8..6143aa5d6634443b666c07c465168d3981f4a49c 100644 GIT binary patch delta 176 zcmZozz}T>Waf7Heqv_-e?Dm?)49N`n3Waf7J!=0=-(;mr%|au@+is|P~> diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len index e6f5dea6e8345fed0e7ab4acd5644e40c59dae62..9bce7eec62eb815018bade9880adf3daf9622914 100644 GIT binary patch literal 8 McmZQz00Fx;009aB#Q*>R literal 8 McmZQz00CPm005i-SO5S3 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len index 8a37598ca2fb4ed211de29c5532c9204ba095fd2..5672451b2662d3aca140b9753d2529939598433c 100644 GIT binary patch literal 8 McmZQz009;O001BW6#xJL literal 8 McmZQz00HJ7009O7`~Uy| diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at index ddf6abeedc962c5b1453dfff223204245c413b72..ca61cd49967d9017383db6194c8bee61746383f8 100644 GIT binary patch delta 112 zcmdmWnDO}u#tlAklbIw}Ox~5i!|cjXIQeRV(&U-mV7{UsLq0eGLluxN z0n+&lc?|AAb|ypK7vwnOx69Ae6 BCWQb1 delta 26 icmaEUf^pYj#tlAklhb8aOgbR a^&&W$CJIJue&F{&VRC~X>*k~R)&c-S04t>c delta 93 zcmV-j0HXhZfC7Mk0t5hW00Waz0i3a*cngsrP_t+Zz!9@x0RaKCDIYfi2ptDFMs22g?v++BR5|hAh1p`ic|NjBf0e1iYmnkzawAuXse+fuS0_n{MnKXC-Ej%IZ delta 97 zcmZorXi(6d?X#PY0SG)m1Plm37zdab7#e`g|3Cl|@tCM9%?eW3ztK0ENn(N`1H)$5 e|Nps^7#LnU|Nox`q-%im=7&u3ypumLDFOi2lNrtc diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream index c405bbc1e82143dace1811668c7294638ef963cc..32a90f7849ca19786fe82d02e68e783023d95312 100644 GIT binary patch delta 148 zcmZojXh_%~uE=OQIf2`rznCGJA)lduA(cU&!IhzK@;gQO$yb>joRP)-fGUa@av2gC zau_lhs(^F}kj`hwV{iwuGl6sfP(B}MQYu3+5Q0=QFxY_17e_WH04P%o)C;obg`z4W LSjFaWMO$6~Up*qN delta 17 YcmZojXh_%~uDH2bsg`%MfJzP{06ZoI-v9sr diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len index d9ceda749d22f2ba2b1e2b0bff5af0bbad1eb243..79004b7b2ecb3216f61cd7cb0834545ec515bdfc 100644 GIT binary patch literal 8 McmZQz00HGs00BS&761SM literal 8 McmZQz00E^o007besQ>@~ diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len index 2553f3773f3c3e5e971aa2559a6dd9eeeea6f412..e5d59a21fbb586838a087c4a05d2be90fa7aa5af 100644 GIT binary patch literal 8 McmZQz00E{O0086w+W-In literal 8 McmZQz00E{0007JY!vFvP diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at index 7fbfa680ef8eadedea351080dc2a4a7bd75b1b20..dbb9f34e75be902dc3101190b16847b9f3b4e1ed 100644 GIT binary patch delta 864 zcmZ`$J8#oa6h6KQel>A!X=u~(Dvy@OLK`Ha#u6%&mNbG2r9!Y?JJ*eoV>^Bz5eY?N zV_>NnnL2eu>d3^#z&}9ZKfuDuxs8RQNLaGJ^F7YF5C0r~eJ_1J+!+0+06x-}O`X)p z_*Dx!p{fq}Rdk}FN@oy-T1718DWMaQ5`F4y4eDe~dZ--9MU~J(o6@sOWNof4)2hH$ z6_s;X&7-n_SZmFiFn$5~nU-)a{@1ybJC_j+>{-rxHtChz zxhfQNt|2Oj*9DHSSjmOe9B&|s5jPQW*^WVrqI&txIFj+e$Yqh7;OdmGA=fATnW=o~ zU;fE>U>OBtqMNC$BvIMW?F|LpqW=F}7n_1?k i>~~{M`j$Oyevs~qzS6kwSr8}NgA>`L(#he*_rhO5FS98C delta 13 VcmX@u$#}17!z9biUmeac0{}6}2G#%o diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i index 58636fb98c4e7174b61de4c56ee226701b0e9a1f..42795a6c5bb9a00f1c881897c906b713743a8ccb 100644 GIT binary patch delta 112 zcmV-$0FVEGfC7Mk0t5g{00Waz0i3a*cngzYAp`{f8fV&(5lFLW48ReSfKCJis&e;C zlTiWqvnU@p0s+9YVIfBXlfZ8T1TlWS&6809n6pSEI4S|ivw=>>8VJw=008{a&-b%Y S0U!ahaB5Hm0l2flZ$A*tXep@x delta 85 zcmV-b0IL6hfC7Mk0t5g^00Waz0i3a*cngsrP_t+Zz!9Th0RgipA2z r0id%;BseMou(N?q#~KK@0ssK~(a-m@Q2`(Uvv6uq1p%P5!EZkhqy-&Y diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab index c2fb24192bc30f2345a4a095923f130c70d4d39c..7833f853dfae4f9fa0105942b6f356a2a6e5b9e8 100644 GIT binary patch delta 109 zcmZorXi(6d?X#PY0SJ^p1Plm37!w#77#e`g|3Cl|QJSbL%?eWJywO*hQDce(14G7v q|NrYG85oZ2|NlQpih)55NIOY0FkIaG|G$b11A{4$-rUGozy|;bbRQW2 delta 89 zcmZorXi(6d?X#PY0SM$l1Plm37!`~R3=Kf$e;@#f$WPRjW(6s<-RLXLC@@cgfno9f X|Nk2#85r1s^yZ0-`FxWXFiHXdhGd3(h609E27LxshQi5llB$zig%;T&i~9jp6fxv7Br@bM zWHM9%=@KBF&ydI94rFHn=>VX7KG398hGHPhpLk@gII;l&K*?gDCXfXYlG>XOiq94W E02Ndr$^ZZW delta 16 XcmZojXh_&_P<(T$)NIjBvP+l%J)Q=j diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len index 7acc4520c5e7c359709141b7a368472b7d524f1e..87807f129f4c0e671c78fb23c110e5f14110e283 100644 GIT binary patch literal 8 McmZQz00EgN00ATc0RR91 literal 8 McmZQz00Ain004XdTL1t6 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len index 3085af4de91a1242522803287919159fba2f3c58..1b7c1f8b1c5305091abf1576128bd7f449f9f1fd 100644 GIT binary patch literal 8 McmZQz00G7c005c*k^lez literal 8 McmZQz00G7d004pjdH?_b diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at index 6d8a5968987a7821dac59ef9ac6344c8f84650d3..3706f9cbfe36ce8c0dc6f3ee45164f7abc6438e1 100644 GIT binary patch delta 114 zcmey?#CW2OaRa;kWJ5vM$-eeH%&rWDllM5-uq|Q$0{h7u1?)k>ihd0F3`GpN42cXm z44Dj7K)M7-=QHFnxC7al40)4}IT*3c0co;=rOaL)81^fU2 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i index e5bbf4d04cb98470ac880b08f3a86fdb63304b64..43de4dc168a94f05511c3fdbed2104d85a8f8652 100644 GIT binary patch delta 91 zcmZo@U}|V!Vqs8XVBQ?aYQwtOfwh5e@&;)ZmfVOY-HC!0j1`+1Mb@)3Hf-J~?HoDjshTOhN}FTje;8(H%};f!7@pNg~hrqNO|)?=>`q}pNkuL delta 63 zcmV-F0Kor%fC7Mk0t5gb00Xm81v~|_Km~vfksv?;V6y=fuLqOB69lug8$?nFZ~*`S VJ!?+vu@R62vygJ=1hdc^hzK!E6U_hs diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at index 27944a72c9415179f6cdede907e34442039c132b..54225d53416f5e169d81f7f1379dc35c104cfeb5 100644 GIT binary patch delta 121 zcmew%wnk!u6*tR!W(L#A2btu>i<9#UQuSO59ZOR(^8<()H diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream index ea60ad8c438eed2e6a0bfc99b5ad2419bf89707a..81aac8a3deb6d1be46b936d7b0b8166f34417e31 100644 GIT binary patch delta 115 zcmZorXi(Vjii^>7BBQ)madLh^s-A11V`)leen3%va%ypLe$nKEoC=B%DZl)p+{B#B qs>G7a{5%sjXv8=!tUh@yb};u6=wki-I1W2}KHHgj=1vj6~!S1F$W delta 22 ecmZorXi(Vjii^>DvLd(Y=EK~!ER#QQZw3HZ?g$6~ diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len index 8fa92eb6649e13d78a6bf24117cb5946ff1d5cb3..e0cad9466ac9dd92de5ccf8dc8f580ed7c827f91 100644 GIT binary patch literal 8 McmZQz00His008#@Hq)$ diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at index 109a4065264896e7488b1feb9c3e319510fc1105..525a8655485ad18a9877b2e7b1a89afad385e1cf 100644 GIT binary patch delta 19 UcmZn>UN5qtg>$kurzH#n073Z$_y7O^ delta 7 OcmZ24(jvT}g%bb^NdlMv diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i index 99b6109b67e2a153c200a677c2e2bc48487374f6..977c319b15167db0a19828ca70f41aa4efc9cefa 100644 GIT binary patch delta 78 zcmV-U0I~mofC7Mk0t5gs00WVce6fJoFq7~<1OyMEU)r;wEZ_{YKt8Ym0r<1=KTi?} k@Bsh-4Ej$ak&&RY=u%J@2V!Z delta 54 zcmV-60LlM=fC7Mk0t5gp00WVce6fJoFtczm&FR0RZ%|9%ujn delta 89 zcmZorXi(6d?X#PY0SM$l1Plm37!`~R3=Kf$e;@#f$WPRjW(6s<-RLXLC@@cgfk6Q% V)gZ~hAO)m1Ph`yJo4kNg5&+{e6;}WN diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream index bfab74da75b521eeb104d5766d5341172d2533ee..ddf2197c2a0646eac53b30c7488c5fab4cd8e65f 100644 GIT binary patch delta 133 zcmZojXh_&_P<(PE$0Gh>hGd3(h609E27LxshQi5llB$zig%;T&i~9jp6fxv7Br@bM zWHM9%=@KBF&ydI94rFHn=>VX7KG398hGHPhpLk@gII;l&K*?gDCXfXYlG>XOiq94W E02Ndr$^ZZW delta 16 XcmZojXh_&_P<(T$)NIjBvP+l%J)Q=j diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len index 7acc4520c5e7c359709141b7a368472b7d524f1e..87807f129f4c0e671c78fb23c110e5f14110e283 100644 GIT binary patch literal 8 McmZQz00EgN00ATc0RR91 literal 8 McmZQz00Ain004XdTL1t6 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len index 3085af4de91a1242522803287919159fba2f3c58..1b7c1f8b1c5305091abf1576128bd7f449f9f1fd 100644 GIT binary patch literal 8 McmZQz00G7c005c*k^lez literal 8 McmZQz00G7d004pjdH?_b diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at index ca46eca96af3721f59926a1ed4301608b3e57169..632ffbed3a604eebfa3726627f627babbeda9fe8 100644 GIT binary patch delta 25 YcmaFO_?L0QYatc}Adm->3Sd$Z08pC)oDjshTOhN}FTje;8(H%};f!7@pNg~hrqNO|)?=>`q}pNkuL delta 63 zcmV-F0Kor%fC7Mk0t5gb00Xm81v~|_Km~vfksv?;V6y=fuLqOB69lug8$?nFZ~*`S VJ!?+vu@R62vygJ=1hdc^hzK!E6U_hs diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab index 331188a6fbe35ec4f7dd43ec4de367e21c6304e2..85468cc72866cc288b27f8812c2f8ce79fcd70f4 100644 GIT binary patch delta 105 zcmZorXi(6d?X#PY0SJ^p1Plm37!w#77#e`g|3Cl|QJSbL&8i4wIBxWnXH-f5{Qtik gkoWP^|Nrtpx(-My0BK$z4N^7xSGV=q9@{?1Gi}Q>0vP()Bieb_Ue)&bY wi8+~7i6xo&dG3jsdB_S2846$u#RKw-OI!;>5(`j`n|z)n3}_Gcq`$1}h4$tIEEWwlOdDtf3;(ZMOQ1b{H diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i index aa13d7ea17de65d6547952dd63631bdf2b83dedd..3056823ebe7b025a9440a344e81626e80c44c924 100644 GIT binary patch delta 39 qcmZo@U}|V!Vq#F7$n;ONf{}qio`Hd(0Z1zV=?);RxS5ghbsYes>IcsN delta 21 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 NR3uQ5!2c5K4u9n-mwX{_T2MAZv| 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 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream index 6423b6eda635dc286d131e5b7333b3000c80c698..bd378723a9490542c2467a7f9f7d46358c92cabd 100644 GIT binary patch delta 3706 zcmaJ@TWl0n7@nWKbT7RCg%(Qh1;JpO6rl-FOiH^YE6{GY1z$>Z*qv=hXJ>ZjQez+3 z@<5Ef7#!kB)Q3$-hzX$-F^=^@6d%-J6rzcV2^dTuhIk22-s0uVnX|LAJJY_M^Z(y} zJ?FnH6eA18$kN5zk;oyI7Z1vOdPZOO?ar$gZ?}e{Q8uOWbF9jZO8O_5_}Tqm10h)x zhgmf>tLN_ZUVZ21O$|=bhY$B({qpZftzN>*)eK=Z@MI8-rz-VoPJM|7} zbhi~^#$Q|EpO8Zyt=$cy#_zpwE@aE|;ta~S!-(OOc6!nXIQ ze?J^XI|t!d-TE0$9n#Xg6j!+{x^n==jfFv&TvJ=2n<)9_33zg?Z*44(3P)ie9E#;h zdN}=7XBR3ape7g?6(m-*@2G7Y9(OupbNs%zSjM%qj7f5q75I5p!}P+}bJ*Vm;aPEK({>Jg4r%9_I=iyjwkdA~0uDfTVk z&+vj^U(rAU^36>m_G6AqXv*xU%w{>ovS_3HzMS;DeTzn9NfXmkk~1W~PvOfp1AZkh zm%R!GG*RVktH)puIz0i~nqqnK5~bK>hJj(m;r72>w8$o^$Wp}yRwHdP}CR4K6BPfhK)dY0Y zr}B~#@Hb9!bG(8Jf=YS$jz1#hdSyE&b8I$80bZ|~v&yW0fMUEnp%_xy6xQPov!22x zu|a_=C3LGB*dTK!HC{-^Gh-Z=wi9cn-C;ytx|T2+YPIBDc`G%=KEJ;?xIXH-tQBzMIU*!6BQ>Q*uG*9Bz-`?at1e9j9cfz zp?^<7v;OuOOV1-NzN4R%2ZP@KVmHyZbv1mced>4Lu+ZCALp#2@ezVxKqPr8&rk_7+ zx$6#h%_|HLx4lO6M-n!As6dA&A?!g9`cyXOp~rZT0J)Y7=Nr>-XuW%)qMOrj z&^bI!oH2a@E_IbhIr{n(6r2;=B0|KMZXnb zKZ+z_9lB6}uT~nhR2FwPaw1tmFD<}c=dh(9T+GyfS-9j#2P`=o*B9ViuMba4G`$F2 z0a60wh>kD9_E5l_YzotfK3arL8%jG3!`O}A;^0PMZ95E8YqixQyvVUK72H^6hvZV* zwj^-_DoK`?KuKo0iN($Kn#nOtL?Aj%5@m;NHkx0A;)?nDzzn4}M>eA&R9J+^P|Ibw z&>Il=EUz*R&$4rDpU6vn2enLw!yP4rvAnr$Ie4i53N)e{m*I6w4D~NTGfFQ(TiAwM zw9`v4@AKs}^y3mdWgd*qD{v@aeUuaL!WWh6W2*s6g)~3M)IBGuIL5vaNy9^xVFFoR z)D#!lpA$T&=GNtc22>s|XfAS%Dp>=|w%AEcMl_L?UpVTvYkY1%4-~34kXq`orHu)v zq->6rxhaXM;`q$?s%SL4DykJ-6|vOHM$8?pibrE~QO%9B8cVhj)Uga3{XV?flM{kv z*!|3~={f!aA`_u8nc__NXngGH<3mS|#VYAld8m*njfDzTXxB2l87d_neY^}k1Nb&M zE8%fkA?`4_q3lYZ?I60l3{!i;*!^*NOcMnAb(5T;3E1a~Tw4YhD@n%Q_u(GUUIW*l z$!Px&lwgm!+(xObEUV699p>8SB%xQX!;TpFWKJ@!%JP0+A%Jz5i$o=uZ2IV}>+sr+ YstlHXqRfgj_{TDK5{=$Z;moD~0XatHfB*mh delta 284 zcmWN}J4=EA7zR)tMe~+LR4#(G7(s<(SPo?hRy0Vc*c@0*(Z!}Fw-%8G<-w+J$t48I z;f)$v%dDY`N@Qe%QCLeuOV{%QPFyj>6+?c%R(Pyg^@ZW*cTFMkW@#%Nd>Kum*28a> zqy-OVIkw|lCprgGI%$sKMJ@f~I0RiDgr(s@NF_-hX4|!l+NKaEke@-qcDf0od}G;zt_D7x5^fm?jLNDZ>*6n!e~0=-2HFmZ-et`w2w z%sKXGD8b6961I3>!i2akFr?we6;3#PjjZh4zy^BD=%!M}2uCZ(uu=K{omB*>RM9EL cDsCZ1cZg{jti#Sy9ov$4!e&n9J`k||0qp5>uK)l5 diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len index f4c29f5ce7c856de5f2cd62d8df0f56ba4a872d4..c357136cc30bef52e947f0953e250e3d2a8dd9a6 100644 GIT binary patch literal 8 NcmZQz00PF()&K%50Wbgn literal 8 NcmZQz00PF#Zvg_H0m1+P diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len index 161e5d58d1b42d8c97fa7b8dc9a462b6a97f57a1..5a3d3809b62311bafddc6aca12c2e8a26ae4bf6d 100644 GIT binary patch literal 8 McmZQz00D;z00961xc~qF literal 8 McmZQz00HX*00BM#`~Uy| diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at index 1bc820815b21e64cb7589cfd5f9aada5273e31fe..90a1ba6c644459451984f15754096feb0b0475ce 100644 GIT binary patch literal 13995 zcmeHO+iu%N5S3l0m)LHSMs*}xj%A?l{fi5 zZ;QYGTmJfRw_5zM{CX7rormKB)R|Ey#`ELt1pF|(35T6b!*Lak8_~E0t>cx>9%`(z_SbLfD zr$9D-%8|uxY}@@`10_}!-UcS{>(>V2m~dQS&^r(@XVwkU!4(zfv%%(BS@t2r61A<|GU+HMa52M*n zjG43_iR}5xVQ*jsz+1ms{60$=RwS)6%EdXvT&){aF-dwG&tRLQpM5IM75uivz}2N4 zM*1|1j9tL7=yjtG>yYP&YeA8iWv*b3%JDQgAJ2G>O4A?GI+CyPw$t2cIJQgr+V$l` zd{i&3myaj2d+{Xe)xFq|jyT{SGVnQPfP2htGq4ZL9v&WckZ}LdBVUKz@H5Lm`+bYv zb>P=SWT&L_hO@Z)uAX490-;;yD^ikaLQu&vhDz;-=93DeRk{>^l+2>3J@54OEW>KI z+%VfCU)>{Yu+uj#$g4#@jpse465eWQmVBL6+T)Zss-<*EzR2%d#Uxj*TasbUH%Qt# zoR(z<)vXw(uWD!fy#CDhz^vyv>g+{~>{0X8p3Tziz|2U6B0`?44d-3#w##tV%Xm&H z_HFpyO625m_wAYP@{eaps?D7F=rxz^vntt%Mx5Axy60v`l5I6@kFqpNzRoJ$DIYzn z(H--tBsC{bOJ5?l{GGHkURLX_Byok`->ct^!W)mv!H(+j=75QG$bBL)UqBMNI;h$k{R?xiWEOgA*sDpF(T~TvT5(m!heob`{7cpgIgdE?B z#b)V}uQ3wA{j@mDb>ETs>N{%2`_i|_aBZw=eQPC4?z63`jAkFv$! z?(5NywUq`=5XBp^{7nSkYucMMLrb&#Wuv(N`L6dIL>u>V_*;2}nIiU}S+12XW3*dk zUiVk;Q}n;!Q&BfVPnGi;${A;RI_G`c2Z!&$>y_eOUFry(-RAeOeMHk8)4w(9q>FkX zTNTBcj=j;PNM zs>Ky=K3>CH*$ZTi>OC;Eb7{X9a7R$P{N}5+@A>^=7$@yAiP{2b(=L4NvA4CmEn=H} zR8p^~+tciV?QwRCpq(9QaAa z0>2$g&m{bki7Pl=$>XJTH0r}e?Ky%gRHKWY7FMyYXwv?!&vW>#mgg(S zBS%i^PcLGIPVB;(hE=cp#>q>lq#Ed^B4UpMX6p>ro{0GDycPA+GEDqLUu1a-*XseflquL=>xwS_EG5U z5s-&{xm-f_eER13HBKSAD1$yoE4mY+bVtpS@_{RV3xXXcv_Jmp!~La5l1`MO6=x6y$+;s z_C9`UaSd}#b33_h?6i}y?Hd@=225v1{VU1X36;Y>z^MLQIV@~A^BA!fwr749m?VQ< zC9BGVNA4~LJr^5nE9-!ezeYM^VX*WRDW`Za`yQLpHW+?*4DaTejA;g}mQi$-Gsegwe)YVAr&xQ<=caV753!8?tUUy8j}Ie-96*7x8D($7<-~&JD6y%<%uD z`ChV_wUupwVKZ{@#iqk~3t^pLUtY)6Q0wva}uht7G6LvCT@6Mgluw31T%we%O?m5uNK diff --git a/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i b/vendor/kotlinaudio/kotlin-audio/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i index 5487aa9ece393953c34a5be6f8c17ef51344cb36..f8c248b2286f7ac4e48ca9e29b0456c0d708ed6a 100644 GIT binary patch delta 15394 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 Cg=K*N delta 14540 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 Q5xYwtc&cpqg;K-Fe_2vi{r~^~ delta 291 zcmexxgyqm$78V8uMy83gbW4lpn<7)=(uD9_2wz{SACz`?*e+3u<}Bh%#Ai`w;)3<5w=DWJFk z12=;a0~?Uf1LgBV`FwD`2SXc!0)rQWB2X1Snl1q-Ul6KG2(AlaDyAtwyR{gE(Ugfm r`Jzx`#9;E1uG@320J={c=pTv64cCuuUV6iXaWc~t)yjG_$t7#TPBN^mnW3U8h%slW&Tas3W= diff --git a/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$Companion.class b/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$Companion.class index 125b5cbf69c179b22fcd84f7d75a64d8eb097158..89e98a492a9a9334c9b9139312eda2449e8a4075 100644 GIT binary patch delta 13 VcmZ3=zLb5#cP7Tz$v>FZ0stk^1)2Z= delta 13 VcmZ3=zLb5#cP7S|$v>FZ0stk<1(^T< diff --git a/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$PlayerListener.class b/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$PlayerListener.class index 8780c9a0201afdad7eaf6777eef1fd09910b2504..7dc00fa3a4de5ad4b659f14d5a087720ce345473 100644 GIT binary patch delta 4181 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 delta 2716 zcmZWr4Oo;_7JknihWTNJpBWH_pBY44K~OZ!)YNbR*-2C$Xzc;p)e%QT5puo}l}$(8 zto%V&Z`#&|nn}22TOy96t-4uSnVG9@nOdoBwe7n8WNX%@d%o`zczTxS8Qyc>bM8Io zo}WAOUim9U_IGdX*#}@GjU*Jwfl(fLJ0?-CjE&h*mTQp?dolTBrM)t>3e&7`!%v9F zx<7lO4JK4uF%>fi_wlwodE4%wnbHwGKdKh9t;j?jVcZ=}tUMVVYkAm;Ija4bE;cvD zVo|IJs%&K!yD%m#vcZaZm?1aE>^Cn!qw4uL@>c9{>5ua{mk^9um6duBzqMj19+ju# z8qAO3anN5E+8U zdXu-J#e(PY0>@ZZG`g_DhHX0YM>!_GH1ra-tCxA1)kS4x6UuCOMOVK{FqJx2Yf zRh3^+G8OF>G9h8Hc^~$xF&vQ138e{d;GnvAlMu}(7t9R=IA`)}>xj@H99A`Nb6_PC zMi+X=7u)cze(n*1rAAS+Qd3v0<+IX;qj+DnKgRZ@C8H}e`{TO(U%S)z;0%9Vncp{U zfek0|zM95UT&dF0zbsed{!q97hVvg4b$LPm{9s;rFu>PR#s*EeF7uOu9{dM4toRn+vD2Bp24CLXU`=ga zNqv>C)^Ec1@%htE;|qpoxU+;Tkq3f{a!&P$ZiK>r3kA z`2z($#h=x`fB$SVnF%!6Ojd?|X0j2iTD=v;SM9e^w01d)A@q?~ZBE&knGuuB=Zgvl#!Sc8}#eO#reI$noF@=W6 zo9>jjdzcZhFz0|1g6(>Hdc~=BR^}sxA8}$j+g5l|dJJXO#4#eC1wuCZY8zJkWJ3*a zn8_RV{r`r6xJ%uz^RmZ6p<#SR6ih!L->v?okm|FMTgfIGkSOLOUo1eeXha3$Wbp{5 zi-nlaxJZPsOf1H7u>?(GDb}+56m!jD8McbYuuVLU?cxc%%5s>w*TqU46svGpG~tL? zjbkjI63&%4Bi7=AScmJ3H^h35c}34W8Y=hnOtlZ@3-*E$1`cYoEY8RdO|9tL4toct z??%l5%nGBv6_q-bu@`f?wSi9Uh%RlgTRXQ?>+aId>(?dXC|a>ZY{NRn^zTq?ET7QxgM*i{SSFcyQjNis9jLrW`C(spB6 z7|Xk&8GtJQwt zQ?k@vWdCDx9KY*8YOZ5l2jWu=qjJOe{g8;XgorIPC^s#PXXS^Px%8Zj=$#R2ZAC#F zUTnokHJxGnsSP_?5ZkqAe}S916`nTiZiOr7HS8gNmn?1gb1SSG89~F1F5@Arb{X&N zYQa)fav3}DmsT{WGKKM$u6^earm0dJ-eYx0CqH-^r8@Z{le%<5+dYh3{*CYq2I!U? zUwaFB=_aQ&lOAmHG4Yp^y?f)M25P-^v^(Q2#QunQqCvfkMwh`DZOk{}dH0Bnki)2z zA%IjKsr^V62k;B=25QAY1jL&>N{9LLHjmOf+!;r(K^(=i;uu~MA7F<#jyAS!7bozR zIEmxpBb*keaZ!AXOX3Vq%UL3Ej-tf{$`luAp!kx8i?66q?5F$1C9Twi?SGl zjMie?)%6ZEi-H$#=M8SmhW5809Ay@`_Fl;xT4b0`io>y!k+$8jz3XAb5m@L7mjF|adQGjKE7Fz^6rAx0|(QAYd8 cxqOMDPCyxFpo|Mp#u+H%1e9@~{Fv_{02wnN1ONa4 delta 159 zcmX>ie?)%6ZEi;C$#=M8SY;Um8097x@`_Fl;xT4b1oD)?y!k+$Dv+lJ=Dh*(G=V%V zFwc_Lm{k|Z(*yE6SPg+JBe3{optvcJX9nge@L7mjGO#mRF>o_lGw=XuAx29EQAWGT cxqOMDjzAeFpo}w6#tA6n2$XS~{Fv_{01>ty=>Px# diff --git a/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$ratingType$1.class b/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer$ratingType$1.class index e6a287551d246b07839b58358567d6b1d852e526..777d6f46c1e88748985f0fbf4035383d520972f7 100644 GIT binary patch delta 79 zcmZ1@zD9gQAt&SJ$wi$0jJqZu;%t)K#~{qGpFy1A0D}a>K?X@6Tb^MbgA&7$$+28{ dlE;CvCxEgifwHH7vL}GD$APkECqLmj0RXs@7c~F? delta 79 zcmZ1@zD9gQAt&Re$wi$0j5{YE;%t)K%OK3Kk3pPaKZ69r0R~ARTb^MrgA&8x$+28{ dlE;9u$APjZfU+lnvd4k4$AGeDCO_di0RXm37bO4y diff --git a/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.class b/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.class index 101498768c27fa5431d40e398c543dae0ee4a83d..2397e2ee772d900164f2004d53c96fa45c559693 100644 GIT binary patch delta 1329 zcmXX_dra146n>tQU-2mniZ;D*J0Q!MqGjt0( z^OEJY)HTgnOf$=Ao7u$7m`%)Vn2Bbamt5Xbx6YVh;)Qt1|pDwzLUi#9AqJ67WYHsT^S;Wuo< zC47Mn9L1mb0awt9POG|(hxiLmaD%|#?1t+Mz%>TpCL?f*LvfqK4JWyAhi{;RZ{jXJ zxW^p)%RD?_F&=V09r;bU4ok_JjgH}V>pk)tIfGPn69q;J~}mT zAZK-<&4%V0iq_7&ILqs7DlhI-n?pja`+# zB9F7l@@p*Lr00sVoz1pdXRBr%Rh+{0T~%5<)Ugpgut{5sC-EZcK9-OTG=2z3x^5Ohlr95&sr zF@Q&G(r+-F-)dcTw)2?j97iO-)6VKO&J*a(@6n$pTb3?bBfe8cwjzwD5zQYF&oj2I z6^Z;AITrbBLnY6l-f%t7Vg=7*CEKxy7tF9<(ZY)s=W&#maDu<1otN!O{9)JN3hwhN z1K7z>Ub8>?DtlW*@j74RW%jj*;SIhV!ShVuO{Vh}vv`|@yu%9q!#dv7ElaOD@0*_w z5X<}8RFm&~Y%V`BM;_~=nsR>uLWPJG7&%Zst{oHNhi>9TfcPU={Lo7R5G{Usul8_s zAo@!X21ziIq$kEn2-2h%yb_9Z%gvO)mY?dnb3!EiBnlzY2mK@(@e+e!@)BN?Sd5nb zNR>E@m3|m618v(NWLQn6#3N4$gZ`ncP;FgJ)B5Bs&WMs({EU@=NnQDhj z!!q$=m84^>OvgHzfen(0Z8BR+>vMTg+v-QrtKA!t?b+54syPiq4C%AAo8FeN6~WK_ E4~9o*6#xJL delta 1335 zcmXX_YfzO{6kY4&6+Yt%guF!o^Oc~JB{=3SF}y-oQ7+&ElbDdp82RukdD^jrqY4`v$u^n@891Cy?%g}*!IF0rAMJvu= z7tUfo&f^=LLpv_w1TNt=F5^C~5cr)vaEX5CVj!+E0@pYa*EvdYq7yeb4rlp1{-g{4 zFb{t-AGcVHe_4*(tiT;s<1SypJ=Wts8X0DY)*(o*G?WeEs7zv$9$>t!8NImVp`9-KYp}Qs0bGu7u8_p4bn6Z6Yo%sZ z%j&8m+ZyHHl#VL5eY56Ub?3Jc!7Y+dJ=wNRxp!q*b((F5YVK6cQ2DF6Eb0?I;cobH zkE(pCx_dE%pXo{WVH7`?`kGwp3)MM*zC0*hH7l$y5y?Xs#IM?F=dUo{Z2pBhJL& zDKh|L%;R|048(Xd2op>!Qp^xtHWbsfCff`{zKKJ%8IBso^Uaf3Vn)iqhVNoVsak@n zjaIcW7;c_{!#GrF44yONw1K3yAg>=MC&O+gAj~-Nw3&#}CI!hRRX1|NXmAs}A Qisa$kNq1Y=;-E+V2Mz~pKmY&$ diff --git a/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt$buildScopeRenderersFactory$1.class b/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt$buildScopeRenderersFactory$1.class index 77d428771afe453ba0ae99e7d9762865b65dbaf9..c8373c3d8b8291d354d74578f0dfe78cc93a68cf 100644 GIT binary patch delta 629 zcmZ9JOHY$Q6o#K^zh3wR(Mm{*w5cGq6f9c7OA#qx#Uf%A6iWpuph**?!obE&m#&*x zxb_z)niOKAxb_#ggtdtacW#WQb!R4-_c=4?I`hN#yRGHhpHE+a5mpts%296j`A>%$ ztsM27oGER+D6AJ>6}F3|jmbiB!@PEe)Nth}X3EY}O;MX(B8nmL^`|$-cGile>{e;@ znbxH(4}CWMM9i`~8qAjTc6xKJ@bZ73xXplh>#TS$lCbe}$t10`e~8O<c+=k);nd*DmqL^b(jP9y!#=K6ocl_6*7mG+oU`` z_1MgMKJ+}1DUfjic>hj&-V!D8J;$==42iFv!rbR6>vFgoXzcVZZ_buYV-=m1YI@y1B^rPgVV(vYv|NliT`JKtLWlC$RyR#j;qOW1fPbgFGvu(~|P0luGAuX1JAM qG9#^IM48HPhv_^@OH(s>j_Eqy)3j)^npRD-CZ)Nr8P`l`9{dGn4SEIu delta 469 zcmXAkOG_J36vuxznTccKi&5evtrC2oCK_L9HL=>Kw&VM)V|F59p}ULS`Uwik1vh?( zHZ4S{v%9>ZCO{Yqm)4HA5B1?*bsl+->!tvCK>2W^yZpaEHB4QBB<}Pt6f+B2&93_7X zTjr0`eG*n)U`rH=Ik)AC2~QHOrAvY= zv29_lWW#F^QIA#D#P0~RSYVl5K@rvo#4n2Cl#F^(Ch9ZoGvo7=XX1J8^MaRUl;#z) bWfD3Q;yF#D=C!6-)1aBqywMajC5`_N^Po)a diff --git a/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt.class b/vendor/kotlinaudio/kotlin-audio/build/tmp/kotlin-classes/debug/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactoryKt.class index 33360a17d0aa37453c6ff10ac056ce80f199cf60..a7c0419f9bc182781b3544d5e93ab39a17e3253b 100644 GIT binary patch delta 17 YcmZ3>xt4Q-G7GB+gCK+YWDS;904DGQ*#H0l delta 17 YcmZ3>xt4Q-G7GB!gCK* playerEventHolder.updateAudioItemTransition( AudioItemTransitionReason.AUTO(oldPosition) diff --git a/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/EqAudioProcessor.kt b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/EqAudioProcessor.kt new file mode 100644 index 0000000..e5fbcc7 --- /dev/null +++ b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/EqAudioProcessor.kt @@ -0,0 +1,258 @@ +package com.doublesymmetry.kotlinaudio.scope + +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.audio.AudioProcessor +import com.google.android.exoplayer2.audio.BaseAudioProcessor +import expo.modules.astrascope.EqBridge +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sin +import kotlin.math.sqrt + +/** + * Parametric EQ as an ExoPlayer AudioProcessor (M4). Reads raw band params from + * [EqBridge] (set from JS) and computes Audio-EQ-Cookbook biquad coefficients at + * the real stream sample rate — mirroring Web Audio's BiquadFilterNode on desktop. + * A cascade of transposed-direct-form-II biquads runs per channel after a preamp. + * + * Passthrough (bit-exact) when the EQ is disabled or has no active bands and unity + * preamp, so toggling EQ off is lossless. Handles PCM float and 16-bit; coefficients + * are rebuilt only when [EqBridge.revision] changes (cheap, off the per-sample path). + */ +class EqAudioProcessor : BaseAudioProcessor() { + private var channels = 0 + private var sampleRate = 0f + + private var lastRevision = Int.MIN_VALUE + private var enabled = false + private var preamp = 1f + private var bandCount = 0 + private var coeffs = FloatArray(0) // 5 per band: b0,b1,b2,a1,a2 (a0-normalized) + private var z1 = FloatArray(0) // bandCount * channels + private var z2 = FloatArray(0) + + private var floatScratch = FloatArray(0) + + override fun onConfigure( + inputAudioFormat: AudioProcessor.AudioFormat + ): AudioProcessor.AudioFormat { + channels = inputAudioFormat.channelCount + sampleRate = inputAudioFormat.sampleRate.toFloat() + lastRevision = Int.MIN_VALUE // force a rebuild on the next buffer + return inputAudioFormat + } + + override fun queueInput(inputBuffer: ByteBuffer) { + val remaining = inputBuffer.remaining() + if (remaining <= 0) return + + rebuildIfNeeded() + + val passthrough = !enabled || channels <= 0 || (bandCount == 0 && preamp == 1f) + if (passthrough) { + val out = replaceOutputBuffer(remaining) + out.put(inputBuffer) + out.flip() + return + } + + when (inputAudioFormat.encoding) { + C.ENCODING_PCM_FLOAT -> processFloat(inputBuffer, remaining) + C.ENCODING_PCM_16BIT -> process16(inputBuffer, remaining) + else -> { + val out = replaceOutputBuffer(remaining) + out.put(inputBuffer) + out.flip() + } + } + } + + override fun onFlush() { + z1.fill(0f) + z2.fill(0f) + } + + private fun rebuildIfNeeded() { + val rev = EqBridge.revision + if (rev == lastRevision) return + lastRevision = rev + + enabled = EqBridge.enabled + preamp = EqBridge.preampLinear + val params = EqBridge.bands + val total = params.size / 5 + + var active = 0 + for (i in 0 until total) if (params[i * 5 + 4] != 0f) active++ + + // Reset filter state only when the band count changes (avoid clicks on tweaks). + if (active != bandCount) { + bandCount = active + coeffs = FloatArray(active * 5) + z1 = FloatArray(active * channels.coerceAtLeast(1)) + z2 = FloatArray(active * channels.coerceAtLeast(1)) + } + + var bi = 0 + for (i in 0 until total) { + if (params[i * 5 + 4] == 0f) continue + computeCoeffs( + params[i * 5].toInt(), + params[i * 5 + 1], + params[i * 5 + 2], + params[i * 5 + 3], + sampleRate, + coeffs, + bi * 5 + ) + bi++ + } + } + + private fun processFloat(inputBuffer: ByteBuffer, remaining: Int) { + val fb = inputBuffer.asFloatBuffer() + val n = fb.remaining() + if (n <= 0) return + if (floatScratch.size < n) floatScratch = FloatArray(n) + fb.get(floatScratch, 0, n) + inputBuffer.position(inputBuffer.limit()) // mark input consumed + + processSamples(floatScratch, n) + + val out = replaceOutputBuffer(n * 4).order(ByteOrder.nativeOrder()) + out.asFloatBuffer().put(floatScratch, 0, n) + out.position(n * 4) + out.flip() + } + + private fun process16(inputBuffer: ByteBuffer, remaining: Int) { + val sb = inputBuffer.asShortBuffer() + val n = sb.remaining() + if (n <= 0) return + if (floatScratch.size < n) floatScratch = FloatArray(n) + var i = 0 + while (i < n) { + floatScratch[i] = sb.get(i) / 32768f + i++ + } + inputBuffer.position(inputBuffer.limit()) + + processSamples(floatScratch, n) + + val out = replaceOutputBuffer(n * 2).order(ByteOrder.nativeOrder()) + val osb = out.asShortBuffer() + i = 0 + while (i < n) { + val v = (floatScratch[i] * 32768f).roundToInt().coerceIn(-32768, 32767) + osb.put(v.toShort()) + i++ + } + out.position(n * 2) + out.flip() + } + + /** Apply preamp + the biquad cascade in place over interleaved samples. */ + private fun processSamples(buf: FloatArray, n: Int) { + val ch = channels + val bc = bandCount + val pre = preamp + var c = 0 + var i = 0 + while (i < n) { + var x = buf[i] * pre + var b = 0 + while (b < bc) { + val co = b * 5 + val b0 = coeffs[co] + val b1 = coeffs[co + 1] + val b2 = coeffs[co + 2] + val a1 = coeffs[co + 3] + val a2 = coeffs[co + 4] + val si = b * ch + c + val s1 = z1[si] + val s2 = z2[si] + val y = b0 * x + s1 + z1[si] = b1 * x - a1 * y + s2 + z2[si] = b2 * x - a2 * y + x = y + b++ + } + buf[i] = x + c++ + if (c == ch) c = 0 + i++ + } + } + + /** + * Audio-EQ-Cookbook biquad coefficients (a0-normalized) into out[off..off+4]. + * Type ordinals match EQ_BAND_TYPE_ORDINAL in src/audio/eq.ts: + * 0 lowshelf, 1 peaking, 2 highshelf, 3 highpass, 4 lowpass. + */ + private fun computeCoeffs( + type: Int, + freq: Float, + gainDb: Float, + q: Float, + sr: Float, + out: FloatArray, + off: Int + ) { + if (sr <= 0f) { + out[off] = 1f; out[off + 1] = 0f; out[off + 2] = 0f; out[off + 3] = 0f; out[off + 4] = 0f + return + } + val w0 = 2.0 * PI * freq / sr + val cosW0 = cos(w0) + val sinW0 = sin(w0) + val a = 10.0.pow(gainDb / 40.0) + val alpha = sinW0 / (2.0 * q.coerceAtLeast(0.0001f)) + + var b0 = 1.0; var b1 = 0.0; var b2 = 0.0 + var a0 = 1.0; var a1 = 0.0; var a2 = 0.0 + + when (type) { + 1 -> { // peaking + b0 = 1 + alpha * a; b1 = -2 * cosW0; b2 = 1 - alpha * a + a0 = 1 + alpha / a; a1 = -2 * cosW0; a2 = 1 - alpha / a + } + 0 -> { // lowshelf + val sqrtA = sqrt(a) + b0 = a * (a + 1 - (a - 1) * cosW0 + 2 * sqrtA * alpha) + b1 = 2 * a * (a - 1 - (a + 1) * cosW0) + b2 = a * (a + 1 - (a - 1) * cosW0 - 2 * sqrtA * alpha) + a0 = a + 1 + (a - 1) * cosW0 + 2 * sqrtA * alpha + a1 = -2 * (a - 1 + (a + 1) * cosW0) + a2 = a + 1 + (a - 1) * cosW0 - 2 * sqrtA * alpha + } + 2 -> { // highshelf + val sqrtA = sqrt(a) + b0 = a * (a + 1 + (a - 1) * cosW0 + 2 * sqrtA * alpha) + b1 = -2 * a * (a - 1 + (a + 1) * cosW0) + b2 = a * (a + 1 + (a - 1) * cosW0 - 2 * sqrtA * alpha) + a0 = a + 1 - (a - 1) * cosW0 + 2 * sqrtA * alpha + a1 = 2 * (a - 1 - (a + 1) * cosW0) + a2 = a + 1 - (a - 1) * cosW0 - 2 * sqrtA * alpha + } + 4 -> { // lowpass + b0 = (1 - cosW0) / 2; b1 = 1 - cosW0; b2 = (1 - cosW0) / 2 + a0 = 1 + alpha; a1 = -2 * cosW0; a2 = 1 - alpha + } + 3 -> { // highpass + b0 = (1 + cosW0) / 2; b1 = -(1 + cosW0); b2 = (1 + cosW0) / 2 + a0 = 1 + alpha; a1 = -2 * cosW0; a2 = 1 - alpha + } + } + + val inv = 1.0 / a0 + out[off] = (b0 * inv).toFloat() + out[off + 1] = (b1 * inv).toFloat() + out[off + 2] = (b2 * inv).toFloat() + out[off + 3] = (a1 * inv).toFloat() + out[off + 4] = (a2 * inv).toFloat() + } +} diff --git a/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/NormalizationGainProcessor.kt b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/NormalizationGainProcessor.kt new file mode 100644 index 0000000..be74d12 --- /dev/null +++ b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/NormalizationGainProcessor.kt @@ -0,0 +1,80 @@ +package com.doublesymmetry.kotlinaudio.scope + +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.audio.AudioProcessor +import com.google.android.exoplayer2.audio.BaseAudioProcessor +import expo.modules.astrascope.GainBridge +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.roundToInt + +/** + * Applies the per-track normalization / ReplayGain gain from [GainBridge] (set from + * JS on track/settings change). Sits FIRST in the chain — before the scope taps — + * so the visualizers see normalized levels (the user's "better for scopes" goal). + * + * Bit-exact passthrough when the gain is unity. Handles PCM float and 16-bit; the + * 16-bit path clamps to int16 range. The gain resolver already backs off so the + * post-gain peak stays <= 0.98, so clipping should not occur here in practice. + */ +class NormalizationGainProcessor : BaseAudioProcessor() { + private var floatScratch = FloatArray(0) + + override fun onConfigure( + inputAudioFormat: AudioProcessor.AudioFormat + ): AudioProcessor.AudioFormat = inputAudioFormat + + override fun queueInput(inputBuffer: ByteBuffer) { + val remaining = inputBuffer.remaining() + if (remaining <= 0) return + + val gain = GainBridge.linearGain + if (gain == 1f) { + val out = replaceOutputBuffer(remaining) + out.put(inputBuffer) + out.flip() + return + } + + when (inputAudioFormat.encoding) { + C.ENCODING_PCM_FLOAT -> { + val fb = inputBuffer.asFloatBuffer() + val n = fb.remaining() + if (n <= 0) return + if (floatScratch.size < n) floatScratch = FloatArray(n) + fb.get(floatScratch, 0, n) + inputBuffer.position(inputBuffer.limit()) + var i = 0 + while (i < n) { + floatScratch[i] = floatScratch[i] * gain + i++ + } + val out = replaceOutputBuffer(n * 4).order(ByteOrder.nativeOrder()) + out.asFloatBuffer().put(floatScratch, 0, n) + out.position(n * 4) + out.flip() + } + C.ENCODING_PCM_16BIT -> { + val sb = inputBuffer.asShortBuffer() + val n = sb.remaining() + if (n <= 0) return + val out = replaceOutputBuffer(n * 2).order(ByteOrder.nativeOrder()) + val osb = out.asShortBuffer() + var i = 0 + while (i < n) { + val v = (sb.get(i) * gain).roundToInt().coerceIn(-32768, 32767) + osb.put(v.toShort()) + i++ + } + inputBuffer.position(inputBuffer.limit()) + out.position(n * 2) + out.flip() + } + else -> { + val out = replaceOutputBuffer(remaining) + out.put(inputBuffer) + out.flip() + } + } + } +} diff --git a/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/PostEqTapAudioProcessor.kt b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/PostEqTapAudioProcessor.kt new file mode 100644 index 0000000..53cdfd2 --- /dev/null +++ b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/PostEqTapAudioProcessor.kt @@ -0,0 +1,66 @@ +package com.doublesymmetry.kotlinaudio.scope + +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.audio.AudioProcessor +import com.google.android.exoplayer2.audio.BaseAudioProcessor +import expo.modules.astrascope.ScopeBridge +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Pass-through tap placed AFTER the EQ processor (M4). Identical to + * ScopeTapAudioProcessor but pushes to the native post-EQ ring (ring #2) which + * feeds the EQ screen's response-curve spectrum overlay. Gated by + * `ScopeBridge.active && ScopeBridge.postEqActive` so it costs ~zero unless the + * EQ screen is open and the app is foregrounded + playing. + */ +class PostEqTapAudioProcessor : BaseAudioProcessor() { + private var scratch = FloatArray(0) + + override fun onConfigure( + inputAudioFormat: AudioProcessor.AudioFormat + ): AudioProcessor.AudioFormat = inputAudioFormat + + override fun queueInput(inputBuffer: ByteBuffer) { + val remaining = inputBuffer.remaining() + if (remaining <= 0) return + + if (ScopeBridge.active && ScopeBridge.postEqActive) { + tap(inputBuffer) + } + + val out = replaceOutputBuffer(remaining) + out.put(inputBuffer) + out.flip() + } + + private fun tap(inputBuffer: ByteBuffer) { + val channels = inputAudioFormat.channelCount + if (channels <= 0) return + val dup = inputBuffer.duplicate().order(ByteOrder.nativeOrder()) + + when (inputAudioFormat.encoding) { + C.ENCODING_PCM_FLOAT -> { + val fb = dup.asFloatBuffer() + val n = fb.remaining() + if (n <= 0) return + if (scratch.size < n) scratch = FloatArray(n) + fb.get(scratch, 0, n) + ScopeBridge.nativePushFramesPostEq(scratch, n / channels, channels) + } + C.ENCODING_PCM_16BIT -> { + val sb = dup.asShortBuffer() + val n = sb.remaining() + if (n <= 0) return + if (scratch.size < n) scratch = FloatArray(n) + var i = 0 + while (i < n) { + scratch[i] = sb.get(i) / 32768f + i++ + } + ScopeBridge.nativePushFramesPostEq(scratch, n / channels, channels) + } + else -> { /* unsupported PCM encoding — forward only */ } + } + } +} diff --git a/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactory.kt b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactory.kt index 09acee3..5e2f480 100644 --- a/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactory.kt +++ b/vendor/kotlinaudio/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/scope/ScopeRenderersFactory.kt @@ -7,10 +7,14 @@ import com.google.android.exoplayer2.audio.AudioSink import com.google.android.exoplayer2.audio.DefaultAudioSink /** - * A DefaultRenderersFactory whose audio sink runs our pre-EQ PCM tap as the - * first (and, for M3, only) AudioProcessor. Float-output / playback-param - * capabilities are preserved by forwarding the flags. M4 will prepend the EQ - * AudioProcessor (and add a second post-EQ tap) to this same chain. + * A DefaultRenderersFactory whose audio sink runs the M4 processing chain. + * Order matters: + * 1. NormalizationGainProcessor — per-track gain (before the taps, so the + * scopes see normalized levels). + * 2. ScopeTapAudioProcessor — the pre-EQ tap (post-normalization) → scope ring #1. + * 3. EqAudioProcessor — preamp + parametric biquad chain. + * 4. PostEqTapAudioProcessor — post-EQ tap → scope ring #2 (EQ screen overlay). + * Float-output / playback-param capabilities are preserved by forwarding the flags. */ fun buildScopeRenderersFactory(context: Context): DefaultRenderersFactory = object : DefaultRenderersFactory(context) { @@ -23,6 +27,13 @@ fun buildScopeRenderersFactory(context: Context): DefaultRenderersFactory = DefaultAudioSink.Builder(context) .setEnableFloatOutput(enableFloatOutput) .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) - .setAudioProcessors(arrayOf(ScopeTapAudioProcessor())) + .setAudioProcessors( + arrayOf( + NormalizationGainProcessor(), + ScopeTapAudioProcessor(), + EqAudioProcessor(), + PostEqTapAudioProcessor() + ) + ) .build() }