diff --git a/packages/alphatab/LICENSE.header b/packages/alphatab/LICENSE.header index 3e4836da8a..ff83506e0b 100644 --- a/packages/alphatab/LICENSE.header +++ b/packages/alphatab/LICENSE.header @@ -20,29 +20,5 @@ Integrated Libraries: URL: https://github.com/stevefolta/SFZero Purpose: TinySoundFont is based on SFZEro - Library: Haxe Standard Library - License: MIT - Copyright: Copyright (C)2005-2025 Haxe Foundation - URL: https://github.com/HaxeFoundation/haxe/tree/development/std - Purpose: Zip Inflate Algorithm - - Library: SharpZipLib - License: MIT - Copyright: Copyright © 2000-2018 SharpZipLib Contributors - URL: https://github.com/icsharpcode/SharpZipLib - Purpose: Zip Deflate Algorithm for writing compressed Zips - - Library: NVorbis - License: MIT - Copyright: Copyright (c) 2020 Andrew Ward - URL: https://github.com/NVorbis/NVorbis - Purpose: Vorbis Stream Decoding - - Library: libvorbis - License: BSD-3-Clause - Copyright: Copyright (c) 2002-2020 Xiph.org Foundation - URL: https://github.com/xiph/vorbis - Purpose: NVorbis adopted some code from libvorbis. - @preserve @license \ No newline at end of file diff --git a/packages/alphatab/src/synth/soundfont/Hydra.ts b/packages/alphatab/src/synth/soundfont/Hydra.ts index a0cdf8dbbb..beda24532a 100644 --- a/packages/alphatab/src/synth/soundfont/Hydra.ts +++ b/packages/alphatab/src/synth/soundfont/Hydra.ts @@ -10,8 +10,7 @@ import type { IReadable } from '@coderline/alphatab/io/IReadable'; import { TypeConversions } from '@coderline/alphatab/io/TypeConversions'; import { FormatError } from '@coderline/alphatab/FormatError'; -import { VorbisFile } from '@coderline/alphatab/synth/vorbis/VorbisFile'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { VorbisDecoder } from '@coderline/alphatab/synth/vorbis/VorbisDecoder'; /** * @internal @@ -30,24 +29,25 @@ export class Hydra { public sampleData: Uint8Array = new Uint8Array(0); private _sampleCache: Map = new Map(); + private _vorbis: VorbisDecoder = new VorbisDecoder(); public decodeSamples(startByte: number, endByte: number, decompressVorbis: boolean): Float32Array { const key = `${startByte}_${endByte}_${decompressVorbis}`; if (!this._sampleCache.has(key)) { let samples: Float32Array; - const sampleBytes = this.sampleData.slice( - // The DWORD dwStart contains the index, in sample data points, from the beginning of the sample data - // field to the first data point of this sample - startByte, - // The DWORD dwEnd contains the index, in sample data points, from the beginning of the sample data - // field to the first of the set of 46 zero valued data points following this sample. - endByte - ); - if (decompressVorbis) { - const vorbis = new VorbisFile(ByteBuffer.fromBuffer(sampleBytes)); - samples = vorbis.streams[0].samples; + // SF3: the sample header holds the byte range of the Ogg Vorbis stream + samples = this._vorbis.decode(this.sampleData, startByte, endByte - startByte).samples; } else { + const sampleBytes = this.sampleData.slice( + // The DWORD dwStart contains the index, in sample data points, from the beginning of the sample data + // field to the first data point of this sample + startByte, + // The DWORD dwEnd contains the index, in sample data points, from the beginning of the sample data + // field to the first of the set of 46 zero valued data points following this sample. + endByte + ); + // 6.1 Sample Data Format in the smpl Sub-chunk // The smpl sub-chunk, if present, contains one or more "samples" of digital audio information in the form // of linearly coded sixteen bit, signed, little endian (least significant byte first) words. diff --git a/packages/alphatab/src/synth/vorbis/IntBitReader.ts b/packages/alphatab/src/synth/vorbis/IntBitReader.ts deleted file mode 100644 index dc45adf5b2..0000000000 --- a/packages/alphatab/src/synth/vorbis/IntBitReader.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * This part of the Vorbis Decoder is adopted from NVorbis and then simplified for alphaTab - * https://github.com/NVorbis/NVorbis - * - * MIT License - * - * Copyright (c) 2020 Andrew Ward - * - * 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. - */ - -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import type { IReadable } from '@coderline/alphatab/io/IReadable'; - -/** - * @internal - */ -class IntBitReaderReadResult { - public value: number = 0; - public bitsRead: number = 0; -} - -/** - * @internal - */ -export class IntBitReader { - private static readonly _byteSize = 8; // size of byte in bits - - private readonly _source: IReadable; - private _bitBucket: bigint = 0n; // 8 - private _bitCount: bigint = 0n; // 4 - private _overflowBits: bigint = 0n; // 1 - - public constructor(source: IReadable) { - this._source = source; - } - - public readByte(): number { - return this.readBits(IntBitReader._byteSize); - } - - public readBit(): boolean { - return this.readBits(1) === 1; - } - - public readBytes(count: number): Uint8Array { - const bytes = new Uint8Array(count); - for (let i = 0; i < count; i++) { - bytes[i] = this.readByte() & 0xff; - } - return bytes; - } - - public readBits(count: number): number { - // short-circuit 0 - if (count === 0) { - return 0; - } - - const result = this.tryPeekBits(count); - - this.skipBits(count); - - return result.value; - } - - public tryPeekBits(count: number): IntBitReaderReadResult { - if (count < 0 || count > 32) { - throw new AlphaTabError(AlphaTabErrorType.General, 'IO: Cannot read more than 32 bits in one go'); - } - if (count === 0) { - return new IntBitReaderReadResult(); - } - - const result = new IntBitReaderReadResult(); - while (this._bitCount < count) { - const val = BigInt(this._source.readByte()); - if (val === -1n) { - result.bitsRead = Number(this._bitCount); - result.value = Number(this._bitBucket); - this._bitBucket = 0n; - this._bitCount = 0n; - - return result; - } - this._bitBucket = ((val & 0xffn) << this._bitCount) | this._bitBucket; - this._bitCount += 8n; - - if (this._bitCount > 32) { - this._overflowBits = (val >> (40n - this._bitCount)) & 0xffn; - } - } - - let bitBucket = this._bitBucket; - if (count < 64) { - bitBucket = bitBucket & ((1n << BigInt(count)) - 1n); - } - - result.value = Number(bitBucket); - - result.bitsRead = count; - return result; - } - - public skipBits(count: number) { - let bigCount = BigInt(count); - if (count === 0) { - // no-op - } else if (this._bitCount > bigCount) { - // we still have bits left over... - if (count > 31) { - this._bitBucket = 0n; - } else { - this._bitBucket = this._bitBucket >> bigCount; - } - if (this._bitCount > 32) { - const overflowCount = this._bitCount - 32n; - this._bitBucket = this._bitBucket | (this._overflowBits << (this._bitCount - bigCount - overflowCount)); - - if (overflowCount > count) { - // ugh, we have to keep bits in overflow - this._overflowBits = (this._overflowBits >> bigCount) & 0xffn; - } - } - - this._bitCount -= bigCount; - } else if (this._bitCount === bigCount) { - this._bitBucket = 0n; - this._bitCount = 0n; - } // _bitCount < count - else { - // we have to move more bits than we have available... - bigCount -= this._bitCount; - this._bitCount = 0n; - this._bitBucket = 0n; - - while (bigCount > 8) { - if (this._source.readByte() === -1) { - bigCount = 0n; - break; - } - bigCount -= 8n; - } - - if (bigCount > 0) { - const temp = BigInt(this._source.readByte()); - if (temp === -1n) { - } else { - this._bitBucket = temp >> bigCount; - this._bitCount = 8n - bigCount; - } - } - } - } -} diff --git a/packages/alphatab/src/synth/vorbis/Mdct.ts b/packages/alphatab/src/synth/vorbis/Mdct.ts new file mode 100644 index 0000000000..72ba0ba47b --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/Mdct.ts @@ -0,0 +1,147 @@ +/** + * The inverse MDCT of Vorbis (Vorbis I specification, section 1.3.2) for one block size `n`: + * `y[i] = sum(k < n/2) x[k] * cos(2pi/n * (i + 1/2 + n/4) * (k + 1/2))` for `i < n`, without normalization. + * + * It is computed through a DCT-IV of size `m = n/2`, which in turn uses a complex FFT of size `n/4` + * (the standard construction, e.g. Duhamel, Mahieux, Petit 1991): + * 1. pre-twiddle: `v[j] = (x[2j] + i * x[m-1-2j]) * e^(-i * pi * (j + 1/4) / m)`, + * 2. forward FFT of `v` (iterative radix-2, the input is stored in bit-reversed order), + * 3. post-twiddle: `w[j] = V[j] * e^(-i * pi * j / m)` giving the DCT-IV `u[2j] = re(w[j])`, `u[m-1-2j] = -im(w[j])`, + * 4. the IMDCT output follows from the DCT-IV by symmetry: + * `y[i] = u[i + m/2]` for `i < m/2`, `y[i] = -u[3m/2 - 1 - i]` for `i < 3m/2`, `y[i] = -u[i - 3m/2]` for the rest. + * @internal + */ +export class Mdct { + private readonly _n: number; + /** + * cos/sin pairs of the pre-twiddle angles `pi * (j + 1/4) / m`, followed by the post-twiddle angles `pi * j / m`. + */ + private readonly _twiddles: Float32Array; + /** + * cos/sin pairs of the FFT angles `2pi * k / (n/4)` for `k < n/8`. + */ + private readonly _fftTwiddles: Float32Array; + private readonly _bitReverse: Int32Array; + + public constructor(n: number) { + this._n = n; + const m = n >> 1; + const l = n >> 2; + + const twiddles = new Float32Array(n); + for (let j = 0; j < l; j++) { + const pre = (Math.PI * (j + 0.25)) / m; + const post = (Math.PI * j) / m; + twiddles[2 * j] = Math.cos(pre); + twiddles[2 * j + 1] = Math.sin(pre); + twiddles[m + 2 * j] = Math.cos(post); + twiddles[m + 2 * j + 1] = Math.sin(post); + } + this._twiddles = twiddles; + + const fftTwiddles = new Float32Array(l); + for (let k = 0; k < l >> 1; k++) { + const angle = (2 * Math.PI * k) / l; + fftTwiddles[2 * k] = Math.cos(angle); + fftTwiddles[2 * k + 1] = Math.sin(angle); + } + this._fftTwiddles = fftTwiddles; + + const bitReverse = new Int32Array(l); + for (let j = 0; j < l; j++) { + let reversed = 0; + for (let bit = 1; bit < l; bit = bit << 1) { + reversed = (reversed << 1) | ((j & bit) !== 0 ? 1 : 0); + } + bitReverse[j] = reversed; + } + this._bitReverse = bitReverse; + } + + /** + * Computes the inverse MDCT. + * @param input The `n/2` spectral coefficients. + * @param output Receives the `n` time domain samples. + * @param work A buffer of at least `n/2` values used as complex FFT buffer. + */ + public inverse(input: Float32Array, output: Float32Array, work: Float32Array) { + const n = this._n; + const m = n >> 1; + const l = n >> 2; + const twiddles = this._twiddles; + const bitReverse = this._bitReverse; + + // 1. pre-twiddle into bit-reversed positions, `work` holds re/im pairs + for (let j = 0; j < l; j++) { + const re = input[2 * j]; + const im = input[m - 1 - 2 * j]; + const c = twiddles[2 * j]; + const s = twiddles[2 * j + 1]; + const p = bitReverse[j] * 2; + work[p] = re * c + im * s; + work[p + 1] = im * c - re * s; + } + + // 2. FFT: the first two stages (twiddles 1 and -i) in one pass + for (let p = 0; p < m; p += 8) { + const r0 = work[p] + work[p + 2]; + const i0 = work[p + 1] + work[p + 3]; + const r1 = work[p] - work[p + 2]; + const i1 = work[p + 1] - work[p + 3]; + const r2 = work[p + 4] + work[p + 6]; + const i2 = work[p + 5] + work[p + 7]; + const r3 = work[p + 4] - work[p + 6]; + const i3 = work[p + 5] - work[p + 7]; + work[p] = r0 + r2; + work[p + 1] = i0 + i2; + work[p + 4] = r0 - r2; + work[p + 5] = i0 - i2; + // (r3 + i * i3) * -i = i3 - i * r3 + work[p + 2] = r1 + i3; + work[p + 3] = i1 - r3; + work[p + 6] = r1 - i3; + work[p + 7] = i1 + r3; + } + const fftTwiddles = this._fftTwiddles; + for (let size = 8; size <= l; size = size << 1) { + const half = size >> 1; + const step = (l / size) * 2; + for (let k = 0; k < half; k++) { + // e^(-2pi * i * k / size) + const c = fftTwiddles[k * step]; + const s = fftTwiddles[k * step + 1]; + for (let p = 2 * k; p < m; p += 2 * size) { + const q = p + size; + const tr = work[q] * c + work[q + 1] * s; + const ti = work[q + 1] * c - work[q] * s; + work[q] = work[p] - tr; + work[q + 1] = work[p + 1] - ti; + work[p] += tr; + work[p + 1] += ti; + } + } + } + + // 3. + 4. post-twiddle and unfold the DCT-IV values u[2j] and u[m-1-2j] into the output + const quarter = m >> 1; + for (let j = 0; j < l; j++) { + const re = work[2 * j]; + const im = work[2 * j + 1]; + const c = twiddles[m + 2 * j]; + const s = twiddles[m + 2 * j + 1]; + const even = re * c + im * s; + const odd = re * s - im * c; + const i = 2 * j; + const o = m - 1 - 2 * j; + if (j < l >> 1) { + output[i + m + quarter] = -even; + output[o - quarter] = odd; + } else { + output[i - quarter] = even; + output[o + m + quarter] = -odd; + } + output[m + quarter - 1 - i] = -even; + output[m + quarter - 1 - o] = -odd; + } + } +} diff --git a/packages/alphatab/src/synth/vorbis/OggReader.ts b/packages/alphatab/src/synth/vorbis/OggReader.ts index b950685378..0aec89f3b7 100644 --- a/packages/alphatab/src/synth/vorbis/OggReader.ts +++ b/packages/alphatab/src/synth/vorbis/OggReader.ts @@ -1,142 +1,100 @@ -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import type { IReadable } from '@coderline/alphatab/io/IReadable'; - /** + * The packets of an Ogg logical bitstream. + * @record * @internal */ -export class OggPacket { - public packetData: Uint8Array; - public isBeginningOfStream: boolean; - public isEndOfStream: boolean; - public granulePosition: number | null; - - public constructor( - data: Uint8Array, - isBeginOfStream: boolean, - isEndOfStream: boolean, - granulePosition: number | null - ) { - this.packetData = data; - this.isBeginningOfStream = isBeginOfStream; - this.isEndOfStream = isEndOfStream; - this.granulePosition = isEndOfStream ? granulePosition : null; - } - - public addData(newData: Uint8Array) { - const oldData = this.packetData; - const newBuffer = new Uint8Array(oldData.length + newData.length); - newBuffer.set(oldData, 0); - newBuffer.set(newData, oldData.length); - } -} - -/** - * @internal - */ -enum PageFlags { - ContinuesPacket = 1, - BeginningOfStream = 2, - EndOfStream = 4 +export interface OggPackets { + /** + * The data of all packets, back to back. + */ + data: Uint8Array; + /** + * The end offset of each packet in {@link data}, a packet starts where the previous one ends. + */ + ends: number[]; + /** + * The granule position per packet: the one of the page for the last packet completed on a page, otherwise -1. + */ + granules: number[]; } /** + * Splits the first logical bitstream of an Ogg container into packets (RFC 3533). + * + * The page bodies are concatenated and the packet boundaries follow from the lacing values alone, + * which makes packets spanning multiple pages work without special handling. + * Pages of other logical bitstreams are skipped, incomplete pages and packets at the end are dropped. * @internal */ export class OggReader { - private _readable: IReadable; - - public constructor(readable: IReadable) { - this._readable = readable; - } - - public read(): OggPacket[] { - const packets: OggPacket[] = []; - while (this._findAndReadPage(packets)) {} - return packets; - } - - private _findAndReadPage(packets: OggPacket[]): boolean { - if (!this._seekPageHeader()) { - return false; - } - - return this._readPage(packets); - } - - private _seekPageHeader(): boolean { - // search for sync byte (max 64KB) - for (let i = 0; i < 65536; i++) { - const magic = IOHelper.readInt32LE(this._readable); - if (magic === 0x5367674f) { - return true; + public static read(data: Uint8Array, offset: number, length: number): OggPackets { + const end = Math.min(offset + length, data.length); + const body = new Uint8Array(Math.max(0, end - offset)); + const ends: number[] = []; + const granules: number[] = []; + let size = 0; + let serial = 0; + let hasSerial = false; + + let pos = offset; + while (pos + 27 <= end) { + // capture pattern 'OggS', resync byte by byte if missing + if (data[pos] !== 0x4f || data[pos + 1] !== 0x67 || data[pos + 2] !== 0x67 || data[pos + 3] !== 0x53) { + pos++; + continue; } - this._readable.position -= 3; - } - - return false; - } - private _readPage(packets: OggPacket[]): boolean { - const version = this._readable.readByte(); - if (version === -1 || version !== 0) { - return false; - } - - const pageFlags = this._readable.readByte(); - const pageGranulePosition = IOHelper.readInt64LE(this._readable); // GranulePosition - this._readable.skip(4); // StreamSerial - this._readable.skip(4); // SequenceNumber - this._readable.skip(4); // Crc - - const segmentCount = this._readable.readByte(); - if (segmentCount === -1) { - return false; - } - - const packetSizes: number[] = []; - let packetIndex = 0; - for (let i = 0; i < segmentCount; i++) { - const size = this._readable.readByte(); - - // ensure packet size exists and add size - if (packetIndex === packetSizes.length) { - packetSizes.push(0); + const segments = data[pos + 26]; + const bodyStart = pos + 27 + segments; + if (bodyStart > end) { + break; } - packetSizes[packetIndex] += size; - - // net packet if current one was not full - if (size < 255) { - packetIndex++; + let bodySize = 0; + for (let i = 0; i < segments; i++) { + bodySize += data[pos + 27 + i]; + } + if (bodyStart + bodySize > end) { + break; } - } - for (let i = 0; i < packetSizes.length; i++) { - const packetData = new Uint8Array(packetSizes[i]); - const c = this._readable.read(packetData, 0, packetData.length); - if (c !== packetData.length) { - return false; + const pageSerial = + data[pos + 14] | (data[pos + 15] << 8) | (data[pos + 16] << 16) | (data[pos + 17] << 24); + if (!hasSerial) { + serial = pageSerial; + hasSerial = true; } - if ((pageFlags & PageFlags.ContinuesPacket) !== 0) { - if (packets.length === 0) { - throw new AlphaTabError( - AlphaTabErrorType.Format, - 'OGG: Continuation page without any previous packets' - ); + if (pageSerial === serial) { + const firstPacket = ends.length; + let source = bodyStart; + for (let i = 0; i < segments; i++) { + const lacing = data[pos + 27 + i]; + for (let j = 0; j < lacing; j++) { + body[size + j] = data[source + j]; + } + source += lacing; + size += lacing; + if (lacing < 255) { + ends.push(size); + granules.push(-1); + } + } + if (ends.length > firstPacket) { + // 64-bit granule position: unsigned low word, signed high word + const low = + data[pos + 6] + data[pos + 7] * 256 + data[pos + 8] * 65536 + data[pos + 9] * 16777216; + const high = + data[pos + 10] | (data[pos + 11] << 8) | (data[pos + 12] << 16) | (data[pos + 13] << 24); + granules[ends.length - 1] = high * 4294967296 + low; + } + if ((data[pos + 5] & 4) !== 0) { + break; } - packets[packets.length - 1].addData(packetData); - } else { - const packet = new OggPacket( - packetData, - (pageFlags & PageFlags.BeginningOfStream) !== 0 && i === 0, - (pageFlags & PageFlags.EndOfStream) !== 0 && i === packetSizes.length - 1, - pageGranulePosition - ); - packets.push(packet); } + + pos = bodyStart + bodySize; } - return true; + return { data: body, ends, granules }; } } diff --git a/packages/alphatab/src/synth/vorbis/VorbisBitReader.ts b/packages/alphatab/src/synth/vorbis/VorbisBitReader.ts new file mode 100644 index 0000000000..5aec9036db --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/VorbisBitReader.ts @@ -0,0 +1,89 @@ +/** + * Reads the bits of a Vorbis packet, least significant bit first (Vorbis I specification, section 2). + * + * Reading past the end of the packet returns zero bits and sets {@link overrun}: the 'end-of-packet' condition. + * Callers check it when it matters, e.g. a codeword is valid if it ends within the packet. + * + * alphaTab is transpiled to C# and Kotlin where `number` becomes a `double` and every bit operation + * converts its operands to a 32-bit integer. To produce identical results on all platforms, the + * Vorbis decoder follows these rules: + * - values taking part in bit operations stay within [0, 2^31) (Huffman tree links `~node` are the only negative values), + * - a single read is at most 24 bits so the bit buffer never exceeds 31 bits, 32-bit fields are read in parts, + * - no `>>>` (it becomes a signed shift) and no compound bit assignments (`x >>= n`), + * - unsigned 32-bit header values are assembled arithmetically, + * - float buffers are `Float32Array` (there is no `Float64Array` on all platforms), integer tables `Int32Array`, + * - no writes through `subarray` (it copies on Kotlin), no `fill` with values other than zero, + * - every index taken from the file is validated in the setup or clamped during audio decode. + * @internal + */ +export class VorbisBitReader { + private _data: Uint8Array = new Uint8Array(0); + private _pos: number = 0; + private _end: number = 0; + private _bits: number = 0; + private _count: number = 0; + + /** + * Whether more bits were consumed than the packet holds. + */ + public get overrun(): boolean { + return (this._pos - this._end) * 8 > this._count; + } + + /** + * The number of bits left in the packet. + */ + public get remaining(): number { + return (this._end - this._pos) * 8 + this._count; + } + + public reset(data: Uint8Array, start: number, end: number) { + this._data = data; + this._pos = start; + this._end = end; + this._bits = 0; + this._count = 0; + } + + /** + * Returns the next bits without consuming them. + * @param count The number of bits, at most 24. + */ + public peek(count: number): number { + while (this._count < count) { + const value = this._pos < this._end ? this._data[this._pos] : 0; + this._bits = this._bits | (value << this._count); + this._pos++; + this._count += 8; + } + return this._bits & ((1 << count) - 1); + } + + /** + * Consumes bits which were peeked before. + */ + public skip(count: number) { + this._bits = this._bits >> count; + this._count -= count; + } + + /** + * Reads an unsigned value of at most 24 bits. + */ + public read(count: number): number { + const value = this.peek(count); + this.skip(count); + return value; + } + + /** + * The number of bits needed to represent the given value (Vorbis I specification, section 9.2.1). + */ + public static ilog(value: number): number { + let bits = 0; + for (let v = value; v > 0; v = v >> 1) { + bits++; + } + return bits; + } +} diff --git a/packages/alphatab/src/synth/vorbis/VorbisCodebook.ts b/packages/alphatab/src/synth/vorbis/VorbisCodebook.ts new file mode 100644 index 0000000000..057b19ca4e --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/VorbisCodebook.ts @@ -0,0 +1,285 @@ +import { FormatError } from '@coderline/alphatab/FormatError'; +import { VorbisBitReader } from '@coderline/alphatab/synth/vorbis/VorbisBitReader'; + +/** + * A Vorbis codebook (Vorbis I specification, section 3): a Huffman code for the entries and + * optionally a vector of values per entry (VQ lookup). + * + * Codewords are assigned in entry order, each one gets the lowest free codeword of its length. + * They are kept in a binary tree, two child links per node: `> 0` a node, `< 0` the leaf `~entry`, `0` a free slot. + * Every node knows the shallowest depth of a free slot below it, so an assignment follows a single path. + * + * Decoding looks up the next {@link _tableBits} bits in a table holding `entry << 5 | length` for + * shorter codewords and `~node` to continue in the tree for longer ones. `0` marks bits no codeword starts with. + * @internal + */ +export class VorbisCodebook { + private static readonly _maxTableBits = 10; + private static readonly _noFreeSlot = 255; + + public readonly dimensions: number; + public readonly entries: number; + /** + * The vector values per entry (`entries * dimensions`), or null if the codebook has no VQ lookup. + */ + public readonly values: Float32Array | null = null; + + private _table: Int32Array; + private _tableBits: number; + private _tree: Int32Array; + + public static invalid(): FormatError { + return new FormatError('Invalid Vorbis stream'); + } + + public constructor(r: VorbisBitReader) { + if (r.read(24) !== 0x564342) { + throw VorbisCodebook.invalid(); + } + const dimensions = r.read(16); + const entries = r.read(24); + if (VorbisBitReader.ilog(dimensions) + VorbisBitReader.ilog(entries) > 24) { + throw VorbisCodebook.invalid(); + } + this.dimensions = dimensions; + this.entries = entries; + + // codeword lengths, 0 = unused entry + const lengths = new Uint8Array(entries); + let used = 0; + let maxLength = 0; + if (r.read(1) === 1) { + let length = r.read(5) + 1; + while (used < entries) { + const count = r.read(VorbisBitReader.ilog(entries - used)); + if (length > 32 || used + count > entries) { + throw VorbisCodebook.invalid(); + } + for (let i = 0; i < count; i++) { + lengths[used + i] = length; + } + if (count > 0) { + used += count; + maxLength = length; + } + length++; + } + } else { + const sparse = r.read(1); + if (r.remaining < entries * (sparse === 1 ? 1 : 5)) { + throw VorbisCodebook.invalid(); + } + for (let i = 0; i < entries; i++) { + if (sparse === 0 || r.read(1) === 1) { + const length = r.read(5) + 1; + lengths[i] = length; + maxLength = Math.max(maxLength, length); + used++; + } + } + } + + if (used < 2) { + // a single used entry has a 1 bit codeword, both bit values decode to it (errata 20150226), + // without any used entry every decode fails + let entry = 0; + while (entry < entries && lengths[entry] === 0) { + entry++; + } + if (used === 1 && maxLength !== 1) { + throw VorbisCodebook.invalid(); + } + this._tableBits = used; + this._table = new Int32Array(used + 1); + this._table[0] = used === 1 ? (entry << 5) | 1 : 0; + this._table[used] = this._table[0]; + this._tree = new Int32Array(0); + } else { + this._tableBits = Math.min(maxLength, VorbisCodebook._maxTableBits); + this._table = new Int32Array(1 << this._tableBits); + this._tree = new Int32Array(2 * used); + this._assignCodewords(lengths, used); + } + + const lookupType = r.read(4); + if (lookupType > 2 || (lookupType > 0 && dimensions === 0)) { + throw VorbisCodebook.invalid(); + } + if (lookupType > 0) { + const minimum = VorbisCodebook._readFloat(r); + const delta = VorbisCodebook._readFloat(r); + const valueBits = r.read(4) + 1; + const sequence = r.read(1) === 1; + const lookupValues = + lookupType === 1 ? VorbisCodebook._lookup1Values(entries, dimensions) : entries * dimensions; + if (lookupValues * valueBits > r.remaining) { + throw VorbisCodebook.invalid(); + } + const multiplicands = new Int32Array(lookupValues); + for (let i = 0; i < lookupValues; i++) { + multiplicands[i] = r.read(valueBits); + } + + const values = new Float32Array(entries * dimensions); + for (let entry = 0; entry < entries; entry++) { + let last = 0; + let divisor = 1; + for (let d = 0; d < dimensions; d++) { + const offset = entry * dimensions + d; + const index = lookupType === 1 ? Math.floor(entry / divisor) % lookupValues : offset; + values[offset] = multiplicands[index] * delta + minimum + last; + if (sequence) { + last = values[offset]; + } + divisor *= lookupValues; + } + } + this.values = values; + } + } + + private _assignCodewords(lengths: Uint8Array, used: number) { + const noFreeSlot = VorbisCodebook._noFreeSlot; + const tree = this._tree; + const table = this._table; + const tableBits = this._tableBits; + // shallowest depth of a free slot below each node + const free = new Uint8Array(used); + const path = new Int32Array(33); + free[0] = 1; + let nodes = 1; + + for (let entry = 0; entry < lengths.length; entry++) { + const length = lengths[entry]; + if (length === 0) { + continue; + } + + // descend towards the leftmost free slot at depth `length` + let node = 0; + let depth = 0; + let index = 0; + while (true) { + path[depth] = node; + let side = 0; + if (VorbisCodebook._freeDepth(tree, free, node, 0, depth) > length) { + side = 1; + if (VorbisCodebook._freeDepth(tree, free, node, 1, depth) > length) { + // over-subscribed code + throw VorbisCodebook.invalid(); + } + } + const slot = 2 * node + side; + if (depth < tableBits) { + index = index | (side << depth); + } + depth++; + if (depth === length) { + tree[slot] = ~entry; + break; + } + + let child = tree[slot]; + if (child === 0) { + // a complete code has `used - 1` nodes + if (nodes === used) { + throw VorbisCodebook.invalid(); + } + child = nodes++; + tree[slot] = child; + free[child] = depth + 1; + if (depth === tableBits) { + table[index] = ~child; + } + } + node = child; + } + + if (length <= tableBits) { + for (let i = index; i < table.length; i += 1 << length) { + table[i] = (entry << 5) | length; + } + } + + for (let d = depth - 1; d >= 0; d--) { + const n = path[d]; + free[n] = Math.min( + VorbisCodebook._freeDepth(tree, free, n, 0, d), + VorbisCodebook._freeDepth(tree, free, n, 1, d) + ); + } + } + + // under-populated code + if (free[0] !== noFreeSlot) { + throw VorbisCodebook.invalid(); + } + } + + private static _freeDepth(tree: Int32Array, free: Uint8Array, node: number, side: number, depth: number): number { + const child = tree[2 * node + side]; + return child === 0 ? depth + 1 : child < 0 ? VorbisCodebook._noFreeSlot : free[child]; + } + + /** + * Reads the next entry number (scalar context). + * @returns The entry or -1 if the packet ended or the bits match no codeword. + */ + public decode(r: VorbisBitReader): number { + const bits = this._tableBits; + const value = this._table[r.peek(bits)]; + if (value > 0) { + r.skip(value & 31); + return r.overrun ? -1 : value >> 5; + } + if (value === 0) { + return -1; + } + r.skip(bits); + const tree = this._tree; + let node = ~value; + while (true) { + const child = tree[2 * node + r.read(1)]; + if (child < 0) { + return r.overrun ? -1 : ~child; + } + if (child === 0) { + return -1; + } + node = child; + } + } + + /** + * Unpacks a codebook float: 21 bits mantissa, 10 bits exponent, 1 bit sign (Vorbis I specification, section 9.2.2). + */ + private static _readFloat(r: VorbisBitReader): number { + const mantissa = r.read(21); + const exponent = r.read(10); + const value = mantissa * Math.pow(2, exponent - 788); + return r.read(1) === 1 ? -value : value; + } + + /** + * The greatest value whose `dimensions`-th power is not greater than `entries` (Vorbis I specification, section 9.2.3). + */ + private static _lookup1Values(entries: number, dimensions: number): number { + let value = Math.floor(Math.exp(Math.log(entries) / dimensions)); + while (VorbisCodebook._power(value + 1, dimensions) <= entries) { + value++; + } + while (value > 0 && VorbisCodebook._power(value, dimensions) > entries) { + value--; + } + return value; + } + + private static _power(base: number, exponent: number): number { + let result = 1; + // entries are below 2^24, stop once above + for (let i = 0; i < exponent && result <= 16777216; i++) { + result *= base; + } + return result; + } +} diff --git a/packages/alphatab/src/synth/vorbis/VorbisDecoder.ts b/packages/alphatab/src/synth/vorbis/VorbisDecoder.ts new file mode 100644 index 0000000000..2ba1a39479 --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/VorbisDecoder.ts @@ -0,0 +1,392 @@ +import { Mdct } from '@coderline/alphatab/synth/vorbis/Mdct'; +import { OggReader } from '@coderline/alphatab/synth/vorbis/OggReader'; +import { VorbisBitReader } from '@coderline/alphatab/synth/vorbis/VorbisBitReader'; +import { VorbisCodebook } from '@coderline/alphatab/synth/vorbis/VorbisCodebook'; +import { type VorbisMode, VorbisSetup } from '@coderline/alphatab/synth/vorbis/VorbisSetup'; + +/** + * A decoded Vorbis stream. + * @record + * @internal + */ +export interface VorbisStream { + channels: number; + sampleRate: number; + /** + * The samples, interleaved if there are multiple channels. + */ + samples: Float32Array; +} + +/** + * @record + * @internal + */ +interface VorbisSetupCacheEntry { + channels: number; + blockSizes: number; + setup: Uint8Array; + value: VorbisSetup; +} + +/** + * Decodes Ogg Vorbis streams like the samples of SF3 soundfonts (Vorbis I specification, section 4.3). + * + * An instance keeps the parsed setups, the MDCTs and the window slopes, as all samples of a soundfont + * share a few setups. Decoding is not thread-safe, use one instance per thread. + * @internal + */ +export class VorbisDecoder { + private static readonly _maxCachedSetups = 16; + + private _setups: VorbisSetupCacheEntry[] = []; + private _nextSetup: number = 0; + private _mdcts: Map = new Map(); + private _slopes: Map = new Map(); + private _reader: VorbisBitReader = new VorbisBitReader(); + + // per stream buffers, reused while the channels and block size stay the same + private _channels: number = 0; + private _half: number = 0; + private _spectra: Float32Array[] = []; + private _curves: Float32Array[] = []; + private _tails: Float32Array[] = []; + private _bundle: Float32Array[] = []; + private _bundleSkip: Uint8Array = new Uint8Array(0); + private _used: Uint8Array = new Uint8Array(0); + private _noResidue: Uint8Array = new Uint8Array(0); + private _block: Float32Array = new Float32Array(0); + private _work: Float32Array = new Float32Array(0); + + /** + * Decodes the first logical bitstream in the given Ogg data. + */ + public decode(data: Uint8Array, offset: number, length: number): VorbisStream { + const ogg = OggReader.read(data, offset, length); + const body = ogg.data; + const ends = ogg.ends; + const granules = ogg.granules; + // identification header (section 4.2.2), byte aligned + if (ends.length < 3 || ends[0] < 30 || !VorbisSetup.isHeader(body, 0, ends[0], 1)) { + throw VorbisCodebook.invalid(); + } + const channels = body[11]; + const sampleRate = body[12] + body[13] * 256 + body[14] * 65536 + body[15] * 16777216; + const blockSizes = body[28]; + if ( + (body[7] | body[8] | body[9] | body[10]) !== 0 || + channels === 0 || + sampleRate === 0 || + (blockSizes & 15) < 6 || + (blockSizes & 15) > blockSizes >> 4 || + blockSizes >> 4 > 13 || + (body[29] & 1) === 0 || + !VorbisSetup.isHeader(body, ends[0], ends[1], 3) + ) { + throw VorbisCodebook.invalid(); + } + const setup = this._setup(channels, blockSizes, body, ends[1], ends[2]); + const blockSize0 = setup.blockSize0; + const half = setup.blockSize1 >> 1; + this._prepare(channels, half); + + // the output ends at the last granule position, limited to what the packets can produce + let last = ends.length - 1; + while (last >= 3 && granules[last] < 0) { + last--; + } + const finalGranule = last >= 3 ? granules[last] : -1; + const bound = Math.max(0, ends.length - 4) * half; + const capacity = finalGranule >= 0 && finalGranule < bound ? finalGranule : bound; + let output = new Float32Array(capacity * channels); + + const r = this._reader; + const block = this._block; + let written = 0; + let decoded = 0; + // granule position minus decoded frames, known with the first granule position + let granuleOffset = 0; + let hasGranuleOffset = false; + let previous = 0; + for (let p = 3; p < ends.length; p++) { + r.reset(body, ends[p - 1], ends[p]); + if (r.read(1) !== 0) { + continue; + } + const modeNumber = r.read(setup.modeBits); + if (modeNumber >= setup.modes.length) { + continue; + } + const mode = setup.modes[modeNumber]; + let n = blockSize0; + let rightSlope = blockSize0 >> 1; + if (mode.isLong) { + n = half * 2; + // the left slope follows the actual previous block, not the flag + r.read(1); + if (r.read(1) === 1) { + rightSlope = half; + } + } + if (r.overrun) { + continue; + } + this._decodeSpectra(r, mode, channels, n >> 1); + + // overlap-add from the center of the previous block to the center of this one (section 4.3.8) + const frames = previous === 0 ? 0 : (previous + n) / 4; + const count = Math.max(0, Math.min(frames, capacity - written)); + const mdct = this._mdct(n); + for (let c = 0; c < channels; c++) { + if (this._used[c] === 1) { + mdct.inverse(this._spectra[c], block, this._work); + } else { + block.fill(0, 0, n); + } + this._overlap(n, previous, rightSlope, this._tails[c], output, written, count, channels, c); + } + written += count; + decoded += frames; + previous = n; + + if (!hasGranuleOffset && granules[p] >= 0) { + hasGranuleOffset = true; + granuleOffset = granules[p] - decoded; + // a stream starting before granule 0 drops the leading samples, unless it ends on the same page + if (granuleOffset < 0 && p < last) { + const drop = Math.min(-granuleOffset, written) * channels; + for (let i = drop; i < written * channels; i++) { + output[i - drop] = output[i]; + } + written -= drop / channels; + } + } + } + + if (finalGranule >= 0) { + written = Math.max(0, Math.min(written, finalGranule - Math.max(granuleOffset, 0))); + } + if (written * channels < output.length) { + const trimmed = new Float32Array(written * channels); + trimmed.set(output.subarray(0, trimmed.length), 0); + output = trimmed; + } + return { channels, sampleRate, samples: output }; + } + + /** + * Decodes the floors and residues of a packet into the spectra of all channels (sections 4.3.2 - 4.3.6). + */ + private _decodeSpectra(r: VorbisBitReader, mode: VorbisMode, channels: number, n: number) { + const mapping = mode.mapping; + const spectra = this._spectra; + const used = this._used; + const noResidue = this._noResidue; + for (let c = 0; c < channels; c++) { + spectra[c].fill(0, 0, n); + const isUsed = mapping.floors[mapping.mux[c]].decode(r, n, this._curves[c]); + used[c] = isUsed ? 1 : 0; + noResidue[c] = isUsed ? 0 : 1; + } + + // coupled channels need the residue if one of them has a floor + const magnitudes = mapping.magnitudes; + const angles = mapping.angles; + for (let i = 0; i < magnitudes.length; i++) { + if (noResidue[magnitudes[i]] === 0 || noResidue[angles[i]] === 0) { + noResidue[magnitudes[i]] = 0; + noResidue[angles[i]] = 0; + } + } + + for (let submap = 0; submap < mapping.residues.length; submap++) { + let count = 0; + for (let c = 0; c < channels; c++) { + if (mapping.mux[c] === submap) { + this._bundle[count] = spectra[c]; + this._bundleSkip[count] = noResidue[c]; + count++; + } + } + mapping.residues[submap].decode(r, this._bundle, this._bundleSkip, count, n); + } + + // inverse square polar coupling + for (let i = magnitudes.length - 1; i >= 0; i--) { + const magnitude = spectra[magnitudes[i]]; + const angle = spectra[angles[i]]; + for (let j = 0; j < n; j++) { + const m = magnitude[j]; + const a = angle[j]; + if (m > 0) { + if (a > 0) { + angle[j] = m - a; + } else { + angle[j] = m; + magnitude[j] = m + a; + } + } else if (a > 0) { + angle[j] = m + a; + } else { + angle[j] = m; + magnitude[j] = m - a; + } + } + } + + // apply the floors, channels with an unused floor stay silent + for (let c = 0; c < channels; c++) { + if (used[c] === 1) { + const spectrum = spectra[c]; + const curve = this._curves[c]; + for (let j = 0; j < n; j++) { + spectrum[j] *= curve[j]; + } + } + } + } + + /** + * Windows the block, adds its left half to the tail of the previous block for the output and keeps its right half as new tail. + * @param n The block size. + * @param previous The size of the previous block, 0 if this is the first block. + * @param rightSlope The length of the right window slope. + * @param count The number of output frames to write. + */ + private _overlap( + n: number, + previous: number, + rightSlope: number, + tail: Float32Array, + output: Float32Array, + written: number, + count: number, + channels: number, + channel: number + ) { + const block = this._block; + // block sizes are powers of two, the divisions are exact + const quarter = n / 4; + + if (previous > 0) { + // the block's left half starts at the center of the previous block's right slope + const leftSlope = Math.min(previous, n) / 2; + const leftWindow = this._slope(leftSlope); + const start = previous / 4 - leftSlope / 2; + const shift = quarter - previous / 4; + let j = 0; + let o = written * channels + channel; + for (; j < Math.min(start, count); j++) { + output[o] = tail[j]; + o += channels; + } + for (; j < Math.min(start + leftSlope, count); j++) { + output[o] = tail[j] + block[j + shift] * leftWindow[j - start]; + o += channels; + } + for (; j < count; j++) { + output[o] = tail[j] + block[j + shift]; + o += channels; + } + } + + const center = n / 2; + const rightWindow = this._slope(rightSlope); + const slopeStart = quarter - rightSlope / 2; + const slopeEnd = quarter + rightSlope / 2; + for (let m = 0; m < slopeStart; m++) { + tail[m] = block[center + m]; + } + for (let m = slopeStart; m < slopeEnd; m++) { + tail[m] = block[center + m] * rightWindow[slopeEnd - 1 - m]; + } + tail.fill(0, slopeEnd, tail.length); + } + + private _prepare(channels: number, half: number) { + if (this._channels === channels && this._half === half) { + return; + } + this._channels = channels; + this._half = half; + this._spectra = []; + this._curves = []; + this._tails = []; + this._bundle = []; + for (let c = 0; c < channels; c++) { + this._spectra.push(new Float32Array(half)); + this._curves.push(new Float32Array(half)); + this._tails.push(new Float32Array(half)); + this._bundle.push(this._spectra[c]); + } + this._bundleSkip = new Uint8Array(channels); + this._used = new Uint8Array(channels); + this._noResidue = new Uint8Array(channels); + this._block = new Float32Array(half * 2); + this._work = new Float32Array(half); + } + + private _mdct(n: number): Mdct { + let mdct = this._mdcts.get(n); + if (!mdct) { + mdct = new Mdct(n); + this._mdcts.set(n, mdct); + } + return mdct; + } + + /** + * The rising window slope `sin(pi/2 * sin^2((i + 0.5) / length * pi/2))` (section 4.3.1). + */ + private _slope(length: number): Float32Array { + let slope = this._slopes.get(length); + if (!slope) { + slope = new Float32Array(length); + for (let i = 0; i < length; i++) { + const x = Math.sin((((i + 0.5) / length) * Math.PI) / 2); + slope[i] = Math.sin((Math.PI / 2) * x * x); + } + this._slopes.set(length, slope); + } + return slope; + } + + /** + * Returns the parsed setup header, parsing it only if it is not cached. + */ + private _setup(channels: number, blockSizes: number, body: Uint8Array, start: number, end: number): VorbisSetup { + for (const cached of this._setups) { + if ( + cached.channels === channels && + cached.blockSizes === blockSizes && + VorbisDecoder._equals(cached.setup, body, start, end) + ) { + return cached.value; + } + } + + const value = new VorbisSetup(channels, 1 << (blockSizes & 15), 1 << (blockSizes >> 4), body, start, end); + const setup = new Uint8Array(end - start); + setup.set(body.subarray(start, end), 0); + const entry: VorbisSetupCacheEntry = { channels, blockSizes, setup, value }; + if (this._setups.length < VorbisDecoder._maxCachedSetups) { + this._setups.push(entry); + } else { + this._setups[this._nextSetup] = entry; + this._nextSetup = (this._nextSetup + 1) % VorbisDecoder._maxCachedSetups; + } + return value; + } + + private static _equals(cached: Uint8Array, data: Uint8Array, start: number, end: number): boolean { + if (cached.length !== end - start) { + return false; + } + for (let i = 0; i < cached.length; i++) { + if (cached[i] !== data[start + i]) { + return false; + } + } + return true; + } +} diff --git a/packages/alphatab/src/synth/vorbis/VorbisFile.ts b/packages/alphatab/src/synth/vorbis/VorbisFile.ts deleted file mode 100644 index 13cdcdeae4..0000000000 --- a/packages/alphatab/src/synth/vorbis/VorbisFile.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { IReadable } from '@coderline/alphatab/io/IReadable'; -import { OggReader } from '@coderline/alphatab/synth/vorbis/OggReader'; -import type { VorbisStream } from '@coderline/alphatab/synth/vorbis/VorbisStream'; -import { VorbisStreamReader } from '@coderline/alphatab/synth/vorbis/VorbisStreamReader'; - -/** - * @internal - */ -export class VorbisFile { - public streams: VorbisStream[] = []; - - public constructor(readable: IReadable) { - const oggContainer = new OggReader(readable); - const packets = oggContainer.read(); - - const decoder = new VorbisStreamReader(packets); - while (true) { - const stream = decoder.read(); - if (stream == null) { - break; - } - this.streams.push(stream); - } - } -} diff --git a/packages/alphatab/src/synth/vorbis/VorbisFloor.ts b/packages/alphatab/src/synth/vorbis/VorbisFloor.ts new file mode 100644 index 0000000000..2feaad6c1e --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/VorbisFloor.ts @@ -0,0 +1,258 @@ +import { VorbisBitReader } from '@coderline/alphatab/synth/vorbis/VorbisBitReader'; +import { VorbisCodebook } from '@coderline/alphatab/synth/vorbis/VorbisCodebook'; + +/** + * A floor of type 1 (Vorbis I specification, section 7): a piecewise linear spectral envelope. + * Floor type 0 is not supported, no encoder uses it since 2002. + * @internal + */ +export class VorbisFloor { + private static readonly _ranges = [256, 128, 86, 64]; + + /** + * The floor values as linear amplitude: `floor1_inverse_dB_table` (section 10.1) has a constant step of + * about 0.547 dB from -140 dB to 0 dB. Computed with the step of its first entry it matches the + * listed values within one float rounding step. + */ + private static readonly _inverseDb: Float32Array = VorbisFloor._buildInverseDb(); + + private readonly _partitionClasses: Int32Array; + private readonly _classDimensions: Int32Array; + private readonly _classBits: Int32Array; + private readonly _classBooks: (VorbisCodebook | null)[] = []; + /** + * Up to 4 subclass books per class. + */ + private readonly _subclassBooks: (VorbisCodebook | null)[] = []; + private readonly _multiplier: number; + private readonly _range: number; + private readonly _yBits: number; + private readonly _x: Int32Array; + private readonly _sorted: Int32Array; + private readonly _low: Int32Array; + private readonly _high: Int32Array; + private readonly _y: Int32Array; + private readonly _used: Uint8Array; + + private static _buildInverseDb(): Float32Array { + const table = new Float32Array(256); + const step = -Math.log(1.0649863e-7) / 255; + for (let i = 0; i < 256; i++) { + table[i] = Math.exp(step * (i - 255)); + } + return table; + } + + public constructor(r: VorbisBitReader, books: VorbisCodebook[]) { + const partitions = r.read(5); + const partitionClasses = new Int32Array(partitions); + let classes = 0; + for (let i = 0; i < partitions; i++) { + partitionClasses[i] = r.read(4); + classes = Math.max(classes, partitionClasses[i] + 1); + } + this._partitionClasses = partitionClasses; + + this._classDimensions = new Int32Array(classes); + this._classBits = new Int32Array(classes); + for (let i = 0; i < classes; i++) { + this._classDimensions[i] = r.read(3) + 1; + const bits = r.read(2); + this._classBits[i] = bits; + this._classBooks.push(bits > 0 ? VorbisFloor._book(books, r.read(8)) : null); + for (let j = 0; j < 4; j++) { + this._subclassBooks.push(j < (1 << bits) ? VorbisFloor._book(books, r.read(8) - 1) : null); + } + } + + this._multiplier = r.read(2) + 1; + this._range = VorbisFloor._ranges[this._multiplier - 1]; + this._yBits = VorbisBitReader.ilog(this._range - 1); + + const rangeBits = r.read(4); + let posts = 2; + for (let i = 0; i < partitions; i++) { + posts += this._classDimensions[partitionClasses[i]]; + } + if (posts > 65) { + throw VorbisCodebook.invalid(); + } + const x = new Int32Array(posts); + x[1] = 1 << rangeBits; + for (let i = 2; i < posts; i++) { + x[i] = r.read(rangeBits); + } + this._x = x; + + // posts sorted by x and the neighbors used for the prediction (section 9.2.4, 9.2.5) + const sorted = new Int32Array(posts); + const low = new Int32Array(posts); + const high = new Int32Array(posts); + for (let i = 0; i < posts; i++) { + let position = 0; + low[i] = 0; + high[i] = 1; + for (let j = 0; j < posts; j++) { + if (j !== i && x[j] === x[i]) { + throw VorbisCodebook.invalid(); + } + if (x[j] < x[i]) { + position++; + if (j < i && x[j] > x[low[i]]) { + low[i] = j; + } + } else if (j < i && x[j] > x[i] && x[j] < x[high[i]]) { + high[i] = j; + } + } + sorted[position] = i; + } + this._sorted = sorted; + this._low = low; + this._high = high; + this._y = new Int32Array(posts); + this._used = new Uint8Array(posts); + } + + private static _book(books: VorbisCodebook[], index: number): VorbisCodebook | null { + if (index >= books.length) { + throw VorbisCodebook.invalid(); + } + return index < 0 ? null : books[index]; + } + + /** + * Decodes the floor of a channel and renders it as linear amplitude curve (section 7.2.3, 7.2.4). + * @param n The curve length (half the block size). + * @returns false if the floor is unused in this packet (or the packet ended). + */ + public decode(r: VorbisBitReader, n: number, curve: Float32Array): boolean { + if (r.read(1) === 0) { + return false; + } + + const y = this._y; + y[0] = r.read(this._yBits); + y[1] = r.read(this._yBits); + let offset = 2; + for (let i = 0; i < this._partitionClasses.length; i++) { + const partitionClass = this._partitionClasses[i]; + const bits = this._classBits[partitionClass]; + let value = 0; + if (bits > 0) { + value = this._classBooks[partitionClass]!.decode(r); + if (value < 0) { + return false; + } + } + const dimensions = this._classDimensions[partitionClass]; + for (let j = 0; j < dimensions; j++) { + const book = this._subclassBooks[partitionClass * 4 + (value & ((1 << bits) - 1))]; + value = value >> bits; + let post = 0; + if (book !== null) { + post = book.decode(r); + if (post < 0) { + return false; + } + } + y[offset + j] = post; + } + offset += dimensions; + } + if (r.overrun) { + return false; + } + + // step 1: unwrap the values relative to the prediction from the neighbors + const x = this._x; + const used = this._used; + const range = this._range; + used[0] = 1; + used[1] = 1; + for (let i = 2; i < y.length; i++) { + const low = this._low[i]; + const high = this._high[i]; + const predicted = VorbisFloor._renderPoint(x[low], y[low], x[high], y[high], x[i]); + const value = y[i]; + const highRoom = range - predicted; + const lowRoom = predicted; + const room = (highRoom < lowRoom ? highRoom : lowRoom) * 2; + if (value === 0) { + used[i] = 0; + y[i] = predicted; + } else { + used[low] = 1; + used[high] = 1; + used[i] = 1; + if (value >= room) { + y[i] = highRoom > lowRoom ? value - lowRoom + predicted : predicted - value + highRoom - 1; + } else if ((value & 1) === 1) { + y[i] = predicted - ((value + 1) >> 1); + } else { + y[i] = predicted + (value >> 1); + } + } + } + + // step 2: lines between the used posts in x order, values clamped as the specification suggests + const multiplier = this._multiplier; + let lx = 0; + let ly = VorbisFloor._clamp(y[0], range) * multiplier; + for (let i = 1; i < y.length; i++) { + const post = this._sorted[i]; + if (used[post] === 1) { + const hx = x[post]; + const hy = VorbisFloor._clamp(y[post], range) * multiplier; + VorbisFloor._renderLine(lx, ly, hx, hy, n, curve); + lx = hx; + ly = hy; + } + } + if (lx < n) { + VorbisFloor._renderLine(lx, ly, n, ly, n, curve); + } + return true; + } + + private static _clamp(y: number, range: number): number { + return y < 0 ? 0 : y >= range ? range - 1 : y; + } + + /** + * The y value at `x` on the line from (x0, y0) to (x1, y1) with integer rounding (section 9.2.6). + */ + private static _renderPoint(x0: number, y0: number, x1: number, y1: number, x: number): number { + const dy = y1 - y0; + const offset = Math.floor((Math.abs(dy) * (x - x0)) / (x1 - x0)); + return dy < 0 ? y0 - offset : y0 + offset; + } + + /** + * Sets the curve from x0 to x1 (exclusive, but not beyond n) to the values on the line (section 9.2.7). + */ + private static _renderLine(x0: number, y0: number, x1: number, y1: number, n: number, curve: Float32Array) { + const inverseDb = VorbisFloor._inverseDb; + const dy = y1 - y0; + const adx = x1 - x0; + const base = (dy / adx) | 0; + const sy = dy < 0 ? base - 1 : base + 1; + const ady = Math.abs(dy) - Math.abs(base) * adx; + const end = Math.min(x1, n); + let y = y0; + let error = 0; + if (x0 < end) { + curve[x0] = inverseDb[y]; + } + for (let x = x0 + 1; x < end; x++) { + error += ady; + if (error >= adx) { + error -= adx; + y += sy; + } else { + y += base; + } + curve[x] = inverseDb[y]; + } + } +} diff --git a/packages/alphatab/src/synth/vorbis/VorbisResidue.ts b/packages/alphatab/src/synth/vorbis/VorbisResidue.ts new file mode 100644 index 0000000000..dbb917c0cb --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/VorbisResidue.ts @@ -0,0 +1,213 @@ +import type { VorbisBitReader } from '@coderline/alphatab/synth/vorbis/VorbisBitReader'; +import { VorbisCodebook } from '@coderline/alphatab/synth/vorbis/VorbisCodebook'; + +/** + * A residue of type 0, 1 or 2 (Vorbis I specification, section 8): the VQ coded spectral fine structure. + * @internal + */ +export class VorbisResidue { + private readonly _type: number; + private readonly _begin: number; + private readonly _end: number; + private readonly _partitionSize: number; + private readonly _classifications: number; + private readonly _classBook: VorbisCodebook; + /** + * The book per classification and pass (8 passes), null if the pass has no book. + */ + private readonly _books: (VorbisCodebook | null)[] = []; + /** + * The decoded classifications per vector and partition. + */ + private readonly _classes: Int32Array; + private readonly _classStride: number; + /** + * The interleaved vector of type 2 and the matching arguments for decoding it. + */ + private readonly _interleaved: Float32Array; + private readonly _interleavedVectors: Float32Array[]; + private readonly _interleavedSkip: Uint8Array = new Uint8Array(1); + + public constructor(r: VorbisBitReader, type: number, books: VorbisCodebook[], channels: number, halfBlock: number) { + this._type = type; + this._begin = r.read(24); + this._end = r.read(24); + this._partitionSize = r.read(24) + 1; + this._classifications = r.read(6) + 1; + const classBook = r.read(8); + if (classBook >= books.length) { + throw VorbisCodebook.invalid(); + } + this._classBook = books[classBook]; + const classDimensions = this._classBook.dimensions; + if (classDimensions === 0 || Math.pow(this._classifications, classDimensions) > this._classBook.entries) { + throw VorbisCodebook.invalid(); + } + + const cascades = new Int32Array(this._classifications); + for (let i = 0; i < cascades.length; i++) { + const lowBits = r.read(3); + cascades[i] = r.read(1) === 1 ? (r.read(5) << 3) | lowBits : lowBits; + } + for (let i = 0; i < cascades.length; i++) { + for (let pass = 0; pass < 8; pass++) { + let book: VorbisCodebook | null = null; + if ((cascades[i] & (1 << pass)) !== 0) { + const index = r.read(8); + // books need a VQ lookup and must fill a partition exactly + if ( + index >= books.length || + books[index].values === null || + this._partitionSize % books[index].dimensions !== 0 + ) { + throw VorbisCodebook.invalid(); + } + book = books[index]; + } + this._books.push(book); + } + } + + // type 2 decodes all channels as one interleaved vector + const vectors = type === 2 ? 1 : channels; + const size = type === 2 ? halfBlock * channels : halfBlock; + const partitions = Math.floor((Math.min(this._end, size) - Math.min(this._begin, size)) / this._partitionSize); + this._classStride = Math.max(0, partitions) + classDimensions; + this._classes = new Int32Array(vectors * this._classStride); + this._interleaved = new Float32Array(type === 2 && channels > 1 ? size : 0); + this._interleavedVectors = [this._interleaved]; + } + + /** + * Decodes the residue vectors of a submap (section 8.6.2), the vectors must be zeroed. + * @param vectors The vectors of the channels in the submap. + * @param skip Per vector 1 if it should not be decoded (its floor is unused). + * @param count The number of vectors in the submap. + * @param n The vector length (half the block size). + */ + public decode(r: VorbisBitReader, vectors: Float32Array[], skip: Uint8Array, count: number, n: number) { + if (this._type !== 2 || count === 1) { + this._decodeVectors(r, vectors, skip, count, n, this._type === 0 ? 0 : 1); + return; + } + + let decode = false; + for (let i = 0; i < count; i++) { + if (skip[i] === 0) { + decode = true; + } + } + if (!decode) { + return; + } + const interleaved = this._interleaved; + const size = n * count; + interleaved.fill(0, 0, size); + this._decodeVectors(r, this._interleavedVectors, this._interleavedSkip, 1, size, 1); + for (let i = 0; i < n; i++) { + for (let c = 0; c < count; c++) { + vectors[c][i] = interleaved[i * count + c]; + } + } + } + + private _decodeVectors( + r: VorbisBitReader, + vectors: Float32Array[], + skip: Uint8Array, + count: number, + size: number, + type: number + ) { + const begin = Math.min(this._begin, size); + const partitionSize = this._partitionSize; + const partitions = Math.floor((Math.min(this._end, size) - begin) / partitionSize); + const classBook = this._classBook; + const classesPerWord = classBook.dimensions; + const classifications = this._classifications; + const classes = this._classes; + const stride = this._classStride; + + for (let pass = 0; pass < 8; pass++) { + let partition = 0; + while (partition < partitions) { + if (pass === 0) { + for (let v = 0; v < count; v++) { + if (skip[v] === 0) { + let word = classBook.decode(r); + if (word < 0) { + return; + } + for (let i = classesPerWord - 1; i >= 0; i--) { + classes[v * stride + partition + i] = word % classifications; + word = Math.floor(word / classifications); + } + } + } + } + for (let i = 0; i < classesPerWord && partition < partitions; i++) { + for (let v = 0; v < count; v++) { + if (skip[v] === 0) { + const book = this._books[classes[v * stride + partition] * 8 + pass]; + if ( + book !== null && + !VorbisResidue._decodePartition( + r, + book, + vectors[v], + begin + partition * partitionSize, + partitionSize, + type + ) + ) { + // end of packet, keep what was decoded so far + return; + } + } + } + partition++; + } + } + } + } + + /** + * Adds the VQ vectors of one partition (section 8.6.3, 8.6.4). + * @returns false if the packet ended. + */ + private static _decodePartition( + r: VorbisBitReader, + book: VorbisCodebook, + vector: Float32Array, + offset: number, + partitionSize: number, + type: number + ): boolean { + const dimensions = book.dimensions; + const values = book.values!; + if (type === 0) { + // type 0 interleaves the vector values with a step of partitionSize / dimensions + const step = partitionSize / dimensions; + for (let i = 0; i < step; i++) { + const entry = book.decode(r); + if (entry < 0) { + return false; + } + for (let j = 0; j < dimensions; j++) { + vector[offset + i + j * step] += values[entry * dimensions + j]; + } + } + } else { + for (let i = 0; i < partitionSize; i += dimensions) { + const entry = book.decode(r); + if (entry < 0) { + return false; + } + for (let j = 0; j < dimensions; j++) { + vector[offset + i + j] += values[entry * dimensions + j]; + } + } + } + return true; + } +} diff --git a/packages/alphatab/src/synth/vorbis/VorbisSetup.ts b/packages/alphatab/src/synth/vorbis/VorbisSetup.ts new file mode 100644 index 0000000000..b374e12ef7 --- /dev/null +++ b/packages/alphatab/src/synth/vorbis/VorbisSetup.ts @@ -0,0 +1,195 @@ +import { FormatError } from '@coderline/alphatab/FormatError'; +import { VorbisBitReader } from '@coderline/alphatab/synth/vorbis/VorbisBitReader'; +import { VorbisCodebook } from '@coderline/alphatab/synth/vorbis/VorbisCodebook'; +import { VorbisFloor } from '@coderline/alphatab/synth/vorbis/VorbisFloor'; +import { VorbisResidue } from '@coderline/alphatab/synth/vorbis/VorbisResidue'; + +/** + * A channel mapping (Vorbis I specification, section 4.2.4). + * @record + * @internal + */ +export interface VorbisMapping { + /** + * The submap per channel. + */ + mux: Int32Array; + /** + * The floor per submap. + */ + floors: VorbisFloor[]; + /** + * The residue per submap. + */ + residues: VorbisResidue[]; + /** + * The magnitude and angle channel per coupling step. + */ + magnitudes: Int32Array; + angles: Int32Array; +} + +/** + * A packet mode (Vorbis I specification, section 4.2.4). + * @record + * @internal + */ +export interface VorbisMode { + isLong: boolean; + mapping: VorbisMapping; +} + +/** + * The decoder configuration from the setup header of a Vorbis stream (Vorbis I specification, section 4.2.4). + * It is not changed by decoding packets with it. + * @internal + */ +export class VorbisSetup { + public readonly channels: number; + public readonly blockSize0: number; + public readonly blockSize1: number; + public readonly modes: VorbisMode[] = []; + public readonly modeBits: number; + + /** + * Whether the packet is a Vorbis header of the given type (section 4.2.1). + */ + public static isHeader(data: Uint8Array, start: number, end: number, type: number): boolean { + return ( + end - start >= 7 && + data[start] === type && + data[start + 1] === 0x76 && + data[start + 2] === 0x6f && + data[start + 3] === 0x72 && + data[start + 4] === 0x62 && + data[start + 5] === 0x69 && + data[start + 6] === 0x73 + ); + } + + /** + * Parses the setup header for the channels and block sizes of the identification header. + */ + public constructor( + channels: number, + blockSize0: number, + blockSize1: number, + data: Uint8Array, + setupStart: number, + setupEnd: number + ) { + this.channels = channels; + this.blockSize0 = blockSize0; + this.blockSize1 = blockSize1; + if (!VorbisSetup.isHeader(data, setupStart, setupEnd, 5)) { + throw VorbisCodebook.invalid(); + } + const r = new VorbisBitReader(); + r.reset(data, setupStart + 7, setupEnd); + + const books: VorbisCodebook[] = []; + for (let i = r.read(8) + 1; i > 0; i--) { + books.push(new VorbisCodebook(r)); + } + + // time domain transforms are placeholders which must be zero + for (let i = r.read(6) + 1; i > 0; i--) { + if (r.read(16) !== 0) { + throw VorbisCodebook.invalid(); + } + } + + const floors: VorbisFloor[] = []; + for (let i = r.read(6) + 1; i > 0; i--) { + const type = r.read(16); + if (type === 0) { + throw new FormatError('Vorbis floor type 0 is not supported'); + } + if (type !== 1) { + throw VorbisCodebook.invalid(); + } + floors.push(new VorbisFloor(r, books)); + } + + const residues: VorbisResidue[] = []; + for (let i = r.read(6) + 1; i > 0; i--) { + const type = r.read(16); + if (type > 2) { + throw VorbisCodebook.invalid(); + } + residues.push(new VorbisResidue(r, type, books, this.channels, this.blockSize1 >> 1)); + } + + const mappings: VorbisMapping[] = []; + for (let i = r.read(6) + 1; i > 0; i--) { + mappings.push(this._readMapping(r, floors, residues)); + } + + for (let i = r.read(6) + 1; i > 0; i--) { + const isLong = r.read(1) === 1; + const windowType = r.read(16); + const transformType = r.read(16); + const mapping = r.read(8); + if (windowType !== 0 || transformType !== 0 || mapping >= mappings.length) { + throw VorbisCodebook.invalid(); + } + this.modes.push({ isLong, mapping: mappings[mapping] }); + } + this.modeBits = VorbisBitReader.ilog(this.modes.length - 1); + + // framing bit + if (r.read(1) !== 1 || r.overrun) { + throw VorbisCodebook.invalid(); + } + } + + private _readMapping(r: VorbisBitReader, floors: VorbisFloor[], residues: VorbisResidue[]): VorbisMapping { + const channels = this.channels; + if (r.read(16) !== 0) { + throw VorbisCodebook.invalid(); + } + const submaps = r.read(1) === 1 ? r.read(4) + 1 : 1; + + const steps = r.read(1) === 1 ? r.read(8) + 1 : 0; + const channelBits = VorbisBitReader.ilog(channels - 1); + const magnitudes = new Int32Array(steps); + const angles = new Int32Array(steps); + for (let i = 0; i < steps; i++) { + magnitudes[i] = r.read(channelBits); + angles[i] = r.read(channelBits); + if (magnitudes[i] === angles[i] || magnitudes[i] >= channels || angles[i] >= channels) { + throw VorbisCodebook.invalid(); + } + } + + if (r.read(2) !== 0) { + throw VorbisCodebook.invalid(); + } + + const mux = new Int32Array(channels); + if (submaps > 1) { + for (let i = 0; i < channels; i++) { + mux[i] = r.read(4); + if (mux[i] >= submaps) { + throw VorbisCodebook.invalid(); + } + } + } + + const mappingFloors: VorbisFloor[] = []; + const mappingResidues: VorbisResidue[] = []; + for (let i = 0; i < submaps; i++) { + // unused time configuration + r.read(8); + const floor = r.read(8); + const residue = r.read(8); + if (floor >= floors.length || residue >= residues.length) { + throw VorbisCodebook.invalid(); + } + mappingFloors.push(floors[floor]); + mappingResidues.push(residues[residue]); + } + + return { mux, floors: mappingFloors, residues: mappingResidues, magnitudes, angles }; + } +} diff --git a/packages/alphatab/src/synth/vorbis/VorbisStream.ts b/packages/alphatab/src/synth/vorbis/VorbisStream.ts deleted file mode 100644 index 8e80f5af48..0000000000 --- a/packages/alphatab/src/synth/vorbis/VorbisStream.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @internal - */ -export class VorbisStream { - public audioChannels: number = 0; - public audioSampleRate: number = 0; - public samples: Float32Array = new Float32Array(0); - public bitrateMaximum: number = 0; - public bitrateNominal: number = 0; - public bitrateMinimum: number = 0; - public blocksize0: number = 0; - public blocksize1: number = 0; -} diff --git a/packages/alphatab/src/synth/vorbis/VorbisStreamDecoder.ts b/packages/alphatab/src/synth/vorbis/VorbisStreamDecoder.ts deleted file mode 100644 index 7890d9b060..0000000000 --- a/packages/alphatab/src/synth/vorbis/VorbisStreamDecoder.ts +++ /dev/null @@ -1,2556 +0,0 @@ -/* - * This part of the Vorbis Decoder is adopted from NVorbis and then simplified for alphaTab - * https://github.com/NVorbis/NVorbis - * - * MIT License - * - * Copyright (c) 2020 Andrew Ward - * - * 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. - */ - -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import type { VorbisStream } from '@coderline/alphatab/synth/vorbis/VorbisStream'; -import type { OggPacket } from '@coderline/alphatab/synth/vorbis/OggReader'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; -import { TypeConversions } from '@coderline/alphatab/io/TypeConversions'; -import { IntBitReader } from '@coderline/alphatab/synth/vorbis/IntBitReader'; - -/** - * @internal - */ -export class VorbisSetupHeader { - public codebooks: VorbisCodebook[] = []; - public timeDomainTransforms: VorbisTimeDomainTransform[] = []; - public floors: VorbisFloor[] = []; - public residues: VorbisResidue[] = []; - public mappings: VorbisMapping[] = []; - public modes: VorbisMode[] = []; -} - -/** - * @internal - */ -class VorbisUtils { - static ilog(x: number): number { - let cnt = 0; - while (x > 0) { - ++cnt; - x >>= 1; // this is safe because we'll never get here if the sign bit is set - } - return cnt; - } - - static bitReverse(on: number, bits: number = 32) { - let bn = BigInt(on); - bn = ((bn & BigInt(0xaaaaaaaa)) >> 1n) | ((bn & BigInt(0x55555555)) << 1n); - bn = ((bn & BigInt(0xcccccccc)) >> 2n) | ((bn & BigInt(0x33333333)) << 2n); - bn = ((bn & BigInt(0xf0f0f0f0)) >> 4n) | ((bn & BigInt(0x0f0f0f0f)) << 4n); - bn = ((bn & BigInt(0xff00ff00)) >> 8n) | ((bn & BigInt(0x00ff00ff)) << 8n); - - bn = ((bn >> 16n) | (bn << 16n)) >> (32n - BigInt(bits)); - const x = Number(BigInt.asUintN(32, bn)); - return x; - } - - static convertFromVorbisFloat32(bits: number): number { - // https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1200009.2.2 - const big = BigInt(bits); - let bmantissa = big & BigInt(0x1fffff); - const bsign = big & BigInt(0x80000000); - const bexponent = (big & BigInt(0x7fe00000)) >> 21n; - if (bsign !== 0n) { - bmantissa = -bmantissa; - } - return Number(bmantissa) * Math.pow(2.0, Number(bexponent) - 788); - } -} - -/** - * @internal - */ -interface IFastList { - get(index: number): number; -} - -/** - * @internal - */ -class FastListArray implements IFastList { - private _data: Int32Array; - public constructor(data: Int32Array) { - this._data = data; - } - public get(index: number): number { - return this._data[index]; - } -} - -/** - * @internal - */ -class FastRange implements IFastList { - public static readonly instance = new FastRange(); - public get(index: number) { - return index; - } -} - -/** - * @internal - */ -export class VorbisCodebook { - private _lengths: Int32Array; - private _maxBits: number = 0; - - private _overflowList: (HuffmanListNode | null)[] | null = null; - private _prefixList: (HuffmanListNode | null)[] | null = null; - private _prefixBitLength: number = 0; - private _lookupTable!: Float32Array; - - public dimensions: number = 0; - public entries: number = 0; - public mapType: number = 0; - - public constructor(packet: IntBitReader, huffman: Huffman) { - // first, check the sync pattern - const chkVal = packet.readBits(24); - if (chkVal !== 0x564342) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Book header had invalid signature!'); - } - - // get the counts - this.dimensions = packet.readBits(16); - this.entries = packet.readBits(24); - - // init the storage - this._lengths = new Int32Array(this.entries); - - this._initTree(packet, huffman); - this._initLookupTable(packet); - } - - public get(entry: number, dim: number): number { - return this._lookupTable[entry * this.dimensions + dim]; - } - - public decodeScalar(packet: IntBitReader): number { - let data = packet.tryPeekBits(this._prefixBitLength); - if (data.bitsRead === 0) { - return -1; - } - - // try to get the value from the prefix list... - let node = this._prefixList![data.value]; - if (node != null) { - packet.skipBits(node.length); - return node.value; - } - - // nope, not possible... run through the overflow nodes - data = packet.tryPeekBits(this._maxBits); - - if (this._overflowList !== null) { - for (let i = 0; i < this._overflowList.length; i++) { - node = this._overflowList[i]!; - const bits = data.value & node.mask; - if (node.bits === bits) { - packet.skipBits(node.length); - return node.value; - } - } - } - - return -1; - } - - private _initTree(packet: IntBitReader, huffman: Huffman) { - let sparse: boolean; - let total = 0; - - let maxLen: number; - if (packet.readBit()) { - // ordered - let len = packet.readBits(5) + 1; - for (let i = 0; i < this.entries; ) { - let cnt = packet.readBits(VorbisUtils.ilog(this.entries - i)); - - while (--cnt >= 0) { - this._lengths[i++] = len; - } - - ++len; - } - total = 0; - sparse = false; - maxLen = len; - } else { - // unordered - maxLen = -1; - sparse = packet.readBit(); - for (let i = 0; i < this.entries; i++) { - if (!sparse || packet.readBit()) { - this._lengths[i] = packet.readBits(5) + 1; - ++total; - } else { - // mark the entry as unused - this._lengths[i] = -1; - } - if (this._lengths[i] > maxLen) { - maxLen = this._lengths[i]; - } - } - } - - // figure out the maximum bit size; if all are unused, don't do anything else - this._maxBits = maxLen; - if (maxLen > -1) { - let codewordLengths: Int32Array | null = null; - if (sparse && total >= this.entries >> 2) { - codewordLengths = new Int32Array(this.entries); - codewordLengths.set(this._lengths.subarray(0, this.entries), 0); - sparse = false; - } - - let sortedCount: number; - // compute size of sorted tables - if (sparse) { - sortedCount = total; - } else { - sortedCount = 0; - } - - let values: Int32Array | null = null; - let codewords: Int32Array | null = null; - if (!sparse) { - codewords = new Int32Array(this.entries); - } else if (sortedCount !== 0) { - codewordLengths = new Int32Array(sortedCount); - codewords = new Int32Array(sortedCount); - values = new Int32Array(sortedCount); - } - - if (!this._computeCodewords(sparse, codewords, codewordLengths, this._lengths, this.entries, values)) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Failed to compute codewords'); - } - - const valueList: IFastList = values == null ? FastRange.instance : new FastListArray(values); - - huffman.generateTable(valueList, codewordLengths ?? this._lengths, codewords); - this._prefixList = huffman.prefixTree; - this._prefixBitLength = huffman.tableBits; - this._overflowList = huffman.overflowList; - } - } - - private _computeCodewords( - sparse: boolean, - codewords: Int32Array | null, - codewordLengths: Int32Array | null, - len: Int32Array, - n: number, - values: Int32Array | null - ): boolean { - const available = new Uint32Array(32); - - let k: number = 0; - let m: number = 0; - - for (k = 0; k < n; ++k) { - if (len[k] > 0) { - break; - } - } - if (k === n) { - return true; - } - - this._addEntry(sparse, codewords, codewordLengths, 0, k, m++, len[k], values); - - for (let i = 1; i <= len[k]; ++i) { - available[i] = 1 << (32 - i); - } - - for (let i = k + 1; i < n; ++i) { - let z = len[i]; - if (z <= 0) { - continue; - } - - while (z > 0 && available[z] === 0) { - --z; - } - if (z === 0) { - return false; - } - - const res = available[z]; - available[z] = 0; - this._addEntry(sparse, codewords, codewordLengths, VorbisUtils.bitReverse(res), i, m++, len[i], values); - - if (z !== len[i]) { - for (let y = len[i]; y > z; --y) { - available[y] = res + (1 << (32 - y)); - } - } - } - - return true; - } - - private _addEntry( - sparse: boolean, - codewords: Int32Array | null, - codewordLengths: Int32Array | null, - huffCode: number, - symbol: number, - count: number, - len: number, - values: Int32Array | null - ) { - if (sparse) { - codewords![count] = huffCode; - codewordLengths![count] = len; - values![count] = symbol; - } else { - codewords![symbol] = huffCode; - } - } - - private _initLookupTable(packet: IntBitReader) { - this.mapType = packet.readBits(4); - if (this.mapType === 0) { - return; - } - - const minValue = VorbisUtils.convertFromVorbisFloat32(packet.readBits(32)); - const deltaValue = VorbisUtils.convertFromVorbisFloat32(packet.readBits(32)); - const valueBits = packet.readBits(4) + 1; - const sequenceP = packet.readBit(); - - let lookupValueCount = this.entries * this.dimensions; - const lookupTable = new Float32Array(lookupValueCount); - if (this.mapType === 1) { - lookupValueCount = this._lookup1Values(); - } - - const multiplicands = new Uint32Array(lookupValueCount); - for (let i = 0; i < lookupValueCount; i++) { - multiplicands[i] = packet.readBits(valueBits); - } - - // now that we have the initial data read in, calculate the entry tree - if (this.mapType === 1) { - for (let idx = 0; idx < this.entries; idx++) { - let last = 0.0; - let idxDiv = 1; - for (let i = 0; i < this.dimensions; i++) { - const moff = ((idx / idxDiv) % lookupValueCount) | 0; - const value = multiplicands[moff] * deltaValue + minValue + last; - lookupTable[idx * this.dimensions + i] = value; - - if (sequenceP) { - last = value; - } - - idxDiv *= lookupValueCount; - } - } - } else { - for (let idx = 0; idx < this.entries; idx++) { - let last = 0.0; - let moff = idx * this.dimensions; - for (let i = 0; i < this.dimensions; i++) { - const value = multiplicands[moff] * deltaValue + minValue + last; - lookupTable[idx * this.dimensions + i] = value; - - if (sequenceP) { - last = value; - } - - ++moff; - } - } - } - - this._lookupTable = lookupTable; - } - - private _lookup1Values(): number { - let r = Math.floor(Math.exp(Math.log(this.entries) / this.dimensions)); - - if (Math.floor(Math.pow(r + 1, this.dimensions)) <= this.entries) { - ++r; - } - - return r; - } -} - -/** - * @internal - */ -class HuffmanListNode { - public value: number = 0; - - public length: number = 0; - public bits: number = 0; - public mask: number = 0; -} - -/** - * @internal - */ -export class Huffman { - public static readonly maxTableBits = 10; - - public tableBits: number = 0; - public prefixTree: (HuffmanListNode | null)[] = []; - public overflowList: (HuffmanListNode | null)[] | null = null; - - public generateTable(values: IFastList, lengthList: Int32Array, codeList: Int32Array | null) { - const list = new Array(lengthList.length); - - let maxLen = 0; - for (let i = 0; i < list.length; i++) { - const node = new HuffmanListNode(); - node.value = values.get(i); - node.length = lengthList[i] <= 0 ? 99999 : lengthList[i]; - node.bits = codeList![i]; - node.mask = (1 << lengthList[i]) - 1; - list[i] = node; - if (lengthList[i] > 0 && maxLen < lengthList[i]) { - maxLen = lengthList[i]; - } - } - - list.sort((a, b) => { - const len = a.length - b.length; - if (len === 0) { - return a.bits - b.bits; - } - return len; - }); - - const tableBits = maxLen > Huffman.maxTableBits ? Huffman.maxTableBits : maxLen; - - const prefixList: (HuffmanListNode | null)[] = []; - let overflowList: (HuffmanListNode | null)[] | null = null; - for (let i = 0; i < list.length && list[i].length < 99999; i++) { - const itemBits = list[i].length; - if (itemBits > tableBits) { - overflowList = []; - for (; i < list.length && list[i].length < 99999; i++) { - overflowList.push(list[i]); - } - } else { - const maxVal = 1 << (tableBits - itemBits); - const item = list[i]; - for (let j = 0; j < maxVal; j++) { - const idx = (j << itemBits) | item.bits; - while (prefixList.length <= idx) { - prefixList.push(null); - } - prefixList[idx] = item; - } - } - } - - while (prefixList.length < 1 << tableBits) { - prefixList.push(null); - } - - this.tableBits = tableBits; - this.prefixTree = prefixList; - this.overflowList = overflowList; - } -} - -/** - * @internal - */ -export class VorbisTimeDomainTransform { - public constructor(packet: IntBitReader) { - packet.readBits(16); - } -} - -/** - * @internal - */ -export interface IVorbisFloorData { - readonly executeChannel: boolean; - forceEnergy: boolean; - forceNoEnergy: boolean; -} - -/** - * @internal - */ -export interface IVorbisFloor { - unpack(packet: IntBitReader, blockSize: number, channel: number): IVorbisFloorData; - apply(floorData: IVorbisFloorData, blockSize: number, residue: Float32Array): void; -} - -/** - * @internal - */ -class VorbisFloorData0 implements IVorbisFloorData { - public coeff: Float32Array; - public amp: number = 0; - - public get executeChannel() { - return (this.forceEnergy || this.amp > 0) && !this.forceNoEnergy; - } - - public forceEnergy: boolean = false; - public forceNoEnergy: boolean = false; - - public constructor(coeff: Float32Array) { - this.coeff = coeff; - } -} - -/** - * @internal - */ -export class VorbisFloor0 implements IVorbisFloor { - private _order: number; - private _rate: number; - private _barkMapSize: number; - private _ampBits: number; - private _ampOfs: number; - private _ampDiv: number; - - private _books: VorbisCodebook[]; - private _bookBits: number; - private _wMap: Map; - private _barkMaps: Map; - - public constructor(packet: IntBitReader, block0Size: number, block1Size: number, codebooks: VorbisCodebook[]) { - this._order = packet.readBits(8); - this._rate = packet.readBits(16); - this._barkMapSize = packet.readBits(16); - this._ampBits = packet.readBits(6); - this._ampOfs = packet.readBits(8); - this._books = new Array(packet.readBits(4) + 1); - - if (this._order < 1 || this._rate < 1 || this._barkMapSize < 1 || this._books.length === 0) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor0 Data'); - } - - this._ampDiv = (1 << this._ampBits) - 1; - - for (let i = 0; i < this._books.length; i++) { - const num = packet.readBits(8); - if (num < 0 || num >= codebooks.length) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor0 Data'); - } - - const book = codebooks[num]; - - if (book.mapType === 0 || book.dimensions < 1) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor0 Data'); - } - - this._books[i] = book; - } - this._bookBits = VorbisUtils.ilog(this._books.length); - - this._barkMaps = new Map([ - [block0Size, this._synthesizeBarkCurve(block0Size / 2)], - [block1Size, this._synthesizeBarkCurve(block1Size / 2)] - ]); - this._wMap = new Map([ - [block0Size, this._synthesizeWDelMap(block0Size / 2)], - [block1Size, this._synthesizeWDelMap(block1Size / 2)] - ]); - } - - private _synthesizeBarkCurve(n: number): Int32Array { - const scale = this._barkMapSize / VorbisFloor0._toBARK(this._rate / 2); - - const map = new Int32Array(n + 1); - - for (let i = 0; i < n - 1; i++) { - map[i] = Math.min( - this._barkMapSize - 1, - Math.floor(VorbisFloor0._toBARK((this._rate / 2 / n) * i) * scale) - ); - } - map[n] = -1; - return map; - } - - private static _toBARK(lsp: number): number { - return 13.1 * Math.atan(0.00074 * lsp) + 2.24 * Math.atan(0.0000000185 * lsp * lsp) + 0.0001 * lsp; - } - - private _synthesizeWDelMap(n: number) { - const wdel = Math.PI / this._barkMapSize; - - const map = new Float32Array(n); - for (let i = 0; i < n; i++) { - map[i] = 2 * Math.cos(wdel * i); - } - return map; - } - - public unpack(packet: IntBitReader, _blockSize: number, _channel: number): IVorbisFloorData { - const data = new VorbisFloorData0(new Float32Array(this._order + 1)); - - data.amp = packet.readBits(this._ampBits); - - if (data.amp > 0) { - data.amp = (data.amp / this._ampDiv) * this._ampOfs; - - const bookNum = packet.readBits(this._bookBits); - if (bookNum >= this._books.length) { - // we ran out of data or the packet is corrupt... 0 the floor and return - data.amp = 0; - return data; - } - const book = this._books[bookNum]; - - // first, the book decode... - for (let i = 0; i < this._order; ) { - const entry = book.decodeScalar(packet); - if (entry === -1) { - // we ran out of data or the packet is corrupt... 0 the floor and return - data.amp = 0; - return data; - } - for (let j = 0; i < this._order && j < book.dimensions; j++, i++) { - data.coeff[i] = book.get(entry, j); - } - } - - // then, the "averaging" - let last = 0; - for (let j = 0; j < this._order; ) { - for (let k = 0; j < this._order && k < book.dimensions; j++, k++) { - data.coeff[j] += last; - } - last = data.coeff[j - 1]; - } - } - - return data; - } - - public apply(floorData: IVorbisFloorData, blockSize: number, residue: Float32Array): void { - const data = floorData as VorbisFloorData0; - const n = blockSize / 2; - - if (data.amp > 0) { - // this is pretty well stolen directly from libvorbis... BSD license - const barkMap = this._barkMaps.get(blockSize)!; - const wMap = this._wMap.get(blockSize)!; - - let i = 0; - for (i = 0; i < this._order; i++) { - data.coeff[i] = 2 * Math.cos(data.coeff[i]); - } - - i = 0; - while (i < n) { - let j: number = 0; - const k = barkMap[i]; - let p = 0.5; - let q = 0.5; - const w = wMap[k]; - for (j = 1; j < this._order; j += 2) { - q *= w - data.coeff[j - 1]; - p *= w - data.coeff[j]; - } - if (j === this._order) { - // odd order filter; slightly assymetric - - q *= w - data.coeff[j - 1]; - // biome-ignore lint/suspicious/noMisrefactoredShorthandAssign: Correct calculation here - p *= p * (4 - w * w); - q *= q; - } else { - // even order filter; still symetric - - // biome-ignore lint/suspicious/noMisrefactoredShorthandAssign: Correct calculation here - p *= p * (2 - w); - // biome-ignore lint/suspicious/noMisrefactoredShorthandAssign: Correct calculation here - q *= q * (2 + w); - } - - // calc the dB of this bark section - q = data.amp / Math.sqrt(p + q) - this._ampOfs; - - // now convert to a linear sample multiplier - q = Math.exp(q * 0.11512925); - - residue[i] *= q; - - while (barkMap[++i] === k) { - residue[i] *= q; - } - } - } else { - residue.fill(0, 0, n); - } - } -} - -/** - * @internal - */ -class VorbisFloor1Data implements IVorbisFloorData { - public posts = new Int32Array(64); - public postCount: number = 0; - - public get executeChannel() { - return (this.forceEnergy || this.postCount > 0) && !this.forceNoEnergy; - } - - public forceEnergy: boolean = false; - public forceNoEnergy: boolean = false; -} - -/** - * @internal - */ -export class VorbisFloor1 implements IVorbisFloor { - private static readonly _rangeLookup = [256, 128, 86, 64]; - private static readonly _yBitsLookup = [8, 7, 7, 6]; - - private _partitionClass: Int32Array; - private _classDimensions: Int32Array; - private _classSubclasses: Int32Array; - private _xList: Int32Array; - private _classMasterBookIndex: Int32Array; - private _hNeigh: Int32Array; - private _lNeigh: Int32Array; - private _sortIdx: Int32Array; - - private _multiplier: number; - private _range: number; - private _yBits: number; - - private _classMasterbooks: VorbisCodebook[]; - private _subclassBooks: (VorbisCodebook | null)[][]; - private _subclassBookIndex: Int32Array[]; - - public constructor(packet: IntBitReader, codebooks: VorbisCodebook[]) { - let maximumClass = -1; - this._partitionClass = new Int32Array(packet.readBits(5)); - for (let i = 0; i < this._partitionClass.length; i++) { - this._partitionClass[i] = packet.readBits(4); - if (this._partitionClass[i] > maximumClass) { - maximumClass = this._partitionClass[i]; - } - } - - ++maximumClass; - - this._classDimensions = new Int32Array(maximumClass); - this._classSubclasses = new Int32Array(maximumClass); - this._classMasterbooks = new Array(maximumClass); - this._classMasterBookIndex = new Int32Array(maximumClass); - this._subclassBooks = new Array<(VorbisCodebook | null)[]>(maximumClass); - this._subclassBookIndex = new Array(maximumClass); - for (let i = 0; i < maximumClass; i++) { - this._classDimensions[i] = packet.readBits(3) + 1; - this._classSubclasses[i] = packet.readBits(2); - if (this._classSubclasses[i] > 0) { - this._classMasterBookIndex[i] = packet.readBits(8); - this._classMasterbooks[i] = codebooks[this._classMasterBookIndex[i]]; - } - - this._subclassBooks[i] = new Array(1 << this._classSubclasses[i]); - this._subclassBookIndex[i] = new Int32Array(this._subclassBooks[i].length); - for (let j = 0; j < this._subclassBooks[i].length; j++) { - const bookNum = packet.readBits(8) - 1; - if (bookNum >= 0) { - this._subclassBooks[i][j] = codebooks[bookNum]; - } - this._subclassBookIndex[i][j] = bookNum; - } - } - - this._multiplier = packet.readBits(2); - - this._range = VorbisFloor1._rangeLookup[this._multiplier]; - this._yBits = VorbisFloor1._yBitsLookup[this._multiplier]; - - ++this._multiplier; - - const rangeBits = packet.readBits(4); - - const xList: number[] = []; - xList.push(0); - xList.push(1 << rangeBits); - - for (let i = 0; i < this._partitionClass.length; i++) { - const classNum = this._partitionClass[i]; - for (let j = 0; j < this._classDimensions[classNum]; j++) { - xList.push(packet.readBits(rangeBits)); - } - } - this._xList = new Int32Array(xList); - - // precalc the low and high neighbors (and init the sort table) - this._lNeigh = new Int32Array(xList.length); - this._hNeigh = new Int32Array(xList.length); - this._sortIdx = new Int32Array(xList.length); - this._sortIdx[0] = 0; - this._sortIdx[1] = 1; - for (let i = 2; i < this._lNeigh.length; i++) { - this._lNeigh[i] = 0; - this._hNeigh[i] = 1; - this._sortIdx[i] = i; - for (let j = 2; j < i; j++) { - const temp = this._xList[j]; - if (temp < this._xList[i]) { - if (temp > this._xList[this._lNeigh[i]]) { - this._lNeigh[i] = j; - } - } else { - if (temp < this._xList[this._hNeigh[i]]) { - this._hNeigh[i] = j; - } - } - } - } - - // precalc the sort table - for (let i = 0; i < this._sortIdx.length - 1; i++) { - for (let j = i + 1; j < this._sortIdx.length; j++) { - if (this._xList[i] === this._xList[j]) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor1 Data'); - } - - if (this._xList[this._sortIdx[i]] > this._xList[this._sortIdx[j]]) { - // swap the sort indexes - const temp = this._sortIdx[i]; - this._sortIdx[i] = this._sortIdx[j]; - this._sortIdx[j] = temp; - } - } - } - } - - public unpack(packet: IntBitReader, _blockSize: number, _channel: number): IVorbisFloorData { - const data = new VorbisFloor1Data(); - - // hoist ReadPosts to here since that's all we're doing... - if (packet.readBit()) { - let postCount = 2; - data.posts[0] = packet.readBits(this._yBits); - data.posts[1] = packet.readBits(this._yBits); - - for (let i = 0; i < this._partitionClass.length; i++) { - const clsNum = this._partitionClass[i]; - const cdim = this._classDimensions[clsNum]; - const cbits = this._classSubclasses[clsNum]; - const csub = (1 << cbits) - 1; - let cval = 0; - if (cbits > 0) { - cval = this._classMasterbooks[clsNum].decodeScalar(packet); - if (cval === -1) { - // we read a bad value... bail - postCount = 0; - break; - } - } - for (let j = 0; j < cdim; j++) { - const book = this._subclassBooks[clsNum][cval & csub]; - cval = cval >> cbits; - if (book != null) { - data.posts[postCount] = book.decodeScalar(packet); - if (data.posts[postCount] === -1) { - // we read a bad value... bail - postCount = 0; - i = this._partitionClass.length; - break; - } - } - ++postCount; - } - } - - data.postCount = postCount; - } - - return data; - } - - public apply(floorData: IVorbisFloorData, blockSize: number, residue: Float32Array) { - const data = floorData as VorbisFloor1Data; - - const n = blockSize / 2; - - if (data.postCount > 0) { - const stepFlags = this._unwrapPosts(data); - - let lx = 0; - - let ly = data.posts[0] * this._multiplier; - for (let i = 1; i < data.postCount; i++) { - const idx = this._sortIdx[i]; - - if (stepFlags[idx]) { - const hx = this._xList[idx]; - const hy = data.posts[idx] * this._multiplier; - if (lx < n) { - this._renderLineMulti(lx, ly, Math.min(hx, n), hy, residue); - } - lx = hx; - ly = hy; - } - if (lx >= n) { - break; - } - } - - if (lx < n) { - this._renderLineMulti(lx, ly, n, ly, residue); - } - } else { - residue.fill(0, 0, n); - } - } - - private _unwrapPosts(data: VorbisFloor1Data): boolean[] { - const stepFlags = new Array(64); - stepFlags.fill(false); - stepFlags[0] = true; - stepFlags[1] = true; - - const finalY = new Int32Array(64); - finalY[0] = data.posts[0]; - finalY[1] = data.posts[1]; - - for (let i = 2; i < data.postCount; i++) { - const lowOfs = this._lNeigh[i]; - const highOfs = this._hNeigh[i]; - - const predicted = this._renderPoint( - this._xList[lowOfs], - finalY[lowOfs], - this._xList[highOfs], - finalY[highOfs], - this._xList[i] - ); - - const val = data.posts[i]; - const highroom = this._range - predicted; - const lowroom = predicted; - let room: number; - if (highroom < lowroom) { - room = highroom * 2; - } else { - room = lowroom * 2; - } - if (val !== 0) { - stepFlags[lowOfs] = true; - stepFlags[highOfs] = true; - stepFlags[i] = true; - - if (val >= room) { - if (highroom > lowroom) { - finalY[i] = val - lowroom + predicted; - } else { - finalY[i] = predicted - val + highroom - 1; - } - } else { - if (val % 2 === 1) { - // odd - finalY[i] = predicted - (val + 1) / 2; - } else { - // even - finalY[i] = predicted + val / 2; - } - } - } else { - stepFlags[i] = false; - finalY[i] = predicted; - } - } - - for (let i = 0; i < data.postCount; i++) { - data.posts[i] = finalY[i]; - } - - return stepFlags; - } - - private _renderPoint(x0: number, y0: number, x1: number, y1: number, X: number) { - const dy = y1 - y0; - const adx = x1 - x0; - const ady = Math.abs(dy); - const err = ady * (X - x0); - const off = (err / adx) | 0; - if (dy < 0) { - return y0 - off; - } - return y0 + off; - } - - private _renderLineMulti(x0: number, y0: number, x1: number, y1: number, v: Float32Array) { - const dy = y1 - y0; - const adx = x1 - x0; - let ady = Math.abs(dy); - const sy = 1 - ((dy >> 31) & 1) * 2; - const b = (dy / adx) | 0; - let x = x0; - let y = y0; - let err = -adx; - - v[x0] *= VorbisFloor1._inverseDbTable[y0]; - ady -= Math.abs(b) * adx; - - while (++x < x1) { - y += b; - err += ady; - if (err >= 0) { - err -= adx; - y += sy; - } - v[x] *= VorbisFloor1._inverseDbTable[y]; - } - } - - private static readonly _inverseDbTable = new Float32Array([ - 1.0649863e-7, 1.1341951e-7, 1.2079015e-7, 1.2863978e-7, 1.3699951e-7, 1.4590251e-7, 1.5538408e-7, 1.6548181e-7, - 1.7623575e-7, 1.8768855e-7, 1.9988561e-7, 2.128753e-7, 2.2670913e-7, 2.4144197e-7, 2.5713223e-7, 2.7384213e-7, - 2.9163793e-7, 3.1059021e-7, 3.3077411e-7, 3.5226968e-7, 3.7516214e-7, 3.9954229e-7, 4.255068e-7, 4.5315863e-7, - 4.8260743e-7, 5.1396998e-7, 5.4737065e-7, 5.8294187e-7, 6.2082472e-7, 6.6116941e-7, 7.0413592e-7, 7.4989464e-7, - 7.9862701e-7, 8.505263e-7, 9.0579828e-7, 9.6466216e-7, 1.0273513e-6, 1.0941144e-6, 1.1652161e-6, 1.2409384e-6, - 1.3215816e-6, 1.4074654e-6, 1.4989305e-6, 1.5963394e-6, 1.7000785e-6, 1.8105592e-6, 1.9282195e-6, 2.0535261e-6, - 2.1869758e-6, 2.3290978e-6, 2.4804557e-6, 2.6416497e-6, 2.813319e-6, 2.9961443e-6, 3.1908506e-6, 3.3982101e-6, - 3.6190449e-6, 3.8542308e-6, 4.1047004e-6, 4.371447e-6, 4.6555282e-6, 4.9580707e-6, 5.280274e-6, 5.623416e-6, - 5.9888572e-6, 6.3780469e-6, 6.7925283e-6, 7.2339451e-6, 7.7040476e-6, 8.2047e-6, 8.7378876e-6, 9.3057248e-6, - 9.9104632e-6, 1.0554501e-5, 1.1240392e-5, 1.1970856e-5, 1.2748789e-5, 1.3577278e-5, 1.4459606e-5, 1.5399272e-5, - 1.6400004e-5, 1.7465768e-5, 1.8600792e-5, 1.9809576e-5, 2.1096914e-5, 2.2467911e-5, 2.3928002e-5, 2.5482978e-5, - 2.7139006e-5, 2.8902651e-5, 3.0780908e-5, 3.2781225e-5, 3.4911534e-5, 3.7180282e-5, 3.9596466e-5, 4.2169667e-5, - 4.491009e-5, 4.7828601e-5, 5.0936773e-5, 5.4246931e-5, 5.7772202e-5, 6.1526565e-5, 6.5524908e-5, 6.9783085e-5, - 7.4317983e-5, 7.9147585e-5, 8.429104e-5, 8.9768747e-5, 9.5602426e-5, 0.00010181521, 0.00010843174, - 0.00011547824, 0.00012298267, 0.00013097477, 0.00013948625, 0.00014855085, 0.00015820453, 0.00016848555, - 0.00017943469, 0.00019109536, 0.00020351382, 0.00021673929, 0.00023082423, 0.00024582449, 0.00026179955, - 0.00027881276, 0.00029693158, 0.00031622787, 0.00033677814, 0.00035866388, 0.00038197188, 0.00040679456, - 0.00043323036, 0.00046138411, 0.00049136745, 0.00052329927, 0.00055730621, 0.00059352311, 0.00063209358, - 0.00067317058, 0.000716917, 0.0007635063, 0.00081312324, 0.00086596457, 0.00092223983, 0.00098217216, - 0.0010459992, 0.0011139742, 0.0011863665, 0.0012634633, 0.0013455702, 0.0014330129, 0.0015261382, 0.0016253153, - 0.0017309374, 0.0018434235, 0.0019632195, 0.0020908006, 0.0022266726, 0.0023713743, 0.0025254795, 0.0026895994, - 0.0028643847, 0.0030505286, 0.0032487691, 0.0034598925, 0.0036847358, 0.0039241906, 0.0041792066, 0.004450795, - 0.0047400328, 0.0050480668, 0.0053761186, 0.0057254891, 0.0060975636, 0.0064938176, 0.0069158225, 0.0073652516, - 0.0078438871, 0.0083536271, 0.0088964928, 0.009474637, 0.010090352, 0.01074608, 0.011444421, 0.012188144, - 0.012980198, 0.013823725, 0.014722068, 0.015678791, 0.016697687, 0.017782797, 0.018938423, 0.020169149, - 0.021479854, 0.022875735, 0.02436233, 0.025945531, 0.027631618, 0.029427276, 0.031339626, 0.033376252, - 0.035545228, 0.037855157, 0.040315199, 0.042935108, 0.045725273, 0.048696758, 0.051861348, 0.055231591, - 0.05882085, 0.062643361, 0.066714279, 0.071049749, 0.075666962, 0.080584227, 0.085821044, 0.091398179, - 0.097337747, 0.1036633, 0.11039993, 0.11757434, 0.12521498, 0.13335215, 0.14201813, 0.15124727, 0.16107617, - 0.1715438, 0.18269168, 0.19456402, 0.20720788, 0.22067342, 0.23501402, 0.25028656, 0.26655159, 0.28387361, - 0.30232132, 0.32196786, 0.34289114, 0.36517414, 0.38890521, 0.41417847, 0.44109412, 0.4697589, 0.50028648, - 0.53279791, 0.56742212, 0.6042964, 0.64356699, 0.68538959, 0.72993007, 0.77736504, 0.8278826, 0.88168307, - 0.9389798, 1.0 - ]); -} - -/** - * @internal - */ -export class VorbisFloor implements IVorbisFloor { - public floor: IVorbisFloor; - - public constructor(packet: IntBitReader, block0Size: number, block1Size: number, codebooks: VorbisCodebook[]) { - const type = packet.readBits(16); - switch (type) { - case 0: - this.floor = new VorbisFloor0(packet, block0Size, block1Size, codebooks); - break; - case 1: - this.floor = new VorbisFloor1(packet, codebooks); - break; - - default: - throw new AlphaTabError(AlphaTabErrorType.Format, `Vorbis: Invalid Floor type: ${type}`); - } - } - public apply(floorData: IVorbisFloorData, blockSize: number, residue: Float32Array): void { - this.floor.apply(floorData, blockSize, residue); - } - - public unpack(packet: IntBitReader, blockSize: number, channel: number): IVorbisFloorData { - return this.floor.unpack(packet, blockSize, channel); - } -} - -/** - * @internal - */ -export interface IVorbisResidue { - decode(packet: IntBitReader, doNotDecodeChannel: boolean[], blockSize: number, buffer: Float32Array[]): void; -} - -/** - * @internal - */ -export class VorbisResidue0 implements IVorbisResidue { - private _channels: number; - private _begin: number; - private _end: number; - private _partitionSize: number; - private _classifications: number; - private _maxStages: number; - - private _books: (VorbisCodebook | null)[][]; - private _classBook: VorbisCodebook; - - private _cascade: Int32Array; - private _decodeMap: Int32Array[]; - - public constructor(packet: IntBitReader, channels: number, codebooks: VorbisCodebook[]) { - this._begin = packet.readBits(24); - this._end = packet.readBits(24); - this._partitionSize = packet.readBits(24) + 1; - this._classifications = packet.readBits(6) + 1; - this._classBook = codebooks[packet.readBits(8)]; - - this._cascade = new Int32Array(this._classifications); - let acc = 0; - for (let i = 0; i < this._classifications; i++) { - const lowBits = packet.readBits(3); - if (packet.readBit()) { - this._cascade[i] = (packet.readBits(5) << 3) | lowBits; - } else { - this._cascade[i] = lowBits; - } - acc += VorbisResidue0._icount(this._cascade[i]); - } - - const bookNums = new Int32Array(acc); - for (let i = 0; i < acc; i++) { - bookNums[i] = packet.readBits(8); - if (codebooks[bookNums[i]].mapType === 0) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Residue 0'); - } - } - - const entries = this._classBook.entries; - let dim = this._classBook.dimensions; - let partvals = 1; - while (dim > 0) { - partvals *= this._classifications; - if (partvals > entries) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Residue 0'); - } - --dim; - } - - // now the lookups - this._books = new Array<(VorbisCodebook | null)[]>(this._classifications); - - acc = 0; - let maxstage = 0; - let stages: number; - for (let j = 0; j < this._classifications; j++) { - stages = VorbisUtils.ilog(this._cascade[j]); - this._books[j] = new Array(stages); - if (stages > 0) { - maxstage = Math.max(maxstage, stages); - for (let k = 0; k < stages; k++) { - if ((this._cascade[j] & (1 << k)) > 0) { - this._books[j][k] = codebooks[bookNums[acc++]]; - } - } - } - } - this._maxStages = maxstage; - - this._decodeMap = new Array(partvals); - for (let j = 0; j < partvals; j++) { - let val = j; - let mult = (partvals / this._classifications) | 0; - this._decodeMap[j] = new Int32Array(this._classBook.dimensions); - for (let k = 0; k < this._classBook.dimensions; k++) { - const deco = (val / mult) | 0; - val -= deco * mult; - mult = (mult / this._classifications) | 0; - this._decodeMap[j][k] = deco; - } - } - - this._channels = channels; - } - - private static _icount(v: number): number { - let ret = 0; - while (v !== 0) { - ret += v & 1; - v >>= 1; - } - return ret; - } - - public decode( - packet: IntBitReader, - doNotDecodeChannel: boolean[], - blockSize: number, - buffer: Float32Array[] - ): void { - const end = this._end < blockSize / 2 ? this._end : blockSize / 2; - const n = end - this._begin; - - if (n > 0 && doNotDecodeChannel.indexOf(false) !== -1) { - const partitionCount = n / this._partitionSize; - - const partitionWords = ((partitionCount + this._classBook.dimensions - 1) / this._classBook.dimensions) | 0; - const partWordCache: Int32Array[][] = []; - for (let i = 0; i < this._channels; i++) { - partWordCache.push(new Array(partitionWords)); - } - - for (let stage = 0; stage < this._maxStages; stage++) { - for (let partitionIdx = 0, entryIdx = 0; partitionIdx < partitionCount; entryIdx++) { - if (stage === 0) { - for (let ch = 0; ch < this._channels; ch++) { - const idx = this._classBook.decodeScalar(packet); - if (idx >= 0 && idx < this._decodeMap.length) { - partWordCache[ch][entryIdx] = this._decodeMap[idx]; - } else { - partitionIdx = partitionCount; - stage = this._maxStages; - break; - } - } - } - for ( - let dimensionIdx = 0; - partitionIdx < partitionCount && dimensionIdx < this._classBook.dimensions; - dimensionIdx++, partitionIdx++ - ) { - const offset = this._begin + partitionIdx * this._partitionSize; - for (let ch = 0; ch < this._channels; ch++) { - const idx = partWordCache[ch][entryIdx][dimensionIdx]; - if ((this._cascade[idx] & (1 << stage)) !== 0) { - const book = this._books[idx][stage]; - if (book) { - if (this.writeVectors(book, packet, buffer, ch, offset, this._partitionSize)) { - // bad packet... exit now and try to use what we already have - partitionIdx = partitionCount; - stage = this._maxStages; - break; - } - } - } - } - } - } - } - } - } - - protected writeVectors( - codebook: VorbisCodebook, - packet: IntBitReader, - residue: Float32Array[], - channel: number, - offset: number, - partitionSize: number - ): boolean { - const res = residue[channel]; - const steps = partitionSize / codebook.dimensions; - const entryCache = new Int32Array(steps); - - for (let i = 0; i < steps; i++) { - entryCache[i] = codebook.decodeScalar(packet); - if (entryCache[i] === -1) { - return true; - } - } - for (let dim = 0; dim < codebook.dimensions; dim++) { - for (let step = 0; step < steps; step++, offset++) { - res[offset] += codebook.get(entryCache[step], dim); - } - } - return false; - } -} - -/** - * @internal - */ -export class VorbisResidue1 extends VorbisResidue0 { - protected override writeVectors( - codebook: VorbisCodebook, - packet: IntBitReader, - residue: Float32Array[], - channel: number, - offset: number, - partitionSize: number - ): boolean { - const res = residue[channel]; - - for (let i = 0; i < partitionSize; ) { - const entry = codebook.decodeScalar(packet); - if (entry === -1) { - return true; - } - for (let j = 0; j < codebook.dimensions; i++, j++) { - res[offset + i] += codebook.get(entry, j); - } - } - - return false; - } -} - -/** - * @internal - */ -export class VorbisResidue2 extends VorbisResidue0 { - private _realChannels: number; - public constructor(packet: IntBitReader, channels: number, codebooks: VorbisCodebook[]) { - super(packet, 1, codebooks); - this._realChannels = channels; - } - - public override decode( - packet: IntBitReader, - doNotDecodeChannel: boolean[], - blockSize: number, - buffer: Float32Array[] - ): void { - // since we're doing all channels in a single pass, the block size has to be multiplied. - // otherwise this is just a pass-through call - super.decode(packet, doNotDecodeChannel, blockSize * this._realChannels, buffer); - } - - protected override writeVectors( - codebook: VorbisCodebook, - packet: IntBitReader, - residue: Float32Array[], - _channel: number, - offset: number, - partitionSize: number - ): boolean { - let chPtr = 0; - - offset /= this._realChannels; - for (let c = 0; c < partitionSize; ) { - const entry = codebook.decodeScalar(packet); - if (entry === -1) { - return true; - } - for (let d = 0; d < codebook.dimensions; d++, c++) { - residue[chPtr][offset] += codebook.get(entry, d); - if (++chPtr === this._realChannels) { - chPtr = 0; - offset++; - } - } - } - - return false; - } -} - -/** - * @internal - */ -export class VorbisResidue implements IVorbisResidue { - public residue: IVorbisResidue; - - public constructor(packet: IntBitReader, channels: number, codebooks: VorbisCodebook[]) { - const type = packet.readBits(16); - switch (type) { - case 0: - this.residue = new VorbisResidue0(packet, channels, codebooks); - break; - case 1: - this.residue = new VorbisResidue1(packet, channels, codebooks); - break; - case 2: - this.residue = new VorbisResidue2(packet, channels, codebooks); - break; - - default: - throw new AlphaTabError(AlphaTabErrorType.Format, `Vorbis: Invalid Residue type: ${type}`); - } - } - - public decode( - packet: IntBitReader, - doNotDecodeChannel: boolean[], - blockSize: number, - buffer: Float32Array[] - ): void { - this.residue.decode(packet, doNotDecodeChannel, blockSize, buffer); - } -} - -/** - * @internal - */ -export class VorbisMapping { - private _mdct: Mdct; - private _couplingAngle: Int32Array; - private _couplingMangitude: Int32Array; - private _submapFloor: VorbisFloor[]; - private _submapResidue: VorbisResidue[]; - private _channelFloor: VorbisFloor[]; - private _channelResidue: VorbisResidue[]; - - public constructor( - packet: IntBitReader, - channels: number, - floors: VorbisFloor[], - residues: VorbisResidue[], - mdct: Mdct - ) { - const mappingType = packet.readBits(16); - if (mappingType !== 0) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid mapping type!'); - } - - let submapCount = 1; - if (packet.readBit()) { - submapCount += packet.readBits(4); - } - - // square polar mapping - let couplingSteps = 0; - if (packet.readBit()) { - couplingSteps = packet.readBits(8) + 1; - } - - const couplingBits = VorbisUtils.ilog(channels - 1); - this._couplingAngle = new Int32Array(couplingSteps); - this._couplingMangitude = new Int32Array(couplingSteps); - for (let j = 0; j < couplingSteps; j++) { - const magnitude = packet.readBits(couplingBits); - const angle = packet.readBits(couplingBits); - if (magnitude === angle || magnitude > channels - 1 || angle > channels - 1) { - throw new AlphaTabError( - AlphaTabErrorType.Format, - 'Vorbis: Invalid magnitude or angle in mapping header!' - ); - } - this._couplingAngle[j] = angle; - this._couplingMangitude[j] = magnitude; - } - - if (packet.readBits(2) !== 0) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Reserved bits not 0 in mapping header.'); - } - - const mux = new Int32Array(channels); - if (submapCount > 1) { - for (let c = 0; c < channels; c++) { - mux[c] = packet.readBits(4); - if (mux[c] > submapCount) { - throw new AlphaTabError( - AlphaTabErrorType.Format, - 'Vorbis: Invalid channel mux submap index in mapping header!' - ); - } - } - } - - this._submapFloor = new Array(submapCount); - this._submapResidue = new Array(submapCount); - for (let j = 0; j < submapCount; j++) { - packet.skipBits(8); // unused placeholder - const floorNum = packet.readBits(8); - if (floorNum >= floors.length) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid floor number in mapping header!'); - } - const residueNum = packet.readBits(8); - if (residueNum >= residues.length) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid residue number in mapping header!'); - } - - this._submapFloor[j] = floors[floorNum]; - this._submapResidue[j] = residues[residueNum]; - } - - this._channelFloor = new Array(channels); - this._channelResidue = new Array(channels); - for (let c = 0; c < channels; c++) { - this._channelFloor[c] = this._submapFloor[mux[c]]; - this._channelResidue[c] = this._submapResidue[mux[c]]; - } - this._mdct = mdct; - } - - public decodePacket(packet: IntBitReader, blockSize: number, buffer: Float32Array[]) { - const halfBlockSize = blockSize >> 1; - - // read the noise floor data - const floorData = new Array(this._channelFloor.length); - const noExecuteChannel = new Array(this._channelFloor.length); - noExecuteChannel.fill(false); - - for (let i = 0; i < this._channelFloor.length; i++) { - floorData[i] = this._channelFloor[i].unpack(packet, blockSize, i); - noExecuteChannel[i] = !floorData[i].executeChannel; - - // pre-clear the residue buffers - buffer[i].fill(0, 0, halfBlockSize); - } - - // make sure we handle no-energy channels correctly given the couplings.. - for (let i = 0; i < this._couplingAngle.length; i++) { - if ( - floorData[this._couplingAngle[i]].executeChannel || - floorData[this._couplingMangitude[i]].executeChannel - ) { - floorData[this._couplingAngle[i]].forceEnergy = true; - floorData[this._couplingMangitude[i]].forceEnergy = true; - } - } - - // decode the submaps into the residue buffer - for (let i = 0; i < this._submapFloor.length; i++) { - for (let j = 0; j < this._channelFloor.length; j++) { - if ( - this._submapFloor[i] !== this._channelFloor[j] || - this._submapResidue[i] !== this._channelResidue[j] - ) { - // the submap doesn't match, so this floor doesn't contribute - floorData[j].forceNoEnergy = true; - } - } - - this._submapResidue[i].decode(packet, noExecuteChannel, blockSize, buffer); - } - - // inverse coupling - for (let i = this._couplingAngle.length - 1; i >= 0; i--) { - if ( - floorData[this._couplingAngle[i]].executeChannel || - floorData[this._couplingMangitude[i]].executeChannel - ) { - const magnitude = buffer[this._couplingMangitude[i]]; - const angle = buffer[this._couplingAngle[i]]; - - // we only have to do the first half; MDCT ignores the last half - for (let j = 0; j < halfBlockSize; j++) { - let newM: number; - let newA: number; - - const oldM = magnitude[j]; - const oldA = angle[j]; - if (oldM > 0) { - if (oldA > 0) { - newM = oldM; - newA = oldM - oldA; - } else { - newA = oldM; - newM = oldM + oldA; - } - } else { - if (oldA > 0) { - newM = oldM; - newA = oldM + oldA; - } else { - newA = oldM; - newM = oldM - oldA; - } - } - - magnitude[j] = newM; - angle[j] = newA; - } - } - } - - // apply floor / dot product / MDCT (only run if we have sound energy in that channel) - for (let c = 0; c < this._channelFloor.length; c++) { - if (floorData[c].executeChannel) { - this._channelFloor[c].apply(floorData[c], blockSize, buffer[c]); - this._mdct.reverse(buffer[c], blockSize); - } else { - // since we aren't doing the IMDCT, we have to explicitly clear the back half of the block - buffer[c].fill(0, halfBlockSize, halfBlockSize * 2); - } - } - } -} - -/** - * @internal - */ -class VorbisModeOverlapInfo { - public packetStartIndex: number = 0; - public packetTotalLength: number = 0; - public packetValidLength: number = 0; -} - -/** - * @internal - */ -class VorbisModePacketInfo { - public overlapInfo: VorbisModeOverlapInfo = new VorbisModeOverlapInfo(); - public windowIndex: number = 0; -} - -/** - * @internal - */ -class VorbisReadNextPacketResult { - public samplePosition: number | null = null; - public constructor(samplePosition: number | null = null) { - this.samplePosition = samplePosition; - } -} - -/** - * @internal - */ -export class VorbisMode { - private static readonly _piHalf = 3.1415926539 / 2; - - private _channels: number; - private _blockFlag: boolean; - private _blockSize: number; - private _mapping: VorbisMapping; - private _windows: Float32Array[]; - private _overlapInfo: VorbisModeOverlapInfo[] | null = null; - - public constructor( - packet: IntBitReader, - channels: number, - block0Size: number, - block1Size: number, - mappings: VorbisMapping[] - ) { - this._channels = channels; - - this._blockFlag = packet.readBit(); - if (0 !== packet.readBits(32)) { - throw new AlphaTabError( - AlphaTabErrorType.Format, - 'Vorbis: Mode header had invalid window or transform type!' - ); - } - - const mappingIdx = packet.readBits(8); - if (mappingIdx >= mappings.length) { - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Mode header had invalid mapping index!'); - } - this._mapping = mappings[mappingIdx]; - - if (this._blockFlag) { - this._blockSize = block1Size; - this._windows = [ - VorbisMode._calcWindow(block0Size, block1Size, block0Size), - VorbisMode._calcWindow(block1Size, block1Size, block0Size), - VorbisMode._calcWindow(block0Size, block1Size, block1Size), - VorbisMode._calcWindow(block1Size, block1Size, block1Size) - ]; - this._overlapInfo = [ - VorbisMode._calcOverlap(block0Size, block1Size, block0Size), - VorbisMode._calcOverlap(block1Size, block1Size, block0Size), - VorbisMode._calcOverlap(block0Size, block1Size, block1Size), - VorbisMode._calcOverlap(block1Size, block1Size, block1Size) - ]; - } else { - this._blockSize = block0Size; - this._windows = [VorbisMode._calcWindow(block0Size, block0Size, block0Size)]; - } - } - - public decode(reader: IntBitReader, buffer: Float32Array[]): VorbisModeOverlapInfo { - const info = this._getPacketInfo(reader); - - this._mapping.decodePacket(reader, this._blockSize, buffer); - - const window = this._windows[info.windowIndex]; - for (let i = 0; i < this._blockSize; i++) { - for (let ch = 0; ch < this._channels; ch++) { - buffer[ch][i] *= window[i]; - } - } - - return info.overlapInfo; - } - - private _getPacketInfo(reader: IntBitReader): VorbisModePacketInfo { - const info = new VorbisModePacketInfo(); - if (this._blockFlag) { - const prevFlag = reader.readBit(); - const nextFlag = reader.readBit(); - - info.windowIndex = (prevFlag ? 1 : 0) + (nextFlag ? 2 : 0); - - const overlapInfo = this._overlapInfo![info.windowIndex]; - info.overlapInfo.packetStartIndex = overlapInfo.packetStartIndex; - info.overlapInfo.packetValidLength = overlapInfo.packetValidLength; - info.overlapInfo.packetTotalLength = overlapInfo.packetTotalLength; - } else { - info.windowIndex = 0; - info.overlapInfo.packetStartIndex = 0; - info.overlapInfo.packetValidLength = this._blockSize / 2; - info.overlapInfo.packetTotalLength = this._blockSize; - } - - return info; - } - - private static _calcWindow(prevBlockSize: number, blockSize: number, nextBlockSize: number): Float32Array { - const array = new Float32Array(blockSize); - - const left = prevBlockSize / 2; - const wnd = blockSize; - const right = nextBlockSize / 2; - - const leftbegin = wnd / 4 - left / 2; - const rightbegin = wnd - wnd / 4 - right / 2; - - for (let i = 0; i < left; i++) { - let x = Math.sin(((i + 0.5) / left) * VorbisMode._piHalf); - x *= x; - array[leftbegin + i] = Math.sin(x * VorbisMode._piHalf); - } - - for (let i = leftbegin + left; i < rightbegin; i++) { - array[i] = 1.0; - } - - for (let i = 0; i < right; i++) { - let x = Math.sin(((right - i - 0.5) / right) * VorbisMode._piHalf); - x *= x; - array[rightbegin + i] = Math.sin(x * VorbisMode._piHalf); - } - - return array; - } - - private static _calcOverlap(prevBlockSize: number, blockSize: number, nextBlockSize: number): VorbisModeOverlapInfo { - const leftOverlapHalfSize = prevBlockSize / 4; - const rightOverlapHalfSize = nextBlockSize / 4; - - const packetStartIndex = blockSize / 4 - leftOverlapHalfSize; - const packetTotalLength = (blockSize / 4) * 3 + rightOverlapHalfSize; - const packetValidLength = packetTotalLength - rightOverlapHalfSize * 2; - - const info = new VorbisModeOverlapInfo(); - info.packetStartIndex = packetStartIndex; - info.packetValidLength = packetValidLength; - info.packetTotalLength = packetTotalLength; - return info; - } -} - -/** - * @internal - */ -class MdctImpl { - // biome-ignore lint/correctness/noPrecisionLoss: High precision PI - // biome-ignore lint/suspicious/noApproximativeNumericConstant: High precision PI - private static readonly _pi = 3.14159265358979323846264; - - private readonly _n: number; - private readonly _n2: number; - private readonly _n4: number; - private readonly _n8: number; - private readonly _ld: number; - - private readonly _a: Float32Array; - private readonly _b: Float32Array; - private readonly _c: Float32Array; - - private readonly _bitrev: Uint16Array; - - public constructor(n: number) { - this._n = n; - this._n2 = n >> 1; - this._n4 = this._n2 >> 1; - this._n8 = this._n4 >> 1; - - this._ld = VorbisUtils.ilog(n) - 1; - - // first, calc the "twiddle factors" - this._a = new Float32Array(this._n2); - this._b = new Float32Array(this._n2); - this._c = new Float32Array(this._n4); - - let k: number = 0; - let k2: number = 0; - for (; k < this._n4; ++k, k2 += 2) { - this._a[k2] = Math.cos((4 * k * MdctImpl._pi) / n); - this._a[k2 + 1] = -Math.sin((4 * k * MdctImpl._pi) / n); - this._b[k2] = Math.cos(((k2 + 1) * MdctImpl._pi) / n / 2) * 0.5; - this._b[k2 + 1] = Math.sin(((k2 + 1) * MdctImpl._pi) / n / 2) * 0.5; - } - - k = 0; - k2 = 0; - for (; k < this._n8; ++k, k2 += 2) { - this._c[k2] = Math.cos((2 * (k2 + 1) * MdctImpl._pi) / n); - this._c[k2 + 1] = -Math.sin((2 * (k2 + 1) * MdctImpl._pi) / n); - } - - // now, calc the bit reverse table - this._bitrev = new Uint16Array(this._n8); - for (let i = 0; i < this._n8; ++i) { - this._bitrev[i] = TypeConversions.int32ToUint16(VorbisUtils.bitReverse(i, this._ld - 3) << 2); - } - } - - public calcReverse(buffer: Float32Array) { - let u: Float32Array; - let v: Float32Array; - - const buf2 = new Float32Array(this._n2); - - // copy and reflect spectral data - // step 0 - - { - let d = this._n2 - 2; // buf2 - let aa = 0; // A - let e = 0; // buffer - const eStop = this._n2; // buffer - while (e !== eStop) { - buf2[d + 1] = buffer[e] * this._a[aa] - buffer[e + 2] * this._a[aa + 1]; - buf2[d] = buffer[e] * this._a[aa + 1] + buffer[e + 2] * this._a[aa]; - d -= 2; - aa += 2; - e += 4; - } - - e = this._n2 - 3; - while (d >= 0) { - buf2[d + 1] = -buffer[e + 2] * this._a[aa] - -buffer[e] * this._a[aa + 1]; - buf2[d] = -buffer[e + 2] * this._a[aa + 1] + -buffer[e] * this._a[aa]; - d -= 2; - aa += 2; - e -= 4; - } - } - - // apply "symbolic" names - u = buffer; - v = buf2; - - // step 2 - - { - let aa = this._n2 - 8; // A - - let e0 = this._n4; // v - let e1 = 0; // v - - let d0 = this._n4; // u - let d1 = 0; // u - - while (aa >= 0) { - let v4020: number; - - let v4121: number; - - v4121 = v[e0 + 1] - v[e1 + 1]; - v4020 = v[e0] - v[e1]; - u[d0 + 1] = v[e0 + 1] + v[e1 + 1]; - u[d0] = v[e0] + v[e1]; - u[d1 + 1] = v4121 * this._a[aa + 4] - v4020 * this._a[aa + 5]; - u[d1] = v4020 * this._a[aa + 4] + v4121 * this._a[aa + 5]; - - v4121 = v[e0 + 3] - v[e1 + 3]; - v4020 = v[e0 + 2] - v[e1 + 2]; - u[d0 + 3] = v[e0 + 3] + v[e1 + 3]; - u[d0 + 2] = v[e0 + 2] + v[e1 + 2]; - u[d1 + 3] = v4121 * this._a[aa] - v4020 * this._a[aa + 1]; - u[d1 + 2] = v4020 * this._a[aa] + v4121 * this._a[aa + 1]; - - aa -= 8; - - d0 += 4; - d1 += 4; - e0 += 4; - e1 += 4; - } - } - - // step 3 - - // iteration 0 - this._step3Iter0Loop(this._n >> 4, u, this._n2 - 1 - this._n4 * 0, -this._n8); - this._step3Iter0Loop(this._n >> 4, u, this._n2 - 1 - this._n4 * 1, -this._n8); - - // iteration 1 - this._step3InnerRLoop(this._n >> 5, u, this._n2 - 1 - this._n8 * 0, -(this._n >> 4), 16); - this._step3InnerRLoop(this._n >> 5, u, this._n2 - 1 - this._n8 * 1, -(this._n >> 4), 16); - this._step3InnerRLoop(this._n >> 5, u, this._n2 - 1 - this._n8 * 2, -(this._n >> 4), 16); - this._step3InnerRLoop(this._n >> 5, u, this._n2 - 1 - this._n8 * 3, -(this._n >> 4), 16); - - // iterations 2 ... x - let l = 2; - for (; l < (this._ld - 3) >> 1; ++l) { - const k0 = this._n >> (l + 2); - const k02 = k0 >> 1; - const lim = 1 << (l + 1); - for (let i = 0; i < lim; ++i) { - this._step3InnerRLoop(this._n >> (l + 4), u, this._n2 - 1 - k0 * i, -k02, 1 << (l + 3)); - } - } - - // iterations x ... end - for (; l < this._ld - 6; ++l) { - const k0 = this._n >> (l + 2); - const k1 = 1 << (l + 3); - const k02 = k0 >> 1; - const rlim = this._n >> (l + 6); - const lim = 1 << (l + 1); - let iOff = this._n2 - 1; - let A0 = 0; - - for (let r = rlim; r > 0; --r) { - this._step3InnerSLoop(lim, u, iOff, -k02, A0, k1, k0); - A0 += k1 * 4; - iOff -= 8; - } - } - - // combine some iteration steps... - this._step3InnerSLoopLd654(this._n >> 5, u, this._n2 - 1, this._n); - - // steps 4, 5, and 6 - { - let bit = 0; - - let d0 = this._n4 - 4; // v - let d1 = this._n2 - 4; // v - while (d0 >= 0) { - let k4: number; - - k4 = this._bitrev[bit]; - v[d1 + 3] = u[k4]; - v[d1 + 2] = u[k4 + 1]; - v[d0 + 3] = u[k4 + 2]; - v[d0 + 2] = u[k4 + 3]; - - k4 = this._bitrev[bit + 1]; - v[d1 + 1] = u[k4]; - v[d1] = u[k4 + 1]; - v[d0 + 1] = u[k4 + 2]; - v[d0] = u[k4 + 3]; - - d0 -= 4; - d1 -= 4; - bit += 2; - } - } - - // step 7 - { - let c = 0; // C - let d = 0; // v - let e = this._n2 - 4; // v - - while (d < e) { - let a02: number; - let a11: number; - let b0: number; - let b1: number; - let b2: number; - let b3: number; - - a02 = v[d] - v[e + 2]; - a11 = v[d + 1] + v[e + 3]; - - b0 = this._c[c + 1] * a02 + this._c[c] * a11; - b1 = this._c[c + 1] * a11 - this._c[c] * a02; - - b2 = v[d] + v[e + 2]; - b3 = v[d + 1] - v[e + 3]; - - v[d] = b2 + b0; - v[d + 1] = b3 + b1; - v[e + 2] = b2 - b0; - v[e + 3] = b1 - b3; - - a02 = v[d + 2] - v[e]; - a11 = v[d + 3] + v[e + 1]; - - b0 = this._c[c + 3] * a02 + this._c[c + 2] * a11; - b1 = this._c[c + 3] * a11 - this._c[c + 2] * a02; - - b2 = v[d + 2] + v[e]; - b3 = v[d + 3] - v[e + 1]; - - v[d + 2] = b2 + b0; - v[d + 3] = b3 + b1; - v[e] = b2 - b0; - v[e + 1] = b1 - b3; - - c += 4; - d += 4; - e -= 4; - } - } - - // step 8 + decode - { - let b = this._n2 - 8; // B - let e = this._n2 - 8; // buf2 - let d0 = 0; // buffer - let d1 = this._n2 - 4; // buffer - let d2 = this._n2; // buffer - let d3 = this._n - 4; // buffer - while (e >= 0) { - let p0: number; - let p1: number; - let p2: number; - let p3: number; - - p3 = buf2[e + 6] * this._b[b + 7] - buf2[e + 7] * this._b[b + 6]; - p2 = -buf2[e + 6] * this._b[b + 6] - buf2[e + 7] * this._b[b + 7]; - - buffer[d0] = p3; - buffer[d1 + 3] = -p3; - buffer[d2] = p2; - buffer[d3 + 3] = p2; - - p1 = buf2[e + 4] * this._b[b + 5] - buf2[e + 5] * this._b[b + 4]; - p0 = -buf2[e + 4] * this._b[b + 4] - buf2[e + 5] * this._b[b + 5]; - - buffer[d0 + 1] = p1; - buffer[d1 + 2] = -p1; - buffer[d2 + 1] = p0; - buffer[d3 + 2] = p0; - - p3 = buf2[e + 2] * this._b[b + 3] - buf2[e + 3] * this._b[b + 2]; - p2 = -buf2[e + 2] * this._b[b + 2] - buf2[e + 3] * this._b[b + 3]; - - buffer[d0 + 2] = p3; - buffer[d1 + 1] = -p3; - buffer[d2 + 2] = p2; - buffer[d3 + 1] = p2; - - p1 = buf2[e] * this._b[b + 1] - buf2[e + 1] * this._b[b]; - p0 = -buf2[e] * this._b[b] - buf2[e + 1] * this._b[b + 1]; - - buffer[d0 + 3] = p1; - buffer[d1] = -p1; - buffer[d2 + 3] = p0; - buffer[d3] = p0; - - b -= 8; - e -= 8; - d0 += 4; - d2 += 4; - d1 -= 4; - d3 -= 4; - } - } - } - - private _step3InnerRLoop(lim: number, e: Float32Array, d0: number, kOff: number, k1: number) { - let k0020: number; - let k0121: number; - - let e0 = d0; // e - let e2 = e0 + kOff; // e - let a = 0; - - for (let i = lim >> 2; i > 0; --i) { - k0020 = e[e0] - e[e2]; - k0121 = e[e0 - 1] - e[e2 - 1]; - e[e0] += e[e2]; - e[e0 - 1] += e[e2 - 1]; - e[e2] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[e2 - 1] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - - a += k1; - - k0020 = e[e0 - 2] - e[e2 - 2]; - k0121 = e[e0 - 3] - e[e2 - 3]; - e[e0 - 2] += e[e2 - 2]; - e[e0 - 3] += e[e2 - 3]; - e[e2 - 2] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[e2 - 3] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - - a += k1; - - k0020 = e[e0 - 4] - e[e2 - 4]; - k0121 = e[e0 - 5] - e[e2 - 5]; - e[e0 - 4] += e[e2 - 4]; - e[e0 - 5] += e[e2 - 5]; - e[e2 - 4] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[e2 - 5] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - - a += k1; - - k0020 = e[e0 - 6] - e[e2 - 6]; - k0121 = e[e0 - 7] - e[e2 - 7]; - e[e0 - 6] += e[e2 - 6]; - e[e0 - 7] += e[e2 - 7]; - e[e2 - 6] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[e2 - 7] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - - a += k1; - - e0 -= 8; - e2 -= 8; - } - } - - private _step3Iter0Loop(n: number, e: Float32Array, iOff: number, kOff: number) { - let ee0 = iOff; // e - let ee2 = ee0 + kOff; // e - let a = 0; - for (let i = n >> 2; i > 0; --i) { - let k0020: number; - let k0121: number; - - k0020 = e[ee0] - e[ee2]; - k0121 = e[ee0 - 1] - e[ee2 - 1]; - e[ee0] += e[ee2]; - e[ee0 - 1] += e[ee2 - 1]; - e[ee2] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[ee2 - 1] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - a += 8; - - k0020 = e[ee0 - 2] - e[ee2 - 2]; - k0121 = e[ee0 - 3] - e[ee2 - 3]; - e[ee0 - 2] += e[ee2 - 2]; - e[ee0 - 3] += e[ee2 - 3]; - e[ee2 - 2] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[ee2 - 3] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - a += 8; - - k0020 = e[ee0 - 4] - e[ee2 - 4]; - k0121 = e[ee0 - 5] - e[ee2 - 5]; - e[ee0 - 4] += e[ee2 - 4]; - e[ee0 - 5] += e[ee2 - 5]; - e[ee2 - 4] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[ee2 - 5] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - a += 8; - - k0020 = e[ee0 - 6] - e[ee2 - 6]; - k0121 = e[ee0 - 7] - e[ee2 - 7]; - e[ee0 - 6] += e[ee2 - 6]; - e[ee0 - 7] += e[ee2 - 7]; - e[ee2 - 6] = k0020 * this._a[a] - k0121 * this._a[a + 1]; - e[ee2 - 7] = k0121 * this._a[a] + k0020 * this._a[a + 1]; - a += 8; - - ee0 -= 8; - ee2 -= 8; - } - } - - private _step3InnerSLoop( - n: number, - e: Float32Array, - iOff: number, - kOff: number, - a: number, - aOff: number, - k0: number - ) { - const A0 = this._a[a]; - const A1 = this._a[a + 1]; - const A2 = this._a[a + aOff]; - const A3 = this._a[a + aOff + 1]; - const A4 = this._a[a + aOff * 2]; - const A5 = this._a[a + aOff * 2 + 1]; - const A6 = this._a[a + aOff * 3]; - const A7 = this._a[a + aOff * 3 + 1]; - - let k00: number; - let k11: number; - - let ee0 = iOff; // e - let ee2 = ee0 + kOff; // e - - for (let i = n; i > 0; --i) { - k00 = e[ee0] - e[ee2]; - k11 = e[ee0 - 1] - e[ee2 - 1]; - e[ee0] += e[ee2]; - e[ee0 - 1] += e[ee2 - 1]; - e[ee2] = k00 * A0 - k11 * A1; - e[ee2 - 1] = k11 * A0 + k00 * A1; - - k00 = e[ee0 - 2] - e[ee2 - 2]; - k11 = e[ee0 - 3] - e[ee2 - 3]; - e[ee0 - 2] += e[ee2 - 2]; - e[ee0 - 3] += e[ee2 - 3]; - e[ee2 - 2] = k00 * A2 - k11 * A3; - e[ee2 - 3] = k11 * A2 + k00 * A3; - - k00 = e[ee0 - 4] - e[ee2 - 4]; - k11 = e[ee0 - 5] - e[ee2 - 5]; - e[ee0 - 4] += e[ee2 - 4]; - e[ee0 - 5] += e[ee2 - 5]; - e[ee2 - 4] = k00 * A4 - k11 * A5; - e[ee2 - 5] = k11 * A4 + k00 * A5; - - k00 = e[ee0 - 6] - e[ee2 - 6]; - k11 = e[ee0 - 7] - e[ee2 - 7]; - e[ee0 - 6] += e[ee2 - 6]; - e[ee0 - 7] += e[ee2 - 7]; - e[ee2 - 6] = k00 * A6 - k11 * A7; - e[ee2 - 7] = k11 * A6 + k00 * A7; - - ee0 -= k0; - ee2 -= k0; - } - } - - private _step3InnerSLoopLd654(n: number, e: Float32Array, iOff: number, baseN: number) { - const aOff = baseN >> 3; - const A2 = this._a[aOff]; - let z = iOff; // e - const b = z - 16 * n; // e - - while (z > b) { - let k00: number; - let k11: number; - - k00 = e[z] - e[z - 8]; - k11 = e[z - 1] - e[z - 9]; - e[z] += e[z - 8]; - e[z - 1] += e[z - 9]; - e[z - 8] = k00; - e[z - 9] = k11; - - k00 = e[z - 2] - e[z - 10]; - k11 = e[z - 3] - e[z - 11]; - e[z - 2] += e[z - 10]; - e[z - 3] += e[z - 11]; - e[z - 10] = (k00 + k11) * A2; - e[z - 11] = (k11 - k00) * A2; - - k00 = e[z - 12] - e[z - 4]; - k11 = e[z - 5] - e[z - 13]; - e[z - 4] += e[z - 12]; - e[z - 5] += e[z - 13]; - e[z - 12] = k11; - e[z - 13] = k00; - - k00 = e[z - 14] - e[z - 6]; - k11 = e[z - 7] - e[z - 15]; - e[z - 6] += e[z - 14]; - e[z - 7] += e[z - 15]; - e[z - 14] = (k00 + k11) * A2; - e[z - 15] = (k00 - k11) * A2; - - this._iter54(e, z); - this._iter54(e, z - 8); - - z -= 16; - } - } - - private _iter54(e: Float32Array, z: number) { - const k00 = e[z] - e[z - 4]; - const y0 = e[z] + e[z - 4]; - const y2 = e[z - 2] + e[z - 6]; - const k22 = e[z - 2] - e[z - 6]; - - e[z] = y0 + y2; - e[z - 2] = y0 - y2; - - const k33 = e[z - 3] - e[z - 7]; - - e[z - 4] = k00 + k33; - e[z - 6] = k00 - k33; - - const k11 = e[z - 1] - e[z - 5]; - const y1 = e[z - 1] + e[z - 5]; - const y3 = e[z - 3] + e[z - 7]; - - e[z - 1] = y1 + y3; - e[z - 3] = y1 - y3; - e[z - 5] = k11 - k22; - e[z - 7] = k11 + k22; - } -} - -/** - * @internal - */ -export class Mdct { - private _setupCache: Map = new Map(); - - public reverse(samples: Float32Array, sampleCount: number) { - let impl: MdctImpl; - if (this._setupCache.has(sampleCount)) { - impl = this._setupCache.get(sampleCount)!; - } else { - impl = new MdctImpl(sampleCount); - this._setupCache.set(sampleCount, impl); - } - - impl.calcReverse(samples); - } -} - -/** - * @internal - */ -export class VorbisStreamDecoder { - private _stream: VorbisStream; - private _setup: VorbisSetupHeader; - private _packets: OggPacket[]; - private _packetIndex: number = 0; - - private _nextPacketBuf: Float32Array[] | null; - private _prevPacketBuf: Float32Array[] | null; - private _prevPacketStart: number; - private _prevPacketEnd: number; - private _prevPacketStop: number; - - private _currentPosition: number; - private _hasPosition: boolean; - private _eosFound: boolean; - private _modeFieldBits: number; - - public constructor(stream: VorbisStream, setup: VorbisSetupHeader, packets: OggPacket[]) { - this._stream = stream; - this._setup = setup; - this._packets = packets; - - this._currentPosition = 0; - - this._prevPacketBuf = null; - this._prevPacketStart = 0; - this._prevPacketEnd = 0; - this._prevPacketStop = 0; - this._nextPacketBuf = null; - this._eosFound = false; - this._hasPosition = false; - - // save off the number of bits to read to determine packet mode - this._modeFieldBits = VorbisUtils.ilog(setup.modes.length - 1); - } - - public decode() { - let allSamples = new Float32Array( - this._packets[this._packets.length - 1].granulePosition! * this._stream.audioChannels - ); - - // 200ms - const buffer = new Float32Array(0.2 * this._stream.audioSampleRate * this._stream.audioChannels); - - let cnt: number = 0; - let pos = 0; - while (true) { - cnt = this.read(buffer, 0, buffer.length); - if (cnt === 0) { - break; - } - // not enough space in buffer -> grow by 1 buffer size - if (pos + cnt >= allSamples.length) { - const newAllSamples = new Float32Array(allSamples.length + buffer.length); - newAllSamples.set(allSamples, 0); - allSamples = newAllSamples; - } - - allSamples.set(buffer.subarray(0, cnt), pos); - pos += cnt; - } - - // truncate excess - return allSamples.subarray(0, pos); - } - - public read(buffer: Float32Array, offset: number, count: number): number { - // if the caller didn't ask for any data, bail early - if (count === 0) { - return 0; - } - - // save off value to track when we're done with the request - let idx = offset; - const tgt = offset + count; - - // try to fill the buffer; drain the last buffer if EOS, resync, bad packet, or parameter change - while (idx < tgt) { - // if we don't have any more valid data in the current packet, read in the next packet - if (this._prevPacketStart === this._prevPacketEnd) { - if (this._eosFound) { - this._nextPacketBuf = null; - this._prevPacketBuf = null; - // no more samples, so just return - break; - } - - const readResult = this._readNextPacket((idx - offset) / this._stream.audioChannels); - - if (readResult === null) { - // drain the current packet (the windowing will fade it out) - this._prevPacketEnd = this._prevPacketStop; - } - - // if we need to pick up a position, and the packet had one, apply the position now - if (readResult !== null && readResult.samplePosition !== null && !this._hasPosition) { - this._hasPosition = true; - this._currentPosition = - readResult!.samplePosition! - - (this._prevPacketEnd - this._prevPacketStart) - - (idx - offset) / this._stream.audioChannels; - } - } - - // we read out the valid samples from the previous packet - const copyLen = Math.min( - (tgt - idx) / this._stream.audioChannels, - this._prevPacketEnd - this._prevPacketStart - ); - if (copyLen > 0) { - idx += this._copyBuffer(buffer, idx, copyLen); - } - } - - // update the count of floats written - count = idx - offset; - - // update the position - this._currentPosition += count / this._stream.audioChannels; - - // return count of floats written - return count; - } - - private _copyBuffer(target: Float32Array, targetIndex: number, count: number) { - let idx = targetIndex; - for (; count > 0; this._prevPacketStart++, count--) { - for (let ch = 0; ch < this._stream.audioChannels; ch++) { - target[idx++] = this._prevPacketBuf![ch][this._prevPacketStart]; - } - } - return idx - targetIndex; - } - - private _readNextPacket(bufferedSamples: number): VorbisReadNextPacketResult | null { - // decode the next packet now so we can start overlapping with it - const res = this._decodeNextPacket(); - this._eosFound = this._eosFound || res.isEndOfStream; - if (res.curPacket == null) { - return null; - } - - // if we get a max sample position, back off our valid length to match - if (res.samplePosition !== null && res.isEndOfStream) { - const actualEnd = this._currentPosition + bufferedSamples + res.validLen - res.startIndex; - const diff = res.samplePosition! - actualEnd; - if (diff < 0) { - res.validLen += diff; - if (res.validLen < 0) { - // TODO: something is wrong that this happens, this._currentPosition is way too high - res.validLen = 0; - } - } - } - - // start overlapping (if we don't have an previous packet data, just loop and the previous packet logic will handle things appropriately) - if (this._prevPacketEnd > 0) { - // overlap the first samples in the packet with the previous packet, then loop - VorbisStreamDecoder._overlapBuffers( - this._prevPacketBuf!, - res.curPacket, - this._prevPacketStart, - this._prevPacketStop, - res.startIndex, - this._stream.audioChannels - ); - this._prevPacketStart = res.startIndex; - } else if (this._prevPacketBuf == null) { - // first packet, so it doesn't have any good data before the valid length - this._prevPacketStart = res.validLen; - } - - // keep the old buffer so the GC doesn't have to reallocate every packet - this._nextPacketBuf = this._prevPacketBuf; - - // save off our current packet's data for the next pass - this._prevPacketEnd = res.validLen; - this._prevPacketStop = res.totalLen; - this._prevPacketBuf = res.curPacket!; - - return new VorbisReadNextPacketResult(res.samplePosition); - } - - private static _overlapBuffers( - previous: Float32Array[], - next: Float32Array[], - prevStart: number, - prevLen: number, - nextStart: number, - channels: number - ) { - for (; prevStart < prevLen; prevStart++, nextStart++) { - for (let c = 0; c < channels; c++) { - next[c][nextStart] += previous[c][prevStart]; - } - } - } - - private _decodeNextPacket(): DecodeNextPacketInfo { - const res = new DecodeNextPacketInfo(); - let packet: OggPacket | null = null; - if (this._packetIndex >= this._packets.length) { - // no packet? we're at the end of the stream - res.isEndOfStream = true; - } else { - packet = this._packets[this._packetIndex++]; - const reader = new IntBitReader(ByteBuffer.fromBuffer(packet.packetData)); - // if the packet is flagged as the end of the stream, we can safely mark _eosFound - res.isEndOfStream = packet.isEndOfStream; - - // make sure the packet starts with a 0 bit as per the spec - if (!reader.readBit()) { - // if we get here, we should have a good packet; decode it and add it to the buffer - const mode = this._setup.modes[reader.readBits(this._modeFieldBits)]; - if (this._nextPacketBuf == null) { - this._nextPacketBuf = new Array(this._stream.audioChannels); - for (let i = 0; i < this._stream.audioChannels; i++) { - this._nextPacketBuf[i] = new Float32Array(this._stream.blocksize1); - } - } - - const decodeRes = mode.decode(reader, this._nextPacketBuf); - res.startIndex = decodeRes.packetStartIndex; - res.validLen = decodeRes.packetValidLength; - res.totalLen = decodeRes.packetTotalLength; - - // per the spec, do not decode more samples than the last granulePosition - res.samplePosition = packet.granulePosition; - res.curPacket = this._nextPacketBuf; - return res; - } - } - - return res; - } -} - -/** - * @internal - */ -class DecodeNextPacketInfo { - public curPacket: Float32Array[] | null = null; - public startIndex: number = 0; - public validLen: number = 0; - public totalLen: number = 0; - public isEndOfStream: boolean = false; - public samplePosition: number | null = null; -} diff --git a/packages/alphatab/src/synth/vorbis/VorbisStreamReader.ts b/packages/alphatab/src/synth/vorbis/VorbisStreamReader.ts deleted file mode 100644 index 6a9ab655d0..0000000000 --- a/packages/alphatab/src/synth/vorbis/VorbisStreamReader.ts +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Vorbis Stream Reader according to https://xiph.org/vorbis/doc/Vorbis_I_spec.html - */ - -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; -import type { OggPacket } from '@coderline/alphatab/synth/vorbis/OggReader'; -import { VorbisStream } from '@coderline/alphatab/synth/vorbis/VorbisStream'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import { - Huffman, - Mdct, - VorbisCodebook, - VorbisFloor, - VorbisMapping, - VorbisMode, - VorbisResidue, - VorbisSetupHeader, - VorbisStreamDecoder, - VorbisTimeDomainTransform -} from '@coderline/alphatab/synth/vorbis/VorbisStreamDecoder'; -import { IntBitReader } from '@coderline/alphatab/synth/vorbis/IntBitReader'; - -/** - * @internal - */ -enum VorbisPacketTypes { - IdentificationHeader = 1, - Comment = 3, - SetupHeader = 5 -} - -/** - * @internal - */ -export class VorbisStreamReader { - static readonly vorbisHeaderMarker = new Uint8Array([ - 'v'.charCodeAt(0), - 'o'.charCodeAt(0), - 'r'.charCodeAt(0), - 'b'.charCodeAt(0), - 'i'.charCodeAt(0), - 's'.charCodeAt(0) - ]); - - private readonly _packets: OggPacket[]; - private _packetIndex: number; - - public constructor(packets: OggPacket[]) { - this._packetIndex = -1; - this._packets = packets; - } - - public read(): VorbisStream | null { - let packet: OggPacket | null; - while (true) { - packet = this._nextPacket(); - if (packet == null) { - return null; - } - - if (packet.isBeginningOfStream) { - const stream = this._readStream(packet); - if (stream != null) { - return stream; - } - } - } - } - - private _nextPacket(): OggPacket | null { - this._packetIndex++; - return this._packetIndex < this._packets.length ? this._packets[this._packetIndex] : null; - } - - private _readStream(startPacket: OggPacket): VorbisStream | null { - // Read vorbis stream info - const stream = new VorbisStream(); - - if (!this._readIdentificationHeader(stream, startPacket)) { - return null; - } - - if (!this._readComments(this._nextPacket())) { - return null; - } - - const vorbisSetup = new VorbisSetupHeader(); - if (!this._readSetupHeader(stream, vorbisSetup, this._nextPacket())) { - return null; - } - - // collect data packets for stream - const streamDataPackets: OggPacket[] = []; - let packet: OggPacket | null; - while (true) { - packet = this._nextPacket(); - if (packet == null) { - break; - } - streamDataPackets.push(packet); - if (packet.isEndOfStream) { - break; - } - } - - // decode samples - const decoder = new VorbisStreamDecoder(stream, vorbisSetup, streamDataPackets); - stream.samples = decoder.decode(); - - return stream; - } - - /** - * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2 - * @param packetType - * @param reader - * @returns - */ - private _commonHeaderDecode(packetType: VorbisPacketTypes, reader: ByteBuffer) { - const data = new Uint8Array(7); - reader.read(data, 0, data.length); - - if (data[0] !== packetType) { - return false; - } - - for (let i = 0; i < VorbisStreamReader.vorbisHeaderMarker.length; i++) { - if (data[1 + i] !== VorbisStreamReader.vorbisHeaderMarker[i]) { - return false; - } - } - - return true; - } - - /** - * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2 - * @param packetType - * @param reader - * @returns - */ - private _commonHeaderDecodeBit(packetType: VorbisPacketTypes, reader: IntBitReader) { - const data = reader.readBytes(7); - - if (data[0] !== packetType) { - return false; - } - - for (let i = 0; i < VorbisStreamReader.vorbisHeaderMarker.length; i++) { - if (data[1 + i] !== VorbisStreamReader.vorbisHeaderMarker[i]) { - return false; - } - } - - return true; - } - - /** - * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2 - * @param stream - * @param packet - * @returns - */ - private _readIdentificationHeader(stream: VorbisStream, packet: OggPacket) { - const reader = ByteBuffer.fromBuffer(packet.packetData); - - if (!this._commonHeaderDecode(VorbisPacketTypes.IdentificationHeader, reader)) { - return false; - } - - const version = IOHelper.readUInt32LE(reader); - // [vorbis_version] is to read ’0’ in order to be compatible with this document. - if (version !== 0) { - return false; - } - - stream.audioChannels = reader.readByte(); - stream.audioSampleRate = IOHelper.readUInt32LE(reader); - - // Both [audio_channels] and [audio_sample_rate] must read greater than zero. - if (stream.audioChannels <= 0 || stream.audioSampleRate <= 0) { - return false; - } - - stream.bitrateMaximum = IOHelper.readInt32LE(reader); - stream.bitrateNominal = IOHelper.readInt32LE(reader); - stream.bitrateMinimum = IOHelper.readInt32LE(reader); - - const blockSize = reader.readByte(); - stream.blocksize0 = 1 << (blockSize & 0x0f); - stream.blocksize1 = 1 << (blockSize >> 4); - if ( - // [blocksize_0] must be less than or equal to [blocksize_1]. - stream.blocksize0 > stream.blocksize1 || - !VorbisStreamReader._isAllowedBlockSize(stream.blocksize0) || - !VorbisStreamReader._isAllowedBlockSize(stream.blocksize0) - ) { - return false; - } - - // The framing bit must be nonzero. - const framing = reader.readByte(); - if (framing === 0) { - return false; - } - - return true; - } - - private static _isAllowedBlockSize(blocksize: number) { - // Allowed final blocksize values are 64, 128, 256, 512, 1024, 2048, 4096 and 8192 in Vorbis I. - switch (blocksize) { - case 64: - case 128: - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - return true; - - default: - return false; - } - } - - /** - * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-820005 - * @param packet - * @returns - */ - private _readComments(packet: OggPacket | null) { - if (packet == null) { - return false; - } - - const reader = ByteBuffer.fromBuffer(packet.packetData); - if (!this._commonHeaderDecode(VorbisPacketTypes.Comment, reader)) { - return false; - } - - const vendorLength = IOHelper.readUInt32LE(reader); - reader.skip(vendorLength); // vendor (unused) - - const userCommentListLength = IOHelper.readUInt32LE(reader); - for (let index = 0; index < userCommentListLength; index++) { - const length = IOHelper.readUInt32LE(reader); - reader.skip(length); // comment (unused) - } - - const framing = reader.readByte(); - if (framing === 0) { - return false; - } - - return true; - } - - /** - * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4 - * @param setup - * @param packet - * @returns - */ - private _readSetupHeader(stream: VorbisStream, setup: VorbisSetupHeader, packet: OggPacket | null) { - if (packet == null) { - return false; - } - - const bitReader = new IntBitReader(ByteBuffer.fromBuffer(packet.packetData)); - - const mdct = new Mdct(); - const huffman = new Huffman(); - - if (!this._commonHeaderDecodeBit(VorbisPacketTypes.SetupHeader, bitReader)) { - return false; - } - - let count = bitReader.readByte() + 1; - for (let i = 0; i < count; i++) { - setup.codebooks.push(new VorbisCodebook(bitReader, huffman)); - } - - count = bitReader.readBits(6) + 1; - for (let i = 0; i < count; i++) { - setup.timeDomainTransforms.push(new VorbisTimeDomainTransform(bitReader)); - } - - count = bitReader.readBits(6) + 1; - for (let i = 0; i < count; i++) { - setup.floors.push(new VorbisFloor(bitReader, stream.blocksize0, stream.blocksize1, setup.codebooks)); - } - - count = bitReader.readBits(6) + 1; - for (let i = 0; i < count; i++) { - setup.residues.push(new VorbisResidue(bitReader, stream.audioChannels, setup.codebooks)); - } - - count = bitReader.readBits(6) + 1; - for (let i = 0; i < count; i++) { - setup.mappings.push(new VorbisMapping(bitReader, stream.audioChannels, setup.floors, setup.residues, mdct)); - } - - count = bitReader.readBits(6) + 1; - for (let i = 0; i < count; i++) { - setup.modes.push( - new VorbisMode(bitReader, stream.audioChannels, stream.blocksize0, stream.blocksize1, setup.mappings) - ); - } - - if (!bitReader.readBit()) { - return false; - } - - return true; - } -} diff --git a/packages/alphatab/src/zip/Adler32.ts b/packages/alphatab/src/zip/Adler32.ts deleted file mode 100644 index 0b7a4d56a0..0000000000 --- a/packages/alphatab/src/zip/Adler32.ts +++ /dev/null @@ -1,85 +0,0 @@ -// This Deflate algorithm is based on the Deflater class of the SharpZipLib (MIT) -// https://github.com/icsharpcode/SharpZipLib -/* - * Copyright © 2000-2018 SharpZipLib Contributors - * 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. - */ - -/** - * Computes Adler32 checksum for a stream of data. An Adler32 - * checksum is not as reliable as a CRC32 checksum, but a lot faster to - * compute. - * - * The specification for Adler32 may be found in RFC 1950. - * ZLIB Compressed Data Format Specification version 3.3) - * - * @internal - */ -export class Adler32 { - /** - * largest prime smaller than 65536 - */ - private static readonly _base: number = 65521; - - /** - * Returns the Adler32 data checksum computed so far. - */ - public value: number = 1; - - /** - * Initialise a default instance of Adler32 - */ - public constructor() { - this.reset(); - } - - /** - * Resets the Adler32 data checksum as if no update was ever called. - */ - public reset() { - this.value = 1; - } - - /** - * Update Adler32 data checksum based on a portion of a block of data - * @param data The array containing the data to add - * @param offset Range start for data (inclusive) - * @param count The number of bytes to checksum starting from offset - */ - public update(data: Uint8Array, offset: number, count: number) { - //(By Per Bothner) - let s1 = this.value & 0xffff; - let s2 = this.value >> 16; - while (count > 0) { - // We can defer the modulo operation: - // s1 maximally grows from 65521 to 65521 + 255 * 3800 - // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 - let n = 3800; - if (n > count) { - n = count; - } - count -= n; - while (--n >= 0) { - s1 = s1 + (data[offset++] & 0xff); - s2 = s2 + s1; - } - s1 %= Adler32._base; - s2 %= Adler32._base; - } - this.value = (s2 << 16) | s1; - } -} diff --git a/packages/alphatab/src/zip/Crc32.ts b/packages/alphatab/src/zip/Crc32.ts index 957d8e608a..d1cf421aa7 100644 --- a/packages/alphatab/src/zip/Crc32.ts +++ b/packages/alphatab/src/zip/Crc32.ts @@ -1,61 +1,59 @@ /** - * CRC-32 with reversed data and unreversed output + * CRC-32 (ISO-HDLC, the variant used by ZIP): reflected polynomial 0xEDB88320, initial value and final xor 0xFFFFFFFF. + * + * The checksum is kept as signed 32-bit integer: values at or above 2^31 would not survive the + * integer conversions in the C# and Kotlin builds (see the rules in DeflateFormat). * @internal */ export class Crc32 { - private static readonly _crc32Lookup: Uint32Array = Crc32._buildCrc32Lookup(); - private static _buildCrc32Lookup(): Uint32Array { - const poly = 0xedb88320; - const lookup = new Uint32Array(256); - for (let i = 0; i < lookup.length; i++) { - let crc = i; - for (let bit = 0; bit < 8; bit++) { - crc = (crc & 1) === 1 ? (crc >>> 1) ^ poly : crc >>> 1; + private static _table: Int32Array | null = null; + + private static _buildTable(): Int32Array { + const table = new Int32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + // -306674912 is 0xEDB88320, the mask turns the signed shift into a logical one + c = (c & 1) !== 0 ? -306674912 ^ ((c >> 1) & 0x7fffffff) : (c >> 1) & 0x7fffffff; } - lookup[i] = crc; + table[n] = c; } - - return lookup; + return table; } - private static readonly _crcInit: number = 0xffffffff; - - /** - * The CRC data checksum so far. - */ - private _checkValue: number = Crc32._crcInit; - - /** - * Returns the CRC data checksum computed so far. - */ - public get value() { - return ~this._checkValue; - } + private _crc: number = -1; /** - * Initialise a default instance of Crc32. + * The checksum of the data added so far (signed 32-bit integer). */ - public constructor() { - this.reset(); + public get value(): number { + return ~this._crc; } /** - * Update CRC data checksum based on a portion of a block of data + * Adds a portion of the given data to the checksum. * @param data The array containing the data to add * @param offset Range start for data (inclusive) - * @param count The number of bytes to checksum starting from offset + * @param count The number of bytes to add starting from offset */ public update(data: Uint8Array, offset: number, count: number) { - for (let i = 0; i < count; i++) { - this._checkValue = - Crc32._crc32Lookup[(this._checkValue ^ data[offset + i]) & 0xff] ^ (this._checkValue >>> 8); + let table = Crc32._table; + if (!table) { + table = Crc32._buildTable(); + Crc32._table = table; + } + let c = this._crc; + const end = offset + count; + for (let i = offset; i < end; i++) { + c = table[(c ^ data[i]) & 0xff] ^ ((c >> 8) & 0xffffff); } + this._crc = c; } /** - * Resets the CRC data checksum as if no update was ever called. + * Resets the checksum as if no data was added. */ public reset() { - this._checkValue = Crc32._crcInit; + this._crc = -1; } } diff --git a/packages/alphatab/src/zip/DeflateFormat.ts b/packages/alphatab/src/zip/DeflateFormat.ts new file mode 100644 index 0000000000..3792786176 --- /dev/null +++ b/packages/alphatab/src/zip/DeflateFormat.ts @@ -0,0 +1,111 @@ +/** + * The parts of the DEFLATE format (RFC 1951) shared by Inflater and Deflater: + * the length and distance code tables, the code length alphabet order, the fixed Huffman code + * and the canonical Huffman code assignment. + * + * alphaTab is transpiled to C# and Kotlin where `number` becomes a `double` and every bit operation + * converts its operands to a 32-bit integer. To produce identical results on all platforms, the + * DEFLATE code follows these rules: + * - values taking part in bit operations stay within [0, 2^31) (CRC-32 uses signed values instead), + * - no `>>>` (it becomes a signed shift), use `(x >> n) & mask` where needed, + * - bit buffers hold less than 24 bits and never get negative values OR'ed in, + * - no compound bit assignments (`x >>= n`): the C# output does not convert a variable right side, + * - no local names reused in nested scopes (not allowed in C#), + * - typed arrays only receive in-range values (bytes are masked, tables are `Int32Array`), + * - integer division only through shifts, + * - no reads past the end of an array, `subarray` ends are clamped by the caller. + * + * @internal + */ +export class DeflateFormat { + /** + * The base value and number of extra bits per length code (symbols 257-285), + * packed as `base << 4 | extraBits`. + */ + public static readonly lengthTable: Int32Array = DeflateFormat._buildTable(29, 2, 3); + + /** + * The base value and number of extra bits per distance code (symbols 0-29), + * packed as `base << 4 | extraBits`. + */ + public static readonly distanceTable: Int32Array = DeflateFormat._buildTable(30, 1, 1); + + /** + * The order in which the code lengths of the code length alphabet are stored (RFC 1951 3.2.7). + */ + public static readonly codeLengthOrder: Uint8Array = new Uint8Array([ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + ]); + + /** + * RFC 1951 3.2.5: the codes come in groups of `1 << groupShift` codes sharing the same number + * of extra bits, starting with zero extra bits for the first two groups. + * Each code covers `1 << extraBits` values following the previous one. + */ + private static _buildTable(count: number, groupShift: number, firstBase: number): Int32Array { + const table = new Int32Array(count); + let base = firstBase; + for (let i = 0; i < count; i++) { + const extraBits = Math.max(0, (i >> groupShift) - 1); + table[i] = (base << 4) | extraBits; + base += 1 << extraBits; + } + if (count === 29) { + // the last length code is the special case 258 without extra bits + table[28] = 258 << 4; + } + return table; + } + + /** + * Creates the code lengths of the fixed Huffman code (RFC 1951 3.2.6): + * 288 literal/length codes followed by 32 distance codes. + */ + public static fixedCodeLengths(): Uint8Array { + const lengths = new Uint8Array(320); + for (let i = 0; i < 320; i++) { + lengths[i] = i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : i < 288 ? 8 : 5; + } + return lengths; + } + + /** + * Assigns the canonical Huffman codes for the given code lengths (RFC 1951 3.2.2) and stores + * them bit-reversed, the order in which they are sent in the LSB-first bit stream. + * @param lengths The code length per symbol (0 = unused symbol, max 15). + * @param codes Receives the bit-reversed code per symbol. + * @returns The unused code space in units of 2^-15: 0 for a complete code, a positive value for + * an incomplete code and a negative value for an over-subscribed (invalid) code. + */ + public static canonicalCodes(lengths: Uint8Array, codes: Int32Array): number { + const count = lengths.length; + const lengthCounts = new Int32Array(16); + for (let i = 0; i < count; i++) { + lengthCounts[lengths[i]]++; + } + + const nextCode = new Int32Array(16); + let code = 0; + let left = 1 << 15; + for (let length = 1; length < 16; length++) { + code = (code + (length > 1 ? lengthCounts[length - 1] : 0)) << 1; + nextCode[length] = code; + left -= lengthCounts[length] << (15 - length); + } + + for (let i = 0; i < count; i++) { + const length = lengths[i]; + if (length !== 0) { + let c = nextCode[length]; + nextCode[length] = c + 1; + let reversed = 0; + for (let k = 0; k < length; k++) { + reversed = (reversed << 1) | (c & 1); + c = c >> 1; + } + codes[i] = reversed; + } + } + return left; + } +} diff --git a/packages/alphatab/src/zip/Deflater.ts b/packages/alphatab/src/zip/Deflater.ts index c3b2e4807f..839595d5a0 100644 --- a/packages/alphatab/src/zip/Deflater.ts +++ b/packages/alphatab/src/zip/Deflater.ts @@ -1,209 +1,585 @@ -// This Deflate algorithm is based on the Deflater class of the SharpZipLib (MIT) -// https://github.com/icsharpcode/SharpZipLib -/* - * Copyright © 2000-2018 SharpZipLib Contributors - * 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. - */ - -import { DeflaterConstants } from '@coderline/alphatab/zip/DeflaterConstants'; -import { DeflaterEngine } from '@coderline/alphatab/zip/DeflaterEngine'; -import { PendingBuffer } from '@coderline/alphatab/zip/PendingBuffer'; +import { DeflateFormat } from '@coderline/alphatab/zip/DeflateFormat'; /** - * This is the Deflater class. The deflater class compresses input - * with the deflate algorithm described in RFC 1951. It has several - * compression levels and three different strategies described below. + * Compresses data into a raw DEFLATE stream (RFC 1951). * - * This class is not thread safe. This is inherent in the API, due - * to the split of deflate and setInput. + * LZ77 matches are found with hash chains and one step of lazy evaluation (RFC 1951 section 4), + * using the search limits of zlib's default compression level. + * Every block is written as stored, fixed or dynamic Huffman block, whichever is smallest. + * Dynamic blocks use length-limited Huffman codes: optimal code lengths are computed with the + * in-place algorithm of Moffat and Katajainen, over-long codes are then limited by rebalancing + * the code space. * - * author of the original java version : Jochen Hoenicke - * + * See the rules in DeflateFormat for the integer handling. * @internal */ export class Deflater { - /* - * The Deflater can do the following state transitions: - * - * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. - * / | (2) (5) | - * / v (5) | - * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) - * \ | (3) | ,--------' - * | | | (3) / - * v v (5) v v - * (1) -> BUSY_STATE ----> FINISHING_STATE - * | (6) - * v - * FINISHED_STATE - * \_____________________________________/ - * | (7) - * v - * CLOSED_STATE - * - * (1) If we should produce a header we start in INIT_STATE, otherwise - * we start in BUSY_STATE. - * (2) A dictionary may be set only when we are in INIT_STATE, then - * we change the state as indicated. - * (3) Whether a dictionary is set or not, on the first call of deflate - * we change to BUSY_STATE. - * (4) -- intentionally left blank -- :) - * (5) FINISHING_STATE is entered, when flush() is called to indicate that - * there is no more INPUT. There are also states indicating, that - * the header wasn't written yet. - * (6) FINISHED_STATE is entered, when everything has been flushed to the - * internal pending output buffer. - * (7) At any time (7) - * - */ + private static readonly _maxSymbols = 16384; + + // hash chains over 3-byte sequences, head and prev hold position + 1 (0 ends a chain) + private _head = new Int32Array(32768); + private _prev = new Int32Array(32768); + + // the LZ77 symbols of the current block: literal byte or match length, and the match distance (0 for literals) + private _symbols = new Int32Array(Deflater._maxSymbols); + private _distances = new Int32Array(Deflater._maxSymbols); + private _symbolCount = 0; + private _literalFrequencies = new Int32Array(286); + private _distanceFrequencies = new Int32Array(30); - private static readonly _isFlushing: number = 0x04; - static readonly isFinishing: number = 0x08; + // the dynamic codes of the current block + private _literalLengths = new Uint8Array(286); + private _literalCodes = new Int32Array(286); + private _distanceLengths = new Uint8Array(30); + private _distanceCodes = new Int32Array(30); + private _codeLengthLengths = new Uint8Array(19); + private _codeLengthCodes = new Int32Array(19); + private _codeLengthFrequencies = new Int32Array(19); + private _allLengths = new Uint8Array(316); + // the run-length encoded code lengths, packed as symbol | repeatExtraBits << 5 + private _codeLengthTokens = new Int32Array(316); - private static readonly _busyState: number = 0x10; - private static readonly _flushingState: number = 0x14; - private static readonly _finishingState: number = 0x1c; - private static readonly _finishedState: number = 0x1e; + private _fixedLiteralLengths: Uint8Array; + private _fixedLiteralCodes = new Int32Array(288); + private _fixedDistanceLengths: Uint8Array; + private _fixedDistanceCodes = new Int32Array(32); - private _state: number = 0; - private _pending: PendingBuffer; - private _engine: DeflaterEngine; + // length (3-258) to length code, and (distance - 1) to distance code (see _distanceCode) + private _lengthCodes = new Uint8Array(259); + private _distanceCodeLookup = new Uint8Array(512); - public get inputCrc(): number { - return this._engine.inputCrc.value; + private _output = new Uint8Array(0); + private _outputPos = 0; + private _bitBuffer = 0; + private _bitCount = 0; + + public constructor() { + const fixed = DeflateFormat.fixedCodeLengths(); + this._fixedLiteralLengths = fixed.subarray(0, 288); + this._fixedDistanceLengths = fixed.subarray(288, 320); + DeflateFormat.canonicalCodes(this._fixedLiteralLengths, this._fixedLiteralCodes); + DeflateFormat.canonicalCodes(this._fixedDistanceLengths, this._fixedDistanceCodes); + + const lengthTable = DeflateFormat.lengthTable; + for (let code = 0; code < 29; code++) { + const base = lengthTable[code] >> 4; + const count = 1 << (lengthTable[code] & 15); + for (let i = 0; i < count; i++) { + this._lengthCodes[base + i] = code; + } + } + + const distanceTable = DeflateFormat.distanceTable; + for (let code = 0; code < 30; code++) { + const base = (distanceTable[code] >> 4) - 1; + const count = 1 << (distanceTable[code] & 15); + for (let i = 0; i < count; i++) { + const d = base + i; + this._distanceCodeLookup[d < 256 ? d : 256 + (d >> 7)] = code; + } + } } /** - * Creates a new deflater with given compression level - * @param level the compression level, a value between NO_COMPRESSION and BEST_COMPRESSION. - * beginning and the adler checksum at the end of the output. This is - * useful for the GZIP/PKZIP formats. + * Compresses the given data into a raw DEFLATE stream. + * @param data The data to compress. + * @returns The compressed data. */ - public constructor() { - this._pending = new PendingBuffer(DeflaterConstants.pendingBufSize); - this._engine = new DeflaterEngine(this._pending); - this.reset(); + public deflate(data: Uint8Array): Uint8Array { + const n = data.length; + this._output = new Uint8Array((n >> 2) + 256); + this._outputPos = 0; + this._bitBuffer = 0; + this._bitCount = 0; + this._head.fill(0); + this._symbolCount = 0; + this._literalFrequencies.fill(0); + this._distanceFrequencies.fill(0); + + // the block covers the input from blockStart up to consumed (the bytes covered by its symbols) + let blockStart = 0; + let consumed = 0; + + // lazy evaluation: the byte before pos is pending, either as literal or as start of the match prevLength/prevDistance + let hasPending = false; + let prevLength = 0; + let prevDistance = 0; + + let pos = 0; + while (pos < n) { + let match = 0; + if (n - pos >= 3) { + const candidate = this._insert(data, pos); + // a long enough pending match is taken without looking for a better one + if (candidate >= 0 && prevLength < 16) { + match = this._findMatch(data, pos, candidate, prevLength); + } + } + + if (prevLength >= 3 && match === 0) { + // the pending match is at least as good as the one starting here + this._tallyMatch(prevLength, prevDistance); + consumed = pos - 1 + prevLength; + for (let p = pos + 1; p < consumed; p++) { + if (n - p >= 3) { + this._insert(data, p); + } + } + pos = consumed; + hasPending = false; + prevLength = 0; + } else { + if (hasPending) { + this._tallyLiteral(data[pos - 1]); + consumed = pos; + } + hasPending = true; + prevLength = match & 511; + prevDistance = match >> 9; + pos++; + } + + // a block never covers more than 65535 bytes, so it always fits a single stored block + if (this._symbolCount === Deflater._maxSymbols || consumed - blockStart > 65535 - 258) { + this._writeBlock(data, blockStart, consumed, 0); + blockStart = consumed; + } + } + if (hasPending) { + this._tallyLiteral(data[pos - 1]); + consumed = pos; + } + this._writeBlock(data, blockStart, consumed, 1); + + if (this._bitCount > 0) { + this._output[this._outputPos++] = this._bitBuffer & 0xff; + } + + const result = new Uint8Array(this._outputPos); + result.set(this._output.subarray(0, this._outputPos), 0); + this._output = new Uint8Array(0); + return result; } /** - * Returns true, if the input buffer is empty. - * You should then call setInput(). - * NOTE: This method can also return true when the stream - * was finished. + * Inserts the 3-byte sequence at pos into the hash chains. + * @returns The previous position with the same hash or -1. */ - public get isNeedingInput() { - return this._engine.needsInput(); + private _insert(data: Uint8Array, pos: number): number { + const hash = ((data[pos] << 10) ^ (data[pos + 1] << 5) ^ data[pos + 2]) & 32767; + const candidate = this._head[hash]; + this._prev[pos & 32767] = candidate; + this._head[hash] = pos + 1; + return candidate - 1; } /** - * Returns true if the stream was finished and no more output bytes - * are available. + * Searches the hash chain for the longest match at pos which is longer than the pending match. + * @returns The match as `distance << 9 | length` or 0 if there is none. */ - public get isFinished() { - return this._state === Deflater._finishedState && this._pending.isFlushed; + private _findMatch(data: Uint8Array, pos: number, candidate: number, prevLength: number): number { + const maxLength = Math.min(258, data.length - pos); + let best = prevLength >= 3 ? prevLength : 2; + if (best >= maxLength) { + return 0; + } + const nice = Math.min(128, maxLength); + // positions in the window are newer than pos - 32768: the prev slot of pos - 32768 was just reused for pos + const limit = Math.max(pos - 32768, -1); + let chain = prevLength >= 8 ? 32 : 128; + let bestDistance = 0; + let c = candidate; + while (c > limit && chain > 0) { + chain--; + if (data[c + best] === data[pos + best] && data[c] === data[pos] && data[c + 1] === data[pos + 1]) { + let length = 2; + while (length < maxLength && data[c + length] === data[pos + length]) { + length++; + } + if (length > best) { + best = length; + bestDistance = pos - c; + if (length >= nice) { + break; + } + } + } + const next = this._prev[c & 32767] - 1; + if (next >= c) { + break; + } + c = next; + } + // short matches far away cost more than the literals + if (bestDistance === 0 || (best === 3 && bestDistance > 4096)) { + return 0; + } + return (bestDistance << 9) | best; + } + + private _tallyLiteral(literal: number) { + const i = this._symbolCount++; + this._symbols[i] = literal; + this._distances[i] = 0; + this._literalFrequencies[literal]++; + } + + private _tallyMatch(length: number, distance: number) { + const i = this._symbolCount++; + this._symbols[i] = length; + this._distances[i] = distance; + this._literalFrequencies[257 + this._lengthCodes[length]]++; + this._distanceFrequencies[this._distanceCode(distance)]++; + } + + private _distanceCode(distance: number): number { + const d = distance - 1; + return this._distanceCodeLookup[d < 256 ? d : 256 + (d >> 7)]; } /** - * Resets the deflater. The deflater acts afterwards as if it was - * just created with the same compression level and strategy as it - * had before. + * Writes the tallied symbols covering data[start..end) as the smallest of a stored, fixed or dynamic block. */ - public reset() { - this._state = Deflater._busyState; - this._pending.reset(); - this._engine.reset(); + private _writeBlock(data: Uint8Array, start: number, end: number, isFinal: number) { + const literalFrequencies = this._literalFrequencies; + const distanceFrequencies = this._distanceFrequencies; + const literalLengths = this._literalLengths; + const distanceLengths = this._distanceLengths; + const codeLengthLengths = this._codeLengthLengths; + const lengthTable = DeflateFormat.lengthTable; + const distanceTable = DeflateFormat.distanceTable; + const order = DeflateFormat.codeLengthOrder; + + literalFrequencies[256] = 1; + Deflater.buildCodeLengths(literalFrequencies, literalLengths, 15); + Deflater.buildCodeLengths(distanceFrequencies, distanceLengths, 15); + + let literalCount = 286; + while (literalCount > 257 && literalLengths[literalCount - 1] === 0) { + literalCount--; + } + let distanceCount = 30; + while (distanceCount > 1 && distanceLengths[distanceCount - 1] === 0) { + distanceCount--; + } + const tokenCount = this._encodeCodeLengths(literalCount, distanceCount); + Deflater.buildCodeLengths(this._codeLengthFrequencies, codeLengthLengths, 7); + let codeLengthCount = 19; + while (codeLengthCount > 4 && codeLengthLengths[order[codeLengthCount - 1]] === 0) { + codeLengthCount--; + } + + // exact sizes in bits + let extraBits = 0; + let dynamicBits = 17 + 3 * codeLengthCount; + let fixedBits = 3; + for (let i = 0; i < 286; i++) { + const f = literalFrequencies[i]; + dynamicBits += f * literalLengths[i]; + fixedBits += f * this._fixedLiteralLengths[i]; + if (i > 256) { + extraBits += f * (lengthTable[i - 257] & 15); + } + } + for (let i = 0; i < 30; i++) { + const f = distanceFrequencies[i]; + dynamicBits += f * distanceLengths[i]; + fixedBits += f * 5; + extraBits += f * (distanceTable[i] & 15); + } + for (let i = 0; i < tokenCount; i++) { + const symbol = this._codeLengthTokens[i] & 31; + dynamicBits += codeLengthLengths[symbol] + Deflater._repeatBits(symbol); + } + dynamicBits += extraBits; + fixedBits += extraBits; + const storedBits = 3 + ((8 - ((this._bitCount + 3) & 7)) & 7) + 32 + (end - start) * 8; + + if (storedBits <= fixedBits && storedBits <= dynamicBits) { + this._ensureCapacity((storedBits >> 3) + 8); + this._writeBits(isFinal, 3); + if (this._bitCount > 0) { + this._output[this._outputPos++] = this._bitBuffer & 0xff; + } + this._bitBuffer = 0; + this._bitCount = 0; + const length = end - start; + const nlength = ~length & 0xffff; + const output = this._output; + output[this._outputPos++] = length & 0xff; + output[this._outputPos++] = length >> 8; + output[this._outputPos++] = nlength & 0xff; + output[this._outputPos++] = nlength >> 8; + output.set(data.subarray(start, end), this._outputPos); + this._outputPos += length; + } else if (fixedBits <= dynamicBits) { + this._ensureCapacity((fixedBits >> 3) + 8); + this._writeBits(isFinal | 2, 3); + this._writeSymbols( + this._fixedLiteralLengths, + this._fixedLiteralCodes, + this._fixedDistanceLengths, + this._fixedDistanceCodes + ); + } else { + this._ensureCapacity((dynamicBits >> 3) + 8); + const codeLengthCodes = this._codeLengthCodes; + DeflateFormat.canonicalCodes(literalLengths, this._literalCodes); + DeflateFormat.canonicalCodes(distanceLengths, this._distanceCodes); + DeflateFormat.canonicalCodes(codeLengthLengths, codeLengthCodes); + + this._writeBits(isFinal | 4, 3); + this._writeBits(literalCount - 257, 5); + this._writeBits(distanceCount - 1, 5); + this._writeBits(codeLengthCount - 4, 4); + for (let i = 0; i < codeLengthCount; i++) { + this._writeBits(codeLengthLengths[order[i]], 3); + } + for (let i = 0; i < tokenCount; i++) { + const token = this._codeLengthTokens[i]; + const symbol = token & 31; + this._writeBits(codeLengthCodes[symbol], codeLengthLengths[symbol]); + this._writeBits(token >> 5, Deflater._repeatBits(symbol)); + } + this._writeSymbols(literalLengths, this._literalCodes, distanceLengths, this._distanceCodes); + } + + this._symbolCount = 0; + literalFrequencies.fill(0); + distanceFrequencies.fill(0); + } + + private static _repeatBits(codeLengthSymbol: number): number { + return codeLengthSymbol === 16 ? 2 : codeLengthSymbol === 17 ? 3 : codeLengthSymbol === 18 ? 7 : 0; + } + + private _writeSymbols( + literalLengths: Uint8Array, + literalCodes: Int32Array, + distanceLengths: Uint8Array, + distanceCodes: Int32Array + ) { + const lengthTable = DeflateFormat.lengthTable; + const distanceTable = DeflateFormat.distanceTable; + const count = this._symbolCount; + for (let i = 0; i < count; i++) { + const symbol = this._symbols[i]; + const distance = this._distances[i]; + if (distance === 0) { + this._writeBits(literalCodes[symbol], literalLengths[symbol]); + } else { + const lengthCode = this._lengthCodes[symbol]; + this._writeBits(literalCodes[257 + lengthCode], literalLengths[257 + lengthCode]); + let entry = lengthTable[lengthCode]; + this._writeBits(symbol - (entry >> 4), entry & 15); + + const distanceCode = this._distanceCode(distance); + this._writeBits(distanceCodes[distanceCode], distanceLengths[distanceCode]); + entry = distanceTable[distanceCode]; + this._writeBits(distance - (entry >> 4), entry & 15); + } + } + this._writeBits(literalCodes[256], literalLengths[256]); } /** - * Sets the data which should be compressed next. This should be - * only called when needsInput indicates that more input is needed. - * The given byte array should not be changed, before needsInput() returns - * true again. - * @param input the buffer containing the input data. - * @param offset the start of the data. - * @param count the number of data bytes of input. + * Run-length encodes the literal/length and distance code lengths as one sequence (RFC 1951 3.2.7) + * into the code length tokens and counts the code length symbol frequencies. + * @returns The number of tokens. */ - public setInput(input: Uint8Array, offset: number, count: number) { - this._engine.setInput(input, offset, count); + private _encodeCodeLengths(literalCount: number, distanceCount: number): number { + const lengths = this._allLengths; + lengths.set(this._literalLengths.subarray(0, literalCount), 0); + lengths.set(this._distanceLengths.subarray(0, distanceCount), literalCount); + const total = literalCount + distanceCount; + const tokens = this._codeLengthTokens; + const frequencies = this._codeLengthFrequencies; + frequencies.fill(0); + + let count = 0; + let i = 0; + while (i < total) { + const value = lengths[i]; + let run = 1; + while (i + run < total && lengths[i + run] === value) { + run++; + } + i += run; + + if (value === 0) { + // 18: 11-138 zeros, 17: 3-10 zeros + while (run >= 11) { + const r = Math.min(run, 138); + tokens[count++] = 18 | ((r - 11) << 5); + frequencies[18]++; + run -= r; + } + if (run >= 3) { + tokens[count++] = 17 | ((run - 3) << 5); + frequencies[17]++; + run = 0; + } + } else { + // 16: repeat the previous length 3-6 times + tokens[count++] = value; + frequencies[value]++; + run--; + while (run >= 3) { + const r = Math.min(run, 6); + tokens[count++] = 16 | ((r - 3) << 5); + frequencies[16]++; + run -= r; + } + } + while (run > 0) { + tokens[count++] = value; + frequencies[value]++; + run--; + } + } + return count; } /** - * Deflates the current input block to the given array. - * @param output Buffer to store the compressed data. - * @param offset Offset into the output array. - * @param length The maximum number of bytes that may be stored. - * @returns The number of compressed bytes added to the output, or 0 if either - * needsInput() or finished() returns true or length is zero. + * Computes length-limited Huffman code lengths for the given symbol frequencies (at most 2^22 per symbol). + * At least two symbols get a code, the resulting code is always complete. + * @param frequencies The frequency per symbol. + * @param lengths Receives the code length per symbol (0 for unused symbols). + * @param limit The maximum code length. */ - public deflate(output: Uint8Array, offset: number, length: number): number { - const origLength = length; - - while (true) { - const count = this._pending.flush(output, offset, length); - offset += count; - length -= count; + public static buildCodeLengths(frequencies: Int32Array, lengths: Uint8Array, limit: number) { + const symbolCount = frequencies.length; + lengths.fill(0); - if (length === 0 || this._state === Deflater._finishedState) { - break; + // used symbols sorted by frequency, the symbol makes the order total (same result on all platforms) + const keys = new Int32Array(symbolCount); + let n = 0; + for (let s = 0; s < symbolCount; s++) { + if (frequencies[s] > 0) { + keys[n++] = frequencies[s] * 512 + s; + } + } + // like zlib, use at least two codes: a single symbol would need a code of zero bits + for (let s = 0; n < 2; s++) { + if (frequencies[s] === 0) { + keys[n++] = 512 + s; } + } + // insertion sort, there are at most 286 symbols + for (let i = 1; i < n; i++) { + const key = keys[i]; + let j = i - 1; + while (j >= 0 && keys[j] > key) { + keys[j + 1] = keys[j]; + j--; + } + keys[j + 1] = key; + } - if ( - !this._engine.deflate( - (this._state & Deflater._isFlushing) !== 0, - (this._state & Deflater.isFinishing) !== 0 - ) - ) { - switch (this._state) { - case Deflater._busyState: - // We need more input now - return origLength - length; - - case Deflater._flushingState: - /* We have to supply some lookahead. 8 bit lookahead - * is needed by the zlib inflater, and we must fill - * the next byte, so that all bits are flushed. - */ - let neededbits = 8 + (-this._pending.bitCount & 7); - while (neededbits > 0) { - /* write a static tree block consisting solely of - * an EOF: - */ - this._pending.writeBits(2, 10); - neededbits -= 10; - } - this._state = Deflater._busyState; - break; + // Moffat and Katajainen: in-place calculation of minimum-redundancy codes + const a = new Int32Array(n); + for (let i = 0; i < n; i++) { + a[i] = keys[i] >> 9; + } + // phase 1: weights of the internal nodes, which are replaced by their parent index once used + a[0] += a[1]; + let root = 0; + let leaf = 2; + for (let node = 1; node < n - 1; node++) { + if (leaf >= n || a[root] < a[leaf]) { + a[node] = a[root]; + a[root++] = node; + } else { + a[node] = a[leaf++]; + } + if (leaf >= n || (root < node && a[root] < a[leaf])) { + a[node] += a[root]; + a[root++] = node; + } else { + a[node] += a[leaf++]; + } + } + // phase 2: depth of the internal nodes + a[n - 2] = 0; + for (let node = n - 3; node >= 0; node--) { + a[node] = a[a[node]] + 1; + } + // phase 3: depth of the leaves + let available = 1; + let used = 0; + let depth = 0; + root = n - 2; + let next = n - 1; + while (available > 0) { + while (root >= 0 && a[root] === depth) { + used++; + root--; + } + while (available > used) { + a[next--] = depth; + available--; + } + available = 2 * used; + depth++; + used = 0; + } - case Deflater._finishingState: - this._pending.alignToByte(); - this._state = Deflater._finishedState; + // count the codes per length, limiting over-long codes + const counts = new Int32Array(limit + 1); + let overflow = false; + for (let i = 0; i < n; i++) { + if (a[i] > limit) { + counts[limit]++; + overflow = true; + } else { + counts[a[i]]++; + } + } + if (overflow) { + // the clamped code is over-subscribed: until the code space (Kraft sum) fits exactly, replace a leaf at the + // limit and the deepest shorter leaf by two leaves one level below that leaf + let kraft = 0; + for (let l = 1; l <= limit; l++) { + kraft += counts[l] << (limit - l); + } + while (kraft > 1 << limit) { + counts[limit]--; + for (let l = limit - 1; l > 0; l--) { + if (counts[l] > 0) { + counts[l]--; + counts[l + 1] += 2; break; + } } + kraft--; + } + } + + // the longest codes go to the least frequent symbols + let k = 0; + for (let l = limit; l > 0; l--) { + for (let c = counts[l]; c > 0; c--) { + lengths[keys[k++] & 511] = l; } } - return origLength - length; } - /** - * Finishes the deflater with the current input block. It is an error - * to give more input after this method was called. This method must - * be called to force all bytes to be flushed. - */ - public finish() { - this._state |= Deflater._isFlushing | Deflater.isFinishing; + private _ensureCapacity(bytes: number) { + const needed = this._outputPos + bytes; + if (needed > this._output.length) { + const grown = new Uint8Array(Math.max(this._output.length * 2, needed)); + grown.set(this._output.subarray(0, this._outputPos), 0); + this._output = grown; + } + } + + private _writeBits(value: number, count: number) { + this._bitBuffer = this._bitBuffer | (value << this._bitCount); + this._bitCount += count; + while (this._bitCount >= 8) { + this._output[this._outputPos++] = this._bitBuffer & 0xff; + this._bitBuffer = this._bitBuffer >> 8; + this._bitCount -= 8; + } } } diff --git a/packages/alphatab/src/zip/DeflaterConstants.ts b/packages/alphatab/src/zip/DeflaterConstants.ts deleted file mode 100644 index 3de6ece5d1..0000000000 --- a/packages/alphatab/src/zip/DeflaterConstants.ts +++ /dev/null @@ -1,46 +0,0 @@ -// This Deflate algorithm is based on the Deflater class of the SharpZipLib (MIT) -// https://github.com/icsharpcode/SharpZipLib -/* - * Copyright © 2000-2018 SharpZipLib Contributors - * 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. - */ - -/** - * This class contains constants used for deflation. - * - * @internal - */ -export class DeflaterConstants { - public static readonly maxWbits: number = 15; - public static readonly wsize: number = 1 << DeflaterConstants.maxWbits; - public static readonly wmask: number = DeflaterConstants.wsize - 1; - - public static readonly minMatch: number = 3; - public static readonly maxMatch: number = 258; - - public static readonly defaultMemLevel: number = 8; - public static readonly pendingBufSize: number = 1 << (DeflaterConstants.defaultMemLevel + 8); - - public static readonly hashBits: number = DeflaterConstants.defaultMemLevel + 7; - public static readonly hashSize: number = 1 << DeflaterConstants.hashBits; - public static readonly hashShift: number = - (DeflaterConstants.hashBits + DeflaterConstants.minMatch - 1) / DeflaterConstants.minMatch; - public static readonly hashMask: number = DeflaterConstants.hashSize - 1; - - public static readonly minLookahead: number = DeflaterConstants.maxMatch + DeflaterConstants.minMatch + 1; - public static readonly maxDist: number = DeflaterConstants.wsize - DeflaterConstants.minLookahead; -} diff --git a/packages/alphatab/src/zip/DeflaterEngine.ts b/packages/alphatab/src/zip/DeflaterEngine.ts deleted file mode 100644 index f9ed3c64de..0000000000 --- a/packages/alphatab/src/zip/DeflaterEngine.ts +++ /dev/null @@ -1,527 +0,0 @@ -// This Deflate algorithm is based on the Deflater class of the SharpZipLib (MIT) -// https://github.com/icsharpcode/SharpZipLib -/* - * Copyright © 2000-2018 SharpZipLib Contributors - * 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. - */ - -import { Crc32 } from '@coderline/alphatab/zip/Crc32'; -import { DeflaterConstants } from '@coderline/alphatab/zip/DeflaterConstants'; -import { DeflaterHuffman } from '@coderline/alphatab/zip/DeflaterHuffman'; -import type { PendingBuffer } from '@coderline/alphatab/zip/PendingBuffer'; - -/** - * Low level compression engine for deflate algorithm which uses a 32K sliding window - * with secondary compression from Huffman/Shannon-Fano codes. - * - * @internal - */ -export class DeflaterEngine { - private static readonly _tooFar: number = 4096; - - private _blockStart: number; - private _maxChain: number = 128; - private _niceLength: number = 128; - private _goodLength: number = 8; - - /** - * Hash index of string to be inserted - */ - private _insertHashIndex: number = 0; - - /** - * Points to the current character in the window. - */ - private _strstart: number; - - /** - * This array contains the part of the uncompressed stream that - * is of relevance. The current character is indexed by strstart. - */ - private _window: Uint8Array; - - /** - * Hashtable, hashing three characters to an index for window, so - * that window[index]..window[index+2] have this hash code. - * Note that the array should really be unsigned short, so you need - * to and the values with 0xffff. - */ - private _head: Int16Array; - - /** - * prev[index & WMASK] points to the previous index that has the - * same hash code as the string starting at index. This way - * entries with the same hash code are in a linked list. - * Note that the array should really be unsigned short, so you need - * to and the values with 0xffff. - */ - private _prev: Int16Array; - - /** - * lookahead is the number of characters starting at strstart in - * window that are valid. - * So window[strstart] until window[strstart+lookahead-1] are valid - * characters. - */ - private _lookahead: number = 0; - - /** - * The input data for compression. - */ - private _inputBuf: Uint8Array | null = null; - - /** - * The offset into inputBuf, where input data starts. - */ - private _inputOff: number = 0; - - /** - * The end offset of the input data. - */ - private _inputEnd: number = 0; - - /** - * Set if previous match exists - */ - private _prevAvailable: boolean = false; - - private _matchStart: number = 0; - - /** - * Length of best match - */ - private _matchLen: number = 0; - - private _pending: PendingBuffer; - private _huffman: DeflaterHuffman; - - public inputCrc: Crc32; - - /** - * Construct instance with pending buffer - * @param pending Pending buffer to use - * @param noAdlerCalculation Pending buffer to use - */ - public constructor(pending: PendingBuffer) { - this._pending = pending; - this._huffman = new DeflaterHuffman(pending); - this.inputCrc = new Crc32(); - - this._window = new Uint8Array(2 * DeflaterConstants.wsize); - this._head = new Int16Array(DeflaterConstants.hashSize); - this._prev = new Int16Array(DeflaterConstants.wsize); - - // We start at index 1, to avoid an implementation deficiency, that - // we cannot build a repeat pattern at index 0. - this._blockStart = 1; - this._strstart = 1; - } - - /** - * Reset internal state - */ - public reset() { - this._huffman.reset(); - this.inputCrc.reset(); - this._blockStart = 1; - this._strstart = 1; - this._lookahead = 0; - this._prevAvailable = false; - this._matchLen = DeflaterConstants.minMatch - 1; - - for (let i = 0; i < DeflaterConstants.hashSize; i++) { - this._head[i] = 0; - } - - for (let i = 0; i < DeflaterConstants.wsize; i++) { - this._prev[i] = 0; - } - } - - private _updateHash() { - this._insertHashIndex = - (this._window[this._strstart] << DeflaterConstants.hashShift) ^ this._window[this._strstart + 1]; - } - - /** - * Determines if more input is needed. - * @returns Return true if input is needed via setInput - */ - public needsInput(): boolean { - return this._inputEnd === this._inputOff; - } - - /** - * Sets input data to be deflated. Should only be called when NeedsInput() - * returns true - * @param buffer The buffer containing input data. - * @param offset The offset of the first byte of data. - * @param count The number of bytes of data to use as input. - */ - public setInput(buffer: Uint8Array, offset: number, count: number) { - const end = offset + count; - this._inputBuf = buffer; - this._inputOff = offset; - this._inputEnd = end; - } - - /** - * Deflate drives actual compression of data - * @param flush True to flush input buffers - * @param finish Finish deflation with the current input. - * @returns Returns true if progress has been made. - */ - public deflate(flush: boolean, finish: boolean): boolean { - let progress: boolean; - do { - this.fillWindow(); - const canFlush = flush && this._inputOff === this._inputEnd; - progress = this._deflateSlow(canFlush, finish); - } while (this._pending.isFlushed && progress); // repeat while we have no pending output and progress was made - return progress; - } - - private _deflateSlow(flush: boolean, finish: boolean): boolean { - if (this._lookahead < DeflaterConstants.minLookahead && !flush) { - return false; - } - - while (this._lookahead >= DeflaterConstants.minLookahead || flush) { - if (this._lookahead === 0) { - if (this._prevAvailable) { - this._huffman.tallyLit(this._window[this._strstart - 1] & 0xff); - } - this._prevAvailable = false; - - // We are flushing everything - this._huffman.flushBlock(this._window, this._blockStart, this._strstart - this._blockStart, finish); - this._blockStart = this._strstart; - return false; - } - - if (this._strstart >= 2 * DeflaterConstants.wsize - DeflaterConstants.minLookahead) { - /* slide window, as FindLongestMatch needs this. - * This should only happen when flushing and the window - * is almost full. - */ - this._slideWindow(); - } - - const prevMatch = this._matchStart; - let prevLen = this._matchLen; - if (this._lookahead >= DeflaterConstants.minMatch) { - const hashHead = this._insertString(); - - if ( - hashHead !== 0 && - this._strstart - hashHead <= DeflaterConstants.maxDist && - this._findLongestMatch(hashHead) - ) { - // longestMatch sets matchStart and matchLen - - // Discard match if too small and too far away - if ( - this._matchLen === DeflaterConstants.minMatch && - this._strstart - this._matchStart > DeflaterEngine._tooFar - ) { - this._matchLen = DeflaterConstants.minMatch - 1; - } - } - } - - // previous match was better - if (prevLen >= DeflaterConstants.minMatch && this._matchLen <= prevLen) { - this._huffman.tallyDist(this._strstart - 1 - prevMatch, prevLen); - prevLen -= 2; - do { - this._strstart++; - this._lookahead--; - if (this._lookahead >= DeflaterConstants.minMatch) { - this._insertString(); - } - } while (--prevLen > 0); - - this._strstart++; - this._lookahead--; - this._prevAvailable = false; - this._matchLen = DeflaterConstants.minMatch - 1; - } else { - if (this._prevAvailable) { - this._huffman.tallyLit(this._window[this._strstart - 1] & 0xff); - } - this._prevAvailable = true; - this._strstart++; - this._lookahead--; - } - - if (this._huffman.isFull()) { - let len = this._strstart - this._blockStart; - if (this._prevAvailable) { - len--; - } - const lastBlock = finish && this._lookahead === 0 && !this._prevAvailable; - this._huffman.flushBlock(this._window, this._blockStart, len, lastBlock); - this._blockStart += len; - return !lastBlock; - } - } - return true; - } - - /** - * Find the best (longest) string in the window matching the - * string starting at strstart. - * @param curMatch - * @returns True if a match greater than the minimum length is found - */ - private _findLongestMatch(curMatch: number): boolean { - let match: number; - let scan = this._strstart; - // scanMax is the highest position that we can look at - const scanMax = scan + Math.min(DeflaterConstants.maxMatch, this._lookahead) - 1; - const limit = Math.max(scan - DeflaterConstants.maxDist, 0); - - const window = this._window; - const prev = this._prev; - let chainLength = this._maxChain; - const niceLength = Math.min(this._niceLength, this._lookahead); - - this._matchLen = Math.max(this._matchLen, DeflaterConstants.minMatch - 1); - - if (scan + this._matchLen > scanMax) { - return false; - } - - let scanEnd1 = window[scan + this._matchLen - 1]; - let scanEnd = window[scan + this._matchLen]; - - // Do not waste too much time if we already have a good match: - if (this._matchLen >= this._goodLength) { - chainLength >>= 2; - } - - do { - match = curMatch; - scan = this._strstart; - - if ( - window[match + this._matchLen] !== scanEnd || - window[match + this._matchLen - 1] !== scanEnd1 || - window[match] !== window[scan] || - window[++match] !== window[++scan] - ) { - continue; - } - - // scan is set to strstart+1 and the comparison passed, so - // scanMax - scan is the maximum number of bytes we can compare. - // below we compare 8 bytes at a time, so first we compare - // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 - - switch ((scanMax - scan) % 8) { - case 1: - if (window[++scan] === window[++match]) { - break; - } - break; - - case 2: - if (window[++scan] === window[++match] && window[++scan] === window[++match]) { - break; - } - break; - - case 3: - if ( - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] - ) { - break; - } - break; - - case 4: - if ( - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] - ) { - break; - } - break; - - case 5: - if ( - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] - ) { - break; - } - break; - - case 6: - if ( - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] - ) { - break; - } - break; - - case 7: - if ( - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] - ) { - break; - } - break; - } - - if (window[scan] === window[match]) { - /* We check for insufficient lookahead only every 8th comparison; - * the 256th check will be made at strstart + 258 unless lookahead is - * exhausted first. - */ - do { - if (scan === scanMax) { - ++scan; // advance to first position not matched - ++match; - - break; - } - } while ( - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] && - window[++scan] === window[++match] - ); - } - - if (scan - this._strstart > this._matchLen) { - this._matchStart = curMatch; - this._matchLen = scan - this._strstart; - - if (this._matchLen >= niceLength) { - break; - } - - scanEnd1 = window[scan - 1]; - scanEnd = window[scan]; - } - curMatch = prev[curMatch & DeflaterConstants.wmask] & 0xffff; - } while (curMatch > limit && 0 !== --chainLength); - - return this._matchLen >= DeflaterConstants.minMatch; - } - - /** - * Inserts the current string in the head hash and returns the previous - * value for this hash. - * @returns The previous hash value - */ - private _insertString(): number { - const hash = - ((this._insertHashIndex << DeflaterConstants.hashShift) ^ - this._window[this._strstart + (DeflaterConstants.minMatch - 1)]) & - DeflaterConstants.hashMask; - - const match = this._head[hash]; - this._prev[this._strstart & DeflaterConstants.wmask] = match; - this._head[hash] = this._strstart; - this._insertHashIndex = hash; - return match & 0xffff; - } - - /** - * Fill the window - */ - public fillWindow() { - /* If the window is almost full and there is insufficient lookahead, - * move the upper half to the lower one to make room in the upper half. - */ - if (this._strstart >= DeflaterConstants.wsize + DeflaterConstants.maxDist) { - this._slideWindow(); - } - - /* If there is not enough lookahead, but still some input left, - * read in the input - */ - if (this._lookahead < DeflaterConstants.minLookahead && this._inputOff < this._inputEnd) { - let more = 2 * DeflaterConstants.wsize - this._lookahead - this._strstart; - - if (more > this._inputEnd - this._inputOff) { - more = this._inputEnd - this._inputOff; - } - - this._window.set( - this._inputBuf!.subarray(this._inputOff, this._inputOff + more), - this._strstart + this._lookahead - ); - this.inputCrc.update(this._inputBuf!, this._inputOff, more); - - this._inputOff += more; - // this.totalIn += more; - this._lookahead += more; - } - - if (this._lookahead >= DeflaterConstants.minMatch) { - this._updateHash(); - } - } - - private _slideWindow() { - this._window.set( - this._window.subarray(DeflaterConstants.wsize, DeflaterConstants.wsize + DeflaterConstants.wsize), - 0 - ); - this._matchStart -= DeflaterConstants.wsize; - this._strstart -= DeflaterConstants.wsize; - this._blockStart -= DeflaterConstants.wsize; - - // Slide the hash table (could be avoided with 32 bit values - // at the expense of memory usage). - for (let i = 0; i < DeflaterConstants.hashSize; ++i) { - const m = this._head[i] & 0xffff; - this._head[i] = m >= DeflaterConstants.wsize ? m - DeflaterConstants.wsize : 0; - } - - // Slide the prev table. - for (let i = 0; i < DeflaterConstants.wsize; i++) { - const m = this._prev[i] & 0xffff; - this._prev[i] = m >= DeflaterConstants.wsize ? m - DeflaterConstants.wsize : 0; - } - } -} diff --git a/packages/alphatab/src/zip/DeflaterHuffman.ts b/packages/alphatab/src/zip/DeflaterHuffman.ts deleted file mode 100644 index d3371b88b5..0000000000 --- a/packages/alphatab/src/zip/DeflaterHuffman.ts +++ /dev/null @@ -1,746 +0,0 @@ -// This Deflate algorithm is based on the Deflater class of the SharpZipLib (MIT) -// https://github.com/icsharpcode/SharpZipLib -/* - * Copyright © 2000-2018 SharpZipLib Contributors - * 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. - */ - -import { DeflaterConstants } from '@coderline/alphatab/zip/DeflaterConstants'; -import type { PendingBuffer } from '@coderline/alphatab/zip/PendingBuffer'; - -/** - * @internal - */ -class Tree { - // repeat previous bit length 3-6 times (2 bits of repeat count) - private static readonly _repeat3To6 = 16; - - // repeat a zero length 3-10 times (3 bits of repeat count) - private static readonly _repeat3To10 = 17; - - // repeat a zero length 11-138 times (7 bits of repeat count) - private static readonly _repeat11To138 = 18; - - public freqs: Int16Array; - - public length: Uint8Array | null = null; - - public minNumCodes: number; - - public numCodes: number = 0; - - private _codes: Int16Array | null = null; - private readonly _bitLengthCounts: Int32Array; - private readonly _maxLength: number; - private _huffman: DeflaterHuffman; - - public constructor(dh: DeflaterHuffman, elems: number, minCodes: number, maxLength: number) { - this._huffman = dh; - this.minNumCodes = minCodes; - this._maxLength = maxLength; - this.freqs = new Int16Array(elems); - this._bitLengthCounts = new Int32Array(maxLength); - } - - /** - * Resets the internal state of the tree - */ - public reset() { - for (let i = 0; i < this.freqs.length; i++) { - this.freqs[i] = 0; - } - this._codes = null; - this.length = null; - } - - public buildTree() { - const numSymbols = this.freqs.length; - - /* heap is a priority queue, sorted by frequency, least frequent - * nodes first. The heap is a binary tree, with the property, that - * the parent node is smaller than both child nodes. This assures - * that the smallest node is the first parent. - * - * The binary tree is encoded in an array: 0 is root node and - * the nodes 2*n+1, 2*n+2 are the child nodes of node n. - */ - const heap = new Int32Array(numSymbols); - let heapLen = 0; - let maxCode = 0; - for (let n = 0; n < numSymbols; n++) { - const freq = this.freqs[n]; - if (freq !== 0) { - // Insert n into heap - let pos = heapLen++; - while (true) { - if (pos > 0) { - const ppos = Math.floor((pos - 1) / 2); - if (this.freqs[heap[ppos]] > freq) { - heap[pos] = heap[ppos]; - pos = ppos; - } else { - break; - } - } else { - break; - } - } - heap[pos] = n; - - maxCode = n; - } - } - - /* We could encode a single literal with 0 bits but then we - * don't see the literals. Therefore we force at least two - * literals to avoid this case. We don't care about order in - * this case, both literals get a 1 bit code. - */ - while (heapLen < 2) { - const node = maxCode < 2 ? ++maxCode : 0; - heap[heapLen++] = node; - } - - this.numCodes = Math.max(maxCode + 1, this.minNumCodes); - - const numLeafs = heapLen; - const childs = new Int32Array(4 * heapLen - 2); - const values = new Int32Array(2 * heapLen - 1); - let numNodes = numLeafs; - for (let i = 0; i < heapLen; i++) { - const node = heap[i]; - childs[2 * i] = node; - childs[2 * i + 1] = -1; - values[i] = this.freqs[node] << 8; - heap[i] = i; - } - - /* Construct the Huffman tree by repeatedly combining the least two - * frequent nodes. - */ - do { - const first = heap[0]; - let last = heap[--heapLen]; - - // Propagate the hole to the leafs of the heap - let ppos = 0; - let path = 1; - - while (path < heapLen) { - if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) { - path++; - } - - heap[ppos] = heap[path]; - ppos = path; - path = path * 2 + 1; - } - - /* Now propagate the last element down along path. Normally - * it shouldn't go too deep. - */ - let lastVal = values[last]; - while (true) { - path = ppos; - if (ppos > 0) { - ppos = Math.floor((path - 1) / 2); - if (values[heap[ppos]] > lastVal) { - heap[path] = heap[ppos]; - } else { - break; - } - } else { - break; - } - } - - heap[path] = last; - - const second = heap[0]; - - // Create a new node father of first and second - last = numNodes++; - childs[2 * last] = first; - childs[2 * last + 1] = second; - const mindepth = Math.min(values[first] & 0xff, values[second] & 0xff); - lastVal = values[first] + values[second] - mindepth + 1; - values[last] = lastVal; - - // Again, propagate the hole to the leafs - ppos = 0; - path = 1; - - while (path < heapLen) { - if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) { - path++; - } - - heap[ppos] = heap[path]; - ppos = path; - path = ppos * 2 + 1; - } - - // Now propagate the new element down along path - while (true) { - path = ppos; - if (path > 0) { - ppos = Math.floor((path - 1) / 2); - if (values[heap[ppos]] > lastVal) { - heap[path] = heap[ppos]; - } else { - break; - } - } else { - break; - } - } - heap[path] = last; - } while (heapLen > 1); - - this._buildLength(childs); - } - - private _buildLength(childs: Int32Array) { - this.length = new Uint8Array(this.freqs.length); - const numNodes = Math.floor(childs.length / 2); - const numLeafs = Math.floor((numNodes + 1) / 2); - let overflow = 0; - - for (let i = 0; i < this._maxLength; i++) { - this._bitLengthCounts[i] = 0; - } - - // First calculate optimal bit lengths - const lengths = new Int32Array(numNodes); - lengths[numNodes - 1] = 0; - - for (let i = numNodes - 1; i >= 0; i--) { - if (childs[2 * i + 1] !== -1) { - let bitLength = lengths[i] + 1; - if (bitLength > this._maxLength) { - bitLength = this._maxLength; - overflow++; - } - lengths[childs[2 * i]] = bitLength; - lengths[childs[2 * i + 1]] = bitLength; - } else { - // A leaf node - const bitLength = lengths[i]; - this._bitLengthCounts[bitLength - 1]++; - this.length[childs[2 * i]] = lengths[i]; - } - } - - if (overflow === 0) { - return; - } - - let incrBitLen = this._maxLength - 1; - do { - // Find the first bit length which could increase: - while (this._bitLengthCounts[--incrBitLen] === 0) {} - - // Move this node one down and remove a corresponding - // number of overflow nodes. - do { - this._bitLengthCounts[incrBitLen]--; - this._bitLengthCounts[++incrBitLen]++; - overflow -= 1 << (this._maxLength - 1 - incrBitLen); - } while (overflow > 0 && incrBitLen < this._maxLength - 1); - } while (overflow > 0); - - /* We may have overshot above. Move some nodes from maxLength to - * maxLength-1 in that case. - */ - this._bitLengthCounts[this._maxLength - 1] += overflow; - this._bitLengthCounts[this._maxLength - 2] -= overflow; - - /* Now recompute all bit lengths, scanning in increasing - * frequency. It is simpler to reconstruct all lengths instead of - * fixing only the wrong ones. This idea is taken from 'ar' - * written by Haruhiko Okumura. - * - * The nodes were inserted with decreasing frequency into the childs - * array. - */ - let nodePtr = 2 * numLeafs; - for (let bits = this._maxLength; bits !== 0; bits--) { - let n = this._bitLengthCounts[bits - 1]; - while (n > 0) { - const childPtr = 2 * childs[nodePtr++]; - if (childs[childPtr + 1] === -1) { - // We found another leaf - this.length[childs[childPtr]] = bits; - n--; - } - } - } - } - - /** - * Get encoded length - * @returns Encoded length, the sum of frequencies * lengths - */ - public getEncodedLength(): number { - let len = 0; - for (let i = 0; i < this.freqs.length; i++) { - len += this.freqs[i] * this.length![i]; - } - return len; - } - - /** - * Scan a literal or distance tree to determine the frequencies of the codes - * in the bit length tree. - * @param blTree - */ - public calcBLFreq(blTree: Tree) { - let maxCount: number; /* max repeat count */ - let minCount: number; /* min repeat count */ - let count: number; /* repeat count of the current code */ - let curlen = -1; /* length of current code */ - - let i = 0; - while (i < this.numCodes) { - count = 1; - const nextlen = this.length![i]; - if (nextlen === 0) { - maxCount = 138; - minCount = 3; - } else { - maxCount = 6; - minCount = 3; - if (curlen !== nextlen) { - blTree.freqs[nextlen]++; - count = 0; - } - } - curlen = nextlen; - i++; - - while (i < this.numCodes && curlen === this.length![i]) { - i++; - if (++count >= maxCount) { - break; - } - } - - if (count < minCount) { - blTree.freqs[curlen] += count; - } else if (curlen !== 0) { - blTree.freqs[Tree._repeat3To6]++; - } else if (count <= 10) { - blTree.freqs[Tree._repeat3To10]++; - } else { - blTree.freqs[Tree._repeat11To138]++; - } - } - } - - /** - * Set static codes and length - * @param staticCodes new codes - * @param staticLengths length for new codes - */ - public setStaticCodes(staticCodes: Int16Array, staticLengths: Uint8Array) { - this._codes = staticCodes; - this.length = staticLengths; - } - - /** - * Build dynamic codes and lengths - */ - public buildCodes() { - const nextCode = new Int32Array(this._maxLength); - let code = 0; - - this._codes = new Int16Array(this.freqs.length); - - for (let bits = 0; bits < this._maxLength; bits++) { - nextCode[bits] = code; - code += this._bitLengthCounts[bits] << (15 - bits); - } - - for (let i = 0; i < this.numCodes; i++) { - const bits = this.length![i]; - if (bits > 0) { - this._codes[i] = DeflaterHuffman.bitReverse(nextCode[bits - 1]); - nextCode[bits - 1] += 1 << (16 - bits); - } - } - } - - /** - * Write tree values - * @param blTree Tree to write - */ - public writeTree(blTree: Tree) { - let maxCount: number; // max repeat count - let minCount: number; // min repeat count - let count: number; // repeat count of the current code - let curlen = -1; // length of current code - - let i = 0; - while (i < this.numCodes) { - count = 1; - const nextlen = this.length![i]; - if (nextlen === 0) { - maxCount = 138; - minCount = 3; - } else { - maxCount = 6; - minCount = 3; - if (curlen !== nextlen) { - blTree.writeSymbol(nextlen); - count = 0; - } - } - curlen = nextlen; - i++; - - while (i < this.numCodes && curlen === this.length![i]) { - i++; - if (++count >= maxCount) { - break; - } - } - - if (count < minCount) { - while (count-- > 0) { - blTree.writeSymbol(curlen); - } - } else if (curlen !== 0) { - blTree.writeSymbol(Tree._repeat3To6); - this._huffman.pending.writeBits(count - 3, 2); - } else if (count <= 10) { - blTree.writeSymbol(Tree._repeat3To10); - this._huffman.pending.writeBits(count - 3, 3); - } else { - blTree.writeSymbol(Tree._repeat11To138); - this._huffman.pending.writeBits(count - 11, 7); - } - } - } - - public writeSymbol(code: number) { - this._huffman.pending.writeBits(this._codes![code] & 0xffff, this.length![code]); - } -} - -/** - * @internal - */ -export class DeflaterHuffman { - private static readonly _bufSize = 1 << (DeflaterConstants.defaultMemLevel + 6); - private static readonly _literalNum = 286; - - /** - * Written to Zip file to identify a stored block - */ - public static readonly storedBlock = 0; - - /** - * Identifies static tree in Zip file - */ - public static readonly staticTrees = 1; - - /** - * Identifies dynamic tree in Zip file - */ - public static readonly dynTrees = 2; - - // Number of distance codes - private static readonly _distNum = 30; - - private static _staticLCodes: Int16Array = new Int16Array(DeflaterHuffman._literalNum); - private static _staticLLength: Uint8Array = new Uint8Array(DeflaterHuffman._literalNum); - private static _staticDCodes: Int16Array = new Int16Array(DeflaterHuffman._distNum); - private static _staticDLength: Uint8Array = new Uint8Array(DeflaterHuffman._distNum); - - public static staticInit() { - // See RFC 1951 3.2.6 - // Literal codes - - let i = 0; - while (i < 144) { - DeflaterHuffman._staticLCodes[i] = DeflaterHuffman.bitReverse((0x030 + i) << 8); - DeflaterHuffman._staticLLength[i++] = 8; - } - - while (i < 256) { - DeflaterHuffman._staticLCodes[i] = DeflaterHuffman.bitReverse((0x190 - 144 + i) << 7); - DeflaterHuffman._staticLLength[i++] = 9; - } - - while (i < 280) { - DeflaterHuffman._staticLCodes[i] = DeflaterHuffman.bitReverse((0x000 - 256 + i) << 9); - DeflaterHuffman._staticLLength[i++] = 7; - } - - while (i < DeflaterHuffman._literalNum) { - DeflaterHuffman._staticLCodes[i] = DeflaterHuffman.bitReverse((0x0c0 - 280 + i) << 8); - DeflaterHuffman._staticLLength[i++] = 8; - } - - // Distance codes - for (i = 0; i < DeflaterHuffman._distNum; i++) { - DeflaterHuffman._staticDCodes[i] = DeflaterHuffman.bitReverse(i << 11); - DeflaterHuffman._staticDLength[i] = 5; - } - } - - // The lengths of the bit length codes are sent in order of decreasing - // probability, to avoid transmitting the lengths for unused bit length codes. - private static readonly _blOrder: number[] = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; - - private static readonly _bit4Reverse: Uint8Array = new Uint8Array([0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]); - - /** - * Reverse the bits of a 16 bit value. - * @param toReverse Value to reverse bits - * @returns Value with bits reversed - */ - public static bitReverse(toReverse: number): number { - return ( - (DeflaterHuffman._bit4Reverse[toReverse & 0xf] << 12) | - (DeflaterHuffman._bit4Reverse[(toReverse >> 4) & 0xf] << 8) | - (DeflaterHuffman._bit4Reverse[(toReverse >> 8) & 0xf] << 4) | - DeflaterHuffman._bit4Reverse[toReverse >> 12] - ); - } - - // Number of codes used to transfer bit lengths - private static readonly _bitLenNum = 19; - - private static readonly _eofSymbol = 256; - - /** - * Pending buffer to use - */ - public pending: PendingBuffer; - - private _literalTree: Tree; - private _distTree: Tree; - private _blTree: Tree; - - // Buffer for distances - private _dBuf: Int16Array; - - private _lBuf: Uint8Array; - private _lastLit: number = 0; - private _extraBits: number = 0; - - public constructor(pending: PendingBuffer) { - this.pending = pending; - - this._literalTree = new Tree(this, DeflaterHuffman._literalNum, 257, 15); - this._distTree = new Tree(this, DeflaterHuffman._distNum, 1, 15); - this._blTree = new Tree(this, DeflaterHuffman._bitLenNum, 4, 7); - - this._dBuf = new Int16Array(DeflaterHuffman._bufSize); - this._lBuf = new Uint8Array(DeflaterHuffman._bufSize); - } - - public isFull(): boolean { - return this._lastLit >= DeflaterHuffman._bufSize; - } - - public reset() { - this._lastLit = 0; - this._extraBits = 0; - this._literalTree.reset(); - this._distTree.reset(); - this._blTree.reset(); - } - - public flushStoredBlock(stored: Uint8Array, storedOffset: number, storedLength: number, lastBlock: boolean) { - this.pending.writeBits((DeflaterHuffman.storedBlock << 1) + (lastBlock ? 1 : 0), 3); - this.pending.alignToByte(); - this.pending.writeShort(storedLength); - this.pending.writeShort(~storedLength); - this.pending.writeBlock(stored, storedOffset, storedLength); - this.reset(); - } - - public flushBlock(stored: Uint8Array, storedOffset: number, storedLength: number, lastBlock: boolean) { - this._literalTree.freqs[DeflaterHuffman._eofSymbol]++; - - // Build trees - this._literalTree.buildTree(); - this._distTree.buildTree(); - - // Calculate bitlen frequency - this._literalTree.calcBLFreq(this._blTree); - this._distTree.calcBLFreq(this._blTree); - - // Build bitlen tree - this._blTree.buildTree(); - - let blTreeCodes = 4; - for (let i = 18; i > blTreeCodes; i--) { - if (this._blTree.length![DeflaterHuffman._blOrder[i]] > 0) { - blTreeCodes = i + 1; - } - } - let optLen = - 14 + - blTreeCodes * 3 + - this._blTree.getEncodedLength() + - this._literalTree.getEncodedLength() + - this._distTree.getEncodedLength() + - this._extraBits; - - let staticLen = this._extraBits; - for (let i = 0; i < DeflaterHuffman._literalNum; i++) { - staticLen += this._literalTree.freqs[i] * DeflaterHuffman._staticLLength[i]; - } - for (let i = 0; i < DeflaterHuffman._distNum; i++) { - staticLen += this._distTree.freqs[i] * DeflaterHuffman._staticDLength[i]; - } - if (optLen >= staticLen) { - // Force static trees - optLen = staticLen; - } - - if (storedOffset >= 0 && storedLength + 4 < optLen >> 3) { - // Store Block - this.flushStoredBlock(stored, storedOffset, storedLength, lastBlock); - } else if (optLen === staticLen) { - // Encode with static tree - this.pending.writeBits((DeflaterHuffman.staticTrees << 1) + (lastBlock ? 1 : 0), 3); - this._literalTree.setStaticCodes(DeflaterHuffman._staticLCodes, DeflaterHuffman._staticLLength); - this._distTree.setStaticCodes(DeflaterHuffman._staticDCodes, DeflaterHuffman._staticDLength); - this.compressBlock(); - this.reset(); - } else { - // Encode with dynamic tree - this.pending.writeBits((DeflaterHuffman.dynTrees << 1) + (lastBlock ? 1 : 0), 3); - this.sendAllTrees(blTreeCodes); - this.compressBlock(); - this.reset(); - } - } - - /** - * Write all trees to pending buffer - * @param blTreeCodes The number/rank of treecodes to send. - */ - public sendAllTrees(blTreeCodes: number) { - this._blTree.buildCodes(); - this._literalTree.buildCodes(); - this._distTree.buildCodes(); - this.pending.writeBits(this._literalTree.numCodes - 257, 5); - this.pending.writeBits(this._distTree.numCodes - 1, 5); - this.pending.writeBits(blTreeCodes - 4, 4); - for (let rank = 0; rank < blTreeCodes; rank++) { - this.pending.writeBits(this._blTree.length![DeflaterHuffman._blOrder[rank]], 3); - } - this._literalTree.writeTree(this._blTree); - this._distTree.writeTree(this._blTree); - } - - /** - * Compress current buffer writing data to pending buffer - */ - public compressBlock() { - for (let i = 0; i < this._lastLit; i++) { - const litlen = this._lBuf[i] & 0xff; - let dist = this._dBuf[i]; - if (dist-- !== 0) { - const lc = DeflaterHuffman._lCode(litlen); - this._literalTree.writeSymbol(lc); - - let bits = Math.floor((lc - 261) / 4); - if (bits > 0 && bits <= 5) { - this.pending.writeBits(litlen & ((1 << bits) - 1), bits); - } - - const dc = DeflaterHuffman._dCode(dist); - this._distTree.writeSymbol(dc); - - bits = Math.floor(dc / 2) - 1; - if (bits > 0) { - this.pending.writeBits(dist & ((1 << bits) - 1), bits); - } - } else { - this._literalTree.writeSymbol(litlen); - } - } - - this._literalTree.writeSymbol(DeflaterHuffman._eofSymbol); - } - - /** - * Add distance code and length to literal and distance trees - * @param distance Distance code - * @param length Length - * @returns Value indicating if internal buffer is full - */ - public tallyDist(distance: number, length: number): boolean { - this._dBuf[this._lastLit] = distance; - this._lBuf[this._lastLit++] = length - 3; - - const lc = DeflaterHuffman._lCode(length - 3); - this._literalTree.freqs[lc]++; - if (lc >= 265 && lc < 285) { - this._extraBits += Math.floor((lc - 261) / 4); - } - - const dc = DeflaterHuffman._dCode(distance - 1); - this._distTree.freqs[dc]++; - if (dc >= 4) { - this._extraBits += Math.floor(dc / 2) - 1; - } - return this.isFull(); - } - - /** - * Add literal to buffer - * @param literal Literal value to add to buffer - * @returns Value indicating internal buffer is full - */ - public tallyLit(literal: number): boolean { - this._dBuf[this._lastLit] = 0; - this._lBuf[this._lastLit++] = literal; - this._literalTree.freqs[literal]++; - return this.isFull(); - } - - private static _lCode(length: number): number { - if (length === 255) { - return 285; - } - - let code = 257; - while (length >= 8) { - code += 4; - length = length >> 1; - } - return code + length; - } - - private static _dCode(distance: number): number { - let code = 0; - while (distance >= 4) { - code += 2; - distance = distance >> 1; - } - return code + distance; - } -} - -DeflaterHuffman.staticInit(); diff --git a/packages/alphatab/src/zip/HuffTools.ts b/packages/alphatab/src/zip/HuffTools.ts deleted file mode 100644 index 7ad8ed43fe..0000000000 --- a/packages/alphatab/src/zip/HuffTools.ts +++ /dev/null @@ -1,128 +0,0 @@ -// This Inflate algorithm is based on the Inflate class of the Haxe Standard Library (MIT) -import { FormatError } from '@coderline/alphatab/FormatError'; -/* - * Copyright (C)2005-2019 Haxe Foundation - * - * 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. - */ -import { Found, type Huffman, NeedBit, NeedBits } from '@coderline/alphatab/zip/Huffman'; - -// This Inflater is based on the Zip Reader of the Haxe Standard Library (MIT) - -/** - * @internal - */ -export class HuffTools { - public static make(lengths: number[], pos: number, nlengths: number, maxbits: number): Huffman { - const counts: number[] = []; - const tmp: number[] = []; - if (maxbits > 32) { - throw new FormatError('Invalid huffman'); - } - for (let i: number = 0; i < maxbits; i++) { - counts.push(0); - tmp.push(0); - } - for (let i: number = 0; i < nlengths; i++) { - const p: number = lengths[i + pos]; - if (p >= maxbits) { - throw new FormatError('Invalid huffman'); - } - counts[p]++; - } - let code: number = 0; - for (let i: number = 1; i < maxbits - 1; i++) { - code = (code + counts[i]) << 1; - tmp[i] = code; - } - const bits: Map = new Map(); - for (let i: number = 0; i < nlengths; i++) { - const l: number = lengths[i + pos]; - if (l !== 0) { - const n: number = tmp[l - 1]; - tmp[l - 1] = n + 1; - bits.set((n << 5) | l, i); - } - } - return HuffTools._treeCompress( - new NeedBit(HuffTools._treeMake(bits, maxbits, 0, 1), HuffTools._treeMake(bits, maxbits, 1, 1)) - ); - } - - private static _treeMake(bits: Map, maxbits: number, v: number, len: number): Huffman { - if (len > maxbits) { - throw new FormatError('Invalid huffman'); - } - const idx: number = (v << 5) | len; - if (bits.has(idx)) { - return new Found(bits.get(idx)!); - } - v = v << 1; - len += 1; - return new NeedBit(HuffTools._treeMake(bits, maxbits, v, len), HuffTools._treeMake(bits, maxbits, v | 1, len)); - } - - private static _treeCompress(t: Huffman): Huffman { - const d: number = HuffTools._treeDepth(t); - if (d === 0) { - return t; - } - if (d === 1) { - if (t instanceof NeedBit) { - return new NeedBit(HuffTools._treeCompress(t.left), HuffTools._treeCompress(t.right)); - } - throw new FormatError('assert'); - } - const size: number = 1 << d; - const table: Huffman[] = []; - for (let i: number = 0; i < size; i++) { - table.push(new Found(-1)); - } - HuffTools._treeWalk(table, 0, 0, d, t); - return new NeedBits(d, table); - } - - private static _treeWalk(table: Huffman[], p: number, cd: number, d: number, t: Huffman): void { - if (t instanceof NeedBit) { - if (d > 0) { - HuffTools._treeWalk(table, p, cd + 1, d - 1, t.left); - HuffTools._treeWalk(table, p | (1 << cd), cd + 1, d - 1, t.right); - } else { - table[p] = HuffTools._treeCompress(t); - } - } else { - table[p] = HuffTools._treeCompress(t); - } - } - - private static _treeDepth(t: Huffman): number { - if (t instanceof Found) { - return 0; - } - if (t instanceof NeedBits) { - throw new FormatError('assert'); - } - if (t instanceof NeedBit) { - const da: number = HuffTools._treeDepth(t.left); - const db: number = HuffTools._treeDepth(t.right); - return 1 + (da < db ? da : db); - } - return 0; - } -} diff --git a/packages/alphatab/src/zip/Huffman.ts b/packages/alphatab/src/zip/Huffman.ts deleted file mode 100644 index 3ed2344a12..0000000000 --- a/packages/alphatab/src/zip/Huffman.ts +++ /dev/null @@ -1,67 +0,0 @@ -// This Inflate algorithm is based on the Inflate class of the Haxe Standard Library (MIT) -/* - * Copyright (C)2005-2019 Haxe Foundation - * - * 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. - */ - -/** - * @internal - */ -export class Huffman {} - -/** - * @internal - */ -export class Found extends Huffman { - public readonly n: number; - - public constructor(n: number) { - super(); - this.n = n; - } -} - -/** - * @internal - */ -export class NeedBit extends Huffman { - public readonly left: Huffman; - public readonly right: Huffman; - - public constructor(left: Huffman, right: Huffman) { - super(); - this.left = left; - this.right = right; - } -} - -/** - * @internal - */ -export class NeedBits extends Huffman { - public readonly n: number; - public readonly table: Huffman[]; - - public constructor(n: number, table: Huffman[]) { - super(); - this.n = n; - this.table = table; - } -} diff --git a/packages/alphatab/src/zip/Inflate.ts b/packages/alphatab/src/zip/Inflate.ts deleted file mode 100644 index 11d43732a8..0000000000 --- a/packages/alphatab/src/zip/Inflate.ts +++ /dev/null @@ -1,407 +0,0 @@ -// This Inflate algorithm is based on the Inflate class of the Haxe Standard Library (MIT) -/* - * Copyright (C)2005-2019 Haxe Foundation - * - * 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. - */ -import { FormatError } from '@coderline/alphatab/FormatError'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import type { IReadable } from '@coderline/alphatab/io/IReadable'; -import { - Found as HuffmanFound, - type Huffman, - NeedBit as HuffmanNeedBit, - NeedBits as HuffmanNeedBits -} from '@coderline/alphatab/zip/Huffman'; -import { HuffTools } from '@coderline/alphatab/zip/HuffTools'; - -/** - * @internal - */ -enum InflateState { - Head = 0, - Block = 1, - CData = 2, - Flat = 3, - Crc = 4, - Dist = 5, - DistOne = 6, - Done = 7 -} - -/** - * @internal - */ -class InflateWindow { - private static readonly _size: number = 1 << 15; - private static readonly _bufferSize: number = 1 << 16; - - public buffer: Uint8Array = new Uint8Array(InflateWindow._bufferSize); - public pos: number = 0; - - public slide(): void { - const b: Uint8Array = new Uint8Array(InflateWindow._bufferSize); - this.pos -= InflateWindow._size; - b.set(this.buffer.subarray(InflateWindow._size, InflateWindow._size + this.pos), 0); - this.buffer = b; - } - - public addBytes(b: Uint8Array, p: number, len: number): void { - if (this.pos + len > InflateWindow._bufferSize) { - this.slide(); - } - this.buffer.set(b.subarray(p, p + len), this.pos); - this.pos += len; - } - - public addByte(c: number): void { - if (this.pos === InflateWindow._bufferSize) { - this.slide(); - } - this.buffer[this.pos] = c; - this.pos++; - } - - public getLastChar(): number { - return this.buffer[this.pos - 1]; - } - - public available(): number { - return this.pos; - } -} - -/** - * @internal - */ -export class Inflate { - private static _lenExtraBitsTbl: number[] = [ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, -1, -1 - ]; - private static _lenBaseValTbl: number[] = [ - 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, - 258 - ]; - private static _distExtraBitsTbl: number[] = [ - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, -1, -1 - ]; - private static _distBaseValTbl: number[] = [ - 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, - 6145, 8193, 12289, 16385, 24577 - ]; - private static _codeLengthsPos: number[] = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; - - private static _fixedHuffman: Huffman = Inflate._buildFixedHuffman(); - - private static _buildFixedHuffman(): Huffman { - const a: number[] = []; - for (let n: number = 0; n < 288; n++) { - a.push(n <= 143 ? 8 : n <= 255 ? 9 : n <= 279 ? 7 : 8); - } - return HuffTools.make(a, 0, 288, 10); - } - - private _nbits: number = 0; - private _bits: number = 0; - private _state: InflateState = InflateState.Block; - private _isFinal: boolean = false; - private _huffman: Huffman = Inflate._fixedHuffman; - private _huffdist: Huffman | null = null; - private _len: number = 0; - private _dist: number = 0; - private _needed: number = 0; - private _output: Uint8Array | null = null; - private _outpos: number = 0; - private _input: IReadable; - private _lengths: number[] = []; - private _window: InflateWindow = new InflateWindow(); - - public constructor(readable: IReadable) { - this._input = readable; - for (let i: number = 0; i < 19; i++) { - this._lengths.push(-1); - } - } - - public readBytes(b: Uint8Array, pos: number, len: number): number { - this._needed = len; - this._outpos = pos; - this._output = b; - if (len > 0) { - while (this._inflateLoop()) { - // inflating... - } - } - return len - this._needed; - } - - private _inflateLoop(): boolean { - switch (this._state) { - case InflateState.Head: - const cmf: number = this._input.readByte(); - const cm: number = cmf & 15; - if (cm !== 8) { - throw new FormatError('Invalid data'); - } - const flg: number = this._input.readByte(); - // var fcheck = flg & 31; - const fdict: boolean = (flg & 32) !== 0; - // var flevel = flg >> 6; - if (((cmf << 8) + flg) % 31 !== 0) { - throw new FormatError('Invalid data'); - } - if (fdict) { - throw new FormatError('Unsupported dictionary'); - } - this._state = InflateState.Block; - return true; - case InflateState.Crc: - this._state = InflateState.Done; - return true; - case InflateState.Done: - // nothing - return false; - case InflateState.Block: - this._isFinal = this._getBit(); - switch (this._getBits(2)) { - case 0: - this._len = IOHelper.readUInt16LE(this._input); - const nlen: number = IOHelper.readUInt16LE(this._input); - if (nlen !== 0xffff - this._len) { - throw new FormatError('Invalid data'); - } - this._state = InflateState.Flat; - const r: boolean = this._inflateLoop(); - this._resetBits(); - return r; - case 1: - this._huffman = Inflate._fixedHuffman; - this._huffdist = null; - this._state = InflateState.CData; - return true; - case 2: - const hlit: number = this._getBits(5) + 257; - const hdist: number = this._getBits(5) + 1; - const hclen: number = this._getBits(4) + 4; - for (let i: number = 0; i < hclen; i++) { - this._lengths[Inflate._codeLengthsPos[i]] = this._getBits(3); - } - for (let i: number = hclen; i < 19; i++) { - this._lengths[Inflate._codeLengthsPos[i]] = 0; - } - this._huffman = HuffTools.make(this._lengths, 0, 19, 8); - const xlengths: number[] = []; - for (let i: number = 0; i < hlit + hdist; i++) { - xlengths.push(0); - } - this._inflateLengths(xlengths, hlit + hdist); - this._huffdist = HuffTools.make(xlengths, hlit, hdist, 16); - this._huffman = HuffTools.make(xlengths, 0, hlit, 16); - this._state = InflateState.CData; - return true; - default: - throw new FormatError('Invalid data'); - } - case InflateState.Flat: { - const rlen: number = this._len < this._needed ? this._len : this._needed; - const bytes: Uint8Array = IOHelper.readByteArray(this._input, rlen); - this._len -= rlen; - this._addBytes(bytes, 0, rlen); - if (this._len === 0) { - this._state = this._isFinal ? InflateState.Crc : InflateState.Block; - } - return this._needed > 0; - } - case InflateState.DistOne: { - const rlen: number = this._len < this._needed ? this._len : this._needed; - this._addDistOne(rlen); - this._len -= rlen; - if (this._len === 0) { - this._state = InflateState.CData; - } - return this._needed > 0; - } - case InflateState.Dist: - while (this._len > 0 && this._needed > 0) { - const rdist: number = this._len < this._dist ? this._len : this._dist; - const rlen: number = this._needed < rdist ? this._needed : rdist; - this._addDist(this._dist, rlen); - this._len -= rlen; - } - if (this._len === 0) { - this._state = InflateState.CData; - } - return this._needed > 0; - case InflateState.CData: - let n: number = this._applyHuffman(this._huffman); - if (n < 256) { - this._addByte(n); - return this._needed > 0; - } - - if (n === 256) { - this._state = this._isFinal ? InflateState.Crc : InflateState.Block; - return true; - } - n = (n - 257) & 0xff; - let extraBits: number = Inflate._lenExtraBitsTbl[n]; - if (extraBits === -1) { - throw new FormatError('Invalid data'); - } - this._len = Inflate._lenBaseValTbl[n] + this._getBits(extraBits); - const huffdist: Huffman | null = this._huffdist; - const distCode: number = !huffdist ? this._getRevBits(5) : this._applyHuffman(huffdist); - extraBits = Inflate._distExtraBitsTbl[distCode]; - if (extraBits === -1) { - throw new FormatError('Invalid data'); - } - this._dist = Inflate._distBaseValTbl[distCode] + this._getBits(extraBits); - if (this._dist > this._window.available()) { - throw new FormatError('Invalid data'); - } - this._state = this._dist === 1 ? InflateState.DistOne : InflateState.Dist; - return true; - } - return false; - } - - private _addDistOne(n: number): void { - const c: number = this._window.getLastChar(); - for (let i: number = 0; i < n; i++) { - this._addByte(c); - } - } - - private _addByte(b: number): void { - this._window.addByte(b); - this._output![this._outpos] = b; - this._needed--; - this._outpos++; - } - - private _addDist(d: number, len: number): void { - this._addBytes(this._window.buffer, this._window.pos - d, len); - } - - private _getBit(): boolean { - if (this._nbits === 0) { - this._nbits = 8; - this._bits = this._input.readByte(); - } - const b: boolean = (this._bits & 1) === 1; - this._nbits--; - this._bits = this._bits >> 1; - return b; - } - - private _getBits(n: number): number { - while (this._nbits < n) { - this._bits = this._bits | (this._input.readByte() << this._nbits); - this._nbits += 8; - } - const b: number = this._bits & ((1 << n) - 1); - this._nbits -= n; - this._bits = this._bits >> n; - return b; - } - - private _getRevBits(n: number): number { - return n === 0 ? 0 : this._getBit() ? (1 << (n - 1)) | this._getRevBits(n - 1) : this._getRevBits(n - 1); - } - - private _resetBits(): void { - this._bits = 0; - this._nbits = 0; - } - - private _addBytes(b: Uint8Array, p: number, len: number): void { - this._window.addBytes(b, p, len); - this._output!.set(b.subarray(p, p + len), this._outpos); - this._needed -= len; - this._outpos += len; - } - - private _inflateLengths(a: number[], max: number): void { - let i: number = 0; - let prev: number = 0; - while (i < max) { - const n: number = this._applyHuffman(this._huffman); - switch (n) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - prev = n; - a[i] = n; - i++; - break; - case 16: - const end: number = i + 3 + this._getBits(2); - if (end > max) { - throw new FormatError('Invalid data'); - } - while (i < end) { - a[i] = prev; - i++; - } - break; - case 17: - i += 3 + this._getBits(3); - if (i > max) { - throw new FormatError('Invalid data'); - } - break; - case 18: - i += 11 + this._getBits(7); - if (i > max) { - throw new FormatError('Invalid data'); - } - break; - default: { - throw new FormatError('Invalid data'); - } - } - } - } - - private _applyHuffman(h: Huffman): number { - if (h instanceof HuffmanFound) { - return h.n; - } - if (h instanceof HuffmanNeedBit) { - return this._applyHuffman(this._getBit() ? h.right : h.left); - } - if (h instanceof HuffmanNeedBits) { - return this._applyHuffman(h.table[this._getBits(h.n)]); - } - throw new FormatError('Invalid data'); - } -} diff --git a/packages/alphatab/src/zip/Inflater.ts b/packages/alphatab/src/zip/Inflater.ts new file mode 100644 index 0000000000..6486e3885b --- /dev/null +++ b/packages/alphatab/src/zip/Inflater.ts @@ -0,0 +1,288 @@ +import { FormatError } from '@coderline/alphatab/FormatError'; +import type { IReadable } from '@coderline/alphatab/io/IReadable'; +import { DeflateFormat } from '@coderline/alphatab/zip/DeflateFormat'; + +/** + * Decompresses raw DEFLATE streams (RFC 1951) from a readable. + * + * Huffman codes are decoded with a single lookup table per code, indexed by the next + * (bit-reversed) bits of the stream. Entries hold `symbol << 4 | codeLength`, a zero entry + * marks bits no code starts with. + * + * Bytes are only read from the input when the current step needs them. After every step at most + * 7 bits stay buffered, so the inflater never reads beyond the end of the compressed stream and the + * input is positioned right after it when done. + * + * Invalid input is rejected like zlib does, see the rules in DeflateFormat for the integer handling. + * @internal + */ +export class Inflater { + private _input: IReadable; + private _bitBuffer: number = 0; + private _bitCount: number = 0; + private _literals: Int32Array | null = null; + private _distances: Int32Array | null = null; + private _fixedLiterals: Int32Array | null = null; + private _fixedDistances: Int32Array | null = null; + + public constructor(input: IReadable) { + this._input = input; + } + + private static _invalid(): FormatError { + return new FormatError('Invalid deflate data'); + } + + /** + * Decompresses the DEFLATE stream starting at the current position of the input. + * @param sizeHint The expected size of the decompressed data (0 if unknown), used for the initial buffer size. + * @param maxSize The maximum allowed size of the decompressed data. + * @returns The decompressed data or null if it would exceed the given maximum size. + */ + public inflate(sizeHint: number, maxSize: number): Uint8Array | null { + this._bitBuffer = 0; + this._bitCount = 0; + + // the hint comes from the file, so it only decides the initial size up to a sane limit + let output: Uint8Array = new Uint8Array(Math.min(Math.min(sizeHint > 0 ? sizeHint : 65536, maxSize), 1048576)); + let pos = 0; + + const lengthTable = DeflateFormat.lengthTable; + const distanceTable = DeflateFormat.distanceTable; + + let isFinal = 0; + do { + isFinal = this._readBits(1); + const type = this._readBits(2); + if (type === 0) { + // stored block: the buffered bits are the rest of the current byte, skip to the byte boundary + this._bitBuffer = 0; + this._bitCount = 0; + const length = this._readBits(16); + if ((length ^ this._readBits(16)) !== 0xffff) { + throw Inflater._invalid(); + } + if (pos + length > output.length) { + const grown = Inflater._grow(output, pos, pos + length, maxSize); + if (!grown) { + return null; + } + output = grown; + } + if (this._input.read(output, pos, length) !== length) { + throw Inflater._invalid(); + } + pos += length; + } else { + if (type === 1) { + if (!this._fixedLiterals) { + const lengths = DeflateFormat.fixedCodeLengths(); + this._fixedLiterals = Inflater._buildTable(lengths.subarray(0, 288), false); + this._fixedDistances = Inflater._buildTable(lengths.subarray(288, 320), false); + } + this._literals = this._fixedLiterals; + this._distances = this._fixedDistances; + } else if (type === 2) { + this._readDynamicTables(); + } else { + throw Inflater._invalid(); + } + + const literals = this._literals!; + const distances = this._distances!; + while (true) { + let symbol = this._decodeSymbol(literals); + if (symbol < 256) { + if (pos === output.length) { + const grown = Inflater._grow(output, pos, pos + 1, maxSize); + if (!grown) { + return null; + } + output = grown; + } + output[pos++] = symbol; + } else if (symbol === 256) { + break; + } else { + symbol -= 257; + if (symbol >= 29) { + throw Inflater._invalid(); + } + let entry = lengthTable[symbol]; + const length = (entry >> 4) + this._readBits(entry & 15); + + symbol = this._decodeSymbol(distances); + if (symbol >= 30) { + throw Inflater._invalid(); + } + entry = distanceTable[symbol]; + const distance = (entry >> 4) + this._readBits(entry & 15); + if (distance > pos) { + throw Inflater._invalid(); + } + + const end = pos + length; + if (end > output.length) { + const grown = Inflater._grow(output, pos, end, maxSize); + if (!grown) { + return null; + } + output = grown; + } + // byte-wise to support overlapping copies (distance < length) + let from = pos - distance; + while (pos < end) { + output[pos++] = output[from++]; + } + } + } + } + } while (isFinal === 0); + + if (pos === output.length) { + return output; + } + const result = new Uint8Array(pos); + result.set(output.subarray(0, pos), 0); + return result; + } + + private static _grow(output: Uint8Array, length: number, needed: number, maxSize: number): Uint8Array | null { + if (needed > maxSize) { + return null; + } + const grown = new Uint8Array(Math.min(Math.max(output.length * 2, needed), maxSize)); + grown.set(output.subarray(0, length), 0); + return grown; + } + + /** + * Reads the code length code and the literal/length and distance code lengths of a dynamic block (RFC 1951 3.2.7). + */ + private _readDynamicTables() { + const literalCount = this._readBits(5) + 257; + const distanceCount = this._readBits(5) + 1; + const codeLengthCount = this._readBits(4) + 4; + if (literalCount > 286 || distanceCount > 30) { + throw Inflater._invalid(); + } + + const order = DeflateFormat.codeLengthOrder; + const codeLengths = new Uint8Array(19); + for (let k = 0; k < codeLengthCount; k++) { + codeLengths[order[k]] = this._readBits(3); + } + const codeLengthTable = Inflater._buildTable(codeLengths, true); + + // literal/length and distance code lengths form one sequence, repeats may cross from one into the other + const total = literalCount + distanceCount; + const lengths = new Uint8Array(total); + let i = 0; + while (i < total) { + const symbol = this._decodeSymbol(codeLengthTable); + if (symbol < 16) { + lengths[i++] = symbol; + } else { + let value = 0; + let repeat = 0; + if (symbol === 16) { + if (i === 0) { + throw Inflater._invalid(); + } + value = lengths[i - 1]; + repeat = 3 + this._readBits(2); + } else if (symbol === 17) { + repeat = 3 + this._readBits(3); + } else { + repeat = 11 + this._readBits(7); + } + const end = i + repeat; + if (end > total) { + throw Inflater._invalid(); + } + while (i < end) { + lengths[i++] = value; + } + } + } + + // a block without end-of-block code cannot be terminated + if (lengths[256] === 0) { + throw Inflater._invalid(); + } + + this._literals = Inflater._buildTable(lengths.subarray(0, literalCount), false); + this._distances = Inflater._buildTable(lengths.subarray(literalCount, total), false); + } + + /** + * Builds the decoding table for a Huffman code given by its code lengths. + * Like zlib, incomplete codes are only accepted if they consist of a single code of length 1 + * (or no code at all), and never for the code length code. + */ + private static _buildTable(lengths: Uint8Array, isCodeLengthCode: boolean): Int32Array { + const count = lengths.length; + const codes = new Int32Array(count); + const left = DeflateFormat.canonicalCodes(lengths, codes); + let maxLength = 0; + for (let i = 0; i < count; i++) { + if (lengths[i] > maxLength) { + maxLength = lengths[i]; + } + } + if (left < 0 || (left > 0 && (isCodeLengthCode || maxLength > 1))) { + throw Inflater._invalid(); + } + + const size = 1 << maxLength; + const table = new Int32Array(size); + for (let symbol = 0; symbol < count; symbol++) { + const length = lengths[symbol]; + if (length !== 0) { + // every index starting with the code decodes to the symbol + const entry = (symbol << 4) | length; + const step = 1 << length; + for (let i = codes[symbol]; i < size; i += step) { + table[i] = entry; + } + } + } + return table; + } + + private _decodeSymbol(table: Int32Array): number { + while (true) { + // bits not read yet are zero, the entry is reliable once its code length is covered by read bits + const entry = table[this._bitBuffer & (table.length - 1)]; + const length = entry & 15; + if (length !== 0 && length <= this._bitCount) { + this._bitBuffer = this._bitBuffer >> length; + this._bitCount -= length; + return entry >> 4; + } + if (length === 0 && 1 << this._bitCount >= table.length) { + throw Inflater._invalid(); + } + this._readByte(); + } + } + + private _readBits(count: number): number { + while (this._bitCount < count) { + this._readByte(); + } + const value = this._bitBuffer & ((1 << count) - 1); + this._bitBuffer = this._bitBuffer >> count; + this._bitCount -= count; + return value; + } + + private _readByte() { + const b = this._input.readByte(); + if (b < 0) { + throw Inflater._invalid(); + } + this._bitBuffer = this._bitBuffer | (b << this._bitCount); + this._bitCount += 8; + } +} diff --git a/packages/alphatab/src/zip/PendingBuffer.ts b/packages/alphatab/src/zip/PendingBuffer.ts deleted file mode 100644 index 8e600fc2a5..0000000000 --- a/packages/alphatab/src/zip/PendingBuffer.ts +++ /dev/null @@ -1,148 +0,0 @@ -// This Deflate algorithm is based on the Deflater class of the SharpZipLib (MIT) -// https://github.com/icsharpcode/SharpZipLib -/* - * Copyright © 2000-2018 SharpZipLib Contributors - * 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. - */ - -/** - * This class is general purpose class for writing data to a buffer. - * It allows you to write bits as well as bytes - * Based on DeflaterPending.java - * @internal - */ -export class PendingBuffer { - private _buffer: Uint8Array; - private _start: number = 0; - private _end: number = 0; - private _bits: number = 0; - - /** - * The number of bits written to the buffer - */ - public bitCount: number = 0; - - /** - * Indicates if buffer has been flushed - */ - public get isFlushed(): boolean { - return this._end === 0; - } - - /** - * construct instance using specified buffer size - * @param bufferSize size to use for internal buffer - */ - public constructor(bufferSize: number) { - this._buffer = new Uint8Array(bufferSize); - } - - /** - * Clear internal state/buffers - */ - public reset() { - this._start = 0; - this._end = 0; - this.bitCount = 0; - } - - /** - * Write a short value to internal buffer most significant byte first - * @param s value to write - */ - public writeShortMSB(s: number) { - this._buffer[this._end++] = (s >> 8) & 0xff; - this._buffer[this._end++] = s & 0xff; - } - - /** - * Write a short value to buffer LSB first - * @param value The value to write. - */ - public writeShort(value: number) { - this._buffer[this._end++] = value; - this._buffer[this._end++] = value >> 8; - } - - /** - * Write a block of data to buffer - * @param block data to write - * @param offset offset of first byte to write - * @param length number of bytes to write - */ - public writeBlock(block: Uint8Array, offset: number, length: number) { - this._buffer.set(block.subarray(offset, offset + length), this._end); - this._end += length; - } - - /** - * Flushes the pending buffer into the given output array. If the - * output array is to small, only a partial flush is done. - * @param output The output array. - * @param offset The offset into output array. - * @param length The maximum number of bytes to store. - * @returns The number of bytes flushed. - */ - public flush(output: Uint8Array, offset: number, length: number) { - if (this.bitCount >= 8) { - this._buffer[this._end++] = this._bits & 0xff; - this._bits >>= 8; - this.bitCount -= 8; - } - - if (length > this._end - this._start) { - length = this._end - this._start; - output.set(this._buffer.subarray(this._start, this._start + length), offset); - this._start = 0; - this._end = 0; - } else { - output.set(this._buffer.subarray(this._start, this._start + length), offset); - this._start += length; - } - return length; - } - - /** - * Write bits to internal buffer - * @param b source of bits - * @param count number of bits to write - */ - public writeBits(b: number, count: number) { - this._bits |= b << this.bitCount; - this.bitCount += count; - if (this.bitCount >= 16) { - this._buffer[this._end++] = this._bits & 0xff; - this._buffer[this._end++] = (this._bits >> 8) & 0xff; - this._bits >>= 16; - this.bitCount -= 16; - } - } - - /** - * Align internal buffer on a byte boundary - */ - public alignToByte() { - if (this.bitCount > 0) { - this._buffer[this._end++] = this._bits & 0xff; - if (this.bitCount > 8) { - this._buffer[this._end++] = (this._bits >> 8) & 0xff; - } - } - this._bits = 0; - this.bitCount = 0; - } -} diff --git a/packages/alphatab/src/zip/ZipReader.ts b/packages/alphatab/src/zip/ZipReader.ts index bf28690bad..d81db4983d 100644 --- a/packages/alphatab/src/zip/ZipReader.ts +++ b/packages/alphatab/src/zip/ZipReader.ts @@ -1,7 +1,6 @@ -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { IOHelper } from '@coderline/alphatab/io/IOHelper'; import { OverflowError, type IReadable } from '@coderline/alphatab/io/IReadable'; -import { Inflate } from '@coderline/alphatab/zip/Inflate'; +import { Inflater } from '@coderline/alphatab/zip/Inflater'; import { ZipEntry } from '@coderline/alphatab/zip/ZipEntry'; /** @@ -10,10 +9,12 @@ import { ZipEntry } from '@coderline/alphatab/zip/ZipEntry'; export class ZipReader { private _readable: IReadable; private _maxDecodingBufferSize: number; + private _inflater: Inflater; public constructor(readable: IReadable, maxDecodingBufferSize: number) { this._readable = readable; this._maxDecodingBufferSize = maxDecodingBufferSize; + this._inflater = new Inflater(readable); } public read(): ZipEntry[] { @@ -66,22 +67,13 @@ export class ZipReader { // 4.3.8 File Data let data: Uint8Array; if (compressed) { - const target: ByteBuffer = ByteBuffer.empty(); - const z: Inflate = new Inflate(this._readable); - const buffer: Uint8Array = new Uint8Array(65536); - while (true) { - const bytes: number = z.readBytes(buffer, 0, buffer.length); - target.write(buffer, 0, bytes); - if (target.length > this._maxDecodingBufferSize) { - throw new OverflowError( - `Zip entry "${fname}" contains data exceeding the configured maxDecodingBufferSize` - ); - } - if (bytes < buffer.length) { - break; - } + const inflated = this._inflater.inflate(uncompressedSize, this._maxDecodingBufferSize); + if (!inflated) { + throw new OverflowError( + `Zip entry "${fname}" contains data exceeding the configured maxDecodingBufferSize` + ); } - data = target.toArray(); + data = inflated; } else { data = IOHelper.readByteArray(this._readable, uncompressedSize); } diff --git a/packages/alphatab/src/zip/ZipWriter.ts b/packages/alphatab/src/zip/ZipWriter.ts index 74d08f2c9e..2e21d8dbb7 100644 --- a/packages/alphatab/src/zip/ZipWriter.ts +++ b/packages/alphatab/src/zip/ZipWriter.ts @@ -1,4 +1,3 @@ -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { IOHelper } from '@coderline/alphatab/io/IOHelper'; import type { IWriteable } from '@coderline/alphatab/io/IWriteable'; import { Crc32 } from '@coderline/alphatab/zip/Crc32'; @@ -46,15 +45,16 @@ export class ZipWriter { // 4.3.7 local file header const compressionMode = ZipEntry.CompressionMethodDeflate; - const compressedData = ByteBuffer.empty(); - const crc32 = this._compress(compressedData, entry.data, compressionMode); - const compressedDataArray = compressedData.toArray(); + const crc = new Crc32(); + crc.update(entry.data, 0, entry.data.length); + const crc32 = crc.value; + const compressedDataArray = this._deflater.deflate(entry.data); const directoryHeader = new ZipCentralDirectoryHeader( entry, crc32, this._data.bytesWritten, compressionMode, - compressedData.length + compressedDataArray.length ); this._centralDirectoryHeaders.push(directoryHeader); @@ -90,43 +90,6 @@ export class ZipWriter { this._data.write(compressedDataArray, 0, compressedDataArray.length); } - private _compress(output: IWriteable, data: Uint8Array, compressionMode: number): number { - if (compressionMode !== ZipEntry.CompressionMethodDeflate) { - const crc = new Crc32(); - crc.update(data, 0, data.length); - output.write(data, 0, data.length); - return crc.value; - } - const buffer: Uint8Array = new Uint8Array(512); - - // init deflater - this._deflater.reset(); - - // write data - this._deflater.setInput(data, 0, data.length); - while (!this._deflater.isNeedingInput) { - const len = this._deflater.deflate(buffer, 0, buffer.length); - if (len <= 0) { - break; - } - - output.write(buffer, 0, len); - } - - // let deflater finish up - this._deflater.finish(); - while (!this._deflater.isFinished) { - const len = this._deflater.deflate(buffer, 0, buffer.length); - if (len <= 0) { - break; - } - - output.write(buffer, 0, len); - } - - return this._deflater.inputCrc; - } - public end() { const startOfCentralDirectory = this._data.bytesWritten; for (const header of this._centralDirectoryHeaders) { diff --git a/packages/alphatab/test-data/audio/Example_alphaTab.pcm b/packages/alphatab/test-data/audio/Example_alphaTab.pcm deleted file mode 100644 index 66862ae63c..0000000000 Binary files a/packages/alphatab/test-data/audio/Example_alphaTab.pcm and /dev/null differ diff --git a/packages/alphatab/test-data/audio/Example_libvorbis.pcm b/packages/alphatab/test-data/audio/Example_libvorbis.pcm new file mode 100644 index 0000000000..bd752f1a00 Binary files /dev/null and b/packages/alphatab/test-data/audio/Example_libvorbis.pcm differ diff --git a/packages/alphatab/test-data/audio/Short-paged.ogg b/packages/alphatab/test-data/audio/Short-paged.ogg new file mode 100644 index 0000000000..e60e8d9c65 Binary files /dev/null and b/packages/alphatab/test-data/audio/Short-paged.ogg differ diff --git a/packages/alphatab/test-data/audio/Short_alphaTab.pcm b/packages/alphatab/test-data/audio/Short_alphaTab.pcm deleted file mode 100644 index b9673cac23..0000000000 Binary files a/packages/alphatab/test-data/audio/Short_alphaTab.pcm and /dev/null differ diff --git a/packages/alphatab/test-data/audio/Short_libvorbis.pcm b/packages/alphatab/test-data/audio/Short_libvorbis.pcm new file mode 100644 index 0000000000..5a212d0308 Binary files /dev/null and b/packages/alphatab/test-data/audio/Short_libvorbis.pcm differ diff --git a/packages/alphatab/test-data/audio/default-sf3_libvorbis.pcm b/packages/alphatab/test-data/audio/default-sf3_libvorbis.pcm new file mode 100644 index 0000000000..79935b1ec7 Binary files /dev/null and b/packages/alphatab/test-data/audio/default-sf3_libvorbis.pcm differ diff --git a/packages/alphatab/test/audio/AlphaSynth.test.ts b/packages/alphatab/test/audio/AlphaSynth.test.ts index 82d2ad48d8..346ce581a3 100644 --- a/packages/alphatab/test/audio/AlphaSynth.test.ts +++ b/packages/alphatab/test/audio/AlphaSynth.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { FormatError } from '@coderline/alphatab/FormatError'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; @@ -15,9 +16,11 @@ import type { Score } from '@coderline/alphatab/model/Score'; import { Settings } from '@coderline/alphatab/Settings'; import { AlphaSynth } from '@coderline/alphatab/synth/AlphaSynth'; import { AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter'; +import { Hydra } from '@coderline/alphatab/synth/soundfont/Hydra'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont'; -import { VorbisFile } from '@coderline/alphatab/synth/vorbis/VorbisFile'; +import { Mdct } from '@coderline/alphatab/synth/vorbis/Mdct'; +import { VorbisDecoder } from '@coderline/alphatab/synth/vorbis/VorbisDecoder'; import { TestOutput } from 'test/audio/TestOutput'; import { TestPlatform } from 'test/TestPlatform'; @@ -107,33 +110,32 @@ describe('AlphaSynthTests', () => { expect(synth.hasSamplesForPercussion(SynthConstants.MetronomeKey)).toBe(true); }); - async function testVorbisFile(name: string) { - const data = await TestPlatform.loadFile(`test-data/audio/${name}.ogg`); - const vorbis = new VorbisFile(ByteBuffer.fromBuffer(data)); - - expect(vorbis.streams.length).toBe(1); - expect(vorbis.streams[0].audioChannels).toBe(2); - expect(vorbis.streams[0].audioSampleRate).toBe(44100); - expect(vorbis.streams[0].samples.length).toBeGreaterThan(44100 * 0.05); - - const generated = vorbis.streams[0].samples; - const reference = new DataView((await TestPlatform.loadFile(`test-data/audio/${name}_alphaTab.pcm`)).buffer); + /** + * Compares decoded samples with the output of libvorbis (libvorbisfile with float output). + */ + async function expectLibVorbisOutput(samples: Float32Array, name: string) { + const reference = new DataView((await TestPlatform.loadFile(`test-data/audio/${name}_libvorbis.pcm`)).buffer); try { - expect(generated.length).toBe(reference.buffer.byteLength / 4); - - for (let i = 0; i < generated.length; i++) { - expect(generated[i], `Difference at index ${i}`).toBe(reference.getFloat32(i * 4, true)); + expect(samples.length).toBe(reference.buffer.byteLength / 4); + let maxDifference = 0; + for (let i = 0; i < samples.length; i++) { + maxDifference = Math.max(maxDifference, Math.abs(samples[i] - reference.getFloat32(i * 4, true))); } + expect(maxDifference).toBeLessThan(1e-5); } catch (e) { - await TestPlatform.saveFile( - `test-data/audio/${name}_alphaTab_new.pcm`, - new Uint8Array(vorbis.streams[0].samples.buffer) - ); - + await TestPlatform.saveFile(`test-data/audio/${name}_alphaTab_new.pcm`, new Uint8Array(samples.buffer)); throw e; } } + async function testVorbisFile(name: string) { + const data = await TestPlatform.loadFile(`test-data/audio/${name}.ogg`); + const stream = new VorbisDecoder().decode(data, 0, data.length); + expect(stream.channels).toBe(2); + expect(stream.sampleRate).toBe(44100); + await expectLibVorbisOutput(stream.samples, name); + } + it('ogg-vorbis-short', async () => { await testVorbisFile('Short'); }); @@ -142,6 +144,100 @@ describe('AlphaSynthTests', () => { await testVorbisFile('Example'); }); + it('vorbis-sf3-samples', async () => { + // one sample per block size pair: 512/1024, 512/512 and 256/2048 + const hydra = new Hydra(); + hydra.load(ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/audio/default.sf3'))); + const parts: Float32Array[] = []; + let length = 0; + for (const name of ['001', '032', '159']) { + const header = hydra.sHdrs.find(h => h.sampleName === name)!; + const samples = hydra.decodeSamples(header.start, header.end, true); + parts.push(samples); + length += samples.length; + } + const all = new Float32Array(length); + let offset = 0; + for (const part of parts) { + all.set(part, offset); + offset += part.length; + } + await expectLibVorbisOutput(all, 'default-sf3'); + }); + + it('vorbis-packets-spanning-pages', async () => { + // the same stream with every packet split across pages + const data = await TestPlatform.loadFile('test-data/audio/Short.ogg'); + const paged = await TestPlatform.loadFile('test-data/audio/Short-paged.ogg'); + const expected = new VorbisDecoder().decode(data, 0, data.length).samples; + const actual = new VorbisDecoder().decode(paged, 0, paged.length).samples; + expect(actual.length).toBe(expected.length); + for (let i = 0; i < actual.length; i++) { + expect(actual[i], `Difference at index ${i}`).toBe(expected[i]); + } + }); + + it('vorbis-corrupt-input', async () => { + // mutated and truncated streams either decode or fail with a FormatError + const data = await TestPlatform.loadFile('test-data/audio/Short.ogg'); + const decoder = new VorbisDecoder(); + let seed = 1; + let formatErrors = 0; + for (let i = 0; i < 300; i++) { + const corrupt = new Uint8Array(data.length); + corrupt.set(data, 0); + seed = (seed * 75 + 74) % 65537; + let length = data.length; + if (i % 4 === 3) { + length = seed % data.length; + } else { + for (let j = 0; j <= i % 3; j++) { + const position = (seed * (j + 1)) % data.length; + corrupt[position] = (corrupt[position] + 1 + (seed % 255)) % 256; + } + } + try { + decoder.decode(corrupt, 0, length); + } catch (e) { + expect(e instanceof FormatError, `Mutation ${i}: ${e}`).toBe(true); + formatErrors++; + } + } + expect(formatErrors).toBeGreaterThan(0); + }); + + it('vorbis-imdct', () => { + // all Vorbis block sizes against the definition, angles as multiples of pi / (2n) from a table + let seed = 7; + for (let n = 64; n <= 8192; n *= 2) { + const half = n / 2; + const input = new Float32Array(half); + for (let k = 0; k < half; k++) { + seed = (seed * 75 + 74) % 65537; + input[k] = seed / 65537 - 0.5; + } + const cos = new Float32Array(4 * n); + for (let i = 0; i < 4 * n; i++) { + cos[i] = Math.cos((Math.PI * i) / (2 * n)); + } + + const output = new Float32Array(n); + new Mdct(n).inverse(input, output, new Float32Array(half)); + + let maxError = 0; + let maxValue = 0; + for (let i = 0; i < n; i++) { + let expected = 0; + for (let k = 0; k < half; k++) { + expected += input[k] * cos[((2 * i + 1 + half) * (2 * k + 1)) % (4 * n)]; + } + maxError = Math.max(maxError, Math.abs(output[i] - expected)); + maxValue = Math.max(maxValue, Math.abs(expected)); + } + expect(maxError / maxValue, `n=${n}`).toBeLessThan(1e-5); + } + }); + async function testAudioExport( score: Score, fileName: string, diff --git a/packages/alphatab/test/zip/Deflate.test.ts b/packages/alphatab/test/zip/Deflate.test.ts new file mode 100644 index 0000000000..7f6355be74 --- /dev/null +++ b/packages/alphatab/test/zip/Deflate.test.ts @@ -0,0 +1,393 @@ +import { describe, expect, it } from 'vitest'; +import { FormatError } from '@coderline/alphatab/FormatError'; +import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { IOHelper } from '@coderline/alphatab/io/IOHelper'; +import { type IReadable, ThrowingReadable } from '@coderline/alphatab/io/IReadable'; +import { Crc32 } from '@coderline/alphatab/zip/Crc32'; +import { Deflater } from '@coderline/alphatab/zip/Deflater'; +import { Inflater } from '@coderline/alphatab/zip/Inflater'; + +const words = ['', 'alphaTab', ' ', 'Beat', '', 'Rhythm', '\n', 'Voice', '="1"', 'Bar']; + +/** + * Deterministic text-like test data (the same generator produced the zlib reference streams). + */ +function text(size: number, seed: number): Uint8Array { + const data = new Uint8Array(size); + let x = seed; + let pos = 0; + while (pos < size) { + x = (x * 75 + 74) % 65537; + const word = words[x % words.length]; + for (let i = 0; i < word.length && pos < size; i++) { + data[pos++] = word.charCodeAt(i); + } + } + return data; +} + +/** + * Deterministic data without repetitions longer than a few bytes. + */ +function noise(size: number, seed: number): Uint8Array { + const data = new Uint8Array(size); + let x = seed; + for (let i = 0; i < size; i++) { + x = (x * 75 + 74) % 65537; + data[i] = x & 0xff; + } + return data; +} + +/** + * Symbols with Fibonacci frequencies in shuffled order. + */ +function skewed(): Uint8Array { + const counts: number[] = [1, 1]; + while (counts.length < 25) { + counts.push(counts[counts.length - 1] + counts[counts.length - 2]); + } + let size = 0; + for (const c of counts) { + size += c; + } + const data = new Uint8Array(size); + let pos = 0; + for (let s = 0; s < counts.length; s++) { + for (let i = 0; i < counts[s]; i++) { + data[pos++] = s * 10; + } + } + let x = 7; + for (let i = size - 1; i > 0; i--) { + x = (x * 75 + 74) % 65537; + const j = (x * 31 + i) % (i + 1); + const t = data[i]; + data[i] = data[j]; + data[j] = t; + } + return data; +} + +function concat(parts: Uint8Array[]): Uint8Array { + let size = 0; + for (const p of parts) { + size += p.length; + } + const data = new Uint8Array(size); + let pos = 0; + for (const p of parts) { + data.set(p, pos); + pos += p.length; + } + return data; +} + +function crcOf(data: Uint8Array): number { + const crc = new Crc32(); + crc.update(data, 0, data.length); + return crc.value; +} + +function expectBytes(actual: Uint8Array | null, expected: Uint8Array) { + expect(actual).toBeTruthy(); + expect(actual!.length).toBe(expected.length); + let mismatch = -1; + for (let i = 0; i < expected.length; i++) { + if (actual![i] !== expected[i]) { + mismatch = i; + break; + } + } + expect(mismatch).toBe(-1); +} + +function inflate(input: IReadable, maxSize: number = 100000000): Uint8Array | null { + return new Inflater(input).inflate(0, maxSize); +} + +describe('Crc32Tests', () => { + function crc(data: Uint8Array): number { + const c = new Crc32(); + c.update(data, 0, data.length); + return c.value; + } + + // check values of CRC-32/ISO-HDLC (as used by ZIP), as signed 32-bit integers + it('empty', () => { + expect(crc(new Uint8Array(0))).toBe(0); + }); + + it('check-value', () => { + // 0xCBF43926 + expect(crc(IOHelper.stringToBytes('123456789'))).toBe(-873187034); + }); + + it('high-bytes', () => { + const data = new Uint8Array(256); + for (let i = 0; i < data.length; i++) { + data[i] = 255 - i; + } + // 0xDA3BA10A + expect(crc(data)).toBe(-633626358); + }); + + it('incremental', () => { + const data = IOHelper.stringToBytes('123456789'); + const c = new Crc32(); + c.update(data, 0, 4); + c.update(data, 4, 5); + expect(c.value).toBe(-873187034); + c.reset(); + c.update(data, 0, data.length); + expect(c.value).toBe(-873187034); + }); +}); + +describe('InflaterTests', () => { + // zlib level 0 strategy 0 of text(200, 1) + const storedStream = new Uint8Array([1, 200, 0, 55, 255, 66, 97, 114, 66, 97, 114, 82, 104, 121, 116, 104, 109, 60, 47, 78, 111, 116, 101, 62, 66, 101, 97, 116, 66, 97, 114, 66, 101, 97, 116, 66, 97, 114, 32, 97, 108, 112, 104, 97, 84, 97, 98, 32, 10, 32, 10, 61, 34, 49, 34, 60, 47, 78, 111, 116, 101, 62, 60, 78, 111, 116, 101, 62, 32, 60, 47, 78, 111, 116, 101, 62, 82, 104, 121, 116, 104, 109, 61, 34, 49, 34, 66, 101, 97, 116, 82, 104, 121, 116, 104, 109, 82, 104, 121, 116, 104, 109, 60, 47, 78, 111, 116, 101, 62, 32, 60, 47, 78, 111, 116, 101, 62, 60, 47, 78, 111, 116, 101, 62, 66, 97, 114, 66, 101, 97, 116, 32, 86, 111, 105, 99, 101, 60, 78, 111, 116, 101, 62, 97, 108, 112, 104, 97, 84, 97, 98, 61, 34, 49, 34, 32, 61, 34, 49, 34, 10, 66, 101, 97, 116, 66, 97, 114, 32, 60, 78, 111, 116, 101, 62, 86, 111, 105, 99, 101, 60, 78, 111, 116, 101, 62, 97, 108, 112, 104, 97, 84, 97, 98, 97, 108, 112, 104, 97, 84, 97, 98, 66, 101, 97]); + // zlib level 6 strategy 4 of text(400, 1) + const fixedStream = new Uint8Array([115, 74, 44, 114, 74, 44, 10, 202, 168, 44, 201, 200, 181, 209, 247, 203, 47, 73, 181, 115, 74, 77, 44, 113, 74, 44, 130, 82, 10, 137, 57, 5, 25, 137, 33, 137, 73, 10, 92, 10, 92, 182, 74, 134, 74, 80, 85, 54, 96, 82, 1, 202, 131, 24, 0, 146, 6, 105, 131, 240, 80, 12, 133, 41, 132, 217, 1, 49, 95, 33, 44, 63, 51, 57, 21, 98, 20, 204, 30, 144, 33, 10, 32, 130, 11, 230, 2, 136, 60, 22, 165, 48, 26, 108, 20, 212, 100, 116, 57, 144, 73, 96, 173, 92, 78, 137, 69, 54, 72, 78, 135, 115, 193, 174, 134, 113, 20, 16, 62, 129, 10, 32, 217, 11, 243, 51, 178, 79, 192, 210, 48, 203, 80, 56, 96, 63, 216, 192, 131, 20, 170, 158, 75, 65, 1, 69, 149, 2, 216, 151, 32, 121, 176, 52, 0]); + // zlib level 9 strategy 0 of text(3000, 1) + const dynamicStream = new Uint8Array([117, 86, 49, 110, 28, 49, 12, 236, 249, 10, 194, 31, 56, 164, 87, 82, 220, 3, 92, 24, 129, 251, 117, 112, 192, 5, 136, 97, 195, 184, 38, 191, 207, 138, 34, 197, 25, 74, 41, 110, 111, 181, 146, 200, 33, 103, 72, 233, 122, 124, 93, 143, 175, 151, 251, 223, 199, 253, 189, 93, 158, 63, 30, 183, 31, 215, 219, 241, 56, 191, 249, 159, 30, 127, 62, 239, 199, 207, 227, 77, 69, 229, 251, 211, 183, 39, 95, 213, 236, 169, 62, 26, 6, 250, 116, 223, 54, 70, 100, 52, 22, 134, 143, 97, 95, 95, 63, 126, 255, 186, 13, 83, 225, 167, 27, 209, 254, 144, 64, 48, 230, 55, 75, 227, 223, 76, 185, 229, 58, 215, 45, 217, 86, 57, 77, 53, 128, 62, 135, 134, 58, 6, 154, 145, 248, 7, 240, 27, 49, 99, 36, 54, 29, 206, 104, 96, 49, 180, 153, 82, 95, 47, 170, 180, 74, 45, 202, 62, 15, 238, 236, 113, 98, 202, 220, 83, 74, 33, 229, 19, 56, 65, 16, 69, 46, 217, 129, 193, 138, 213, 68, 230, 76, 85, 62, 164, 208, 54, 24, 177, 41, 93, 50, 60, 233, 56, 57, 11, 41, 168, 164, 107, 6, 202, 2, 114, 18, 144, 107, 237, 177, 145, 251, 134, 218, 233, 191, 177, 110, 205, 202, 120, 106, 64, 53, 221, 2, 29, 186, 77, 143, 243, 227, 169, 214, 224, 64, 196, 66, 72, 69, 48, 38, 147, 107, 215, 86, 46, 64, 60, 62, 129, 92, 130, 160, 224, 51, 110, 45, 236, 0, 29, 146, 239, 89, 73, 2, 102, 136, 136, 141, 190, 135, 131, 97, 102, 188, 215, 87, 99, 109, 181, 128, 196, 64, 27, 112, 166, 206, 61, 195, 102, 100, 58, 185, 243, 116, 146, 123, 193, 228, 165, 64, 166, 5, 138, 166, 173, 117, 6, 1, 155, 9, 114, 117, 18, 166, 209, 95, 212, 177, 248, 24, 131, 152, 206, 122, 6, 21, 195, 105, 165, 165, 133, 150, 155, 11, 164, 235, 116, 233, 152, 84, 183, 185, 87, 150, 42, 161, 38, 146, 102, 163, 104, 6, 100, 143, 4, 240, 202, 212, 106, 136, 58, 94, 166, 176, 84, 35, 215, 151, 98, 124, 46, 65, 105, 98, 152, 202, 181, 207, 163, 168, 12, 172, 45, 71, 34, 165, 235, 178, 187, 138, 176, 113, 182, 88, 39, 131, 165, 25, 166, 98, 115, 159, 91, 144, 166, 158, 49, 129, 253, 93, 131, 179, 155, 130, 170, 130, 190, 255, 86, 149, 165, 189, 246, 125, 176, 138, 138, 76, 114, 136, 93, 156, 142, 168, 185, 150, 39, 100, 208, 46, 180, 29, 40, 192, 181, 137, 232, 121, 246, 114, 1, 85, 191, 88, 146, 1, 185, 86, 140, 83, 198, 176, 98, 49, 38, 40, 254, 37, 38, 50, 85, 92, 152, 151, 232, 166, 120, 116, 82, 147, 78, 110, 91, 73, 122, 9, 132, 14, 88, 4, 19, 0, 43, 63, 84, 128, 212, 16, 59, 251, 171, 206, 41, 150, 51, 161, 187, 72, 138, 152, 77, 96, 53, 139, 43, 69, 217, 135, 137, 75, 118, 155, 61, 3, 174, 74, 172, 4, 19, 148, 59, 84, 77, 172, 92, 66, 151, 60, 197, 167, 120, 119, 167, 36, 53, 158, 86, 238, 94, 124, 158, 18, 79, 0, 24, 241, 178, 106, 176, 34, 242, 30, 146, 249, 223, 8, 28, 168, 157, 161, 69, 151, 219, 221, 49, 240, 20, 86, 199, 21, 216, 208, 125, 58, 22, 252, 12, 211, 177, 19, 74, 172, 52, 202, 173, 74, 33, 177, 51, 217, 188, 109, 87, 224, 118, 19, 29, 161, 150, 184, 116, 105, 168, 123, 103, 203, 117, 124, 142, 97, 83, 251, 7]); + // zlib level 6 strategy 3 of text(400, 1) + const rleStream = new Uint8Array([5, 193, 49, 10, 133, 48, 16, 5, 192, 222, 83, 132, 92, 32, 216, 175, 91, 228, 0, 91, 136, 216, 63, 229, 193, 10, 145, 13, 178, 205, 191, 253, 159, 233, 248, 58, 190, 221, 127, 233, 175, 52, 139, 164, 118, 34, 59, 190, 78, 100, 199, 87, 48, 166, 227, 192, 85, 150, 178, 108, 117, 173, 210, 44, 146, 42, 22, 73, 45, 210, 44, 146, 186, 251, 47, 253, 221, 234, 90, 59, 145, 187, 255, 210, 223, 221, 127, 233, 175, 52, 139, 164, 22, 105, 22, 73, 149, 102, 145, 212, 142, 175, 19, 89, 206, 120, 110, 138, 69, 82, 49, 166, 227, 192, 181, 213, 181, 150, 173, 174, 117, 233, 68, 118, 124, 69, 44, 146, 122, 198, 115, 83, 44, 146, 138, 49, 29, 7, 46, 140, 233, 56, 112, 117, 34, 139, 52, 139, 164, 98, 76, 199, 129, 11, 99, 58, 14, 92, 91, 93, 235, 25, 207, 205, 165, 227, 19, 139, 164, 138, 69, 82, 59, 62, 177, 72, 234, 86, 215, 218, 241, 137, 69, 82, 203, 238, 191, 244, 119, 171, 107, 21, 139, 164, 150, 51, 158, 155, 98, 145, 84, 177, 72, 106, 17, 139, 164, 74, 179, 72, 234, 25, 207, 77, 140, 233, 56, 112, 157, 241, 220, 196, 152, 142, 3, 215, 86, 215, 186, 136, 69, 82, 59, 145, 210, 44, 146, 186, 148, 114, 198, 115, 19, 99, 58, 14, 92, 165, 19, 217, 137, 20, 139, 164, 254, 1]); + // zlib level 6 strategy 2 of text(400, 1) + const huffmanOnlyStream = new Uint8Array([5, 193, 49, 10, 133, 48, 16, 5, 192, 222, 83, 132, 92, 32, 216, 175, 91, 228, 0, 91, 136, 216, 63, 229, 193, 10, 145, 13, 178, 205, 191, 253, 159, 233, 248, 58, 190, 221, 127, 233, 175, 52, 139, 164, 118, 34, 59, 190, 78, 100, 199, 87, 48, 166, 227, 192, 85, 150, 178, 108, 117, 173, 210, 44, 146, 42, 22, 73, 45, 210, 44, 146, 186, 251, 47, 253, 221, 234, 90, 59, 145, 187, 255, 210, 223, 221, 127, 233, 175, 52, 139, 164, 22, 105, 22, 73, 149, 102, 145, 212, 142, 175, 19, 89, 206, 120, 110, 138, 69, 82, 49, 166, 227, 192, 181, 213, 181, 150, 173, 174, 117, 233, 68, 118, 124, 69, 44, 146, 122, 198, 115, 83, 44, 146, 138, 49, 29, 7, 46, 140, 233, 56, 112, 117, 34, 139, 52, 139, 164, 98, 76, 199, 129, 11, 99, 58, 14, 92, 91, 93, 235, 25, 207, 205, 165, 227, 19, 139, 164, 138, 69, 82, 59, 62, 177, 72, 234, 86, 215, 218, 241, 137, 69, 82, 203, 238, 191, 244, 119, 171, 107, 21, 139, 164, 150, 51, 158, 155, 98, 145, 84, 177, 72, 106, 17, 139, 164, 74, 179, 72, 234, 25, 207, 77, 140, 233, 56, 112, 157, 241, 220, 196, 152, 142, 3, 215, 86, 215, 186, 136, 69, 82, 59, 145, 210, 44, 146, 186, 148, 114, 198, 115, 19, 99, 58, 14, 92, 165, 19, 217, 137, 20, 139, 164, 254, 1]); + // zlib with a sync flush after 200 bytes of text(400, 1) + const syncFlushStream = new Uint8Array([114, 74, 44, 114, 74, 44, 10, 202, 168, 44, 201, 200, 181, 209, 247, 203, 47, 73, 181, 115, 74, 77, 44, 113, 74, 44, 130, 82, 10, 137, 57, 5, 25, 137, 33, 137, 73, 10, 92, 10, 92, 182, 74, 134, 74, 80, 85, 54, 96, 82, 1, 202, 131, 24, 0, 146, 6, 105, 131, 240, 80, 12, 133, 41, 132, 217, 1, 49, 95, 33, 44, 63, 51, 57, 21, 98, 20, 204, 30, 144, 33, 10, 32, 130, 11, 230, 2, 136, 60, 22, 165, 48, 218, 41, 53, 17, 0, 0, 0, 255, 255, 43, 129, 89, 128, 46, 7, 50, 9, 172, 149, 203, 41, 177, 200, 6, 201, 233, 112, 46, 216, 213, 48, 142, 2, 194, 39, 80, 1, 36, 123, 97, 126, 70, 246, 9, 88, 26, 102, 25, 10, 7, 236, 7, 27, 120, 144, 66, 213, 115, 41, 40, 160, 168, 82, 0, 251, 18, 36, 15, 150, 6, 0]); + // dynamicStream block with a single distance code of length 1, decodes to b'abaabaaba' + const singleDistanceCodeStream = new Uint8Array([13, 194, 1, 9, 0, 0, 0, 128, 160, 173, 245, 127, 68, 129, 200, 22]); + // dynamicStream block without distance codes and max code length 2, decodes to b'xyyx' + const noDistanceCodesStream = new Uint8Array([5, 192, 1, 9, 0, 0, 0, 128, 160, 219, 245, 94, 250, 0]); + // dynamicStream block whose code length repeat crosses into the distance code lengths, decodes to b'pqpqqqqpqpqq' + const repeatAcrossBoundaryStream = new Uint8Array([29, 134, 183, 9, 0, 0, 0, 131, 94, 54, 255, 47, 166, 163, 132, 52, 12, 2]); + const invalidStreams: [string, Uint8Array][] = [ + ['blockType3', new Uint8Array([7, 0])], + ['storedLengthMismatch', new Uint8Array([1, 5, 0, 0, 0, 1, 2, 3, 4, 5])], + ['distanceTooFar', new Uint8Array([3, 2, 0])], + ['invalidLengthSymbol', new Uint8Array([75, 28, 3, 0])], + ['invalidDistanceSymbol', new Uint8Array([75, 4, 62, 0])], + ['oversubscribedCodeLengthCode', new Uint8Array([5, 224, 147, 36, 73, 146, 36, 73, 146, 0, 0, 0])], + ['tooManyLiteralCodes', new Uint8Array([245, 0, 0, 0, 0])], + ['repeatWithoutPrevious', new Uint8Array([5, 0, 2, 36, 0, 0])], + ['missingEndOfBlock', new Uint8Array([5, 192, 129, 0, 0, 0, 0, 0, 144, 86, 254, 43, 0, 0, 0])], + ['incompleteLiteralCode', new Uint8Array([5, 128, 1, 5, 0, 0, 0, 128, 182, 246, 255, 68, 16])], + ['truncated', new Uint8Array([115, 74, 44, 114, 74, 44, 10, 202, 168, 44, 201, 200, 181, 209, 247, 203, 47, 73, 181, 115, 74, 77, 44, 113, 74, 44, 130, 82, 10, 137, 57, 5, 25, 137, 33, 137, 73, 10, 92, 10, 92, 182, 74, 134, 74, 80, 85, 54, 96, 82, 1, 202, 131, 24, 0, 146, 6, 105, 131, 240, 80, 12, 133, 41, 132, 217, 1, 49, 95, 33, 44, 63, 51, 57, 21, 98, 20, 204, 30, 144, 33, 10, 32, 130, 11, 230, 2, 136, 60, 22, 165, 48, 26, 108, 20, 212, 100, 116, 57, 144, 73, 96, 173, 92, 78, 137, 69, 54, 72, 78, 135, 115, 193, 174, 134, 113, 20, 16, 62, 129, 10, 32, 217, 11, 243, 51, 178, 79, 192, 210, 48, 203, 80, 56, 96, 63, 216, 192, 131, 20, 170, 158, 75, 65, 1, 69, 149, 2, 216, 151, 32, 121])], + ]; + + it('zlib-streams', () => { + expectBytes(inflate(ByteBuffer.fromBuffer(storedStream)), text(200, 1)); + expectBytes(inflate(ByteBuffer.fromBuffer(fixedStream)), text(400, 1)); + expectBytes(inflate(ByteBuffer.fromBuffer(dynamicStream)), text(3000, 1)); + expectBytes(inflate(ByteBuffer.fromBuffer(rleStream)), text(400, 1)); + expectBytes(inflate(ByteBuffer.fromBuffer(huffmanOnlyStream)), text(400, 1)); + }); + + it('sync-flush', () => { + expectBytes(inflate(ByteBuffer.fromBuffer(syncFlushStream)), text(400, 1)); + }); + + it('single-distance-code', () => { + expectBytes(inflate(ByteBuffer.fromBuffer(singleDistanceCodeStream)), IOHelper.stringToBytes('abaabaaba')); + }); + + it('no-distance-codes', () => { + expectBytes(inflate(ByteBuffer.fromBuffer(noDistanceCodesStream)), IOHelper.stringToBytes('xyyx')); + }); + + it('repeat-across-boundary', () => { + expectBytes(inflate(ByteBuffer.fromBuffer(repeatAcrossBoundaryStream)), IOHelper.stringToBytes('pqpqqqqpqpqq')); + }); + + it('size-hint', () => { + const expected = text(3000, 1); + // exact, too small and too large hints all give the same data + expectBytes(new Inflater(ByteBuffer.fromBuffer(dynamicStream)).inflate(3000, 100000), expected); + expectBytes(new Inflater(ByteBuffer.fromBuffer(dynamicStream)).inflate(10, 100000), expected); + expectBytes(new Inflater(ByteBuffer.fromBuffer(dynamicStream)).inflate(5000, 100000), expected); + }); + + it('max-size', () => { + expect(inflate(ByteBuffer.fromBuffer(dynamicStream), 2999)).toBeNull(); + expect(inflate(ByteBuffer.fromBuffer(storedStream), 199)).toBeNull(); + expectBytes(inflate(ByteBuffer.fromBuffer(dynamicStream), 3000), text(3000, 1)); + }); + + it('stops-at-stream-end', () => { + for (const stream of [storedStream, fixedStream, dynamicStream, syncFlushStream, singleDistanceCodeStream]) { + // the inflater must not consume any of the bytes following the stream + const data = new Uint8Array(stream.length + 3); + data.set(stream, 0); + data[stream.length] = 0xaa; + data[stream.length + 1] = 0xbb; + data[stream.length + 2] = 0xcc; + + const buffer = ByteBuffer.fromBuffer(data); + expect(inflate(buffer)).toBeTruthy(); + expect(buffer.position).toBe(stream.length); + + // also when the stream ends with the data + const throwing = new ThrowingReadable(ByteBuffer.fromBuffer(stream)); + expect(inflate(throwing)).toBeTruthy(); + expect(throwing.position).toBe(stream.length); + } + }); + + it('reuse', () => { + const data = new Uint8Array(fixedStream.length + dynamicStream.length + fixedStream.length); + data.set(fixedStream, 0); + data.set(dynamicStream, fixedStream.length); + data.set(fixedStream, fixedStream.length + dynamicStream.length); + const inflater = new Inflater(ByteBuffer.fromBuffer(data)); + expectBytes(inflater.inflate(0, 100000), text(400, 1)); + expectBytes(inflater.inflate(0, 100000), text(3000, 1)); + expectBytes(inflater.inflate(0, 100000), text(400, 1)); + }); + + it('invalid-data', () => { + for (const invalid of invalidStreams) { + const stream = invalid[1]; + expect(() => { + inflate(ByteBuffer.fromBuffer(stream)); + }, invalid[0]).toThrow(FormatError); + } + }); +}); + +describe('DeflaterTests', () => { + function roundTrip(data: Uint8Array): Uint8Array { + const compressed = new Deflater().deflate(data); + expectBytes(inflate(ByteBuffer.fromBuffer(compressed)), data); + return compressed; + } + + it('empty', () => { + const compressed = roundTrip(new Uint8Array(0)); + // a final fixed block with only the end-of-block code + expectBytes(compressed, new Uint8Array([3, 0])); + }); + + it('single-byte', () => { + roundTrip(new Uint8Array([42])); + }); + + it('two-bytes', () => { + roundTrip(new Uint8Array([1, 200])); + }); + + it('text', () => { + const data = text(200000, 3); + const compressed = roundTrip(data); + expect(compressed.length).toBeLessThan(data.length / 5); + }); + + it('long-runs', () => { + // overlapping matches with distance 1 and maximum length, blocks limited by their input size + const compressed = roundTrip(concat([new Uint8Array(100000), new Uint8Array([1, 2, 3]), new Uint8Array(70000)])); + expect(compressed.length).toBeLessThan(1000); + }); + + it('far-repeat', () => { + // the second copy matches at distance 32767, the maximum distance used + const part = noise(32767, 5); + const data = concat([part, part]); + const compressed = roundTrip(data); + expect(compressed.length).toBeLessThan(part.length + 2000); + }); + + it('incompressible', () => { + // stored blocks + const data = noise(150000, 9); + const compressed = roundTrip(data); + expect(compressed.length).toBeLessThan(data.length + 100); + }); + + it('skewed-frequencies', () => { + roundTrip(skewed()); + }); + + it('mixed', () => { + roundTrip(concat([text(50000, 1), noise(70000, 2), new Uint8Array(1000), text(20000, 4), noise(10, 3)])); + }); + + it('reuse', () => { + const deflater = new Deflater(); + const first = deflater.deflate(text(30000, 1)); + deflater.deflate(noise(70000, 2)); + const again = deflater.deflate(text(30000, 1)); + expectBytes(again, first); + }); + + function fibonacci(count: number): Int32Array { + const frequencies = new Int32Array(count); + frequencies[0] = 1; + frequencies[1] = 1; + for (let i = 2; i < count; i++) { + frequencies[i] = frequencies[i - 1] + frequencies[i - 2]; + } + return frequencies; + } + + function checkCodeLengths(frequencies: Int32Array, limit: number): Uint8Array { + const lengths = new Uint8Array(frequencies.length); + Deflater.buildCodeLengths(frequencies, lengths, limit); + let kraft = 0; + for (let i = 0; i < lengths.length; i++) { + expect(lengths[i]).toBeLessThanOrEqual(limit); + if (frequencies[i] > 0) { + expect(lengths[i]).toBeGreaterThan(0); + } + if (lengths[i] > 0) { + kraft += 1 << (15 - lengths[i]); + } + // more frequent symbols never get longer codes + for (let j = 0; j < lengths.length; j++) { + if (frequencies[i] > frequencies[j] && frequencies[j] > 0) { + expect(lengths[i]).toBeLessThanOrEqual(lengths[j]); + } + } + } + // a complete code + expect(kraft).toBe(32768); + return lengths; + } + + it('code-lengths-optimal', () => { + const lengths = checkCodeLengths(new Int32Array([1, 1, 2, 3, 0, 10]), 15); + const parts: string[] = []; + for (let i = 0; i < lengths.length; i++) { + parts.push(`${lengths[i]}`); + } + expect(parts.join(',')).toBe('4,4,3,2,0,1'); + }); + + it('code-lengths-limited', () => { + // the optimal code for Fibonacci frequencies is as deep as there are symbols + checkCodeLengths(fibonacci(30), 15); + checkCodeLengths(fibonacci(19), 7); + const lengths = checkCodeLengths(fibonacci(16), 15); + expect(lengths[0]).toBe(15); + }); + + it('code-lengths-minimum-two', () => { + const single = new Int32Array(30); + single[7] = 5; + const lengths = checkCodeLengths(single, 15); + expect(lengths[7]).toBe(1); + expect(lengths[0]).toBe(1); + checkCodeLengths(new Int32Array(30), 15); + }); + + it('golden', () => { + // the compressed data must be identical on all platforms + const cases: Uint8Array[] = [text(3000, 1), text(200000, 3), skewed(), noise(1000, 4)]; + const results: string[] = []; + for (const data of cases) { + const compressed = new Deflater().deflate(data); + results.push(`${compressed.length}:${crcOf(compressed)}`); + } + expect(results.join(',')).toBe('634:1518865721,32538:-661243832,76301:652498640,1005:957314967'); + }); +}); diff --git a/packages/alphatab/test/zip/DeflateInterop.test.ts b/packages/alphatab/test/zip/DeflateInterop.test.ts new file mode 100644 index 0000000000..9b7af3e253 --- /dev/null +++ b/packages/alphatab/test/zip/DeflateInterop.test.ts @@ -0,0 +1,88 @@ +/** @target web */ +import zlib from 'node:zlib'; +import { describe, expect, it } from 'vitest'; +import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { Deflater } from '@coderline/alphatab/zip/Deflater'; +import { Inflater } from '@coderline/alphatab/zip/Inflater'; +import { ZipReader } from '@coderline/alphatab/zip/ZipReader'; +import { TestPlatform } from 'test/TestPlatform'; + +/** + * Checks the DEFLATE implementation against zlib, the reference implementation: our compressed data must be + * accepted by zlib (which rejects incomplete and over-subscribed codes), and zlib's output in all its modes must + * be accepted by us. + */ +describe('DeflateInteropTests', () => { + async function corpus(): Promise { + const result: Uint8Array[] = []; + const folders = [ + 'test-data/guitarpro7', + 'test-data/guitarpro8', + 'test-data/musicxml-samples', + 'test-data/visual-tests/layout' + ]; + for (const folder of folders) { + for (const file of await TestPlatform.listDirectory(folder)) { + if (!file.endsWith('.gp') && !file.endsWith('.mxl')) { + continue; + } + const data = await TestPlatform.loadFile(`${folder}/${file}`); + for (const entry of new ZipReader(ByteBuffer.fromBuffer(data), 100000000).read()) { + result.push(entry.data); + } + } + } + + // synthetic data: long runs, incompressible data and a larger text + result.push(new Uint8Array(300000)); + const noise = new Uint8Array(200000); + let x = 1; + for (let i = 0; i < noise.length; i++) { + x = (x * 75 + 74) % 65537; + noise[i] = x & 0xff; + } + result.push(noise); + result.push(new TextEncoder().encode('C4\n'.repeat(20000))); + return result; + } + + function expectEqual(actual: Uint8Array, expected: Uint8Array) { + expect(actual.length).toBe(expected.length); + expect(Buffer.compare(Buffer.from(actual), Buffer.from(expected))).toBe(0); + } + + it('zlib-inflates-our-data', async () => { + const deflater = new Deflater(); + let compressedSize = 0; + let zlibSize = 0; + for (const data of await corpus()) { + const compressed = deflater.deflate(data); + expectEqual(zlib.inflateRawSync(compressed), data); + compressedSize += compressed.length; + zlibSize += zlib.deflateRawSync(data).length; + } + // within 2% of zlib's default level + expect(compressedSize).toBeLessThan(zlibSize * 1.02); + }); + + it('we-inflate-zlib-data', async () => { + const options: zlib.ZlibOptions[] = [ + { level: 0 }, + { level: 1 }, + { level: 6 }, + { level: 9 }, + { level: 9, memLevel: 1, windowBits: 9 }, + { strategy: zlib.constants.Z_FIXED }, + { strategy: zlib.constants.Z_RLE }, + { strategy: zlib.constants.Z_HUFFMAN_ONLY }, + { strategy: zlib.constants.Z_FILTERED } + ]; + for (const data of await corpus()) { + for (const option of options) { + const compressed = zlib.deflateRawSync(data, option); + const inflated = new Inflater(ByteBuffer.fromBuffer(compressed)).inflate(0, 100000000); + expectEqual(inflated!, data); + } + } + }); +});