summaryrefslogtreecommitdiff
path: root/tools/mq_editor
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mq_editor')
-rw-r--r--tools/mq_editor/README.md2
-rw-r--r--tools/mq_editor/app.js16
-rw-r--r--tools/mq_editor/editor.js25
-rw-r--r--tools/mq_editor/mq_extract.js19
-rw-r--r--tools/mq_editor/mq_synth.js20
-rw-r--r--tools/mq_editor/viewer.js7
6 files changed, 40 insertions, 49 deletions
diff --git a/tools/mq_editor/README.md b/tools/mq_editor/README.md
index 7d7d06b..3827d4f 100644
--- a/tools/mq_editor/README.md
+++ b/tools/mq_editor/README.md
@@ -180,7 +180,7 @@ y[n] = 2r·cos(ω₀)·y[n-1] − r²·y[n-2] + A(t)·√(1−r²)·noise[n]
| `gain` | 1.0 | [0, ∞) | Output multiplier on top of power normalization. |
**Coefficient translation from spread:**
-`r = exp(−π · BW / SR)` where `BW = f₀ · (spread_above + spread_below) / 2`.
+`r = exp(−π · BW / SR)` where `BW = f₀ · spread`.
For a partial at 440 Hz with `spread = 0.02`: `BW ≈ 8.8 Hz`, `r ≈ exp(−π·8.8/32000) ≈ 0.9991`.
**When to use:**
diff --git a/tools/mq_editor/app.js b/tools/mq_editor/app.js
index d54e5f3..380bb12 100644
--- a/tools/mq_editor/app.js
+++ b/tools/mq_editor/app.js
@@ -231,10 +231,9 @@ function loadAudioBuffer(buffer, label) {
viewer.onExploreCommit = (partial) => {
if (!extractedPartials) extractedPartials = [];
pushUndo();
- const {spread_above, spread_below} = autodetectSpread(partial, stftCache, fftSize, audioBuffer.sampleRate);
- if (!partial.harmonics) partial.harmonics = { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 };
- partial.harmonics.spread_above = spread_above;
- partial.harmonics.spread_below = spread_below;
+ const {spread} = autodetectSpread(partial, stftCache, fftSize, audioBuffer.sampleRate);
+ if (!partial.harmonics) partial.harmonics = { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread: 0.02 };
+ partial.harmonics.spread = spread;
extractedPartials.unshift(partial);
refreshPartialsView(0);
setStatus(`${exploreMode}: added partial (${extractedPartials.length} total)`, 'info');
@@ -345,7 +344,7 @@ function createNewPartial() {
v0: 440, v1: 440, v2: 440, v3: 440,
a0: 1.0, a1: 1.0, a2: 1.0, a3: 1.0,
},
- harmonics: { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 },
+ harmonics: { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread: 0.02 },
};
extractedPartials.unshift(newPartial);
refreshPartialsView(0);
@@ -369,12 +368,11 @@ function autoSpreadAll() {
if (!extractedPartials || !stftCache) return;
const fs = stftCache.fftSize;
const sr = audioBuffer.sampleRate;
- const defaults = { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 };
+ const defaults = { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread: 0.02 };
for (const p of extractedPartials) {
- const {spread_above, spread_below} = autodetectSpread(p, stftCache, fs, sr);
+ const {spread} = autodetectSpread(p, stftCache, fs, sr);
if (!p.harmonics) p.harmonics = { ...defaults };
- p.harmonics.spread_above = spread_above;
- p.harmonics.spread_below = spread_below;
+ p.harmonics.spread = spread;
}
if (viewer) viewer.render();
const sel = viewer ? viewer.selectedPartial : -1;
diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js
index b07664e..98c92e5 100644
--- a/tools/mq_editor/editor.js
+++ b/tools/mq_editor/editor.js
@@ -124,7 +124,7 @@ class PartialEditor {
const grid = this._synthGrid;
grid.innerHTML = '';
- const harmDefaults = { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 };
+ const harmDefaults = { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread: 0.02 };
const resDefaults = { r: 0.995, gainComp: 1.0 };
const isResonator = !!(partial.resonator && partial.resonator.enabled);
@@ -158,11 +158,10 @@ class PartialEditor {
const harm = partial.harmonics || {};
const sinParams = [
- { key: 'decay', label: 'h.decay', step: '0.01', max: '0.90' },
- { key: 'freq_mult', label: 'h.freq', step: '0.01' },
- { key: 'jitter', label: 'jitter', step: '0.001' },
- { key: 'spread_above', label: 'spread ↑', step: '0.001' },
- { key: 'spread_below', label: 'spread ↓', step: '0.001' },
+ { key: 'decay', label: 'h.decay', step: '0.01', max: '0.90' },
+ { key: 'freq_mult', label: 'h.freq', step: '0.01' },
+ { key: 'jitter', label: 'jitter', step: '0.001' },
+ { key: 'spread', label: 'spread', step: '0.001' },
];
const sinInputs = {};
for (const p of sinParams) {
@@ -206,22 +205,20 @@ class PartialEditor {
// Auto-detect spread button
const autoLbl = document.createElement('span');
- autoLbl.textContent = 'spread';
+ autoLbl.textContent = '';
const autoBtn = document.createElement('button');
- autoBtn.textContent = 'Auto';
- autoBtn.title = 'Infer spread_above/below from frequency variance around the bezier curve';
+ autoBtn.textContent = 'Auto spread';
+ autoBtn.title = 'Infer spread from half-power bandwidth around the bezier curve';
autoBtn.addEventListener('click', () => {
if (!this.partials) return;
const p = this.partials[index];
const sc = this.viewer ? this.viewer.stftCache : null;
const sr = this.viewer ? this.viewer.audioBuffer.sampleRate : 44100;
const fs = sc ? sc.fftSize : 2048;
- const {spread_above, spread_below} = autodetectSpread(p, sc, fs, sr);
+ const {spread} = autodetectSpread(p, sc, fs, sr);
if (!p.harmonics) p.harmonics = { ...harmDefaults };
- p.harmonics.spread_above = spread_above;
- p.harmonics.spread_below = spread_below;
- sinInputs['spread_above'].value = spread_above.toFixed(4);
- sinInputs['spread_below'].value = spread_below.toFixed(4);
+ p.harmonics.spread = spread;
+ sinInputs['spread'].value = spread.toFixed(4);
});
sinSection.appendChild(autoLbl);
sinSection.appendChild(autoBtn);
diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js
index 47c21b9..0940cf1 100644
--- a/tools/mq_editor/mq_extract.js
+++ b/tools/mq_editor/mq_extract.js
@@ -270,13 +270,12 @@ function expandPartialsLeft(partials, frames) {
}
}
-// Autodetect spread_above / spread_below from the spectrogram.
-// For each (subsampled) STFT frame within the partial, measures the
-// half-power (-3dB) width of the spectral peak above and below the center.
-// spread = half_bandwidth / f0 (fractional).
+// Autodetect spread from the spectrogram.
+// Measures -3dB half-bandwidth above and below the peak in each STFT frame,
+// returns spread = max(above, below) / f0 as a fractional frequency offset.
function autodetectSpread(partial, stftCache, fftSize, sampleRate) {
const curve = partial.freqCurve;
- if (!curve || !stftCache) return {spread_above: 0.02, spread_below: 0.02};
+ if (!curve || !stftCache) return {spread: 0.02};
const numFrames = stftCache.getNumFrames();
const binHz = sampleRate / fftSize;
@@ -331,9 +330,9 @@ function autodetectSpread(partial, stftCache, fftSize, sampleRate) {
++count;
}
- const spread_above = count > 0 ? Math.sqrt(sumAbove / count) : 0.01;
- const spread_below = count > 0 ? Math.sqrt(sumBelow / count) : 0.01;
- return {spread_above, spread_below};
+ const sa = count > 0 ? Math.sqrt(sumAbove / count) : 0.01;
+ const sb = count > 0 ? Math.sqrt(sumBelow / count) : 0.01;
+ return {spread: Math.max(sa, sb)};
}
// Track a single partial starting from a (time, freq) seed position.
@@ -428,7 +427,7 @@ function trackFromSeed(frames, seedTime, seedFreq, params) {
return {
times: allTimes, freqs: allFreqs, amps: allAmps, phases: allPhases,
muted: false, freqCurve,
- harmonics: { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 },
+ harmonics: { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread: 0.02 },
};
}
@@ -522,7 +521,7 @@ function trackIsoContour(stftCache, seedTime, seedFreq, params) {
times: allTimes, freqs: allFreqs, amps: allAmps,
phases: new Array(allTimes.length).fill(0),
muted: false, freqCurve,
- harmonics: { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread_above: 0.15, spread_below: 0.15 },
+ harmonics: { decay: 0.0, freq_mult: 2.0, jitter: 0.05, spread: 0.15 },
};
}
diff --git a/tools/mq_editor/mq_synth.js b/tools/mq_editor/mq_synth.js
index e5f7e1a..eeb3b00 100644
--- a/tools/mq_editor/mq_synth.js
+++ b/tools/mq_editor/mq_synth.js
@@ -26,7 +26,7 @@ function buildHarmonics(harmonics) {
// Synthesize audio from MQ partials
// partials: array of {freqCurve (with a0-a3 for amp), harmonics?, resonator?}
-// harmonics: {decay, freq_mult, jitter, spread_above, spread_below}
+// harmonics: {decay, freq_mult, jitter, spread}
// resonator: {enabled, r, gainComp} — two-pole resonator mode per partial
// integratePhase: true = accumulate 2π*f/SR per sample (correct for varying freq)
// false = 2π*f*t (simpler, only correct for constant freq)
@@ -37,11 +37,10 @@ function synthesizeMQ(partials, sampleRate, duration, integratePhase = true, opt
const pcm = new Float32Array(numSamples);
const defaultHarmonics = {
- decay: 0.0,
- freq_mult: 1.0,
- jitter: 0.05,
- spread_above: 0.02,
- spread_below: 0.02
+ decay: 0.0,
+ freq_mult: 1.0,
+ jitter: 0.05,
+ spread: 0.02
};
// Pre-build per-partial configs with fixed spread/jitter and phase accumulators
@@ -78,17 +77,16 @@ function synthesizeMQ(partials, sampleRate, duration, integratePhase = true, opt
} else {
// --- Sinusoidal (harmonic) mode ---
const harm = partial.harmonics || defaultHarmonics;
- const spread_above = harm.spread_above ?? 0.0;
- const spread_below = harm.spread_below ?? 0.0;
- const jitter = harm.jitter ?? 0.0;
+ const spread = harm.spread ?? 0.0;
+ const jitter = harm.jitter ?? 0.0;
const harmonicList = buildHarmonics(harm);
const replicaData = [];
for (let h = 0; h < harmonicList.length; ++h) {
const hc = harmonicList[h];
- const spread = randFloat(p * 67890 + h * 999, -spread_below, spread_above);
+ const spreadVal = randFloat(p * 67890 + h * 999, -spread, spread);
const initPhase = randFloat(p * 67890 + h, 0.0, 1.0) * jitter * 2.0 * Math.PI;
- replicaData.push({ ratio: hc.ratio, ampMult: hc.ampMult, spread, phase: initPhase });
+ replicaData.push({ ratio: hc.ratio, ampMult: hc.ampMult, spread: spreadVal, phase: initPhase });
}
configs.push({ mode: 'sinusoid', fc, replicaData });
}
diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js
index 1ed609c..c841acb 100644
--- a/tools/mq_editor/viewer.js
+++ b/tools/mq_editor/viewer.js
@@ -304,12 +304,11 @@ class SpectrogramViewer {
const {ctx} = this;
const curve = partial.freqCurve;
const harm = partial.harmonics || {};
- const sa = harm.spread_above != null ? harm.spread_above : 0.02;
- const sb = harm.spread_below != null ? harm.spread_below : 0.02;
+ const spread = harm.spread != null ? harm.spread : 0.02;
const decay = harm.decay != null ? harm.decay : 0.0;
const freqMult = harm.freq_mult != null ? harm.freq_mult : 2.0;
- const {upper, lower} = buildBandPoints(this, curve, sa, sb);
+ const {upper, lower} = buildBandPoints(this, curve, spread, spread);
if (upper.length < 2) return;
const savedAlpha = ctx.globalAlpha;
@@ -379,7 +378,7 @@ class SpectrogramViewer {
}
// Spread band fill + boundary dashes
- const {upper: hu, lower: hl} = buildBandPoints(this, curve, sa, sb, hRatio);
+ const {upper: hu, lower: hl} = buildBandPoints(this, curve, spread, spread, hRatio);
if (hu.length >= 2) {
ctx.beginPath();
ctx.moveTo(hu[0][0], hu[0][1]);