summaryrefslogtreecommitdiff
path: root/tools/spectral_editor/script.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/spectral_editor/script.js')
-rw-r--r--tools/spectral_editor/script.js195
1 files changed, 57 insertions, 138 deletions
diff --git a/tools/spectral_editor/script.js b/tools/spectral_editor/script.js
index cafd7e9..70e55ec 100644
--- a/tools/spectral_editor/script.js
+++ b/tools/spectral_editor/script.js
@@ -359,19 +359,23 @@ function audioToSpectrogram(audioData) {
const spectrogram = new Float32Array(DCT_SIZE * numFrames);
const window = hanningWindowArray;
+ // Reuse single buffer for all frames (eliminates N-1 allocations)
+ const frameBuffer = new Float32Array(DCT_SIZE);
+
for (let frameIdx = 0; frameIdx < numFrames; frameIdx++) {
const frameStart = frameIdx * hopSize;
- const frame = new Float32Array(DCT_SIZE);
- // Extract windowed frame
+ // Extract windowed frame into reused buffer
for (let i = 0; i < DCT_SIZE; i++) {
if (frameStart + i < audioData.length) {
- frame[i] = audioData[frameStart + i] * window[i];
+ frameBuffer[i] = audioData[frameStart + i] * window[i];
+ } else {
+ frameBuffer[i] = 0; // Zero-pad if needed
}
}
// Compute DCT (forward transform)
- const dctCoeffs = javascript_dct_512(frame);
+ const dctCoeffs = javascript_dct_512(frameBuffer);
// Store in spectrogram
for (let b = 0; b < DCT_SIZE; b++) {
@@ -437,17 +441,10 @@ function addCurve() {
];
const color = colors[state.curves.length % colors.length];
- const curve = {
- id: state.nextCurveId++,
- controlPoints: [], // Empty initially, user will place points
- profile: {
- type: 'gaussian',
- param1: 30.0, // sigma
- param2: 0.0
- },
- color: color,
- volume: 1.0 // Per-curve volume multiplier (0.0-1.0)
- };
+ // Create new Curve instance with current dimensions
+ const numFrames = state.referenceNumFrames || 100;
+ const curve = new Curve(state.nextCurveId++, state.referenceDctSize, numFrames);
+ curve.setColor(color);
state.curves.push(curve);
state.selectedCurveId = curve.id;
@@ -543,8 +540,8 @@ function updateCurveUI() {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (curve) {
document.getElementById('profileType').value = curve.profile.type;
- document.getElementById('sigmaSlider').value = curve.profile.param1;
- document.getElementById('sigmaValue').value = curve.profile.param1;
+ document.getElementById('sigmaSlider').value = curve.profile.sigma;
+ document.getElementById('sigmaValue').value = curve.profile.sigma;
// Update curve volume slider
const volumePercent = Math.round(curve.volume * 100);
@@ -594,7 +591,7 @@ function onProfileChanged(e) {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (!curve) return;
- curve.profile.type = e.target.value;
+ curve.setProfileType(e.target.value);
// Update label based on profile type
const label = document.getElementById('sigmaLabel');
@@ -616,8 +613,8 @@ function onSigmaChanged(e) {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (!curve) return;
- curve.profile.param1 = parseFloat(e.target.value);
- document.getElementById('sigmaValue').value = curve.profile.param1;
+ curve.setProfileSigma(parseFloat(e.target.value));
+ document.getElementById('sigmaValue').value = curve.profile.sigma;
render();
}
@@ -628,8 +625,8 @@ function onSigmaValueChanged(e) {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (!curve) return;
- curve.profile.param1 = parseFloat(e.target.value);
- document.getElementById('sigmaSlider').value = curve.profile.param1;
+ curve.setProfileSigma(parseFloat(e.target.value));
+ document.getElementById('sigmaSlider').value = curve.profile.sigma;
render();
}
@@ -672,7 +669,7 @@ function onCurveVolumeChanged(e) {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (!curve) return;
- curve.volume = parseFloat(e.target.value) / 100.0; // Convert 0-100 to 0.0-1.0
+ curve.setVolume(parseFloat(e.target.value) / 100.0); // Convert 0-100 to 0.0-1.0
document.getElementById('curveVolumeValue').value = e.target.value;
render();
@@ -684,7 +681,7 @@ function onCurveVolumeValueChanged(e) {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (!curve) return;
- curve.volume = parseFloat(e.target.value) / 100.0;
+ curve.setVolume(parseFloat(e.target.value) / 100.0);
document.getElementById('curveVolumeSlider').value = e.target.value;
render();
@@ -721,7 +718,7 @@ function onCanvasMouseDown(e) {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (curve) {
const point = screenToSpectrogram(x, y);
- curve.controlPoints.push(point);
+ curve.addControlPoint(point);
// Sort by frame
curve.controlPoints.sort((a, b) => a.frame - b.frame);
@@ -750,9 +747,7 @@ function onCanvasMouseMove(e) {
// Update point position
const newPoint = screenToSpectrogram(x, y);
- point.frame = newPoint.frame;
- point.freqHz = newPoint.freqHz;
- point.amplitude = newPoint.amplitude;
+ curve.updateControlPoint(state.selectedControlPointIdx, newPoint);
// Re-sort by frame
curve.controlPoints.sort((a, b) => a.frame - b.frame);
@@ -781,7 +776,7 @@ function onCanvasRightClick(e) {
if (clickedPoint) {
const curve = state.curves.find(c => c.id === clickedPoint.curveId);
if (curve) {
- curve.controlPoints.splice(clickedPoint.pointIdx, 1);
+ curve.deleteControlPoint(clickedPoint.pointIdx);
state.selectedControlPointIdx = null;
saveHistoryState('Delete control point');
@@ -817,7 +812,7 @@ function deleteSelectedControlPoint() {
const curve = state.curves.find(c => c.id === state.selectedCurveId);
if (curve && state.selectedControlPointIdx < curve.controlPoints.length) {
- curve.controlPoints.splice(state.selectedControlPointIdx, 1);
+ curve.deleteControlPoint(state.selectedControlPointIdx);
state.selectedControlPointIdx = null;
saveHistoryState('Delete control point');
@@ -1115,9 +1110,8 @@ function drawProceduralSpectrogram(ctx) {
offscreen.height = state.canvasHeight;
const offscreenCtx = offscreen.getContext('2d');
- // Generate spectrogram for this curve only
- const curveSpec = new Float32Array(state.referenceDctSize * numFrames);
- drawCurveToSpectrogram(curve, curveSpec, state.referenceDctSize, numFrames);
+ // Get cached spectrogram for this curve (computed only if dirty)
+ const curveSpec = curve.getSpectrogram();
// Parse curve color (hex to RGB)
const color = hexToRgb(curve.color || '#0e639c');
@@ -1260,103 +1254,21 @@ function drawFrequencyAxis(ctx) {
function generateProceduralSpectrogram(numFrames) {
const spectrogram = new Float32Array(state.referenceDctSize * numFrames);
- // For each curve, draw its contribution
+ // For each curve, add its cached spectrogram contribution
state.curves.forEach(curve => {
- drawCurveToSpectrogram(curve, spectrogram, state.referenceDctSize, numFrames);
+ const curveSpec = curve.getSpectrogram();
+ for (let i = 0; i < spectrogram.length; i++) {
+ spectrogram[i] += curveSpec[i];
+ }
});
return spectrogram;
}
-function drawCurveToSpectrogram(curve, spectrogram, dctSize, numFrames) {
- if (curve.controlPoints.length === 0) return;
-
- // Find the frame range covered by control points
- const frames = curve.controlPoints.map(p => p.frame);
- const minFrame = Math.max(0, Math.min(...frames)); // Clamp to valid range
- const maxFrame = Math.min(numFrames - 1, Math.max(...frames));
-
- // Amplitude scaling factor to match typical DCT coefficient magnitudes
- // Increased from 10.0 to 50.0 for better audibility
- const AMPLITUDE_SCALE = 50.0;
-
- // Apply curve volume to the amplitude
- const curveVolume = curve.volume || 1.0;
-
- // Only iterate over the range where control points exist
- for (let frame = minFrame; frame <= maxFrame; frame++) {
- // Evaluate Bezier curve at this frame
- const freqHz = evaluateBezierLinear(curve.controlPoints, frame, 'freqHz');
- const amplitude = evaluateBezierLinear(curve.controlPoints, frame, 'amplitude');
-
- // Convert freq to bin
- const freqBin0 = (freqHz / (SAMPLE_RATE / 2)) * dctSize;
-
- // Apply vertical profile
- for (let bin = 0; bin < dctSize; bin++) {
- const dist = Math.abs(bin - freqBin0);
- const profileValue = evaluateProfile(curve.profile, dist);
-
- const idx = frame * dctSize + bin;
- spectrogram[idx] += amplitude * profileValue * AMPLITUDE_SCALE * curveVolume;
- }
- }
-}
-
-function evaluateBezierLinear(controlPoints, frame, property) {
- if (controlPoints.length === 0) return 0;
- if (controlPoints.length === 1) return controlPoints[0][property];
-
- const frames = controlPoints.map(p => p.frame);
- const values = controlPoints.map(p => p[property]);
-
- // Clamp to range
- if (frame <= frames[0]) return values[0];
- if (frame >= frames[frames.length - 1]) return values[values.length - 1];
-
- // Find segment
- for (let i = 0; i < frames.length - 1; i++) {
- if (frame >= frames[i] && frame <= frames[i + 1]) {
- const t = (frame - frames[i]) / (frames[i + 1] - frames[i]);
- return values[i] * (1 - t) + values[i + 1] * t;
- }
- }
-
- return values[values.length - 1];
-}
-
-function evaluateProfile(profile, distance) {
- switch (profile.type) {
- case 'gaussian': {
- const sigma = profile.param1;
- return Math.exp(-(distance * distance) / (sigma * sigma));
- }
-
- case 'decaying_sinusoid': {
- const decay = profile.param1;
- const omega = profile.param2 || 0.5;
- return Math.exp(-decay * distance) * Math.cos(omega * distance);
- }
-
- case 'noise': {
- const amplitude = profile.param1;
- const decay = profile.param2 || 30.0; // Decay rate (like sigma for Gaussian)
-
- // Deterministic noise based on distance
- const seed = 1234;
- const hash = Math.floor((seed + distance * 17.13) * 1000) % 10000;
- const noise = (hash / 10000) * 2.0 - 1.0; // Random value: -1 to +1
-
- // Apply exponential decay (like Gaussian)
- const decayFactor = Math.exp(-(distance * distance) / (decay * decay));
-
- return amplitude * noise * decayFactor;
- }
-
- default:
- return 0;
- }
-}
+// NOTE: drawCurveToSpectrogram, evaluateBezierLinear, and evaluateProfile
+// have been moved to the Curve class in curve.js for better encapsulation
+// and caching. The Curve class maintains a cached spectrogram that is only
+// recomputed when the curve is marked dirty (parameters changed).
// ============================================================================
// Audio Playback
@@ -1520,7 +1432,10 @@ function drawSpectrumViewer() {
const numFrames = state.referenceNumFrames || 100;
const fullProcSpec = new Float32Array(state.referenceDctSize * numFrames);
state.curves.forEach(curve => {
- drawCurveToSpectrogram(curve, fullProcSpec, state.referenceDctSize, numFrames);
+ const curveSpec = curve.getSpectrogram();
+ for (let i = 0; i < fullProcSpec.length; i++) {
+ fullProcSpec[i] += curveSpec[i];
+ }
});
// Extract just this frame
@@ -1562,11 +1477,9 @@ function spectrogramToAudio(spectrogram, dctSize, numFrames) {
const window = hanningWindowArray;
for (let frameIdx = 0; frameIdx < numFrames; frameIdx++) {
- // Extract frame (no windowing - window is only for analysis, not synthesis)
- const frame = new Float32Array(dctSize);
- for (let b = 0; b < dctSize; b++) {
- frame[b] = spectrogram[frameIdx * dctSize + b];
- }
+ // Extract frame directly using subarray (no copy - IDCT doesn't modify input)
+ const pos = frameIdx * dctSize;
+ const frame = spectrogram.subarray(pos, pos + dctSize);
// IDCT
const timeFrame = javascript_idct_512(frame);
@@ -1591,10 +1504,10 @@ function saveHistoryState(action) {
// Remove any states after current index
state.history = state.history.slice(0, state.historyIndex + 1);
- // Save current state
+ // Save current state (serialize Curve instances to JSON)
const snapshot = {
action,
- curves: JSON.parse(JSON.stringify(state.curves)),
+ curves: state.curves.map(c => c.toJSON()),
selectedCurveId: state.selectedCurveId
};
@@ -1616,7 +1529,10 @@ function undo() {
state.historyIndex--;
const snapshot = state.history[state.historyIndex];
- state.curves = JSON.parse(JSON.stringify(snapshot.curves));
+ // Restore curves from JSON (reconstruct Curve instances)
+ state.curves = snapshot.curves.map(json =>
+ Curve.fromJSON(json, state.referenceDctSize, state.referenceNumFrames || 100)
+ );
state.selectedCurveId = snapshot.selectedCurveId;
state.selectedControlPointIdx = null;
@@ -1633,7 +1549,10 @@ function redo() {
state.historyIndex++;
const snapshot = state.history[state.historyIndex];
- state.curves = JSON.parse(JSON.stringify(snapshot.curves));
+ // Restore curves from JSON (reconstruct Curve instances)
+ state.curves = snapshot.curves.map(json =>
+ Curve.fromJSON(json, state.referenceDctSize, state.referenceNumFrames || 100)
+ );
state.selectedCurveId = snapshot.selectedCurveId;
state.selectedControlPointIdx = null;
@@ -1676,11 +1595,11 @@ function generateProceduralParamsText() {
text += ` PROFILE ${curve.profile.type}`;
if (curve.profile.type === 'gaussian') {
- text += ` sigma=${curve.profile.param1.toFixed(1)}`;
+ text += ` sigma=${curve.profile.sigma.toFixed(1)}`;
} else if (curve.profile.type === 'decaying_sinusoid') {
- text += ` decay=${curve.profile.param1.toFixed(2)} frequency=${curve.profile.param2.toFixed(2)}`;
+ text += ` decay=${curve.profile.sigma.toFixed(2)} frequency=${curve.profile.param2.toFixed(2)}`;
} else if (curve.profile.type === 'noise') {
- text += ` amplitude=${curve.profile.param1.toFixed(2)} seed=${curve.profile.param2.toFixed(0)}`;
+ text += ` amplitude=${curve.profile.sigma.toFixed(2)} seed=${curve.profile.param2.toFixed(0)}`;
}
text += '\n';
@@ -1747,7 +1666,7 @@ function generateCppCodeText() {
code += ` draw_bezier_curve_add(spec, dct_size, num_frames,\n`;
}
code += ` frames, freqs, amps, ${numPoints},\n`;
- code += ` ${profileEnum}, ${curve.profile.param1.toFixed(2)}f`;
+ code += ` ${profileEnum}, ${curve.profile.sigma.toFixed(2)}f`;
if (curve.profile.type === 'decaying_sinusoid' || curve.profile.type === 'noise') {
code += `, ${curve.profile.param2.toFixed(2)}f`;