summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 21:07:40 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 21:07:40 +0100
commitfc9cfd50a584faa2f6621ea1b4ff4eeb6b50f8e4 (patch)
treebdf5f9e9c052b7bcb4d1c5e271c9b32410b6e472
parent04756e1c595c8c8391b1d6f2ee47dc34a6642c39 (diff)
fix(mq_editor): key '3' plays partial from t_start; add getAudioBuffer()
- synthesizeMQ output trimmed to [t_start-50ms, t_end+50ms] so playback starts immediately at the partial instead of t=0 - Extract synth+trim logic into getAudioBuffer(partials, margin=0) - Stack params vertically in dropdown (grid layout) handoff(Claude): partial playback and CSS param layout fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--tools/mq_editor/app.js36
-rw-r--r--tools/mq_editor/style.css4
2 files changed, 26 insertions, 14 deletions
diff --git a/tools/mq_editor/app.js b/tools/mq_editor/app.js
index 20224a0..9857c64 100644
--- a/tools/mq_editor/app.js
+++ b/tools/mq_editor/app.js
@@ -481,6 +481,25 @@ function getSynthParams() {
};
}
+// Synthesize partials and return an AudioBuffer, trimmed to [t_start-margin, t_end+margin]
+function getAudioBuffer(partials, margin = 0) {
+ const sr = audioBuffer.sampleRate;
+ const {integratePhase, opts} = getSynthParams();
+ const pcm = synthesizeMQ(partials, sr, audioBuffer.duration, integratePhase, opts);
+ let startSample = 0, endSample = pcm.length;
+ if (margin >= 0 && partials.length > 0) {
+ const times = partials.flatMap(p => p.times);
+ const tStart = Math.min(...times);
+ const tEnd = Math.max(...times);
+ startSample = Math.max(0, Math.floor((tStart - margin) * sr));
+ endSample = Math.min(pcm.length, Math.ceil((tEnd + margin) * sr));
+ }
+ const trimmed = pcm.subarray(startSample, endSample);
+ const buf = audioContext.createBuffer(1, trimmed.length, sr);
+ buf.getChannelData(0).set(trimmed);
+ return buf;
+}
+
// Play synthesized audio
function playSynthesized() {
if (!extractedPartials || extractedPartials.length === 0) {
@@ -497,16 +516,14 @@ function playSynthesized() {
const partialsToUse = extractedPartials.slice(0, keepCount).filter(p => !p.muted);
setStatus(`Synthesizing ${partialsToUse.length}/${extractedPartials.length} partials (${keepPct.value}%)...`, 'info');
- const {integratePhase, opts} = getSynthParams();
- const pcm = synthesizeMQ(partialsToUse, audioBuffer.sampleRate, audioBuffer.duration,
- integratePhase, opts);
-
if (viewer) {
+ const {integratePhase, opts} = getSynthParams();
+ const pcm = synthesizeMQ(partialsToUse, audioBuffer.sampleRate, audioBuffer.duration,
+ integratePhase, opts);
viewer.setSynthStftCache(new STFTCache(pcm, audioBuffer.sampleRate, fftSize, parseInt(hopSize.value)));
}
- const synthBuffer = audioContext.createBuffer(1, pcm.length, audioBuffer.sampleRate);
- synthBuffer.getChannelData(0).set(pcm);
+ const synthBuffer = getAudioBuffer(partialsToUse);
playAudioBuffer(synthBuffer, `Playing synthesized (${partialsToUse.length}/${extractedPartials.length} partials, ${keepPct.value}%)...`);
}
@@ -535,12 +552,7 @@ document.addEventListener('keydown', (e) => {
const partial = extractedPartials[sel];
if (!partial) return;
stopAudio();
- const {integratePhase, opts} = getSynthParams();
- const pcm = synthesizeMQ([partial], audioBuffer.sampleRate, audioBuffer.duration,
- integratePhase, opts);
- const buf = audioContext.createBuffer(1, pcm.length, audioBuffer.sampleRate);
- buf.getChannelData(0).set(pcm);
- playAudioBuffer(buf, `Playing partial #${sel}...`);
+ playAudioBuffer(getAudioBuffer([partial], 0.05), `Playing partial #${sel}...`);
} else if (e.code === 'KeyP') {
e.preventDefault();
if (viewer) viewer.togglePeaks();
diff --git a/tools/mq_editor/style.css b/tools/mq_editor/style.css
index 87f033d..9563a57 100644
--- a/tools/mq_editor/style.css
+++ b/tools/mq_editor/style.css
@@ -29,9 +29,9 @@ button.contour-active { background: #145; border-color: #0cc; color: #aff; }
#paramsPanel { display: none; position: absolute; z-index: 200; top: 100%; left: 0; margin-top: 4px; background: #222; border: 1px solid #555; border-radius: 4px; padding: 6px 0; white-space: nowrap; box-shadow: 0 4px 12px rgba(0,0,0,.5); }
#paramsPanel.open { display: block; }
#paramsBtn.params-open { background: #4a4a4a; border-color: #888; }
-.param-group { display: flex; align-items: center; gap: 6px; padding: 5px 14px; border-bottom: 1px solid #333; }
+.param-group { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; padding: 8px 14px; border-bottom: 1px solid #333; align-items: center; }
.param-group:last-child { border-bottom: none; }
-.group-label { font-size: 9px; color: #666; text-transform: uppercase; letter-spacing: 1px; white-space: nowrap; width: 80px; flex-shrink: 0; }
+.group-label { grid-column: 1 / -1; font-size: 9px; color: #666; text-transform: uppercase; letter-spacing: 1px; }
/* === Canvas & overlays === */
#canvas { border: 1px solid #555; background: #000; cursor: crosshair; display: block; flex-shrink: 0; }