carta1 1.1.9 → 1.2.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/README.md +149 -75
- package/bin/cli.js +8 -4
- package/codec/{coding/bitallocation.js → allocation/solve.js} +131 -97
- package/codec/allocation/source.js +69 -0
- package/codec/allocation/stage.js +38 -0
- package/codec/analysis/bands.js +49 -0
- package/codec/analysis/{transient.js → blocks.js} +90 -47
- package/codec/analysis/spectrum.js +160 -0
- package/codec/analysis/stage.js +32 -0
- package/codec/{browser/interface.js → boundary/browser.js} +1 -1
- package/codec/boundary/container.js +96 -0
- package/codec/boundary/pcm.js +193 -0
- package/codec/boundary/processor.js +208 -0
- package/codec/{io/readers.js → boundary/reader.js} +2 -1
- package/codec/boundary/stream.js +361 -0
- package/codec/{browser → boundary}/worker.js +45 -42
- package/codec/core/constants.js +9 -131
- package/codec/core/geometry.js +25 -0
- package/codec/core/tables.js +117 -0
- package/codec/dequantization.js +54 -0
- package/codec/index.js +12 -11
- package/codec/pipeline/decoder.js +10 -399
- package/codec/pipeline/encoder.js +12 -434
- package/codec/quantization/spectrum.js +67 -0
- package/codec/quantization/stage.js +45 -0
- package/codec/{transforms/qmf.js → signal/bands.js} +8 -5
- package/codec/{transforms → signal}/fft.js +1 -1
- package/codec/{transforms/mdct.js → signal/spectrum.js} +16 -1
- package/codec/{core/buffers.js → state.js} +46 -35
- package/codec/{io → syntax}/bitstream.js +28 -9
- package/codec/{io/serialization.js → syntax/frame.js} +5 -101
- package/codec/synthesis/bands.js +201 -0
- package/codec/synthesis/pcm.js +50 -0
- package/codec/synthesis/stage.js +17 -0
- package/codec/utils.js +9 -52
- 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/codec/coding/quantization.js +0 -149
- package/codec/io/processor.js +0 -558
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
|
|
package/bin/cli.js
CHANGED
|
@@ -19,9 +19,10 @@ import { performance } from 'perf_hooks'
|
|
|
19
19
|
import cliProgress from 'cli-progress'
|
|
20
20
|
import wav from 'wav'
|
|
21
21
|
|
|
22
|
-
import { AudioProcessor } from '../codec/
|
|
23
|
-
import { AeaReader } from '../codec/
|
|
24
|
-
import {
|
|
22
|
+
import { AudioProcessor } from '../codec/boundary/processor.js'
|
|
23
|
+
import { AeaReader } from '../codec/boundary/reader.js'
|
|
24
|
+
import { AeaFile } from '../codec/boundary/container.js'
|
|
25
|
+
import { deserializeFrame } from '../codec/syntax/frame.js'
|
|
25
26
|
import { EncoderOptions } from '../codec/core/options.js'
|
|
26
27
|
import {
|
|
27
28
|
SAMPLE_RATE,
|
|
@@ -483,6 +484,7 @@ async function encodeFile(inputFile, outputFile, options) {
|
|
|
483
484
|
const blob = await AudioProcessor.createAeaBlob(encodedFrames, {
|
|
484
485
|
title,
|
|
485
486
|
channelCount: reader.channels,
|
|
487
|
+
sampleCount: reader.totalSamples,
|
|
486
488
|
})
|
|
487
489
|
|
|
488
490
|
const buffer = await blob.arrayBuffer()
|
|
@@ -533,11 +535,13 @@ async function decodeFile(inputFile, outputFile, options) {
|
|
|
533
535
|
|
|
534
536
|
const decodedFrames = AudioProcessor.decodeStream(reader, {
|
|
535
537
|
channelCount: metadata.channelCount,
|
|
538
|
+
primingSampleCount: metadata.primingSampleCount,
|
|
539
|
+
sampleCount: metadata.sampleCount,
|
|
536
540
|
onProgress,
|
|
537
541
|
})
|
|
538
542
|
|
|
539
543
|
const pcmFrames = await AudioProcessor.collectFrames(decodedFrames)
|
|
540
|
-
const wavBlob = AudioProcessor.
|
|
544
|
+
const wavBlob = AudioProcessor.createPcmWaveBlob(
|
|
541
545
|
pcmFrames,
|
|
542
546
|
metadata.channelCount,
|
|
543
547
|
SAMPLE_RATE
|
|
@@ -27,13 +27,15 @@ import {
|
|
|
27
27
|
FRAME_OVERHEAD_BITS,
|
|
28
28
|
BITS_PER_BFU_METADATA,
|
|
29
29
|
MAX_WORD_LENGTH_INDEX,
|
|
30
|
+
} from '../core/constants.js'
|
|
31
|
+
import {
|
|
30
32
|
WORD_LENGTH_BITS,
|
|
31
33
|
INV_POWER_OF_TWO,
|
|
32
34
|
SCALE_FACTORS,
|
|
33
35
|
BFU_AMOUNTS,
|
|
34
36
|
WORD_LENGTH_DELTA_BITS,
|
|
35
37
|
DISTORTION_DELTA_FACTORS,
|
|
36
|
-
} from '../core/
|
|
38
|
+
} from '../core/tables.js'
|
|
37
39
|
|
|
38
40
|
const biasedScaleFactorCache = new Map()
|
|
39
41
|
|
|
@@ -61,84 +63,43 @@ function buildBiasedScaleFactorTable(bias) {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
|
-
*
|
|
65
|
-
* This function searches through the valid BFU counts to find the one that
|
|
66
|
-
* minimizes the total perceptual distortion.
|
|
66
|
+
* Evaluate one legal active-BFU count.
|
|
67
67
|
*
|
|
68
|
-
* @param {
|
|
69
|
-
* @param {Int32Array} bfuSizes
|
|
68
|
+
* @param {number} activeBfuCount
|
|
70
69
|
* @param {number} maxBfuCount
|
|
71
|
-
* @param {
|
|
72
|
-
* @
|
|
70
|
+
* @param {Int32Array} bfuSizes
|
|
71
|
+
* @param {number} remainingBits
|
|
72
|
+
* @param {Float64Array} biasedScaleFactors
|
|
73
|
+
* @param {Int32Array} allScaleFactorIndices
|
|
74
|
+
* @param {Float32Array} zeroBitDistortions
|
|
75
|
+
* @returns {object} Allocation result.
|
|
73
76
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
candidateBfuCount * BITS_PER_BFU_METADATA
|
|
101
|
-
|
|
102
|
-
if (availableBits < 0) continue
|
|
103
|
-
|
|
104
|
-
const rdoResult = distributeBitsRDO(
|
|
105
|
-
candidateBfuCount,
|
|
106
|
-
bfuSizes,
|
|
107
|
-
availableBits,
|
|
108
|
-
biasedScaleFactors,
|
|
109
|
-
allScaleFactorIndices
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
const totalDistortion = calculateTotalDistortion(
|
|
113
|
-
candidateBfuCount,
|
|
114
|
-
maxBfuCount,
|
|
115
|
-
bfuSizes,
|
|
116
|
-
rdoResult.wordLengths,
|
|
117
|
-
rdoResult.scaleFactorIndices,
|
|
118
|
-
biasedScaleFactors,
|
|
119
|
-
zeroBitDistortions
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
if (totalDistortion < minTotalDistortion) {
|
|
123
|
-
minTotalDistortion = totalDistortion
|
|
124
|
-
bestResult = {
|
|
125
|
-
bfuCount: candidateBfuCount,
|
|
126
|
-
allocation: rdoResult.wordLengths,
|
|
127
|
-
scaleFactorIndices: rdoResult.scaleFactorIndices,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (!bestResult) {
|
|
133
|
-
const fallbackBfuCount = BFU_AMOUNTS[0]
|
|
134
|
-
return {
|
|
135
|
-
bfuCount: fallbackBfuCount,
|
|
136
|
-
allocation: new Int32Array(fallbackBfuCount),
|
|
137
|
-
scaleFactorIndices: new Int32Array(NUM_BFUS),
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return bestResult
|
|
77
|
+
function search(
|
|
78
|
+
activeBfuCount,
|
|
79
|
+
maxBfuCount,
|
|
80
|
+
bfuSizes,
|
|
81
|
+
remainingBits,
|
|
82
|
+
biasedScaleFactors,
|
|
83
|
+
allScaleFactorIndices,
|
|
84
|
+
zeroBitDistortions
|
|
85
|
+
) {
|
|
86
|
+
const allocation = distributeBits(
|
|
87
|
+
activeBfuCount,
|
|
88
|
+
bfuSizes,
|
|
89
|
+
remainingBits,
|
|
90
|
+
biasedScaleFactors,
|
|
91
|
+
allScaleFactorIndices
|
|
92
|
+
)
|
|
93
|
+
const distortion = measureDistortion(
|
|
94
|
+
activeBfuCount,
|
|
95
|
+
maxBfuCount,
|
|
96
|
+
bfuSizes,
|
|
97
|
+
allocation.wordLengths,
|
|
98
|
+
allocation.scaleFactorIndices,
|
|
99
|
+
biasedScaleFactors,
|
|
100
|
+
zeroBitDistortions
|
|
101
|
+
)
|
|
102
|
+
return { distortion, ...allocation }
|
|
142
103
|
}
|
|
143
104
|
|
|
144
105
|
/**
|
|
@@ -154,7 +115,7 @@ export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
|
154
115
|
* @param {Float32Array} zeroBitDistortions
|
|
155
116
|
* @returns {number}
|
|
156
117
|
*/
|
|
157
|
-
function
|
|
118
|
+
function measureDistortion(
|
|
158
119
|
activeBfuCount,
|
|
159
120
|
maxBfuCount,
|
|
160
121
|
bfuSizes,
|
|
@@ -198,9 +159,9 @@ function calculateTotalDistortion(
|
|
|
198
159
|
* @param {number} remainingBits
|
|
199
160
|
* @param {Float64Array} biasedScaleFactors
|
|
200
161
|
* @param {Int32Array} allScaleFactorIndices
|
|
201
|
-
* @returns {
|
|
162
|
+
* @returns {object} Retained allocation decisions.
|
|
202
163
|
*/
|
|
203
|
-
function
|
|
164
|
+
function distributeBits(
|
|
204
165
|
activeBfuCount,
|
|
205
166
|
bfuSizes,
|
|
206
167
|
remainingBits,
|
|
@@ -280,24 +241,6 @@ function distributeBitsRDO(
|
|
|
280
241
|
return { wordLengths, scaleFactorIndices: allScaleFactorIndices }
|
|
281
242
|
}
|
|
282
243
|
|
|
283
|
-
/**
|
|
284
|
-
* Find the optimal scale factor index for a set of coefficients.
|
|
285
|
-
* Same semantics as the original.
|
|
286
|
-
* @param {Float32Array} coefficients
|
|
287
|
-
* @param {number} length
|
|
288
|
-
* @returns {number}
|
|
289
|
-
*/
|
|
290
|
-
export function findScaleFactor(coefficients, length) {
|
|
291
|
-
let maxAmplitude = 0.0
|
|
292
|
-
for (let i = 0; i < length; i++) {
|
|
293
|
-
const a = Math.abs(coefficients[i])
|
|
294
|
-
if (a > maxAmplitude) maxAmplitude = a
|
|
295
|
-
}
|
|
296
|
-
if (maxAmplitude === 0) return 0
|
|
297
|
-
const index = Math.ceil(3 * (Math.log2(maxAmplitude) + 21))
|
|
298
|
-
return Math.max(0, Math.min(63, index))
|
|
299
|
-
}
|
|
300
|
-
|
|
301
244
|
/**
|
|
302
245
|
* Restores max-heap property by sifting an element down from a given position.
|
|
303
246
|
*
|
|
@@ -339,3 +282,94 @@ function siftDown(heapIndices, heapPriorities, startIndex, heapSize) {
|
|
|
339
282
|
heapIndices[i] = idxVal
|
|
340
283
|
heapPriorities[i] = prVal
|
|
341
284
|
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Allocate bits by finding the optimal BFU count and distribution using RDO.
|
|
288
|
+
* This function searches through the valid BFU counts to find the one that
|
|
289
|
+
* minimizes the total perceptual distortion.
|
|
290
|
+
*
|
|
291
|
+
* @param {Array<Float32Array>} bfuData
|
|
292
|
+
* @param {Int32Array} bfuSizes
|
|
293
|
+
* @param {number} maxBfuCount
|
|
294
|
+
* @param {number} allocationBias
|
|
295
|
+
* @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
|
|
296
|
+
*/
|
|
297
|
+
export function solve(bfuData, bfuSizes, maxBfuCount, allocationBias) {
|
|
298
|
+
const allScaleFactorIndices = new Int32Array(maxBfuCount)
|
|
299
|
+
const zeroBitDistortions = new Float32Array(maxBfuCount)
|
|
300
|
+
const biasedScaleFactors = buildBiasedScaleFactorTable(allocationBias)
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < maxBfuCount; i++) {
|
|
303
|
+
const sz = bfuSizes[i] | 0
|
|
304
|
+
if (sz === 0) continue
|
|
305
|
+
|
|
306
|
+
const sfi = findScaleFactor(bfuData[i], sz)
|
|
307
|
+
allScaleFactorIndices[i] = sfi
|
|
308
|
+
if (sfi > 0) {
|
|
309
|
+
const effectiveScaleFactor = biasedScaleFactors[sfi]
|
|
310
|
+
zeroBitDistortions[i] = effectiveScaleFactor * 2.0 * sz
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let bestResult = null
|
|
315
|
+
let minTotalDistortion = Infinity
|
|
316
|
+
|
|
317
|
+
for (const candidateBfuCount of BFU_AMOUNTS) {
|
|
318
|
+
if (candidateBfuCount > maxBfuCount) continue
|
|
319
|
+
|
|
320
|
+
const availableBits =
|
|
321
|
+
FRAME_BITS -
|
|
322
|
+
FRAME_OVERHEAD_BITS -
|
|
323
|
+
candidateBfuCount * BITS_PER_BFU_METADATA
|
|
324
|
+
|
|
325
|
+
if (availableBits < 0) continue
|
|
326
|
+
|
|
327
|
+
const result = search(
|
|
328
|
+
candidateBfuCount,
|
|
329
|
+
maxBfuCount,
|
|
330
|
+
bfuSizes,
|
|
331
|
+
availableBits,
|
|
332
|
+
biasedScaleFactors,
|
|
333
|
+
allScaleFactorIndices,
|
|
334
|
+
zeroBitDistortions
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if (result.distortion < minTotalDistortion) {
|
|
338
|
+
minTotalDistortion = result.distortion
|
|
339
|
+
bestResult = {
|
|
340
|
+
bfuCount: candidateBfuCount,
|
|
341
|
+
allocation: result.wordLengths,
|
|
342
|
+
scaleFactorIndices: result.scaleFactorIndices,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!bestResult) {
|
|
348
|
+
const fallbackBfuCount = BFU_AMOUNTS[0]
|
|
349
|
+
return {
|
|
350
|
+
bfuCount: fallbackBfuCount,
|
|
351
|
+
allocation: new Int32Array(fallbackBfuCount),
|
|
352
|
+
scaleFactorIndices: new Int32Array(NUM_BFUS),
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return bestResult
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Find the optimal scale factor index for a set of coefficients.
|
|
361
|
+
* Same semantics as the original.
|
|
362
|
+
* @param {Float32Array} coefficients
|
|
363
|
+
* @param {number} length
|
|
364
|
+
* @returns {number}
|
|
365
|
+
*/
|
|
366
|
+
export function findScaleFactor(coefficients, length) {
|
|
367
|
+
let maxAmplitude = 0.0
|
|
368
|
+
for (let i = 0; i < length; i++) {
|
|
369
|
+
const a = Math.abs(coefficients[i])
|
|
370
|
+
if (a > maxAmplitude) maxAmplitude = a
|
|
371
|
+
}
|
|
372
|
+
if (maxAmplitude === 0) return 0
|
|
373
|
+
const index = Math.ceil(3 * (Math.log2(maxAmplitude) + 21))
|
|
374
|
+
return Math.max(0, Math.min(63, index))
|
|
375
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 allocation-source preparation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BFU_BAND_BOUNDARIES,
|
|
7
|
+
BFU_START_LONG,
|
|
8
|
+
BFU_START_SHORT,
|
|
9
|
+
SPECS_PER_BFU,
|
|
10
|
+
} from '../core/tables.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Group spectral coefficients into block floating units.
|
|
14
|
+
*
|
|
15
|
+
* @param {Float32Array} coefficients
|
|
16
|
+
* @param {Array<number>} blockModes
|
|
17
|
+
* @returns {object} Grouped bit-frequency units.
|
|
18
|
+
*/
|
|
19
|
+
export function groupIntoBFUs(coefficients, blockModes) {
|
|
20
|
+
const bfuData = []
|
|
21
|
+
const bfuSizes = []
|
|
22
|
+
let coeffIndex = 0
|
|
23
|
+
let bfuIndex = 0
|
|
24
|
+
|
|
25
|
+
for (let band = 0; band < 3; band++) {
|
|
26
|
+
const bandStart = coeffIndex
|
|
27
|
+
const bandSize = band === 2 ? 256 : 128
|
|
28
|
+
const bandEnd = band < 2 ? BFU_BAND_BOUNDARIES[band] : SPECS_PER_BFU.length
|
|
29
|
+
const startPositions =
|
|
30
|
+
blockModes[band] === 0 ? BFU_START_LONG : BFU_START_SHORT
|
|
31
|
+
|
|
32
|
+
while (bfuIndex < bandEnd) {
|
|
33
|
+
const size = SPECS_PER_BFU[bfuIndex]
|
|
34
|
+
const startPos = startPositions[bfuIndex] - bandStart
|
|
35
|
+
const endPos = startPos + size
|
|
36
|
+
const bfu = new Float32Array(size)
|
|
37
|
+
|
|
38
|
+
if (startPos >= 0 && endPos <= bandSize) {
|
|
39
|
+
bfu.set(coefficients.subarray(bandStart + startPos, bandStart + endPos))
|
|
40
|
+
} else if (startPos < bandSize && endPos > 0) {
|
|
41
|
+
const srcStart = Math.max(0, startPos)
|
|
42
|
+
const srcEnd = Math.min(bandSize, endPos)
|
|
43
|
+
bfu.set(
|
|
44
|
+
coefficients.subarray(bandStart + srcStart, bandStart + srcEnd),
|
|
45
|
+
Math.max(0, -startPos)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bfuData.push(bfu)
|
|
50
|
+
bfuSizes.push(size)
|
|
51
|
+
bfuIndex++
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
coeffIndex += bandSize
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { bfuData, bfuSizes, bfuCount: bfuIndex }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Prepare the spectrum as an allocation source.
|
|
62
|
+
*
|
|
63
|
+
* @param {Float32Array} coefficients
|
|
64
|
+
* @param {Array<number>} blockModes
|
|
65
|
+
* @returns {object} Allocation source.
|
|
66
|
+
*/
|
|
67
|
+
export function prepare(coefficients, blockModes) {
|
|
68
|
+
return groupIntoBFUs(coefficients, blockModes)
|
|
69
|
+
}
|