@swiftbrowser/native 0.21.0 → 0.25.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.
@@ -8,8 +8,8 @@ import Foundation
8
8
  /// register, the opcode's operands and, for a stateful builtin, its instance.
9
9
  /// A voice may have a right side (`outputRight`), and the `master` stage
10
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).
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).
13
13
  public struct DSPProgram: Sendable, Equatable {
14
14
  public struct Parameter: Sendable, Equatable {
15
15
  public let name: String
@@ -53,8 +53,6 @@ public struct DSPProgram: Sendable, Equatable {
53
53
  /// over at every note.
54
54
  public struct Master: Sendable, Equatable {
55
55
  public let code: [Int32]
56
- /// The part of `code` that runs frame by frame, in words.
57
- public let serial: Range<Int>?
58
56
  public let inputs: [Int]
59
57
  public let outputs: [Int]
60
58
  }
@@ -68,6 +66,10 @@ public struct DSPProgram: Sendable, Equatable {
68
66
  public let voices: Int
69
67
  /// Absent, polyphonic.
70
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?
71
73
  public let registers: Int
72
74
  public let constants: [(register: Int, value: Float)]
73
75
  public let parameters: [Parameter]
@@ -82,18 +84,50 @@ public struct DSPProgram: Sendable, Equatable {
82
84
  public let curves: [Curve]
83
85
  public let block: [Int32]
84
86
  public let sample: [Int32]
85
- /// The part of `sample` that runs frame by frame, in words.
86
- public let serial: Range<Int>?
87
87
  /// The voice's left side, or both with no `outputRight`.
88
88
  public let output: Int
89
89
  public let outputRight: Int?
90
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
+ }
91
125
 
92
126
  public static func == (a: DSPProgram, b: DSPProgram) -> Bool {
93
127
  a.voices == b.voices && a.mode == b.mode && a.registers == b.registers && a.constants.map(\.register) == b.constants.map(\.register)
94
128
  && a.constants.map(\.value) == b.constants.map(\.value) && a.parameters == b.parameters && a.state == b.state
95
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
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
130
+ && a.block == b.block && a.sample == b.sample && a.output == b.output && a.outputRight == b.outputRight && a.master == b.master
97
131
  }
98
132
 
99
133
  public static let format = "swiftbrowser-dsp"
@@ -116,9 +150,10 @@ public struct DSPProgram: Sendable, Equatable {
116
150
  /// An envelope's curves: the exponents its attack, decay and release are bent by (1 as without).
117
151
  public struct Curve: Equatable, Sendable {
118
152
  public let instance: Int
119
- public let attack: Float
120
- public let decay: Float
121
- public let release: Float
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
122
157
  }
123
158
  /// The longest a delay may be, in seconds.
124
159
  public static let delaySecondsMax: Float = 2
@@ -126,6 +161,8 @@ public struct DSPProgram: Sendable, Equatable {
126
161
  public static let shapePointsMax = 4096
127
162
  public static let shapesPointsMax = 65_536
128
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
129
166
 
130
167
  /// The program the JSON describes, checked: every register, operand and instance in range.
131
168
  public init(json: String) throws {
@@ -279,6 +316,7 @@ public struct DSPProgram: Sendable, Equatable {
279
316
  guard let lists = routes as? [[Any]] else { throw DSPProgramError("routes must be an array of lists") }
280
317
  parsedRoutes = try lists.map { list in
281
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") }
282
320
  let registers = try list.map { try register($0, "route") }
283
321
  // An amount is a parameter, the same in every lane: the machine tests it once for all of them.
284
322
  for k in stride(from: 1, to: registers.count, by: 2) where !parsedParameters.contains(where: { $0.register == registers[k] }) {
@@ -294,8 +332,7 @@ public struct DSPProgram: Sendable, Equatable {
294
332
  guard entry.count == 4, let instance = Self.int(entry[0]), instances.indices.contains(instance), instances[instance] == .adsr else {
295
333
  throw DSPProgramError("a curve is [instance, attack, decay, release] for an adsr")
296
334
  }
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") }
335
+ let exponents = try entry.dropFirst().map { try register($0, "curve") }
299
336
  return Curve(instance: instance, attack: exponents[0], decay: exponents[1], release: exponents[2])
300
337
  }
301
338
  }
@@ -304,33 +341,21 @@ public struct DSPProgram: Sendable, Equatable {
304
341
  for r in [output, outputRight].compactMap({ $0 }) + (masterParts?.outputs ?? []) where stateRegisters.contains(r) {
305
342
  throw DSPProgramError("output register \(r) is state")
306
343
  }
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] {
344
+ /// Every register an instruction writes.
345
+ var written = Set<Int>()
346
+ func code(_ key: String) throws -> [Int32] {
315
347
  guard let words = root[key] as? [Any] else { throw DSPProgramError("\(key) must be an array") }
316
348
  let code = try words.map { word -> Int32 in
317
349
  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") }
318
350
  return Int32(n)
319
351
  }
320
- if let serial, serial.upperBound > code.count { throw DSPProgramError("\(key)'s serial part is not [from, to) within it") }
321
- var boundaries = 0
322
352
  var pc = 0
323
353
  while pc < code.count {
324
- if let serial { boundaries += (pc == serial.lowerBound ? 1 : 0) + (pc == serial.upperBound ? 1 : 0) }
325
354
  guard let op = DSPOpcode(rawValue: code[pc]) else { throw DSPProgramError("\(key): unknown opcode \(code[pc]) at \(pc)") }
326
355
  let length = 2 + op.arity + (op.builtin == nil ? 0 : 1)
327
356
  guard pc + length <= code.count else { throw DSPProgramError("\(key): \(op) at \(pc) is cut off") }
328
357
  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
- }
358
+ written.insert(Int(code[pc + 1]))
334
359
  if let builtin = op.builtin {
335
360
  let instance = Int(code[pc + length - 1])
336
361
  guard instances.indices.contains(instance), instances[instance] == builtin else {
@@ -351,9 +376,6 @@ public struct DSPProgram: Sendable, Equatable {
351
376
  guard !parsedRoutes[Int(index)].contains(Int(code[pc + 1])) else {
352
377
  throw DSPProgramError("\(key): modsum at \(pc) writes a register its routes read")
353
378
  }
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
379
  } else if let longestAt = op.longest {
358
380
  guard let longest = constantValues[Int(code[pc + 1 + longestAt])], longest > 0, longest <= Self.delaySecondsMax else {
359
381
  throw DSPProgramError("\(key): \(op) at \(pc)'s longest time is not a constant up to \(Self.delaySecondsMax) s")
@@ -361,10 +383,6 @@ public struct DSPProgram: Sendable, Equatable {
361
383
  }
362
384
  pc += length
363
385
  }
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
- }
368
386
  return code
369
387
  }
370
388
  self.voices = voices
@@ -379,14 +397,92 @@ public struct DSPProgram: Sendable, Equatable {
379
397
  self.samples = parsedSamples
380
398
  self.routes = parsedRoutes
381
399
  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)
400
+ self.block = try code("block")
401
+ self.sample = try code("sample")
386
402
  self.output = output
387
403
  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) }
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)
390
486
  }
391
487
 
392
488
  private static func int(_ value: Any?) -> Int? {
@@ -224,7 +224,10 @@ struct TextEditorElement: View {
224
224
  #if !canImport(FoundationBridge)
225
225
  /// The plain `texteditor` with `selection` (iOS 18, `TextEditor(text:selection:)`):
226
226
  /// the viewer's selection reported in UTF-16 offsets, and the caret moved
227
- /// to one the app sets, as the rich editor does.
227
+ /// to one the app sets, as the rich editor does. A text that arrives from
228
+ /// outside (not the echo of what the viewer typed) keeps the caret: the
229
+ /// app's selection is applied even when its numbers did not change, else
230
+ /// the caret is moved past the change (`caretAfterChange`).
228
231
  @available(iOS 18, macOS 15, *)
229
232
  struct PlainTextEditorElement: View {
230
233
  let node: ElementNode
@@ -232,16 +235,22 @@ struct PlainTextEditorElement: View {
232
235
  @State private var selection: TextSelection?
233
236
  @State private var shown = Shown()
234
237
 
235
- /// The selection last sent, or last moved to (a class: set while drawing).
238
+ /// The selection last sent, or last moved to, and the text last sent (a class: set while drawing).
236
239
  final class Shown {
237
240
  var sent: TextSelectionSpec?
241
+ var sentText: String?
238
242
  }
239
243
 
240
244
  var body: some View {
241
245
  TextEditor(
242
246
  text: Binding(
243
247
  get: { node.props.string("text") ?? "" },
244
- set: { if let event = textChange(of: node, to: $0) { app?.dispatch(event) } }
248
+ set: { edited in
249
+ if let event = textChange(of: node, to: edited) {
250
+ shown.sentText = edited
251
+ app?.dispatch(event)
252
+ }
253
+ }
245
254
  ),
246
255
  selection: Binding(
247
256
  get: { selection },
@@ -256,12 +265,25 @@ struct PlainTextEditorElement: View {
256
265
  )
257
266
  )
258
267
  .onChange(of: TextEditorProps(props: node.props).selection) { _, wanted in move(to: wanted) }
268
+ .onChange(of: node.props.string("text") ?? "") { old, new in textArrived(from: old, to: new) }
259
269
  .onAppear { move(to: TextEditorProps(props: node.props).selection) }
260
270
  }
261
271
 
262
- /// A selection the app set (not the one this editor sent): the caret goes there.
263
- private func move(to wanted: TextSelectionSpec?) {
264
- guard let wanted, wanted != shown.sent else { return }
272
+ /// The app's text changed: the echo of what the viewer typed leaves the caret alone; a text from outside keeps it in place.
273
+ private func textArrived(from old: String, to new: String) {
274
+ if new == shown.sentText { return }
275
+ if let wanted = TextEditorProps(props: node.props).selection {
276
+ move(to: wanted, force: true)
277
+ } else if let current = selection, let range = offsets(of: current, in: old) {
278
+ let start = stringIndex(utf16: caretAfterChange(before: old, after: new, offset: range.start), in: new)
279
+ let end = stringIndex(utf16: caretAfterChange(before: old, after: new, offset: range.end), in: new)
280
+ selection = start == end ? TextSelection(insertionPoint: start) : TextSelection(range: start..<end)
281
+ }
282
+ }
283
+
284
+ /// A selection the app set (not the one this editor sent): the caret goes there; `force` moves even to the numbers last sent (the text changed under them).
285
+ private func move(to wanted: TextSelectionSpec?, force: Bool = false) {
286
+ guard let wanted, force || wanted != shown.sent else { return }
265
287
  shown.sent = wanted
266
288
  let text = node.props.string("text") ?? ""
267
289
  let start = stringIndex(utf16: wanted.start, in: text)
@@ -0,0 +1,17 @@
1
+ /// Where a caret goes when a text changes under it from outside (another
2
+ /// person's edit arriving in a `texteditor`): an offset before the change
3
+ /// (the common prefix of the two texts) stays; one after it (the common
4
+ /// suffix) moves by the change's length; one inside the change goes to the
5
+ /// change's end. Offsets are UTF-16 units, as a `selection` is on the wire.
6
+ public func caretAfterChange(before: String, after: String, offset: Int) -> Int {
7
+ let a = Array(before.utf16)
8
+ let b = Array(after.utf16)
9
+ let shortest = min(a.count, b.count)
10
+ var prefix = 0
11
+ while prefix < shortest, a[prefix] == b[prefix] { prefix += 1 }
12
+ var suffix = 0
13
+ while suffix < shortest - prefix, a[a.count - 1 - suffix] == b[b.count - 1 - suffix] { suffix += 1 }
14
+ if offset <= prefix { return offset }
15
+ if offset >= a.count - suffix { return offset + (b.count - a.count) }
16
+ return b.count - suffix
17
+ }
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/wasm/SwiftBrowserNative.wasm
3
- size: 50264748 bytes
4
- sha256: 7712613a6aed40822224c0e4042dab20c7de0e5708ff6ad8ce79f52013b76472
3
+ size: 50265117 bytes
4
+ sha256: f7cb92276a436e8c765a5eca556c7436d02752c411256b0d6ff4e2ca33950635