Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

project-s: テンポ・拍子の表示と設定を行う機能を追加 #1025

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 122 additions & 10 deletions src/components/Sing/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,46 @@
<button type="button" class="sing-button-temp">戻る</button>
<button type="button" class="sing-button-temp">再生</button>
<div class="sing-player-position">00:00</div>
<input type="number" value="120" class="sing-bpm" />
<input type="number" value="4" class="sing-tempo" />/
<input type="number" value="4" class="sing-tempo" />
<q-input
type="number"
:model-value="tempoInputBuffer"
dense
hide-bottom-space
class="sing-tempo"
@update:model-value="setTempoInputBuffer"
@change="setTempo()"
>
<template v-slot:prepend>
<div />
</template>
</q-input>
<q-input
type="number"
:model-value="beatsInputBuffer"
sigprogramming marked this conversation as resolved.
Show resolved Hide resolved
dense
hide-bottom-space
class="sing-time-signature"
@update:model-value="setBeatsInputBuffer"
@change="setTimeSignature()"
>
<template v-slot:prepend>
<div />
</template>
</q-input>
/
<q-input
type="number"
:model-value="beatTypeInputBuffer"
dense
hide-bottom-space
class="sing-time-signature"
@update:model-value="setBeatTypeInputBuffer"
@change="setTimeSignature()"
>
<template v-slot:prepend>
<div />
</template>
</q-input>
</div>
<div class="sing-setting">
<input type="range" min="0" max="100" class="sing-volume" />
Expand All @@ -25,7 +62,7 @@
</template>

<script lang="ts">
import { defineComponent, computed } from "vue";
import { defineComponent, computed, watch, ref } from "vue";
import { useStore } from "@/store";

export default defineComponent({
Expand Down Expand Up @@ -62,12 +99,87 @@ export default defineComponent({
)?.iconPath
);

const tempoInputBuffer = ref(0);
const beatsInputBuffer = ref(0);
const beatTypeInputBuffer = ref(0);

const setTempoInputBuffer = (tempoStr: string) => {
const tempo = Number(tempoStr);
if (Number.isNaN(tempo) || tempo <= 0) return;
tempoInputBuffer.value = tempo;
};
const setBeatsInputBuffer = (beatsStr: string) => {
const beats = Number(beatsStr);
if (!Number.isInteger(beats) || beats <= 0) return;
beatsInputBuffer.value = beats;
};
const setBeatTypeInputBuffer = (beatTypeStr: string) => {
const beatType = Number(beatTypeStr);
if (!Number.isInteger(beatType) || beatType <= 0) return;
beatTypeInputBuffer.value = beatType;
};

const tempos = computed(() => store.state.score?.tempos);
const timeSignatures = computed(() => store.state.score?.timeSignatures);

watch(
tempos,
() => {
tempoInputBuffer.value = tempos.value?.[0].tempo ?? 0;
},
{ deep: true }
);
watch(
timeSignatures,
() => {
beatsInputBuffer.value = timeSignatures.value?.[0].beats ?? 0;
},
{ deep: true }
);
watch(
timeSignatures,
() => {
beatTypeInputBuffer.value = timeSignatures.value?.[0].beatType ?? 0;
},
{ deep: true }
);

const setTempo = async () => {
const tempo = tempoInputBuffer.value;
if (tempo === 0) return;
await store.dispatch("SET_TEMPO", {
tempo: {
position: 0,
tempo: tempo,
},
});
};

const setTimeSignature = async () => {
const beats = beatsInputBuffer.value;
const beatType = beatTypeInputBuffer.value;
if (beats === 0 || beatType === 0) return;
await store.dispatch("SET_TIME_SIGNATURE", {
timeSignature: {
position: 0,
beats: beats,
beatType: beatType,
},
});
};

return {
isShowSinger,
toggleShowSinger,
userOrderedCharacterInfos,
selectedCharacterInfo,
selectedStyleIconPath,
tempoInputBuffer,
beatsInputBuffer,
beatTypeInputBuffer,
setTempoInputBuffer,
setBeatsInputBuffer,
setBeatTypeInputBuffer,
setTempo,
setTimeSignature,
};
},
});
Expand Down Expand Up @@ -120,14 +232,14 @@ export default defineComponent({
margin: 0 4px;
}

.sing-bpm {
.sing-tempo {
margin: 0 4px;
width: 56px;
width: 64px;
}

.sing-tempo {
.sing-time-signature {
margin: 0 4px;
width: 32px;
width: 36px;
}

.sing-player-position {
Expand Down
160 changes: 159 additions & 1 deletion src/store/singing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Note, Score, SingingStoreState, SingingStoreTypes } from "./type";
import {
Score,
Tempo,
TimeSignature,
Note,
SingingStoreState,
SingingStoreTypes,
} from "./type";
import { createPartialStore } from "./vuex";
import { createUILockAction } from "./ui";
import { Midi } from "@tonejs/midi";
Expand Down Expand Up @@ -80,6 +87,17 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
timeSignatures: [{ position: 0, beats: 4, beatType: 4 }],
notes: [],
};
if (score.tempos.length !== 1 || score.tempos[0].position !== 0) {
throw new Error("Tempo does not exist at the beginning of the score.");
}
if (
score.timeSignatures.length !== 1 ||
score.timeSignatures[0].position !== 0
) {
throw new Error(
"Time signature does not exist at the beginning of the score."
);
}
return score;
},
},
Expand All @@ -93,6 +111,146 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
},
},

SET_TEMPO: {
mutation(state, { index, tempo }: { index: number; tempo: Tempo }) {
state.score?.tempos.splice(index, 0, tempo);
},
// テンポを設定する。既に同じ位置にテンポが存在する場合は置き換える。
async action({ state, commit }, { tempo }: { tempo: Tempo }) {
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
const score = state.score;
if (score === undefined || score.tempos.length === 0) {
throw new Error("Score is not initialized.");
}
if (
Number.isNaN(tempo.position) ||
tempo.position < 0 ||
Number.isNaN(tempo.tempo) ||
tempo.tempo <= 0
) {
throw new Error("The value is invalid.");
}
const duplicate = score.tempos.some((value) => {
return value.position === tempo.position;
});
const index = score.tempos.findIndex((value) => {
return value.position >= tempo.position;
});
if (index === -1) return;

const round = (value: number, digits: number) => {
const powerOf10 = 10 ** digits;
return Math.round(value * powerOf10) / powerOf10;
};

tempo.tempo = round(tempo.tempo, 2);

if (duplicate) {
commit("REMOVE_TEMPO", { index });
}
commit("SET_TEMPO", { index, tempo });
},
},

REMOVE_TEMPO: {
mutation(state, { index }: { index: number }) {
state.score?.tempos.splice(index, 1);
},
// テンポを削除する。先頭のテンポの場合はデフォルトのテンポに置き換える。
async action(
{ state, commit, dispatch },
{ position }: { position: number }
) {
const emptyScore = await dispatch("GET_EMPTY_SCORE");
const defaultTempo = emptyScore.tempos[0];

const score = state.score;
if (score === undefined || score.tempos.length === 0) {
throw new Error("Score is not initialized.");
}
const index = score.tempos.findIndex((value) => {
return value.position === position;
});
if (index === -1) return;

commit("REMOVE_TEMPO", { index });
if (score.tempos.length === 0) {
commit("SET_TEMPO", { index, tempo: defaultTempo });
}
},
},

SET_TIME_SIGNATURE: {
mutation(
state,
{ index, timeSignature }: { index: number; timeSignature: TimeSignature }
) {
state.score?.timeSignatures.splice(index, 0, timeSignature);
},
// 拍子を設定する。既に同じ位置に拍子が存在する場合は置き換える。
async action(
{ state, commit },
{ timeSignature }: { timeSignature: TimeSignature }
) {
const score = state.score;
if (score === undefined || score.timeSignatures.length === 0) {
throw new Error("Score is not initialized.");
}
if (
Number.isNaN(timeSignature.position) ||
timeSignature.position < 0 ||
!Number.isInteger(timeSignature.beats) ||
!Number.isInteger(timeSignature.beatType) ||
timeSignature.beats <= 0 ||
timeSignature.beatType <= 0
) {
throw new Error("The value is invalid.");
}
const duplicate = score.timeSignatures.some((value) => {
return value.position === timeSignature.position;
});
const index = score.timeSignatures.findIndex((value) => {
return value.position >= timeSignature.position;
});
if (index === -1) return;

if (duplicate) {
commit("REMOVE_TIME_SIGNATURE", { index });
}
commit("SET_TIME_SIGNATURE", { index, timeSignature });
},
},

REMOVE_TIME_SIGNATURE: {
mutation(state, { index }: { index: number }) {
state.score?.timeSignatures.splice(index, 1);
},
// 拍子を削除する。先頭の拍子の場合はデフォルトの拍子に置き換える。
async action(
{ state, commit, dispatch },
{ position }: { position: number }
) {
const emptyScore = await dispatch("GET_EMPTY_SCORE");
const defaultTimeSignature = emptyScore.timeSignatures[0];

const score = state.score;
if (score === undefined || score.timeSignatures.length === 0) {
throw new Error("Score is not initialized.");
}
const index = score.timeSignatures.findIndex((value) => {
return value.position === position;
});
if (index === -1) return;

commit("REMOVE_TIME_SIGNATURE", { index });
if (score.timeSignatures.length === 0) {
commit("SET_TIME_SIGNATURE", {
index,
timeSignature: defaultTimeSignature,
});
}
},
},

IMPORT_MIDI_FILE: {
action: createUILockAction(
async ({ dispatch }, { filePath }: { filePath?: string }) => {
Expand Down
20 changes: 20 additions & 0 deletions src/store/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,26 @@ export type SingingStoreTypes = {
action(payload: { score: Score }): void;
};

SET_TEMPO: {
mutation: { index: number; tempo: Tempo };
action(payload: { tempo: Tempo }): void;
};

REMOVE_TEMPO: {
mutation: { index: number };
action(payload: { position: number }): void;
};

SET_TIME_SIGNATURE: {
mutation: { index: number; timeSignature: TimeSignature };
action(payload: { timeSignature: TimeSignature }): void;
};

REMOVE_TIME_SIGNATURE: {
mutation: { index: number };
action(payload: { position: number }): void;
};

IMPORT_MIDI_FILE: {
action(payload: { filePath?: string }): void;
};
Expand Down