code wiki / _hdl_build / nx_squeaker_design.nx

nx_squeaker_design.nx source

↩ module page · 138 lines · 7472 B

1// nx_squeaker_design.nx -- INVERT the toy model: given the sound you want and the dog that will bite 2// it, return a geometry somebody can mould. Rung R5, and the deliverable of the whole lane. 3// 4// The forward model (nx_squeaker) turns geometry into sound. A product needs the other direction. 5// The Helmholtz law is invertible in closed form, which is why the model was written in millimetres 6// and newtons rather than in convenient normalised units: 7// 8// f = (c / 2pi) * sqrt(A / (V * L)) -> A = (2*pi*f/c)^2 * V * L 9// 10// and the bite sets the shell compliance, because compressing the chamber is what moves the pitch: 11// 12// f_rest^2 / f_bitten^2 = V_bitten / V_rest = (V0 - dVdF * F) / V0 13// -> dVdF = V0 * (1 - (f_rest/f_bitten)^2) / F 14// 15// So: pick the chamber volume and neck length from what is mouldable, and the neck area and the shell 16// compliance FALL OUT of the target sound and the dog's jaw. Different dogs get different shells for 17// the same sound -- a terrier and a mastiff biting the same geometry do not produce the same pitch, and 18// this is the organ that accounts for that. 19// 20// SOLVED IS NOT VERIFIED. A closed-form solution can be arithmetically perfect and still describe an 21// object that does not work: the neck can come out too small to mould, the shell too soft to survive a 22// chew, the compression beyond what an elastomer will take. So every design is (1) solved, (2) checked 23// against manufacturing limits, and (3) RENDERED through the forward model and measured, with the 24// realised frequency reported next to the requested one. A design that is never rendered is a hope. 25// 26// HONESTY ABOUT THE PREY TARGETS. The target frequencies below are DESIGN TARGETS taken from published 27// ranges for each prey class. They are NOT fitted to measured recordings, because this lane has no 28// licensed reference corpus yet -- see the gap filed against it. Nothing here should be read as 29// "matches a real rabbit"; it means "lands in the band that published work puts rabbit distress in". 30// The distinction matters, and collapsing it is how a lane starts believing its own brochure. 31// 32// license_tier: ORIGINAL expect_exit: 0 33 34import "nx_squeaker.nx" 35import "nx_bioacoustic_bench.nx" 36 37const SQD_C: i64 = 343000 // speed of sound, mm/s 38const SQD_TWOPI6: i64 = 6283185 // 2*pi * 1e6 39 40// ---------------------------------------------------------------- manufacturing limits 41// Rule 11: these are the constraints a moulder and a safety standard impose, named and in one place. 42const SQD_A_MIN_X100: i64 = 200 // 2.00 mm^2 -- below this the neck will not mould or will clog 43const SQD_A_MAX_X100: i64 = 20000 // 200.00 mm^2 44const SQD_V_MIN: i64 = 500 // mm^3 45const SQD_V_MAX: i64 = 120000 // mm^3 -- fits inside a 65 mm ball with wall thickness 46const SQD_COMP_MAX_PM: i64 = 700 // per-mille of chamber volume an elastomer shell may give up 47 48// ---------------------------------------------------------------- design record 49const SQD_R_V0: i64 = 0 50const SQD_R_AN_X100: i64 = 1 51const SQD_R_LN_X100: i64 = 2 52const SQD_R_DVDF100: i64 = 3 53const SQD_R_F_REST: i64 = 4 // realised, from the geometry 54const SQD_R_F_BITE: i64 = 5 55const SQD_R_OK: i64 = 6 // 1 = manufacturable 56const SQD_R_WHY: i64 = 7 // rejection code when OK = 0 57const SQD_R_N: i64 = 8 58 59const SQD_WHY_OK: i64 = 0 60const SQD_WHY_A_SMALL: i64 = 1 61const SQD_WHY_A_BIG: i64 = 2 62const SQD_WHY_V: i64 = 3 63const SQD_WHY_COMP: i64 = 4 64const SQD_WHY_TARGET: i64 = 5 65 66// Neck area for a target rest frequency: A = (2*pi*f/c)^2 * V * L, returned x100. 67func sqd_area_for(f: i64, vol: i64, ln_x100: i64) -> i64 { 68 if f <= 0 { return 0 } 69 let k: i64 = (SQD_TWOPI6 * f) / SQD_C // (2*pi*f/c) * 1e6 70 // A_mm2 = k^2 * V * L_mm / 1e12 71 // A_x100 = k^2 * V * L_x100 / 1e12 (the two factors of 100 cancel) 72 // Staged as /1e5 then /1e7 to keep the intermediate inside i64 for large chambers. 73 // The divisors must multiply to 1e12; they were 1e5 * 1e8 = 1e13, which made every neck ten times 74 // too small and every realised pitch sqrt(10) = 3.16x too low -- uniformly, across every target, 75 // which is exactly what made it look like a modelling problem rather than a typo. 76 let num: i64 = (k * k) / 100000 77 return (num * vol * ln_x100) / 10000000 78} 79 80// Solve a full geometry. Returns 0 and fills rec; rec[SQD_R_OK] says whether it can be made. 81func sqd_solve(f_rest: i64, f_bitten: i64, bite_n: i64, vol: i64, ln_x100: i64, rec: *i64) -> i64 { 82 var i: i64 = 0 83 while i < SQD_R_N { rec[i] = 0; i = i + 1 } 84 rec[SQD_R_V0] = vol 85 rec[SQD_R_LN_X100] = ln_x100 86 rec[SQD_R_OK] = 0 87 if f_bitten <= f_rest { rec[SQD_R_WHY] = SQD_WHY_TARGET; return 0 } 88 if bite_n <= 0 { rec[SQD_R_WHY] = SQD_WHY_TARGET; return 0 } 89 if vol < SQD_V_MIN { rec[SQD_R_WHY] = SQD_WHY_V; return 0 } 90 if vol > SQD_V_MAX { rec[SQD_R_WHY] = SQD_WHY_V; return 0 } 91 92 let a: i64 = sqd_area_for(f_rest, vol, ln_x100) 93 rec[SQD_R_AN_X100] = a 94 if a < SQD_A_MIN_X100 { rec[SQD_R_WHY] = SQD_WHY_A_SMALL; return 0 } 95 if a > SQD_A_MAX_X100 { rec[SQD_R_WHY] = SQD_WHY_A_BIG; return 0 } 96 97 // dVdF = V0 * (1 - (f_rest/f_bitten)^2) / F 98 let r2: i64 = (f_rest * f_rest * 1000000) / (f_bitten * f_bitten) // (f_rest/f_bitten)^2 * 1e6 99 let frac: i64 = 1000000 - r2 // fraction of volume removed, 1e6 100 let dvdf100: i64 = (vol * frac) / (10000 * bite_n) // mm^3/N x100 101 rec[SQD_R_DVDF100] = dvdf100 102 // the shell must not be asked to give up more volume than an elastomer can 103 if (frac / 1000) > SQD_COMP_MAX_PM { rec[SQD_R_WHY] = SQD_WHY_COMP; return 0 } 104 if dvdf100 <= 0 { rec[SQD_R_WHY] = SQD_WHY_COMP; return 0 } 105 106 rec[SQD_R_OK] = 1 107 rec[SQD_R_WHY] = SQD_WHY_OK 108 return 0 109} 110 111// Load a solved design into forward-model parameters so it can actually be rendered. 112func sqd_to_params(rec: *i64, bite_n: i64, p: *i64) -> i64 { 113 sq_defaults(p) 114 p[SQ_P_V0] = rec[SQD_R_V0] 115 p[SQ_P_AN_X100] = rec[SQD_R_AN_X100] 116 p[SQ_P_LN_X100] = rec[SQD_R_LN_X100] 117 p[SQ_P_DVDF_X100] = rec[SQD_R_DVDF100] 118 p[SQ_P_BITE_N] = bite_n 119 return 0 120} 121 122// Print a spec card a moulder could work from, with the REALISED frequencies beside the requested ones. 123func sqd_card(name: *u8, f_rest: i64, f_bitten: i64, bite_n: i64, rec: *i64, p: *i64) -> i64 { 124 bb_w("SQUEAKER-SPEC target="); bb_w(name) 125 bb_w(" want_rest="); bb_n(f_rest); bb_w("Hz want_bitten="); bb_n(f_bitten); bb_w("Hz" as *u8) 126 bb_w(" bite="); bb_n(bite_n); bb_w("N" as *u8) 127 if rec[SQD_R_OK] == 0 { 128 bb_w(" NOT-MANUFACTURABLE why="); bb_n(rec[SQD_R_WHY]) 129 bb_w(" (1=neck too small 2=neck too large 3=volume out of range 4=shell too compliant 5=bad target)\n" as *u8) 130 return 0 131 } 132 bb_w("\n chamber="); bb_n(rec[SQD_R_V0]); bb_w("mm3" as *u8) 133 bb_w(" neck_area="); bb_n(rec[SQD_R_AN_X100] / 100); bb_w("."); bb_n(rec[SQD_R_AN_X100] % 100); bb_w("mm2" as *u8) 134 bb_w(" neck_len="); bb_n(rec[SQD_R_LN_X100] / 100); bb_w("mm" as *u8) 135 bb_w(" shell="); bb_n(rec[SQD_R_DVDF100] / 100); bb_w("."); bb_n(rec[SQD_R_DVDF100] % 100); bb_w("mm3/N\n" as *u8) 136 bb_w(" REALISED f_rest="); bb_n(sq_f_rest(p)); bb_w("Hz f_bitten="); bb_n(sq_f_bitten(p)); bb_w("Hz\n" as *u8) 137 return 0 138}