양손 악보를 피아노 독주로 통일하고 난이도 정렬과 곡 목록 통신 복구를 보완했다. 재생 위치에 맞춰 악보 커서와 화면이 이동하도록 고쳤으며 높은음과 낮은음에는 8va와 8vb 표기를 적용했다. 상단 브랜드와 저장소 링크를 ITOSM 피아노 학습실 기준으로 바꿨다. 검사: 프런트엔드 테스트 67건 통과, 백엔드 테스트 4건 통과, 타입 검사와 빌드 통과, Docker 재빌드와 운영 화면 확인 운영 반영: 완료 백업: /home/syskiller/piano/backups/bach-to-basics/20261003-1635-classification-scroll
This commit is contained in:
+11
-11
@@ -112,11 +112,11 @@ SAMPLES = (
|
||||
"storage_name": "minuet-bwv-anh-114-beginner.mid",
|
||||
"title": "미뉴에트 사장조 BWV Anh. 114",
|
||||
"artist": "크리스티안 페촐트",
|
||||
"description": "오른손 선율을 느린 속도로 먼저 익히는 입문 편곡입니다. 공개 도메인 원곡에서 연습용으로 구성했습니다.",
|
||||
"description": "오른손 선율과 단순화한 왼손을 느린 속도로 함께 익히는 입문 편곡입니다. 손별 연습은 연습 화면에서 선택할 수 있습니다.",
|
||||
"difficulty": "beginner",
|
||||
"genre": "바로크",
|
||||
"arrangement": "melody",
|
||||
"tags": ["미뉴에트", "사장조", "오른손", "입문", "공개 도메인"],
|
||||
"arrangement": "piano_solo",
|
||||
"tags": ["미뉴에트", "사장조", "양손", "입문", "공개 도메인"],
|
||||
},
|
||||
{
|
||||
"storage_name": "minuet-bwv-anh-114.mid",
|
||||
@@ -132,11 +132,11 @@ SAMPLES = (
|
||||
"storage_name": "menuett-woo-82-easy.mid",
|
||||
"title": "미뉴에트 내림마장조 WoO 82",
|
||||
"artist": "루트비히 판 베토벤",
|
||||
"description": "오른손 선율을 느린 속도로 익히는 쉬운 편곡입니다. 공개 도메인 원곡에서 연습용으로 구성했습니다.",
|
||||
"description": "오른손 선율과 단순화한 왼손을 느린 속도로 함께 익히는 쉬운 편곡입니다. 손별 연습은 연습 화면에서 선택할 수 있습니다.",
|
||||
"difficulty": "easy",
|
||||
"genre": "고전",
|
||||
"arrangement": "melody",
|
||||
"tags": ["미뉴에트", "내림마장조", "오른손", "쉬움", "공개 도메인"],
|
||||
"arrangement": "piano_solo",
|
||||
"tags": ["미뉴에트", "내림마장조", "양손", "쉬움", "공개 도메인"],
|
||||
},
|
||||
{
|
||||
"storage_name": "menuett-woo-82.mid",
|
||||
@@ -152,11 +152,11 @@ SAMPLES = (
|
||||
"storage_name": "fur-elise-beginner.mid",
|
||||
"title": "엘리제를 위하여 WoO 59",
|
||||
"artist": "루트비히 판 베토벤",
|
||||
"description": "유명한 첫 주제를 오른손으로 천천히 익히는 입문 편곡입니다. 공개 도메인 원곡에서 연습용으로 구성했습니다.",
|
||||
"description": "유명한 첫 주제의 오른손 선율과 단순화한 왼손을 천천히 함께 익히는 입문 편곡입니다. 손별 연습은 연습 화면에서 선택할 수 있습니다.",
|
||||
"difficulty": "beginner",
|
||||
"genre": "고전",
|
||||
"arrangement": "melody",
|
||||
"tags": ["오른손", "주제 연습", "입문", "베토벤", "공개 도메인"],
|
||||
"arrangement": "piano_solo",
|
||||
"tags": ["양손", "주제 연습", "입문", "베토벤", "공개 도메인"],
|
||||
},
|
||||
{
|
||||
"storage_name": "fur-elise-easy.mid",
|
||||
@@ -165,8 +165,8 @@ SAMPLES = (
|
||||
"description": "첫 주제의 오른손 선율과 단순화한 왼손 근음을 함께 연습하는 쉬운 양손 편곡입니다.",
|
||||
"difficulty": "easy",
|
||||
"genre": "고전",
|
||||
"arrangement": "accompaniment",
|
||||
"tags": ["양손", "쉬운 반주", "주제 연습", "베토벤", "공개 도메인"],
|
||||
"arrangement": "piano_solo",
|
||||
"tags": ["양손", "쉬운 양손", "주제 연습", "베토벤", "공개 도메인"],
|
||||
},
|
||||
{
|
||||
"storage_name": "fur-elise.mid",
|
||||
|
||||
@@ -446,7 +446,16 @@ def list_midi(
|
||||
SELECT m.*, u.display_name AS owner_name, u.username AS owner_username
|
||||
FROM midi_items m LEFT JOIN users u ON u.id = m.owner_id
|
||||
WHERE {' AND '.join(clauses)}
|
||||
ORDER BY m.artist COLLATE NOCASE, m.title COLLATE NOCASE, m.difficulty, m.created_at
|
||||
ORDER BY m.artist COLLATE NOCASE, m.title COLLATE NOCASE,
|
||||
CASE m.difficulty
|
||||
WHEN 'beginner' THEN 0
|
||||
WHEN 'easy' THEN 1
|
||||
WHEN 'intermediate' THEN 2
|
||||
WHEN 'advanced' THEN 3
|
||||
WHEN 'expert' THEN 4
|
||||
ELSE 5
|
||||
END,
|
||||
m.created_at
|
||||
""",
|
||||
params,
|
||||
).fetchall()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -177,6 +177,7 @@ def _apply_piano_split(score): # noqa: C901
|
||||
import copy
|
||||
from music21 import clef as m21clef, instrument as m21inst, layout as m21layout
|
||||
from music21 import note as m21note, chord as m21chord
|
||||
from music21 import spanner as m21spanner
|
||||
from music21 import stream as m21s
|
||||
|
||||
parts = list(score.parts)
|
||||
@@ -313,6 +314,58 @@ def _apply_piano_split(score): # noqa: C901
|
||||
first_m.remove(c)
|
||||
first_m.insert(0, clef_obj)
|
||||
|
||||
def _add_readable_ottavas(staff, *, upper: bool) -> None:
|
||||
"""긴 덧줄 대신 연속된 고음과 저음 구간에 8va 또는 8vb를 붙인다.
|
||||
|
||||
한 음에만 기호를 붙이면 악보가 더 복잡해진다. 한 마디 안에 기준을
|
||||
넘는 음이 두 개 이상 있거나 극단적인 음이 하나라도 있을 때만 그
|
||||
마디를 대상으로 삼는다. 이어진 대상 마디는 하나의 옥타브 선으로
|
||||
묶는다. non-transposing으로 내보내 원래 MIDI 음높이와 재생 결과는
|
||||
바뀌지 않고 MusicXML 표기만 읽기 쉬운 위치로 이동한다.
|
||||
"""
|
||||
|
||||
measures = list(staff.getElementsByClass("Measure"))
|
||||
marked: list[int] = []
|
||||
for index, measure in enumerate(measures):
|
||||
pitched = list(measure.recurse().notes)
|
||||
qualifying = 0
|
||||
extreme = False
|
||||
for element in pitched:
|
||||
pitches = list(element.pitches) if hasattr(element, "pitches") else [element.pitch]
|
||||
midi_values = [pitch.midi for pitch in pitches]
|
||||
if upper:
|
||||
qualifying += sum(value >= 84 for value in midi_values) # C6 이상
|
||||
extreme = extreme or any(value >= 96 for value in midi_values) # C7 이상
|
||||
else:
|
||||
qualifying += sum(value <= 36 for value in midi_values) # C2 이하
|
||||
extreme = extreme or any(value <= 24 for value in midi_values) # C1 이하
|
||||
if qualifying >= 2 or extreme:
|
||||
marked.append(index)
|
||||
|
||||
groups: list[list[int]] = []
|
||||
for index in marked:
|
||||
if groups and index == groups[-1][-1] + 1:
|
||||
groups[-1].append(index)
|
||||
else:
|
||||
groups.append([index])
|
||||
|
||||
for group in groups:
|
||||
elements = []
|
||||
for index in group:
|
||||
elements.extend(measures[index].recurse().notes)
|
||||
if not elements:
|
||||
continue
|
||||
ottava = m21spanner.Ottava(
|
||||
*elements,
|
||||
type="8va" if upper else "8vb",
|
||||
transposing=False,
|
||||
placement="above" if upper else "below",
|
||||
)
|
||||
staff.insert(0, ottava)
|
||||
|
||||
_add_readable_ottavas(treble_staff, upper=True)
|
||||
_add_readable_ottavas(bass_staff, upper=False)
|
||||
|
||||
# ── Assemble score with brace StaffGroup (= grand staff) ─────────────────
|
||||
new_score = m21s.Score()
|
||||
if score.metadata:
|
||||
|
||||
@@ -50,6 +50,13 @@ class LibraryDatabaseTest(unittest.TestCase):
|
||||
self.assertTrue(Path(self.temp.name, "library.sqlite3").is_file())
|
||||
self.assertTrue(Path(self.temp.name, "midi").is_dir())
|
||||
|
||||
def test_graded_samples_are_two_hand_piano_solos(self) -> None:
|
||||
for sample in self.db.SAMPLES:
|
||||
self.assertEqual(sample["arrangement"], "piano_solo")
|
||||
self.assertIn("양손", sample["tags"])
|
||||
self.assertNotIn("오른손", sample["tags"])
|
||||
self.assertNotIn("쉬운 반주", sample["tags"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user