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

audioRendering.tsとsinging.tsを変更 #1

Merged
merged 2 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
166 changes: 112 additions & 54 deletions src/sing/audioRendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import {
noteNumberToFrequency,
decibelToLinear,
linearToDecibel,
tickToSecond,
} from "@/sing/domain";
import { Timer } from "@/sing/utility";
import { Tempo } from "@/store/type";

const getEarliestSchedulableContextTime = (audioContext: BaseAudioContext) => {
const renderQuantumSize = 128;
Expand Down Expand Up @@ -58,6 +56,10 @@ interface EventScheduler {
* 再生、停止、再生位置の変更などの機能を提供します。
*/
export class Transport {
loop = false;
loopStartTime = 0;
loopEndTime = 0;

private readonly audioContext: AudioContext;
private readonly timer: Timer;
private readonly scheduleAheadTime: number;
Expand All @@ -69,24 +71,12 @@ export class Transport {
private startContextTime = 0;
private startTime = 0;
private schedulers = new Map<Sequence, EventScheduler>();

// ループ設定
// TODO: いったん動作するようにする
private isLoopEnabled = false;
private loopStartTime = 0;
private loopEndTime = 0;

setLoopSettings(
isLoopEnabled: boolean,
startTick: number,
endTick: number,
tempos: Tempo[],
tpqn: number,
) {
this.isLoopEnabled = isLoopEnabled;
this.loopStartTime = tickToSecond(startTick, tempos, tpqn);
this.loopEndTime = tickToSecond(endTick, tempos, tpqn);
}
private scheduledContextTime = 0;
private uncompletedLoopInfos: {
readonly contextTime: number;
readonly timeBeforeLoop: number;
readonly timeAfterLoop: number;
}[] = [];

get state() {
return this._state;
Expand All @@ -97,10 +87,8 @@ export class Transport {
*/
get time() {
if (this._state === "started") {
// 再生中の場合は、現在時刻から再生位置を計算する
const contextTime = this.audioContext.currentTime;
const elapsedTime = contextTime - this.startContextTime;
this._time = this.startTime + elapsedTime;
this._time = this.calcTime(contextTime);
}
return this._time;
}
Expand Down Expand Up @@ -133,13 +121,38 @@ export class Transport {
this.audioContext = audioContext;
this.scheduleAheadTime = scheduleAheadTime;
this.timer = new Timer(lookahead * 1000);

this.timer.start(() => {
if (this._state === "started") {
this.schedule(this.audioContext.currentTime);
this.scheduleEvents(this.audioContext.currentTime);
}
});
}

/**
* 再生位置を計算します。再生中にのみ使用可能です。
* @param contextTime コンテキスト時刻(この時刻から再生位置を計算)
* @returns 計算された再生位置(秒)
*/
private calcTime(contextTime: number) {
if (this._state !== "started") {
throw new Error("This method can only be used during playback.");
}
if (contextTime >= this.startContextTime) {
const elapsedTime = contextTime - this.startContextTime;
return this.startTime + elapsedTime;
}
while (this.uncompletedLoopInfos.length !== 0) {
const loopInfo = this.uncompletedLoopInfos[0];
if (contextTime < loopInfo.contextTime) {
const timeUntilLoop = loopInfo.contextTime - contextTime;
return loopInfo.timeBeforeLoop - timeUntilLoop;
}
this.uncompletedLoopInfos.shift();
}
throw new Error("Loop events are not scheduled correctly.");
}

/**
* スケジューラーを作成します。
* @param sequence スケジューラーでスケジューリングを行うシーケンス
Expand All @@ -158,35 +171,16 @@ export class Transport {
}

/**
* スケジューリングを行います
* シーケンスのイベントのスケジューリングを行います
* @param contextTime スケジューリングを行う時刻(コンテキスト時刻)
*/
private schedule(contextTime: number) {
// 再生位置を計算
const elapsedTime = contextTime - this.startContextTime;
let time = this.startTime + elapsedTime;

// ループ処理
// TODO: いったん動作するようにする
if (this.isLoopEnabled && time >= this.loopEndTime) {
const loopDuration = this.loopEndTime - this.loopStartTime;
// おそらくscheduleAheadTimeを考慮する必要ある
time = this.loopStartTime + ((time - this.loopStartTime) % loopDuration);
this.startTime = time;
this.startContextTime = contextTime;
// スケジューラーをループ後に初期化して再スケジューリングする...ダメな気がする
// うまく使い回すほうがよさそうだが、動作があまり理解できていない...
// クリアしないとループ中に行った変更が反映されないように思えるが違う?
this.schedulers.forEach((scheduler) => {
scheduler.stop(contextTime);
});
this.schedulers.clear();
this.sequences.forEach((sequence) => {
const scheduler = this.createScheduler(sequence);
scheduler.start(contextTime, time);
this.schedulers.set(sequence, scheduler);
});
private scheduleSequenceEvents(contextTime: number) {
if (contextTime < this.startContextTime) {
// NOTE: ループ未完了の場合にここに来る
return;
}
const time = this.calcTime(contextTime);

// シーケンスの削除を反映
const removedSequences: Sequence[] = [];
this.schedulers.forEach((scheduler, sequence) => {
Expand All @@ -198,6 +192,7 @@ export class Transport {
removedSequences.forEach((sequence) => {
this.schedulers.delete(sequence);
});

// シーケンスの追加を反映
this.sequences.forEach((sequence) => {
if (!this.schedulers.has(sequence)) {
Expand All @@ -206,10 +201,72 @@ export class Transport {
this.schedulers.set(sequence, scheduler);
}
});

// スケジューリングを行う
this.schedulers.forEach((scheduler) => {
scheduler.schedule(time + this.scheduleAheadTime);
});
this._time = time;
}

/**
* ループイベントのスケジューリングを行います。
* @param contextTime スケジューリングを行う時刻(コンテキスト時刻)
*/
private scheduleLoopEvents(contextTime: number) {
if (
!this.loop ||
this.loopEndTime <= this.loopStartTime ||
this.startTime >= this.loopEndTime
Copy link
Author

@sigprogramming sigprogramming Oct 3, 2024

Choose a reason for hiding this comment

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

再生ヘッドがループ範囲外にある場合は、ループ範囲内に戻さずにそのまま再生されるようにしています。(DAWと同じ挙動)

ここのthis.startTime >= this.loopEndTimeと、下の

if (contextTimeToLoop < this.scheduledContextTime) {
  return;
}

がその処理です。

) {
return;
}

const timeUntilLoop = this.loopEndTime - this.startTime;
let contextTimeToLoop = this.startContextTime + timeUntilLoop;
if (contextTimeToLoop < this.scheduledContextTime) {
return;
}
if (contextTimeToLoop < contextTime) {
contextTimeToLoop = contextTime;
}

const loopDuration = this.loopEndTime - this.loopStartTime;

while (contextTimeToLoop < contextTime + this.scheduleAheadTime) {
this.uncompletedLoopInfos.push({
contextTime: contextTimeToLoop,
timeBeforeLoop: this.loopEndTime,
timeAfterLoop: this.loopStartTime,
});

this.startContextTime = contextTimeToLoop;
this.startTime = this.loopStartTime;

this.schedulers.forEach((value) => {
value.stop(contextTimeToLoop);
});
this.schedulers.clear();

this.sequences.forEach((sequence) => {
const scheduler = this.createScheduler(sequence);
scheduler.start(contextTimeToLoop, this.loopStartTime);
scheduler.schedule(this.loopStartTime + this.scheduleAheadTime);
this.schedulers.set(sequence, scheduler);
});

contextTimeToLoop += loopDuration;
}
}

/**
* イベントのスケジューリングを行います。
* @param contextTime スケジューリングを行う時刻(コンテキスト時刻)
*/
private scheduleEvents(contextTime: number) {
this.scheduleSequenceEvents(contextTime);
this.scheduleLoopEvents(contextTime);

this.scheduledContextTime = contextTime + this.scheduleAheadTime;
}

/**
Expand Down Expand Up @@ -245,8 +302,10 @@ export class Transport {

this.startContextTime = contextTime;
this.startTime = this._time;
this.scheduledContextTime = contextTime;
this.uncompletedLoopInfos = [];

this.schedule(contextTime);
this.scheduleEvents(contextTime);
}

/**
Expand All @@ -257,8 +316,7 @@ export class Transport {
const contextTime = this.audioContext.currentTime;

// 停止する前に再生位置を更新する
const elapsedTime = contextTime - this.startContextTime;
this._time = this.startTime + elapsedTime;
this._time = this.calcTime(contextTime);

this._state = "stopped";

Expand Down
69 changes: 54 additions & 15 deletions src/store/singing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,9 +1165,16 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
}
mutations.SET_PLAYBACK_STATE({ nowPlaying: true });

transport.setLoopSettings(
state.isLoopEnabled,
// TODO: 以下の処理(ループの設定)は再生開始時に毎回行う必要はないので、
// ソングエディタ初期化時に1回だけ行うようにする
// NOTE: 初期化のactionを作った方が良いかも
transport.loop = state.isLoopEnabled;
transport.loopStartTime = tickToSecond(
state.loopStartTick,
state.tempos,
state.tpqn,
);
transport.loopEndTime = tickToSecond(
state.loopEndTick,
state.tempos,
state.tpqn,
Expand Down Expand Up @@ -2459,8 +2466,13 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
mutation(state, { isLoopEnabled }) {
state.isLoopEnabled = isLoopEnabled;
},
action({ mutations }, { isLoopEnabled }) {
action({ mutations, state }, { isLoopEnabled }) {
if (!transport) {
throw new Error("transport is undefined.");
}
mutations.SET_LOOP_ENABLED({ isLoopEnabled });

transport.loop = state.isLoopEnabled;
},
},

Expand All @@ -2470,17 +2482,21 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
state.loopEndTick = loopEndTick;
},
action({ mutations, state }, { loopStartTick, loopEndTick }) {
mutations.SET_LOOP_RANGE({ loopStartTick, loopEndTick });
if (transport) {
// TODO: いったん動作するようにする
transport.setLoopSettings(
true,
loopStartTick,
loopEndTick,
state.tempos,
state.tpqn,
);
if (!transport) {
throw new Error("transport is undefined.");
}
mutations.SET_LOOP_RANGE({ loopStartTick, loopEndTick });

transport.loopStartTime = tickToSecond(
state.loopStartTick,
state.tempos,
state.tpqn,
);
transport.loopEndTime = tickToSecond(
state.loopEndTick,
state.tempos,
state.tpqn,
);
},
},

Expand All @@ -2491,8 +2507,17 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
}
state.loopStartTick = loopStartTick;
},
action({ mutations }, { loopStartTick }) {
action({ mutations, state }, { loopStartTick }) {
if (!transport) {
throw new Error("transport is undefined.");
}
mutations.SET_LOOP_START({ loopStartTick });

transport.loopStartTime = tickToSecond(
state.loopStartTick,
state.tempos,
state.tpqn,
);
},
},

Expand All @@ -2503,14 +2528,28 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
}
state.loopEndTick = loopEndTick;
},
action({ mutations }, { loopEndTick }) {
action({ mutations, state }, { loopEndTick }) {
if (!transport) {
throw new Error("transport is undefined.");
}
mutations.SET_LOOP_END({ loopEndTick });

transport.loopEndTime = tickToSecond(
state.loopEndTick,
state.tempos,
state.tpqn,
);
},
},

TOGGLE_LOOP: {
action({ state, mutations }) {
if (!transport) {
throw new Error("transport is undefined.");
}
mutations.SET_LOOP_ENABLED({ isLoopEnabled: !state.isLoopEnabled });

transport.loop = state.isLoopEnabled;
},
},
});
Expand Down