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

ソング:歌詞の一括入力を追加 #1952

Merged
merged 33 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
26f2610
WIP
sevenc-nanashi Mar 10, 2024
cd7ab8a
Merge: upstream/main -> add/multi-mora-at-once
sevenc-nanashi Mar 19, 2024
49f551d
Merge: main -> add/multi-mora-at-once
sevenc-nanashi Mar 19, 2024
4d45e12
Merge: upstream/main -> add/multi-mora-at-once
sevenc-nanashi Mar 23, 2024
3a84949
Add: 実装
sevenc-nanashi Mar 23, 2024
4692530
Add: テストを追加
sevenc-nanashi Mar 23, 2024
d281830
Change: splitMorasAndNonMoras -> splitLyricsByMoras
sevenc-nanashi Mar 27, 2024
2e042b5
Change: with-preview-lyric -> preview-lyric
sevenc-nanashi Mar 27, 2024
c7af372
Code: 参考を追加
sevenc-nanashi Mar 27, 2024
866ca2e
Refactor: splitLyricsByMorasを読みやすく
sevenc-nanashi Mar 27, 2024
a3fa553
Refactor: もう少し詳しく
sevenc-nanashi Mar 27, 2024
452c778
Add: テストケースを追加
sevenc-nanashi Mar 27, 2024
07477df
Change: lyricUpdated -> lyricUpdate
sevenc-nanashi Mar 27, 2024
343828d
Code: lyric周りのコメントを追加
sevenc-nanashi Mar 27, 2024
c38078e
Code: ScoreSequencerにコメントを追加
sevenc-nanashi Mar 27, 2024
668c821
Refactor: useLyricInputに切り出し
sevenc-nanashi Mar 27, 2024
bb2a86f
Add: TODOコメントを追加
sevenc-nanashi Mar 27, 2024
30ed8f3
Code: 日本語を修正
sevenc-nanashi Mar 27, 2024
f0ecdae
Change: !! -> != undefiend
sevenc-nanashi Apr 5, 2024
1664d82
Change: displayedLyric -> lyricToDisplay
sevenc-nanashi Apr 5, 2024
80b15bc
Change: lyricUpdate -> lyric-input
sevenc-nanashi Apr 5, 2024
66a6537
Change: 良い感じの名前に
sevenc-nanashi Apr 5, 2024
4775728
Code: 可読性を向上
sevenc-nanashi Apr 5, 2024
146cf30
Merge: upstream/main -> add/multi-mora-at-once
sevenc-nanashi Apr 5, 2024
2dc420e
Improve: ひらがな/カタカナを保持するように
sevenc-nanashi Apr 5, 2024
c99deb0
Fix: 長いときの表示を修正
sevenc-nanashi Apr 5, 2024
68f32c0
Code: ドキュメントを追加
sevenc-nanashi Apr 5, 2024
2eb9712
Refactor: v-elseにまとめる
sevenc-nanashi Apr 5, 2024
2e180cb
Refactor: splitLyricsByMorasをリファクタ
sevenc-nanashi Apr 5, 2024
2f2ba18
Merge: main -> add/multi-mora-at-once
sevenc-nanashi Apr 14, 2024
f4582f7
いくつか更新
Hiroshiba Apr 17, 2024
29363f6
Merge remote-tracking branch 'upstream/main' into pr/sevenc-nanashi/1…
Hiroshiba Apr 17, 2024
6d8a0e5
fmt
Hiroshiba Apr 17, 2024
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
49 changes: 49 additions & 0 deletions src/components/Sing/ScoreSequencer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,32 @@
}"
></div>
<!-- TODO: 1つのv-forで全てのノートを描画できるようにする -->
<!-- undefinedだと警告が出るのでnullを渡す -->
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
<SequencerNote
v-for="note in unselectedNotes"
:key="note.id"
:note="note"
:preview-lyric="previewLyrics.get(note.id) || null"
:is-selected="false"
@bar-mousedown="onNoteBarMouseDown($event, note)"
@left-edge-mousedown="onNoteLeftEdgeMouseDown($event, note)"
@right-edge-mousedown="onNoteRightEdgeMouseDown($event, note)"
@lyric-mouse-down="onNoteLyricMouseDown($event, note)"
@lyric-updated="onNoteLyricUpdated($event, note)"
@lyric-blur="onNoteLyricBlur()"
/>
<SequencerNote
v-for="note in nowPreviewing ? previewNotes : selectedNotes"
:key="note.id"
:note="note"
:preview-lyric="previewLyrics.get(note.id) || null"
:is-selected="true"
@bar-mousedown="onNoteBarMouseDown($event, note)"
@left-edge-mousedown="onNoteLeftEdgeMouseDown($event, note)"
@right-edge-mousedown="onNoteRightEdgeMouseDown($event, note)"
@lyric-mouse-down="onNoteLyricMouseDown($event, note)"
@lyric-updated="onNoteLyricUpdated($event, note)"
@lyric-blur="onNoteLyricBlur()"
/>
</div>
<SequencerPitch
Expand Down Expand Up @@ -233,6 +240,7 @@ import {
getMeasureDuration,
getNoteDuration,
getNumOfMeasures,
splitMorasAndNonMoras,
} from "@/sing/domain";
import {
getKeyBaseHeight,
Expand Down Expand Up @@ -750,6 +758,47 @@ const onNoteLyricMouseDown = (event: MouseEvent, note: Note) => {
}
};

const previewLyrics = ref<Map<string, string>>(new Map());
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
const onNoteLyricUpdated = (lyric: string, note: Note) => {
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
// TODO: マルチトラック対応
const inputNoteIndex = store.state.tracks[0].notes.findIndex(
(value) => value.id === note.id
);
if (inputNoteIndex === -1) {
throw new Error("inputNoteIndex is -1.");
}
const newPreviewLyrics = new Map();

const moraAndNonMoras = splitMorasAndNonMoras(
lyric,
store.state.tracks[0].notes.length - inputNoteIndex
);
for (const [index, mora] of moraAndNonMoras.entries()) {
const noteIndex = inputNoteIndex + index;
if (noteIndex >= notes.value.length) {
break;
}
const note = notes.value[noteIndex];
newPreviewLyrics.set(note.id, mora);
}
previewLyrics.value = newPreviewLyrics;
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
};
const onNoteLyricBlur = () => {
const newNotes: Note[] = [];
if (previewLyrics.value.size === 0) {
return;
}
for (const [noteId, lyric] of previewLyrics.value) {
const note = notes.value.find((value) => value.id === noteId);
if (!note) {
continue;
}
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
newNotes.push({ ...note, lyric });
}
previewLyrics.value = new Map();
store.dispatch("COMMAND_UPDATE_NOTES", { notes: newNotes });
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
};

const onMouseDown = (event: MouseEvent) => {
if (!isSelfEventTarget(event)) {
return;
Expand Down
38 changes: 24 additions & 14 deletions src/components/Sing/SequencerNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class="note"
:class="{
selected: noteState === 'SELECTED',
'with-preview-lyric': !!props.previewLyric,
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
overlapping: hasOverlappingError,
'invalid-phrase': hasPhraseError,
'below-pitch': showPitch,
Expand All @@ -20,6 +21,14 @@
</div>
<!-- TODO: ピッチの上に歌詞入力のinputが表示されるようにする -->
<div
v-if="props.previewLyric"
class="note-lyric preview-lyric"
data-testid="note-lyric"
>
{{ props.previewLyric }}
</div>
<div
v-else
class="note-lyric"
data-testid="note-lyric"
@mousedown="onLyricMouseDown"
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -28,9 +37,10 @@
</div>
<input
v-if="showLyricInput"
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
v-model.lazy.trim="lyric"
v-focus
:value="lyric"
class="note-lyric-input"
@input="onLyricInput"
@mousedown.stop
@dblclick.stop
@keydown.stop="onLyricInputKeyDown"
Expand Down Expand Up @@ -88,6 +98,7 @@ const props = withDefaults(
defineProps<{
note: Note;
isSelected: boolean;
previewLyric: string | null;
}>(),
{
isSelected: false,
Expand All @@ -100,6 +111,8 @@ const emit =
(name: "rightEdgeMousedown", event: MouseEvent): void;
(name: "leftEdgeMousedown", event: MouseEvent): void;
(name: "lyricMouseDown", event: MouseEvent): void;
(name: "lyricUpdated", text: string): void;
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
(name: "lyricBlur"): void;
}>();

const store = useStore();
Expand Down Expand Up @@ -145,18 +158,8 @@ const hasPhraseError = computed(() => {
);
});

const lyric = computed({
get() {
return props.note.lyric;
},
set(value) {
if (!value) {
return;
}
const note: Note = { ...props.note, lyric: value };
store.dispatch("COMMAND_UPDATE_NOTES", { notes: [note] });
},
});
const lyric = computed(() => temporaryLyric.value ?? props.note.lyric);
const temporaryLyric = ref<string | undefined>(undefined);
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
const showLyricInput = computed(() => {
return state.editingLyricNoteId === props.note.id;
});
Expand Down Expand Up @@ -255,6 +258,12 @@ const onLyricInputBlur = () => {
if (state.editingLyricNoteId === props.note.id) {
store.dispatch("SET_EDITING_LYRIC_NOTE_ID", { noteId: undefined });
}
temporaryLyric.value = undefined;
emit("lyricBlur");
};
const onLyricInput = (event: Event) => {
temporaryLyric.value = (event.target as HTMLInputElement).value;
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
emit("lyricUpdated", temporaryLyric.value);
};
</script>

Expand All @@ -274,7 +283,8 @@ const onLyricInputBlur = () => {
}
}

&.selected {
&.selected,
&.with-preview-lyric {
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
// 色は仮
.note-bar {
background-color: hsl(130, 35%, 90%);
Expand Down
48 changes: 48 additions & 0 deletions src/sing/domain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Note, Phrase, Score, Tempo, TimeSignature } from "@/store/type";
import { convertHiraToKana, convertLongVowel } from "@/store/utility";

const BEAT_TYPES = [2, 4, 8, 16];
const MIN_BPM = 40;
Expand Down Expand Up @@ -334,3 +335,50 @@ export function selectPriorPhrase(
// 再生位置より前のPhrase
return sortedPhrases[0];
}

export const moraPattern = new RegExp(
"(?:" +
"[イ][ェ]|[ヴ][ャュョ]|[トド][ゥ]|[テデ][ィャュョ]|[デ][ェ]|[クグ][ヮ]|" + // rule_others
"[キシチニヒミリギジビピ][ェャュョ]|" + // rule_line_i
"[ツフヴ][ァ]|[ウスツフヴズ][ィ]|[ウツフヴ][ェォ]|" + // rule_line_u
"[ァ-ヴー]" + // rule_one_mora
")",
"g"
);
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved

/**
* 文字列をモーラと非モーラに分割する。
* 例:"カナ漢字" -> ["カ", "ナ", "漢字"]
*
* @param text 分割する文字列
* @param maxLength 最大の要素数(0の場合は制限なし)
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
* @returns 分割された文字列
*/
export const splitMorasAndNonMoras = (
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
text: string,
maxLength = 0
): string[] => {
const baseMoraAndNonMoras: string[] = [];
const matches = convertLongVowel(convertHiraToKana(text)).matchAll(
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
moraPattern
);
let lastMatchEnd = 0;
for (const match of matches) {
baseMoraAndNonMoras.push(text.substring(lastMatchEnd, match.index));
baseMoraAndNonMoras.push(match[0]);
if (match.index == undefined) {
throw new Error("match.index is undefined.");
}
lastMatchEnd = match.index + match[0].length;
}
baseMoraAndNonMoras.push(text.substring(lastMatchEnd));
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
const moraAndNonMoras = baseMoraAndNonMoras.filter((value) => value !== "");
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
if (maxLength !== 0 && moraAndNonMoras.length > maxLength) {
moraAndNonMoras.splice(
maxLength - 1,
moraAndNonMoras.length,
moraAndNonMoras.slice(maxLength - 1).join("")
);
}
return moraAndNonMoras;
};
36 changes: 36 additions & 0 deletions tests/unit/lib/splitMorasAndNonMoras.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, it } from "vitest";
import { splitMorasAndNonMoras } from "@/sing/domain";

it("モーラを分割する", () => {
expect(splitMorasAndNonMoras("アイウエオ")).toEqual([
"ア",
"イ",
"ウ",
"エ",
"オ",
]);
expect(splitMorasAndNonMoras("キャット")).toEqual(["キャ", "ッ", "ト"]);
});
it("平仮名対応", () => {
expect(splitMorasAndNonMoras("あいうえお")).toEqual([
"ア",
"イ",
"ウ",
"エ",
"オ",
]);
});
it("長音対応", () => {
expect(splitMorasAndNonMoras("アーイー")).toEqual(["ア", "ア", "イ", "イ"]);
});

it("モーラ以外が混ざっても残す", () => {
expect(splitMorasAndNonMoras("アaイ")).toEqual(["ア", "a", "イ"]);
});
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
it("最大の要素数を指定できる", () => {
expect(splitMorasAndNonMoras("アイウエオ", 3)).toEqual([
"ア",
"イ",
"ウエオ",
]);
});
Loading