@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.
- package/package.json +3 -3
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +63 -5
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +954 -126
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +137 -41
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +28 -6
- package/swift/Sources/SwiftBrowserProtocol/TextChange.swift +17 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
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.
|
|
67
|
-
"@swiftbrowser/web": "0.
|
|
66
|
+
"@swiftbrowser/react": "0.25.0",
|
|
67
|
+
"@swiftbrowser/web": "0.25.0",
|
|
68
68
|
"esbuild": "^0.28.2",
|
|
69
69
|
"react-refresh": "^0.17.0"
|
|
70
70
|
},
|
|
@@ -99,6 +99,36 @@ final class ProgramNode: AudioEngineCustomNode {
|
|
|
99
99
|
}
|
|
100
100
|
return true
|
|
101
101
|
}
|
|
102
|
+
if key.hasPrefix("table.") || key.hasPrefix("shape.") || key.hasPrefix("sample.") {
|
|
103
|
+
// Data replaced by name (DSPNode `setTable` …): made ready here, off the audio thread, then swapped in.
|
|
104
|
+
guard let json = value.string, let data = json.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data) else {
|
|
105
|
+
throw ProgramNodeError("unsupported: \(key) crosses as JSON")
|
|
106
|
+
}
|
|
107
|
+
let name = String(key[key.index(after: key.firstIndex(of: ".")!)...])
|
|
108
|
+
func floats(_ value: Any?) -> [Float] { (value as? [NSNumber] ?? []).map(\.floatValue) }
|
|
109
|
+
if key.hasPrefix("table."), let index = program?.tables.firstIndex(where: { $0.name == name }) {
|
|
110
|
+
let table = object as? [String: Any] ?? [:]
|
|
111
|
+
let frames = (table["frames"] as? [Any] ?? []).map { floats($0) }
|
|
112
|
+
let cosines = (table["cosines"] as? [Any])?.map { floats($0) }
|
|
113
|
+
renderer.send(.table(Int32(index), DSPMachine.tableLevels(frames: frames, cosines: cosines), Int32(frames.count)))
|
|
114
|
+
} else if key.hasPrefix("shape."), let index = program?.shapes.firstIndex(where: { $0.name == name }) {
|
|
115
|
+
renderer.send(.shape(Int32(index), DSPBuffer(floats(object))))
|
|
116
|
+
} else if key.hasPrefix("sample."), let index = program?.samples.firstIndex(where: { $0.name == name }) {
|
|
117
|
+
let sample = object as? [String: Any] ?? [:]
|
|
118
|
+
let bytes = Data(base64Encoded: sample["data"] as? String ?? "") ?? Data()
|
|
119
|
+
var audio = [Float](repeating: 0, count: bytes.count / 4)
|
|
120
|
+
_ = audio.withUnsafeMutableBytes { bytes.copyBytes(to: $0) }
|
|
121
|
+
renderer.send(.sample(Int32(index), DSPBuffer(audio), (sample["rate"] as? NSNumber)?.doubleValue ?? 44100))
|
|
122
|
+
}
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
if key.hasPrefix("routes.") {
|
|
126
|
+
// A route list replaced (DSPNode `setRoutes`): its registers, copied in by the machine.
|
|
127
|
+
guard let json = value.string, let data = json.data(using: .utf8), let list = (try? JSONSerialization.jsonObject(with: data)) as? [NSNumber],
|
|
128
|
+
let index = Int32(key.dropFirst(7)) else { throw ProgramNodeError("unsupported: \(key) crosses as JSON") }
|
|
129
|
+
renderer.send(.routes(index, DSPBuffer(list.map(\.floatValue))))
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
102
132
|
if key.hasPrefix("param.") {
|
|
103
133
|
guard let number = value.double else { throw ProgramNodeError("unsupported: \(key) takes a number") }
|
|
104
134
|
let name = String(key.dropFirst(6))
|
|
@@ -145,6 +175,12 @@ enum ProgramEvent: Sendable {
|
|
|
145
175
|
case controller(Int32, Int32, Double?)
|
|
146
176
|
case pitchBend(Int32, Double?)
|
|
147
177
|
case parameter(Int32, Float)
|
|
178
|
+
/// Data replaced (a table's samples and frames, a shape's points, a sample's audio and rate), made off the audio
|
|
179
|
+
/// thread; once swapped in, the buffer holds what the machine let go of, freed back off the audio thread.
|
|
180
|
+
case table(Int32, DSPBuffer, Int32)
|
|
181
|
+
case shape(Int32, DSPBuffer)
|
|
182
|
+
case sample(Int32, DSPBuffer, Double)
|
|
183
|
+
case routes(Int32, DSPBuffer)
|
|
148
184
|
case tempo(Double)
|
|
149
185
|
case seek(Double)
|
|
150
186
|
case reset
|
|
@@ -161,6 +197,8 @@ final class Renderer: @unchecked Sendable {
|
|
|
161
197
|
var events: [ProgramEvent] = []
|
|
162
198
|
var machine: DSPMachine?
|
|
163
199
|
var retired: DSPMachine?
|
|
200
|
+
/// Buffers the audio thread swapped out, for the main actor to free.
|
|
201
|
+
var spent: [DSPBuffer] = []
|
|
164
202
|
}
|
|
165
203
|
|
|
166
204
|
private let lock = OSAllocatedUnfairLock(initialState: Shared())
|
|
@@ -169,10 +207,16 @@ final class Renderer: @unchecked Sendable {
|
|
|
169
207
|
// The audio thread's own.
|
|
170
208
|
private var machine: DSPMachine?
|
|
171
209
|
private var inbox: [ProgramEvent] = []
|
|
210
|
+
/// Buffers swapped out since the lock was last free, waiting to go to `Shared.spent`.
|
|
211
|
+
private var spent: [DSPBuffer] = []
|
|
172
212
|
|
|
173
213
|
init() {
|
|
174
214
|
inbox.reserveCapacity(256)
|
|
175
|
-
|
|
215
|
+
spent.reserveCapacity(64)
|
|
216
|
+
lock.withLockUnchecked {
|
|
217
|
+
$0.events.reserveCapacity(256)
|
|
218
|
+
$0.spent.reserveCapacity(64)
|
|
219
|
+
}
|
|
176
220
|
}
|
|
177
221
|
|
|
178
222
|
func send(_ event: ProgramEvent) {
|
|
@@ -193,11 +237,14 @@ final class Renderer: @unchecked Sendable {
|
|
|
193
237
|
|
|
194
238
|
/// Releases, on the caller's thread, a machine the audio thread let go of.
|
|
195
239
|
func collect() {
|
|
196
|
-
let retired = lock.withLockUnchecked { shared -> DSPMachine
|
|
197
|
-
defer {
|
|
198
|
-
|
|
240
|
+
let (retired, spent) = lock.withLockUnchecked { shared -> (DSPMachine?, [DSPBuffer]) in
|
|
241
|
+
defer {
|
|
242
|
+
shared.retired = nil
|
|
243
|
+
shared.spent.removeAll(keepingCapacity: true)
|
|
244
|
+
}
|
|
245
|
+
return (shared.retired, shared.spent)
|
|
199
246
|
}
|
|
200
|
-
_ = retired
|
|
247
|
+
_ = (retired, spent)
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
/// The audio thread: false when there is no program yet (silence). With `right`, the left side into `out`.
|
|
@@ -211,6 +258,8 @@ final class Renderer: @unchecked Sendable {
|
|
|
211
258
|
shared.machine = nil
|
|
212
259
|
}
|
|
213
260
|
swap(&shared.events, &inbox)
|
|
261
|
+
// What was swapped out, for the main actor to free (when it has taken the last).
|
|
262
|
+
if shared.spent.isEmpty && !spent.isEmpty { swap(&shared.spent, &spent) }
|
|
214
263
|
}
|
|
215
264
|
guard let machine else {
|
|
216
265
|
inbox.removeAll(keepingCapacity: true)
|
|
@@ -223,6 +272,15 @@ final class Renderer: @unchecked Sendable {
|
|
|
223
272
|
case let .controller(number, value, beat): machine.controller(number, value: value, atBeat: beat)
|
|
224
273
|
case let .pitchBend(value, beat): machine.pitchBend(value, atBeat: beat)
|
|
225
274
|
case let .parameter(index, value): machine.setParameter(at: Int(index), value)
|
|
275
|
+
case let .table(index, levels, frames):
|
|
276
|
+
if machine.replaceTable(Int(index), levels: levels, frames: Int(frames)) { spent.append(levels) }
|
|
277
|
+
case let .shape(index, points):
|
|
278
|
+
if machine.replaceShape(Int(index), points: points) { spent.append(points) }
|
|
279
|
+
case let .sample(index, audio, rate):
|
|
280
|
+
if machine.replaceSample(Int(index), audio: audio, rate: rate) { spent.append(audio) }
|
|
281
|
+
case let .routes(index, list):
|
|
282
|
+
_ = machine.replaceRoutes(Int(index), registers: list)
|
|
283
|
+
spent.append(list)
|
|
226
284
|
case let .tempo(bpm): machine.setTempo(bpm)
|
|
227
285
|
case let .seek(beat): machine.seek(toBeat: beat)
|
|
228
286
|
case .reset: machine.reset()
|