Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
60 changes: 60 additions & 0 deletions packages/sound/README.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 28 additions & 0 deletions packages/sound/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
29 changes: 29 additions & 0 deletions packages/sound/src/Sound.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
26 changes: 26 additions & 0 deletions packages/sound/src/SoundContentStore.ts
Original file line number Diff line number Diff line change
@@ -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<Sound> {
constructor(
resource: Sound,
public url: string,
public requestConfig: any
) {
super(resource);
}

override restoreContent(): AssetPromise<Sound> {
return request<ArrayBuffer>(this.url, this.requestConfig).then((audio) => {
return GlobalAudioContext.decodeAudioData(audio);
}).then((audio) => {
const resource = this.resource;
resource.setAudioSource(audio);
return resource;
});
}
}
53 changes: 53 additions & 0 deletions packages/sound/src/SoundLoader.ts
Original file line number Diff line number Diff line change
@@ -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<Sound> {

override load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<Sound> {
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 = <any>{
...item,
type: "arraybuffer"
};
this.request<ArrayBuffer>(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);
});
});
}
}
75 changes: 75 additions & 0 deletions packages/sound/src/SoundPlayer.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
18 changes: 18 additions & 0 deletions packages/sound/src/global.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 4 additions & 0 deletions packages/sound/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import "./SoundLoader";

export * from "./Sound";
export * from "./SoundPlayer";
19 changes: 19 additions & 0 deletions packages/sound/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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/**/*"
]
}