diff --git a/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt b/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt index 80f17cc..1135278 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt @@ -12,7 +12,7 @@ interface SessionRepository { suspend fun connectSyncClient() suspend fun requestData() - + fun observeSyncClientState(): Flow fun observeProgressbar(): Flow diff --git a/clients/web/dsp-asm/asconfig.json b/clients/web/dsp-asm/asconfig.json new file mode 100644 index 0000000..a023a23 --- /dev/null +++ b/clients/web/dsp-asm/asconfig.json @@ -0,0 +1,24 @@ +{ + "targets": { + "debug": { + "outFile": "build/debug.wasm", + "textFile": "build/debug.wat", + "sourceMap": true, + "debug": true, + "bindings": "esm" + }, + "release": { + "outFile": "build/release.wasm", + "textFile": "build/release.wat", + "sourceMap": true, + "optimizeLevel": 3, + "shrinkLevel": 0, + "converge": false, + "noAssert": false + } + }, + "options": { + "bindings": "raw", + "enable": ["simd"] + } +} diff --git a/clients/web/dsp-asm/assembly/fastutils.ts b/clients/web/dsp-asm/assembly/fastutils.ts new file mode 100644 index 0000000..072feb6 --- /dev/null +++ b/clients/web/dsp-asm/assembly/fastutils.ts @@ -0,0 +1,165 @@ +// uwr fastutils for assemblyscript +// a small and application specific wrapper library for otherwise unsafe ops by hazel + +/** UNSAFE CONTRACT: len MUST be a multiple of 4, and within buffer bounds */ +export function listAbsMax_f32_UNCHECKED(buf: Float32Array, len: i32): f32 { + let running = v128.splat(0); + + for (let i = 0; i < len; i += sizeof() / sizeof()) { + // pmax is faster than max due to not treating nulls etc + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/code-generator-x64.cc;drc=8ab75a56a24f34d4f582261c99939ffa1446a3b7;l=2712 + running = f32x4.pmax(running, f32x4.abs(f32x4_load(buf, i))); + } + + return Mathf.max( + Mathf.max(f32x4.extract_lane(running, 0), f32x4.extract_lane(running, 1)), + Mathf.max(f32x4.extract_lane(running, 2), f32x4.extract_lane(running, 3)), + ); +} + +/** UNSAFE CONTRACT: len MUST be a multiple of 4, and within buffer bounds */ +export function listMax_f32_UNCHECKED(buf: Float32Array, len: i32): f32 { + let running = v128.splat(f32.MIN_VALUE); + + for (let i = 0; i < len; i += sizeof() / sizeof()) { + running = f32x4.pmax(running, f32x4_load(buf, i)); + } + + return Mathf.max( + Mathf.max(f32x4.extract_lane(running, 0), f32x4.extract_lane(running, 1)), + Mathf.max(f32x4.extract_lane(running, 2), f32x4.extract_lane(running, 3)), + ); +} + +/** finds the maximum element of the buffer with SIMD */ +export function listMax_f32(buf: Float32Array): f32 { + assert(buf.length, "cannot get max of an empty buffer"); + + const simdSafeLen = 4 * (buf.length / 4); + + // if buffer is smaller than 4, this will return f32.MIN_VALUE + let running = listMax_f32_UNCHECKED(buf, simdSafeLen); + + for (let i = simdSafeLen; i < buf.length; i++) { + running = Mathf.max(running, buf[i]); + } + + return running; +} + +/** finds the maximum element of the absolute values of the buffer with SIMD */ +export function listAbsMax_f32(buf: Float32Array): f32 { + assert(buf.length, "cannot get abs max of an empty buffer"); + + const simdSafeLen = 4 * (buf.length / 4); + + // if buffer is smaller than 4, this will return 0 + let running = listAbsMax_f32_UNCHECKED(buf, simdSafeLen); + + for (let i = simdSafeLen; i < buf.length; i++) { + running = Mathf.max(running, Mathf.abs(buf[i])); + } + + return running; +} + +/** UNSAFE CONTRACT: len MUST be a multiple of 4, and within buffer bounds */ +export function listSqSum_f32_UNCHECKED(buf: Float32Array, len: i32): f32 { + let running = v128.splat(0); + + for (let i = 0; i < len; i += sizeof() / sizeof()) { + const vec = f32x4_load(buf, i) + running = f32x4.add(running, f32x4.mul(vec, vec)); + } + + return f32x4.extract_lane(running, 0) + + f32x4.extract_lane(running, 1) + + f32x4.extract_lane(running, 2) + + f32x4.extract_lane(running, 3); +} + +/** finds the sum of the squares of the buffer with SIMD */ +export function listSqSum_f32(buf: Float32Array): f32 { + assert(buf.length, "cannot get abs max of an empty buffer"); + + const simdSafeLen = 4 * (buf.length / 4); + + // if buffer is smaller than 4, this will return 0 + let running = listSqSum_f32_UNCHECKED(buf, simdSafeLen); + + for (let i = simdSafeLen; i < buf.length; i++) { + running += buf[i] * buf[i]; + } + + return running; +} + +/** negates a buffer with SIMD */ +export function listNeg_f32(buf: Float32Array): void { + if (!buf.length) return; + + const simdSafeLen = 4 * (buf.length / 4); + for (let i = 0; i < simdSafeLen; i += sizeof() / sizeof()) { + f32x4_store(f32x4.neg(f32x4_load(buf, i)), buf, i); + } + + for (let i = simdSafeLen; i < buf.length; i++) { + buf[i] = -buf[i]; + } +} + +/** checked fast memory copy between two buffers. + * pass two buffers, the bytes per element, the offsets for where to write to, and the number of elemments. + * a raw byte copy can be achieved with sz = 1. + * takes isize not usize for checking reasons. */ +export function memcpy( + src: ArrayBuffer, + dst: ArrayBuffer, + srcOset: isize, + dstOset: isize, + elSz: isize, + n: isize, +): void { + assert((srcOset + n) * elSz <= src.byteLength, "source will overflow"); + assert((dstOset + n) * elSz <= dst.byteLength, "destination will overflow"); + assert(elSz > 0, "element size must be >0"); + assert(srcOset >= 0, "source offset must be >=0"); + assert(dstOset >= 0, "destination offset must be >=0"); + assert(n >= 0, "elems copied must be >=0"); + + // https://github.com/AssemblyScript/assemblyscript/issues/743#issuecomment-518022808 + const srcPtr = changetype(src); + const dstPtr = changetype(dst); + + if (n !== 0) { + memory.copy(dstPtr + elSz * dstOset, srcPtr + elSz * srcOset, elSz * n); + } +} + +/** checked fast memory copy between two float32arrays */ +@inline +export function memcpy_f32( + src: Float32Array, + dst: Float32Array, + srcOset: isize, + dstOset: isize, + n: isize, +): void { + return memcpy(src.buffer, dst.buffer, srcOset, dstOset, Float32Array.BYTES_PER_ELEMENT, n); +} + +/** checked load f32x4 from Float32Array */ +export function f32x4_load(buf: Float32Array, i: isize): v128 { + assert(i >= 0, "index must be >=0"); + assert(i + 3 < buf.length, "index will overflow"); + + return v128.load(buf.dataStart + i * Float32Array.BYTES_PER_ELEMENT); +} + +/** checked store f32x4 to Float32Array */ +export function f32x4_store(v: v128, buf: Float32Array, i: isize): void { + assert(i >= 0, "index must be >=0"); + assert(i + 3 < buf.length, "index will overflow"); + + v128.store(buf.dataStart + i * Float32Array.BYTES_PER_ELEMENT, v); +} diff --git a/clients/web/dsp-asm/assembly/fft.ts b/clients/web/dsp-asm/assembly/fft.ts new file mode 100644 index 0000000..e07e25e --- /dev/null +++ b/clients/web/dsp-asm/assembly/fft.ts @@ -0,0 +1,280 @@ +// a PARTIAL assemblyscript port of fft.js by Fedor Indutny +// which is licensed under the MIT license +// ported by Hazel Atkinson 2024 +// only implements realTransform. + +/* +Copyright (c) 2021 Fedor Indutny + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +export class FFT { + size: i32; + private _csize: i32; + table: Float32Array; + private _width: i32; + private _bitrev: Int32Array; + private _out: Float32Array | null; + private _data: Float32Array | null; + private _inv: i32; + + constructor(size: i32) { + this._out = null; + this._data = null; + this._inv = 0; + + this.size = size; + if (size <= 1 || (size & (size - 1)) !== 0) { + throw new Error("FFT size must be a power of 2 and bigger than 1"); + } + + this._csize = size << 1; + + const table = new Float32Array(size * 2); + for (let i = 0; i < table.length; i += 2) { + const angle = (Mathf.PI * i) / size; + table[i] = Mathf.cos(angle); + table[i + 1] = -Mathf.sin(angle); + } + this.table = table; + + let power = 0; + for (let t = 1; t < size; t <<= 1) power++; + + // Calculate initial step's width: + // * If we are full radix-4 - it is 2x smaller to give inital len=8 + // * Otherwise it is the same as `power` to give len=4 + const width = power % 2 === 0 ? power - 1 : power; + this._width = width; + + this._bitrev = new Int32Array(1 << width); + for (let j = 0; j < this._bitrev.length; j++) { + for (let shift = 0; shift < this._width; shift += 2) { + const revShift = this._width - shift - 2; + this._bitrev[j] |= ((j >>> shift) & 3) << revShift; + } + } + } + + createComplexArray(): Float32Array { + return new Float32Array(this._csize); + } + + realTransform(out: Float32Array, data: Float32Array): void { + if (out === data) { + throw new Error("Input and output buffers must be different"); + } + + this._out = out; + this._data = data; + this._inv = 0; + this._realTransform4(); + this._out = null; + this._data = null; + } + + // Real input radix-4 implementation + private _realTransform4(): void { + const out = this._out!; + const size = this._csize; + + // Initial step (permute and transform) + const width = this._width; + let step = 1 << width; + let len = (size / step) << 1; + + let outOff: i32; + let t: i32; + const bitrev = this._bitrev; + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleRealTransform2(outOff, off >>> 1, step >>> 1); + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleRealTransform4(outOff, off >>> 1, step >>> 1); + } + } + + // Loop through steps in decreasing order + const inv: f32 = this._inv ? -1 : 1; + const table = this.table; + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1; + const halfLen = len >>> 1; + const quarterLen = halfLen >>> 1; + const hquarterLen = quarterLen >>> 1; + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + for (let i = 0, k = 0; i <= hquarterLen; i += 2, k += step) { + const A = outOff + i; + const B = A + quarterLen; + const C = B + quarterLen; + const D = C + quarterLen; + + // Original values + const Ar = out[A]; + const Ai = out[A + 1]; + const Br = out[B]; + const Bi = out[B + 1]; + const Cr = out[C]; + const Ci = out[C + 1]; + const Dr = out[D]; + const Di = out[D + 1]; + + // Middle values + const MAr = Ar; + const MAi = Ai; + + const tableBr = table[k]; + const tableBi = inv * table[k + 1]; + const MBr = Br * tableBr - Bi * tableBi; + const MBi = Br * tableBi + Bi * tableBr; + + const tableCr = table[2 * k]; + const tableCi = inv * table[2 * k + 1]; + const MCr = Cr * tableCr - Ci * tableCi; + const MCi = Cr * tableCi + Ci * tableCr; + + const tableDr = table[3 * k]; + const tableDi = inv * table[3 * k + 1]; + const MDr = Dr * tableDr - Di * tableDi; + const MDi = Dr * tableDi + Di * tableDr; + + // Pre-Final values + const T0r = MAr + MCr; + const T0i = MAi + MCi; + const T1r = MAr - MCr; + const T1i = MAi - MCi; + const T2r = MBr + MDr; + const T2i = MBi + MDi; + const T3r = inv * (MBr - MDr); + const T3i = inv * (MBi - MDi); + + // Final values + const FAr = T0r + T2r; + const FAi = T0i + T2i; + + const FBr = T1r + T3i; + const FBi = T1i - T3r; + + out[A] = FAr; + out[A + 1] = FAi; + out[B] = FBr; + out[B + 1] = FBi; + + // Output final middle point + if (i === 0) { + const FCr = T0r - T2r; + const FCi = T0i - T2i; + out[C] = FCr; + out[C + 1] = FCi; + continue; + } + + // Do not overwrite ourselves + if (i === hquarterLen) { + continue; + } + + // In the flipped case: + // MAi = -MAi + // MBr=-MBi, MBi=-MBr + // MCr=-MCr + // MDr=MDi, MDi=MDr + const ST0r = T1r; + const ST0i = -T1i; + const ST1r = T0r; + const ST1i = -T0i; + const ST2r = -inv * T3i; + const ST2i = -inv * T3r; + const ST3r = -inv * T2i; + const ST3i = -inv * T2r; + + const SFAr = ST0r + ST2r; + const SFAi = ST0i + ST2i; + + const SFBr = ST1r + ST3i; + const SFBi = ST1i - ST3r; + + const SA = outOff + quarterLen - i; + const SB = outOff + halfLen - i; + + out[SA] = SFAr; + out[SA + 1] = SFAi; + out[SB] = SFBr; + out[SB + 1] = SFBi; + } + } + } + } + + // radix-2 implementation + // NOTE: Only called for len=4 + private _singleRealTransform2(outOff: i32, off: i32, step: i32): void { + const out = this._out!; + const data = this._data!; + + const evenR = data[off]; + const oddR = data[off + step]; + + const leftR = evenR + oddR; + const rightR = evenR - oddR; + + out[outOff] = leftR; + out[outOff + 1] = 0; + out[outOff + 2] = rightR; + out[outOff + 3] = 0; + } + + // radix-4 + // NOTE: Only called for len=8 + private _singleRealTransform4(outOff: i32, off: i32, step: i32): void { + const out = this._out!; + const data = this._data!; + const inv: f32 = this._inv ? -1 : 1; + const step2 = step * 2; + const step3 = step * 3; + + // Original values + const Ar = data[off]; + const Br = data[off + step]; + const Cr = data[off + step2]; + const Dr = data[off + step3]; + + // Pre-Final values + const T0r = Ar + Cr; + const T1r = Ar - Cr; + const T2r = Br + Dr; + const T3r = inv * (Br - Dr); + + // Final values + const FAr = T0r + T2r; + + const FBr = T1r; + const FBi = -T3r; + + const FCr = T0r - T2r; + + const FDr = T1r; + const FDi = T3r; + + out[outOff] = FAr; + out[outOff + 1] = 0; + out[outOff + 2] = FBr; + out[outOff + 3] = FBi; + out[outOff + 4] = FCr; + out[outOff + 5] = 0; + out[outOff + 6] = FDr; + out[outOff + 7] = FDi; + } +} diff --git a/clients/web/dsp-asm/assembly/index.ts b/clients/web/dsp-asm/assembly/index.ts new file mode 100644 index 0000000..e756872 --- /dev/null +++ b/clients/web/dsp-asm/assembly/index.ts @@ -0,0 +1,391 @@ +import { FFT } from "./fft"; +import { + f32x4_load, + f32x4_store, + listAbsMax_f32, + listMax_f32, + listNeg_f32, + listSqSum_f32, + memcpy_f32, +} from "./fastutils"; + +// copying arrays across thread boundaries is very expensive, so provide a mechanism to reuse it. +let currentBuf1: Float32Array | null; // left +let currentBuf2: Float32Array | null; // right +let currentBuf3: Float32Array | null; // mid +let currentBuf4: Float32Array | null; // side + +let lastFft: Float32Array | null; + +// common logic for bsel, _buf params +function resolveBuffer(bsel: i32, _b: null | Float32Array): Float32Array { + switch (bsel) { + case 0: + assert(_b, "cannot pass null buffer and use it"); + return _b!; + + case 1: + assert(currentBuf1, "cannot try to use buffer 1 when its not uploaded"); + return currentBuf1!; + + case 2: + assert(currentBuf2, "cannot try to use buffer 2 when its not uploaded"); + return currentBuf2!; + + case 3: + assert(currentBuf3, "cannot try to use buffer 3 when its not set"); + return currentBuf3!; + + case 4: + assert(currentBuf4, "cannot try to use buffer 4 when its not set"); + return currentBuf4!; + + default: + assert(false, "cannot try to use a buffer selection value outside than 0-4 inclusive"); + return null!; // for static analysis + } +} + +export function uploadBuf(buf1: Float32Array, buf2: null | Float32Array): void { + currentBuf1 = buf1; + if (buf2) currentBuf2 = buf2; + // reset fft as we have a new buffer + lastFft = null; + // fill buffers 3 & 4 + calcMidSide(); +} + +function calcMidSide(): void { + assert(currentBuf1, "both buffers must be in place to swap them to M-S"); + assert(currentBuf2, "both buffers must be in place to swap them to M-S"); + assert( + currentBuf1!.length === currentBuf2!.length, + "both buffers must be the same size to swap them to M-S", + ); + + currentBuf3 = new Float32Array(currentBuf1!.length); + currentBuf4 = new Float32Array(currentBuf1!.length); + + // unsafe simd stuff here because this feels way too specific to go in fastutils? + const simdSafeLen = 4 * (currentBuf1!.length / 4); + + for (let i = 0; i < simdSafeLen; i += 4) { + const s1 = f32x4_load(currentBuf1!, i); + const s2 = f32x4_load(currentBuf2!, i); + + f32x4_store(f32x4.div(f32x4.add(s1, s2), f32x4.splat(2)), currentBuf3!, i); + f32x4_store(f32x4.div(f32x4.sub(s1, s2), f32x4.splat(2)), currentBuf4!, i); + } + + for (let i = simdSafeLen; i < currentBuf1!.length; i++) { + const s1 = currentBuf1![i]; + const s2 = currentBuf2![i]; + currentBuf3![i] = (s1 + s2) / 2; + currentBuf4![i] = (s1 - s2) / 2; + } +} + +export function downscale(bsel: i32, _buf: Float32Array | null, size: i32, ash: i32): Float32Array { + const buf = resolveBuffer(bsel, _buf); + + // part of the scrolling waveform's anti-shimmer measures + if (ash < 0) ash = 0; + + // round chunk up to a multiple of 4 + // this increases the # of chunks that are maxed *entirely* with simd + const fchunkSz = 4 * Math.ceil(buf.length / size / 4); + const nChunks = Math.floor(buf.length / fchunkSz); + const chunkSz = fchunkSz; + + ash %= chunkSz; // the caller does not know the chunk size, so `ash` is likely to be huge + ash = -ash; // counteract scrolling motion (i think this is the correct direction?) + + const res = new Float32Array(nChunks); + for (let ci = 0; ci < nChunks; ci++) { + const b = buf.subarray(ci * chunkSz + ash, (ci + 1) * chunkSz + ash); + if (b.length) { + res[ci] = listMax_f32(b); + } // TODO: why? + } + + return res; +} + +export function fft( + bsel: i32, + _buf: Float32Array | null, + start: i32, + end: i32, + pad: i32, + persistence: f32, +): Float32Array { + const buf = resolveBuffer(bsel, _buf); + if (start === -1) start = 0; + if (end === -1) end = buf.length; + if (pad === -1) pad = 0; + + assert(end < buf.length, "(fft) end must not overflow the buf"); + + // min power of 2 that is <= buf.length + const size = Math.pow(2, Math.ceil(Math.log2(pad + end - start))); + + const fftInput = new Float32Array(size); + + // copy in buffer + memcpy_f32(buf, fftInput, start, 0, end - start); + + const fft = new FFT(size); + const output = fft.createComplexArray(); + + fft.realTransform(output, fftInput); + + const result = output.subarray(0, size); + if (Mathf.abs(persistence) < 0.0001) return result; // no persistence, just return the raw fft + if (!lastFft || lastFft!.length !== result.length) { + lastFft = result; + return result; + } + + // blend last fft with this fft to reduce jitter + // round size down to 4 for simd purposes + const out = new Float32Array(lastFft!.length); + + const simdSafeSize = 4 * (out.length / 4); + + for (let i = 0; i < simdSafeSize; i += 4) { + // out_i = max(res_i, last_i * persistence) + const persisted = f32x4.mul(f32x4_load(lastFft!, i), f32x4.splat(persistence)); + + f32x4_store(f32x4.max(f32x4_load(result, i), persisted), out, i); + } + + // do last few elements + for (let i = simdSafeSize; i < out.length; i++) { + out[i] = result[i] + lastFft![i] * persistence; + } + + lastFft = out; + return out.subarray(0, lastFft!.length); +} + +function sliceByCrossings( + bsel: i32, + _buf: Float32Array | null, + start: i32, + end: i32, + n: i32, +): Array { + const buf = resolveBuffer(bsel, _buf); + const buffers = new Array(); + if (start === -1) start = 0; + if (end === -1) end = buf.length; + if (n === -1) n = 1; + + assert(end < buf.length, "(slicebycrossings) end must not overflow the buf"); + + let startNext = start; // the start of the next buffer to be sliced + let isPos = buf[start] > 0; // was the last sample positive? + + let cutN = 0; // counter for `n` + + for (let i = start + 1; i < end; i++) { + const thisSampPos = buf[i] > 0; + if (isPos !== thisSampPos && ++cutN === n) { + // flip! + cutN = 0; + buffers.push(buf.subarray(startNext, i)); + startNext = i; + } + isPos = thisSampPos; + } + + if (cutN !== 0) { + // a lil bit left + buffers.push(buf.subarray(startNext, end - 1)); + } + + return buffers; +} + +function maxBufLength(bufs: Array): Float32Array { + // don't just error because this is a common case with an all zeroes input, so fake a value + //assert(bufs.length, "(maxAbsPeakOf) cannot pick a max of no items"); + if (!bufs.length) return new Float32Array(0); + + let maxBuf = bufs[0]; + let max: i32 = 0; + + for (let bi = 0; bi < bufs.length; bi++) { + const b = bufs[bi]; + + if (b.length < max) continue; + max = b.length; + maxBuf = b; + } + + // attempt to normalize wave phase - take the sample 1/4 of the way in and make it positive + // this should always give an up-then-down motion + if (maxBuf[maxBuf.length / 4] < 0) listNeg_f32(maxBuf); + + return maxBuf; +} + +// sliceByCrossings -> maxBufLength all in one go as an opt +export function sbcMax( + bsel: i32, + _buf: Float32Array | null, + start: i32, + end: i32, + n: i32, +): Float32Array { + return maxBufLength(sliceByCrossings(bsel, _buf, start, end, n)); +} + +export function centeredSlice( + bsel: i32, + _buf: Float32Array | null, + pos: i32, + width: i32, + downs: i32, +): Float32Array { + const buf = resolveBuffer(bsel, _buf); + + const start = pos - width / 2; + const end = pos + width / 2; + + const padS = start < 0 ? -start : 0; + const padE = end >= buf.length ? end - buf.length /*+ 1*/ : 0; + + let padded: Float32Array | null; + + // happy path just returns this + if (padS === 0 && padE === 0) { + padded = buf.subarray(start, end); + } else { + // unhappy path - we need to pad it! + padded = new Float32Array(width); + memcpy_f32(buf, padded, start < 0 ? 0 : start, padS, width - padE - padS); + } + + // offset scrolls the chunks used for downscaling with the wave to fix shimmering + // increasing the downs size enough to remove shimmering while scrolling is too slow + return downs == -1 ? padded : downscale(0, padded, downs, pos); +} + +export function samplePeak(bsel: i32, _buf: Float32Array | null, start: i32, end: i32): f32 { + const buf = resolveBuffer(bsel, _buf); + if (start < 1) start = 0; + if (end < 1) end = buf.length; + + return listAbsMax_f32(buf.subarray(start, end)); +} + +export function rms(bsel: i32, _buf: Float32Array | null, start: i32, end: i32): f32 { + const buf = resolveBuffer(bsel, _buf); + if (start < 1) start = 0; + if (end < 1) end = buf.length; + + // root-mean-square is [the root of [the mean of [the squares]]] + return Mathf.sqrt(listSqSum_f32(buf.subarray(start, end)) / (end - start)); +} + +export function getGoniometerPoints(start: i32, length: i32 /*, points: i32*/): Float32Array { + /*let start = _start; + let length = _length;*/ + + if (start < 0) start = 0; + assert(length > 0, "length must be > 0"); + + assert(currentBuf3, "M channel must be set"); + assert(currentBuf4, "S channel must be set"); + assert(currentBuf3!.length === currentBuf4!.length, "channel sizes must match"); + + assert(start >= 0, "start must not underflow"); + assert(start + length < currentBuf3!.length, "end must not overflow"); + + const output = new Float32Array(length * 2 /*points * 2*/); + + // i had overflow errors before within the math for sampling + /*const sta64 = start; + const len64 = length; + const pts64 = points;*/ + + for (let i = 0; i < length; i++) { + const inputI = start + i; /*sta64 + ((i * len64) / pts64);*/ + + assert(inputI >= 0, "inputI underflowed"); + assert(inputI < currentBuf3!.length, "inputI will overflow"); + assert(inputI <= i32.MAX_VALUE, "inputI is bigger than an i32"); + + output[i * 2] = currentBuf3![inputI]; + output[i * 2 + 1] = currentBuf4![inputI]; + } + + return output; +} + +function spectogram( + bsel: i32, + _buf: null | Float32Array, + start: i32, + end: i32, + pad: i32, + size: i32, +): Float32Array { + const fftSize = Math.pow(2, Math.ceil(Math.log2(size + pad/*pad + end - start*/))); + const outputSize = size * fftSize; + + if (start < 0) start = 0; + assert(end > start, "end out of range"); + + // element 0 is the height of the spectrogram + const output = new Float32Array(outputSize + 1); + output[0] = fftSize; + + for (let i = 0; i < size; i++) { + const osetS = start + ((i * (end - start)) / size); + + const res = fft(bsel, _buf, osetS, osetS + fftSize, pad, 0); + + memcpy_f32(res, output, 0, 1 + i * fftSize, fftSize); + } + + return output; +} + +export function centeredSpectogram( + bsel: i32, + _buf: null | Float32Array, + pos: i32, + width: i32, + padFft: i32, + ds: i32, +): Float32Array { + const buf = resolveBuffer(bsel, _buf); + + const start = pos - width / 2; + const end = pos + width / 2; + + const padS = start < 0 ? -start : 0; + const padE = end >= buf.length ? end - buf.length : 0; + + let padded: Float32Array | null; + + // happy path just returns this + if (padS === 0 && padE === 0) { + padded = spectogram(0, buf, start, end, padFft, ds); + } else { + // unhappy path - we need to pad it! + const raw = spectogram(0, buf, start - padS, end - padE, padFft, ds); + const fftSize = raw[0]; + const chunksUnpadded = raw.subarray(1); + + const paddedSize = 1 + fftSize * width; + + padded = new Float32Array(paddedSize); + padded[0] = fftSize; + memcpy_f32(chunksUnpadded, padded, 0, padS, paddedSize - fftSize * (padE + padS)); + } + + return padded; +} diff --git a/clients/web/dsp-asm/assembly/tsconfig.json b/clients/web/dsp-asm/assembly/tsconfig.json new file mode 100644 index 0000000..998f782 --- /dev/null +++ b/clients/web/dsp-asm/assembly/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../node_modules/.pnpm/assemblyscript@0.27.27/node_modules/assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/clients/web/dsp-asm/build/.gitignore b/clients/web/dsp-asm/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/clients/web/dsp-asm/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/clients/web/dsp-asm/package.json b/clients/web/dsp-asm/package.json new file mode 100644 index 0000000..6f59da4 --- /dev/null +++ b/clients/web/dsp-asm/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "test": "pnpm run asbuild:release && deno test tests/index.ts --allow-read", + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "pnpm run asbuild:debug && pnpm run asbuild:release", + "start": "npx serve ." + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "assemblyscript": "^0.27.27" + }, + "type": "module", + "exports": { + ".": { + "import": "./build/release.js", + "types": "./build/release.d.ts" + } + } +} diff --git a/clients/web/dsp-asm/pnpm-lock.yaml b/clients/web/dsp-asm/pnpm-lock.yaml new file mode 100644 index 0000000..984ea05 --- /dev/null +++ b/clients/web/dsp-asm/pnpm-lock.yaml @@ -0,0 +1,1294 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + assemblyscript: + specifier: ^0.27.27 + version: 0.27.27 + +packages: + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + dev: true + + /@babel/compat-data@7.24.7: + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.24.7: + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.24.7: + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.24.7: + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-environment-visitor@7.24.7: + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-function-name@7.24.7: + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-hoist-variables@7.24.7: + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-plugin-utils@7.24.7: + resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-simple-access@7.24.7: + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-split-export-declaration@7.24.7: + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.24.7: + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.24.7: + resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + dev: true + + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/traverse@7.24.7: + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: true + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.14.2 + jest-mock: 29.7.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.14.2 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.24.7 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.7 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.14.2 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.14.2 + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/node@20.14.2: + resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + dev: true + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /assemblyscript@0.27.27: + resolution: {integrity: sha512-z4ijXsjjk3uespEeCWpO1K2GQySc6bn+LL5dL0tsC2VXNYKFnKDmAh3wefcKazxXHFVhYlxqNfyv96ajaQyINQ==} + engines: {node: '>=16', npm: '>=7'} + hasBin: true + dependencies: + binaryen: 116.0.0-nightly.20240114 + long: 5.2.3 + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.24.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.7): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binaryen@116.0.0-nightly.20240114: + resolution: {integrity: sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==} + hasBin: true + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /browserslist@4.23.1: + resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001629 + electron-to-chromium: 1.4.796 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.1) + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /caniuse-lite@1.0.30001629: + resolution: {integrity: sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /electron-to-chromium@1.4.796: + resolution: {integrity: sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==} + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.14.2 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.7 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.24.7 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.7 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.14.2 + jest-util: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.24.7 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.14.2 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.14.2 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /update-browserslist-db@1.0.16(browserslist@4.23.1): + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true diff --git a/clients/web/dsp-asm/tests/index.ts b/clients/web/dsp-asm/tests/index.ts new file mode 100644 index 0000000..73f6248 --- /dev/null +++ b/clients/web/dsp-asm/tests/index.ts @@ -0,0 +1,30 @@ +import { assertAlmostEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import FFT from "npm:fft.js"; +import { fft } from "../build/debug.js"; + +function jsfft(buf: Float32Array) { + const end = buf.length; + const start = 0; + + const size = Math.pow(2, Math.ceil(Math.log2(end - start))); + + const input = [...buf.slice(start, end), ...Array(size - (end - start)).fill(0)]; + const fft = new FFT(size); + const output = fft.createComplexArray(); + fft.realTransform(output, input); + + return new Float32Array(output.slice(0, size)); +} + +Deno.test("FFT", () => { + const input = new Float32Array(100); + for (let i = 0; i < input.length; i++) input[i] = Math.random(); + + const expected = jsfft(input); + const actual = fft(input, -1, -1, -1); + + // wasm is using f32 for memory usage reasons + for (let i = 0; i < actual.length; i++) { + assertAlmostEquals(actual[i], expected[i], 5e-6); + } +}); diff --git a/clients/web/package.json b/clients/web/package.json index c37b59b..fd84e18 100644 --- a/clients/web/package.json +++ b/clients/web/package.json @@ -2,12 +2,14 @@ "name": "uwuradio-client", "version": "0.0.0", "scripts": { - "dev": "vite", - "build": "run-p type-check build-only", + "dev": "pnpm build:as && vite", + "build": "pnpm build:as && run-p type-check build-only", + "build:as": "cd dsp-asm && pnpm asbuild:release && cd ..", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "postinstall": "pnpm i -C dsp-asm" }, "dependencies": { "@headlessui/vue": "^1.7.16", diff --git a/clients/web/src/App.vue b/clients/web/src/App.vue index b75f919..c7738c7 100644 --- a/clients/web/src/App.vue +++ b/clients/web/src/App.vue @@ -4,9 +4,11 @@ import { defineAsyncComponent, ref } from "vue"; // @ts-expect-error this lib is not typed lol import canAutoplay from "can-autoplay"; import { visualizerEnabled } from "./visualizer"; +import { enableAnalysis } from "./analysis"; import { setupMediaSession } from "./mediaSession"; import { audioCtx } from "./audio"; const TheChurner = defineAsyncComponent(() => import("./components/TheChurner.vue")); +const TheRedundantInfo = defineAsyncComponent(() => import("./components/TheRedundantInfo.vue")); interface CanAutoplay { result: boolean; @@ -35,6 +37,9 @@ function handleEnterClick() { + + + diff --git a/clients/web/src/analysis.ts b/clients/web/src/analysis.ts new file mode 100644 index 0000000..779f618 --- /dev/null +++ b/clients/web/src/analysis.ts @@ -0,0 +1,246 @@ +import { ref, watch, watchEffect, type WatchStopHandle } from "vue"; +import WORKER from "./analysisWorker.js?worker"; +import { seek } from "./audio"; + +// audio analyzers for people who need constant visual stimulation :D + +// === DRIVING STATE === + +export const enableAnalysis = ref(false); +const buf = ref(); +let moodbarBuildToken = 0; + +let cleanup1: WatchStopHandle; +let cleanup2: WatchStopHandle; +export function setAnalysisBuf(b: AudioBuffer) { + const buildToken = ++moodbarBuildToken; + buf.value = b; + reset(); + + cleanup1?.(); + cleanup1 = watchEffect(async () => { + cleanup2?.(); + const left = b.getChannelData(0); + const right = b.numberOfChannels > 1 ? b.getChannelData(1) : left; + + // will cause a brief stutter on song change + // oh well! we're already repainting the ui + // without interaction so its not toooo noticeable i figure + await uploadBuffer(left, right, b.sampleRate); + if (buildToken !== moodbarBuildToken) return; + + const scaled = await downscale(bufMain, 5000); + if (buildToken !== moodbarBuildToken) return; + + downscaled.value = scaled; + void buildMoodbar(1000).then((data) => { + if (buildToken === moodbarBuildToken && data.length) { + moodbar.value = data; + } + }); + + cleanup2 = watchEffect(updateVis); + }); +} + +// === WORKER HANDLING CODE === + +const worker = new WORKER(); // wrong type +const pendingRequests: Record void> = {}; + +// init the wasm in the worker +const initPromise = callWorker(0, []); + +worker.onmessage = (e) => { + if (e.data[0] === "ERR") throw new Error(`error in analysis worker: ${e.data[1]}`); + if (!(e.data[0] in pendingRequests)) + return console.warn("unwanted response from analysis worker, dropping", e.data); + + pendingRequests[e.data[0]](...e.data.slice(1)); + delete pendingRequests[e.data[0]]; +}; + +async function callWorker(cmd: number, args: unknown[]): Promise { + if (cmd !== 0) await initPromise; + + const id = Math.random().toString(16); + + worker.postMessage([cmd, id, ...args]); + return new Promise((res) => (pendingRequests[id] = res as any)); +} + +// === BINDINGS === + +// sets the current worker's default buffer to this one +// this avoids unnecessarily sending the same buffer multiple times +const uploadBuffer = (buf1: Float32Array, buf2?: Float32Array, sampleRate?: number) => + callWorker(1, [buf1, buf2, sampleRate]); + +// downscales buf to size, using naive sampling +const downscale = (buf: WasmBuf, size: number) => callWorker(2, [buf, size]); + +const buildMoodbar = (width: number) => callWorker(10, [width]); + +const samplePeak = (buf: WasmBuf, start?: number, end?: number) => + callWorker(6, [buf, start, end]); + +const rms = (buf: WasmBuf, start?: number, end?: number) => + callWorker(7, [buf, start, end]); + +const getGoniometerPoints = (start: number, length: number) => + callWorker(8, [start, length]); + +type WasmBuf = Float32Array | BUF; + +enum BUF { + L = 1, + R, + M, + S, +} + +const bufMain = BUF.M; // most metering is done off this channel +// left side peak & rms metering +const bufM1 = () => (volumeMeteringMidSide.value ? BUF.M : BUF.L); +// right side peak & rms metering +const bufM2 = () => (volumeMeteringMidSide.value ? BUF.S : BUF.R); + +// === USEFUL REACTIVE STUFF === + +export const downscaled = ref(); +export const moodbar = ref(); + +export const currentPeakL = ref(0); +export const currentPeakR = ref(0); +export const currentRmsL = ref(0); +export const currentRmsR = ref(0); +export const currentPeakHoldL = ref(0); +export const currentPeakHoldR = ref(0); + +export const gonioPoints = ref(); + +export const peakDbfsL = () => 20 * Math.log10(currentPeakL.value); +export const peakDbfsR = () => 20 * Math.log10(currentPeakR.value); +export const rmsDbfsL = () => 20 * Math.log10(currentRmsL.value); +export const rmsDbfsR = () => 20 * Math.log10(currentRmsR.value); +export const peakHoldDbfsL = () => 20 * Math.log10(currentPeakHoldL.value); +export const peakHoldDbfsR = () => 20 * Math.log10(currentPeakHoldR.value); + +let peakHoldSetTimeL: number; +let peakHoldSetTimeR: number; +let lastGonioIndex = 0; +let updateRunning = false; +let updateQueued = false; +let lastPeakUpdateAt = 0; +let lastRmsUpdateAt = 0; +let lastGonioUpdateAt = 0; + +const PEAK_UPDATE_MS = 33; +const RMS_UPDATE_MS = 100; +const GONIO_UPDATE_MS = 33; + +function reset() { + downscaled.value = undefined; + moodbar.value = undefined; + currentPeakL.value = 0; + currentPeakR.value = 0; + currentPeakHoldL.value = 0; + currentPeakHoldR.value = 0; + currentRmsL.value = 0; + currentRmsR.value = 0; + gonioPoints.value = undefined; + lastGonioIndex = 0; + updateQueued = false; + lastPeakUpdateAt = 0; + lastRmsUpdateAt = 0; + lastGonioUpdateAt = 0; +} + +// for ui purposes +export const volumeMeteringMidSide = ref(false); + +async function updateVisFrame() { + if (seek.value === undefined || !enableAnalysis.value) return; + + const now = performance.now(); + const seekSamples = Math.floor(seek.value * buf.value!.sampleRate); + + const s16m = ~~(buf.value!.sampleRate / 30); + const s300m = ~~(buf.value!.sampleRate * 0.3); + if (now - lastPeakUpdateAt >= PEAK_UPDATE_MS && seekSamples - s16m >= 0) { + lastPeakUpdateAt = now; + const [pkl, pkr] = await Promise.all([ + samplePeak(bufM1(), seekSamples - s16m, seekSamples), + samplePeak(bufM2(), seekSamples - s16m, seekSamples), + ]); + currentPeakL.value = Math.max(currentPeakL.value * 0.97, pkl); + currentPeakR.value = Math.max(currentPeakR.value * 0.97, pkr); + + if (pkl > currentPeakHoldL.value) { + currentPeakHoldL.value = pkl; + peakHoldSetTimeL = performance.now(); + } + if (pkr > currentPeakHoldR.value) { + currentPeakHoldR.value = pkr; + peakHoldSetTimeR = performance.now(); + } + + if (performance.now() - peakHoldSetTimeL > 500) { + currentPeakHoldL.value *= 0.985; + } + if (performance.now() - peakHoldSetTimeR > 500) { + currentPeakHoldR.value *= 0.985; + } + } + + if (now - lastRmsUpdateAt >= RMS_UPDATE_MS && seekSamples - s300m >= 0) { + lastRmsUpdateAt = now; + const [rmsL, rmsR] = await Promise.all([ + rms(bufM1(), seekSamples - s300m, seekSamples), + rms(bufM2(), seekSamples - s300m, seekSamples), + ]); + currentRmsL.value = rmsL; + currentRmsR.value = rmsR; + } + + if (now - lastGonioUpdateAt >= GONIO_UPDATE_MS && seekSamples) { + lastGonioUpdateAt = now; + if (seekSamples < lastGonioIndex || seekSamples - lastGonioIndex > buf.value!.sampleRate) { + lastGonioIndex = Math.max(0, seekSamples - 1470); + } + + // max length of 1/30th of a second at 44.1khz, to prevent stutters on copying like half a song back from wasm + const available = Math.max(0, buf.value!.length - 1 - lastGonioIndex); + const len = Math.min(seekSamples - lastGonioIndex, 1470, available); + + if (len > 0) { + gonioPoints.value = await getGoniometerPoints(lastGonioIndex, len); + lastGonioIndex = seekSamples; + } + } +} + +// this is usable directly as a watchEffect() arg. +async function updateVis() { + const enabled = enableAnalysis.value; + const currentSeek = seek.value; + const currentBuf = buf.value; + volumeMeteringMidSide.value; + + if (!enabled || currentSeek === undefined || !currentBuf) return; + + if (updateRunning) { + updateQueued = true; + return; + } + + updateRunning = true; + try { + do { + updateQueued = false; + await updateVisFrame(); + } while (updateQueued && enableAnalysis.value); + } finally { + updateRunning = false; + } +} diff --git a/clients/web/src/analysisWorker.js b/clients/web/src/analysisWorker.js new file mode 100644 index 0000000..378287b --- /dev/null +++ b/clients/web/src/analysisWorker.js @@ -0,0 +1,361 @@ +import wasmUrl from "../dsp-asm/build/release.wasm?url"; +import { instantiate } from "../dsp-asm/build/release.js"; + +let wasmDS, + wasmFFT, + wasmUpload, + wasmSbcMax, + wasmCentSlic, + wasmSamPk, + wasmRms, + wasmGonio, + wasmSpecto, + init; + +const MOODBAR_BANDS = 128; +const MOODBAR_FFT_SIZE = 256; +const MOODBAR_INTERVAL_SECONDS = 0.1; +const MOODBAR_MAX_FFTS_PER_INTERVAL = 16; +const MOODBAR_MID_BUFFER = 3; +const MOODBAR_BARK_BANDS = [ + 100, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720, 2000, 2320, 2700, 3150, 3700, + 4400, 5300, 6400, 7700, 9500, 12000, 15500, +]; + +let moodbarLeft; +let moodbarRight; +let moodbarSampleRate = 44100; +let moodbarGeneration = 0; +let moodbarRequest = 0; +let moodbarWindow; + +function wasmInit() { + if (init) return init; + return (init = WebAssembly.compileStreaming(fetch(wasmUrl)) + .then(instantiate) + .then( + ({ + downscale, + fft, + uploadBuf, + sbcMax, + centeredSlice, + samplePeak, + rms, + getGoniometerPoints, + centeredSpectogram, + }) => { + wasmDS = downscale; + wasmFFT = fft; + wasmUpload = uploadBuf; + wasmSbcMax = sbcMax; + wasmCentSlic = centeredSlice; + wasmSamPk = samplePeak; + wasmRms = rms; + wasmGonio = getGoniometerPoints; + wasmSpecto = centeredSpectogram; + }, + )); +} + +const yieldWorker = () => new Promise((resolve) => setTimeout(resolve, 0)); + +function readMoodbarSample(index) { + if (!moodbarLeft || index < 0 || index >= moodbarLeft.length) return 0; + + const left = moodbarLeft[index]; + const right = + moodbarRight && index < moodbarRight.length ? moodbarRight[index] : moodbarLeft[index]; + + return (left + right) * 0.5; +} + +function createMoodbarWindow(start) { + moodbarWindow ??= new Float32Array(MOODBAR_FFT_SIZE); + + for (let i = 0; i < MOODBAR_FFT_SIZE; i++) { + moodbarWindow[i] = readMoodbarSample(start + i); + } + + return moodbarWindow; +} + +function moodbarFft(start) { + const end = start + MOODBAR_FFT_SIZE; + if (start >= 0 && end <= moodbarLeft.length) { + return wasmFFT(MOODBAR_MID_BUFFER, undefined, start, end, 0, 0); + } + + return wasmFFT(0, createMoodbarWindow(start), 0, MOODBAR_FFT_SIZE, 0, 0); +} + +function addSpectrum(start, magnitudes) { + const spectrum = moodbarFft(start); + + const scale = MOODBAR_FFT_SIZE * MOODBAR_FFT_SIZE; + for (let band = 0; band < MOODBAR_BANDS; band++) { + const offset = band * 2; + const real = spectrum[offset]; + const imaginary = spectrum[offset + 1]; + magnitudes[band] += (real * real + imaginary * imaginary) / scale; + } +} + +function bandFrequency(band, bands, sampleRate) { + return Math.trunc((Math.trunc(sampleRate / 2) * band + Math.trunc(sampleRate / 4)) / bands); +} + +function createBarkBandTable(bands, sampleRate) { + const table = new Uint8Array(bands + 1); + let barkband = 0; + + for (let i = 0; i < bands + 1; i++) { + if ( + barkband < MOODBAR_BARK_BANDS.length - 1 && + bandFrequency(i, bands, sampleRate) >= MOODBAR_BARK_BANDS[barkband] + ) { + barkband++; + } + + table[i] = barkband; + } + + return table; +} + +function writeMoodbarFrame(frames, frameIndex, magnitudes, barkBandTable, barkMagnitudes) { + barkMagnitudes.fill(0); + + for (let i = 0; i < MOODBAR_BANDS; i++) { + barkMagnitudes[barkBandTable[i]] += magnitudes[i]; + } + + let red = 0; + let green = 0; + let blue = 0; + + for (let i = 0; i < MOODBAR_BARK_BANDS.length; i++) { + const value = barkMagnitudes[i] * barkMagnitudes[i]; + const channel = Math.floor((i * 3) / MOODBAR_BARK_BANDS.length); + + if (channel === 0) red += value; + else if (channel === 1) green += value; + else blue += value; + } + + const offset = frameIndex * 3; + frames[offset] = Math.sqrt(red); + frames[offset + 1] = Math.sqrt(green); + frames[offset + 2] = Math.sqrt(blue); +} + +function normalizeMoodbarChannel(frames, frameCount, channel) { + let mini = frames[channel]; + let maxi = frames[channel]; + + for (let i = 1; i < frameCount; i++) { + const value = frames[i * 3 + channel]; + if (value > maxi) maxi = value; + else if (value < mini) mini = value; + } + + let avg = 0; + for (let i = 0; i < frameCount; i++) { + const value = frames[i * 3 + channel]; + if (value !== mini && value !== maxi) avg += value / frameCount; + } + + let tu = 0; + let tb = 0; + let avgu = 0; + let avgb = 0; + + for (let i = 0; i < frameCount; i++) { + const value = frames[i * 3 + channel]; + if (value === mini || value === maxi) continue; + + if (value > avg) { + avgu += value; + tu++; + } else { + avgb += value; + tb++; + } + } + + avgu = tu ? avgu / tu : avg; + avgb = tb ? avgb / tb : avg; + + tu = 0; + tb = 0; + let avguu = 0; + let avgbb = 0; + + for (let i = 0; i < frameCount; i++) { + const value = frames[i * 3 + channel]; + if (value === mini || value === maxi) continue; + + if (value > avgu) { + avguu += value; + tu++; + } else if (value < avgb) { + avgbb += value; + tb++; + } + } + + avguu = tu ? avguu / tu : avgu; + avgbb = tb ? avgbb / tb : avgb; + + mini = Math.max(avg + (avgb - avg) * 2, avgbb); + maxi = Math.min(avg + (avgu - avg) * 2, avguu); + + let delta = maxi - mini; + if (!Number.isFinite(delta) || delta === 0) delta = 1; + + for (let i = 0; i < frameCount; i++) { + const offset = i * 3 + channel; + const value = frames[offset]; + frames[offset] = Number.isFinite(value) ? Math.max(0, Math.min((value - mini) / delta, 1)) : 0; + } +} + +function finishMoodbar(frames, frameCount, width) { + const data = new Uint8ClampedArray(width * 3); + if (!frameCount) return data; + + normalizeMoodbarChannel(frames, frameCount, 0); + normalizeMoodbarChannel(frames, frameCount, 1); + normalizeMoodbarChannel(frames, frameCount, 2); + + for (let i = 0; i < width; i++) { + let start = Math.floor((i * frameCount) / width); + let end = Math.floor(((i + 1) * frameCount) / width); + if (start === end) end = start + 1; + end = Math.min(end, frameCount); + + let red = 0; + let green = 0; + let blue = 0; + + for (let frame = start; frame < end; frame++) { + const offset = frame * 3; + red += frames[offset] * 255; + green += frames[offset + 1] * 255; + blue += frames[offset + 2] * 255; + } + + const n = end - start; + const offset = i * 3; + data[offset] = Math.floor(red / n); + data[offset + 1] = Math.floor(green / n); + data[offset + 2] = Math.floor(blue / n); + } + + return data; +} + +async function buildMoodbar(width = 1000) { + const left = moodbarLeft; + if (!left?.length) return new Uint8ClampedArray(0); + + const generation = moodbarGeneration; + const request = ++moodbarRequest; + const sampleRate = moodbarSampleRate || 44100; + const framesPerInterval = Math.max(1, Math.floor(sampleRate * MOODBAR_INTERVAL_SECONDS)); + const frameCount = Math.max(1, Math.ceil(left.length / framesPerInterval)); + const frames = new Float64Array(frameCount * 3); + const magnitudes = new Float64Array(MOODBAR_BANDS); + const barkMagnitudes = new Float64Array(MOODBAR_BARK_BANDS.length); + const barkBandTable = createBarkBandTable(MOODBAR_BANDS, sampleRate); + + for (let frame = 0; frame < frameCount; frame++) { + if (generation !== moodbarGeneration || request !== moodbarRequest) { + return new Uint8ClampedArray(0); + } + + magnitudes.fill(0); + + const intervalStart = frame * framesPerInterval; + const intervalEnd = Math.min(left.length, intervalStart + framesPerInterval); + const possibleFfts = Math.floor((intervalEnd - intervalStart) / MOODBAR_FFT_SIZE); + const fftCount = Math.min(possibleFfts, MOODBAR_MAX_FFTS_PER_INTERVAL); + + if (fftCount > 0) { + for (let i = 0; i < fftCount; i++) { + const segment = + fftCount === 1 ? 0 : Math.floor((i * (possibleFfts - 1)) / Math.max(1, fftCount - 1)); + addSpectrum(intervalStart + segment * MOODBAR_FFT_SIZE, magnitudes); + } + + const inverse = 1 / fftCount; + for (let band = 0; band < MOODBAR_BANDS; band++) { + magnitudes[band] *= inverse; + } + } else { + const center = Math.floor((intervalStart + intervalEnd) / 2); + addSpectrum(center - (MOODBAR_FFT_SIZE >> 1), magnitudes); + } + + writeMoodbarFrame(frames, frame, magnitudes, barkBandTable, barkMagnitudes); + + if ((frame & 7) === 7) await yieldWorker(); + } + + return finishMoodbar(frames, frameCount, width); +} + +const bsel = (buf) => (typeof buf === "number" ? buf : 0); +const bval = (buf) => (typeof buf === "number" ? undefined : buf); + +const uploadBuffer = (buf1, buf2, sampleRate) => { + moodbarLeft = buf1; + moodbarRight = buf2; + moodbarSampleRate = sampleRate || moodbarSampleRate; + moodbarGeneration++; + moodbarRequest++; + + return wasmUpload(buf1, buf2); +}; + +const downscale = (buf, size) => wasmDS(bsel(buf), bval(buf), size, -1); // -1 to disable antishimmer + +const sbcMax = (buf, start, end, n) => + wasmSbcMax(bsel(buf), bval(buf), start ?? -1, end ?? -1, n ?? -1); + +const fft = (buf, start, end, pad, persistence) => + wasmFFT(bsel(buf), bval(buf), start ?? -1, end ?? -1, pad ?? -1, persistence ?? 0); + +const centeredSlice = (buf, pos, width, downs) => + wasmCentSlic(bsel(buf), bval(buf), pos, width, downs ?? -1); + +const samplePeak = (buf, start, end) => wasmSamPk(bsel(buf), bval(buf), start ?? -1, end ?? -1); + +const rms = (buf, start, end) => wasmRms(bsel(buf), bval(buf), start ?? -1, end ?? -1); + +const getGoniometerPoints = (start, length) => wasmGonio(start, length); + +const centeredSpectogram = (buf, pos, width, padFft, ds) => + wasmSpecto(bsel(buf), bval(buf), pos, width, padFft, ds); + +onmessage = (e) => { + // rip type safety + const func = [ + wasmInit, + uploadBuffer, + downscale, + sbcMax, + fft, + centeredSlice, + samplePeak, + rms, + getGoniometerPoints, + centeredSpectogram, + buildMoodbar, + ][e.data[0]]; + if (!func) postMessage(["ERR", `${e.data[0]} is not a command`]); + + const res = func(...e.data.slice(2)); + if (res instanceof Promise) res.then((r) => postMessage([e.data[1], r])); + else postMessage([e.data[1], res]); +}; diff --git a/clients/web/src/assets/styles.css b/clients/web/src/assets/styles.css index 62fe149..55b5016 100644 --- a/clients/web/src/assets/styles.css +++ b/clients/web/src/assets/styles.css @@ -1,5 +1,6 @@ * { @apply font-mono; + box-sizing: border-box; } html, @@ -7,6 +8,8 @@ body, #app { width: 100%; height: 100%; + margin: 0; + overflow: hidden; background-color: black; color: white; } diff --git a/clients/web/src/audio.ts b/clients/web/src/audio.ts index 64361a4..50ab4fb 100644 --- a/clients/web/src/audio.ts +++ b/clients/web/src/audio.ts @@ -2,6 +2,7 @@ import { ref, reactive, computed, watchEffect } from "vue"; import { setupMediaSession } from "./mediaSession"; import type { Song } from "./syncClient"; import { currentTimestamp } from "./util"; +import { setAnalysisBuf } from "./analysis"; export const audioCtx = new AudioContext(); export const audioAnalyser = new AnalyserNode(audioCtx); @@ -32,23 +33,34 @@ watchEffect(() => { const prettyFormatTime = (time: number) => `${~~(time / 60)}:${(~~(time % 60)).toString().padStart(2, "0")}`; +export const getDuration = () => audioSource?.buffer?.duration ?? 0; +export const prettyDuration = () => prettyFormatTime(getDuration()); + const seek = ref(); export { seek }; export const prettySeek = computed(() => prettyFormatTime(seek.value!)); -setInterval( +/*setInterval( () => (seek.value = Math.min(audioCtx.currentTime - startTime + startSeek, getDuration())), - 100, -); + 20, +);*/ + +let i = 0; +const loop = () => { + if (++i === 2) { + seek.value = Math.min(audioCtx.currentTime - startTime + startSeek, getDuration()); + i = 0; + } + + requestAnimationFrame(loop); +}; +loop(); export const seekTo = (seek: number) => { startSeek = seek; audioSource?.start(0, seek); }; -export const getDuration = () => audioSource?.buffer?.duration ?? 0; -export const prettyDuration = () => prettyFormatTime(getDuration()); - async function loadAudio(url: string) { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); @@ -70,6 +82,7 @@ export async function play(song: Song, seek: number) { audioSource = new AudioBufferSourceNode(audioCtx, { buffer: await (songs[url] ?? preload(url)), }); + setAnalysisBuf(audioSource.buffer!); audioSource.connect(audioAnalyser).connect(audioGain).connect(audioCtx.destination); diff --git a/clients/web/src/components/AudioSpectrum.vue b/clients/web/src/components/AudioSpectrum.vue new file mode 100644 index 0000000..7682afe --- /dev/null +++ b/clients/web/src/components/AudioSpectrum.vue @@ -0,0 +1,85 @@ + + + diff --git a/clients/web/src/components/LiveWaveForm.vue b/clients/web/src/components/LiveWaveForm.vue new file mode 100644 index 0000000..ddb4aa3 --- /dev/null +++ b/clients/web/src/components/LiveWaveForm.vue @@ -0,0 +1,80 @@ + + + diff --git a/clients/web/src/components/MoodBar.vue b/clients/web/src/components/MoodBar.vue new file mode 100644 index 0000000..907b885 --- /dev/null +++ b/clients/web/src/components/MoodBar.vue @@ -0,0 +1,78 @@ + + + diff --git a/clients/web/src/components/TheGoniometer.vue b/clients/web/src/components/TheGoniometer.vue new file mode 100644 index 0000000..080fb32 --- /dev/null +++ b/clients/web/src/components/TheGoniometer.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/clients/web/src/components/ThePlayer.vue b/clients/web/src/components/ThePlayer.vue index 9f82443..19945ae 100644 --- a/clients/web/src/components/ThePlayer.vue +++ b/clients/web/src/components/ThePlayer.vue @@ -6,6 +6,7 @@ import { prettySeek, prettyDuration, volumeDbfs, getDuration, seek } from "@/aud import { getClient } from "@/syncClient"; import { timePromise } from "@/util"; import { visualizerEnabled } from "@/visualizer"; +import { enableAnalysis } from "@/analysis"; import fallbackart from "@/assets/fallbackart_10x.png"; const visualizerSupported = isButterchurnSupported(); @@ -24,8 +25,9 @@ const client = await timePromise.then(() => getClient());
+ :style="{ + background: `center / contain no-repeat url(${client.currentSong?.artUrl ?? fallbackart})`, + }" />
{{ client.currentSong?.name }}
@@ -40,20 +42,31 @@ const client = await timePromise.then(() => getClient());
+ :style="{ + scale: (seek ?? 0) / getDuration(), + translate: -50 + (100 * (seek ?? 0)) / getDuration() / 2 + '%', + }" /> +
+ +
+ VOL
-
VOL
{{ client.currentSong?.quote }} - +
+ + +
diff --git a/clients/web/src/components/TheRedundantInfo.vue b/clients/web/src/components/TheRedundantInfo.vue new file mode 100644 index 0000000..97f38db --- /dev/null +++ b/clients/web/src/components/TheRedundantInfo.vue @@ -0,0 +1,412 @@ + + + + + diff --git a/clients/web/src/components/TheSpectogram.vue b/clients/web/src/components/TheSpectogram.vue new file mode 100644 index 0000000..be5fe97 --- /dev/null +++ b/clients/web/src/components/TheSpectogram.vue @@ -0,0 +1,291 @@ + + + diff --git a/clients/web/src/components/WaveForm.vue b/clients/web/src/components/WaveForm.vue new file mode 100644 index 0000000..85d0cf4 --- /dev/null +++ b/clients/web/src/components/WaveForm.vue @@ -0,0 +1,130 @@ + + +