code wiki / (root) / nx_audio_session.nx

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}