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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Carta1 allocation stage.
3
+ */
4
+
5
+ import { throwError } from '../utils.js'
6
+ import { prepare } from './source.js'
7
+ import { solve } from './solve.js'
8
+
9
+ /**
10
+ * Allocate the frame budget across block floating units.
11
+ *
12
+ * @param {object} context
13
+ * @returns {Function}
14
+ */
15
+ export function allocate(context) {
16
+ const options =
17
+ context?.options ?? throwError('allocate: options is required')
18
+
19
+ /**
20
+ * @param {object} input
21
+ * @returns {object}
22
+ */
23
+ return (input) => {
24
+ const { coefficients, blockModes } = input
25
+ const { bfuData, bfuSizes, bfuCount } = prepare(coefficients, blockModes)
26
+ const result = solve(bfuData, bfuSizes, bfuCount, options.allocationBias)
27
+ const nBfu = result.bfuCount
28
+
29
+ return {
30
+ bfuData,
31
+ bfuSizes,
32
+ nBfu,
33
+ scaleFactorIndices: result.scaleFactorIndices.slice(0, nBfu),
34
+ wordLengthIndices: result.allocation.slice(0, nBfu),
35
+ blockModes,
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Carta1 QMF analysis application.
3
+ */
4
+
5
+ import { analyzeQmfPair } from '../signal/bands.js'
6
+ import { throwError } from '../utils.js'
7
+
8
+ /**
9
+ * Split PCM into the three ATRAC1 frequency bands.
10
+ *
11
+ * @param {object} context
12
+ * @returns {Function}
13
+ * @throws {Error} If the buffer pool is missing
14
+ */
15
+ export function analyzeBands(context) {
16
+ const bufferPool =
17
+ context?.bufferPool ?? throwError('analyzeBands: bufferPool is required')
18
+ const delays = bufferPool.qmfDelays
19
+
20
+ /**
21
+ * @param {Float32Array} pcmSamples
22
+ * @returns {object}
23
+ */
24
+ return (pcmSamples) => {
25
+ const stage1 = analyzeQmfPair(
26
+ pcmSamples,
27
+ delays.lowBand,
28
+ bufferPool.qmfWorkBuffers
29
+ )
30
+ delays.lowBand = stage1.newDelay
31
+
32
+ const stage2 = analyzeQmfPair(
33
+ stage1.lowBand,
34
+ delays.midBand,
35
+ bufferPool.qmfWorkBuffers
36
+ )
37
+ delays.midBand = stage2.newDelay
38
+
39
+ const delayedHigh =
40
+ bufferPool.qmfWorkBuffers.highBandDelay[stage1.highBand.length]
41
+ delayedHigh.set(delays.highBand)
42
+ delayedHigh.set(stage1.highBand, delays.highBand.length)
43
+
44
+ const highBand = delayedHigh.slice(0, stage1.highBand.length)
45
+ delays.highBand = delayedHigh.slice(stage1.highBand.length)
46
+
47
+ return { bands: [stage2.lowBand, stage2.highBand, highBand] }
48
+ }
49
+ }
@@ -6,53 +6,9 @@
6
6
  * characteristics that require special handling during encoding.
7
7
  */
8
8
 
9
- import { FFT } from '../transforms/fft.js'
10
-
11
- /**
12
- * Perform FFT on time-domain samples and return magnitude spectrum
13
- * @param {Float32Array} samples - Time-domain samples
14
- * @param {number} fftSize - FFT size (must be power of 2)
15
- * @returns {Float32Array} Magnitude spectrum (positive frequencies only)
16
- */
17
- export function performFFT(samples, fftSize) {
18
- const real = new Float32Array(fftSize)
19
- const imag = new Float32Array(fftSize)
20
-
21
- // Copy samples with zero-padding if necessary
22
- const copyLen = Math.min(samples.length, fftSize)
23
- real.set(samples.subarray(0, copyLen))
24
-
25
- // Perform FFT
26
- FFT.fft(real, imag)
27
-
28
- // Calculate magnitude spectrum for positive frequencies
29
- const magnitude = new Float32Array(fftSize / 2)
30
- for (let i = 0; i < fftSize / 2; i++) {
31
- magnitude[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i])
32
- }
33
-
34
- return magnitude
35
- }
36
-
37
- /**
38
- * Detect transients by analyzing spectral changes between frames
39
- * @param {Float32Array} currentCoeffs - Current frame coefficients
40
- * @param {Float32Array} prevCoeffs - Previous frame coefficients
41
- * @param {number} threshold - Detection threshold
42
- * @returns {boolean} True if transient detected
43
- */
44
- export function detectTransient(currentCoeffs, prevCoeffs, threshold) {
45
- // Can't detect transients without previous frame
46
- if (!prevCoeffs) return false
47
-
48
- // Calculate all spectral features
49
- const features = calculateSpectralFeatures(currentCoeffs, prevCoeffs)
50
-
51
- // Combine features using perceptually-motivated weighting
52
- const transientScore = calculateTransientScore(features)
53
-
54
- return transientScore > threshold
55
- }
9
+ import { FFT_SIZE_HIGH, FFT_SIZE_LOW, FFT_SIZE_MID } from '../core/constants.js'
10
+ import { FFT } from '../signal/fft.js'
11
+ import { throwError } from '../utils.js'
56
12
 
57
13
  /**
58
14
  * Calculate all spectral features for transient detection
@@ -224,3 +180,90 @@ function calculateTransientScore(features) {
224
180
  4
225
181
  )
226
182
  }
183
+
184
+ /**
185
+ * Perform FFT on time-domain samples and return magnitude spectrum
186
+ * @param {Float32Array} samples - Time-domain samples
187
+ * @param {number} fftSize - FFT size (must be power of 2)
188
+ * @returns {Float32Array} Magnitude spectrum (positive frequencies only)
189
+ */
190
+ export function performFFT(samples, fftSize) {
191
+ const real = new Float32Array(fftSize)
192
+ const imag = new Float32Array(fftSize)
193
+
194
+ // Copy samples with zero-padding if necessary
195
+ const copyLen = Math.min(samples.length, fftSize)
196
+ real.set(samples.subarray(0, copyLen))
197
+
198
+ // Perform FFT
199
+ FFT.fft(real, imag)
200
+
201
+ // Calculate magnitude spectrum for positive frequencies
202
+ const magnitude = new Float32Array(fftSize / 2)
203
+ for (let i = 0; i < fftSize / 2; i++) {
204
+ magnitude[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i])
205
+ }
206
+
207
+ return magnitude
208
+ }
209
+
210
+ /**
211
+ * Detect transients by analyzing spectral changes between frames
212
+ * @param {Float32Array} currentCoeffs - Current frame coefficients
213
+ * @param {Float32Array} prevCoeffs - Previous frame coefficients
214
+ * @param {number} threshold - Detection threshold
215
+ * @returns {boolean} True if transient detected
216
+ */
217
+ export function detectTransient(currentCoeffs, prevCoeffs, threshold) {
218
+ // Can't detect transients without previous frame
219
+ if (!prevCoeffs) return false
220
+
221
+ // Calculate all spectral features
222
+ const features = calculateSpectralFeatures(currentCoeffs, prevCoeffs)
223
+
224
+ // Combine features using perceptually-motivated weighting
225
+ const transientScore = calculateTransientScore(features)
226
+
227
+ return transientScore > threshold
228
+ }
229
+
230
+ /**
231
+ * Select transform block sizes from transient analysis.
232
+ *
233
+ * @param {object} context
234
+ * @returns {Function}
235
+ * @throws {Error} If the required context is missing
236
+ */
237
+ export function selectBlocks(context) {
238
+ const bufferPool =
239
+ context?.bufferPool ?? throwError('selectBlocks: bufferPool is required')
240
+ const options =
241
+ context?.options ?? throwError('selectBlocks: options is required')
242
+ const fftSizes = [FFT_SIZE_LOW, FFT_SIZE_MID, FFT_SIZE_HIGH]
243
+
244
+ /**
245
+ * @param {object} input
246
+ * @returns {object}
247
+ */
248
+ return (input) => {
249
+ const { bands } = input
250
+ let blockModes
251
+
252
+ if (options.fixedBlockModes) {
253
+ blockModes = options.fixedBlockModes
254
+ } else {
255
+ blockModes = bands.map((bandSamples, bandIndex) => {
256
+ const coeffs = performFFT(bandSamples, fftSizes[bandIndex])
257
+ const transient = detectTransient(
258
+ coeffs,
259
+ bufferPool.transientDetection[bandIndex],
260
+ options.transientThresholdLow
261
+ )
262
+ bufferPool.transientDetection[bandIndex] = coeffs
263
+ return transient * Math.max(bandIndex + 1, 2)
264
+ })
265
+ }
266
+
267
+ return { bands, blockModes }
268
+ }
269
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Carta1 spectral analysis application.
3
+ */
4
+
5
+ import {
6
+ MDCT_OVERLAP_SIZE,
7
+ MDCT_SHORT_BLOCK_SIZE,
8
+ MDCT_SIZE_LONG,
9
+ MDCT_SIZE_MID,
10
+ MDCT_SIZE_SHORT,
11
+ } from '../core/constants.js'
12
+ import { calculateBandOffset } from '../core/geometry.js'
13
+ import { MDCT_BAND_CONFIGS, WINDOW_SHORT } from '../core/tables.js'
14
+ import {
15
+ mdct64,
16
+ mdct256,
17
+ mdct512,
18
+ reverseSpectrum,
19
+ } from '../signal/spectrum.js'
20
+ import { throwError } from '../utils.js'
21
+
22
+ /**
23
+ * Build the ATRAC1 spectrum from analyzed bands.
24
+ *
25
+ * @param {object} context
26
+ * @returns {Function}
27
+ * @throws {Error} If the buffer pool is missing
28
+ */
29
+ export function transformSpectrum(context) {
30
+ const bufferPool =
31
+ context?.bufferPool ??
32
+ throwError('transformSpectrum: bufferPool is required')
33
+ const overlapBuffers = bufferPool.mdctOverlap
34
+ const transformFunctions = [mdct256, mdct256, mdct512]
35
+
36
+ /**
37
+ * @param {Float32Array} samples
38
+ * @param {number} bandIndex
39
+ * @param {number} blockMode
40
+ * @param {Float32Array} overlapBuffer
41
+ * @returns {Float32Array}
42
+ */
43
+ function transformBand(samples, bandIndex, blockMode, overlapBuffer) {
44
+ const config = MDCT_BAND_CONFIGS[bandIndex]
45
+
46
+ if (blockMode === 0) {
47
+ return transformLongBlock(
48
+ samples,
49
+ bandIndex,
50
+ config,
51
+ transformFunctions[bandIndex],
52
+ overlapBuffer
53
+ )
54
+ }
55
+
56
+ return transformShortBlocks(samples, bandIndex, config, overlapBuffer)
57
+ }
58
+
59
+ /**
60
+ * @param {Float32Array} samples
61
+ * @param {number} bandIndex
62
+ * @param {Object} config
63
+ * @param {Object} transformFunction
64
+ * @param {Float32Array} overlapBuffer
65
+ * @returns {Float32Array}
66
+ */
67
+ function transformLongBlock(
68
+ samples,
69
+ bandIndex,
70
+ config,
71
+ transformFunction,
72
+ overlapBuffer
73
+ ) {
74
+ const mdctSize = bandIndex === 2 ? MDCT_SIZE_LONG : MDCT_SIZE_MID
75
+ const mdctInput = bufferPool.transformBuffers[mdctSize]
76
+ mdctInput.fill(0)
77
+ mdctInput.set(overlapBuffer, config.windowStart)
78
+
79
+ applyTailWindowing(samples, overlapBuffer, config.size)
80
+ mdctInput.set(samples, config.windowStart + MDCT_OVERLAP_SIZE)
81
+
82
+ let spectrum = transformFunction.transform(
83
+ mdctInput,
84
+ bufferPool.mdctBuffers
85
+ )
86
+ if (bandIndex > 0) {
87
+ spectrum = reverseSpectrum(spectrum, bufferPool.reversalBuffers)
88
+ }
89
+ return spectrum
90
+ }
91
+
92
+ /**
93
+ * @param {Float32Array} samples
94
+ * @param {number} bandIndex
95
+ * @param {Object} config
96
+ * @param {Float32Array} overlapBuffer
97
+ * @returns {Float32Array}
98
+ */
99
+ function transformShortBlocks(samples, bandIndex, config, overlapBuffer) {
100
+ const numBlocks = 1 << (config.size === 256 ? 3 : 2)
101
+ const output = new Float32Array(config.size)
102
+
103
+ for (let block = 0; block < numBlocks; block++) {
104
+ const blockStart = block * MDCT_SHORT_BLOCK_SIZE
105
+ const blockSamples = samples.subarray(
106
+ blockStart,
107
+ blockStart + MDCT_SHORT_BLOCK_SIZE
108
+ )
109
+ const mdctInput = bufferPool.transformBuffers[MDCT_SIZE_SHORT]
110
+ mdctInput.fill(0)
111
+ mdctInput.set(overlapBuffer, 0)
112
+
113
+ applyTailWindowing(blockSamples, overlapBuffer, MDCT_SHORT_BLOCK_SIZE)
114
+ mdctInput.set(blockSamples, MDCT_OVERLAP_SIZE)
115
+
116
+ let spectrum = mdct64.transform(mdctInput, bufferPool.mdctBuffers)
117
+ if (bandIndex > 0) {
118
+ spectrum = reverseSpectrum(spectrum, bufferPool.reversalBuffers)
119
+ }
120
+ output.set(spectrum, blockStart)
121
+ }
122
+
123
+ return output
124
+ }
125
+
126
+ /**
127
+ * @param {Float32Array} samples
128
+ * @param {Float32Array} overlapBuffer
129
+ * @param {number} blockSize
130
+ */
131
+ function applyTailWindowing(samples, overlapBuffer, blockSize) {
132
+ const tailStart = blockSize - MDCT_OVERLAP_SIZE
133
+ for (let i = 0; i < MDCT_OVERLAP_SIZE; i++) {
134
+ const tailValue = samples[tailStart + i]
135
+ overlapBuffer[i] = WINDOW_SHORT[i] * tailValue
136
+ samples[tailStart + i] = tailValue * WINDOW_SHORT[31 - i]
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {object} input
142
+ * @returns {object}
143
+ */
144
+ return (input) => {
145
+ const { bands, blockModes, originalFrame } = input
146
+ const coefficients = new Float32Array(512)
147
+
148
+ bands.forEach((bandSamples, bandIndex) => {
149
+ const transformed = transformBand(
150
+ bandSamples,
151
+ bandIndex,
152
+ blockModes[bandIndex],
153
+ overlapBuffers[bandIndex]
154
+ )
155
+ coefficients.set(transformed, calculateBandOffset(bandIndex))
156
+ })
157
+
158
+ return { bands, coefficients, blockModes, originalFrame }
159
+ }
160
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Carta1 analysis stage.
3
+ */
4
+
5
+ import { pipe } from '../utils.js'
6
+ import { analyzeBands } from './bands.js'
7
+ import { selectBlocks } from './blocks.js'
8
+ import { transformSpectrum } from './spectrum.js'
9
+
10
+ /**
11
+ * Analyze a PCM frame in the ATRAC1 signal domain.
12
+ *
13
+ * @param {object} context
14
+ * @returns {Function}
15
+ */
16
+ export function analyze(context) {
17
+ const analyzeFrame = pipe(
18
+ context,
19
+ analyzeBands,
20
+ selectBlocks,
21
+ transformSpectrum
22
+ )
23
+
24
+ /**
25
+ * @param {Float32Array} pcmSamples
26
+ * @returns {object}
27
+ */
28
+ return (pcmSamples) => {
29
+ const { coefficients, blockModes } = analyzeFrame(pcmSamples)
30
+ return { coefficients, blockModes }
31
+ }
32
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Carta1 Worker Interface - Client-side API
2
+ * Carta1 browser worker boundary.
3
3
  *
4
4
  * Provides a clean async interface for encoding and decoding ATRAC1 audio
5
5
  * using Web Workers for non-blocking operation.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Carta1 AEA container boundary.
3
+ */
4
+
5
+ import {
6
+ AEA_CHANNEL_COUNT_OFFSET,
7
+ AEA_FRAME_COUNT_OFFSET,
8
+ AEA_HEADER_SIZE,
9
+ AEA_PRIMING_SAMPLE_COUNT_OFFSET,
10
+ AEA_SOURCE_SAMPLE_COUNT_OFFSET,
11
+ AEA_TITLE_OFFSET,
12
+ AEA_TITLE_SIZE,
13
+ STREAM_PRIMING_SAMPLES,
14
+ } from '../core/constants.js'
15
+ import { AEA_MAGIC } from '../core/tables.js'
16
+
17
+ /**
18
+ * AEA file-format handler.
19
+ */
20
+ export class AeaFile {
21
+ /**
22
+ * Create an AEA header.
23
+ *
24
+ * @param {string} [title='']
25
+ * @param {number} [frameCount=0]
26
+ * @param {number} [channelCount=1]
27
+ * @param {number} [sampleCount=0]
28
+ * @param {number} [primingSampleCount=STREAM_PRIMING_SAMPLES]
29
+ * @returns {Uint8Array}
30
+ */
31
+ static createHeader(
32
+ title = '',
33
+ frameCount = 0,
34
+ channelCount = 1,
35
+ sampleCount = 0,
36
+ primingSampleCount = STREAM_PRIMING_SAMPLES
37
+ ) {
38
+ const header = new Uint8Array(AEA_HEADER_SIZE)
39
+ const view = new DataView(header.buffer)
40
+ header.set(AEA_MAGIC, 0)
41
+
42
+ const titleBytes = new TextEncoder().encode(title)
43
+ header.set(
44
+ titleBytes.subarray(0, Math.min(titleBytes.length, AEA_TITLE_SIZE - 1)),
45
+ AEA_TITLE_OFFSET
46
+ )
47
+ view.setUint32(AEA_FRAME_COUNT_OFFSET, frameCount, true)
48
+ header[AEA_CHANNEL_COUNT_OFFSET] = channelCount
49
+ view.setUint32(
50
+ AEA_PRIMING_SAMPLE_COUNT_OFFSET,
51
+ primingSampleCount >>> 0,
52
+ true
53
+ )
54
+ view.setUint32(AEA_SOURCE_SAMPLE_COUNT_OFFSET, sampleCount >>> 0, true)
55
+ return header
56
+ }
57
+
58
+ /**
59
+ * Parse an AEA header.
60
+ *
61
+ * @param {Uint8Array} header
62
+ * @returns {object} Container metadata.
63
+ * @throws {Error} If the header is invalid
64
+ */
65
+ static parseHeader(header) {
66
+ if (header.length !== AEA_HEADER_SIZE) {
67
+ throw new Error(`Header must be ${AEA_HEADER_SIZE} bytes`)
68
+ }
69
+
70
+ for (let i = 0; i < AEA_MAGIC.length; i++) {
71
+ if (header[i] !== AEA_MAGIC[i]) {
72
+ throw new Error('Invalid AEA file')
73
+ }
74
+ }
75
+
76
+ const view = new DataView(
77
+ header.buffer,
78
+ header.byteOffset,
79
+ header.byteLength
80
+ )
81
+ const titleEnd = header.indexOf(0, AEA_TITLE_OFFSET)
82
+ const titleLength =
83
+ titleEnd === -1 ? AEA_TITLE_SIZE : titleEnd - AEA_TITLE_OFFSET
84
+ const title = new TextDecoder().decode(
85
+ header.subarray(AEA_TITLE_OFFSET, AEA_TITLE_OFFSET + titleLength)
86
+ )
87
+
88
+ return {
89
+ title,
90
+ frameCount: view.getUint32(AEA_FRAME_COUNT_OFFSET, true),
91
+ channelCount: header[AEA_CHANNEL_COUNT_OFFSET],
92
+ primingSampleCount: view.getUint32(AEA_PRIMING_SAMPLE_COUNT_OFFSET, true),
93
+ sampleCount: view.getUint32(AEA_SOURCE_SAMPLE_COUNT_OFFSET, true),
94
+ }
95
+ }
96
+ }