Add boilerplate files and licenses
This commit is contained in:
316
src/lib/transformers/ableton.ts
Normal file
316
src/lib/transformers/ableton.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import omit from 'lodash/omit';
|
||||
import {
|
||||
ExporterParams,
|
||||
FaderParam,
|
||||
Note,
|
||||
Pad,
|
||||
PadCode,
|
||||
ProjectRawData,
|
||||
TimeSignature,
|
||||
} from '../../types/types';
|
||||
import { getSampleName } from '../exporters/utils';
|
||||
import { findPad, findSoundByPad, findSoundIdByPad } from '../utils';
|
||||
|
||||
export type AblData = {
|
||||
tracks: AblTrack[];
|
||||
scenes: AblScene[];
|
||||
};
|
||||
|
||||
export type AblTrack = Omit<Pad, 'file' | 'rawData' | 'midiChannel'> & {
|
||||
padCode: PadCode;
|
||||
group: string;
|
||||
sampleName: string;
|
||||
sampleChannels: number;
|
||||
sampleRate: number;
|
||||
sampleRootNote: number;
|
||||
samplePitch: number;
|
||||
bpm: number;
|
||||
drumRack: boolean;
|
||||
lane?: AblLane;
|
||||
tracks: AblTrack[];
|
||||
faderParams: { [K in FaderParam]: number };
|
||||
timeSignature: TimeSignature;
|
||||
};
|
||||
|
||||
export type AblLane = {
|
||||
padCode: PadCode;
|
||||
clips: AblClip[];
|
||||
};
|
||||
|
||||
export type AblNote = Note;
|
||||
|
||||
export type AblClip = {
|
||||
notes: AblNote[];
|
||||
bars: number;
|
||||
offset: number;
|
||||
sceneBars: number;
|
||||
sceneIndex: number;
|
||||
sceneName: string;
|
||||
timeSignature: TimeSignature;
|
||||
faderParams: { [K in FaderParam]: number };
|
||||
};
|
||||
|
||||
export type AblScene = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams) {
|
||||
const { pads, scenes } = data;
|
||||
const customSceneNames = exporterParams.customSceneNames || {};
|
||||
const lanes: AblLane[] = [];
|
||||
const ablScenes: AblScene[] = [];
|
||||
let tracks: AblTrack[] = [];
|
||||
let offset = 0;
|
||||
|
||||
scenes.forEach((scene, sceneIndex) => {
|
||||
const sceneBars = Math.max(...scene.patterns.map((p) => p.bars));
|
||||
const ablScene: AblScene = {
|
||||
name: customSceneNames[scene.name] || scene.name,
|
||||
};
|
||||
|
||||
scene.patterns.forEach((pattern) => {
|
||||
let track = tracks.find((c) => c.padCode === pattern.pad);
|
||||
let pad = findPad(pattern.pad, pads);
|
||||
|
||||
if (!pad) {
|
||||
console.warn('Could not find pad data for', pattern.pad);
|
||||
|
||||
pad = {
|
||||
group: pattern.group,
|
||||
volume: 100,
|
||||
attack: 0,
|
||||
release: 0,
|
||||
trimLeft: 0,
|
||||
trimRight: 0,
|
||||
pad: 0,
|
||||
playMode: 'oneshot',
|
||||
pan: 0,
|
||||
pitch: 0,
|
||||
rootNote: 60,
|
||||
timeStretch: 'off',
|
||||
timeStretchBpm: 0,
|
||||
timeStretchBars: 0,
|
||||
soundId: 0,
|
||||
name: '',
|
||||
file: null,
|
||||
rawData: null,
|
||||
soundLength: 0,
|
||||
inChokeGroup: false,
|
||||
midiChannel: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const faderParams = data.settings.groupFaderParams[pad.group];
|
||||
|
||||
if (!track) {
|
||||
const soundId = findSoundIdByPad(pattern.pad, pads) || 0;
|
||||
const sound = findSoundByPad(pattern.pad, pads, data.sounds);
|
||||
track = {
|
||||
...omit(pad, ['file', 'rawData']),
|
||||
soundId,
|
||||
padCode: pattern.pad,
|
||||
name: sound?.meta?.name || pattern.pad,
|
||||
volume: pad.volume / 200, // converting from 0-200 to 0.0-1.0
|
||||
sampleName: getSampleName(sound?.meta?.name, soundId),
|
||||
sampleChannels: sound?.meta?.channels || 0,
|
||||
sampleRate: sound?.meta?.samplerate || 0,
|
||||
sampleRootNote: pad.rootNote,
|
||||
samplePitch: sound?.meta?.['sound.pitch'] ?? 0,
|
||||
bpm: data.settings.bpm,
|
||||
drumRack: false,
|
||||
tracks: [],
|
||||
faderParams,
|
||||
timeSignature: data.scenesSettings.timeSignature,
|
||||
};
|
||||
|
||||
tracks.push(track);
|
||||
}
|
||||
|
||||
let lane = lanes.find((c) => c.padCode === pattern.pad);
|
||||
if (!lane) {
|
||||
lane = {
|
||||
padCode: pattern.pad,
|
||||
clips: [],
|
||||
};
|
||||
|
||||
lanes.push(lane);
|
||||
}
|
||||
|
||||
lane.clips.push({
|
||||
offset,
|
||||
notes: pattern.notes,
|
||||
bars: pattern.bars,
|
||||
sceneBars,
|
||||
sceneIndex,
|
||||
sceneName: customSceneNames[scene.name] || scene.name,
|
||||
timeSignature: data.scenesSettings.timeSignature,
|
||||
faderParams,
|
||||
});
|
||||
|
||||
track.lane = lane;
|
||||
});
|
||||
|
||||
offset += sceneBars;
|
||||
|
||||
ablScenes.push(ablScene);
|
||||
});
|
||||
|
||||
tracks.sort((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
|
||||
if (exporterParams.exportAllPadsWithSamples) {
|
||||
for (const group in pads) {
|
||||
pads[group].forEach((pad, index) => {
|
||||
if (pad.soundId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padCode = `${group}${index}` as PadCode;
|
||||
if (tracks.some((t) => t.padCode === padCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sound = data.sounds.find((s) => s.id === pad.soundId);
|
||||
const faderParams = data.settings.groupFaderParams[pad.group];
|
||||
|
||||
tracks.push({
|
||||
...omit(pad, ['file', 'rawData']),
|
||||
soundId: pad.soundId,
|
||||
padCode,
|
||||
name: sound?.meta?.name || padCode,
|
||||
volume: pad.volume / 200,
|
||||
sampleName: getSampleName(sound?.meta?.name, pad.soundId),
|
||||
sampleChannels: sound?.meta?.channels || 0,
|
||||
sampleRate: sound?.meta?.samplerate || 0,
|
||||
sampleRootNote: pad.rootNote,
|
||||
samplePitch: sound?.meta?.['sound.pitch'] ?? 0,
|
||||
bpm: data.settings.bpm,
|
||||
drumRack: false,
|
||||
tracks: [],
|
||||
faderParams,
|
||||
timeSignature: data.scenesSettings.timeSignature,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tracks.sort((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to create a drum rack track for a specific group
|
||||
const createDrumRackTrack = (group: 'a' | 'b' | 'c' | 'd'): AblTrack | null => {
|
||||
const groupTracksForDrumRack = tracks.filter((t) => t.group === group);
|
||||
if (groupTracksForDrumRack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drumTrack: AblTrack = {
|
||||
padCode: `${group}0` as PadCode,
|
||||
group,
|
||||
sampleName: '',
|
||||
sampleChannels: 0,
|
||||
sampleRate: 0,
|
||||
sampleRootNote: 60,
|
||||
samplePitch: 0,
|
||||
bpm: data.settings.bpm,
|
||||
drumRack: true,
|
||||
soundId: 0,
|
||||
name: `Drums ${group.toUpperCase()}`,
|
||||
volume: 1, // 1 means 0dB
|
||||
attack: 0,
|
||||
release: 0,
|
||||
trimLeft: 0,
|
||||
trimRight: 0,
|
||||
pad: 0,
|
||||
lane: undefined,
|
||||
playMode: 'oneshot',
|
||||
pan: 0,
|
||||
pitch: 0,
|
||||
rootNote: 60,
|
||||
timeStretch: 'off',
|
||||
timeStretchBpm: 0,
|
||||
timeStretchBars: 0,
|
||||
soundLength: 0,
|
||||
tracks: groupTracksForDrumRack,
|
||||
inChokeGroup: false,
|
||||
faderParams: data.settings.groupFaderParams[group],
|
||||
timeSignature: data.scenesSettings.timeSignature,
|
||||
};
|
||||
|
||||
// We need to merge notes from all tracks in the group into one track
|
||||
const newClips: Record<string, AblClip> = {};
|
||||
|
||||
drumTrack.tracks
|
||||
.toSorted((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
)
|
||||
.forEach((track) => {
|
||||
const padNumber = parseInt(track.padCode.slice(1), 10);
|
||||
// EP133 is 3x4 (top→bottom), Ableton Drum Rack is 4x4 (bottom→top)
|
||||
// flip vertically: row 3 → row 0, row 0 → row 3
|
||||
const row = 3 - Math.floor(padNumber / 3);
|
||||
const col = padNumber % 3;
|
||||
const drumPad = 36 + row * 4 + col; // remaping notes starting from C1 (36)
|
||||
|
||||
track.lane?.clips.forEach((clip) => {
|
||||
if (!newClips[clip.sceneName]) {
|
||||
newClips[clip.sceneName] = structuredClone(clip);
|
||||
newClips[clip.sceneName].notes = [];
|
||||
}
|
||||
|
||||
newClips[clip.sceneName].notes = [
|
||||
...newClips[clip.sceneName].notes,
|
||||
...clip.notes.map((n) => ({
|
||||
...n,
|
||||
note: drumPad,
|
||||
})),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
drumTrack.lane = {
|
||||
padCode: `${group}0` as PadCode,
|
||||
clips: Object.values(newClips),
|
||||
};
|
||||
|
||||
return drumTrack;
|
||||
};
|
||||
|
||||
// Process drum racks for each group that has the option enabled
|
||||
const drumRackTracks: AblTrack[] = [];
|
||||
const groupsToProcess: Array<{ group: 'a' | 'b' | 'c' | 'd'; enabled: boolean }> = [
|
||||
{ group: 'a', enabled: exporterParams.drumRackGroupA || false },
|
||||
{ group: 'b', enabled: exporterParams.drumRackGroupB || false },
|
||||
{ group: 'c', enabled: exporterParams.drumRackGroupC || false },
|
||||
{ group: 'd', enabled: exporterParams.drumRackGroupD || false },
|
||||
];
|
||||
|
||||
for (const { group, enabled } of groupsToProcess) {
|
||||
if (enabled) {
|
||||
const drumTrack = createDrumRackTrack(group);
|
||||
if (drumTrack) {
|
||||
drumRackTracks.push(drumTrack);
|
||||
// Remove tracks from this group from the main tracks array
|
||||
tracks = tracks.filter((t) => t.group !== group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert drum rack tracks at the beginning, maintaining group order (A, B, C, D)
|
||||
tracks.unshift(...drumRackTracks);
|
||||
|
||||
Sentry.setContext(`abletonData`, {
|
||||
tracks,
|
||||
scenes: ablScenes,
|
||||
});
|
||||
|
||||
return {
|
||||
tracks,
|
||||
scenes: ablScenes,
|
||||
} as AblData;
|
||||
}
|
||||
|
||||
export default abletonTransformer;
|
||||
290
src/lib/transformers/dawProject.ts
Normal file
290
src/lib/transformers/dawProject.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
ExporterParams,
|
||||
Note,
|
||||
Pad,
|
||||
PadCode,
|
||||
ProjectRawData,
|
||||
TimeSignature,
|
||||
} from '../../types/types';
|
||||
import { getSampleName } from '../exporters/utils';
|
||||
import { findPad, findSoundByPad, findSoundIdByPad } from '../utils';
|
||||
|
||||
export type DawData = {
|
||||
tracks: DawTrack[];
|
||||
lanes: DawLane[];
|
||||
scenes: DawScene[];
|
||||
};
|
||||
|
||||
export type DawTrack = Omit<Pad, 'file' | 'rawData' | 'midiChannel'> & {
|
||||
padCode: PadCode;
|
||||
group: string;
|
||||
sampleName: string;
|
||||
sampleChannels: number;
|
||||
sampleRate: number;
|
||||
bpm: number;
|
||||
};
|
||||
|
||||
export type DawLane = {
|
||||
padCode: PadCode;
|
||||
group?: string;
|
||||
clips: DawClip[];
|
||||
};
|
||||
|
||||
export type DawClip = {
|
||||
notes: Note[];
|
||||
bars: number;
|
||||
offset: number;
|
||||
sceneBars: number;
|
||||
sceneIndex: number;
|
||||
sceneName: string;
|
||||
sceneTimeSignature: TimeSignature;
|
||||
};
|
||||
|
||||
export type DawClipSlot = {
|
||||
clip: DawClip[];
|
||||
track: DawTrack;
|
||||
};
|
||||
|
||||
export type DawScene = {
|
||||
name: string;
|
||||
clipSlot: DawClipSlot[];
|
||||
};
|
||||
|
||||
function dawProjectTransformer(data: ProjectRawData, exporterParams: ExporterParams) {
|
||||
const { pads, scenes } = data;
|
||||
const customSceneNames = exporterParams.customSceneNames || {};
|
||||
const dawScenes: DawScene[] = [];
|
||||
let lanes: DawLane[] = [];
|
||||
let tracks: DawTrack[] = [];
|
||||
let offset = 0;
|
||||
|
||||
scenes.forEach((scene, sceneIndex) => {
|
||||
const sceneBars = Math.max(...scene.patterns.map((p) => p.bars));
|
||||
const dawScene: DawScene = {
|
||||
name: customSceneNames[scene.name] || scene.name,
|
||||
clipSlot: [],
|
||||
};
|
||||
|
||||
for (const pattern of scene.patterns) {
|
||||
let track = tracks.find((c) => c.padCode === pattern.pad);
|
||||
|
||||
if (!track) {
|
||||
const soundId = findSoundIdByPad(pattern.pad, pads) || 0;
|
||||
const sound = findSoundByPad(pattern.pad, pads, data.sounds);
|
||||
const pad = findPad(pattern.pad, pads);
|
||||
|
||||
if (!pad) {
|
||||
throw new Error(`Could not find pad for ${pattern.pad}`);
|
||||
}
|
||||
|
||||
track = {
|
||||
...omit(pad, ['file', 'rawData']),
|
||||
soundId,
|
||||
padCode: pattern.pad,
|
||||
name: sound?.meta?.name || pattern.pad,
|
||||
volume: pad.volume * (2 / 200),
|
||||
sampleName: getSampleName(sound?.meta?.name, soundId),
|
||||
sampleChannels: sound?.meta?.channels || 0,
|
||||
sampleRate: sound?.meta?.samplerate || 0,
|
||||
bpm: data.settings.bpm,
|
||||
};
|
||||
|
||||
tracks.push(track);
|
||||
}
|
||||
|
||||
let lane = lanes.find((c) => c.padCode === pattern.pad);
|
||||
|
||||
if (!lane) {
|
||||
lane = {
|
||||
padCode: pattern.pad,
|
||||
group: track.group,
|
||||
clips: [],
|
||||
};
|
||||
|
||||
lanes.push(lane);
|
||||
}
|
||||
|
||||
lane.clips.push({
|
||||
offset,
|
||||
notes: pattern.notes,
|
||||
bars: pattern.bars,
|
||||
sceneBars,
|
||||
sceneIndex,
|
||||
sceneName: customSceneNames[scene.name] || scene.name,
|
||||
sceneTimeSignature: data.scenesSettings.timeSignature,
|
||||
});
|
||||
|
||||
dawScene.clipSlot.push({
|
||||
clip: lane.clips,
|
||||
track: track,
|
||||
});
|
||||
}
|
||||
|
||||
offset += sceneBars;
|
||||
|
||||
dawScenes.push(dawScene);
|
||||
});
|
||||
|
||||
if (exporterParams.exportAllPadsWithSamples) {
|
||||
for (const group in pads) {
|
||||
pads[group].forEach((pad, index) => {
|
||||
if (pad.soundId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padCode = `${group}${index}` as PadCode;
|
||||
if (tracks.some((t) => t.padCode === padCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sound = data.sounds.find((s) => s.id === pad.soundId);
|
||||
|
||||
tracks.push({
|
||||
...omit(pad, ['file', 'rawData']),
|
||||
soundId: pad.soundId,
|
||||
padCode,
|
||||
name: sound?.meta?.name || padCode,
|
||||
volume: pad.volume * (2 / 200),
|
||||
sampleName: getSampleName(sound?.meta?.name, pad.soundId),
|
||||
sampleChannels: sound?.meta?.channels || 0,
|
||||
sampleRate: sound?.meta?.samplerate || 0,
|
||||
bpm: data.settings.bpm,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tracks.sort((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to create a drum rack track and lane for a specific group
|
||||
const createDrumRackForGroup = (
|
||||
group: 'a' | 'b' | 'c' | 'd',
|
||||
): {
|
||||
drumTrack: DawTrack;
|
||||
drumLane: DawLane;
|
||||
} | null => {
|
||||
const groupTracks = tracks.filter((t) => t.group === group);
|
||||
const groupLanes = lanes.filter((l) => l.group === group);
|
||||
|
||||
if (groupTracks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drumTrack: DawTrack = {
|
||||
padCode: `${group}0` as PadCode,
|
||||
group,
|
||||
sampleName: '',
|
||||
sampleChannels: 0,
|
||||
sampleRate: 0,
|
||||
bpm: data.settings.bpm,
|
||||
soundId: 0,
|
||||
name: `Drums ${group.toUpperCase()}`,
|
||||
volume: 1,
|
||||
attack: 0,
|
||||
release: 0,
|
||||
trimLeft: 0,
|
||||
trimRight: 0,
|
||||
pad: 0,
|
||||
playMode: 'oneshot',
|
||||
pan: 0,
|
||||
pitch: 0,
|
||||
rootNote: 60,
|
||||
timeStretch: 'off',
|
||||
timeStretchBpm: 0,
|
||||
timeStretchBars: 0,
|
||||
soundLength: 0,
|
||||
inChokeGroup: false,
|
||||
};
|
||||
|
||||
const drumLane: DawLane = {
|
||||
padCode: `${group}0` as PadCode,
|
||||
group,
|
||||
clips: [],
|
||||
};
|
||||
|
||||
const newClips: Record<string, DawClip> = {};
|
||||
|
||||
groupLanes
|
||||
.toSorted((a, b) => a.padCode.localeCompare(b.padCode))
|
||||
.forEach((lane, idx) => {
|
||||
lane.clips.forEach((clip) => {
|
||||
if (!newClips[clip.sceneName]) {
|
||||
newClips[clip.sceneName] = structuredClone(clip);
|
||||
newClips[clip.sceneName].notes = []; // resetting notes, they will be added below with new mapping
|
||||
}
|
||||
|
||||
newClips[clip.sceneName].notes = [
|
||||
...newClips[clip.sceneName].notes,
|
||||
...clip.notes.map((n) => ({
|
||||
...n,
|
||||
note: 36 + idx,
|
||||
})), // remaping notes starting from C1 (36)
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
drumLane.clips = Object.values(newClips);
|
||||
|
||||
return { drumTrack, drumLane };
|
||||
};
|
||||
|
||||
// Process drum racks for each group that has the option enabled
|
||||
const drumRackData: Array<{ drumTrack: DawTrack; drumLane: DawLane }> = [];
|
||||
const groupsToProcess: Array<{ group: 'a' | 'b' | 'c' | 'd'; enabled: boolean }> = [
|
||||
{ group: 'a', enabled: exporterParams.drumRackGroupA || false },
|
||||
{ group: 'b', enabled: exporterParams.drumRackGroupB || false },
|
||||
{ group: 'c', enabled: exporterParams.drumRackGroupC || false },
|
||||
{ group: 'd', enabled: exporterParams.drumRackGroupD || false },
|
||||
];
|
||||
|
||||
for (const { group, enabled } of groupsToProcess) {
|
||||
if (enabled) {
|
||||
const drumRack = createDrumRackForGroup(group);
|
||||
if (drumRack) {
|
||||
drumRackData.push(drumRack);
|
||||
// Remove tracks and lanes from this group from the main arrays
|
||||
tracks = tracks.filter((t) => t.group !== group);
|
||||
lanes = lanes.filter((l) => l.group !== group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert drum rack tracks and lanes at the beginning, maintaining group order (A, B, C, D)
|
||||
tracks.unshift(...drumRackData.map((d) => d.drumTrack));
|
||||
lanes.unshift(...drumRackData.map((d) => d.drumLane));
|
||||
|
||||
// Update scenes to include drum rack clip slots
|
||||
const groupsConvertedToDrumRack = drumRackData.map((d) => d.drumTrack.group);
|
||||
dawScenes.forEach((scene) => {
|
||||
// Remove clip slots for groups that were converted to drum racks
|
||||
scene.clipSlot = scene.clipSlot.filter(
|
||||
(cs) => !groupsConvertedToDrumRack.includes(cs.track.group),
|
||||
);
|
||||
|
||||
// Add drum rack clip slots at the beginning, maintaining group order
|
||||
for (const { drumTrack, drumLane } of drumRackData) {
|
||||
scene.clipSlot.unshift({
|
||||
clip: drumLane.clips.filter((c) => c.sceneName === scene.name),
|
||||
track: drumTrack,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Sentry.setContext(`dawprojectData`, {
|
||||
tracks,
|
||||
lanes,
|
||||
scenes: dawScenes,
|
||||
});
|
||||
|
||||
return {
|
||||
tracks,
|
||||
lanes,
|
||||
scenes: dawScenes,
|
||||
} as DawData;
|
||||
}
|
||||
|
||||
export default dawProjectTransformer;
|
||||
160
src/lib/transformers/midi.ts
Normal file
160
src/lib/transformers/midi.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { ExporterParams, Note, ProjectRawData } from '../../types/types';
|
||||
import { getQuarterNotesPerBar, TICKS_PER_BEAT } from '../exporters/utils';
|
||||
import { findSoundByPad } from '../utils';
|
||||
|
||||
export type MidiData = {
|
||||
tracks: MidiTrack[];
|
||||
};
|
||||
|
||||
export type MidiTrack = {
|
||||
name: string;
|
||||
group: string;
|
||||
padCode: string;
|
||||
notes: Note[];
|
||||
};
|
||||
|
||||
function midiTransformer(data: ProjectRawData, exporterParams: ExporterParams) {
|
||||
const { pads, scenes } = data;
|
||||
let midiTracks: MidiTrack[] = [];
|
||||
let offset = 0;
|
||||
|
||||
const barLength =
|
||||
getQuarterNotesPerBar(
|
||||
data.scenesSettings.timeSignature.numerator,
|
||||
data.scenesSettings.timeSignature.denominator,
|
||||
) * TICKS_PER_BEAT;
|
||||
|
||||
scenes.forEach((scene) => {
|
||||
const sceneMaxBars = Math.max(...scene.patterns.map((p) => p.bars));
|
||||
scene.patterns.forEach((pattern) => {
|
||||
let track = midiTracks.find((t) => t.padCode === pattern.pad);
|
||||
if (!track) {
|
||||
const sound = findSoundByPad(pattern.pad, pads, data.sounds);
|
||||
|
||||
track = {
|
||||
name: sound?.meta.name || pattern.pad,
|
||||
padCode: pattern.pad,
|
||||
group: pattern.group,
|
||||
notes: [],
|
||||
};
|
||||
|
||||
midiTracks.push(track);
|
||||
}
|
||||
|
||||
track.notes.push(
|
||||
...pattern.notes.map((note) => ({
|
||||
...note,
|
||||
position: note.position + offset * barLength,
|
||||
})),
|
||||
);
|
||||
|
||||
// copy pattern for the rest of the scene
|
||||
if (pattern.bars < sceneMaxBars) {
|
||||
for (let ofs = offset + pattern.bars; ofs < offset + sceneMaxBars; ofs++) {
|
||||
track.notes.push(
|
||||
...pattern.notes.map((note) => ({
|
||||
...note,
|
||||
position: note.position + ofs * barLength,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
offset += sceneMaxBars;
|
||||
});
|
||||
|
||||
if (exporterParams.exportAllPadsWithSamples) {
|
||||
for (const group in pads) {
|
||||
pads[group].forEach((pad, index) => {
|
||||
if (pad.soundId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padCode = `${group}${index}`;
|
||||
if (midiTracks.some((t) => t.padCode === padCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sound = data.sounds.find((s) => s.id === pad.soundId);
|
||||
|
||||
midiTracks.push({
|
||||
name: sound?.meta?.name || padCode,
|
||||
padCode,
|
||||
group,
|
||||
notes: [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
midiTracks.sort((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to create a drum rack track for a specific group
|
||||
const createDrumRackTrack = (group: 'a' | 'b' | 'c' | 'd'): MidiTrack | null => {
|
||||
const groupTracks = midiTracks.filter((t) => t.group === group);
|
||||
if (groupTracks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mergedNotes: Note[] = [];
|
||||
|
||||
groupTracks
|
||||
.toSorted((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
)
|
||||
.forEach((track, idx) => {
|
||||
mergedNotes.push(
|
||||
...track.notes.map((note) => ({
|
||||
...note,
|
||||
note: 36 + idx,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
const drumTrack: MidiTrack = {
|
||||
name: `Drums ${group.toUpperCase()}`,
|
||||
group,
|
||||
padCode: `${group}0`,
|
||||
notes: mergedNotes,
|
||||
};
|
||||
|
||||
return drumTrack;
|
||||
};
|
||||
|
||||
// Process drum racks for each group that has the option enabled
|
||||
const drumRackTracks: MidiTrack[] = [];
|
||||
const groupsToProcess: Array<{ group: 'a' | 'b' | 'c' | 'd'; enabled: boolean }> = [
|
||||
{ group: 'a', enabled: exporterParams.drumRackGroupA || false },
|
||||
{ group: 'b', enabled: exporterParams.drumRackGroupB || false },
|
||||
{ group: 'c', enabled: exporterParams.drumRackGroupC || false },
|
||||
{ group: 'd', enabled: exporterParams.drumRackGroupD || false },
|
||||
];
|
||||
|
||||
for (const { group, enabled } of groupsToProcess) {
|
||||
if (enabled) {
|
||||
const drumTrack = createDrumRackTrack(group);
|
||||
if (drumTrack) {
|
||||
drumRackTracks.push(drumTrack);
|
||||
// Remove tracks from this group from the main tracks array
|
||||
midiTracks = midiTracks.filter((t) => t.group !== group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert drum rack tracks at the beginning, maintaining group order (A, B, C, D)
|
||||
midiTracks.unshift(...drumRackTracks);
|
||||
|
||||
Sentry.setContext('midiData', {
|
||||
tracks: midiTracks,
|
||||
});
|
||||
|
||||
return {
|
||||
tracks: midiTracks,
|
||||
} as MidiData;
|
||||
}
|
||||
|
||||
export default midiTransformer;
|
||||
132
src/lib/transformers/reaper.ts
Normal file
132
src/lib/transformers/reaper.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
ExporterParams,
|
||||
Note,
|
||||
Pad,
|
||||
PadCode,
|
||||
ProjectRawData,
|
||||
TimeSignature,
|
||||
} from '../../types/types';
|
||||
import { getSampleName } from '../exporters/utils';
|
||||
import { findPad, findSoundByPad, findSoundIdByPad } from '../utils';
|
||||
|
||||
export type RprData = {
|
||||
tracks: RprTrack[];
|
||||
};
|
||||
|
||||
export type RprTrack = Omit<Pad, 'file' | 'rawData' | 'group' | 'midiChannel'> & {
|
||||
padCode: PadCode;
|
||||
group: string;
|
||||
sampleName: string;
|
||||
sampleChannels: number;
|
||||
sampleRate: number;
|
||||
bpm: number;
|
||||
items: RprTrackItem[];
|
||||
timeSignature: TimeSignature;
|
||||
};
|
||||
|
||||
export type RprTrackItem = {
|
||||
notes: Note[];
|
||||
bars: number;
|
||||
offset: number;
|
||||
sceneBars: number;
|
||||
sceneIndex: number;
|
||||
sceneName: string;
|
||||
};
|
||||
|
||||
export function reaperTransform(data: ProjectRawData, exporterParams: ExporterParams) {
|
||||
const { pads, scenes } = data;
|
||||
const customSceneNames = exporterParams.customSceneNames || {};
|
||||
const tracks: RprTrack[] = [];
|
||||
let offset = 0;
|
||||
|
||||
scenes.forEach((scene, sceneIndex) => {
|
||||
const sceneBars = Math.max(...scene.patterns.map((p) => p.bars));
|
||||
scene.patterns.forEach((pattern) => {
|
||||
let track = tracks.find((c) => c.padCode === pattern.pad);
|
||||
|
||||
if (!track) {
|
||||
const soundId = findSoundIdByPad(pattern.pad, pads) || 0;
|
||||
const sound = findSoundByPad(pattern.pad, pads, data.sounds);
|
||||
const pad = findPad(pattern.pad, pads);
|
||||
|
||||
if (!pad) {
|
||||
throw new Error(`Could not find pad for ${pattern.pad}`);
|
||||
}
|
||||
|
||||
track = {
|
||||
...omit(pad, ['file', 'rawData']),
|
||||
soundId,
|
||||
padCode: pattern.pad,
|
||||
name: sound?.meta?.name || pattern.pad,
|
||||
volume: pad.volume * (2 / 200),
|
||||
sampleName: getSampleName(sound?.meta?.name, soundId),
|
||||
sampleChannels: sound?.meta?.channels || 0,
|
||||
sampleRate: sound?.meta?.samplerate || 0,
|
||||
bpm: data.settings.bpm,
|
||||
timeSignature: data.scenesSettings.timeSignature,
|
||||
items: [],
|
||||
};
|
||||
|
||||
tracks.push(track);
|
||||
}
|
||||
|
||||
track.items.push({
|
||||
offset,
|
||||
notes: pattern.notes,
|
||||
bars: pattern.bars,
|
||||
sceneBars,
|
||||
sceneIndex,
|
||||
sceneName: customSceneNames[scene.name] || scene.name,
|
||||
});
|
||||
});
|
||||
|
||||
offset += sceneBars;
|
||||
});
|
||||
|
||||
if (exporterParams.exportAllPadsWithSamples) {
|
||||
for (const group in pads) {
|
||||
pads[group].forEach((pad, index) => {
|
||||
if (pad.soundId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padCode = `${group}${index}` as PadCode;
|
||||
if (tracks.some((t) => t.padCode === padCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sound = data.sounds.find((s) => s.id === pad.soundId);
|
||||
|
||||
tracks.push({
|
||||
...omit(pad, ['file', 'rawData']),
|
||||
soundId: pad.soundId,
|
||||
padCode,
|
||||
name: sound?.meta?.name || padCode,
|
||||
volume: pad.volume * (2 / 200),
|
||||
sampleName: getSampleName(sound?.meta?.name, pad.soundId),
|
||||
sampleChannels: sound?.meta?.channels || 0,
|
||||
sampleRate: sound?.meta?.samplerate || 0,
|
||||
bpm: data.settings.bpm,
|
||||
timeSignature: data.scenesSettings.timeSignature,
|
||||
items: [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tracks.sort((a, b) =>
|
||||
a.padCode.localeCompare(b.padCode, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
}
|
||||
|
||||
Sentry.setContext('reaperData', {
|
||||
tracks,
|
||||
});
|
||||
|
||||
return {
|
||||
tracks,
|
||||
} as RprData;
|
||||
}
|
||||
|
||||
export default reaperTransform;
|
||||
136
src/lib/transformers/webView.ts
Normal file
136
src/lib/transformers/webView.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Note, Pad, ProjectRawData } from '../../types/types';
|
||||
import { getSampleName } from '../exporters/utils';
|
||||
import { noteNumberToName } from '../parsers';
|
||||
import { findPad, findSoundByPad, findSoundIdByPad } from '../utils';
|
||||
|
||||
export type ViewNote = Note & { name: string };
|
||||
|
||||
export type ViewPattern = {
|
||||
pad: string;
|
||||
bars: number;
|
||||
soundName: string;
|
||||
notes: ViewNote[];
|
||||
group: string;
|
||||
padNumber: number;
|
||||
midiChannel: number;
|
||||
soundId: number;
|
||||
};
|
||||
|
||||
export type ViewScene = {
|
||||
name: string;
|
||||
patterns: ViewPattern[];
|
||||
maxBars: number;
|
||||
};
|
||||
|
||||
export type ViewPad = Pad;
|
||||
|
||||
export type ViewData = {
|
||||
pads: Record<string, ViewPad[]>;
|
||||
scenes: ViewScene[];
|
||||
scenesSettings: ProjectRawData['scenesSettings'];
|
||||
};
|
||||
|
||||
function webViewTransformer(data: ProjectRawData): ViewData {
|
||||
const { pads, scenes } = data;
|
||||
const newScenes: ViewScene[] = [];
|
||||
const usedPads = new Set<string>();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
// construct new scenes array
|
||||
// and collect the used pads
|
||||
scenes.forEach((scene, idx) => {
|
||||
newScenes[idx] = {
|
||||
name: scene.name,
|
||||
maxBars: Math.max(...scene.patterns.map((p) => p.bars), 0),
|
||||
patterns: [],
|
||||
};
|
||||
|
||||
for (const pattern of scene.patterns) {
|
||||
if (pattern.notes.length > 0) {
|
||||
usedPads.add(pattern.pad);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// also add pads that have samples assigned (even without patterns)
|
||||
for (const group in pads) {
|
||||
pads[group].forEach((pad, index) => {
|
||||
if (pad.soundId > 0) {
|
||||
usedPads.add(`${group}${index}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newScenes.forEach((scene, idx) => {
|
||||
// make sure each scene have the same tracks/pads
|
||||
usedPads.forEach((pad) => {
|
||||
const patternByPad = scene.patterns.find((p) => p.pad === pad);
|
||||
if (!patternByPad) {
|
||||
const group = pad[0];
|
||||
const padNumber = parseInt(pad.slice(1), 10);
|
||||
const soundId = findSoundIdByPad(pad, pads) || 0;
|
||||
const sound = findSoundByPad(pad, pads, data.sounds);
|
||||
const padObject = findPad(pad, pads);
|
||||
|
||||
scene.patterns.push({
|
||||
pad,
|
||||
notes: [],
|
||||
bars: 0,
|
||||
group,
|
||||
padNumber,
|
||||
soundName: getSampleName(sound?.meta?.name, soundId, false),
|
||||
midiChannel: padObject?.midiChannel || 0,
|
||||
soundId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// copy notes to exiting patterns
|
||||
scenes[idx].patterns.forEach((pattern) => {
|
||||
const patternInScene = scene.patterns.find((p) => p.pad === pattern.pad);
|
||||
if (!patternInScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
patternInScene.notes = pattern.notes
|
||||
.map((note) => ({
|
||||
...note,
|
||||
name: noteNumberToName(note.note),
|
||||
}))
|
||||
.reduce((acc, note) => {
|
||||
const existingNoteInThisPosition = acc.find((n) => n.position === note.position);
|
||||
|
||||
if (existingNoteInThisPosition) {
|
||||
existingNoteInThisPosition.name = `${existingNoteInThisPosition.name},${note.name}`;
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({ ...note });
|
||||
return acc;
|
||||
}, [] as ViewNote[]);
|
||||
patternInScene.bars = pattern.bars;
|
||||
});
|
||||
|
||||
scene.patterns.sort((a, b) =>
|
||||
a.pad.localeCompare(b.pad, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
});
|
||||
|
||||
Sentry.setContext('webViewData', {
|
||||
pads,
|
||||
scenes: newScenes,
|
||||
scenesSettings: data.scenesSettings,
|
||||
});
|
||||
|
||||
return {
|
||||
pads,
|
||||
scenes: newScenes,
|
||||
scenesSettings: data.scenesSettings,
|
||||
};
|
||||
}
|
||||
|
||||
export default webViewTransformer;
|
||||
Reference in New Issue
Block a user