craft-native 0.0.150 → 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.
@@ -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
+ }