726 lines
19 KiB
TypeScript
726 lines
19 KiB
TypeScript
import JSZip from 'jszip';
|
|
import {
|
|
Group,
|
|
GroupFaderParam,
|
|
GroupPattern,
|
|
Note,
|
|
Pad,
|
|
PadCode,
|
|
Pattern,
|
|
PatternEvent,
|
|
PatternEventKind,
|
|
ProjectSettings,
|
|
Scene,
|
|
SceneGroupPatternData,
|
|
ScenesSettings,
|
|
SongPosition,
|
|
Sound,
|
|
TimeSignature,
|
|
} from '../types/types';
|
|
import { GROUPS, NOTE_NAMES, PADS, SKU_EP40 } from './constants';
|
|
import { parseWavMetadata } from './exporters/utils';
|
|
import { getFileMetadata, getFileNodeByPath } from './midi/fs';
|
|
import { TESoundMetadata } from './midi/types';
|
|
import { TarFile } from './untar';
|
|
import { calculateSoundLength } from './utils';
|
|
|
|
type SceneRecord = {
|
|
number: number;
|
|
patternNumbers: Record<Group, number>;
|
|
timeSignature: TimeSignature;
|
|
};
|
|
|
|
export type ParsedPatternData = {
|
|
bars: number;
|
|
recordCount: number;
|
|
events: PatternEvent[];
|
|
notes: Record<number, Note[]>;
|
|
};
|
|
|
|
const defaultProjectSettings = {
|
|
bpm: 120,
|
|
scale: 0,
|
|
rootNote: 0,
|
|
groupFaderParams: {
|
|
a: { 0: -1, 1: -1, 2: -1, 3: -1, 4: -1, 5: -1, 6: -1, 7: -1, 8: -1, 9: -1, 10: -1, 11: -1 },
|
|
b: { 0: -1, 1: -1, 2: -1, 3: -1, 4: -1, 5: -1, 6: -1, 7: -1, 8: -1, 9: -1, 10: -1, 11: -1 },
|
|
c: { 0: -1, 1: -1, 2: -1, 3: -1, 4: -1, 5: -1, 6: -1, 7: -1, 8: -1, 9: -1, 10: -1, 11: -1 },
|
|
d: { 0: -1, 1: -1, 2: -1, 3: -1, 4: -1, 5: -1, 6: -1, 7: -1, 8: -1, 9: -1, 10: -1, 11: -1 },
|
|
},
|
|
faderAssignment: { a: 0, b: 0, c: 0, d: 0 },
|
|
};
|
|
|
|
const defaultScenesSettings = {
|
|
timeSignature: { numerator: 4, denominator: 4 },
|
|
};
|
|
|
|
const projectGroups: Group[] = ['a', 'b', 'c', 'd'];
|
|
|
|
export function noteNumberToName(noteNumber: number): string {
|
|
const noteIndex = noteNumber % 12;
|
|
const octave = Math.floor(noteNumber / 12) - 1;
|
|
|
|
return `${NOTE_NAMES[noteIndex]}${octave}`;
|
|
}
|
|
|
|
function timeStretch(data: number): Pad['timeStretch'] {
|
|
switch (data) {
|
|
case 1:
|
|
return 'bpm';
|
|
case 2:
|
|
return 'bars';
|
|
case 3:
|
|
return 'rev';
|
|
default:
|
|
return 'off';
|
|
}
|
|
}
|
|
|
|
function timeStretchBars(data: number) {
|
|
switch (data) {
|
|
case 0:
|
|
return 1;
|
|
case 1:
|
|
return 2;
|
|
case 2:
|
|
return 4;
|
|
case 255:
|
|
return 0.5;
|
|
case 254:
|
|
return 0.25;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function bytesToFloat32(bytes: Uint8Array): number {
|
|
const buffer = new ArrayBuffer(4);
|
|
const view = new DataView(buffer);
|
|
|
|
bytes.forEach((b, i) => {
|
|
view.setUint8(i, b);
|
|
});
|
|
|
|
return view.getFloat32(0, true);
|
|
}
|
|
|
|
function genPadFileName(group: string, pad: number) {
|
|
return `pads/${group}/p${String(pad).padStart(2, '0')}`;
|
|
}
|
|
|
|
function chunkArray(arr: Uint8Array, size: number, offset = 0) {
|
|
const result = [];
|
|
|
|
for (let i = offset; i < arr.length; i += size) {
|
|
result.push(arr.slice(i, i + size));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export async function collectSounds(files: TarFile[]) {
|
|
const soundIds = new Set<number>();
|
|
|
|
for (let g = 0; g < 4; g++) {
|
|
for (let i = 1; i <= 12; i++) {
|
|
const file = files.find((f) => f.name === genPadFileName(GROUPS[g].id, i));
|
|
|
|
if (file?.data) {
|
|
const soundId = (file.data[2] << 8) + file.data[1];
|
|
if (soundId > 0 && soundId < 1000) {
|
|
soundIds.add(soundId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const selectedSounds: Sound[] = [];
|
|
|
|
for (const soundId of soundIds) {
|
|
const fileNode = await getFileNodeByPath(`/sounds/${String(soundId).padStart(3, '0')}.pcm`);
|
|
|
|
if (!fileNode) {
|
|
console.warn(`Sound file for sound ID ${soundId} not found`);
|
|
continue;
|
|
}
|
|
|
|
const meta = await getFileMetadata<TESoundMetadata>(fileNode.nodeId);
|
|
|
|
selectedSounds.push({
|
|
id: soundId,
|
|
fileNode,
|
|
meta,
|
|
});
|
|
}
|
|
|
|
return selectedSounds;
|
|
}
|
|
|
|
export function collectPads(files: TarFile[], sounds: Sound[]) {
|
|
const result: Record<string, Pad[]> = {};
|
|
|
|
for (const group of GROUPS) {
|
|
result[group.id] = [];
|
|
|
|
for (let i = 1; i <= 12; i++) {
|
|
const file = files.find((f) => f.name === genPadFileName(group.id, i));
|
|
if (file?.data) {
|
|
const soundId = (file.data[2] << 8) + file.data[1];
|
|
const sound = sounds.find((s) => s.id === soundId);
|
|
const pitch = file.data[17] <= 12 ? file.data[17] : -(256 - file.data[17]); // pitch from -12 to +12
|
|
const pitchDecimal = file.data[26];
|
|
const pan = (file.data[18] >= 240 ? -(256 - file.data[18]) : file.data[18]) / 16; // normalized pan
|
|
const trimLeft = (file.data[6] << 16) + (file.data[5] << 8) + file.data[4];
|
|
const trimRight = trimLeft + (file.data[10] << 16) + (file.data[9] << 8) + file.data[8];
|
|
|
|
result[group.id].push({
|
|
pad: i,
|
|
group: group.id,
|
|
name: `${group.name} ${PADS[i - 1]}`,
|
|
file,
|
|
rawData: file.data,
|
|
soundId,
|
|
pan,
|
|
volume: file.data[16],
|
|
attack: file.data[19],
|
|
release: file.data[20],
|
|
trimLeft,
|
|
trimRight,
|
|
playMode: file.data[23] === 0 ? 'oneshot' : file.data[23] === 1 ? 'key' : 'legato',
|
|
soundLength: sound ? calculateSoundLength(sound) : 0,
|
|
pitch: Math.max(-12, Math.min(12, parseFloat(`${pitch}.${pitchDecimal}`))),
|
|
rootNote: file.data[24],
|
|
timeStretch: timeStretch(file.data[21]),
|
|
timeStretchBpm: Number(bytesToFloat32(file.data.slice(12, 16)).toFixed(2)),
|
|
timeStretchBars: timeStretchBars(file.data[25]),
|
|
inChokeGroup: file.data[22] === 1,
|
|
midiChannel: file.data[3],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function parsePatternEvents(data: Uint8Array, startOffset: number = 4): ParsedPatternData {
|
|
const recordCount = data.length >= 4 ? data[2] | (data[3] << 8) : 0;
|
|
const availableRecordCount = Math.max(0, Math.floor((data.length - startOffset) / 8));
|
|
const chunks = chunkArray(data, 8, startOffset).slice(
|
|
0,
|
|
Math.min(recordCount, availableRecordCount),
|
|
);
|
|
const events: PatternEvent[] = [];
|
|
const notes: Record<number, Note[]> = {};
|
|
|
|
chunks.forEach((chunk) => {
|
|
const kind = chunk[2] & 0x07;
|
|
const source = chunk[2] >> 3;
|
|
const position = chunk[0] | (chunk[1] << 8);
|
|
const flags = chunk[7];
|
|
const rawData = chunk.slice();
|
|
|
|
if (kind === PatternEventKind.Note) {
|
|
const duration = chunk[5] | (chunk[6] << 8);
|
|
const noteEvent: PatternEvent = {
|
|
type: 'note',
|
|
kind: PatternEventKind.Note,
|
|
position,
|
|
source,
|
|
flags,
|
|
rawData,
|
|
pad: source,
|
|
note: chunk[3],
|
|
velocity: chunk[4],
|
|
duration,
|
|
};
|
|
|
|
events.push(noteEvent);
|
|
|
|
if (!notes[source]) {
|
|
notes[source] = [];
|
|
}
|
|
|
|
notes[source].push({
|
|
note: noteEvent.note,
|
|
position: noteEvent.position,
|
|
duration: noteEvent.duration,
|
|
velocity: noteEvent.velocity,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (kind === PatternEventKind.Fader) {
|
|
const rawValue = chunk[5] | (chunk[6] << 8);
|
|
|
|
events.push({
|
|
type: 'fader',
|
|
kind: PatternEventKind.Fader,
|
|
position,
|
|
source,
|
|
flags,
|
|
rawData,
|
|
parameter: chunk[3],
|
|
reserved: chunk[4],
|
|
rawValue,
|
|
value: rawValue / 32767,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const eventData = chunk.slice(3, 7);
|
|
|
|
if (kind === PatternEventKind.Midi) {
|
|
events.push({
|
|
type: 'midi',
|
|
kind: PatternEventKind.Midi,
|
|
position,
|
|
source,
|
|
flags,
|
|
rawData,
|
|
data: eventData,
|
|
});
|
|
return;
|
|
}
|
|
|
|
events.push({
|
|
type: 'unknown',
|
|
kind,
|
|
position,
|
|
source,
|
|
flags,
|
|
rawData,
|
|
data: eventData,
|
|
});
|
|
});
|
|
|
|
return {
|
|
bars: data[1],
|
|
recordCount,
|
|
events,
|
|
notes,
|
|
};
|
|
}
|
|
|
|
function getPatternKey(group: Group, patternNumber: number) {
|
|
return `${group}${patternNumber}`;
|
|
}
|
|
|
|
function normalizeTimeSignature(
|
|
numerator: number,
|
|
denominator: number,
|
|
fallback: TimeSignature,
|
|
): TimeSignature {
|
|
const validNumerator = Number.isInteger(numerator) && numerator > 0 && numerator <= 99;
|
|
const validDenominator =
|
|
Number.isInteger(denominator) &&
|
|
denominator > 0 &&
|
|
denominator <= 64 &&
|
|
(denominator & (denominator - 1)) === 0;
|
|
|
|
return {
|
|
numerator: validNumerator ? numerator : fallback.numerator,
|
|
denominator: validDenominator ? denominator : fallback.denominator,
|
|
};
|
|
}
|
|
|
|
function collectSceneRecords(data: Uint8Array): SceneRecord[] {
|
|
const headerTimeSignature = normalizeTimeSignature(
|
|
data[5],
|
|
data[6],
|
|
defaultScenesSettings.timeSignature,
|
|
);
|
|
const sceneRecords: SceneRecord[] = [];
|
|
|
|
for (let sceneIndex = 0; sceneIndex < 99; sceneIndex++) {
|
|
const offset = 7 + sceneIndex * 6;
|
|
if (offset + 6 > data.length) {
|
|
break;
|
|
}
|
|
|
|
const patternNumbers: Record<Group, number> = {
|
|
a: data[offset],
|
|
b: data[offset + 1],
|
|
c: data[offset + 2],
|
|
d: data[offset + 3],
|
|
};
|
|
|
|
if (!projectGroups.some((group) => patternNumbers[group] !== 0)) {
|
|
continue;
|
|
}
|
|
|
|
sceneRecords.push({
|
|
number: sceneIndex + 1,
|
|
patternNumbers,
|
|
timeSignature: normalizeTimeSignature(
|
|
data[offset + 4],
|
|
data[offset + 5],
|
|
headerTimeSignature,
|
|
),
|
|
});
|
|
}
|
|
|
|
return sceneRecords;
|
|
}
|
|
|
|
function collectGroupPatterns(files: TarFile[], sku: string): GroupPattern[] {
|
|
const groupPatterns = new Map<string, GroupPattern>();
|
|
|
|
for (const file of files) {
|
|
if (file.type !== 'file' || !file.data) {
|
|
continue;
|
|
}
|
|
|
|
const matches = file.name.match(/^patterns\/([abcd])(\d{1,2})$/);
|
|
if (!matches) {
|
|
continue;
|
|
}
|
|
|
|
const group = matches[1] as Group;
|
|
const patternNumber = Number(matches[2]);
|
|
if (patternNumber < 1 || patternNumber > 99) {
|
|
continue;
|
|
}
|
|
|
|
const parsedPattern = parsePatternEvents(file.data, sku === SKU_EP40 ? 6 : 4);
|
|
groupPatterns.set(getPatternKey(group, patternNumber), {
|
|
group,
|
|
patternNumber,
|
|
...parsedPattern,
|
|
});
|
|
}
|
|
|
|
return [...groupPatterns.values()].toSorted((a, b) => {
|
|
const numberDifference = a.patternNumber - b.patternNumber;
|
|
if (numberDifference !== 0) {
|
|
return numberDifference;
|
|
}
|
|
|
|
return projectGroups.indexOf(a.group) - projectGroups.indexOf(b.group);
|
|
});
|
|
}
|
|
|
|
function getPatternsForScene(
|
|
groupPatterns: Map<string, GroupPattern>,
|
|
patternNumbers: Record<Group, number>,
|
|
) {
|
|
const patterns: Pattern[] = [];
|
|
|
|
for (const group of projectGroups) {
|
|
const groupPattern = groupPatterns.get(getPatternKey(group, patternNumbers[group]));
|
|
|
|
if (groupPattern) {
|
|
const sources = new Set(Object.keys(groupPattern.notes).map(Number));
|
|
groupPattern.events.forEach((event) => {
|
|
if (event.type === 'midi' && event.source >= 0 && event.source < 12) {
|
|
sources.add(event.source);
|
|
}
|
|
});
|
|
|
|
[...sources]
|
|
.toSorted((a, b) => a - b)
|
|
.forEach((padNum) => {
|
|
patterns.push({
|
|
pad: `${group}${padNum}` as PadCode,
|
|
group,
|
|
notes: groupPattern.notes[padNum] || [],
|
|
bars: groupPattern.bars,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
return patterns;
|
|
}
|
|
|
|
function getPatternEventsForScene(
|
|
groupPatterns: Map<string, GroupPattern>,
|
|
patternNumbers: Record<Group, number>,
|
|
) {
|
|
const patternEvents: Partial<Record<Group, SceneGroupPatternData>> = {};
|
|
|
|
for (const group of projectGroups) {
|
|
const patternNumber = patternNumbers[group];
|
|
const groupPattern = groupPatterns.get(getPatternKey(group, patternNumber));
|
|
|
|
if (groupPattern) {
|
|
patternEvents[group] = {
|
|
group,
|
|
patternNumber,
|
|
bars: groupPattern.bars,
|
|
recordCount: groupPattern.recordCount,
|
|
events: groupPattern.events,
|
|
};
|
|
}
|
|
}
|
|
|
|
return patternEvents;
|
|
}
|
|
|
|
function buildScene(sceneRecord: SceneRecord, groupPatterns: Map<string, GroupPattern>): Scene {
|
|
return {
|
|
number: sceneRecord.number,
|
|
name: String(sceneRecord.number).padStart(2, '0'),
|
|
patternNumbers: sceneRecord.patternNumbers,
|
|
timeSignature: sceneRecord.timeSignature,
|
|
patterns: getPatternsForScene(groupPatterns, sceneRecord.patternNumbers),
|
|
patternEvents: getPatternEventsForScene(groupPatterns, sceneRecord.patternNumbers),
|
|
};
|
|
}
|
|
|
|
function collectSongPositions(data: Uint8Array): SongPosition[] {
|
|
const songCountOffset = 612;
|
|
const firstSongPositionOffset = 613;
|
|
if (data.length <= songCountOffset) {
|
|
return [];
|
|
}
|
|
|
|
const availablePositions = Math.max(0, data.length - firstSongPositionOffset);
|
|
const declaredSongCount = data[songCountOffset];
|
|
const songCount = Math.min(declaredSongCount, 99, availablePositions);
|
|
if (declaredSongCount === 1 && data[firstSongPositionOffset] === 1) {
|
|
return [];
|
|
}
|
|
|
|
const songPositions: SongPosition[] = [];
|
|
|
|
for (let index = 0; index < songCount; index++) {
|
|
const sceneNumber = data[firstSongPositionOffset + index];
|
|
if (sceneNumber < 1 || sceneNumber > 99) {
|
|
continue;
|
|
}
|
|
|
|
songPositions.push({
|
|
position: index + 1,
|
|
sceneNumber,
|
|
});
|
|
}
|
|
|
|
return songPositions;
|
|
}
|
|
|
|
export function collectProjectStructure(files: TarFile[], sku: string) {
|
|
const groupPatterns = collectGroupPatterns(files, sku);
|
|
const groupPatternLookup = new Map(
|
|
groupPatterns.map((pattern) => [getPatternKey(pattern.group, pattern.patternNumber), pattern]),
|
|
);
|
|
const scenesFile = files.find((file) => file.name === 'scenes' && file.type === 'file');
|
|
let sceneRecords: SceneRecord[];
|
|
|
|
if (scenesFile?.data) {
|
|
sceneRecords = collectSceneRecords(scenesFile.data);
|
|
} else {
|
|
const patternNumbers = [
|
|
...new Set(groupPatterns.map((pattern) => pattern.patternNumber)),
|
|
].toSorted((a, b) => a - b);
|
|
sceneRecords = patternNumbers.map((patternNumber) => ({
|
|
number: patternNumber,
|
|
patternNumbers: {
|
|
a: groupPatternLookup.has(getPatternKey('a', patternNumber)) ? patternNumber : 0,
|
|
b: groupPatternLookup.has(getPatternKey('b', patternNumber)) ? patternNumber : 0,
|
|
c: groupPatternLookup.has(getPatternKey('c', patternNumber)) ? patternNumber : 0,
|
|
d: groupPatternLookup.has(getPatternKey('d', patternNumber)) ? patternNumber : 0,
|
|
},
|
|
timeSignature: defaultScenesSettings.timeSignature,
|
|
}));
|
|
}
|
|
|
|
const scenes = sceneRecords.map((sceneRecord) => buildScene(sceneRecord, groupPatternLookup));
|
|
const scenesSettings = {
|
|
timeSignature: scenes[0]?.timeSignature ?? defaultScenesSettings.timeSignature,
|
|
};
|
|
|
|
return {
|
|
groupPatterns,
|
|
scenes,
|
|
songPositions: scenesFile?.data ? collectSongPositions(scenesFile.data) : [],
|
|
scenesSettings,
|
|
};
|
|
}
|
|
|
|
export function collectScenesAndPatterns(files: TarFile[], sku: string) {
|
|
return collectProjectStructure(files, sku).scenes;
|
|
}
|
|
|
|
export function collectSettings(files: TarFile[]): ProjectSettings {
|
|
const settings = files.find((f) => f.name === 'settings' && f.type === 'file');
|
|
|
|
if (!settings || !settings.data) {
|
|
console.warn('Could not find "settings" file');
|
|
|
|
return {
|
|
...defaultProjectSettings,
|
|
rawData: new Uint8Array(),
|
|
};
|
|
}
|
|
|
|
const faderParamsData: GroupFaderParam = {};
|
|
|
|
for (const groupNum of [0, 1, 2, 3]) {
|
|
const groupId = GROUPS[groupNum].id;
|
|
for (let paramNum = 0; paramNum <= 11; paramNum++) {
|
|
faderParamsData[groupId] = {
|
|
...faderParamsData[groupId],
|
|
[paramNum]: bytesToFloat32(
|
|
settings.data.slice(24 + groupNum * 48 + paramNum * 4, 28 + groupNum * 48 + paramNum * 4),
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
bpm: Number(bytesToFloat32(settings.data.slice(4, 8)).toFixed(2)),
|
|
scale: settings.data?.[222] ?? 0,
|
|
rootNote: settings.data?.[223] ?? 0,
|
|
groupFaderParams: faderParamsData,
|
|
faderAssignment: {
|
|
a: settings.data[216],
|
|
b: settings.data[217],
|
|
c: settings.data[218],
|
|
d: settings.data[219],
|
|
},
|
|
rawData: settings.data,
|
|
};
|
|
}
|
|
|
|
export function collectEffects(files: TarFile[]) {
|
|
const fxFile = files.find((f) => f.name === 'fx_settings' && f.type === 'file');
|
|
|
|
if (!fxFile || !fxFile.data) {
|
|
console.warn('Could not find "fx_settings" file');
|
|
|
|
return {
|
|
rawData: new Uint8Array(),
|
|
effectType: 0,
|
|
param1: 0,
|
|
param2: 0,
|
|
};
|
|
}
|
|
|
|
const effectType = fxFile.data[4];
|
|
const param1 = bytesToFloat32(
|
|
fxFile.data.slice(12 + (effectType - 1) * 4, 16 + (effectType - 1) * 4),
|
|
);
|
|
const param2 = bytesToFloat32(
|
|
fxFile.data.slice(76 + (effectType - 1) * 4, 80 + (effectType - 1) * 4),
|
|
);
|
|
|
|
return {
|
|
rawData: fxFile.data,
|
|
effectType,
|
|
param1,
|
|
param2,
|
|
};
|
|
}
|
|
|
|
export function collectScenesSettings(files: TarFile[]): ScenesSettings {
|
|
const scenesSettingsFile = files.find((f) => f.name === 'scenes' && f.type === 'file');
|
|
|
|
if (!scenesSettingsFile || !scenesSettingsFile.data) {
|
|
console.warn('Could not find "scenes" file');
|
|
return defaultScenesSettings;
|
|
}
|
|
|
|
const sceneRecord = collectSceneRecords(scenesSettingsFile.data)[0];
|
|
if (!sceneRecord) {
|
|
return defaultScenesSettings;
|
|
}
|
|
|
|
return {
|
|
timeSignature: sceneRecord.timeSignature,
|
|
};
|
|
}
|
|
|
|
export async function loadSoundsFromBackup(
|
|
unzippedBackup: JSZip,
|
|
projectFiles: TarFile[],
|
|
): Promise<Sound[]> {
|
|
const soundIds = new Set<number>();
|
|
|
|
for (let group = 0; group < 4; group++) {
|
|
for (let pad = 1; pad <= 12; pad++) {
|
|
const file = projectFiles.find((f) => f.name === genPadFileName(GROUPS[group].id, pad));
|
|
|
|
if (file?.data) {
|
|
const soundId = (file.data[2] << 8) + file.data[1];
|
|
if (soundId > 0 && soundId < 1000) {
|
|
soundIds.add(soundId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const selectedSounds: Sound[] = [];
|
|
|
|
for (const soundId of soundIds) {
|
|
const wavFile = Object.values(unzippedBackup.files).find((file) =>
|
|
file.name.startsWith(`/sounds/${String(soundId).padStart(3, '0')} `),
|
|
);
|
|
|
|
if (!wavFile) {
|
|
console.warn(`Sound file for sound ID ${soundId} not found in backup`);
|
|
continue;
|
|
}
|
|
|
|
const soundFile = unzippedBackup.file(wavFile.name);
|
|
if (!soundFile) {
|
|
console.warn(`Sound file for sound ID ${soundId} not found in backup`);
|
|
continue;
|
|
}
|
|
|
|
const soundData = await soundFile.async('uint8array');
|
|
const fileName = (soundFile.name.split('/').pop()?.split(' ').slice(1).join(' ') || '').replace(
|
|
'.wav',
|
|
'',
|
|
);
|
|
const fileNode = {
|
|
nodeId: soundId,
|
|
flags: 0,
|
|
fileSize: soundData.length,
|
|
fileName,
|
|
fileType: 'file' as const,
|
|
};
|
|
|
|
let wavMeta: ReturnType<typeof parseWavMetadata>;
|
|
try {
|
|
wavMeta = parseWavMetadata(soundData);
|
|
} catch (err) {
|
|
console.warn(`Failed to parse WAV metadata for sound ${soundId}:`, err);
|
|
wavMeta = {
|
|
channels: 1,
|
|
samplerate: 44100,
|
|
format: 's16' as const,
|
|
rootNote: 60,
|
|
teMeta: null,
|
|
};
|
|
}
|
|
|
|
const te = wavMeta.teMeta;
|
|
|
|
selectedSounds.push({
|
|
id: soundId,
|
|
fileNode,
|
|
meta: {
|
|
channels: wavMeta.channels,
|
|
samplerate: wavMeta.samplerate,
|
|
format: wavMeta.format,
|
|
crc: 0,
|
|
name: fileName,
|
|
'sound.loopstart': (te?.['sound.loopstart'] as number) ?? 0,
|
|
'sound.loopend': (te?.['sound.loopend'] as number) ?? 0,
|
|
'sound.amplitude': (te?.['sound.amplitude'] as number) ?? 1,
|
|
'sound.playmode':
|
|
(te?.['sound.playmode'] as TESoundMetadata['sound.playmode']) ?? 'oneshot',
|
|
'sound.pan': (te?.['sound.pan'] as number) ?? 0,
|
|
'sound.pitch': (te?.['sound.pitch'] as number) ?? 0,
|
|
'sound.rootnote': wavMeta.rootNote,
|
|
'time.mode': (te?.['time.mode'] as string) ?? '',
|
|
'sound.bpm': 0,
|
|
'envelope.attack': (te?.['envelope.attack'] as number) ?? 0,
|
|
'envelope.release': (te?.['envelope.release'] as number) ?? 0,
|
|
},
|
|
});
|
|
}
|
|
|
|
return selectedSounds;
|
|
}
|