diff --git a/packages/sound/README.md b/packages/sound/README.md new file mode 100644 index 00000000..77654f0f --- /dev/null +++ b/packages/sound/README.md @@ -0,0 +1,60 @@ +# Sound + +A sound library for playing audio files + +## Features +- loop: support changing loop; +- volume: support changing volume; +- speed: support changing playbackRate; + +## npm + +The `Sound` is published on npm with full typing support. To install, use: + +```sh +npm install @galacean/engine-toolkit-sound +``` + +This will allow you to import tween entirely using: + +```javascript +import * as SOUND from "@galacean/engine-toolkit-sound"; +``` + +or individual classes using: + +```javascript +import { SoundPlayer } from "@galacean/engine-toolkit-sound"; +``` + +## How to use +Step 1: Loading file +```typescript +await engine.resourceManager.load([{ + url: "./test.mp3", + type: "audio", +}]); +// or just use url string. file suffix could be mp3, wav or ogg +await engine.resourceManager.load(["./test.mp3"]); +``` +Step 2: Add sound player component +```typescript +const player = entity.addComponent(SoundPlayer); +player.sound = engine.resourceManager.getFromCache("./test.mp3"); +``` +Step 3: You can set some options and play sound +```typescript +player.volume = 0.5; +player.playbackRate = 0.5; +player.loop = true; +player.play(); +``` + +### Attention: Make sure user should have already made a gesture before playing a sound. +## Links + +- [Repository](https://github.com/galacean/engine-toolkit) + +## License + +The engine is released under the [MIT](https://opensource.org/licenses/MIT) license. See LICENSE file. diff --git a/packages/sound/package.json b/packages/sound/package.json new file mode 100644 index 00000000..d72f8fdd --- /dev/null +++ b/packages/sound/package.json @@ -0,0 +1,28 @@ +{ + "name": "@galacean/engine-toolkit-sound", + "version": "1.1.0-beta.0", + "license": "MIT", + "scripts": { + "b:types": "tsc" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "homepage": "https://oasisengine.cn/", + "repository": { + "type": "git", + "url": "https://github.com/galacean/engine-toolkit" + }, + "bugs": "https://github.com/galacean/engine-toolkit/issues", + "types": "types/index.d.ts", + "module": "dist/es/index.js", + "main": "dist/commonjs/browser.js", + "files": [ + "dist/**/*", + "types/**/*" + ], + "peerDependencies": { + "@galacean/engine": "^1.1.0-alpha" + } +} diff --git a/packages/sound/src/Sound.ts b/packages/sound/src/Sound.ts new file mode 100644 index 00000000..269c9a80 --- /dev/null +++ b/packages/sound/src/Sound.ts @@ -0,0 +1,29 @@ +import { Engine, ReferResource } from "@galacean/engine"; + +export class Sound extends ReferResource { + name: string; + buffer: AudioBuffer | null = null;; + constructor(engine: Engine, name: string = "") { + super(engine); + this.name = name; + } + + get duration(): number { + return this.buffer?.duration ?? -1; + } + + setAudioSource(buffer: AudioBuffer) { + this.buffer = buffer; + } + + override destroy(): boolean { + if (this._destroyed) { + return false; + } + + this.buffer = null; + this._destroyed = true; + + return true; + } +} diff --git a/packages/sound/src/SoundContentStore.ts b/packages/sound/src/SoundContentStore.ts new file mode 100644 index 00000000..1bf8af86 --- /dev/null +++ b/packages/sound/src/SoundContentStore.ts @@ -0,0 +1,26 @@ +import { AssetPromise, ContentRestorer, request } from "@galacean/engine"; +import { GlobalAudioContext } from "./global"; +import { Sound } from "./Sound"; + +/** + * @internal + */ +export class AudioContentRestorer extends ContentRestorer { + constructor( + resource: Sound, + public url: string, + public requestConfig: any + ) { + super(resource); + } + + override restoreContent(): AssetPromise { + return request(this.url, this.requestConfig).then((audio) => { + return GlobalAudioContext.decodeAudioData(audio); + }).then((audio) => { + const resource = this.resource; + resource.setAudioSource(audio); + return resource; + }); + } +} diff --git a/packages/sound/src/SoundLoader.ts b/packages/sound/src/SoundLoader.ts new file mode 100644 index 00000000..b42b9de1 --- /dev/null +++ b/packages/sound/src/SoundLoader.ts @@ -0,0 +1,53 @@ +import { WebGLEngine } from "@galacean/engine"; +import { + AssetPromise, + Loader, + LoadItem, + resourceLoader, + ResourceManager +} from "@galacean/engine"; +import { GlobalAudioContext } from "./global"; +import { Sound } from "./Sound"; +import { AudioContentRestorer } from "./SoundContentStore"; +import { SoundPlayer } from "./SoundPlayer"; + +let hasAddListener = false; +function listener(e: Event) { + SoundPlayer.triggerUserGesture(); + e.target!.removeEventListener("pointerdown", listener); +} + +@resourceLoader("audio", ["mp3", "wav", "ogg"]) +class SoundLoader extends Loader { + + override load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + if (!hasAddListener) { + hasAddListener = true; + ((resourceManager.engine as WebGLEngine).canvas._webCanvas as HTMLCanvasElement).addEventListener("pointerdown", listener); + } + return new AssetPromise((resolve, reject) => { + const url = item.url!; + const requestConfig = { + ...item, + type: "arraybuffer" + }; + this.request(url, requestConfig) + .then((buffer) => { + return GlobalAudioContext.decodeAudioData(buffer); + }).then((buffer) => { + const sound = new Sound(resourceManager.engine); + sound.setAudioSource(buffer); + if (url.indexOf("data:") !== 0) { + const index = url.lastIndexOf("/"); + sound.name = url.substring(index + 1); + } + + resourceManager.addContentRestorer(new AudioContentRestorer(sound, url, requestConfig)); + resolve(sound); + }) + .catch((e) => { + reject!(e); + }); + }); + } +} diff --git a/packages/sound/src/SoundPlayer.ts b/packages/sound/src/SoundPlayer.ts new file mode 100644 index 00000000..35c18288 --- /dev/null +++ b/packages/sound/src/SoundPlayer.ts @@ -0,0 +1,75 @@ +import { Component } from "@galacean/engine"; +import { makeSound } from "./global"; +import { Sound } from "./Sound"; + +export class SoundPlayer extends Component { + private _ctx = new AudioContext(); + private _gainNode = this._ctx.createGain(); + private _source: AudioBufferSourceNode | null = null; + private _volume = 1; + private _playbackRate = 1; + private static _isUserGestureTriggered = false; + + static triggerUserGesture() { + SoundPlayer._isUserGestureTriggered = true; + makeSound(); + } + + loop = false; + sound: Sound | null = null;; + + get volume(): number { + return this._volume; + } + + set volume(v: number) { + this._gainNode.gain.value = v; + this._volume = v; + } + + get playbackRate(): number { + return this._playbackRate; + } + + set playbackRate(v: number) { + if (this._source) { + this._source.playbackRate.value = v; + } + this._playbackRate = v; + } + + play() { + if (!SoundPlayer._isUserGestureTriggered) { + console.warn("User gesture should be triggered first before audio created or play."); + return; + } + if (this.sound) { + this.stop(); + this._source = this._ctx.createBufferSource(); + this._source.buffer = this.sound.buffer; + this._source.connect(this._gainNode).connect(this._ctx.destination); + this._source.playbackRate.value = this._playbackRate; + this._source.loop = this.loop; + this._source.start(); + } + } + + pause() { + if (this._ctx.state === "running") { + this._ctx.suspend(); + } + } + + resume() { + if (this._ctx.state === "suspended") { + this._ctx.resume(); + } + } + + stop(when?: number) { + if (this._source) { + this._source.stop(when); + this._source.onended = this._source.disconnect; + } + } +} diff --git a/packages/sound/src/global.ts b/packages/sound/src/global.ts new file mode 100644 index 00000000..35a1575b --- /dev/null +++ b/packages/sound/src/global.ts @@ -0,0 +1,18 @@ +export const GlobalAudioContext = new AudioContext(); + +export function makeSound() { + const osc = GlobalAudioContext.createOscillator(); + const g = GlobalAudioContext.createGain(); + + osc.connect(g); + osc.frequency.value = 1; + + const wave = GlobalAudioContext.createPeriodicWave(new Float32Array(2), new Float32Array(2)); + osc.setPeriodicWave(wave); + g.connect(GlobalAudioContext.destination); + g.gain.value = 0; + osc.start(); + g.gain.linearRampToValueAtTime(0.6, GlobalAudioContext.currentTime + 0.01); + osc.stop(GlobalAudioContext.currentTime + 0.01); + g.gain.exponentialRampToValueAtTime(0.01, GlobalAudioContext.currentTime + 0.01); +} diff --git a/packages/sound/src/index.ts b/packages/sound/src/index.ts new file mode 100644 index 00000000..846196f2 --- /dev/null +++ b/packages/sound/src/index.ts @@ -0,0 +1,4 @@ +import "./SoundLoader"; + +export * from "./Sound"; +export * from "./SoundPlayer"; diff --git a/packages/sound/tsconfig.json b/packages/sound/tsconfig.json new file mode 100644 index 00000000..3939a36c --- /dev/null +++ b/packages/sound/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "sourceMap": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "incremental": false + }, + "include": [ + "src/**/*" + ] +}