fix: extract stems from trimmed audio is using full song
This commit is contained in:
@@ -10,6 +10,7 @@ import { KGAudioFileStorage } from '../core/io/KGAudioFileStorage';
|
|||||||
import { ConfigManager } from '../core/config/ConfigManager';
|
import { ConfigManager } from '../core/config/ConfigManager';
|
||||||
import { DEBUG_MODE } from '../constants/uiConstants';
|
import { DEBUG_MODE } from '../constants/uiConstants';
|
||||||
import { fetchWithRetry } from '../util/retryUtil';
|
import { fetchWithRetry } from '../util/retryUtil';
|
||||||
|
import { sliceAudioToWav } from '../util/audioUtil';
|
||||||
import type { KeySignature } from '../core/KGProject';
|
import type { KeySignature } from '../core/KGProject';
|
||||||
import { ImportStemsCommand } from '../core/commands';
|
import { ImportStemsCommand } from '../core/commands';
|
||||||
import type { StemImportEntry } from '../core/commands';
|
import type { StemImportEntry } from '../core/commands';
|
||||||
@@ -915,18 +916,40 @@ const SeparatorTab: React.FC = () => {
|
|||||||
|
|
||||||
const audioFileId = selectedAudioRegion.region.getAudioFileId();
|
const audioFileId = selectedAudioRegion.region.getAudioFileId();
|
||||||
const audioFileName = selectedAudioRegion.region.getAudioFileName();
|
const audioFileName = selectedAudioRegion.region.getAudioFileName();
|
||||||
const arrayBuffer = await KGAudioFileStorage.loadAudioFile(projectName, audioFileId);
|
const clipStart = selectedAudioRegion.region.getClipStartOffsetSeconds();
|
||||||
const audioFile = new File([arrayBuffer], audioFileName, { type: 'audio/mpeg' });
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', audioFile, audioFileName);
|
formData.append('file', audioFile, uploadFileName);
|
||||||
formData.append('model_filename', model);
|
formData.append('model_filename', model);
|
||||||
|
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
|
|
||||||
setGenHint('Submitting separation request...');
|
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`, {
|
const sepResp = await fetch(`${baseUrl}/v1/separator/separate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|||||||
@@ -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<ArrayBuffer> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user