carta1 1.2.0 → 1.3.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/codec/analysis/bands.js +41 -22
- package/codec/analysis/blocks.js +1 -1
- package/codec/analysis/spectrum.js +8 -8
- package/codec/core/constants.js +10 -1
- package/codec/core/tables.js +99 -1
- package/codec/dequantization.js +2 -2
- package/codec/index.js +4 -4
- package/codec/quantization/stage.js +6 -2
- package/codec/signal/fft.js +37 -38
- package/codec/signal/{spectrum.js → mdct.js} +37 -80
- package/codec/signal/order.js +16 -0
- package/codec/signal/qmf.js +539 -0
- package/codec/state.js +34 -14
- package/codec/synthesis/bands.js +36 -9
- package/codec/synthesis/pcm.js +35 -21
- 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 +1 -1
- package/codec/signal/bands.js +0 -108
package/codec/analysis/bands.js
CHANGED
|
@@ -2,9 +2,34 @@
|
|
|
2
2
|
* Carta1 QMF analysis application.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { QMF_ANALYSIS_DELAY_SAMPLES } from '../core/constants.js'
|
|
6
|
+
import { analyzeQmf } from '../signal/qmf.js'
|
|
6
7
|
import { throwError } from '../utils.js'
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Split one sample row through a two-band configurable QMF stage.
|
|
11
|
+
*
|
|
12
|
+
* @param {Float32Array} input
|
|
13
|
+
* @param {Float32Array} delay
|
|
14
|
+
* @param {object} scratch
|
|
15
|
+
* @returns {Float32Array[]}
|
|
16
|
+
*/
|
|
17
|
+
function analyzePair(input, delay, scratch) {
|
|
18
|
+
const window =
|
|
19
|
+
scratch.analysisWindows[input.length] ??
|
|
20
|
+
throwError(`QMF analysis window for ${input.length} samples is required`)
|
|
21
|
+
window.set(delay)
|
|
22
|
+
window.set(input, delay.length)
|
|
23
|
+
|
|
24
|
+
const sampleCount = input.length / 2
|
|
25
|
+
const bands = [new Float32Array(sampleCount), new Float32Array(sampleCount)]
|
|
26
|
+
analyzeQmf(window, bands, scratch)
|
|
27
|
+
delay.set(
|
|
28
|
+
window.subarray(input.length, input.length + QMF_ANALYSIS_DELAY_SAMPLES)
|
|
29
|
+
)
|
|
30
|
+
return bands
|
|
31
|
+
}
|
|
32
|
+
|
|
8
33
|
/**
|
|
9
34
|
* Split PCM into the three ATRAC1 frequency bands.
|
|
10
35
|
*
|
|
@@ -15,35 +40,29 @@ import { throwError } from '../utils.js'
|
|
|
15
40
|
export function analyzeBands(context) {
|
|
16
41
|
const bufferPool =
|
|
17
42
|
context?.bufferPool ?? throwError('analyzeBands: bufferPool is required')
|
|
18
|
-
const delays = bufferPool.
|
|
43
|
+
const delays = bufferPool.qmfAnalysisDelays
|
|
44
|
+
const scratch = bufferPool.qmfScratch
|
|
19
45
|
|
|
20
46
|
/**
|
|
21
47
|
* @param {Float32Array} pcmSamples
|
|
22
48
|
* @returns {object}
|
|
23
49
|
*/
|
|
24
50
|
return (pcmSamples) => {
|
|
25
|
-
const stage1 =
|
|
26
|
-
|
|
27
|
-
delays.lowBand,
|
|
28
|
-
bufferPool.qmfWorkBuffers
|
|
29
|
-
)
|
|
30
|
-
delays.lowBand = stage1.newDelay
|
|
51
|
+
const stage1 = analyzePair(pcmSamples, delays.lowBand, scratch)
|
|
52
|
+
const stage2 = analyzePair(stage1[0], delays.midBand, scratch)
|
|
31
53
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
bufferPool.qmfWorkBuffers
|
|
36
|
-
)
|
|
37
|
-
delays.midBand = stage2.newDelay
|
|
54
|
+
const delayedHigh = scratch.highBandWindows[stage1[1].length]
|
|
55
|
+
delayedHigh.set(bufferPool.qmfHighBandDelay)
|
|
56
|
+
delayedHigh.set(stage1[1], bufferPool.qmfHighBandDelay.length)
|
|
38
57
|
|
|
39
|
-
const delayedHigh
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
const highBand = delayedHigh.slice(0, stage1[1].length)
|
|
59
|
+
bufferPool.qmfHighBandDelay.set(
|
|
60
|
+
delayedHigh.subarray(
|
|
61
|
+
stage1[1].length,
|
|
62
|
+
stage1[1].length + bufferPool.qmfHighBandDelay.length
|
|
63
|
+
)
|
|
64
|
+
)
|
|
46
65
|
|
|
47
|
-
return { bands: [stage2
|
|
66
|
+
return { bands: [stage2[0], stage2[1], highBand] }
|
|
48
67
|
}
|
|
49
68
|
}
|
package/codec/analysis/blocks.js
CHANGED
|
@@ -196,7 +196,7 @@ export function performFFT(samples, fftSize) {
|
|
|
196
196
|
real.set(samples.subarray(0, copyLen))
|
|
197
197
|
|
|
198
198
|
// Perform FFT
|
|
199
|
-
FFT.
|
|
199
|
+
FFT.transform(real, imag)
|
|
200
200
|
|
|
201
201
|
// Calculate magnitude spectrum for positive frequencies
|
|
202
202
|
const magnitude = new Float32Array(fftSize / 2)
|
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
} from '../core/constants.js'
|
|
12
12
|
import { calculateBandOffset } from '../core/geometry.js'
|
|
13
13
|
import { MDCT_BAND_CONFIGS, WINDOW_SHORT } from '../core/tables.js'
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
mdct256,
|
|
17
|
-
mdct512,
|
|
18
|
-
reverseSpectrum,
|
|
19
|
-
} from '../signal/spectrum.js'
|
|
14
|
+
import { MDCT } from '../signal/mdct.js'
|
|
15
|
+
import { reverseSpectrum } from '../signal/order.js'
|
|
20
16
|
import { throwError } from '../utils.js'
|
|
21
17
|
|
|
18
|
+
const shortMdct = new MDCT(MDCT_SIZE_SHORT, 0.5)
|
|
19
|
+
const middleMdct = new MDCT(MDCT_SIZE_MID, 0.5)
|
|
20
|
+
const longMdct = new MDCT(MDCT_SIZE_LONG, 1)
|
|
21
|
+
|
|
22
22
|
/**
|
|
23
23
|
* Build the ATRAC1 spectrum from analyzed bands.
|
|
24
24
|
*
|
|
@@ -31,7 +31,7 @@ export function transformSpectrum(context) {
|
|
|
31
31
|
context?.bufferPool ??
|
|
32
32
|
throwError('transformSpectrum: bufferPool is required')
|
|
33
33
|
const overlapBuffers = bufferPool.mdctOverlap
|
|
34
|
-
const transformFunctions = [
|
|
34
|
+
const transformFunctions = [middleMdct, middleMdct, longMdct]
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* @param {Float32Array} samples
|
|
@@ -113,7 +113,7 @@ export function transformSpectrum(context) {
|
|
|
113
113
|
applyTailWindowing(blockSamples, overlapBuffer, MDCT_SHORT_BLOCK_SIZE)
|
|
114
114
|
mdctInput.set(blockSamples, MDCT_OVERLAP_SIZE)
|
|
115
115
|
|
|
116
|
-
let spectrum =
|
|
116
|
+
let spectrum = shortMdct.transform(mdctInput, bufferPool.mdctBuffers)
|
|
117
117
|
if (bandIndex > 0) {
|
|
118
118
|
spectrum = reverseSpectrum(spectrum, bufferPool.reversalBuffers)
|
|
119
119
|
}
|
package/codec/core/constants.js
CHANGED
|
@@ -34,8 +34,17 @@ export const MDCT_SHORT_BLOCK_SIZE = 32
|
|
|
34
34
|
export const MDCT_OVERLAP_SIZE = 32
|
|
35
35
|
export const MDCT_TAIL_WINDOW_SIZE = 16
|
|
36
36
|
|
|
37
|
+
export const QMF_BANDS = 2
|
|
37
38
|
export const QMF_TAPS = 48
|
|
38
|
-
export const
|
|
39
|
+
export const QMF_ANALYSIS_START_SAMPLE = 0
|
|
40
|
+
export const QMF_ANALYSIS_STEP_SAMPLES = QMF_BANDS
|
|
41
|
+
export const QMF_ANALYSIS_WINDOW_SAMPLES = QMF_TAPS
|
|
42
|
+
export const QMF_ANALYSIS_MODULATION_SCALE = 1
|
|
43
|
+
export const QMF_ANALYSIS_TERMS = 24
|
|
44
|
+
export const QMF_ANALYSIS_DELAY_SAMPLES = QMF_TAPS - QMF_BANDS
|
|
45
|
+
export const QMF_SYNTHESIS_DELAY_ROWS = 24
|
|
46
|
+
export const QMF_SYNTHESIS_TERMS = 24
|
|
47
|
+
export const QMF_SYNTHESIS_DELAY_SAMPLES = QMF_BANDS * QMF_SYNTHESIS_DELAY_ROWS
|
|
39
48
|
export const QMF_HIGH_BAND_DELAY = 39
|
|
40
49
|
|
|
41
50
|
export const FFT_SIZE_LOW = 128
|
package/codec/core/tables.js
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
* Stable ATRAC1 geometry, signal, allocation, and quantization tables.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MAX_WORD_LENGTH_INDEX,
|
|
7
|
+
QMF_ANALYSIS_TERMS,
|
|
8
|
+
QMF_BANDS,
|
|
9
|
+
QMF_SYNTHESIS_DELAY_ROWS,
|
|
10
|
+
QMF_SYNTHESIS_TERMS,
|
|
11
|
+
QMF_TAPS,
|
|
12
|
+
} from './constants.js'
|
|
6
13
|
|
|
7
14
|
export const AEA_MAGIC = new Uint8Array([0x00, 0x08, 0x00, 0x00])
|
|
8
15
|
|
|
@@ -70,6 +77,97 @@ export const QMF_ODD = (() => {
|
|
|
70
77
|
return odd
|
|
71
78
|
})()
|
|
72
79
|
|
|
80
|
+
/** Sample positions read by each component of the configurable QMF analysis. */
|
|
81
|
+
export const QMF_ANALYSIS_SAMPLE_OFFSETS = (() => {
|
|
82
|
+
const offsets = new Uint8Array(QMF_BANDS * QMF_ANALYSIS_TERMS)
|
|
83
|
+
for (let term = 0; term < QMF_ANALYSIS_TERMS; term++) {
|
|
84
|
+
offsets[term] = QMF_TAPS - 1 - term * QMF_BANDS
|
|
85
|
+
offsets[QMF_ANALYSIS_TERMS + term] = QMF_TAPS - QMF_BANDS - term * QMF_BANDS
|
|
86
|
+
}
|
|
87
|
+
return offsets
|
|
88
|
+
})()
|
|
89
|
+
|
|
90
|
+
/** Prototype-filter coefficients in component-major QMF analysis order. */
|
|
91
|
+
export const QMF_ANALYSIS_FILTER_COEFFICIENTS = (() => {
|
|
92
|
+
const coefficients = new Float32Array(QMF_BANDS * QMF_ANALYSIS_TERMS)
|
|
93
|
+
coefficients.set(QMF_EVEN)
|
|
94
|
+
coefficients.set(QMF_ODD, QMF_ANALYSIS_TERMS)
|
|
95
|
+
return coefficients
|
|
96
|
+
})()
|
|
97
|
+
|
|
98
|
+
/** Dense modulation applied after Carta1 QMF analysis filtering. */
|
|
99
|
+
export const QMF_ANALYSIS_MODULATION_COEFFICIENTS = new Float32Array([
|
|
100
|
+
1, 1, 1, -1,
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
/* eslint-disable no-loss-of-precision -- Float32Array applies the intended float32 rounding. */
|
|
104
|
+
|
|
105
|
+
/** Reference factors used by the optimized sixteen-band modulation path. */
|
|
106
|
+
export const QMF_ANALYSIS_PI_OVER_8_BUTTERFLY_SCALES = new Float32Array([
|
|
107
|
+
1.8477590084075928, 0.7653668522834778, 1.8477590084075928,
|
|
108
|
+
0.7653668522834778,
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
/** Reference half-angle factors used by sixteen-band modulation. */
|
|
112
|
+
export const QMF_ANALYSIS_HALF_BUTTERFLY_SCALES = new Float32Array([
|
|
113
|
+
0.7071067690849304, 0.2071067839860916, 1.2071068286895752,
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
/** Reference odd pi/32 cosines used by sixteen-band modulation. */
|
|
117
|
+
export const QMF_ANALYSIS_ODD_PI_OVER_32_COSINES = new Float32Array([
|
|
118
|
+
1.990369439125061, 1.913880705833435, 1.7638425827026367, 1.5460208654403687,
|
|
119
|
+
1.2687865495681763, 0.9427934885025024, 0.5805693864822388,
|
|
120
|
+
0.1960342824459076,
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
/** Reference odd pi/16 cosines used by sixteen-band modulation. */
|
|
124
|
+
export const QMF_ANALYSIS_ODD_PI_OVER_16_COSINES = new Float32Array([
|
|
125
|
+
1.9615705013275146, 1.662939190864563, 1.111140489578247, 0.39018064737319946,
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
/** Reference odd pi/64 cosines used by sixteen-band modulation. */
|
|
129
|
+
export const QMF_ANALYSIS_ODD_PI_OVER_64_COSINES = new Float32Array([
|
|
130
|
+
1.9975908994674683, 1.9783530235290527, 1.9400625228881836,
|
|
131
|
+
1.8830881118774414, 1.807978630065918, 1.7154572010040283, 1.606415033340454,
|
|
132
|
+
1.4819022417068481, 1.3431179523468018, 1.1913986206054688,
|
|
133
|
+
1.0282055139541626, 0.8551101684570312, 0.6737797260284424,
|
|
134
|
+
0.48596036434173584, 0.2934609651565552, 0.09813535213470459,
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
/* eslint-enable no-loss-of-precision */
|
|
138
|
+
|
|
139
|
+
/** Delay-row offsets read by each sample of configurable QMF synthesis. */
|
|
140
|
+
export const QMF_SYNTHESIS_DELAY_ROW_OFFSETS = (() => {
|
|
141
|
+
const offsets = new Uint8Array(QMF_BANDS * QMF_SYNTHESIS_TERMS)
|
|
142
|
+
for (let sample = 0; sample < QMF_BANDS; sample++) {
|
|
143
|
+
const sampleOffset = sample * QMF_SYNTHESIS_TERMS
|
|
144
|
+
for (let term = 0; term < QMF_SYNTHESIS_TERMS; term++) {
|
|
145
|
+
offsets[sampleOffset + term] = QMF_SYNTHESIS_DELAY_ROWS - 1 - term
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return offsets
|
|
149
|
+
})()
|
|
150
|
+
|
|
151
|
+
/** Delay components read by each sample of configurable QMF synthesis. */
|
|
152
|
+
export const QMF_SYNTHESIS_DELAY_COMPONENTS = (() => {
|
|
153
|
+
const components = new Uint8Array(QMF_BANDS * QMF_SYNTHESIS_TERMS)
|
|
154
|
+
components.fill(1, 0, QMF_SYNTHESIS_TERMS)
|
|
155
|
+
return components
|
|
156
|
+
})()
|
|
157
|
+
|
|
158
|
+
/** Prototype-filter coefficients in output-sample-major QMF synthesis order. */
|
|
159
|
+
export const QMF_SYNTHESIS_FILTER_COEFFICIENTS = (() => {
|
|
160
|
+
const coefficients = new Float64Array(QMF_BANDS * QMF_SYNTHESIS_TERMS)
|
|
161
|
+
coefficients.set(QMF_ODD)
|
|
162
|
+
coefficients.set(QMF_EVEN, QMF_SYNTHESIS_TERMS)
|
|
163
|
+
return coefficients
|
|
164
|
+
})()
|
|
165
|
+
|
|
166
|
+
/** Dense modulation applied before Carta1 QMF synthesis filtering. */
|
|
167
|
+
export const QMF_SYNTHESIS_MODULATION_COEFFICIENTS = new Float64Array([
|
|
168
|
+
0.5, 0.5, 0.5, -0.5,
|
|
169
|
+
])
|
|
170
|
+
|
|
73
171
|
export const MDCT_BAND_CONFIGS = [
|
|
74
172
|
{ size: 128, windowStart: 48 },
|
|
75
173
|
{ size: 128, windowStart: 48 },
|
package/codec/dequantization.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
BFU_START_SHORT,
|
|
9
9
|
WORD_LENGTH_BITS,
|
|
10
10
|
} from './core/tables.js'
|
|
11
|
-
import
|
|
11
|
+
import * as spectrumQuantization from './quantization/spectrum.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Reconstruct the continuous ATRAC1 spectrum.
|
|
@@ -40,7 +40,7 @@ export function dequantize() {
|
|
|
40
40
|
blockModes[band] === 0 ? BFU_START_LONG[bfu] : BFU_START_SHORT[bfu]
|
|
41
41
|
|
|
42
42
|
if (bitsPerSample > 0) {
|
|
43
|
-
const dequantized =
|
|
43
|
+
const dequantized = spectrumQuantization.dequantize(
|
|
44
44
|
quantizedCoefficients[bfu],
|
|
45
45
|
scaleFactorIndices[bfu],
|
|
46
46
|
bitsPerSample
|
package/codec/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { decode } from './pipeline/decoder.js'
|
|
|
7
7
|
import { AeaFile } from './boundary/container.js'
|
|
8
8
|
import { deserializeFrame, serializeFrame } from './syntax/frame.js'
|
|
9
9
|
import { quantize, dequantize } from './quantization/spectrum.js'
|
|
10
|
-
import { analyzeBands
|
|
11
|
-
import { transformSpectrum
|
|
10
|
+
import { analyzeBands } from './analysis/bands.js'
|
|
11
|
+
import { transformSpectrum } from './analysis/spectrum.js'
|
|
12
12
|
import { pipe } from './utils.js'
|
|
13
13
|
import { BufferPool } from './state.js'
|
|
14
14
|
import { EncoderOptions } from './core/options.js'
|
|
@@ -26,8 +26,8 @@ export {
|
|
|
26
26
|
pipe,
|
|
27
27
|
encode,
|
|
28
28
|
decode,
|
|
29
|
-
qmfAnalysisStage,
|
|
30
|
-
mdctStage,
|
|
29
|
+
analyzeBands as qmfAnalysisStage,
|
|
30
|
+
transformSpectrum as mdctStage,
|
|
31
31
|
serializeFrame,
|
|
32
32
|
deserializeFrame,
|
|
33
33
|
quantize,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { WORD_LENGTH_BITS } from '../core/tables.js'
|
|
6
|
-
import
|
|
6
|
+
import * as spectrumQuantization from './spectrum.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Materialize retained allocation as quantized ATRAC1 symbols.
|
|
@@ -30,7 +30,11 @@ export function quantize() {
|
|
|
30
30
|
const data = bfuData[bfu].subarray(0, bfuSizes[bfu])
|
|
31
31
|
const bitsPerSample = WORD_LENGTH_BITS[wordLengthIndices[bfu]]
|
|
32
32
|
quantizedCoefficients.push(
|
|
33
|
-
|
|
33
|
+
spectrumQuantization.quantize(
|
|
34
|
+
data,
|
|
35
|
+
scaleFactorIndices[bfu],
|
|
36
|
+
bitsPerSample
|
|
37
|
+
)
|
|
34
38
|
)
|
|
35
39
|
}
|
|
36
40
|
|
package/codec/signal/fft.js
CHANGED
|
@@ -1,66 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Carta1 Fast Fourier Transform operator.
|
|
3
|
-
*
|
|
4
|
-
* This module implements a radix-2 Cooley-Tukey FFT algorithm for frequency domain
|
|
5
|
-
* analysis used in psychoacoustic modeling and transient detection.
|
|
6
|
-
*/
|
|
1
|
+
/** Complex radix-2 Fast Fourier Transform. */
|
|
7
2
|
|
|
8
3
|
export class FFT {
|
|
9
4
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @param {Float32Array}
|
|
5
|
+
* Transform split complex data in place.
|
|
6
|
+
*
|
|
7
|
+
* @param {Float32Array} real Real input and output components.
|
|
8
|
+
* @param {Float32Array} imaginary Imaginary input and output components.
|
|
13
9
|
*/
|
|
14
|
-
static
|
|
10
|
+
static transform(real, imaginary) {
|
|
15
11
|
const size = real.length
|
|
16
12
|
if (size === 1) return
|
|
17
13
|
|
|
18
14
|
const bits = Math.log2(size)
|
|
19
|
-
|
|
20
|
-
// Bit reversal
|
|
21
|
-
for (let i = 0; i < size; i++) {
|
|
15
|
+
for (let index = 0; index < size; index++) {
|
|
22
16
|
let reversed = 0
|
|
23
|
-
let
|
|
24
|
-
for (let
|
|
25
|
-
reversed = (reversed << 1) | (
|
|
26
|
-
|
|
17
|
+
let remaining = index
|
|
18
|
+
for (let bit = 0; bit < bits; bit++) {
|
|
19
|
+
reversed = (reversed << 1) | (remaining & 1)
|
|
20
|
+
remaining >>= 1
|
|
27
21
|
}
|
|
28
|
-
if (reversed >
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
if (reversed > index) {
|
|
23
|
+
const realValue = real[index]
|
|
24
|
+
real[index] = real[reversed]
|
|
25
|
+
real[reversed] = realValue
|
|
26
|
+
const imaginaryValue = imaginary[index]
|
|
27
|
+
imaginary[index] = imaginary[reversed]
|
|
28
|
+
imaginary[reversed] = imaginaryValue
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
// Cooley-Tukey decimation-in-time
|
|
35
32
|
for (let stride = 2; stride <= size; stride <<= 1) {
|
|
36
33
|
const halfStride = stride >> 1
|
|
37
34
|
const angle = (-2 * Math.PI) / stride
|
|
38
|
-
const
|
|
39
|
-
const
|
|
35
|
+
const stepReal = Math.cos(angle)
|
|
36
|
+
const stepImaginary = Math.sin(angle)
|
|
40
37
|
|
|
41
38
|
for (let start = 0; start < size; start += stride) {
|
|
42
39
|
let twiddleReal = 1
|
|
43
|
-
let
|
|
40
|
+
let twiddleImaginary = 0
|
|
44
41
|
|
|
45
|
-
for (let
|
|
46
|
-
const evenIndex = start +
|
|
42
|
+
for (let offset = 0; offset < halfStride; offset++) {
|
|
43
|
+
const evenIndex = start + offset
|
|
47
44
|
const oddIndex = evenIndex + halfStride
|
|
48
|
-
|
|
49
45
|
const evenReal = real[evenIndex]
|
|
50
|
-
const
|
|
46
|
+
const evenImaginary = imaginary[evenIndex]
|
|
51
47
|
const oddReal = real[oddIndex]
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
48
|
+
const oddImaginary = imaginary[oddIndex]
|
|
49
|
+
const rotatedReal =
|
|
50
|
+
oddReal * twiddleReal - oddImaginary * twiddleImaginary
|
|
51
|
+
const rotatedImaginary =
|
|
52
|
+
oddReal * twiddleImaginary + oddImaginary * twiddleReal
|
|
56
53
|
|
|
57
|
-
real[evenIndex] = evenReal +
|
|
58
|
-
|
|
59
|
-
real[oddIndex] = evenReal -
|
|
60
|
-
|
|
54
|
+
real[evenIndex] = evenReal + rotatedReal
|
|
55
|
+
imaginary[evenIndex] = evenImaginary + rotatedImaginary
|
|
56
|
+
real[oddIndex] = evenReal - rotatedReal
|
|
57
|
+
imaginary[oddIndex] = evenImaginary - rotatedImaginary
|
|
61
58
|
|
|
62
|
-
const nextReal =
|
|
63
|
-
|
|
59
|
+
const nextReal =
|
|
60
|
+
twiddleReal * stepReal - twiddleImaginary * stepImaginary
|
|
61
|
+
twiddleImaginary =
|
|
62
|
+
twiddleReal * stepImaginary + twiddleImaginary * stepReal
|
|
64
63
|
twiddleReal = nextReal
|
|
65
64
|
}
|
|
66
65
|
}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Modified discrete cosine transforms backed by a complex FFT.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The evaluation order is part of Carta1's numerical contract. The staged
|
|
5
|
+
* Float32 decomposition used by Carta5 is mathematically equivalent but does
|
|
6
|
+
* not preserve Carta1's encoded output or perceptual-quality floor.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { FFT } from './fft.js'
|
|
10
|
-
import {
|
|
11
|
-
MDCT_SIZE_SHORT,
|
|
12
|
-
MDCT_SIZE_MID,
|
|
13
|
-
MDCT_SIZE_LONG,
|
|
14
|
-
} from '../core/constants.js'
|
|
15
10
|
import { throwError } from '../utils.js'
|
|
16
11
|
|
|
17
12
|
/**
|
|
18
13
|
* Base class for MDCT and IMDCT transforms
|
|
19
14
|
*/
|
|
20
|
-
class
|
|
15
|
+
class MdctBase {
|
|
21
16
|
constructor(size, scale) {
|
|
22
17
|
this.size = size
|
|
23
18
|
this.halfSize = size >> 1
|
|
@@ -40,7 +35,7 @@ class MDCTBase {
|
|
|
40
35
|
/**
|
|
41
36
|
* Forward MDCT transform
|
|
42
37
|
*/
|
|
43
|
-
export class MDCT extends
|
|
38
|
+
export class MDCT extends MdctBase {
|
|
44
39
|
constructor(size, scale) {
|
|
45
40
|
super(size, scale)
|
|
46
41
|
}
|
|
@@ -51,7 +46,12 @@ export class MDCT extends MDCTBase {
|
|
|
51
46
|
* @param {Object} mdctBuffers - Optional work buffers for FFT
|
|
52
47
|
* @returns {Float32Array} Frequency-domain MDCT coefficients
|
|
53
48
|
*/
|
|
54
|
-
transform(
|
|
49
|
+
transform(
|
|
50
|
+
input,
|
|
51
|
+
mdctBuffers = null,
|
|
52
|
+
destination = null,
|
|
53
|
+
destinationOffset = 0
|
|
54
|
+
) {
|
|
55
55
|
const buffers =
|
|
56
56
|
mdctBuffers?.[this.fftSize] ??
|
|
57
57
|
throwError(`MDCT.transform: mdctBuffers[${this.fftSize}] is required`)
|
|
@@ -61,9 +61,9 @@ export class MDCT extends MDCTBase {
|
|
|
61
61
|
`MDCT.transform: mdctBuffers[${this.fftSize}].real is required`
|
|
62
62
|
)
|
|
63
63
|
const imag =
|
|
64
|
-
buffers?.
|
|
64
|
+
buffers?.imaginary ??
|
|
65
65
|
throwError(
|
|
66
|
-
`MDCT.transform: mdctBuffers[${this.fftSize}].
|
|
66
|
+
`MDCT.transform: mdctBuffers[${this.fftSize}].imaginary is required`
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
const n4 = this.quarterSize
|
|
@@ -104,18 +104,18 @@ export class MDCT extends MDCTBase {
|
|
|
104
104
|
imag[idx] = im * c - r * s
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
FFT.
|
|
107
|
+
FFT.transform(real, imag)
|
|
108
108
|
|
|
109
109
|
// Post-FFT processing
|
|
110
|
-
const output = new Float32Array(this.halfSize)
|
|
110
|
+
const output = destination ?? new Float32Array(this.halfSize)
|
|
111
111
|
for (let i = 0; i < this.fftSize; i++) {
|
|
112
112
|
const c = this.sinCosTable[i * 2]
|
|
113
113
|
const s = this.sinCosTable[i * 2 + 1]
|
|
114
114
|
const re = real[i]
|
|
115
115
|
const im = imag[i]
|
|
116
116
|
|
|
117
|
-
output[i * 2] = -re * c - im * s
|
|
118
|
-
output[this.halfSize - 1 - i * 2] = -re * s + im * c
|
|
117
|
+
output[destinationOffset + i * 2] = -re * c - im * s
|
|
118
|
+
output[destinationOffset + this.halfSize - 1 - i * 2] = -re * s + im * c
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
return output
|
|
@@ -125,7 +125,7 @@ export class MDCT extends MDCTBase {
|
|
|
125
125
|
/**
|
|
126
126
|
* Inverse MDCT transform
|
|
127
127
|
*/
|
|
128
|
-
export class IMDCT extends
|
|
128
|
+
export class IMDCT extends MdctBase {
|
|
129
129
|
constructor(size, scale = null) {
|
|
130
130
|
super(size, scale || size)
|
|
131
131
|
}
|
|
@@ -136,7 +136,12 @@ export class IMDCT extends MDCTBase {
|
|
|
136
136
|
* @param {Object} mdctBuffers - Optional work buffers for FFT
|
|
137
137
|
* @returns {Float32Array} Time-domain output samples
|
|
138
138
|
*/
|
|
139
|
-
transform(
|
|
139
|
+
transform(
|
|
140
|
+
input,
|
|
141
|
+
mdctBuffers = null,
|
|
142
|
+
destination = null,
|
|
143
|
+
destinationOffset = 0
|
|
144
|
+
) {
|
|
140
145
|
const buffers =
|
|
141
146
|
mdctBuffers?.[this.fftSize] ??
|
|
142
147
|
throwError(`IMDCT.transform: mdctBuffers[${this.fftSize}] is required`)
|
|
@@ -146,9 +151,9 @@ export class IMDCT extends MDCTBase {
|
|
|
146
151
|
`IMDCT.transform: mdctBuffers[${this.fftSize}].real is required`
|
|
147
152
|
)
|
|
148
153
|
const imag =
|
|
149
|
-
buffers?.
|
|
154
|
+
buffers?.imaginary ??
|
|
150
155
|
throwError(
|
|
151
|
-
`IMDCT.transform: mdctBuffers[${this.fftSize}].
|
|
156
|
+
`IMDCT.transform: mdctBuffers[${this.fftSize}].imaginary is required`
|
|
152
157
|
)
|
|
153
158
|
|
|
154
159
|
const n4 = this.quarterSize
|
|
@@ -169,10 +174,10 @@ export class IMDCT extends MDCTBase {
|
|
|
169
174
|
imag[i] = im * c - r * s
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
FFT.
|
|
177
|
+
FFT.transform(real, imag)
|
|
173
178
|
|
|
174
179
|
// Post-FFT butterfly
|
|
175
|
-
const output = new Float32Array(this.size)
|
|
180
|
+
const output = destination ?? new Float32Array(this.size)
|
|
176
181
|
|
|
177
182
|
for (let i = 0; i < this.fftSize / 2; i++) {
|
|
178
183
|
const i2 = i * 2
|
|
@@ -184,10 +189,10 @@ export class IMDCT extends MDCTBase {
|
|
|
184
189
|
const r1 = re * c + im * s
|
|
185
190
|
const i1 = re * s - im * c
|
|
186
191
|
|
|
187
|
-
output[n34 - 1 - i2] = r1
|
|
188
|
-
output[n34 + i2] = r1
|
|
189
|
-
output[n4 + i2] = i1
|
|
190
|
-
output[n4 - 1 - i2] = -i1
|
|
192
|
+
output[destinationOffset + n34 - 1 - i2] = r1
|
|
193
|
+
output[destinationOffset + n34 + i2] = r1
|
|
194
|
+
output[destinationOffset + n4 + i2] = i1
|
|
195
|
+
output[destinationOffset + n4 - 1 - i2] = -i1
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
for (let i = this.fftSize / 2; i < this.fftSize; i++) {
|
|
@@ -201,60 +206,12 @@ export class IMDCT extends MDCTBase {
|
|
|
201
206
|
const r1 = re * c + im * s
|
|
202
207
|
const i1 = re * s - im * c
|
|
203
208
|
|
|
204
|
-
output[n34 - 1 - idx] = r1
|
|
205
|
-
output[idx - n4] = -r1
|
|
206
|
-
output[n4 + idx] = i1
|
|
207
|
-
output[5 * n4 - 1 - idx] = i1
|
|
209
|
+
output[destinationOffset + n34 - 1 - idx] = r1
|
|
210
|
+
output[destinationOffset + idx - n4] = -r1
|
|
211
|
+
output[destinationOffset + n4 + idx] = i1
|
|
212
|
+
output[destinationOffset + 5 * n4 - 1 - idx] = i1
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
return output
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
|
-
|
|
214
|
-
// Pre-instantiated transforms
|
|
215
|
-
export const mdct64 = new MDCT(MDCT_SIZE_SHORT, 0.5)
|
|
216
|
-
export const mdct256 = new MDCT(MDCT_SIZE_MID, 0.5)
|
|
217
|
-
export const mdct512 = new MDCT(MDCT_SIZE_LONG, 1.0)
|
|
218
|
-
|
|
219
|
-
export const imdct64 = new IMDCT(MDCT_SIZE_SHORT, MDCT_SIZE_SHORT * 8)
|
|
220
|
-
export const imdct256 = new IMDCT(MDCT_SIZE_MID, MDCT_SIZE_MID * 8)
|
|
221
|
-
export const imdct512 = new IMDCT(MDCT_SIZE_LONG, MDCT_SIZE_LONG * 4)
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Perform overlap-add operation for MDCT reconstruction
|
|
225
|
-
* @param {Float32Array} prev - Previous block samples
|
|
226
|
-
* @param {Float32Array} curr - Current block samples
|
|
227
|
-
* @param {Float32Array} window - Window function coefficients
|
|
228
|
-
* @returns {Float32Array} Overlap-added output samples
|
|
229
|
-
*/
|
|
230
|
-
export function overlapAdd(prev, curr, window) {
|
|
231
|
-
const size = prev.length
|
|
232
|
-
const output = new Float32Array(size * 2)
|
|
233
|
-
|
|
234
|
-
for (let i = 0; i < size; i++) {
|
|
235
|
-
const w1 = window[i]
|
|
236
|
-
const w2 = window[2 * size - 1 - i]
|
|
237
|
-
const p = prev[i]
|
|
238
|
-
const c = curr[size - 1 - i]
|
|
239
|
-
|
|
240
|
-
output[i] = p * w2 - c * w1
|
|
241
|
-
output[2 * size - 1 - i] = p * w1 + c * w2
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return output
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Reverse spectral order for ATRAC1 mid and high bands.
|
|
249
|
-
*
|
|
250
|
-
* @param {Float32Array} spectrum
|
|
251
|
-
* @param {Object} reversalBuffers
|
|
252
|
-
* @returns {Float32Array}
|
|
253
|
-
*/
|
|
254
|
-
export function reverseSpectrum(spectrum, reversalBuffers) {
|
|
255
|
-
const reversed = reversalBuffers[spectrum.length]
|
|
256
|
-
for (let i = 0; i < spectrum.length; i++) {
|
|
257
|
-
reversed[i] = spectrum[spectrum.length - 1 - i]
|
|
258
|
-
}
|
|
259
|
-
return reversed
|
|
260
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** ATRAC1 spectral ordering. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reverse spectral order for ATRAC1 mid and high bands.
|
|
5
|
+
*
|
|
6
|
+
* @param {Float32Array} spectrum Spectrum in transform order.
|
|
7
|
+
* @param {object} reversalBuffers Reusable outputs indexed by spectrum length.
|
|
8
|
+
* @returns {Float32Array} Reversed spectrum.
|
|
9
|
+
*/
|
|
10
|
+
export function reverseSpectrum(spectrum, reversalBuffers) {
|
|
11
|
+
const reversed = reversalBuffers[spectrum.length]
|
|
12
|
+
for (let index = 0; index < spectrum.length; index++) {
|
|
13
|
+
reversed[index] = spectrum[spectrum.length - 1 - index]
|
|
14
|
+
}
|
|
15
|
+
return reversed
|
|
16
|
+
}
|