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 3 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
127 changes: 117 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,82 @@ 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 () => {
await store.dispatch("ADD_TEMPO", {
tempo: {
position: 0,
tempo: tempoInputBuffer.value,
},
});
};

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

return {
isShowSinger,
toggleShowSinger,
userOrderedCharacterInfos,
selectedCharacterInfo,
selectedStyleIconPath,
tempoInputBuffer,
beatsInputBuffer,
beatTypeInputBuffer,
setTempoInputBuffer,
setBeatsInputBuffer,
setBeatTypeInputBuffer,
setTempo,
setTimeSignature,
};
},
});
Expand Down Expand Up @@ -120,14 +227,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
156 changes: 155 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,142 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
},
},

ADD_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("ADD_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("ADD_TEMPO", { index, tempo: defaultTempo });
}
},
},

ADD_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("ADD_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("ADD_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;
};

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

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

ADD_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