code wiki / (root) / nx_audio_scene_quality.nx

nx_audio_scene_quality.nx source

↩ module page · 288 lines · 10611 B

1// nx_audio_scene_quality.nx -- audio-scene quality grader. 2// 3// Per honest audit 2026-05-16: visual graders dominate; sound design 4// is graded only via the layer's FFT primitives, not composed. This 5// primitive grades a soundscape on 5 axes (Elder AI sound-grader 6// parallel). 7// 8// Inputs: caller provides 9// - spectral_bins: caller-computed FFT magnitudes (8 octave bands; 10// bin[0] = sub-bass ... bin[7] = brilliance) 11// - rms_window: a per-time-frame RMS energy array (for dynamic range) 12// - n_sources: count of distinct sound sources playing 13// - n_frames: temporal frame count for the rms_window 14// 15// AXES (Q14 [0, Q]; higher = better): 16// 17// 0. FREQUENCY_BALANCE: stddev of spectral bins; healthy mix has 18// moderate stddev (not flat, not extreme). Bell band [0.15Q, 0.4Q]. 19// 1. DYNAMIC_RANGE: max(rms) / mean(rms). Target [2x, 8x]. Squashed 20// audio (compression) scores low. 21// 2. SPECTRAL_CENTROID_BALANCE: weighted average bin index / 8; target 22// band [0.3, 0.6] (not too dark, not too bright). 23// 3. SOURCE_DIVERSITY: n_sources / target (8). Single-source scenes 24// are sparse; 4-8 sources score well. 25// 4. TEMPORAL_SMOOTHNESS: penalise rms second-difference (catches 26// glitchy / clicky scenes); also penalise zero variance (catches 27// static drones). 28// 29// EMITS LAYER_VERDICT (kind = NX_LAYER_KIND_AUDIO). 30// 31// genealogy_id: theile_aes_1981_audio_dynamics + elder_ai_sound_grader_canon 32// lineage_id: nx_audio_scene_quality_5axis_v1 33// 34// nx_safety_envelope: 35// intended_use: "Audio scene quality grader -- 5-axis verdict 36// (dynamics, balance, masking, intelligibility, 37// envelope) for substrate-emitted audio assets" 38// sil_target: SIL1 (quality grader, not deployed runtime) 39// asil_target: QM 40// dal_target: NONE 41// evidence: [sealed_5_axis_verdict, no_FP_in_grader_dispatch, 42// Theile_AES_1981_dynamics_basis] 43// hazard_register: [bug-tape-quiet-clip-passes-dynamics-axis, 44// bug-tape-stereo-imbalance-not-detected] 45// residual_risk: "Grader audits content from substrate audio 46// synth pipeline only. External audio (e.g. 47// user-uploaded) needs sanitisation upstream." 48// verdict: NOT_YET_EVALUATED 49 50import "nx_syscalls.nx" 51import "nx_tier.nx" 52import "nx_layer_verdict.nx" 53 54const NX_AQ_Q: nx_int = 16384 55 56const NX_AQ_AXIS_FREQ_BALANCE: nx_int = 0 57const NX_AQ_AXIS_DYNAMIC_RANGE: nx_int = 1 58const NX_AQ_AXIS_SPECTRAL_CENTROID: nx_int = 2 59const NX_AQ_AXIS_SOURCE_DIVERSITY: nx_int = 3 60const NX_AQ_AXIS_TEMPORAL_SMOOTH: nx_int = 4 61const NX_AQ_AXIS_COUNT: nx_int = 5 62 63func _aq_band_score(val: nx_int, lo_q: nx_int, hi_q: nx_int) -> nx_int { 64 let q: nx_int = NX_AQ_Q 65 if val < 0 { return 0 } 66 if val < lo_q { 67 if lo_q > 0 { return (val * q) / lo_q } 68 return 0 69 } 70 if val <= hi_q { return q } 71 let span: nx_int = hi_q - lo_q 72 if span <= 0 { return 0 } 73 let excess: nx_int = val - hi_q 74 var s: nx_int = q - (excess * q) / span 75 if s < 0 { s = 0 } 76 return s 77} 78 79// ===== Axis 0: frequency balance =================================== 80// CV (mean abs deviation / mean) of 8 spectral bins. 81func _aq_freq_balance_q14(bins: *i64, n_bins: nx_int) -> nx_int { 82 if n_bins < 2 { return 0 } 83 let q: nx_int = NX_AQ_Q 84 var sum: nx_int = 0 85 var i: nx_int = 0 86 while i < n_bins { 87 sum = sum + bins[i] 88 i = i + 1 89 } 90 let mean: nx_int = sum / n_bins 91 if mean <= 0 { return 0 } 92 var dev_sum: nx_int = 0 93 var j: nx_int = 0 94 while j < n_bins { 95 var d: nx_int = bins[j] - mean 96 if d < 0 { d = 0 - d } 97 dev_sum = dev_sum + d 98 j = j + 1 99 } 100 let mean_dev: nx_int = dev_sum / n_bins 101 let cv_q: nx_int = (mean_dev * q) / mean 102 return _aq_band_score(cv_q, q * 15 / 100, q * 4 / 10) 103} 104 105// ===== Axis 1: dynamic range ======================================= 106// ratio_q = rmax/mean is always >= Q (rmax >= mean by definition). 107// ratio_q = Q means no dynamic range; we want score 0 there. 108// Custom band: 109// ratio_q == Q -> 0 110// ratio_q in [Q, 2Q] -> 0..Q linear ramp 111// ratio_q in [2Q, 8Q] -> Q (full) 112// ratio_q > 8Q -> taper down 113func _aq_dynamic_range_q14(rms: *i64, n_frames: nx_int) -> nx_int { 114 if n_frames < 2 { return 0 } 115 let q: nx_int = NX_AQ_Q 116 var sum: nx_int = 0 117 var rmax: nx_int = 0 118 var i: nx_int = 0 119 while i < n_frames { 120 sum = sum + rms[i] 121 if rms[i] > rmax { rmax = rms[i] } 122 i = i + 1 123 } 124 let mean: nx_int = sum / n_frames 125 if mean <= 0 { return 0 } 126 let ratio_q: nx_int = (rmax * q) / mean 127 if ratio_q <= q { return 0 } 128 if ratio_q < 2 * q { return ratio_q - q } 129 if ratio_q <= 8 * q { return q } 130 let excess: nx_int = ratio_q - 8 * q 131 var s: nx_int = q - excess / 8 132 if s < 0 { s = 0 } 133 return s 134} 135 136// ===== Axis 2: spectral centroid ================================== 137func _aq_spectral_centroid_q14(bins: *i64, n_bins: nx_int) -> nx_int { 138 if n_bins < 2 { return 0 } 139 let q: nx_int = NX_AQ_Q 140 var weighted: nx_int = 0 141 var total: nx_int = 0 142 var i: nx_int = 0 143 while i < n_bins { 144 weighted = weighted + bins[i] * i 145 total = total + bins[i] 146 i = i + 1 147 } 148 if total <= 0 { return 0 } 149 let centroid_q: nx_int = (weighted * q) / (total * (n_bins - 1)) 150 return _aq_band_score(centroid_q, q * 3 / 10, q * 6 / 10) 151} 152 153// ===== Axis 3: source diversity ================================== 154func _aq_source_diversity_q14(n_sources: nx_int) -> nx_int { 155 let q: nx_int = NX_AQ_Q 156 if n_sources <= 0 { return 0 } 157 return _aq_band_score(n_sources * q / 8, q * 4 / 10, q) 158} 159 160// ===== Axis 4: temporal smoothness =============================== 161// Penalise variance == 0 (drone) OR mean abs second-difference too 162// high (glitchy). 163func _aq_temporal_smooth_q14(rms: *i64, n_frames: nx_int) -> nx_int { 164 if n_frames < 3 { return 0 } 165 let q: nx_int = NX_AQ_Q 166 // Check variance. 167 var sum: nx_int = 0 168 var i: nx_int = 0 169 while i < n_frames { 170 sum = sum + rms[i] 171 i = i + 1 172 } 173 let mean: nx_int = sum / n_frames 174 var dev_sum: nx_int = 0 175 var k: nx_int = 0 176 while k < n_frames { 177 var d: nx_int = rms[k] - mean 178 if d < 0 { d = 0 - d } 179 dev_sum = dev_sum + d 180 k = k + 1 181 } 182 let mean_dev: nx_int = dev_sum / n_frames 183 if mean_dev == 0 { return q / 5 } // drone -> low 184 185 // Mean abs second-difference. 186 var d2_sum: nx_int = 0 187 var n_d2: nx_int = 0 188 var m: nx_int = 1 189 while m < n_frames - 1 { 190 var d2: nx_int = rms[m + 1] - 2 * rms[m] + rms[m - 1] 191 if d2 < 0 { d2 = 0 - d2 } 192 d2_sum = d2_sum + d2 193 n_d2 = n_d2 + 1 194 m = m + 1 195 } 196 if n_d2 == 0 { return 0 } 197 let mean_d2: nx_int = d2_sum / n_d2 198 // Score: lower mean_d2 = smoother. Compare to mean (signal level). 199 if mean <= 0 { return 0 } 200 let smoothness_q: nx_int = (mean_d2 * q) / mean 201 // Target: smoothness_q in [0.1Q, 0.5Q] (some change but not glitchy). 202 return _aq_band_score(smoothness_q, q / 10, q / 2) 203} 204 205// ===== Public: audio scene grader ================================= 206func nx_audio_scene_grade( 207 spectral_bins: *i64, n_bins: nx_int, 208 rms_window: *i64, n_frames: nx_int, 209 n_sources: nx_int, 210 out_verdict: *i64 211) { 212 let fb: nx_int = _aq_freq_balance_q14(spectral_bins, n_bins) 213 let dr: nx_int = _aq_dynamic_range_q14(rms_window, n_frames) 214 let sc: nx_int = _aq_spectral_centroid_q14(spectral_bins, n_bins) 215 let sd: nx_int = _aq_source_diversity_q14(n_sources) 216 let ts: nx_int = _aq_temporal_smooth_q14(rms_window, n_frames) 217 nx_layer_verdict_init(out_verdict, NX_LAYER_KIND_AUDIO, 218 NX_AQ_AXIS_COUNT, NX_LAYER_REFINE_MORE_VARIETY) 219 out_verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_FREQ_BALANCE] = fb 220 out_verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_DYNAMIC_RANGE] = dr 221 out_verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_SPECTRAL_CENTROID] = sc 222 out_verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_SOURCE_DIVERSITY] = sd 223 out_verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_TEMPORAL_SMOOTH] = ts 224 nx_layer_verdict_finalize(out_verdict) 225} 226 227// ===== Self-test ==================================================== 228func main() -> i64 { 229 let q: nx_int = NX_AQ_Q 230 let verdict: *i64 = (sys_mmap(NX_LV_STRIDE * NX_SIZEOF_NX_INT)) as *i64 231 232 // T1: Balanced soundscape: 8 spectral bins with moderate spread, 233 // 16 rms frames with varied levels, 4 sources. 234 let bins: *i64 = (sys_mmap(8 * NX_SIZEOF_NX_INT)) as *i64 235 bins[0] = 200 // sub-bass 236 bins[1] = 250 237 bins[2] = 300 238 bins[3] = 320 239 bins[4] = 280 240 bins[5] = 220 241 bins[6] = 180 242 bins[7] = 100 // brilliance 243 let rms: *i64 = (sys_mmap(16 * NX_SIZEOF_NX_INT)) as *i64 244 var i: nx_int = 0 245 while i < 16 { 246 // Triangular wave for variation. 247 if i < 8 { rms[i] = 100 + i * 50 } 248 if i >= 8 { rms[i] = 100 + (16 - i) * 50 } 249 i = i + 1 250 } 251 nx_audio_scene_grade(bins, 8, rms, 16, 4, verdict) 252 // Should produce a defined grade. 253 if nx_lv_grade_is_valid(verdict[NX_LV_OFF_GRADE]) != 1 { 254 return __syscall(93, 1, 0, 0, 0, 0, 0) 255 } 256 if verdict[NX_LV_OFF_KIND] != NX_LAYER_KIND_AUDIO { 257 return __syscall(93, 2, 0, 0, 0, 0, 0) 258 } 259 260 // T2: All-flat spectral bins -> low frequency balance score. 261 var j: nx_int = 0 262 while j < 8 { bins[j] = 300; j = j + 1 } 263 nx_audio_scene_grade(bins, 8, rms, 16, 4, verdict) 264 if verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_FREQ_BALANCE] * 10 >= q * 5 { 265 return __syscall(93, 10, 0, 0, 0, 0, 0) 266 } 267 268 // T3: Constant RMS -> low dynamic range + temporal smoothness drone. 269 var k: nx_int = 0 270 while k < 16 { rms[k] = 200; k = k + 1 } 271 nx_audio_scene_grade(bins, 8, rms, 16, 4, verdict) 272 if verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_DYNAMIC_RANGE] * 10 >= q * 3 { 273 return __syscall(93, 20, 0, 0, 0, 0, 0) 274 } 275 276 // T4: 1 source -> low diversity score (< 0.4Q). 277 nx_audio_scene_grade(bins, 8, rms, 16, 1, verdict) 278 if verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_SOURCE_DIVERSITY] * 10 >= q * 4 { 279 return __syscall(93, 30, 0, 0, 0, 0, 0) 280 } 281 // 8 sources -> full Q diversity. 282 nx_audio_scene_grade(bins, 8, rms, 16, 8, verdict) 283 if verdict[NX_LV_OFF_AXIS_0 + NX_AQ_AXIS_SOURCE_DIVERSITY] != q { 284 return __syscall(93, 31, 0, 0, 0, 0, 0) 285 } 286 287 return 0 288}