@swiftbrowser/native 0.26.1 → 0.26.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/native",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "type": "module",
5
5
  "description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
6
6
  "repository": {
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@babel/core": "^7.29.7",
66
- "@swiftbrowser/react": "0.26.1",
67
- "@swiftbrowser/web": "0.26.1",
66
+ "@swiftbrowser/react": "0.26.2",
67
+ "@swiftbrowser/web": "0.26.2",
68
68
  "esbuild": "^0.28.2",
69
69
  "react-refresh": "^0.17.0"
70
70
  },
@@ -198,6 +198,11 @@ public final class DSPMachine: @unchecked Sendable {
198
198
  /// Re-rating: per register whether it may change from frame to frame, and the sample stream's instructions kept.
199
199
  private let changing: UnsafeMutablePointer<UInt8>
200
200
  private let keptSample: UnsafeMutablePointer<Int32>
201
+ /// Per register whether it may change from frame to frame within a block, as the streams were last made: what the
202
+ /// sample stream (as it stays after re-rating) and the master stage write, state, the beat, a region's results and
203
+ /// a choice. A filter whose cutoff and resonance do not works out its coefficients once a chunk.
204
+ private let framewise: UnsafeMutablePointer<UInt8>
205
+ private let registerCount: Int
201
206
 
202
207
  private let laneNote: UnsafeMutablePointer<Int32>
203
208
  private let laneGate: UnsafeMutablePointer<Float>
@@ -310,6 +315,8 @@ public final class DSPMachine: @unchecked Sendable {
310
315
  pinnedMask = mask()
311
316
  routedMask = mask()
312
317
  changing = mask()
318
+ framewise = mask()
319
+ self.registerCount = registerCount
313
320
  for p in program.parameters { parameterMask[p.register] = 1 }
314
321
  for r in stateRegisters { stateMask[r] = 1 }
315
322
  for c in program.choices { renamedMask[c.register] = 1 }
@@ -505,7 +512,7 @@ public final class DSPMachine: @unchecked Sendable {
505
512
  routeRegisters.deallocate()
506
513
  routeCounts.deallocate()
507
514
  routeState.deallocate()
508
- for pointer in [parameterMask, stateMask, renamedMask, pinnedMask, routedMask, changing] { pointer.deallocate() }
515
+ for pointer in [parameterMask, stateMask, renamedMask, pinnedMask, routedMask, changing, framewise] { pointer.deallocate() }
509
516
  constantValues.deallocate()
510
517
  parameterLowest.deallocate()
511
518
  parameterHighest.deallocate()
@@ -617,6 +624,11 @@ public final class DSPMachine: @unchecked Sendable {
617
624
  sampleSerial = reschedule(sampleCount, from: keptSample, into: sampleCode)
618
625
  masterCount = masters
619
626
  masterSerial = reschedule(masters, from: orderedMaster, into: masterCode)
627
+ let stride = Int32(width)
628
+ for r in 0..<registerCount { framewise[r] = stateMask[r] | renamedMask[r] }
629
+ framewise[6] = 1
630
+ for i in 0..<sampleCount { framewise[Int(keptSample[i * 12 + 1] / stride)] = 1 }
631
+ for i in 0..<masters { framewise[Int(orderedMaster[i * 12 + 1] / stride)] = 1 }
620
632
  return true
621
633
  }
622
634
 
@@ -1656,17 +1668,49 @@ public final class DSPMachine: @unchecked Sendable {
1656
1668
  }
1657
1669
  }
1658
1670
 
1671
+ /// PolyBLEP's correction near a wrap (`t` the phase, `dt` its step; 0 for a step of 0 or less): both sides worked
1672
+ /// out and one chosen, with no branch, so a loop of them vectorizes.
1659
1673
  @inline(__always)
1660
1674
  private static func blep(_ t: Float, _ dt: Float) -> Float {
1661
- if t < dt {
1662
- let x = t / dt
1663
- return x + x - x * x - 1
1664
- }
1665
- if t > 1 - dt {
1666
- let x = (t - 1) / dt
1667
- return x * x + x + x + 1
1668
- }
1669
- return 0
1675
+ let x0 = t / dt
1676
+ let x1 = (t - 1) / dt
1677
+ let rising = x0 + x0 - x0 * x0 - 1
1678
+ let falling = x1 * x1 + x1 + x1 + 1
1679
+ return dt > 0 ? (t < dt ? rising : t > 1 - dt ? falling : 0) : 0
1680
+ }
1681
+
1682
+ /// sin(2π p) for a phase `p`: folded into a quarter cycle, then Taylor's polynomial to x¹¹, in floats with no
1683
+ /// branch, so a loop of them vectorizes. Within 2e-7 of sin; machine.ts takes JavaScript's own.
1684
+ @inline(__always)
1685
+ static func sin2pi(_ p: Float) -> Float {
1686
+ let q = p - (p + 0.5).rounded(.down)
1687
+ let h = q > 0.25 ? 0.5 - q : q < -0.25 ? -0.5 - q : q
1688
+ let x = h * (2 * Float.pi)
1689
+ let z = x * x
1690
+ var s: Float = -2.5052108e-8
1691
+ s = s * z + 2.7557319e-6
1692
+ s = s * z - 1.9841270e-4
1693
+ s = s * z + 8.3333333e-3
1694
+ s = s * z - 0.16666667
1695
+ return x + x * z * s
1696
+ }
1697
+
1698
+ /// tan(x) for 0 … π/2 (a filter's prewarp): Cephes' tanf polynomial up to π/4, and 1 / tan(π/2 − x) past it, in
1699
+ /// floats with no branch, so a loop of them vectorizes. Within 1.5e-6 of tan (a cutoff 0.0002% off at the top of
1700
+ /// the range), where machine.ts takes JavaScript's own, faster there than any polynomial in emulated floats.
1701
+ @inline(__always)
1702
+ static func tan(_ x: Float) -> Float {
1703
+ let upper = x > 0.78539816
1704
+ let y = upper ? 1.5707964 - x : x
1705
+ let z = y * y
1706
+ var p: Float = 9.38540185543e-3
1707
+ p = p * z + 3.11992232697e-3
1708
+ p = p * z + 2.44301354525e-2
1709
+ p = p * z + 5.34112807005e-2
1710
+ p = p * z + 1.33387994085e-1
1711
+ p = p * z + 3.33331568548e-1
1712
+ let t = p * z * y + y
1713
+ return upper ? 1 / t : t
1670
1714
  }
1671
1715
 
1672
1716
  /// A table's, shape's or sample's index from its register: in range, or the first.
@@ -1851,32 +1895,63 @@ public final class DSPMachine: @unchecked Sendable {
1851
1895
  }
1852
1896
  }
1853
1897
  case 40, 41, 42, 43, 44:
1854
- // Oscillators: the output from the phase now, then the phase steps by freq / sampleRate.
1898
+ // Oscillators: the output from the phase now, then the phase steps by freq / sampleRate. A loop for
1899
+ // each, with no branch in it and its index arithmetic unchecked (an overflow check would keep it from
1900
+ // vectorizing), so each runs across the lanes at once.
1855
1901
  let s = S + Int(code[at + 7])
1856
- for fk in 0..<fc {
1857
- for l in 0..<lanes {
1858
- let el = fk * V + l
1859
- let p = s[l]
1860
- let dt = a[el] / sr
1861
- var v: Float
1862
- switch op {
1863
- case 40: v = p
1864
- case 41: v = Foundation.sin(2 * Float.pi * p)
1865
- case 42:
1866
- v = 2 * p - 1
1867
- if dt > 0 { v -= Self.blep(p, dt) }
1868
- case 43:
1902
+ @inline(__always) func step(_ l: Int, _ p: Float, _ dt: Float) {
1903
+ let next = p + dt
1904
+ s[l] = next >= 0 && next < 1 ? next : next - next.rounded(.down)
1905
+ }
1906
+ switch op {
1907
+ case 40:
1908
+ for fk in 0..<fc {
1909
+ for l in 0..<lanes {
1910
+ let el = fk &* V &+ l
1911
+ let p = s[l]
1912
+ d[el] = p
1913
+ step(l, p, a[el] / sr)
1914
+ }
1915
+ }
1916
+ case 41:
1917
+ for fk in 0..<fc {
1918
+ for l in 0..<lanes {
1919
+ let el = fk &* V &+ l
1920
+ let p = s[l]
1921
+ d[el] = Self.sin2pi(p)
1922
+ step(l, p, a[el] / sr)
1923
+ }
1924
+ }
1925
+ case 42:
1926
+ for fk in 0..<fc {
1927
+ for l in 0..<lanes {
1928
+ let el = fk &* V &+ l
1929
+ let p = s[l]
1930
+ let dt = a[el] / sr
1931
+ d[el] = 2 * p - 1 - Self.blep(p, dt)
1932
+ step(l, p, dt)
1933
+ }
1934
+ }
1935
+ case 43:
1936
+ for fk in 0..<fc {
1937
+ for l in 0..<lanes {
1938
+ let el = fk &* V &+ l
1939
+ let p = s[l]
1940
+ let dt = a[el] / sr
1869
1941
  let w = Swift.min(Swift.max(b[el], 0), 1)
1870
- v = p < w ? 1 : -1
1871
- if dt > 0 {
1872
- let q = p + 1 - w
1873
- v += Self.blep(p, dt) - Self.blep(q - q.rounded(.down), dt)
1874
- }
1875
- default: v = 1 - 4 * Swift.abs(p - 0.5)
1942
+ let q = p + 1 - w
1943
+ d[el] = (p < w ? 1 : -1) + Self.blep(p, dt) - Self.blep(q - q.rounded(.down), dt)
1944
+ step(l, p, dt)
1945
+ }
1946
+ }
1947
+ default:
1948
+ for fk in 0..<fc {
1949
+ for l in 0..<lanes {
1950
+ let el = fk &* V &+ l
1951
+ let p = s[l]
1952
+ d[el] = 1 - 4 * Swift.abs(p - 0.5)
1953
+ step(l, p, a[el] / sr)
1876
1954
  }
1877
- d[el] = v
1878
- let next = p + dt
1879
- s[l] = next >= 0 && next < 1 ? next : next - next.rounded(.down)
1880
1955
  }
1881
1956
  }
1882
1957
  case 45:
@@ -1990,34 +2065,49 @@ public final class DSPMachine: @unchecked Sendable {
1990
2065
  case 47, 48, 49:
1991
2066
  // A state-variable filter (Simper's form): resonance 0 … 1.
1992
2067
  let s = S + Int(code[at + 7])
1993
- for fk in 0..<fc {
2068
+ // Its coefficients with the polynomial tan, in loops with no branch and their index arithmetic
2069
+ // unchecked, so they vectorize: once a chunk where the cutoff and resonance are set once a block, every
2070
+ // frame where they move (an envelope's cutoff), which costs less than keeping them for when they do not.
2071
+ @inline(__always) func coefficients(_ cutoffIn: Float, _ resonanceIn: Float) -> (a1: Float, a2: Float, a3: Float, k: Float) {
2072
+ let cutoff = Swift.min(Swift.max(cutoffIn, 10), 0.49 * sr)
2073
+ let resonance = Swift.min(Swift.max(resonanceIn, 0), 0.98)
2074
+ let g = Self.tan(Float.pi * cutoff / sr)
2075
+ let k = 2 - 2 * resonance
2076
+ let a1 = 1 / (1 + g * (g + k))
2077
+ return (a1, g * a1, g * g * a1, k)
2078
+ }
2079
+ @inline(__always) func step(_ l: Int, _ el: Int, _ a1: Float, _ a2: Float, _ a3: Float, _ k: Float) {
2080
+ let ic1 = s[l]
2081
+ let ic2 = s[L &+ l]
2082
+ let x = a[el]
2083
+ let v3 = x - ic2
2084
+ let v1 = a1 * ic1 + a2 * v3
2085
+ let v2 = ic2 + a2 * ic1 + a3 * v3
2086
+ s[l] = 2 * v1 - ic1
2087
+ s[L &+ l] = 2 * v2 - ic2
2088
+ d[el] = op == 47 ? v2 : op == 49 ? v1 : x - k * v1 - v2
2089
+ }
2090
+ let held = !serial && framewise[Int(code[at + 3]) / width] == 0 && framewise[Int(code[at + 4]) / width] == 0
2091
+ if held {
1994
2092
  for l in 0..<lanes {
1995
- let el = fk * V + l
1996
- let cutoff = Swift.min(Swift.max(b[el], 10), 0.49 * sr)
1997
- let resonance = Swift.min(Swift.max(c[el], 0), 0.98)
1998
- if !(s[2 * L + l] == cutoff && s[3 * L + l] == resonance) {
1999
- s[2 * L + l] = cutoff
2000
- s[3 * L + l] = resonance
2001
- let g = Foundation.tan(Float.pi * cutoff / sr)
2002
- let k = 2 - 2 * resonance
2003
- let a1 = 1 / (1 + g * (g + k))
2004
- s[4 * L + l] = a1
2005
- s[5 * L + l] = g * a1
2006
- s[6 * L + l] = g * g * a1
2007
- s[7 * L + l] = k
2093
+ let (a1, a2, a3, k) = coefficients(b[l], c[l])
2094
+ s[4 &* L &+ l] = a1
2095
+ s[5 &* L &+ l] = a2
2096
+ s[6 &* L &+ l] = a3
2097
+ s[7 &* L &+ l] = k
2098
+ }
2099
+ for fk in 0..<fc {
2100
+ for l in 0..<lanes {
2101
+ step(l, fk &* V &+ l, s[4 &* L &+ l], s[5 &* L &+ l], s[6 &* L &+ l], s[7 &* L &+ l])
2102
+ }
2103
+ }
2104
+ } else {
2105
+ for fk in 0..<fc {
2106
+ for l in 0..<lanes {
2107
+ let el = fk &* V &+ l
2108
+ let (a1, a2, a3, k) = coefficients(b[el], c[el])
2109
+ step(l, el, a1, a2, a3, k)
2008
2110
  }
2009
- let a1 = s[4 * L + l]
2010
- let a2 = s[5 * L + l]
2011
- let a3 = s[6 * L + l]
2012
- let ic1 = s[l]
2013
- let ic2 = s[L + l]
2014
- let x = a[el]
2015
- let v3 = x - ic2
2016
- let v1 = a1 * ic1 + a2 * v3
2017
- let v2 = ic2 + a2 * ic1 + a3 * v3
2018
- s[l] = 2 * v1 - ic1
2019
- s[L + l] = 2 * v2 - ic2
2020
- d[el] = op == 47 ? v2 : op == 49 ? v1 : x - s[7 * L + l] * v1 - v2
2021
2111
  }
2022
2112
  }
2023
2113
  case 50:
@@ -79,7 +79,9 @@ extension AnyShape {
79
79
  case "ellipse": self = AnyShape(Ellipse())
80
80
  case "roundedRectangle", "containerRelative": self = AnyShape(RoundedRectangle(cornerRadius: spec.cornerRadius, style: .continuous))
81
81
  case "path":
82
- if let data = spec.path, !data.isEmpty, let parsed = Path(svg: data) {
82
+ if let affine = spec.affinePath {
83
+ self = AnyShape(AffinePathShape(affine: affine))
84
+ } else if let data = spec.path, !data.isEmpty, let parsed = Path(svg: data) {
83
85
  self = AnyShape(PathShape(path: parsed, size: spec.size.map { CGSize(width: $0.width, height: $0.height) }))
84
86
  } else {
85
87
  // No path yet (the size is not reported), or none to read: nothing to draw.
@@ -117,6 +119,15 @@ struct NoPathShape: Shape {
117
119
  func path(in rect: CGRect) -> Path { Path() }
118
120
  }
119
121
 
122
+ /// A custom shape whose path is drawn for the rectangle it is laid out in,
123
+ /// from its first layout (`AffinePath`).
124
+ struct AffinePathShape: Shape {
125
+ let affine: AffinePath
126
+ func path(in rect: CGRect) -> Path {
127
+ (Path(svg: affine.data(width: rect.width, height: rect.height)) ?? Path()).offsetBy(dx: rect.minX, dy: rect.minY)
128
+ }
129
+ }
130
+
120
131
  struct PathShape: Shape {
121
132
  let path: Path
122
133
  let size: CGSize?
@@ -376,6 +376,8 @@ public struct ShapeProps: Equatable, Sendable {
376
376
  public var path: String?
377
377
  /// The size `path` was drawn for.
378
378
  public var size: SizeSpec?
379
+ /// A path for any size, in place of `path` and `size`.
380
+ public var affinePath: AffinePath?
379
381
  public init(props: JSONObject) {
380
382
  shape = props.string("shape") ?? "rectangle"
381
383
  cornerRadius = props.double("cornerRadius") ?? 0
@@ -386,6 +388,7 @@ public struct ShapeProps: Equatable, Sendable {
386
388
  trimTo = props.object("trim")?.double("to")
387
389
  path = props.string("path")
388
390
  size = SizeSpec(json: props.value("size"))
391
+ affinePath = AffinePath(json: props.value("affinePath"))
389
392
  }
390
393
  }
391
394
 
@@ -258,3 +258,70 @@ private struct PathScanner {
258
258
  return byte == UInt8(ascii: "1")
259
259
  }
260
260
  }
261
+
262
+ /// A custom shape's path for any size (`affinePath`): SVG path data whose
263
+ /// numbers are their values for an empty rectangle, the `i`th growing
264
+ /// `width[i]` a point of the shape's width and `height[i]` a point of its
265
+ /// height. @swiftbrowser/react sends one for a path whose every number is
266
+ /// such a function of the size, so the shape is drawn at the size it is laid
267
+ /// out at with no size reported.
268
+ public struct AffinePath: Equatable, Sendable {
269
+ public var path: String
270
+ public var width: [Double]
271
+ public var height: [Double]
272
+
273
+ public init(path: String, width: [Double], height: [Double]) {
274
+ self.path = path
275
+ self.width = width
276
+ self.height = height
277
+ }
278
+
279
+ public init?(json: JSONValue) {
280
+ guard let path = json["path"].string else { return nil }
281
+ self.path = path
282
+ width = (json["width"].array ?? []).map { $0.double ?? 0 }
283
+ height = (json["height"].array ?? []).map { $0.double ?? 0 }
284
+ }
285
+
286
+ /// The path data for a `width` by `height` rectangle, its points rounded
287
+ /// to hundredths as @swiftbrowser/react's `Path` rounds them.
288
+ public func data(width w: Double, height h: Double) -> String {
289
+ let bytes = Array(path.utf8)
290
+ var out = ""
291
+ out.reserveCapacity(bytes.count + bytes.count / 4)
292
+ var index = 0
293
+ var at = 0
294
+ func isDigit(_ i: Int) -> Bool { i < bytes.count && bytes[i] >= 0x30 && bytes[i] <= 0x39 }
295
+ while at < bytes.count {
296
+ let byte = bytes[at]
297
+ // A number: an optional minus, digits with a point among or before them, an exponent.
298
+ let starts = isDigit(at) || (byte == 0x2E && isDigit(at + 1)) || (byte == 0x2D && (isDigit(at + 1) || (at + 2 < bytes.count && bytes[at + 1] == 0x2E && isDigit(at + 2))))
299
+ guard starts else {
300
+ out.unicodeScalars.append(Unicode.Scalar(byte))
301
+ at += 1
302
+ continue
303
+ }
304
+ var end = at + (byte == 0x2D ? 1 : 0)
305
+ while isDigit(end) { end += 1 }
306
+ if end < bytes.count && bytes[end] == 0x2E {
307
+ end += 1
308
+ while isDigit(end) { end += 1 }
309
+ }
310
+ if end < bytes.count && (bytes[end] == 0x65 || bytes[end] == 0x45) {
311
+ var exponent = end + 1
312
+ if exponent < bytes.count && (bytes[exponent] == 0x2B || bytes[exponent] == 0x2D) { exponent += 1 }
313
+ if isDigit(exponent) {
314
+ end = exponent
315
+ while isDigit(end) { end += 1 }
316
+ }
317
+ }
318
+ let value = (Double(String(decoding: bytes[at..<end], as: UTF8.self)) ?? 0)
319
+ + w * (index < width.count ? width[index] : 0) + h * (index < height.count ? height[index] : 0)
320
+ let rounded = (value * 100).rounded() / 100
321
+ out += rounded == 0 ? "0" : rounded == rounded.rounded() && Swift.abs(rounded) < 1e15 ? String(Int(rounded)) : String(rounded)
322
+ index += 1
323
+ at = end
324
+ }
325
+ return out
326
+ }
327
+ }
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/wasm/SwiftBrowserNative.wasm
3
- size: 50265117 bytes
4
- sha256: f7cb92276a436e8c765a5eca556c7436d02752c411256b0d6ff4e2ca33950635
3
+ size: 50270516 bytes
4
+ sha256: 4ab90fcf8d11104965a65927418d4cf6f47e4595cfc42a018cecb08ee849644e