Skip to content

Commit

Permalink
fixed: AudioManager
Browse files Browse the repository at this point in the history
  • Loading branch information
helloyork committed Jan 21, 2025
1 parent 1c313b4 commit f294444
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 42 deletions.
30 changes: 18 additions & 12 deletions src/game/nlcore/action/actions/soundAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,46 @@ export class SoundAction<T extends typeof SoundActionTypes[keyof typeof SoundAct
public executeAction(state: GameState): CalledActionResult | Awaitable<CalledActionResult, any> {
if (this.type === SoundActionTypes.play) {
const [options] = (this.contentNode as ContentNode<SoundActionContentType["sound:play"]>).getContent();

return Awaitable.forward(state.audioManager.play(this.callee, options), {
type: this.type as any,
type: this.type,
node: this.contentNode?.getChild()
});
}).then(() => state.stage.next());
} else if (this.type === SoundActionTypes.stop) {
const [options] = (this.contentNode as ContentNode<SoundActionContentType["sound:play"]>).getContent();

return Awaitable.forward(state.audioManager.stop(this.callee, options.duration), {
type: this.type as any,
type: this.type,
node: this.contentNode?.getChild()
});
}).then(() => state.stage.next());
} else if (this.type === SoundActionTypes.setVolume) {
const [volume, duration] = (this.contentNode as ContentNode<SoundActionContentType["sound:setVolume"]>).getContent();

return Awaitable.forward(state.audioManager.setVolume(this.callee, volume, duration), {
type: this.type as any,
type: this.type,
node: this.contentNode?.getChild()
});
}).then(() => state.stage.next());
} else if (this.type === SoundActionTypes.setRate) {
const [rate] = (this.contentNode as ContentNode<SoundActionContentType["sound:setRate"]>).getContent();

return Awaitable.forward(state.audioManager.setRate(this.callee, rate), {
type: this.type as any,
type: this.type,
node: this.contentNode?.getChild()
});
}).then(() => state.stage.next());
} else if (this.type === SoundActionTypes.pause) {
const [options] = (this.contentNode as ContentNode<SoundActionContentType["sound:pause"]>).getContent();

return Awaitable.forward(state.audioManager.pause(this.callee, options.duration), {
type: this.type as any,
type: this.type,
node: this.contentNode?.getChild()
});
}).then(() => state.stage.next());
} else if (this.type === SoundActionTypes.resume) {
const [options] = (this.contentNode as ContentNode<SoundActionContentType["sound:resume"]>).getContent();

return Awaitable.forward(state.audioManager.resume(this.callee, options.duration), {
type: this.type as any,
type: this.type ,
node: this.contentNode?.getChild()
});
}).then(() => state.stage.next());
}

throw super.unknownTypeError();
Expand Down
11 changes: 0 additions & 11 deletions src/game/nlcore/elements/sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ export interface ISoundUserConfig {
* @default false
*/
streaming: boolean;
/**
* Automatically begin downloading the audio file when the Howl is defined.
*
* This preloading behavior is provided by the [Howler.js](https://github.com/goldfire/howler.js) library.
* For license and dependencies, see [NarraLeaf-React: License](https://react.narraleaf.com/documentation/info/license).
* @default false
*/
preload: boolean;
/**
* Initial position in seconds
* @default 0
Expand All @@ -64,7 +56,6 @@ type SoundConfig = {
src: string;
loop: boolean;
streaming: boolean;
preload: boolean;
seek: number;
};

Expand All @@ -85,7 +76,6 @@ export class Sound extends Actionable<SoundDataRaw, Sound> {
volume: 1,
streaming: false,
rate: 1,
preload: false,
seek: 0,
});

Expand All @@ -94,7 +84,6 @@ export class Sound extends Actionable<SoundDataRaw, Sound> {
src: Sound.noSound,
loop: false,
streaming: false,
preload: false,
seek: 0,
});

Expand Down
95 changes: 80 additions & 15 deletions src/game/player/lib/AudioManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,21 @@ export class AudioManager {
if (this.state.has(sound)) {
this.abortTask(this.getState(sound).group);
}
const {group, token} = this.initSound(sound);
const {group, token, onPlayTask, onEndTask} = this.initSound(sound);

this.state.set(sound, {group, token});
return this.pushTask(group, new ChainedAwaitable()
.addTask(onPlayTask)
.addTask(this.fadeTo(group, token, {
...options,
start: 0,
}))
.addTask(this.createTask(() => {
.addTask(this.createTask((resolve) => {
sound.state.volume = options.end;
sound.state.paused = false;
resolve();
}))
.addTask(onEndTask)
.run());
}

Expand All @@ -62,8 +65,9 @@ export class AudioManager {
}
return this.pushTask(state.group, new ChainedAwaitable()
.addTask(this.fadeTo(state.group, state.token, {start: sound.state.volume, end: 0, duration}))
.addTask(this.createTask(() => {
.addTask(this.createTask((resolve) => {
state.group.volume(sound.state.volume, state.token);
resolve();
}))
.addTask(this.stopSound(state.group, state.token))
.run());
Expand All @@ -78,8 +82,9 @@ export class AudioManager {
}
return this.pushTask(state.group, new ChainedAwaitable()
.addTask(this.fadeTo(state.group, state.token, {start: sound.state.volume, end: volume, duration}))
.addTask(this.createTask(() => {
.addTask(this.createTask((resolve) => {
sound.state.volume = volume;
resolve();
}))
.run());
}
Expand All @@ -94,9 +99,10 @@ export class AudioManager {
return this.pushTask(state.group, new ChainedAwaitable()
.addTask(this.fadeTo(state.group, state.token, {start: sound.state.volume, end: 0, duration}))
.addTask(this.pauseSound(state.group, state.token))
.addTask(this.createTask(() => {
.addTask(this.createTask((resolve) => {
state.group.volume(sound.state.volume, state.token);
sound.state.paused = true;
resolve();
}))
.run());
}
Expand All @@ -111,8 +117,9 @@ export class AudioManager {
return this.pushTask(state.group, new ChainedAwaitable()
.addTask(this.fadeTo(state.group, state.token, {start: 0, end: sound.state.volume, duration}))
.addTask(this.resumeSound(state.group, state.token))
.addTask(this.createTask(() => {
.addTask(this.createTask((resolve) => {
sound.state.paused = false;
resolve();
}))
.run());
}
Expand Down Expand Up @@ -190,11 +197,45 @@ export class AudioManager {
this.tasks.clear();
}

private initSound(sound: Sound): SoundState {
private initSound(sound: Sound): SoundState & {
onPlayTask?: ChainedAwaitableTask;
onEndTask?: ChainedAwaitableTask;
} {
if (this.state.has(sound)) {
return this.state.get(sound)!;
}
const group = Reflect.construct(this.gameState.getHowl(), [this.getHowlConfig(sound)]);
const audioManager = this;
const [onPlay, onPlayTask] = this.wrapTask();
const [onEnd, onEndTask] = this.wrapTask();
const group = Reflect.construct(this.gameState.getHowl(), [this.getHowlConfig(sound, {
onend() {
onEnd.resolve();
},
onplay() {
onPlay.resolve();
},
onloaderror(_, error: unknown) {
const code = error as 1 | 2 | 3 | 4;
/**
* 1 - The fetching process for the media resource was aborted by the user agent at the user's request.
* 2 - A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable.
* 3 - An error of some description occurred while decoding the media resource, after the resource was established to be usable.
* 4 - The media resource indicated by the src attribute or assigned media provider object was not suitable.
* For more information, see https://github.com/goldfire/howler.js?tab=readme-ov-file#onloaderror-function
*/
const messages: {
[K in 1 | 2 | 3 | 4]: string;
} = {
1: "The fetching process for the media resource was aborted by the user agent at the user's request.",
2: "A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable.",
3: "An error of some description occurred while decoding the media resource, after the resource was established to be usable.",
4: "The media resource indicated by the src attribute or assigned media provider object was not suitable.",
};
audioManager.gameState.logger.error("AudioManager", `Failed to load sound (src: "${sound.config.src}")`
+ ` \n${messages[code]}`
+ " \nFor more information, see https://github.com/goldfire/howler.js?tab=readme-ov-file#onloaderror-function");
}
})]);
const token = group.play();
this.state.set(sound, {group, token});
group
Expand All @@ -204,7 +245,7 @@ export class AudioManager {
if (sound.state.paused) {
group.pause(token);
}
return {group, token};
return {group, token, onPlayTask, onEndTask};
}

private pushTask(spirit: Howler.Howl, awaitable: Awaitable<void>): Awaitable<void> {
Expand All @@ -230,22 +271,32 @@ export class AudioManager {
}

private fadeTo(group: Howler.Howl, token: number, options: FadeOptions): ChainedAwaitableTask {
let fadeHandler: () => void;
let fadeHandler: () => void, schedule: VoidFunction | undefined;

const start = options.start ?? group.volume();
const end = options.end ?? group.volume();
const duration = options.duration;
const skipController = new SkipController<void>(() => {
group.volume(end, token);
group.off("fade", fadeHandler, token);
fadeHandler();
});
const handler = (awaitable: Awaitable<void>) => {
group.volume(start, token);
group.fade(start, end, duration, token);
fadeHandler = () => {
if (awaitable.isSolved()) {
return;
}
if (schedule) {
schedule();
schedule = undefined;
}
awaitable.resolve();
};
group.once("fade", fadeHandler, token);
schedule = this.gameState.schedule(() => {
schedule = undefined;
fadeHandler();
}, duration);
};
return [handler, skipController];
}
Expand Down Expand Up @@ -275,13 +326,27 @@ export class AudioManager {
loop: sound.config.loop,
rate: sound.state.rate,
html5: sound.config.streaming,
preload: sound.config.preload,
...options,
} satisfies Howler.HowlOptions;
}

private createTask(handler: () => void): ChainedAwaitableTask {
return [handler];
private createTask(handler: (resolve: () => void) => void): ChainedAwaitableTask {
return [(awaitable) => {
handler(awaitable.resolve.bind(awaitable));
}];
}

private wrapTask(): [awaitable: Awaitable<void>, task: ChainedAwaitableTask] {
const awaitable = new Awaitable<void>();
return [awaitable, [(a) => {
if (awaitable.isSolved()) {
a.resolve();
} else {
awaitable.then(() => {
a.resolve();
});
}
}]];
}
}

11 changes: 7 additions & 4 deletions src/util/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,10 @@ export class ChainedAwaitable extends Awaitable<void, void> {
if (skipController) this.registerSkipController(skipController);
}

addTask(task: [handler: ChainedAwaitableTaskHandler, skipController?: SkipController<void, []>]): this {
addTask(task?: [handler: ChainedAwaitableTaskHandler, skipController?: SkipController<void, []>]): this {
if (!task) {
return this;
}
this.tasks.push(task);
return this;
}
Expand Down Expand Up @@ -1031,11 +1034,11 @@ export class ChainedAwaitable extends Awaitable<void, void> {
return;
}
const [handler, skipController] = this.tasks.shift()!;
const awaitable = new Awaitable<void, void>(() => {
handler(awaitable);
}, skipController);
const awaitable = new Awaitable<void, void>(Awaitable.nothing, skipController);
this.current = awaitable;
this.current.then(() => this.onTaskComplete());

handler(awaitable);
}
}

Expand Down

0 comments on commit f294444

Please sign in to comment.