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
|
@@ -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 {
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|