diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5a974e6fb4..5e6dcfe71d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -135,7 +135,7 @@ VITE_DEFAULT_ENGINE_INFOS=`[
"uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d",
"name": "VOICEVOX Engine",
"executionEnabled": true,
- "executionFilePath": "vv-engine/run.exe",
+ "executionFilePath": "C:/Users/(ユーザー名)/AppData/Local/Programs/VOICEVOX/vv-engine/run.exe",
"executionArgs": [],
"host": "http://127.0.0.1:50021"
}
diff --git a/README.md b/README.md
index 30518730d1..ab3172a3fd 100644
--- a/README.md
+++ b/README.md
@@ -49,21 +49,26 @@ npm ci
## 実行
-`.env.production`をコピーして`.env`を作成し、`VITE_DEFAULT_ENGINE_INFOS`内の`executionFilePath`に`voicevox_engine`のパスを指定します。
+### エンジンの準備
-[製品版 VOICEVOX](https://voicevox.hiroshiba.jp/) のディレクトリのパスを指定すれば動きます。
+`.env.production`をコピーして`.env`を作成し、`VITE_DEFAULT_ENGINE_INFOS`内の`executionFilePath`に
+[製品版 VOICEVOX](https://voicevox.hiroshiba.jp/) 内の`vv-engine/run.exe`を指定すれば動きます。
-Windows の場合でもパスの区切り文字は`\`ではなく`/`なのでご注意ください。
+Windows でインストール先を変更していない場合は`C:/Users/(ユーザー名)/AppData/Local/Programs/VOICEVOX/vv-engine/run.exe`を指定してください。
+パスの区切り文字は`\`ではなく`/`なのでご注意ください。
-また、macOS 向けの`VOICEVOX.app`を利用している場合は`/path/to/VOICEVOX.app/Contents/MacOS/vv-engine/run`を指定してください。
+macOS 向けの`VOICEVOX.app`を利用している場合は`/path/to/VOICEVOX.app/Contents/MacOS/vv-engine/run`を指定してください。
-Linux の場合は、[Releases](https://github.com/VOICEVOX/voicevox/releases/)から入手できる tar.gz 版に含まれる`run`コマンドを指定してください。
+Linux の場合は、[Releases](https://github.com/VOICEVOX/voicevox/releases/)から入手できる tar.gz 版に含まれる`vv-engine/run`コマンドを指定してください。
AppImage 版の場合は`$ /path/to/VOICEVOX.AppImage --appimage-mount`でファイルシステムをマウントできます。
-VOICEVOX エディタの実行とは別にエンジン API のサーバを立てている場合は`executionFilePath`を指定する必要はありません。
+VOICEVOX エディタの実行とは別にエンジン API のサーバを立てている場合は`executionFilePath`を指定する必要はありませんが、
+代わりに`executionEnabled`を`false`にしてください。
これは製品版 VOICEVOX を起動している場合もあてはまります。
-また、エンジン API の宛先エンドポイントを変更する場合は`VITE_DEFAULT_ENGINE_INFOS`内の`host`を変更してください。
+エンジン API の宛先エンドポイントを変更する場合は`VITE_DEFAULT_ENGINE_INFOS`内の`host`を変更してください。
+
+### Electron の実行
```bash
# 開発しやすい環境で実行
diff --git a/package-lock.json b/package-lock.json
index b3f784215b..23581591b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"@gtm-support/vue-gtm": "1.2.3",
"@quasar/extras": "1.10.10",
- "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.3.2",
+ "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.4.0",
"async-lock": "1.4.0",
"colorjs.io": "0.5.2",
"dayjs": "1.10.7",
@@ -5198,10 +5198,11 @@
},
"node_modules/@sevenc-nanashi/utaformatix-ts": {
"name": "@jsr/sevenc-nanashi__utaformatix-ts",
- "version": "0.3.2",
- "resolved": "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__utaformatix-ts/0.3.2.tgz",
- "integrity": "sha512-TdCME7wLGNnv7vcTRXZ4IXZpv/7uVUqy1ACIxuZQNg2M9jYk3aA9CSRNHkrrTIAeH4JuDBpONfn9zb9GkXsscg==",
+ "version": "0.4.0",
+ "resolved": "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__utaformatix-ts/0.4.0.tgz",
+ "integrity": "sha512-/rI7ZOy51LjOP+OwWclbuJ6K7gRTs9s2DHGrxBgJ1ho4TeE9LpFgNyQReKwsznLAgleAvJWJbW3a2MMAoNHeGA==",
"dependencies": {
+ "defu": "^6.1.4",
"jszip": "^3.10.1",
"utaformatix-data": "^1.1.0"
}
@@ -11213,8 +11214,7 @@
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
- "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
- "dev": true
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/delayed-stream": {
"version": "1.0.0",
diff --git a/package.json b/package.json
index 9755c59f5a..ad15746422 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"dependencies": {
"@gtm-support/vue-gtm": "1.2.3",
"@quasar/extras": "1.10.10",
- "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.3.2",
+ "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.4.0",
"async-lock": "1.4.0",
"colorjs.io": "0.5.2",
"dayjs": "1.10.7",
diff --git a/src/components/Dialog/DictionaryManageDialog.vue b/src/components/Dialog/DictionaryManageDialog.vue
index 682a61c8fa..b491b4de7b 100644
--- a/src/components/Dialog/DictionaryManageDialog.vue
+++ b/src/components/Dialog/DictionaryManageDialog.vue
@@ -90,7 +90,10 @@
dense
round
icon="edit"
- @click.stop="editWord"
+ @click.stop="
+ selectWord(key);
+ editWord();
+ "
>
編集
@@ -100,7 +103,10 @@
dense
round
icon="delete_outline"
- @click.stop="deleteWord"
+ @click.stop="
+ selectWord(key);
+ deleteWord();
+ "
>
削除
diff --git a/src/components/Sing/SideBar/TrackItem.vue b/src/components/Sing/SideBar/TrackItem.vue
index eaeb3e0c32..79da221fb3 100644
--- a/src/components/Sing/SideBar/TrackItem.vue
+++ b/src/components/Sing/SideBar/TrackItem.vue
@@ -214,13 +214,16 @@ watchEffect(() => {
const updateTrackName = () => {
if (temporaryTrackName.value === track.value.name) return;
+
+ // 空のトラック名だと空欄のようになってしまうので許容しない
if (temporaryTrackName.value === "") {
temporaryTrackName.value = track.value.name;
return;
}
+
store.dispatch("COMMAND_SET_TRACK_NAME", {
trackId: props.trackId,
- name: track.value.name,
+ name: temporaryTrackName.value,
});
};
diff --git a/src/components/Sing/SingEditor.vue b/src/components/Sing/SingEditor.vue
index 1d7432a579..69c28c5da9 100644
--- a/src/components/Sing/SingEditor.vue
+++ b/src/components/Sing/SingEditor.vue
@@ -124,6 +124,7 @@ onetimeWatch(
await store.dispatch("SET_VOLUME", { volume: 0.6 });
await store.dispatch("SET_PLAYHEAD_POSITION", { position: 0 });
+ await store.dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
isCompletedInitialStartup.value = true;
return "unwatch";
diff --git a/src/store/command.ts b/src/store/command.ts
index 8b64796edd..649e415823 100644
--- a/src/store/command.ts
+++ b/src/store/command.ts
@@ -111,6 +111,7 @@ export const commandStore = createPartialStore({
if (editor === "song") {
// TODO: 存在しないノートのみ選択解除、あるいはSELECTED_NOTE_IDS getterを作る
mutations.DESELECT_ALL_NOTES();
+ actions.SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS();
actions.RENDER();
}
},
@@ -129,6 +130,7 @@ export const commandStore = createPartialStore({
if (editor === "song") {
// TODO: 存在しないノートのみ選択解除、あるいはSELECTED_NOTE_IDS getterを作る
mutations.DESELECT_ALL_NOTES();
+ actions.SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS();
actions.RENDER();
}
},
diff --git a/src/store/singing.ts b/src/store/singing.ts
index 187620a989..6ac6bf7443 100644
--- a/src/store/singing.ts
+++ b/src/store/singing.ts
@@ -21,6 +21,7 @@ import {
SequencerEditTarget,
PhraseSourceHash,
Track,
+ SequenceId,
} from "./type";
import { DEFAULT_PROJECT_NAME, sanitizeFileName } from "./utility";
import {
@@ -40,7 +41,6 @@ import {
Clipper,
Limiter,
NoteEvent,
- NoteSequence,
OfflineTransport,
PolySynth,
Sequence,
@@ -250,7 +250,7 @@ if (window.AudioContext) {
const playheadPosition = new FrequentlyUpdatedState(0);
const singingVoices = new Map();
-const sequences = new Map();
+const sequences = new Map();
const animationTimer = new AnimationTimer();
const singingGuideCache = new Map();
@@ -258,6 +258,139 @@ const singingVoiceCache = new Map();
const initialTrackId = TrackId(crypto.randomUUID());
+/**
+ * シーケンスの音源の出力を取得する。
+ * @param sequence シーケンス
+ * @returns シーケンスの音源の出力
+ */
+const getOutputOfAudioSource = (sequence: Sequence) => {
+ if (sequence.type === "note") {
+ return sequence.instrument.output;
+ } else if (sequence.type === "audio") {
+ return sequence.audioPlayer.output;
+ } else {
+ throw new Error("Unknown type of sequence.");
+ }
+};
+
+/**
+ * シーケンスを登録する。
+ * ChannelStripが存在する場合は、ChannelStripにシーケンスを接続する。
+ * @param sequenceId シーケンスID
+ * @param sequence トラックIDを持つシーケンス
+ */
+const registerSequence = (
+ sequenceId: SequenceId,
+ sequence: Sequence & { trackId: TrackId },
+) => {
+ if (transport == undefined) {
+ throw new Error("transport is undefined.");
+ }
+ if (sequences.has(sequenceId)) {
+ throw new Error("Sequence already exists.");
+ }
+ sequences.set(sequenceId, sequence);
+
+ // Transportに追加する
+ transport.addSequence(sequence);
+
+ // ChannelStripがある場合は接続する
+ const channelStrip = trackChannelStrips.get(sequence.trackId);
+ if (channelStrip != undefined) {
+ getOutputOfAudioSource(sequence).connect(channelStrip.input);
+ }
+};
+
+/**
+ * シーケンスを削除する。
+ * ChannelStripが存在する場合は、ChannelStripとシーケンスの接続を解除する。
+ * @param sequenceId シーケンスID
+ */
+const deleteSequence = (sequenceId: SequenceId) => {
+ if (transport == undefined) {
+ throw new Error("transport is undefined.");
+ }
+ const sequence = sequences.get(sequenceId);
+ if (sequence == undefined) {
+ throw new Error("Sequence does not exist.");
+ }
+ sequences.delete(sequenceId);
+
+ // Transportから削除する
+ transport.removeSequence(sequence);
+
+ // ChannelStripがある場合は接続を解除する
+ if (trackChannelStrips.has(sequence.trackId)) {
+ getOutputOfAudioSource(sequence).disconnect();
+ }
+};
+
+/**
+ * `tracks`と`trackChannelStrips`を同期する。
+ * シーケンスが存在する場合は、ChannelStripとシーケンスの接続・接続の解除を行う。
+ * @param tracks `state`の`tracks`
+ * @param enableMultiTrack マルチトラックが有効かどうか
+ */
+const syncTracksAndTrackChannelStrips = (
+ tracks: Map,
+ enableMultiTrack: boolean,
+) => {
+ if (audioContext == undefined) {
+ throw new Error("audioContext is undefined.");
+ }
+ if (mainChannelStrip == undefined) {
+ throw new Error("mainChannelStrip is undefined.");
+ }
+
+ const shouldPlays = shouldPlayTracks(tracks);
+ for (const [trackId, track] of tracks) {
+ if (!trackChannelStrips.has(trackId)) {
+ const channelStrip = new ChannelStrip(audioContext);
+ channelStrip.output.connect(mainChannelStrip.input);
+ trackChannelStrips.set(trackId, channelStrip);
+
+ // シーケンスがある場合は、それらを接続する
+ for (const [sequenceId, sequence] of sequences) {
+ if (trackId === sequence.trackId) {
+ const sequence = sequences.get(sequenceId);
+ if (sequence == undefined) {
+ throw new Error("Sequence does not exist.");
+ }
+ getOutputOfAudioSource(sequence).connect(channelStrip.input);
+ }
+ }
+ }
+
+ const channelStrip = getOrThrow(trackChannelStrips, trackId);
+ if (enableMultiTrack) {
+ channelStrip.volume = track.gain;
+ channelStrip.pan = track.pan;
+ channelStrip.mute = !shouldPlays.has(trackId);
+ } else {
+ channelStrip.volume = 1;
+ channelStrip.pan = 0;
+ channelStrip.mute = false;
+ }
+ }
+ for (const [trackId, channelStrip] of trackChannelStrips) {
+ if (!tracks.has(trackId)) {
+ channelStrip.output.disconnect();
+ trackChannelStrips.delete(trackId);
+
+ // シーケンスがある場合は、それらの接続を解除する
+ for (const [sequenceId, sequence] of sequences) {
+ if (trackId === sequence.trackId) {
+ const sequence = sequences.get(sequenceId);
+ if (sequence == undefined) {
+ throw new Error("Sequence does not exist.");
+ }
+ getOutputOfAudioSource(sequence).disconnect();
+ }
+ }
+ }
+ }
+};
+
/** トラックを取得する。見付からないときはフォールバックとして最初のトラックを返す。 */
const getSelectedTrackWithFallback = (partialState: {
tracks: Map;
@@ -520,15 +653,13 @@ export const singingStore = createPartialStore({
state.timeSignatures = timeSignatures;
},
async action(
- { commit, dispatch },
+ { commit },
{ timeSignatures }: { timeSignatures: TimeSignature[] },
) {
if (!isValidTimeSignatures(timeSignatures)) {
throw new Error("The time signatures are invalid.");
}
commit("SET_TIME_SIGNATURES", { timeSignatures });
-
- dispatch("RENDER");
},
},
@@ -795,6 +926,23 @@ export const singingStore = createPartialStore({
},
},
+ SET_SEQUENCE_ID_TO_PHRASE: {
+ mutation(
+ state,
+ {
+ phraseKey,
+ sequenceId,
+ }: {
+ phraseKey: PhraseSourceHash;
+ sequenceId: SequenceId | undefined;
+ },
+ ) {
+ const phrase = getOrThrow(state.phrases, phraseKey);
+
+ phrase.sequenceId = sequenceId;
+ },
+ },
+
SET_SINGING_GUIDE: {
mutation(
state,
@@ -1070,6 +1218,7 @@ export const singingStore = createPartialStore({
}
commit("INSERT_TRACK", { trackId, track, prevTrackId });
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
},
@@ -1085,6 +1234,7 @@ export const singingStore = createPartialStore({
}
commit("DELETE_TRACK", { trackId });
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
},
@@ -1117,6 +1267,7 @@ export const singingStore = createPartialStore({
commit("SET_TRACK", { trackId, track });
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
},
@@ -1132,10 +1283,20 @@ export const singingStore = createPartialStore({
}
commit("SET_TRACKS", { tracks });
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
},
+ SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS: {
+ async action({ state }) {
+ syncTracksAndTrackChannelStrips(
+ state.tracks,
+ state.experimentalSetting.enableMultiTrack,
+ );
+ },
+ },
+
/**
* レンダリングを行う。レンダリング中だった場合は停止して再レンダリングする。
*/
@@ -1451,32 +1612,24 @@ export const singingStore = createPartialStore({
}
};
- const getAudioSourceNode = (sequence: Sequence) => {
- if (sequence.type === "note") {
- return sequence.instrument.output;
- } else if (sequence.type === "audio") {
- return sequence.audioPlayer.output;
- } else {
- throw new Error("Unknown type of sequence.");
- }
- };
-
// NOTE: 型推論でawaitの前か後かが考慮されないので、関数を介して取得する(型がbooleanになるようにする)
const startRenderingRequested = () => state.startRenderingRequested;
const stopRenderingRequested = () => state.stopRenderingRequested;
+ /**
+ * フレーズが持つシーケンスのIDを取得する。
+ * @param phraseKey フレーズのキー
+ * @returns シーケンスID
+ */
+ const getPhraseSequenceId = (phraseKey: PhraseSourceHash) => {
+ return getOrThrow(state.phrases, phraseKey).sequenceId;
+ };
+
const render = async () => {
if (!audioContext) {
throw new Error("audioContext is undefined.");
}
- if (!transport) {
- throw new Error("transport is undefined.");
- }
- if (!mainChannelStrip) {
- throw new Error("channelStrip is undefined.");
- }
const audioContextRef = audioContext;
- const transportRef = transport;
// レンダリング中に変更される可能性のあるデータをコピーする
const tracks = cloneWithUnwrapProxy(state.tracks);
@@ -1488,40 +1641,6 @@ export const singingStore = createPartialStore({
]),
);
- // trackChannelStripsを同期する。
- // ここで更新されたChannelStripに既存のAudioPlayerなどを繋げる必要がある。
- // そのため、Phraseが変わっていなくてもPhraseの更新=AudioPlayerなどの再接続は毎回行う必要がある。
- // trackChannelStripsを同期した後、フレーズの更新が完了するまではreturnやthrowをしないこと。
- // TODO: 良い設計を考える
- // ref: https://github.com/VOICEVOX/voicevox/pull/2176#discussion_r1693991784
-
- const shouldPlays = shouldPlayTracks(tracks);
- for (const [trackId, track] of tracks) {
- if (!trackChannelStrips.has(trackId)) {
- const channelStrip = new ChannelStrip(audioContext);
- channelStrip.output.connect(mainChannelStrip.input);
- trackChannelStrips.set(trackId, channelStrip);
- }
-
- const channelStrip = getOrThrow(trackChannelStrips, trackId);
- channelStrip.volume = state.experimentalSetting.enableMultiTrack
- ? track.gain
- : 1;
- channelStrip.pan = state.experimentalSetting.enableMultiTrack
- ? track.pan
- : 0;
- channelStrip.mute = state.experimentalSetting.enableMultiTrack
- ? !shouldPlays.has(trackId)
- : false;
- }
- for (const trackId of trackChannelStrips.keys()) {
- if (!tracks.has(trackId)) {
- const channelStrip = getOrThrow(trackChannelStrips, trackId);
- channelStrip.output.disconnect();
- trackChannelStrips.delete(trackId);
- }
- }
-
const singerAndFrameRates = new Map(
[...tracks].map(([trackId, track]) => [
trackId,
@@ -1657,13 +1776,15 @@ export const singingStore = createPartialStore({
}
}
- // 無くなったフレーズの音源とシーケンスの接続を解除して削除する
+ // 無くなったフレーズのシーケンスを削除する
for (const phraseKey of disappearedPhraseKeys) {
- const sequence = sequences.get(phraseKey);
- if (sequence) {
- getAudioSourceNode(sequence).disconnect();
- transportRef.removeSequence(sequence);
- sequences.delete(phraseKey);
+ const phraseSequenceId = getPhraseSequenceId(phraseKey);
+ if (phraseSequenceId != undefined) {
+ deleteSequence(phraseSequenceId);
+ commit("SET_SEQUENCE_ID_TO_PHRASE", {
+ phraseKey,
+ sequenceId: undefined,
+ });
}
}
@@ -1705,31 +1826,30 @@ export const singingStore = createPartialStore({
}),
);
for (const [phraseKey, phrase] of phrasesToBeRendered) {
- // シーケンスが存在する場合、シーケンスの接続を解除して削除する
+ // シーケンスが存在する場合は、シーケンスを削除する
// TODO: ピッチを編集したときは行わないようにする
- const sequence = sequences.get(phraseKey);
- if (sequence) {
- getAudioSourceNode(sequence).disconnect();
- transportRef.removeSequence(sequence);
- sequences.delete(phraseKey);
+ const phraseSequenceId = getPhraseSequenceId(phraseKey);
+ if (phraseSequenceId != undefined) {
+ deleteSequence(phraseSequenceId);
+ commit("SET_SEQUENCE_ID_TO_PHRASE", {
+ phraseKey,
+ sequenceId: undefined,
+ });
}
- // シーケンスが存在しない場合、ノートシーケンスを作成してプレビュー音が鳴るようにする
+ // ノートシーケンスを作成して登録し、プレビュー音が鳴るようにする
- if (!sequences.has(phraseKey)) {
- const noteEvents = generateNoteEvents(phrase.notes, tempos, tpqn);
- const polySynth = new PolySynth(audioContextRef);
- const noteSequence: NoteSequence = {
- type: "note",
- instrument: polySynth,
- noteEvents,
- };
- const channelStrip = getOrThrow(trackChannelStrips, phrase.trackId);
- polySynth.output.connect(channelStrip.input);
- transportRef.addSequence(noteSequence);
- sequences.set(phraseKey, noteSequence);
- }
+ const noteEvents = generateNoteEvents(phrase.notes, tempos, tpqn);
+ const polySynth = new PolySynth(audioContextRef);
+ const sequenceId = SequenceId(uuid4());
+ registerSequence(sequenceId, {
+ type: "note",
+ instrument: polySynth,
+ noteEvents,
+ trackId: phrase.trackId,
+ });
+ commit("SET_SEQUENCE_ID_TO_PHRASE", { phraseKey, sequenceId });
}
while (phrasesToBeRendered.size > 0) {
if (startRenderingRequested() || stopRenderingRequested()) {
@@ -1908,16 +2028,18 @@ export const singingStore = createPartialStore({
singingVoiceKey,
});
- // シーケンスが存在する場合、シーケンスの接続を解除して削除する
+ // シーケンスが存在する場合、シーケンスを削除する
- const sequence = sequences.get(phraseKey);
- if (sequence) {
- getAudioSourceNode(sequence).disconnect();
- transportRef.removeSequence(sequence);
- sequences.delete(phraseKey);
+ const phraseSequenceId = getPhraseSequenceId(phraseKey);
+ if (phraseSequenceId != undefined) {
+ deleteSequence(phraseSequenceId);
+ commit("SET_SEQUENCE_ID_TO_PHRASE", {
+ phraseKey,
+ sequenceId: undefined,
+ });
}
- // オーディオシーケンスを作成して接続する
+ // オーディオシーケンスを作成して登録する
const audioEvents = await generateAudioEvents(
audioContextRef,
@@ -1925,15 +2047,14 @@ export const singingStore = createPartialStore({
singingVoice.blob,
);
const audioPlayer = new AudioPlayer(audioContext);
- const audioSequence: AudioSequence = {
+ const sequenceId = SequenceId(uuid4());
+ registerSequence(sequenceId, {
type: "audio",
audioPlayer,
audioEvents,
- };
- const channelStrip = getOrThrow(trackChannelStrips, phrase.trackId);
- audioPlayer.output.connect(channelStrip.input);
- transportRef.addSequence(audioSequence);
- sequences.set(phraseKey, audioSequence);
+ trackId: phrase.trackId,
+ });
+ commit("SET_SEQUENCE_ID_TO_PHRASE", { phraseKey, sequenceId });
commit("SET_STATE_TO_PHRASE", {
phraseKey,
@@ -2301,7 +2422,7 @@ export const singingStore = createPartialStore({
action({ commit, dispatch }, { trackId, mute }) {
commit("SET_TRACK_MUTE", { trackId, mute });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2313,7 +2434,7 @@ export const singingStore = createPartialStore({
action({ commit, dispatch }, { trackId, solo }) {
commit("SET_TRACK_SOLO", { trackId, solo });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2325,7 +2446,7 @@ export const singingStore = createPartialStore({
action({ commit, dispatch }, { trackId, gain }) {
commit("SET_TRACK_GAIN", { trackId, gain });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2337,7 +2458,7 @@ export const singingStore = createPartialStore({
action({ commit, dispatch }, { trackId, pan }) {
commit("SET_TRACK_PAN", { trackId, pan });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2365,8 +2486,11 @@ export const singingStore = createPartialStore({
track.solo = false;
}
},
- action({ commit }) {
+ action({ commit, dispatch }) {
commit("UNSOLO_ALL_TRACKS");
+
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
+ dispatch("RENDER");
},
},
@@ -2658,6 +2782,9 @@ export const singingCommandStore = transformCommandStore(
track: cloneWithUnwrapProxy(track),
prevTrackId,
});
+
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
+ dispatch("RENDER");
},
},
@@ -2668,6 +2795,7 @@ export const singingCommandStore = transformCommandStore(
action({ commit, dispatch }, { trackId }) {
commit("COMMAND_DELETE_TRACK", { trackId });
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
},
@@ -2688,7 +2816,7 @@ export const singingCommandStore = transformCommandStore(
action({ commit, dispatch }, { trackId, mute }) {
commit("COMMAND_SET_TRACK_MUTE", { trackId, mute });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2699,7 +2827,7 @@ export const singingCommandStore = transformCommandStore(
action({ commit, dispatch }, { trackId, solo }) {
commit("COMMAND_SET_TRACK_SOLO", { trackId, solo });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2710,7 +2838,7 @@ export const singingCommandStore = transformCommandStore(
action({ commit, dispatch }, { trackId, gain }) {
commit("COMMAND_SET_TRACK_GAIN", { trackId, gain });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2721,7 +2849,7 @@ export const singingCommandStore = transformCommandStore(
action({ commit, dispatch }, { trackId, pan }) {
commit("COMMAND_SET_TRACK_PAN", { trackId, pan });
- dispatch("RENDER");
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
},
},
@@ -2738,8 +2866,11 @@ export const singingCommandStore = transformCommandStore(
mutation(draft) {
singingStore.mutations.UNSOLO_ALL_TRACKS(draft, undefined);
},
- action({ commit }) {
+ action({ commit, dispatch }) {
commit("COMMAND_UNSOLO_ALL_TRACKS");
+
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
+ dispatch("RENDER");
},
},
@@ -2807,6 +2938,7 @@ export const singingCommandStore = transformCommandStore(
tracks: payload,
});
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
},
@@ -2857,6 +2989,7 @@ export const singingCommandStore = transformCommandStore(
tracks: filteredTracks,
});
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
),
@@ -2896,6 +3029,7 @@ export const singingCommandStore = transformCommandStore(
tracks: filteredTracks,
});
+ dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS");
dispatch("RENDER");
},
),
diff --git a/src/store/type.ts b/src/store/type.ts
index 2f1b0ed2d9..6656aded76 100644
--- a/src/store/type.ts
+++ b/src/store/type.ts
@@ -798,6 +798,11 @@ export type SingingVoiceSourceHash = z.infer<
typeof singingVoiceSourceHashSchema
>;
+export const sequenceIdSchema = z.string().brand<"SequenceId">();
+export type SequenceId = z.infer;
+export const SequenceId = (id: string): SequenceId =>
+ sequenceIdSchema.parse(id);
+
/**
* フレーズ(レンダリング区間)
*/
@@ -808,6 +813,7 @@ export type Phrase = {
state: PhraseState;
singingGuideKey?: SingingGuideSourceHash;
singingVoiceKey?: SingingVoiceSourceHash;
+ sequenceId?: SequenceId;
};
/**
@@ -1007,6 +1013,13 @@ export type SingingStoreTypes = {
};
};
+ SET_SEQUENCE_ID_TO_PHRASE: {
+ mutation: {
+ phraseKey: PhraseSourceHash;
+ sequenceId: SequenceId | undefined;
+ };
+ };
+
SET_SINGING_GUIDE: {
mutation: {
singingGuideKey: SingingGuideSourceHash;
@@ -1246,6 +1259,10 @@ export type SingingStoreTypes = {
CALC_RENDER_DURATION: {
getter: number;
};
+
+ SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS: {
+ action(): void;
+ };
};
export type SingingCommandStoreState = {