Files
ITOSMPiano-BachToBasics/scripts/build_sample_variants.mjs
T
이창현 92bc93c01b
CI / Frontend - type-check & test (push) Canceled after 0s
악보 진행과 연습곡 분류를 바로잡음
양손 악보를 피아노 독주로 통일하고 난이도 정렬과 곡 목록 통신 복구를 보완했다.
재생 위치에 맞춰 악보 커서와 화면이 이동하도록 고쳤으며 높은음과 낮은음에는 8va와 8vb 표기를 적용했다.
상단 브랜드와 저장소 링크를 ITOSM 피아노 학습실 기준으로 바꿨다.

검사: 프런트엔드 테스트 67건 통과, 백엔드 테스트 4건 통과, 타입 검사와 빌드 통과, Docker 재빌드와 운영 화면 확인
운영 반영: 완료
백업: /home/syskiller/piano/backups/bach-to-basics/20261003-1635-classification-scroll
2026-10-03 16:56:26 +09:00

111 lines
3.2 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import midiPackage from "../frontend/node_modules/@tonejs/midi/build/Midi.js";
const { Midi } = midiPackage;
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const sampleRoot = path.join(projectRoot, "backend", "sample_midi");
function load(name) {
return new Midi(fs.readFileSync(path.join(sampleRoot, name)));
}
function createVariant(source, options) {
const result = new Midi();
const header = source.header.toJSON();
result.header.fromJSON({
...header,
name: options.name,
tempos: [{ ticks: 0, bpm: options.tempo }],
meta: [],
});
const originalTempo = source.header.tempos[0]?.bpm ?? options.tempo;
const timeScale = originalTempo / options.tempo;
for (const trackIndex of options.trackIndexes) {
const sourceTrack = source.tracks[trackIndex];
if (!sourceTrack) continue;
const targetTrack = result.addTrack();
targetTrack.name = sourceTrack.name;
targetTrack.channel = sourceTrack.channel;
targetTrack.instrument.number = sourceTrack.instrument.number;
let notes = sourceTrack.notes.filter((note) => note.time < options.endTime);
if (options.leftHandRoots && trackIndex === 1) {
const measureSeconds = (60 / originalTempo) * (header.timeSignatures[0]?.timeSignature[0] ?? 4);
const roots = new Map();
for (const note of notes) {
const measure = Math.floor(note.time / measureSeconds);
const current = roots.get(measure);
if (!current || note.midi < current.midi) roots.set(measure, note);
}
notes = [...roots.values()];
}
for (const note of notes) {
targetTrack.addNote({
midi: note.midi,
time: note.time * timeScale,
duration: options.leftHandRoots && trackIndex === 1
? Math.max(note.duration * timeScale, 0.8)
: note.duration * timeScale,
velocity: note.velocity,
});
}
}
return result;
}
function write(name, midi) {
fs.writeFileSync(path.join(sampleRoot, name), Buffer.from(midi.toArray()));
}
const furElise = load("fur-elise.mid");
write(
"fur-elise-beginner.mid",
createVariant(furElise, {
name: "엘리제를 위하여 입문 양손 연습",
tempo: 60,
endTime: 32,
trackIndexes: [0, 1],
leftHandRoots: true,
}),
);
write(
"fur-elise-easy.mid",
createVariant(furElise, {
name: "엘리제를 위하여 쉬운 양손 연습",
tempo: 66,
endTime: 45,
trackIndexes: [0, 1],
leftHandRoots: true,
}),
);
const bachMinuet = load("minuet-bwv-anh-114.mid");
write(
"minuet-bwv-anh-114-beginner.mid",
createVariant(bachMinuet, {
name: "미뉴에트 사장조 입문 양손 연습",
tempo: 100,
endTime: bachMinuet.duration + 1,
trackIndexes: [0, 1],
leftHandRoots: true,
}),
);
const beethovenMinuet = load("menuett-woo-82.mid");
write(
"menuett-woo-82-easy.mid",
createVariant(beethovenMinuet, {
name: "미뉴에트 내림마장조 쉬운 양손 연습",
tempo: 88,
endTime: beethovenMinuet.duration + 1,
trackIndexes: [0, 1],
leftHandRoots: true,
}),
);
console.log("양손이 포함된 난이도별 연습 MIDI 4개를 만들었습니다.");