def strike_bar(profile, f1=261.6, dur=1.2, xe=0.4, xo=0.23, KH=3e10, alpha=2.5, MH=0.02, vH=1.0):
om, V, x = bar_modes(150, profile)
keep = om * f1 / om[0] < 2 * np.pi * 18000 # modes below 18 kHz
om, V = om[keep] * 2 * np.pi * f1 / om[0], V[:, keep]
sig = 0.6 + 2e-8 * om**2 # higher modes decay faster
phe, pho = V[int(xe * 150)], V[int(xo * 150)] # spreading / interpolation weights
k = 1 / fs; c1, c2 = 2 * np.exp(-sig * k) * np.cos(om * k), np.exp(-2 * sig * k)
q, qp = np.zeros(len(om)), np.zeros(len(om))
uH, uHp, out, F_hist = -1e-5, -1e-5 - k * vH, np.zeros(int(dur * fs)), [] # just short of contact, moving in
for n in range(len(out)):
F = KH * max(uH - phe @ q, 0.0)**alpha # contact force
q, qp = c1 * q - c2 * qp + k**2 * phe * F, q
uH, uHp = 2 * uH - uHp - k**2 * F / MH, uH
out[n] = pho @ q; F_hist.append(F)
return out, np.array(F_hist), om / (2 * np.pi)
uniform = lambda x: np.ones_like(x)
fig, axs = plt.subplots(2, 1, figsize=(4.9, 3.8), height_ratios=[1.6, 1])
for prof, col, lab, off in [(uniform, "0.5", "uniform bar", 0), (arch(d_mar), COL["bar"], "marimba bar", -40)]:
y, F, fm = strike_bar(prof) # struck slightly off center
Y = np.abs(np.fft.rfft(y * np.hanning(len(y)))); f = np.fft.rfftfreq(len(y), 1 / fs)
axs[0].plot(f / 261.6, 20 * np.log10(Y / Y.max() + 1e-9) + off, color=col, lw=0.9, label=lab)
if prof is uniform:
axs[1].plot(1e3 * np.arange(len(F)) / fs, F / F.max(), color="k", lw=1.2)
for r in [1, 2.76, 4, 9.35]:
axs[0].axvline(r, color="0.85", ls="--", lw=0.8, zorder=0)
axs[0].set_xlim(0, 12); axs[0].set_ylim(-120, 5); axs[0].set_yticks([])
axs[0].set_xticks([1, 2.76, 4, 5.4, 9.35]); axs[0].set_xlabel("frequency / $f_1$ ($f_1$ = C4)")
axs[0].set_title("same mallet, same pitch: mode 2 at 2.76 vs 4", fontsize=9); axs[0].legend(fontsize=7.5)
axs[1].set_xlim(0, 3); axs[1].set_xlabel("time (ms)"); axs[1].set_yticks([])
axs[1].set_title("contact force", fontsize=9)
plt.tight_layout()
plt.show()