Skip to content

Commit

Permalink
Basic audio system for engine sound
Browse files Browse the repository at this point in the history
  • Loading branch information
ruben3d committed Dec 11, 2022
1 parent 1b8dd4c commit 4431939
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 10 deletions.
Binary file added assets/engine-loop-01.ogg
Binary file not shown.
Binary file added assets/engine-loop-02.ogg
Binary file not shown.
Binary file added dist/assets/engine-loop-01.ogg
Binary file not shown.
Binary file added dist/assets/engine-loop-02.ogg
Binary file not shown.
185 changes: 180 additions & 5 deletions dist/bundle.js

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions src/script/audio/audioResources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { assertIsDefined } from "../utils/asserts";


export enum AudioResourceState {
LOADING,
READY,
ERROR // Just in case I may decide to try again due to network issues...
};

export type AudioResourceListener = (resource: AudioResource) => void;

export class AudioResource {
private state: AudioResourceState = AudioResourceState.LOADING;
private buffer: AudioBuffer | undefined;

constructor(public readonly url: string, context: AudioContext, listener: AudioResourceListener) {
const request = new XMLHttpRequest();
request.responseType = 'arraybuffer';
request.open('GET', url, true);
request.onload = () => {
context.decodeAudioData(request.response,
buffer => {
this.buffer = buffer;
this.state = AudioResourceState.READY;
listener(this);
},
e => {
this.state = AudioResourceState.ERROR;
console.error(`Error decoding audio data from resource "${url}".`, e);
});
};
request.onerror = () => {
this.state = AudioResourceState.ERROR;
console.error(`Error loading audio resource "${url}"`);
};
request.send();
}

getBuffer(): AudioBuffer {
assertIsDefined(this.buffer, `Trying to use an invalid AudioResource: ${this.url}`);
return this.buffer;
}

get isReady(): boolean { return this.state === AudioResourceState.READY; }
}

interface AudioResourceWrapper {
resource: AudioResource;
listeners: AudioResourceListener[];
}

export class AudioResourceManager {
private resources: Map<string, AudioResourceWrapper> = new Map();

constructor(private context: AudioContext) { }

load(url: string, listener: AudioResourceListener): AudioResource {
let wrapper = this.resources.get(url);

if (!wrapper) {
const resource = new AudioResource(url, this.context, resource => this.onResourceLoaded(resource));
wrapper = { resource, listeners: [listener] };
this.resources.set(url, wrapper);
} else if (!wrapper.resource.isReady) {
wrapper.listeners.push(listener);
}

return wrapper.resource;
}

private onResourceLoaded(resource: AudioResource) {
const wrapper = this.resources.get(resource.url);
assertIsDefined(wrapper);
wrapper.listeners.forEach(l => l(resource));
wrapper.listeners.length = 0;
}
}
65 changes: 65 additions & 0 deletions src/script/audio/audioSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AudioResourceManager } from "./audioResources";

export class AudioClip {
private audioSource: AudioBufferSourceNode | undefined;
private gainNode: GainNode | undefined;
private _rate = 1.0;
private _gain = 1.0;
private started = false;

constructor(url: string, private context: AudioContext, resources: AudioResourceManager, private loop: boolean) {
resources.load(url, resource => {
this.audioSource = new AudioBufferSourceNode(context, { buffer: resource.getBuffer(), loop, playbackRate: this._rate });
this.gainNode = new GainNode(context, { gain: this._gain });
this.audioSource.connect(this.gainNode).connect(context.destination);
});
}

set rate(value: number) {
this._rate = value;
if (this.audioSource) {
this.audioSource.playbackRate.value = value;
}
}

set gain(value: number) {
this._gain = value;
if (this.gainNode) {
this.gainNode.gain.value = value;
}
}

play() {
if (!this.started) {
this.started = true;
this.audioSource?.start();
} else {
this.gainNode?.connect(this.context.destination);
}
}

stop() {
this.gainNode?.disconnect(this.context.destination);
}
}

export class AudioSystem {
private context = new AudioContext();
private resources = new AudioResourceManager(this.context);
private globals: Map<string, AudioClip> = new Map();

getGlobal(url: string, loop: boolean): AudioClip {
let clip = this.globals.get(url);

if (!clip) {
clip = new AudioClip(url, this.context, this.resources, loop);
this.globals.set(url, clip);
}

return clip;
}

getInstance(url: string, loop: boolean): AudioClip {
return new AudioClip(url, this.context, this.resources, loop);
}
}
5 changes: 3 additions & 2 deletions src/script/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AudioSystem } from './audio/audioSystem';
import { ConfigService } from './config/configService';
import { DefaultPalette, PaletteCategory } from './config/palettes/palette';
import { CGAProfile } from './config/profiles/cga';
Expand Down Expand Up @@ -36,8 +37,8 @@ function setup(): [Kernel, ConfigService, KeyboardControlDevice, JoystickControl
new MountainModelLibBuilder('hill', 700, 300, PaletteCategory.SCENERY_MOUNTAIN_GRASS),
new MountainModelLibBuilder('mountain', 1400, 600, PaletteCategory.SCENERY_MOUNTAIN_BARE)
]);

const game = new Game(config, models, materials, renderer);
const audio = new AudioSystem();
const game = new Game(config, models, materials, renderer, audio);
game.setup();

const keyboardInput = new KeyboardControlDevice(game.getPlayer());
Expand Down
28 changes: 27 additions & 1 deletion src/script/scene/entities/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Model } from '../models/models';
import { Palette } from "../../config/palettes/palette";
import { FORWARD, RIGHT, Scene, SceneLayers, UP } from "../scene";
import { GroundTargetEntity } from './groundTarget';
import { AudioClip } from '../../audio/audioSystem';


export class PlayerEntity implements Entity {
Expand All @@ -19,6 +20,9 @@ export class PlayerEntity implements Entity {
private shadowQuaternion = new THREE.Quaternion();
private shadowScale = new THREE.Vector3();

private engineAudio: AudioClip;
private enginePlaying: boolean = false;

private obj = new THREE.Object3D();

private pitch: number = 0; // [-1, 1]
Expand All @@ -40,11 +44,12 @@ export class PlayerEntity implements Entity {
exteriorView: boolean = false;

// Bearing increases CCW, radians
constructor(model: Model, shadow: Model, position: THREE.Vector3, bearing: number) {
constructor(model: Model, shadow: Model, engineAudio: AudioClip, position: THREE.Vector3, bearing: number) {
this.lodHelper = new LODHelper(model, DEFAULT_LOD_BIAS);
this.lodHelperShadow = new LODHelper(shadow, 5);
this.obj.position.copy(position);
this.obj.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), bearing);
this.engineAudio = engineAudio;
}

init(scene: Scene): void {
Expand Down Expand Up @@ -101,6 +106,27 @@ export class PlayerEntity implements Entity {
if (this.obj.position.x < -terrainHalfSize) this.obj.position.x = terrainHalfSize;
if (this.obj.position.z > terrainHalfSize) this.obj.position.z = -terrainHalfSize;
if (this.obj.position.z < -terrainHalfSize) this.obj.position.z = terrainHalfSize;

this.updateAudio();
}

private updateAudio() {
if (this.throttle > 0) {
if (this.enginePlaying === false) {
this.engineAudio.play();
this.enginePlaying = true;
}
const x = this.throttle;
const factorRate = 1 - (1 - x) * (1 - x); // easeOutQuad
const factorGain = 1 - Math.pow(1 - x, 5); // easeOutQuint
this.engineAudio.rate = 0.25 + 1.75 * factorRate;
this.engineAudio.gain = 1.0 * factorGain;
} else {
if (this.enginePlaying === true) {
this.engineAudio.stop();
this.enginePlaying = false;
}
}
}

render3D(targetWidth: number, targetHeight: number, camera: THREE.Camera, lists: Map<string, THREE.Scene>, palette: Palette): void {
Expand Down
11 changes: 9 additions & 2 deletions src/script/state/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { EGAMidnightPalette } from '../config/palettes/ega-midnight';
import { SVGANoonPalette } from '../config/palettes/svga-noon';
import { SVGAMidnightPalette } from '../config/palettes/svga-midnight';
import { CGAMidnightPalette } from '../config/palettes/cga-midnight';
import { AudioSystem } from '../audio/audioSystem';


const MAIN_RENDER_TARGET_LO = 'MAIN_RENDER_TARGET_LO';
Expand Down Expand Up @@ -99,13 +100,19 @@ export class Game {
private cockpitEntities: Entity[] = [];
private exteriorEntities: Entity[] = [];

constructor(private configService: ConfigService, private models: ModelManager, private materials: SceneMaterialManager, private renderer: Renderer) {
constructor(private configService: ConfigService, private models: ModelManager, private materials: SceneMaterialManager, private renderer: Renderer,
private audio: AudioSystem) {

this.playerCamera = new SceneCamera(new THREE.PerspectiveCamera(COCKPIT_FOV, H_RES / V_RES, PLANE_DISTANCE_TO_GROUND, COCKPIT_FAR));
this.targetCamera = new SceneCamera(new THREE.PerspectiveCamera(COCKPIT_FOV, 1, PLANE_DISTANCE_TO_GROUND, COCKPIT_FAR));
this.mapCamera = new THREE.OrthographicCamera(-10000, 10000, 10000, -10000, 10, 1000);
this.mapCamera.setRotationFromAxisAngle(RIGHT, -Math.PI / 2);
this.mapCamera.position.set(0, 500, 0);
this.player = new PlayerEntity(this.models.getModel('assets/f22.glb'), this.models.getModel('assets/f22_shadow.gltf'), new THREE.Vector3(1500, PLANE_DISTANCE_TO_GROUND, -1160), Math.PI);
this.player = new PlayerEntity(
this.models.getModel('assets/f22.glb'),
this.models.getModel('assets/f22_shadow.gltf'),
this.audio.getGlobal('assets/engine-loop-02.ogg', true),
new THREE.Vector3(1500, PLANE_DISTANCE_TO_GROUND, -1160), Math.PI);
this.cameraUpdaters.set(PlayerViewState.COCKPIT_FRONT, new CockpitFrontCameraUpdater(this.player, this.playerCamera.main));
this.cameraUpdaters.set(PlayerViewState.EXTERIOR_BEHIND, new ExteriorBehindCameraUpdater(this.player, this.playerCamera.main));
this.cameraUpdaters.set(PlayerViewState.EXTERIOR_LEFT, new ExteriorSideCameraUpdater(this.player, this.playerCamera.main, ExteriorSide.LEFT));
Expand Down

0 comments on commit 4431939

Please sign in to comment.