mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-10 17:10:18 -10:00
fix(pet): keep Apple audio aligned without stopping the world
Transfer checked Float32 PCM directly from JavaScriptCore. Re-anchor native presentation after UI stalls, retire stale voice callbacks by ticket, and mute output failures while preserving world ticks, saves and exports. Validation: npm test && npm run check:web passed: 66 package, 14 SDK, 446 web; 0 failures, 0 web errors, 2 existing warnings. Pet check 63/0/0. Actual TUI Watch 12/0; Android instrumentation 11/0 with APK/lint build. Apple 11 checkpoint workflows plus 27 exact PCM ranges and malformed guards; final macOS compile/sign/verify and iOS Simulator build passed. Real AVAudioEngine completed 150 frames with a 750ms stall, all 12 onsets scheduled in the future. Previous host fails the injected PCM-error control; current host mutes sound, continues world ticks and exports. Audio-frame p95 70.6ms -> 23.4ms in synthetic profiling; concurrent-machine wall outliers remain, so no hard frame-time or subjective listening guarantee is claimed. Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
1 parent
5fd45f13ae
commit
ee35326e7f
10 files changed
+196
-41
No files matched your search
@@ -12,7 +12,7 @@ const pet_telemetry_js_1 = require("./pet-telemetry.js");
|
||||
const pet_sim_js_1 = require("./pet-sim.js");
|
||||
const pet_audio_js_1 = require("./pet-audio.js");
|
||||
const pet_engine_js_1 = require("./pet-engine.js");
|
||||
/** Synchronous JSON boundary for JavaScriptCore and embedded JS runtimes.
|
||||
/** Synchronous native boundary: JSON state and directly transferable PCM.
|
||||
* Native hosts share the actual world / score implementation, not a rewrite. */
|
||||
class PetNative {
|
||||
world;
|
||||
@@ -101,9 +101,12 @@ class PetNative {
|
||||
return JSON.stringify({ width, height, cells, timeMs: frame.timeMs, channel: frame.state.channel, arch: pet_sim_js_1.ARCH_OF[frame.state.channel],
|
||||
hollow: frame.state.observed < .92, dozing: frame.behaviour === 'doze', lit: frame.state.lit });
|
||||
}
|
||||
pcm(voicesJSON, startSample, length, rate) {
|
||||
pcmChannels(voicesJSON, startSample, length, rate) {
|
||||
const p = (0, pet_audio_js_1.renderPetPCM)(JSON.parse(voicesJSON), startSample, length, rate);
|
||||
return JSON.stringify([Array.from(p.left), Array.from(p.right)]);
|
||||
return [p.left, p.right];
|
||||
}
|
||||
pcm(voicesJSON, startSample, length, rate) {
|
||||
return JSON.stringify(this.pcmChannels(voicesJSON, startSample, length, rate).map(channel => Array.from(channel)));
|
||||
}
|
||||
}
|
||||
exports.PetNative = PetNative;
|
||||
|
||||
@@ -155,10 +155,12 @@ live delivery to finish before extending any request as current. These frames
|
||||
are transport metadata; they do not enter the event journal or pet tape.
|
||||
The Runtime importer separately pairs request
|
||||
lifecycles and preserves failure receipt times. Its local SSE/CLI and browser
|
||||
fixtures pass. Authenticated read-only observation of an existing local Runtime
|
||||
journal is verified; new active-session delivery through the native companions
|
||||
still needs acceptance QA. The
|
||||
source and liveness contracts are documented in `pet/README.md`.
|
||||
fixtures pass. Typed synthetic Engine events have also passed through the actual
|
||||
Runtime journal/SSE endpoint, recorder process and Apple file watcher/PetHost:
|
||||
a fresh request appeared at 1,233 ms and cleared after its answer at 2,000 ms.
|
||||
That provider-free fixture proves active native file delivery; physical-phone
|
||||
transport and visual popover inspection are separate evidence. The source and
|
||||
liveness contracts are documented in `pet/README.md`.
|
||||
|
||||
The full TUI binary builds offline, its foreground metadata projection test
|
||||
passes, and the real PTY walkthrough covers Watch selection, keyboard, mouse
|
||||
|
||||
@@ -17,9 +17,10 @@ npm test
|
||||
npm run check:web
|
||||
```
|
||||
|
||||
The pet suite currently contains 48 tests: event occupancy/unknown coverage,
|
||||
The pet suite currently contains 63 tests: event occupancy/unknown coverage,
|
||||
late failures, human request pairing, read-only local SSE reconnect/cursor
|
||||
recovery and cancellation after garbage collection, deterministic world/score/PCM,
|
||||
recovery, replay readiness and cancellation after garbage collection, bounded
|
||||
long sessions and recorder process restart, deterministic world/score/PCM,
|
||||
checkpoint integrity and continuation, immutable segment boundaries, pending input
|
||||
retention, browser saves overlapping source changes, live-file freshness/restarts,
|
||||
delayed browser reads crossing suspension, expression-version validation and legacy replay.
|
||||
@@ -67,6 +68,9 @@ workflows, including a generated two-hour synthetic unknown recording, segment
|
||||
publication, a save conflict that must preserve the running world, and actual
|
||||
live-file appends, pause/resume, in-place restarts, replacement and recreation. It writes
|
||||
only disposable fixtures under ignored conformance results and temporary storage.
|
||||
The same executable checks 27 tone/noise/long-clock PCM ranges at three sample
|
||||
rates through the actual JavaScriptCore Float32 boundary, comparing sample bits
|
||||
with the retained JSON path and rejecting malformed or nonfinite buffers.
|
||||
Kotlin 2.3.0 can instead be on PATH; its verifier builds only the particle core
|
||||
and conformance runner. The actual Compose application is a separate Gradle build:
|
||||
|
||||
@@ -182,8 +186,9 @@ open junctions; the same particle identities return to the whale at rest.
|
||||
companion acceptance. The old idle-stream shutdown failure is covered by a
|
||||
real HTTP regression fixture that forces garbage collection before closing.
|
||||
|
||||
Hosted CI and Grokbot findings are separate evidence. Their actual results belong
|
||||
on the pull request. Open implementation gaps are listed in [README.md](README.md).
|
||||
These chronological receipts describe the source at each milestone; later
|
||||
sections supersede earlier open items. Hosted CI and Grokbot findings are separate
|
||||
evidence on the pull request. Current limits are listed in [README.md](README.md).
|
||||
|
||||
|
||||
## Recorder process restart — September 12, 2026
|
||||
@@ -257,3 +262,55 @@ These are synthetic event fixtures with real transports, not provider calls.
|
||||
|
||||
World, score and native bundle bytes are unchanged. Hosted CI, independent
|
||||
Codewhalebot QA, and active-session native delivery remain separate evidence.
|
||||
|
||||
|
||||
## Apple audio and scope audit — September 12, 2026
|
||||
|
||||
Apple now transfers the shared generator's Float32 channels directly from
|
||||
JavaScriptCore. Each channel's type, length and finite samples are checked; its
|
||||
borrowed pointer is copied immediately while the JS value remains rooted.
|
||||
The JSON PCM method remains available to QuickJS consumers. There is one score
|
||||
and sample generator.
|
||||
|
||||
Actual AVAudioEngine playback exposed schedules more than a second in the past
|
||||
after a 750 ms UI stall. Presentation now rebases against the current world and
|
||||
retires stale voices after long gaps. Completion callbacks carry numeric tickets
|
||||
back to the main actor, preventing an old completion from retiring a newer node.
|
||||
A sound failure mutes and persists the sound preference while world ticks and
|
||||
recording exports continue. The previous Apple host stops the world on the same
|
||||
injected PCM failure; the corrected host advances from 400 ms to 933 ms and exports.
|
||||
|
||||
Local final-source receipts:
|
||||
|
||||
- Pet checks: 63 passed, zero failed/canceled. Required product gate: 66 package,
|
||||
14 SDK and 446 web tests passed; web check has zero errors and two existing warnings.
|
||||
- Apple: all 11 checkpoint/live-file/storage workflows and 27 exact PCM ranges
|
||||
pass, plus empty/invalid range and four malformed-buffer checks. macOS builds,
|
||||
signs and verifies; iOS Simulator builds from the same final Swift sources.
|
||||
- Real AVAudioEngine: 150 frames complete with a forced 750 ms stall. All 12
|
||||
scheduled onsets remain in the future (42–118 ms); the prior output drifts
|
||||
down to 1,703 ms late. This proves scheduling, not subjective listening quality.
|
||||
- An 80-second synthetic profile renders 122 voices. Audio-frame p95 falls from
|
||||
70.6 ms on JSON transport to 23.4 ms on final typed transfer, with the same
|
||||
final particle digest and sample peak. An earlier typed-transfer run had zero
|
||||
frames over 33 ms; the final run had six wall-time outliers (worst 738 ms) on
|
||||
the concurrently used machine. This is not a hard frame-time guarantee.
|
||||
- The regenerated bundle passes 12 actual TUI Watch tests and all 11 Android
|
||||
instrumentation tests together, plus APK assembly and lint. The score and
|
||||
particle algorithms are unchanged.
|
||||
|
||||
The original A–F workstreams map to implemented, buildable source:
|
||||
|
||||
| Workstream | Implementation and evidence |
|
||||
| --- | --- |
|
||||
| A — World | Shared seeded behavior state machine, environment, interaction journal, doze/wake, persistent pod identities and versioned forms. Browser/Apple/Android replay and checkpoint workflows; 380 four-port conformance checkpoints. |
|
||||
| B — Audio | One thirteen-category score and PCM generator; browser WebAudio, Apple AVAudioEngine, Android AudioTrack and opt-in TUI ffplay. Deterministic PCM, native cursor tests, Apple output/stall/failure receipts. Listening quality remains a separate human assessment. |
|
||||
| C — TUI | Existing Engine events feed Watch through the canonical bucketer; bounded cameo honors open-water collision, palette and motion rules. Actual product build, PTY interaction and 12 Watch checks. |
|
||||
| D — macOS | Locally signed LSUIElement application, menu-bar controls, launch-at-login option, Still/sound/source settings and DispatchSource live files. Real process persistence and host workflows; current popover inspection is unverified. |
|
||||
| E — Mobile | SwiftUI Simulator app and Compose Android app use the same core and local-file contract. iOS build plus prior Simulator restoration/export/recovery; Android APK/lint plus 11 real-engine/device-lifecycle tests. No automatic phone link is claimed. |
|
||||
| F — Live authority | Existing Engine journal and opt-in Runtime replay progress feed one event-v1 bucketer. Synthetic Engine → actual Runtime/SSE → recorder → native Apple host passes without provider calls or raw prompt capture. |
|
||||
|
||||
Source and build instructions are published on PR #6110. Commit-specific
|
||||
Grokbot review and GitHub checks are recorded there. Shipping, signing for public
|
||||
distribution and subjective audiovisual acceptance are not implied by these
|
||||
local builds or by a prior commit's green CI.
|
||||
+18
-9
@@ -72,6 +72,11 @@ It journals accepted telemetry, interactions, behaviors and persistent pod slots
|
||||
positions, so buffer partitioning cannot change the score. Audio mixing is a
|
||||
host concern. `pet-native.ts` exposes this exact core to QuickJS/JavaScriptCore.
|
||||
The TUI, Apple and Android hosts do not carry their own event bucketers or score schedulers.
|
||||
Apple copies validated Float32 PCM channels directly from JavaScriptCore into
|
||||
native buffers; it does not serialize sample arrays as JSON. AVAudioEngine
|
||||
follows the world clock, rebases presentation after a stall, and discards stale
|
||||
voices after a long gap. Device or rendering failures mute sound and report a
|
||||
message while the world, persistence and export continue.
|
||||
|
||||
The Rust particle implementation lives in the product's
|
||||
`crates/tui/src/tui/ambient_life/pet_sim.rs`; this package's runner imports it.
|
||||
@@ -167,11 +172,13 @@ tail on an IO worker, closes it on pause/background, and discards delayed delive
|
||||
The file must be updated by a producer; selecting a completed tape does not make
|
||||
it live. Use Import to replay that tape. Device files are not automatically synced
|
||||
from the desktop recorder.
|
||||
Authenticated read-only attachment has been exercised against a running local
|
||||
Runtime 0.9.13 and an existing session journal. Old completed work remains unknown
|
||||
at the recorder's current clock. The recorder also closes an idle stream after
|
||||
garbage collection. Receiving new active-session work through native companion
|
||||
surfaces still needs acceptance QA.
|
||||
The current live chain has been exercised with typed synthetic Engine events
|
||||
through the actual Runtime journal/SSE endpoint, recorder process and Apple
|
||||
file watcher/host. The world receives a fresh human request and clears it after
|
||||
its answer; prompt, question and answer text stay out of the recording. This
|
||||
uses the existing mock Engine handle without calling a provider. The older
|
||||
read-only Runtime 0.9.13 receipt predates the required progress capability and
|
||||
does not establish current compatibility.
|
||||
|
||||
## Persistence and current limits
|
||||
|
||||
@@ -214,7 +221,9 @@ A 30-hour synthetic Still run of the shared core rotated 263 segments and kept
|
||||
the active file below 0.45 MB, with exact checkpoint continuation after every
|
||||
rotation. Apple and Android separately exercise archive publication, failed saves
|
||||
and continued execution in their actual embedded engines. This is not 30 hours
|
||||
of animated native-device or power testing. Physical Android device acceptance,
|
||||
macOS popover inspection, active-session companion acceptance and final
|
||||
listening/power quality remain unfinished. This branch is a reviewable prototype,
|
||||
not a release candidate.
|
||||
of animated native-device or power testing. Current platform builds, replay,
|
||||
live file delivery and measured audio output have separate receipts in [QA.md](QA.md).
|
||||
Automatic desktop-to-phone transport is not included: mobile hosts consume a
|
||||
local file using the shared contract. Physical-phone listening/battery quality
|
||||
and current macOS popover inspection have not been established. The source is
|
||||
available as a development build; this branch has not been released.
|
||||
@@ -12,7 +12,7 @@ const pet_telemetry_js_1 = require("./pet-telemetry.js");
|
||||
const pet_sim_js_1 = require("./pet-sim.js");
|
||||
const pet_audio_js_1 = require("./pet-audio.js");
|
||||
const pet_engine_js_1 = require("./pet-engine.js");
|
||||
/** Synchronous JSON boundary for JavaScriptCore and embedded JS runtimes.
|
||||
/** Synchronous native boundary: JSON state and directly transferable PCM.
|
||||
* Native hosts share the actual world / score implementation, not a rewrite. */
|
||||
class PetNative {
|
||||
world;
|
||||
@@ -101,9 +101,12 @@ class PetNative {
|
||||
return JSON.stringify({ width, height, cells, timeMs: frame.timeMs, channel: frame.state.channel, arch: pet_sim_js_1.ARCH_OF[frame.state.channel],
|
||||
hollow: frame.state.observed < .92, dozing: frame.behaviour === 'doze', lit: frame.state.lit });
|
||||
}
|
||||
pcm(voicesJSON, startSample, length, rate) {
|
||||
pcmChannels(voicesJSON, startSample, length, rate) {
|
||||
const p = (0, pet_audio_js_1.renderPetPCM)(JSON.parse(voicesJSON), startSample, length, rate);
|
||||
return JSON.stringify([Array.from(p.left), Array.from(p.right)]);
|
||||
return [p.left, p.right];
|
||||
}
|
||||
pcm(voicesJSON, startSample, length, rate) {
|
||||
return JSON.stringify(this.pcmChannels(voicesJSON, startSample, length, rate).map(channel => Array.from(channel)));
|
||||
}
|
||||
}
|
||||
exports.PetNative = PetNative;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ARCH_OF, digest, layout } from './pet-sim.js';
|
||||
import { renderPetPCM, type PetVoice } from './pet-audio.js';
|
||||
import { PetEngineTelemetry } from './pet-engine.js';
|
||||
|
||||
/** Synchronous JSON boundary for JavaScriptCore and embedded JS runtimes.
|
||||
/** Synchronous native boundary: JSON state and directly transferable PCM.
|
||||
* Native hosts share the actual world / score implementation, not a rewrite. */
|
||||
export class PetNative {
|
||||
private world: PetWorld;
|
||||
@@ -80,8 +80,11 @@ export class PetNative {
|
||||
return JSON.stringify({ width, height, cells, timeMs: frame.timeMs, channel: frame.state.channel, arch: ARCH_OF[frame.state.channel],
|
||||
hollow: frame.state.observed < .92, dozing: frame.behaviour === 'doze', lit: frame.state.lit });
|
||||
}
|
||||
pcm(voicesJSON: string, startSample: number, length: number, rate: number): string {
|
||||
pcmChannels(voicesJSON: string, startSample: number, length: number, rate: number): [Float32Array, Float32Array] {
|
||||
const p = renderPetPCM(JSON.parse(voicesJSON) as PetVoice[], startSample, length, rate);
|
||||
return JSON.stringify([Array.from(p.left), Array.from(p.right)]);
|
||||
return [p.left, p.right];
|
||||
}
|
||||
pcm(voicesJSON: string, startSample: number, length: number, rate: number): string {
|
||||
return JSON.stringify(this.pcmChannels(voicesJSON, startSample, length, rate).map(channel => Array.from(channel)));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import AVFoundation
|
||||
/// schedules bounded buffers against AVAudioEngine's presentation clock.
|
||||
@MainActor public final class PetAudioOutput {
|
||||
private let engine = AVAudioEngine()
|
||||
private var active: [AVAudioPlayerNode] = []
|
||||
private var active: [UInt64: AVAudioPlayerNode] = [:]
|
||||
private var nextVoiceID: UInt64 = 0
|
||||
private let format = AVAudioFormat(standardFormatWithSampleRate: 48_000, channels: 2)!
|
||||
public private(set) var enabled = false
|
||||
private var epoch: UInt64 = 0
|
||||
@@ -15,6 +16,7 @@ import AVFoundation
|
||||
public func setEnabled(_ value: Bool, simulationTime: Double) throws {
|
||||
stop()
|
||||
if value {
|
||||
guard simulationTime.isFinite && simulationTime >= 0 else { throw PetCoreError.invalid("Invalid audio presentation time.") }
|
||||
// Materialize the output graph before starting. With no voices yet,
|
||||
// AVAudioEngine otherwise raises an Objective-C exception (not Error).
|
||||
_ = engine.mainMixerNode; _ = engine.outputNode
|
||||
@@ -23,15 +25,28 @@ import AVFoundation
|
||||
}
|
||||
enabled = value
|
||||
}
|
||||
private func discardVoices() {
|
||||
for node in active.values { node.stop(); engine.detach(node) }
|
||||
active.removeAll()
|
||||
}
|
||||
public func stop() {
|
||||
for node in active { node.stop(); engine.detach(node) }
|
||||
active.removeAll(); engine.stop(); enabled = false
|
||||
discardVoices(); engine.stop(); enabled = false
|
||||
}
|
||||
public func present(_ voices: [PetVoice], core: PetNativeCore) throws {
|
||||
guard enabled else { return }
|
||||
guard engine.isRunning else { throw PetCoreError.invalid("The audio output device stopped.") }
|
||||
let now = mach_absolute_time(), simulationTime = core.frame.timeMs / 1000
|
||||
let drift = AVAudioTime.seconds(forHostTime: now - epoch) - (simulationTime - simulationStart)
|
||||
// Device time continues through a stalled UI. Re-anchor presentation to
|
||||
// the current world instead of playing an ever-growing delayed score.
|
||||
// Small corrections let current buffers finish; long gaps discard them.
|
||||
if abs(drift) > 0.05 {
|
||||
if abs(drift) > 0.25 { discardVoices() }
|
||||
epoch = now; simulationStart = simulationTime
|
||||
}
|
||||
// A stopped/replaced replay cannot leave an old voice attached.
|
||||
active.removeAll { node in
|
||||
if !node.isPlaying { engine.detach(node); return true }; return false
|
||||
active = active.filter { _, node in
|
||||
if !node.isPlaying { engine.detach(node); return false }; return true
|
||||
}
|
||||
for voice in voices {
|
||||
if active.count >= 32 { break }
|
||||
@@ -45,14 +60,16 @@ import AVFoundation
|
||||
let node = AVAudioPlayerNode(); engine.attach(node); engine.connect(node, to: engine.mainMixerNode, format: format)
|
||||
let elapsed = max(0, Double(start) / 48_000 - simulationStart + 0.1)
|
||||
let when = AVAudioTime(hostTime: epoch + AVAudioTime.hostTime(forSeconds: elapsed))
|
||||
let identity = ObjectIdentifier(node)
|
||||
// A numeric ticket crosses the callback boundary; AVAudio nodes
|
||||
// stay on the main actor and address reuse cannot retire a new voice.
|
||||
nextVoiceID += 1; let identity = nextVoiceID
|
||||
node.scheduleBuffer(buffer, at: nil, options: [], completionCallbackType: .dataPlayedBack) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
guard let self, let node = self.active.first(where: { ObjectIdentifier($0) == identity }) else { return }
|
||||
node.stop(); self.engine.detach(node); self.active.removeAll { $0 === node }
|
||||
guard let self, let node = self.active.removeValue(forKey: identity) else { return }
|
||||
node.stop(); self.engine.detach(node)
|
||||
}
|
||||
}
|
||||
node.play(at: when); active.append(node)
|
||||
active[identity] = node; node.play(at: when)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,13 +150,14 @@ public func petArchiveLabel(_ name: String) -> String {
|
||||
}
|
||||
private func configureSound() {
|
||||
do { try audio.setEnabled(sound && !paused && !failed && !restoring, simulationTime: (core?.frame.timeMs ?? 0) / 1000) }
|
||||
catch { message = "Sound unavailable: \(error.localizedDescription)" }
|
||||
catch { sound = false; message = "Sound unavailable: \(error.localizedDescription)" }
|
||||
}
|
||||
private func tick() {
|
||||
guard !paused, !failed, !restoring, let core else { return }
|
||||
do {
|
||||
let f = try core.tick(motion: !(still || systemReducedMotion))
|
||||
try audio.present(f.voices, core: core)
|
||||
do { try audio.present(f.voices, core: core) }
|
||||
catch { sound = false; message = "Sound unavailable: \(error.localizedDescription)" }
|
||||
count += 1; if count % 150 == 0 { save() }
|
||||
objectWillChange.send()
|
||||
} catch { audio.stop(); failed = true; message = error.localizedDescription }
|
||||
|
||||
@@ -141,8 +141,27 @@ public enum PetCoreError: Error, LocalizedError {
|
||||
public func pcm(voice: PetVoice, startSample: Int, length: Int, rate: Int) throws -> [[Float]] {
|
||||
let voices = String(decoding: try JSONEncoder().encode([voice]), as: UTF8.self)
|
||||
failure = nil
|
||||
let text = world.invokeMethod("pcm", withArguments: [voices, startSample, length, rate])?.toString()
|
||||
guard let text, failure == nil else { throw PetCoreError.invalid(failure ?? "Unable to render pet audio.") }
|
||||
return try JSONDecoder().decode([[Float]].self, from: Data(text.utf8))
|
||||
guard let channels = world.invokeMethod("pcmChannels", withArguments: [voices, startSample, length, rate]), failure == nil,
|
||||
channels.isArray, channels.forProperty("length")?.toInt32() == 2
|
||||
else { throw PetCoreError.invalid(failure ?? "Unable to render pet audio.") }
|
||||
return try (0..<2).map { index in
|
||||
guard let channel = channels.atIndex(index) else { throw PetCoreError.invalid("Missing pet audio channel.") }
|
||||
let ctx = context.jsGlobalContextRef
|
||||
var exception: JSValueRef?
|
||||
guard JSValueGetTypedArrayType(ctx, channel.jsValueRef, &exception) == kJSTypedArrayTypeFloat32Array,
|
||||
exception == nil, let object = JSValueToObject(ctx, channel.jsValueRef, &exception), exception == nil,
|
||||
JSObjectGetTypedArrayLength(ctx, object, &exception) == length, exception == nil
|
||||
else { throw PetCoreError.invalid("Invalid pet PCM buffer.") }
|
||||
if length == 0 { return [] }
|
||||
guard let bytes = JSObjectGetTypedArrayBytesPtr(ctx, object, &exception), exception == nil
|
||||
else { throw PetCoreError.invalid("Pet PCM buffer is unavailable.") }
|
||||
// JavaScriptCore guarantees this pointer only until its next API
|
||||
// call. Copy immediately; no JS-backed memory escapes the method.
|
||||
let samples = withExtendedLifetime(channel) {
|
||||
Array(UnsafeBufferPointer(start: bytes.assumingMemoryBound(to: Float.self), count: length))
|
||||
}
|
||||
guard samples.allSatisfy(\.isFinite) else { throw PetCoreError.invalid("Invalid pet PCM samples.") }
|
||||
return samples
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Darwin
|
||||
import JavaScriptCore
|
||||
|
||||
@main struct AppleCheckpointProof {
|
||||
static func require(_ condition: @autoclosure () throws -> Bool, _ message: String) throws {
|
||||
@@ -23,12 +24,52 @@ import Darwin
|
||||
@MainActor static func main() async throws {
|
||||
let args = CommandLine.arguments
|
||||
let pet = URL(fileURLWithPath: args[1]), out = URL(fileURLWithPath: args[2])
|
||||
let bundle = Bundle(url: pet.appendingPathComponent("macos/CodewhalePet.app"))!
|
||||
let bundle = Bundle(url: args.count > 3 ? URL(fileURLWithPath: args[3]) : pet.appendingPathComponent("macos/CodewhalePet.app"))!
|
||||
let script = bundle.url(forResource: "pet-native", withExtension: "js")!
|
||||
let tape = try String(contentsOf: bundle.url(forResource: "demo", withExtension: "jsonl")!, encoding: .utf8)
|
||||
let points = try String(contentsOf: pet.appendingPathComponent("public/whale-points.tsv"), encoding: .utf8).split(separator: "\n").map { line -> (Double, Double) in
|
||||
let v = line.split(separator: "\t").map { Double($0)! }; return (v[0], v[1])
|
||||
}
|
||||
// Exercise the actual JavaScriptCore typed-array boundary, including
|
||||
// partial and empty ranges, against the retained JSON transport.
|
||||
let referenceContext = JSContext()!
|
||||
referenceContext.evaluateScript(try String(contentsOf: script, encoding: .utf8))
|
||||
let pointJSON = String(decoding: try JSONSerialization.data(withJSONObject: points.map { [$0.0, $0.1] }), as: UTF8.self)
|
||||
let reference = referenceContext.objectForKeyedSubscript("PetNative")!.construct(withArguments: [pointJSON])!
|
||||
let audioCore = try PetNativeCore(points: points, bundle: script)
|
||||
let audioVoices = [
|
||||
PetVoice(id: "tone", start: 0, duration: 0.44, frequency: 196, gain: 0.04, pan: -0.6, kind: "tone"),
|
||||
PetVoice(id: "noise", start: 12.033, duration: 0.22, frequency: 740, gain: 0.05, pan: 0.4, kind: "noise"),
|
||||
PetVoice(id: "long-clock", start: 600_000, duration: 0.3, frequency: 49, gain: 0.03, pan: 0, kind: "tone"),
|
||||
]
|
||||
var pcmCases = 0
|
||||
for voice in audioVoices {
|
||||
let voiceJSON = String(decoding: try JSONEncoder().encode([voice]), as: UTF8.self)
|
||||
for rate in [8000, 48000, 96000] {
|
||||
for offset in [-8, 0, 31] {
|
||||
let start = max(0, Int((voice.start * Double(rate)).rounded(.down)) + offset), count = rate / 10
|
||||
let transferred = try audioCore.pcm(voice: voice, startSample: start, length: count, rate: rate)
|
||||
let json = reference.invokeMethod("pcm", withArguments: [voiceJSON, start, count, rate])!.toString()!
|
||||
try require(referenceContext.exception == nil, "Reference PCM failed")
|
||||
let expected = try JSONDecoder().decode([[Float]].self, from: Data(json.utf8))
|
||||
try require(transferred.map { $0.map(\.bitPattern) } == expected.map { $0.map(\.bitPattern) }, "Typed PCM samples differ from JSON at \(rate) Hz / \(offset)")
|
||||
pcmCases += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
try require(audioCore.pcm(voice: audioVoices[0], startSample: 0, length: 0, rate: 48000) == [[], []], "Empty PCM range rejected")
|
||||
try reject({ _ = try audioCore.pcm(voice: audioVoices[0], startSample: -1, length: 4, rate: 48000) }, "Invalid PCM range accepted")
|
||||
let pcmRoot = out.appendingPathComponent("apple-pcm-fixture-" + UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: pcmRoot, withIntermediateDirectories: true)
|
||||
let originalScript = try String(contentsOf: script, encoding: .utf8)
|
||||
for (index, response) in ["null", "[new Float64Array(n), new Float64Array(n)]", "[new Float32Array(n + 1), new Float32Array(n)]", "[new Float32Array(n).fill(NaN), new Float32Array(n)]"].enumerated() {
|
||||
let invalid = pcmRoot.appendingPathComponent("invalid-\(index).js")
|
||||
try (originalScript + "\nPetNative.prototype.pcmChannels = function(v, s, n, r) { return " + response + "; };\n").write(to: invalid, atomically: true, encoding: .utf8)
|
||||
let badCore = try PetNativeCore(points: points, bundle: invalid)
|
||||
try reject({ _ = try badCore.pcm(voice: audioVoices[0], startSample: 0, length: 4, rate: 48000) }, "Malformed PCM channel accepted")
|
||||
}
|
||||
print("PASS Apple PCM transfer: \(pcmCases) exact tone/noise/long-clock sample ranges, empty input and malformed-buffer rejection")
|
||||
|
||||
for input in ["", tape] {
|
||||
let original = try PetNativeCore(points: points, bundle: script, tape: input)
|
||||
for i in 0..<750 {
|
||||
|
||||
Reference in new issue
Block a user