carta1 1.1.8 → 1.1.10
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/README.md +149 -75
- package/codec/coding/bitallocation.js +93 -91
- package/codec/core/constants.js +19 -0
- package/codec/core/options.js +1 -1
- package/codec/index.js +3 -1
- package/codec/io/bitstream.js +1 -1
- package/codec/io/processor.js +113 -0
- package/dist/carta1-worker-interface.min.js +1 -1
- package/dist/carta1-worker-interface.min.js.map +1 -1
- package/dist/carta1-worker.min.js +1 -1
- package/dist/carta1-worker.min.js.map +1 -1
- package/dist/carta1.min.js +1 -1
- package/dist/carta1.min.js.map +1 -1
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -1,128 +1,202 @@
|
|
|
1
1
|
# Carta1
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Carta1 is an ATRAC1 encoder and decoder in JavaScript. It converts PCM WAVE
|
|
4
|
+
audio to Sony's AEA container, decodes AEA back to PCM WAVE, exposes stateful
|
|
5
|
+
frame and stream APIs, and ships browser Web Worker bundles.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## Supported format
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
| Property | Support |
|
|
10
|
+
| ------------------ | --------------------------------------------- |
|
|
11
|
+
| Sample rate | 44.1 kHz |
|
|
12
|
+
| Channels | Mono or stereo |
|
|
13
|
+
| Frame size | 512 PCM samples per channel |
|
|
14
|
+
| Encoded sound unit | 212 bytes per channel |
|
|
15
|
+
| Nominal bitrate | About 146 kbps per channel / 292 kbps stereo |
|
|
16
|
+
| Container | AEA |
|
|
17
|
+
| JavaScript PCM | Planar `Float32Array`, normalized `-1` to `1` |
|
|
18
|
+
|
|
19
|
+
Stereo channels are encoded independently and stored as interleaved AEA sound
|
|
20
|
+
units.
|
|
13
21
|
|
|
14
22
|
## Installation
|
|
15
23
|
|
|
24
|
+
Carta1 is an ES module and requires Node.js 20.16 or newer.
|
|
25
|
+
|
|
16
26
|
```bash
|
|
17
27
|
npm install carta1
|
|
18
28
|
```
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Encoding
|
|
30
|
+
To work from a repository checkout:
|
|
23
31
|
|
|
24
32
|
```bash
|
|
25
|
-
|
|
33
|
+
npm ci
|
|
34
|
+
npm run check
|
|
26
35
|
```
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
## CLI
|
|
38
|
+
|
|
39
|
+
Run the installed executable directly, or use `npx carta1` from a project that
|
|
40
|
+
depends on Carta1:
|
|
29
41
|
|
|
30
42
|
```bash
|
|
43
|
+
carta1 --encode input.wav output.aea
|
|
31
44
|
carta1 --decode input.aea output.wav
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Options
|
|
35
|
-
|
|
36
|
-
- `-e, --encode` - Encode WAV to AEA
|
|
37
|
-
- `-d, --decode` - Decode AEA to WAV
|
|
38
|
-
- `-j, --json` - Dump AEA file structure to JSON
|
|
39
|
-
- `-q, --quiet` - Suppress all output except errors
|
|
40
|
-
- `-f, --force` - Overwrite output file if it exists
|
|
41
|
-
- `-t, --title <title>` - Custom title for AEA file metadata (encoding only)
|
|
45
|
+
carta1 --json input.aea structure.json
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```html
|
|
46
|
-
<script src="https://unpkg.com/carta1/dist/carta1-worker.min.js"></script>
|
|
47
|
-
<script src="https://unpkg.com/carta1/dist/carta1-worker-interface.min.js"></script>
|
|
47
|
+
npx carta1 --encode input.wav output.aea
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
The encoder accepts mono or stereo PCM WAVE input. Carta1 is designed for
|
|
51
|
+
44.1 kHz audio; it warns when the input sample rate differs. Decoding writes
|
|
52
|
+
signed 16-bit PCM WAVE output.
|
|
53
|
+
|
|
54
|
+
| Option | Meaning |
|
|
55
|
+
| --------------------- | ---------------------------------------------------- |
|
|
56
|
+
| `-e, --encode` | Encode PCM WAVE to AEA. |
|
|
57
|
+
| `-d, --decode` | Decode AEA to PCM WAVE. |
|
|
58
|
+
| `-j, --json` | Write AEA metadata and frame structure as JSON. |
|
|
59
|
+
| `-q, --quiet` | Suppress normal output and progress. |
|
|
60
|
+
| `-f, --force` | Overwrite an existing output file. |
|
|
61
|
+
| `-t, --title <title>` | Set the AEA title while encoding. |
|
|
62
|
+
| `-b, --bias <value>` | Set the bit-allocation bias; defaults to `1.0`. |
|
|
63
|
+
| `-m, --modes <modes>` | Fix low, mid, and high block modes, such as `0,0,0`. |
|
|
64
|
+
| `-V, --version` | Print the Carta1 version. |
|
|
65
|
+
| `-h, --help` | Print command help. |
|
|
66
|
+
|
|
67
|
+
Exactly one of `--encode`, `--decode`, and `--json` is required. The input and
|
|
68
|
+
output are positional paths. Existing output is preserved unless `--force` is
|
|
69
|
+
provided.
|
|
70
|
+
|
|
71
|
+
## JavaScript API
|
|
72
|
+
|
|
73
|
+
### Complete AEA files
|
|
74
|
+
|
|
75
|
+
Use `encodeAeaPcm()` and `decodeAeaPcm()` when the complete input fits in
|
|
76
|
+
memory:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { decodeAeaPcm, encodeAeaPcm } from 'carta1'
|
|
80
|
+
|
|
81
|
+
const aea = await encodeAeaPcm([left, right], {
|
|
82
|
+
allocationBias: 1.0,
|
|
83
|
+
title: 'Example',
|
|
84
|
+
})
|
|
85
|
+
const [decodedLeft, decodedRight] = await decodeAeaPcm(aea)
|
|
86
|
+
```
|
|
53
87
|
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
`encodeAeaPcm()` returns a `Uint8Array` containing a complete AEA file.
|
|
89
|
+
`decodeAeaPcm()` accepts AEA bytes or a `Blob` and returns normalized planar
|
|
90
|
+
`Float32Array` channels. A mono input uses `[mono]`. The final partial input
|
|
91
|
+
frame is zero-padded.
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
const wavData = await codec.decode(aeaData)
|
|
93
|
+
### Stateful frames
|
|
59
94
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```
|
|
95
|
+
`encode()` and `decode()` create closures for chronological, complete mono
|
|
96
|
+
frames. Reuse each closure for one stream:
|
|
63
97
|
|
|
64
|
-
|
|
98
|
+
```js
|
|
99
|
+
import { decode, encode, EncoderOptions } from 'carta1'
|
|
65
100
|
|
|
66
|
-
|
|
101
|
+
const encodeFrame = encode(new EncoderOptions({ transientThresholdLow: 1.0 }))
|
|
102
|
+
const decodeFrame = decode()
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
const encoded = encodeFrame(new Float32Array(512))
|
|
105
|
+
const decoded = decodeFrame(encoded)
|
|
106
|
+
```
|
|
70
107
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
Use separate closures for separate streams and for the left and right channels
|
|
109
|
+
of stereo audio. `AudioProcessor.encodeStream()` and `decodeStream()` handle
|
|
110
|
+
that ownership automatically.
|
|
74
111
|
|
|
75
|
-
|
|
76
|
-
const decoder = decode()
|
|
77
|
-
const pcmFrame = decoder(encodedFrame)
|
|
78
|
-
```
|
|
112
|
+
### Encoder options
|
|
79
113
|
|
|
80
|
-
|
|
114
|
+
`EncoderOptions` validates these settings:
|
|
81
115
|
|
|
82
|
-
|
|
116
|
+
- `transientThresholdLow`, `transientThresholdMid`, and
|
|
117
|
+
`transientThresholdHigh` tune short-block detection per frequency band.
|
|
118
|
+
- `allocationBias` changes how strongly allocation favors louder spectral
|
|
119
|
+
components.
|
|
120
|
+
- `fixedBlockModes` accepts `[low, mid, high]`, where low and mid are `0` or
|
|
121
|
+
`2`, and high is `0` or `3`; this bypasses transient detection.
|
|
83
122
|
|
|
84
|
-
|
|
123
|
+
## Browser worker
|
|
85
124
|
|
|
86
|
-
|
|
125
|
+
The production build writes a worker and an ES module client to `dist/`. Serve
|
|
126
|
+
both from a location allowed by your application's worker and Content Security
|
|
127
|
+
Policy settings:
|
|
87
128
|
|
|
88
|
-
|
|
129
|
+
```js
|
|
130
|
+
import { Carta1Worker } from '/vendor/carta1-worker-interface.min.js'
|
|
89
131
|
|
|
90
|
-
|
|
132
|
+
const codec = new Carta1Worker('/vendor/carta1-worker.min.js')
|
|
91
133
|
|
|
92
|
-
|
|
134
|
+
const { aeaBlob } = await codec.encode([left, right], {
|
|
135
|
+
allocationBias: 1.0,
|
|
136
|
+
})
|
|
137
|
+
const parsed = await codec.parseAeaBlob(aeaBlob)
|
|
138
|
+
const { wavBlob, info } = await codec.decode({
|
|
139
|
+
aeaData: parsed.frameData,
|
|
140
|
+
info: parsed.info,
|
|
141
|
+
})
|
|
142
|
+
const optionMetadata = await codec.getEncoderOptions()
|
|
93
143
|
|
|
94
|
-
|
|
144
|
+
codec.terminate()
|
|
145
|
+
```
|
|
95
146
|
|
|
96
|
-
|
|
147
|
+
Always call `terminate()` when the worker is no longer needed. The build also
|
|
148
|
+
produces `dist/carta1.min.js`, a UMD bundle exposing the main JavaScript API as
|
|
149
|
+
the global `Carta1` object.
|
|
97
150
|
|
|
98
|
-
|
|
151
|
+
## Compatibility and limitations
|
|
99
152
|
|
|
100
|
-
|
|
153
|
+
- Encoding is fixed to the 44.1 kHz ATRAC1 profile described above; Carta1 does
|
|
154
|
+
not resample input.
|
|
155
|
+
- JavaScript APIs use normalized planar PCM. Web Audio channel data already
|
|
156
|
+
follows this convention.
|
|
157
|
+
- Stateful frame APIs must receive frames in order and must not be shared by
|
|
158
|
+
independent streams.
|
|
159
|
+
- AEA stores whole frames rather than an exact source sample count, so a final
|
|
160
|
+
partial frame is represented with zero padding.
|
|
161
|
+
- Browser worker assets must be served from a location permitted by the page's
|
|
162
|
+
worker and Content Security Policy rules.
|
|
101
163
|
|
|
102
164
|
## Development
|
|
103
165
|
|
|
104
|
-
|
|
105
|
-
# Install dependencies
|
|
106
|
-
npm install
|
|
107
|
-
|
|
108
|
-
# Run tests
|
|
109
|
-
npm test
|
|
166
|
+
Implementation concerns are split without hiding the top-level pipeline:
|
|
110
167
|
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
| Path | Responsibility |
|
|
169
|
+
| ------------------- | --------------------------------------------------- |
|
|
170
|
+
| `codec/pipeline/` | Ordered encoder and decoder stage composition. |
|
|
171
|
+
| `codec/analysis/` | Transient analysis and encoder decisions. |
|
|
172
|
+
| `codec/coding/` | Bit allocation and quantization. |
|
|
173
|
+
| `codec/transforms/` | FFT, QMF, MDCT, and inverse transforms. |
|
|
174
|
+
| `codec/io/` | Bitstreams, AEA serialization, and stream adapters. |
|
|
175
|
+
| `codec/core/` | Constants, options, and reusable buffer ownership. |
|
|
176
|
+
| `codec/browser/` | Web Worker implementation and client. |
|
|
177
|
+
| `bin/` | Command-line boundary. |
|
|
178
|
+
| `tests/` | Unit, serialization, and pipeline tests. |
|
|
113
179
|
|
|
114
|
-
|
|
115
|
-
npm run lint
|
|
180
|
+
Common commands are:
|
|
116
181
|
|
|
117
|
-
|
|
118
|
-
npm
|
|
182
|
+
```bash
|
|
183
|
+
npm test # Run the test suite once
|
|
184
|
+
npm run test:watch # Re-run tests while editing
|
|
185
|
+
npm run lint # Check JavaScript and formatting
|
|
186
|
+
npm run format # Apply repository formatting
|
|
187
|
+
npm run build # Build the three browser bundles
|
|
188
|
+
npm run check # Run lint, tests, and the production build
|
|
119
189
|
```
|
|
120
190
|
|
|
121
|
-
|
|
191
|
+
Run `npm run check` before submitting a change. Pull requests and pushes run
|
|
192
|
+
the same gate in CI.
|
|
122
193
|
|
|
123
|
-
|
|
194
|
+
## Acknowledgements
|
|
124
195
|
|
|
125
|
-
|
|
196
|
+
This project would not have been possible without
|
|
197
|
+
[AtracDEnc](https://github.com/dcherednik/atracdenc) by
|
|
198
|
+
[Daniel Cherednik](https://github.com/dcherednik), the reference ATRAC
|
|
199
|
+
implementation used to understand the ATRAC1 format and codec behavior.
|
|
126
200
|
|
|
127
201
|
## License
|
|
128
202
|
|
|
@@ -31,8 +31,35 @@ import {
|
|
|
31
31
|
INV_POWER_OF_TWO,
|
|
32
32
|
SCALE_FACTORS,
|
|
33
33
|
BFU_AMOUNTS,
|
|
34
|
+
WORD_LENGTH_DELTA_BITS,
|
|
35
|
+
DISTORTION_DELTA_FACTORS,
|
|
34
36
|
} from '../core/constants.js'
|
|
35
37
|
|
|
38
|
+
const biasedScaleFactorCache = new Map()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds and returns a cached table of biased scale factors.
|
|
42
|
+
*
|
|
43
|
+
* @param {number} bias
|
|
44
|
+
* @returns {Float64Array}
|
|
45
|
+
*/
|
|
46
|
+
function buildBiasedScaleFactorTable(bias) {
|
|
47
|
+
if (biasedScaleFactorCache.has(bias)) {
|
|
48
|
+
return biasedScaleFactorCache.get(bias)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const out = new Float64Array(64)
|
|
52
|
+
if (bias === 1) {
|
|
53
|
+
out.set(SCALE_FACTORS)
|
|
54
|
+
} else {
|
|
55
|
+
for (let i = 0; i < 64; i++) {
|
|
56
|
+
out[i] = Math.pow(SCALE_FACTORS[i], bias)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
biasedScaleFactorCache.set(bias, out)
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
/**
|
|
37
64
|
* Allocate bits by finding the optimal BFU count and distribution using RDO.
|
|
38
65
|
* This function searches through the valid BFU counts to find the one that
|
|
@@ -40,23 +67,23 @@ import {
|
|
|
40
67
|
*
|
|
41
68
|
* @param {Array<Float32Array>} bfuData
|
|
42
69
|
* @param {Int32Array} bfuSizes
|
|
43
|
-
* @param {number} maxBfuCount
|
|
70
|
+
* @param {number} maxBfuCount
|
|
44
71
|
* @param {number} allocationBias
|
|
45
72
|
* @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
|
|
46
73
|
*/
|
|
47
74
|
export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
48
75
|
const allScaleFactorIndices = new Int32Array(maxBfuCount)
|
|
49
76
|
const zeroBitDistortions = new Float32Array(maxBfuCount)
|
|
77
|
+
const biasedScaleFactors = buildBiasedScaleFactorTable(allocationBias)
|
|
50
78
|
|
|
51
79
|
for (let i = 0; i < maxBfuCount; i++) {
|
|
52
80
|
const sz = bfuSizes[i] | 0
|
|
53
81
|
if (sz === 0) continue
|
|
54
82
|
|
|
55
|
-
const sfi = findScaleFactor(bfuData[i]
|
|
83
|
+
const sfi = findScaleFactor(bfuData[i], sz)
|
|
56
84
|
allScaleFactorIndices[i] = sfi
|
|
57
85
|
if (sfi > 0) {
|
|
58
|
-
const
|
|
59
|
-
const effectiveScaleFactor = Math.pow(scaleFactor, allocationBias)
|
|
86
|
+
const effectiveScaleFactor = biasedScaleFactors[sfi]
|
|
60
87
|
zeroBitDistortions[i] = effectiveScaleFactor * 2.0 * sz
|
|
61
88
|
}
|
|
62
89
|
}
|
|
@@ -78,7 +105,7 @@ export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
|
78
105
|
candidateBfuCount,
|
|
79
106
|
bfuSizes,
|
|
80
107
|
availableBits,
|
|
81
|
-
|
|
108
|
+
biasedScaleFactors,
|
|
82
109
|
allScaleFactorIndices
|
|
83
110
|
)
|
|
84
111
|
|
|
@@ -88,7 +115,7 @@ export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
|
88
115
|
bfuSizes,
|
|
89
116
|
rdoResult.wordLengths,
|
|
90
117
|
rdoResult.scaleFactorIndices,
|
|
91
|
-
|
|
118
|
+
biasedScaleFactors,
|
|
92
119
|
zeroBitDistortions
|
|
93
120
|
)
|
|
94
121
|
|
|
@@ -118,14 +145,14 @@ export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
|
118
145
|
* Calculates the total distortion for a given allocation.
|
|
119
146
|
* This includes quantization distortion for coded bands and truncation
|
|
120
147
|
* distortion for uncoded bands.
|
|
121
|
-
* @param {number} activeBfuCount
|
|
122
|
-
* @param {number} maxBfuCount
|
|
123
|
-
* @param {Int32Array} bfuSizes
|
|
124
|
-
* @param {Int32Array} wordLengths
|
|
125
|
-
* @param {Int32Array} scaleFactorIndices
|
|
126
|
-
* @param {number}
|
|
127
|
-
* @param {Float32Array} zeroBitDistortions
|
|
128
|
-
* @returns {number}
|
|
148
|
+
* @param {number} activeBfuCount
|
|
149
|
+
* @param {number} maxBfuCount
|
|
150
|
+
* @param {Int32Array} bfuSizes
|
|
151
|
+
* @param {Int32Array} wordLengths
|
|
152
|
+
* @param {Int32Array} scaleFactorIndices
|
|
153
|
+
* @param {number} biasedScaleFactors
|
|
154
|
+
* @param {Float32Array} zeroBitDistortions
|
|
155
|
+
* @returns {number}
|
|
129
156
|
*/
|
|
130
157
|
function calculateTotalDistortion(
|
|
131
158
|
activeBfuCount,
|
|
@@ -133,7 +160,7 @@ function calculateTotalDistortion(
|
|
|
133
160
|
bfuSizes,
|
|
134
161
|
wordLengths,
|
|
135
162
|
scaleFactorIndices,
|
|
136
|
-
|
|
163
|
+
biasedScaleFactors,
|
|
137
164
|
zeroBitDistortions
|
|
138
165
|
) {
|
|
139
166
|
let totalDistortion = 0.0
|
|
@@ -149,8 +176,7 @@ function calculateTotalDistortion(
|
|
|
149
176
|
const sfi = scaleFactorIndices[i]
|
|
150
177
|
if (sfi === 0) continue
|
|
151
178
|
|
|
152
|
-
const
|
|
153
|
-
const effectiveScaleFactor = Math.pow(scaleFactor, allocationBias)
|
|
179
|
+
const effectiveScaleFactor = biasedScaleFactors[sfi]
|
|
154
180
|
const distortionFactor = INV_POWER_OF_TWO[bits]
|
|
155
181
|
|
|
156
182
|
totalDistortion += effectiveScaleFactor * distortionFactor * bfuSizes[i]
|
|
@@ -170,105 +196,100 @@ function calculateTotalDistortion(
|
|
|
170
196
|
* @param {number} activeBfuCount
|
|
171
197
|
* @param {Int32Array} bfuSizes
|
|
172
198
|
* @param {number} remainingBits
|
|
173
|
-
* @param {
|
|
174
|
-
* @param {Int32Array} allScaleFactorIndices
|
|
199
|
+
* @param {Float64Array} biasedScaleFactors
|
|
200
|
+
* @param {Int32Array} allScaleFactorIndices
|
|
175
201
|
* @returns {{wordLengths: Int32Array, scaleFactorIndices: Int32Array}}
|
|
176
202
|
*/
|
|
177
203
|
function distributeBitsRDO(
|
|
178
204
|
activeBfuCount,
|
|
179
205
|
bfuSizes,
|
|
180
206
|
remainingBits,
|
|
181
|
-
|
|
207
|
+
biasedScaleFactors,
|
|
182
208
|
allScaleFactorIndices
|
|
183
209
|
) {
|
|
184
|
-
const scaleFactorTable = allScaleFactorIndices
|
|
185
210
|
const wordLengths = new Int32Array(activeBfuCount)
|
|
186
211
|
|
|
187
|
-
const heapIndices =
|
|
188
|
-
const heapPriorities =
|
|
189
|
-
|
|
190
|
-
const deltaDistPerCoeff = (bfuIndex, currentWl, nextWl) => {
|
|
191
|
-
const sfi = scaleFactorTable[bfuIndex]
|
|
192
|
-
const scaleFactor = SCALE_FACTORS[sfi]
|
|
193
|
-
if (scaleFactor === 0) return 0
|
|
194
|
-
|
|
195
|
-
const effectiveScaleFactor = Math.pow(scaleFactor, allocationBias)
|
|
196
|
-
|
|
197
|
-
const bits1 = WORD_LENGTH_BITS[currentWl] | 0
|
|
198
|
-
const bits2 = WORD_LENGTH_BITS[nextWl] | 0
|
|
199
|
-
|
|
200
|
-
const f1 = bits1 === 0 ? 2.0 : INV_POWER_OF_TWO[bits1]
|
|
201
|
-
const f2 = INV_POWER_OF_TWO[bits2]
|
|
202
|
-
|
|
203
|
-
return effectiveScaleFactor * (f1 - f2)
|
|
204
|
-
}
|
|
212
|
+
const heapIndices = new Int32Array(activeBfuCount)
|
|
213
|
+
const heapPriorities = new Float32Array(activeBfuCount)
|
|
214
|
+
let heapSize = 0
|
|
205
215
|
|
|
206
216
|
for (let bfuIndex = 0; bfuIndex < activeBfuCount; bfuIndex++) {
|
|
207
217
|
const sz = bfuSizes[bfuIndex] | 0
|
|
208
218
|
if (sz === 0) continue
|
|
209
219
|
|
|
210
|
-
const sfi =
|
|
220
|
+
const sfi = allScaleFactorIndices[bfuIndex]
|
|
211
221
|
if (sfi === 0) continue
|
|
212
222
|
|
|
213
|
-
const
|
|
214
|
-
const nextWl = 1
|
|
215
|
-
const deltaBits =
|
|
216
|
-
(WORD_LENGTH_BITS[nextWl] - WORD_LENGTH_BITS[currentWl]) | 0
|
|
223
|
+
const deltaBits = WORD_LENGTH_DELTA_BITS[0]
|
|
217
224
|
if (deltaBits <= 0) continue
|
|
218
225
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
const distortionDelta =
|
|
227
|
+
biasedScaleFactors[sfi] * DISTORTION_DELTA_FACTORS[0]
|
|
228
|
+
|
|
229
|
+
heapIndices[heapSize] = bfuIndex
|
|
230
|
+
heapPriorities[heapSize] = distortionDelta / deltaBits
|
|
231
|
+
heapSize++
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (heapSize === 0) {
|
|
235
|
+
return { wordLengths, scaleFactorIndices: allScaleFactorIndices }
|
|
222
236
|
}
|
|
223
237
|
|
|
224
238
|
// Heapify
|
|
225
|
-
for (let i = (
|
|
226
|
-
siftDown(heapIndices, heapPriorities, i)
|
|
239
|
+
for (let i = (heapSize >> 1) - 1; i >= 0; i--) {
|
|
240
|
+
siftDown(heapIndices, heapPriorities, i, heapSize)
|
|
241
|
+
}
|
|
227
242
|
|
|
228
|
-
// Greedy spending
|
|
229
|
-
while (remainingBits > 0 &&
|
|
243
|
+
// Greedy spending loop
|
|
244
|
+
while (remainingBits > 0 && heapSize > 0) {
|
|
230
245
|
const bfu = heapIndices[0]
|
|
231
246
|
const cur = wordLengths[bfu] | 0
|
|
232
|
-
const nxt = cur + 1
|
|
233
|
-
|
|
234
247
|
const sz = bfuSizes[bfu] | 0
|
|
235
|
-
const deltaBits =
|
|
248
|
+
const deltaBits = WORD_LENGTH_DELTA_BITS[cur]
|
|
236
249
|
const cost = deltaBits * sz
|
|
237
250
|
|
|
238
251
|
if (cost > remainingBits || cost <= 0) {
|
|
239
|
-
|
|
252
|
+
const last = heapSize - 1
|
|
253
|
+
heapIndices[0] = heapIndices[last]
|
|
254
|
+
heapPriorities[0] = heapPriorities[last]
|
|
255
|
+
heapSize--
|
|
256
|
+
if (heapSize > 0) siftDown(heapIndices, heapPriorities, 0, heapSize)
|
|
240
257
|
continue
|
|
241
258
|
}
|
|
242
259
|
|
|
243
260
|
remainingBits -= cost
|
|
261
|
+
const nxt = cur + 1
|
|
244
262
|
wordLengths[bfu] = nxt
|
|
245
263
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
popRoot(heapIndices, heapPriorities)
|
|
254
|
-
}
|
|
264
|
+
const deltaBitsNext = WORD_LENGTH_DELTA_BITS[nxt]
|
|
265
|
+
if (nxt < MAX_WORD_LENGTH_INDEX && deltaBitsNext > 0) {
|
|
266
|
+
const sfi = allScaleFactorIndices[bfu]
|
|
267
|
+
const distortionDelta =
|
|
268
|
+
biasedScaleFactors[sfi] * DISTORTION_DELTA_FACTORS[nxt]
|
|
269
|
+
heapPriorities[0] = distortionDelta / deltaBitsNext
|
|
270
|
+
siftDown(heapIndices, heapPriorities, 0, heapSize)
|
|
255
271
|
} else {
|
|
256
|
-
|
|
272
|
+
const last = heapSize - 1
|
|
273
|
+
heapIndices[0] = heapIndices[last]
|
|
274
|
+
heapPriorities[0] = heapPriorities[last]
|
|
275
|
+
heapSize--
|
|
276
|
+
if (heapSize > 0) siftDown(heapIndices, heapPriorities, 0, heapSize)
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
|
|
260
|
-
return { wordLengths, scaleFactorIndices:
|
|
280
|
+
return { wordLengths, scaleFactorIndices: allScaleFactorIndices }
|
|
261
281
|
}
|
|
262
282
|
|
|
263
283
|
/**
|
|
264
284
|
* Find the optimal scale factor index for a set of coefficients.
|
|
265
285
|
* Same semantics as the original.
|
|
266
286
|
* @param {Float32Array} coefficients
|
|
287
|
+
* @param {number} length
|
|
267
288
|
* @returns {number}
|
|
268
289
|
*/
|
|
269
|
-
export function findScaleFactor(coefficients) {
|
|
290
|
+
export function findScaleFactor(coefficients, length) {
|
|
270
291
|
let maxAmplitude = 0.0
|
|
271
|
-
for (let i = 0; i <
|
|
292
|
+
for (let i = 0; i < length; i++) {
|
|
272
293
|
const a = Math.abs(coefficients[i])
|
|
273
294
|
if (a > maxAmplitude) maxAmplitude = a
|
|
274
295
|
}
|
|
@@ -277,25 +298,6 @@ export function findScaleFactor(coefficients) {
|
|
|
277
298
|
return Math.max(0, Math.min(63, index))
|
|
278
299
|
}
|
|
279
300
|
|
|
280
|
-
/**
|
|
281
|
-
* Removes the root element from a max-heap and maintains heap property.
|
|
282
|
-
*
|
|
283
|
-
* This function efficiently removes the maximum priority element from the heap
|
|
284
|
-
* by replacing it with the last element and then restoring the heap property
|
|
285
|
-
* through sifting down.
|
|
286
|
-
*
|
|
287
|
-
* @param {Array<number>} heapIndices
|
|
288
|
-
* @param {Array<number>} heapPriorities
|
|
289
|
-
*/
|
|
290
|
-
function popRoot(heapIndices, heapPriorities) {
|
|
291
|
-
const last = heapIndices.length - 1
|
|
292
|
-
heapIndices[0] = heapIndices[last]
|
|
293
|
-
heapPriorities[0] = heapPriorities[last]
|
|
294
|
-
heapIndices.pop()
|
|
295
|
-
heapPriorities.pop()
|
|
296
|
-
if (heapIndices.length) siftDown(heapIndices, heapPriorities, 0)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
301
|
/**
|
|
300
302
|
* Restores max-heap property by sifting an element down from a given position.
|
|
301
303
|
*
|
|
@@ -304,12 +306,12 @@ function popRoot(heapIndices, heapPriorities) {
|
|
|
304
306
|
* with its children and swaps with the larger child until the heap property
|
|
305
307
|
* is satisfied, or it reaches a leaf position.
|
|
306
308
|
*
|
|
307
|
-
* @param {
|
|
308
|
-
* @param {
|
|
309
|
+
* @param {Int32Array} heapIndices
|
|
310
|
+
* @param {Float32Array} heapPriorities
|
|
309
311
|
* @param {number} startIndex
|
|
312
|
+
* @param {number} heapSize
|
|
310
313
|
*/
|
|
311
|
-
function siftDown(heapIndices, heapPriorities, startIndex) {
|
|
312
|
-
const heapSize = heapIndices.length
|
|
314
|
+
function siftDown(heapIndices, heapPriorities, startIndex, heapSize) {
|
|
313
315
|
let i = startIndex
|
|
314
316
|
const idxVal = heapIndices[i]
|
|
315
317
|
const prVal = heapPriorities[i]
|
package/codec/core/constants.js
CHANGED
|
@@ -158,3 +158,22 @@ export const INV_POWER_OF_TWO = (() => {
|
|
|
158
158
|
}
|
|
159
159
|
return table
|
|
160
160
|
})()
|
|
161
|
+
|
|
162
|
+
export const WORD_LENGTH_DELTA_BITS = (() => {
|
|
163
|
+
const table = new Int32Array(MAX_WORD_LENGTH_INDEX)
|
|
164
|
+
for (let i = 0; i < MAX_WORD_LENGTH_INDEX; i++) {
|
|
165
|
+
table[i] = (WORD_LENGTH_BITS[i + 1] - WORD_LENGTH_BITS[i]) | 0
|
|
166
|
+
}
|
|
167
|
+
return table
|
|
168
|
+
})()
|
|
169
|
+
|
|
170
|
+
export const DISTORTION_DELTA_FACTORS = (() => {
|
|
171
|
+
const table = new Float64Array(MAX_WORD_LENGTH_INDEX)
|
|
172
|
+
table[0] = 2.0 - INV_POWER_OF_TWO[WORD_LENGTH_BITS[1]]
|
|
173
|
+
for (let i = 1; i < MAX_WORD_LENGTH_INDEX; i++) {
|
|
174
|
+
const bits1 = WORD_LENGTH_BITS[i] | 0
|
|
175
|
+
const bits2 = WORD_LENGTH_BITS[i + 1] | 0
|
|
176
|
+
table[i] = INV_POWER_OF_TWO[bits1] - INV_POWER_OF_TWO[bits2]
|
|
177
|
+
}
|
|
178
|
+
return table
|
|
179
|
+
})()
|
package/codec/core/options.js
CHANGED
|
@@ -52,7 +52,7 @@ class EncoderOptions {
|
|
|
52
52
|
name: 'Bit allocation bias',
|
|
53
53
|
description:
|
|
54
54
|
'Controls how bits are distributed across the frequency spectrum during allocation. Higher values focus more bits on loud spectral components, while lower values spread bits more evenly across frequencies.',
|
|
55
|
-
range: [0.
|
|
55
|
+
range: [0.0, 5.0],
|
|
56
56
|
step: 0.01,
|
|
57
57
|
},
|
|
58
58
|
fixedBlockModes: {
|
package/codec/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { qmfAnalysisStage, mdctStage } from './pipeline/encoder.js'
|
|
|
14
14
|
import { pipe } from './utils.js'
|
|
15
15
|
import { BufferPool } from './core/buffers.js'
|
|
16
16
|
import { EncoderOptions } from './core/options.js'
|
|
17
|
-
import { AudioProcessor } from './io/processor.js'
|
|
17
|
+
import { AudioProcessor, decodeAeaPcm, encodeAeaPcm } from './io/processor.js'
|
|
18
18
|
import { FFT } from './transforms/fft.js'
|
|
19
19
|
import {
|
|
20
20
|
WORD_LENGTH_BITS,
|
|
@@ -37,6 +37,8 @@ export {
|
|
|
37
37
|
BufferPool,
|
|
38
38
|
EncoderOptions,
|
|
39
39
|
AudioProcessor,
|
|
40
|
+
decodeAeaPcm,
|
|
41
|
+
encodeAeaPcm,
|
|
40
42
|
FFT,
|
|
41
43
|
WORD_LENGTH_BITS,
|
|
42
44
|
SPECS_PER_BFU,
|
package/codec/io/bitstream.js
CHANGED
|
@@ -52,7 +52,7 @@ export function unpackBits(buffer, bitPosition, bitCount) {
|
|
|
52
52
|
let bitOffset = bitPosition % 8
|
|
53
53
|
let value = 0
|
|
54
54
|
|
|
55
|
-
for (let bitsRead = 0; bitsRead < bitCount && byteIndex < buffer.length;
|
|
55
|
+
for (let bitsRead = 0; bitsRead < bitCount && byteIndex < buffer.length;) {
|
|
56
56
|
const bitsAvailable = 8 - bitOffset
|
|
57
57
|
const bitsToRead = Math.min(bitCount - bitsRead, bitsAvailable)
|
|
58
58
|
|