Update midi editor
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
export type Lang = 'zh' | 'en'
|
||||
|
||||
const zh = {
|
||||
// Header
|
||||
eyebrow: '歌声 MIDI 编辑器',
|
||||
title: 'SoulX-Singer MIDI Editor',
|
||||
subtitle: '导入、拖拽、实时修改歌词并导出标准 MIDI。',
|
||||
switchToLight: '切换到亮色',
|
||||
switchToDark: '切换到暗色',
|
||||
importJson: '导入 JSON',
|
||||
exportJson: '导出 JSON',
|
||||
importMidi: '导入 MIDI',
|
||||
exportMidi: '导出 MIDI',
|
||||
transpose: '移调',
|
||||
transposeTooltip: '整体升降调:所有音符的音高同步改变',
|
||||
transposed: (n: number) => `已移调 ${n > 0 ? '+' : ''}${n} 半音`,
|
||||
fixOverlaps: '消除重叠',
|
||||
fixOverlapsTooltip: '自动消除重叠:将重叠音符的音尾提前到下一个音的音头',
|
||||
jsonImported: (name: string) => `已从 JSON 载入 ${name}`,
|
||||
jsonImportFailed: 'JSON 导入失败,请确认文件格式正确',
|
||||
jsonExported: '已导出 META JSON 文件',
|
||||
|
||||
// Audio bar
|
||||
importAudio: '对齐音频导入',
|
||||
audioHint: '导入后显示音频波形并与 MIDI 同步走带',
|
||||
midiLabel: 'MIDI',
|
||||
audioLabel: '音频',
|
||||
|
||||
// Controls
|
||||
horizontalZoom: '水平缩放',
|
||||
verticalZoom: '垂直缩放',
|
||||
goToStart: '回到开头',
|
||||
back2s: '后退 2 秒',
|
||||
pause: '暂停',
|
||||
playSelection: '播放选区',
|
||||
play: '播放',
|
||||
forward2s: '前进 2 秒',
|
||||
goToEnd: '回到结尾',
|
||||
selectingRange: '选区中',
|
||||
setRange: '设选区',
|
||||
exitSelectMode: '退出选区模式(并清除选区)',
|
||||
setRangeTooltip: '设置选区:在时间轴上拖拽选择播放范围',
|
||||
|
||||
// Status
|
||||
ready: '准备就绪',
|
||||
selectionPlayback: '选区回放中...',
|
||||
playing: '正在回放...',
|
||||
selectionDone: '选区播放完毕',
|
||||
paused: '已暂停',
|
||||
imported: (name: string) => `已载入 ${name}`,
|
||||
importFailed: '导入失败,请确认文件合法',
|
||||
audioImported: (name: string) => `已载入音频 ${name}`,
|
||||
unsupportedFormat: (exts: string) => `不支持的文件格式,请选择音频文件(${exts})`,
|
||||
fixedOverlaps: (count: number) => `已修复 ${count} 个重叠音符`,
|
||||
noOverlaps: '没有检测到重叠音符',
|
||||
exported: '已导出包含歌词的 MIDI 文件',
|
||||
|
||||
// Lyric table
|
||||
fillPlaceholderSelected: '从选中音符开始按词/字填充',
|
||||
fillPlaceholderDefault: '输入歌词,点击按词/字填充',
|
||||
fillButton: '按词\n填充',
|
||||
lyricPlaceholder: '输入歌词',
|
||||
emptyHint: '导入或双击钢琴卷帘以添加音符',
|
||||
confirmEdit: '确认修改 (Enter)',
|
||||
}
|
||||
|
||||
const en: typeof zh = {
|
||||
// Header
|
||||
eyebrow: 'Vocal MIDI Editor',
|
||||
title: 'SoulX-Singer MIDI Editor',
|
||||
subtitle: 'Import, drag, edit lyrics in real-time, and export standard MIDI.',
|
||||
switchToLight: 'Switch to light',
|
||||
switchToDark: 'Switch to dark',
|
||||
importJson: 'Import JSON',
|
||||
exportJson: 'Export JSON',
|
||||
importMidi: 'Import MIDI',
|
||||
exportMidi: 'Export MIDI',
|
||||
transpose: 'Transpose',
|
||||
transposeTooltip: 'Transpose all notes up or down by semitones',
|
||||
transposed: (n: number) => `Transposed ${n > 0 ? '+' : ''}${n} semitone(s)`,
|
||||
fixOverlaps: 'Fix Overlaps',
|
||||
fixOverlapsTooltip: 'Auto fix overlaps: trim note end to the start of the next note',
|
||||
jsonImported: (name: string) => `Loaded from JSON ${name}`,
|
||||
jsonImportFailed: 'JSON import failed, please check the file format',
|
||||
jsonExported: 'Exported META JSON file',
|
||||
|
||||
// Audio bar
|
||||
importAudio: 'Import Audio',
|
||||
audioHint: 'Display audio waveform synced with MIDI transport',
|
||||
midiLabel: 'MIDI',
|
||||
audioLabel: 'Audio',
|
||||
|
||||
// Controls
|
||||
horizontalZoom: 'H-Zoom',
|
||||
verticalZoom: 'V-Zoom',
|
||||
goToStart: 'Go to start',
|
||||
back2s: 'Back 2s',
|
||||
pause: 'Pause',
|
||||
playSelection: 'Play selection',
|
||||
play: 'Play',
|
||||
forward2s: 'Forward 2s',
|
||||
goToEnd: 'Go to end',
|
||||
selectingRange: 'Selecting',
|
||||
setRange: 'Select',
|
||||
exitSelectMode: 'Exit selection mode (and clear selection)',
|
||||
setRangeTooltip: 'Set selection: drag on the timeline to select playback range',
|
||||
|
||||
// Status
|
||||
ready: 'Ready',
|
||||
selectionPlayback: 'Playing selection...',
|
||||
playing: 'Playing...',
|
||||
selectionDone: 'Selection playback done',
|
||||
paused: 'Paused',
|
||||
imported: (name: string) => `Loaded ${name}`,
|
||||
importFailed: 'Import failed, please check the file',
|
||||
audioImported: (name: string) => `Loaded audio ${name}`,
|
||||
unsupportedFormat: (exts: string) => `Unsupported format, please select an audio file (${exts})`,
|
||||
fixedOverlaps: (count: number) => `Fixed ${count} overlapping note(s)`,
|
||||
noOverlaps: 'No overlapping notes detected',
|
||||
exported: 'Exported MIDI file with lyrics',
|
||||
|
||||
// Lyric table
|
||||
fillPlaceholderSelected: 'Fill words from selected note',
|
||||
fillPlaceholderDefault: 'Enter lyrics, click fill button',
|
||||
fillButton: 'Fill\nWords',
|
||||
lyricPlaceholder: 'Type lyric',
|
||||
emptyHint: 'Import or double-click piano roll to add notes',
|
||||
confirmEdit: 'Confirm (Enter)',
|
||||
}
|
||||
|
||||
const translations: Record<Lang, typeof zh> = { zh, en }
|
||||
|
||||
export type Translations = typeof zh
|
||||
|
||||
export function getTranslations(lang: Lang): Translations {
|
||||
return translations[lang]
|
||||
}
|
||||
|
||||
// Smart tokenizer for lyrics: CJK characters are individual tokens, Latin words are grouped
|
||||
function isCJK(char: string): boolean {
|
||||
const code = char.codePointAt(0) || 0
|
||||
return (
|
||||
(code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified Ideographs
|
||||
(code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A
|
||||
(code >= 0x20000 && code <= 0x2A6DF) || // CJK Extension B
|
||||
(code >= 0x3040 && code <= 0x309F) || // Hiragana
|
||||
(code >= 0x30A0 && code <= 0x30FF) || // Katakana
|
||||
(code >= 0xAC00 && code <= 0xD7AF) // Hangul Syllables
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize lyrics text for note filling.
|
||||
* - CJK characters: each character becomes one token (one per note)
|
||||
* - Latin/English words: each space-separated word becomes one token (one per note)
|
||||
* - Mixed text is handled correctly
|
||||
*
|
||||
* Examples:
|
||||
* "你好世界" -> ["你", "好", "世", "界"]
|
||||
* "hello world" -> ["hello", "world"]
|
||||
* "I love 你" -> ["I", "love", "你"]
|
||||
* "something wrong" -> ["something", "wrong"]
|
||||
*/
|
||||
export function tokenizeLyrics(text: string): string[] {
|
||||
const tokens: string[] = []
|
||||
const cleaned = text.trim()
|
||||
if (!cleaned) return tokens
|
||||
|
||||
let i = 0
|
||||
while (i < cleaned.length) {
|
||||
const char = cleaned[i]
|
||||
|
||||
// Skip whitespace
|
||||
if (/\s/.test(char)) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// CJK character - each is a separate token
|
||||
if (isCJK(char)) {
|
||||
tokens.push(char)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Latin/number/other - collect until whitespace or CJK
|
||||
let word = ''
|
||||
while (i < cleaned.length && !/\s/.test(cleaned[i]) && !isCJK(cleaned[i])) {
|
||||
word += cleaned[i]
|
||||
i++
|
||||
}
|
||||
if (word) tokens.push(word)
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
Reference in New Issue
Block a user