Modeling Piano Acoustics in Modalys
The primary goal of this phase was to create a reasonable physical model of the response of a grand piano bichord (two strings tuned to the same pitch, struck by a single hammer) using Modalys, IRCAM’s physical modeling software. This model served as a foundation during testing of the induction algorithm.
My research into piano acoustics focused on two aspects:
- string parameters (density, diameter, stiffness, etc.),
- the physics of “double decay”
Several resources proved indispensable to this:
Bensa, Julien, et al. “Parameter fitting for piano sound synthesis by physical modeling.” Journal of the Acoustical Society of America. vol. 118, no. 1, July 2005, pp 495-504.
Burred, Juan José. The Acoustics of the Piano. Translated by David Ripplinger, Professional Conservatory of Music Arturo Soria, Revised version, September 2004.
Delacour, John. “Formulæ for Piano String Calculation.” Delacour Pianos, 2006, pianomaker.co.uk/technical/string_formulae/.
Hall, Donald E. “The hammer and the string.” Five Lectures on the Acoustics of the Piano. Royal Swedish Academy of Music, 1990. www.speech.kth.se/music/5_lectures/hall/hall.html.
Weinreich, Gabriel. “The coupled motion of piano strings.” Five Lectures on the Acoustics of the Piano. Royal Swedish Academy of Music, 1990. www.speech.kth.se/music/5_lectures/weinreic/weinreic.html.
Implementation in Modalys
I hope to soon publish a paper providing details on how these the physical parameters were adapted to string creation in Modalys. Until then, I hope the following brief explanation, followed by example code, will suffice.
In creating this physical model, I worked primarily in ModaLisp, the Modalys textual environment based on Lisp. The use of this environment, which functions out of real-time, allowed me to build my initial model with minimal concern for processing power limitations. Once I had achieved a reasonable model I built an interface in Max/MSP to allow for real-time control. In doing so I used the ModaLisp code to generate a .mlys script file. A modalys~ object in Max then read that script file. In doing so I found that the initial model had to be simplified somewhat, particularly with regard to my experimentation with string double decay.
ModaLisp code for a physical model of a set of tuned piano strings attached to a bridge (also available as a download).
;;;-*-Mode: Lisp; Package: MODALYS -*- ;;;---------------------------------------------------------------------- ;;; Modalys, Piano String ;;; Physical model of piano bichord (two strings tuned to the same pitch, ;;; struck by a single hammer. ;;;---------------------------------------------------------------------- ;; string names: str1a, str1b, etc. ;; string pitch controller labels: STR1A-PITCH, STR1B-PITCH, etc. (labels in Max must be capitalized) ;; string on-off controller labels: STR1A-ON, STR1B-ON, etc. ;; string freq-loss controller labels: STR1A-DAMP, STR1B-DAMP, etc. ;; string access names: ;; str1a-hammer, str1a-bridge, str1a-listen, str1a-damper ;; hammer names: hammer1, hammer2 etc. ;; hammer access names: hammer1-hit, hammer1-mov, hammer2-hit, hammer2-mov, etc. ;; hammer controller labels: hammer1-move, hammer2-move, etc ;; (manipulated with a Max signal rather than a message, so case doesn't matter) ;;; **NOTE: ;;; 1) the *env* parameter several lines below determines the output - an audio file or mlys script ;;; 2) if generating a script, the file path must be reset to your own. This is controlled in the final "if" statement at the bottom (new) (set-precision 'float) (set-message-level 3) ;; output - nil=generate audio, t=generate mlys script (defparameter *env* nil) ;; set string parameters (setq *string-freqs* (list 261.62 329.628 391.995)) ; a major triad: C4 (middle C), E, G ;; note that the number of frequencies listed determines the number of bichords created (setq *detune-range* (list 0.5 3)); (min max) detune in cents - controls the variation in tuning between strings of the bichord (setf *hammer-loc-onstring* 0.7) ; between 0 and 1 (setf *listen-loc-onstring* 0.293) (setf *bridge-loc-onstring* 0.95) ;; make bridge (setf small-mass-ctl (make-controller 'dynamic 1 0 .0015 "small-mass")) ; must be low to allow the mass to move at same freq as string (setf large-mass-ctl (make-controller 'dynamic 1 0 100000 "large-mass")) (setf stiffness-ctl (make-controller 'dynamic 1 0 291000 "stiffness")) ; high stiffness allows more transfer to soundboard (setf freq-loss-ctl (make-controller 'dynamic 1 0 4 "freq-loss")) (setf const-loss-ctl (make-controller 'dynamic 1 0 13 "const-loss")) (setq bridge (make-object 'mono-two-mass (small-mass small-mass-ctl) ; mass 1 (large-mass large-mass-ctl) ; mass 0 (stiffness0 stiffness-ctl) (freq-loss0 freq-loss-ctl) (const-loss0 const-loss-ctl))) ; actual bridge objects exist in Modalys, however the 'mono-two-mass was selected as it is more efficient (setq bridge-string-access (make-access bridge (const 1) 'trans0)) ; small mass connected to strings (setq bridge-position-access (make-access bridge (const 0) 'trans0)) ; large mass locked in place (setq bridge-pos1 (make-controller 'access-position 1 bridge-string-access)) ; small mass (connected to strings) for graph (setq bridge-pos0 (make-controller 'access-position 1 bridge-position-access)); large mass locked in place (make-connection 'position bridge-position-access (const 0)) ; locks large mass to "soundboard" (setq bridge-rigidity (make-controller 'dynamic 1 0 '(.5) "bridge-rigidity")) ; sets amount of resonance (make-connection 'position bridge-string-access (const 0) bridge-rigidity) ; use this to lock bridge in place, so no resonance ;; build strings (defun name-string (string-num ab) (read-from-string (format nil "str~A~A" string-num ab))) (setf string-names ; creates a list of strings, grouped into bichords: ((str1a str1b) (str2a str2b) etc.) (loop for x from 1 to (length *string-freqs*) collect (list (name-string x 'a) (name-string x 'b)))) (defun detune (pitch) (let* ((min (float (first *detune-range*))) (max (second *detune-range*)) (range (- max min))) (midi-to-freq (+ (freq-to-midi pitch) (/ (+ min (if (= 0 range) 0 ; if min=max, just add min to freq (random range))) ; otherwise, pick a random # w/in range and add it 100))))) (defun bridge-adjust (string-freq) ; compensates for the change in string pitch caused by attachment to bridge (* string-freq *bridge-loc-onstring*)) (defun highest-mode (string-freq) (+ 1 (/ 22050.0 string-freq))) ;==================================================================== ; string creation formulas - based on measurements from a grand piano ; exponential scaling formula: (+ minout (* (expt (* (- n minin) (/ 1 (- maxin minin))) (exp curve)) (- maxout minout))))) (defun string-length (string-freq) (if (> string-freq 82.4) (realpart (/ (+ 47 (* (expt (* (- (freq-to-midi string-freq) 108) -0.01492537313433) 2.0137527) 1428)) 1000)) (/ (+ 1505 (* (- (freq-to-midi string-freq) 40) -78.6842105263158)) 1000))) ; result in mm, converted to meters ; linear scaling formula: (+ minout (* (- self minin) (/ (- maxout minout) (- maxin minin)))) ; provides a linear scaling from inputs 41-108 to outputs 0.0005625-0.000375 (radius in meters) (defun string-radius (string-freq) (/ (/ (+ 1.125 (* (- (freq-to-midi string-freq) 41) -0.00559701492537)) ; delivers diameter in mm 2) 1000)) ; converts to radius, then to meters ; inharmonicity used to compute youngs modulus (defun inharmonicity (string-freq) (if (> string-freq 110) (realpart (+ 3.761e-5 (* (expt (* (- string-freq 130.813) 0.00024690157047) 1.5372576) 0.009433391))) (realpart (+ 1e-16 (* (expt (* (- string-freq 27.5) 0.01212121212121) 54.59815) 3.761e-5))))) (defun youngs (string-freq) (/ (* (inharmonicity string-freq) 134400 (expt (string-length string-freq) 2)) (* 31.006276680299827 (expt (* 2 (string-radius string-freq)) 4)))) ; freq-loss and const-loss formulas done by ear... (defun freq-loss-adjust (string-freq) (realpart (+ 0.01 (* (sin (* (expt (* (- (freq-to-midi string-freq) 45) 0.01666666666667) 2.2255409) pi)) 0.03)))) (defun const-loss-adjust (string-freq) (realpart (+ 0.1 (* (expt (* (- string-freq 110) 0.00029325513196) 3.320117) 4.9)))) ;==================================================================== ; create single string, with a controller for freq-loss (used for damping in Max) (defun create-string (name string-freq freq-loss-ctl) (set name (make-object 'bi-string (modes (highest-mode string-freq)) (length (string-length string-freq)) (density 5000) ; I cheated with this - should be 7850 (radius (string-radius string-freq)) (young (youngs string-freq)) (freq-loss freq-loss-ctl) (const-loss (const-loss-adjust string-freq)) ))) (defun create-name-string (string-name type) (format nil "~A-~A" string-name type)) ; create both strings of a bichord (double strings for a single pitch), ; and controllers for damping and pitch adjustment (defun create-bichord (name-a name-b string-freq) (let* ((string-freq2 (detune string-freq)) (actual-string-freq (bridge-adjust string-freq)) ; adjust pitch for bridge location (actual-string-freq2 (bridge-adjust string-freq2)) (controller-name (read-from-string (create-name-string name-a 'pitch))) ; pitch adjustment in Max (controller-name2 (read-from-string (create-name-string name-b 'pitch))) (controller-string (create-name-string name-a 'pitch)) (controller-string2 (create-name-string name-b 'pitch)) (controller-name3 (read-from-string (create-name-string name-a 'damp))) ; imitates dampers by raising freq-loss (in Max) (controller-name4 (read-from-string (create-name-string name-b 'damp))) (controller-string3 (create-name-string name-a 'damp)) (controller-string4 (create-name-string name-b 'damp)) ) (list (setf controller-name3 (make-controller 'dynamic 1 -1 (freq-loss-adjust string-freq) controller-string3)) (setf controller-name4 (make-controller 'dynamic 1 -1 (freq-loss-adjust string-freq) controller-string4)) (create-string name-a string-freq controller-name3) ; create the strings... (create-string name-b string-freq2 controller-name4) (setf controller-name (make-controller 'dynamic 1 -1 (list actual-string-freq) controller-string)) (setf controller-name2 (make-controller 'dynamic 1 -1 (list actual-string-freq2) controller-string2)) (set-pitch (eval name-a) 'tension controller-name) ; ...and set their pitch (set-pitch (eval name-b) 'tension controller-name2) ))) ; create all the strings my applying "create-bichord" to string name list (mapcar #'(lambda (string-name pitch) (create-bichord (first string-name) (second string-name) pitch)) string-names *string-freqs*) ;; make hammers, accesses, and connections to movers (defun hammer-name (string-num) (read-from-string (format nil "hammer~A" string-num))) ; create a list of hammers: ((hammer1 1) (hammer2 2) etc.) (setf hammer-names (loop for x from 1 to (length *string-freqs*) collect (list (hammer-name x) x))) ; create all hammers, their accesses, and connect them to mover-controllers ; mover-controllers are either envelopes, if generating a file, or signal inputs is generating a script (mapcar #'(lambda (hammer-name-num) (let* ((hammer-name (first hammer-name-num)) (hammer-num (- (second hammer-name-num) 1)) (hammer-access-name1 (read-from-string (create-name-string hammer-name 'hit))) ; access for string strike (hammer-access-name2 (read-from-string (create-name-string hammer-name 'mov))) ; access for position controller (mover-name (create-name-string hammer-name 'move)) ) (list (set hammer-name (make-object 'mono-two-mass)) (set hammer-access-name1 (make-access (eval hammer-name) (const 1) 'trans0)) ; connected to strings (set hammer-access-name2 (make-access (eval hammer-name) (const 0) 'trans0)) ; connected to position controller (if *env* (make-connection 'position (eval hammer-access-name2) ; if generating a script, use an input signal to move hammer (make-controller 'signal 1 (make-point-input hammer-num (const 1)))) (make-connection 'position (eval hammer-access-name2) ; if generating an audio file, use envelope to move hammer (make-controller 'envelope 1 (list (list 0.00 .1) (list 0.025 -.001) (list 0.05 .1)))) ; this envelope controls the speed of the hammer. currently mf )))) hammer-names) ;; some different dynamics to plug into envelope, above: ;(list (list 0.00 .1) (list 0.0025 -.001) (list 0.005 .1)) ;; f ;(list (list 0.00 .1) (list 0.025 -.001) (list 0.05 .1)) ;; mf ;(list (list 0.00 .1) (list 0.045 -.001) (list 0.1 .1)) ;; mp ;(list (list 0.00 .1) (list .1 0.001) (list .17 .1)) ;; p ;; make all string accesses and connections to hammers (defun create-access-name (string-name access-type) (read-from-string (format nil "~A-~A" string-name access-type))) (defun create-accesses (string-name) (let* ((obj-name (eval string-name)) (hammer-access (create-access-name string-name 'hammer)) ; contact point with hammer (bridge-access (create-access-name string-name 'bridge)) ; contact point with bridge (listen-access (create-access-name string-name 'listen)) ; output point ) (set hammer-access (make-access obj-name *hammer-loc-onstring* 'trans0)) (set bridge-access (make-access obj-name *bridge-loc-onstring* 'trans0)) (set listen-access (make-access obj-name *listen-loc-onstring* 'trans0)) )) (mapcar #'(lambda (string-name hammer-name-num) (let* ((name-a (first string-name)) ; reads through list of strings, extracting individual string names (name-b (second string-name)) (hammer-name (first hammer-name-num)) ; extracts hammer names (hammer-acc-a (create-access-name name-a 'hammer)) ; create the string access, 3 per string (hammer-acc-b (create-access-name name-b 'hammer)) (listen-acc-a (create-access-name name-a 'listen)) (listen-acc-b (create-access-name name-b 'listen)) (bridge-acc-a (create-access-name name-a 'bridge)) (bridge-acc-b (create-access-name name-b 'bridge)) (hammer-hit-acc (read-from-string (create-name-string hammer-name 'hit))) ; just get the name of the access on the hammer (controller-name (read-from-string (create-name-string name-a 'on))) ; controller to mute string - turns off point-output (controller-string (create-name-string name-a 'on)) ) (list (create-accesses name-a) (create-accesses name-b) (make-connection 'felt (eval hammer-hit-acc) 0 (eval hammer-acc-a) -0.1 ; string init location is -0.1 (const .01) ;; thickness - so actual hammer init location is -0.01 (const 1e+11) ;; F0 (const 2.5) ;; alpha (const .6) ;; epsilon (const 15e-05) ;; tau ) (make-connection 'felt (eval hammer-hit-acc) 0 (eval hammer-acc-b) -0.1 (const .01) ;; thickness (const 1e+11) ;; F0 (const 2.5) ;; alpha (const .6) ;; epsilon (const 15e-05) ;; tau ) (make-connection 'adhere (eval bridge-acc-a) bridge-string-access) ; connect strings to bridge (make-connection 'adhere (eval bridge-acc-b) bridge-string-access) (setf controller-name (make-controller 'dynamic 1 -1 (list 1) controller-string)) ; string mute (make-point-output (eval listen-acc-a) 0 controller-name) ; outputs for strings (make-point-output (eval listen-acc-b) 1 controller-name) ))) string-names hammer-names) (setq str1a-pos0 (make-controller 'access-position 1 str1a-listen)) ; these 3 are for the graph (setq hammer1-hit-pos (make-controller 'access-position 1 hammer1-hit)) (setq hammer1-mov-pos (make-controller 'access-position 1 hammer1-mov)) (if *env* ; either make a script for Max -or- generate an audio file and a graph (save-script "/Users/mus/Documents/Data/-in\ progress/ircam-Project/final-file-summaries/5-piano/Piano-With-Hammers.mlys") (list (setq graph (make-plot)) (plot-value graph "string1" str1a-pos0) ; (plot-value graph "bridge0" bridge-pos0) ; this is locked in place, but graph it to check (plot-value graph "bridge1" bridge-pos1) ; connected to strings ; (plot-value graph "hammer0" hammer1-hit-pos) ; connected to strings ; (plot-value graph "hammer1" hammer1-mov-pos) ; connected to position controller (run 15) (plot graph "My Plotted Controller Data") (play)))
Continue on to Phase 2 – Induction