nx_audio_session.nx source
↩ module page · 278 lines · 10226 B
1// nx_audio_session.nx -- concrete audio-session subtype.
2//
3// Extends nx_media_session by embedding it as the FIRST field of the
4// struct, so any `*nx_audio_session` casts safely to
5// `*nx_media_session` for the base portion (classic C inheritance
6// pattern; works because NishiLang structs are byte-flat).
7//
8// Composes:
9// nx_media_session.nx -- base lifecycle + vtable
10// nx_codec.nx -- codec abstraction; encode/decode dispatch
11// nx_audio_capture.nx -- mic capture abstract + null/synth backends
12// nx_audio_playback.nx -- speaker playback abstract + null backend
13//
14// Consumers (audio session is composed by):
15// nx_video_room.nx -- audio + video + chat + signaling
16// nx_call.nx -- 1:1 family/friends call
17// nx_game_voice.nx -- spatial audio in voxels
18// nx_iot_intercom.nx -- kitchen tablet <-> garage
19// nx_cli_meet.nx -- terminal-only audio
20//
21// genealogy_id: nx_media_session_q10 + nx_codec_q10 + nx_voice_codec_v1 +
22// nx_lpc_q15 + nx_voice_frame_q10
23// lineage_id: nx_audio_session_q10
24
25// nx_safety_envelope:
26// intended_use: reusable audio session for sovereign media stack
27// sil_target: SIL2
28// evidence: composes shipped NishiVoice v1 + nx_media_session +
29// nx_codec; bench/nx_audio_session_smoke.nx proves
30// end-to-end capture -> encode -> decode -> playback
31// verdict: NOT_YET_EVALUATED -- pending smoke
32
33import "nx_syscalls_x86_64.nx"
34import "nx_media_session.nx"
35import "nx_codec.nx"
36import "nx_audio_capture.nx"
37import "nx_audio_playback.nx"
38
39// ---- Sealed enum: FEC strategy ----
40
41const NX_AUDIO_FEC_NONE: i64 = 0
42const NX_AUDIO_FEC_NEIGHBOR: i64 = 1 // half-res copy of frame N-1
43const NX_AUDIO_FEC_ADAPTIVE: i64 = 2 // tunes by recent loss measurement
44
45// ---- Sealed enum: AEC/NS state ----
46//
47// Today nx_aec + nx_ns are FUTURE per NISHIFAMILY_BITS_UP_ROADMAP L4.
48// The session carries opaque pointer slots so consumers can wire them
49// when they land without re-versioning the struct.
50
51const NX_AUDIO_AEC_NONE: i64 = 0
52const NX_AUDIO_AEC_BUILTIN: i64 = 1 // when nx_aec ships
53
54const NX_AUDIO_NS_NONE: i64 = 0
55const NX_AUDIO_NS_BUILTIN: i64 = 1 // when nx_ns ships
56
57// ---- The struct ----
58//
59// First field MUST be nx_media_session for the pointer-cast-up trick.
60// Layout: 136 bytes base + 11 i64 own fields = 224 bytes.
61
62struct nx_audio_session {
63 base: nx_media_session, // FIRST -- enables pointer-cast-up
64
65 // -- audio config --
66 sample_rate: i64, // 8000 / 16000 / 24000 / 48000
67 channels: i64, // 1 (mono) / 2 (stereo)
68 frame_samples: i64, // samples per frame (codec.frame_size_in)
69
70 // -- substrate references --
71 codec: *nx_codec, // points at concrete voice codec (v1 or v2)
72 capture: *nx_audio_capture,
73 playback: *nx_audio_playback,
74
75 // -- optional polish (null until L4 lands) --
76 aec_state: i64, // *nx_aec_state when shipped; 0 otherwise
77 ns_state: i64, // *nx_ns_state
78 jitter_state: i64, // *nx_jitter_buffer
79
80 // -- config knobs --
81 fec_strategy: i64, // NX_AUDIO_FEC_*
82 aec_mode: i64, // NX_AUDIO_AEC_*
83 ns_mode: i64, // NX_AUDIO_NS_*
84}
85
86const NX_AUDIO_SESSION_BYTES: i64 = 224
87
88// ---- Vtable implementations -- these wire into nx_media_session base ----
89
90func nx_audio_session_open_impl(ms: *nx_media_session) -> i64 {
91 // Cast the base ptr back up to the concrete subtype. Safe because
92 // nx_audio_session embeds nx_media_session as its FIRST field.
93 let s: *nx_audio_session = ms as *nx_audio_session
94
95 // Open backends if attached.
96 if s.capture as i64 != 0 {
97 let rc: i64 = nx_audio_capture_open(s.capture)
98 if rc != 0 {
99 s.base.state = NX_MEDIA_STATE_ERROR
100 s.base.err_code = rc
101 return rc
102 }
103 }
104 if s.playback as i64 != 0 {
105 let rc2: i64 = nx_audio_playback_open(s.playback)
106 if rc2 != 0 {
107 s.base.state = NX_MEDIA_STATE_ERROR
108 s.base.err_code = rc2
109 return rc2
110 }
111 }
112
113 s.base.state = NX_MEDIA_STATE_ACTIVE
114 return 0
115}
116
117func nx_audio_session_close_impl(ms: *nx_media_session) -> i64 {
118 let s: *nx_audio_session = ms as *nx_audio_session
119 if s.capture as i64 != 0 { nx_audio_capture_close(s.capture) }
120 if s.playback as i64 != 0 { nx_audio_playback_close(s.playback) }
121 s.base.state = NX_MEDIA_STATE_CLOSED
122 return 0
123}
124
125func nx_audio_session_on_peer_join_impl(ms: *nx_media_session,
126 peer_id: i64, reason: i64) -> i64 {
127 // Audio sessions don't allocate per-peer state in v1 -- mesh layer
128 // handles per-peer streams. Future v2 with mixing-on-server may
129 // grow this hook.
130 return 0
131}
132
133func nx_audio_session_on_peer_leave_impl(ms: *nx_media_session,
134 peer_id: i64, reason: i64) -> i64 {
135 return 0
136}
137
138func nx_audio_session_on_tier_change_impl(ms: *nx_media_session,
139 new_bw_tier: i64,
140 new_hw_tier: i64) -> i64 {
141 // hw-tier change -> reconsider codec config. For v1 we don't
142 // re-init the codec mid-stream (would drop frames); v2 will hot-
143 // swap via codec_id-stable but config-different paths.
144 return 0
145}
146
147func nx_audio_session_health_check_impl(ms: *nx_media_session) -> i64 {
148 let s: *nx_audio_session = ms as *nx_audio_session
149 if s.base.state == NX_MEDIA_STATE_ACTIVE { return NX_MEDIA_HEALTH_OK }
150 if s.base.state == NX_MEDIA_STATE_ERROR { return NX_MEDIA_HEALTH_FAILING }
151 if s.base.state == NX_MEDIA_STATE_CLOSED { return NX_MEDIA_HEALTH_DEAD }
152 return NX_MEDIA_HEALTH_UNKNOWN
153}
154
155// ---- Constructor ----
156
157func nx_audio_session_new(out: *nx_audio_session,
158 codec: *nx_codec,
159 sample_rate: i64, channels: i64,
160 room_id: i64) -> i64 {
161 // Initialize the base portion first (sets default vtable). The
162 // cast-up works because nx_audio_session embeds nx_media_session
163 // as its FIRST field, so the two pointers identify the same byte
164 // for the base portion of the struct.
165 let ms: *nx_media_session = out as *nx_media_session
166 nx_media_session_init(ms, NX_MEDIA_KIND_AUDIO, room_id)
167
168 // Wire OUR vtable implementations.
169 out.base.open_fn = nx_audio_session_open_impl
170 out.base.close_fn = nx_audio_session_close_impl
171 out.base.on_peer_join_fn = nx_audio_session_on_peer_join_impl
172 out.base.on_peer_leave_fn = nx_audio_session_on_peer_leave_impl
173 out.base.on_tier_change_fn = nx_audio_session_on_tier_change_impl
174 out.base.health_check_fn = nx_audio_session_health_check_impl
175
176 // Audio-specific fields.
177 out.sample_rate = sample_rate
178 out.channels = channels
179 out.codec = codec
180 if codec as i64 != 0 {
181 out.frame_samples = codec.frame_size_in
182 }
183 out.capture = 0 as *nx_audio_capture
184 out.playback = 0 as *nx_audio_playback
185 out.aec_state = 0
186 out.ns_state = 0
187 out.jitter_state = 0
188 out.fec_strategy = NX_AUDIO_FEC_NEIGHBOR
189 out.aec_mode = NX_AUDIO_AEC_NONE
190 out.ns_mode = NX_AUDIO_NS_NONE
191
192 return 0
193}
194
195// ---- Backend attach methods ----
196
197func nx_audio_session_attach_capture(s: *nx_audio_session,
198 cap: *nx_audio_capture) -> i64 {
199 s.capture = cap
200 return 0
201}
202
203func nx_audio_session_attach_playback(s: *nx_audio_session,
204 pb: *nx_audio_playback) -> i64 {
205 s.playback = pb
206 return 0
207}
208
209func nx_audio_session_set_codec(s: *nx_audio_session, c: *nx_codec) -> i64 {
210 s.codec = c
211 if c as i64 != 0 {
212 s.frame_samples = c.frame_size_in
213 }
214 return 0
215}
216
217// ---- Frame I/O ----
218//
219// Capture: read PCM from the capture backend; encode through codec;
220// caller takes the wire bytes and ships them through transport.
221//
222// Playback: caller hands us wire bytes (decoded by transport from
223// the remote peer); we run them through codec decode and push PCM to
224// the playback backend.
225
226// Encode one frame from the capture backend into `wire_out`.
227// Returns wire byte count (>0), 0 if no capture frame available,
228// or NX_CODEC_ERR_* on error.
229func nx_audio_session_capture_encode(s: *nx_audio_session,
230 wire_out: *u8, wire_cap: i64) -> i64 {
231 if s.codec as i64 == 0 { return NX_CODEC_ERR_NOT_SUPPORTED }
232 if s.capture as i64 == 0 { return 0 }
233
234 // Pull one PCM frame from capture.
235 let pcm_bytes: i64 = s.frame_samples * 2 * s.channels
236 let pcm_buf: *u8 = sys_mmap(pcm_bytes)
237 let got: i64 = nx_audio_capture_pull_frame(s.capture, pcm_buf, pcm_bytes)
238 if got <= 0 { return got }
239
240 // Encode through codec.
241 return nx_codec_encode(s.codec, pcm_buf, got, wire_out, wire_cap)
242}
243
244// Decode wire bytes (from remote) and push PCM into the playback backend.
245// Returns 0 on success or NX_CODEC_ERR_*.
246func nx_audio_session_decode_play(s: *nx_audio_session,
247 wire_in: *u8, wire_n: i64) -> i64 {
248 if s.codec as i64 == 0 { return NX_CODEC_ERR_NOT_SUPPORTED }
249
250 // Decode through codec.
251 let pcm_cap: i64 = s.frame_samples * 2 * s.channels * 2 // *2 slack
252 let pcm_buf: *u8 = sys_mmap(pcm_cap)
253 let pcm_n: i64 = nx_codec_decode(s.codec, wire_in, wire_n, pcm_buf, pcm_cap)
254 if pcm_n < 0 { return pcm_n }
255
256 if s.playback as i64 != 0 {
257 return nx_audio_playback_push_frame(s.playback, pcm_buf, pcm_n)
258 }
259 return 0
260}
261
262// ---- Accessors ----
263
264func nx_audio_session_sample_rate(s: *nx_audio_session) -> i64 {
265 return s.sample_rate
266}
267
268func nx_audio_session_channels(s: *nx_audio_session) -> i64 {
269 return s.channels
270}
271
272func nx_audio_session_frame_samples(s: *nx_audio_session) -> i64 {
273 return s.frame_samples
274}
275
276func nx_audio_session_codec(s: *nx_audio_session) -> *nx_codec {
277 return s.codec
278}