Skip to content

Commit 10e5b64

Browse files
romot-coRomotHiroshiba
authored
#1818 ソング:MIDIトラックを選択できるようにする (#1892)
* #1818 MIDIトラックを選択できる ようにする * #1818 調整 * #1818 リファクタ * #1818 不要ファイル削除 * #1818 指摘点修正 * Update src/store/singing.ts Co-authored-by: Hiroshiba <[email protected]> * #1818 指摘点修正 --------- Co-authored-by: Romot <[email protected]> Co-authored-by: Hiroshiba <[email protected]>
1 parent 05926fb commit 10e5b64

File tree

6 files changed

+193
-15
lines changed

6 files changed

+193
-15
lines changed

src/components/Dialog/AllDialog.vue

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<UpdateNotificationDialogContainer
2323
:can-open-dialog="canOpenNotificationDialog"
2424
/>
25+
<ImportMidiDialog v-model="isImportMidiDialogOpenComputed" />
2526
</template>
2627

2728
<script setup lang="ts">
@@ -37,6 +38,7 @@ import AcceptTermsDialog from "@/components/Dialog/AcceptTermsDialog.vue";
3738
import DictionaryManageDialog from "@/components/Dialog/DictionaryManageDialog.vue";
3839
import EngineManageDialog from "@/components/Dialog/EngineManageDialog.vue";
3940
import UpdateNotificationDialogContainer from "@/components/Dialog/UpdateNotificationDialog/Container.vue";
41+
import ImportMidiDialog from "@/components/Dialog/ImportMidiDialog.vue";
4042
import { useStore } from "@/store";
4143
import { filterCharacterInfosByStyleType } from "@/store/utility";
4244
@@ -158,4 +160,13 @@ const canOpenNotificationDialog = computed(() => {
158160
props.isEnginesReady
159161
);
160162
});
163+
164+
// MIDIインポート時の設定ダイアログ
165+
const isImportMidiDialogOpenComputed = computed({
166+
get: () => store.state.isImportMidiDialogOpen,
167+
set: (val) =>
168+
store.dispatch("SET_DIALOG_OPEN", {
169+
isImportMidiDialogOpen: val,
170+
}),
171+
});
161172
</script>
+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<template>
2+
<QDialog ref="dialogRef" auto-scroll @before-show="initializeValues">
3+
<QLayout container view="hHh lpr fFf" class="q-dialog-plugin bg-background">
4+
<QHeader>
5+
<QToolbar>
6+
<QToolbarTitle>MIDIファイルのインポート</QToolbarTitle>
7+
</QToolbar>
8+
</QHeader>
9+
<QPageContainer class="q-px-lg q-py-md">
10+
<QFile
11+
v-model="midiFile"
12+
label="インポートするMIDIファイル"
13+
class="q-my-sm"
14+
accept=".mid,.midi"
15+
:error-message="midiFileError"
16+
:error="!!midiFileError"
17+
placeholder="MIDIファイルを選択してください"
18+
@input="handleMidiFileChange"
19+
/>
20+
<QSelect
21+
v-if="midi"
22+
v-model="selectedTrack"
23+
:options="tracks"
24+
emit-value
25+
map-options
26+
label="インポートするトラック"
27+
/>
28+
</QPageContainer>
29+
<QFooter>
30+
<QToolbar>
31+
<QSpace />
32+
<QBtn
33+
unelevated
34+
align="right"
35+
label="キャンセル"
36+
color="toolbar-button"
37+
text-color="toolbar-button-display"
38+
class="text-no-wrap text-bold q-mr-sm"
39+
@click="handleCancel"
40+
/>
41+
<QBtn
42+
unelevated
43+
align="right"
44+
label="インポート"
45+
color="toolbar-button"
46+
text-color="toolbar-button-display"
47+
class="text-no-wrap text-bold q-mr-sm"
48+
:disabled="selectedTrack === null"
49+
@click="handleImportTrack"
50+
/>
51+
</QToolbar>
52+
</QFooter>
53+
</QLayout>
54+
</QDialog>
55+
</template>
56+
57+
<script setup lang="ts">
58+
import { computed, ref } from "vue";
59+
import { useDialogPluginComponent } from "quasar";
60+
import { Midi } from "@tonejs/midi";
61+
import { useStore } from "@/store";
62+
63+
const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
64+
65+
const store = useStore();
66+
67+
// MIDIファイル
68+
const midiFile = ref<File | null>(null);
69+
70+
// MIDIファイルエラー
71+
const midiFileError = computed(() => {
72+
if (midiFile.value && !midi.value) {
73+
return "MIDIファイルの読み込みに失敗しました";
74+
} else if (midiFile.value && midi.value && !midi.value.tracks.length) {
75+
return "トラックがありません";
76+
}
77+
return undefined;
78+
});
79+
// MIDIデータ(tone.jsでパースしたもの)
80+
const midi = ref<Midi | null>(null);
81+
// トラック
82+
const tracks = computed(() => {
83+
if (!midi.value) {
84+
return [];
85+
}
86+
// トラックリストを生成
87+
// トラックNo: トラック名 形式
88+
return midi.value.tracks.map((track, index) => ({
89+
label: `${index + 1}: ${track.name}`,
90+
value: index,
91+
}));
92+
});
93+
// 選択中のトラック
94+
const selectedTrack = ref<string | number | null>(null);
95+
96+
// データ初期化
97+
const initializeValues = () => {
98+
midiFile.value = null;
99+
midi.value = null;
100+
selectedTrack.value = null;
101+
};
102+
103+
// MIDIファイル変更時
104+
const handleMidiFileChange = (event: Event) => {
105+
if (!(event.target instanceof HTMLInputElement)) {
106+
throw new Error("Event target is not an HTMLInputElement");
107+
}
108+
109+
const input = event.target;
110+
111+
// 入力ファイルが存在しない場合はエラー
112+
if (!input.files || input.files.length === 0) {
113+
throw new Error("No file selected");
114+
}
115+
116+
// 既存のMIDIデータおよび選択中のトラックをクリア
117+
midi.value = null;
118+
selectedTrack.value = null;
119+
120+
const file = input.files[0];
121+
const reader = new FileReader();
122+
reader.onload = (e) => {
123+
try {
124+
if (
125+
e.target &&
126+
e.target.result &&
127+
e.target.result instanceof ArrayBuffer
128+
) {
129+
// MIDIファイルをパース
130+
midi.value = new Midi(e.target.result);
131+
const DEFAULT_TRACK = 0;
132+
selectedTrack.value = DEFAULT_TRACK;
133+
} else {
134+
throw new Error("Could not find MIDI file data");
135+
}
136+
} catch (error) {
137+
throw new Error("Failed to parse MIDI file", { cause: error });
138+
}
139+
};
140+
reader.onerror = () => {
141+
throw new Error("Failed to read MIDI file");
142+
};
143+
144+
// MIDIファイルをバイナリデータとして読み込む
145+
reader.readAsArrayBuffer(file);
146+
};
147+
148+
// トラックインポート実行時
149+
const handleImportTrack = () => {
150+
// MIDIファイルまたは選択中のトラックが未設定の場合はエラー
151+
if (midiFile.value == null || typeof selectedTrack.value !== "number") {
152+
throw new Error("MIDI file or selected track is not set");
153+
}
154+
// トラックをインポート
155+
store.dispatch("IMPORT_MIDI_FILE", {
156+
filePath: midiFile.value.path,
157+
trackIndex: selectedTrack.value,
158+
});
159+
onDialogOK();
160+
};
161+
162+
// キャンセルボタンクリック時
163+
const handleCancel = () => {
164+
onDialogCancel();
165+
};
166+
</script>

src/components/Sing/MenuBar.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const uiLocked = computed(() => store.getters.UI_LOCKED);
1313
1414
const importMidiFile = async () => {
1515
if (uiLocked.value) return;
16-
await store.dispatch("IMPORT_MIDI_FILE", {});
16+
await store.dispatch("SET_DIALOG_OPEN", {
17+
isImportMidiDialogOpen: true,
18+
});
1719
};
1820
1921
const importMusicXMLFile = async () => {

src/store/singing.ts

+7-13
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,10 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
12201220

12211221
IMPORT_MIDI_FILE: {
12221222
action: createUILockAction(
1223-
async ({ dispatch }, { filePath }: { filePath?: string }) => {
1223+
async (
1224+
{ dispatch },
1225+
{ filePath, trackIndex = 0 }: { filePath: string; trackIndex: number }
1226+
) => {
12241227
const convertPosition = (
12251228
position: number,
12261229
sourceTpqn: number,
@@ -1289,25 +1292,16 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
12891292
});
12901293
};
12911294

1292-
if (!filePath) {
1293-
filePath = await window.backend.showImportFileDialog({
1294-
title: "MIDI読み込み",
1295-
name: "MIDI",
1296-
extensions: ["mid", "midi"],
1297-
});
1298-
if (!filePath) return;
1299-
}
1300-
1295+
// NOTE: トラック選択のために一度ファイルを読み込んでいるので、Midiを渡すなどでもよさそう
13011296
const midiData = getValueOrThrow(
13021297
await window.backend.readFile({ filePath })
13031298
);
13041299
const midi = new Midi(midiData);
1305-
13061300
const midiTpqn = midi.header.ppq;
13071301
const midiTempos = [...midi.header.tempos];
13081302
const midiTimeSignatures = [...midi.header.timeSignatures];
1309-
// TODO: UIで読み込むトラックを選択できるようにする
1310-
const midiNotes = [...midi.tracks[0].notes]; // ひとまず1トラック目のみを読み込む
1303+
1304+
const midiNotes = [...midi.tracks[trackIndex].notes];
13111305

13121306
midiTempos.sort((a, b) => a.ticks - b.ticks);
13131307
midiTimeSignatures.sort((a, b) => a.ticks - b.ticks);

src/store/type.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ export type SingingStoreTypes = {
923923
};
924924

925925
IMPORT_MIDI_FILE: {
926-
action(payload: { filePath?: string }): void;
926+
action(payload: { filePath: string; trackIndex: number }): void;
927927
};
928928

929929
IMPORT_MUSICXML_FILE: {
@@ -1521,6 +1521,7 @@ export type UiStoreState = {
15211521
isDictionaryManageDialogOpen: boolean;
15221522
isEngineManageDialogOpen: boolean;
15231523
isUpdateNotificationDialogOpen: boolean;
1524+
isImportMidiDialogOpen: boolean;
15241525
isMaximized: boolean;
15251526
isPinned: boolean;
15261527
isFullscreen: boolean;
@@ -1592,6 +1593,7 @@ export type UiStoreTypes = {
15921593
isCharacterOrderDialogOpen?: boolean;
15931594
isEngineManageDialogOpen?: boolean;
15941595
isUpdateNotificationDialogOpen?: boolean;
1596+
isImportMidiDialogOpen?: boolean;
15951597
};
15961598
action(payload: {
15971599
isDefaultStyleSelectDialogOpen?: boolean;
@@ -1605,6 +1607,7 @@ export type UiStoreTypes = {
16051607
isCharacterOrderDialogOpen?: boolean;
16061608
isEngineManageDialogOpen?: boolean;
16071609
isUpdateNotificationDialogOpen?: boolean;
1610+
isImportMidiDialogOpen?: boolean;
16081611
}): void;
16091612
};
16101613

src/store/ui.ts

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const uiStoreState: UiStoreState = {
6262
isDictionaryManageDialogOpen: false,
6363
isEngineManageDialogOpen: false,
6464
isUpdateNotificationDialogOpen: false,
65+
isImportMidiDialogOpen: false,
6566
isMaximized: false,
6667
isPinned: false,
6768
isFullscreen: false,
@@ -181,6 +182,7 @@ export const uiStore = createPartialStore<UiStoreTypes>({
181182
isCharacterOrderDialogOpen?: boolean;
182183
isEngineManageDialogOpen?: boolean;
183184
isUpdateNotificationDialogOpen?: boolean;
185+
isImportMidiDialogOpen?: boolean;
184186
}
185187
) {
186188
for (const [key, value] of Object.entries(dialogState)) {

0 commit comments

Comments
 (0)