carta1 1.1.10 → 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/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 +68 -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 +19 -132
- package/codec/core/geometry.js +25 -0
- package/codec/core/tables.js +215 -0
- package/codec/dequantization.js +54 -0
- package/codec/index.js +12 -13
- 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 +49 -0
- package/codec/signal/fft.js +68 -0
- package/codec/{transforms → signal}/mdct.js +37 -65
- package/codec/signal/order.js +16 -0
- package/codec/signal/qmf.js +539 -0
- package/codec/state.js +112 -0
- package/codec/{io → syntax}/bitstream.js +27 -8
- package/codec/{io/serialization.js → syntax/frame.js} +5 -101
- package/codec/synthesis/bands.js +228 -0
- package/codec/synthesis/pcm.js +64 -0
- package/codec/synthesis/stage.js +17 -0
- package/codec/utils.js +9 -52
- 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 +1 -1
- package/codec/coding/quantization.js +0 -149
- package/codec/core/buffers.js +0 -81
- package/codec/io/processor.js +0 -671
- package/codec/transforms/fft.js +0 -69
- package/codec/transforms/qmf.js +0 -105
|
@@ -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 { MDCT } from '../signal/mdct.js'
|
|
15
|
+
import { reverseSpectrum } from '../signal/order.js'
|
|
16
|
+
import { throwError } from '../utils.js'
|
|
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
|
+
/**
|
|
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 = [middleMdct, middleMdct, longMdct]
|
|
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 = shortMdct.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
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 PCM boundary conversions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SAMPLE_RATE,
|
|
7
|
+
SAMPLES_PER_FRAME,
|
|
8
|
+
WAV_BITS_PER_SAMPLE,
|
|
9
|
+
WAV_BYTES_PER_SAMPLE,
|
|
10
|
+
WAV_DATA_OFFSET,
|
|
11
|
+
WAV_HEADER_SIZE,
|
|
12
|
+
WAV_PCM_MAX_NEGATIVE,
|
|
13
|
+
WAV_PCM_MAX_POSITIVE,
|
|
14
|
+
} from '../core/constants.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write a four-character code.
|
|
18
|
+
*
|
|
19
|
+
* @param {DataView} output
|
|
20
|
+
* @param {number} offset
|
|
21
|
+
* @param {string} value
|
|
22
|
+
*/
|
|
23
|
+
function writeFourCc(output, offset, value) {
|
|
24
|
+
for (let i = 0; i < value.length; i++) {
|
|
25
|
+
output.setUint8(offset + i, value.charCodeAt(i))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Join complete PCM frames into one channel.
|
|
31
|
+
*
|
|
32
|
+
* @param {Float32Array[]} frames
|
|
33
|
+
* @returns {Float32Array}
|
|
34
|
+
*/
|
|
35
|
+
export function joinChannelFrames(frames) {
|
|
36
|
+
const sampleCount = frames.reduce((total, frame) => total + frame.length, 0)
|
|
37
|
+
const channel = new Float32Array(sampleCount)
|
|
38
|
+
let offset = 0
|
|
39
|
+
|
|
40
|
+
for (const frame of frames) {
|
|
41
|
+
channel.set(frame, offset)
|
|
42
|
+
offset += frame.length
|
|
43
|
+
}
|
|
44
|
+
return channel
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split planar PCM buffers into complete zero-padded codec frames.
|
|
49
|
+
*
|
|
50
|
+
* @param {Float32Array[]} buffers
|
|
51
|
+
* @param {number} [frameSize=SAMPLES_PER_FRAME]
|
|
52
|
+
* @returns {Generator<Float32Array>|Generator<[Float32Array, Float32Array]>}
|
|
53
|
+
* @throws {Error} If the channel count is unsupported
|
|
54
|
+
*/
|
|
55
|
+
export function* frameBufferToFrames(buffers, frameSize = SAMPLES_PER_FRAME) {
|
|
56
|
+
if (
|
|
57
|
+
!Array.isArray(buffers) ||
|
|
58
|
+
(buffers.length !== 1 && buffers.length !== 2) ||
|
|
59
|
+
!buffers.every(
|
|
60
|
+
(buffer) =>
|
|
61
|
+
buffer instanceof Float32Array && buffer.length === buffers[0].length
|
|
62
|
+
) ||
|
|
63
|
+
!Number.isInteger(frameSize) ||
|
|
64
|
+
frameSize < 1
|
|
65
|
+
) {
|
|
66
|
+
throw new RangeError(
|
|
67
|
+
'ATRAC1 framing requires one or two equally sized Float32 channels and a positive integer frame size'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (buffers.length === 1) {
|
|
71
|
+
const [left] = buffers
|
|
72
|
+
for (let offset = 0; offset < left.length; offset += frameSize) {
|
|
73
|
+
const frame = new Float32Array(frameSize)
|
|
74
|
+
frame.set(left.subarray(offset, offset + frameSize))
|
|
75
|
+
yield frame
|
|
76
|
+
}
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (buffers.length === 2) {
|
|
81
|
+
const [left, right] = buffers
|
|
82
|
+
const sampleCount = left.length
|
|
83
|
+
for (let offset = 0; offset < sampleCount; offset += frameSize) {
|
|
84
|
+
const leftFrame = new Float32Array(frameSize)
|
|
85
|
+
const rightFrame = new Float32Array(frameSize)
|
|
86
|
+
leftFrame.set(left.subarray(offset, offset + frameSize))
|
|
87
|
+
rightFrame.set(right.subarray(offset, offset + frameSize))
|
|
88
|
+
yield [leftFrame, rightFrame]
|
|
89
|
+
}
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convert one normalized sample to signed PCM16.
|
|
96
|
+
*
|
|
97
|
+
* @param {number} sample
|
|
98
|
+
* @returns {number}
|
|
99
|
+
*/
|
|
100
|
+
export function floatToPcm16(sample) {
|
|
101
|
+
const clipped = Math.max(-1, Math.min(1, sample))
|
|
102
|
+
return clipped < 0
|
|
103
|
+
? clipped * WAV_PCM_MAX_NEGATIVE
|
|
104
|
+
: clipped * WAV_PCM_MAX_POSITIVE
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a PCM WAVE header.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} options
|
|
111
|
+
* @param {number} options.sampleCount
|
|
112
|
+
* @param {number} options.sampleRate
|
|
113
|
+
* @param {number} options.channels
|
|
114
|
+
* @returns {Uint8Array}
|
|
115
|
+
*/
|
|
116
|
+
export function createPcmWaveHeader({ sampleCount, sampleRate, channels }) {
|
|
117
|
+
const byteRate = sampleRate * channels * WAV_BYTES_PER_SAMPLE
|
|
118
|
+
const blockAlign = channels * WAV_BYTES_PER_SAMPLE
|
|
119
|
+
const dataSize = sampleCount * blockAlign
|
|
120
|
+
const output = new Uint8Array(WAV_HEADER_SIZE)
|
|
121
|
+
const view = new DataView(output.buffer)
|
|
122
|
+
|
|
123
|
+
writeFourCc(view, 0, 'RIFF')
|
|
124
|
+
view.setUint32(4, WAV_DATA_OFFSET + dataSize, true)
|
|
125
|
+
writeFourCc(view, 8, 'WAVE')
|
|
126
|
+
writeFourCc(view, 12, 'fmt ')
|
|
127
|
+
view.setUint32(16, 16, true)
|
|
128
|
+
view.setUint16(20, 1, true)
|
|
129
|
+
view.setUint16(22, channels, true)
|
|
130
|
+
view.setUint32(24, sampleRate, true)
|
|
131
|
+
view.setUint32(28, byteRate, true)
|
|
132
|
+
view.setUint16(32, blockAlign, true)
|
|
133
|
+
view.setUint16(34, WAV_BITS_PER_SAMPLE, true)
|
|
134
|
+
writeFourCc(view, 36, 'data')
|
|
135
|
+
view.setUint32(40, dataSize, true)
|
|
136
|
+
return output
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Interleave planar normalized channels as PCM16.
|
|
141
|
+
*
|
|
142
|
+
* @param {Float32Array[]} channels
|
|
143
|
+
* @returns {Uint8Array}
|
|
144
|
+
*/
|
|
145
|
+
export function interleavePcm16(channels) {
|
|
146
|
+
if (
|
|
147
|
+
!Array.isArray(channels) ||
|
|
148
|
+
channels.length < 1 ||
|
|
149
|
+
!channels.every(
|
|
150
|
+
(channel) =>
|
|
151
|
+
channel instanceof Float32Array && channel.length === channels[0].length
|
|
152
|
+
)
|
|
153
|
+
) {
|
|
154
|
+
throw new RangeError('PCM must contain equally sized Float32 channels')
|
|
155
|
+
}
|
|
156
|
+
const sampleCount = channels[0].length
|
|
157
|
+
const output = new Uint8Array(
|
|
158
|
+
sampleCount * channels.length * WAV_BYTES_PER_SAMPLE
|
|
159
|
+
)
|
|
160
|
+
const view = new DataView(output.buffer)
|
|
161
|
+
let offset = 0
|
|
162
|
+
|
|
163
|
+
for (let sample = 0; sample < sampleCount; sample++) {
|
|
164
|
+
for (const channel of channels) {
|
|
165
|
+
view.setInt16(offset, floatToPcm16(channel[sample]), true)
|
|
166
|
+
offset += WAV_BYTES_PER_SAMPLE
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return output
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a complete PCM WAVE byte image from planar channels.
|
|
174
|
+
*
|
|
175
|
+
* @param {Float32Array[]} channels
|
|
176
|
+
* @param {Object} [options]
|
|
177
|
+
* @param {number} [options.sampleRate=SAMPLE_RATE]
|
|
178
|
+
* @returns {Uint8Array}
|
|
179
|
+
*/
|
|
180
|
+
export function createPcmWave(channels, options = {}) {
|
|
181
|
+
const { sampleRate = SAMPLE_RATE } = options
|
|
182
|
+
const pcm = interleavePcm16(channels)
|
|
183
|
+
const sampleCount = channels[0].length
|
|
184
|
+
const header = createPcmWaveHeader({
|
|
185
|
+
sampleCount,
|
|
186
|
+
sampleRate,
|
|
187
|
+
channels: channels.length,
|
|
188
|
+
})
|
|
189
|
+
const output = new Uint8Array(header.length + pcm.length)
|
|
190
|
+
output.set(header)
|
|
191
|
+
output.set(pcm, header.length)
|
|
192
|
+
return output
|
|
193
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta1 public processing facade.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AEA_HEADER_SIZE,
|
|
7
|
+
SAMPLE_RATE,
|
|
8
|
+
SAMPLES_PER_FRAME,
|
|
9
|
+
SOUND_UNIT_SIZE,
|
|
10
|
+
STREAM_FLUSH_TAIL_FRAMES,
|
|
11
|
+
STREAM_PRIMING_SAMPLES,
|
|
12
|
+
} from '../core/constants.js'
|
|
13
|
+
import { deserializeFrame, serializeFrame } from '../syntax/frame.js'
|
|
14
|
+
import { AeaFile } from './container.js'
|
|
15
|
+
import { createPcmWave, frameBufferToFrames, joinChannelFrames } from './pcm.js'
|
|
16
|
+
import {
|
|
17
|
+
createAeaStreamingDecoder,
|
|
18
|
+
createAeaStreamingEncoder,
|
|
19
|
+
decodeAeaPcm,
|
|
20
|
+
encodeAeaPcm,
|
|
21
|
+
} from './stream.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* High-level audio processing facade.
|
|
25
|
+
*/
|
|
26
|
+
export class AudioProcessor {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Float32Array[]} channels
|
|
29
|
+
* @param {object} [options]
|
|
30
|
+
* @returns {Promise<Uint8Array>}
|
|
31
|
+
*/
|
|
32
|
+
static encodeAeaPcm(channels, options = {}) {
|
|
33
|
+
return encodeAeaPcm(channels, options)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {Uint8Array|ArrayBuffer|Blob} input
|
|
38
|
+
* @returns {Promise<Float32Array[]>}
|
|
39
|
+
*/
|
|
40
|
+
static decodeAeaPcm(input) {
|
|
41
|
+
return decodeAeaPcm(input)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {AsyncIterable<object>} audioFrames
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @returns {AsyncGenerator<object>}
|
|
48
|
+
*/
|
|
49
|
+
static async *encodeStream(audioFrames, options = {}) {
|
|
50
|
+
const encoder = createAeaStreamingEncoder(options)
|
|
51
|
+
yield* encoder.frames(audioFrames)
|
|
52
|
+
yield* encoder.finish()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {AsyncIterable<object>} encodedFrames
|
|
57
|
+
* @param {object} [options]
|
|
58
|
+
* @returns {AsyncGenerator<object>}
|
|
59
|
+
*/
|
|
60
|
+
static async *decodeStream(encodedFrames, options = {}) {
|
|
61
|
+
const decoder = createAeaStreamingDecoder(options)
|
|
62
|
+
yield* decoder.frames(encodedFrames)
|
|
63
|
+
yield* decoder.finish()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Float32Array[]} buffers
|
|
68
|
+
* @param {number} [frameSize]
|
|
69
|
+
* @returns {Generator<Float32Array>|Generator<[Float32Array, Float32Array]>}
|
|
70
|
+
*/
|
|
71
|
+
static *frameBufferToFrames(buffers, frameSize) {
|
|
72
|
+
yield* frameBufferToFrames(buffers, frameSize)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {AsyncIterable|Iterable} frameStream
|
|
77
|
+
* @returns {Promise<Array>}
|
|
78
|
+
*/
|
|
79
|
+
static async collectFrames(frameStream) {
|
|
80
|
+
const frames = []
|
|
81
|
+
for await (const frame of frameStream) {
|
|
82
|
+
frames.push(frame)
|
|
83
|
+
}
|
|
84
|
+
return frames
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {AsyncIterable<object>} encodedFrames
|
|
89
|
+
* @param {object} [options]
|
|
90
|
+
* @returns {Promise<Blob>}
|
|
91
|
+
*/
|
|
92
|
+
static async createAeaBlob(encodedFrames, options = {}) {
|
|
93
|
+
const { title = 'encoded by atrac1.js', channelCount = 1 } = options
|
|
94
|
+
const frames = []
|
|
95
|
+
for await (const frame of encodedFrames) {
|
|
96
|
+
frames.push(serializeFrame(frame))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const timelineFrames = frames.length / channelCount
|
|
100
|
+
const sampleCount =
|
|
101
|
+
options.sampleCount ??
|
|
102
|
+
Math.max(0, timelineFrames - STREAM_FLUSH_TAIL_FRAMES) * SAMPLES_PER_FRAME
|
|
103
|
+
const header = AeaFile.createHeader(
|
|
104
|
+
title,
|
|
105
|
+
frames.length,
|
|
106
|
+
channelCount,
|
|
107
|
+
sampleCount,
|
|
108
|
+
STREAM_PRIMING_SAMPLES
|
|
109
|
+
)
|
|
110
|
+
return new Blob([header, ...frames], { type: 'application/octet-stream' })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {Blob} blob
|
|
115
|
+
* @returns {Promise<object>} Parsed AEA data.
|
|
116
|
+
*/
|
|
117
|
+
static async parseAeaBlob(blob) {
|
|
118
|
+
const buffer = await blob.arrayBuffer()
|
|
119
|
+
const header = new Uint8Array(buffer.slice(0, AEA_HEADER_SIZE))
|
|
120
|
+
const info = AeaFile.parseHeader(header)
|
|
121
|
+
const frameData = []
|
|
122
|
+
|
|
123
|
+
for (let offset = AEA_HEADER_SIZE; offset < buffer.byteLength;) {
|
|
124
|
+
const frame = new Uint8Array(
|
|
125
|
+
buffer.slice(offset, offset + SOUND_UNIT_SIZE)
|
|
126
|
+
)
|
|
127
|
+
if (frame.length === SOUND_UNIT_SIZE) {
|
|
128
|
+
frameData.push(frame)
|
|
129
|
+
}
|
|
130
|
+
offset += SOUND_UNIT_SIZE
|
|
131
|
+
}
|
|
132
|
+
return { info, frameData }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {Uint8Array[]} frameData
|
|
137
|
+
* @returns {Generator<object>}
|
|
138
|
+
*/
|
|
139
|
+
static *deserializedFrameStream(frameData) {
|
|
140
|
+
for (const frame of frameData) {
|
|
141
|
+
yield deserializeFrame(frame)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames
|
|
147
|
+
* @param {number} [channelCount=1]
|
|
148
|
+
* @param {number} [sampleRate=SAMPLE_RATE]
|
|
149
|
+
* @returns {Blob}
|
|
150
|
+
*/
|
|
151
|
+
static createPcmWaveBlob(
|
|
152
|
+
pcmFrames,
|
|
153
|
+
channelCount = 1,
|
|
154
|
+
sampleRate = SAMPLE_RATE
|
|
155
|
+
) {
|
|
156
|
+
let channels
|
|
157
|
+
if (channelCount === 1) {
|
|
158
|
+
channels = [joinChannelFrames(pcmFrames)]
|
|
159
|
+
} else if (channelCount === 2) {
|
|
160
|
+
channels = [
|
|
161
|
+
joinChannelFrames(pcmFrames.map(([left]) => left)),
|
|
162
|
+
joinChannelFrames(pcmFrames.map(([, right]) => right)),
|
|
163
|
+
]
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
166
|
+
}
|
|
167
|
+
return new Blob([createPcmWave(channels, { sampleRate })], {
|
|
168
|
+
type: 'audio/wav',
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Preserve the supported WAVE facade name.
|
|
174
|
+
*
|
|
175
|
+
* @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames
|
|
176
|
+
* @param {number} [channelCount=1]
|
|
177
|
+
* @param {number} [sampleRate=SAMPLE_RATE]
|
|
178
|
+
* @returns {Blob}
|
|
179
|
+
*/
|
|
180
|
+
static createWavBlob(pcmFrames, channelCount = 1, sampleRate = SAMPLE_RATE) {
|
|
181
|
+
return AudioProcessor.createPcmWaveBlob(pcmFrames, channelCount, sampleRate)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Assemble PCM frames using the supported interleaved result shape.
|
|
186
|
+
*
|
|
187
|
+
* @param {Float32Array[]|[Float32Array, Float32Array][]} pcmFrames
|
|
188
|
+
* @param {number} channelCount
|
|
189
|
+
* @returns {Float32Array}
|
|
190
|
+
*/
|
|
191
|
+
static assemblePcmFrames(pcmFrames, channelCount) {
|
|
192
|
+
if (channelCount === 1) {
|
|
193
|
+
return joinChannelFrames(pcmFrames)
|
|
194
|
+
}
|
|
195
|
+
if (channelCount !== 2) {
|
|
196
|
+
throw new Error(`Unsupported channel count: ${channelCount}`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const left = joinChannelFrames(pcmFrames.map(([channel]) => channel))
|
|
200
|
+
const right = joinChannelFrames(pcmFrames.map(([, channel]) => channel))
|
|
201
|
+
const pcm = new Float32Array(Math.max(left.length, right.length) * 2)
|
|
202
|
+
for (let i = 0; i < pcm.length / 2; i++) {
|
|
203
|
+
pcm[i * 2] = left[i] ?? 0
|
|
204
|
+
pcm[i * 2 + 1] = right[i] ?? 0
|
|
205
|
+
}
|
|
206
|
+
return pcm
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import fs from 'fs'
|
|
9
|
-
import { AeaFile
|
|
9
|
+
import { AeaFile } from './container.js'
|
|
10
|
+
import { deserializeFrame } from '../syntax/frame.js'
|
|
10
11
|
import { AEA_HEADER_SIZE, SOUND_UNIT_SIZE } from '../core/constants.js'
|
|
11
12
|
|
|
12
13
|
/**
|