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

ソング:再生位置を小節・拍が分かる形式で表示するようにする #2306

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
160 changes: 160 additions & 0 deletions src/components/Sing/PlayheadPositionDisplay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<template>
<div>
<div v-if="displayMode === 'Seconds'" class="playhead-position">
<div>{{ minAndSecStr }}</div>
<div class="millisec">.{{ milliSecStr }}</div>
</div>
<div v-if="displayMode === 'MeasuresBeats'" class="playhead-position">
<div>{{ measuresStr }}.</div>
<div>{{ beatsIntegerPartStr }}</div>
<div class="beats-fractional-part">.{{ beatsFractionalPartStr }}</div>
</div>
<ContextMenu ref="contextMenu" :menudata="contextMenuData" />
</div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useStore } from "@/store";
import ContextMenu, {
ContextMenuItemData,
} from "@/components/Menu/ContextMenu.vue";
import { getTimeSignaturePositions, ticksToMeasuresBeats } from "@/sing/domain";
import { MeasuresBeats } from "@/store/type";
import { useRootMiscSetting } from "@/composables/useRootMiscSetting";

const store = useStore();

const playheadTicks = ref(0);
const [displayMode, setDisplayMode] = useRootMiscSetting(
store,
"playheadPositionDisplayMode",
);

const timeSignatures = computed(() => {
const tpqn = store.state.tpqn;
const timeSignatures = store.state.timeSignatures;
const tsPositions = getTimeSignaturePositions(timeSignatures, tpqn);
return timeSignatures.map((value, index) => ({
...value,
position: tsPositions[index],
}));
});

const measuresBeats = computed((): MeasuresBeats => {
if (displayMode.value !== "MeasuresBeats") {
return { measures: 1, beats: 1 };
}
const tpqn = store.state.tpqn;
return ticksToMeasuresBeats(playheadTicks.value, timeSignatures.value, tpqn);
});

const measuresStr = computed(() => {
return measuresBeats.value.measures >= 0
? String(measuresBeats.value.measures).padStart(3, "0")
: String(measuresBeats.value.measures);
});

const beatsIntegerPartStr = computed(() => {
const integerPart = Math.floor(measuresBeats.value.beats);
return String(integerPart).padStart(2, "0");
});

const beatsFractionalPartStr = computed(() => {
const integerPart = Math.floor(measuresBeats.value.beats);
const fractionalPart = Math.floor(
(measuresBeats.value.beats - integerPart) * 100,
);
return String(fractionalPart).padStart(2, "0");
});

const minAndSecStr = computed(() => {
if (displayMode.value !== "Seconds") {
return "";
}
const ticks = playheadTicks.value;
const time = store.getters.TICK_TO_SECOND(ticks);
const intTime = Math.trunc(time);
const min = Math.trunc(intTime / 60);
const minStr = String(min).padStart(2, "0");
const secStr = String(intTime - min * 60).padStart(2, "0");
return `${minStr}:${secStr}`;
});

const milliSecStr = computed(() => {
if (displayMode.value !== "Seconds") {
return "";
}
const ticks = playheadTicks.value;
const time = store.getters.TICK_TO_SECOND(ticks);
const intTime = Math.trunc(time);
const milliSec = Math.trunc((time - intTime) * 1000);
const milliSecStr = String(milliSec).padStart(3, "0");
return milliSecStr;
});

const contextMenu = ref<InstanceType<typeof ContextMenu>>();
const contextMenuData = computed<ContextMenuItemData[]>(() => {
return [
{
type: "button",
label: "小節.拍",
disabled: displayMode.value === "MeasuresBeats",
onClick: async () => {
contextMenu.value?.hide();
setDisplayMode("MeasuresBeats");
},
disableWhenUiLocked: false,
},
{
type: "button",
label: "分:秒",
disabled: displayMode.value === "Seconds",
onClick: async () => {
contextMenu.value?.hide();
setDisplayMode("Seconds");
},
disableWhenUiLocked: false,
},
];
});

const playheadPositionChangeListener = (position: number) => {
playheadTicks.value = position;
};

onMounted(() => {
void store.dispatch("ADD_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});

onUnmounted(() => {
void store.dispatch("REMOVE_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});
</script>

<style scoped lang="scss">
@use "@/styles/v2/variables" as vars;
@use "@/styles/colors" as colors;

.playhead-position {
align-items: center;
display: flex;
font-weight: 700;
font-size: 28px;
color: var(--scheme-color-on-surface);
}

.millisec {
font-size: 16px;
margin: 10px 0 0 2px;
}

.beats-fractional-part {
font-size: 16px;
margin: 10px 0 0 2px;
}
</style>
62 changes: 3 additions & 59 deletions src/components/Sing/ToolBar/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,7 @@
icon="stop"
@click="stop"
/>
<div class="sing-playhead-position">
<div>{{ playheadPositionMinSecStr }}</div>
<div class="sing-playhead-position-millisec">
.{{ playHeadPositionMilliSecStr }}
</div>
</div>
<PlayheadPositionDisplay class="sing-playhead-position" />
</div>
<!-- settings for edit controls -->
<div class="sing-controls">
Expand Down Expand Up @@ -164,7 +159,8 @@
</template>

<script setup lang="ts">
import { computed, watch, ref, onMounted, onUnmounted } from "vue";
import { computed, watch, ref } from "vue";
import PlayheadPositionDisplay from "../PlayheadPositionDisplay.vue";
import EditTargetSwicher from "./EditTargetSwicher.vue";
import { useStore } from "@/store";

Expand Down Expand Up @@ -383,30 +379,6 @@ const setVolumeRangeAdjustment = () => {
});
};

const playheadTicks = ref(0);

/// 再生時間の分と秒
const playheadPositionMinSecStr = computed(() => {
const ticks = playheadTicks.value;
const time = store.getters.TICK_TO_SECOND(ticks);

const intTime = Math.trunc(time);
const min = Math.trunc(intTime / 60);
const minStr = String(min).padStart(2, "0");
const secStr = String(intTime - min * 60).padStart(2, "0");

return `${minStr}:${secStr}`;
});

const playHeadPositionMilliSecStr = computed(() => {
const ticks = playheadTicks.value;
const time = store.getters.TICK_TO_SECOND(ticks);
const intTime = Math.trunc(time);
const milliSec = Math.trunc((time - intTime) * 1000);
const milliSecStr = String(milliSec).padStart(3, "0");
return milliSecStr;
});

const nowPlaying = computed(() => store.state.nowPlaying);

const play = () => {
Expand Down Expand Up @@ -463,22 +435,6 @@ const snapTypeSelectModel = computed({
});
},
});

const playheadPositionChangeListener = (position: number) => {
playheadTicks.value = position;
};

onMounted(() => {
void store.dispatch("ADD_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});

onUnmounted(() => {
void store.dispatch("REMOVE_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});
</script>

<style scoped lang="scss">
Expand Down Expand Up @@ -714,19 +670,7 @@ onUnmounted(() => {
}

.sing-playhead-position {
align-items: center;
display: flex;
font-size: 28px;
font-weight: 700;
margin-left: 16px;
color: var(--scheme-color-on-surface);
}

.sing-playhead-position-millisec {
font-size: 16px;
font-weight: 700;
margin: 10px 0 0 2px;
color: var(--scheme-color-on-surface);
}

.sing-controls {
Expand Down
48 changes: 46 additions & 2 deletions src/sing/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PhraseKey,
Track,
EditorFrameAudioQuery,
MeasuresBeats,
} from "@/store/type";
import { FramePhoneme } from "@/openapi";
import { TrackId } from "@/type/preload";
Expand Down Expand Up @@ -210,10 +211,53 @@ export function getMeasureDuration(
beatType: number,
tpqn: number,
) {
const wholeNoteDuration = tpqn * 4;
return (wholeNoteDuration / beatType) * beats;
return ((tpqn * 4) / beatType) * beats;
}

// NOTE: 戻り値の単位はtick
export function getBeatDuration(beatType: number, tpqn: number) {
return (tpqn * 4) / beatType;
}

const findTimeSignatureIndex = (
ticks: number,
timeSignatures: (TimeSignature & { position: number })[],
) => {
if (ticks < 0) {
return 0;
}
for (let i = 0; i < timeSignatures.length - 1; i++) {
if (
timeSignatures[i].position <= ticks &&
timeSignatures[i + 1].position > ticks
) {
return i;
}
}
return timeSignatures.length - 1;
};

export const ticksToMeasuresBeats = (
ticks: number,
timeSignatures: (TimeSignature & { position: number })[],
tpqn: number,
): MeasuresBeats => {
const tsIndex = findTimeSignatureIndex(ticks, timeSignatures);
const ts = timeSignatures[tsIndex];

const measureDuration = getMeasureDuration(ts.beats, ts.beatType, tpqn);
const beatDuration = getBeatDuration(ts.beatType, tpqn);

const posInTs = ticks - ts.position;
const measuresInTs = Math.floor(posInTs / measureDuration);
const measures = ts.measureNumber + measuresInTs;

const posInMeasure = posInTs - measureDuration * measuresInTs;
const beats = 1 + posInMeasure / beatDuration;

return { measures, beats };
};

export function getNumMeasures(
notes: Note[],
tempos: Tempo[],
Expand Down
2 changes: 2 additions & 0 deletions src/store/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const settingStoreState: SettingStoreState = {
panAndGain: true,
},
showSingCharacterPortrait: true,
playheadPositionDisplayMode: "Seconds",
};

export const settingStore = createPartialStore<SettingStoreTypes>({
Expand Down Expand Up @@ -141,6 +142,7 @@ export const settingStore = createPartialStore<SettingStoreTypes>({
"skipUpdateVersion",
"undoableTrackOperations",
"showSingCharacterPortrait",
"playheadPositionDisplayMode",
] as const;

// rootMiscSettingKeysに値を足し忘れていたときに型エラーを出す検出用コード
Expand Down
5 changes: 5 additions & 0 deletions src/store/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,11 @@ export type SongExportSetting = {
withTrackParameters: TrackParameters;
};

export type MeasuresBeats = {
measures: number;
beats: number;
};

export type SingingStoreState = {
tpqn: number; // Ticks Per Quarter Note
tempos: Tempo[];
Expand Down
3 changes: 3 additions & 0 deletions src/type/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,9 @@ export const rootMiscSettingSchema = z.object({
})
.default({}),
showSingCharacterPortrait: z.boolean().default(true), // ソングエディタで立ち絵を表示するか
playheadPositionDisplayMode: z
.enum(["Seconds", "MeasuresBeats"])
.default("Seconds"), // 再生ヘッド位置の表示モード
});
export type RootMiscSettingType = z.infer<typeof rootMiscSettingSchema>;

Expand Down
Loading