Synth Class の実装

テスト

最終的には次のようなテストとなりました。前項との大きな違いは、一定時間後に停止するという非同期処理の部分を修正したことです。async / await と Promise Object を使って、500ms 後に停止を実行しています。
Synth.spec.js
1
import Synth from '/src/Synth'
2
import createAudioContext from '/src/WebAudioAPI/createAudioContext'
3
4
const audioContext = createAudioContext()
5
const nextNode = audioContext.destination
6
const synth = new Synth({ audioContext, nextNode })
7
const now = audioContext.currentTime
8
9
// 再生時に期待する動き
10
describe('play', () => {
11
const options = {
12
midiNoteNumber: 70,
13
startTime: now,
14
}
15
16
it('state', () => {
17
synth.play(options)
18
19
const state = synth.oscillatorArray[options.midiNoteNumber].state
20
const expectedState = {
21
play: true,
22
midiNoteNumber: 70,
23
}
24
25
expect(state).toEqual(expectedState)
26
})
27
28
it('osc', () => {
29
const osc = synth.oscillatorArray[options.midiNoteNumber].osc
30
31
expect(!!osc).toBe(true)
32
})
33
})
34
35
// 停止時に期待する動き
36
describe('stop', () => {
37
const options = {
38
midiNoteNumber: 70,
39
}
40
41
it('state', async () => {
42
const promise = new Promise((resolve, reject) => {
43
setTimeout(() => {
44
synth.stop(options)
45
resolve()
46
}, 500)
47
})
48
await promise
49
50
const state = synth.oscillatorArray[options.midiNoteNumber].state
51
const expectedState = {
52
play: false,
53
midiNoteNumber: null,
54
}
55
56
expect(state).toEqual(expectedState)
57
58
const osc = synth.oscillatorArray[options.midiNoteNumber].osc
59
60
expect(!!osc).toBe(false)
61
})
62
63
it('osc', () => {
64
const osc = synth.oscillatorArray[options.midiNoteNumber].osc
65
expect(!!osc).toBe(false)
66
})
67
})
68
69
// すでに停止している音を停止させる時の挙動
70
describe('double stop', () => {
71
const options = {
72
midiNoteNumber: 70,
73
}
74
75
it('state', () => {
76
const state = synth.oscillatorArray[options.midiNoteNumber].state
77
const expectedState = {
78
play: false,
79
midiNoteNumber: null,
80
}
81
82
expect(state).toEqual(expectedState)
83
})
84
85
it('osc', () => {
86
const osc = synth.oscillatorArray[options.midiNoteNumber].osc
87
expect(!!osc).toBe(false)
88
})
89
})
90
Copied!

Synth Class 本体

Synth.js
1
import createOscillator from '/src/WebAudioAPI/createOscillator'
2
3
class Synth {
4
constructor({ audioContext, nextNode }) {
5
this.audioContext = audioContext
6
this.output = nextNode
7
this.oscillatorArray = this.createOscillatorArray()
8
this.createOscillator = createOscillator({ audioContext })
9
}
10
11
createOscillatorArray() {
12
const vacantArray = Array.from({ length: 128 })
13
return vacantArray.map(o => ({
14
state: { midiNoteNumber: null, play: false },
15
osc: null,
16
}))
17
}
18
19
createGain(volume = 0.2) {
20
const gainNode = this.audioContext.createGain()
21
gainNode.gain.value = volume
22
return gainNode
23
}
24
25
play({ midiNoteNumber, startTime = 0 }) {
26
const osc = this.createOscillator({ midiNoteNumber })
27
const gain = this.createGain()
28
osc.connect(gain)
29
gain.connect(this.output)
30
osc.start(startTime)
31
32
const state = {
33
play: true,
34
midiNoteNumber,
35
}
36
37
const item = {
38
osc,
39
state,
40
}
41
42
this.oscillatorArray[midiNoteNumber] = item
43
return `play ${midiNoteNumber}`
44
}
45
46
stop({ midiNoteNumber, endTime = 0 }) {
47
const targetOscillator = this.oscillatorArray[midiNoteNumber].osc
48
if (!targetOscillator) {
49
return `not ${midiNoteNumber} playing`
50
}
51
52
targetOscillator.stop(endTime)
53
54
const state = {
55
play: false,
56
midiNoteNumber: null,
57
}
58
this.oscillatorArray[midiNoteNumber].state = state
59
this.oscillatorArray[midiNoteNumber].osc = null
60
return `stop ${midiNoteNumber}`
61
}
62
}
63
64
export default Synth
65
Copied!

実装のポイント

audioContext を外から受け取る

Synth クラスはインスタンスを作成する際に、audioContext を受け取ります。これによって、複数のシンセを作成しても、全て同じ audioContext から作成することができます。異なる audioContext から作成してしまうと時間等が一致しません。
1
const synth = new Synth({ audioContext, nextNode })
Copied!

次に接続する先を外から受け取る

同様に Synth クラスは、次の出力先である nextNode も受け取ります。これによって自由なルーティングを実現し、例えば直接 destination に出力するのではなく、新たに作ったリバーブモジュールに出力する、といったことが可能になります。

oscillatorArray の default 値を設定するメソッド

createOscillatorArray メソッドは、oscillatorArray の default 値を作成するための関数です。stateosc というプロパティを item として持つようにします。演奏状態が変更されるたびに、この配列の対応するプロパティの値が変更されます。
1
createOscillatorArray() {
2
const vacantArray = Array.from({ length: 128 })
3
return vacantArray.map(o => ({
4
state: { midiNoteNumber: null, play: false },
5
osc: null,
6
}))
7
}
Copied!

createOscillator

Synth.js
1
import createOscillator from '/src/WebAudioAPI/createOscillator'
2
this.createOscillator = createOscillator({ audioContext })
Copied!
import している createOscillator は、関数を返す関数、つまり高階関数になっています。この関数は、audioContext を受け取って、関数を返します。その結果、this.createOscillator() と実行することで、高階関数から返ってきて保存されていた関数が実行されます。
次のコードは、createOscillator です。先ほど述べたように、これは高階関数なので、実行すると関数を返します。
なぜこのようにする必要があるかというと、audioContext.createOscillator() で使用する audioContext は引数として一度受ければいいわけですが、midiNoteNumberwave は、音程が変更されるたびに引数として受けなくてはいけないからです。こういう場合には高階関数を利用するのがスマートです。
createOscillator.js
1
import convertMidiNoteToFrequency from '/src/Util/convertMidiNoteToFrequency'
2
3
const createOscillator = ({ audioContext }) => {
4
return ({ midiNoteNumber, wave = 'sine' }) => {
5
const osc = audioContext.createOscillator()
6
osc.type = wave
7
osc.frequency.value = convertMidiNoteToFrequency(midiNoteNumber)
8
return osc
9
}
10
}
11
12
export default createOscillator
13
Copied!

Synth Class をインスタンス化して使用する

作成した Synth Class をインスタンス化して音を鳴らしてみましょう。きっちりできましたね。
index.js
1
import Synth from '/src/Synth'
2
import createAudioContext from '/src/WebAudioAPI/createAudioContext'
3
4
const audioContext = createAudioContext()
5
const destination = audioContext.destination
6
7
const synth = new Synth({
8
audioContext,
9
nextNode: destination,
10
})
11
12
const option1 = {
13
midiNoteNumber: 75,
14
wave: 'sawtooth',
15
velocity: 100,
16
}
17
18
synth.play(option1)
19
20
const option2 = {
21
midiNoteNumber: 75,
22
}
23
24
setTimeout(() => synth.stop(option2), 2000)
Copied!
では次に MIDI キーボードを接続して、このブラウザシンセサイザーを演奏できるようにしていきます。そのためには Web MIDI API を使用します。
Last modified 3yr ago