@swiftbrowser/native 0.19.0 → 0.21.0

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.
Files changed (64) hide show
  1. package/README.md +34 -11
  2. package/dist/blob.d.ts +85 -11
  3. package/dist/blob.d.ts.map +1 -1
  4. package/dist/blob.js +184 -34
  5. package/dist/blob.js.map +1 -1
  6. package/dist/bridge.d.ts +33 -2
  7. package/dist/bridge.d.ts.map +1 -1
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/fetch.d.ts +19 -11
  10. package/dist/fetch.d.ts.map +1 -1
  11. package/dist/fetch.js +105 -41
  12. package/dist/fetch.js.map +1 -1
  13. package/dist/hostRequests.d.ts +3 -1
  14. package/dist/hostRequests.d.ts.map +1 -1
  15. package/dist/hostRequests.js +47 -6
  16. package/dist/hostRequests.js.map +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/runtime.js +1 -1
  22. package/dist/runtime.js.map +1 -1
  23. package/dist/webSocket.d.ts.map +1 -1
  24. package/dist/webSocket.js +13 -1
  25. package/dist/webSocket.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/blob.ts +215 -34
  28. package/src/bridge.ts +27 -2
  29. package/src/fetch.ts +99 -41
  30. package/src/hostRequests.ts +62 -8
  31. package/src/index.ts +1 -2
  32. package/src/runtime.ts +1 -1
  33. package/src/webSocket.ts +10 -1
  34. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +122 -19
  35. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
  36. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1247 -209
  37. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +314 -19
  38. package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
  39. package/swift/Packages/SwiftBrowserShader/README.md +45 -0
  40. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
  41. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
  42. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
  43. package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
  44. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +141 -7
  45. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +47 -1
  46. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +98 -29
  47. package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
  48. package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
  49. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +11 -7
  50. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
  51. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +15 -10
  52. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +31 -28
  53. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +142 -0
  54. package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
  55. package/swift/Sources/SwiftBrowserProtocol/Props.swift +5 -2
  56. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
  57. package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
  58. package/wasm/SwiftBrowserNative.wasm +2 -2
  59. package/dist/native.d.ts +0 -19
  60. package/dist/native.d.ts.map +0 -1
  61. package/dist/native.js +0 -17
  62. package/dist/native.js.map +0 -1
  63. package/src/native.ts +0 -35
  64. package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
@@ -6,6 +6,10 @@ import Foundation
6
6
  /// voice; `block` runs once per block of up to 128 frames and `sample` once
7
7
  /// per frame, each a flat list of instructions: an opcode, a destination
8
8
  /// register, the opcode's operands and, for a stateful builtin, its instance.
9
+ /// A voice may have a right side (`outputRight`), and the `master` stage
10
+ /// runs after the voices (in lane 0). The machine runs `sample` and `master`
11
+ /// a chunk of frames at a time, all but their `serial` parts (word offsets),
12
+ /// which touch state and run frame by frame (block processing).
9
13
  public struct DSPProgram: Sendable, Equatable {
10
14
  public struct Parameter: Sendable, Equatable {
11
15
  public let name: String
@@ -19,30 +23,109 @@ public struct DSPProgram: Sendable, Equatable {
19
23
  public let name: String
20
24
  public let register: Int
21
25
  public let initial: Float
26
+ /// The master stage's: reset when the program loads and on `reset`, not when a voice starts.
27
+ public var master = false
28
+ }
29
+
30
+ /// A wavetable: its frames, each the amplitudes of its sine harmonics from the fundamental up, and with
31
+ /// `cosines` (one list a frame, each as long as its frame's) their cosine parts.
32
+ public struct Table: Sendable, Equatable {
33
+ public let name: String
34
+ public let frames: [[Float]]
35
+ public var cosines: [[Float]]? = nil
36
+ }
37
+
38
+ /// A drawn curve: values at evenly spaced points from x = 0 to x = 1.
39
+ public struct Shape: Sendable, Equatable {
40
+ public let name: String
41
+ public let points: [Float]
42
+ }
43
+
44
+ /// Audio: mono, `rate` samples a second.
45
+ public struct Sample: Sendable, Equatable {
46
+ public let name: String
47
+ public let rate: Double
48
+ public let data: [Float]
49
+ }
50
+
51
+ /// The master stage: the voices' sum goes into `inputs` (left, right) each frame, `code` runs in lane 0, and
52
+ /// `outputs` is the program's output. Its notes are the newest one's (registers 7 … 10), and its envelopes start
53
+ /// over at every note.
54
+ public struct Master: Sendable, Equatable {
55
+ public let code: [Int32]
56
+ /// The part of `code` that runs frame by frame, in words.
57
+ public let serial: Range<Int>?
58
+ public let inputs: [Int]
59
+ public let outputs: [Int]
60
+ }
61
+
62
+ /// One voice playing the newest held note: `mono` starts its envelopes over at each note, `legato` only when
63
+ /// no other note was held.
64
+ public enum Mode: String, Sendable, Equatable {
65
+ case mono, legato
22
66
  }
23
67
 
24
68
  public let voices: Int
69
+ /// Absent, polyphonic.
70
+ public let mode: Mode?
25
71
  public let registers: Int
26
72
  public let constants: [(register: Int, value: Float)]
27
73
  public let parameters: [Parameter]
28
74
  public let state: [State]
29
75
  public let instances: [DSPBuiltin]
76
+ public let tables: [Table]
77
+ public let shapes: [Shape]
78
+ public let samples: [Sample]
79
+ /// `modsum`'s route lists: a source's register, then its amount's, pair after pair.
80
+ public let routes: [[Int]]
81
+ /// Envelopes with curves (program.ts `curves`): an `adsr` instance and its attack, decay and release exponents.
82
+ public let curves: [Curve]
30
83
  public let block: [Int32]
31
84
  public let sample: [Int32]
85
+ /// The part of `sample` that runs frame by frame, in words.
86
+ public let serial: Range<Int>?
87
+ /// The voice's left side, or both with no `outputRight`.
32
88
  public let output: Int
89
+ public let outputRight: Int?
90
+ public let master: Master?
33
91
 
34
92
  public static func == (a: DSPProgram, b: DSPProgram) -> Bool {
35
- a.voices == b.voices && a.registers == b.registers && a.constants.map(\.register) == b.constants.map(\.register)
93
+ a.voices == b.voices && a.mode == b.mode && a.registers == b.registers && a.constants.map(\.register) == b.constants.map(\.register)
36
94
  && a.constants.map(\.value) == b.constants.map(\.value) && a.parameters == b.parameters && a.state == b.state
37
- && a.instances == b.instances && a.block == b.block && a.sample == b.sample && a.output == b.output
95
+ && a.instances == b.instances && a.tables == b.tables && a.shapes == b.shapes && a.samples == b.samples && a.routes == b.routes && a.curves == b.curves
96
+ && a.block == b.block && a.sample == b.sample && a.serial == b.serial && a.output == b.output && a.outputRight == b.outputRight && a.master == b.master
38
97
  }
39
98
 
40
99
  public static let format = "swiftbrowser-dsp"
100
+ /// The format's version: the machine runs only its own.
41
101
  public static let version = 1
42
102
  public static let lanes = 16
43
103
  public static let blockFrames = 128
44
- /// The machine's own registers: `sampleRate`, then a voice's `freq`, `gate`, `velocity` and `note`.
45
- public static let machineRegisters = 5
104
+ /// The machine's own registers: `sampleRate`, a voice's `freq`, `gate`, `velocity` and `note`, then the
105
+ /// transport's `tempo` and `beat`.
106
+ public static let machineRegisters = 11
107
+ /// Samples a wavetable frame has, at each of its band-limited levels (program.ts `TABLE_FRAME`, `TABLE_LEVELS`).
108
+ public static let tableFrame = 2048
109
+ public static let tableLevels = 10
110
+ /// The most harmonics a frame gives, and frames a program's tables have between them.
111
+ public static let tableHarmonics = 512
112
+ public static let tableFramesMax = 256
113
+ /// The bounds of an envelope curve's exponents (program.ts `CURVE_MIN`, `CURVE_MAX`).
114
+ public static let curveRange: ClosedRange<Float> = 1.0 / 16 ... 16
115
+
116
+ /// An envelope's curves: the exponents its attack, decay and release are bent by (1 as without).
117
+ public struct Curve: Equatable, Sendable {
118
+ public let instance: Int
119
+ public let attack: Float
120
+ public let decay: Float
121
+ public let release: Float
122
+ }
123
+ /// The longest a delay may be, in seconds.
124
+ public static let delaySecondsMax: Float = 2
125
+ /// Points a shape has, and its shapes between them; samples a program's samples have between them.
126
+ public static let shapePointsMax = 4096
127
+ public static let shapesPointsMax = 65_536
128
+ public static let samplesMax = 8_000_000
46
129
 
47
130
  /// The program the JSON describes, checked: every register, operand and instance in range.
48
131
  public init(json: String) throws {
@@ -64,8 +147,16 @@ public struct DSPProgram: Sendable, Equatable {
64
147
  public init(object: Any) throws {
65
148
  guard let root = object as? [String: Any] else { throw DSPProgramError("not an object") }
66
149
  guard root["format"] as? String == Self.format else { throw DSPProgramError("format is not \(Self.format)") }
67
- guard Self.int(root["version"]) == Self.version else { throw DSPProgramError("version is not \(Self.version)") }
150
+ guard Self.int(root["version"]) == Self.version else {
151
+ throw DSPProgramError("version \(root["version"].map { "\($0)" } ?? "none") is not \(Self.version)")
152
+ }
68
153
  guard let voices = Self.int(root["voices"]), (1...Self.lanes).contains(voices) else { throw DSPProgramError("voices must be 1 … \(Self.lanes)") }
154
+ var mode: Mode?
155
+ if let value = root["mode"] {
156
+ guard let name = value as? String, let parsed = Mode(rawValue: name) else { throw DSPProgramError("mode is not mono or legato") }
157
+ guard voices == 1 else { throw DSPProgramError("a mono or legato program has 1 voice") }
158
+ mode = parsed
159
+ }
69
160
  guard let registers = Self.int(root["registers"]), (Self.machineRegisters...65_536).contains(registers) else { throw DSPProgramError("registers out of range") }
70
161
  func register(_ value: Any?, _ what: String) throws -> Int {
71
162
  guard let r = Self.int(value), r >= 0, r < registers else { throw DSPProgramError("\(what): register out of range") }
@@ -101,40 +192,201 @@ public struct DSPProgram: Sendable, Equatable {
101
192
  guard let state = root["state"] as? [[String: Any]] else { throw DSPProgramError("state missing") }
102
193
  let parsedState = try state.map { s -> State in
103
194
  let name = s["name"] as? String ?? "state"
104
- return State(name: name, register: try register(s["register"], "state \(name)"), initial: try number(s["initial"], "state \(name)'s initial value"))
195
+ return State(
196
+ name: name, register: try register(s["register"], "state \(name)"), initial: try number(s["initial"], "state \(name)'s initial value"),
197
+ master: s["master"] as? Bool ?? false
198
+ )
105
199
  }
106
200
  let output = try register(root["output"], "output")
107
- func code(_ key: String) throws -> [Int32] {
201
+ let outputRight = try root["outputRight"].map { try register($0, "outputRight") }
202
+ var masterParts: (inputs: [Int], outputs: [Int])?
203
+ if root["master"] != nil {
204
+ guard let inputs = root["masterInputs"] as? [Any], inputs.count == 2 else { throw DSPProgramError("master needs masterInputs [left, right]") }
205
+ guard let outputs = root["masterOutput"] as? [Any], outputs.count == 2 else { throw DSPProgramError("master needs masterOutput [left, right]") }
206
+ masterParts = (try inputs.map { try register($0, "master") }, try outputs.map { try register($0, "master") })
207
+ }
208
+ var parsedShapes: [Shape] = []
209
+ if let shapes = root["shapes"] {
210
+ guard let list = shapes as? [[String: Any]] else { throw DSPProgramError("shapes must be an array") }
211
+ var points = 0
212
+ for shape in list {
213
+ guard let name = shape["name"] as? String, let values = shape["points"] as? [Any], (2...Self.shapePointsMax).contains(values.count) else {
214
+ throw DSPProgramError("a shape is { name, points }, 2 … \(Self.shapePointsMax) numbers")
215
+ }
216
+ let parsed = try values.map { value in
217
+ let point = try number(value, "shape \(name)'s point")
218
+ guard point.isFinite else { throw DSPProgramError("shape \(name): a point is not finite") }
219
+ return point
220
+ }
221
+ points += parsed.count
222
+ parsedShapes.append(Shape(name: name, points: parsed))
223
+ }
224
+ guard points <= Self.shapesPointsMax else { throw DSPProgramError("the shapes have more than \(Self.shapesPointsMax) points between them") }
225
+ }
226
+ var parsedSamples: [Sample] = []
227
+ if let samples = root["samples"] {
228
+ guard let list = samples as? [[String: Any]] else { throw DSPProgramError("samples must be an array") }
229
+ var total = 0
230
+ for sample in list {
231
+ guard let name = sample["name"] as? String, let text = sample["data"] as? String, let rate = (sample["rate"] as? NSNumber)?.doubleValue,
232
+ rate >= 1000, rate <= 384_000
233
+ else { throw DSPProgramError("a sample is { name, rate, data }") }
234
+ // Base64 of 32-bit little-endian floats; at least two.
235
+ guard let bytes = Data(base64Encoded: text), bytes.count % 4 == 0, bytes.count >= 8 else {
236
+ throw DSPProgramError("sample \(name)'s data is not base64 of at least 2 32-bit floats")
237
+ }
238
+ total += bytes.count / 4
239
+ guard total <= Self.samplesMax else { throw DSPProgramError("the samples have more than \(Self.samplesMax) samples between them") }
240
+ let data = bytes.withUnsafeBytes { raw in
241
+ (0..<(bytes.count / 4)).map { Float(bitPattern: UInt32(littleEndian: raw.loadUnaligned(fromByteOffset: $0 * 4, as: UInt32.self))) }
242
+ }
243
+ parsedSamples.append(Sample(name: name, rate: rate, data: data))
244
+ }
245
+ }
246
+ var parsedTables: [Table] = []
247
+ if let tables = root["tables"] {
248
+ guard let list = tables as? [[String: Any]] else { throw DSPProgramError("tables must be an array") }
249
+ var frameCount = 0
250
+ for table in list {
251
+ guard let name = table["name"] as? String, let frames = table["frames"] as? [[Any]], !frames.isEmpty else { throw DSPProgramError("a table is { name, frames }") }
252
+ let parsedFrames = try frames.map { frame -> [Float] in
253
+ guard !frame.isEmpty, frame.count <= Self.tableHarmonics else { throw DSPProgramError("table \(name): a frame is 1 … \(Self.tableHarmonics) harmonic amplitudes") }
254
+ return try frame.map { value in
255
+ let amplitude = try number(value, "table \(name)'s amplitude")
256
+ guard amplitude.isFinite else { throw DSPProgramError("table \(name): an amplitude is not finite") }
257
+ return amplitude
258
+ }
259
+ }
260
+ var parsedCosines: [[Float]]?
261
+ if let cosines = table["cosines"] {
262
+ guard let lists = cosines as? [[Any]], lists.count == parsedFrames.count else { throw DSPProgramError("table \(name): cosines are one list a frame") }
263
+ parsedCosines = try lists.enumerated().map { f, list in
264
+ guard list.count == parsedFrames[f].count else { throw DSPProgramError("table \(name): a frame's cosines are as many numbers as its sines") }
265
+ return try list.map { value in
266
+ let amplitude = try number(value, "table \(name)'s cosine")
267
+ guard amplitude.isFinite else { throw DSPProgramError("table \(name): a cosine is not finite") }
268
+ return amplitude
269
+ }
270
+ }
271
+ }
272
+ frameCount += parsedFrames.count
273
+ parsedTables.append(Table(name: name, frames: parsedFrames, cosines: parsedCosines))
274
+ }
275
+ guard frameCount <= Self.tableFramesMax else { throw DSPProgramError("the tables have more than \(Self.tableFramesMax) frames between them") }
276
+ }
277
+ var parsedRoutes: [[Int]] = []
278
+ if let routes = root["routes"] {
279
+ guard let lists = routes as? [[Any]] else { throw DSPProgramError("routes must be an array of lists") }
280
+ parsedRoutes = try lists.map { list in
281
+ guard list.count % 2 == 0 else { throw DSPProgramError("a route list is source and amount registers, pair after pair") }
282
+ let registers = try list.map { try register($0, "route") }
283
+ // An amount is a parameter, the same in every lane: the machine tests it once for all of them.
284
+ for k in stride(from: 1, to: registers.count, by: 2) where !parsedParameters.contains(where: { $0.register == registers[k] }) {
285
+ throw DSPProgramError("route amount register \(registers[k]) is not a parameter")
286
+ }
287
+ return registers
288
+ }
289
+ }
290
+ var parsedCurves: [Curve] = []
291
+ if let curves = root["curves"] {
292
+ guard let entries = curves as? [[Any]] else { throw DSPProgramError("curves must be an array") }
293
+ parsedCurves = try entries.map { entry in
294
+ guard entry.count == 4, let instance = Self.int(entry[0]), instances.indices.contains(instance), instances[instance] == .adsr else {
295
+ throw DSPProgramError("a curve is [instance, attack, decay, release] for an adsr")
296
+ }
297
+ let exponents = try entry.dropFirst().map { try number($0, "curve") }
298
+ guard exponents.allSatisfy({ Self.curveRange.contains($0) }) else { throw DSPProgramError("a curve's exponents are 1/16 … 16") }
299
+ return Curve(instance: instance, attack: exponents[0], decay: exponents[1], release: exponents[2])
300
+ }
301
+ }
302
+ let constantValues = Dictionary(parsedConstants.map { ($0.register, $0.value) }, uniquingKeysWith: { _, last in last })
303
+ let stateRegisters = Set(parsedState.map(\.register))
304
+ for r in [output, outputRight].compactMap({ $0 }) + (masterParts?.outputs ?? []) where stateRegisters.contains(r) {
305
+ throw DSPProgramError("output register \(r) is state")
306
+ }
307
+ func serial(_ key: String) throws -> Range<Int>? {
308
+ guard let value = root[key] else { return nil }
309
+ guard let pair = value as? [Any], pair.count == 2, let from = Self.int(pair[0]), let to = Self.int(pair[1]), from >= 0, from <= to else {
310
+ throw DSPProgramError("\(key) is not [from, to)")
311
+ }
312
+ return from..<to
313
+ }
314
+ func code(_ key: String, serial: Range<Int>?) throws -> [Int32] {
108
315
  guard let words = root[key] as? [Any] else { throw DSPProgramError("\(key) must be an array") }
109
316
  let code = try words.map { word -> Int32 in
110
317
  guard let n = Self.int(word), n >= Int(Int32.min), n <= Int(Int32.max) else { throw DSPProgramError("\(key): a word is not a whole number") }
111
318
  return Int32(n)
112
319
  }
320
+ if let serial, serial.upperBound > code.count { throw DSPProgramError("\(key)'s serial part is not [from, to) within it") }
321
+ var boundaries = 0
113
322
  var pc = 0
114
323
  while pc < code.count {
324
+ if let serial { boundaries += (pc == serial.lowerBound ? 1 : 0) + (pc == serial.upperBound ? 1 : 0) }
115
325
  guard let op = DSPOpcode(rawValue: code[pc]) else { throw DSPProgramError("\(key): unknown opcode \(code[pc]) at \(pc)") }
116
326
  let length = 2 + op.arity + (op.builtin == nil ? 0 : 1)
117
327
  guard pc + length <= code.count else { throw DSPProgramError("\(key): \(op) at \(pc) is cut off") }
118
328
  for i in 1..<(2 + op.arity) { _ = try register(Int(code[pc + i]), "\(key) \(op) at \(pc)") }
329
+ // State is a voice's value, not a frame's: only the part that runs frame by frame reads or writes it.
330
+ let inSerial = serial?.contains(pc) ?? false
331
+ if !inSerial && (1..<(2 + op.arity)).contains(where: { stateRegisters.contains(Int(code[pc + $0])) }) {
332
+ throw DSPProgramError("\(key): \(op) at \(pc) reads or writes state outside the serial part")
333
+ }
119
334
  if let builtin = op.builtin {
120
335
  let instance = Int(code[pc + length - 1])
121
336
  guard instances.indices.contains(instance), instances[instance] == builtin else {
122
337
  throw DSPProgramError("\(key): \(op) at \(pc) names instance \(instance), not a \(builtin)")
123
338
  }
124
339
  }
340
+ // What sizes memory comes from a constant register.
341
+ if op == .wavetable || op == .tableread || op == .shape || op == .play || op == .loop || op == .grains {
342
+ let (count, what) = op == .wavetable || op == .tableread ? (parsedTables.count, "a table") : op == .shape ? (parsedShapes.count, "a shape") : (parsedSamples.count, "a sample")
343
+ guard let index = constantValues[Int(code[pc + 2])], index.rounded() == index, index >= 0, index < Float(count) else {
344
+ throw DSPProgramError("\(key): \(op) at \(pc) does not name \(what)")
345
+ }
346
+ } else if op == .modsum {
347
+ guard let index = constantValues[Int(code[pc + 2])], index.rounded() == index, index >= 0, index < Float(parsedRoutes.count) else {
348
+ throw DSPProgramError("\(key): modsum at \(pc) does not name a route list")
349
+ }
350
+ // It sums into its destination route by route, so the destination is none of the registers it reads.
351
+ guard !parsedRoutes[Int(index)].contains(Int(code[pc + 1])) else {
352
+ throw DSPProgramError("\(key): modsum at \(pc) writes a register its routes read")
353
+ }
354
+ guard inSerial || !parsedRoutes[Int(index)].contains(where: { stateRegisters.contains($0) }) else {
355
+ throw DSPProgramError("\(key): modsum at \(pc) reads state outside the serial part")
356
+ }
357
+ } else if let longestAt = op.longest {
358
+ guard let longest = constantValues[Int(code[pc + 1 + longestAt])], longest > 0, longest <= Self.delaySecondsMax else {
359
+ throw DSPProgramError("\(key): \(op) at \(pc)'s longest time is not a constant up to \(Self.delaySecondsMax) s")
360
+ }
361
+ }
125
362
  pc += length
126
363
  }
364
+ if let serial {
365
+ boundaries += (pc == serial.lowerBound ? 1 : 0) + (pc == serial.upperBound ? 1 : 0)
366
+ guard boundaries == 2 else { throw DSPProgramError("\(key)'s serial part does not fall between instructions") }
367
+ }
127
368
  return code
128
369
  }
129
370
  self.voices = voices
371
+ self.mode = mode
130
372
  self.registers = registers
131
373
  self.constants = parsedConstants
132
374
  self.parameters = parsedParameters
133
375
  self.state = parsedState
134
376
  self.instances = instances
135
- self.block = try code("block")
136
- self.sample = try code("sample")
377
+ self.tables = parsedTables
378
+ self.shapes = parsedShapes
379
+ self.samples = parsedSamples
380
+ self.routes = parsedRoutes
381
+ self.curves = parsedCurves
382
+ self.block = try code("block", serial: nil)
383
+ let sampleSerial = try serial("serial")
384
+ self.serial = sampleSerial
385
+ self.sample = try code("sample", serial: sampleSerial)
137
386
  self.output = output
387
+ self.outputRight = outputRight
388
+ let masterSerial = try serial("masterSerial")
389
+ self.master = try masterParts.map { Master(code: try code("master", serial: masterSerial), serial: masterSerial, inputs: $0.inputs, outputs: $0.outputs) }
138
390
  }
139
391
 
140
392
  private static func int(_ value: Any?) -> Int? {
@@ -152,13 +404,26 @@ public struct DSPProgramError: Error, CustomStringConvertible, Equatable {
152
404
 
153
405
  /// The stateful builtins, by the name a program's `instances` gives them.
154
406
  public enum DSPBuiltin: String, Sendable, CaseIterable {
155
- case phasor, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass
407
+ case phasor, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass, wavetable, delay, play, grains, smooth, hold
408
+ case onepole, dcblock, comb, allpass, tableread, loop
156
409
 
157
- /// Words of state per lane (program.ts BUILTIN_STATE).
410
+ /// Words of state per lane (program.ts BUILTIN_STATE): a delay's, a comb's and an all-pass's samples are a
411
+ /// buffer of their own.
158
412
  var words: Int {
159
413
  switch self {
160
- case .phasor, .sine, .saw, .square, .triangle, .noise: 1
161
- case .adsr, .lowpass, .highpass, .bandpass: 8
414
+ case .phasor, .sine, .saw, .square, .triangle, .noise, .delay, .allpass: 1
415
+ case .hold, .dcblock, .comb: 2
416
+ // A table read's frequency last seen and the level it reads at that frequency.
417
+ case .tableread: 2
418
+ // A one-pole's place, then the cutoff last seen and its coefficient.
419
+ case .onepole: 3
420
+ // A wavetable's phase, then the frequency last seen and the level it reads at that frequency.
421
+ case .play, .loop, .smooth, .wavetable: 3
422
+ // An envelope's stage, level, trigger and gate before, its decay and release times and coefficients, then
423
+ // (with curves) what it put out last and where its release started.
424
+ case .adsr: 10
425
+ case .lowpass, .highpass, .bandpass: 8
426
+ case .grains: 2 + 4 * 8
162
427
  }
163
428
  }
164
429
  }
@@ -167,22 +432,52 @@ public enum DSPBuiltin: String, Sendable, CaseIterable {
167
432
  public enum DSPOpcode: Int32, Sendable, CaseIterable {
168
433
  case mov = 0, add, sub, mul, div, mod, neg, min, max, abs, floor, ceil, round, sqrt, sin, cos, tan, exp, log, pow, tanh
169
434
  case lt = 21, le, gt, ge, eq, ne, and, or, not, select, clamp, mix, wrap, sign, atan
170
- case phasor = 40, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass
435
+ case phasor = 40, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass, wavetable, delay, shape, play, grains, smooth, hold, modsum
436
+ /// The superinstructions the compiler fuses: a × b + c and (a + b) × c.
437
+ case madd = 58, amul
438
+ /// The rest of Math, and softclip.
439
+ case log2 = 60, log10, exp2, trunc, asin, acos, atan2, sinh, cosh, cbrt, hypot, expm1, log1p, softclip
440
+ case onepole = 74, dcblock, comb, allpass, tableread, loop
171
441
 
172
442
  /// Operands after the destination.
173
443
  public var arity: Int {
174
444
  switch self {
175
445
  case .noise: 0
446
+ case .modsum: 1
176
447
  case .mov, .neg, .abs, .floor, .ceil, .round, .sqrt, .sin, .cos, .tan, .exp, .log, .tanh, .not, .wrap, .sign, .atan,
177
- .phasor, .sine, .saw, .triangle: 1
178
- case .select, .clamp, .mix, .lowpass, .highpass, .bandpass: 3
179
- case .adsr: 5
448
+ .phasor, .sine, .saw, .triangle, .log2, .log10, .exp2, .trunc, .asin, .acos, .sinh, .cosh, .cbrt, .expm1, .log1p,
449
+ .softclip, .dcblock: 1
450
+ case .select, .clamp, .mix, .lowpass, .highpass, .bandpass, .wavetable, .delay, .play, .loop, .madd, .amul: 3
451
+ case .allpass, .tableread: 4
452
+ case .adsr, .grains, .comb: 5
180
453
  default: 2
181
454
  }
182
455
  }
183
456
 
184
- /// The builtin whose state it keeps, for a stateful one.
457
+ /// The builtin whose state it keeps, for a stateful one (`shape` keeps none).
185
458
  public var builtin: DSPBuiltin? {
186
- rawValue >= 40 ? DSPBuiltin.allCases[Int(rawValue) - 40] : nil
459
+ switch self {
460
+ case .play: .play
461
+ case .grains: .grains
462
+ case .smooth: .smooth
463
+ case .hold: .hold
464
+ case .onepole: .onepole
465
+ case .dcblock: .dcblock
466
+ case .comb: .comb
467
+ case .allpass: .allpass
468
+ case .tableread: .tableread
469
+ case .loop: .loop
470
+ default: rawValue >= 40 && rawValue <= 51 ? DSPBuiltin.allCases[Int(rawValue) - 40] : nil
471
+ }
472
+ }
473
+
474
+ /// A delay line's operand (after the destination) that names its longest time: a delay's, a comb's, an all-pass's.
475
+ public var longest: Int? {
476
+ switch self {
477
+ case .delay: 3
478
+ case .comb: 5
479
+ case .allpass: 4
480
+ default: nil
481
+ }
187
482
  }
188
483
  }
@@ -0,0 +1,31 @@
1
+ // swift-tools-version: 6.0
2
+ import PackageDescription
3
+
4
+ // SwiftBrowserShader: the native side of @swiftbrowser/shader. A shader
5
+ // written in the shader language and compiled by the package's TypeScript
6
+ // compiler draws here in an `MTKView`, its Metal compiled when a view first
7
+ // draws it; JavaScript gives it as a `ShaderView`. A package of its own, so
8
+ // only an app that draws with shaders links it; it installs itself as the
9
+ // `SwiftBrowserShader.view` registered view (`SwiftBrowserShader.install()`).
10
+ //
11
+ // SwiftBrowserShaderCore is the view's props alone (the shader's layout, its
12
+ // uniforms packed as the draw reads them, its textures), with no Apple
13
+ // framework, so it builds and tests on Linux too.
14
+ let package = Package(
15
+ name: "SwiftBrowserShader",
16
+ platforms: [.iOS(.v17), .macOS(.v14)],
17
+ products: [
18
+ .library(name: "SwiftBrowserShader", targets: ["SwiftBrowserShader"]),
19
+ .library(name: "SwiftBrowserShaderCore", targets: ["SwiftBrowserShaderCore"]),
20
+ ],
21
+ dependencies: [
22
+ .package(path: "../.."),
23
+ ],
24
+ targets: [
25
+ .target(name: "SwiftBrowserShaderCore", dependencies: [.product(name: "SwiftBrowserProtocol", package: "swift")]),
26
+ .target(
27
+ name: "SwiftBrowserShader",
28
+ dependencies: ["SwiftBrowserShaderCore", .product(name: "SwiftBrowserNative", package: "swift")]
29
+ ),
30
+ ]
31
+ )
@@ -0,0 +1,45 @@
1
+ # SwiftBrowserShader
2
+
3
+ The native side of [@swiftbrowser/shader](../../../packages/shader): a
4
+ shader written in the shader language, compiled by the package's TypeScript
5
+ compiler to the Metal Shading Language (beside GLSL and JavaScript for a
6
+ page), drawn by an `MTKView` over a `ShaderView`'s box. The JavaScript only
7
+ sends the shader, its uniforms and its textures; nothing per pixel runs
8
+ there.
9
+
10
+ It is a package of its own, as SwiftBrowserDSP is, so only an app that
11
+ draws with shaders links it. @swiftbrowser/native ships it beside the core
12
+ (`node_modules/@swiftbrowser/native/swift/Packages/SwiftBrowserShader`; it
13
+ depends on `SwiftBrowserNative` at `../..`, here and there). Link it into
14
+ the app's Xcode project with `npx swiftbrowser native add shader` (or
15
+ `native init --with shader`) and install it at launch:
16
+
17
+ ```swift
18
+ import SwiftBrowserShader
19
+
20
+ init() {
21
+ SwiftBrowserShader.install()
22
+ }
23
+ ```
24
+
25
+ That registers `ShaderView`'s view (`SwiftBrowserShader.view`) with
26
+ SwiftBrowserNative's `ViewRegistry`. Without it, a `ShaderView` shows its
27
+ name, as any registered view nothing registered does.
28
+
29
+ - **SwiftBrowserShaderCore** is the view's props alone: the shader's layout
30
+ (`ShaderLayout`, every uniform inside the floats a draw reads), its
31
+ textures, and the floats packed as the JavaScript packs them on a page
32
+ (`packedUniforms`). No Apple framework, so it builds and tests on Linux.
33
+ - **SwiftBrowserShader** is the view: `ShaderRenderer` compiles a shader's
34
+ Metal when a view first draws it (`makeLibrary(source:)`, kept for every
35
+ view that draws the same source), uploads its textures as `r32Float`, and
36
+ draws one triangle over the view with the uniforms in a fragment buffer.
37
+ The `MTKView` draws when its props or its size change, or every display
38
+ frame when the view is `animated`. A shader its device does not compile
39
+ draws nothing and sends the compiler's message to `onError`, once.
40
+
41
+ The tests (`swift test`) read @swiftbrowser/shader's render fixtures
42
+ (packages/shader/test/fixtures/render.json) and, on a Mac, compile each
43
+ case's Metal, draw it into a texture and hold its pixels to the ones the
44
+ package's JavaScript backend drew (printed as `[shader render]`), as the
45
+ page's tests hold WebGL2's.
@@ -0,0 +1,103 @@
1
+ #if canImport(Metal)
2
+ import Metal
3
+ @_exported import SwiftBrowserShaderCore
4
+
5
+ /// What draws a shader with Metal: its pipeline (the library compiled once
6
+ /// for a source, kept for every view that draws it), its textures, and a
7
+ /// draw into a render pass. `ShaderView`'s `MTKView` draws through it, and
8
+ /// the tests draw into a texture of their own.
9
+ @MainActor
10
+ public final class ShaderRenderer {
11
+ public let device: MTLDevice
12
+ public let pixelFormat: MTLPixelFormat
13
+ private var pipeline: MTLRenderPipelineState?
14
+ private var source = ""
15
+ private var textures: [MTLTexture] = []
16
+ private var uploaded: [ShaderTextureData?] = []
17
+ /// A texture of one zero for a slot the view was given nothing for: a draw reads every slot it declares.
18
+ private lazy var blank: MTLTexture? = Self.texture(device: device, width: 1, height: 1, data: [0])
19
+
20
+ /// Pipelines by source and pixel format, for every view: a library compiles in tens of milliseconds.
21
+ private static var pipelines: [String: MTLRenderPipelineState] = [:]
22
+
23
+ public init(device: MTLDevice, pixelFormat: MTLPixelFormat = .bgra8Unorm) {
24
+ self.device = device
25
+ self.pixelFormat = pixelFormat
26
+ }
27
+
28
+ public enum Failure: Error, CustomStringConvertible {
29
+ case compile(String)
30
+ case missing(String)
31
+
32
+ public var description: String {
33
+ switch self {
34
+ case .compile(let message): return message
35
+ case .missing(let name): return "the library has no function \(name)"
36
+ }
37
+ }
38
+ }
39
+
40
+ /// Makes ready to draw `props`: its pipeline compiled (or found) and its textures uploaded when they changed.
41
+ /// Throws the compiler's message when its Metal does not compile.
42
+ public func prepare(_ props: ShaderProps) throws {
43
+ guard let shader = props.shader else {
44
+ pipeline = nil
45
+ return
46
+ }
47
+ if shader.msl != source {
48
+ source = shader.msl
49
+ pipeline = nil
50
+ pipeline = try Self.pipeline(device: device, source: shader.msl, pixelFormat: pixelFormat)
51
+ }
52
+ if props.textures != uploaded || textures.count != shader.textures.count {
53
+ uploaded = props.textures
54
+ textures = shader.textures.indices.compactMap { slot in
55
+ let data = slot < props.textures.count ? props.textures[slot] : nil
56
+ return data.flatMap { Self.texture(device: device, width: $0.width, height: $0.height, data: $0.data) } ?? blank
57
+ }
58
+ }
59
+ }
60
+
61
+ /// Draws into `encoder`'s pass (which `prepare` made ready), the view `width` × `height` points at `scale` pixels
62
+ /// a point, `time` seconds in.
63
+ public func draw(_ props: ShaderProps, into encoder: MTLRenderCommandEncoder, width: Double, height: Double, scale: Double, time: Double) {
64
+ guard let pipeline, let shader = props.shader, textures.count == shader.textures.count else { return }
65
+ var floats = props.packedUniforms(width: width, height: height, scale: scale, time: time)
66
+ encoder.setRenderPipelineState(pipeline)
67
+ encoder.setFragmentBytes(&floats, length: MemoryLayout<Float>.stride * floats.count, index: 0)
68
+ for (slot, texture) in textures.enumerated() { encoder.setFragmentTexture(texture, index: slot) }
69
+ encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
70
+ }
71
+
72
+ static func pipeline(device: MTLDevice, source: String, pixelFormat: MTLPixelFormat) throws -> MTLRenderPipelineState {
73
+ let key = "\(pixelFormat.rawValue)\n\(source)"
74
+ if let found = pipelines[key] { return found }
75
+ let library: MTLLibrary
76
+ do {
77
+ library = try device.makeLibrary(source: source, options: nil)
78
+ } catch {
79
+ throw Failure.compile(String(describing: error))
80
+ }
81
+ guard let vertex = library.makeFunction(name: "sb_vertex") else { throw Failure.missing("sb_vertex") }
82
+ guard let fragment = library.makeFunction(name: "sb_fragment") else { throw Failure.missing("sb_fragment") }
83
+ let descriptor = MTLRenderPipelineDescriptor()
84
+ descriptor.vertexFunction = vertex
85
+ descriptor.fragmentFunction = fragment
86
+ descriptor.colorAttachments[0].pixelFormat = pixelFormat
87
+ let made = try device.makeRenderPipelineState(descriptor: descriptor)
88
+ pipelines[key] = made
89
+ return made
90
+ }
91
+
92
+ /// A texture of one float a texel, read with `read` (no filtering: the shader's `sample` interpolates).
93
+ static func texture(device: MTLDevice, width: Int, height: Int, data: [Float]) -> MTLTexture? {
94
+ let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r32Float, width: width, height: height, mipmapped: false)
95
+ descriptor.usage = .shaderRead
96
+ guard let texture = device.makeTexture(descriptor: descriptor), data.count >= width * height else { return nil }
97
+ data.withUnsafeBytes { bytes in
98
+ texture.replace(region: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0, withBytes: bytes.baseAddress!, bytesPerRow: width * MemoryLayout<Float>.stride)
99
+ }
100
+ return texture
101
+ }
102
+ }
103
+ #endif