Compare commits
2
Commits
c918f02685
...
92bc93c01b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92bc93c01b | ||
|
|
0522e48013 |
@@ -0,0 +1,42 @@
|
|||||||
|
# ITOSM Bach to Basics 프로젝트 기록
|
||||||
|
|
||||||
|
## 프로젝트 소속
|
||||||
|
|
||||||
|
- 서비스: ITOSM 피아노 학습실의 Bach to Basics 한국어판
|
||||||
|
- 공개 주소: `https://learn.itosm.co.kr`
|
||||||
|
- 운영 소스: `/home/syskiller/piano/bach-to-basics`
|
||||||
|
- Gitea 저장소: `syskiller/ITOSMPiano-BachToBasics`
|
||||||
|
- ITOSMGames와 `game.itosm.co.kr`에는 이 프로젝트 파일과 기록을 넣지 않는다.
|
||||||
|
|
||||||
|
## 2026-10-03 현재 상태
|
||||||
|
|
||||||
|
- Docker의 `piano-bach-frontend`와 `piano-bach-backend`로 운영한다.
|
||||||
|
- 로그인과 회원 가입과 비밀번호 변경과 회원 탈퇴를 지원한다.
|
||||||
|
- 관리자 권한이 확인된 계정에만 관리자 패널 진입 버튼을 표시한다.
|
||||||
|
- 곡별 공개 범위와 최소 회원 등급과 지정 회원 공유를 지원한다.
|
||||||
|
- 보관함 업로드 형식은 MIDI와 MusicXML과 MXL 및 PDF다.
|
||||||
|
- 공개 샘플은 Mutopia Project 공개 도메인 자료와 그 단계별 연습 편곡 7개다.
|
||||||
|
- 악보 표시에는 AlphaTab과 Bravura SMuFL 글꼴을 사용한다.
|
||||||
|
- MIDI 변환 악보에서 C6 이상의 연속 고음은 8va로 표시한다. C2 이하의 연속 저음은 8vb로 표시해 덧줄을 줄인다. 실제 재생 음높이는 바꾸지 않는다.
|
||||||
|
- 화면과 브라우저 제목의 서비스 이름은 `ITOSM 피아노 학습실`이다. 왼쪽 위 표식과 설정 화면은 전용 음표 표식을 사용하며 서비스 Gitea 저장소로 연결한다.
|
||||||
|
|
||||||
|
## 확인된 운영 결과
|
||||||
|
|
||||||
|
- 기존 회원 업로드 `20360.mid`는 `보글보글`이라는 제목으로 보존돼 있다.
|
||||||
|
- 엘리제를 위하여는 입문과 쉬움과 고급 3개 버전으로 묶여 표시된다.
|
||||||
|
- 두 미뉴에트는 각각 단순화한 양손 연습과 양손 원곡 2개 버전으로 묶여 표시된다.
|
||||||
|
- 단계별 공개 샘플은 모두 오른손과 왼손을 포함한 피아노 독주다. 손별 연습은 연습 화면에서 선택한다.
|
||||||
|
- 곡 목록 조회는 일시적인 연결 실패 때 한 번 재시도한다. 정상 응답을 받으면 남아 있던 연결 오류를 지운다.
|
||||||
|
- 악보 커서는 변환된 악보의 시간 위치와 원본 템포 지도를 환산한 AlphaTab tick을 함께 사용한다. 음소거된 내부 플레이어가 위치 이벤트를 보내지 않는 경우에도 악보 렌더가 끝나면 커서를 직접 갱신한다. 확인한 AlphaTab 1.8.3을 고정해 커서 경로 변경으로 인한 재발을 막는다.
|
||||||
|
- 운영 브라우저에서 미뉴에트와 엘리제를 위하여 입문 악보의 오선과 음표 표시를 확인했다.
|
||||||
|
- 운영 백엔드에서 MIDI와 MusicXML과 MXL 및 PDF 검증기를 모두 통과했다.
|
||||||
|
- 프런트 타입 검사와 67개 테스트와 운영 빌드를 통과했다.
|
||||||
|
|
||||||
|
## 백업
|
||||||
|
|
||||||
|
- 작업 전 DB와 수정 대상 소스: `/home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload`
|
||||||
|
|
||||||
|
## 남은 확인
|
||||||
|
|
||||||
|
- 실제 회원 화면에서 MusicXML과 MXL 및 PDF 파일을 각각 업로드한 뒤 삭제하는 통합 검사를 추가로 수행한다.
|
||||||
|
- 실제 USB MIDI 피아노를 연결한 모바일 또는 태블릿 Chromium 환경에서 입력 지연과 따라치기 동작을 확인한다.
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
|
|
||||||
A browser-based piano practice tool. Drop in a **MIDI file**, a **MusicXML score**, or even a **PDF of sheet music**. Bach to Basics turns it into synced views of falling notes, an interactive 88-key piano, and rendered sheet music, then layers on practice tools (A/B loop, speed trainer, wait mode, metronome, transpose) to help you learn the piece.
|
A browser-based piano practice tool. Drop in a **MIDI file**, a **MusicXML score**, or even a **PDF of sheet music**. Bach to Basics turns it into synced views of falling notes, an interactive 88-key piano, and rendered sheet music, then layers on practice tools (A/B loop, speed trainer, wait mode, metronome, transpose) to help you learn the piece.
|
||||||
|
|
||||||
|
## ITOSM 한국어 운영판
|
||||||
|
|
||||||
|
이 저장소는 원본 Bach to Basics를 바탕으로 `learn.itosm.co.kr`에서 운영하는 한국어판이다. 독립 회원과 곡·악보 보관함을 제공한다. 비회원은 공개 곡을 연주할 수 있으며 회원은 MIDI와 MusicXML과 MXL 및 PDF 악보를 올려 공개 범위와 회원 등급을 지정할 수 있다.
|
||||||
|
|
||||||
|
기본 공개 자료는 Mutopia Project의 공개 도메인 MIDI와 그 원곡에서 만든 단계별 연습 편곡이다. `엘리제를 위하여 WoO 59`는 입문과 쉬움과 고급 버전을 같은 제목 아래에서 선택한다. 출처와 편곡 범위는 [기본 공개 샘플 출처](docs/THIRD_PARTY_SAMPLES.md)에 기록했다. 회원과 운영 구성은 [회원과 곡 보관함 운영 문서](docs/LIBRARY-AUTH-KO.md)를 따른다.
|
||||||
|
|
||||||
[](https://raw.githubusercontent.com/gigliof/bach-to-basics/main/docs/demo.gif)
|
[](https://raw.githubusercontent.com/gigliof/bach-to-basics/main/docs/demo.gif)
|
||||||
|
|
||||||
| Sheet music | All views | Settings |
|
| Sheet music | All views | Settings |
|
||||||
|
|||||||
+101
-37
@@ -109,62 +109,126 @@ def delete_session(token: str | None) -> None:
|
|||||||
|
|
||||||
SAMPLES = (
|
SAMPLES = (
|
||||||
{
|
{
|
||||||
"storage_name": "first-snow-melody.mid",
|
"storage_name": "minuet-bwv-anh-114-beginner.mid",
|
||||||
"title": "첫눈이 내리는 밤",
|
"title": "미뉴에트 사장조 BWV Anh. 114",
|
||||||
"artist": "ITOSM 연습곡",
|
"artist": "크리스티안 페촐트",
|
||||||
"description": "오른손으로 천천히 익히는 다장조 멜로디 연습곡",
|
"description": "오른손 선율과 단순화한 왼손을 느린 속도로 함께 익히는 입문 편곡입니다. 손별 연습은 연습 화면에서 선택할 수 있습니다.",
|
||||||
"difficulty": "beginner",
|
"difficulty": "beginner",
|
||||||
"genre": "연습곡",
|
"genre": "바로크",
|
||||||
"arrangement": "melody",
|
|
||||||
"tags": ["다장조", "오른손", "겨울", "입문"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"storage_name": "first-snow-accompaniment.mid",
|
|
||||||
"title": "첫눈이 내리는 밤",
|
|
||||||
"artist": "ITOSM 연습곡",
|
|
||||||
"description": "같은 곡을 양손 반주형으로 편곡한 중급 연습 버전",
|
|
||||||
"difficulty": "intermediate",
|
|
||||||
"genre": "연습곡",
|
|
||||||
"arrangement": "accompaniment",
|
|
||||||
"tags": ["다장조", "양손", "반주", "겨울"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"storage_name": "blue-breeze-walk.mid",
|
|
||||||
"title": "파란 바람 산책",
|
|
||||||
"artist": "ITOSM 연습곡",
|
|
||||||
"description": "가벼운 리듬으로 연주하는 쉬운 멜로디",
|
|
||||||
"difficulty": "easy",
|
|
||||||
"genre": "가요풍",
|
|
||||||
"arrangement": "melody",
|
|
||||||
"tags": ["멜로디", "리듬", "쉬움", "가요풍"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"storage_name": "little-morning-waltz.mid",
|
|
||||||
"title": "아침의 작은 왈츠",
|
|
||||||
"artist": "ITOSM 연습곡",
|
|
||||||
"description": "3박자와 왼손 화음을 함께 익히는 피아노 독주곡",
|
|
||||||
"difficulty": "intermediate",
|
|
||||||
"genre": "왈츠",
|
|
||||||
"arrangement": "piano_solo",
|
"arrangement": "piano_solo",
|
||||||
"tags": ["3박자", "왈츠", "양손", "화음"],
|
"tags": ["미뉴에트", "사장조", "양손", "입문", "공개 도메인"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"storage_name": "minuet-bwv-anh-114.mid",
|
||||||
|
"title": "미뉴에트 사장조 BWV Anh. 114",
|
||||||
|
"artist": "크리스티안 페촐트",
|
||||||
|
"description": "안나 막달레나 바흐 음악수첩에 수록된 양손 원곡입니다. Mutopia Project 공개 도메인 자료를 사용했습니다.",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"genre": "바로크",
|
||||||
|
"arrangement": "piano_solo",
|
||||||
|
"tags": ["미뉴에트", "사장조", "양손", "입문", "공개 도메인"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"storage_name": "menuett-woo-82-easy.mid",
|
||||||
|
"title": "미뉴에트 내림마장조 WoO 82",
|
||||||
|
"artist": "루트비히 판 베토벤",
|
||||||
|
"description": "오른손 선율과 단순화한 왼손을 느린 속도로 함께 익히는 쉬운 편곡입니다. 손별 연습은 연습 화면에서 선택할 수 있습니다.",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"genre": "고전",
|
||||||
|
"arrangement": "piano_solo",
|
||||||
|
"tags": ["미뉴에트", "내림마장조", "양손", "쉬움", "공개 도메인"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"storage_name": "menuett-woo-82.mid",
|
||||||
|
"title": "미뉴에트 내림마장조 WoO 82",
|
||||||
|
"artist": "루트비히 판 베토벤",
|
||||||
|
"description": "고전주의의 프레이징과 양손 균형을 익히는 미뉴에트입니다. Mutopia Project 공개 도메인 자료를 사용했습니다.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"genre": "고전",
|
||||||
|
"arrangement": "piano_solo",
|
||||||
|
"tags": ["미뉴에트", "내림마장조", "양손", "고전", "공개 도메인"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"storage_name": "fur-elise-beginner.mid",
|
||||||
|
"title": "엘리제를 위하여 WoO 59",
|
||||||
|
"artist": "루트비히 판 베토벤",
|
||||||
|
"description": "유명한 첫 주제의 오른손 선율과 단순화한 왼손을 천천히 함께 익히는 입문 편곡입니다. 손별 연습은 연습 화면에서 선택할 수 있습니다.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"genre": "고전",
|
||||||
|
"arrangement": "piano_solo",
|
||||||
|
"tags": ["양손", "주제 연습", "입문", "베토벤", "공개 도메인"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"storage_name": "fur-elise-easy.mid",
|
||||||
|
"title": "엘리제를 위하여 WoO 59",
|
||||||
|
"artist": "루트비히 판 베토벤",
|
||||||
|
"description": "첫 주제의 오른손 선율과 단순화한 왼손 근음을 함께 연습하는 쉬운 양손 편곡입니다.",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"genre": "고전",
|
||||||
|
"arrangement": "piano_solo",
|
||||||
|
"tags": ["양손", "쉬운 양손", "주제 연습", "베토벤", "공개 도메인"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"storage_name": "fur-elise.mid",
|
||||||
|
"title": "엘리제를 위하여 WoO 59",
|
||||||
|
"artist": "루트비히 판 베토벤",
|
||||||
|
"description": "오른손 아르페지오와 양손 전환을 연습하는 대표적인 피아노 소품입니다. Mutopia Project 공개 도메인 자료를 사용했습니다.",
|
||||||
|
"difficulty": "advanced",
|
||||||
|
"genre": "고전",
|
||||||
|
"arrangement": "piano_solo",
|
||||||
|
"tags": ["아르페지오", "양손", "피아노 소품", "베토벤", "공개 도메인"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
LEGACY_SAMPLE_KEYS = (
|
||||||
|
"first-snow-melody.mid",
|
||||||
|
"first-snow-accompaniment.mid",
|
||||||
|
"blue-breeze-walk.mid",
|
||||||
|
"little-morning-waltz.mid",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _seed_samples() -> None:
|
def _seed_samples() -> None:
|
||||||
source_root = Path(__file__).with_name("sample_midi")
|
source_root = Path(__file__).with_name("sample_midi")
|
||||||
with connect() as connection:
|
with connect() as connection:
|
||||||
|
# 초기 시험용으로 만든 짧은 음형은 실제 공개 도메인 연습곡으로 교체한다.
|
||||||
|
# seed_key가 정확히 일치하는 기본 자료만 지우므로 회원 업로드에는 영향을 주지 않는다.
|
||||||
|
placeholders = ",".join("?" for _ in LEGACY_SAMPLE_KEYS)
|
||||||
|
connection.execute(
|
||||||
|
f"DELETE FROM midi_items WHERE owner_id IS NULL AND seed_key IN ({placeholders})",
|
||||||
|
LEGACY_SAMPLE_KEYS,
|
||||||
|
)
|
||||||
|
for legacy_name in LEGACY_SAMPLE_KEYS:
|
||||||
|
(MIDI_ROOT / legacy_name).unlink(missing_ok=True)
|
||||||
for sample in SAMPLES:
|
for sample in SAMPLES:
|
||||||
source = source_root / sample["storage_name"]
|
source = source_root / sample["storage_name"]
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
continue
|
continue
|
||||||
target = MIDI_ROOT / sample["storage_name"]
|
target = MIDI_ROOT / sample["storage_name"]
|
||||||
if not target.exists():
|
if not target.exists() or hashlib.sha256(target.read_bytes()).digest() != hashlib.sha256(source.read_bytes()).digest():
|
||||||
shutil.copyfile(source, target)
|
shutil.copyfile(source, target)
|
||||||
existing = connection.execute(
|
existing = connection.execute(
|
||||||
"SELECT id FROM midi_items WHERE seed_key = ?", (sample["storage_name"],)
|
"SELECT id FROM midi_items WHERE seed_key = ?", (sample["storage_name"],)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
|
content = target.read_bytes()
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
UPDATE midi_items
|
||||||
|
SET title = ?, artist = ?, description = ?, original_name = ?,
|
||||||
|
storage_name = ?, size_bytes = ?, sha256 = ?, visibility = 'public',
|
||||||
|
min_tier = 0, difficulty = ?, genre = ?, arrangement = ?,
|
||||||
|
tags_json = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
sample["title"], sample["artist"], sample["description"],
|
||||||
|
sample["storage_name"], sample["storage_name"], len(content),
|
||||||
|
hashlib.sha256(content).hexdigest(), sample["difficulty"], sample["genre"],
|
||||||
|
sample["arrangement"], json.dumps(sample["tags"], ensure_ascii=False),
|
||||||
|
utc_now(), existing["id"],
|
||||||
|
),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
content = target.read_bytes()
|
content = target.read_bytes()
|
||||||
connection.execute(
|
connection.execute(
|
||||||
|
|||||||
+97
-12
@@ -2,13 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from zipfile import BadZipFile, ZipFile
|
||||||
|
|
||||||
|
from defusedxml import ElementTree as SafeElementTree
|
||||||
from fastapi import APIRouter, Cookie, File, Form, HTTPException, Query, Request, Response, UploadFile
|
from fastapi import APIRouter, Cookie, File, Form, HTTPException, Query, Request, Response, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -27,13 +30,23 @@ from library_db import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
COOKIE_NAME = "b2b_session"
|
COOKIE_NAME = "b2b_session"
|
||||||
MAX_MIDI_BYTES = 10 * 1024 * 1024
|
MAX_SCORE_BYTES = 25 * 1024 * 1024
|
||||||
|
MAX_MXL_EXPANDED_BYTES = 50 * 1024 * 1024
|
||||||
|
MAX_MXL_ENTRIES = 200
|
||||||
MAX_USER_FILES = 100
|
MAX_USER_FILES = 100
|
||||||
MAX_USER_BYTES = 500 * 1024 * 1024
|
MAX_USER_BYTES = 500 * 1024 * 1024
|
||||||
USERNAME_RE = re.compile(r"^[A-Za-z0-9_-]{3,30}$")
|
USERNAME_RE = re.compile(r"^[A-Za-z0-9_-]{3,30}$")
|
||||||
DIFFICULTIES = {"beginner", "easy", "intermediate", "advanced", "expert"}
|
DIFFICULTIES = {"beginner", "easy", "intermediate", "advanced", "expert"}
|
||||||
ARRANGEMENTS = {"melody", "accompaniment", "piano_solo", "duet", "exercise"}
|
ARRANGEMENTS = {"melody", "accompaniment", "piano_solo", "duet", "exercise"}
|
||||||
VISIBILITIES = {"public", "members", "shared", "private"}
|
VISIBILITIES = {"public", "members", "shared", "private"}
|
||||||
|
SUPPORTED_UPLOADS = {
|
||||||
|
".mid": "audio/midi",
|
||||||
|
".midi": "audio/midi",
|
||||||
|
".xml": "application/vnd.recordare.musicxml+xml",
|
||||||
|
".musicxml": "application/vnd.recordare.musicxml+xml",
|
||||||
|
".mxl": "application/vnd.recordare.musicxml",
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AuthPayload(BaseModel):
|
class AuthPayload(BaseModel):
|
||||||
@@ -142,6 +155,65 @@ def _validate_metadata(visibility: str, min_tier: int, difficulty: str, arrangem
|
|||||||
raise HTTPException(status_code=400, detail="회원 전용 곡은 최소 등급이 1 이상이어야 합니다")
|
raise HTTPException(status_code=400, detail="회원 전용 곡은 최소 등급이 1 이상이어야 합니다")
|
||||||
|
|
||||||
|
|
||||||
|
def _xml_local_name(tag: str) -> str:
|
||||||
|
return tag.rsplit("}", 1)[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_musicxml(content: bytes) -> None:
|
||||||
|
try:
|
||||||
|
root = SafeElementTree.fromstring(content)
|
||||||
|
except Exception as error:
|
||||||
|
raise HTTPException(status_code=400, detail="올바른 MusicXML 악보가 아닙니다") from error
|
||||||
|
if _xml_local_name(root.tag) not in {"score-partwise", "score-timewise"}:
|
||||||
|
raise HTTPException(status_code=400, detail="MusicXML 악보의 최상위 요소가 올바르지 않습니다")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_mxl(content: bytes) -> None:
|
||||||
|
try:
|
||||||
|
with ZipFile(io.BytesIO(content)) as archive:
|
||||||
|
entries = archive.infolist()
|
||||||
|
if not entries or len(entries) > MAX_MXL_ENTRIES:
|
||||||
|
raise HTTPException(status_code=400, detail="MXL 악보의 내부 파일 구성이 올바르지 않습니다")
|
||||||
|
if sum(entry.file_size for entry in entries) > MAX_MXL_EXPANDED_BYTES:
|
||||||
|
raise HTTPException(status_code=413, detail="압축을 푼 MXL 악보는 50MB 이하여야 합니다")
|
||||||
|
names = {entry.filename for entry in entries}
|
||||||
|
container_name = "META-INF/container.xml"
|
||||||
|
if container_name not in names:
|
||||||
|
raise HTTPException(status_code=400, detail="MXL 악보에 container.xml이 없습니다")
|
||||||
|
container = SafeElementTree.fromstring(archive.read(container_name))
|
||||||
|
rootfile = next(
|
||||||
|
(
|
||||||
|
element.attrib.get("full-path", "")
|
||||||
|
for element in container.iter()
|
||||||
|
if _xml_local_name(element.tag) == "rootfile"
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
root_path = Path(rootfile)
|
||||||
|
if not rootfile or root_path.is_absolute() or ".." in root_path.parts or rootfile not in names:
|
||||||
|
raise HTTPException(status_code=400, detail="MXL 악보의 주 악보 경로가 올바르지 않습니다")
|
||||||
|
_parse_musicxml(archive.read(rootfile))
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except (BadZipFile, KeyError, ValueError) as error:
|
||||||
|
raise HTTPException(status_code=400, detail="올바른 MXL 악보가 아닙니다") from error
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_uploaded_score(extension: str, content: bytes) -> None:
|
||||||
|
if extension in {".mid", ".midi"}:
|
||||||
|
if len(content) < 14 or not content.startswith(b"MThd"):
|
||||||
|
raise HTTPException(status_code=400, detail="올바른 MIDI 파일이 아닙니다")
|
||||||
|
return
|
||||||
|
if extension in {".xml", ".musicxml"}:
|
||||||
|
_parse_musicxml(content)
|
||||||
|
return
|
||||||
|
if extension == ".mxl":
|
||||||
|
_validate_mxl(content)
|
||||||
|
return
|
||||||
|
if extension == ".pdf" and not content.startswith(b"%PDF-"):
|
||||||
|
raise HTTPException(status_code=400, detail="올바른 PDF 악보가 아닙니다")
|
||||||
|
|
||||||
|
|
||||||
def _resolve_share_ids(connection, usernames: list[str]) -> list[int]:
|
def _resolve_share_ids(connection, usernames: list[str]) -> list[int]:
|
||||||
normalized = [name.strip() for name in usernames if name.strip()]
|
normalized = [name.strip() for name in usernames if name.strip()]
|
||||||
if not normalized:
|
if not normalized:
|
||||||
@@ -183,6 +255,7 @@ def _serialize_item(row: dict, user: dict | None, shared_usernames: list[str]) -
|
|||||||
"artist": row["artist"],
|
"artist": row["artist"],
|
||||||
"description": row["description"],
|
"description": row["description"],
|
||||||
"original_name": row["original_name"],
|
"original_name": row["original_name"],
|
||||||
|
"file_type": Path(row["original_name"]).suffix.lower().lstrip("."),
|
||||||
"size_bytes": row["size_bytes"],
|
"size_bytes": row["size_bytes"],
|
||||||
"visibility": row["visibility"],
|
"visibility": row["visibility"],
|
||||||
"min_tier": row["min_tier"],
|
"min_tier": row["min_tier"],
|
||||||
@@ -373,7 +446,16 @@ def list_midi(
|
|||||||
SELECT m.*, u.display_name AS owner_name, u.username AS owner_username
|
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
|
FROM midi_items m LEFT JOIN users u ON u.id = m.owner_id
|
||||||
WHERE {' AND '.join(clauses)}
|
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,
|
params,
|
||||||
).fetchall()
|
).fetchall()
|
||||||
@@ -407,11 +489,12 @@ def download_midi(item_id: str, session_token: str | None = Cookie(default=None,
|
|||||||
raise HTTPException(status_code=403, detail="이 곡을 사용할 권한이 없습니다")
|
raise HTTPException(status_code=403, detail="이 곡을 사용할 권한이 없습니다")
|
||||||
path = MIDI_ROOT / row["storage_name"]
|
path = MIDI_ROOT / row["storage_name"]
|
||||||
if not path.is_file() or path.parent.resolve() != MIDI_ROOT.resolve():
|
if not path.is_file() or path.parent.resolve() != MIDI_ROOT.resolve():
|
||||||
raise HTTPException(status_code=404, detail="MIDI 파일을 찾을 수 없습니다")
|
raise HTTPException(status_code=404, detail="곡 또는 악보 파일을 찾을 수 없습니다")
|
||||||
encoded = quote(row["original_name"].replace("\r", "").replace("\n", ""))
|
encoded = quote(row["original_name"].replace("\r", "").replace("\n", ""))
|
||||||
|
extension = Path(row["original_name"]).suffix.lower()
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path,
|
path,
|
||||||
media_type="audio/midi",
|
media_type=SUPPORTED_UPLOADS.get(extension, "application/octet-stream"),
|
||||||
headers={"Content-Disposition": f"inline; filename*=UTF-8''{encoded}"},
|
headers={"Content-Disposition": f"inline; filename*=UTF-8''{encoded}"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -439,15 +522,17 @@ async def upload_midi(
|
|||||||
raise HTTPException(status_code=400, detail="곡 제목은 1~120자로 입력하세요")
|
raise HTTPException(status_code=400, detail="곡 제목은 1~120자로 입력하세요")
|
||||||
_validate_metadata(visibility, min_tier, difficulty, arrangement)
|
_validate_metadata(visibility, min_tier, difficulty, arrangement)
|
||||||
original_name = Path(file.filename or "upload.mid").name
|
original_name = Path(file.filename or "upload.mid").name
|
||||||
if Path(original_name).suffix.lower() not in {".mid", ".midi"}:
|
extension = Path(original_name).suffix.lower()
|
||||||
raise HTTPException(status_code=400, detail="MIDI 파일만 업로드할 수 있습니다")
|
if extension not in SUPPORTED_UPLOADS:
|
||||||
content = await file.read(MAX_MIDI_BYTES + 1)
|
raise HTTPException(status_code=400, detail="MIDI와 MusicXML과 MXL 및 PDF 악보만 업로드할 수 있습니다")
|
||||||
if len(content) > MAX_MIDI_BYTES:
|
content = await file.read(MAX_SCORE_BYTES + 1)
|
||||||
raise HTTPException(status_code=413, detail="MIDI 파일은 10MB 이하만 업로드할 수 있습니다")
|
if len(content) > MAX_SCORE_BYTES:
|
||||||
if len(content) < 14 or not content.startswith(b"MThd"):
|
raise HTTPException(status_code=413, detail="곡 또는 악보 파일은 25MB 이하여야 합니다")
|
||||||
raise HTTPException(status_code=400, detail="올바른 MIDI 파일이 아닙니다")
|
if not content:
|
||||||
|
raise HTTPException(status_code=400, detail="빈 파일은 업로드할 수 없습니다")
|
||||||
|
_validate_uploaded_score(extension, content)
|
||||||
item_id = secrets.token_hex(16)
|
item_id = secrets.token_hex(16)
|
||||||
storage_name = f"{item_id}.mid"
|
storage_name = f"{item_id}{extension}"
|
||||||
target = MIDI_ROOT / storage_name
|
target = MIDI_ROOT / storage_name
|
||||||
tag_values = _normalize_tags(tags)
|
tag_values = _normalize_tags(tags)
|
||||||
share_names = [name.strip() for name in shared_usernames.split(",") if name.strip()]
|
share_names = [name.strip() for name in shared_usernames.split(",") if name.strip()]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
|
import copy
|
||||||
from music21 import clef as m21clef, instrument as m21inst, layout as m21layout
|
from music21 import clef as m21clef, instrument as m21inst, layout as m21layout
|
||||||
from music21 import note as m21note, chord as m21chord
|
from music21 import note as m21note, chord as m21chord
|
||||||
|
from music21 import spanner as m21spanner
|
||||||
from music21 import stream as m21s
|
from music21 import stream as m21s
|
||||||
|
|
||||||
parts = list(score.parts)
|
parts = list(score.parts)
|
||||||
@@ -313,6 +314,58 @@ def _apply_piano_split(score): # noqa: C901
|
|||||||
first_m.remove(c)
|
first_m.remove(c)
|
||||||
first_m.insert(0, clef_obj)
|
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) ─────────────────
|
# ── Assemble score with brace StaffGroup (= grand staff) ─────────────────
|
||||||
new_score = m21s.Score()
|
new_score = m21s.Score()
|
||||||
if score.metadata:
|
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, "library.sqlite3").is_file())
|
||||||
self.assertTrue(Path(self.temp.name, "midi").is_dir())
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
+42
-17
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 기능 범위
|
## 기능 범위
|
||||||
|
|
||||||
Bach to Basics의 기존 파일 가져오기 기능을 유지하면서 서버 곡 보관함을 추가했다. 비회원은 전체 공개 MIDI를 바로 연주할 수 있다. 회원은 MIDI를 올리고 공개 범위와 회원 등급을 지정할 수 있다.
|
Bach to Basics의 기존 파일 가져오기 기능을 유지하면서 서버 곡 보관함을 추가했다. 비회원은 전체 공개 곡과 악보를 바로 연주할 수 있다. 회원은 MIDI와 악보를 올리고 공개 범위와 회원 등급을 지정할 수 있다.
|
||||||
|
|
||||||
공개 범위는 다음 네 단계다.
|
공개 범위는 다음 네 단계다.
|
||||||
|
|
||||||
@@ -28,22 +28,32 @@ Bach to Basics의 기존 파일 가져오기 기능을 유지하면서 서버
|
|||||||
|
|
||||||
## 화면 진입
|
## 화면 진입
|
||||||
|
|
||||||
학습 화면 상단에 다음 메뉴를 항상 표시한다.
|
학습 화면 상단에 계정 상태에 맞는 메뉴를 표시한다.
|
||||||
|
|
||||||
- `곡 보관함`: 공개 샘플과 접근 가능한 회원 곡을 검색하고 연주한다.
|
- `곡 보관함`: 공개 샘플과 접근 가능한 회원 곡을 검색하고 연주한다.
|
||||||
- `로그인·가입`: 로그인과 회원 가입을 전환한다. 로그인 뒤에는 내 계정과 비밀번호 변경 및 회원 탈퇴를 관리한다.
|
- `로그인·회원가입`: 비회원에게 표시한다. 창 안에서 로그인과 회원 가입을 전환한다.
|
||||||
- `관리자`: 비회원에게는 관리자 로그인 안내를 표시한다. 관리자 로그인 뒤에는 회원 등급과 역할 및 이용 상태를 관리하는 회원 관리 화면으로 바로 이동한다.
|
- `내 계정`: 로그인한 회원에게 표시한다. 비밀번호 변경과 회원 탈퇴를 관리한다.
|
||||||
|
- `관리자 패널`: 관리자 권한으로 로그인했을 때만 표시한다. 회원 등급과 역할 및 이용 상태를 관리하는 화면으로 바로 이동한다.
|
||||||
|
|
||||||
390픽셀 모바일 화면에서는 첫 줄에 회원 메뉴를 배치하고 둘째 줄에 화면 전환 메뉴와 MIDI 장치를 배치한다. PWA 시작 문서와 서비스 워커는 새 배포를 즉시 확인하도록 설정한다. 예전 화면이 열린 상태라면 한 번 새로고침하면 새 서비스 워커가 활성화된다.
|
390픽셀 모바일 화면에서는 첫 줄에 회원 메뉴를 배치하고 둘째 줄에 화면 전환 메뉴와 MIDI 장치를 배치한다. 이전 서비스 워커가 로그인과 관리자 메뉴의 오래된 화면을 계속 제공하는 문제가 있어 오프라인 캐시를 해제했다. 설치형 바로가기는 유지하며 온라인 접속 때는 항상 최신 HTML과 해시 자산을 사용한다.
|
||||||
|
|
||||||
## MIDI 업로드
|
## 연주 시작과 음표 표시
|
||||||
|
|
||||||
- 허용 확장자: `.mid`, `.midi`
|
곡을 맨 처음부터 재생하면 시계를 3초 전부터 움직인다. 이 준비 시간 동안 첫 음표가 건반 쪽으로 내려오며 화면 가운데에 3부터 1까지 남은 시간을 표시한다. 실제 MIDI 음은 원래 시작 시각인 0초부터 재생하므로 첫 음을 놓치지 않고 준비할 수 있다. 중간 위치에서 다시 재생할 때는 준비 시간을 반복하지 않는다.
|
||||||
- 최대 파일 크기: 10MB
|
|
||||||
|
화면 건반을 누를 때와 MIDI 파일을 재생할 때는 같은 피아노 샘플 엔진과 현재 선택한 악기를 사용한다. 검은 건반에 대응하는 내려오는 음표는 손 구분 색상을 유지하되 흰 건반 음표보다 명도를 낮춰 구분한다.
|
||||||
|
|
||||||
|
## 곡과 악보 업로드
|
||||||
|
|
||||||
|
- 허용 확장자: `.mid`, `.midi`, `.xml`, `.musicxml`, `.mxl`, `.pdf`
|
||||||
|
- 최대 파일 크기: 25MB
|
||||||
- 회원별 최대 파일 수: 100개
|
- 회원별 최대 파일 수: 100개
|
||||||
- 회원별 최대 보관 용량: 500MB
|
- 회원별 최대 보관 용량: 500MB
|
||||||
- 파일 이름에서 디렉터리 요소 제거
|
- 파일 이름에서 디렉터리 요소 제거
|
||||||
- `MThd` 헤더 확인
|
- MIDI는 `MThd` 헤더 확인
|
||||||
|
- MusicXML은 안전한 XML 파서로 루트 악보 구조 확인
|
||||||
|
- MXL은 압축 경로와 내부 파일 수 및 해제 용량과 주 악보 구조 확인
|
||||||
|
- PDF는 파일 서명 확인
|
||||||
- 임의 저장 이름과 SHA-256 해시 기록
|
- 임의 저장 이름과 SHA-256 해시 기록
|
||||||
- 곡 제목과 아티스트 및 설명
|
- 곡 제목과 아티스트 및 설명
|
||||||
- 난이도와 장르 및 편곡 형태
|
- 난이도와 장르 및 편곡 형태
|
||||||
@@ -58,14 +68,27 @@ Bach to Basics의 기존 파일 가져오기 기능을 유지하면서 서버
|
|||||||
|
|
||||||
## 공개 샘플
|
## 공개 샘플
|
||||||
|
|
||||||
상업 가요를 복제하지 않고 직접 만든 다음 연습곡을 전체 공개 샘플로 제공한다.
|
Mutopia Project가 공개 도메인으로 배포하는 실제 클래식 MIDI를 전체 공개 샘플로 제공한다. 출처와 파일별 권리 표시는 `THIRD_PARTY_SAMPLES.md`에 기록한다.
|
||||||
|
|
||||||
- 첫눈이 내리는 밤: 입문 멜로디
|
- 미뉴에트 사장조 BWV Anh. 114: 입문 양손 편곡과 쉬운 양손 원곡
|
||||||
- 첫눈이 내리는 밤: 중급 양손 반주
|
- 미뉴에트 내림마장조 WoO 82: 쉬운 양손 편곡과 중급 양손 원곡
|
||||||
- 파란 바람 산책: 쉬운 가요풍 멜로디
|
- 엘리제를 위하여 WoO 59: 입문 양손과 쉬운 양손 및 고급 원곡
|
||||||
- 아침의 작은 왈츠: 중급 피아노 독주
|
|
||||||
|
|
||||||
같은 제목의 난이도와 편곡 형태를 한 묶음에서 고르는 동작은 `첫눈이 내리는 밤`의 두 버전으로 확인한다.
|
같은 제목의 난이도별 버전은 한 묶음에서 선택한다. 기본 단계별 연습곡은 모두 오른손과 왼손을 포함한 피아노 독주로 제공한다. 손별 연습은 연습 화면에서 선택한다. 공개 도메인 원곡에서 만든 단계별 연습 편곡은 `scripts/build_sample_variants.mjs`로 다시 만들 수 있다.
|
||||||
|
|
||||||
|
## 악보 표시
|
||||||
|
|
||||||
|
MIDI를 선택하면 서버에서 MusicXML로 변환한 뒤 AlphaTab으로 오선 악보를 표시한다. 배포 파일에는 다음 자산을 포함한다.
|
||||||
|
|
||||||
|
- Bravura SMuFL 악보 글꼴과 OFL 고지
|
||||||
|
- AlphaTab 전용 Worker와 AudioWorklet 및 코어 모듈
|
||||||
|
- `.mjs` 파일의 `application/javascript` MIME 설정
|
||||||
|
|
||||||
|
운영 환경에서는 악보 렌더링을 UI 스레드에서 처리한다. Vite 번들과 별도 Worker가 렌더링 자원을 서로 다르게 초기화해 빈 악보가 되던 문제를 피하기 위한 설정이다.
|
||||||
|
|
||||||
|
전체 화면의 가로 악보는 외부 재생 시계의 시간과 AlphaTab tick을 함께 사용해 커서를 갱신한다. 커서가 화면 너비의 약 40퍼센트 지점에 오면 악보가 부드럽게 따라간다. 확인한 AlphaTab 1.8.3을 고정해 내부 커서 경로가 예고 없이 바뀌지 않게 한다.
|
||||||
|
|
||||||
|
MIDI 변환 악보에서 C6 이상의 음이 한 마디에 두 개 이상 이어지면 8va를 적용한다. C2 이하의 연속 저음에는 8vb를 적용한다. C7 이상과 C1 이하는 한 음만 있어도 적용한다. 옥타브 기호는 표기 위치만 바꾸며 실제 재생 음높이는 유지한다.
|
||||||
|
|
||||||
## 초기 관리자
|
## 초기 관리자
|
||||||
|
|
||||||
@@ -90,8 +113,10 @@ Bach to Basics의 기존 파일 가져오기 기능을 유지하면서 서버
|
|||||||
2. 회원 가입과 로그인 및 로그아웃을 확인한다.
|
2. 회원 가입과 로그인 및 로그아웃을 확인한다.
|
||||||
3. 비공개 곡이 다른 회원과 비회원에게 보이지 않는지 확인한다.
|
3. 비공개 곡이 다른 회원과 비회원에게 보이지 않는지 확인한다.
|
||||||
4. 회원 공개와 지정 공유 및 최소 등급 제한을 확인한다.
|
4. 회원 공개와 지정 공유 및 최소 등급 제한을 확인한다.
|
||||||
5. MIDI 업로드와 한글 제목 및 태그 검색을 확인한다.
|
5. MIDI와 MusicXML과 MXL 및 PDF 업로드와 한글 제목 및 태그 검색을 확인한다.
|
||||||
6. 같은 제목의 멜로디와 반주 버전을 각각 불러온다.
|
6. 같은 제목의 입문과 쉬움 및 원곡 버전을 각각 불러온다. 모든 단계에서 오른손과 왼손 악보가 함께 표시되는지 확인한다.
|
||||||
7. 비밀번호 변경 후 기존 세션이 끝나는지 확인한다.
|
7. 비밀번호 변경 후 기존 세션이 끝나는지 확인한다.
|
||||||
8. 시험 회원 탈퇴 후 계정과 업로드가 제거되는지 확인한다.
|
8. 시험 회원 탈퇴 후 계정과 업로드가 제거되는지 확인한다.
|
||||||
9. 모바일 화면에서 검색과 업로드 및 계정 화면이 가로로 넘치지 않는지 확인한다.
|
9. 모바일 화면에서 검색과 업로드 및 계정 화면이 가로로 넘치지 않는지 확인한다.
|
||||||
|
10. 전체 화면에서 50초 이상 이동한 뒤 악보 커서와 가로 스크롤이 진행 막대를 따라가는지 확인한다.
|
||||||
|
11. 고음과 저음이 많은 MIDI를 변환해 MusicXML에 8va와 8vb가 생성되는지 확인한다.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 기본 공개 샘플 출처
|
||||||
|
|
||||||
|
곡 보관함의 기본 샘플은 Mutopia Project에서 공개 도메인으로 배포하는 MIDI 자료다. 회원이 올린 파일과 구분하기 위해 서버 시작 시 `seed_key`가 있는 기본 자료로 등록한다.
|
||||||
|
|
||||||
|
| 표시 제목 | 작곡가 | 원본 페이지 | 파일 | 권리 표시 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 미뉴에트 사장조 BWV Anh. 114 | 크리스티안 페촐트 | <https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=75> | `minuet-bwv-anh-114.mid` | Public Domain |
|
||||||
|
| 미뉴에트 내림마장조 WoO 82 | 루트비히 판 베토벤 | <https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=904> | `menuett-woo-82.mid` | Public Domain |
|
||||||
|
| 엘리제를 위하여 WoO 59 | 루트비히 판 베토벤 | <https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=931> | `fur-elise.mid` | Public Domain |
|
||||||
|
|
||||||
|
Mutopia Project의 배포 안내는 <https://www.mutopiaproject.org/legal.html>에서 확인할 수 있다. 원본 악보의 작곡 저작권과 MIDI 조판 자료의 표시를 혼동하지 않도록 각 곡의 원본 페이지도 함께 기록한다.
|
||||||
|
|
||||||
|
## 난이도별 연습 편곡
|
||||||
|
|
||||||
|
같은 곡을 단계별로 익힐 수 있도록 위 공개 도메인 MIDI에서 다음 연습 편곡을 만들었다. 원곡의 제목과 작곡가는 유지한다. 모든 단계별 파일에는 오른손과 왼손을 함께 넣는다. 곡 보관함에서는 같은 제목 아래에 난이도가 다른 피아노 독주 버전으로 표시한다. 손별 연습은 연습 화면의 왼손과 오른손 선택 기능을 사용한다.
|
||||||
|
|
||||||
|
| 원곡 | 단계 | 편곡 내용 | 파일 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 미뉴에트 사장조 BWV Anh. 114 | 입문 | 느린 오른손 선율 | `minuet-bwv-anh-114-beginner.mid` |
|
||||||
|
| 미뉴에트 사장조 BWV Anh. 114 | 쉬움 | 양손 원곡 | `minuet-bwv-anh-114.mid` |
|
||||||
|
| 미뉴에트 내림마장조 WoO 82 | 쉬움 | 느린 오른손 선율 | `menuett-woo-82-easy.mid` |
|
||||||
|
| 미뉴에트 내림마장조 WoO 82 | 중급 | 양손 원곡 | `menuett-woo-82.mid` |
|
||||||
|
| 엘리제를 위하여 WoO 59 | 입문 | 첫 주제 오른손 선율 | `fur-elise-beginner.mid` |
|
||||||
|
| 엘리제를 위하여 WoO 59 | 쉬움 | 첫 주제와 단순화한 왼손 근음 | `fur-elise-easy.mid` |
|
||||||
|
| 엘리제를 위하여 WoO 59 | 고급 | 전체 양손 원곡 | `fur-elise.mid` |
|
||||||
|
|
||||||
|
편곡 파일은 `node scripts/build_sample_variants.mjs`로 다시 만들 수 있다. 원본 MIDI가 바뀌면 생성 결과도 달라지므로 반영 전에 파일 크기와 음표 수와 실제 재생을 다시 검사한다.
|
||||||
+2
-2
@@ -7,9 +7,9 @@
|
|||||||
'unsafe-inline' for script-src is needed for the error overlay below;
|
'unsafe-inline' for script-src is needed for the error overlay below;
|
||||||
tighten by moving it to a separate .js file if desired. -->
|
tighten by moving it to a separate .js file if desired. -->
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' https://gleitz.github.io https://danigb.github.io https://smpldsnds.github.io https://goldst.dev; worker-src 'self' blob:; object-src 'none'; base-uri 'self'" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' https://gleitz.github.io https://danigb.github.io https://smpldsnds.github.io https://goldst.dev; worker-src 'self' blob:; object-src 'none'; base-uri 'self'" />
|
||||||
<title>Bach to Basics 피아노 학습</title>
|
<title>ITOSM 피아노 학습실</title>
|
||||||
<meta name="description" content="MIDI와 MusicXML 악보를 불러와 건반과 내려오는 음표를 보며 연습하는 한국어 피아노 학습 도구" />
|
<meta name="description" content="MIDI와 MusicXML 악보를 불러와 건반과 내려오는 음표를 보며 연습하는 한국어 피아노 학습 도구" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/itosm-piano-mark.svg" />
|
||||||
<link rel="icon" type="image/png" href="/favicon-light.png" />
|
<link rel="icon" type="image/png" href="/favicon-light.png" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
|||||||
@@ -38,6 +38,18 @@ server {
|
|||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# AlphaTab의 Worker와 AudioWorklet은 ES 모듈이다. nginx 기본 MIME 표에
|
||||||
|
# mjs가 없으면 브라우저가 nosniff 정책에 따라 실행을 차단한다.
|
||||||
|
location ~* \.mjs$ {
|
||||||
|
default_type application/javascript;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
}
|
||||||
|
|
||||||
# API proxy, strips the /api prefix on the way through
|
# API proxy, strips the /api prefix on the way through
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend:8000/;
|
proxy_pass http://backend:8000/;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bach-to-basics/shared": "workspace:*",
|
"@bach-to-basics/shared": "workspace:*",
|
||||||
"@coderline/alphatab": "^1.8.3",
|
"@coderline/alphatab": "1.8.3",
|
||||||
"@tonejs/midi": "^2.0.6",
|
"@tonejs/midi": "^2.0.6",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"pixi.js": "^8.18.1",
|
"pixi.js": "^8.18.1",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
/*!
|
||||||
|
* alphaTab v1.8.3 (, build 32)
|
||||||
|
*
|
||||||
|
* Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* Integrated Libraries:
|
||||||
|
*
|
||||||
|
* Library: TinySoundFont
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (C) 2017, 2018 Bernhard Schelling
|
||||||
|
* URL: https://github.com/schellingb/TinySoundFont
|
||||||
|
* Purpose: SoundFont loading and Audio Synthesis
|
||||||
|
*
|
||||||
|
* Library: SFZero
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (C) 2012 Steve Folta ()
|
||||||
|
* URL: https://github.com/stevefolta/SFZero
|
||||||
|
* Purpose: TinySoundFont is based on SFZEro
|
||||||
|
*
|
||||||
|
* Library: Haxe Standard Library
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (C)2005-2025 Haxe Foundation
|
||||||
|
* URL: https://github.com/HaxeFoundation/haxe/tree/development/std
|
||||||
|
* Purpose: XML Parser & Zip Inflate Algorithm
|
||||||
|
*
|
||||||
|
* Library: SharpZipLib
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright © 2000-2018 SharpZipLib Contributors
|
||||||
|
* URL: https://github.com/icsharpcode/SharpZipLib
|
||||||
|
* Purpose: Zip Deflate Algorithm for writing compressed Zips
|
||||||
|
*
|
||||||
|
* Library: NVorbis
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (c) 2020 Andrew Ward
|
||||||
|
* URL: https://github.com/NVorbis/NVorbis
|
||||||
|
* Purpose: Vorbis Stream Decoding
|
||||||
|
*
|
||||||
|
* Library: libvorbis
|
||||||
|
* License: BSD-3-Clause
|
||||||
|
* Copyright: Copyright (c) 2002-2020 Xiph.org Foundation
|
||||||
|
* URL: https://github.com/xiph/vorbis
|
||||||
|
* Purpose: NVorbis adopted some code from libvorbis.
|
||||||
|
*
|
||||||
|
* @preserve
|
||||||
|
* @license
|
||||||
|
*/
|
||||||
|
import * as alphaTab from "./alphaTab.core.mjs";
|
||||||
|
//#region src/alphaTab.worker.ts
|
||||||
|
/**@target web */
|
||||||
|
alphaTab.Environment.initializeWorker();
|
||||||
|
//#endregion
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*!
|
||||||
|
* alphaTab v1.8.3 (, build 32)
|
||||||
|
*
|
||||||
|
* Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* Integrated Libraries:
|
||||||
|
*
|
||||||
|
* Library: TinySoundFont
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (C) 2017, 2018 Bernhard Schelling
|
||||||
|
* URL: https://github.com/schellingb/TinySoundFont
|
||||||
|
* Purpose: SoundFont loading and Audio Synthesis
|
||||||
|
*
|
||||||
|
* Library: SFZero
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (C) 2012 Steve Folta ()
|
||||||
|
* URL: https://github.com/stevefolta/SFZero
|
||||||
|
* Purpose: TinySoundFont is based on SFZEro
|
||||||
|
*
|
||||||
|
* Library: Haxe Standard Library
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (C)2005-2025 Haxe Foundation
|
||||||
|
* URL: https://github.com/HaxeFoundation/haxe/tree/development/std
|
||||||
|
* Purpose: XML Parser & Zip Inflate Algorithm
|
||||||
|
*
|
||||||
|
* Library: SharpZipLib
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright © 2000-2018 SharpZipLib Contributors
|
||||||
|
* URL: https://github.com/icsharpcode/SharpZipLib
|
||||||
|
* Purpose: Zip Deflate Algorithm for writing compressed Zips
|
||||||
|
*
|
||||||
|
* Library: NVorbis
|
||||||
|
* License: MIT
|
||||||
|
* Copyright: Copyright (c) 2020 Andrew Ward
|
||||||
|
* URL: https://github.com/NVorbis/NVorbis
|
||||||
|
* Purpose: Vorbis Stream Decoding
|
||||||
|
*
|
||||||
|
* Library: libvorbis
|
||||||
|
* License: BSD-3-Clause
|
||||||
|
* Copyright: Copyright (c) 2002-2020 Xiph.org Foundation
|
||||||
|
* URL: https://github.com/xiph/vorbis
|
||||||
|
* Purpose: NVorbis adopted some code from libvorbis.
|
||||||
|
*
|
||||||
|
* @preserve
|
||||||
|
* @license
|
||||||
|
*/
|
||||||
|
import * as alphaTab from "./alphaTab.core.mjs";
|
||||||
|
//#region src/alphaTab.worklet.ts
|
||||||
|
/**@target web */
|
||||||
|
alphaTab.Environment.initializeAudioWorklet();
|
||||||
|
//#endregion
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
FONTLOG for the Bravura font
|
||||||
|
============================
|
||||||
|
|
||||||
|
This file provides detailed information on the Bravura Font Software. This information should be distributed along with the Bravura fonts and any derivative works.
|
||||||
|
|
||||||
|
The Bravura Font Software is a family consisting of two fonts, Bravura and Bravura Text.
|
||||||
|
|
||||||
|
|
||||||
|
Basic font information
|
||||||
|
----------------------
|
||||||
|
Bravura is a Unicode typeface designed by Steinberg for its in-development music notation and scoring application.
|
||||||
|
|
||||||
|
It is compliant with version 1.2 of the Standard Music Font Layout (SMuFL), a community-driven standard for how music symbols should be laid out in the Unicode Private Use Area (PUA) in the Basic Multilingual Plane (BMP) for compatibility between different scoring applications.
|
||||||
|
|
||||||
|
More information about Steinberg's scoring application can be found at:
|
||||||
|
|
||||||
|
http://www.steinberg.net/
|
||||||
|
|
||||||
|
The latest version of the SMuFL specification can be found at:
|
||||||
|
|
||||||
|
https://w3c.github.io/smufl/gitbook/
|
||||||
|
|
||||||
|
|
||||||
|
Note for developers
|
||||||
|
-------------------
|
||||||
|
If you are intending to use Bravura as part of your own software application, please refer to the SMuFL specification for useful information about Bravura's metrics and how glyphs are registered:
|
||||||
|
|
||||||
|
https://w3c.github.io/smufl/gitbook/specification/index.html
|
||||||
|
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
---------
|
||||||
|
|
||||||
|
22 May 2013 (Daniel Spreadbury) Bravura version 0.1
|
||||||
|
- Initial pre-release to coincide with 2013 Music Encoding Conference
|
||||||
|
|
||||||
|
5 July 2013 (Daniel Spreadbury) Bravura version 0.2
|
||||||
|
- Bravura 0.2 is designed to be compatible with SMuFL 0.5
|
||||||
|
- Thanks to Mark Johnson and Emil Wojtacki for contributing a number of glyphs to the font
|
||||||
|
- Many existing code points have been changed, as a result of hundreds of new glyphs being added, plus a number of new ranges.
|
||||||
|
- Added long (U+E008) and very long (U+E009) system dividers for very large scores.
|
||||||
|
- Added heavy (U+E034), double heavy (U+E035) and dotted (U+E037) barlines.
|
||||||
|
- Added square coda (U+E047) and small repeat signs for repeats within bars (U+E04A, U+E04B).
|
||||||
|
- Added recommended stylistic alternates for segno (U+E045) and coda (U+E046) for the appearance preferred by Japanese publishers.
|
||||||
|
- Added quindicesima bassa G clef (U+E061) and F clef (U+E070), G clef combined with C clef (U+E066), G clefs designed to be ligated with numbers below and above to show the transposition of an instrument (U+E067, U+E068), plus recommended ligatures for G and F clefs with numbers above and below; also added G, C and F clefs with arrows up and down, which may be used either as alternatives for octave clefs or to represent the extremes of register on an instrument (U+E069, U+E06A, U+E06C, U+E06D, U+E074, U+E075), and semi-pitched percussion clefs (U+E078, U+E079), plus a bridge clef (U+E085).
|
||||||
|
- Removed “tall” versions of 6- and 4-string tab clefs, and instead made them recommended stylistic alternates, together with versions that use letterforms with serifs.
|
||||||
|
- Added +, -, X (multiply), comma, parentheses glyphs for time signatures (U+E09C–U+E0A6), plus basic fractions (U+E0A7–U+E0AB), and Penderecki-style open time signature (U+E0AD).
|
||||||
|
- Added specific noteheads for double whole note (U+E100) and whole note (U+E101) to the noteheads range rather than relying on the glyphs in the pre-composed notes range.
|
||||||
|
- Added shaped noteheads for specific note values (double whole note, whole note, half note, and quarter note and shorter); also added large up- and down-pointing triangles for highest/lowest notes played by an instrument (U+E102–U+E109).
|
||||||
|
- Added large slashed circular noteheads (U+E136–U+E139) as used by Stockhausen for notating gong/tam-tam hits.
|
||||||
|
- Added combining glyphs for note clusters of specific note values (U+E154–U+E171).
|
||||||
|
- Added noteheads with solfège and chromatic note names embedded within them, as seen in “EZ-Play” educational scores (U+E180–U+E1D9).
|
||||||
|
- Added specific range of noteheads for sacred harp shape note singing (U+E1E0–U+E1EF).
|
||||||
|
- Added pre-composed 1024th notes (U+E214–U+E215), tails (U+E28E–U+E28F) and rest (U+E52C).
|
||||||
|
- Added range for typing simple beamed groups of notes in text-based applications, (U+E220–U+E233). Designed to be used in conjunction with pre-composed notes (U+E200–U+E216), and allowing beamed groups with rhythmic values between 8th notes and 64th notes, plus ties and triplets.
|
||||||
|
- Added combining stems for multiphonics, damp, sussurando, Saunders vibrato pulse accent (U+E248–U+E24D).
|
||||||
|
- Added four- and five-stroke tremolos (U+E263–U+E264, U+E268–U+E269) plus Wieniawski-style unmeasured tremolo glyphs (U+E26C–U+26D).
|
||||||
|
- Added stylistic alternates for flags (U+E280–U+E28F): straight flags; and shorter stem-up flags to avoid collisions with augmentation dots.
|
||||||
|
- Separated accidentals into several discrete ranges based around the various accidental systems, including 12-edo, 24-edo, the system of up- and down-pointing arrows favoured by Gould, Stein-Zimmermann (also known as Tartini-Couper), Maneri-Sims, Ben Johnston, Marc Sabat and Wolfgang von Schweinitz’s Extended Helmholtz-Ellis Just Intonation Pitch Notation. A range has also been reserved for George Secor and Dave Keenan’s Sagittal system of accidentals (U+E360–U+E47F).
|
||||||
|
- Added accidentals used in Turkish folk music (U+E4C8–U+E4CF).
|
||||||
|
- Added Persian accidentals (U+E4D0–U+E4D1).
|
||||||
|
- Added staccatissimo wedge (U+E4E5–U+E4E6) and stroke (U+E4E7–U+E4E8) glyphs.
|
||||||
|
- Added very short (U+E502–U+E503) and very long (U+E508–U+E509) fermatas, plus short caesura (U+E50D).
|
||||||
|
- Added left and right halves of multirest H-bars (U+E52E–U+E52F) and old-style quarter rest as seen in e.g. Novello editions (U+E530).
|
||||||
|
- Added ventiduesima (three octaves, “22”) glyphs to octaves range (U+E557–U+E559).
|
||||||
|
- Added precomposed glyphs for common dynamics (U+E567–U+E57B) and niente circle for hairpins (U+E57E).
|
||||||
|
- Added schleifer (long mordent, U+E5AA) and Haydn ornament (U+E5D1).
|
||||||
|
- Added additional brass techniques, including short, medium and long versions of lift, doit, lip fall, smooth fall, rough fall, plus jazz turn (U+E5E0–U+E5FB).
|
||||||
|
- Added range of glyphs for embouchure tightness (U+E61B–U+E623), reed position (U+E624–U+E626), multiphonics (U+E627–U+E629), and stylistic alternates for double- and triple-tonguing with no slurs.
|
||||||
|
- Added further overpressure glyphs (U+E64B–U+E64D), plus jété, fouetté, Rebecca Saunders’s “vibrato pulse” accent and thumb position (U+E64E–U+E652) to string techniques range.
|
||||||
|
- Added plectrum pictogram (U+E669) and combining damp glyph for note stems (U+E66A) to plucked techniques range.
|
||||||
|
- Added arrows for breathing and intonation, plus combining sussurando glyph for note stems (U+E687–U+E68B), to vocal techniques range.
|
||||||
|
- Added pedal pictograms, sostenuto pedal symbols, and half-pedal marks (U+E6A5–U+E6AC) to keyboard techniques range.
|
||||||
|
- Added pictograms for metal rod (U+E6CF) and tuning key (U+E6D0) to harp techniques range.
|
||||||
|
- Added Smith Brindle’s pictograms for tuned percussion instruments (U+E6E5–U+E6E8).
|
||||||
|
- Added pictogram for Indian table (U+E720), plus stylistic alternate for tambourine (U+E718) as used by Stockhausen.
|
||||||
|
- Added pictogram for football rattle (U+E735), plus Smith Brindle’s pictogram for castanets (U+E738) as a stylistic alternate.
|
||||||
|
- Added pictogram for handbell (U+E765), plus stylistic alternates for cow bell (from Berio) and sleigh bell (from Smith Brindle).
|
||||||
|
- Added pictogram for Chinese cymbal (U+E776).
|
||||||
|
- Added pictogram for tam-tam with beater from Smith Brindle (U+E791).
|
||||||
|
- Added pictogram for maracas (U+E7A2), rainstick (U+E7A7), plus stylistic alternate for maraca from Smith Brindle (U+E7A1).
|
||||||
|
- Added pictogram for megaphone (U+E7B9).
|
||||||
|
- Added soft and hard glockenspiel beaters (U+E7F0–U+E7F7), superball beaters (U+E81C–U+E81F), wound beaters with hard and soft cores (U+E821–U+E828), plus soft, medium and hard gum beaters (U+E829–U+E834).
|
||||||
|
- Added pluck lift (U+E887) to handbells range.
|
||||||
|
- Added “Theme” indicators to analytics range (U+E8E4–U+E8E9).
|
||||||
|
- Added minor (minus sign) glyph (U+E904) to chord symbols range.
|
||||||
|
- Added mensural proportion glyphs (U+E9DB–U+E9DF).
|
||||||
|
- Added combining raise and lower glyphs (U+EA2C–U+EA2D) to figured bass range.
|
||||||
|
- Added repetition, angle brackets, and prefix + and ring glyphs to function symbols range (U+EA63–U+EA68).
|
||||||
|
- Added new range for multi-segment lines, including moving all of the various “wiggle” glyphs (for trill, glissando, arpeggiando, vibrato, etc.) plus the 11 ornament strokes from the Unicode Musical Symbols range into this range, and adding further glyphs for variable speed trills, alternate arpeggiato ending glyphs, wavy lines, squaretooth and sawtooth lines, group glissando, circular motion, and variable speed and intensity of vibrato (U+EA80–U+EAD0).
|
||||||
|
- Added new range of pictograms for electronic music, including microphone, loudspeaker, transport controls, volume level and MIDI controller level (U+EAE0–U+EAF5).
|
||||||
|
- Added new “do not copy” glyphs (U+EB02–U+EB03), eyeglasses (U+EB04) and choral divide arrows (U+EB05–U+EB07) glyphs to the miscellaneous symbols range.
|
||||||
|
|
||||||
|
29 July 2013 (Daniel Spreadbury) Bravura version 0.3
|
||||||
|
- Bravura 0.3 is designed to be compatible with SMuFL 0.6
|
||||||
|
- Bravura now incorporates glyphs from the Sagittal font by George Secor and Dave Keenan (U+E360–U+E47F). For more information about the Sagittal system of accidentals and to download the stand-alone Sagittal font, see http://sagittal.org.
|
||||||
|
- Changed registration of many glyphs in the font to match the interim guidelines for font metrics and glyph registration in SMuFL 0.6.
|
||||||
|
- Added complete set of dynamics to liga table, and duplicated these entries in the ccmp table.
|
||||||
|
- Added opening parenthesis and closing parenthesis for noteheads (U+E10A–U+E10B), circled slash notehead, heavy X and heavy X with hat noteheads, as used in Dante Agostini's drum method (U+E10C–U+E10E).
|
||||||
|
- Added muted slash noteheads (U+E125–U+E127).
|
||||||
|
- Added "si" note name noteheads for French solfège (U+E187, U+E18F, U+E197), and H sharp note name noteheads for German (U+E1AE, U+E1C5, U+E1DC).
|
||||||
|
- Fixed the vertical position of U+E227 and U+E228 (continuing beams for 8th notes with short and long stems), and made the stem thickness of beamed groups of notes consistent with those of individual notes.
|
||||||
|
- Added combining rim shot stem (U+E24E).
|
||||||
|
- Added "sharp sharp" accidental for compatibility with MusicXML (U+E2A9).
|
||||||
|
- Added extended Stein-Zimmermann accidentals with arrows (U+E2F8–U+E304).
|
||||||
|
- Adjusted size and registration of Sims microtonal accidentals (U+E308–U+E30D).
|
||||||
|
- Added one-third-tone sharp and two-third-tones sharp accidentals as used by Xenakis (U+E4B0–U+E4B1).
|
||||||
|
- Significant revision to the ornaments range, including splitting into separate ranges (common ornaments, other baroque ornaments, combining strokes for trills/mordents, precomposed trills/mordents). A small number of glyphs from previous versions of SMuFL have been removed to make way for symbols drawn from Frederick Neumann’s authoritative book on baroque ornamentation.
|
||||||
|
- The turn symbol and its derivates (U+E594 et al) have been redrawn to have a more open appearance, for better reproduction at small staff sizes or when read at a distance.
|
||||||
|
- Added thumb pizzicato (U+E654).
|
||||||
|
- Added recommended stylistic alternates for Bartok pizzicato above/below (U+E660–U+E661).
|
||||||
|
- Added recommended stylistic alternates for 'Ped.' (U+E6A0) and 'Sost.' (U+E6A5) that do not include terminal dots.
|
||||||
|
- Added choke cymbal glyph (U+E875) from Weinberg.
|
||||||
|
- Added open, half-open and closed wah/volume pedals, left- and right-hand tapping glyphs for guitar (U+E8AD–U+E8B1).
|
||||||
|
- Added new range for arrows and arrowheads (U+EB00–U+EB1F), including moving the up/down/right/left arrows from the vocal techniques range (previously U+E687–U+E68A) into this new range.
|
||||||
|
- Redesigned 'do not copy' glyph (U+E833) to be a bit more aggressive.
|
||||||
|
|
||||||
|
27 November 2013 (Daniel Spreadbury) Bravura version 0.7
|
||||||
|
- Bravura 0.7 is designed to be compliant with SMuFL 0.7; its version number has been bumped such that it matches the version number of SMuFL it implements
|
||||||
|
- Bravura now defines three stylistic sets: ss01 is for glyphs for use on small staves (i.e. different optical size); ss02 is for short up-stem flags to avoid augmentation dots; ss03 is for straight flags
|
||||||
|
- The Bravura distribution now includes a JSON metadata file describing various recommended metrics and engraving defaults for applications that wish to use Bravura; the specification for this metadata file is documented in the main SMuFL specification
|
||||||
|
- Significantly expanded the repertoire of glyphs for Medieval and Renaissance notation, with new ranges for clefs, accidentals and ligatures, plus considerable reworking of the notes and prolations ranges.
|
||||||
|
- Significantly expanded the repertoire of glyphs for plainchant notation, with new ranges for staves, divisions, clefs and articulations, and a wider range of neumes.
|
||||||
|
- Added range for Daseian notation, as found in the ninth century treatises Musica enchiriadis and Scolica enchiriadis.
|
||||||
|
- Added new range of control characters for adjusting the staff position of staff-relative glyphs; no ligatures using these control characters are yet defined in Bravura, however.
|
||||||
|
- Added control characters for time signature digits to allow digits to be stacked vertically, and ligatures for numerator and denominator digits.
|
||||||
|
- Added narrow and wide staff line glyphs, intended for fonts designed for text-based applications.
|
||||||
|
- Added C clef ottava bassa, and recommended stylistic alternate for G clef ottava bassa with parentheses around the 8.
|
||||||
|
- Added square double whole note (breve) notehead.
|
||||||
|
- Added new combining harp string noise for stem glyph, and corresponding precomposed stem glyph.
|
||||||
|
- Added four further quarter-tone accidental symbols to "other microtonal accidentals" group.
|
||||||
|
- Added some percussion playing technique symbols from Dante Agostini’s method books.
|
||||||
|
- Added a golpe (tap the pick guard) glyph from Claude Worm’s flamenco guitar method book.
|
||||||
|
- Added short and long fermata glyphs as used by Henze.
|
||||||
|
- Added combining glyphs for accordion couplers, allowing the creation of any coupler diagram not explicitly encoded.
|
||||||
|
- Added 'pf' dynamic.
|
||||||
|
|
||||||
|
3 February 2014 (Daniel Spreadbury) Bravura version 0.80
|
||||||
|
- Many minor changes to glyph outlines following corrections suggested by the Adobe FDK checkOutlines tool
|
||||||
|
- Corrected JSON metadata for stem connection points on flag glyphs (e.g. flag8thDown, flag16thUp, etc.)
|
||||||
|
- Changed glyph registration of left and right parentheses for noteheads (noteheadParenthesisLeft, noteheadParenthesisRight)
|
||||||
|
- Changed font name settings in accordance with FontLab's OpenType PS font name guidelines (http://forum.fontlab.com/index.php?topic=313.0).
|
||||||
|
- Added Ivan Wyschnegradsky’s system of 72-EDO accidentals.
|
||||||
|
- Added Bosanquet’s comma up/down.
|
||||||
|
- Added left/right parentheses for accidentals.
|
||||||
|
- Added new ranges for Renaissance lute tablature, covering French/English, Italian/Spanish and German conventions.
|
||||||
|
- Added Britten’s curlew sign for a pause of an indeterminate length.
|
||||||
|
- Added push/pull signs for accordion.
|
||||||
|
- Added slashed sharp/flat accidentals used by John Tavener in his Byzantine-inspired choral works.
|
||||||
|
- Added separate noteheads for white mensural notation.
|
||||||
|
- Added inverted signum congruentiae.
|
||||||
|
- Added combined tenuto-accent articulation.
|
||||||
|
- Added quasi-random wiggly lines (wiggleRandom1, wiggleRandom2, wiggleRandom3, wiggleRandom4) to multi-segment lines range.
|
||||||
|
- Added flipped and large versions of constant circular motion (wiggleCircularConstantFlipped, wiggleCircularConstantLarge, wiggleCircularConstantFlippedLarge) to multi-segment lines range.
|
||||||
|
- Added combining top/middle/bottom segments for black and white rectangular note clusters.
|
||||||
|
- Added 2, 3, 4 and 6-dot divisi indicators for measured tremolos (tremoloDivisiDots2, tremoloDivisiDots3, etc.) to tremolos range.
|
||||||
|
- Added clavichord bebung glyphs for 2, 3, and 4 finger movements (keyboardBebung2DotsAbove, keyboardBebung3DotsBelow, etc.) to the keyboard techniques range.
|
||||||
|
- Added double-height parentheses and brackets (csymParensLeftTall, csymParensRightTall, csymBracketLeftTall, csymBracketRightTall) to the chord symbols range.
|
||||||
|
- Added stylistic alternates for time signature digits 0–9 suitable for use as large time signatures shown above/between staves (timeSig0Large through timeSig9Large).
|
||||||
|
– Added 'sfzp' (sforzato-piano) dynamic and ligature.
|
||||||
|
– Added Penderecki's quarter-flat and Busotti's three-quarter sharp accidentals.
|
||||||
|
– Added six further accordion coupler diagrams for right-hand 3 rank accordions, and accordion ricochet glyphs.
|
||||||
|
|
||||||
|
9 March 2014 (Daniel Spreadbury) Bravura version 0.85
|
||||||
|
– Updated glyph registration for articulations, such that articulations above the note are positioned sitting on the baseline, and articulations below the note are positioned hanging from the baseline.
|
||||||
|
– Added whole and half rests with leger lines, i.e. as if displayed outside the staff.
|
||||||
|
– Added clef for diatonic accordion.
|
||||||
|
– Added recommended stylistic alternates for C and F clef forms used in 18th century French music, and for an F clef form used in 19th century music across Europe.
|
||||||
|
– Added half-brackets for keyboard notation to show notes that should be played by the other hand.
|
||||||
|
– Many code point changes, including moving the staff divide arrows, percussion swish arrow, and quite a few of the microtonal accidentals; refer to the SMuFL documentation for further details.
|
||||||
|
– Further revisions to the plainchant ranges, including adding reversed virga, smaller version of punctum inclinatum, moving the punctum mora to the plainchant articulations range, and eliminating the precomposed podatus and clivis glyphs in favour of individual components that provide the means to construct these easily for any interval.
|
||||||
|
– Added new range for Kievian square notation, as used for liturgical chant in the Russian Orthodox Church.
|
||||||
|
– Added new glyphs for tabling one handbell and tabling a pair of handbells.
|
||||||
|
– Added alternative pedal heel glyph and pedal heel or toe glyph.
|
||||||
|
– Added recommended stylistic alternates for braces designed for use across different sizes of gaps, designed to be scaled uniformly rather than simply stretched vertically.
|
||||||
|
– Added many new electronic music pictograms, including speaker configurations, more transport controls, additional hardware devices, and so on.
|
||||||
|
– Added guitar fade in, fade out and swell glyphs.
|
||||||
|
– Added the glyphs used by the Corpus Monodicum project for modern transcription of plainchant.
|
||||||
|
|
||||||
|
17 April 2014 (Daniel Spreadbury) Bravura version 0.9
|
||||||
|
– Distribution now includes Bravura Text font, intended for use in text-based applications.
|
||||||
|
– Added new range for Kodály solfège hand signs.
|
||||||
|
– Added new range for Peter Hayes George’s Simplified Music Notation.
|
||||||
|
– Added narrow and wide versions of the sine wave, square wave and sawtooth wavy lines.
|
||||||
|
– Added wide versions of the black and white diamond noteheads, as used in some handbells music.
|
||||||
|
– Added turned (i.e. inverted) versions of up bow and down bow marks.
|
||||||
|
– Added oriscus liquescens and strophicus liquescens (for intervals of a second up to a fifth).
|
||||||
|
– Added oblique ligature forms for mensural notes describing intervals of a second up to a fifth for black, void, black and void, and white noteheads.
|
||||||
|
– Added single glyph for right and left repeat barlines, and a stylistic alternate using thick-thick rather than thin-thick-thin barlines.
|
||||||
|
– Added reversed versions of brackets to denote play with right/left hand, to allow the demarcation of the end of a passage to be played with the other hand.
|
||||||
|
– Extended stylistic set 01 (ss01) with new glyphs redesigned for display on smaller staff sizes: time signature digits; G, C and F clef; black, half, whole and double whole noteheads; standard articulations; dynamics letter forms; flags.
|
||||||
|
– Added recommended ligatures for standard noteheads and accidentals in parentheses.
|
||||||
|
– Improved registration of ornamentMiddleVerticalStroke.
|
||||||
|
– Added open arrowheads and arrows.
|
||||||
|
– Added Kievan half note on space, and Kievan beam.
|
||||||
|
– Added new percussion pictograms from the books by Sevsay and Peinkofer/Tannigel, plus new combining glyphs for stems showing the "crush" rudiment, "dead" notes, and to instruct the performer to turn the instrument.
|
||||||
|
– Added five further mensural proportion signs, from Apel's book.
|
||||||
|
– Added 12 new precomposed trills and mordents, based on Bach's ornamentation chart and ornaments found in Emmentaler.
|
||||||
|
– Added restHBarMiddle glyph, for text-based applications to construct H-bar multirests of variable width.
|
||||||
|
– Added noteheadWholeFilled and noteheadHalfFilled, for modern transcriptions of coloration in Medieval and Renaissance music.
|
||||||
|
– Consolidated breath marks into a single range, and added a new upbow-like breath mark (as used in music from Russia).
|
||||||
|
– Added range of glyphs for lyrics, including three lengths of elision undertie, and baseline hyphen (as used in music from Russia).
|
||||||
|
– Added a wider slash notehead, for whole note (semibreve) duration.
|
||||||
|
– Added more shape note noteheads to support the 7-shape conventions of Joseph Funk and William Walker.
|
||||||
|
– Added maxima rest, and double whole (breve) rest with leger lines above and below.
|
||||||
|
– Added curved caesura.
|
||||||
|
– Added separate glyphs for the 'e', 'd' and dot in keyboard Ped. marks, plus a curved hyphen to be used along with the 'P' to show start/end pedal in some editions.
|
||||||
|
– Added new mensural C clef, plus variations of the Petrucci C clef for different staff positions.
|
||||||
|
– Added different custos for different staff positions.
|
||||||
|
– Added stylistic alternates for the Medieval and Renaissance "soft b" flat accidental.
|
||||||
|
– Added dedicated glyphs for C, G, and F clef changes, plus new combining clef change character to produce other clef change glyphs by way of glyph substitution (other clef change glyphs not currently implemented in Bravura).
|
||||||
|
– Added one- and two-third tones sharp and flat accidentals as used by Brian Ferneyhough.
|
||||||
|
– Added "just air" open diamond notehead as used by Brian Ferneyhough.
|
||||||
|
– Added white and wide white diamond noteheads.
|
||||||
|
– Added a range of glyphs for denoting accel./rit. beam lines above the staff.
|
||||||
|
– Added normal, wide and narrow leger line glyphs.
|
||||||
|
|
||||||
|
2 June 2014 (Daniel Spreadbury) Bravura version 0.99
|
||||||
|
– Added '8' and '15' digits scaled correctly for positioning on G and F clefs
|
||||||
|
– Added stylistic alternates for common time, cut time and + intended for use as large time signatures printed above the staff; and added an additional stylistic set (ss04) for time signature digits intended for use outside the staff
|
||||||
|
– Added a set of noteheads enclosed in large circles, used by some drummers
|
||||||
|
– Added an ornate X notehead contained within an ellipse
|
||||||
|
– Added Couperin's pincé and tremblement appuyé ornaments
|
||||||
|
– Redesigned the thumb position string technique glyph to more closely resemble a zero digit, and added a turned version
|
||||||
|
– Added a zero-width rectangle intended to enclose single percussion beaters inside a box
|
||||||
|
– Added strum direction arrows for guitar, and a stylistic alternate for the golpe glyph as used by Antonis Vounelakos
|
||||||
|
– Added an additional raised 7 digit for figured bass
|
||||||
|
– Added left- and right-pointing arrows for use in metric modulations
|
||||||
|
– Reduced the width of stems throughout Bravura (from 0.16 spaces to 0.12 spaces); this is also reflected in the 'stemThickness' value in the 'engravingDefaults' structure in the JSON metadata
|
||||||
|
– Redesigned the half note noteheads containing note names to improve stem connections
|
||||||
|
– Added hundreds of new kerning pairs for beamed notes in text, percussion beaters (for overlapping tilted beaters, and overprinting of parentheses, dashed circle and rectangular outline for up/down beaters), ligated time signature digits (for positioning as numerator and denominator)
|
||||||
|
– Fixed registration for stem decorations (e.g. tremolos, buzz roll, etc.) such that these glyphs are correctly visually centered around x = 0 and y = 0
|
||||||
|
– Fixed registration of stylistic alternates for tablature clefs
|
||||||
|
– Added ligatures for Johnston accidentals in various combinations
|
||||||
|
– Removed 407 wind fingering chart glyphs; these glyphs are no longer part of the SMuFL standard, and will be released as a separate font in due course
|
||||||
|
|
||||||
|
16 June 2014 (Daniel Spreadbury) Bravura 1.0
|
||||||
|
– Redesigned the bracket top and bottom glyphs to better abut with the vertical line of the bracket
|
||||||
|
– Added punctum deminutum (chantPunctumDeminutum)
|
||||||
|
|
||||||
|
17 June 2014 (Daniel Spreadbury) Bravura 1.01
|
||||||
|
– Corrected the duration of stem-up notes in the 'Individual notes' range (64th note was missing, and 1024th note was duplicated)
|
||||||
|
|
||||||
|
23 June 2014 (Daniel Spreadbury) Bravura 1.02
|
||||||
|
– Corrected the accidentalSims12Down glyph, which had inadvertently been replaced with the accidentalSims6Down glyph
|
||||||
|
|
||||||
|
22 July 2014 (Daniel Spreadbury) Bravura 1.03
|
||||||
|
– Reduced the size of the augmentationDot glyph (now 2/5 spaces in diameter rather than 1/2 space), and likewise reduced the size of the staccato articulations (now 1/3 space in diameter rather than 1/2 space)
|
||||||
|
|
||||||
|
25 July 2014 (Daniel Spreadbury) Bravura 1.05
|
||||||
|
– Corrected the side-bearings of the timeSigParensLeft, timeSigParensRight, timeSigParensLeftSmall and timeSigParensRightSmall glyphs; also made the curvature of the small parentheses less extreme
|
||||||
|
– Added new timeSigBracketLeft, timeSigBracketRight, timeSigBracketLeftSmall, timeSigBracketRightSmall and timeSigSlash glyphs, in a new range at U+EC80 (the first new range of recommended glyphs to be added after SMuFL version 1.0)
|
||||||
|
|
||||||
|
26 August 2014 (Daniel Spreadbury) Bravura 1.06
|
||||||
|
– Corrected the registration of flag64thDownStraight.
|
||||||
|
– Corrected the appearance of U+1D1A9 in the Unicode Musical Symbols Range: it should have been equivalent to csymHalfDiminished, but it was incorrectly mapped to csymDiminished.
|
||||||
|
|
||||||
|
16 October 2014 (Daniel Spreadbury) Bravura 1.07
|
||||||
|
– Corrected the appearance of articMarcatoBelow, which was incorrectly flipped horizontally.
|
||||||
|
– Updated flag8thUp, flag8thDown, flag16thUp, and flag16thDown to be 3¼ spaces in length (previously 2¾ spaces), in keeping with the recommendations for flag length in Gould and Ross.
|
||||||
|
– Updated the glyphs in the 'Individual notes' (U+E1D0–U+E1EF) and 'Beamed groups of notes' (U+E1F0–U+E20F) such that notes have stem lengths of 2¾ spaces in length (previously 3½ for 'Individual notes', and inconsistently around 2¾ spaces in length for 'Beamed groups of notes'). These glyphs are intended to be used in close proximity to text set in other fonts, and the shorter stems provide a more harmonious appearance with typical text fonts.
|
||||||
|
|
||||||
|
5 November 2014 (Daniel Spreadbury) Bravura 1.08
|
||||||
|
– Improved versions of G, F, and C clefs, and all their variants, including the forms with ligated numbers; thanks to Tucker Meyers.
|
||||||
|
– Improved versions of the octave markers (8, 8va, 8vb, et al); thanks to Tucker Meyers.
|
||||||
|
– Improved versions of the tuplet digits (0–9, plus colon); thanks to Tucker Meyers.
|
||||||
|
|
||||||
|
11 December 2014 (Daniel Spreadbury) Bravura 1.10
|
||||||
|
– Reworked the octave markers, so that the glyphs for raising and lowering by one, two, and three octaves use the correct abbreviations with appropriate placement (superscript for pitches to be raised, at the baseline for pitches to be lowered); added new '8vb', '15mb' and '22mb' glyphs, even though these abbreviations for lowering by one, two, and three octaves are incorrect; added 'loco' and 'basso' glyphs.
|
||||||
|
|
||||||
|
7 January 2015 (Daniel Spreadbury) Bravura 1.12
|
||||||
|
– Added Hauptrhythmus (RH) and Choralmelodie (CH) glyphs, plus stylistic alternate for Hauptrhythmus(R), as used by Alban Berg.
|
||||||
|
– Added short, medium, and long smooth brass lifts.
|
||||||
|
– Added Salzedo's symbols for ascending and descending Aeolian chords.
|
||||||
|
– Added right-pointing black and white triangles for down-stem sharp notes in Simplified Music Notation.
|
||||||
|
– Added 'cut 2' time signature, used by Bach as alternative to cut C.
|
||||||
|
– Added new ligatures for common time signatures (2/4, 2/2, 3/2, 3/4, 3/8, 4/4, 5/4, 5/8, 6/4, 6/8, 7/8, 9/8, 12/8) for Bravura Text. To produce e.g. the 3/4 ligature, the string "timeSigCombNumerator timeSig3 timeSigCombDenominator timeSig4" should be used. These ligatures provide correct alignment of the numerator and denominator digits relative to each other.
|
||||||
|
– Improved metadata for flat-like accidentals, adding 'cutOutSE' points for more of them.
|
||||||
|
– Added new 'noteheadOrigin' point to metadata to assist in the alignment of noteheads of different rhythmic values, e.g. the double whole note (breve) with two vertical lines to either side of the oval notehead.
|
||||||
|
– Added new 'opticalCenter' point to metadata for dynamics glyphs, to assist in their correct alignment with noteheads and stems.
|
||||||
|
|
||||||
|
26 February 2015 (Daniel Spreadbury) Bravura 1.15
|
||||||
|
– Improved U+EB2A (elecPowerOnOff) so that it more closely matches the standard appearance of the IEEE 5009 symbol.
|
||||||
|
– Improved consistency of registration of U+EB2E–U+EB33 (elecVolumeLevel...) glyphs.
|
||||||
|
– Corrected typo in metadata for dynamicSforzadoFF glyph.
|
||||||
|
– Improved U+E26A (accidentalParensLeft) and U+E26B (accidentalParensRight).
|
||||||
|
– Fixed contour direction for many glyphs; this may have resulted in incorrect appearance of some glyphs in some display contexts, and when printed on some printers.
|
||||||
|
– Resized all notehead glyphs, except for those in the 'Note name noteheads' (U+E150–U+E1AF), 'Individual notes' (U+E1D0–U+E1EF), and 'Beamed groups of notes' (U+E1F0–U+E20F) ranges, such that they are exactly 0.25 em (equivalent to one staff space) in height; previously, many of the notehead glyphs had been optically sized and were taller.
|
||||||
|
– Added a new stylistic set (ss05) containing optically-sized versions of the most common noteheads (double whole, square double whole, whole, half, and black). These glyphs are oversized, and are designed to be the largest possible noteheads that will work with staff lines of the thickness recommended in the Bravura font-specific metadata.
|
||||||
|
– Many glyphs have small improvements to outline quality (via 'checkOutlines -i -e' from the Adobe FDK).
|
||||||
|
|
||||||
|
14 April 2015 (Daniel Spreadbury) Bravura 1.16
|
||||||
|
– Improved versions of bold dynamics characters; thanks to Tucker Meyers.
|
||||||
|
– Improved versions of keyboard pedalling characters; thanks to Tucker Meyers.
|
||||||
|
– Improved versions of trill, turn etc. characters; thanks to Tucker Meyers.
|
||||||
|
– Improved symmetry of the whole note name noteheads; removed counters from noteWholeEmpty and noteHalfEmpty, since these are designed to be printed in white underneath note name noteheads, so that stave lines and ledger lines are blanked out, making the note names in the counters of the note name noteheads clearer to read.
|
||||||
|
– Corrected side-bearings on Sagittal accidentals.
|
||||||
|
– Corrected registration of glyphs designed to be combined with stems (descriptions ending '...for stem'), now consistently visually centered around both x=0 and y=0.
|
||||||
|
– Corrected vertical registration of precomposed square (U+E120, U+E121), round (U+E122, U+E123), and diamond (U+E138–U+E13B) cluster noteheads, so they are consistent with the cluster notehead components in the same range.
|
||||||
|
|
||||||
|
29 April 2015 (Daniel Spreadbury) Bravura 1.17
|
||||||
|
– Added new 'Metronome marks' for notes with 2.75sp stems to balance with text.
|
||||||
|
– Reverted notes in 'Individual notes' range to have 3.5sp stems.
|
||||||
|
– Added separate glyphs for v, b, a, m, both superscript and on the baseline, to 'Octaves supplement' range.
|
||||||
|
– Added marcato-tenuto above and below to 'Articulations' range.
|
||||||
|
– Added 6 with slashed stem (figbass6Raised2) to 'Figured bass' range.
|
||||||
|
|
||||||
|
18 May 2015 (Daniel Spreadbury) Bravura 1.18
|
||||||
|
– Added side-bearings to figured bass digits, function symbols, and tuplet digits.
|
||||||
|
– Corrected triangular clefs at U+E06F–U+E072 to match the actual usage by the Polish composer Schäffer.
|
||||||
|
– Added z-style quarter (crotchet) rest.
|
||||||
|
|
||||||
|
25 April 2016 (Daniel Spreadbury) Bravura 1.20
|
||||||
|
– Added double whole note slash notehead (U+E10A)
|
||||||
|
– Added double-slashed black and white round noteheads, often used to denote striking piano strings (U+E11C, U+E11D)
|
||||||
|
– Added irregular tremolo mark, used by Stockhausen (U+E232)
|
||||||
|
– Added square brackets for editorial accidentals (U+E26C, U+E26D)
|
||||||
|
– Added equal-tempered quarter-tone flat and quarter-tone sharp, combining glyphs to raise and lower by a 53-limit comma, and tilde and equals characters to indicate enharmonic equivalence, to the 'Extended Helmholtz-Ellis (Just Intonation) accidentals' range (U+E2F5-U+E2FB)
|
||||||
|
– Added quarter-tone sharp and flat accidentals, used by Ferneyhough (U+E48E, U+E48F)
|
||||||
|
– Added parentheses and brackets for hairpins (U+E542–U+E545)
|
||||||
|
– Added hyphen, colon, and space separators for use in combined dynamics, e.g. p-mp (U+E546–U+E548)
|
||||||
|
– Added brass valve trill to 'Brass techniques' range (U+E5EF)
|
||||||
|
– Added wind mouthpiece pop and rim only to 'Wind techniques' range (U+E60A, U+E60B)
|
||||||
|
– Added bow behind bridge on one, two, three, or four strings to 'String techniques' range (U+E627–U+E62A)
|
||||||
|
– Added nasal voice, tongue click, finger click, and tongue and finger click (as used by Stockhausen) to 'Voical techniques' range (U+E648–U+E64A)
|
||||||
|
– Added L and reversed-L hooks used instead of Ped. to start and stop sustain pedal indications to 'Keyboard techniques' range (U+E672, U+E673)
|
||||||
|
– Added pedal-to-heel and heel-to-pedal transitions to 'Keyboard techniques' range (U+E674, U+E675)
|
||||||
|
– Added damp low strings, damp with both hands, damp below, damp above, metallic sounds on a single string, isolated sounds, and snare drum techniques as used by Salzedo to 'Harp techniques' range (U+E697–U+E69D)
|
||||||
|
– Added clockwise variant of scrape around rim to 'Percussion playing technique pictograms' range (U+E80E)
|
||||||
|
- Added full barré and half-barré to 'Guitar' range (U+E848, U+E849), plus recommended stylistic alternate with horizontal fraction slash for half-barré
|
||||||
|
– Added unconducted/free passages to 'Conductor symbols' range (U+E89A)
|
||||||
|
– Added upper case F, I, K, L, and lower case i, k, l, glyphs to existing 'Function theory symbols' range (U+EA99–U+EA9F), plus a new 'Function theory symbols supplement' range including upper case M and N, and lower case m and r (U+ED00–U+ED03)
|
||||||
|
– Added "cut 3" to 'Time signature supplement' range (U+EC86)
|
||||||
|
– Added diminished 7 to new 'Figured bass supplement' range (U+ECC0)
|
||||||
|
– Added new 'Shape note noteheads supplement' range containing double whole note versions of all of the different notehead shapes in the existing 'Shape note noteheads' range (U+ECD0–U+ECDD)
|
||||||
|
– Added turned time signature digits, common time and cut common time (U+ECE0–U+ECEB)
|
||||||
|
– Added reversed time signature digits, common time and cut common time (U+ECF0–U+ECFB)
|
||||||
|
– Added new 'Fingering' range, containing digits bold 0–5 suitable for keyboard fingering, and a variety of symbols used in guitar fingering (U+ED10–U+ED23)
|
||||||
|
– Added new 'Arabic accidentals' range (U+ED30–U+ED38)
|
||||||
|
– Added new 'Articulation supplement' range containing so-called "soft accent", plus combinations with staccato and tenuto (U+ED40–U+ED47)
|
||||||
|
– Added new 'Stockhausen accidentals (24-EDO)' range (U+ED50–U+ED5E)
|
||||||
|
– Added stylistic alternates for cClef and cClefChange in the style of 20th century French publishers
|
||||||
|
– Added stylistic alternates for 15/22 octave markings using 16/24, as used by some 20th century French publishers
|
||||||
|
– Added stylistic alternates for wiggleArpeggiatoUpSwash and wiggleArpeggiatoDownSwash based on Couperin's 'L'Art de Toucher Le Clavecin'
|
||||||
|
– Changed the appearance of clefBridge (U+E078) to match the design used by Lachenmann in "...zwei Gefühle..." and added a stylistic alternate with the previous design
|
||||||
|
– Fixed the appearance of figBassRaised5 to have a forward slash rather than a backward slash, the correct appearance for a diminished fifth (U+EA5A)
|
||||||
|
|
||||||
|
27 January 2017 (Daniel Spreadbury) Bravura 1.211
|
||||||
|
– Added dynamicCombinedSeparatorSlash (U+E549)
|
||||||
|
– Added stylistic alternates for tuplet digits in lighter weight
|
||||||
|
– Improved the tessellation of multi-segment lines, particularly the various wiggles for trills at different speeds.
|
||||||
|
– Fixed timesig9denominator and timesig9numerator ligatures
|
||||||
|
|
||||||
|
11 February 2017 (Daniel Spreadbury) Bravura 1.212
|
||||||
|
– Added keyboardPedalParensLeft (U+E676) and keyboardPedalParensRight (U+E677)
|
||||||
|
– Added vulgar fractions for 1/8 (U+215B), 1/4 (U+00BC), 1/2 (U+00BD), 3/4 (U+00BE)
|
||||||
|
|
||||||
|
18 February 2017 (Daniel Spreadbury) Bravura 1.213
|
||||||
|
– Decreased the width of the space character (U+0020) to 100du
|
||||||
|
|
||||||
|
27 May 2017 (Daniel Spreadbury) Bravura 1.214
|
||||||
|
– Improved the appearance of the glyphs in the 'Chord symbols' range (U+E870–U+E87F)
|
||||||
|
– Added new 'Chord symbol accidentals' range (U+ED60-U+ED66)
|
||||||
|
– BEWARE: Renumbered the optional glyphs from U+F400 upwards; many optional glyphs have moved
|
||||||
|
|
||||||
|
31 May 2017 (Daniel Spreadbury) Bravura 1.215
|
||||||
|
– Added optical variants for some chord symbols glyphs, e.g. +/- for augmented/diminished chord quality
|
||||||
|
|
||||||
|
3 June 2017 (Daniel Spreadbury) Bravura 1.216
|
||||||
|
– Added optical variants for chord symbol accidentals shown at smaller sizes
|
||||||
|
|
||||||
|
18 July 2017 (Daniel Spreadbury) Bravura 1.217
|
||||||
|
– Improved the shape and stem connection data of glyphs in the 'Shape note noteheads' range (U+E1B0–U+E1CF)
|
||||||
|
|
||||||
|
9 August 2017 (Daniel Spreadbury) Bravura 1.218
|
||||||
|
– Added new glyphs to the 'Fingering' range, with digits between 6 and 9, parentheses, brackets, and middle dot separator
|
||||||
|
|
||||||
|
10 August 2017 (Daniel Spreadbury) Bravura 1.219
|
||||||
|
– Improved the shape and stem connection data of glyphs in the 'Shape note noteheads' range (U+E1B0–U+E1CF)
|
||||||
|
|
||||||
|
16 August 2017 (Daniel Spreadbury) Bravura 1.220
|
||||||
|
– Increased the size of black and half noteheads in 'Shape note noteheads' range (U+E1B0–U+E1CF)
|
||||||
|
|
||||||
|
25 October 2017 (Daniel Spreadbury) Bravura 1.250
|
||||||
|
– Added new 'Kahnotation' range, with thanks to Matthew Dougherty, Sam Katz and Sam Weber (U+EDA0–U+EDF1)
|
||||||
|
– Added new 'German organ tablature' range, with thanks to John McKean (U+EE00–U+EE3F)
|
||||||
|
– Added new 'Clefs supplement' range, including new universal Indian drum notation clef (U+ED70)
|
||||||
|
– Added new 'Fingering supplement' range, with italic fingering digits, parentheses and brackets (U+ED80-U+ED8D)
|
||||||
|
– Added more separators for brass fingering (U+ED2D–U+ED2E) to 'Fingering' range
|
||||||
|
– Added pictBeaterMalletDown (U+E7EC), pictBeaterBrassMalletsRight (U+E7ED), pictBeaterBrassMalletsLeft (U+E7EE), pictTriangleBeaterPlain (U+E7EF) to 'Beaters pictograms' range
|
||||||
|
– Added vocalHalbGesungen (semi-sprechgesang) to 'Vocal techniques' range (U+E64B)
|
||||||
|
– Added separate glyphs for upper dot, lower dot and central slash (U+E503–U+E505) to 'Bar repeats' range, to allow construction of bar repeats for arbitrary number of bars
|
||||||
|
– Added lyricsTextRepeat glyph (U+E555) to 'Lyrics' range
|
||||||
|
– Added one-handed roll (U+E233) and double lateral roll (U+E234) for mallet percussion, popularised by Leigh Howard Stevens, to 'Tremolos' range
|
||||||
|
|
||||||
|
7 November 2017 (Daniel Spreadbury) Bravura 1.251
|
||||||
|
– Resized csymAugmented and csymMinor
|
||||||
|
– Adjusted the side bearings of noteheads whose stem attachment points should be offset from the edge of the glyph's bounding box
|
||||||
|
|
||||||
|
14 November 2017 (Daniel Spreadbury) Bravura Text 1.252
|
||||||
|
– Corrected scaling for chord symbols glyphs in Bravura Text
|
||||||
|
|
||||||
|
11 December 2017 (Daniel Spreadbury) Bravura 1.260
|
||||||
|
– Added new stylistic set (ss08) for oversized versions of the 'Slash notehead' range
|
||||||
|
|
||||||
|
16 December 2017 (Daniel Spreadbury) Bravura Text 1.261
|
||||||
|
– Corrected scaling for chord symbols standard accidentals glyphs in Bravura Text
|
||||||
|
|
||||||
|
23 February 2018 (Daniel Spreadbury) Bravura 1.270
|
||||||
|
– Expanded 'Large, narrow sans serif time signatures' stylistic set (ss04) to complete 'Time signatures' and 'Time signatures supplement' ranges
|
||||||
|
– Added new 'Large, narrow bold serif time signatures' stylistic set (ss09)
|
||||||
|
|
||||||
|
26 February 2018 (Daniel Spreadbury) Bravura 1.271
|
||||||
|
– Resized 'Large, narrow sans serif time signatures' stylistic set (ss04) glyphs to give them the same scale factor as the other time signatures glyphs
|
||||||
|
|
||||||
|
29 April 2018 (Daniel Spreadbury) Bravura 1.272
|
||||||
|
– Improved 'Large, narrow sans serif time signatures' stylistic set (ss04) glyphs to make the forms more condensed
|
||||||
|
|
||||||
|
29 January 2019 (Daniel Spreadbury) Bravura 1.276 / Bravura Text 1.277
|
||||||
|
– Fixed the scale factor for optical variants for chord symbol accidentals
|
||||||
|
|
||||||
|
31 August 2019 (Daniel Spreadbury) Bravura 1.31 / Bravura Text 1.31
|
||||||
|
– New fingeringQLower (U+ED8E) and fingeringSLower (U+ED8F) glyphs in the 'Fingering supplement' range
|
||||||
|
|
||||||
|
6 December 2019 (Daniel Spreadbury) Bravura 1.32
|
||||||
|
– Reduced height of accidentalBracketLeft (U+E26C) and accidentalBracketRight (U+E26D)
|
||||||
|
– Corrected registration of a number of glyphs in the 'Extended Helmholtz-Ellis Just Intonation accidentals' range (U+E2DF, U+E2E6, U+E2E7, U+E2E8, U+E2E9, U+E2F7, U+E2F8, U+E2EB).
|
||||||
|
|
||||||
|
18 March 2020 (Daniel Spreadbury) Bravura 1.34
|
||||||
|
– Swapped direction of wiggleArpeggiatoDownArrow (U+EAAE) and wiggleArpeggiatoDownSwash (U+EAAC)
|
||||||
|
|
||||||
|
27 March 2020 (Daniel Spreadbury) Bravura 1.35
|
||||||
|
– Added figbassTripleFlat (U+ECC1) and figbassTripleSharp (U+ECC2)
|
||||||
|
|
||||||
|
1 April 2020 (Daniel Spreadbury) Bravura 1.36
|
||||||
|
– Reduced size of accidentals in 'Figured bass' and 'Figured bass supplement' range to match published editions more closely
|
||||||
|
|
||||||
|
3 May 2020 (Daniel Spreadbury) Bravura 1.37
|
||||||
|
– Improved shape of accidentals in 'Figured bass' and 'Figured bass supplement' range, reducing the length of ascenders and descenders and increasing the size of the counters
|
||||||
|
– Improved the design of figBass4Raised (U+EA56) to make the vertical stroke more prominent, aiding legibility at small sizes
|
||||||
|
|
||||||
|
25 June 2020 (Daniel Spreadbury) Bravura 1.38
|
||||||
|
– Added new stylistic set (ss10) for accidentals for figured bass with longer ascenders and descenders
|
||||||
|
– Improved the shape of the tips of each of the up- and down-stem flags in the 'Flags' range (U+E240–U+E251)
|
||||||
|
– Improved the shape of ornamentTrill (U+E566) and ornamentShortTrill (U+E56C)
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
OFL FAQ - Frequently Asked Questions about the SIL Open Font License (OFL)
|
||||||
|
Version 1.1-update2 - 23 August 2010
|
||||||
|
(See http://scripts.sil.org/OFL for updates)
|
||||||
|
|
||||||
|
|
||||||
|
CONTENTS OF THIS FAQ
|
||||||
|
1 USING AND DISTRIBUTING FONTS LICENSED UNDER THE OFL
|
||||||
|
2 USING OFL FONTS FOR WEB PAGES AND ONLINE WEBFONT SERVICES
|
||||||
|
3 MODIFYING OFL-LICENSED FONTS
|
||||||
|
4 LICENSING YOUR ORIGINAL FONTS UNDER THE OFL
|
||||||
|
5 CHOOSING RESERVED FONT NAMES
|
||||||
|
6 ABOUT THE FONTLOG
|
||||||
|
7 MAKING CONTRIBUTIONS TO OFL PROJECTS
|
||||||
|
8 ABOUT THE LICENSE ITSELF
|
||||||
|
9 ABOUT SIL INTERNATIONAL
|
||||||
|
APPENDIX A - FONTLOG EXAMPLE
|
||||||
|
|
||||||
|
|
||||||
|
1 USING AND DISTRIBUTING FONTS LICENSED UNDER THE OFL
|
||||||
|
|
||||||
|
1.1 Can I use the fonts for a book or other print publication?
|
||||||
|
Yes. You can mention the font and author in the book's colophon if you wish, but that is not required.
|
||||||
|
|
||||||
|
1.2 Can the fonts be included with Free/Libre and Open Source Software collections such as GNU/Linux and BSD distributions?
|
||||||
|
Yes! Fonts licensed under the OFL can be freely included alongside other software under FLOSS (Free/Libre and Open Source Software) licenses. Since fonts are typically aggregated with, not merged into, existing software, there is little need to be concerned about incompatibility with existing software licenses. You may also repackage the fonts and the accompanying components in a .rpm or .deb package and include them in distribution CD/DVDs and online repositories. (Also see section 5.9 about rebuilding from source.)
|
||||||
|
|
||||||
|
1.3 I want to distribute the fonts with my program. Does this mean my program also has to be Free/Libre and Open Source Software?
|
||||||
|
No. Only the portions based on the Font Software are required to be released under the OFL. The intent of the license is to allow aggregation or bundling with software under restricted licensing as well.
|
||||||
|
|
||||||
|
1.4 Can I sell a software package that includes these fonts?
|
||||||
|
Yes, you can do this with both the Original Version and a Modified Version of the fonts. Examples of bundling made possible by the OFL would include: word processors, design and publishing applications, training and educational software, games and entertainment software, mobile device applications, etc.
|
||||||
|
|
||||||
|
1.5 Can I include the fonts on a CD of freeware or commercial fonts?
|
||||||
|
Yes, as long some other font or software is also on the disk, so the OFL font is not sold by itself.
|
||||||
|
|
||||||
|
1.6 Why won't the OFL let me sell the fonts alone?
|
||||||
|
The intent is to keep people from making money by simply redistributing the fonts. The only people who ought to profit directly from the fonts should be the original authors, and those authors have kindly given up potential direct income to distribute their fonts under the OFL. Please honour and respect their contribution!
|
||||||
|
|
||||||
|
1.7 What about sharing OFL fonts with friends on a CD, DVD or USB stick?
|
||||||
|
You are very welcome to share open fonts with friends, family and colleagues through removable media. Just remember to include the full font package, including any copyright notices and licensing information as available in OFL.txt. In the case where you sell the font, it has to come bundled with software.
|
||||||
|
|
||||||
|
1.8 Can I host the fonts on a web site for others to use?
|
||||||
|
Yes, as long as you make the full font package available. In most cases it may be best to point users to the main site that distributes the Original Version so they always get the most recent stable and complete version. See also discussion of webfonts in Section 2.
|
||||||
|
|
||||||
|
1.9 Can I host the fonts on a server for use over our internal network?
|
||||||
|
Yes. If the fonts are transferred from the server to the client computer by means that allow them to be used even if the computer is no longer attached to the network, the full package (copyright notices, licensing information, etc.) should be included.
|
||||||
|
|
||||||
|
1.10 Does the full OFL license text always need to accompany the font?
|
||||||
|
The only situation in which an OFL font can be distributed without the text of the OFL (either in a separate file or in font metadata), is when a font is embedded in a document or bundled within a program. In the case of metadata included within a font, it is legally sufficient to include only a link to the text of the OFL on http://scripts.sil.org/OFL, but we strongly recommend against this. Most modern font formats include metadata fields that will accept the full OFL text, and full inclusion increases the likelihood that users will understand and properly apply the license.
|
||||||
|
|
||||||
|
1.11 What do you mean by 'embedding'? How does that differ from other means of distribution?
|
||||||
|
By 'embedding' we mean inclusion of the font in a document or file in a way that makes extraction (and redistribution) difficult or clearly discouraged. In many cases the names of embedded fonts might also not be obvious to those reading the document, the font data format might be altered, and only a subset of the font - only the glyphs required for the text - might be included. Any other means of delivering a font to another person is considered 'distribution', and needs to be accompanied by any copyright notices and licensing information available in OFL.txt.
|
||||||
|
|
||||||
|
1.12 So can I embed OFL fonts in my document?
|
||||||
|
Yes, either in full or a subset. The restrictions regarding font modification and redistribution do not apply, as the font is not intended for use outside the document.
|
||||||
|
|
||||||
|
1.13 Does embedding alter the license of the document itself?
|
||||||
|
No. Referencing or embedding an OFL font in any document does not change the license of the document itself. The requirement for fonts to remain under the OFL does not apply to any document created using the fonts and their derivatives. Similarly, creating any kind of graphic using a font under OFL does not make the resulting artwork subject to the OFL.
|
||||||
|
|
||||||
|
1.14 If OFL fonts are extracted from a document in which they are embedded (such as a PDF file), what can be done with them? Is this a risk to author(s)?
|
||||||
|
The few utilities that can extract fonts embedded in a PDF will typically output limited amounts of outlines - not a complete font. To create a working font from this method is much more difficult and time consuming than finding the source of the original OFL font. So there is little chance that an OFL font would be extracted and redistributed inappropriately through this method. Even so, copyright laws address any misrepresentation of authorship. All Font Software released under the OFL and marked as such by the author(s) is intended to remain under this license regardless of the distribution method, and cannot be redistributed under any other license. We strongly discourage any font extraction - we recommend directly using the font sources instead - but if you extract font outlines from a document, please be considerate: use your common sense and respect the work of the author(s) and the licensing model.
|
||||||
|
|
||||||
|
1.15 What about distributing fonts with a document? Within a compressed folder structure? Is it distribution, bundling or embedding?
|
||||||
|
Certain document formats may allow the inclusion of an unmodified font within their file structure which consists of a compressed folder containing the various resources forming the document (such as pictures and thumbnails). Including fonts within such a structure is understood as being different from embedding but rather similar to bundling (or mere aggregation) which the license explicitly allows. In this case the font is conveyed unchanged whereas embedding a font usually transforms it from the original format. The OFL does not allow anyone to extract the font from such a structure to then redistribute it under another license. The explicit permission to redistribute and embed does not cancel the requirement for the Font Software to remain under the license chosen by its author(s).
|
||||||
|
|
||||||
|
1.16 What about ebooks shipping with open fonts?
|
||||||
|
The requirements differ depending on whether the fonts are linked, embedded or distributed (bundled or aggregated). Some ebook formats use web technologies to do font linking via @font-face, others are designed for font embedding, some use fonts distributed with the document or reading software, and a few rely solely on the fonts already present on the target system. The license requirements depend on the type of inclusion as discussed in 1.15.
|
||||||
|
|
||||||
|
1.17 Can Font Software released under the OFL be subject to URL-based access restrictions methods or DRM (Digital Rights Management) mechanisms?
|
||||||
|
Yes, but these issues are out-of-scope for the OFL. The license itself neither encourages their use nor prohibits them since such mechanisms are not implemented in the components of the Font Software but through external software. Such restrictions are put in place for many different purposes corresponding to various usage scenarios. One common example is to limit potentially dangerous cross-site scripting attacks. However, in the spirit of libre/open fonts and unrestricted writing systems, we strongly encourage open sharing and reuse of OFL fonts, and the establishment of an environment where such restrictions are unnecessary. Note that whether you wish to use such mechanisms or you prefer not to, you must still abide by the rules set forth by the OFL when using fonts released by their authors under this license. Derivative fonts must be licensed under the OFL, even if they are part of a service for which you charge fees and/or for which access to source code is restricted. You may not sell the fonts on their own - they must be part of a larger software package, bundle or subscription plan. For example, even if the OFL font is distributed in a software package or via an online service using a DRM mechanism, the user would still have the right to extract that font, use, study, modify and redistribute it under the OFL.
|
||||||
|
|
||||||
|
1.18 I've come across a font released under the OFL. How can I easily get more information about the Original Version? How can I know where it stands compared to the Original Version or other Modified Versions?
|
||||||
|
Consult the copyright statement(s) in the license for ways to contact the original authors. Consult the FONTLOG for information on how the font differs from the Original Version, and get in touch with the various contributors via the information in the acknowledgement section. Please consider using the Original Versions of the fonts whenever possible.
|
||||||
|
|
||||||
|
1.19 What do you mean in condition 4? Can you provide examples of abusive promotion / endorsement / advertisement vs. normal acknowledgement?
|
||||||
|
The intent is that the goodwill and reputation of the author(s) should not be used in a way that makes it sound like the original author(s) endorse or approve of a specific Modified Version or software bundle. For example, it would not be right to advertise a word processor by naming the author(s) in a listing of software features, or to promote a Modified Version on a web site by saying "designed by ...". However, it would be appropriate to acknowledge the author(s) if your software package has a list of people who deserve thanks. We realize that this can seem to be a grey area, but the standard used to judge an acknowledgement is that if the acknowledgement benefits the author(s) it is allowed, but if it primarily benefits other parties, or could reflect poorly on the author(s), then it is not.
|
||||||
|
|
||||||
|
|
||||||
|
2 USING OFL FONTS FOR WEBPAGES AND ONLINE WEBFONT SERVICES
|
||||||
|
|
||||||
|
2.1 Can I make webpages using these fonts?
|
||||||
|
Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Your three best options:
|
||||||
|
- referring directly in your stylesheet to open fonts which may be available on the user's system
|
||||||
|
- providing links to download the full package of the font - either from your own website or from elsewhere - so users can install it themselves
|
||||||
|
- using @font-face to distribute the font directly to browsers. This is recommended and explicitly allowed by the licensing model because it is distribution. The font file itself is distributed with other components of the webpage. It is not embedded in the webpage but referenced through a web address which will cause the browser to retrieve and use the corresponding font to render the webpage (see 1.11 and 1.15 for details related to embedding fonts into documents). As you take advantage of the @font-face cross-platform standard, be aware that webfonts are often tuned for a web environment and not intended for installation and use outside a browser. The reasons in favour of using webfonts are to allow design of dynamic text elements instead of static graphics, to make it easier for content to be localized and translated, indexed and searched, and all this with cross-platform open standards without depending on restricted extensions or plugins. You should check the CSS cascade (the order in which fonts are being called or delivered to your users) when testing.
|
||||||
|
|
||||||
|
2.2 Can I make and use WOFF (Web Open Font Format) versions of OFL fonts?
|
||||||
|
Yes, but you need to be careful. A change in font format normally is considered modification, and Reserved Font Names (RFNs) cannot be used. Because of the design of the WOFF format, however, it is possible to create a WOFF version that is not considered modification, and so would not require a name change. You are allowed to create, use and distribute a WOFF version of an OFL font without changing the font name, but only if:
|
||||||
|
|
||||||
|
- the original font data remains unchanged except for WOFF compression, and
|
||||||
|
- WOFF-specific metadata is either omitted altogether or present and includes, unaltered, the contents of all equivalent metadata in the original font.
|
||||||
|
|
||||||
|
If the original font data or metadata is changed, or the WOFF-specific metadata is incomplete, the font must be considered a Modified Version, the OFL restrictions would apply and the name of the font must be changed: any RFNs cannot be used and copyright notices and licensing information must be included and cannot be deleted or modified. You must come up with a unique name - we recommend one corresponding to your domain or your particular web application. Be aware that only the original author(s) can use RFNs. This is to prevent collisions between a derivative tuned to your audience and the original upstream version and so to reduce confusion.
|
||||||
|
|
||||||
|
Please note that most WOFF conversion tools and online services do not meet the two requirements listed above, and so their output must be considered a Modified Version. So be very careful and check to be sure that the tool or service you're using is compressing unchanged data and completely and accurately reflecting the original font metadata.
|
||||||
|
|
||||||
|
2.3 What about other webfont formats such as EOT/EOTLite/CWT/etc.?
|
||||||
|
In most cases these formats alter the original font data more than WOFF, and do not completely support appropriate metadata, so their use must be considered modification and RFNs may not be used.
|
||||||
|
|
||||||
|
2.4 Can I make OFL fonts available through webfont online services?
|
||||||
|
Yes, you are welcome to include OFL fonts in online webfont services as long as you properly meet all the conditions of the license. The origin and open status of the font should be clear among the other fonts you are hosting. Authorship, copyright notices and license information must be sufficiently visible to your users or subscribers so they know where the font comes from and the rights granted by the author(s). Make sure the font file contains the needed copyright notice(s) and licensing information in its metadata. Please double-check the accuracy of every field to prevent contradictory information. Other font formats, including EOT/EOTLite/CWT and superior alternatives like WOFF, already provide fields for this information. Remember that if you modify the font within your library or convert it to another format for any reason the OFL restrictions apply and you need to change the names accordingly. Please respect the author's wishes as expressed in the OFL and do not misrepresent original designers and their work. Don't lump quality open fonts together with dubious freeware or public domain fonts. Consider how you can best work with the original designers and foundries, support their efforts and generate goodwill that will benefit your service. (See 1.17 for details related to URL-based access restrictions methods or DRM mechanisms).
|
||||||
|
|
||||||
|
2.5 Can I make and publish CMS themes or templates that use OFL fonts? Can I include the fonts themselves in the themes or templates? Can I sell the whole package?
|
||||||
|
Yes, you are very welcome to integrate open fonts into themes and templates for your preferred CMS and make them more widely available. Be aware that you can only sell the fonts and your CMS add-on as part of a software bundle. (See 1.4 for details and examples about selling bundles).
|
||||||
|
|
||||||
|
2.6 Some webfont formats and services provide ways of "optimising" the font for a particular website or web application; is that allowed?
|
||||||
|
Yes, it is permitted, but remember that these optimised versions are Modified Versions and so must follow OFL requirements like appropriate renaming. Also you need to bear in mind the other important parameters beyond compression, speed and responsiveness: you need to consider the audience of your particular website or web application, as choosing some optimisation parameters may turn out to be less than ideal for them. Subsetting by removing certain glyphs or features may seriously limit functionality of the font in various languages used by your users. It may also introduce degradation of quality in the rendering or specific bugs on the various platforms compared to the original font. In other words, remember that one person's optimised font may be another person's missing feature. Various advanced typographic features are also available through CSS and may provide the desired effects without the need to modify the font.
|
||||||
|
|
||||||
|
|
||||||
|
3 MODIFYING OFL-LICENSED FONTS
|
||||||
|
|
||||||
|
3.1 Can I change the fonts? Are there any limitations to what things I can and cannot change?
|
||||||
|
You are allowed to change anything, as long as such changes do not violate the terms of the license. In other words, you are not allowed to remove the copyright statement(s) from the font, but you could put additional information into it that covers your contribution.
|
||||||
|
|
||||||
|
3.2 I have a font that needs a few extra glyphs - can I take them from an OFL licensed font and copy them into mine?
|
||||||
|
Yes, but if you distribute that font to others it must be under the OFL, and include the information mentioned in condition 2 of the license.
|
||||||
|
|
||||||
|
3.3 Can I charge people for my additional work? In other words, if I add a bunch of special glyphs and/or OpenType/Graphite code, can I sell the enhanced font?
|
||||||
|
Not by itself. Derivative fonts must be released under the OFL and cannot be sold by themselves. It is permitted, however, to include them in a larger software package (such as text editors, office suites or operating systems), even if the larger package is sold. In that case, you are strongly encouraged, but not required, to also make that derived font easily and freely available outside of the larger package.
|
||||||
|
|
||||||
|
3.4 Can I pay someone to enhance the fonts for my use and distribution?
|
||||||
|
Yes. This is a good way to fund the further development of the fonts. Keep in mind, however, that if the font is distributed to others it must be under the OFL. You won't be able to recover your investment by exclusively selling the font, but you will be making a valuable contribution to the community. Please remember how you have benefited from the contributions of others.
|
||||||
|
|
||||||
|
3.5 I need to make substantial revisions to the font to make it work with my program. It will be a lot of work, and a big investment, and I want to be sure that it can only be distributed with my program. Can I restrict its use?
|
||||||
|
No. If you redistribute a Modified Version of the font it must be under the OFL. You may not restrict it in any way beyond what the OFL permits and requires. This is intended to ensure that all released improvements to the fonts become available to everyone. But you will likely get an edge over competitors by being the first to distribute a bundle with the enhancements. Again, please remember how you have benefited from the contributions of others.
|
||||||
|
|
||||||
|
3.6 Do I have to make any derivative fonts (including extended source files, build scripts, documentation, etc.) publicly available?
|
||||||
|
No, but please consider sharing your improvements with others. You may find that you receive in return more than what you gave.
|
||||||
|
|
||||||
|
3.7 If a trademark is claimed in the OFL font, does that trademark need to remain in modified fonts?
|
||||||
|
Yes, any trademark notices must remain in any derivative fonts to respect trademark laws, but you may add any additional trademarks you claim, officially registered or not. For example if an OFL font called "Foo" contains a notice that "Foo is a trademark of Acme", then if you rename the font to "Bar" when creating a Modified Version, the new trademark notice could say "Foo is a trademark of Acme Inc. - Bar is a trademark of Roadrunner Technologies Ltd.". Trademarks work alongside the OFL and are not subject to the terms of the licensing agreement. Please refer to the appropriate trademark laws.
|
||||||
|
|
||||||
|
|
||||||
|
4 LICENSING YOUR ORIGINAL FONTS UNDER THE OFL
|
||||||
|
|
||||||
|
4.1 Can I use the SIL OFL for my own fonts?
|
||||||
|
Yes! We heartily encourage everyone to use the OFL to distribute their own original fonts. It is a carefully constructed license that allows great freedom along with enough artistic integrity protection for the work of the authors as well as clear rules for other contributors and those who redistribute the fonts. The licensing model is used successfully by various organisations, both for-profit and not-for-profit, to release fonts of varying levels of scope and complexity.
|
||||||
|
|
||||||
|
4.2 What do I have to do to apply the OFL to my font?
|
||||||
|
If you want to release your fonts under the OFL, we recommend you do the following:
|
||||||
|
|
||||||
|
4.2.1 Put your copyright and Reserved Font Names information at the beginning of the main OFL.txt file in place of the dedicated placeholders. Include this file in your release package.
|
||||||
|
|
||||||
|
4.2.2 Put your copyright and the OFL text with Reserved Font Names into your font files (the copyright and license fields). A link to the OFL text on the OFL web site is an acceptable (but not recommended) alternative. Also add this information to any other components (build scripts, glyph databases, documentation, test files, etc). Depending on the format of your fonts and sources, you can use template human-readable headers or machine-readable metadata.
|
||||||
|
|
||||||
|
4.2.3 Write an initial FONTLOG.txt for your font and include it in the release package.
|
||||||
|
|
||||||
|
4.2.4 Include the relevant practical documentation on the license by including the OFL-FAQ.txt in your package.
|
||||||
|
|
||||||
|
4.3 Will you make my font OFL for me?
|
||||||
|
We won't do the work for you. We can, however, try to answer your questions, unfortunately we do not have the resources to review and check your font packages for correct use of the OFL.
|
||||||
|
|
||||||
|
4.4 Will you distribute my OFL font for me?
|
||||||
|
No, although if the font is of sufficient quality and general interest we may include a link to it on our partial list of OFL fonts on the OFL web site. You may wish to consider other open font catalogs or hosting services, such as the Unifont Font Guide (http://unifont.org/fontguide), The League of Movable Type (http://theleagueofmovabletype.com), Kernest (http://kernest.com/) or the Open Font Library (http://openfontlibrary.org/), which despite the name has no direct relationship to the OFL or SIL. We do not endorse any particular catalog or hosting service - it is your responsibility to determine if the service is right for you.
|
||||||
|
|
||||||
|
4.5 Why should I use the OFL for my fonts?
|
||||||
|
- to meet needs for fonts that can be modified to support minority languages
|
||||||
|
- to provide a legal and clear way for people to respect your work but still use it (and reduce piracy)
|
||||||
|
- to involve others in your font project
|
||||||
|
- to enable your fonts to be expanded with new weights and improved writing system/language support
|
||||||
|
- to allow more technical font developers to add features to your design (such as OpenType and Graphite support)
|
||||||
|
- to renew the life of an old font lying on your hard drive with no business model
|
||||||
|
- to allow your font to be included in Libre Software operating systems like Ubuntu
|
||||||
|
- to give your font world status and wide, unrestricted distribution
|
||||||
|
- to educate students about quality typeface and font design
|
||||||
|
- to expand your test base and get more useful feedback
|
||||||
|
- to extend your reach to new markets when users see your metadata and go to your website
|
||||||
|
- to get your font more easily into one of the webfont online services
|
||||||
|
- to attract attention for your commercial fonts
|
||||||
|
- to make money through webfont services
|
||||||
|
- to make money by bundling fonts with applications
|
||||||
|
- to make money adjusting and extending existing open fonts
|
||||||
|
- to get a better chance that foundations/NGOs/charities/companies who commission fonts will pick you
|
||||||
|
- to be part of a sharing design and development community
|
||||||
|
- to give back and contribute to a growing body of font sources
|
||||||
|
|
||||||
|
|
||||||
|
5 CHOOSING RESERVED FONT NAMES
|
||||||
|
|
||||||
|
5.1 What are Reserved Font Names?
|
||||||
|
These are font names, or portions of font names, that the author has chosen to reserve for use only with the Original Version of the font, or for Modified Version(s) created by the original author.
|
||||||
|
|
||||||
|
5.2 Why can't I use the Reserved Font Names in my derivative font names? I'd like people to know where the design came from.
|
||||||
|
The best way to acknowledge the source of the design is to thank the original authors and any other contributors in the files that are distributed with your revised font (although no acknowledgement is required). The FONTLOG is a natural place to do this. Reserved Font Names ensure that the only fonts that have the original names are the unmodified Original Versions. This allows designers to maintain artistic integrity while allowing collaboration to happen. It eliminates potential confusion and name conflicts. When choosing a name, be creative and avoid names that reuse almost all the same letters in the same order or sound like the original. It will help everyone if Original Versions and Modified Versions can easily be distinguished from one another and from other derivatives. Any substitution and matching mechanism is outside the scope of the license.
|
||||||
|
|
||||||
|
5.3 What do you mean by "primary name as presented to the user"? Are you referring to the font menu name?
|
||||||
|
Yes, this applies to the font menu name and other mechanisms that specify a font in a document. It would be fine, however, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement). Users who install derivatives (Modified Versions) on their systems should not see any of the original Reserved Font Names in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake one font for another and so expect features only another derivative or the Original Version can actually offer.
|
||||||
|
|
||||||
|
5.4 Am I not allowed to use any part of the Reserved Font Names?
|
||||||
|
You may not use individual words from the Reserved Font Names, but you would be allowed to use parts of words, as long as you do not use any word from the Reserved Font Names entirely. We do not recommend using parts of words because of potential confusion, but it is allowed. For example, if "Foobar" was a Reserved Font Name, you would be allowed to use "Foo" or "bar", although we would not recommend it. Such an unfortunate choice would confuse the users of your fonts as well as make it harder for other designers to contribute.
|
||||||
|
|
||||||
|
5.5 So what should I, as an author, identify as Reserved Font Names?
|
||||||
|
Original authors are encouraged to name their fonts using clear, distinct names, and only declare the unique parts of the name as Reserved Font Names. For example, the author of a font called "Foobar Sans" would declare "Foobar" as a Reserved Font Name, but not "Sans", as that is a common typographical term, and may be a useful word to use in a derivative font name. Reserved Font Names should also be single words. A font called "Flowing River" should have Reserved Font Names "Flowing" and "River", not "Flowing River". You also need to be very careful about reserving font names which are already linked to trademarks (whether registered or not) which you do not own.
|
||||||
|
|
||||||
|
5.6 Do I, as an author, have to identify any Reserved Font Names?
|
||||||
|
No, but we strongly encourage you to do so. This is to avoid confusion between your work and Modified Versions.
|
||||||
|
|
||||||
|
5.7 Are any names (such as the main font name) reserved by default?
|
||||||
|
No. That is a change to the license as of version 1.1. If you want any names to be Reserved Font Names, they must be specified after the copyright statement(s).
|
||||||
|
|
||||||
|
5.8 Is there any situation in which I can use Reserved Font Names for a Modified Version?
|
||||||
|
The Copyright Holder(s) can give certain trusted parties the right to use any of the Reserved Font Names through separate written agreements. For example, even if "Foobar" is a RFN, you could write up an agreement to give company "XYZ" the right to distribute a modified version with a name that includes "Foobar". This allows for freedom without confusion.
|
||||||
|
|
||||||
|
5.9 Do font rebuilds require a name change? Do I have to change the name of the font when my packaging workflow includes a full rebuild from source?
|
||||||
|
Yes, all rebuilds which change the font data and the smart code are Modified Versions and the requirements of the OFL apply: you need to respect what the Author(s) have chosen in terms of Reserved Font Names. However if a package (or installer) is simply a wrapper or a compressed structure around the final font - leaving them intact on the inside - then no name change is required. Please get in touch with the author(s) and copyright holder(s) to inquire about the presence of font sources beyond the final font file(s) and the recommended build path. That build path may very well be non-trivial and hard to reproduce accurately by the maintainer. If a full font build path is made available by the upstream author(s) please be aware that any regressions and changes you may introduce when doing a rebuild for packaging purposes is your responsibility as a package maintainer since you are effectively creating a separate branch. You should make it very clear to your users that your rebuilt version is not the canonical one from upstream.
|
||||||
|
|
||||||
|
5.10 Can I add other Reserved Font Names when making a derivative font?
|
||||||
|
Yes. List your additional Reserved Font Names after your additional copyright statement, as indicated with example placeholders at the top of the OFL.txt file. Be sure you do not remove any exiting RFNs but only add your own.
|
||||||
|
|
||||||
|
|
||||||
|
6 ABOUT THE FONTLOG
|
||||||
|
|
||||||
|
6.1 What is this FONTLOG thing exactly?
|
||||||
|
It has three purposes: 1) to provide basic information on the font to users and other developers, 2) to document changes that have been made to the font or accompanying files, either by the original authors or others, and 3) to provide a place to acknowledge authors and other contributors. Please use it!
|
||||||
|
|
||||||
|
6.2 Is the FONTLOG required?
|
||||||
|
It is not a requirement of the license, but we strongly recommend you have one.
|
||||||
|
|
||||||
|
6.3 Am I required to update the FONTLOG when making Modified Versions?
|
||||||
|
No, but users, designers and other developers might get very frustrated with you if you don't. People need to know how derivative fonts differ from the original, and how to take advantage of the changes, or build on them. There are utilities that can help create and maintain a FONTLOG, such as the FONTLOG support in FontForge.
|
||||||
|
|
||||||
|
6.4 What should the FONTLOG look like?
|
||||||
|
It is typically a separate text file (FONTLOG.txt), but can take other formats. It commonly includes these four sections:
|
||||||
|
|
||||||
|
- brief header describing the FONTLOG itself and name of the font family
|
||||||
|
- Basic Font Information - description of the font family, purpose and breadth
|
||||||
|
- ChangeLog - chronological listing of changes
|
||||||
|
- Acknowledgements - list of authors and contributors with contact information
|
||||||
|
|
||||||
|
It could also include other sections, such as: where to find documentation, how to make contributions, information on contributing organizations, source code details, and a short design guide. See Appendix A for an example FONTLOG.
|
||||||
|
|
||||||
|
|
||||||
|
7 MAKING CONTRIBUTIONS TO OFL PROJECTS
|
||||||
|
|
||||||
|
7.1 Can I contribute work to OFL projects?
|
||||||
|
In many cases, yes. It is common for OFL fonts to be developed by a team of people who welcome contributions from the wider community. Contact the original authors for specific information on how to participate in their projects.
|
||||||
|
|
||||||
|
7.2 Why should I contribute my changes back to the original authors?
|
||||||
|
It would benefit many people if you contributed back in response to what you've received. Your contributions and improvements to the fonts and other components could be a tremendous help and would encourage others to contribute as well and 'give back'. You will then benefit from other people's contributions as well. Sometimes maintaining your own separate version takes more effort than merging back with the original. Be aware that any contributions, however, must be either your own original creation or work that you own, and you may be asked to affirm that clearly when you contribute.
|
||||||
|
|
||||||
|
7.3 I've made some very nice improvements to the font. Will you consider adopting them and putting them into future Original Versions?
|
||||||
|
Most authors would be very happy to receive such contributions. Keep in mind that it is unlikely that they would want to incorporate major changes that would require additional work on their end. Any contributions would likely need to be made for all the fonts in a family and match the overall design and style. Authors are encouraged to include a guide to the design with the fonts. It would also help to have contributions submitted as patches or clearly marked changes - the use of smart source revision control systems like subversion, svk, mercurial, git or bzr is a good idea. Please follow the recommendations given by the author(s) in terms of preferred source formats and configuration parameters for sending contributions. If this is not indicated in a FONTLOG or other documentation of the font, consider asking them directly. Examples of useful contributions are bug fixes, additional glyphs, stylistic alternates (and the smart font code to access them) or improved hinting. Keep in mind that some kinds of changes (esp. hinting) may be technically difficult to integrate.
|
||||||
|
|
||||||
|
7.4 How can I financially support the development of OFL fonts?
|
||||||
|
It is likely that most authors of OFL fonts would accept financial contributions - contact them for instructions on how to do this. Such contributions would support future development. You can also pay for others to enhance the fonts and contribute the results back to the original authors for inclusion in the Original Version.
|
||||||
|
|
||||||
|
|
||||||
|
8 ABOUT THE LICENSE ITSELF
|
||||||
|
|
||||||
|
8.1 I see that this is version 1.1 of the license. Will there be later changes?
|
||||||
|
Version 1.1 is the first minor revision of the OFL. We are confident that version 1.1 will meet most needs, but are open to future improvements. Any revisions would be for future font releases, and previously existing licenses would remain in effect. No retroactive changes are possible, although the Copyright Holder(s) can re-release the font under a revised OFL. All versions will be available on our web site: http://scripts.sil.org/OFL.
|
||||||
|
|
||||||
|
8.2 Does this license restrict the rights of the Copyright Holder(s)?
|
||||||
|
No. The Copyright Holder(s) still retain(s) all the rights to their creation; they are only releasing a portion of it for use in a specific way. For example, the Copyright Holder(s) may choose to release a 'basic' version of their font under the OFL, but sell a restricted 'enhanced' version. Only the Copyright Holder(s) can do this.
|
||||||
|
|
||||||
|
8.3 Is the OFL a contract or a license?
|
||||||
|
The OFL is a license and not a contract and so does not require you to sign it to have legal validity. By using, modifying and redistributing components under the OFL you indicate that you accept the license.
|
||||||
|
|
||||||
|
8.4 I really like the terms of the OFL, but want to change it a little. Am I allowed to take ideas and actual wording from the OFL and put them into my own custom license for distributing my fonts?
|
||||||
|
We strongly recommend against creating your very own unique open licensing model. Using a modified or derivative license will likely cut you off - along with the font(s) under that license - from the community of designers using the OFL, potentially expose you and your users to legal liabilities, and possibly put your work and rights at risk. The OFL went though a community and legal review process that took years of effort, and that review is only applicable to an unmodified OFL. The text of the OFL has been written by SIL (with review and consultation from the community) and is copyright (c) 2005-2010 SIL International. You may re-use the ideas and wording (in part, not in whole) in another non-proprietary license provided that you call your license by another unambiguous name, that you do not use the preamble, that you do not mention SIL and that you clearly present your license as different from the OFL so as not to cause confusion by being too similar to the original. If you feel the OFL does not meet your needs for an open license, please contact us.
|
||||||
|
|
||||||
|
8.5 Can I translate the license and the FAQ into other languages?
|
||||||
|
SIL certainly recognises the need for people who are not familiar with English to be able to understand the OFL and its use. Making the license very clear and readable has been a key goal for the OFL, but we know that people understand their own language best.
|
||||||
|
|
||||||
|
If you are an experienced translator, you are very welcome to translate the OFL and OFL-FAQ so that designers and users in your language community can understand the license better. But only the original English version of the license has legal value and has been approved by the community. Translations do not count as legal substitutes and should only serve as a way to explain the original license. SIL - as the author and steward of the license for the community at large - does not approve any translation of the OFL as legally valid because even small translation ambiguities could be abused and create problems.
|
||||||
|
|
||||||
|
SIL gives permission to publish unofficial translations into other languages provided that they comply with the following guidelines:
|
||||||
|
|
||||||
|
- Put the following disclaimer in both English and the target language stating clearly that the translation is unofficial:
|
||||||
|
|
||||||
|
"This is an unofficial translation of the SIL Open Font License into <language_name>. It was not published by SIL International, and does not legally state the distribution terms for fonts that use the OFL. A release under the OFL is only valid when using the original English text. However, we recognize that this unofficial translation will help users and designers not familiar with English to better understand and use the OFL. We encourage designers who consider releasing their creation under the OFL to read the OFL-FAQ in their own language if it is available. Please go to http://scripts.sil.org/OFL for the official version of the license and the accompanying OFL-FAQ."
|
||||||
|
|
||||||
|
- Keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion.
|
||||||
|
|
||||||
|
If you start such a unofficial translation effort of the OFL and OFL-FAQ please let us know.
|
||||||
|
|
||||||
|
|
||||||
|
9 ABOUT SIL INTERNATIONAL
|
||||||
|
|
||||||
|
9.1 Who is SIL International and what do they do?
|
||||||
|
SIL serves language communities worldwide, building their capacity for sustainable language development, by means of research, translation, training and materials development. SIL makes its services available to all without regard to religious belief, political ideology, gender, race, or ethnic background. SIL's members and volunteers share a Christian commitment.
|
||||||
|
|
||||||
|
9.2 What does this have to do with font licensing?
|
||||||
|
The ability to read, write, type and publish in one's own language is one of the most critical needs for millions of people around the world. This requires fonts that are widely available and support lesser-known languages. SIL develops - and encourages others to develop - a complete stack of writing systems implementation components available under open licenses. This open stack includes input methods, smart fonts, smart rendering libraries and smart applications. There has been a need for a common open license that is specifically applicable to fonts and related software (a crucial component of this stack), so SIL developed the SIL Open Font License with the help of the Free/Libre and Open Source Software community.
|
||||||
|
|
||||||
|
9.3 How can I contact SIL?
|
||||||
|
Our main web site is: http://www.sil.org/
|
||||||
|
Our site about complex scripts is: http://scripts.sil.org/
|
||||||
|
Information about this license (and contact information) is at: http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
APPENDIX A - FONTLOG EXAMPLE
|
||||||
|
|
||||||
|
Here is an example of the recommended format for a FONTLOG, although other formats are allowed.
|
||||||
|
|
||||||
|
-----
|
||||||
|
FONTLOG for the GlobalFontFamily fonts
|
||||||
|
|
||||||
|
This file provides detailed information on the GlobalFontFamily Font Software. This information should be distributed along with the GlobalFontFamily fonts and any derivative works.
|
||||||
|
|
||||||
|
Basic Font Information
|
||||||
|
|
||||||
|
GlobalFontFamily is a Unicode typeface family that supports all languages that use the Latin script and its variants, and could be expanded to support other scripts.
|
||||||
|
|
||||||
|
NewWorldFontFamily is based on the GlobalFontFamily and also supports Greek, Hebrew, Cyrillic and Armenian.
|
||||||
|
|
||||||
|
More specifically, this release supports the following Unicode ranges...
|
||||||
|
This release contains...
|
||||||
|
Documentation can be found at...
|
||||||
|
To contribute to the project...
|
||||||
|
|
||||||
|
ChangeLog
|
||||||
|
|
||||||
|
1 August 2008 (Tom Parker) GlobalFontFamily version 1.2.1
|
||||||
|
- Tweaked the smart font code (Branch merged with trunk version)
|
||||||
|
- Provided improved build and debugging environment for smart behaviours
|
||||||
|
|
||||||
|
7 February 2007 (Pat Johnson) NewWorldFontFamily Version 1.3
|
||||||
|
- Added Greek and Cyrillic glyphs
|
||||||
|
|
||||||
|
7 March 2006 (Fred Foobar) NewWorldFontFamily Version 1.2
|
||||||
|
- Tweaked contextual behaviours
|
||||||
|
|
||||||
|
1 Feb 2005 (Jane Doe) NewWorldFontFamily Version 1.1
|
||||||
|
- Improved build script performance and verbosity
|
||||||
|
- Extended the smart code documentation
|
||||||
|
- Corrected minor typos in the documentation
|
||||||
|
- Fixed position of combining inverted breve below (U+032F)
|
||||||
|
- Added OpenType/Graphite smart code for Armenian
|
||||||
|
- Added Armenian glyphs (U+0531 -> U+0587)
|
||||||
|
- Released as "NewWorldFontFamily"
|
||||||
|
|
||||||
|
1 Jan 2005 (Joe Smith) GlobalFontFamily Version 1.0
|
||||||
|
- Initial release
|
||||||
|
|
||||||
|
Acknowledgements
|
||||||
|
|
||||||
|
If you make modifications be sure to add your name (N), email (E), web-address (if you have one) (W) and description (D). This list is in alphabetical order.
|
||||||
|
|
||||||
|
N: Jane Doe
|
||||||
|
E: jane@university.edu
|
||||||
|
W: http://art.university.edu/projects/fonts
|
||||||
|
D: Contributor - Armenian glyphs and code
|
||||||
|
|
||||||
|
N: Fred Foobar
|
||||||
|
E: fred@foobar.org
|
||||||
|
W: http://foobar.org
|
||||||
|
D: Contributor - misc Graphite fixes
|
||||||
|
|
||||||
|
N: Pat Johnson
|
||||||
|
E: pat@fontstudio.org
|
||||||
|
W: http://pat.fontstudio.org
|
||||||
|
D: Designer - Greek & Cyrillic glyphs based on Roman design
|
||||||
|
|
||||||
|
N: Tom Parker
|
||||||
|
E: tom@company.com
|
||||||
|
W: http://www.company.com/tom/projects/fonts
|
||||||
|
D: Engineer - original smart font code
|
||||||
|
|
||||||
|
N: Joe Smith
|
||||||
|
E: joe@fontstudio.org
|
||||||
|
W: http://joe.fontstudio.org
|
||||||
|
D: Designer - original Roman glyphs
|
||||||
|
|
||||||
|
Fontstudio.org is an not-for-profit design group whose purpose is...
|
||||||
|
Foobar.org is a distributed community of developers...
|
||||||
|
Company.com is a small business who likes to support community designers...
|
||||||
|
University.edu is a renowed educational institution with a strong design department...
|
||||||
|
-----
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright © 2015, Steinberg Media Technologies GmbH (http://www.steinberg.net/),
|
||||||
|
with Reserved Font Name "Bravura".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">ITOSM 피아노 학습실</title>
|
||||||
|
<desc id="desc">보라색 음표와 피아노 건반을 조합한 표식</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#5b5cf0"/>
|
||||||
|
<stop offset="1" stop-color="#8b3fd6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#bg)"/>
|
||||||
|
<path d="M18 19v20.5a7.5 7.5 0 1 0 5 7.1V28l22-4v11.5a7.5 7.5 0 1 0 5 7.1V14L18 19Z" fill="#fff"/>
|
||||||
|
<path d="M13 51h38" stroke="#d9d8ff" stroke-width="3" stroke-linecap="round" opacity=".9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 720 B |
@@ -29,7 +29,8 @@ for (let m = MIDI_MIN; m <= MIDI_MAX; m++) {
|
|||||||
const TOTAL_WHITE_KEYS = wi;
|
const TOTAL_WHITE_KEYS = wi;
|
||||||
|
|
||||||
// ── Theme palette ─────────────────────────────────────────────────────────────
|
// ── Theme palette ─────────────────────────────────────────────────────────────
|
||||||
// Each theme has: left hand, right hand, unknown, and black-key variant (slightly lighter)
|
// 검은 건반 음표는 같은 손 색상을 유지하면서 명도를 낮춘다. 흰 건반 음표와
|
||||||
|
// 나란히 내려올 때 폭뿐 아니라 색만으로도 즉시 구분할 수 있어야 한다.
|
||||||
interface ThemePalette {
|
interface ThemePalette {
|
||||||
left: number;
|
left: number;
|
||||||
right: number;
|
right: number;
|
||||||
@@ -44,45 +45,45 @@ const THEMES: Record<Exclude<ColorTheme, "custom">, ThemePalette> = {
|
|||||||
left: 0x7c3aed,
|
left: 0x7c3aed,
|
||||||
right: 0x8b5cf6,
|
right: 0x8b5cf6,
|
||||||
unknown: 0x8b5cf6,
|
unknown: 0x8b5cf6,
|
||||||
leftBlack: 0xa78bfa,
|
leftBlack: 0x4c1d95,
|
||||||
rightBlack: 0xc4b5fd,
|
rightBlack: 0x5b21b6,
|
||||||
unknownBlack: 0xc4b5fd,
|
unknownBlack: 0x5b21b6,
|
||||||
},
|
},
|
||||||
classic: {
|
classic: {
|
||||||
left: 0x2563eb,
|
left: 0x2563eb,
|
||||||
right: 0xdc2626,
|
right: 0xdc2626,
|
||||||
unknown: 0x7c3aed,
|
unknown: 0x7c3aed,
|
||||||
leftBlack: 0x60a5fa,
|
leftBlack: 0x1e3a8a,
|
||||||
rightBlack: 0xf87171,
|
rightBlack: 0x991b1b,
|
||||||
unknownBlack: 0xa78bfa,
|
unknownBlack: 0x581c87,
|
||||||
},
|
},
|
||||||
ocean: {
|
ocean: {
|
||||||
left: 0x0284c7,
|
left: 0x0284c7,
|
||||||
right: 0x06b6d4,
|
right: 0x06b6d4,
|
||||||
unknown: 0x0ea5e9,
|
unknown: 0x0ea5e9,
|
||||||
leftBlack: 0x38bdf8,
|
leftBlack: 0x075985,
|
||||||
rightBlack: 0x67e8f9,
|
rightBlack: 0x0e7490,
|
||||||
unknownBlack: 0x7dd3fc,
|
unknownBlack: 0x0369a1,
|
||||||
},
|
},
|
||||||
forest: {
|
forest: {
|
||||||
left: 0x15803d,
|
left: 0x15803d,
|
||||||
right: 0x16a34a,
|
right: 0x16a34a,
|
||||||
unknown: 0x22c55e,
|
unknown: 0x22c55e,
|
||||||
leftBlack: 0x4ade80,
|
leftBlack: 0x14532d,
|
||||||
rightBlack: 0x86efac,
|
rightBlack: 0x166534,
|
||||||
unknownBlack: 0xbbf7d0,
|
unknownBlack: 0x15803d,
|
||||||
},
|
},
|
||||||
cascade: {
|
cascade: {
|
||||||
left: 0x9333ea,
|
left: 0x9333ea,
|
||||||
right: 0x22d3ee,
|
right: 0x22d3ee,
|
||||||
unknown: 0xa855f7,
|
unknown: 0xa855f7,
|
||||||
leftBlack: 0xb06af5,
|
leftBlack: 0x581c87,
|
||||||
rightBlack: 0x4dd5ec,
|
rightBlack: 0x0e7490,
|
||||||
unknownBlack: 0xb87af8,
|
unknownBlack: 0x6b21a8,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Build a ThemePalette from three base hex colors (black-key variants auto-lightened). */
|
/** 사용자 지정 색상도 검은 건반 음표는 원래 색상의 약 68% 명도로 낮춘다. */
|
||||||
function paletteFromCustom(c: CustomColors): ThemePalette {
|
function paletteFromCustom(c: CustomColors): ThemePalette {
|
||||||
const l = hexToNum(c.leftHand);
|
const l = hexToNum(c.leftHand);
|
||||||
const r = hexToNum(c.rightHand);
|
const r = hexToNum(c.rightHand);
|
||||||
@@ -91,9 +92,9 @@ function paletteFromCustom(c: CustomColors): ThemePalette {
|
|||||||
left: l,
|
left: l,
|
||||||
right: r,
|
right: r,
|
||||||
unknown: u,
|
unknown: u,
|
||||||
leftBlack: lighten(l, 1.55),
|
leftBlack: lighten(l, 0.68),
|
||||||
rightBlack: lighten(r, 1.55),
|
rightBlack: lighten(r, 0.68),
|
||||||
unknownBlack: lighten(u, 1.55),
|
unknownBlack: lighten(u, 0.68),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function FallingNotesView() {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const rendererRef = useRef<FallingNotesRenderer | null>(null);
|
const rendererRef = useRef<FallingNotesRenderer | null>(null);
|
||||||
const lastSecondsRef = useRef(0);
|
const lastSecondsRef = useRef(0);
|
||||||
const { settings, document: doc, midiEnabled, status, seek } = useAppStore();
|
const { settings, document: doc, midiEnabled, status, currentSeconds, seek } = useAppStore();
|
||||||
|
|
||||||
// Accuracy scoring - active when MIDI device connected
|
// Accuracy scoring - active when MIDI device connected
|
||||||
const accuracy = useAccuracy(midiEnabled && (status === "playing" || status === "paused"));
|
const accuracy = useAccuracy(midiEnabled && (status === "playing" || status === "paused"));
|
||||||
@@ -181,6 +181,32 @@ export function FallingNotesView() {
|
|||||||
>
|
>
|
||||||
<canvas ref={canvasRef} style={{ display: "block", width: "100%", height: "100%" }} />
|
<canvas ref={canvasRef} style={{ display: "block", width: "100%", height: "100%" }} />
|
||||||
|
|
||||||
|
{status === "playing" && currentSeconds < 0 && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-10 flex items-center justify-center pointer-events-none"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label={`${Math.ceil(-currentSeconds)}초 뒤 재생`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: 88,
|
||||||
|
padding: "12px 20px",
|
||||||
|
borderRadius: 18,
|
||||||
|
background: "rgba(15, 23, 42, 0.78)",
|
||||||
|
border: "1px solid rgba(255, 255, 255, 0.24)",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: 38,
|
||||||
|
fontWeight: 800,
|
||||||
|
lineHeight: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
boxShadow: "0 10px 32px rgba(0, 0, 0, 0.28)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.ceil(-currentSeconds)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{midiEnabled && accuracy.correct + accuracy.wrong + accuracy.missed > 0 && (
|
{midiEnabled && accuracy.correct + accuracy.wrong + accuracy.missed > 0 && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-2 left-2 z-10 pointer-events-none text-xs px-2 py-0.5 rounded-full font-medium"
|
className="absolute top-2 left-2 z-10 pointer-events-none text-xs px-2 py-0.5 rounded-full font-medium"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
type User = {
|
export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
@@ -18,6 +18,7 @@ type MidiItem = {
|
|||||||
artist: string;
|
artist: string;
|
||||||
description: string;
|
description: string;
|
||||||
original_name: string;
|
original_name: string;
|
||||||
|
file_type: string;
|
||||||
size_bytes: number;
|
size_bytes: number;
|
||||||
visibility: "public" | "members" | "shared" | "private";
|
visibility: "public" | "members" | "shared" | "private";
|
||||||
min_tier: number;
|
min_tier: number;
|
||||||
@@ -68,7 +69,22 @@ const VISIBILITY_LABELS: Record<MidiItem["visibility"], string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
|
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(url, init);
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, init);
|
||||||
|
} catch {
|
||||||
|
if (init?.method && init.method !== "GET") {
|
||||||
|
throw new Error("서버에 연결하지 못했습니다. 잠시 뒤 다시 시도해 주세요");
|
||||||
|
}
|
||||||
|
// 컨테이너를 갱신하는 짧은 순간에는 연결이 한 번 끊길 수 있다.
|
||||||
|
// 조회 요청만 한 차례 다시 보내 사용자가 오래된 오류 문구를 보지 않게 한다.
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 500));
|
||||||
|
try {
|
||||||
|
response = await fetch(url, init);
|
||||||
|
} catch {
|
||||||
|
throw new Error("서버에 연결하지 못했습니다. 잠시 뒤 다시 시도해 주세요");
|
||||||
|
}
|
||||||
|
}
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
if (!response.ok) throw new Error(data.detail || "요청을 처리하지 못했습니다");
|
if (!response.ok) throw new Error(data.detail || "요청을 처리하지 못했습니다");
|
||||||
return data as T;
|
return data as T;
|
||||||
@@ -85,11 +101,13 @@ export function LibraryPanel({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onLoad,
|
onLoad,
|
||||||
|
onAuthChange,
|
||||||
initialTab = "browse",
|
initialTab = "browse",
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onLoad: (file: File) => void;
|
onLoad: (file: File) => void;
|
||||||
|
onAuthChange?: (user: User | null) => void;
|
||||||
initialTab?: LibraryTab;
|
initialTab?: LibraryTab;
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<LibraryTab>("browse");
|
const [tab, setTab] = useState<LibraryTab>("browse");
|
||||||
@@ -110,8 +128,9 @@ export function LibraryPanel({
|
|||||||
const result = await apiJson<AuthResult>("/api/library/auth/me");
|
const result = await apiJson<AuthResult>("/api/library/auth/me");
|
||||||
setUser(result.user);
|
setUser(result.user);
|
||||||
setCsrf(result.csrf_token);
|
setCsrf(result.csrf_token);
|
||||||
|
onAuthChange?.(result.user);
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, [onAuthChange]);
|
||||||
|
|
||||||
const refreshItems = useCallback(async () => {
|
const refreshItems = useCallback(async () => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -127,6 +146,9 @@ export function LibraryPanel({
|
|||||||
setItems(result.items);
|
setItems(result.items);
|
||||||
setGenres(result.facets.genres);
|
setGenres(result.facets.genres);
|
||||||
setTags(result.facets.tags);
|
setTags(result.facets.tags);
|
||||||
|
setMessage((current) =>
|
||||||
|
current === "서버에 연결하지 못했습니다. 잠시 뒤 다시 시도해 주세요" ? "" : current
|
||||||
|
);
|
||||||
}, [search, genre, difficulty, arrangement, activeTag]);
|
}, [search, genre, difficulty, arrangement, activeTag]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -185,10 +207,10 @@ export function LibraryPanel({
|
|||||||
const response = await fetch(`/api/library/midi/${item.id}/file`);
|
const response = await fetch(`/api/library/midi/${item.id}/file`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
throw new Error(data.detail || "MIDI 파일을 불러오지 못했습니다");
|
throw new Error(data.detail || "곡 또는 악보 파일을 불러오지 못했습니다");
|
||||||
}
|
}
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
onLoad(new File([blob], item.original_name, { type: "audio/midi" }));
|
onLoad(new File([blob], item.original_name, { type: blob.type || "application/octet-stream" }));
|
||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,6 +323,7 @@ export function LibraryPanel({
|
|||||||
if (!authenticatedUser) throw new Error("로그인 정보를 확인하지 못했습니다");
|
if (!authenticatedUser) throw new Error("로그인 정보를 확인하지 못했습니다");
|
||||||
setUser(authenticatedUser);
|
setUser(authenticatedUser);
|
||||||
setCsrf(result.csrf_token);
|
setCsrf(result.csrf_token);
|
||||||
|
onAuthChange?.(authenticatedUser);
|
||||||
setTab(
|
setTab(
|
||||||
initialTab === "admin" && authenticatedUser.role === "admin" ? "admin" : "browse"
|
initialTab === "admin" && authenticatedUser.role === "admin" ? "admin" : "browse"
|
||||||
);
|
);
|
||||||
@@ -314,6 +337,7 @@ export function LibraryPanel({
|
|||||||
onSignedOut={() => {
|
onSignedOut={() => {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setCsrf(null);
|
setCsrf(null);
|
||||||
|
onAuthChange?.(null);
|
||||||
setTab("browse");
|
setTab("browse");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -736,7 +760,11 @@ function UploadTab({
|
|||||||
className="upload-form"
|
className="upload-form"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = new FormData(event.currentTarget);
|
// React 이벤트의 currentTarget은 이벤트 처리기가 끝나면 null이 될 수 있다.
|
||||||
|
// 업로드 응답을 기다린 뒤에도 안전하게 초기화할 수 있도록 실제 폼 요소를
|
||||||
|
// 비동기 작업 시작 전에 별도로 보관한다.
|
||||||
|
const formElement = event.currentTarget;
|
||||||
|
const form = new FormData(formElement);
|
||||||
if (visibility === "public") form.set("min_tier", "0");
|
if (visibility === "public") form.set("min_tier", "0");
|
||||||
run(async () => {
|
run(async () => {
|
||||||
const response = await fetch("/api/library/midi", {
|
const response = await fetch("/api/library/midi", {
|
||||||
@@ -746,19 +774,19 @@ function UploadTab({
|
|||||||
});
|
});
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
if (!response.ok) throw new Error(data.detail || "업로드하지 못했습니다");
|
if (!response.ok) throw new Error(data.detail || "업로드하지 못했습니다");
|
||||||
event.currentTarget.reset();
|
formElement.reset();
|
||||||
setVisibility("private");
|
setVisibility("private");
|
||||||
await onUploaded();
|
await onUploaded();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>MIDI 업로드</h3>
|
<h3>곡·악보 업로드</h3>
|
||||||
<p>
|
<p>
|
||||||
파일은 MIDI 헤더와 크기를 검사합니다. 저작권을 보유하거나 공유 허가를 받은 곡만 올려 주세요.
|
MIDI와 MusicXML과 MXL 및 PDF 악보를 지원합니다. 파일 형식과 크기를 검사하며 저작권을 보유하거나 공유 허가를 받은 자료만 올려 주세요.
|
||||||
</p>
|
</p>
|
||||||
<label className="wide">
|
<label className="wide">
|
||||||
MIDI 파일
|
MIDI 또는 악보 파일
|
||||||
<input type="file" name="file" accept=".mid,.midi" required />
|
<input type="file" name="file" accept=".mid,.midi,.xml,.musicxml,.mxl,.pdf" required />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
곡 제목
|
곡 제목
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import bus from "../../engine/EventBus";
|
import bus from "../../engine/EventBus";
|
||||||
import { useAppStore } from "../../store/useAppStore";
|
import { useAppStore } from "../../store/useAppStore";
|
||||||
import { syncEngine } from "../../engine/SyncEngine";
|
import { syncEngine } from "../../engine/SyncEngine";
|
||||||
@@ -21,6 +21,8 @@ export function SheetMusicView() {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null); // inner AlphaTab host
|
const containerRef = useRef<HTMLDivElement>(null); // inner AlphaTab host
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const apiRef = useRef<any>(null);
|
const apiRef = useRef<any>(null);
|
||||||
|
const [apiReady, setApiReady] = useState(false);
|
||||||
|
const [sheetError, setSheetError] = useState("");
|
||||||
const playerReady = useRef(false); // true once soundFont is loaded
|
const playerReady = useRef(false); // true once soundFont is loaded
|
||||||
const scoreLoaded = useRef(false); // true once score SVG is rendered
|
const scoreLoaded = useRef(false); // true once score SVG is rendered
|
||||||
const lastScrollTop = useRef(-1);
|
const lastScrollTop = useRef(-1);
|
||||||
@@ -35,9 +37,10 @@ export function SheetMusicView() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
let cleanupBus: (() => void) | null = null;
|
let cleanupBus: (() => void) | null = null;
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
import("@coderline/alphatab").then(({ AlphaTabApi }) => {
|
import("@coderline/alphatab").then(({ AlphaTabApi }) => {
|
||||||
if (!containerRef.current) return;
|
if (disposed || !containerRef.current) return;
|
||||||
|
|
||||||
// In the "all" view the sheet sits in a full-width band at the top and
|
// In the "all" view the sheet sits in a full-width band at the top and
|
||||||
// renders as a single horizontal strip (layoutMode 1) that scrolls
|
// renders as a single horizontal strip (layoutMode 1) that scrolls
|
||||||
@@ -48,7 +51,20 @@ export function SheetMusicView() {
|
|||||||
const horizontal = useAppStore.getState().settings.layoutMode === "all";
|
const horizontal = useAppStore.getState().settings.layoutMode === "all";
|
||||||
|
|
||||||
const api = new AlphaTabApi(containerRef.current, {
|
const api = new AlphaTabApi(containerRef.current, {
|
||||||
core: { engine: "svg", enableLazyLoading: false, logLevel: 0 },
|
core: {
|
||||||
|
engine: "svg",
|
||||||
|
enableLazyLoading: false,
|
||||||
|
logLevel: 0,
|
||||||
|
// 악보는 UI 스레드에서 동기 렌더링한다. Vite 번들과 AlphaTab의
|
||||||
|
// 별도 Worker가 서로 다른 렌더링 자원을 잡아 빈 악보가 되는 것을 막는다.
|
||||||
|
useWorkers: false,
|
||||||
|
// AlphaTab은 일반 글꼴이 아니라 SMuFL Bravura 글꼴로 오선과 음표를
|
||||||
|
// 그린다. 이 경로가 없으면 제목만 보이고 악보 기호는 빈 화면이 된다.
|
||||||
|
fontDirectory: "/font/",
|
||||||
|
// Vite로 묶인 본체 파일을 Worker로 다시 실행하면 DOM 참조 때문에
|
||||||
|
// 실패한다. AlphaTab이 제공하는 전용 Worker 진입점을 고정한다.
|
||||||
|
scriptFile: "/assets/alphaTab.worker.mjs?v=20261003-1",
|
||||||
|
},
|
||||||
display: {
|
display: {
|
||||||
layoutMode: horizontal ? 1 : 0, // 1 = horizontal strip, 0 = page (vertical)
|
layoutMode: horizontal ? 1 : 0, // 1 = horizontal strip, 0 = page (vertical)
|
||||||
// Smaller scale in the horizontal "all" band so the full grand staff
|
// Smaller scale in the horizontal "all" band so the full grand staff
|
||||||
@@ -75,6 +91,23 @@ export function SheetMusicView() {
|
|||||||
} as never);
|
} as never);
|
||||||
|
|
||||||
apiRef.current = api;
|
apiRef.current = api;
|
||||||
|
// 플레이어가 매우 빨리 준비되면 아래 이벤트를 구독하기 전에
|
||||||
|
// playerReady가 끝날 수 있다. 음소거 값은 생성 직후에도 저장되므로
|
||||||
|
// 먼저 적용하고 이벤트에서는 한 번 더 보정한다.
|
||||||
|
api.masterVolume = 0;
|
||||||
|
setApiReady(true);
|
||||||
|
|
||||||
|
// AlphaTab의 파일 해석과 렌더링 오류는 비동기로 전달된다. 빈 화면만
|
||||||
|
// 남기지 않고 사용자가 원인을 알 수 있도록 화면에 오류를 표시한다.
|
||||||
|
try {
|
||||||
|
api.error.on((error: unknown) => {
|
||||||
|
if (disposed) return;
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
setSheetError(`악보를 표시하지 못했습니다: ${message}`);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* 이전 AlphaTab 버전에서는 오류 이벤트가 없을 수 있다. */
|
||||||
|
}
|
||||||
|
|
||||||
// ── Mute AlphaTab AFTER soundFont finishes loading ───────────────────
|
// ── Mute AlphaTab AFTER soundFont finishes loading ───────────────────
|
||||||
try {
|
try {
|
||||||
@@ -90,20 +123,12 @@ export function SheetMusicView() {
|
|||||||
try {
|
try {
|
||||||
api.scoreLoaded.on(() => {
|
api.scoreLoaded.on(() => {
|
||||||
scoreLoaded.current = true;
|
scoreLoaded.current = true;
|
||||||
|
if (!disposed) setSheetError("");
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load score if MusicXML is already available ──────────────────────
|
|
||||||
const initialDoc = useAppStore.getState().document;
|
|
||||||
if (initialDoc?.musicXml) {
|
|
||||||
api.load(new TextEncoder().encode(sanitizeMusicXml(initialDoc.musicXml)));
|
|
||||||
} else if (initialDoc?.mxlBuffer) {
|
|
||||||
// .mxl is ZIP-compressed MusicXML; AlphaTab handles it natively
|
|
||||||
api.load(new Uint8Array(initialDoc.mxlBuffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Click note to play sound ─────────────────────────────────────────
|
// ── Click note to play sound ─────────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -148,15 +173,32 @@ export function SheetMusicView() {
|
|||||||
// IMPORTANT: do NOT close over `doc` here - it would always be null
|
// IMPORTANT: do NOT close over `doc` here - it would always be null
|
||||||
// (stale closure from first render). Always read fresh state instead.
|
// (stale closure from first render). Always read fresh state instead.
|
||||||
const onTick = ({ seconds }: { seconds: number; tick: number }) => {
|
const onTick = ({ seconds }: { seconds: number; tick: number }) => {
|
||||||
if (!apiRef.current || !playerReady.current || !scoreLoaded.current) return;
|
// playerReady는 생성 직후 이미 발생해 구독에서 놓칠 수 있다.
|
||||||
|
// 이를 필수 조건으로 사용하면 시간 막대만 움직이고 악보 커서는
|
||||||
|
// 첫 마디에 영구히 멈춘다. 악보 렌더 완료만 확인해 위치를 갱신한다.
|
||||||
|
if (!apiRef.current || !scoreLoaded.current) return;
|
||||||
|
|
||||||
// Always-fresh doc - avoids stale closure bug
|
// Always-fresh doc - avoids stale closure bug
|
||||||
const doc = useAppStore.getState().document;
|
const doc = useAppStore.getState().document;
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
|
|
||||||
const tick = secondsToTick(seconds, doc.tempoMap ?? [], doc.ppq ?? 480);
|
|
||||||
try {
|
try {
|
||||||
apiRef.current.tickPosition = tick;
|
// 변환된 MusicXML의 마디와 템포 정보는 원본 MIDI의 tick 구조와
|
||||||
|
// 달라질 수 있다. 원본 tick을 다시 계산해 넣으면 재생 시간은
|
||||||
|
// 진행해도 악보 커서는 첫 마디 근처에 머문다. AlphaTab이 현재
|
||||||
|
// 악보의 템포 지도를 직접 사용하도록 밀리초 위치를 전달한다.
|
||||||
|
apiRef.current.timePosition = Math.max(0, seconds * 1000);
|
||||||
|
|
||||||
|
// AlphaTab의 내부 플레이어는 이 화면에서 음소거 상태다. 브라우저와
|
||||||
|
// 로딩 순서에 따라 timePosition 변경을 커서 이벤트로 돌려주지 않는
|
||||||
|
// 경우가 있으므로 현재 고정 버전의 커서 갱신기를 함께 호출한다.
|
||||||
|
// 다음 버전에서 내부 이름이 바뀌어도 시간 설정은 계속 작동하도록
|
||||||
|
// 존재 여부를 확인한다.
|
||||||
|
const updateCursor = apiRef.current._cursorUpdateTick;
|
||||||
|
if (typeof updateCursor === "function") {
|
||||||
|
const tick = secondsToAlphaTabTick(seconds, doc.tempoMap ?? [], doc.ppq ?? 480);
|
||||||
|
updateCursor.call(apiRef.current, tick, false, 1, false, true);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -202,6 +244,7 @@ export function SheetMusicView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
disposed = true;
|
||||||
cleanupBus?.();
|
cleanupBus?.();
|
||||||
if (rafScrollId.current !== null) {
|
if (rafScrollId.current !== null) {
|
||||||
cancelAnimationFrame(rafScrollId.current);
|
cancelAnimationFrame(rafScrollId.current);
|
||||||
@@ -248,7 +291,7 @@ export function SheetMusicView() {
|
|||||||
|
|
||||||
// ── Load score when MusicXML/mxlBuffer arrives ───────────────────────────
|
// ── Load score when MusicXML/mxlBuffer arrives ───────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!apiRef.current) return;
|
if (!apiReady || !apiRef.current) return;
|
||||||
// Apply the correct stretchForce before every load so AlphaTab uses it
|
// Apply the correct stretchForce before every load so AlphaTab uses it
|
||||||
// in the render it triggers internally.
|
// in the render it triggers internally.
|
||||||
try {
|
try {
|
||||||
@@ -259,13 +302,17 @@ export function SheetMusicView() {
|
|||||||
}
|
}
|
||||||
if (doc?.musicXml) {
|
if (doc?.musicXml) {
|
||||||
scoreLoaded.current = false;
|
scoreLoaded.current = false;
|
||||||
apiRef.current.load(new TextEncoder().encode(sanitizeMusicXml(doc.musicXml)));
|
setSheetError("");
|
||||||
|
const accepted = apiRef.current.load(new TextEncoder().encode(sanitizeMusicXml(doc.musicXml)));
|
||||||
|
if (!accepted) setSheetError("악보 파일을 읽기 시작하지 못했습니다");
|
||||||
} else if (doc?.mxlBuffer) {
|
} else if (doc?.mxlBuffer) {
|
||||||
// .mxl is ZIP-compressed MusicXML; AlphaTab handles it natively
|
// .mxl is ZIP-compressed MusicXML; AlphaTab handles it natively
|
||||||
scoreLoaded.current = false;
|
scoreLoaded.current = false;
|
||||||
apiRef.current.load(new Uint8Array(doc.mxlBuffer));
|
setSheetError("");
|
||||||
|
const accepted = apiRef.current.load(new Uint8Array(doc.mxlBuffer));
|
||||||
|
if (!accepted) setSheetError("압축 악보 파일을 읽기 시작하지 못했습니다");
|
||||||
}
|
}
|
||||||
}, [doc?.musicXml, doc?.mxlBuffer]);
|
}, [apiReady, doc?.musicXml, doc?.mxlBuffer]);
|
||||||
|
|
||||||
// Background logic:
|
// Background logic:
|
||||||
// dark + keep white: explicit #fff so black notation stays readable on white
|
// dark + keep white: explicit #fff so black notation stays readable on white
|
||||||
@@ -311,6 +358,20 @@ export function SheetMusicView() {
|
|||||||
>
|
>
|
||||||
<div ref={containerRef} className={invertSheet ? "sheet-dark-invert" : ""} />
|
<div ref={containerRef} className={invertSheet ? "sheet-dark-invert" : ""} />
|
||||||
|
|
||||||
|
{sheetError && (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="mx-auto my-8 max-w-xl rounded-lg border px-4 py-3 text-sm"
|
||||||
|
style={{
|
||||||
|
borderColor: "#f0a0a0",
|
||||||
|
background: "#fff4f4",
|
||||||
|
color: "#9b1c1c",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sheetError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{settings.showKeySignature && doc?.keySignature && settings.layoutMode !== "all" && (
|
{settings.showKeySignature && doc?.keySignature && settings.layoutMode !== "all" && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-2 right-4 z-10 pointer-events-none text-xs px-2 py-0.5 rounded"
|
className="absolute top-2 right-4 z-10 pointer-events-none text-xs px-2 py-0.5 rounded"
|
||||||
@@ -329,42 +390,30 @@ export function SheetMusicView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// AlphaTab 악보 내부는 사분음표당 960 tick을 사용한다. 원본 MIDI의
|
||||||
|
// 템포 변화 구간을 초 단위로 따라가며 악보 커서용 tick으로 변환한다.
|
||||||
|
const ALPHATAB_PPQ = 960;
|
||||||
|
|
||||||
// AlphaTab uses 960 ticks per quarter note internally, regardless of the MIDI file's PPQ.
|
function secondsToAlphaTabTick(targetSeconds: number, tempoMap: TempoEvent[], ppq: number): number {
|
||||||
// This function converts playback seconds to AlphaTab internal ticks (960 PPQ output).
|
if (!tempoMap.length) return Math.round(targetSeconds * 2 * ALPHATAB_PPQ);
|
||||||
// The tempo map uses MIDI-file ticks for segment boundaries, so we still need `ppq`
|
|
||||||
// to convert those boundaries to seconds, but the output is always in 960-PPQ space.
|
|
||||||
const AT_PPQ = 960;
|
|
||||||
|
|
||||||
function secondsToTick(targetSec: number, tempoMap: TempoEvent[], ppq: number): number {
|
let result = 0;
|
||||||
if (!tempoMap.length) return Math.round(targetSec * 2 * AT_PPQ);
|
let remaining = Math.max(0, targetSeconds);
|
||||||
let atTick = 0; // accumulated output in AlphaTab 960-PPQ ticks
|
for (let index = 0; index < tempoMap.length; index++) {
|
||||||
let remainingSec = targetSec;
|
const current = tempoMap[index];
|
||||||
|
const next = tempoMap[index + 1];
|
||||||
for (let i = 0; i < tempoMap.length; i++) {
|
const secondsPerMidiTick = 60 / (current.bpm * ppq);
|
||||||
const ev = tempoMap[i];
|
const alphaTabPerMidiTick = ALPHATAB_PPQ / ppq;
|
||||||
const next = tempoMap[i + 1];
|
if (!next) {
|
||||||
|
return result + Math.round((remaining / secondsPerMidiTick) * alphaTabPerMidiTick);
|
||||||
// seconds per MIDI tick at this tempo
|
|
||||||
const secPerMidiTick = 60 / (ev.bpm * ppq);
|
|
||||||
// AlphaTab ticks per MIDI tick (scale factor between file PPQ and AT PPQ)
|
|
||||||
const atPerMidi = AT_PPQ / ppq;
|
|
||||||
|
|
||||||
if (next) {
|
|
||||||
const midiSpan = next.tick - ev.tick; // segment length in MIDI ticks
|
|
||||||
const secSpan = midiSpan * secPerMidiTick;
|
|
||||||
if (remainingSec <= secSpan) {
|
|
||||||
atTick += Math.round((remainingSec / secPerMidiTick) * atPerMidi);
|
|
||||||
return atTick;
|
|
||||||
}
|
|
||||||
remainingSec -= secSpan;
|
|
||||||
atTick += Math.round(midiSpan * atPerMidi);
|
|
||||||
} else {
|
|
||||||
// Last segment - extends to end of piece
|
|
||||||
atTick += Math.round((remainingSec / secPerMidiTick) * atPerMidi);
|
|
||||||
return atTick;
|
|
||||||
}
|
}
|
||||||
|
const midiSpan = next.tick - current.tick;
|
||||||
|
const secondsSpan = midiSpan * secondsPerMidiTick;
|
||||||
|
if (remaining <= secondsSpan) {
|
||||||
|
return result + Math.round((remaining / secondsPerMidiTick) * alphaTabPerMidiTick);
|
||||||
|
}
|
||||||
|
remaining -= secondsSpan;
|
||||||
|
result += Math.round(midiSpan * alphaTabPerMidiTick);
|
||||||
}
|
}
|
||||||
return atTick;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,23 +402,22 @@ function ResetButton() {
|
|||||||
{confirmed ? "Click again to confirm reset" : "Reset to defaults"}
|
{confirmed ? "Click again to confirm reset" : "Reset to defaults"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Logo mark - links to GitHub repo */}
|
{/* 서비스 표식과 저장소 연결 */}
|
||||||
<div style={{ display: "flex", justifyContent: "center", paddingTop: 8, paddingBottom: 4 }}>
|
<div style={{ display: "flex", justifyContent: "center", paddingTop: 8, paddingBottom: 4 }}>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/gigliof/bach-to-basics"
|
href="https://gitea.itosm.co.kr/syskiller/ITOSMPiano-BachToBasics"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
style={{ lineHeight: 0 }}
|
style={{ lineHeight: 0 }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/logo.png"
|
src="/itosm-piano-mark.svg"
|
||||||
alt="Bach to Basics on GitHub"
|
alt="ITOSM 피아노 학습실 Gitea 저장소"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
style={{
|
||||||
width: 72,
|
width: 56,
|
||||||
height: "auto",
|
height: "auto",
|
||||||
opacity: isDark ? 0.35 : 0.18,
|
opacity: isDark ? 0.75 : 0.62,
|
||||||
filter: isDark ? "invert(1)" : "none",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function TransportControls() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const total = doc?.totalDuration ?? 0;
|
const total = doc?.totalDuration ?? 0;
|
||||||
const progress = total > 0 ? Math.min(1, currentSeconds / total) : 0;
|
const progress = total > 0 ? Math.max(0, Math.min(1, currentSeconds / total)) : 0;
|
||||||
const loopActive = loopStart !== null && loopEnd !== null;
|
const loopActive = loopStart !== null && loopEnd !== null;
|
||||||
const isPlaying = status === "playing";
|
const isPlaying = status === "playing";
|
||||||
const isPaused = status === "paused";
|
const isPaused = status === "paused";
|
||||||
@@ -144,7 +144,7 @@ export function TransportControls() {
|
|||||||
style={{ color: "var(--color-text-muted)", paddingLeft: 12 }}
|
style={{ color: "var(--color-text-muted)", paddingLeft: 12 }}
|
||||||
aria-label="current time"
|
aria-label="current time"
|
||||||
>
|
>
|
||||||
{formatTime(currentSeconds)}
|
{formatTime(Math.max(0, currentSeconds))}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Enlarged hit area (vertical padding) around the visual bar */}
|
{/* Enlarged hit area (vertical padding) around the visual bar */}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ interface SpeedTrainerOpts {
|
|||||||
// How many doc-seconds ahead to emit "note:upcoming" for FallingNotes pre-rendering.
|
// How many doc-seconds ahead to emit "note:upcoming" for FallingNotes pre-rendering.
|
||||||
const LOOKAHEAD_DOC_SECONDS = 5.0;
|
const LOOKAHEAD_DOC_SECONDS = 5.0;
|
||||||
const YT_DRIFT_THRESHOLD = 0.25;
|
const YT_DRIFT_THRESHOLD = 0.25;
|
||||||
|
// 곡의 맨 처음을 재생할 때 음표가 이미 건반에 닿은 상태로 시작하지 않도록
|
||||||
|
// 음수 시간부터 시계를 움직인다. 이 구간에는 음표만 내려오며 실제 MIDI 음은
|
||||||
|
// 각 음표의 원래 시작 시각인 0초 이후에 같은 AudioEngine으로 재생된다.
|
||||||
|
const START_LEAD_IN_SECONDS = 3.0;
|
||||||
|
|
||||||
export interface SyncEngineState {
|
export interface SyncEngineState {
|
||||||
status: PlaybackStatus;
|
status: PlaybackStatus;
|
||||||
@@ -147,7 +151,12 @@ class SyncEngine {
|
|||||||
bus.emit("speedTrainer:stepped", { currentPct: this.speedTrainer.startPct });
|
bus.emit("speedTrainer:stepped", { currentPct: this.speedTrainer.startPct });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.countInBars > 0) {
|
if (startFrom <= 0.01) {
|
||||||
|
this.seekTo(-START_LEAD_IN_SECONDS);
|
||||||
|
await this.clock.start(-START_LEAD_IN_SECONDS);
|
||||||
|
this.startAnimLoop();
|
||||||
|
this.emitState("playing");
|
||||||
|
} else if (this.countInBars > 0) {
|
||||||
await this._playWithCountIn(startFrom);
|
await this._playWithCountIn(startFrom);
|
||||||
} else {
|
} else {
|
||||||
this.seekTo(startFrom);
|
this.seekTo(startFrom);
|
||||||
@@ -558,7 +567,7 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── YouTube sync ──────────────────────────────────────────────────────────
|
// ── YouTube sync ──────────────────────────────────────────────────────────
|
||||||
if (this.ytPlayer && this.doc.youtubeId) {
|
if (docSeconds >= 0 && this.ytPlayer && this.doc.youtubeId) {
|
||||||
const expectedYtTime = docSeconds + this.doc.youtubeSyncOffset;
|
const expectedYtTime = docSeconds + this.doc.youtubeSyncOffset;
|
||||||
try {
|
try {
|
||||||
const actual = this.ytPlayer.getCurrentTime();
|
const actual = this.ytPlayer.getCurrentTime();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { TransportControls } from "../components/Transport/TransportControls";
|
|||||||
import { SettingsPanel } from "../components/Transport/SettingsPanel";
|
import { SettingsPanel } from "../components/Transport/SettingsPanel";
|
||||||
import { DevicePanel } from "../components/DevicePanel/DevicePanel";
|
import { DevicePanel } from "../components/DevicePanel/DevicePanel";
|
||||||
import { LibraryPanel } from "../components/Library/LibraryPanel";
|
import { LibraryPanel } from "../components/Library/LibraryPanel";
|
||||||
import type { LibraryTab } from "../components/Library/LibraryPanel";
|
import type { LibraryTab, User } from "../components/Library/LibraryPanel";
|
||||||
import { useAppStore } from "../store/useAppStore";
|
import { useAppStore } from "../store/useAppStore";
|
||||||
import type { LayoutMode } from "../store/useAppStore";
|
import type { LayoutMode } from "../store/useAppStore";
|
||||||
import bus from "../engine/EventBus";
|
import bus from "../engine/EventBus";
|
||||||
@@ -31,6 +31,26 @@ export function PracticeView() {
|
|||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [libraryOpen, setLibraryOpen] = useState(false);
|
const [libraryOpen, setLibraryOpen] = useState(false);
|
||||||
const [libraryInitialTab, setLibraryInitialTab] = useState<LibraryTab>("browse");
|
const [libraryInitialTab, setLibraryInitialTab] = useState<LibraryTab>("browse");
|
||||||
|
const [signedInUser, setSignedInUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
const updateSignedInUser = useCallback((user: User | null) => {
|
||||||
|
setSignedInUser(user);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
fetch("/api/library/auth/me")
|
||||||
|
.then((response) => (response.ok ? response.json() : Promise.reject()))
|
||||||
|
.then((result) => {
|
||||||
|
if (active) setSignedInUser(result.user ?? null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (active) setSignedInUser(null);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const openLibrary = (tab: LibraryTab) => {
|
const openLibrary = (tab: LibraryTab) => {
|
||||||
setLibraryInitialTab(tab);
|
setLibraryInitialTab(tab);
|
||||||
@@ -167,39 +187,30 @@ export function PracticeView() {
|
|||||||
paddingRight: 10,
|
paddingRight: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Left: gradient logo + app name → links to GitHub repo */}
|
{/* 왼쪽 브랜드 표식은 서비스 저장소로 연결한다. */}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/gigliof/bach-to-basics"
|
href="https://gitea.itosm.co.kr/syskiller/ITOSMPiano-BachToBasics"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="b2b-brand flex items-center gap-2 shrink-0"
|
className="b2b-brand flex items-center gap-2 shrink-0"
|
||||||
style={{ textDecoration: "none" }}
|
style={{ textDecoration: "none" }}
|
||||||
>
|
>
|
||||||
<div
|
<img
|
||||||
role="img"
|
src="/itosm-piano-mark.svg"
|
||||||
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="flex items-center justify-center shrink-0 select-none"
|
className="shrink-0 select-none"
|
||||||
style={{
|
width="32"
|
||||||
width: 32,
|
height="32"
|
||||||
height: 32,
|
draggable={false}
|
||||||
background: "linear-gradient(135deg, #7c3aed 0%, #9333ea 100%)",
|
style={{ filter: "drop-shadow(0 3px 5px rgba(91,92,240,0.35))" }}
|
||||||
borderRadius: 8,
|
/>
|
||||||
boxShadow: "0 3px 10px rgba(147,51,234,0.45)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PianoKeysIcon />
|
|
||||||
</div>
|
|
||||||
{/* Typographic pun: "Bach" and "Basics" bold, "to" muted */}
|
|
||||||
<span
|
<span
|
||||||
className="b2b-brand-name"
|
className="b2b-brand-name"
|
||||||
style={{ fontSize: 15, whiteSpace: "nowrap", letterSpacing: "-0.3px", lineHeight: 1 }}
|
style={{ fontSize: 15, whiteSpace: "nowrap", letterSpacing: "-0.3px", lineHeight: 1 }}
|
||||||
>
|
>
|
||||||
<span style={{ fontWeight: 700, color: "var(--color-text)" }}>Bach</span>
|
<span style={{ fontWeight: 700, color: "var(--color-text)" }}>ITOSM 피아노 학습실</span>
|
||||||
<span style={{ fontWeight: 400, color: "var(--color-text-muted)", margin: "0 3px" }}>
|
<span className="sr-only"> Gitea 저장소</span>
|
||||||
to
|
|
||||||
</span>
|
|
||||||
<span style={{ fontWeight: 700, color: "var(--color-text)" }}>Basics</span>
|
|
||||||
<span className="sr-only"> on GitHub</span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -260,16 +271,18 @@ export function PracticeView() {
|
|||||||
aria-label="로그인 또는 회원 가입"
|
aria-label="로그인 또는 회원 가입"
|
||||||
className="library-open-button library-account-button"
|
className="library-open-button library-account-button"
|
||||||
>
|
>
|
||||||
로그인·가입
|
{signedInUser ? "내 계정" : "로그인·회원가입"}
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => openLibrary("admin")}
|
|
||||||
title="관리자 패널 열기"
|
|
||||||
aria-label="관리자 패널 열기"
|
|
||||||
className="library-open-button library-admin-button"
|
|
||||||
>
|
|
||||||
관리자
|
|
||||||
</button>
|
</button>
|
||||||
|
{signedInUser?.role === "admin" && (
|
||||||
|
<button
|
||||||
|
onClick={() => openLibrary("admin")}
|
||||||
|
title="관리자 패널 열기"
|
||||||
|
aria-label="관리자 패널 열기"
|
||||||
|
className="library-open-button library-admin-button"
|
||||||
|
>
|
||||||
|
관리자 패널
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="b2b-midi-device">
|
<div className="b2b-midi-device">
|
||||||
<DevicePanel />
|
<DevicePanel />
|
||||||
</div>
|
</div>
|
||||||
@@ -378,6 +391,7 @@ export function PracticeView() {
|
|||||||
initialTab={libraryInitialTab}
|
initialTab={libraryInitialTab}
|
||||||
onClose={() => setLibraryOpen(false)}
|
onClose={() => setLibraryOpen(false)}
|
||||||
onLoad={loadFile}
|
onLoad={loadFile}
|
||||||
|
onAuthChange={updateSignedInUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -758,22 +772,6 @@ const MusicNoteIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Piano keyboard icon: 5 white keys with 3 black keys (C-D-E-F-G pattern) */
|
|
||||||
const PianoKeysIcon = () => (
|
|
||||||
<svg width="18" height="14" viewBox="0 0 17 14" fill="none" aria-hidden="true">
|
|
||||||
{/* White keys */}
|
|
||||||
<rect x="0" y="0" width="3" height="13" rx="0.5" fill="white" fillOpacity="0.95" />
|
|
||||||
<rect x="3.5" y="0" width="3" height="13" rx="0.5" fill="white" fillOpacity="0.95" />
|
|
||||||
<rect x="7" y="0" width="3" height="13" rx="0.5" fill="white" fillOpacity="0.95" />
|
|
||||||
<rect x="10.5" y="0" width="3" height="13" rx="0.5" fill="white" fillOpacity="0.95" />
|
|
||||||
<rect x="14" y="0" width="3" height="13" rx="0.5" fill="white" fillOpacity="0.95" />
|
|
||||||
{/* Black keys */}
|
|
||||||
<rect x="2" y="0" width="2.5" height="8.5" rx="0.5" fill="rgba(0,0,0,0.5)" />
|
|
||||||
<rect x="5.5" y="0" width="2.5" height="8.5" rx="0.5" fill="rgba(0,0,0,0.5)" />
|
|
||||||
<rect x="12.5" y="0" width="2.5" height="8.5" rx="0.5" fill="rgba(0,0,0,0.5)" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const SunIcon = () => (
|
const SunIcon = () => (
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2v-2H2v2zm18 0h2v-2h-2v2zM11 2v2h2V2h-2zm0 18v2h2v-2h-2zM5.64 6.35 4.22 4.93a1 1 0 0 0-1.41 1.41l1.41 1.42 1.42-1.41zM18.36 17.65l1.42 1.41a1 1 0 0 0 1.41-1.41l-1.41-1.42-1.42 1.42zM5.64 17.66l-1.42 1.41a1 1 0 0 1-1.41-1.41l1.41-1.42 1.42 1.42zM19.78 4.93l-1.42 1.42-1.41-1.42 1.42-1.41a1 1 0 0 1 1.41 1.41z" />
|
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2v-2H2v2zm18 0h2v-2h-2v2zM11 2v2h2V2h-2zm0 18v2h2v-2h-2zM5.64 6.35 4.22 4.93a1 1 0 0 0-1.41 1.41l1.41 1.42 1.42-1.41zM18.36 17.65l1.42 1.41a1 1 0 0 0 1.41-1.41l-1.41-1.42-1.42 1.42zM5.64 17.66l-1.42 1.41a1 1 0 0 1-1.41-1.41l1.41-1.42 1.42 1.42zM19.78 4.93l-1.42 1.42-1.41-1.42 1.42-1.41a1 1 0 0 1 1.41 1.41z" />
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
// Silently install updates - new SW takes over on next page open (no popup, no reload prompt).
|
// 이전 서비스 워커가 시작 문서를 우선 제공하면 서버를 갱신한 뒤에도
|
||||||
// Matches the consumer-app pattern (Gmail, Spotify Web, etc.).
|
// 로그인과 관리자 메뉴가 예전 화면으로 남는다. 이 서비스는 온라인
|
||||||
registerType: "autoUpdate",
|
// 학습과 회원 API가 중심이므로 서비스 워커를 스스로 해제해 항상 최신
|
||||||
|
// HTML과 해시 자산을 서버에서 받게 한다. 설치형 바로가기는 manifest로
|
||||||
|
// 계속 제공되며 오프라인 캐시만 사용하지 않는다.
|
||||||
|
selfDestroying: true,
|
||||||
|
|
||||||
// Off in dev so HMR isn't interfered with by cached responses.
|
// Off in dev so HMR isn't interfered with by cached responses.
|
||||||
devOptions: { enabled: false },
|
devOptions: { enabled: false },
|
||||||
|
|||||||
Generated
+2
-28
@@ -50,7 +50,7 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../shared
|
version: link:../shared
|
||||||
'@coderline/alphatab':
|
'@coderline/alphatab':
|
||||||
specifier: ^1.8.3
|
specifier: 1.8.3
|
||||||
version: 1.8.3
|
version: 1.8.3
|
||||||
'@tonejs/midi':
|
'@tonejs/midi':
|
||||||
specifier: ^2.0.6
|
specifier: ^2.0.6
|
||||||
@@ -788,42 +788,36 @@ packages:
|
|||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.1.2':
|
'@rolldown/binding-linux-arm64-musl@1.1.2':
|
||||||
resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==}
|
resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.1.2':
|
'@rolldown/binding-linux-ppc64-gnu@1.1.2':
|
||||||
resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==}
|
resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.1.2':
|
'@rolldown/binding-linux-s390x-gnu@1.1.2':
|
||||||
resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==}
|
resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.1.2':
|
'@rolldown/binding-linux-x64-gnu@1.1.2':
|
||||||
resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==}
|
resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.1.2':
|
'@rolldown/binding-linux-x64-musl@1.1.2':
|
||||||
resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==}
|
resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.1.2':
|
'@rolldown/binding-openharmony-arm64@1.1.2':
|
||||||
resolution: {integrity: sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==}
|
resolution: {integrity: sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==}
|
||||||
@@ -934,79 +928,66 @@ packages:
|
|||||||
resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==}
|
resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.62.2':
|
'@rollup/rollup-linux-arm-musleabihf@4.62.2':
|
||||||
resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==}
|
resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.62.2':
|
'@rollup/rollup-linux-arm64-gnu@4.62.2':
|
||||||
resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==}
|
resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.62.2':
|
'@rollup/rollup-linux-arm64-musl@4.62.2':
|
||||||
resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==}
|
resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.62.2':
|
'@rollup/rollup-linux-loong64-gnu@4.62.2':
|
||||||
resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==}
|
resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.62.2':
|
'@rollup/rollup-linux-loong64-musl@4.62.2':
|
||||||
resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==}
|
resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.62.2':
|
'@rollup/rollup-linux-ppc64-gnu@4.62.2':
|
||||||
resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==}
|
resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.62.2':
|
'@rollup/rollup-linux-ppc64-musl@4.62.2':
|
||||||
resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==}
|
resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.62.2':
|
'@rollup/rollup-linux-riscv64-gnu@4.62.2':
|
||||||
resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==}
|
resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.62.2':
|
'@rollup/rollup-linux-riscv64-musl@4.62.2':
|
||||||
resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==}
|
resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.62.2':
|
'@rollup/rollup-linux-s390x-gnu@4.62.2':
|
||||||
resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==}
|
resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.62.2':
|
'@rollup/rollup-linux-x64-gnu@4.62.2':
|
||||||
resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==}
|
resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.62.2':
|
'@rollup/rollup-linux-x64-musl@4.62.2':
|
||||||
resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==}
|
resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.62.2':
|
'@rollup/rollup-openbsd-x64@4.62.2':
|
||||||
resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==}
|
resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==}
|
||||||
@@ -1079,28 +1060,24 @@ packages:
|
|||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.3.1':
|
'@tailwindcss/oxide-linux-arm64-musl@4.3.1':
|
||||||
resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==}
|
resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.3.1':
|
'@tailwindcss/oxide-linux-x64-gnu@4.3.1':
|
||||||
resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==}
|
resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.3.1':
|
'@tailwindcss/oxide-linux-x64-musl@4.3.1':
|
||||||
resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==}
|
resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.3.1':
|
'@tailwindcss/oxide-wasm32-wasi@4.3.1':
|
||||||
resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==}
|
resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==}
|
||||||
@@ -1289,6 +1266,7 @@ packages:
|
|||||||
'@xmldom/xmldom@0.8.13':
|
'@xmldom/xmldom@0.8.13':
|
||||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
deprecated: this version has critical issues, please update to the latest version
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
@@ -2079,28 +2057,24 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.32.0:
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.32.0:
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.32.0:
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.32.0:
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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개를 만들었습니다.");
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
2026-10-03
|
||||||
|
|
||||||
|
- 업로드 완료 뒤 폼 초기화에서 발생하던 null 참조 오류를 수정했다.
|
||||||
|
- 곡 보관함 업로드를 MIDI와 MusicXML과 MXL 및 PDF로 확장했다.
|
||||||
|
- MusicXML 구조와 MXL 압축 경로 및 해제 용량과 PDF 파일 서명을 검사하도록 했다.
|
||||||
|
- 파일 종류에 맞는 확장자와 MIME으로 저장하고 다시 불러오도록 수정했다.
|
||||||
|
- AlphaTab 악보용 Bravura 글꼴과 전용 모듈 자산을 배포했다.
|
||||||
|
- Nginx에서 mjs 파일을 JavaScript MIME으로 제공하도록 수정했다.
|
||||||
|
- 운영 악보 렌더링을 UI 스레드로 고정해 빈 악보 문제를 해결했다.
|
||||||
|
- 임시 음형 샘플 4개를 제거했다.
|
||||||
|
- Mutopia Project 공개 도메인 클래식 3곡과 단계별 편곡을 합쳐 7개 버전으로 등록했다.
|
||||||
|
- 엘리제를 위하여는 입문 양손과 쉬운 양손 및 고급 원곡을 같은 제목 아래에 표시한다.
|
||||||
|
- 단계별 공개 샘플은 모두 오른손과 왼손을 포함한 피아노 독주로 통일했다. 손별 연습은 연습 화면에서 선택한다.
|
||||||
|
- 서버 갱신 중 일시적인 조회 실패가 발생하면 한 번 재시도한다. 정상 응답 뒤에는 오래된 연결 오류를 지운다.
|
||||||
|
- 왼쪽 위 이름과 브라우저 제목을 ITOSM 피아노 학습실로 바꾸고 전용 음표 표식을 적용했다.
|
||||||
|
- 서비스 표식과 설정 화면의 저장소 연결을 ITOSM 피아노 학습실 Gitea 저장소로 변경했다.
|
||||||
|
- MIDI에서 변환한 악보가 빠른 플레이어 준비 이벤트 누락과 음소거된 내부 플레이어의 위치 이벤트 누락 때문에 첫 마디에 머무르던 문제를 수정했다. 악보 시간과 AlphaTab tick을 함께 동기화하고 AlphaTab 1.8.3을 고정했다.
|
||||||
|
- 연속된 고음과 저음 구간에 표준 8va와 8vb 옥타브 기호를 자동 적용해 덧줄을 줄였다. 한 음짜리 짧은 구간에는 기호를 남발하지 않으며 실제 재생 음높이는 유지한다.
|
||||||
|
- 기존 회원 업로드 20360.mid가 유지되는 것을 확인했다.
|
||||||
|
- 프런트 타입 검사와 67개 테스트와 운영 빌드를 통과했다.
|
||||||
|
- 운영 백엔드에서 MIDI와 MusicXML과 MXL 및 PDF 검증을 통과했다.
|
||||||
|
- 운영 브라우저에서 난이도별 묶음과 미뉴에트 및 엘리제를 위하여 악보 표시를 확인했다.
|
||||||
|
- 작업 전 백업은 /home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload에 저장했다.
|
||||||
Reference in New Issue
Block a user