Skip to content

Commit

Permalink
feat(map): play zone music when available
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak committed Jan 16, 2024
1 parent b2c4587 commit 5559c88
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 7 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"three.js"
],
"dependencies": {
"@wowserhq/format": "^0.17.0"
"@wowserhq/format": "^0.18.0"
},
"peerDependencies": {
"three": "^0.160.0"
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export * from './db/DbManager.js';
export * from './light/SceneLight.js';
export * from './map/MapManager.js';
export * from './model/ModelManager.js';
export * from './sound/SoundManager.js';
export * from './texture/TextureManager.js';
export * from './util.js';
15 changes: 13 additions & 2 deletions src/lib/map/MapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import MapLoader from './loader/MapLoader.js';
import { MapAreaSpec, MapSpec } from './loader/types.js';
import MapLight from './light/MapLight.js';
import DbManager from '../db/DbManager.js';
import SoundManager from '../sound/SoundManager.js';

const DEFAULT_VIEW_DISTANCE = 1277.0;
const DETAIL_DISTANCE_EXTENSION = MAP_CHUNK_HEIGHT;
Expand All @@ -24,6 +25,7 @@ type MapManagerOptions = {
host: AssetHost;
textureManager?: TextureManager;
dbManager?: DbManager;
soundManager?: SoundManager;
viewDistance?: number;
};

Expand All @@ -44,6 +46,7 @@ class MapManager extends EventTarget {
#terrainManager: TerrainManager;
#doodadManager: DoodadManager;
#dbManager: DbManager;
#soundManager: SoundManager;

#mapLight: MapLight;

Expand Down Expand Up @@ -75,6 +78,8 @@ class MapManager extends EventTarget {

this.#textureManager = options.textureManager ?? new TextureManager({ host: options.host });
this.#dbManager = options.dbManager ?? new DbManager({ host: options.host });
this.#soundManager =
options.soundManager ?? new SoundManager({ host: options.host, dbManager: this.#dbManager });
this.#loader = new MapLoader({ host: options.host });

this.#mapLight = new MapLight({ dbManager: this.#dbManager });
Expand Down Expand Up @@ -160,7 +165,7 @@ class MapManager extends EventTarget {
}

if (previousAreaTableId !== this.#targetAreaTableId) {
this.#handleAreaTableIdChange();
this.#handleAreaTableChange();
}
}

Expand Down Expand Up @@ -198,7 +203,7 @@ class MapManager extends EventTarget {
}
}

#handleAreaTableIdChange() {
#handleAreaTableChange() {
if (!this.#areaTableDb || !this.#targetAreaTableId) {
return;
}
Expand All @@ -210,6 +215,12 @@ class MapManager extends EventTarget {

const parentAreaTableRecord = this.#areaTableDb.getRecord(areaTableRecord.parentAreaId);

// Sound

this.#soundManager.setZoneMusic(areaTableRecord.zoneMusic);

// Event

const detail = {
areaName: areaTableRecord.areaName,
areaId: areaTableRecord.id,
Expand Down
151 changes: 151 additions & 0 deletions src/lib/sound/SoundManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as THREE from 'three';
import { ClientDb, SoundEntriesRecord, ZoneMusicRecord } from '@wowserhq/format';
import DbManager from '../db/DbManager.js';
import { AssetHost, loadAsset, normalizePath } from '../asset.js';
import ZoneMusic from './ZoneMusic.js';
import zoneMusic from './ZoneMusic.js';

type SoundManagerOptions = {
host: AssetHost;
dbManager?: DbManager;
};

class SoundManager {
#host: AssetHost;

#loadedSounds = new Map<string, AudioBuffer>();
#loadingSounds = new Map<string, Promise<AudioBuffer>>();

#context: AudioContext;
#listener: THREE.AudioListener;
#camera: THREE.Camera;

#dbManager: DbManager;
#zoneMusicDb: ClientDb<ZoneMusicRecord>;
#soundEntriesDb: ClientDb<SoundEntriesRecord>;

#desiredZoneMusicId: number;
#currentZoneMusicId: number;
#zoneMusic: ZoneMusic;

#musicSource: THREE.Audio;

constructor(options: SoundManagerOptions) {
this.#host = options.host;
this.#dbManager = options.dbManager ?? new DbManager({ host: options.host });

this.#context = THREE.AudioContext.getContext();
this.#listener = new THREE.AudioListener();
this.#musicSource = new THREE.Audio(this.#listener);
this.#zoneMusic = new ZoneMusic(this.#musicSource);

this.#loadDbs().catch((error) => console.error(error));
this.#syncZoneMusic().catch((error) => console.error(error));
}

get camera() {
return this.#camera;
}

set camera(camera: THREE.Camera) {
this.#camera = camera;
this.#camera.add(this.#listener);
}

setZoneMusic(zoneMusicId: number) {
this.#desiredZoneMusicId = zoneMusicId;
}

async #loadDbs() {
this.#zoneMusicDb = await this.#dbManager.get('ZoneMusic.dbc', ZoneMusicRecord);
this.#soundEntriesDb = await this.#dbManager.get('SoundEntries.dbc', SoundEntriesRecord);
}

async #syncZoneMusic() {
if (!this.#zoneMusicDb || !this.#soundEntriesDb) {
requestAnimationFrame(() => this.#syncZoneMusic().catch((error) => console.error(error)));
return;
}

if (this.#desiredZoneMusicId !== this.#currentZoneMusicId) {
await this.#updateZoneMusic(this.#desiredZoneMusicId);
}

requestAnimationFrame(() => this.#syncZoneMusic().catch((error) => console.error(error)));
}

async #updateZoneMusic(zoneMusicId: number) {
if (this.#currentZoneMusicId === zoneMusicId) {
return;
}

// Let current zone music finish gracefully
this.#zoneMusic.suspend();

const musicRecord = this.#zoneMusicDb.getRecord(zoneMusicId);
if (!musicRecord) {
this.#currentZoneMusicId = zoneMusicId;
return;
}

const soundRecords = musicRecord.sounds.map((soundEntriesId) =>
this.#soundEntriesDb.getRecord(soundEntriesId),
);

const sounds = {};
for (const soundRecord of soundRecords) {
for (const file of soundRecord.file) {
if (file.trim().length === 0) {
continue;
}

const path = `${soundRecord.directoryBase}\\${file.trim()}`;
const buffer = await this.#getSound(path);
sounds[file] = buffer;
}
}

this.#zoneMusic.update(musicRecord, soundRecords, sounds);

// Resume with new zone music
this.#zoneMusic.resume();

this.#currentZoneMusicId = zoneMusicId;
}

#getSound(path: string): Promise<AudioBuffer> {
const refId = normalizePath(path);

const loaded = this.#loadedSounds.get(refId);
if (loaded) {
return Promise.resolve(loaded);
}

const alreadyLoading = this.#loadingSounds.get(refId);
if (alreadyLoading) {
return alreadyLoading;
}

const loading = this.#loadSound(refId, path);
this.#loadingSounds.set(refId, loading);

return loading;
}

async #loadSound(refId: string, path: string) {
let buffer: AudioBuffer;
try {
const data = await loadAsset(this.#host, path);
buffer = await this.#context.decodeAudioData(data);

this.#loadedSounds.set(refId, buffer);
} finally {
this.#loadingSounds.delete(refId);
}

return buffer;
}
}

export default SoundManager;
export { SoundManager };
92 changes: 92 additions & 0 deletions src/lib/sound/ZoneMusic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as THREE from 'three';
import { SoundEntriesRecord, ZoneMusicRecord } from '@wowserhq/format';
import { SOUND_AMBIENCE } from './const.js';
import { getRandomInt } from './util.js';

class ZoneMusic {
#musicRecord: ZoneMusicRecord;
#soundRecords: SoundEntriesRecord[];
#sounds: Record<string, AudioBuffer>;
#ambience: SOUND_AMBIENCE;
#audioSource: THREE.Audio;
#scheduleTimeout: ReturnType<typeof setTimeout>;
#lastPlayed: number;

constructor(audioSource: THREE.Audio) {
this.#audioSource = audioSource;
this.#ambience = SOUND_AMBIENCE.AMBIENCE_DAY;
}

get ambience() {
return this.#ambience;
}

set ambience(ambience: SOUND_AMBIENCE) {
this.#ambience = ambience;
}

resume() {
this.#schedule();
}

stop() {
this.#audioSource.stop();
this.#audioSource.onEnded = null;

this.suspend();
}

suspend() {
clearTimeout(this.#scheduleTimeout);
this.#scheduleTimeout = null;
}

update(
musicRecord: ZoneMusicRecord,
soundRecords: SoundEntriesRecord[],
sounds: Record<string, AudioBuffer>,
) {
this.#musicRecord = musicRecord;
this.#soundRecords = soundRecords;
this.#sounds = sounds;
}

#play() {
const soundEntries = this.#soundRecords[this.#ambience];
const soundFiles = soundEntries.file.filter((file) => file.trim().length > 0);

// TODO use freq as weight
const soundFile = soundFiles[getRandomInt(0, soundFiles.length - 1)];

const soundBuffer = this.#sounds[soundFile];
this.#audioSource.setBuffer(soundBuffer);

this.#audioSource.onEnded = () => {
this.#audioSource.isPlaying = false;
this.#lastPlayed = Date.now();
this.#scheduleTimeout = null;

this.#schedule();
};

this.#audioSource.play();
}

#schedule() {
if (this.#audioSource.isPlaying || this.#scheduleTimeout) {
return;
}

const silenceMin = this.#musicRecord.silenceIntervalMin[this.#ambience];
const silenceMax = this.#musicRecord.silenceIntervalMax[this.#ambience];
const silence = getRandomInt(silenceMin, silenceMax);

// Truncate silence by last played
const elapsed = this.#lastPlayed ? Date.now() - this.#lastPlayed : 0;
const delay = Math.max(silence - elapsed, 0);

this.#scheduleTimeout = setTimeout(() => this.#play(), delay);
}
}

export default ZoneMusic;
7 changes: 7 additions & 0 deletions src/lib/sound/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum SOUND_AMBIENCE {
AMBIENCE_DAY = 0,
AMBIENCE_NIGHT,
AMBIENCE_COUNT,
}

export { SOUND_AMBIENCE };
7 changes: 7 additions & 0 deletions src/lib/sound/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const getRandomInt = (min: number, max: number) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};

export { getRandomInt };

0 comments on commit 5559c88

Please sign in to comment.