Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 11 additions & 112 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
_rollAddByPitch, _rollDragPitchMove, _withStableSelection,
} from './commands.js';
import {
_uploadAudioForMode, createState, editorApplyCreateResult, editorArtSearch,
editorApplyCreateResult, editorArtSearch,
editorAutoSyncAudioSelected, editorAutoSyncYtFetch, editorBuild,
editorContentImportSelected, editorCreateArtSelected, editorDoCreate,
editorEofFilesSelected, editorGPFileSelected, editorHideCreateModal, editorIdentifyAudio,
Expand Down Expand Up @@ -84,6 +84,10 @@
editorHideSaveFormatModal, editorSaveAsSloppakConfirm, filterSongs, loadCDLC,
saveCDLC, showLoadModal,
} from './file-ops.js';
import {
editorApplyReplaceAudio, editorHideReplaceAudioModal, editorSetReplaceAudioMode,
editorShowReplaceAudioModal,
} from './replace-audio.js';
import { setHostHooks } from './host.js';
import {
MIN_MEASURE, TempoGridCmd, TempoMapCmd, _editorModulateTempoAtSelection,
Expand Down Expand Up @@ -818,6 +822,12 @@
window.editorHideSaveFormatModal = editorHideSaveFormatModal;
window.editorSaveAsSloppakConfirm = editorSaveAsSloppakConfirm;

// Replace-audio modal (replace-audio.js owns the logic; HTML calls these by name).
window.editorShowReplaceAudioModal = editorShowReplaceAudioModal;
window.editorHideReplaceAudioModal = editorHideReplaceAudioModal;
window.editorSetReplaceAudioMode = editorSetReplaceAudioMode;
window.editorApplyReplaceAudio = editorApplyReplaceAudio;

window.editorHideRecordMidiModal = editorHideRecordMidiModal;
window.editorRecordMidiDeviceChanged = editorRecordMidiDeviceChanged;
window.editorChordSetCaged = editorChordSetCaged;
Expand Down Expand Up @@ -1300,7 +1310,7 @@
function onMouseUp(e) {
if (!S.drag) return;
if (_loopStripOnMouseUp()) return;
const { x, y } = getMousePos(e);

Check warning on line 1313 in src/main.js

View workflow job for this annotation

GitHub Actions / lint

'x' is assigned a value but never used

// Bar-range select finalise — refresh the Loop-in-3D button state.
if (S.drag.type === 'barsel') {
Expand Down Expand Up @@ -3925,7 +3935,7 @@
// the same save path as the Save button (in-place sloppak write, not the
// heavy create-mode build).
if (S.sessionId) {
try { await saveCDLC(); } catch (e) { /* surfaced via setStatus */ }

Check warning on line 3938 in src/main.js

View workflow job for this annotation

GitHub Actions / lint

'e' is defined but never used. Allowed unused caught errors must match /^_/u
}
// Capture where we are so the return trip lands on the same spot.
const returnCtx = {
Expand Down Expand Up @@ -4273,117 +4283,6 @@
window.editorShowStartLanding();
}

// ════════════════════════════════════════════════════════════════════
// Replace audio
// ════════════════════════════════════════════════════════════════════

let replaceAudioState = { audioMode: 'file' };

window.editorShowReplaceAudioModal = () => {
if (!S.sessionId) return;
replaceAudioState = { audioMode: 'file' };
document.getElementById('editor-replace-audio').value = '';
document.getElementById('editor-replace-yt-url').value = '';
document.getElementById('editor-replace-audio-status').textContent = '';
document.getElementById('editor-replace-audio-apply').disabled = false;
document.getElementById('editor-replace-audio-modal').classList.remove('hidden');
window.editorSetReplaceAudioMode('file');
};

window.editorHideReplaceAudioModal = () => {
document.getElementById('editor-replace-audio-modal').classList.add('hidden');
};

window.editorSetReplaceAudioMode = (mode) => {
replaceAudioState.audioMode = mode;
document.getElementById('editor-replace-audio-file-input').classList.toggle('hidden', mode !== 'file');
document.getElementById('editor-replace-audio-yt-input').classList.toggle('hidden', mode !== 'youtube');
document.getElementById('editor-replace-mode-file').classList.toggle('is-active', mode === 'file');
document.getElementById('editor-replace-mode-yt').classList.toggle('is-active', mode === 'youtube');
};

async function _uploadReplaceAudio() {
const statusEl = document.getElementById('editor-replace-audio-status');
// Pre-check missing input so we surface a hint here (the shared helper
// returns null silently on missing input so the create-modal flow's
// optional-audio path keeps its existing no-status behavior).
if (replaceAudioState.audioMode === 'youtube') {
if (!document.getElementById('editor-replace-yt-url').value.trim()) {
statusEl.textContent = 'Enter a YouTube URL';
return null;
}
} else if (!document.getElementById('editor-replace-audio').files.length) {
statusEl.textContent = 'Choose a file';
return null;
}
return _uploadAudioForMode({
mode: replaceAudioState.audioMode,
ytInputId: 'editor-replace-yt-url',
fileInputId: 'editor-replace-audio',
statusEl,
});
}

window.editorApplyReplaceAudio = async () => {
if (!S.sessionId) return;
const status = document.getElementById('editor-replace-audio-status');
const apply = document.getElementById('editor-replace-audio-apply');
apply.disabled = true;
try {
const audioUrl = await _uploadReplaceAudio();
if (!audioUrl) { apply.disabled = false; return; }

const resp = await fetch('/api/plugins/editor/replace-audio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: S.sessionId, audio_url: audioUrl }),
});
const data = await resp.json();
if (data.error) {
status.textContent = 'Error: ' + data.error;
apply.disabled = false;
return;
}

// Keep create-mode build in sync — Build Song reads createState.audioUrl.
if (S.createMode) createState.audioUrl = audioUrl;

// Stop active playback before swapping the buffer; otherwise the old
// BufferSource keeps playing under the new S.audioBuffer/duration and
// playbackTick desyncs against the new track length.
if (S.playing) stopPlayback();
// loadAudio() swallows fetch/decode errors and only logs to console,
// so detect failure by checking that the buffer reference actually
// changed. Without this we would close the modal and announce
// "Audio replaced" even on an unsupported / corrupt upload.
const prevBuffer = S.audioBuffer;
await loadAudio(audioUrl);
if (!S.audioBuffer || S.audioBuffer === prevBuffer) {
status.textContent = 'Failed to decode audio (unsupported format?)';
apply.disabled = false;
return;
}
if (S.cursorTime > S.duration) S.cursorTime = 0;
_editorApplyScrollBounds();
document.getElementById('editor-play-btn').disabled = false;
document.getElementById('editor-sync-btn').classList.remove('hidden');
updateTimeDisplay();
draw();

const HINTS = {
none: 'Audio replaced',
save: 'Audio replaced (Save to persist to .sloppak)',
build: 'Audio replaced (will persist on next Build feedpak)',
rebuild: "Audio replaced (playback only — archive won't be repacked)",
};
window.editorHideReplaceAudioModal();
setStatus(HINTS[data.next_step] || (data.persisted ? HINTS.none : HINTS.rebuild));
} catch (e) {
status.textContent = 'Failed: ' + e.message;
apply.disabled = false;
}
};

// ════════════════════════════════════════════════════════════════════
// Init
// ════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -5069,7 +4968,7 @@
}

function _partsViewOnDblClick(e) {
const { x, y } = getMousePos(e);

Check warning on line 4971 in src/main.js

View workflow job for this annotation

GitHub Actions / lint

'x' is assigned a value but never used
const parts = _partsListPure(S.arrangements, S.drumTab);
const { laneH } = _partsLaneLayoutPure((canvas.height / DPR) - WAVEFORM_H, parts.length);
const i = _partsLaneAtYPure(y, WAVEFORM_H, laneH, parts.length);
Expand Down
118 changes: 118 additions & 0 deletions src/replace-audio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Replace-audio modal: swap the session's audio track from a file or a YouTube
// URL, decode it, and re-sync playback state. The window.editor* entry points
// are re-attached by main.js; display refreshers (draw, updateTimeDisplay) are
// reached through host.

import { loadAudio, stopPlayback } from './audio.js';
import { _uploadAudioForMode, createState } from './create.js';
import { _editorApplyScrollBounds } from './loop.js';
import { S } from './state.js';
import { setStatus } from './ui.js';
import { host } from './host.js';

let replaceAudioState = { audioMode: 'file' };

export function editorShowReplaceAudioModal() {
if (!S.sessionId) return;
replaceAudioState = { audioMode: 'file' };
document.getElementById('editor-replace-audio').value = '';
document.getElementById('editor-replace-yt-url').value = '';
document.getElementById('editor-replace-audio-status').textContent = '';
document.getElementById('editor-replace-audio-apply').disabled = false;
document.getElementById('editor-replace-audio-modal').classList.remove('hidden');
editorSetReplaceAudioMode('file');
}

export function editorHideReplaceAudioModal() {
document.getElementById('editor-replace-audio-modal').classList.add('hidden');
}

export function editorSetReplaceAudioMode(mode) {
replaceAudioState.audioMode = mode;
document.getElementById('editor-replace-audio-file-input').classList.toggle('hidden', mode !== 'file');
document.getElementById('editor-replace-audio-yt-input').classList.toggle('hidden', mode !== 'youtube');
document.getElementById('editor-replace-mode-file').classList.toggle('is-active', mode === 'file');
document.getElementById('editor-replace-mode-yt').classList.toggle('is-active', mode === 'youtube');
}

async function _uploadReplaceAudio() {
const statusEl = document.getElementById('editor-replace-audio-status');
// Pre-check missing input so we surface a hint here (the shared helper
// returns null silently on missing input so the create-modal flow's
// optional-audio path keeps its existing no-status behavior).
if (replaceAudioState.audioMode === 'youtube') {
if (!document.getElementById('editor-replace-yt-url').value.trim()) {
statusEl.textContent = 'Enter a YouTube URL';
return null;
}
} else if (!document.getElementById('editor-replace-audio').files.length) {
statusEl.textContent = 'Choose a file';
return null;
}
return _uploadAudioForMode({
mode: replaceAudioState.audioMode,
ytInputId: 'editor-replace-yt-url',
fileInputId: 'editor-replace-audio',
statusEl,
});
}

export async function editorApplyReplaceAudio() {
if (!S.sessionId) return;
const status = document.getElementById('editor-replace-audio-status');
const apply = document.getElementById('editor-replace-audio-apply');
apply.disabled = true;
try {
const audioUrl = await _uploadReplaceAudio();
if (!audioUrl) { apply.disabled = false; return; }

const resp = await fetch('/api/plugins/editor/replace-audio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: S.sessionId, audio_url: audioUrl }),
});
const data = await resp.json();
if (data.error) {
status.textContent = 'Error: ' + data.error;
apply.disabled = false;
return;
}

// Keep create-mode build in sync — Build Song reads createState.audioUrl.
if (S.createMode) createState.audioUrl = audioUrl;

// Stop active playback before swapping the buffer; otherwise the old
// BufferSource keeps playing under the new S.audioBuffer/duration and
// playbackTick desyncs against the new track length.
if (S.playing) stopPlayback();
// loadAudio() swallows fetch/decode errors and only logs to console,
// so detect failure by checking that the buffer reference actually
// changed. Without this we would close the modal and announce
// "Audio replaced" even on an unsupported / corrupt upload.
const prevBuffer = S.audioBuffer;
await loadAudio(audioUrl);
if (!S.audioBuffer || S.audioBuffer === prevBuffer) {
status.textContent = 'Failed to decode audio (unsupported format?)';
apply.disabled = false;
return;
}
if (S.cursorTime > S.duration) S.cursorTime = 0;
_editorApplyScrollBounds();
document.getElementById('editor-play-btn').disabled = false;
document.getElementById('editor-sync-btn').classList.remove('hidden');
host.updateTimeDisplay();
host.draw();

const HINTS = {
none: 'Audio replaced',
save: 'Audio replaced (Save to persist to .sloppak)',
build: 'Audio replaced (will persist on next Build feedpak)',
rebuild: "Audio replaced (playback only — archive won't be repacked)",
};
editorHideReplaceAudioModal();
setStatus(HINTS[data.next_step] || (data.persisted ? HINTS.none : HINTS.rebuild));
} catch (e) {
status.textContent = 'Failed: ' + e.message;
apply.disabled = false;
}
}
Loading