"""Exploratory mass-string/body/contact model. No recorded audio. SI units; uncalibrated dimensions/masses. Velocity Verlet at 88.2 kHz. String and body exchange equal/opposite bridge and fingerboard forces. This is a reduced model, not a validated acoustic double bass. """ import numpy as np from numba import njit from scipy.signal import resample_poly from scipy.io.wavfile import write import json from pathlib import Path @njit def simulate(pitch=38, amplitude=.002, contact=True, seconds=3., release_seconds=.003, grid_limit=160, note_off=-1.): sr=88200; dt=1/sr; f=440*2**((pitch-69)/12) open_pitch=43 if pitch>=43 else 38 if pitch>=38 else 33 if pitch>=33 else 28 tension=180.; speed=2*1.05*440*2**((open_pitch-69)/12); density=tension/speed**2 length=speed/(2*f); n=min(grid_limit,int(sr*.36/(2*f))) h=length/(n+1); mass=density*h; k=tension/h # Modal masses and frequencies are provisional, not measured responses. freq=np.array([68.,108.,175.,285.,460.,735.,1200.,2100.]) bm=np.array([.9,.65,.5,.35,.26,.18,.12,.08]) weight=np.array([.6,.8,.7,.5,.35,.24,.16,.1]) damping=np.array([18.,24.,32.,42.,58.,85.,130.,210.]) q=np.zeros(8);vbody=np.zeros(8);y=np.zeros(n);v=np.zeros(n) pluck_index=int(.76*(n+1))-1 pluck=(pluck_index+1)/(n+1) for j in range(n): x=(j+1)/(n+1);y[j]=amplitude*(x/pluck if x0: # Lossy, distributed compliant contact; no independent noise. cf=contact_density*h*penetration**1.5*max(0.,1+contact_loss*(neck_velocity-v[j])) force[j]+=cf;total_contact+=cf if half==0:contacts+=1 peak_penetration=max(peak_penetration,penetration) # Finite release of the force holding the string, not an audio envelope. time=(frame+half)*dt if time=0 and frame*dt>note_off: progress=min(1.,(frame*dt-note_off)/.022) stop_amount=.5-.5*np.cos(np.pi*progress) for j in range(n): v[j]*=np.exp(-dt*(.9+700.*stop_amount*stop_patch[j])) y[j]+=dt*v[j] for j in range(8):vbody[j]*=np.exp(-dt*damping[j]);q[j]+=dt*vbody[j] out[frame]=np.sum(vbody*weight) if frame%882==0: bridge=np.sum(q*weight) energy=.5*mass*np.sum(v*v)+.5*np.sum(bm*vbody*vbody)+.5*np.sum(bm*(2*np.pi*freq)**2*q*q) energy+=.5*neck_mass*neck_velocity**2+.5*neck_stiffness*(neck_position-np.sum(q*neck_weight))**2 prev=0. for j in range(n): energy+=.5*k*(y[j]-prev)**2;prev=y[j] x=(j+1)/(n+1);pen=max(0.,neck_position-(.0007+.0024*x)-y[j]) if contact and x<.72:energy+=contact_density*h*pen**2.5/2.5 energy+=.5*k*(bridge-y[-1])**2 energies[frame//882]=energy return out,energies,contacts,peak_penetration,n