/* * PianoPhysicsData.h * * Implementation-ready parameter values for piano physical modeling * Extracted from peer-reviewed acoustic research papers * * Sources: * - Chaigne et al., JASA 133(4), 2013 - Soundboard dynamics * - Hall & Askenfelt, JASA 83(4), 1988 - Hammer-string interaction * - Russell & Rossing, Acta Acustica 84, 1998 - Hammer nonlinearity * - Giordano, JASA 103(4), 1998 - Bridge impedance * - Conklin, JASA 99-100, 1996 - Piano design * - Fletcher, JASA 36(1), 1964 - String inharmonicity * * This data is scientific fact and not subject to copyright. */ #pragma once #include #include namespace PianoPhysics { //============================================================================== // MATERIAL PROPERTIES //============================================================================== // European Spruce (Picea excelsa) - Soundboard namespace Spruce { constexpr float density = 440.0f; // kg/m³ constexpr float E_longitudinal = 15.9e9f; // Pa (along grain) constexpr float E_radial = 0.69e9f; // Pa constexpr float E_tangential = 0.39e9f; // Pa constexpr float G_LR = 0.62e9f; // Pa (shear modulus) constexpr float G_LT = 0.77e9f; // Pa constexpr float G_RT = 0.0036e9f; // Pa constexpr float nu_LR = 0.44f; // Poisson's ratio constexpr float nu_LT = 0.38f; constexpr float nu_RT = 0.47f; } // Beech - Bridge material namespace Beech { constexpr float density = 674.0f; // kg/m³ constexpr float E_longitudinal = 14.0e9f; // Pa constexpr float E_radial = 2.28e9f; // Pa constexpr float E_tangential = 1.16e9f; // Pa } // Steel - Strings namespace Steel { constexpr float density = 7800.0f; // kg/m³ constexpr float youngsModulus = 210.0e9f; // Pa } // Copper - Bass string wrapping namespace Copper { constexpr float density = 8960.0f; // kg/m³ } //============================================================================== // SOUNDBOARD PARAMETERS //============================================================================== namespace Soundboard { // Modal damping (loss factor eta) constexpr float lossFactor_low = 0.007f; // < 300 Hz (0.7%) constexpr float lossFactor_mid = 0.012f; // 300-1000 Hz (1.2%) constexpr float lossFactor_high = 0.020f; // 1000-3000 Hz (2.0%) // Typical loss factor for good overall match constexpr float typicalLossFactor = 0.015f; // 1.5% // Waveguide transition frequency (ribs become waveguides above this) constexpr float waveguideTransitionHz = 1100.0f; // Mean rib spacing (Pleyel P131 measurements) constexpr float meanRibSpacing_m = 0.132f; // 13.2 cm // T60 values by frequency band (seconds) inline float getT60(float freqHz) { if (freqHz < 120.0f) return 1.8f - (freqHz - 60.0f) * 0.012f; if (freqHz < 500.0f) return 1.1f - (freqHz - 120.0f) * 0.002f; if (freqHz < 2000.0f) return 0.7f - (freqHz - 500.0f) * 0.0003f; return 0.25f - (freqHz - 2000.0f) * 0.00003f; } } //============================================================================== // STRING PARAMETERS (Broadwood Grand measurements) //============================================================================== struct StringData { int midiNote; float frequency; // Hz (equal temperament) float length; // m float coreDiameter; // mm float wrapDiameter; // mm (0 = unwrapped) float mass; // g (total vibrating mass) float tension; // N float strikingRatio; // hammer position / string length }; // Measured string parameters for C notes constexpr std::array stringDataC = {{ // MIDI, freq, length, core, wrap, mass, tension, strike { 24, 32.7f, 1.945f, 1.00f, 4.55f, 142.0f, 680.0f, 0.117f }, // C1 { 36, 65.4f, 1.425f, 1.00f, 3.60f, 52.0f, 710.0f, 0.102f }, // C2 { 48, 130.8f, 0.975f, 1.025f,1.75f, 21.0f, 740.0f, 0.109f }, // C3 { 60, 261.6f, 0.620f, 1.025f,0.0f, 15.0f, 770.0f, 0.125f }, // C4 { 72, 523.3f, 0.337f, 1.00f, 0.0f, 8.1f, 810.0f, 0.134f }, // C5 { 84, 1046.5f, 0.172f, 0.925f,0.0f, 3.9f, 680.0f, 0.151f }, // C6 { 96, 2093.0f, 0.089f, 0.850f,0.0f, 1.7f, 560.0f, 0.180f }, // C7 }}; //============================================================================== // INHARMONICITY //============================================================================== namespace Inharmonicity { // Measured B values at specific points // f_n = n * f1 * sqrt(1 + B * n²) constexpr float B_A0 = 0.00020f; // MIDI 21 constexpr float B_C2 = 0.00030f; // MIDI 36 (wound) constexpr float B_C3 = 0.00070f; // MIDI 48 constexpr float B_C4 = 0.0015f; // MIDI 60 constexpr float B_C5 = 0.0050f; // MIDI 72 constexpr float B_C6 = 0.018f; // MIDI 84 constexpr float B_C7 = 0.080f; // MIDI 96 constexpr float B_C8 = 0.40f; // MIDI 108 // Interpolation function for any MIDI note inline float getB(int midiNote) { // Bass wound strings (MIDI 21-52): slow exponential growth if (midiNote <= 52) { float norm = static_cast(midiNote - 21) / 31.0f; return 0.00018f * std::pow(10.0f, norm * 0.7f); } // Plain steel strings (MIDI 53-108): faster growth float norm = static_cast(midiNote - 53) / 55.0f; return 0.0008f * std::pow(10.0f, norm * 2.7f); } // Compute partial frequency with inharmonicity inline float partialFreq(float f1, int n, float B) { return static_cast(n) * f1 * std::sqrt(1.0f + B * n * n); } } //============================================================================== // HAMMER PARAMETERS //============================================================================== namespace Hammer { // Nonlinear felt model: F = K * C^p // Measured values for specific notes (Chaigne & Askenfelt) struct HammerData { int midiNote; float K; // Stiffness (SI units, varies with p) float p; // Nonlinearity exponent float mass_kg; // Mass in kg }; constexpr std::array measuredHammers = {{ { 36, 4.0e8f, 2.3f, 0.0110f }, // C2 { 60, 4.5e9f, 2.8f, 0.0092f }, // C4 { 84, 1.0e10f, 3.0f, 0.0052f }, // C6 }}; // Exponent p varies smoothly from bass to treble // Source: Russell & Rossing (1998) inline float getExponent(int midiNote) { // Bass: p ≈ 2.0-2.3 // Middle: p ≈ 2.6-3.0 // Treble: p ≈ 3.0-4.0 float norm = static_cast(midiNote - 21) / 87.0f; return 2.0f + norm * 2.0f; // 2.0 to 4.0 } // Stiffness coefficient K (log-interpolated) inline float getStiffness(int midiNote) { float norm = static_cast(midiNote - 21) / 87.0f; return 4.0e8f * std::pow(10.0f, norm * 1.4f); } // Hammer mass (kg) inline float getMass(int midiNote) { float norm = static_cast(midiNote - 21) / 87.0f; return 0.012f - norm * 0.008f; // 12g to 4g } // Hysteresis (reduces force on return stroke) // Source: Stulov inline float getHysteresis(int midiNote) { float norm = static_cast(midiNote - 21) / 87.0f; return 0.18f - norm * 0.10f; // 0.18 to 0.08 } // Striking position as fraction of string length inline float getStrikingRatio(int midiNote) { // Varies from ~1/8.5 in bass to ~1/5.5 in treble float norm = static_cast(midiNote - 21) / 87.0f; return 0.10f + norm * 0.08f; // 0.10 to 0.18 } // Contact duration scaling with velocity // τ ∝ F_max^((1-p)/(2p)) // At higher velocity, contact is shorter inline float getContactDurationScale(float velocity01, float p) { // velocity01: 0 = pp, 1 = ff // Returns multiplier relative to pp duration float Fscale = 0.2f + 0.8f * velocity01; // Force scales with velocity float exponent = (1.0f - p) / (2.0f * p); return std::pow(Fscale, exponent); } } //============================================================================== // BRIDGE IMPEDANCE //============================================================================== namespace Bridge { // Mean impedance values (kg/s) // Source: Wogram, Conklin, Giordano constexpr float Z_low = 1000.0f; // < 1 kHz constexpr float Z_mid = 700.0f; // 1-2 kHz constexpr float Z_high = 400.0f; // 2-5 kHz constexpr float Z_veryHigh = 250.0f; // > 5 kHz // Typical fluctuation around mean: ±12 dB // High-pass cutoff frequencies constexpr float bassBridgeHP_Hz = 70.0f; constexpr float trebleBridgeHP_Hz = 140.0f; // Bridge/soundboard coupling strength constexpr float bassCoupling = 0.06f; constexpr float trebleCoupling = 0.04f; // Bass/treble split point constexpr int splitMidi = 52; // E3 inline float getImpedance(float freqHz) { if (freqHz < 1000.0f) return Z_low; if (freqHz < 2000.0f) return Z_low - (freqHz - 1000.0f) * 0.0003f; if (freqHz < 5000.0f) return Z_mid - (freqHz - 2000.0f) * 0.0001f; return Z_veryHigh; } } //============================================================================== // DAMPER PARAMETERS //============================================================================== namespace Damper { // Damping time constant when damper engages (approximate) constexpr float engageTime_s = 0.015f; // 15 ms // Final damping coefficient (very high = quick stop) constexpr float maxDamping = 0.90f; // Bass strings have more mass, damp slower inline float getDampingRate(int midiNote) { float norm = static_cast(midiNote - 21) / 87.0f; return 0.85f + norm * 0.10f; // 0.85 to 0.95 } } //============================================================================== // OVERALL STRING DECAY (T60) //============================================================================== namespace StringDecay { // Frequency-dependent T60 for string modes // Higher partials decay faster inline float getT60(float freqHz, int midiNote) { // Base T60 varies with register float norm = static_cast(midiNote - 21) / 87.0f; float baseT60 = 20.0f - norm * 16.5f; // 20s bass to 3.5s treble // High frequencies decay faster (loss filter effect) float freqFactor = 1.0f / (1.0f + freqHz / 5000.0f); return baseT60 * freqFactor; } } //============================================================================== // VELOCITY MAPPING //============================================================================== namespace Velocity { // MIDI velocity to hammer velocity (m/s) // Typical range: 0.5 m/s (pp) to 5.5 m/s (fff) inline float midiToHammerVelocity(int midiVel) { float norm = static_cast(midiVel) / 127.0f; // Logarithmic-ish mapping for musical response return 0.5f + 5.0f * norm * norm; } // Brightness tilt with velocity // Louder = brighter (more high frequency content) inline float getBrightnessTilt(float hammerVel) { // 0 to 1 range return std::min(1.0f, hammerVel / 5.0f); } } } // namespace PianoPhysics