곡 보관함과 악보 학습 기능을 보완
업로드 형식을 MIDI와 MusicXML과 MXL 및 PDF로 확장하고 파일별 검증을 추가했다. AlphaTab 악보 자산과 Nginx MIME 설정을 보완해 운영에서 오선과 음표가 표시되도록 수정했다. 공개 도메인 클래식과 난이도별 연습 편곡 7개를 같은 제목별로 묶고 출처와 운영 검증 결과를 문서화했다. 검사: TypeScript 검사와 Vitest 67개 및 Vite 운영 빌드 통과. 운영 브라우저 악보 표시와 네 가지 업로드 검증 형식 확인. 백업: /home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload 운영 반영: 완료
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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],
|
||||
}),
|
||||
);
|
||||
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],
|
||||
}),
|
||||
);
|
||||
|
||||
const beethovenMinuet = load("menuett-woo-82.mid");
|
||||
write(
|
||||
"menuett-woo-82-easy.mid",
|
||||
createVariant(beethovenMinuet, {
|
||||
name: "미뉴에트 내림마장조 오른손 연습",
|
||||
tempo: 88,
|
||||
endTime: beethovenMinuet.duration + 1,
|
||||
trackIndexes: [0],
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("난이도별 연습 MIDI 4개를 만들었습니다.");
|
||||
Reference in New Issue
Block a user