@swiftbrowser/native 0.19.0 → 0.23.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.
- package/README.md +34 -11
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +33 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/fetch.d.ts +19 -11
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +105 -41
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +3 -1
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +47 -6
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +13 -1
- package/dist/webSocket.js.map +1 -1
- package/package.json +3 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +27 -2
- package/src/fetch.ts +99 -41
- package/src/hostRequests.ts +62 -8
- package/src/index.ts +1 -2
- package/src/runtime.ts +1 -1
- package/src/webSocket.ts +10 -1
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +185 -24
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +2075 -213
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +407 -16
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +141 -7
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +47 -1
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +98 -29
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +11 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +28 -6
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +15 -10
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +31 -28
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +142 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +5 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserProtocol/TextChange.swift +17 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- 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 the parts that touch state, which it
|
|
12
|
+
/// schedules to run frame by frame when it loads them (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,146 @@ 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
|
+
public let inputs: [Int]
|
|
57
|
+
public let outputs: [Int]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// One voice playing the newest held note: `mono` starts its envelopes over at each note, `legato` only when
|
|
61
|
+
/// no other note was held.
|
|
62
|
+
public enum Mode: String, Sendable, Equatable {
|
|
63
|
+
case mono, legato
|
|
22
64
|
}
|
|
23
65
|
|
|
24
66
|
public let voices: Int
|
|
67
|
+
/// Absent, polyphonic.
|
|
68
|
+
public let mode: Mode?
|
|
69
|
+
/// The voicing as parameters (program.ts `voicing`, their indices), read at each note: how many voices play
|
|
70
|
+
/// (rounded, 1 … `voices`), and the mode (rounded: 0 as many, 1 mono, 2 legato) in place of `mode`.
|
|
71
|
+
public let voicingVoices: Int?
|
|
72
|
+
public let voicingMode: Int?
|
|
25
73
|
public let registers: Int
|
|
26
74
|
public let constants: [(register: Int, value: Float)]
|
|
27
75
|
public let parameters: [Parameter]
|
|
28
76
|
public let state: [State]
|
|
29
77
|
public let instances: [DSPBuiltin]
|
|
78
|
+
public let tables: [Table]
|
|
79
|
+
public let shapes: [Shape]
|
|
80
|
+
public let samples: [Sample]
|
|
81
|
+
/// `modsum`'s route lists: a source's register, then its amount's, pair after pair.
|
|
82
|
+
public let routes: [[Int]]
|
|
83
|
+
/// Envelopes with curves (program.ts `curves`): an `adsr` instance and its attack, decay and release exponents.
|
|
84
|
+
public let curves: [Curve]
|
|
30
85
|
public let block: [Int32]
|
|
31
86
|
public let sample: [Int32]
|
|
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?
|
|
91
|
+
/// Gated regions (program.ts `groups`): each one's parameter, the region it is inside (-1 none) and its results.
|
|
92
|
+
public let groups: [Gate]
|
|
93
|
+
/// Each instruction's region in `sample` and `master` (-1 none).
|
|
94
|
+
public let sampleGates: [Int32]?
|
|
95
|
+
public let masterGates: [Int32]?
|
|
96
|
+
/// Choices (program.ts `choices`): each a register read as one of its options, the one its parameter names.
|
|
97
|
+
public let choices: [Choice]
|
|
98
|
+
|
|
99
|
+
/// A choice: `register`, which no instruction writes, read as `options[round(parameter's set value × scale)]`
|
|
100
|
+
/// (clamped). What no choice picks, nor anything else needs, the machine does not run.
|
|
101
|
+
public struct Choice: Sendable, Equatable {
|
|
102
|
+
public let parameter: Int
|
|
103
|
+
public let register: Int
|
|
104
|
+
public let options: [Int]
|
|
105
|
+
/// What the parameter's value is multiplied by before it is rounded to an option.
|
|
106
|
+
public var scale: Double = 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The most options a choice has.
|
|
110
|
+
public static let choiceOptionsMax = 256
|
|
111
|
+
|
|
112
|
+
/// A region: it runs while `parameter` (its index) is set above 0.5 and the region around it runs.
|
|
113
|
+
public struct Gate: Sendable, Equatable {
|
|
114
|
+
public let parameter: Int
|
|
115
|
+
public let parent: Int
|
|
116
|
+
public let outputs: [Output]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// A region's result: a register no instruction writes, read as `on` while the region runs and `off` while not.
|
|
120
|
+
public struct Output: Sendable, Equatable {
|
|
121
|
+
public let register: Int
|
|
122
|
+
public let on: Int
|
|
123
|
+
public let off: Int
|
|
124
|
+
}
|
|
33
125
|
|
|
34
126
|
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)
|
|
127
|
+
a.voices == b.voices && a.mode == b.mode && a.registers == b.registers && a.constants.map(\.register) == b.constants.map(\.register)
|
|
36
128
|
&& a.constants.map(\.value) == b.constants.map(\.value) && a.parameters == b.parameters && a.state == b.state
|
|
37
|
-
&& a.instances == b.instances && a.
|
|
129
|
+
&& a.instances == b.instances && a.tables == b.tables && a.shapes == b.shapes && a.samples == b.samples && a.routes == b.routes && a.curves == b.curves
|
|
130
|
+
&& a.block == b.block && a.sample == b.sample && a.output == b.output && a.outputRight == b.outputRight && a.master == b.master
|
|
38
131
|
}
|
|
39
132
|
|
|
40
133
|
public static let format = "swiftbrowser-dsp"
|
|
134
|
+
/// The format's version: the machine runs only its own.
|
|
41
135
|
public static let version = 1
|
|
42
136
|
public static let lanes = 16
|
|
43
137
|
public static let blockFrames = 128
|
|
44
|
-
/// The machine's own registers: `sampleRate`,
|
|
45
|
-
|
|
138
|
+
/// The machine's own registers: `sampleRate`, a voice's `freq`, `gate`, `velocity` and `note`, then the
|
|
139
|
+
/// transport's `tempo` and `beat`.
|
|
140
|
+
public static let machineRegisters = 11
|
|
141
|
+
/// Samples a wavetable frame has, at each of its band-limited levels (program.ts `TABLE_FRAME`, `TABLE_LEVELS`).
|
|
142
|
+
public static let tableFrame = 2048
|
|
143
|
+
public static let tableLevels = 10
|
|
144
|
+
/// The most harmonics a frame gives, and frames a program's tables have between them.
|
|
145
|
+
public static let tableHarmonics = 512
|
|
146
|
+
public static let tableFramesMax = 256
|
|
147
|
+
/// The bounds of an envelope curve's exponents (program.ts `CURVE_MIN`, `CURVE_MAX`).
|
|
148
|
+
public static let curveRange: ClosedRange<Float> = 1.0 / 16 ... 16
|
|
149
|
+
|
|
150
|
+
/// An envelope's curves: the exponents its attack, decay and release are bent by (1 as without).
|
|
151
|
+
public struct Curve: Equatable, Sendable {
|
|
152
|
+
public let instance: Int
|
|
153
|
+
/// The registers its exponents are read from, each frame (held to 1/16 … 16).
|
|
154
|
+
public let attack: Int
|
|
155
|
+
public let decay: Int
|
|
156
|
+
public let release: Int
|
|
157
|
+
}
|
|
158
|
+
/// The longest a delay may be, in seconds.
|
|
159
|
+
public static let delaySecondsMax: Float = 2
|
|
160
|
+
/// Points a shape has, and its shapes between them; samples a program's samples have between them.
|
|
161
|
+
public static let shapePointsMax = 4096
|
|
162
|
+
public static let shapesPointsMax = 65_536
|
|
163
|
+
public static let samplesMax = 8_000_000
|
|
164
|
+
/// The most registers a route list has (128 routes): a machine keeps room for a list this long.
|
|
165
|
+
public static let routesMax = 256
|
|
46
166
|
|
|
47
167
|
/// The program the JSON describes, checked: every register, operand and instance in range.
|
|
48
168
|
public init(json: String) throws {
|
|
@@ -64,8 +184,16 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
64
184
|
public init(object: Any) throws {
|
|
65
185
|
guard let root = object as? [String: Any] else { throw DSPProgramError("not an object") }
|
|
66
186
|
guard root["format"] as? String == Self.format else { throw DSPProgramError("format is not \(Self.format)") }
|
|
67
|
-
guard Self.int(root["version"]) == Self.version else {
|
|
187
|
+
guard Self.int(root["version"]) == Self.version else {
|
|
188
|
+
throw DSPProgramError("version \(root["version"].map { "\($0)" } ?? "none") is not \(Self.version)")
|
|
189
|
+
}
|
|
68
190
|
guard let voices = Self.int(root["voices"]), (1...Self.lanes).contains(voices) else { throw DSPProgramError("voices must be 1 … \(Self.lanes)") }
|
|
191
|
+
var mode: Mode?
|
|
192
|
+
if let value = root["mode"] {
|
|
193
|
+
guard let name = value as? String, let parsed = Mode(rawValue: name) else { throw DSPProgramError("mode is not mono or legato") }
|
|
194
|
+
guard voices == 1 else { throw DSPProgramError("a mono or legato program has 1 voice") }
|
|
195
|
+
mode = parsed
|
|
196
|
+
}
|
|
69
197
|
guard let registers = Self.int(root["registers"]), (Self.machineRegisters...65_536).contains(registers) else { throw DSPProgramError("registers out of range") }
|
|
70
198
|
func register(_ value: Any?, _ what: String) throws -> Int {
|
|
71
199
|
guard let r = Self.int(value), r >= 0, r < registers else { throw DSPProgramError("\(what): register out of range") }
|
|
@@ -101,9 +229,120 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
101
229
|
guard let state = root["state"] as? [[String: Any]] else { throw DSPProgramError("state missing") }
|
|
102
230
|
let parsedState = try state.map { s -> State in
|
|
103
231
|
let name = s["name"] as? String ?? "state"
|
|
104
|
-
return State(
|
|
232
|
+
return State(
|
|
233
|
+
name: name, register: try register(s["register"], "state \(name)"), initial: try number(s["initial"], "state \(name)'s initial value"),
|
|
234
|
+
master: s["master"] as? Bool ?? false
|
|
235
|
+
)
|
|
105
236
|
}
|
|
106
237
|
let output = try register(root["output"], "output")
|
|
238
|
+
let outputRight = try root["outputRight"].map { try register($0, "outputRight") }
|
|
239
|
+
var masterParts: (inputs: [Int], outputs: [Int])?
|
|
240
|
+
if root["master"] != nil {
|
|
241
|
+
guard let inputs = root["masterInputs"] as? [Any], inputs.count == 2 else { throw DSPProgramError("master needs masterInputs [left, right]") }
|
|
242
|
+
guard let outputs = root["masterOutput"] as? [Any], outputs.count == 2 else { throw DSPProgramError("master needs masterOutput [left, right]") }
|
|
243
|
+
masterParts = (try inputs.map { try register($0, "master") }, try outputs.map { try register($0, "master") })
|
|
244
|
+
}
|
|
245
|
+
var parsedShapes: [Shape] = []
|
|
246
|
+
if let shapes = root["shapes"] {
|
|
247
|
+
guard let list = shapes as? [[String: Any]] else { throw DSPProgramError("shapes must be an array") }
|
|
248
|
+
var points = 0
|
|
249
|
+
for shape in list {
|
|
250
|
+
guard let name = shape["name"] as? String, let values = shape["points"] as? [Any], (2...Self.shapePointsMax).contains(values.count) else {
|
|
251
|
+
throw DSPProgramError("a shape is { name, points }, 2 … \(Self.shapePointsMax) numbers")
|
|
252
|
+
}
|
|
253
|
+
let parsed = try values.map { value in
|
|
254
|
+
let point = try number(value, "shape \(name)'s point")
|
|
255
|
+
guard point.isFinite else { throw DSPProgramError("shape \(name): a point is not finite") }
|
|
256
|
+
return point
|
|
257
|
+
}
|
|
258
|
+
points += parsed.count
|
|
259
|
+
parsedShapes.append(Shape(name: name, points: parsed))
|
|
260
|
+
}
|
|
261
|
+
guard points <= Self.shapesPointsMax else { throw DSPProgramError("the shapes have more than \(Self.shapesPointsMax) points between them") }
|
|
262
|
+
}
|
|
263
|
+
var parsedSamples: [Sample] = []
|
|
264
|
+
if let samples = root["samples"] {
|
|
265
|
+
guard let list = samples as? [[String: Any]] else { throw DSPProgramError("samples must be an array") }
|
|
266
|
+
var total = 0
|
|
267
|
+
for sample in list {
|
|
268
|
+
guard let name = sample["name"] as? String, let text = sample["data"] as? String, let rate = (sample["rate"] as? NSNumber)?.doubleValue,
|
|
269
|
+
rate >= 1000, rate <= 384_000
|
|
270
|
+
else { throw DSPProgramError("a sample is { name, rate, data }") }
|
|
271
|
+
// Base64 of 32-bit little-endian floats; at least two.
|
|
272
|
+
guard let bytes = Data(base64Encoded: text), bytes.count % 4 == 0, bytes.count >= 8 else {
|
|
273
|
+
throw DSPProgramError("sample \(name)'s data is not base64 of at least 2 32-bit floats")
|
|
274
|
+
}
|
|
275
|
+
total += bytes.count / 4
|
|
276
|
+
guard total <= Self.samplesMax else { throw DSPProgramError("the samples have more than \(Self.samplesMax) samples between them") }
|
|
277
|
+
let data = bytes.withUnsafeBytes { raw in
|
|
278
|
+
(0..<(bytes.count / 4)).map { Float(bitPattern: UInt32(littleEndian: raw.loadUnaligned(fromByteOffset: $0 * 4, as: UInt32.self))) }
|
|
279
|
+
}
|
|
280
|
+
parsedSamples.append(Sample(name: name, rate: rate, data: data))
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
var parsedTables: [Table] = []
|
|
284
|
+
if let tables = root["tables"] {
|
|
285
|
+
guard let list = tables as? [[String: Any]] else { throw DSPProgramError("tables must be an array") }
|
|
286
|
+
var frameCount = 0
|
|
287
|
+
for table in list {
|
|
288
|
+
guard let name = table["name"] as? String, let frames = table["frames"] as? [[Any]], !frames.isEmpty else { throw DSPProgramError("a table is { name, frames }") }
|
|
289
|
+
let parsedFrames = try frames.map { frame -> [Float] in
|
|
290
|
+
guard !frame.isEmpty, frame.count <= Self.tableHarmonics else { throw DSPProgramError("table \(name): a frame is 1 … \(Self.tableHarmonics) harmonic amplitudes") }
|
|
291
|
+
return try frame.map { value in
|
|
292
|
+
let amplitude = try number(value, "table \(name)'s amplitude")
|
|
293
|
+
guard amplitude.isFinite else { throw DSPProgramError("table \(name): an amplitude is not finite") }
|
|
294
|
+
return amplitude
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
var parsedCosines: [[Float]]?
|
|
298
|
+
if let cosines = table["cosines"] {
|
|
299
|
+
guard let lists = cosines as? [[Any]], lists.count == parsedFrames.count else { throw DSPProgramError("table \(name): cosines are one list a frame") }
|
|
300
|
+
parsedCosines = try lists.enumerated().map { f, list in
|
|
301
|
+
guard list.count == parsedFrames[f].count else { throw DSPProgramError("table \(name): a frame's cosines are as many numbers as its sines") }
|
|
302
|
+
return try list.map { value in
|
|
303
|
+
let amplitude = try number(value, "table \(name)'s cosine")
|
|
304
|
+
guard amplitude.isFinite else { throw DSPProgramError("table \(name): a cosine is not finite") }
|
|
305
|
+
return amplitude
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
frameCount += parsedFrames.count
|
|
310
|
+
parsedTables.append(Table(name: name, frames: parsedFrames, cosines: parsedCosines))
|
|
311
|
+
}
|
|
312
|
+
guard frameCount <= Self.tableFramesMax else { throw DSPProgramError("the tables have more than \(Self.tableFramesMax) frames between them") }
|
|
313
|
+
}
|
|
314
|
+
var parsedRoutes: [[Int]] = []
|
|
315
|
+
if let routes = root["routes"] {
|
|
316
|
+
guard let lists = routes as? [[Any]] else { throw DSPProgramError("routes must be an array of lists") }
|
|
317
|
+
parsedRoutes = try lists.map { list in
|
|
318
|
+
guard list.count % 2 == 0 else { throw DSPProgramError("a route list is source and amount registers, pair after pair") }
|
|
319
|
+
guard list.count <= Self.routesMax else { throw DSPProgramError("a route list has more than \(Self.routesMax) registers") }
|
|
320
|
+
let registers = try list.map { try register($0, "route") }
|
|
321
|
+
// An amount is a parameter, the same in every lane: the machine tests it once for all of them.
|
|
322
|
+
for k in stride(from: 1, to: registers.count, by: 2) where !parsedParameters.contains(where: { $0.register == registers[k] }) {
|
|
323
|
+
throw DSPProgramError("route amount register \(registers[k]) is not a parameter")
|
|
324
|
+
}
|
|
325
|
+
return registers
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
var parsedCurves: [Curve] = []
|
|
329
|
+
if let curves = root["curves"] {
|
|
330
|
+
guard let entries = curves as? [[Any]] else { throw DSPProgramError("curves must be an array") }
|
|
331
|
+
parsedCurves = try entries.map { entry in
|
|
332
|
+
guard entry.count == 4, let instance = Self.int(entry[0]), instances.indices.contains(instance), instances[instance] == .adsr else {
|
|
333
|
+
throw DSPProgramError("a curve is [instance, attack, decay, release] for an adsr")
|
|
334
|
+
}
|
|
335
|
+
let exponents = try entry.dropFirst().map { try register($0, "curve") }
|
|
336
|
+
return Curve(instance: instance, attack: exponents[0], decay: exponents[1], release: exponents[2])
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
let constantValues = Dictionary(parsedConstants.map { ($0.register, $0.value) }, uniquingKeysWith: { _, last in last })
|
|
340
|
+
let stateRegisters = Set(parsedState.map(\.register))
|
|
341
|
+
for r in [output, outputRight].compactMap({ $0 }) + (masterParts?.outputs ?? []) where stateRegisters.contains(r) {
|
|
342
|
+
throw DSPProgramError("output register \(r) is state")
|
|
343
|
+
}
|
|
344
|
+
/// Every register an instruction writes.
|
|
345
|
+
var written = Set<Int>()
|
|
107
346
|
func code(_ key: String) throws -> [Int32] {
|
|
108
347
|
guard let words = root[key] as? [Any] else { throw DSPProgramError("\(key) must be an array") }
|
|
109
348
|
let code = try words.map { word -> Int32 in
|
|
@@ -116,25 +355,134 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
116
355
|
let length = 2 + op.arity + (op.builtin == nil ? 0 : 1)
|
|
117
356
|
guard pc + length <= code.count else { throw DSPProgramError("\(key): \(op) at \(pc) is cut off") }
|
|
118
357
|
for i in 1..<(2 + op.arity) { _ = try register(Int(code[pc + i]), "\(key) \(op) at \(pc)") }
|
|
358
|
+
written.insert(Int(code[pc + 1]))
|
|
119
359
|
if let builtin = op.builtin {
|
|
120
360
|
let instance = Int(code[pc + length - 1])
|
|
121
361
|
guard instances.indices.contains(instance), instances[instance] == builtin else {
|
|
122
362
|
throw DSPProgramError("\(key): \(op) at \(pc) names instance \(instance), not a \(builtin)")
|
|
123
363
|
}
|
|
124
364
|
}
|
|
365
|
+
// What sizes memory comes from a constant register.
|
|
366
|
+
if op == .wavetable || op == .tableread || op == .shape || op == .play || op == .loop || op == .grains {
|
|
367
|
+
let (count, what) = op == .wavetable || op == .tableread ? (parsedTables.count, "a table") : op == .shape ? (parsedShapes.count, "a shape") : (parsedSamples.count, "a sample")
|
|
368
|
+
guard let index = constantValues[Int(code[pc + 2])], index.rounded() == index, index >= 0, index < Float(count) else {
|
|
369
|
+
throw DSPProgramError("\(key): \(op) at \(pc) does not name \(what)")
|
|
370
|
+
}
|
|
371
|
+
} else if op == .modsum {
|
|
372
|
+
guard let index = constantValues[Int(code[pc + 2])], index.rounded() == index, index >= 0, index < Float(parsedRoutes.count) else {
|
|
373
|
+
throw DSPProgramError("\(key): modsum at \(pc) does not name a route list")
|
|
374
|
+
}
|
|
375
|
+
// It sums into its destination route by route, so the destination is none of the registers it reads.
|
|
376
|
+
guard !parsedRoutes[Int(index)].contains(Int(code[pc + 1])) else {
|
|
377
|
+
throw DSPProgramError("\(key): modsum at \(pc) writes a register its routes read")
|
|
378
|
+
}
|
|
379
|
+
} else if let longestAt = op.longest {
|
|
380
|
+
guard let longest = constantValues[Int(code[pc + 1 + longestAt])], longest > 0, longest <= Self.delaySecondsMax else {
|
|
381
|
+
throw DSPProgramError("\(key): \(op) at \(pc)'s longest time is not a constant up to \(Self.delaySecondsMax) s")
|
|
382
|
+
}
|
|
383
|
+
}
|
|
125
384
|
pc += length
|
|
126
385
|
}
|
|
127
386
|
return code
|
|
128
387
|
}
|
|
129
388
|
self.voices = voices
|
|
389
|
+
self.mode = mode
|
|
130
390
|
self.registers = registers
|
|
131
391
|
self.constants = parsedConstants
|
|
132
392
|
self.parameters = parsedParameters
|
|
133
393
|
self.state = parsedState
|
|
134
394
|
self.instances = instances
|
|
395
|
+
self.tables = parsedTables
|
|
396
|
+
self.shapes = parsedShapes
|
|
397
|
+
self.samples = parsedSamples
|
|
398
|
+
self.routes = parsedRoutes
|
|
399
|
+
self.curves = parsedCurves
|
|
135
400
|
self.block = try code("block")
|
|
136
401
|
self.sample = try code("sample")
|
|
137
402
|
self.output = output
|
|
403
|
+
self.outputRight = outputRight
|
|
404
|
+
self.master = try masterParts.map { Master(code: try code("master"), inputs: $0.inputs, outputs: $0.outputs) }
|
|
405
|
+
// Gated regions: each one's parameter one of the program's, the region around it an earlier one, and its
|
|
406
|
+
// results registers no instruction writes and nothing else names (no output, state, parameter, constant, route
|
|
407
|
+
// or another result), each read as a register while on and one while off, neither of them state (one value a
|
|
408
|
+
// voice, not one a frame).
|
|
409
|
+
var named = Set([output, outputRight].compactMap { $0 } + (masterParts?.inputs ?? []) + (masterParts?.outputs ?? []) + parsedRoutes.flatMap { $0 })
|
|
410
|
+
named.formUnion(stateRegisters)
|
|
411
|
+
named.formUnion(parsedParameters.map(\.register))
|
|
412
|
+
named.formUnion(parsedConstants.map(\.register))
|
|
413
|
+
named.formUnion(parsedCurves.flatMap { [$0.attack, $0.decay, $0.release] })
|
|
414
|
+
var results = Set<Int>()
|
|
415
|
+
var parsedGroups: [Gate] = []
|
|
416
|
+
for (index, item) in (root["groups"] as? [Any] ?? []).enumerated() {
|
|
417
|
+
guard let group = item as? [String: Any], let parameter = Self.int(group["parameter"]), parsedParameters.indices.contains(parameter) else {
|
|
418
|
+
throw DSPProgramError("groups[\(index)] does not name a parameter")
|
|
419
|
+
}
|
|
420
|
+
guard let parent = Self.int(group["parent"]), parent >= -1, parent < index else { throw DSPProgramError("groups[\(index)]'s parent is not an earlier region") }
|
|
421
|
+
guard let list = group["outputs"] as? [Any] else { throw DSPProgramError("groups[\(index)]'s outputs must be an array") }
|
|
422
|
+
let outputs = try list.map { entry -> Output in
|
|
423
|
+
guard let triple = entry as? [Any], triple.count == 3 else { throw DSPProgramError("groups[\(index)]: a result is [register, on, off]") }
|
|
424
|
+
let (r, on, off) = (try register(triple[0], "groups[\(index)]"), try register(triple[1], "groups[\(index)]"), try register(triple[2], "groups[\(index)]"))
|
|
425
|
+
guard r >= Self.machineRegisters, !written.contains(r), !named.contains(r), !results.contains(r) else {
|
|
426
|
+
throw DSPProgramError("groups[\(index)]: result register \(r) is written or named elsewhere")
|
|
427
|
+
}
|
|
428
|
+
guard !stateRegisters.contains(on), !stateRegisters.contains(off) else { throw DSPProgramError("groups[\(index)]: result register \(r) reads as state") }
|
|
429
|
+
results.insert(r)
|
|
430
|
+
return Output(register: r, on: on, off: off)
|
|
431
|
+
}
|
|
432
|
+
parsedGroups.append(Gate(parameter: parameter, parent: parent, outputs: outputs))
|
|
433
|
+
}
|
|
434
|
+
self.groups = parsedGroups
|
|
435
|
+
// Choices: each one's parameter one of the program's, its register one nothing else writes or names (nor a
|
|
436
|
+
// region's result, nor another choice), its options 1 … choiceOptionsMax registers, none of them state.
|
|
437
|
+
let voicing = root["voicing"] as? [String: Any] ?? [:]
|
|
438
|
+
for key in voicing.keys {
|
|
439
|
+
guard ["voices", "mode"].contains(key), let index = Self.int(voicing[key]), parsedParameters.indices.contains(index) else {
|
|
440
|
+
throw DSPProgramError("voicing.\(key) does not name a parameter")
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
self.voicingVoices = Self.int(voicing["voices"])
|
|
444
|
+
self.voicingMode = Self.int(voicing["mode"])
|
|
445
|
+
self.choices = try (root["choices"] as? [Any] ?? []).enumerated().map { index, item in
|
|
446
|
+
guard let choice = item as? [String: Any], let parameter = Self.int(choice["parameter"]), parsedParameters.indices.contains(parameter) else {
|
|
447
|
+
throw DSPProgramError("choices[\(index)] does not name a parameter")
|
|
448
|
+
}
|
|
449
|
+
var scale = 1.0
|
|
450
|
+
if let value = choice["scale"] {
|
|
451
|
+
guard let number = value as? NSNumber, !(value is Bool), number.doubleValue.isFinite, number.doubleValue > 0 else {
|
|
452
|
+
throw DSPProgramError("choices[\(index)]'s scale is not a number above 0")
|
|
453
|
+
}
|
|
454
|
+
scale = number.doubleValue
|
|
455
|
+
}
|
|
456
|
+
let r = try register(choice["register"], "choices[\(index)]")
|
|
457
|
+
guard r >= Self.machineRegisters, !written.contains(r), !named.contains(r), !results.contains(r) else {
|
|
458
|
+
throw DSPProgramError("choices[\(index)]: register \(r) is written or named elsewhere")
|
|
459
|
+
}
|
|
460
|
+
results.insert(r)
|
|
461
|
+
guard let list = choice["options"] as? [Any], (1...Self.choiceOptionsMax).contains(list.count) else {
|
|
462
|
+
throw DSPProgramError("choices[\(index)] has 1 … \(Self.choiceOptionsMax) options")
|
|
463
|
+
}
|
|
464
|
+
let options = try list.map { try register($0, "choices[\(index)]") }
|
|
465
|
+
guard !options.contains(where: stateRegisters.contains) else { throw DSPProgramError("choices[\(index)]: an option is state") }
|
|
466
|
+
return Choice(parameter: parameter, register: r, options: options, scale: scale)
|
|
467
|
+
}
|
|
468
|
+
/// One region (or -1) per instruction of `code`.
|
|
469
|
+
func regions(_ key: String, _ code: [Int32]?) throws -> [Int32]? {
|
|
470
|
+
guard let value = root[key] else { return nil }
|
|
471
|
+
guard let list = value as? [Any], let code else { throw DSPProgramError("\(key) must be an array") }
|
|
472
|
+
var count = 0
|
|
473
|
+
var pc = 0
|
|
474
|
+
while pc < code.count, let op = DSPOpcode(rawValue: code[pc]) {
|
|
475
|
+
count += 1
|
|
476
|
+
pc += 2 + op.arity + (op.builtin == nil ? 0 : 1)
|
|
477
|
+
}
|
|
478
|
+
guard list.count == count else { throw DSPProgramError("\(key) has \(list.count) entries for \(count) instructions") }
|
|
479
|
+
return try list.map { item in
|
|
480
|
+
guard let g = Self.int(item), g >= -1, g < parsedGroups.count else { throw DSPProgramError("\(key): a region is out of range") }
|
|
481
|
+
return Int32(g)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
self.sampleGates = try regions("sampleGates", sample)
|
|
485
|
+
self.masterGates = try regions("masterGates", master?.code)
|
|
138
486
|
}
|
|
139
487
|
|
|
140
488
|
private static func int(_ value: Any?) -> Int? {
|
|
@@ -152,13 +500,26 @@ public struct DSPProgramError: Error, CustomStringConvertible, Equatable {
|
|
|
152
500
|
|
|
153
501
|
/// The stateful builtins, by the name a program's `instances` gives them.
|
|
154
502
|
public enum DSPBuiltin: String, Sendable, CaseIterable {
|
|
155
|
-
case phasor, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass
|
|
503
|
+
case phasor, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass, wavetable, delay, play, grains, smooth, hold
|
|
504
|
+
case onepole, dcblock, comb, allpass, tableread, loop
|
|
156
505
|
|
|
157
|
-
/// Words of state per lane (program.ts BUILTIN_STATE)
|
|
506
|
+
/// Words of state per lane (program.ts BUILTIN_STATE): a delay's, a comb's and an all-pass's samples are a
|
|
507
|
+
/// buffer of their own.
|
|
158
508
|
var words: Int {
|
|
159
509
|
switch self {
|
|
160
|
-
case .phasor, .sine, .saw, .square, .triangle, .noise: 1
|
|
161
|
-
case .
|
|
510
|
+
case .phasor, .sine, .saw, .square, .triangle, .noise, .delay, .allpass: 1
|
|
511
|
+
case .hold, .dcblock, .comb: 2
|
|
512
|
+
// A table read's frequency last seen and the level it reads at that frequency.
|
|
513
|
+
case .tableread: 2
|
|
514
|
+
// A one-pole's place, then the cutoff last seen and its coefficient.
|
|
515
|
+
case .onepole: 3
|
|
516
|
+
// A wavetable's phase, then the frequency last seen and the level it reads at that frequency.
|
|
517
|
+
case .play, .loop, .smooth, .wavetable: 3
|
|
518
|
+
// An envelope's stage, level, trigger and gate before, its decay and release times and coefficients, then
|
|
519
|
+
// (with curves) what it put out last and where its release started.
|
|
520
|
+
case .adsr: 10
|
|
521
|
+
case .lowpass, .highpass, .bandpass: 8
|
|
522
|
+
case .grains: 2 + 4 * 8
|
|
162
523
|
}
|
|
163
524
|
}
|
|
164
525
|
}
|
|
@@ -167,22 +528,52 @@ public enum DSPBuiltin: String, Sendable, CaseIterable {
|
|
|
167
528
|
public enum DSPOpcode: Int32, Sendable, CaseIterable {
|
|
168
529
|
case mov = 0, add, sub, mul, div, mod, neg, min, max, abs, floor, ceil, round, sqrt, sin, cos, tan, exp, log, pow, tanh
|
|
169
530
|
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
|
|
531
|
+
case phasor = 40, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass, wavetable, delay, shape, play, grains, smooth, hold, modsum
|
|
532
|
+
/// The superinstructions the compiler fuses: a × b + c and (a + b) × c.
|
|
533
|
+
case madd = 58, amul
|
|
534
|
+
/// The rest of Math, and softclip.
|
|
535
|
+
case log2 = 60, log10, exp2, trunc, asin, acos, atan2, sinh, cosh, cbrt, hypot, expm1, log1p, softclip
|
|
536
|
+
case onepole = 74, dcblock, comb, allpass, tableread, loop
|
|
171
537
|
|
|
172
538
|
/// Operands after the destination.
|
|
173
539
|
public var arity: Int {
|
|
174
540
|
switch self {
|
|
175
541
|
case .noise: 0
|
|
542
|
+
case .modsum: 1
|
|
176
543
|
case .mov, .neg, .abs, .floor, .ceil, .round, .sqrt, .sin, .cos, .tan, .exp, .log, .tanh, .not, .wrap, .sign, .atan,
|
|
177
|
-
.phasor, .sine, .saw, .triangle
|
|
178
|
-
|
|
179
|
-
case .
|
|
544
|
+
.phasor, .sine, .saw, .triangle, .log2, .log10, .exp2, .trunc, .asin, .acos, .sinh, .cosh, .cbrt, .expm1, .log1p,
|
|
545
|
+
.softclip, .dcblock: 1
|
|
546
|
+
case .select, .clamp, .mix, .lowpass, .highpass, .bandpass, .wavetable, .delay, .play, .loop, .madd, .amul: 3
|
|
547
|
+
case .allpass, .tableread: 4
|
|
548
|
+
case .adsr, .grains, .comb: 5
|
|
180
549
|
default: 2
|
|
181
550
|
}
|
|
182
551
|
}
|
|
183
552
|
|
|
184
|
-
/// The builtin whose state it keeps, for a stateful one.
|
|
553
|
+
/// The builtin whose state it keeps, for a stateful one (`shape` keeps none).
|
|
185
554
|
public var builtin: DSPBuiltin? {
|
|
186
|
-
|
|
555
|
+
switch self {
|
|
556
|
+
case .play: .play
|
|
557
|
+
case .grains: .grains
|
|
558
|
+
case .smooth: .smooth
|
|
559
|
+
case .hold: .hold
|
|
560
|
+
case .onepole: .onepole
|
|
561
|
+
case .dcblock: .dcblock
|
|
562
|
+
case .comb: .comb
|
|
563
|
+
case .allpass: .allpass
|
|
564
|
+
case .tableread: .tableread
|
|
565
|
+
case .loop: .loop
|
|
566
|
+
default: rawValue >= 40 && rawValue <= 51 ? DSPBuiltin.allCases[Int(rawValue) - 40] : nil
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// A delay line's operand (after the destination) that names its longest time: a delay's, a comb's, an all-pass's.
|
|
571
|
+
public var longest: Int? {
|
|
572
|
+
switch self {
|
|
573
|
+
case .delay: 3
|
|
574
|
+
case .comb: 5
|
|
575
|
+
case .allpass: 4
|
|
576
|
+
default: nil
|
|
577
|
+
}
|
|
187
578
|
}
|
|
188
579
|
}
|
|
@@ -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.
|