m3, ui/ux, and more

This commit is contained in:
Boof2015
2026-06-17 16:12:51 -04:00
parent 0afbb37c08
commit 9f2ce3bb75
533 changed files with 5933 additions and 147 deletions
@@ -3,12 +3,16 @@ package expo.modules.astralibraryscanner
import android.content.Context
import android.content.Intent
import android.media.AudioFormat
import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import java.nio.ByteOrder
import kotlin.math.max
import kotlin.math.sqrt
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
@@ -37,6 +41,9 @@ class AstraLibraryScannerModule : Module() {
// read and hashed once, not once per track.
private val coverHashMemo = ConcurrentHashMap<String, String>()
// Waveform decode is whole-file and CPU-heavy; throttle concurrent decodes.
private val waveformSemaphore = Semaphore(2)
override fun definition() = ModuleDefinition {
Name("AstraLibraryScanner")
@@ -55,6 +62,15 @@ class AstraLibraryScannerModule : Module() {
}
}
// Offline waveform peaks for the seek bar: full PCM decode -> RMS per bin,
// normalized to [0,1]. Heavy (whole-file decode), so cap concurrency and
// 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) }
}
}
Function("getArtworkDirPath") {
artworkDir().absolutePath
}
@@ -267,6 +283,158 @@ class AstraLibraryScannerModule : Module() {
return result
}
// ---------------------------------------------------------------------------
// 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 {
val context = requireContext()
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 FloatArray(0)
extractor.selectTrack(trackIndex)
val sampleRate =
if (format.containsKey(MediaFormat.KEY_SAMPLE_RATE)) format.getInteger(MediaFormat.KEY_SAMPLE_RATE) else 44100
val durationUs =
if (format.containsKey(MediaFormat.KEY_DURATION)) format.getLong(MediaFormat.KEY_DURATION) else 0L
val totalFrames = max(1L, (durationUs / 1_000_000.0 * sampleRate).toLong())
var channelCount =
if (format.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) else 2
var pcmFloat = false
val sumSquares = DoubleArray(bins)
val counts = LongArray(bins)
codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!)
codec.configure(format, null, null, 0)
codec.start()
val info = MediaCodec.BufferInfo()
var sawInputEOS = false
var sawOutputEOS = false
var frame = 0L
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())
frame = accumulate(out, pcmFloat, channelCount, bins, totalFrames, frame, sumSquares, counts)
}
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
}
}
}
val peaks = FloatArray(bins)
var globalMax = 0.0
for (i in 0 until bins) {
if (counts[i] > 0) {
val rms = sqrt(sumSquares[i] / counts[i])
peaks[i] = rms.toFloat()
if (rms > globalMax) globalMax = rms
}
}
if (globalMax > 0) {
for (i in 0 until bins) peaks[i] = (peaks[i] / globalMax).toFloat()
}
return peaks
} catch (_: Throwable) {
return FloatArray(0)
} finally {
try { codec?.stop() } catch (_: Throwable) {}
try { codec?.release() } catch (_: Throwable) {}
try { extractor.release() } catch (_: Throwable) {}
}
}
// Folds one decoded PCM buffer into the per-bin RMS accumulators. Handles
// 16-bit (default) and float PCM. Returns the updated running frame index.
private fun accumulate(
out: java.nio.ByteBuffer,
pcmFloat: Boolean,
channelCount: Int,
bins: Int,
totalFrames: Long,
startFrame: Long,
sumSquares: DoubleArray,
counts: LongArray
): Long {
var frame = startFrame
if (pcmFloat) {
val fb = out.asFloatBuffer()
val n = fb.remaining()
var k = 0
while (k < n) {
val bin = ((frame.toDouble() / totalFrames) * bins).toInt().coerceIn(0, bins - 1)
var c = 0
while (c < channelCount && k < n) {
val s = fb.get(k).toDouble()
sumSquares[bin] += s * s
k++; c++
}
counts[bin] += c.toLong()
frame++
}
} else {
val sb = out.asShortBuffer()
val n = sb.remaining()
var k = 0
while (k < n) {
val bin = ((frame.toDouble() / totalFrames) * bins).toInt().coerceIn(0, bins - 1)
var c = 0
while (c < channelCount && k < n) {
val s = sb.get(k) / 32768.0
sumSquares[bin] += s * s
k++; c++
}
counts[bin] += c.toLong()
frame++
}
}
return frame
}
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.