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.
Files changed (44) hide show
  1. package/README.md +149 -75
  2. package/bin/cli.js +8 -4
  3. package/codec/{coding/bitallocation.js → allocation/solve.js} +131 -97
  4. package/codec/allocation/source.js +69 -0
  5. package/codec/allocation/stage.js +38 -0
  6. package/codec/analysis/bands.js +49 -0
  7. package/codec/analysis/{transient.js → blocks.js} +90 -47
  8. package/codec/analysis/spectrum.js +160 -0
  9. package/codec/analysis/stage.js +32 -0
  10. package/codec/{browser/interface.js → boundary/browser.js} +1 -1
  11. package/codec/boundary/container.js +96 -0
  12. package/codec/boundary/pcm.js +193 -0
  13. package/codec/boundary/processor.js +208 -0
  14. package/codec/{io/readers.js → boundary/reader.js} +2 -1
  15. package/codec/boundary/stream.js +361 -0
  16. package/codec/{browser → boundary}/worker.js +45 -42
  17. package/codec/core/constants.js +9 -131
  18. package/codec/core/geometry.js +25 -0
  19. package/codec/core/tables.js +117 -0
  20. package/codec/dequantization.js +54 -0
  21. package/codec/index.js +12 -11
  22. package/codec/pipeline/decoder.js +10 -399
  23. package/codec/pipeline/encoder.js +12 -434
  24. package/codec/quantization/spectrum.js +67 -0
  25. package/codec/quantization/stage.js +45 -0
  26. package/codec/{transforms/qmf.js → signal/bands.js} +8 -5
  27. package/codec/{transforms → signal}/fft.js +1 -1
  28. package/codec/{transforms/mdct.js → signal/spectrum.js} +16 -1
  29. package/codec/{core/buffers.js → state.js} +46 -35
  30. package/codec/{io → syntax}/bitstream.js +28 -9
  31. package/codec/{io/serialization.js → syntax/frame.js} +5 -101
  32. package/codec/synthesis/bands.js +201 -0
  33. package/codec/synthesis/pcm.js +50 -0
  34. package/codec/synthesis/stage.js +17 -0
  35. package/codec/utils.js +9 -52
  36. package/dist/carta1-worker-interface.min.js +1 -1
  37. package/dist/carta1-worker-interface.min.js.map +1 -1
  38. package/dist/carta1-worker.min.js +1 -1
  39. package/dist/carta1-worker.min.js.map +1 -1
  40. package/dist/carta1.min.js +1 -1
  41. package/dist/carta1.min.js.map +1 -1
  42. package/package.json +16 -16
  43. package/codec/coding/quantization.js +0 -149
  44. package/codec/io/processor.js +0 -558
package/README.md CHANGED
@@ -1,128 +1,202 @@
1
1
  # Carta1
2
2
 
3
- An ATRAC1 audio codec implementation in JS.
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
- ## Features
7
+ ## Supported format
6
8
 
7
- - Pure JavaScript implementation of ATRAC1 codec
8
- - Encoding WAV to AEA (ATRAC1) format
9
- - Decoding AEA to WAV format
10
- - Command-line interface
11
- - Browser-compatible with Web Workers
12
- - Streaming support for large files
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
- ## CLI Usage
21
-
22
- ### Encoding
30
+ To work from a repository checkout:
23
31
 
24
32
  ```bash
25
- carta1 --encode input.wav output.aea
33
+ npm ci
34
+ npm run check
26
35
  ```
27
36
 
28
- ### Decoding
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
- ## Browser Usage
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
- ```javascript
51
- // Create codec instance
52
- const codec = new Carta1Worker('carta1-worker.min.js')
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
- // Encode PCM to ATRAC1
55
- const result = await codec.encode(pcmData, options)
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
- // Decode ATRAC1 to PCM
58
- const wavData = await codec.decode(aeaData)
93
+ ### Stateful frames
59
94
 
60
- // Clean up
61
- codec.terminate()
62
- ```
95
+ `encode()` and `decode()` create closures for chronological, complete mono
96
+ frames. Reuse each closure for one stream:
63
97
 
64
- ## API Reference
98
+ ```js
99
+ import { decode, encode, EncoderOptions } from 'carta1'
65
100
 
66
- ### Node.js
101
+ const encodeFrame = encode(new EncoderOptions({ transientThresholdLow: 1.0 }))
102
+ const decodeFrame = decode()
67
103
 
68
- ```javascript
69
- import { encode, decode, AudioProcessor } from 'carta1'
104
+ const encoded = encodeFrame(new Float32Array(512))
105
+ const decoded = decodeFrame(encoded)
106
+ ```
70
107
 
71
- // Encode PCM frames
72
- const encoder = encode(options)
73
- const encodedFrame = encoder(pcmFrame)
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
- // Decode ATRAC1 frames
76
- const decoder = decode()
77
- const pcmFrame = decoder(encodedFrame)
78
- ```
112
+ ### Encoder options
79
113
 
80
- ### Browser Worker API
114
+ `EncoderOptions` validates these settings:
81
115
 
82
- #### `new Carta1Worker(workerPath)`
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
- Create a new codec worker instance.
123
+ ## Browser worker
85
124
 
86
- #### `encode(pcmData, options)`
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
- Encode PCM audio data to ATRAC1 format.
129
+ ```js
130
+ import { Carta1Worker } from '/vendor/carta1-worker-interface.min.js'
89
131
 
90
- #### `decode(aeaData)`
132
+ const codec = new Carta1Worker('/vendor/carta1-worker.min.js')
91
133
 
92
- Decode ATRAC1 data to PCM format.
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
- #### `parseAeaBlob(blob)`
144
+ codec.terminate()
145
+ ```
95
146
 
96
- Parse AEA file blob to extract metadata.
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
- #### `terminate()`
151
+ ## Compatibility and limitations
99
152
 
100
- Clean up worker resources.
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
- ```bash
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
- # Build minified versions
112
- npm run build
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
- # Lint code
115
- npm run lint
180
+ Common commands are:
116
181
 
117
- # Format code
118
- npm run format
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
- ## Acknowledgements
191
+ Run `npm run check` before submitting a change. Pull requests and pushes run
192
+ the same gate in CI.
122
193
 
123
- This project would not have been possible without:
194
+ ## Acknowledgements
124
195
 
125
- - [**AtracDEnc**](https://github.com/dcherednik/atracdenc) by [Daniel Cherednik](https://github.com/dcherednik) - The reference ATRAC implementation that provided invaluable insights into the codec's inner workings and served as the primary source for understanding the ATRAC1 format specification.
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/io/processor.js'
23
- import { AeaReader } from '../codec/io/readers.js'
24
- import { deserializeFrame, AeaFile } from '../codec/io/serialization.js'
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.createWavBlob(
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/constants.js'
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
- * Allocate bits by finding the optimal BFU count and distribution using RDO.
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 {Array<Float32Array>} bfuData
69
- * @param {Int32Array} bfuSizes
68
+ * @param {number} activeBfuCount
70
69
  * @param {number} maxBfuCount
71
- * @param {number} allocationBias
72
- * @returns {{bfuCount:number, allocation:Int32Array, scaleFactorIndices:Int32Array}}
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
- export function allocateBits(bfuData, bfuSizes, maxBfuCount, allocationBias) {
75
- const allScaleFactorIndices = new Int32Array(maxBfuCount)
76
- const zeroBitDistortions = new Float32Array(maxBfuCount)
77
- const biasedScaleFactors = buildBiasedScaleFactorTable(allocationBias)
78
-
79
- for (let i = 0; i < maxBfuCount; i++) {
80
- const sz = bfuSizes[i] | 0
81
- if (sz === 0) continue
82
-
83
- const sfi = findScaleFactor(bfuData[i], sz)
84
- allScaleFactorIndices[i] = sfi
85
- if (sfi > 0) {
86
- const effectiveScaleFactor = biasedScaleFactors[sfi]
87
- zeroBitDistortions[i] = effectiveScaleFactor * 2.0 * sz
88
- }
89
- }
90
-
91
- let bestResult = null
92
- let minTotalDistortion = Infinity
93
-
94
- for (const candidateBfuCount of BFU_AMOUNTS) {
95
- if (candidateBfuCount > maxBfuCount) continue
96
-
97
- const availableBits =
98
- FRAME_BITS -
99
- FRAME_OVERHEAD_BITS -
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 calculateTotalDistortion(
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 {{wordLengths: Int32Array, scaleFactorIndices: Int32Array}}
162
+ * @returns {object} Retained allocation decisions.
202
163
  */
203
- function distributeBitsRDO(
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
+ }