-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
155 lines (143 loc) · 5.27 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { BufferSound } from './types/index.js'
const DEFAULT_GROUPS = [
{ name: 'master', level: 1 }
]
const decode = async(ctx, url) => {
const res = await fetch(url)
const buff = await res.arrayBuffer()
return ctx.decodeAudioData(buff)
}
export class SoundKit {
// Support Vue components
install(app, { name = 'sound' } = {}) {
// Vue 3
if (app.config) {
app.config.globalProperties[`$${name}`] = this
app.provide(`$${name}`, this)
// Vue 2
} else {
app.prototype[`$${name}`] = this
}
}
constructor({ defaultFadeDuration = 0.5 } = {}) {
this.defaultFadeDuration = defaultFadeDuration
}
init(groupConfig) {
this.context = new AudioContext()
this.groups = {}
this.addGroups(groupConfig || DEFAULT_GROUPS)
this.sounds = {}
}
// Create a group of sounds
addGroup(parent, { name, level = 1, muted }) {
// If channel already exists, ignore
if (this.groups[name]) return console.warn(`Group ${name} already exists!`)
const group = {
// If level is 0, set default to 1 otherwise fading can never work
defaultLevel: level ? level : 1,
level,
muted
}
// Create gain
const gainNode = this.context.createGain()
gainNode.channelCount = 2
gainNode.gain.value = muted ? 0 : level
group.gain = gainNode
// Connect group to gain
group.connector = group.gain
group.connector.connect(parent ? this.groups[parent].connector : this.context.destination)
// Save
this.groups[name] = group
}
// Add a group hierarchy at once
addGroups(groups, parent) {
for (const group of groups) {
this.addGroup(parent, group)
// Recursively add children
if (group.children) this.addGroups(group.children, group.name)
}
}
fadeIn(group, ...args) {
group = group || 'master'
return this.fadeTo(this.groups[group].defaultLevel, group, ...args)
}
fadeOut(...args) {
return this.fadeTo(0, ...args)
}
async fadeTo(value, groupName, duration, force = false) {
const group = this.groups[groupName || 'master']
duration = duration === undefined ? this.defaultFadeDuration : duration
// Don't fade if this group is muted
if (!force && group.muted) return Promise.resolve()
const gain = group.gain.gain
if (Math.abs(value - gain.value) < 0.03 && !force) return Promise.resolve()
// 25% of total time reaches 98.2% gain
// More info: https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime
gain.setTargetAtTime(value, 0, duration / 4)
await new Promise(res => setTimeout(res, duration * 1000))
group.level = value
}
load(sounds) {
const tasks = []
for (const [key, sound] of Object.entries(sounds)) {
// Ignore if this sound already exists
if (this.sounds[key]) {
tasks.push(this.sounds[key].loading)
continue
}
// Kick-off the loading tasks for this sound
// Don't `await` here or multiple successive calls could cause buffers
// to be decoded multiple times
const loading = decode(this.context, sound)
this.sounds[key] = {
loading,
instances: []
}
tasks.push(loading)
// Resolve to buffer when done loading
loading.then(buff => this.sounds[key].buffer = buff)
}
return Promise.all(tasks)
}
play(key, options) {
// Ensure sound is available
if (!this.sounds[key]) {
return console.warn(`Sound ${key} not found!`)
}
if (!this.sounds[key].buffer) {
return console.warn(`Sound ${key} not yet loaded!`)
}
const sound = this._play(BufferSound, this.sounds[key].buffer, options)
// Track instances and remove when done
const instances = this.sounds[key].instances
instances.push(sound)
sound.on('end', () => {
instances.splice(instances.indexOf(sound), 1)
})
return sound
}
_play(soundClass, arg, { group, loop, playbackRate } = {}) {
// Connect to different group if required
let destination = this.groups.master
if (group) destination = this.groups[group]
const sound = new soundClass(arg, this.context, destination, { loop, playbackRate })
sound.play()
return sound
}
setGain(group, value) {
if (this.groups[group].muted) return
this.groups[group].level = value
this.groups[group].gain.gain.setValueAtTime(parseFloat(value), 0)
}
stop() {
// Close/remove audio context
if (this.context && this.context.state !== 'closed') this.context.close()
this.context = undefined
}
mute(groupName, onOrOff) {
const group = this.groups[groupName || 'master']
if (!group.muted) group.muted = false
group.muted = onOrOff !== undefined ? onOrOff : !group.muted
return this.fadeTo(group.muted ? 0 : group.defaultLevel, groupName, undefined, true)
}
}