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 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 the AEA title while encoding. |
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 length and ASCII encoding
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
- if (!title) {
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>',
@@ -2,9 +2,34 @@
2
2
  * Carta1 QMF analysis application.
3
3
  */
4
4
 
5
- import { analyzeQmfPair } from '../signal/bands.js'
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.qmfDelays
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 = analyzeQmfPair(
26
- pcmSamples,
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 stage2 = analyzeQmfPair(
33
- stage1.lowBand,
34
- delays.midBand,
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
- 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)
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.lowBand, stage2.highBand, highBand] }
66
+ return { bands: [stage2[0], stage2[1], highBand] }
48
67
  }
49
68
  }
@@ -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.fft(real, imag)
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
- mdct64,
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 = [mdct256, mdct256, mdct512]
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 = mdct64.transform(mdctInput, bufferPool.mdctBuffers)
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
- 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
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 = new TextDecoder().decode(
106
+ const title = UTF8_DECODER.decode(
85
107
  header.subarray(AEA_TITLE_OFFSET, AEA_TITLE_OFFSET + titleLength)
86
108
  )
87
109
 
@@ -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 QMF_DELAY = 46
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
@@ -2,7 +2,14 @@
2
2
  * Stable ATRAC1 geometry, signal, allocation, and quantization tables.
3
3
  */
4
4
 
5
- import { MAX_WORD_LENGTH_INDEX, QMF_TAPS } from './constants.js'
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 },
@@ -8,7 +8,7 @@ import {
8
8
  BFU_START_SHORT,
9
9
  WORD_LENGTH_BITS,
10
10
  } from './core/tables.js'
11
- import { dequantize as dequantizeCoefficient } from './quantization/spectrum.js'
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 = dequantizeCoefficient(
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 as qmfAnalysisStage } from './analysis/bands.js'
11
- import { transformSpectrum as mdctStage } from './analysis/spectrum.js'
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 { quantize as quantizeCoefficient } from './spectrum.js'
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
- quantizeCoefficient(data, scaleFactorIndices[bfu], bitsPerSample)
33
+ spectrumQuantization.quantize(
34
+ data,
35
+ scaleFactorIndices[bfu],
36
+ bitsPerSample
37
+ )
34
38
  )
35
39
  }
36
40
 
@@ -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
- * Perform in-place FFT on complex data
11
- * @param {Float32Array} real - Real part of input/output data
12
- * @param {Float32Array} imag - Imaginary part of input/output data
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 fft(real, imag) {
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 temp = i
24
- for (let b = 0; b < bits; b++) {
25
- reversed = (reversed << 1) | (temp & 1)
26
- temp >>= 1
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 > i) {
29
- ;[real[i], real[reversed]] = [real[reversed], real[i]]
30
- ;[imag[i], imag[reversed]] = [imag[reversed], imag[i]]
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 wReal = Math.cos(angle)
39
- const wImag = Math.sin(angle)
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 twiddleImag = 0
40
+ let twiddleImaginary = 0
44
41
 
45
- for (let k = 0; k < halfStride; k++) {
46
- const evenIndex = start + k
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 evenImag = imag[evenIndex]
46
+ const evenImaginary = imaginary[evenIndex]
51
47
  const oddReal = real[oddIndex]
52
- const oddImag = imag[oddIndex]
53
-
54
- const tReal = oddReal * twiddleReal - oddImag * twiddleImag
55
- const tImag = oddReal * twiddleImag + oddImag * twiddleReal
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 + tReal
58
- imag[evenIndex] = evenImag + tImag
59
- real[oddIndex] = evenReal - tReal
60
- imag[oddIndex] = evenImag - tImag
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 = twiddleReal * wReal - twiddleImag * wImag
63
- twiddleImag = twiddleReal * wImag + twiddleImag * wReal
59
+ const nextReal =
60
+ twiddleReal * stepReal - twiddleImaginary * stepImaginary
61
+ twiddleImaginary =
62
+ twiddleReal * stepImaginary + twiddleImaginary * stepReal
64
63
  twiddleReal = nextReal
65
64
  }
66
65
  }