carta1 1.2.0 → 1.4.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 +1 -1
- package/bin/cli.js +4 -29
- package/codec/analysis/bands.js +41 -22
- package/codec/analysis/blocks.js +1 -1
- package/codec/analysis/spectrum.js +8 -8
- package/codec/boundary/container.js +27 -5
- 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/README.md
CHANGED
|
@@ -58,7 +58,7 @@ signed 16-bit PCM WAVE output.
|
|
|
58
58
|
| `-j, --json` | Write AEA metadata and frame structure as JSON. |
|
|
59
59
|
| `-q, --quiet` | Suppress normal output and progress. |
|
|
60
60
|
| `-f, --force` | Overwrite an existing output file. |
|
|
61
|
-
| `-t, --title <title>` | Set
|
|
61
|
+
| `-t, --title <title>` | Set a UTF-8 AEA title (up to 255 encoded bytes). |
|
|
62
62
|
| `-b, --bias <value>` | Set the bit-allocation bias; defaults to `1.0`. |
|
|
63
63
|
| `-m, --modes <modes>` | Fix low, mid, and high block modes, such as `0,0,0`. |
|
|
64
64
|
| `-V, --version` | Print the Carta1 version. |
|
package/bin/cli.js
CHANGED
|
@@ -21,14 +21,13 @@ import wav from 'wav'
|
|
|
21
21
|
|
|
22
22
|
import { AudioProcessor } from '../codec/boundary/processor.js'
|
|
23
23
|
import { AeaReader } from '../codec/boundary/reader.js'
|
|
24
|
-
import { AeaFile } from '../codec/boundary/container.js'
|
|
24
|
+
import { AeaFile, validateAeaTitle } from '../codec/boundary/container.js'
|
|
25
25
|
import { deserializeFrame } from '../codec/syntax/frame.js'
|
|
26
26
|
import { EncoderOptions } from '../codec/core/options.js'
|
|
27
27
|
import {
|
|
28
28
|
SAMPLE_RATE,
|
|
29
29
|
SAMPLES_PER_FRAME,
|
|
30
30
|
BITRATE_PER_CHANNEL,
|
|
31
|
-
AEA_TITLE_SIZE,
|
|
32
31
|
AEA_HEADER_SIZE,
|
|
33
32
|
SOUND_UNIT_SIZE,
|
|
34
33
|
} from '../codec/core/constants.js'
|
|
@@ -48,7 +47,7 @@ function formatTime(seconds) {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
/**
|
|
51
|
-
* Validate AEA title for
|
|
50
|
+
* Validate an AEA title for its fixed-width UTF-8 field.
|
|
52
51
|
*
|
|
53
52
|
* @param {string} title - Title to validate
|
|
54
53
|
* @returns {Object} Validation result
|
|
@@ -56,31 +55,7 @@ function formatTime(seconds) {
|
|
|
56
55
|
* @returns {string} [returns.error] - Error message if invalid
|
|
57
56
|
*/
|
|
58
57
|
function validateTitle(title) {
|
|
59
|
-
|
|
60
|
-
return { valid: true }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Check ASCII encoding
|
|
64
|
-
for (let i = 0; i < title.length; i++) {
|
|
65
|
-
const charCode = title.charCodeAt(i)
|
|
66
|
-
if (charCode > 127) {
|
|
67
|
-
return {
|
|
68
|
-
valid: false,
|
|
69
|
-
error: `Title contains non-ASCII character at position ${i}: "${title[i]}"`,
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Check length (AEA_TITLE_SIZE includes null terminator)
|
|
75
|
-
const maxLength = AEA_TITLE_SIZE - 1
|
|
76
|
-
if (title.length > maxLength) {
|
|
77
|
-
return {
|
|
78
|
-
valid: false,
|
|
79
|
-
error: `Title is too long (${title.length} chars). Maximum allowed: ${maxLength} characters`,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { valid: true }
|
|
58
|
+
return validateAeaTitle(title)
|
|
84
59
|
}
|
|
85
60
|
|
|
86
61
|
/**
|
|
@@ -703,7 +678,7 @@ async function main() {
|
|
|
703
678
|
.option('-f, --force', 'Overwrite output file if it exists')
|
|
704
679
|
.option(
|
|
705
680
|
'-t, --title <title>',
|
|
706
|
-
'Custom title for AEA file metadata (encoding only)'
|
|
681
|
+
'Custom UTF-8 title for AEA file metadata (encoding only)'
|
|
707
682
|
)
|
|
708
683
|
.option(
|
|
709
684
|
'-b, --bias <value>',
|
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
|
}
|
|
@@ -14,6 +14,29 @@ import {
|
|
|
14
14
|
} from '../core/constants.js'
|
|
15
15
|
import { AEA_MAGIC } from '../core/tables.js'
|
|
16
16
|
|
|
17
|
+
const UTF8_ENCODER = new TextEncoder()
|
|
18
|
+
const UTF8_DECODER = new TextDecoder()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a title for the fixed-width AEA title field.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} title
|
|
24
|
+
* @returns {{valid: boolean, error?: string}}
|
|
25
|
+
*/
|
|
26
|
+
export function validateAeaTitle(title) {
|
|
27
|
+
const byteLength = UTF8_ENCODER.encode(title).length
|
|
28
|
+
const maxByteLength = AEA_TITLE_SIZE - 1
|
|
29
|
+
|
|
30
|
+
if (byteLength > maxByteLength) {
|
|
31
|
+
return {
|
|
32
|
+
valid: false,
|
|
33
|
+
error: `Title is too long (${byteLength} UTF-8 bytes). Maximum allowed: ${maxByteLength} bytes`,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { valid: true }
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
/**
|
|
18
41
|
* AEA file-format handler.
|
|
19
42
|
*/
|
|
@@ -39,10 +62,9 @@ export class AeaFile {
|
|
|
39
62
|
const view = new DataView(header.buffer)
|
|
40
63
|
header.set(AEA_MAGIC, 0)
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
AEA_TITLE_OFFSET
|
|
65
|
+
UTF8_ENCODER.encodeInto(
|
|
66
|
+
title,
|
|
67
|
+
header.subarray(AEA_TITLE_OFFSET, AEA_TITLE_OFFSET + AEA_TITLE_SIZE - 1)
|
|
46
68
|
)
|
|
47
69
|
view.setUint32(AEA_FRAME_COUNT_OFFSET, frameCount, true)
|
|
48
70
|
header[AEA_CHANNEL_COUNT_OFFSET] = channelCount
|
|
@@ -81,7 +103,7 @@ export class AeaFile {
|
|
|
81
103
|
const titleEnd = header.indexOf(0, AEA_TITLE_OFFSET)
|
|
82
104
|
const titleLength =
|
|
83
105
|
titleEnd === -1 ? AEA_TITLE_SIZE : titleEnd - AEA_TITLE_OFFSET
|
|
84
|
-
const title =
|
|
106
|
+
const title = UTF8_DECODER.decode(
|
|
85
107
|
header.subarray(AEA_TITLE_OFFSET, AEA_TITLE_OFFSET + titleLength)
|
|
86
108
|
)
|
|
87
109
|
|
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
|
}
|