diff --git a/src/components/KGOnePanel.tsx b/src/components/KGOnePanel.tsx index 59d5de6..db30a23 100644 --- a/src/components/KGOnePanel.tsx +++ b/src/components/KGOnePanel.tsx @@ -10,6 +10,7 @@ import { KGAudioFileStorage } from '../core/io/KGAudioFileStorage'; import { ConfigManager } from '../core/config/ConfigManager'; import { DEBUG_MODE } from '../constants/uiConstants'; import { fetchWithRetry } from '../util/retryUtil'; +import { sliceAudioToWav } from '../util/audioUtil'; import type { KeySignature } from '../core/KGProject'; import { ImportStemsCommand } from '../core/commands'; import type { StemImportEntry } from '../core/commands'; @@ -915,18 +916,40 @@ const SeparatorTab: React.FC = () => { const audioFileId = selectedAudioRegion.region.getAudioFileId(); const audioFileName = selectedAudioRegion.region.getAudioFileName(); - const arrayBuffer = await KGAudioFileStorage.loadAudioFile(projectName, audioFileId); - const audioFile = new File([arrayBuffer], audioFileName, { type: 'audio/mpeg' }); + const clipStart = selectedAudioRegion.region.getClipStartOffsetSeconds(); + const fullDuration = selectedAudioRegion.region.getAudioDurationSeconds(); + const regionLengthSec = selectedAudioRegion.region.getLength() * (60 / bpm); + const effectiveDuration = Math.min(regionLengthSec, fullDuration - clipStart); + + const rawBuffer = await KGAudioFileStorage.loadAudioFile(projectName, audioFileId); + const needsSlice = clipStart > 0.01 || effectiveDuration < fullDuration - 0.01; + + let uploadBuffer: ArrayBuffer; + let uploadFileName: string; + let uploadMimeType: string; + + if (needsSlice) { + setGenHint('Trimming audio to region range...'); + uploadBuffer = await sliceAudioToWav(rawBuffer, clipStart, effectiveDuration); + uploadFileName = audioFileName.replace(/\.[^.]+$/, '.wav'); + uploadMimeType = 'audio/wav'; + } else { + uploadBuffer = rawBuffer; + uploadFileName = audioFileName; + uploadMimeType = 'audio/mpeg'; + } + + const audioFile = new File([uploadBuffer], uploadFileName, { type: uploadMimeType }); const formData = new FormData(); - formData.append('file', audioFile, audioFileName); + formData.append('file', audioFile, uploadFileName); formData.append('model_filename', model); if (signal.aborted) return; setGenHint('Submitting separation request...'); - kgoneLog('REQ', 'POST /v1/separator/separate', { file: audioFileName, model_filename: model }); + kgoneLog('REQ', 'POST /v1/separator/separate', { file: uploadFileName, model_filename: model }); const sepResp = await fetch(`${baseUrl}/v1/separator/separate`, { method: 'POST', body: formData, diff --git a/src/util/audioUtil.ts b/src/util/audioUtil.ts new file mode 100644 index 0000000..e3ec6ca --- /dev/null +++ b/src/util/audioUtil.ts @@ -0,0 +1,52 @@ +function writeString(view: DataView, offset: number, str: string) { + for (let i = 0; i < str.length; i++) + view.setUint8(offset + i, str.charCodeAt(i)); +} + +export async function sliceAudioToWav( + arrayBuffer: ArrayBuffer, + startSeconds: number, + durationSeconds: number +): Promise { + const audioCtx = new AudioContext(); + const decoded = await audioCtx.decodeAudioData(arrayBuffer); + audioCtx.close(); + + const { sampleRate, numberOfChannels: numCh } = decoded; + const startSample = Math.floor(startSeconds * sampleRate); + const numSamples = Math.min( + Math.floor(durationSeconds * sampleRate), + decoded.length - startSample + ); + + const bytesPerSample = 2; // 16-bit PCM + const dataSize = numSamples * numCh * bytesPerSample; + const buf = new ArrayBuffer(44 + dataSize); + const view = new DataView(buf); + + writeString(view, 0, 'RIFF'); + view.setUint32(4, 36 + dataSize, true); + writeString(view, 8, 'WAVE'); + writeString(view, 12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); // PCM + view.setUint16(22, numCh, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * numCh * bytesPerSample, true); + view.setUint16(32, numCh * bytesPerSample, true); + view.setUint16(34, 16, true); + writeString(view, 36, 'data'); + view.setUint32(40, dataSize, true); + + let offset = 44; + for (let i = 0; i < numSamples; i++) { + for (let ch = 0; ch < numCh; ch++) { + const s = decoded.getChannelData(ch)[startSample + i]; + const c = Math.max(-1, Math.min(1, s)); + view.setInt16(offset, c < 0 ? c * 32768 : c * 32767, true); + offset += 2; + } + } + + return buf; +}