@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.
- 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 +122 -19
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1247 -209
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +314 -19
- 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/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/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 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.
|
|
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`,
|
|
45
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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.
|
|
136
|
-
self.
|
|
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 .
|
|
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
|
|
178
|
-
|
|
179
|
-
case .
|
|
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
|
-
|
|
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
|