Repository navigation
Conversation
houko
force-pushed
the
feat/audio-apple
branch
from
November 14, 2026 16:06
9518ca0 to
2665cc9
Compare
houko
force-pushed
the
feat/audio-apple
branch
from
November 15, 2026 06:06
6f100b4 to
10257a2
Compare
houko
added a commit
to houko/nsgo
that referenced
this pull request
Nov 15, 2026
The audio package joins speech recognition and the other services in their bullet, with nsgo-dev#84 and nsgo-dev#89 beside nsgo-dev#71, as the changelog lists the services.
houko
force-pushed
the
feat/audio-apple
branch
from
November 15, 2026 09:05
10257a2 to
5242652
Compare
houko
added a commit
to houko/nsgo
that referenced
this pull request
Nov 15, 2026
The audio package joins speech recognition and the other services in their bullet, with nsgo-dev#84 and nsgo-dev#89 beside nsgo-dev#71, as the changelog lists the services.
houko
force-pushed
the
feat/audio-apple
branch
2 times, most recently
from
November 15, 2026 11:46
2994db9 to
82e54e5
Compare
Apple's part of audio playback (docs/architecture/audio.md), shared by macOS and iOS and modeled on NSH's audio.rs: an Audio per engine, on the main thread, carrying out ItemAudio and reporting EventAudio. Sounds are read through imageload, held to their 8 MiB before decoding, written to a temporary file and decoded into an AVAudioPCMBuffer by AVAudioFile on a goroutine of their own inside an autorelease pool; one longer than the cap is reported cut without decoding. Each playback is a voice of up to 32 on the engine's one AVAudioEngine: an AVAudioPlayerNode into an AVAudioUnitVarispeed for the rate and a mixer of its own for the volume, as an effect keeps the player's own volume from the main mixer. A varispeed connected once takes no other format, so a sound of another format gets a voice of its own. The engine starts at a playback and pauses two seconds after the last, so the output is not held open. Players are layerless AVPlayers made at their first preload or play, with bytes and data: URLs played from a temporary file removed as they close. Their load is polled, their position comes from a periodic time observer with the seeks and laps counted, their end from AVPlayerItemDidPlayToEndTimeNotification, where a looping one seeks to the start and counts a lap, and a stop no command asked for is reported as the system's pause. A seek before the load applies at it. A path that is not UTF-8 or holds a NUL fails with not-supported before it reaches NSURL, as do local sources that cannot be read, while http(s) ones fail with network. On iOS sounds hold the shared audio session ambient while they play and players at the level their AudioSession asks for, through internal/audiosession. The tests run AVFoundation for real with the main run loop running in TestMain: decoding, cuts, quotas, the voice limit across formats, a player's end and replay, loops, seeks before and after the load, pauses, failures and the temporary files' removal.
…ple backend Each surface makes its apple.Audio with its first ItemAudio and closes it as it closes, so an engine's sounds and players go with its window or embedded view. macOS and iOS report Env.CanPlayAudio, tvOS not, as the design leaves it out of the first stages, and iOS pauses the players as the app enters the background, as NSH's do. A macOS test runs an app in a process of its own that loads and plays a sound and plays a player to its end through the window's engine, the core's callbacks seeing the load, the play and the end. The design, the roadmap's row, ARCHITECTURE.md and the services subject say macOS and iOS play audio now, iOS not yet on a device.
…ayed The design says a failed player plays nothing until Play or Preload loads it again, and core's Preload takes a failed player into loading, but AudioPreload only opened it, which a failed player refused, so the app waited in loading for good. A preload now lets the failed player go and loads it again as a play does. A load after a failure also started the player at the last seek or AudioOpen's position, while the core keeps the position it last reported and takes no lower one without a seek, so OnProgress froze until the player caught up. The player now keeps the position of its last progress or pause report and a load after a failure seeks there, or plays from the start after an end. Its seeks are in nanoseconds rather than 600ths of a second, which could land just before that position. Tests cover a preload after a failure, playing on after a failure mid-play, M4A (AAC) and FLAC files afconvert encodes at test time as sounds and through a player, and undecodable M4A, FLAC and MP3 files failing with Decode.
…as a system pause at once On an iPhone, another app's audio interrupting a playing player left it reported playing: the time observer's call as the AVPlayer stopped came while its rate was not yet 0, and none followed, so the player reported no pause, kept holding the session at its level and only paused at the next command. The engine's audio now observes AVAudioSessionInterruptionNotification, which only iOS posts, and pauses its players as an interruption begins, letting their session go. They stay paused as it ends, even when its options say they may resume, as the core has them paused by the system and the app's Play resumes them, as after the app left the foreground. Each player also observes AVPlayerRateDidChangeNotification, which macOS 12 and iOS 15 post, so a stop no command asked for is reported as the rate goes to 0, the time observer remaining for older macOS. Tests post an interruption as iOS does and stop a player without its time observer, both reported paused at once with no session held; the design says how an interruption pauses and stays paused.
…d after the last release On an iPhone XR, every pause of a player blocked the main thread for about 600 ms and iOS showed its hang banner: the last holder letting go deactivated the shared AVAudioSession at once, with NotifyOthersOnDeactivation, on the caller's thread, and the call waits for the system while an AVPlayer's output still runs down after its pause. The player's AudioPaused waited for it too, as the release comes before the report. The last release now only arranges the deactivation, which runs a second later on a goroutine of its own inside an autorelease pool. A hold before it keeps the session active, with no call at all when its level is the session's, as for a play right after a pause, and moves it from the level it still has otherwise; a hold while it runs waits for it under the holders' mutex, then activates the session again, so a deactivation and an activation never overlap and the new level is still held before the old one goes. A new level, Kept, below Ambient, holds the session in the category from before the first holder, the app's own, for a video playing with the default session, so that the session is not deactivated under it. Tests record the session's moves: the last release moves nothing on its caller's thread, a hold of the same level before the deactivation moves nothing, one of another level moves across, and a hold while the deactivation runs waits for it and activates again. The design says how the deactivation is deferred and what Kept is.
…e system stops the video A playing video whose AudioSessionFunc switched to AudioSessionDefault stalled on an iPhone about 0.8 s later, its position kept: the default session held nothing, so letting the old level go deactivated the session under the playing AVPlayer. A video playing with the default session now holds the session at Kept, the app's own category, taken before the old level goes, so the switch moves the session across and the video plays on; it is deactivated, a second later, once the video pauses, ends or closes. A video's session also followed only its Paused, so an interruption, a lost route or the controls' pause left it held at its level until the next command, as players' did before. The video now observes AVPlayerRateDidChangeNotification, which macOS 12 and iOS 15 post, and lets the session go as the rate goes to 0, holding it again as the controls play the video.
houko
force-pushed
the
feat/audio-apple
branch
from
November 15, 2026 13:44
82e54e5 to
63a5d11
Compare
This branch has not been deployed
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.
What changed
Stage 3 of audio playback for macOS and iOS: the Apple backend behind the
audiopackage of #84, which is merged. The branch is rebased ontomain(with #271 and #106) and holds only this PR's seven commits.internal/platform/apple/audio.gois oneAudioper engine, on the main thread, modeled on NSH'saudio.rs. A surface makes it with its firstItemAudioand closes it as it closes.imageload, held to 8 MiB before decoding, written to a temporary file and decoded into anAVAudioPCMBufferbyAVAudioFileon a goroutine of their own inside an autorelease pool. A sound longer than 5 seconds is reported cut without decoding. Each playback is a voice, up to 32 of them, on oneAVAudioEngineper engine. A voice is anAVAudioPlayerNodeinto anAVAudioUnitVarispeedfor the rate, then a mixer of its own for the volume, because an effect node keeps the player's own volume from the main mixer. A varispeed that is already connected refuses another format (-10868), so a sound of another format gets a new voice. The engine starts at a playback, starts again after the system stopped it, and pauses two seconds after the last playback ends.AVPlayers, asVideouses, made at the first preload or play. Bytes anddata:URLs play from a temporary file that is removed as the player closes.AVPlayerItemDidPlayToEndTimeNotification. A looping player seeks to the start there and counts a lap.AVPlayerRateDidChangeNotification(macOS 12, iOS 15) says the rate went to 0, and also when the time observer sees it, which covers macOS 11.AVAudioSessionInterruptionNotification) pauses the players as it begins: they reportAudioPaused, stop reporting progress and let their session level go. They stay paused when the interruption ends, even withShouldResume, because the core has them paused by the system andPlayresumes them, as after the app goes to the background. The design now says this.PlayorPreload, as the design says, and plays on from the position it last reported, which the core keeps, or from the start after an end.NotSupportedbefore it reachesNSURL, and so does a local source that cannot be read.http(s)sources fail withNetwork, and sources that cannot be decoded fail withDecode.internal/audiosession) as ambient while they play, and players hold the level theirAudioSessionasks for while they play. iOS pauses players as the app enters the background.Pauseand every background pause blocked the main thread for 593 to 634 ms, and iOS showed its hang banner: the last holder letting go calledsetActive:NOwithNotifyOthersOnDeactivationat once, on the main thread, while theAVPlayer's output was still running down, andAudioPausedwaited for it because the release comes before the report. The last release now only arranges the deactivation. It runs one second later on a goroutine of its own, inside an autorelease pool. A hold that comes first cancels it: with the same level it makes no session call at all (a play right after a pause), and with another level it moves the session from the level it still has. A hold that comes while the deactivation runs waits for it under the holders' mutex and then activates the session again, so a deactivation and an activation never overlap, and a new level is still held before the old one goes.VideowhoseAudioSessionFuncswitched toAudioSessionDefaultstalled about 0.8 s later with a ~620 ms hang, also onmain: the default session held nothing, so releasing the old level deactivated the session under the playingAVPlayer. A new lowest level,Kept, holds the session active in the category from before the first holder (the app's own) for a video that plays with the default session, so Playback -> Default -> Playback moves the session across without deactivating it.Paused, so an interruption, a lost route or a pause from the controls kept it held at its level until the next command, which is the gap fixed for players. The video now observesAVPlayerRateDidChangeNotificationand lets the session go when the rate goes to 0. It holds the session again if the controls play the video.Env.CanPlayAudio. tvOS does not, because the design leaves it out of the first stages.Keptand the deferred deactivation),native-views.md, the roadmap row,ARCHITECTURE.mdand the services subject now say that macOS and iOS play audio.CHANGELOG.mdis not changed, as it is frozen until 0.1.0.This replaces the earlier
AVAudioPlayerbranch, which was written against an API that #84 no longer has. The earlier review findings either no longer apply to this design or are fixed here:AVPlayerItemnotification, not a missingAVAudioPlayerone.NSURL.internal/audiosession.Validation
go testininternal/platform/appleon macOS: 14 audio tests through real AVFoundation, with the main run loop running. They cover decoding, cuts, quotas, the voice limit across two formats, a player's end and replay, loops, seeks before and after the load, pauses, failures, and a preload and a play that load a failed player again from where it played. M4A (AAC) and FLAC files thatafconvertencodes at test time are decoded as sounds and played to their end; these are skipped whereafconvertis missing. Undecodable M4A, FLAC and MP3 files fail withDecode, and temporary files are removed. Two of the tests are new:AVPlayertoo, with no session held and no more progress. The interruption's end withShouldResumeleaves it paused, andPlayresumes it.go testininternal/platform/macos: an app that loads and plays a sound and plays a player to its end through the window's engine.go testinaudio,coreandinternal/audiosession, andgo test -run 'Layer|Skills|Doc' .from the root.internal/audiosession's tests (also run with-race -count=3) record the session's moves. They check that the last release moves nothing on its caller's thread, that a hold of the same level before the deactivation moves nothing, that a hold of another level moves across from the level the session still has, and that a hold that comes while the deactivation runs waits for it and then activates the session again.go veton macOS (arm64 and amd64). An iOS (arm64, cgo) vet and build ofinternal/audiosession,microphone,internal/platform/appleandinternal/platform/ios. Builds ofinternal/audiosessionandmicrophonefor Linux, Windows and Android.Device checks
On the Mac (output muted, volume left as it was), the current head passed a probe app:
PausereportedAudioPausedat once, with the main thread's longest stall at 0.4 ms. MP3, M4A and FLAC each played to their end once. A loop kept playing with no end. A file that appeared late failed withNotSupported, then loaded withPreloadand played after a seek to its end. AVideoplayed across Default, Playback and MixWithOthers.Earlier heads passed these on the iPhone XR: a sound while Music plays (Music keeps playing, and the silent switch silences the sound);
AudioSessionPlaybackpausing other apps' audio andAudioSessionMixWithOthersmixing with it, both heard in silent mode; a sound during aVideo,microphone.CaptureandStartSpeechstopping none of them; going to the background pausing players; MP3, M4A and FLAC through a player; and on the Mac, open and close cycles leaving no audio device open.Needed on the iPhone XR for this head, which has not run there yet:
Pauseand the background pause: no hang banner,AudioPausedat once, and the session back to its earlier category about a second later.Playbackplayer at once and releases its level. When Music pauses again, the player stays paused, andPlayresumes it.Videoswitched Playback -> Default -> Playback keeps playing with no stall and no hang.Videointerrupted by Music lets its session go.Preload, and MixWithOthers vs Playback.