@audio/wma-decode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +76 -0
- package/package.json +45 -0
- package/src/wma.wasm.cjs +0 -0
- package/wma-decode.d.ts +33 -0
- package/wma-decode.js +488 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
wma-decode - WMA audio decoder
|
|
2
|
+
Copyright (c) 2025 audiojs
|
|
3
|
+
|
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
|
5
|
+
under the terms of the GNU Lesser General Public License as published by
|
|
6
|
+
the Free Software Foundation; either version 2.1 of the License, or (at
|
|
7
|
+
your option) any later version.
|
|
8
|
+
|
|
9
|
+
This library is distributed in the hope that it will be useful, but
|
|
10
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
11
|
+
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
|
12
|
+
License for more details.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# wma-decode
|
|
2
|
+
|
|
3
|
+
Decode WMA audio to PCM float samples. ASF demuxer in pure JS, WMA decoding via [RockBox](https://www.rockbox.org/) fixed-point decoder compiled to WASM (70 KB).
|
|
4
|
+
|
|
5
|
+
Part of [audio-decode](https://github.com/audiojs/audio-decode).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm i @audio/wma-decode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import decode from '@audio/wma-decode'
|
|
17
|
+
|
|
18
|
+
let { channelData, sampleRate } = await decode(wmaBuffer)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Streaming
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { decoder } from '@audio/wma-decode'
|
|
25
|
+
|
|
26
|
+
let dec = await decoder()
|
|
27
|
+
let result = dec.decode(chunk)
|
|
28
|
+
dec.free()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### ASF demuxer only
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { demuxASF } from '@audio/wma-decode'
|
|
35
|
+
|
|
36
|
+
let { channels, sampleRate, bitRate, packets } = demuxASF(buffer)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `decode(src): Promise<AudioData>`
|
|
42
|
+
|
|
43
|
+
Whole-file decode. Accepts `Uint8Array` or `ArrayBuffer`.
|
|
44
|
+
|
|
45
|
+
### `decoder(): Promise<WMADecoder>`
|
|
46
|
+
|
|
47
|
+
Creates a decoder instance.
|
|
48
|
+
|
|
49
|
+
- **`dec.decode(data)`** — decode chunk, returns `{ channelData, sampleRate }`
|
|
50
|
+
- **`dec.flush()`** — flush (returns empty)
|
|
51
|
+
- **`dec.free()`** — release WASM memory
|
|
52
|
+
|
|
53
|
+
### `demuxASF(buf): ASFInfo`
|
|
54
|
+
|
|
55
|
+
Parse ASF container without decoding. Returns stream properties and raw packets.
|
|
56
|
+
|
|
57
|
+
## Formats
|
|
58
|
+
|
|
59
|
+
- WMA v1 (0x0160)
|
|
60
|
+
- WMA v2 (0x0161)
|
|
61
|
+
|
|
62
|
+
WMA Pro and Lossless are not supported. An FFmpeg-based build is available via `build-ffmpeg.sh` for those formats.
|
|
63
|
+
|
|
64
|
+
## Building WASM
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
npm run build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
RockBox source is included in `lib/rockbox-wma/` (3 files, 152 KB).
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
GPL-2.0+ (RockBox)
|
|
75
|
+
|
|
76
|
+
<a href="https://github.com/krishnized/license/">ॐ</a>
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/wma-decode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Decode WMA audio via RockBox WASM",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "wma-decode.js",
|
|
7
|
+
"types": "wma-decode.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./wma-decode.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"wma-decode.js",
|
|
14
|
+
"wma-decode.d.ts",
|
|
15
|
+
"src/wma.wasm.cjs",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bash build.sh",
|
|
20
|
+
"prepack": "npm run build",
|
|
21
|
+
"test": "node test.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"wma",
|
|
25
|
+
"windows-media-audio",
|
|
26
|
+
"asf",
|
|
27
|
+
"audio",
|
|
28
|
+
"decode",
|
|
29
|
+
"decoder",
|
|
30
|
+
"wasm",
|
|
31
|
+
"rockbox"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": { "access": "public" },
|
|
34
|
+
"license": "GPL-2.0-or-later",
|
|
35
|
+
"author": "audiojs",
|
|
36
|
+
"homepage": "https://github.com/audiojs/wma-decode#readme",
|
|
37
|
+
"bugs": "https://github.com/audiojs/wma-decode/issues",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/audiojs/wma-decode.git"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=16"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/wma.wasm.cjs
ADDED
|
Binary file
|
package/wma-decode.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface AudioData {
|
|
2
|
+
channelData: Float32Array[];
|
|
3
|
+
sampleRate: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface WmaDecoder {
|
|
7
|
+
decode(data: Uint8Array): AudioData;
|
|
8
|
+
flush(): AudioData;
|
|
9
|
+
free(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Decode WMA audio buffer to PCM samples */
|
|
13
|
+
export default function decode(src: ArrayBuffer | Uint8Array): Promise<AudioData>;
|
|
14
|
+
|
|
15
|
+
/** Create streaming decoder instance */
|
|
16
|
+
export function decoder(): Promise<WmaDecoder>;
|
|
17
|
+
|
|
18
|
+
/** Parse ASF data packet, extract compressed audio payloads */
|
|
19
|
+
export function parsePacket(pkt: Uint8Array, packetSize: number): Uint8Array[];
|
|
20
|
+
|
|
21
|
+
/** Parse ASF container, extract audio properties and raw packets */
|
|
22
|
+
export function demuxASF(buf: Uint8Array): {
|
|
23
|
+
channels: number;
|
|
24
|
+
sampleRate: number;
|
|
25
|
+
bitRate: number;
|
|
26
|
+
blockAlign: number;
|
|
27
|
+
bitsPerSample: number;
|
|
28
|
+
formatTag: number;
|
|
29
|
+
codecData: Uint8Array | null;
|
|
30
|
+
duration: number;
|
|
31
|
+
packetSize: number;
|
|
32
|
+
packets: Uint8Array[];
|
|
33
|
+
};
|
package/wma-decode.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WMA decoder — ASF demuxer (pure JS) + RockBox wmadec (WASM)
|
|
3
|
+
* Decodes WMA v1/v2 in ASF containers
|
|
4
|
+
*
|
|
5
|
+
* let { channelData, sampleRate } = await decode(wmabuf)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const EMPTY = Object.freeze({ channelData: [], sampleRate: 0 })
|
|
9
|
+
|
|
10
|
+
// ASF GUIDs (16 bytes each, little-endian)
|
|
11
|
+
const GUID = {
|
|
12
|
+
header: [0x30,0x26,0xb2,0x75,0x8e,0x66,0xcf,0x11,0xa6,0xd9,0x00,0xaa,0x00,0x62,0xce,0x6c],
|
|
13
|
+
fileProps: [0xa1,0xdc,0xab,0x8c,0x47,0xa9,0xcf,0x11,0x8e,0xe4,0x00,0xc0,0x0c,0x20,0x53,0x65],
|
|
14
|
+
streamProps: [0x91,0x07,0xdc,0xb7,0xb7,0xa9,0xcf,0x11,0x8e,0xe6,0x00,0xc0,0x0c,0x20,0x53,0x65],
|
|
15
|
+
audioMedia: [0x40,0x9e,0x69,0xf8,0x4d,0x5b,0xcf,0x11,0xa8,0xfd,0x00,0x80,0x5f,0x5c,0x44,0x2b],
|
|
16
|
+
data: [0x36,0x26,0xb2,0x75,0x8e,0x66,0xcf,0x11,0xa6,0xd9,0x00,0xaa,0x00,0x62,0xce,0x6c],
|
|
17
|
+
headerExt: [0xb5,0x03,0xbf,0x5f,0x2e,0xa9,0xcf,0x11,0x8e,0xe3,0x00,0xc0,0x0c,0x20,0x53,0x65],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function guidEq(buf, off, guid) {
|
|
21
|
+
for (let i = 0; i < 16; i++) if (buf[off + i] !== guid[i]) return false
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Little-endian readers
|
|
26
|
+
function u16(b, o) { return b[o] | (b[o + 1] << 8) }
|
|
27
|
+
function u32(b, o) { return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0 }
|
|
28
|
+
function u64(b, o) {
|
|
29
|
+
// JS safe up to 2^53; read low 32 + high 32
|
|
30
|
+
let lo = u32(b, o), hi = u32(b, o + 4)
|
|
31
|
+
return hi * 0x100000000 + lo
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse ASF container — extract audio stream properties + data packets
|
|
36
|
+
* @param {Uint8Array} buf
|
|
37
|
+
* @returns {{ channels, sampleRate, bitRate, blockAlign, bitsPerSample, formatTag, codecData, duration, packetSize, packets }}
|
|
38
|
+
*/
|
|
39
|
+
export function demuxASF(buf) {
|
|
40
|
+
if (!(buf instanceof Uint8Array)) buf = new Uint8Array(buf)
|
|
41
|
+
if (buf.length < 30) throw Error('Not an ASF/WMA file')
|
|
42
|
+
|
|
43
|
+
// Verify ASF Header Object GUID
|
|
44
|
+
if (!guidEq(buf, 0, GUID.header)) throw Error('Not an ASF/WMA file')
|
|
45
|
+
|
|
46
|
+
let headerSize = u64(buf, 16)
|
|
47
|
+
let numObjects = u32(buf, 24)
|
|
48
|
+
// reserved1(1) + reserved2(1) at offset 28-29
|
|
49
|
+
|
|
50
|
+
let audio = null // WAVEFORMATEX fields
|
|
51
|
+
let packetSize = 0
|
|
52
|
+
let duration = 0 // in seconds
|
|
53
|
+
let datOff = 0 // data object offset
|
|
54
|
+
let datSize = 0 // data object total size
|
|
55
|
+
let totalPackets = 0
|
|
56
|
+
|
|
57
|
+
// Parse header sub-objects
|
|
58
|
+
let pos = 30
|
|
59
|
+
let headerEnd = Math.min(Number(headerSize), buf.length)
|
|
60
|
+
for (let i = 0; i < numObjects && pos < headerEnd - 24; i++) {
|
|
61
|
+
let objSize = u64(buf, pos + 16)
|
|
62
|
+
if (objSize < 24) break
|
|
63
|
+
let objEnd = pos + Math.min(Number(objSize), headerEnd - pos)
|
|
64
|
+
|
|
65
|
+
if (guidEq(buf, pos, GUID.streamProps)) {
|
|
66
|
+
audio = parseStreamProps(buf, pos + 24, objEnd) || audio
|
|
67
|
+
} else if (guidEq(buf, pos, GUID.fileProps)) {
|
|
68
|
+
let fp = parseFileProps(buf, pos + 24, objEnd)
|
|
69
|
+
if (fp) { packetSize = fp.packetSize; duration = fp.duration }
|
|
70
|
+
} else if (guidEq(buf, pos, GUID.headerExt)) {
|
|
71
|
+
// Header Extension Object — nested sub-objects skipped (no metadata needed for decode)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pos = objEnd
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!audio) throw Error('No audio stream in ASF')
|
|
78
|
+
|
|
79
|
+
// Find Data Object after header
|
|
80
|
+
pos = Number(headerSize)
|
|
81
|
+
if (pos + 50 <= buf.length && guidEq(buf, pos, GUID.data)) {
|
|
82
|
+
datSize = u64(buf, pos + 16)
|
|
83
|
+
// fileId(16) + totalPackets(8) + reserved(2) = 26 bytes after object header
|
|
84
|
+
totalPackets = u64(buf, pos + 24 + 16)
|
|
85
|
+
datOff = pos + 24 + 26 // start of packet data
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract packets
|
|
89
|
+
let packets = []
|
|
90
|
+
if (datOff && packetSize > 0) {
|
|
91
|
+
let datEnd = Math.min(pos + Number(datSize), buf.length)
|
|
92
|
+
let ppos = datOff
|
|
93
|
+
while (ppos + packetSize <= datEnd) {
|
|
94
|
+
packets.push(buf.subarray(ppos, ppos + packetSize))
|
|
95
|
+
ppos += packetSize
|
|
96
|
+
}
|
|
97
|
+
} else if (datOff && totalPackets > 0) {
|
|
98
|
+
// Variable-size packets — fallback: treat remaining data as one blob
|
|
99
|
+
let datEnd = Math.min(pos + Number(datSize), buf.length)
|
|
100
|
+
if (datOff < datEnd) packets.push(buf.subarray(datOff, datEnd))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
channels: audio.channels,
|
|
105
|
+
sampleRate: audio.sampleRate,
|
|
106
|
+
bitRate: audio.avgBytesPerSec * 8,
|
|
107
|
+
blockAlign: audio.blockAlign,
|
|
108
|
+
bitsPerSample: audio.bitsPerSample,
|
|
109
|
+
formatTag: audio.formatTag,
|
|
110
|
+
codecData: audio.codecData,
|
|
111
|
+
duration,
|
|
112
|
+
packetSize,
|
|
113
|
+
packets
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse Stream Properties Object body
|
|
119
|
+
* Returns audio WAVEFORMATEX fields if this is an audio stream, null otherwise
|
|
120
|
+
*/
|
|
121
|
+
function parseStreamProps(buf, start, end) {
|
|
122
|
+
// Stream Type GUID(16) + Error Correction Type GUID(16) + Time Offset(8)
|
|
123
|
+
// + Type-Specific Data Length(4) + Error Correction Data Length(4)
|
|
124
|
+
// + Flags(2) + Reserved(4)
|
|
125
|
+
if (start + 54 > end) return null
|
|
126
|
+
|
|
127
|
+
// Check that stream type is Audio Media
|
|
128
|
+
if (!guidEq(buf, start, GUID.audioMedia)) return null
|
|
129
|
+
|
|
130
|
+
// flags(2) + reserved(4) = 6 bytes after type/ec data lengths
|
|
131
|
+
let waveOff = start + 54
|
|
132
|
+
|
|
133
|
+
if (waveOff + 18 > end) return null
|
|
134
|
+
|
|
135
|
+
// WAVEFORMATEX
|
|
136
|
+
let formatTag = u16(buf, waveOff)
|
|
137
|
+
let channels = u16(buf, waveOff + 2)
|
|
138
|
+
let sampleRate = u32(buf, waveOff + 4)
|
|
139
|
+
let avgBytesPerSec = u32(buf, waveOff + 8)
|
|
140
|
+
let blockAlign = u16(buf, waveOff + 12)
|
|
141
|
+
let bitsPerSample = u16(buf, waveOff + 14)
|
|
142
|
+
let cbSize = u16(buf, waveOff + 16)
|
|
143
|
+
|
|
144
|
+
// Codec-specific data follows WAVEFORMATEX
|
|
145
|
+
let codecData = null
|
|
146
|
+
if (cbSize > 0 && waveOff + 18 + cbSize <= end) {
|
|
147
|
+
codecData = buf.subarray(waveOff + 18, waveOff + 18 + cbSize)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { formatTag, channels, sampleRate, avgBytesPerSec, blockAlign, bitsPerSample, codecData }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse File Properties Object body
|
|
155
|
+
* Returns { packetSize, duration } or null
|
|
156
|
+
*/
|
|
157
|
+
function parseFileProps(buf, start, end) {
|
|
158
|
+
// File ID(16) + File Size(8) + Creation Date(8) + Data Packets Count(8)
|
|
159
|
+
// + Play Duration(8) + Send Duration(8) + Preroll(8)
|
|
160
|
+
// + Flags(4) + Min Data Packet Size(4) + Max Data Packet Size(4)
|
|
161
|
+
// + Max Bitrate(4)
|
|
162
|
+
if (start + 80 > end) return null
|
|
163
|
+
|
|
164
|
+
// Play Duration is 100-ns units (offset 40)
|
|
165
|
+
let playDur = u64(buf, start + 40)
|
|
166
|
+
// Preroll in ms (offset 56)
|
|
167
|
+
let preroll = u64(buf, start + 56)
|
|
168
|
+
// Duration in seconds
|
|
169
|
+
let duration = Math.max(0, playDur / 10000000 - preroll / 1000)
|
|
170
|
+
|
|
171
|
+
// Max packet size at offset 72 (min at 68, always equal for WMA)
|
|
172
|
+
let packetSize = u32(buf, start + 72)
|
|
173
|
+
|
|
174
|
+
return { packetSize, duration }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Parse ASF data packet — extract compressed audio payload(s)
|
|
179
|
+
* Returns array of payload buffers
|
|
180
|
+
*/
|
|
181
|
+
export function parsePacket(pkt, packetSize) {
|
|
182
|
+
if (!pkt || pkt.length < 3) return []
|
|
183
|
+
|
|
184
|
+
let pos = 0
|
|
185
|
+
|
|
186
|
+
// Error Correction — first byte flags
|
|
187
|
+
let ecFlags = pkt[pos++]
|
|
188
|
+
if (ecFlags & 0x80) {
|
|
189
|
+
// Error correction present
|
|
190
|
+
let ecLen = ecFlags & 0x0F
|
|
191
|
+
// Opaque data flag is bit 4
|
|
192
|
+
pos += ecLen
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (pos >= pkt.length) return []
|
|
196
|
+
|
|
197
|
+
// Payload Parsing Information
|
|
198
|
+
let ppFlags = pkt[pos++]
|
|
199
|
+
let lenFlags = pkt[pos++]
|
|
200
|
+
|
|
201
|
+
let multiplePayloads = !!(ppFlags & 0x01)
|
|
202
|
+
let seqType = (ppFlags >> 1) & 0x03
|
|
203
|
+
let padType = (ppFlags >> 3) & 0x03
|
|
204
|
+
let pktLenType = (ppFlags >> 5) & 0x03
|
|
205
|
+
|
|
206
|
+
let repType = lenFlags & 0x03
|
|
207
|
+
let offType = (lenFlags >> 2) & 0x03
|
|
208
|
+
let medNumType = (lenFlags >> 4) & 0x03
|
|
209
|
+
|
|
210
|
+
// Skip optional packet length field
|
|
211
|
+
if (pktLenType === 1) pos += 1
|
|
212
|
+
else if (pktLenType === 2) pos += 2
|
|
213
|
+
else if (pktLenType === 3) pos += 4
|
|
214
|
+
|
|
215
|
+
// Sequence (skip)
|
|
216
|
+
if (seqType === 1) pos += 1
|
|
217
|
+
else if (seqType === 2) pos += 2
|
|
218
|
+
else if (seqType === 3) pos += 4
|
|
219
|
+
|
|
220
|
+
// Padding length
|
|
221
|
+
let padLen = 0
|
|
222
|
+
if (padType === 1) { padLen = pkt[pos++] }
|
|
223
|
+
else if (padType === 2) { padLen = u16(pkt, pos); pos += 2 }
|
|
224
|
+
else if (padType === 3) { padLen = u32(pkt, pos); pos += 4 }
|
|
225
|
+
|
|
226
|
+
// Send time (4 bytes) + duration (2 bytes)
|
|
227
|
+
pos += 6
|
|
228
|
+
|
|
229
|
+
if (pos >= pkt.length) return []
|
|
230
|
+
|
|
231
|
+
let payloads = []
|
|
232
|
+
|
|
233
|
+
if (!multiplePayloads) {
|
|
234
|
+
// Single payload — rest of packet minus padding
|
|
235
|
+
pos += 1 // stream number (always 1 byte)
|
|
236
|
+
|
|
237
|
+
// Media object number
|
|
238
|
+
pos += fieldSize(medNumType)
|
|
239
|
+
|
|
240
|
+
// Offset into media object
|
|
241
|
+
pos += fieldSize(offType)
|
|
242
|
+
|
|
243
|
+
// Replicated data length
|
|
244
|
+
let repLen = readVarField(pkt, pos, repType)
|
|
245
|
+
pos += fieldSize(repType)
|
|
246
|
+
|
|
247
|
+
// Skip replicated data
|
|
248
|
+
if (repLen === 1) {
|
|
249
|
+
// Compressed payload: repLen==1 means compressed
|
|
250
|
+
pos += 1 // presentation time delta
|
|
251
|
+
} else {
|
|
252
|
+
pos += repLen
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let payloadLen = pkt.length - pos - padLen
|
|
256
|
+
if (payloadLen > 0 && pos + payloadLen <= pkt.length) {
|
|
257
|
+
payloads.push(pkt.subarray(pos, pos + payloadLen))
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// Multiple payloads
|
|
261
|
+
let payloadFlags = pkt[pos++]
|
|
262
|
+
let numPayloads = payloadFlags & 0x3F
|
|
263
|
+
let payLenType = (payloadFlags >> 6) & 0x03
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < numPayloads && pos < pkt.length; i++) {
|
|
266
|
+
// Stream number (1 byte, lower 7 = number, bit 7 = key frame)
|
|
267
|
+
pos += 1
|
|
268
|
+
|
|
269
|
+
// Media object number
|
|
270
|
+
pos += fieldSize(medNumType)
|
|
271
|
+
|
|
272
|
+
// Offset into media object
|
|
273
|
+
pos += fieldSize(offType)
|
|
274
|
+
|
|
275
|
+
// Replicated data
|
|
276
|
+
let repLen = readVarField(pkt, pos, repType)
|
|
277
|
+
pos += fieldSize(repType)
|
|
278
|
+
|
|
279
|
+
if (repLen === 1) {
|
|
280
|
+
pos += 1 // compressed: presentation time delta
|
|
281
|
+
} else {
|
|
282
|
+
pos += repLen
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Payload length
|
|
286
|
+
let payLen = 0
|
|
287
|
+
if (payLenType === 1) { payLen = pkt[pos++] }
|
|
288
|
+
else if (payLenType === 2) { payLen = u16(pkt, pos); pos += 2 }
|
|
289
|
+
else if (payLenType === 3) { payLen = u32(pkt, pos); pos += 4 }
|
|
290
|
+
else { payLen = pkt.length - pos }
|
|
291
|
+
|
|
292
|
+
if (payLen > 0 && pos + payLen <= pkt.length) {
|
|
293
|
+
payloads.push(pkt.subarray(pos, pos + payLen))
|
|
294
|
+
}
|
|
295
|
+
pos += payLen
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return payloads
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function fieldSize(type) {
|
|
303
|
+
return type === 0 ? 0 : type === 1 ? 1 : type === 2 ? 2 : 4
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function readVarField(buf, off, type) {
|
|
307
|
+
if (type === 0) return 0
|
|
308
|
+
if (type === 1) return buf[off] || 0
|
|
309
|
+
if (type === 2) return (off + 2 <= buf.length) ? u16(buf, off) : 0
|
|
310
|
+
return (off + 4 <= buf.length) ? u32(buf, off) : 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ===== WMA format tag names =====
|
|
314
|
+
|
|
315
|
+
const WMA_FORMATS = {
|
|
316
|
+
0x0160: 'WMAv1',
|
|
317
|
+
0x0161: 'WMAv2',
|
|
318
|
+
0x0162: 'WMAPro',
|
|
319
|
+
0x0163: 'WMALossless',
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ===== WASM module loader =====
|
|
323
|
+
|
|
324
|
+
let _modP
|
|
325
|
+
|
|
326
|
+
async function getMod() {
|
|
327
|
+
if (_modP) return _modP
|
|
328
|
+
let p = (async () => {
|
|
329
|
+
let createWMA
|
|
330
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
331
|
+
let m = 'module'
|
|
332
|
+
let { createRequire } = await import(m)
|
|
333
|
+
createWMA = createRequire(import.meta.url)('./src/wma.wasm.cjs')
|
|
334
|
+
} else {
|
|
335
|
+
let mod = await import('./src/wma.wasm.cjs')
|
|
336
|
+
createWMA = mod.default || mod
|
|
337
|
+
}
|
|
338
|
+
return createWMA()
|
|
339
|
+
})()
|
|
340
|
+
_modP = p
|
|
341
|
+
try { return await p }
|
|
342
|
+
catch (e) { _modP = null; throw e }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whole-file decode
|
|
347
|
+
* @param {Uint8Array|ArrayBuffer} src
|
|
348
|
+
* @returns {Promise<{channelData: Float32Array[], sampleRate: number}>}
|
|
349
|
+
*/
|
|
350
|
+
export default async function decode(src) {
|
|
351
|
+
let buf = src instanceof Uint8Array ? src : new Uint8Array(src)
|
|
352
|
+
let dec = await decoder()
|
|
353
|
+
try {
|
|
354
|
+
return dec.decode(buf)
|
|
355
|
+
} finally {
|
|
356
|
+
dec.free()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create decoder instance
|
|
362
|
+
* @returns {Promise<{decode(chunk: Uint8Array): {channelData, sampleRate}, flush(), free()}>}
|
|
363
|
+
*/
|
|
364
|
+
export async function decoder() {
|
|
365
|
+
return new WMADecoder(await getMod())
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
class WMADecoder {
|
|
369
|
+
constructor(mod) {
|
|
370
|
+
this.m = mod
|
|
371
|
+
this.h = null
|
|
372
|
+
this.sr = 0
|
|
373
|
+
this.ch = 0
|
|
374
|
+
this.done = false
|
|
375
|
+
this._ptr = 0
|
|
376
|
+
this._cap = 0
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
decode(data) {
|
|
380
|
+
if (this.done) throw Error('Decoder already freed')
|
|
381
|
+
if (!data?.length) return EMPTY
|
|
382
|
+
|
|
383
|
+
let buf = data instanceof Uint8Array ? data : new Uint8Array(data)
|
|
384
|
+
let asf = demuxASF(buf)
|
|
385
|
+
|
|
386
|
+
if (!asf.packets.length) return EMPTY
|
|
387
|
+
|
|
388
|
+
let fmt = WMA_FORMATS[asf.formatTag]
|
|
389
|
+
if (!fmt) throw Error('Unsupported WMA format tag: 0x' + asf.formatTag.toString(16))
|
|
390
|
+
|
|
391
|
+
this.sr = asf.sampleRate
|
|
392
|
+
this.ch = asf.channels
|
|
393
|
+
|
|
394
|
+
let m = this.m
|
|
395
|
+
|
|
396
|
+
// Init WASM decoder with audio properties
|
|
397
|
+
let extraPtr = 0, extraLen = 0
|
|
398
|
+
if (asf.codecData?.length) {
|
|
399
|
+
extraPtr = this._alloc(asf.codecData.length)
|
|
400
|
+
m.HEAPU8.set(asf.codecData, extraPtr)
|
|
401
|
+
extraLen = asf.codecData.length
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.h = m._wma_create(
|
|
405
|
+
asf.channels, asf.sampleRate, asf.bitRate,
|
|
406
|
+
asf.blockAlign, asf.formatTag, asf.bitsPerSample,
|
|
407
|
+
extraPtr, extraLen
|
|
408
|
+
)
|
|
409
|
+
if (!this.h) throw Error('WMA decoder init failed')
|
|
410
|
+
|
|
411
|
+
// Extract payloads from packets and decode
|
|
412
|
+
// Each payload is one blockAlign-sized WMA superframe
|
|
413
|
+
let chunks = []
|
|
414
|
+
let totalPerCh = 0
|
|
415
|
+
let channels = asf.channels
|
|
416
|
+
let errors = 0
|
|
417
|
+
let ba = asf.blockAlign
|
|
418
|
+
|
|
419
|
+
for (let pkt of asf.packets) {
|
|
420
|
+
let payloads = parsePacket(pkt, asf.packetSize)
|
|
421
|
+
for (let payload of payloads) {
|
|
422
|
+
// Split payload into blockAlign-sized frames
|
|
423
|
+
for (let off = 0; off + ba <= payload.length; off += ba) {
|
|
424
|
+
let frame = payload.subarray(off, off + ba)
|
|
425
|
+
let ptr = this._alloc(ba)
|
|
426
|
+
m.HEAPU8.set(frame, ptr)
|
|
427
|
+
let out = m._wma_decode(this.h, ptr, ba)
|
|
428
|
+
if (!out) { errors++; continue }
|
|
429
|
+
|
|
430
|
+
let n = m._wma_samples()
|
|
431
|
+
let sr = m._wma_samplerate()
|
|
432
|
+
if (sr) this.sr = sr
|
|
433
|
+
let ch = m._wma_channels()
|
|
434
|
+
if (ch) channels = ch
|
|
435
|
+
|
|
436
|
+
let spc = n / channels
|
|
437
|
+
let samples = new Float32Array(m.HEAPF32.buffer, out, n).slice()
|
|
438
|
+
chunks.push({ data: samples, ch: channels, spc })
|
|
439
|
+
totalPerCh += spc
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!totalPerCh) {
|
|
445
|
+
if (errors) throw Error(errors + ' frame(s) failed to decode')
|
|
446
|
+
return EMPTY
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// De-interleave
|
|
450
|
+
let channelData = Array.from({ length: channels }, () => new Float32Array(totalPerCh))
|
|
451
|
+
let pos = 0
|
|
452
|
+
for (let { data, ch, spc } of chunks) {
|
|
453
|
+
for (let c = 0; c < ch; c++) {
|
|
454
|
+
let out = channelData[c]
|
|
455
|
+
for (let s = 0; s < spc; s++) out[pos + s] = data[s * ch + c]
|
|
456
|
+
}
|
|
457
|
+
pos += spc
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { channelData, sampleRate: this.sr }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
flush() { return EMPTY }
|
|
464
|
+
|
|
465
|
+
free() {
|
|
466
|
+
if (this.done) return
|
|
467
|
+
this.done = true
|
|
468
|
+
if (this.h) {
|
|
469
|
+
this.m._wma_close(this.h)
|
|
470
|
+
this.m._wma_free_buf()
|
|
471
|
+
this.h = null
|
|
472
|
+
}
|
|
473
|
+
if (this._ptr) {
|
|
474
|
+
this.m._free(this._ptr)
|
|
475
|
+
this._ptr = 0
|
|
476
|
+
this._cap = 0
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
_alloc(len) {
|
|
481
|
+
if (len > this._cap) {
|
|
482
|
+
if (this._ptr) this.m._free(this._ptr)
|
|
483
|
+
this._cap = len
|
|
484
|
+
this._ptr = this.m._malloc(len)
|
|
485
|
+
}
|
|
486
|
+
return this._ptr
|
|
487
|
+
}
|
|
488
|
+
}
|