From 86d2e8ebd7c8bfbac45981559a4d6f7f1cb27b4a Mon Sep 17 00:00:00 2001 From: Romot Date: Thu, 21 Mar 2024 07:51:53 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=BD=E3=83=B3=E3=82=B0:=20UST=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE=E3=82=A4=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=88=E6=A9=9F=E8=83=BD=E3=81=AE=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20(#1933)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #1908 USTファイルを一応読める試行実装 * #1908 USTインポート修正 * #1908 調整[update snapshots] * Update src/store/singing.ts Co-authored-by: Hiroshiba * Update src/store/singing.ts Co-authored-by: Hiroshiba * エラー内容を修正 --------- Co-authored-by: Romot Co-authored-by: Hiroshiba --- src/components/Sing/MenuBar.vue | 13 ++++ src/store/singing.ts | 121 ++++++++++++++++++++++++++++++++ src/store/type.ts | 4 ++ 3 files changed, 138 insertions(+) diff --git a/src/components/Sing/MenuBar.vue b/src/components/Sing/MenuBar.vue index f9a1802ec1..a3cabf012d 100644 --- a/src/components/Sing/MenuBar.vue +++ b/src/components/Sing/MenuBar.vue @@ -23,6 +23,11 @@ const importMusicXMLFile = async () => { await store.dispatch("IMPORT_MUSICXML_FILE", {}); }; +const importUstFile = async () => { + if (uiLocked.value) return; + await store.dispatch("IMPORT_UST_FILE", {}); +}; + const exportWaveFile = async () => { if (uiLocked.value) return; await store.dispatch("EXPORT_WAVE_FILE", {}); @@ -54,5 +59,13 @@ const fileSubMenuData: MenuItemData[] = [ }, disableWhenUiLocked: true, }, + { + type: "button", + label: "UST読み込み", + onClick: () => { + importUstFile(); + }, + disableWhenUiLocked: true, + }, ]; diff --git a/src/store/singing.ts b/src/store/singing.ts index f1bfe8f60d..cecafe43f2 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -1699,6 +1699,127 @@ export const singingStore = createPartialStore({ ), }, + IMPORT_UST_FILE: { + action: createUILockAction( + async ({ dispatch }, { filePath }: { filePath?: string }) => { + // USTファイルの読み込み + if (!filePath) { + filePath = await window.backend.showImportFileDialog({ + title: "UST読み込み", + name: "UST", + extensions: ["ust"], + }); + if (!filePath) return; + } + // ファイルの読み込み + const fileData = getValueOrThrow( + await window.backend.readFile({ filePath }) + ); + + // ファイルフォーマットに応じてエンコーディングを変える + // UTF-8とShiftJISの2種類に対応 + let ustData; + try { + ustData = new TextDecoder("utf-8").decode(fileData); + // ShiftJISの場合はShiftJISでデコードし直す + if (ustData.includes("\ufffd")) { + ustData = new TextDecoder("shift-jis").decode(fileData); + } + } catch (error) { + throw new Error("Failed to decode UST file.", { cause: error }); + } + if (!ustData || typeof ustData !== "string") { + throw new Error("Failed to decode UST file."); + } + + // 初期化 + const tpqn = DEFAULT_TPQN; + const tempos: Tempo[] = [ + { + position: 0, + bpm: DEFAULT_BPM, + }, + ]; + const timeSignatures: TimeSignature[] = [ + { + measureNumber: 1, + beats: DEFAULT_BEATS, + beatType: DEFAULT_BEAT_TYPE, + }, + ]; + const notes: Note[] = []; + + // USTファイルのセクションをパース + const parseSection = (section: string): { [key: string]: string } => { + const sectionNameMatch = section.match(/\[(.+)\]/); + if (!sectionNameMatch) { + throw new Error("UST section name not found"); + } + const params = section.split(/[\r\n]+/).reduce((acc, line) => { + const [key, value] = line.split("="); + if (key && value) { + acc[key] = value; + } + return acc; + }, {} as { [key: string]: string }); + return { + ...params, + sectionName: sectionNameMatch[1], + }; + }; + + // セクションを分割 + const sections = ustData.split(/^(?=\[)/m); + // ポジション + let position = 0; + // セクションごとに処理 + sections.forEach((section) => { + const params = parseSection(section); + // SETTINGセクション + if (params.sectionName === "#SETTING") { + const tempo = Number(params["Tempo"]); + if (tempo) tempos[0].bpm = tempo; + } + // ノートセクション + if (params.sectionName.match(/^#\d{4}/)) { + // テンポ変更があれば追加 + const tempo = Number(params["Tempo"]); + if (tempo) tempos.push({ position, bpm: tempo }); + const noteNumber = Number(params["NoteNum"]); + const duration = Number(params["Length"]); + // 歌詞の前に連続音が含まれている場合は除去 + const lyric = params["Lyric"].includes(" ") + ? params["Lyric"].split(" ")[1] + : params["Lyric"]; + // 休符であればポジションを進めるのみ + if (lyric === "R") { + position += duration; + } else { + // それ以外の場合はノートを追加 + notes.push({ + id: uuidv4(), + position, + duration, + noteNumber, + lyric, + }); + position += duration; + } + } + }); + + await dispatch("SET_SCORE", { + score: { + tpqn, + tempos, + timeSignatures, + notes, + }, + }); + } + ), + }, + SET_NOW_AUDIO_EXPORTING: { mutation(state, { nowAudioExporting }) { state.nowAudioExporting = nowAudioExporting; diff --git a/src/store/type.ts b/src/store/type.ts index ebf1bac765..8073810ff3 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -930,6 +930,10 @@ export type SingingStoreTypes = { action(payload: { filePath?: string }): void; }; + IMPORT_UST_FILE: { + action(payload: { filePath?: string }): void; + }; + EXPORT_WAVE_FILE: { action(payload: { filePath?: string }): SaveResultObject; };