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}