PHASE A: Complete fix for Platform Audio Unity C# side - #378
Draft
MaxHeimbrock wants to merge 38 commits into
Draft
PHASE A: Complete fix for Platform Audio Unity C# side#378MaxHeimbrock wants to merge 38 commits into
MaxHeimbrock wants to merge 38 commits into
Conversation
On iOS the WebRTC ADM ran the shared AVAudioSession in automatic mode, so joining a call rerouted other app audio to the earpiece and hanging up deactivated the session out from under Unity (ambient audio died). Put RTCAudioSession into manual mode and have the app own the session: hold one permanent activation (so WebRTC's per-call setActive:NO never deactivates it), set PlayAndRecord + VideoChat (loudspeaker by default), and gate the VPIO unit via isAudioEnabled around connect/disconnect. Also restore the session on PlatformAudio.Dispose (previously dead code). Validated: Meet sample compiles for iOS (Unity 6000.3.10f1). Manual-mode audio behavior needs on-device validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Snapshot the app's audio session category/mode/options the first time LiveKit configures the session, and on the last PlatformAudio dispose (Interlocked instance counter) restore that snapshot and reactivate the session with setActive:YES so Unity audio output resumes. Previously the restore path hardcoded the Ambient category and left the session deactivated, which killed Unity audio at dispose time. Also removes the now-fixed README known issue and updates stale VoiceChat references in doc comments (the session uses VideoChat mode). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backgrounding interrupts the shared AVAudioSession and WebRTC stops its VPIO unit. On foreground RTCAudioSession restarts the unit exactly once, with no retry, while Unity/FMOD restarts its own audio around the same moment and can reconfigure the shared session underneath it (observed on Unity 6; Unity 2022 happens to win the race). In manual audio mode nothing else ever restarts the unit, leaving calls with no audio output and no mic input. Observe UIApplicationDidBecomeActive and interruption-ended in the plugin and, after Unity's delayed restart has settled, re-assert LiveKit's category/mode/options, reactivate the session, and cycle isAudioEnabled to force a clean rebuild of the audio unit. Logs the session state before the re-assert so device tests can see who won the focus race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Device-verified: with the foreground recovery in place, platform audio survives background/foreground on Unity 6, and the change is backwards compatible with Unity 2022.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Define the phase-A routing API on PlatformAudio, purely in C# and shaped identically to the planned FFI-backed implementation so app code written against it survives the plumbing swap: - AudioOutputKind enum (values mirror the planned FFI proto enum 1:1) - AudioDevice.Kind / AudioDevice.IsSelected - OutputPreference ranked policy (default BT > wired > speaker > earpiece) - IsSpeakerOutputPreferred as documented sugar over the list order - SelectOutput / ClearOutputOverride sticky override - DevicesChanged event (playout, recording) on the Unity main thread - internal IRouteController seam with desktop (FFI enumeration/GUID selection) and unsupported-mobile implementations; Android/iOS backends plug into the seam in follow-up work Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the PAR-019 routing seam on Android as an SDK-owned backend, hardened from the device-validated sample hotfix (PR #364): - AndroidRouteController (API 31+) pins the communication device to the sticky SelectOutput override while its device is available, else the highest-ranked available kind per OutputPreference; kinds missing from the ranking are never auto-selected (pin released, OS default applies) - session-scoped MODE_IN_COMMUNICATION with save/restore of the prior mode; optional audio-focus request (internal, default off) - change detection via OnCommunicationDeviceChangedListener plus a 1.5 s poll thread for the trace-verified transitions that fire no OS event (device added while pinned; BT headset leaving the available list ~10 s after the route already fell back); no-op re-pin guard stops the feedback loop from our own setCommunicationDevice - GetDevices playout list and DevicesChanged now report real Android communication devices with Kind and IsSelected; recording stays the FFI default-input placeholder - StartRecording re-asserts the policy: since Android 13 the mode request is only honored while voice-communication capture is active - pre-API-31 stays a documented unsupported placeholder, matching the hotfix gate; platform notes on the public API updated accordingly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… into max/par-021-unity-ios-session-features
The audio session config is now derived from a small state machine (idle / playout-only / recording) driven from C#, where PlatformAudio knows the recording state, plus a speaker-vs-earpiece preference expressed through the session mode (VideoChat/VoiceChat) — never overrideOutputAudioPort, so external devices always win. Non-recording states drop to mode Default + MixWithOthers to keep Unity audio unprocessed between calls; PlayAndRecord stays because the fork's ADM supports playout-only via an input-disabled VPIO unit but nothing guarantees VPIO under the Playback category. Every apply is mirrored into WebRTC's RTCAudioSessionConfiguration snapshot (reflected, no link-time dependency) so ADM-driven restarts re-apply the same config. Route changes are observed via AVAudioSessionRouteChangeNotification and forwarded to the new IosRouteController, which implements the PAR-019 seam: the playout list is the session's current output route with real Kind/IsSelected, DevicesChanged is raised on route changes (marshalled to the Unity main thread by PlatformAudio), OutputPreference reduces to the Speaker/Earpiece relative order, and SelectOutput throws the documented NotSupportedException pointing at AVRoutePickerView. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ADM skips the VPIO rebuild on route changes that keep the hardware sample rate (HandleValidRouteChange -> HandleSampleRateChange no-ops), so a live VoiceChat -> VideoChat switch left the unit calibrated for the receiver — device-observed as an attenuated loudspeaker after an earpiece -> speaker toggle. Cycle isAudioEnabled after a mode change (when call audio is wanted) to force a clean rebuild against the new route, the same mechanism the foreground recovery already uses. The recovery and configure paths pass NO: the former cycles itself, the latter runs before the unit exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…11-samples-migration # Conflicts: # Runtime/Scripts/Audio/PlatformAudio.cs # Runtime/Scripts/Audio/RouteController.cs
…ration # Conflicts: # Samples~/Meet/Assets/Runtime/MeetManager.cs
…011) With PAR-019/020/021 the SDK owns output routing (OutputPreference policy, sticky SelectOutput, DevicesChanged, communication-mode ownership on Android), so the sample-level reimplementation is deleted: RouteRank, the AudioManager JNI plumbing (setMode/setCommunicationDevice, the communication-device change listener) and the route watchdog poll, plus their call sites in MeetManager and LiveKitAgentSession. The controller now only demonstrates the API: it relies on the default OutputPreference ranking (customizing it is shown as a one-liner) and logs DevicesChanged with device kind and selection state. The Android capture lifecycle policy stays: capture starts at call begin and keeps running while muted, since Android 13 only honors the communication-mode request — and with it the SDK's route pin — while the app has active voice-communication capture. Both PlatformAudioController copies (Meet + Agents) stay byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an "Audio Output Routing" section to the platform-audio docs: the default OutputPreference order, the IsSpeakerOutputPreferred convenience toggle, sticky SelectOutput/ClearOutputOverride, DevicesChanged, and per-platform behavior (Android 12+/older Android/iOS/desktop), including the MODIFY_AUDIO_SETTINGS requirement and the Android 13 active-capture constraint. The Meet sample README points at it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Android routing backend took MODE_IN_COMMUNICATION and pinned the output route in its constructor and held both until Dispose, so an app that creates PlatformAudio at startup to keep one ADM alive sat in call mode from launch to quit. SetSessionAudioEnabled — the switch iOS already uses for this — was a documented no-op on Android. It is now plumbed through the routing backends: the Android backend takes the session on enable and hands it back on disable (pin cleared, replaced mode restored per transition, never an unconditional MODE_NORMAL), while enumeration, the change listener and the poll thread stay alive in both states so GetDevices and DevicesChanged keep working while idle. Every re-evaluation path — listener, poll, and the StartRecording re-assert — is observation-only while disabled, so none of them can resurrect a released session. The documented default (enabled at creation, uniform with iOS) is unchanged; the samples disable it right after creating PlatformAudio and MeetManager re-enables it for the duration of a call. The samples also recover Unity's own audio across output route changes: it does not follow a route change on its own, so the controller reopens the audio engine with AudioSettings.Reset, driven by Unity's OnAudioConfigurationChanged and by the SDK's DevicesChanged (logging both, since which one Android delivers is device behavior), coalesced so one route change resets once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Device testing contradicted them: Unity audio kept playing over a Bluetooth headset both while idle and alongside an active call, so the docs must not assert that holding the call session forces a headset onto a call link instead of A2DP media. What the SDK actually does — request communication mode, pin the route, and release both when session audio is disabled — is stated instead; how a given device carries call and media audio concurrently is left to the device observation the card still owes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eoff Device testing (Pixel 8a / Android 16, classic BR/EDR headset) established what actually breaks Unity's audio engine: a device being added or removed, not the route moving between the devices already connected. Joining a call leaves game audio playing, including when the platform moves media from the headset's A2DP link onto its call link. Resetting on every route change would therefore have restarted the game's audio at each join and hang-up for nothing, so the sample now triggers on the device set and resumes each source at the position it reached instead of from the start of the clip. The same run answered what the platform does with media during a call on a classic Bluetooth headset: dumpsys shows STREAM_MUSIC on bt_sco_hs while the call is active and back on bt_a2dp afterwards, with A2DP suspended for the duration — media is carried by the call link, not diverted or dropped. The README documents that as the observed behavior on that device, and as a further reason to hold the session only while a call is in progress. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Device testing (Pixel 8a / Android 16): disconnecting a Bluetooth headset does raise AudioSettings.OnAudioConfigurationChanged, but with deviceWasChanged=false, so the flag cannot separate a device change from any other reconfiguration and the recovery never ran. The sample now reacts to the callback either way; the callback AudioSettings.Reset raises itself always lands inside the coalescing window, so the recovery still cannot feed itself. This also makes Unity's callback the fast path rather than the SDK's event: a powered-off headset can stay in the platform's device list for seconds after the route moved, so the device-set signal trails the disconnect. The reset now reports how many sources came back and on which output, so a run can tell a failed reopen from a successful one that stayed silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adb traces of a Bluetooth connect on a Pixel 8a (Android 16) show why the
recovery left the app silent: Unity stops every AudioSource when it reinitializes
its engine and raises OnAudioConfigurationChanged 25 ms afterwards, so the
handler's snapshot of "what is playing" was always empty and it restored nothing.
34.920 AudioTrack stop(11092): called with 92104 frames delivered
34.945 Unity audio configuration changed (deviceWasChanged=False, ...)
34.947 Reopening Unity's audio output (...), resuming 0 source(s)
The same trace shows the two events are ordered the other way round than assumed:
a headset's call profile appears ~650 ms before its media profile takes over
(32.912 SCO available, 33.576 setA2dpActiveDevice), so the SDK's DevicesChanged
arrives while the engine is still healthy and Unity's callback arrives after the
damage. They are now used accordingly — DevicesChanged remembers what is audible
and touches nothing, Unity's callback puts it back — instead of both racing to
reopen the engine, which is what made the outcome depend on timing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adb traces of a mid-call Bluetooth connect on a Pixel 8a (Android 16) show
AudioSettings.Reset is not merely unnecessary but actively harmful. Reinitializing
the engine makes Unity claim the headset's call link through the deprecated
AudioManager.startBluetoothSco(), which evicts the setCommunicationDevice pin the
routing backend holds and leaves the platform unable to bring SCO up again:
03.281 setCommunicationRouteForClient … bt_sco_hs addr:…E0:03 (setCommunicationDevice, ours)
03.928 [AudioSettings.Reset]
04.060 setCommunicationRouteForClient … null (stopBluetoothSco, Unity)
04.083 setCommunicationRouteForClient … bt_sco addr: (startBluetoothSco, Unity)
04.783 AS.BtHelper: requestScoState: failed to connect in state 1
06.295 … and on every 1.5 s re-pin thereafter
Call audio and game audio both stayed on the loudspeaker for the rest of the
session as a result. Unity has already reopened its output by the time it raises
OnAudioConfigurationChanged, so the recovery only has to restart the app's own
sources — which it now does, idempotently, leaving anything Unity kept running
alone. The README's advice to reset is replaced with the reason not to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting a Bluetooth device is not synchronous: setCommunicationDevice starts an
SCO negotiation and the platform keeps reporting the previous communication device
until it completes. The 1.5 s poll took that for a dropped pin and re-issued the
request into its own pending activation, which the platform refuses — and the
refusal aborts the activation, so the route never arrived at all. Every failure in
the trace lands on the poll's cadence, with nothing else in the process asking for
SCO:
10:41:38.685 setCommunicationDevice() -> updateCommunicationRoute,
preferredCommunicationDevice: null
10:41:40.193 … 41.705 … 43.216 … (every 1.5 s for the whole call)
10:41:50.764 AS.BtHelper: requestScoState: failed to connect in state 1
AS.AudioDeviceBroker: failure to start BT SCO for uid: 10424
Call audio and the app's own media both stayed on the loudspeaker for the duration,
and both returned to the headset on hang-up, when the pin was cleared.
An outstanding pin now gets PinSettleTimeout to take effect before being issued
again. Recovering from a pin the platform drops silently — what the poll exists for
since PAR-020 — is unaffected: once the pin has been seen honored, any later
divergence re-pins immediately, and real route changes arrive through the change
listener rather than the poll.
This only became reachable with the session gating: while the route was pinned at
construction, it had long since been applied before any call, so the poll never
re-issued it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Withdrawing the outstanding legacy SCO request from here does not work. Measured on
device: stopBluetoothSco ran 158 ms before the pin and the pin was refused all the
same ("requestScoState: failed to connect in state 1"), because the request belongs
to a different client in the process and cannot be cancelled by this one.
So this is a situation the SDK can report but not repair. A pin the platform takes
without acting on is now retried with a doubling backoff up to 30 s instead of every
6 s, and the first failure logs a warning naming the likely cause and the
consequence, so it is diagnosable without LK_VERBOSE. The README documents it as a
known limitation with the workaround that does work — connect the headset after the
app has started.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A trace containing both outcomes inside one call settled it. Joining with the
headset connected since before launch left the pin unapplied; the headset then
dropped, the platform ran resetBluetoothSco, and on reconnect the same pin was
applied 708 ms later:
13:47:32.690 setCommunicationDevice(bt_sco_hs) -> preferredCommunicationDevice: null
13:47:40.227 updateCommunicationRoute … eventSource: resetBluetoothSco
13:47:49.828 setCommunicationDevice(bt_sco_hs) -> null
13:47:50.536 … preferredCommunicationDevice: bt_sco_hs
eventSource: BtHelper.onScoAudioStateChanged, state: 12
So the limitation is a platform SCO state that can be left pending, not Unity's
grab specifically — the grab provokes it but the previous wording made it the
whole cause, and the same failure occurs on Unity 6, which does not grab. The
note now says what the state does, what clears it (a headset reconnect, a
Bluetooth toggle, a reboot), and that routing is prompt once it is clear.
This also refutes the A2DP-streaming theory the card carried: the failing join
happened with the app's own audio stopped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Agents sample never re-enabled session audio. The shared controller's Initialize() hands the session back, and only MeetManager took it again on join — LiveKitAgentSession did not, so every agent call ran with session audio off: no VPIO unit on iOS (no mic, no agent audio at all), no communication mode and no route pin on Android 12+. The session is now enabled after the room connects and disabled in EndSession, and the controller comment no longer claims MeetManager is the only caller. A server-initiated disconnect left the microphone recording. OnDisconnected only disabled session audio; Unpublish/StopCapture ran solely from OnEndCall/OnDestroy. The capture is deliberately kept open across mute cycles, so after a kick, a deleted room, or a token expiry the mic stayed hot — indicator on — until the user pressed End Call. Both paths now share one idempotent TeardownCall. The pin settle/backoff state was only maintained on the success path. _pinnedAtUtc was stamped only when setCommunicationDevice returned true, so one refused re-issue froze the anchor; once the elapsed time passed the 30 s cap, "settling" was false on every pass and the backoff decayed into the warn+re-issue every 1.5 s poll tick it exists to prevent. The stamp now lands on every attempt, both release sites reset the tracking through one ResetPinTracking(), and the window is measured with the monotonic Stopwatch clock instead of DateTime.UtcNow, so a wall-clock step can no longer stretch or cut it. Compile-checked 8/8: package + Assembly-CSharp for Meet (editor, iOS player) and Agents (editor, Android player); sample controller copies byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- SetSessionAudioEnabled (Android backend) promised the sticky override survives a disable/enable transition unconditionally. It does not: the drop-on-disappear bookkeeping keeps running while the session is disabled, so a device that leaves the list between calls clears the override for good. The doc now says so instead of promising otherwise. - SelectOutput never stated its session-enabled precondition on Android: while session audio is disabled the choice is only recorded, no pin is issued, and the device lists keep reporting the platform's route. Added to the XML doc and the README, which endorses pre-call device pickers. - StartRecording claimed recording starts automatically when PlatformAudio is created. The Rust side only acquires the ADM in PlatformAudio::new(); capture starts solely through start_recording. - The sample recovery comment told readers an "echo window" swallows the callback its own reset raises — both were removed on this branch; the real re-entry guard is idempotence (a playing source is left alone). Compile-checked 8/8; sample controller copies byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two route-reporting/recovery changes in AndroidRouteController: - The pin settle window now applies only to Bluetooth targets. Its whole justification is the asynchronous SCO negotiation; the other kinds apply without one, so a divergence there is always a dropped or ignored pin and is re-issued immediately — restoring the pre-gate recovery speed for speaker/wired/USB. A pending Bluetooth pin is deliberately still not re-issued by the StartRecording re-assert: re-issuing lands in our own pending activation and aborts it (the F11 livelock), so the up-to-6 s wait in the join flow is the safer side of that trade. - Reevaluate now reports the device the platform actually has, never the one merely requested: the selected id is re-read after each issue and left at the current device while a pin settles. DevicesChanged and GetDevices() agree again, a stuck pin no longer shows the target as selected for the whole call, and the real arrival raises its own DevicesChanged (the optimistic report used to swallow it, since the signature never changed). And the sample recovery (both controller copies, byte-identical) fixes the holes the review verified: - Restore runs BEFORE remember in OnUnityAudioConfigurationChanged. The engine has already stopped every source at that point, so remembering first purged every interrupted one-shot and the restore put back nothing but loops. - The healthy-signal remember pass (OnDevicesChanged) now forgets any stopped source, loops included: it runs before the engine reinit, so a stopped source there was stopped by the app, and a deliberate Stop() must not be undone by the next device change. The post-restore pass only refreshes positions, so a Play() the engine rejected mid-burst keeps its slot for the next callback. - The adopt-all-loops fallback is gated to the very first switch (before any sweep has run); afterwards it would force-play loops the app stopped or never started. - Sources on deactivated GameObjects are dropped instead of being retried with a warning on every callback and force-played on reactivation. Compile-checked 8/8; sample controller copies byte-identical. Needs one device pass: BT join flow (settle + arrival event) and the Unity-audio recovery scenarios. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StartRecording gated on the permission dialog on Android but not on iOS, so an app that starts its capture automatically — the Agents sample does, right after connecting — opened the ADM's input unit while the first-run permission prompt was still pending. A capture opened before the grant records silence, and nothing reopens the input after the user taps Allow, so the whole session published a silent microphone track: the agent never heard the user. Meet never shows this because the capture starts from the unmute button, on installs whose permission was granted long ago. The coroutine now mirrors the Android gate: request the authorization, yield until the user answers, throw on denial, and only then open the capture. Already-granted installs pass straight through. Inferred from the code and platform behavior; needs the device pass on a fresh install (prompt path) plus a second run (granted path). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LeaveCommunicationMode swallows exceptions, and its caller flips _sessionAudioEnabled before it runs — so one transient JNI failure during a hang-up (e.g. the activity briefly unavailable during pause) left the platform in MODE_IN_COMMUNICATION with the route still pinned, while the controller reported the session released. A repeat disable early-returns on the unchanged flag, and the disabled-mode Reevaluate never issues a clear, so nothing repaired it until the next call or Dispose. The enable side had the same hole in the opposite direction. A failed transition now marks itself pending, and Reevaluate — where every trigger already funnels: the poll, the change listener, the StartRecording re-assert — retries the transition for the current desired state, so a flip that happened in between is never undone. Both methods are idempotent when re-run partially completed (mode save/restore already paired per transition; clearing an unheld pin is a no-op). The failure warns once; retries are silent and the next success clears the flag. Compile-checked 8/8. No dedicated device scenario: the path only differs when the platform throws mid-transition; a normal join/leave pass covers the regression risk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AndroidRouteController's constructor entered MODE_IN_COMMUNICATION and pinned the route because session audio defaults to enabled, and the recommended pattern of disabling right after construction then produced a take -> pin -> clear flap at every app launch (a measured 320 ms transient; with a Bluetooth headset connected, an SCO activation started only to be cleared mid-negotiation, inside the same startup window where the platform's SCO state machine is fragile). The enabled default and the PAR-019 API surface are unchanged. The constructor now only records the state; the session is first acquired by the first trigger that needs it while enabled: an explicit SetSessionAudioEnabled(true) call (also when the state was already enabled - the way in for receive-only apps), ApplyOutputPreference (which the StartRecording re-assert funnels through, where Android 13 starts honoring the mode request anyway) or SelectOutput. Disabling or disposing before anything acquired the session releases nothing, so the create -> disable startup path issues no audio-mode traffic at all. The mode save now happens at the actual acquisition and the save/restore idempotency rules are untouched; enumeration, the change listener and the poll thread still start in the constructor, and neither the poll nor the listener can take a lazily-deferred session. Behavior change, deliberate: an app that plays remote audio, never records, never touches routing and never calls SetSessionAudioEnabled no longer gets the mode and pin from construction; it opts in with SetSessionAudioEnabled(true) at its call boundary, per the documented "enabled == a call is in progress" contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every public member now throws ObjectDisposedException after Dispose() instead of reaching a disposed FfiHandle (or, on iOS, driving the audio session after it was handed back). DevicesChanged subscribe/unsubscribe and double-Dispose stay safe, as before. The constructor no longer leaks the FFI handle when route-controller creation fails after the handle exists: the post-handle work is wrapped so the handle (and a half-built controller) are disposed before the exception propagates, instead of waiting on the SafeHandle finalizer. The SelectOutput deferral while session audio is disabled deliberately gets no pending flag (PAR-019 keeps the surface frozen); the XML doc and the README routing section now spell out how a pre-call picker tracks its deferred choice through the existing IsSelected/DevicesChanged surface, including the drop-on-disappear case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-022) The last missing entry from PAR-022's desktop-runnable test list: a fresh PlatformAudio created after a full dispose in the same session must come up working (the native ADM ref-count has to return to zero cleanly), and the first instance's preference mutations must not leak into it. The other listed tests (preference roundtrip/precedence, bogus SelectOutput, event safety across dispose) already exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
We had several issues with platform audio on mobile (iOS & Android), especially with:
Changes
What did you do?