Repository navigation
feat(audio): add the audio API, core's part and the nsgotest model - #84
Merged
Merged
Conversation
houko
force-pushed
the
feat/audio-api
branch
from
November 14, 2026 16:05
6121a16 to
b0361cd
Compare
houko
force-pushed
the
feat/audio-api
branch
from
November 15, 2026 03:18
ac20924 to
182e277
Compare
houko
force-pushed
the
feat/audio-api
branch
from
November 15, 2026 04:56
182e277 to
e92c409
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-api
branch
from
November 15, 2026 10:18
e92c409 to
927916e
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.
Core numbers sounds and players from one counter of the process and sends their commands in the batch of the turn that calls them as ItemAudio requests, keeping a player's position, duration and state from the platform's EventAudio reports. A sound's source is decoded once per surface and shared by digest or URI, within NSH's limits: 8 MiB encoded, 5 seconds decoded, 64 sounds and 16 MiB decoded per engine. Env.CanPlayAudio says the platform plays audio; elsewhere loads and plays fail with AudioUnavailable without asking it. AccessAudio, and the audio capability of TSX packages, hold code to it.
audio.LoadSound, Sound with Play(Voice), audio.NewPlayer with PlayerOptions, PlayerState and audio.Error with NSH's codes are the API of docs/architecture/audio.md, on core's part: the package imports core alone, as mobile does, and stands among the portable packages. The architecture, layers, services and roadmap documents say what exists: the API and core's part, with no platform playing yet.
Where the environment says Audio, the model records the sounds loaded and played (App.Sounds) and each player's commands (App.Players), decodes the durations AudioDurations gives and fails the rest with Decode, refuses plays with NotAllowed under RefusePlayback, and moves a player on only as the test's clock moves, reporting its progress, loops and end. Tests of the audio package assert on what a tap played and a player's progress without sleeping.
iOS has one AVAudioSession per process, which videos, speech and microphone each set without knowing of the others, so a video that started playing with a session of its own took it from a recording. internal/audiosession now holds it for all of them, and for the audio package's players and sounds to come: each holder asks for a level (ambient, mixing, playback, recording or dictation), the highest held sets the category, and once none is held the session goes back to the category from before and is deactivated, telling other apps they may play. It imports nothing of the module, guards its holders with a mutex and does nothing outside iOS.
core.Video read AudioSession only as it mounted, and TSX's audioSession with it, so a changed prop remounted the video and started a new player at 0, and the hold-before-release path of the iOS backend never ran for videos. AudioSessionFunc binds it as the video's other reactive props are, TSX's audioSession goes through it, and the platforms apply the changed config as they already could: Apple without reopening the source, Android by opening it again.
houko
force-pushed
the
feat/audio-api
branch
from
November 15, 2026 11:35
927916e to
0624c81
Compare
Collaborator
Author
|
Reviewed. The audio API's core, its nsgotest model and the shared audio session were checked through #89's Apple backend on an iPhone XR (iOS 18.7) and on macOS:
The branch is rebased onto main; every commit builds, and the |
houko
marked this pull request as ready for review
November 15, 2026 11:35
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.
Summary
Implements the first stages of the audio design merged with #83 (docs/architecture/audio.md) for #36: the
audioAPI,core's part of it, the protocol, an unsupported default on every platform, thensgotestmodel, andinternal/audiosession. Platform backends come in their own PRs (#89 is Apple). This replaces the earlierPlayFile/PlayBytesAPI, whose callbacks ran off the engine's goroutine, which the design rejects.audiopackage (importscorealone, asmobiledoes, and is portable):LoadSound(cx, Source, func(*Sound, error)),Sound.Play(Voice),Stop,Duration,Close;NewPlayer(cx, Source, PlayerOptions)withPlay(func(error)),Preload,Pause,Seek,SetVolume,SetRate,SetLoop,Position,Duration,StateandClose;PlayerState;audio.Errorwith NSH's codes (NotAllowed,Network,Decode,NotSupported,QuotaExceeded,Unavailable,Failed), whicherrors.Isfinds in a failure that carries the platform's words.core's part: sounds and players are numbered from one counter of the process, and every call is a command in the batch of the turn that makes it (ItemAudio), answered byEventAudio. A sound's source is decoded once and shared, by URI or by the SHA-256 of its bytes, within NSH's limits (8 MiB encoded, 5 s decoded with the cut reported by the platform, 64 sounds and 16 MiB decoded per engine). A player's position, duration and state are kept from the platform's events. Callbacks run on the engine's goroutine in a later turn, and never once the sound or player closed or its owner went.coredrops a report from before the last seek and the position goes back only with a seek or a loop. A seek while paused shows at once andPlaystarts from there, andPlayafter the end starts over. A pause the system makes (AudioPaused) makes the player paused.Playright afterPause, before the platform reports the pause, asks the platform to play again rather than taking the player for playing, so a restart (Pause,Seek(0),Play) in one turn plays.Env.CanPlayAudio(driver.Environment.Audio) is false on every backend, soLoadSoundandPlayfail at once withUnavailablewithout asking the platform.core.AccessAudioand the TSX packages'audiocapability hold sounds and players, refused withNotAllowed(wrappingcore.Error'sERR_ACCESS_DENIED); anhttp(s)source needsnetwork, as an image's does.nsgotest: where the environment saysAudio,App.Soundsrecords the sounds decoded and played andApp.Playerseach player's commands;AudioDurationsgives the durations the platform decodes (the rest fail withDecode),RefusePlaybackfails plays withNotAllowed, and players move on only as the test's clock does.internal/audiosession: holds iOS's oneAVAudioSessionfor videos, speech,microphoneand the players and sounds to come, at the highest level held (ambient, mixing, playback, recording, dictation), restoring the category from before and deactivating withNotifyOthersOnDeactivationonce none is held. Videos, speech andmicrophonemoved onto it, so a video that starts playing with a session of its own no longer takes it from a recording. A playing video that changes its session holds the new level before releasing the old, so the session moves across without being deactivated.core.VideogainsAudioSessionFunc, bound as its other reactive props are, and TSX'saudioSessionfollows its prop through it, so a playing video's session changes in place instead of being read only at mount; before, a changed TSXaudioSessionremounted the video, starting a new player at 0. Apple applies a changedVideoConfig.AudioSessionwithout reopening the source; Android opens it again, as before.driver.AudioTypenames a source's bytes by their first bytes for the backends' decoders.Choices the design left open: sharing a decoded sound stops at a surface (window or embedded view), because a surface's audio goes as it closes;
SetVolumeandSetRatetakeVoice's rule of 0 as 1, asVideo's; a player that failed reportsPlayerFaileduntilPlayorPreloadloads it again, and a preloaded one isPlayerPaused. The design has no open decisions, so ARCHITECTURE.md's open questions are unchanged. No CHANGELOG bullet, as nothing plays on any platform yet.Validation
go tool nsgo check -target allgo tool nsgo test: 2027 passed, 14 skipped; the one failure isexamples/orbit'sTestASwipeTurnsTheRing, which fails about one run in ten without the video session commit too and passes on rerunTestVideoChangesItsAudioSessionInPlace(core) andTestVideoAudioSessionFollowsItsProp(TSX, throughnsgotest) change a mounted video's session and check that the same view gets the new configTestPlayerPlaysAfterPausecoversPause,Seek(0),Playin one turn and a secondPlaybefore the platform reports the pauseGOOS=windows,GOOS=linux,GOOS=android GOARCH=arm64andGOOS=js GOARCH=wasmgo vetof the touched packages;GOOS=iosvet ofinternal/audiosession,internal/platform/appleandmicrophoneTestLayeringandTestPublicAPINamesNoInternalTypeDevice checks still needed (iPhone,
internal/audiosession):microphone.Capturerecording, then play aVideowithAudioSessionPlayback: the recording keeps receiving samples and the video plays.StartSpeech, then play aVideowithAudioSessionMixWithOthers: the transcript keeps updating.VideowithAudioSessionPlaybackand stop it: Music pauses, then resumes once the video stops.VideowhoseAudioSessionFuncreturnsAudioSessionPlayback(or TSX'saudioSessionfrom a signal), then switch it toAudioSessionMixWithOtherswhile it plays: the video keeps playing without a gap, and background Music may start again alongside it.