craft-native 0.0.149 → 0.0.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +13 -1
- package/dist/ios/templates/CraftNativeScreen.swift +24 -263
- package/dist/ios/templates/CraftNativeServices.swift +6 -3
- package/dist/ios/templates/CraftWebAudio.swift +2269 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2269 @@
|
|
|
1
|
+
import Accelerate
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import Foundation
|
|
4
|
+
import JavaScriptCore
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
// MARK: - Web Audio on native screens
|
|
8
|
+
//
|
|
9
|
+
// JavaScriptCore has no Web Audio. A native screen gets the standard
|
|
10
|
+
// `AudioContext` API anyway (the script at the end of this file), so code
|
|
11
|
+
// written for a page - synthesised music, breath tones, filtered noise - runs
|
|
12
|
+
// unchanged. The script sends the graph it builds here, and each context
|
|
13
|
+
// renders that graph itself, in 128-frame quanta as the Web Audio spec does,
|
|
14
|
+
// inside one AVAudioSourceNode on the screen's AVAudioEngine.
|
|
15
|
+
//
|
|
16
|
+
// Why not one AVAudioUnit per node: a Web Audio graph has cycles through
|
|
17
|
+
// delays (echo feedback), nodes that drive another node's parameter (an LFO
|
|
18
|
+
// on a gain) and a stream of short-lived nodes (a step sequencer makes a few
|
|
19
|
+
// dozen a second). AVAudioEngine allows none of those, so the graph is
|
|
20
|
+
// rendered here, with Accelerate for the heavy parts.
|
|
21
|
+
//
|
|
22
|
+
// Supported: AudioContext (currentTime, sampleRate, state, resume, suspend,
|
|
23
|
+
// close, statechange), AudioParam (value, setValueAtTime,
|
|
24
|
+
// linearRampToValueAtTime, exponentialRampToValueAtTime, setTargetAtTime,
|
|
25
|
+
// setValueCurveAtTime, cancelScheduledValues, cancelAndHoldAtTime, audio-rate
|
|
26
|
+
// connections into a param), GainNode, OscillatorNode (sine, square,
|
|
27
|
+
// sawtooth, triangle, frequency, detune), AudioBuffer + AudioBufferSourceNode
|
|
28
|
+
// (loop, loopStart/End, offset, duration, playbackRate, detune),
|
|
29
|
+
// BiquadFilterNode (all eight types), DelayNode (cycles clamp to one quantum,
|
|
30
|
+
// as the spec says), DynamicsCompressorNode, ConvolverNode (normalised as the
|
|
31
|
+
// spec says), connect/disconnect, start/stop, onended.
|
|
32
|
+
//
|
|
33
|
+
// Approximations, all inaudible for synthesis: square and sawtooth are
|
|
34
|
+
// PolyBLEP band-limited rather than the browser's wavetables; a biquad takes
|
|
35
|
+
// its coefficients once per quantum (2.9 ms); the compressor has no 6 ms
|
|
36
|
+
// look-ahead; a 4-channel ("true stereo") impulse response uses its first two
|
|
37
|
+
// channels; every node is stereo inside.
|
|
38
|
+
//
|
|
39
|
+
// The session is `.ambient` and mixes with other apps: the silent switch
|
|
40
|
+
// mutes it, as it mutes a web view's Web Audio, and it stops when the app
|
|
41
|
+
// leaves the screen.
|
|
42
|
+
|
|
43
|
+
let craftAudioQuantum = 128
|
|
44
|
+
|
|
45
|
+
/// Where in the timeline a quantum is.
|
|
46
|
+
struct CraftAudioRender {
|
|
47
|
+
let quantum: Int64
|
|
48
|
+
let sampleRate: Double
|
|
49
|
+
|
|
50
|
+
var frame: Int64 { quantum * Int64(craftAudioQuantum) }
|
|
51
|
+
var time: Double { Double(frame) / sampleRate }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// An `AudioBuffer`'s samples as the script last handed them over. It never
|
|
55
|
+
/// changes, so a source that took it keeps playing it while the script
|
|
56
|
+
/// writes into its own copy.
|
|
57
|
+
final class CraftAudioBufferData {
|
|
58
|
+
let sampleRate: Double
|
|
59
|
+
let length: Int
|
|
60
|
+
let channels: [UnsafeMutablePointer<Float>]
|
|
61
|
+
|
|
62
|
+
init(sampleRate: Double, length: Int, channelCount: Int) {
|
|
63
|
+
self.sampleRate = sampleRate
|
|
64
|
+
self.length = max(1, length)
|
|
65
|
+
channels = (0..<max(1, channelCount)).map { _ in
|
|
66
|
+
let data = UnsafeMutablePointer<Float>.allocate(capacity: max(1, length))
|
|
67
|
+
data.initialize(repeating: 0, count: max(1, length))
|
|
68
|
+
return data
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
deinit { channels.forEach { $0.deallocate() } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - Automation
|
|
76
|
+
|
|
77
|
+
/// One AudioParam: its timeline of events and the values it takes in the
|
|
78
|
+
/// quantum being rendered.
|
|
79
|
+
final class CraftAudioParam {
|
|
80
|
+
enum Kind { case set, linear, exponential, target, curve }
|
|
81
|
+
|
|
82
|
+
struct Event {
|
|
83
|
+
var kind: Kind
|
|
84
|
+
var time: Double
|
|
85
|
+
var value: Double
|
|
86
|
+
var timeConstant = 0.0
|
|
87
|
+
var duration = 0.0
|
|
88
|
+
var curve: [Double] = []
|
|
89
|
+
/// When the script asked for a ramp: where a ramp after a
|
|
90
|
+
/// setTargetAtTime (or with nothing before it) starts.
|
|
91
|
+
var scheduledAt = 0.0
|
|
92
|
+
/// Where this event's segment starts, worked out once from the event
|
|
93
|
+
/// before it, so the events before can be dropped as time passes.
|
|
94
|
+
var fromTime = 0.0
|
|
95
|
+
var fromValue = 0.0
|
|
96
|
+
|
|
97
|
+
var isRamp: Bool { kind == .linear || kind == .exponential }
|
|
98
|
+
var end: Double { kind == .curve ? time + duration : time }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static let largest = Double(Float.greatestFiniteMagnitude)
|
|
102
|
+
|
|
103
|
+
let name: String
|
|
104
|
+
let minValue: Double
|
|
105
|
+
let maxValue: Double
|
|
106
|
+
/// a-rate (a value per frame) or k-rate (one per quantum).
|
|
107
|
+
let audioRate: Bool
|
|
108
|
+
private(set) var events: [Event] = []
|
|
109
|
+
private var intrinsic: Double
|
|
110
|
+
/// Nodes connected into this param; their output is added to its value.
|
|
111
|
+
var inputs: [CraftAudioNode] = []
|
|
112
|
+
let values: UnsafeMutablePointer<Float>
|
|
113
|
+
private let scratch: UnsafeMutablePointer<Float>
|
|
114
|
+
/// Every frame of `values` holds the same number.
|
|
115
|
+
private(set) var constant = true
|
|
116
|
+
/// The value without its inputs at the start of the last quantum: what
|
|
117
|
+
/// the script reads from `param.value`.
|
|
118
|
+
private(set) var current: Double
|
|
119
|
+
|
|
120
|
+
init(_ name: String, _ value: Double, min: Double = -largest, max: Double = largest, audioRate: Bool = true) {
|
|
121
|
+
self.name = name
|
|
122
|
+
minValue = min
|
|
123
|
+
maxValue = max
|
|
124
|
+
self.audioRate = audioRate
|
|
125
|
+
intrinsic = value
|
|
126
|
+
current = value
|
|
127
|
+
values = .allocate(capacity: craftAudioQuantum)
|
|
128
|
+
values.initialize(repeating: Float(value), count: craftAudioQuantum)
|
|
129
|
+
scratch = .allocate(capacity: craftAudioQuantum)
|
|
130
|
+
scratch.initialize(repeating: 0, count: craftAudioQuantum)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
deinit {
|
|
134
|
+
values.deallocate()
|
|
135
|
+
scratch.deallocate()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Adds an event after any others at the same time, as the spec orders them.
|
|
139
|
+
func schedule(_ event: Event) {
|
|
140
|
+
var index = events.count
|
|
141
|
+
while index > 0, events[index - 1].time > event.time { index -= 1 }
|
|
142
|
+
events.insert(event, at: index)
|
|
143
|
+
resolve(from: index)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// cancelScheduledValues (and, holding, cancelAndHoldAtTime).
|
|
147
|
+
func cancel(from time: Double, hold: Bool) {
|
|
148
|
+
let held = value(at: time)
|
|
149
|
+
events.removeAll { $0.time >= time || ($0.kind == .curve && $0.end > time) }
|
|
150
|
+
if hold { schedule(Event(kind: .set, time: time, value: held)) }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// The value the timeline gives at `time`, before inputs and clamping.
|
|
154
|
+
func value(at time: Double) -> Double {
|
|
155
|
+
var index = 0
|
|
156
|
+
while index < events.count, events[index].time <= time { index += 1 }
|
|
157
|
+
if index < events.count, events[index].isRamp, time >= events[index].fromTime {
|
|
158
|
+
return Self.ramp(events[index], at: time)
|
|
159
|
+
}
|
|
160
|
+
return index == 0 ? intrinsic : Self.after(events[index - 1], at: time)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Fills `values` for this quantum and drops events it no longer needs.
|
|
164
|
+
func compute(_ render: CraftAudioRender) {
|
|
165
|
+
let frames = craftAudioQuantum
|
|
166
|
+
let start = render.time
|
|
167
|
+
let step = 1 / render.sampleRate
|
|
168
|
+
let end = start + Double(frames) * step
|
|
169
|
+
if !audioRate || constant(from: start, to: end) {
|
|
170
|
+
let value = value(at: start)
|
|
171
|
+
current = value
|
|
172
|
+
values.update(repeating: Float(value), count: frames)
|
|
173
|
+
constant = true
|
|
174
|
+
} else {
|
|
175
|
+
for frame in 0..<frames {
|
|
176
|
+
values[frame] = Float(value(at: start + Double(frame) * step))
|
|
177
|
+
}
|
|
178
|
+
current = Double(values[0])
|
|
179
|
+
constant = false
|
|
180
|
+
}
|
|
181
|
+
var modulated = false
|
|
182
|
+
for input in inputs where input.pull(render) && !input.silent {
|
|
183
|
+
// A param's input is mixed down to one channel.
|
|
184
|
+
var half: Float = 0.5
|
|
185
|
+
vDSP_vadd(input.left, 1, input.right, 1, scratch, 1, vDSP_Length(frames))
|
|
186
|
+
vDSP_vsma(scratch, 1, &half, values, 1, values, 1, vDSP_Length(frames))
|
|
187
|
+
modulated = true
|
|
188
|
+
}
|
|
189
|
+
if modulated {
|
|
190
|
+
if audioRate { constant = false } else { values.update(repeating: values[0], count: frames) }
|
|
191
|
+
}
|
|
192
|
+
var low = Float(max(minValue, -Self.largest))
|
|
193
|
+
var high = Float(min(maxValue, Self.largest))
|
|
194
|
+
vDSP_vclip(values, 1, &low, &high, values, 1, vDSP_Length(frames))
|
|
195
|
+
while events.count >= 2, events[1].time <= end {
|
|
196
|
+
intrinsic = current
|
|
197
|
+
events.removeFirst()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private func constant(from start: Double, to end: Double) -> Bool {
|
|
202
|
+
var index = 0
|
|
203
|
+
while index < events.count, events[index].time <= start { index += 1 }
|
|
204
|
+
if index < events.count {
|
|
205
|
+
let next = events[index]
|
|
206
|
+
if next.time < end || (next.isRamp && next.fromTime < end) { return false }
|
|
207
|
+
}
|
|
208
|
+
guard index > 0 else { return true }
|
|
209
|
+
let previous = events[index - 1]
|
|
210
|
+
switch previous.kind {
|
|
211
|
+
case .target: return previous.timeConstant <= 0
|
|
212
|
+
case .curve: return previous.end <= start
|
|
213
|
+
default: return true
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private func resolve(from start: Int) {
|
|
218
|
+
guard start < events.count else { return }
|
|
219
|
+
for index in start..<events.count {
|
|
220
|
+
var event = events[index]
|
|
221
|
+
if index == 0 {
|
|
222
|
+
event.fromTime = event.isRamp ? min(event.scheduledAt, event.time) : event.time
|
|
223
|
+
event.fromValue = intrinsic
|
|
224
|
+
} else {
|
|
225
|
+
let previous = events[index - 1]
|
|
226
|
+
if event.isRamp {
|
|
227
|
+
// After setTargetAtTime a ramp starts where the curve is
|
|
228
|
+
// when the ramp was asked for.
|
|
229
|
+
event.fromTime = previous.kind == .target
|
|
230
|
+
? min(max(previous.time, event.scheduledAt), event.time)
|
|
231
|
+
: min(previous.end, event.time)
|
|
232
|
+
} else {
|
|
233
|
+
event.fromTime = event.time
|
|
234
|
+
}
|
|
235
|
+
event.fromValue = Self.after(previous, at: event.fromTime)
|
|
236
|
+
}
|
|
237
|
+
events[index] = event
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// An event's own segment, at or after its time.
|
|
242
|
+
private static func after(_ event: Event, at time: Double) -> Double {
|
|
243
|
+
switch event.kind {
|
|
244
|
+
case .set, .linear, .exponential:
|
|
245
|
+
return event.value
|
|
246
|
+
case .target:
|
|
247
|
+
guard event.timeConstant > 0 else { return event.value }
|
|
248
|
+
return event.value + (event.fromValue - event.value) * exp(-(time - event.time) / event.timeConstant)
|
|
249
|
+
case .curve:
|
|
250
|
+
let curve = event.curve
|
|
251
|
+
guard let last = curve.last else { return event.value }
|
|
252
|
+
guard time < event.end, event.duration > 0, curve.count > 1 else { return last }
|
|
253
|
+
let position = max(0, (time - event.time) / event.duration) * Double(curve.count - 1)
|
|
254
|
+
let index = min(curve.count - 2, Int(position))
|
|
255
|
+
return curve[index] + (curve[index + 1] - curve[index]) * (position - Double(index))
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private static func ramp(_ event: Event, at time: Double) -> Double {
|
|
260
|
+
let span = event.time - event.fromTime
|
|
261
|
+
guard span > 0 else { return event.value }
|
|
262
|
+
let fraction = min(1, max(0, (time - event.fromTime) / span))
|
|
263
|
+
if event.kind == .linear { return event.fromValue + (event.value - event.fromValue) * fraction }
|
|
264
|
+
// The spec holds the start value when the ramp cannot be exponential.
|
|
265
|
+
guard event.fromValue != 0, (event.fromValue > 0) == (event.value > 0) else { return event.fromValue }
|
|
266
|
+
return event.fromValue * pow(event.value / event.fromValue, fraction)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// MARK: - Nodes
|
|
271
|
+
|
|
272
|
+
/// A node of the graph. Every node renders stereo; a mono source writes the
|
|
273
|
+
/// same samples to both channels, which is what the spec's up-mixing makes
|
|
274
|
+
/// of it anyway.
|
|
275
|
+
class CraftAudioNode {
|
|
276
|
+
let id: Int
|
|
277
|
+
var inputs: [CraftAudioNode] = []
|
|
278
|
+
var outputs: [CraftAudioNode] = []
|
|
279
|
+
var paramOutputs: [CraftAudioParam] = []
|
|
280
|
+
var params: [CraftAudioParam] = []
|
|
281
|
+
let left: UnsafeMutablePointer<Float>
|
|
282
|
+
let right: UnsafeMutablePointer<Float>
|
|
283
|
+
/// This quantum's output is silence (`left`/`right` are not read then).
|
|
284
|
+
var silent = true
|
|
285
|
+
/// The script let go of it: once nothing upstream can sound, it goes.
|
|
286
|
+
var released = false
|
|
287
|
+
var dead = false
|
|
288
|
+
/// The last quantum it took in or gave out sound.
|
|
289
|
+
var lastActive: Int64 = -1_000_000
|
|
290
|
+
/// Quanta after `lastActive` before it is certainly quiet.
|
|
291
|
+
var tail: Int64 { 2 }
|
|
292
|
+
/// For graph searches.
|
|
293
|
+
var mark = 0
|
|
294
|
+
private var renderedQuantum: Int64 = -1
|
|
295
|
+
private var busy = false
|
|
296
|
+
|
|
297
|
+
init(id: Int) {
|
|
298
|
+
self.id = id
|
|
299
|
+
left = .allocate(capacity: craftAudioQuantum)
|
|
300
|
+
left.initialize(repeating: 0, count: craftAudioQuantum)
|
|
301
|
+
right = .allocate(capacity: craftAudioQuantum)
|
|
302
|
+
right.initialize(repeating: 0, count: craftAudioQuantum)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
deinit {
|
|
306
|
+
left.deallocate()
|
|
307
|
+
right.deallocate()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
func param(_ name: String) -> CraftAudioParam? {
|
|
311
|
+
params.first { $0.name == name }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Renders this quantum once, however many nodes read it. False when a
|
|
315
|
+
/// cycle came back to a node still working on it: a cycle without a delay
|
|
316
|
+
/// is silent, as in the spec.
|
|
317
|
+
@discardableResult
|
|
318
|
+
final func pull(_ render: CraftAudioRender) -> Bool {
|
|
319
|
+
if renderedQuantum == render.quantum { return busy ? reenter(render) : true }
|
|
320
|
+
renderedQuantum = render.quantum
|
|
321
|
+
busy = true
|
|
322
|
+
process(render)
|
|
323
|
+
busy = false
|
|
324
|
+
if !silent { lastActive = render.quantum }
|
|
325
|
+
return true
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func reenter(_ render: CraftAudioRender) -> Bool { false }
|
|
329
|
+
|
|
330
|
+
func process(_ render: CraftAudioRender) {
|
|
331
|
+
silent = !mix(render, into: left, right)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Sums the inputs into `l`/`r`; false (and the buffers untouched) when
|
|
335
|
+
/// every input is silent.
|
|
336
|
+
final func mix(_ render: CraftAudioRender, into l: UnsafeMutablePointer<Float>, _ r: UnsafeMutablePointer<Float>) -> Bool {
|
|
337
|
+
let frames = vDSP_Length(craftAudioQuantum)
|
|
338
|
+
var audible = false
|
|
339
|
+
for input in inputs where input.pull(render) && !input.silent {
|
|
340
|
+
if audible {
|
|
341
|
+
vDSP_vadd(l, 1, input.left, 1, l, 1, frames)
|
|
342
|
+
vDSP_vadd(r, 1, input.right, 1, r, 1, frames)
|
|
343
|
+
} else {
|
|
344
|
+
l.update(from: input.left, count: craftAudioQuantum)
|
|
345
|
+
r.update(from: input.right, count: craftAudioQuantum)
|
|
346
|
+
audible = true
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return audible
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
final class CraftAudioGain: CraftAudioNode {
|
|
354
|
+
let gain = CraftAudioParam("gain", 1)
|
|
355
|
+
|
|
356
|
+
override init(id: Int) {
|
|
357
|
+
super.init(id: id)
|
|
358
|
+
params = [gain]
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
override func process(_ render: CraftAudioRender) {
|
|
362
|
+
gain.compute(render)
|
|
363
|
+
guard mix(render, into: left, right) else { silent = true; return }
|
|
364
|
+
let frames = vDSP_Length(craftAudioQuantum)
|
|
365
|
+
if gain.constant {
|
|
366
|
+
var value = gain.values[0]
|
|
367
|
+
guard value != 0 else { silent = true; return }
|
|
368
|
+
if value != 1 {
|
|
369
|
+
vDSP_vsmul(left, 1, &value, left, 1, frames)
|
|
370
|
+
vDSP_vsmul(right, 1, &value, right, 1, frames)
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
vDSP_vmul(left, 1, gain.values, 1, left, 1, frames)
|
|
374
|
+
vDSP_vmul(right, 1, gain.values, 1, right, 1, frames)
|
|
375
|
+
}
|
|
376
|
+
silent = false
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// OscillatorNode and AudioBufferSourceNode: start, stop, ended.
|
|
381
|
+
class CraftAudioSource: CraftAudioNode {
|
|
382
|
+
var started = false
|
|
383
|
+
var startTime = 0.0
|
|
384
|
+
var stopTime = Double.infinity
|
|
385
|
+
var ended = false
|
|
386
|
+
|
|
387
|
+
/// The frames of this quantum the source plays, [begin, end).
|
|
388
|
+
final func window(_ render: CraftAudioRender) -> (begin: Int, end: Int) {
|
|
389
|
+
guard started, !ended else { return (0, 0) }
|
|
390
|
+
let quantum = Int64(craftAudioQuantum)
|
|
391
|
+
let first = frame(startTime, render.sampleRate)
|
|
392
|
+
let last = stopTime.isFinite ? frame(stopTime, render.sampleRate) : Int64.max
|
|
393
|
+
let begin = Int(min(max(first - render.frame, 0), quantum))
|
|
394
|
+
let end = Int(min(max(last - render.frame, 0), quantum))
|
|
395
|
+
return (begin, max(begin, end))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// Whether `stop` takes effect by the end of this quantum.
|
|
399
|
+
final func stops(_ render: CraftAudioRender) -> Bool {
|
|
400
|
+
started && stopTime.isFinite && frame(stopTime, render.sampleRate) <= render.frame + Int64(craftAudioQuantum)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// The first frame at or after `time`. A time a whole frame in seconds
|
|
404
|
+
/// (100 / 48000) lands on that frame despite rounding.
|
|
405
|
+
private func frame(_ time: Double, _ sampleRate: Double) -> Int64 {
|
|
406
|
+
Int64(min(9e18, max(0, time * sampleRate - 1e-6).rounded(.up)))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
final func clear(outside window: (begin: Int, end: Int)) {
|
|
410
|
+
if window.begin > 0 {
|
|
411
|
+
left.update(repeating: 0, count: window.begin)
|
|
412
|
+
right.update(repeating: 0, count: window.begin)
|
|
413
|
+
}
|
|
414
|
+
if window.end < craftAudioQuantum {
|
|
415
|
+
(left + window.end).update(repeating: 0, count: craftAudioQuantum - window.end)
|
|
416
|
+
(right + window.end).update(repeating: 0, count: craftAudioQuantum - window.end)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
final class CraftAudioOscillator: CraftAudioSource {
|
|
422
|
+
/// sine, square, sawtooth, triangle.
|
|
423
|
+
var shape = 0
|
|
424
|
+
let frequency: CraftAudioParam
|
|
425
|
+
let detune = CraftAudioParam("detune", 0, min: -153_600, max: 153_600)
|
|
426
|
+
private var phase = 0.0
|
|
427
|
+
|
|
428
|
+
init(id: Int, sampleRate: Double) {
|
|
429
|
+
frequency = CraftAudioParam("frequency", 440, min: -sampleRate / 2, max: sampleRate / 2)
|
|
430
|
+
super.init(id: id)
|
|
431
|
+
params = [frequency, detune]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
override func process(_ render: CraftAudioRender) {
|
|
435
|
+
frequency.compute(render)
|
|
436
|
+
detune.compute(render)
|
|
437
|
+
let window = window(render)
|
|
438
|
+
if stops(render) { ended = true }
|
|
439
|
+
guard window.begin < window.end else { silent = true; return }
|
|
440
|
+
clear(outside: window)
|
|
441
|
+
let rate = render.sampleRate
|
|
442
|
+
let nyquist = rate / 2
|
|
443
|
+
let fixedDetune = detune.constant ? pow(2, Double(detune.values[0]) / 1200) : 1
|
|
444
|
+
for frame in window.begin..<window.end {
|
|
445
|
+
let cents = detune.constant ? fixedDetune : pow(2, Double(detune.values[frame]) / 1200)
|
|
446
|
+
let step = min(nyquist, max(-nyquist, Double(frequency.values[frame]) * cents)) / rate
|
|
447
|
+
left[frame] = Float(Self.sample(shape, phase, step))
|
|
448
|
+
phase += step
|
|
449
|
+
phase -= phase.rounded(.down)
|
|
450
|
+
}
|
|
451
|
+
(right + window.begin).update(from: left + window.begin, count: window.end - window.begin)
|
|
452
|
+
silent = false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/// One sample at `phase` (0..<1 of a cycle), starting at 0 and rising as
|
|
456
|
+
/// the browser's waves do. Square and sawtooth are PolyBLEP band-limited.
|
|
457
|
+
static func sample(_ shape: Int, _ phase: Double, _ step: Double) -> Double {
|
|
458
|
+
switch shape {
|
|
459
|
+
case 1:
|
|
460
|
+
var value = phase < 0.5 ? 1.0 : -1.0
|
|
461
|
+
value += blep(phase, step)
|
|
462
|
+
let half = phase + 0.5
|
|
463
|
+
value -= blep(half - half.rounded(.down), step)
|
|
464
|
+
return value
|
|
465
|
+
case 2:
|
|
466
|
+
let shifted = phase + 0.5
|
|
467
|
+
let saw = shifted - shifted.rounded(.down)
|
|
468
|
+
return 2 * saw - 1 - blep(saw, step)
|
|
469
|
+
case 3:
|
|
470
|
+
let shifted = phase + 0.75
|
|
471
|
+
return 4 * abs(shifted - shifted.rounded(.down) - 0.5) - 1
|
|
472
|
+
default:
|
|
473
|
+
return sin(2 * Double.pi * phase)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private static func blep(_ t: Double, _ step: Double) -> Double {
|
|
478
|
+
let dt = abs(step)
|
|
479
|
+
guard dt > 0 else { return 0 }
|
|
480
|
+
if t < dt {
|
|
481
|
+
let x = t / dt
|
|
482
|
+
return x + x - x * x - 1
|
|
483
|
+
}
|
|
484
|
+
if t > 1 - dt {
|
|
485
|
+
let x = (t - 1) / dt
|
|
486
|
+
return x * x + x + x + 1
|
|
487
|
+
}
|
|
488
|
+
return 0
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
final class CraftAudioBufferSource: CraftAudioSource {
|
|
493
|
+
var buffer: CraftAudioBufferData?
|
|
494
|
+
var loop = false
|
|
495
|
+
var loopStart = 0.0
|
|
496
|
+
var loopEnd = 0.0
|
|
497
|
+
var offset = 0.0
|
|
498
|
+
/// Seconds of the buffer to play; negative is all of it.
|
|
499
|
+
var duration = -1.0
|
|
500
|
+
let playbackRate = CraftAudioParam("playbackRate", 1, audioRate: false)
|
|
501
|
+
let detune = CraftAudioParam("detune", 0, audioRate: false)
|
|
502
|
+
private var position = 0.0
|
|
503
|
+
private var played = 0.0
|
|
504
|
+
private var began = false
|
|
505
|
+
|
|
506
|
+
override init(id: Int) {
|
|
507
|
+
super.init(id: id)
|
|
508
|
+
params = [playbackRate, detune]
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
override func process(_ render: CraftAudioRender) {
|
|
512
|
+
playbackRate.compute(render)
|
|
513
|
+
detune.compute(render)
|
|
514
|
+
let window = window(render)
|
|
515
|
+
if stops(render) { ended = true }
|
|
516
|
+
guard window.begin < window.end, let buffer else { silent = true; return }
|
|
517
|
+
clear(outside: window)
|
|
518
|
+
let length = Double(buffer.length)
|
|
519
|
+
var loopFrom = 0.0
|
|
520
|
+
var loopTo = length
|
|
521
|
+
if loopStart >= 0, loopEnd > 0, loopStart < loopEnd {
|
|
522
|
+
loopFrom = min(length, loopStart * buffer.sampleRate)
|
|
523
|
+
loopTo = min(length, loopEnd * buffer.sampleRate)
|
|
524
|
+
}
|
|
525
|
+
let span = loopTo - loopFrom
|
|
526
|
+
if !began {
|
|
527
|
+
began = true
|
|
528
|
+
position = max(0, offset) * buffer.sampleRate
|
|
529
|
+
if loop, span > 0, position >= loopTo {
|
|
530
|
+
position = loopFrom + (position - loopFrom).truncatingRemainder(dividingBy: span)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
let rate = buffer.sampleRate / render.sampleRate * Double(playbackRate.values[0]) * pow(2, Double(detune.values[0]) / 1200)
|
|
534
|
+
let first = buffer.channels[0]
|
|
535
|
+
let second = buffer.channels.count > 1 ? buffer.channels[1] : first
|
|
536
|
+
let frames = buffer.length
|
|
537
|
+
var finishedAt: Int?
|
|
538
|
+
for frame in window.begin..<window.end {
|
|
539
|
+
if duration >= 0, played >= duration { finishedAt = frame; break }
|
|
540
|
+
if loop, span > 0 {
|
|
541
|
+
if position >= loopTo { position = loopFrom + (position - loopTo).truncatingRemainder(dividingBy: span) }
|
|
542
|
+
else if position < loopFrom, rate < 0 { position = loopTo - (loopFrom - position).truncatingRemainder(dividingBy: span) }
|
|
543
|
+
} else if position >= length || position < 0 {
|
|
544
|
+
finishedAt = frame
|
|
545
|
+
break
|
|
546
|
+
}
|
|
547
|
+
let index = Int(position)
|
|
548
|
+
let fraction = Float(position - Double(index))
|
|
549
|
+
var next = index + 1
|
|
550
|
+
if loop, span > 0, Double(next) >= loopTo { next = Int(loopFrom) }
|
|
551
|
+
let a0 = index < frames ? first[index] : 0
|
|
552
|
+
let a1 = next < frames ? first[next] : 0
|
|
553
|
+
let b0 = index < frames ? second[index] : 0
|
|
554
|
+
let b1 = next < frames ? second[next] : 0
|
|
555
|
+
left[frame] = a0 + (a1 - a0) * fraction
|
|
556
|
+
right[frame] = b0 + (b1 - b0) * fraction
|
|
557
|
+
position += rate
|
|
558
|
+
played += abs(rate) / buffer.sampleRate
|
|
559
|
+
}
|
|
560
|
+
if let finishedAt {
|
|
561
|
+
(left + finishedAt).update(repeating: 0, count: craftAudioQuantum - finishedAt)
|
|
562
|
+
(right + finishedAt).update(repeating: 0, count: craftAudioQuantum - finishedAt)
|
|
563
|
+
ended = true
|
|
564
|
+
}
|
|
565
|
+
silent = false
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
final class CraftAudioBiquad: CraftAudioNode {
|
|
570
|
+
/// lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass.
|
|
571
|
+
var type = 0
|
|
572
|
+
let frequency: CraftAudioParam
|
|
573
|
+
let detune = CraftAudioParam("detune", 0, min: -153_600, max: 153_600)
|
|
574
|
+
let q = CraftAudioParam("Q", 1)
|
|
575
|
+
let gain = CraftAudioParam("gain", 0, max: 1541)
|
|
576
|
+
/// x1, x2, y1, y2 for each channel.
|
|
577
|
+
private var state = [Double](repeating: 0, count: 8)
|
|
578
|
+
|
|
579
|
+
init(id: Int, sampleRate: Double) {
|
|
580
|
+
frequency = CraftAudioParam("frequency", 350, min: 0, max: sampleRate / 2)
|
|
581
|
+
super.init(id: id)
|
|
582
|
+
params = [frequency, detune, q, gain]
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
override func process(_ render: CraftAudioRender) {
|
|
586
|
+
frequency.compute(render)
|
|
587
|
+
detune.compute(render)
|
|
588
|
+
q.compute(render)
|
|
589
|
+
gain.compute(render)
|
|
590
|
+
let audible = mix(render, into: left, right)
|
|
591
|
+
if !audible {
|
|
592
|
+
if state.allSatisfy({ abs($0) < 1e-12 }) {
|
|
593
|
+
state = [Double](repeating: 0, count: 8)
|
|
594
|
+
silent = true
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
left.update(repeating: 0, count: craftAudioQuantum)
|
|
598
|
+
right.update(repeating: 0, count: craftAudioQuantum)
|
|
599
|
+
}
|
|
600
|
+
let c = coefficients(render.sampleRate)
|
|
601
|
+
filter(left, c, 0)
|
|
602
|
+
filter(right, c, 4)
|
|
603
|
+
silent = false
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private func filter(_ samples: UnsafeMutablePointer<Float>, _ c: (b0: Double, b1: Double, b2: Double, a1: Double, a2: Double), _ at: Int) {
|
|
607
|
+
var x1 = state[at], x2 = state[at + 1], y1 = state[at + 2], y2 = state[at + 3]
|
|
608
|
+
for frame in 0..<craftAudioQuantum {
|
|
609
|
+
let x = Double(samples[frame])
|
|
610
|
+
var y = c.b0 * x + c.b1 * x1 + c.b2 * x2 - c.a1 * y1 - c.a2 * y2
|
|
611
|
+
// Flush denormals (and anything that blew up) to zero.
|
|
612
|
+
if !y.isFinite || abs(y) < 1e-30 { y = 0 }
|
|
613
|
+
x2 = x1
|
|
614
|
+
x1 = x
|
|
615
|
+
y2 = y1
|
|
616
|
+
y1 = y
|
|
617
|
+
samples[frame] = Float(y)
|
|
618
|
+
}
|
|
619
|
+
state[at] = x1
|
|
620
|
+
state[at + 1] = x2
|
|
621
|
+
state[at + 2] = y1
|
|
622
|
+
state[at + 3] = y2
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/// The spec's formulas (the Audio EQ Cookbook, with lowpass and highpass
|
|
626
|
+
/// Q in dB), normalised by a0.
|
|
627
|
+
func coefficients(_ sampleRate: Double) -> (b0: Double, b1: Double, b2: Double, a1: Double, a2: Double) {
|
|
628
|
+
let nyquist = sampleRate / 2
|
|
629
|
+
let hz = min(nyquist, max(0, Double(frequency.values[0]) * pow(2, Double(detune.values[0]) / 1200)))
|
|
630
|
+
let normalized = hz / nyquist
|
|
631
|
+
let quality = Double(q.values[0])
|
|
632
|
+
let a = pow(10, Double(gain.values[0]) / 40)
|
|
633
|
+
let pass = (b0: 1.0, b1: 0.0, b2: 0.0, a1: 0.0, a2: 0.0)
|
|
634
|
+
let mute = (b0: 0.0, b1: 0.0, b2: 0.0, a1: 0.0, a2: 0.0)
|
|
635
|
+
let gainOnly = (b0: a * a, b1: 0.0, b2: 0.0, a1: 0.0, a2: 0.0)
|
|
636
|
+
switch type {
|
|
637
|
+
case 0 where normalized >= 1: return pass
|
|
638
|
+
case 0 where normalized <= 0: return mute
|
|
639
|
+
case 1 where normalized >= 1: return mute
|
|
640
|
+
case 1 where normalized <= 0: return pass
|
|
641
|
+
case 2 where normalized <= 0 || normalized >= 1: return mute
|
|
642
|
+
case 2 where quality <= 0: return pass
|
|
643
|
+
case 3 where normalized >= 1, 4 where normalized <= 0: return gainOnly
|
|
644
|
+
case 3 where normalized <= 0, 4 where normalized >= 1: return pass
|
|
645
|
+
case 5 where normalized <= 0 || normalized >= 1: return pass
|
|
646
|
+
case 5 where quality <= 0: return gainOnly
|
|
647
|
+
case 6 where normalized <= 0 || normalized >= 1: return pass
|
|
648
|
+
case 6 where quality <= 0: return mute
|
|
649
|
+
case 7 where normalized <= 0 || normalized >= 1: return pass
|
|
650
|
+
case 7 where quality <= 0: return (b0: -1, b1: 0, b2: 0, a1: 0, a2: 0)
|
|
651
|
+
default: break
|
|
652
|
+
}
|
|
653
|
+
let w0 = Double.pi * normalized
|
|
654
|
+
let cosine = cos(w0)
|
|
655
|
+
let alphaQ = sin(w0) / (2 * quality)
|
|
656
|
+
let alphaDB = sin(w0) / (2 * pow(10, quality / 20))
|
|
657
|
+
let alphaS = sin(w0) / 2 * sqrt(2.0)
|
|
658
|
+
let root = sqrt(a)
|
|
659
|
+
var b0 = 1.0, b1 = 0.0, b2 = 0.0, a0 = 1.0, a1 = 0.0, a2 = 0.0
|
|
660
|
+
switch type {
|
|
661
|
+
case 1:
|
|
662
|
+
b0 = (1 + cosine) / 2; b1 = -(1 + cosine); b2 = (1 + cosine) / 2
|
|
663
|
+
a0 = 1 + alphaDB; a1 = -2 * cosine; a2 = 1 - alphaDB
|
|
664
|
+
case 2:
|
|
665
|
+
b0 = alphaQ; b1 = 0; b2 = -alphaQ
|
|
666
|
+
a0 = 1 + alphaQ; a1 = -2 * cosine; a2 = 1 - alphaQ
|
|
667
|
+
case 3:
|
|
668
|
+
b0 = a * ((a + 1) - (a - 1) * cosine + 2 * alphaS * root)
|
|
669
|
+
b1 = 2 * a * ((a - 1) - (a + 1) * cosine)
|
|
670
|
+
b2 = a * ((a + 1) - (a - 1) * cosine - 2 * alphaS * root)
|
|
671
|
+
a0 = (a + 1) + (a - 1) * cosine + 2 * alphaS * root
|
|
672
|
+
a1 = -2 * ((a - 1) + (a + 1) * cosine)
|
|
673
|
+
a2 = (a + 1) + (a - 1) * cosine - 2 * alphaS * root
|
|
674
|
+
case 4:
|
|
675
|
+
b0 = a * ((a + 1) + (a - 1) * cosine + 2 * alphaS * root)
|
|
676
|
+
b1 = -2 * a * ((a - 1) + (a + 1) * cosine)
|
|
677
|
+
b2 = a * ((a + 1) + (a - 1) * cosine - 2 * alphaS * root)
|
|
678
|
+
a0 = (a + 1) - (a - 1) * cosine + 2 * alphaS * root
|
|
679
|
+
a1 = 2 * ((a - 1) - (a + 1) * cosine)
|
|
680
|
+
a2 = (a + 1) - (a - 1) * cosine - 2 * alphaS * root
|
|
681
|
+
case 5:
|
|
682
|
+
b0 = 1 + alphaQ * a; b1 = -2 * cosine; b2 = 1 - alphaQ * a
|
|
683
|
+
a0 = 1 + alphaQ / a; a1 = -2 * cosine; a2 = 1 - alphaQ / a
|
|
684
|
+
case 6:
|
|
685
|
+
b0 = 1; b1 = -2 * cosine; b2 = 1
|
|
686
|
+
a0 = 1 + alphaQ; a1 = -2 * cosine; a2 = 1 - alphaQ
|
|
687
|
+
case 7:
|
|
688
|
+
b0 = 1 - alphaQ; b1 = -2 * cosine; b2 = 1 + alphaQ
|
|
689
|
+
a0 = 1 + alphaQ; a1 = -2 * cosine; a2 = 1 - alphaQ
|
|
690
|
+
default:
|
|
691
|
+
b0 = (1 - cosine) / 2; b1 = 1 - cosine; b2 = (1 - cosine) / 2
|
|
692
|
+
a0 = 1 + alphaDB; a1 = -2 * cosine; a2 = 1 - alphaDB
|
|
693
|
+
}
|
|
694
|
+
return (b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
final class CraftAudioDelay: CraftAudioNode {
|
|
699
|
+
let delayTime: CraftAudioParam
|
|
700
|
+
private let capacity: Int
|
|
701
|
+
private let ringLeft: UnsafeMutablePointer<Float>
|
|
702
|
+
private let ringRight: UnsafeMutablePointer<Float>
|
|
703
|
+
private let inLeft: UnsafeMutablePointer<Float>
|
|
704
|
+
private let inRight: UnsafeMutablePointer<Float>
|
|
705
|
+
/// Frames written since the start.
|
|
706
|
+
private var written = 0
|
|
707
|
+
/// A cycle needed this quantum's output before the input was in.
|
|
708
|
+
private var early = false
|
|
709
|
+
/// Frames since the input last sounded.
|
|
710
|
+
private var quiet = Int.max / 2
|
|
711
|
+
|
|
712
|
+
override var tail: Int64 { Int64(capacity / craftAudioQuantum) + 2 }
|
|
713
|
+
|
|
714
|
+
init(id: Int, sampleRate: Double, maxDelay: Double) {
|
|
715
|
+
delayTime = CraftAudioParam("delayTime", 0, min: 0, max: maxDelay)
|
|
716
|
+
capacity = Int((maxDelay * sampleRate).rounded(.up)) + 2 * craftAudioQuantum + 2
|
|
717
|
+
ringLeft = .allocate(capacity: capacity)
|
|
718
|
+
ringLeft.initialize(repeating: 0, count: capacity)
|
|
719
|
+
ringRight = .allocate(capacity: capacity)
|
|
720
|
+
ringRight.initialize(repeating: 0, count: capacity)
|
|
721
|
+
inLeft = .allocate(capacity: craftAudioQuantum)
|
|
722
|
+
inLeft.initialize(repeating: 0, count: craftAudioQuantum)
|
|
723
|
+
inRight = .allocate(capacity: craftAudioQuantum)
|
|
724
|
+
inRight.initialize(repeating: 0, count: craftAudioQuantum)
|
|
725
|
+
super.init(id: id)
|
|
726
|
+
params = [delayTime]
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
deinit {
|
|
730
|
+
ringLeft.deallocate()
|
|
731
|
+
ringRight.deallocate()
|
|
732
|
+
inLeft.deallocate()
|
|
733
|
+
inRight.deallocate()
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/// In a cycle the delay is at least one quantum (the spec's clamp), so its
|
|
737
|
+
/// output comes from what was written before this quantum.
|
|
738
|
+
override func reenter(_ render: CraftAudioRender) -> Bool {
|
|
739
|
+
if !early {
|
|
740
|
+
early = true
|
|
741
|
+
if quiet > capacity { silent = true } else { read(render, minimum: Double(craftAudioQuantum)) }
|
|
742
|
+
}
|
|
743
|
+
return true
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
override func process(_ render: CraftAudioRender) {
|
|
747
|
+
early = false
|
|
748
|
+
delayTime.compute(render)
|
|
749
|
+
let audible = mix(render, into: inLeft, inRight)
|
|
750
|
+
quiet = audible ? 0 : quiet + craftAudioQuantum
|
|
751
|
+
if audible { lastActive = render.quantum }
|
|
752
|
+
if quiet > capacity {
|
|
753
|
+
// Nothing has gone in for longer than the line: it holds zeros.
|
|
754
|
+
if !early { silent = true }
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
if !audible {
|
|
758
|
+
inLeft.update(repeating: 0, count: craftAudioQuantum)
|
|
759
|
+
inRight.update(repeating: 0, count: craftAudioQuantum)
|
|
760
|
+
}
|
|
761
|
+
for frame in 0..<craftAudioQuantum {
|
|
762
|
+
let slot = (written + frame) % capacity
|
|
763
|
+
ringLeft[slot] = inLeft[frame]
|
|
764
|
+
ringRight[slot] = inRight[frame]
|
|
765
|
+
}
|
|
766
|
+
if !early { read(render, minimum: 0) }
|
|
767
|
+
written += craftAudioQuantum
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private func read(_ render: CraftAudioRender, minimum: Double) {
|
|
771
|
+
let limit = Double(capacity - craftAudioQuantum - 2)
|
|
772
|
+
for frame in 0..<craftAudioQuantum {
|
|
773
|
+
let delay = min(limit, max(minimum, Double(delayTime.values[frame]) * render.sampleRate))
|
|
774
|
+
let position = Double(written + frame) - delay
|
|
775
|
+
let base = position.rounded(.down)
|
|
776
|
+
let fraction = Float(position - base)
|
|
777
|
+
let index = Int(base)
|
|
778
|
+
left[frame] = sample(ringLeft, index) * (1 - fraction) + sample(ringLeft, index + 1) * fraction
|
|
779
|
+
right[frame] = sample(ringRight, index) * (1 - fraction) + sample(ringRight, index + 1) * fraction
|
|
780
|
+
}
|
|
781
|
+
silent = false
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private func sample(_ ring: UnsafeMutablePointer<Float>, _ index: Int) -> Float {
|
|
785
|
+
index < 0 ? 0 : ring[index % capacity]
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
final class CraftAudioCompressor: CraftAudioNode {
|
|
790
|
+
let threshold = CraftAudioParam("threshold", -24, min: -100, max: 0, audioRate: false)
|
|
791
|
+
let knee = CraftAudioParam("knee", 30, min: 0, max: 40, audioRate: false)
|
|
792
|
+
let ratio = CraftAudioParam("ratio", 12, min: 1, max: 20, audioRate: false)
|
|
793
|
+
let attack = CraftAudioParam("attack", 0.003, min: 0, max: 1, audioRate: false)
|
|
794
|
+
let release = CraftAudioParam("release", 0.25, min: 0, max: 1, audioRate: false)
|
|
795
|
+
/// The gain reduction now, in dB (zero or below): `reduction`.
|
|
796
|
+
private(set) var reduction = 0.0
|
|
797
|
+
private var envelope = 0.0
|
|
798
|
+
|
|
799
|
+
override init(id: Int) {
|
|
800
|
+
super.init(id: id)
|
|
801
|
+
params = [threshold, knee, ratio, attack, release]
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
override func process(_ render: CraftAudioRender) {
|
|
805
|
+
params.forEach { $0.compute(render) }
|
|
806
|
+
let rate = render.sampleRate
|
|
807
|
+
let attackTime = Double(attack.values[0])
|
|
808
|
+
let releaseTime = Double(release.values[0])
|
|
809
|
+
let attackCoefficient = attackTime > 0 ? exp(-1 / (attackTime * rate)) : 0
|
|
810
|
+
let releaseCoefficient = releaseTime > 0 ? exp(-1 / (releaseTime * rate)) : 0
|
|
811
|
+
guard mix(render, into: left, right) else {
|
|
812
|
+
envelope *= pow(releaseCoefficient, Double(craftAudioQuantum))
|
|
813
|
+
reduction = envelope
|
|
814
|
+
silent = true
|
|
815
|
+
return
|
|
816
|
+
}
|
|
817
|
+
let t = Double(threshold.values[0])
|
|
818
|
+
let k = Double(knee.values[0])
|
|
819
|
+
let r = Double(ratio.values[0])
|
|
820
|
+
// The spec's makeup gain: (1 / the curve's gain at 0 dBFS) ^ 0.6.
|
|
821
|
+
let makeup = pow(1 / pow(10, Self.curve(0, t, k, r) / 20), 0.6)
|
|
822
|
+
for frame in 0..<craftAudioQuantum {
|
|
823
|
+
let level = Double(max(abs(left[frame]), abs(right[frame])))
|
|
824
|
+
let decibels = level > 1e-6 ? 20 * log10(level) : -120
|
|
825
|
+
let target = Self.curve(decibels, t, k, r) - decibels
|
|
826
|
+
let coefficient = target < envelope ? attackCoefficient : releaseCoefficient
|
|
827
|
+
envelope = coefficient * envelope + (1 - coefficient) * target
|
|
828
|
+
let gain = Float(pow(10, envelope / 20) * makeup)
|
|
829
|
+
left[frame] *= gain
|
|
830
|
+
right[frame] *= gain
|
|
831
|
+
}
|
|
832
|
+
reduction = envelope
|
|
833
|
+
silent = false
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/// The static curve in dB: straight to the threshold, a quadratic knee
|
|
837
|
+
/// above it, then 1:ratio.
|
|
838
|
+
static func curve(_ x: Double, _ threshold: Double, _ knee: Double, _ ratio: Double) -> Double {
|
|
839
|
+
if x <= threshold { return x }
|
|
840
|
+
let slope = 1 / ratio - 1
|
|
841
|
+
if knee > 0, x < threshold + knee {
|
|
842
|
+
let over = x - threshold
|
|
843
|
+
return x + slope * over * over / (2 * knee)
|
|
844
|
+
}
|
|
845
|
+
return threshold + knee + slope * knee / 2 + (x - threshold - knee) / ratio
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
final class CraftAudioConvolver: CraftAudioNode {
|
|
850
|
+
var convolution: CraftAudioConvolution?
|
|
851
|
+
private let inLeft: UnsafeMutablePointer<Float>
|
|
852
|
+
private let inRight: UnsafeMutablePointer<Float>
|
|
853
|
+
|
|
854
|
+
override var tail: Int64 { Int64((convolution?.tailFrames ?? 0) / craftAudioQuantum) + 2 }
|
|
855
|
+
|
|
856
|
+
override init(id: Int) {
|
|
857
|
+
inLeft = .allocate(capacity: craftAudioQuantum)
|
|
858
|
+
inLeft.initialize(repeating: 0, count: craftAudioQuantum)
|
|
859
|
+
inRight = .allocate(capacity: craftAudioQuantum)
|
|
860
|
+
inRight.initialize(repeating: 0, count: craftAudioQuantum)
|
|
861
|
+
super.init(id: id)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
deinit {
|
|
865
|
+
inLeft.deallocate()
|
|
866
|
+
inRight.deallocate()
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
override func process(_ render: CraftAudioRender) {
|
|
870
|
+
let audible = mix(render, into: inLeft, inRight)
|
|
871
|
+
if audible { lastActive = render.quantum }
|
|
872
|
+
guard let convolution else { silent = true; return }
|
|
873
|
+
silent = !convolution.process(inLeft, inRight, audible: audible, into: left, right)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/// A ConvolverNode's impulse response, ready to run: the first 512 taps
|
|
878
|
+
/// directly (no latency), the rest as uniformly partitioned FFT convolution
|
|
879
|
+
/// whose one-block latency the first 512 taps cover exactly.
|
|
880
|
+
final class CraftAudioConvolution {
|
|
881
|
+
static let block = 512
|
|
882
|
+
private static let log2n = vDSP_Length(10)
|
|
883
|
+
private static let setup: FFTSetup = vDSP_create_fftsetup(10, FFTRadix(kFFTRadix2))!
|
|
884
|
+
|
|
885
|
+
/// How long it rings after the input stops, in frames.
|
|
886
|
+
let tailFrames: Int
|
|
887
|
+
private let partitions: Int
|
|
888
|
+
private let responses: Int
|
|
889
|
+
private let direct: [UnsafeMutablePointer<Float>]
|
|
890
|
+
private let spectraReal: [UnsafeMutablePointer<Float>]
|
|
891
|
+
private let spectraImag: [UnsafeMutablePointer<Float>]
|
|
892
|
+
private let history: [UnsafeMutablePointer<Float>]
|
|
893
|
+
private let windows: [UnsafeMutablePointer<Float>]
|
|
894
|
+
private let delayReal: [UnsafeMutablePointer<Float>]
|
|
895
|
+
private let delayImag: [UnsafeMutablePointer<Float>]
|
|
896
|
+
private let tails: [UnsafeMutablePointer<Float>]
|
|
897
|
+
private let sumReal: UnsafeMutablePointer<Float>
|
|
898
|
+
private let sumImag: UnsafeMutablePointer<Float>
|
|
899
|
+
private let time: UnsafeMutablePointer<Float>
|
|
900
|
+
private let allocations: [UnsafeMutablePointer<Float>]
|
|
901
|
+
private var fill = 0
|
|
902
|
+
private var head = 0
|
|
903
|
+
private var quiet = Int.max / 2
|
|
904
|
+
|
|
905
|
+
/// The spec's normalisation: 1 / RMS power, calibrated (0.00125) and
|
|
906
|
+
/// scaled for the sample rate.
|
|
907
|
+
static func normalization(_ buffer: CraftAudioBufferData) -> Float {
|
|
908
|
+
var power = 0.0
|
|
909
|
+
for channel in buffer.channels {
|
|
910
|
+
var sum: Float = 0
|
|
911
|
+
vDSP_svesq(channel, 1, &sum, vDSP_Length(buffer.length))
|
|
912
|
+
power += Double(sum)
|
|
913
|
+
}
|
|
914
|
+
power = sqrt(power / Double(buffer.channels.count * buffer.length))
|
|
915
|
+
if !power.isFinite || power < 0.000125 { power = 0.000125 }
|
|
916
|
+
var scale = 1 / power * 0.00125
|
|
917
|
+
if buffer.sampleRate > 0 { scale *= 44_100 / buffer.sampleRate }
|
|
918
|
+
if buffer.channels.count == 4 { scale *= 0.5 }
|
|
919
|
+
return Float(scale)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
init(buffer: CraftAudioBufferData, normalize: Bool) {
|
|
923
|
+
let block = Self.block
|
|
924
|
+
let size = block * 2
|
|
925
|
+
let length = buffer.length
|
|
926
|
+
let responses = min(2, buffer.channels.count)
|
|
927
|
+
let partitions = length > block ? (length - block + block - 1) / block : 0
|
|
928
|
+
let scale = normalize ? Self.normalization(buffer) : 1
|
|
929
|
+
var made: [UnsafeMutablePointer<Float>] = []
|
|
930
|
+
func zeros(_ count: Int) -> UnsafeMutablePointer<Float> {
|
|
931
|
+
let data = UnsafeMutablePointer<Float>.allocate(capacity: max(1, count))
|
|
932
|
+
data.initialize(repeating: 0, count: max(1, count))
|
|
933
|
+
made.append(data)
|
|
934
|
+
return data
|
|
935
|
+
}
|
|
936
|
+
let time = zeros(size)
|
|
937
|
+
var direct: [UnsafeMutablePointer<Float>] = []
|
|
938
|
+
var spectraReal: [UnsafeMutablePointer<Float>] = []
|
|
939
|
+
var spectraImag: [UnsafeMutablePointer<Float>] = []
|
|
940
|
+
// The FFT's own scale: a forward real FFT is 2x, the inverse N x, so a
|
|
941
|
+
// product of two forward transforms comes back 4N x too large.
|
|
942
|
+
let fftScale = scale / Float(4 * size)
|
|
943
|
+
for channel in 0..<responses {
|
|
944
|
+
let source = buffer.channels[channel]
|
|
945
|
+
let taps = zeros(block)
|
|
946
|
+
var factor = scale
|
|
947
|
+
vDSP_vsmul(source, 1, &factor, taps, 1, vDSP_Length(min(block, length)))
|
|
948
|
+
direct.append(taps)
|
|
949
|
+
let real = zeros(partitions * block)
|
|
950
|
+
let imag = zeros(partitions * block)
|
|
951
|
+
for partition in 0..<partitions {
|
|
952
|
+
time.update(repeating: 0, count: size)
|
|
953
|
+
let start = block + partition * block
|
|
954
|
+
var tapsScale = fftScale
|
|
955
|
+
vDSP_vsmul(source + start, 1, &tapsScale, time, 1, vDSP_Length(min(block, length - start)))
|
|
956
|
+
var split = DSPSplitComplex(realp: real + partition * block, imagp: imag + partition * block)
|
|
957
|
+
time.withMemoryRebound(to: DSPComplex.self, capacity: block) { vDSP_ctoz($0, 2, &split, 1, vDSP_Length(block)) }
|
|
958
|
+
vDSP_fft_zrip(Self.setup, &split, 1, Self.log2n, FFTDirection(kFFTDirection_Forward))
|
|
959
|
+
}
|
|
960
|
+
spectraReal.append(real)
|
|
961
|
+
spectraImag.append(imag)
|
|
962
|
+
}
|
|
963
|
+
self.responses = responses
|
|
964
|
+
self.partitions = partitions
|
|
965
|
+
self.time = time
|
|
966
|
+
tailFrames = length + block
|
|
967
|
+
self.direct = direct
|
|
968
|
+
self.spectraReal = spectraReal
|
|
969
|
+
self.spectraImag = spectraImag
|
|
970
|
+
sumReal = zeros(block)
|
|
971
|
+
sumImag = zeros(block)
|
|
972
|
+
history = (0..<2).map { _ in zeros(block - 1 + craftAudioQuantum) }
|
|
973
|
+
windows = (0..<2).map { _ in zeros(size) }
|
|
974
|
+
delayReal = (0..<2).map { _ in zeros(partitions * block) }
|
|
975
|
+
delayImag = (0..<2).map { _ in zeros(partitions * block) }
|
|
976
|
+
tails = (0..<2).map { _ in zeros(block) }
|
|
977
|
+
allocations = made
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
deinit { allocations.forEach { $0.deallocate() } }
|
|
981
|
+
|
|
982
|
+
/// One quantum. False once it has rung out and the output is silence.
|
|
983
|
+
func process(_ inLeft: UnsafeMutablePointer<Float>, _ inRight: UnsafeMutablePointer<Float>, audible: Bool, into outLeft: UnsafeMutablePointer<Float>, _ outRight: UnsafeMutablePointer<Float>) -> Bool {
|
|
984
|
+
let block = Self.block
|
|
985
|
+
let frames = craftAudioQuantum
|
|
986
|
+
quiet = audible ? 0 : quiet + frames
|
|
987
|
+
// Every buffer holds zeros by then, so skipping keeps them right.
|
|
988
|
+
if quiet > tailFrames + 3 * block { return false }
|
|
989
|
+
for channel in 0..<2 {
|
|
990
|
+
let input = channel == 0 ? inLeft : inRight
|
|
991
|
+
let output = channel == 0 ? outLeft : outRight
|
|
992
|
+
let response = min(channel, responses - 1)
|
|
993
|
+
let past = history[channel]
|
|
994
|
+
let current = past + (block - 1)
|
|
995
|
+
if audible { current.update(from: input, count: frames) } else { current.update(repeating: 0, count: frames) }
|
|
996
|
+
vDSP_conv(past, 1, direct[response] + (block - 1), -1, output, 1, vDSP_Length(frames), vDSP_Length(block))
|
|
997
|
+
if partitions > 0 {
|
|
998
|
+
(windows[channel] + block + fill).update(from: current, count: frames)
|
|
999
|
+
vDSP_vadd(output, 1, tails[channel] + fill, 1, output, 1, vDSP_Length(frames))
|
|
1000
|
+
}
|
|
1001
|
+
memmove(past, past + frames, (block - 1) * MemoryLayout<Float>.size)
|
|
1002
|
+
}
|
|
1003
|
+
fill += frames
|
|
1004
|
+
if fill == block {
|
|
1005
|
+
fill = 0
|
|
1006
|
+
if partitions > 0 {
|
|
1007
|
+
runBlock(0)
|
|
1008
|
+
runBlock(1)
|
|
1009
|
+
head = (head + 1) % partitions
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return true
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/// A whole block is in: its spectrum joins the delay line, and the tail
|
|
1016
|
+
/// partitions' sum becomes the next block's tail output.
|
|
1017
|
+
private func runBlock(_ channel: Int) {
|
|
1018
|
+
let block = Self.block
|
|
1019
|
+
let window = windows[channel]
|
|
1020
|
+
let slotReal = delayReal[channel] + head * block
|
|
1021
|
+
let slotImag = delayImag[channel] + head * block
|
|
1022
|
+
var input = DSPSplitComplex(realp: slotReal, imagp: slotImag)
|
|
1023
|
+
window.withMemoryRebound(to: DSPComplex.self, capacity: block) { vDSP_ctoz($0, 2, &input, 1, vDSP_Length(block)) }
|
|
1024
|
+
vDSP_fft_zrip(Self.setup, &input, 1, Self.log2n, FFTDirection(kFFTDirection_Forward))
|
|
1025
|
+
sumReal.update(repeating: 0, count: block)
|
|
1026
|
+
sumImag.update(repeating: 0, count: block)
|
|
1027
|
+
var sum = DSPSplitComplex(realp: sumReal, imagp: sumImag)
|
|
1028
|
+
let response = min(channel, responses - 1)
|
|
1029
|
+
// Packed real spectra keep DC in real[0] and Nyquist in imag[0]:
|
|
1030
|
+
// they multiply on their own.
|
|
1031
|
+
var dc: Float = 0
|
|
1032
|
+
var nyquist: Float = 0
|
|
1033
|
+
for partition in 0..<partitions {
|
|
1034
|
+
let slot = ((head - partition) % partitions + partitions) % partitions
|
|
1035
|
+
var x = DSPSplitComplex(realp: delayReal[channel] + slot * block, imagp: delayImag[channel] + slot * block)
|
|
1036
|
+
var h = DSPSplitComplex(realp: spectraReal[response] + partition * block, imagp: spectraImag[response] + partition * block)
|
|
1037
|
+
dc += x.realp[0] * h.realp[0]
|
|
1038
|
+
nyquist += x.imagp[0] * h.imagp[0]
|
|
1039
|
+
withUnsafeMutablePointer(to: &sum) { total in
|
|
1040
|
+
vDSP_zvma(&x, 1, &h, 1, total, 1, total, 1, vDSP_Length(block))
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
sumReal[0] = dc
|
|
1044
|
+
sumImag[0] = nyquist
|
|
1045
|
+
vDSP_fft_zrip(Self.setup, &sum, 1, Self.log2n, FFTDirection(kFFTDirection_Inverse))
|
|
1046
|
+
time.withMemoryRebound(to: DSPComplex.self, capacity: block) { vDSP_ztoc(&sum, 1, $0, 2, vDSP_Length(block)) }
|
|
1047
|
+
tails[channel].update(from: time + block, count: block)
|
|
1048
|
+
window.update(from: window + block, count: block)
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// MARK: - Graph
|
|
1053
|
+
|
|
1054
|
+
enum CraftAudioOp {
|
|
1055
|
+
case connect(CraftAudioNode, CraftAudioNode)
|
|
1056
|
+
case connectParam(CraftAudioNode, CraftAudioParam, owner: CraftAudioNode)
|
|
1057
|
+
case disconnect(CraftAudioNode, CraftAudioNode?)
|
|
1058
|
+
case disconnectParam(CraftAudioNode, CraftAudioParam)
|
|
1059
|
+
case schedule(CraftAudioParam, CraftAudioParam.Event)
|
|
1060
|
+
case cancel(CraftAudioParam, Double, hold: Bool)
|
|
1061
|
+
case start(CraftAudioSource, when: Double, offset: Double, duration: Double)
|
|
1062
|
+
case stop(CraftAudioSource, Double)
|
|
1063
|
+
case shape(CraftAudioOscillator, Int)
|
|
1064
|
+
case filter(CraftAudioBiquad, Int)
|
|
1065
|
+
case buffer(CraftAudioBufferSource, CraftAudioBufferData?)
|
|
1066
|
+
case loop(CraftAudioBufferSource, Bool, Double, Double)
|
|
1067
|
+
case convolution(CraftAudioConvolver, CraftAudioConvolution?)
|
|
1068
|
+
case release(CraftAudioNode)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
enum CraftAudioNotice {
|
|
1072
|
+
case ended(Int)
|
|
1073
|
+
case removed(Int)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/// One AudioContext's graph. The main thread hands it ops and collects
|
|
1077
|
+
/// notices through a mailbox; the render thread owns everything else and
|
|
1078
|
+
/// never waits for the mailbox (it tries the lock and comes back next time).
|
|
1079
|
+
final class CraftAudioGraph {
|
|
1080
|
+
let sampleRate: Double
|
|
1081
|
+
let destination = CraftAudioNode(id: 0)
|
|
1082
|
+
private var quantum: Int64 = 0
|
|
1083
|
+
private var sources: [CraftAudioSource] = []
|
|
1084
|
+
private var released: [CraftAudioNode] = []
|
|
1085
|
+
private var notices: [CraftAudioNotice] = []
|
|
1086
|
+
private var ops: [CraftAudioOp] = []
|
|
1087
|
+
private let carryLeft: UnsafeMutablePointer<Float>
|
|
1088
|
+
private let carryRight: UnsafeMutablePointer<Float>
|
|
1089
|
+
private var carried = craftAudioQuantum
|
|
1090
|
+
private var searchMark = 0
|
|
1091
|
+
private var rendering = true
|
|
1092
|
+
private let lock: UnsafeMutablePointer<os_unfair_lock>
|
|
1093
|
+
private var inbox: [CraftAudioOp] = []
|
|
1094
|
+
private var outbox: [CraftAudioNotice] = []
|
|
1095
|
+
private var publishedQuantum: Int64 = 0
|
|
1096
|
+
private var wantsRunning = true
|
|
1097
|
+
|
|
1098
|
+
init(sampleRate: Double) {
|
|
1099
|
+
self.sampleRate = sampleRate
|
|
1100
|
+
carryLeft = .allocate(capacity: craftAudioQuantum)
|
|
1101
|
+
carryLeft.initialize(repeating: 0, count: craftAudioQuantum)
|
|
1102
|
+
carryRight = .allocate(capacity: craftAudioQuantum)
|
|
1103
|
+
carryRight.initialize(repeating: 0, count: craftAudioQuantum)
|
|
1104
|
+
lock = .allocate(capacity: 1)
|
|
1105
|
+
lock.initialize(to: os_unfair_lock())
|
|
1106
|
+
ops.reserveCapacity(512)
|
|
1107
|
+
notices.reserveCapacity(512)
|
|
1108
|
+
released.reserveCapacity(512)
|
|
1109
|
+
sources.reserveCapacity(256)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
deinit {
|
|
1113
|
+
carryLeft.deallocate()
|
|
1114
|
+
carryRight.deallocate()
|
|
1115
|
+
lock.deinitialize(count: 1)
|
|
1116
|
+
lock.deallocate()
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// MARK: Main thread
|
|
1120
|
+
|
|
1121
|
+
func enqueue(_ batch: [CraftAudioOp]) {
|
|
1122
|
+
guard !batch.isEmpty else { return }
|
|
1123
|
+
os_unfair_lock_lock(lock)
|
|
1124
|
+
inbox.append(contentsOf: batch)
|
|
1125
|
+
os_unfair_lock_unlock(lock)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/// Suspended or running: a suspended context renders nothing and its
|
|
1129
|
+
/// clock stands still.
|
|
1130
|
+
func setRunning(_ running: Bool) {
|
|
1131
|
+
os_unfair_lock_lock(lock)
|
|
1132
|
+
wantsRunning = running
|
|
1133
|
+
os_unfair_lock_unlock(lock)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/// The context's clock: the start of the next quantum to render.
|
|
1137
|
+
var currentTime: Double {
|
|
1138
|
+
os_unfair_lock_lock(lock)
|
|
1139
|
+
let quantum = publishedQuantum
|
|
1140
|
+
os_unfair_lock_unlock(lock)
|
|
1141
|
+
return Double(quantum * Int64(craftAudioQuantum)) / sampleRate
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
func collect() -> [CraftAudioNotice] {
|
|
1145
|
+
os_unfair_lock_lock(lock)
|
|
1146
|
+
let collected = outbox
|
|
1147
|
+
outbox.removeAll(keepingCapacity: true)
|
|
1148
|
+
os_unfair_lock_unlock(lock)
|
|
1149
|
+
return collected
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// MARK: Render thread
|
|
1153
|
+
|
|
1154
|
+
/// Renders `frames` into the engine's buffers. True when it was all silence.
|
|
1155
|
+
func render(_ frames: Int, into buffers: UnsafeMutableAudioBufferListPointer) -> Bool {
|
|
1156
|
+
guard let first = buffers.first?.mData?.assumingMemoryBound(to: Float.self) else { return true }
|
|
1157
|
+
let second = buffers.count > 1 ? buffers[1].mData?.assumingMemoryBound(to: Float.self) : nil
|
|
1158
|
+
return render(frames, left: first, right: second)
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
@discardableResult
|
|
1162
|
+
func render(_ frames: Int, left: UnsafeMutablePointer<Float>, right: UnsafeMutablePointer<Float>?) -> Bool {
|
|
1163
|
+
receive()
|
|
1164
|
+
guard rendering else {
|
|
1165
|
+
left.update(repeating: 0, count: frames)
|
|
1166
|
+
right?.update(repeating: 0, count: frames)
|
|
1167
|
+
return true
|
|
1168
|
+
}
|
|
1169
|
+
var done = 0
|
|
1170
|
+
var audible = false
|
|
1171
|
+
while done < frames {
|
|
1172
|
+
if carried == craftAudioQuantum {
|
|
1173
|
+
renderQuantum()
|
|
1174
|
+
carried = 0
|
|
1175
|
+
}
|
|
1176
|
+
if !destination.silent { audible = true }
|
|
1177
|
+
let count = min(craftAudioQuantum - carried, frames - done)
|
|
1178
|
+
(left + done).update(from: carryLeft + carried, count: count)
|
|
1179
|
+
(right.map { $0 + done })?.update(from: carryRight + carried, count: count)
|
|
1180
|
+
carried += count
|
|
1181
|
+
done += count
|
|
1182
|
+
}
|
|
1183
|
+
publish()
|
|
1184
|
+
return !audible
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private func receive() {
|
|
1188
|
+
guard os_unfair_lock_trylock(lock) else { return }
|
|
1189
|
+
swap(&inbox, &ops)
|
|
1190
|
+
rendering = wantsRunning
|
|
1191
|
+
os_unfair_lock_unlock(lock)
|
|
1192
|
+
for op in ops { apply(op) }
|
|
1193
|
+
ops.removeAll(keepingCapacity: true)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private func publish() {
|
|
1197
|
+
guard os_unfair_lock_trylock(lock) else { return }
|
|
1198
|
+
publishedQuantum = quantum
|
|
1199
|
+
outbox.append(contentsOf: notices)
|
|
1200
|
+
os_unfair_lock_unlock(lock)
|
|
1201
|
+
notices.removeAll(keepingCapacity: true)
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private func renderQuantum() {
|
|
1205
|
+
let render = CraftAudioRender(quantum: quantum, sampleRate: sampleRate)
|
|
1206
|
+
destination.pull(render)
|
|
1207
|
+
if destination.silent {
|
|
1208
|
+
carryLeft.update(repeating: 0, count: craftAudioQuantum)
|
|
1209
|
+
carryRight.update(repeating: 0, count: craftAudioQuantum)
|
|
1210
|
+
} else {
|
|
1211
|
+
carryLeft.update(from: destination.left, count: craftAudioQuantum)
|
|
1212
|
+
carryRight.update(from: destination.right, count: craftAudioQuantum)
|
|
1213
|
+
}
|
|
1214
|
+
// A playing source keeps time even when nothing it feeds is heard.
|
|
1215
|
+
for source in sources { source.pull(render) }
|
|
1216
|
+
var index = 0
|
|
1217
|
+
while index < sources.count {
|
|
1218
|
+
let source = sources[index]
|
|
1219
|
+
if source.ended {
|
|
1220
|
+
sources.remove(at: index)
|
|
1221
|
+
notices.append(.ended(source.id))
|
|
1222
|
+
remove(source)
|
|
1223
|
+
} else {
|
|
1224
|
+
index += 1
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if quantum % 8 == 0 { sweep(render) }
|
|
1228
|
+
quantum += 1
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
private func apply(_ op: CraftAudioOp) {
|
|
1232
|
+
switch op {
|
|
1233
|
+
case .connect(let from, let to):
|
|
1234
|
+
guard !from.dead, !to.dead, !from.outputs.contains(where: { $0 === to }) else { return }
|
|
1235
|
+
from.outputs.append(to)
|
|
1236
|
+
to.inputs.append(from)
|
|
1237
|
+
case .connectParam(let from, let param, let owner):
|
|
1238
|
+
guard !from.dead, !owner.dead, !from.paramOutputs.contains(where: { $0 === param }) else { return }
|
|
1239
|
+
from.paramOutputs.append(param)
|
|
1240
|
+
param.inputs.append(from)
|
|
1241
|
+
case .disconnect(let from, let to):
|
|
1242
|
+
if let to {
|
|
1243
|
+
from.outputs.removeAll { $0 === to }
|
|
1244
|
+
to.inputs.removeAll { $0 === from }
|
|
1245
|
+
} else {
|
|
1246
|
+
for output in from.outputs { output.inputs.removeAll { $0 === from } }
|
|
1247
|
+
for param in from.paramOutputs { param.inputs.removeAll { $0 === from } }
|
|
1248
|
+
from.outputs.removeAll(keepingCapacity: true)
|
|
1249
|
+
from.paramOutputs.removeAll(keepingCapacity: true)
|
|
1250
|
+
}
|
|
1251
|
+
case .disconnectParam(let from, let param):
|
|
1252
|
+
from.paramOutputs.removeAll { $0 === param }
|
|
1253
|
+
param.inputs.removeAll { $0 === from }
|
|
1254
|
+
case .schedule(let param, let event):
|
|
1255
|
+
param.schedule(event)
|
|
1256
|
+
case .cancel(let param, let time, let hold):
|
|
1257
|
+
param.cancel(from: time, hold: hold)
|
|
1258
|
+
case .start(let source, let time, let offset, let duration):
|
|
1259
|
+
guard !source.dead, !source.started else { return }
|
|
1260
|
+
source.started = true
|
|
1261
|
+
source.startTime = time
|
|
1262
|
+
if let buffered = source as? CraftAudioBufferSource {
|
|
1263
|
+
buffered.offset = offset
|
|
1264
|
+
buffered.duration = duration
|
|
1265
|
+
}
|
|
1266
|
+
sources.append(source)
|
|
1267
|
+
case .stop(let source, let time):
|
|
1268
|
+
guard !source.dead, source.started else { return }
|
|
1269
|
+
source.stopTime = time
|
|
1270
|
+
case .shape(let oscillator, let shape):
|
|
1271
|
+
oscillator.shape = shape
|
|
1272
|
+
case .filter(let biquad, let type):
|
|
1273
|
+
biquad.type = type
|
|
1274
|
+
case .buffer(let source, let data):
|
|
1275
|
+
source.buffer = data
|
|
1276
|
+
case .loop(let source, let loop, let start, let end):
|
|
1277
|
+
source.loop = loop
|
|
1278
|
+
source.loopStart = start
|
|
1279
|
+
source.loopEnd = end
|
|
1280
|
+
case .convolution(let node, let convolution):
|
|
1281
|
+
node.convolution = convolution
|
|
1282
|
+
case .release(let node):
|
|
1283
|
+
guard !node.dead, !node.released else { return }
|
|
1284
|
+
node.released = true
|
|
1285
|
+
released.append(node)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/// Removes the released nodes that can never sound again: everything
|
|
1290
|
+
/// upstream of them is released too (the script holds none of it, so
|
|
1291
|
+
/// nothing new can be connected) and has gone quiet.
|
|
1292
|
+
private func sweep(_ render: CraftAudioRender) {
|
|
1293
|
+
var index = 0
|
|
1294
|
+
while index < released.count {
|
|
1295
|
+
let node = released[index]
|
|
1296
|
+
if node.dead {
|
|
1297
|
+
released.remove(at: index)
|
|
1298
|
+
continue
|
|
1299
|
+
}
|
|
1300
|
+
searchMark += 1
|
|
1301
|
+
if finished(node, render) {
|
|
1302
|
+
released.remove(at: index)
|
|
1303
|
+
remove(node)
|
|
1304
|
+
} else {
|
|
1305
|
+
index += 1
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
private func finished(_ node: CraftAudioNode, _ render: CraftAudioRender) -> Bool {
|
|
1311
|
+
if node.mark == searchMark { return true }
|
|
1312
|
+
node.mark = searchMark
|
|
1313
|
+
if node.dead { return true }
|
|
1314
|
+
guard node.released, render.quantum - node.lastActive > node.tail else { return false }
|
|
1315
|
+
if let source = node as? CraftAudioSource, source.started, !source.ended { return false }
|
|
1316
|
+
for input in node.inputs where !finished(input, render) { return false }
|
|
1317
|
+
for param in node.params {
|
|
1318
|
+
for input in param.inputs where !finished(input, render) { return false }
|
|
1319
|
+
}
|
|
1320
|
+
return true
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private func remove(_ node: CraftAudioNode) {
|
|
1324
|
+
guard !node.dead else { return }
|
|
1325
|
+
node.dead = true
|
|
1326
|
+
for output in node.outputs { output.inputs.removeAll { $0 === node } }
|
|
1327
|
+
for param in node.paramOutputs { param.inputs.removeAll { $0 === node } }
|
|
1328
|
+
for input in node.inputs { input.outputs.removeAll { $0 === node } }
|
|
1329
|
+
for param in node.params {
|
|
1330
|
+
for input in param.inputs { input.paramOutputs.removeAll { $0 === param } }
|
|
1331
|
+
param.inputs.removeAll(keepingCapacity: true)
|
|
1332
|
+
}
|
|
1333
|
+
node.inputs.removeAll(keepingCapacity: true)
|
|
1334
|
+
node.outputs.removeAll(keepingCapacity: true)
|
|
1335
|
+
node.paramOutputs.removeAll(keepingCapacity: true)
|
|
1336
|
+
// The main thread drops its reference, so the node is freed there.
|
|
1337
|
+
notices.append(.removed(node.id))
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// MARK: - The screen's audio
|
|
1342
|
+
|
|
1343
|
+
/// Every AudioContext a native screen makes, on one AVAudioEngine that the
|
|
1344
|
+
/// screen owns and stops when it closes.
|
|
1345
|
+
final class CraftWebAudio {
|
|
1346
|
+
private final class Context {
|
|
1347
|
+
let graph: CraftAudioGraph
|
|
1348
|
+
var source: AVAudioSourceNode?
|
|
1349
|
+
var nodes: [Int: CraftAudioNode] = [:]
|
|
1350
|
+
var running = true
|
|
1351
|
+
|
|
1352
|
+
init(graph: CraftAudioGraph) {
|
|
1353
|
+
self.graph = graph
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/// False in tests: no engine, the graph is rendered by hand.
|
|
1358
|
+
private let live: Bool
|
|
1359
|
+
private var contexts: [Int: Context] = [:]
|
|
1360
|
+
private var buffers: [Int: CraftAudioBufferData] = [:]
|
|
1361
|
+
private var engine: AVAudioEngine?
|
|
1362
|
+
private var claimedSession = false
|
|
1363
|
+
private var pump: Timer?
|
|
1364
|
+
private var observers: [NSObjectProtocol] = []
|
|
1365
|
+
private weak var script: JSContext?
|
|
1366
|
+
|
|
1367
|
+
init(live: Bool = true) {
|
|
1368
|
+
self.live = live
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
deinit {
|
|
1372
|
+
pump?.invalidate()
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/// Installs the native half and the `AudioContext` API in a screen's
|
|
1376
|
+
/// JavaScript, before its bundle runs.
|
|
1377
|
+
func install(in context: JSContext) {
|
|
1378
|
+
script = context
|
|
1379
|
+
let open: @convention(block) (Int, Double) -> NSDictionary? = { [weak self] id, rate in
|
|
1380
|
+
self?.open(id, requestedRate: rate) as NSDictionary?
|
|
1381
|
+
}
|
|
1382
|
+
let send: @convention(block) (JSValue) -> Void = { [weak self] batch in
|
|
1383
|
+
self?.receive(batch.toArray() ?? [])
|
|
1384
|
+
}
|
|
1385
|
+
let time: @convention(block) (Int) -> Double = { [weak self] id in
|
|
1386
|
+
self?.contexts[id]?.graph.currentTime ?? 0
|
|
1387
|
+
}
|
|
1388
|
+
let read: @convention(block) (Int, Int, String) -> Double = { [weak self] contextId, nodeId, key in
|
|
1389
|
+
self?.read(contextId, nodeId, key) ?? .nan
|
|
1390
|
+
}
|
|
1391
|
+
let state: @convention(block) (Int, String) -> Bool = { [weak self] id, state in
|
|
1392
|
+
self?.setState(id, state) ?? false
|
|
1393
|
+
}
|
|
1394
|
+
let buffer: @convention(block) (Int, Double, Int, JSValue) -> Bool = { [weak self] id, rate, length, channels in
|
|
1395
|
+
self?.storeBuffer(id, rate, length, channels) ?? false
|
|
1396
|
+
}
|
|
1397
|
+
context.setObject(open, forKeyedSubscript: "craftNativeAudioOpen" as NSString)
|
|
1398
|
+
context.setObject(send, forKeyedSubscript: "craftNativeAudioSend" as NSString)
|
|
1399
|
+
context.setObject(time, forKeyedSubscript: "craftNativeAudioTime" as NSString)
|
|
1400
|
+
context.setObject(read, forKeyedSubscript: "craftNativeAudioRead" as NSString)
|
|
1401
|
+
context.setObject(state, forKeyedSubscript: "craftNativeAudioState" as NSString)
|
|
1402
|
+
context.setObject(buffer, forKeyedSubscript: "craftNativeAudioBuffer" as NSString)
|
|
1403
|
+
context.evaluateScript(Self.script)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/// The screen closed: every context stops and the session is handed back.
|
|
1407
|
+
func close() {
|
|
1408
|
+
for id in Array(contexts.keys) { closeContext(id) }
|
|
1409
|
+
buffers.removeAll()
|
|
1410
|
+
stopEngine()
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// MARK: Contexts
|
|
1414
|
+
|
|
1415
|
+
private func open(_ id: Int, requestedRate: Double) -> [String: Any]? {
|
|
1416
|
+
let session = AVAudioSession.sharedInstance()
|
|
1417
|
+
let hardware = session.sampleRate > 0 ? session.sampleRate : 44_100
|
|
1418
|
+
let rate = (3_000...768_000).contains(requestedRate) ? requestedRate : hardware
|
|
1419
|
+
let graph = CraftAudioGraph(sampleRate: rate)
|
|
1420
|
+
let context = Context(graph: graph)
|
|
1421
|
+
contexts[id] = context
|
|
1422
|
+
context.nodes[0] = graph.destination
|
|
1423
|
+
if live {
|
|
1424
|
+
guard let format = AVAudioFormat(standardFormatWithSampleRate: rate, channels: 2) else {
|
|
1425
|
+
contexts[id] = nil
|
|
1426
|
+
return nil
|
|
1427
|
+
}
|
|
1428
|
+
let engine = self.engine ?? makeEngine()
|
|
1429
|
+
let source = AVAudioSourceNode(format: format) { silence, _, frameCount, bufferList in
|
|
1430
|
+
let buffers = UnsafeMutableAudioBufferListPointer(bufferList)
|
|
1431
|
+
silence.pointee = ObjCBool(graph.render(Int(frameCount), into: buffers))
|
|
1432
|
+
return noErr
|
|
1433
|
+
}
|
|
1434
|
+
engine.attach(source)
|
|
1435
|
+
engine.connect(source, to: engine.mainMixerNode, format: format)
|
|
1436
|
+
context.source = source
|
|
1437
|
+
refreshEngine()
|
|
1438
|
+
}
|
|
1439
|
+
startPump()
|
|
1440
|
+
return ["sampleRate": rate, "outputLatency": live ? session.outputLatency : 0]
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
private func setState(_ id: Int, _ state: String) -> Bool {
|
|
1444
|
+
guard let context = contexts[id] else { return false }
|
|
1445
|
+
switch state {
|
|
1446
|
+
case "running", "suspended":
|
|
1447
|
+
context.running = state == "running"
|
|
1448
|
+
context.graph.setRunning(context.running)
|
|
1449
|
+
refreshEngine()
|
|
1450
|
+
case "closed":
|
|
1451
|
+
closeContext(id)
|
|
1452
|
+
default:
|
|
1453
|
+
return false
|
|
1454
|
+
}
|
|
1455
|
+
return true
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
private func closeContext(_ id: Int) {
|
|
1459
|
+
guard let context = contexts.removeValue(forKey: id) else { return }
|
|
1460
|
+
if let source = context.source, let engine {
|
|
1461
|
+
engine.disconnectNodeOutput(source)
|
|
1462
|
+
engine.detach(source)
|
|
1463
|
+
}
|
|
1464
|
+
context.graph.setRunning(false)
|
|
1465
|
+
if contexts.isEmpty {
|
|
1466
|
+
stopEngine()
|
|
1467
|
+
pump?.invalidate()
|
|
1468
|
+
pump = nil
|
|
1469
|
+
} else {
|
|
1470
|
+
refreshEngine()
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// MARK: Engine and session
|
|
1475
|
+
|
|
1476
|
+
private func makeEngine() -> AVAudioEngine {
|
|
1477
|
+
let engine = AVAudioEngine()
|
|
1478
|
+
self.engine = engine
|
|
1479
|
+
let center = NotificationCenter.default
|
|
1480
|
+
// A route change (headphones) or an interruption (a call) stops the
|
|
1481
|
+
// engine; it starts again when the context still plays.
|
|
1482
|
+
observers.append(center.addObserver(forName: .AVAudioEngineConfigurationChange, object: engine, queue: .main) { [weak self] _ in
|
|
1483
|
+
self?.refreshEngine()
|
|
1484
|
+
})
|
|
1485
|
+
observers.append(center.addObserver(forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { [weak self] note in
|
|
1486
|
+
let type = (note.userInfo?[AVAudioSessionInterruptionTypeKey] as? NSNumber)?.uintValue
|
|
1487
|
+
if type == AVAudioSession.InterruptionType.ended.rawValue { self?.refreshEngine() }
|
|
1488
|
+
})
|
|
1489
|
+
observers.append(center.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
|
|
1490
|
+
self?.refreshEngine()
|
|
1491
|
+
})
|
|
1492
|
+
return engine
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private func refreshEngine() {
|
|
1496
|
+
guard live, let engine else { return }
|
|
1497
|
+
let playing = contexts.values.contains { $0.running }
|
|
1498
|
+
if playing {
|
|
1499
|
+
if !claimedSession {
|
|
1500
|
+
claimedSession = true
|
|
1501
|
+
CraftNativeSounds.claimSession(respectSilentSwitch: true)
|
|
1502
|
+
}
|
|
1503
|
+
guard !engine.isRunning else { return }
|
|
1504
|
+
engine.prepare()
|
|
1505
|
+
do {
|
|
1506
|
+
try engine.start()
|
|
1507
|
+
} catch {
|
|
1508
|
+
NSLog("[craft native] Web Audio could not start: %@", error.localizedDescription)
|
|
1509
|
+
}
|
|
1510
|
+
} else if engine.isRunning {
|
|
1511
|
+
engine.pause()
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
private func stopEngine() {
|
|
1516
|
+
observers.forEach(NotificationCenter.default.removeObserver)
|
|
1517
|
+
observers.removeAll()
|
|
1518
|
+
engine?.stop()
|
|
1519
|
+
engine = nil
|
|
1520
|
+
if claimedSession {
|
|
1521
|
+
claimedSession = false
|
|
1522
|
+
CraftNativeSounds.releaseSession()
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// MARK: Commands
|
|
1527
|
+
|
|
1528
|
+
private func receive(_ commands: [Any]) {
|
|
1529
|
+
var batches: [Int: [CraftAudioOp]] = [:]
|
|
1530
|
+
for case let command as [Any] in commands {
|
|
1531
|
+
guard let name = command.first as? String else { continue }
|
|
1532
|
+
if name == "releaseBuffer" {
|
|
1533
|
+
buffers[int(command, 2)] = nil
|
|
1534
|
+
continue
|
|
1535
|
+
}
|
|
1536
|
+
let contextId = int(command, 1)
|
|
1537
|
+
guard let context = contexts[contextId] else { continue }
|
|
1538
|
+
if let op = op(name, command, context) { batches[contextId, default: []].append(op) }
|
|
1539
|
+
}
|
|
1540
|
+
for (id, ops) in batches { contexts[id]?.graph.enqueue(ops) }
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
private func op(_ name: String, _ command: [Any], _ context: Context) -> CraftAudioOp? {
|
|
1544
|
+
let id = int(command, 2)
|
|
1545
|
+
if name == "create" {
|
|
1546
|
+
create(id, kind: command.count > 3 ? command[3] as? String ?? "" : "", extra: double(command, 4), in: context)
|
|
1547
|
+
return nil
|
|
1548
|
+
}
|
|
1549
|
+
guard let node = context.nodes[id] else { return nil }
|
|
1550
|
+
switch name {
|
|
1551
|
+
case "connect":
|
|
1552
|
+
guard let target = context.nodes[int(command, 3)] else { return nil }
|
|
1553
|
+
return .connect(node, target)
|
|
1554
|
+
case "connectParam":
|
|
1555
|
+
guard let owner = context.nodes[int(command, 3)], let param = owner.param(string(command, 4)) else { return nil }
|
|
1556
|
+
return .connectParam(node, param, owner: owner)
|
|
1557
|
+
case "disconnect":
|
|
1558
|
+
if command.count <= 3 || command[3] is NSNull { return .disconnect(node, nil) }
|
|
1559
|
+
guard let target = context.nodes[int(command, 3)] else { return nil }
|
|
1560
|
+
if command.count > 4, let paramName = command[4] as? String {
|
|
1561
|
+
guard let param = target.param(paramName) else { return nil }
|
|
1562
|
+
return .disconnectParam(node, param)
|
|
1563
|
+
}
|
|
1564
|
+
return .disconnect(node, target)
|
|
1565
|
+
case "param":
|
|
1566
|
+
guard let param = node.param(string(command, 3)) else { return nil }
|
|
1567
|
+
let value = double(command, 5)
|
|
1568
|
+
let time = max(0, double(command, 6))
|
|
1569
|
+
let extra = double(command, 7)
|
|
1570
|
+
switch string(command, 4) {
|
|
1571
|
+
case "set": return .schedule(param, .init(kind: .set, time: time, value: value))
|
|
1572
|
+
case "linear": return .schedule(param, .init(kind: .linear, time: time, value: value, scheduledAt: extra))
|
|
1573
|
+
case "exponential": return .schedule(param, .init(kind: .exponential, time: time, value: value, scheduledAt: extra))
|
|
1574
|
+
case "target": return .schedule(param, .init(kind: .target, time: time, value: value, timeConstant: max(0, extra)))
|
|
1575
|
+
case "curve":
|
|
1576
|
+
let curve = (command.count > 5 ? command[5] as? [Any] : nil)?.compactMap { ($0 as? NSNumber)?.doubleValue } ?? []
|
|
1577
|
+
guard curve.count > 1 else { return nil }
|
|
1578
|
+
return .schedule(param, .init(kind: .curve, time: time, value: curve.last ?? 0, duration: max(0, extra), curve: curve))
|
|
1579
|
+
case "cancel": return .cancel(param, time, hold: false)
|
|
1580
|
+
case "hold": return .cancel(param, time, hold: true)
|
|
1581
|
+
default: return nil
|
|
1582
|
+
}
|
|
1583
|
+
case "start":
|
|
1584
|
+
guard let source = node as? CraftAudioSource else { return nil }
|
|
1585
|
+
return .start(source, when: max(0, double(command, 3)), offset: max(0, double(command, 4)), duration: double(command, 5))
|
|
1586
|
+
case "stop":
|
|
1587
|
+
guard let source = node as? CraftAudioSource else { return nil }
|
|
1588
|
+
return .stop(source, max(0, double(command, 3)))
|
|
1589
|
+
case "type":
|
|
1590
|
+
if let oscillator = node as? CraftAudioOscillator { return .shape(oscillator, int(command, 3)) }
|
|
1591
|
+
if let biquad = node as? CraftAudioBiquad { return .filter(biquad, int(command, 3)) }
|
|
1592
|
+
return nil
|
|
1593
|
+
case "buffer":
|
|
1594
|
+
guard let source = node as? CraftAudioBufferSource else { return nil }
|
|
1595
|
+
return .buffer(source, buffers[int(command, 3)])
|
|
1596
|
+
case "loop":
|
|
1597
|
+
guard let source = node as? CraftAudioBufferSource else { return nil }
|
|
1598
|
+
return .loop(source, (command.count > 3 ? command[3] as? Bool : nil) ?? false, double(command, 4), double(command, 5))
|
|
1599
|
+
case "convolver":
|
|
1600
|
+
guard let convolver = node as? CraftAudioConvolver else { return nil }
|
|
1601
|
+
let normalize = (command.count > 4 ? command[4] as? Bool : nil) ?? true
|
|
1602
|
+
let data = command.count > 3 && !(command[3] is NSNull) ? buffers[int(command, 3)] : nil
|
|
1603
|
+
// The FFTs of the impulse response are worked out here, off the
|
|
1604
|
+
// render thread.
|
|
1605
|
+
return .convolution(convolver, data.map { CraftAudioConvolution(buffer: $0, normalize: normalize) })
|
|
1606
|
+
case "release":
|
|
1607
|
+
return .release(node)
|
|
1608
|
+
default:
|
|
1609
|
+
return nil
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private func create(_ id: Int, kind: String, extra: Double, in context: Context) {
|
|
1614
|
+
guard id > 0, context.nodes[id] == nil else { return }
|
|
1615
|
+
let rate = context.graph.sampleRate
|
|
1616
|
+
let node: CraftAudioNode
|
|
1617
|
+
switch kind {
|
|
1618
|
+
case "destination":
|
|
1619
|
+
context.nodes[id] = context.graph.destination
|
|
1620
|
+
return
|
|
1621
|
+
case "gain": node = CraftAudioGain(id: id)
|
|
1622
|
+
case "oscillator": node = CraftAudioOscillator(id: id, sampleRate: rate)
|
|
1623
|
+
case "bufferSource": node = CraftAudioBufferSource(id: id)
|
|
1624
|
+
case "biquad": node = CraftAudioBiquad(id: id, sampleRate: rate)
|
|
1625
|
+
case "delay": node = CraftAudioDelay(id: id, sampleRate: rate, maxDelay: min(180, max(1 / rate, extra > 0 ? extra : 1)))
|
|
1626
|
+
case "compressor": node = CraftAudioCompressor(id: id)
|
|
1627
|
+
case "convolver": node = CraftAudioConvolver(id: id)
|
|
1628
|
+
default: return
|
|
1629
|
+
}
|
|
1630
|
+
context.nodes[id] = node
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
private func read(_ contextId: Int, _ nodeId: Int, _ key: String) -> Double {
|
|
1634
|
+
guard let node = contexts[contextId]?.nodes[nodeId] else { return .nan }
|
|
1635
|
+
if key == "reduction" { return (node as? CraftAudioCompressor)?.reduction ?? .nan }
|
|
1636
|
+
return node.param(key)?.current ?? .nan
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/// An AudioBuffer's channels, copied straight out of its Float32Arrays.
|
|
1640
|
+
private func storeBuffer(_ id: Int, _ rate: Double, _ length: Int, _ channels: JSValue) -> Bool {
|
|
1641
|
+
guard length > 0, rate > 0, let context = JSContext.current() else { return false }
|
|
1642
|
+
let count = Int(channels.objectForKeyedSubscript("length")?.toInt32() ?? 0)
|
|
1643
|
+
guard count > 0 else { return false }
|
|
1644
|
+
let data = CraftAudioBufferData(sampleRate: rate, length: length, channelCount: count)
|
|
1645
|
+
let script = context.jsGlobalContextRef
|
|
1646
|
+
for index in 0..<count {
|
|
1647
|
+
guard let channel = channels.atIndex(index), !channel.isNull, !channel.isUndefined,
|
|
1648
|
+
JSValueGetTypedArrayType(script, channel.jsValueRef, nil) == kJSTypedArrayTypeFloat32Array,
|
|
1649
|
+
let object = JSValueToObject(script, channel.jsValueRef, nil),
|
|
1650
|
+
let bytes = JSObjectGetTypedArrayBytesPtr(script, object, nil) else { continue }
|
|
1651
|
+
let available = JSObjectGetTypedArrayLength(script, object, nil)
|
|
1652
|
+
data.channels[index].update(from: bytes.assumingMemoryBound(to: Float.self), count: min(length, available))
|
|
1653
|
+
}
|
|
1654
|
+
buffers[id] = data
|
|
1655
|
+
return true
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// MARK: Notices
|
|
1659
|
+
|
|
1660
|
+
private func startPump() {
|
|
1661
|
+
guard pump == nil else { return }
|
|
1662
|
+
let timer = Timer(timeInterval: 0.02, repeats: true) { [weak self] _ in self?.deliver() }
|
|
1663
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
1664
|
+
pump = timer
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/// Ended sources to their `onended`, removed nodes out of the table.
|
|
1668
|
+
func deliver() {
|
|
1669
|
+
var ended: [[Any]] = []
|
|
1670
|
+
for (id, context) in contexts {
|
|
1671
|
+
for notice in context.graph.collect() {
|
|
1672
|
+
switch notice {
|
|
1673
|
+
case .ended(let node): ended.append([id, node, "ended"])
|
|
1674
|
+
case .removed(let node): context.nodes[node] = nil
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
guard !ended.isEmpty else { return }
|
|
1679
|
+
script?.objectForKeyedSubscript("__craftNativeAudioEvents")?.call(withArguments: [ended])
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// MARK: Tests
|
|
1683
|
+
|
|
1684
|
+
/// Renders a context by hand (no engine), as `live: false` tests do.
|
|
1685
|
+
func renderForTesting(_ contextId: Int, frames: Int) -> (left: [Float], right: [Float]) {
|
|
1686
|
+
guard let graph = contexts[contextId]?.graph else { return ([], []) }
|
|
1687
|
+
var left = [Float](repeating: 0, count: frames)
|
|
1688
|
+
var right = [Float](repeating: 0, count: frames)
|
|
1689
|
+
left.withUnsafeMutableBufferPointer { l in
|
|
1690
|
+
right.withUnsafeMutableBufferPointer { r in
|
|
1691
|
+
var done = 0
|
|
1692
|
+
while done < frames {
|
|
1693
|
+
let count = min(512, frames - done)
|
|
1694
|
+
graph.render(count, left: l.baseAddress! + done, right: r.baseAddress! + done)
|
|
1695
|
+
done += count
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
deliver()
|
|
1700
|
+
return (left, right)
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/// The nodes a context still keeps, the destination included.
|
|
1704
|
+
func nodeCountForTesting(_ contextId: Int) -> Int {
|
|
1705
|
+
contexts[contextId]?.nodes.count ?? 0
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
var contextIdsForTesting: [Int] { Array(contexts.keys).sorted() }
|
|
1709
|
+
|
|
1710
|
+
// MARK: Helpers
|
|
1711
|
+
|
|
1712
|
+
private func int(_ command: [Any], _ index: Int) -> Int {
|
|
1713
|
+
index < command.count ? (command[index] as? NSNumber)?.intValue ?? 0 : 0
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
private func double(_ command: [Any], _ index: Int) -> Double {
|
|
1717
|
+
index < command.count ? (command[index] as? NSNumber)?.doubleValue ?? 0 : 0
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
private func string(_ command: [Any], _ index: Int) -> String {
|
|
1721
|
+
index < command.count ? command[index] as? String ?? "" : ""
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// MARK: - The script's half
|
|
1726
|
+
|
|
1727
|
+
extension CraftWebAudio {
|
|
1728
|
+
/// The standard Web Audio API for a native screen's JavaScript. Calls are
|
|
1729
|
+
/// queued and sent once per turn of the script; the clock and param reads
|
|
1730
|
+
/// are synchronous. A node the script lets go of (FinalizationRegistry)
|
|
1731
|
+
/// is released here and freed once it can no longer sound.
|
|
1732
|
+
static let script = #"""
|
|
1733
|
+
(function() {
|
|
1734
|
+
if (typeof globalThis.AudioContext === 'function' || typeof craftNativeAudioOpen !== 'function') return;
|
|
1735
|
+
var FLT = 3.4028234663852886e38;
|
|
1736
|
+
var nextId = 0;
|
|
1737
|
+
var queue = [];
|
|
1738
|
+
var pending = false;
|
|
1739
|
+
var contexts = {};
|
|
1740
|
+
|
|
1741
|
+
function flush() {
|
|
1742
|
+
pending = false;
|
|
1743
|
+
if (!queue.length) return;
|
|
1744
|
+
var batch = queue;
|
|
1745
|
+
queue = [];
|
|
1746
|
+
craftNativeAudioSend(batch);
|
|
1747
|
+
}
|
|
1748
|
+
function send(command) {
|
|
1749
|
+
queue.push(command);
|
|
1750
|
+
if (!pending) {
|
|
1751
|
+
pending = true;
|
|
1752
|
+
Promise.resolve().then(flush);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
var finalizer = typeof FinalizationRegistry === 'function'
|
|
1756
|
+
? new FinalizationRegistry(function(held) { send(held); })
|
|
1757
|
+
: null;
|
|
1758
|
+
function track(object, held) {
|
|
1759
|
+
if (finalizer) finalizer.register(object, held);
|
|
1760
|
+
}
|
|
1761
|
+
function failure(name, message) {
|
|
1762
|
+
if (typeof DOMException === 'function') return new DOMException(message, name);
|
|
1763
|
+
var error = new Error(message);
|
|
1764
|
+
error.name = name;
|
|
1765
|
+
return error;
|
|
1766
|
+
}
|
|
1767
|
+
function finite(value, what) {
|
|
1768
|
+
var number = Number(value);
|
|
1769
|
+
if (!isFinite(number)) throw new TypeError(what + ' must be a finite number');
|
|
1770
|
+
return number;
|
|
1771
|
+
}
|
|
1772
|
+
function when(value, what) {
|
|
1773
|
+
var number = finite(value === undefined ? 0 : value, what);
|
|
1774
|
+
if (number < 0) throw new RangeError(what + ' must not be negative');
|
|
1775
|
+
return number;
|
|
1776
|
+
}
|
|
1777
|
+
function define(prototype, name, get, set) {
|
|
1778
|
+
Object.defineProperty(prototype, name, { configurable: true, enumerable: true, get: get, set: set });
|
|
1779
|
+
}
|
|
1780
|
+
function inherit(Child, Parent) {
|
|
1781
|
+
Child.prototype = Object.create(Parent.prototype);
|
|
1782
|
+
Child.prototype.constructor = Child;
|
|
1783
|
+
}
|
|
1784
|
+
function makeEvent(type, target) {
|
|
1785
|
+
return {
|
|
1786
|
+
type: type, target: target, currentTarget: target, timeStamp: Date.now(),
|
|
1787
|
+
bubbles: false, cancelable: false, defaultPrevented: false,
|
|
1788
|
+
preventDefault: function() {}, stopPropagation: function() {}, stopImmediatePropagation: function() {}
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function report(error) {
|
|
1792
|
+
if (typeof console !== 'undefined' && console.error) console.error(error && error.stack ? error.stack : String(error));
|
|
1793
|
+
}
|
|
1794
|
+
function eventTarget(prototype) {
|
|
1795
|
+
prototype.addEventListener = function(type, listener) {
|
|
1796
|
+
if (!listener) return;
|
|
1797
|
+
var all = this._listeners || (this._listeners = {});
|
|
1798
|
+
var list = all[type] || (all[type] = []);
|
|
1799
|
+
if (list.indexOf(listener) < 0) list.push(listener);
|
|
1800
|
+
};
|
|
1801
|
+
prototype.removeEventListener = function(type, listener) {
|
|
1802
|
+
var list = this._listeners && this._listeners[type];
|
|
1803
|
+
var index = list ? list.indexOf(listener) : -1;
|
|
1804
|
+
if (index >= 0) list.splice(index, 1);
|
|
1805
|
+
};
|
|
1806
|
+
prototype.dispatchEvent = function(event) {
|
|
1807
|
+
var listeners = [];
|
|
1808
|
+
var handler = this['on' + event.type];
|
|
1809
|
+
if (typeof handler === 'function') listeners.push(handler);
|
|
1810
|
+
var list = this._listeners && this._listeners[event.type];
|
|
1811
|
+
if (list) listeners = listeners.concat(list);
|
|
1812
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
1813
|
+
try {
|
|
1814
|
+
if (typeof listeners[i] === 'function') listeners[i].call(this, event);
|
|
1815
|
+
else if (listeners[i] && typeof listeners[i].handleEvent === 'function') listeners[i].handleEvent(event);
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
report(error);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return true;
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// AudioParam --------------------------------------------------------
|
|
1825
|
+
|
|
1826
|
+
function AudioParam(node, name, value, min, max, rate) {
|
|
1827
|
+
this._context = node.context;
|
|
1828
|
+
this._node = node._id;
|
|
1829
|
+
this._name = name;
|
|
1830
|
+
this._value = value;
|
|
1831
|
+
this._automated = false;
|
|
1832
|
+
this._fresh = true;
|
|
1833
|
+
this.defaultValue = value;
|
|
1834
|
+
this.minValue = min;
|
|
1835
|
+
this.maxValue = max;
|
|
1836
|
+
this.automationRate = rate;
|
|
1837
|
+
}
|
|
1838
|
+
define(AudioParam.prototype, 'value', function() {
|
|
1839
|
+
if (this._automated && !this._fresh) {
|
|
1840
|
+
flush();
|
|
1841
|
+
var read = craftNativeAudioRead(this._context._id, this._node, this._name);
|
|
1842
|
+
if (typeof read === 'number' && isFinite(read)) return read;
|
|
1843
|
+
}
|
|
1844
|
+
return this._value;
|
|
1845
|
+
}, function(value) {
|
|
1846
|
+
var number = finite(value, 'AudioParam.value');
|
|
1847
|
+
this._value = number;
|
|
1848
|
+
this._fresh = true;
|
|
1849
|
+
send(['param', this._context._id, this._node, this._name, 'set', number, this._context.currentTime, 0]);
|
|
1850
|
+
});
|
|
1851
|
+
AudioParam.prototype._schedule = function(kind, value, time, extra) {
|
|
1852
|
+
this._automated = true;
|
|
1853
|
+
this._fresh = false;
|
|
1854
|
+
send(['param', this._context._id, this._node, this._name, kind, value, time, extra]);
|
|
1855
|
+
return this;
|
|
1856
|
+
};
|
|
1857
|
+
AudioParam.prototype.setValueAtTime = function(value, startTime) {
|
|
1858
|
+
return this._schedule('set', finite(value, 'value'), when(startTime, 'startTime'), 0);
|
|
1859
|
+
};
|
|
1860
|
+
AudioParam.prototype.linearRampToValueAtTime = function(value, endTime) {
|
|
1861
|
+
return this._schedule('linear', finite(value, 'value'), when(endTime, 'endTime'), this._context.currentTime);
|
|
1862
|
+
};
|
|
1863
|
+
AudioParam.prototype.exponentialRampToValueAtTime = function(value, endTime) {
|
|
1864
|
+
var number = finite(value, 'value');
|
|
1865
|
+
if (number === 0) throw new RangeError('exponentialRampToValueAtTime needs a value other than 0');
|
|
1866
|
+
return this._schedule('exponential', number, when(endTime, 'endTime'), this._context.currentTime);
|
|
1867
|
+
};
|
|
1868
|
+
AudioParam.prototype.setTargetAtTime = function(target, startTime, timeConstant) {
|
|
1869
|
+
var constant = finite(timeConstant, 'timeConstant');
|
|
1870
|
+
if (constant < 0) throw new RangeError('timeConstant must not be negative');
|
|
1871
|
+
return this._schedule('target', finite(target, 'target'), when(startTime, 'startTime'), constant);
|
|
1872
|
+
};
|
|
1873
|
+
AudioParam.prototype.setValueCurveAtTime = function(values, startTime, duration) {
|
|
1874
|
+
var curve = Array.prototype.slice.call(values || []).map(function(value) { return finite(value, 'curve value'); });
|
|
1875
|
+
if (curve.length < 2) throw failure('InvalidStateError', 'setValueCurveAtTime needs at least two values');
|
|
1876
|
+
var length = finite(duration, 'duration');
|
|
1877
|
+
if (length <= 0) throw new RangeError('duration must be above 0');
|
|
1878
|
+
return this._schedule('curve', curve, when(startTime, 'startTime'), length);
|
|
1879
|
+
};
|
|
1880
|
+
AudioParam.prototype.cancelScheduledValues = function(cancelTime) {
|
|
1881
|
+
return this._schedule('cancel', 0, when(cancelTime, 'cancelTime'), 0);
|
|
1882
|
+
};
|
|
1883
|
+
AudioParam.prototype.cancelAndHoldAtTime = function(cancelTime) {
|
|
1884
|
+
return this._schedule('hold', 0, when(cancelTime, 'cancelTime'), 0);
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// Nodes -------------------------------------------------------------
|
|
1888
|
+
|
|
1889
|
+
function BaseAudioContext() { throw new TypeError('Illegal constructor'); }
|
|
1890
|
+
eventTarget(BaseAudioContext.prototype);
|
|
1891
|
+
|
|
1892
|
+
function AudioNode(context, kind, inputs, outputs, extra) {
|
|
1893
|
+
if (!(context instanceof BaseAudioContext)) throw new TypeError('An audio node needs its AudioContext');
|
|
1894
|
+
this.context = context;
|
|
1895
|
+
this._id = ++nextId;
|
|
1896
|
+
this.numberOfInputs = inputs;
|
|
1897
|
+
this.numberOfOutputs = outputs;
|
|
1898
|
+
this.channelCount = 2;
|
|
1899
|
+
this.channelCountMode = 'max';
|
|
1900
|
+
this.channelInterpretation = 'speakers';
|
|
1901
|
+
send(['create', context._id, this._id, kind, extra === undefined ? 0 : extra]);
|
|
1902
|
+
if (kind !== 'destination') track(this, ['release', context._id, this._id]);
|
|
1903
|
+
}
|
|
1904
|
+
eventTarget(AudioNode.prototype);
|
|
1905
|
+
AudioNode.prototype.connect = function(destination, output, input) {
|
|
1906
|
+
if (destination instanceof AudioParam) {
|
|
1907
|
+
if (destination._context !== this.context) throw failure('InvalidAccessError', 'The AudioParam belongs to another AudioContext');
|
|
1908
|
+
send(['connectParam', this.context._id, this._id, destination._node, destination._name]);
|
|
1909
|
+
return undefined;
|
|
1910
|
+
}
|
|
1911
|
+
if (!(destination instanceof AudioNode)) throw new TypeError('connect needs an AudioNode or an AudioParam');
|
|
1912
|
+
if (destination.context !== this.context) throw failure('InvalidAccessError', 'The AudioNode belongs to another AudioContext');
|
|
1913
|
+
if ((output || 0) >= this.numberOfOutputs) throw failure('IndexSizeError', 'The node has no output ' + output);
|
|
1914
|
+
if ((input || 0) >= destination.numberOfInputs) throw failure('IndexSizeError', 'The node has no input ' + input);
|
|
1915
|
+
send(['connect', this.context._id, this._id, destination._id]);
|
|
1916
|
+
return destination;
|
|
1917
|
+
};
|
|
1918
|
+
AudioNode.prototype.disconnect = function(destination) {
|
|
1919
|
+
if (destination === undefined || typeof destination === 'number') send(['disconnect', this.context._id, this._id, null, null]);
|
|
1920
|
+
else if (destination instanceof AudioParam) send(['disconnect', this.context._id, this._id, destination._node, destination._name]);
|
|
1921
|
+
else if (destination instanceof AudioNode) send(['disconnect', this.context._id, this._id, destination._id, null]);
|
|
1922
|
+
else throw new TypeError('disconnect needs an AudioNode or an AudioParam');
|
|
1923
|
+
};
|
|
1924
|
+
function param(node, name, value, min, max, rate) {
|
|
1925
|
+
var created = new AudioParam(node, name, value, min, max, rate || 'a-rate');
|
|
1926
|
+
Object.defineProperty(node, name, { value: created, enumerable: true });
|
|
1927
|
+
return created;
|
|
1928
|
+
}
|
|
1929
|
+
function apply(node, given, names) {
|
|
1930
|
+
names.forEach(function(name) {
|
|
1931
|
+
if (given && given[name] !== undefined) node[name].value = given[name];
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function AudioDestinationNode(context) {
|
|
1936
|
+
AudioNode.call(this, context, 'destination', 1, 0);
|
|
1937
|
+
this.maxChannelCount = 2;
|
|
1938
|
+
}
|
|
1939
|
+
inherit(AudioDestinationNode, AudioNode);
|
|
1940
|
+
|
|
1941
|
+
function GainNode(context, given) {
|
|
1942
|
+
AudioNode.call(this, context, 'gain', 1, 1);
|
|
1943
|
+
param(this, 'gain', 1, -FLT, FLT);
|
|
1944
|
+
apply(this, given, ['gain']);
|
|
1945
|
+
}
|
|
1946
|
+
inherit(GainNode, AudioNode);
|
|
1947
|
+
|
|
1948
|
+
function AudioScheduledSourceNode(context, kind) {
|
|
1949
|
+
AudioNode.call(this, context, kind, 0, 1);
|
|
1950
|
+
this.onended = null;
|
|
1951
|
+
this._started = false;
|
|
1952
|
+
}
|
|
1953
|
+
inherit(AudioScheduledSourceNode, AudioNode);
|
|
1954
|
+
AudioScheduledSourceNode.prototype._begin = function(time, offset, duration) {
|
|
1955
|
+
if (this._started) throw failure('InvalidStateError', 'start() may be called only once');
|
|
1956
|
+
var at = when(time, 'when');
|
|
1957
|
+
this._started = true;
|
|
1958
|
+
// Playing keeps the node alive until it ends, as in a browser.
|
|
1959
|
+
if (this.context._state !== 'closed') this.context._playing[this._id] = this;
|
|
1960
|
+
send(['start', this.context._id, this._id, at, offset, duration]);
|
|
1961
|
+
};
|
|
1962
|
+
AudioScheduledSourceNode.prototype.start = function(time) {
|
|
1963
|
+
this._begin(time, 0, -1);
|
|
1964
|
+
};
|
|
1965
|
+
AudioScheduledSourceNode.prototype.stop = function(time) {
|
|
1966
|
+
if (!this._started) throw failure('InvalidStateError', 'stop() needs start() first');
|
|
1967
|
+
send(['stop', this.context._id, this._id, when(time, 'when')]);
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
var oscillatorTypes = ['sine', 'square', 'sawtooth', 'triangle'];
|
|
1971
|
+
function OscillatorNode(context, given) {
|
|
1972
|
+
AudioScheduledSourceNode.call(this, context, 'oscillator');
|
|
1973
|
+
var nyquist = context.sampleRate / 2;
|
|
1974
|
+
param(this, 'frequency', 440, -nyquist, nyquist);
|
|
1975
|
+
param(this, 'detune', 0, -153600, 153600);
|
|
1976
|
+
this._type = 'sine';
|
|
1977
|
+
if (given && given.type !== undefined) this.type = given.type;
|
|
1978
|
+
apply(this, given, ['frequency', 'detune']);
|
|
1979
|
+
}
|
|
1980
|
+
inherit(OscillatorNode, AudioScheduledSourceNode);
|
|
1981
|
+
define(OscillatorNode.prototype, 'type', function() { return this._type; }, function(value) {
|
|
1982
|
+
if (value === 'custom') throw failure('InvalidStateError', 'A custom oscillator needs setPeriodicWave');
|
|
1983
|
+
var index = oscillatorTypes.indexOf(value);
|
|
1984
|
+
if (index < 0 || value === this._type) return;
|
|
1985
|
+
this._type = value;
|
|
1986
|
+
send(['type', this.context._id, this._id, index]);
|
|
1987
|
+
});
|
|
1988
|
+
OscillatorNode.prototype.setPeriodicWave = function() {
|
|
1989
|
+
throw failure('NotSupportedError', 'Native screens play sine, square, sawtooth and triangle oscillators');
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
function AudioBufferSourceNode(context, given) {
|
|
1993
|
+
AudioScheduledSourceNode.call(this, context, 'bufferSource');
|
|
1994
|
+
param(this, 'playbackRate', 1, -FLT, FLT, 'k-rate');
|
|
1995
|
+
param(this, 'detune', 0, -FLT, FLT, 'k-rate');
|
|
1996
|
+
this._buffer = null;
|
|
1997
|
+
this._loop = false;
|
|
1998
|
+
this._loopStart = 0;
|
|
1999
|
+
this._loopEnd = 0;
|
|
2000
|
+
given = given || {};
|
|
2001
|
+
if (given.buffer) this.buffer = given.buffer;
|
|
2002
|
+
if (given.loop !== undefined) this.loop = given.loop;
|
|
2003
|
+
if (given.loopStart !== undefined) this.loopStart = given.loopStart;
|
|
2004
|
+
if (given.loopEnd !== undefined) this.loopEnd = given.loopEnd;
|
|
2005
|
+
apply(this, given, ['playbackRate', 'detune']);
|
|
2006
|
+
}
|
|
2007
|
+
inherit(AudioBufferSourceNode, AudioScheduledSourceNode);
|
|
2008
|
+
define(AudioBufferSourceNode.prototype, 'buffer', function() { return this._buffer; }, function(value) {
|
|
2009
|
+
if (value === undefined) value = null;
|
|
2010
|
+
if (value !== null && !(value instanceof AudioBuffer)) throw new TypeError('buffer must be an AudioBuffer');
|
|
2011
|
+
if (value !== null && this._buffer !== null) throw failure('InvalidStateError', 'The buffer may be set only once');
|
|
2012
|
+
this._buffer = value;
|
|
2013
|
+
if (value && this._started) this._acquire();
|
|
2014
|
+
});
|
|
2015
|
+
AudioBufferSourceNode.prototype._acquire = function() {
|
|
2016
|
+
upload(this._buffer);
|
|
2017
|
+
send(['buffer', this.context._id, this._id, this._buffer._id]);
|
|
2018
|
+
};
|
|
2019
|
+
AudioBufferSourceNode.prototype._sendLoop = function() {
|
|
2020
|
+
send(['loop', this.context._id, this._id, this._loop, this._loopStart, this._loopEnd]);
|
|
2021
|
+
};
|
|
2022
|
+
define(AudioBufferSourceNode.prototype, 'loop', function() { return this._loop; }, function(value) {
|
|
2023
|
+
this._loop = !!value;
|
|
2024
|
+
this._sendLoop();
|
|
2025
|
+
});
|
|
2026
|
+
define(AudioBufferSourceNode.prototype, 'loopStart', function() { return this._loopStart; }, function(value) {
|
|
2027
|
+
this._loopStart = finite(value, 'loopStart');
|
|
2028
|
+
this._sendLoop();
|
|
2029
|
+
});
|
|
2030
|
+
define(AudioBufferSourceNode.prototype, 'loopEnd', function() { return this._loopEnd; }, function(value) {
|
|
2031
|
+
this._loopEnd = finite(value, 'loopEnd');
|
|
2032
|
+
this._sendLoop();
|
|
2033
|
+
});
|
|
2034
|
+
AudioBufferSourceNode.prototype.start = function(time, offset, duration) {
|
|
2035
|
+
if (this._started) throw failure('InvalidStateError', 'start() may be called only once');
|
|
2036
|
+
var from = offset === undefined ? 0 : when(offset, 'offset');
|
|
2037
|
+
var length = duration === undefined ? -1 : when(duration, 'duration');
|
|
2038
|
+
if (this._buffer) this._acquire();
|
|
2039
|
+
this._begin(time, from, length);
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
var filterTypes = ['lowpass', 'highpass', 'bandpass', 'lowshelf', 'highshelf', 'peaking', 'notch', 'allpass'];
|
|
2043
|
+
function BiquadFilterNode(context, given) {
|
|
2044
|
+
AudioNode.call(this, context, 'biquad', 1, 1);
|
|
2045
|
+
param(this, 'frequency', 350, 0, context.sampleRate / 2);
|
|
2046
|
+
param(this, 'detune', 0, -153600, 153600);
|
|
2047
|
+
param(this, 'Q', 1, -FLT, FLT);
|
|
2048
|
+
param(this, 'gain', 0, -FLT, 1541);
|
|
2049
|
+
this._type = 'lowpass';
|
|
2050
|
+
if (given && given.type !== undefined) this.type = given.type;
|
|
2051
|
+
apply(this, given, ['frequency', 'detune', 'Q', 'gain']);
|
|
2052
|
+
}
|
|
2053
|
+
inherit(BiquadFilterNode, AudioNode);
|
|
2054
|
+
define(BiquadFilterNode.prototype, 'type', function() { return this._type; }, function(value) {
|
|
2055
|
+
var index = filterTypes.indexOf(value);
|
|
2056
|
+
if (index < 0 || value === this._type) return;
|
|
2057
|
+
this._type = value;
|
|
2058
|
+
send(['type', this.context._id, this._id, index]);
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
function DelayNode(context, given) {
|
|
2062
|
+
var max = given && given.maxDelayTime !== undefined ? Number(given.maxDelayTime) : 1;
|
|
2063
|
+
if (!(max > 0 && max < 180)) throw failure('NotSupportedError', 'maxDelayTime must be above 0 and under 180 seconds');
|
|
2064
|
+
AudioNode.call(this, context, 'delay', 1, 1, max);
|
|
2065
|
+
param(this, 'delayTime', 0, 0, max);
|
|
2066
|
+
apply(this, given, ['delayTime']);
|
|
2067
|
+
}
|
|
2068
|
+
inherit(DelayNode, AudioNode);
|
|
2069
|
+
|
|
2070
|
+
function DynamicsCompressorNode(context, given) {
|
|
2071
|
+
AudioNode.call(this, context, 'compressor', 1, 1);
|
|
2072
|
+
param(this, 'threshold', -24, -100, 0, 'k-rate');
|
|
2073
|
+
param(this, 'knee', 30, 0, 40, 'k-rate');
|
|
2074
|
+
param(this, 'ratio', 12, 1, 20, 'k-rate');
|
|
2075
|
+
param(this, 'attack', 0.003, 0, 1, 'k-rate');
|
|
2076
|
+
param(this, 'release', 0.25, 0, 1, 'k-rate');
|
|
2077
|
+
apply(this, given, ['threshold', 'knee', 'ratio', 'attack', 'release']);
|
|
2078
|
+
}
|
|
2079
|
+
inherit(DynamicsCompressorNode, AudioNode);
|
|
2080
|
+
define(DynamicsCompressorNode.prototype, 'reduction', function() {
|
|
2081
|
+
flush();
|
|
2082
|
+
var value = craftNativeAudioRead(this.context._id, this._id, 'reduction');
|
|
2083
|
+
return typeof value === 'number' && isFinite(value) ? value : 0;
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
function ConvolverNode(context, given) {
|
|
2087
|
+
AudioNode.call(this, context, 'convolver', 1, 1);
|
|
2088
|
+
this._buffer = null;
|
|
2089
|
+
this.normalize = !(given && given.disableNormalization);
|
|
2090
|
+
if (given && given.buffer) this.buffer = given.buffer;
|
|
2091
|
+
}
|
|
2092
|
+
inherit(ConvolverNode, AudioNode);
|
|
2093
|
+
define(ConvolverNode.prototype, 'buffer', function() { return this._buffer; }, function(value) {
|
|
2094
|
+
if (value === undefined) value = null;
|
|
2095
|
+
if (value !== null) {
|
|
2096
|
+
if (!(value instanceof AudioBuffer)) throw new TypeError('buffer must be an AudioBuffer');
|
|
2097
|
+
var channels = value.numberOfChannels;
|
|
2098
|
+
if (channels !== 1 && channels !== 2 && channels !== 4) throw failure('NotSupportedError', 'A convolver takes an impulse response of 1, 2 or 4 channels');
|
|
2099
|
+
if (value.sampleRate !== this.context.sampleRate) throw failure('NotSupportedError', "The impulse response must have the context's sample rate");
|
|
2100
|
+
upload(value);
|
|
2101
|
+
}
|
|
2102
|
+
this._buffer = value;
|
|
2103
|
+
send(['convolver', this.context._id, this._id, value ? value._id : null, !!this.normalize]);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
// AudioBuffer -------------------------------------------------------
|
|
2107
|
+
|
|
2108
|
+
function AudioBuffer(given) {
|
|
2109
|
+
if (!given || typeof given !== 'object') throw new TypeError('AudioBuffer needs { length, sampleRate }');
|
|
2110
|
+
var channels = given.numberOfChannels === undefined ? 1 : Math.floor(Number(given.numberOfChannels));
|
|
2111
|
+
var length = Math.floor(Number(given.length));
|
|
2112
|
+
var rate = Number(given.sampleRate);
|
|
2113
|
+
if (!(channels >= 1 && channels <= 32)) throw failure('NotSupportedError', 'An AudioBuffer has 1 to 32 channels');
|
|
2114
|
+
if (!(length >= 1)) throw failure('NotSupportedError', 'An AudioBuffer needs at least one frame');
|
|
2115
|
+
if (!(rate >= 3000 && rate <= 768000)) throw failure('NotSupportedError', 'An AudioBuffer sample rate is 3000 to 768000');
|
|
2116
|
+
this._id = ++nextId;
|
|
2117
|
+
this._length = length;
|
|
2118
|
+
this._rate = rate;
|
|
2119
|
+
this._channels = [];
|
|
2120
|
+
for (var i = 0; i < channels; i++) this._channels.push(null);
|
|
2121
|
+
this._dirty = false;
|
|
2122
|
+
this._uploaded = false;
|
|
2123
|
+
track(this, ['releaseBuffer', 0, this._id]);
|
|
2124
|
+
}
|
|
2125
|
+
define(AudioBuffer.prototype, 'length', function() { return this._length; });
|
|
2126
|
+
define(AudioBuffer.prototype, 'sampleRate', function() { return this._rate; });
|
|
2127
|
+
define(AudioBuffer.prototype, 'numberOfChannels', function() { return this._channels.length; });
|
|
2128
|
+
define(AudioBuffer.prototype, 'duration', function() { return this._length / this._rate; });
|
|
2129
|
+
AudioBuffer.prototype._channel = function(channel) {
|
|
2130
|
+
var index = Number(channel);
|
|
2131
|
+
if (!(index >= 0 && index < this._channels.length) || Math.floor(index) !== index) throw failure('IndexSizeError', 'The AudioBuffer has no channel ' + channel);
|
|
2132
|
+
return index;
|
|
2133
|
+
};
|
|
2134
|
+
AudioBuffer.prototype.getChannelData = function(channel) {
|
|
2135
|
+
var index = this._channel(channel);
|
|
2136
|
+
var data = this._channels[index] || (this._channels[index] = new Float32Array(this._length));
|
|
2137
|
+
// The caller may write into it: send it again when next used.
|
|
2138
|
+
this._dirty = true;
|
|
2139
|
+
return data;
|
|
2140
|
+
};
|
|
2141
|
+
AudioBuffer.prototype.copyFromChannel = function(destination, channel, start) {
|
|
2142
|
+
if (!(destination instanceof Float32Array)) throw new TypeError('copyFromChannel needs a Float32Array');
|
|
2143
|
+
var from = start === undefined ? 0 : Math.max(0, Math.floor(Number(start)) || 0);
|
|
2144
|
+
var data = this._channels[this._channel(channel)];
|
|
2145
|
+
var count = Math.max(0, Math.min(destination.length, this._length - from));
|
|
2146
|
+
if (!data) destination.fill(0, 0, count);
|
|
2147
|
+
else destination.set(data.subarray(from, from + count));
|
|
2148
|
+
};
|
|
2149
|
+
AudioBuffer.prototype.copyToChannel = function(source, channel, start) {
|
|
2150
|
+
if (!(source instanceof Float32Array)) throw new TypeError('copyToChannel needs a Float32Array');
|
|
2151
|
+
var from = start === undefined ? 0 : Math.max(0, Math.floor(Number(start)) || 0);
|
|
2152
|
+
var data = this.getChannelData(channel);
|
|
2153
|
+
var count = Math.max(0, Math.min(source.length, this._length - from));
|
|
2154
|
+
data.set(source.subarray(0, count), from);
|
|
2155
|
+
};
|
|
2156
|
+
function upload(buffer) {
|
|
2157
|
+
if (buffer._uploaded && !buffer._dirty) return;
|
|
2158
|
+
buffer._uploaded = true;
|
|
2159
|
+
buffer._dirty = false;
|
|
2160
|
+
craftNativeAudioBuffer(buffer._id, buffer._rate, buffer._length, buffer._channels);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// AudioContext ------------------------------------------------------
|
|
2164
|
+
|
|
2165
|
+
function AudioContext(given) {
|
|
2166
|
+
var requested = 0;
|
|
2167
|
+
if (given && given.sampleRate !== undefined) {
|
|
2168
|
+
requested = Number(given.sampleRate);
|
|
2169
|
+
if (!(requested >= 3000 && requested <= 768000)) throw failure('NotSupportedError', 'sampleRate must be 3000 to 768000');
|
|
2170
|
+
}
|
|
2171
|
+
var id = ++nextId;
|
|
2172
|
+
var info = craftNativeAudioOpen(id, requested);
|
|
2173
|
+
if (!info) throw failure('NotSupportedError', 'Audio could not start on this device');
|
|
2174
|
+
this._id = id;
|
|
2175
|
+
this._state = 'running';
|
|
2176
|
+
this._playing = {};
|
|
2177
|
+
this._closedTime = 0;
|
|
2178
|
+
this.sampleRate = info.sampleRate;
|
|
2179
|
+
this.baseLatency = 128 / info.sampleRate;
|
|
2180
|
+
this.outputLatency = info.outputLatency || 0;
|
|
2181
|
+
this.onstatechange = null;
|
|
2182
|
+
this.listener = {};
|
|
2183
|
+
contexts[id] = this;
|
|
2184
|
+
this.destination = new AudioDestinationNode(this);
|
|
2185
|
+
}
|
|
2186
|
+
AudioContext.prototype = Object.create(BaseAudioContext.prototype);
|
|
2187
|
+
AudioContext.prototype.constructor = AudioContext;
|
|
2188
|
+
define(BaseAudioContext.prototype, 'currentTime', function() {
|
|
2189
|
+
return this._state === 'closed' ? this._closedTime : craftNativeAudioTime(this._id);
|
|
2190
|
+
});
|
|
2191
|
+
define(BaseAudioContext.prototype, 'state', function() { return this._state; });
|
|
2192
|
+
BaseAudioContext.prototype._change = function(state) {
|
|
2193
|
+
flush();
|
|
2194
|
+
if (state === 'closed') this._closedTime = craftNativeAudioTime(this._id);
|
|
2195
|
+
craftNativeAudioState(this._id, state);
|
|
2196
|
+
if (this._state === state) return;
|
|
2197
|
+
this._state = state;
|
|
2198
|
+
var context = this;
|
|
2199
|
+
Promise.resolve().then(function() { context.dispatchEvent(makeEvent('statechange', context)); });
|
|
2200
|
+
};
|
|
2201
|
+
AudioContext.prototype.resume = function() {
|
|
2202
|
+
if (this._state === 'closed') return Promise.reject(failure('InvalidStateError', 'The AudioContext is closed'));
|
|
2203
|
+
this._change('running');
|
|
2204
|
+
return Promise.resolve();
|
|
2205
|
+
};
|
|
2206
|
+
AudioContext.prototype.suspend = function() {
|
|
2207
|
+
if (this._state === 'closed') return Promise.reject(failure('InvalidStateError', 'The AudioContext is closed'));
|
|
2208
|
+
this._change('suspended');
|
|
2209
|
+
return Promise.resolve();
|
|
2210
|
+
};
|
|
2211
|
+
AudioContext.prototype.close = function() {
|
|
2212
|
+
if (this._state === 'closed') return Promise.reject(failure('InvalidStateError', 'The AudioContext is already closed'));
|
|
2213
|
+
this._change('closed');
|
|
2214
|
+
delete contexts[this._id];
|
|
2215
|
+
this._playing = {};
|
|
2216
|
+
return Promise.resolve();
|
|
2217
|
+
};
|
|
2218
|
+
AudioContext.prototype.getOutputTimestamp = function() {
|
|
2219
|
+
var now = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
|
|
2220
|
+
return { contextTime: this.currentTime, performanceTime: now };
|
|
2221
|
+
};
|
|
2222
|
+
BaseAudioContext.prototype.createGain = function() { return new GainNode(this); };
|
|
2223
|
+
BaseAudioContext.prototype.createOscillator = function() { return new OscillatorNode(this); };
|
|
2224
|
+
BaseAudioContext.prototype.createBufferSource = function() { return new AudioBufferSourceNode(this); };
|
|
2225
|
+
BaseAudioContext.prototype.createBiquadFilter = function() { return new BiquadFilterNode(this); };
|
|
2226
|
+
BaseAudioContext.prototype.createDelay = function(maxDelayTime) {
|
|
2227
|
+
return new DelayNode(this, { maxDelayTime: maxDelayTime === undefined ? 1 : maxDelayTime });
|
|
2228
|
+
};
|
|
2229
|
+
BaseAudioContext.prototype.createDynamicsCompressor = function() { return new DynamicsCompressorNode(this); };
|
|
2230
|
+
BaseAudioContext.prototype.createConvolver = function() { return new ConvolverNode(this); };
|
|
2231
|
+
BaseAudioContext.prototype.createBuffer = function(numberOfChannels, length, sampleRate) {
|
|
2232
|
+
return new AudioBuffer({ numberOfChannels: numberOfChannels, length: length, sampleRate: sampleRate });
|
|
2233
|
+
};
|
|
2234
|
+
BaseAudioContext.prototype.decodeAudioData = function(data, success, error) {
|
|
2235
|
+
var problem = failure('NotSupportedError', 'Native screens decode no audio files: synthesise the sound, or play a file with craft.audio');
|
|
2236
|
+
if (typeof error === 'function') error(problem);
|
|
2237
|
+
return Promise.reject(problem);
|
|
2238
|
+
};
|
|
2239
|
+
|
|
2240
|
+
// Ended sources, from the host.
|
|
2241
|
+
globalThis.__craftNativeAudioEvents = function(events) {
|
|
2242
|
+
for (var i = 0; i < events.length; i++) {
|
|
2243
|
+
var entry = events[i];
|
|
2244
|
+
var context = contexts[entry[0]];
|
|
2245
|
+
var node = context && context._playing[entry[1]];
|
|
2246
|
+
if (!node) continue;
|
|
2247
|
+
delete context._playing[entry[1]];
|
|
2248
|
+
if (entry[2] === 'ended') node.dispatchEvent(makeEvent('ended', node));
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
globalThis.AudioContext = AudioContext;
|
|
2253
|
+
globalThis.webkitAudioContext = AudioContext;
|
|
2254
|
+
globalThis.BaseAudioContext = BaseAudioContext;
|
|
2255
|
+
globalThis.AudioNode = AudioNode;
|
|
2256
|
+
globalThis.AudioParam = AudioParam;
|
|
2257
|
+
globalThis.AudioBuffer = AudioBuffer;
|
|
2258
|
+
globalThis.AudioScheduledSourceNode = AudioScheduledSourceNode;
|
|
2259
|
+
globalThis.AudioDestinationNode = AudioDestinationNode;
|
|
2260
|
+
globalThis.GainNode = GainNode;
|
|
2261
|
+
globalThis.OscillatorNode = OscillatorNode;
|
|
2262
|
+
globalThis.AudioBufferSourceNode = AudioBufferSourceNode;
|
|
2263
|
+
globalThis.BiquadFilterNode = BiquadFilterNode;
|
|
2264
|
+
globalThis.DelayNode = DelayNode;
|
|
2265
|
+
globalThis.DynamicsCompressorNode = DynamicsCompressorNode;
|
|
2266
|
+
globalThis.ConvolverNode = ConvolverNode;
|
|
2267
|
+
})();
|
|
2268
|
+
"""#
|
|
2269
|
+
}
|