2024-07-09 01:53:02 +08:00
|
|
|
|
import { StringType } from 'token-types';
|
2024-08-08 10:19:14 +02:00
|
|
|
|
import type { IRatio } from '../type.js';
|
2024-08-28 17:37:22 +02:00
|
|
|
|
import { FieldDecodingError } from '../ParseError.js';
|
2025-09-15 18:24:19 +02:00
|
|
|
|
import { getUintBE } from 'uint8array-extras';
|
2013-09-04 12:10:50 -04:00
|
|
|
|
|
2021-07-13 17:13:32 +02:00
|
|
|
|
export type StringEncoding =
|
2024-07-09 01:53:02 +08:00
|
|
|
|
'ascii' // Use 'utf-8' or latin1 instead
|
2021-07-13 17:13:32 +02:00
|
|
|
|
| 'utf8' // alias: 'utf-8'
|
2024-07-09 01:53:02 +08:00
|
|
|
|
| 'utf-16le' // alias: 'ucs2', 'ucs-2'
|
|
|
|
|
|
| 'ucs2' // 'utf-16le'
|
2021-07-13 17:13:32 +02:00
|
|
|
|
| 'base64url'
|
|
|
|
|
|
| 'latin1' // Same as ISO-8859-1 (alias: 'binary')
|
|
|
|
|
|
| 'hex';
|
2017-07-08 11:17:43 +02:00
|
|
|
|
|
2021-07-12 21:21:52 +02:00
|
|
|
|
export function getBit(buf: Uint8Array, off: number, bit: number): boolean {
|
|
|
|
|
|
return (buf[off] & (1 << bit)) !== 0;
|
|
|
|
|
|
}
|
2018-09-02 21:36:01 +02:00
|
|
|
|
|
2021-07-11 16:58:53 +02:00
|
|
|
|
/**
|
2026-01-06 12:42:46 +01:00
|
|
|
|
* Find delimiting zero in uint8Array
|
2021-07-13 17:13:32 +02:00
|
|
|
|
* @param uint8Array Uint8Array to find the zero delimiter in
|
|
|
|
|
|
* @param encoding The string encoding used
|
2026-01-06 12:42:46 +01:00
|
|
|
|
* @return position in uint8Array where zero found, or uint8Array.length if not found
|
2021-07-11 16:58:53 +02:00
|
|
|
|
*/
|
2026-01-06 10:38:33 +01:00
|
|
|
|
export function findZero(uint8Array: Uint8Array, encoding?: StringEncoding): number {
|
2026-01-06 12:42:46 +01:00
|
|
|
|
const len = uint8Array.length;
|
|
|
|
|
|
|
2024-07-09 01:53:02 +08:00
|
|
|
|
if (encoding === 'utf-16le') {
|
2026-01-06 12:57:31 +01:00
|
|
|
|
// Look for 0x00 0x00 on 2-byte boundary
|
2026-01-06 12:42:46 +01:00
|
|
|
|
for (let i = 0; i + 1 < len; i += 2) {
|
|
|
|
|
|
if (uint8Array[i] === 0 && uint8Array[i + 1] === 0) return i;
|
2015-02-13 11:38:00 +00:00
|
|
|
|
}
|
2026-01-06 12:42:46 +01:00
|
|
|
|
return len;
|
2024-08-08 10:19:14 +02:00
|
|
|
|
}
|
2026-01-06 12:42:46 +01:00
|
|
|
|
|
|
|
|
|
|
// latin1 / utf8 / utf16be (caller typically handles utf16be separately or via decode)
|
|
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
|
|
if (uint8Array[i] === 0) return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
return len;
|
2021-07-11 16:58:53 +02:00
|
|
|
|
}
|
2016-11-27 15:35:11 +01:00
|
|
|
|
|
2021-07-11 16:58:53 +02:00
|
|
|
|
export function trimRightNull(x: string): string {
|
|
|
|
|
|
const pos0 = x.indexOf('\0');
|
2026-01-06 10:38:33 +01:00
|
|
|
|
return pos0 === -1 ? x : x.substring(0, pos0);
|
2021-07-11 16:58:53 +02:00
|
|
|
|
}
|
2013-09-04 12:10:50 -04:00
|
|
|
|
|
2021-07-13 17:13:32 +02:00
|
|
|
|
function swapBytes<T extends Uint8Array>(uint8Array: T): T {
|
2021-07-12 21:21:52 +02:00
|
|
|
|
const l = uint8Array.length;
|
2024-08-28 17:37:22 +02:00
|
|
|
|
if ((l & 1) !== 0) throw new FieldDecodingError('Buffer length must be even');
|
2021-07-11 16:58:53 +02:00
|
|
|
|
for (let i = 0; i < l; i += 2) {
|
2021-07-12 21:21:52 +02:00
|
|
|
|
const a = uint8Array[i];
|
|
|
|
|
|
uint8Array[i] = uint8Array[i + 1];
|
|
|
|
|
|
uint8Array[i + 1] = a;
|
2016-11-13 23:15:57 +01:00
|
|
|
|
}
|
2021-07-12 21:21:52 +02:00
|
|
|
|
return uint8Array;
|
2021-07-11 16:58:53 +02:00
|
|
|
|
}
|
2016-11-13 23:15:57 +01:00
|
|
|
|
|
2021-07-11 16:58:53 +02:00
|
|
|
|
/**
|
2022-01-17 15:39:09 +01:00
|
|
|
|
* Decode string
|
2021-07-11 16:58:53 +02:00
|
|
|
|
*/
|
2022-02-24 22:46:59 +01:00
|
|
|
|
export function decodeString(uint8Array: Uint8Array, encoding: StringEncoding): string {
|
2021-07-11 16:58:53 +02:00
|
|
|
|
// annoying workaround for a double BOM issue
|
|
|
|
|
|
// https://github.com/leetreveil/musicmetadata/issues/84
|
2022-02-24 22:46:59 +01:00
|
|
|
|
if (uint8Array[0] === 0xFF && uint8Array[1] === 0xFE) { // little endian
|
|
|
|
|
|
return decodeString(uint8Array.subarray(2), encoding);
|
2026-01-05 16:23:37 +01:00
|
|
|
|
}
|
|
|
|
|
|
if (encoding === 'utf-16le' && uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) {
|
2021-07-13 17:13:32 +02:00
|
|
|
|
// BOM, indicating big endian decoding
|
2022-02-24 22:46:59 +01:00
|
|
|
|
if ((uint8Array.length & 1) !== 0)
|
2024-08-28 17:37:22 +02:00
|
|
|
|
throw new FieldDecodingError('Expected even number of octets for 16-bit unicode string');
|
2022-02-24 22:46:59 +01:00
|
|
|
|
return decodeString(swapBytes(uint8Array), encoding);
|
2013-05-22 21:18:52 +01:00
|
|
|
|
}
|
2024-07-09 01:53:02 +08:00
|
|
|
|
return new StringType(uint8Array.length, encoding).get(uint8Array, 0);
|
2021-07-11 16:58:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function stripNulls(str: string): string {
|
|
|
|
|
|
str = str.replace(/^\x00+/g, '');
|
|
|
|
|
|
str = str.replace(/\x00+$/g, '');
|
|
|
|
|
|
return str;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Read bit-aligned number start from buffer
|
|
|
|
|
|
* Total offset in bits = byteOffset * 8 + bitOffset
|
2022-02-27 12:15:18 +01:00
|
|
|
|
* @param source Byte buffer
|
2021-07-11 16:58:53 +02:00
|
|
|
|
* @param byteOffset Starting offset in bytes
|
|
|
|
|
|
* @param bitOffset Starting offset in bits: 0 = lsb
|
|
|
|
|
|
* @param len Length of number in bits
|
2022-02-27 12:15:18 +01:00
|
|
|
|
* @return Decoded bit aligned number
|
2021-07-11 16:58:53 +02:00
|
|
|
|
*/
|
2022-02-27 12:15:18 +01:00
|
|
|
|
export function getBitAllignedNumber(source: Uint8Array, byteOffset: number, bitOffset: number, len: number): number {
|
2021-07-11 16:58:53 +02:00
|
|
|
|
const byteOff = byteOffset + ~~(bitOffset / 8);
|
|
|
|
|
|
const bitOff = bitOffset % 8;
|
2022-02-27 12:15:18 +01:00
|
|
|
|
let value = source[byteOff];
|
2021-07-11 16:58:53 +02:00
|
|
|
|
value &= 0xff >> bitOff;
|
|
|
|
|
|
const bitsRead = 8 - bitOff;
|
|
|
|
|
|
const bitsLeft = len - bitsRead;
|
|
|
|
|
|
if (bitsLeft < 0) {
|
|
|
|
|
|
value >>= (8 - bitOff - len);
|
|
|
|
|
|
} else if (bitsLeft > 0) {
|
|
|
|
|
|
value <<= bitsLeft;
|
2022-02-27 12:15:18 +01:00
|
|
|
|
value |= getBitAllignedNumber(source, byteOffset, bitOffset + bitsRead, bitsLeft);
|
2018-10-04 20:24:58 +02:00
|
|
|
|
}
|
2021-07-11 16:58:53 +02:00
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Read bit-aligned number start from buffer
|
|
|
|
|
|
* Total offset in bits = byteOffset * 8 + bitOffset
|
2022-02-27 12:15:18 +01:00
|
|
|
|
* @param source Byte Uint8Array
|
2021-07-11 16:58:53 +02:00
|
|
|
|
* @param byteOffset Starting offset in bytes
|
2022-02-27 12:15:18 +01:00
|
|
|
|
* @param bitOffset Starting offset in bits: 0 = most significant bit, 7 is the least significant bit
|
|
|
|
|
|
* @return True if bit is set
|
2021-07-11 16:58:53 +02:00
|
|
|
|
*/
|
2022-02-27 12:15:18 +01:00
|
|
|
|
export function isBitSet(source: Uint8Array, byteOffset: number, bitOffset: number): boolean {
|
|
|
|
|
|
return getBitAllignedNumber(source, byteOffset, bitOffset, 1) === 1;
|
2021-07-11 16:58:53 +02:00
|
|
|
|
}
|
2018-10-04 20:24:58 +02:00
|
|
|
|
|
2021-07-11 16:58:53 +02:00
|
|
|
|
export function a2hex(str: string) {
|
|
|
|
|
|
const arr = [];
|
|
|
|
|
|
for (let i = 0, l = str.length; i < l; i++) {
|
|
|
|
|
|
const hex = Number(str.charCodeAt(i)).toString(16);
|
2024-08-08 10:19:14 +02:00
|
|
|
|
arr.push(hex.length === 1 ? `0${hex}` : hex);
|
2021-07-11 16:58:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
return arr.join(' ');
|
2013-05-22 21:18:52 +01:00
|
|
|
|
}
|
2019-02-07 15:52:15 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Convert power ratio to DB
|
|
|
|
|
|
* ratio: [0..1]
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function ratioToDb(ratio: number): number {
|
|
|
|
|
|
return 10 * Math.log10(ratio);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Convert dB to ratio
|
|
|
|
|
|
* db Decibels
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function dbToRatio(dB: number): number {
|
2024-08-08 10:19:14 +02:00
|
|
|
|
return 10 ** (dB / 10);
|
2019-02-07 15:52:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Convert replay gain to ratio and Decibel
|
|
|
|
|
|
* @param value string holding a ratio like '0.034' or '-7.54 dB'
|
|
|
|
|
|
*/
|
2024-08-09 09:57:18 +02:00
|
|
|
|
export function toRatio(value: string): IRatio | undefined {
|
2019-02-07 15:52:15 +01:00
|
|
|
|
const ps = value.split(' ').map(p => p.trim().toLowerCase());
|
|
|
|
|
|
if (ps.length >= 1) {
|
2024-08-08 10:19:14 +02:00
|
|
|
|
const v = Number.parseFloat(ps[0]);
|
2022-01-28 14:15:02 +01:00
|
|
|
|
return ps.length === 2 && ps[1] === 'db' ? {
|
|
|
|
|
|
dB: v,
|
|
|
|
|
|
ratio: dbToRatio(v)
|
|
|
|
|
|
} : {
|
|
|
|
|
|
dB: ratioToDb(v),
|
|
|
|
|
|
ratio: v
|
|
|
|
|
|
};
|
2019-02-07 15:52:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-15 18:24:19 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Decode a big-endian unsigned integer from a Uint8Array.
|
|
|
|
|
|
* Supports dynamic length (1–8 bytes).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function decodeUintBE(uint8Array: Uint8Array): number {
|
|
|
|
|
|
if (uint8Array.length === 0) {
|
|
|
|
|
|
throw new Error("decodeUintBE: empty Uint8Array");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const view = new DataView(
|
|
|
|
|
|
uint8Array.buffer,
|
|
|
|
|
|
uint8Array.byteOffset,
|
|
|
|
|
|
uint8Array.byteLength
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return getUintBE(view);
|
|
|
|
|
|
}
|