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] 歌ボ形式で歌声合成する機能を追加 #1255

Merged
merged 8 commits into from
Apr 11, 2023
4 changes: 2 additions & 2 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,7 @@ async function createWindow() {
win.on("close", (event) => {
if (!willQuit) {
event.preventDefault();
ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE");
ipcMainSend(win, "PROCESS_BEFORE_QUITTING");
return;
}
});
Expand Down Expand Up @@ -1214,7 +1214,7 @@ app.on("window-all-closed", () => {
app.on("before-quit", (event) => {
if (!willQuit) {
event.preventDefault();
ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE");
ipcMainSend(win, "PROCESS_BEFORE_QUITTING");
return;
}

Expand Down
4 changes: 3 additions & 1 deletion src/components/AcceptTermsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ export default defineComponent({
store.dispatch("SET_ACCEPT_TERMS", {
acceptTerms: acceptTerms ? "Accepted" : "Rejected",
});
!acceptTerms ? store.dispatch("CHECK_EDITED_AND_NOT_SAVE") : undefined;
if (!acceptTerms) {
store.dispatch("PROCESS_BEFORE_QUITTING");
}

modelValueComputed.value = false;
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/MinMaxCloseButtons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export default defineComponent({
setup() {
const store = useStore();

const closeWindow = async () => {
store.dispatch("CHECK_EDITED_AND_NOT_SAVE");
const closeWindow = () => {
store.dispatch("PROCESS_BEFORE_QUITTING");
};
const minimizeWindow = () => window.electron.minimizeWindow();
const maximizeWindow = () => window.electron.maximizeWindow();
Expand Down
4 changes: 4 additions & 0 deletions src/components/Sing/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@
root.type === 'button' ? (subMenuOpenFlags[index] = false) : undefined
"
/>
<q-space />
<min-max-close-buttons v-if="!$q.platform.is.mac" />
</q-bar>
</template>

<script lang="ts">
import { defineComponent, ref, computed, ComputedRef } from "vue";
import { useStore } from "@/store";
import MenuButton from "@/components/MenuButton.vue";
import MinMaxCloseButtons from "@/components/MinMaxCloseButtons.vue";
import { HotkeyAction, HotkeyReturnType } from "@/type/preload";
import { setHotkeyFunctions } from "@/store/setting";

Expand Down Expand Up @@ -57,6 +60,7 @@ export default defineComponent({

components: {
MenuButton,
MinMaxCloseButtons,
},

setup() {
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/singHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,7 @@ export function round(value: number, digits: number) {
const powerOf10 = 10 ** digits;
return Math.round(value * powerOf10) / powerOf10;
}

export function midiToFrequency(midi: number) {
return 440 * 2 ** ((midi - 69) / 12);
}
138 changes: 135 additions & 3 deletions src/infrastructures/AudioRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,45 @@ export class OfflineTransport implements BaseTransport {
}
}

export type AudioEvent = {
readonly time: number;
readonly buffer: AudioBuffer;
};

export class AudioSequence implements SoundSequence {
private readonly audioPlayer: AudioPlayer;
private readonly audioEvents: AudioEvent[];

constructor(audioPlayer: AudioPlayer, audioEvents: AudioEvent[]) {
this.audioPlayer = audioPlayer;
this.audioEvents = audioEvents;
}
Comment on lines +285 to +288
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほどです、設計がなんとなく見えてきた気がします!

AudioSequenceがAudioPlayerを持つ設計ではなく、AudioSequenceを持つ誰かがAudioPlayerを1つだけ持つ設計にすると、Playerが散らばらずに1箇所に集まるので管理しやすい・・・かも・・・・・?
いやでも一様に扱えるので今の設計もありかも・・・?

まあ設計をガチャガチャ変えながら実装するよりも、一旦とりあえず全部実装してみてあとで整理して再設計するのが良いのかなと思いました。
なのでとりあえず問題なく動いているなら今は一旦良し・・・!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transportで全てのイベントのスケジューリングを行うためにこの形になっていますが、私も「AudioSequenceを持つ誰かがAudioPlayerを1つだけ持つ」形のほうが自然な気がするので、後でまた設計を検討・試行したいと思います!
おそらく「AudioSequenceを持つ誰か」がそれぞれスケジューリングを行うことになり、そのスケジューリングの実行をTransportが管理することになると思います。
(一応今の設計でもループやマルチトラックの実装は可能です)


// スケジュール可能なイベントを生成する
generateEvents(startTime: number) {
return this.audioEvents
.sort((a, b) => a.time - b.time)
.filter((value) => {
const audioEndTime = value.time + value.buffer.duration;
return audioEndTime > startTime;
})
.map((value): SchedulableEvent => {
const offset = Math.max(startTime - value.time, 0);
return {
time: Math.max(value.time, startTime),
schedule: (contextTime: number) => {
this.audioPlayer.play(contextTime, offset, value.buffer);
},
};
});
}

// シーケンスの停止をスケジュールする
scheduleStop(contextTime: number) {
this.audioPlayer.allStop(contextTime);
}
}

export interface Instrument {
noteOn(contextTime: number, midi: number): void;
noteOff(contextTime: number, midi: number): void;
Expand Down Expand Up @@ -317,12 +356,101 @@ export class NoteSequence implements SoundSequence {
.sort((a, b) => a.time - b.time);
}

// シーケンス(楽器)の停止をスケジュールする
// シーケンスの停止をスケジュールする
scheduleStop(contextTime: number) {
this.instrument.allSoundOff(contextTime);
}
}

class AudioPlayerVoice {
private readonly audioBufferSourceNode: AudioBufferSourceNode;
private readonly buffer: AudioBuffer;

private _isStopped = false;
private stopContextTime?: number;

get isStopped() {
return this._isStopped;
}

constructor(audioContext: BaseAudioContext, buffer: AudioBuffer) {
this.audioBufferSourceNode = audioContext.createBufferSource();
this.audioBufferSourceNode.buffer = buffer;
this.audioBufferSourceNode.onended = () => {
this._isStopped = true;
};
this.buffer = buffer;
}

connect(destination: AudioNode) {
this.audioBufferSourceNode.connect(destination);
}

play(contextTime: number, offset: number) {
this.audioBufferSourceNode.start(contextTime, offset);
this.stopContextTime = contextTime + this.buffer.duration;
}

stop(contextTime?: number) {
if (this.stopContextTime === undefined) {
throw new Error("Not started.");
}
if (contextTime === undefined || contextTime < this.stopContextTime) {
this.audioBufferSourceNode.stop(contextTime);
this.stopContextTime = contextTime ?? 0;
}
}
}

export type AudioPlayerOptions = {
readonly volume: number;
};

export class AudioPlayer {
private readonly audioContext: BaseAudioContext;
private readonly gainNode: GainNode;

private voices: AudioPlayerVoice[] = [];

constructor(context: Context, options: AudioPlayerOptions = { volume: 1.0 }) {
this.audioContext = context.audioContext;

this.gainNode = this.audioContext.createGain();
this.gainNode.gain.value = options.volume;
}

connect(destination: AudioNode) {
this.gainNode.connect(destination);
}

disconnect() {
this.gainNode.disconnect();
}

play(contextTime: number, offset: number, buffer: AudioBuffer) {
const voice = new AudioPlayerVoice(this.audioContext, buffer);
this.voices = this.voices.filter((value) => {
return !value.isStopped;
});
this.voices.push(voice);
voice.connect(this.gainNode);
voice.play(contextTime, offset);
}

allStop(contextTime?: number) {
if (contextTime === undefined) {
this.voices.forEach((value) => {
value.stop();
});
this.voices = [];
} else {
this.voices.forEach((value) => {
value.stop(contextTime);
});
}
}
}

export type Envelope = {
readonly attack: number;
readonly decay: number;
Expand Down Expand Up @@ -371,8 +499,8 @@ class SynthVoice {
return 440 * 2 ** ((midi - 69) / 12);
}

connect(inputNode: AudioNode) {
this.gainNode.connect(inputNode);
connect(destination: AudioNode) {
this.gainNode.connect(destination);
}

noteOn(contextTime: number) {
Expand Down Expand Up @@ -532,6 +660,10 @@ export class AudioRenderer {
};
}

get audioContext() {
return this.onlineContext.audioContext;
}

get transport() {
return this.onlineContext.transport;
}
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/ipcMessageReceiverPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const ipcMessageReceiver: Plugin = {
options.store.dispatch("DETECT_LEAVE_FULLSCREEN")
);

window.electron.onReceivedIPCMsg("CHECK_EDITED_AND_NOT_SAVE", () => {
options.store.dispatch("CHECK_EDITED_AND_NOT_SAVE");
window.electron.onReceivedIPCMsg("PROCESS_BEFORE_QUITTING", () => {
options.store.dispatch("PROCESS_BEFORE_QUITTING");
});

window.electron.onReceivedIPCMsg(
Expand Down
Loading