1// ============================================ 2// SOUNDFONTS - Instrument Sound Generation 3// ============================================ 4// Handles different instrument soundfonts and their synthesis 5 6// Note: SF2Loader import is now dynamic (in initSF2Loader) to support FluidSynth 7// The old SF2Loader (sf2synth.js) is being replaced with FluidSynthLoader (js-synthesizer) 8 9import { registerSynthPresets, normalizeSynthPreset } from './synthPresets.js'; 10 11export class Soundfonts { 12 constructor(audioContext, masterGain) { 13 this.audioContext = audioContext; 14 this.masterGain = masterGain; 15 this.currentSoundfont = 'synthesizer'; 16 this.soundfontSettings = {}; 17 this.percussionMap = new Map(); 18 this.sf2Loader = null; // Will be initialized if SF2 support is available 19 this.initPercussionMap(); 20 this.initSoundfontPresets(); 21 this.initSF2Loader(); 22 } 23 24 // Initialize SF2 loader if available 25 initSF2Loader() { 26 try { 27 // Wait a bit for the library to load (it loads asynchronously) 28 setTimeout(() => { 29 // Check if FluidSynth (js-synthesizer) is available 30 // It might be exposed as Synthesizer or JSSynth.Synthesizer 31 const hasSynthesizer = (typeof Synthesizer !== 'undefined' && Synthesizer !== null) || 32 (typeof window.JSSynth !== 'undefined' && window.JSSynth && window.JSSynth.Synthesizer); 33 34 if (hasSynthesizer) { 35 // Import FluidSynthLoader dynamically 36 import('./fluidsynth-loader.js').then(module => { 37 this.sf2Loader = new module.FluidSynthLoader(this.audioContext, this.masterGain); 38 console.log('â SF2 loader initialized successfully (using FluidSynth/js-synthesizer)'); 39 40 // Dispatch event to notify that SF2 loader is ready 41 window.dispatchEvent(new CustomEvent('sf2LoaderReady')); 42 }).catch(error => { 43 console.error('Failed to load FluidSynth loader module:', error); 44 this.sf2Loader = null; 45 }); 46 } else { 47 console.warn('â FluidSynth (js-synthesizer) not available - SF2 soundfonts will be disabled'); 48 console.warn('Synthesizer type:', typeof Synthesizer, 'JSSynth type:', typeof window.JSSynth); 49 console.warn('Please ensure js-synthesizer library is loaded in daw.html'); 50 console.warn('Download from: https://github.com/jet2jet/js-synthesizer'); 51 console.warn('Place libfluidsynth-*.js and js-synthesizer.js in scripts/daw/libs/'); 52 console.warn('SF2 features will be disabled - only synthesized sounds will work'); 53 this.sf2Loader = null; // Explicitly set to null 54 } 55 }, 1500); // Wait 1.5 seconds for async library loading 56 } catch (error) { 57 console.error('Error initializing SF2 loader:', error); 58 this.sf2Loader = null; 59 } 60 } 61 62 // Initialize percussion mapping (MIDI note -> drum sound name) 63 initPercussionMap() { 64 // Standard General MIDI drum mapping 65 this.percussionMap.set(36, 'kick'); // C1 - Kick Drum 66 this.percussionMap.set(38, 'snare'); // D1 - Snare Drum 67 this.percussionMap.set(40, 'snare'); // E1 - Electric Snare 68 this.percussionMap.set(42, 'hihat'); // F#1 - Hi-Hat Closed 69 this.percussionMap.set(44, 'hihat'); // G#1 - Hi-Hat Pedal 70 this.percussionMap.set(46, 'hihat'); // A#1 - Hi-Hat Open 71 this.percussionMap.set(47, 'tom'); // B1 - Low-Mid Tom 72 this.percussionMap.set(48, 'tom'); // C2 - Hi-Mid Tom 73 this.percussionMap.set(50, 'tom'); // D2 - High Tom 74 this.percussionMap.set(49, 'crash'); // C#2 - Crash Cymbal 1 75 this.percussionMap.set(57, 'crash'); // A2 - Crash Cymbal 2 76 this.percussionMap.set(51, 'ride'); // D#2 - Ride Cymbal 1 77 this.percussionMap.set(59, 'ride'); // B2 - Ride Cymbal 2 78 this.percussionMap.set(52, 'china'); // E2 - Chinese Cymbal 79 this.percussionMap.set(55, 'splash'); // G2 - Splash Cymbal 80 this.percussionMap.set(56, 'cowbell'); // G#2 - Cowbell 81 this.percussionMap.set(54, 'tambourine'); // F#2 - Tambourine 82 this.percussionMap.set(39, 'clap'); // D#1 - Hand Clap 83 } 84 85 // Initialize soundfont presets with default settings 86 initSoundfontPresets() { 87 // Synthesizer (default / manual) 88 this.soundfontSettings.synthesizer = normalizeSynthPreset({ 89 type: 'synthesizer', 90 waveform: 'sine', 91 pulseWidth: 0.5, 92 attack: 0.01, 93 decay: 0.1, 94 sustain: 0.7, 95 release: 0.3 96 }); 97 98 registerSynthPresets(this.soundfontSettings); 99 100 // Brass instruments - made more distinct 101 this.soundfontSettings['brass-trumpet'] = { 102 type: 'brass', 103 instrument: 'trumpet', 104 waveform: 'sawtooth', // Bright, piercing 105 brightness: 0.9, // Very bright, piercing 106 attack: 0.02, // Quick attack 107 decay: 0.08, 108 sustain: 0.85, 109 release: 0.15, // Quick release 110 vibratoRate: 6, // Faster vibrato 111 vibratoDepth: 0.15 // More vibrato 112 }; 113 114 this.soundfontSettings['brass-french-horn'] = { 115 type: 'brass', 116 instrument: 'french-horn', 117 waveform: 'triangle', // Mellow, warm 118 brightness: 0.4, // Mellow, warm 119 attack: 0.12, // Slower attack 120 decay: 0.2, 121 sustain: 0.7, 122 release: 0.4, // Longer release 123 vibratoRate: 3.5, // Slower vibrato 124 vibratoDepth: 0.06 // Subtle vibrato 125 }; 126 127 this.soundfontSettings['brass-trombone'] = { 128 type: 'brass', 129 instrument: 'trombone', 130 waveform: 'square', // Medium brightness, distinct 131 brightness: 0.65, // Medium brightness 132 attack: 0.04, // Medium attack 133 decay: 0.1, 134 sustain: 0.82, 135 release: 0.22, // Medium release 136 vibratoRate: 4, // Medium vibrato rate 137 vibratoDepth: 0.12 // Medium vibrato depth 138 }; 139 140 this.soundfontSettings['brass-tuba'] = { 141 type: 'brass', 142 instrument: 'tuba', 143 waveform: 'sine', // Deep, dark 144 brightness: 0.2, // Very dark, deep 145 attack: 0.15, // Very slow attack 146 decay: 0.25, 147 sustain: 0.65, 148 release: 0.5, // Very long release 149 vibratoRate: 2.5, // Very slow vibrato 150 vibratoDepth: 0.03 // Very subtle vibrato 151 }; 152 153 // Woodwinds - made more distinct 154 this.soundfontSettings['woodwind-flute'] = { 155 type: 'woodwind', 156 instrument: 'flute', 157 waveform: 'sine', // Very clean, pure 158 breathiness: 0.2, // Very clean, pure 159 attack: 0.01, // Very quick attack 160 decay: 0.03, 161 sustain: 0.95, // Very high sustain 162 release: 0.1, // Quick release 163 vibratoRate: 7, // Fast, expressive vibrato 164 vibratoDepth: 0.18 // More vibrato 165 }; 166 167 this.soundfontSettings['woodwind-saxophone'] = { 168 type: 'woodwind', 169 instrument: 'saxophone', 170 waveform: 'sawtooth', // Rich, jazzy 171 breathiness: 0.7, // Very breathy, jazzy 172 attack: 0.05, // Slower attack 173 decay: 0.12, 174 sustain: 0.8, 175 release: 0.25, // Longer release 176 vibratoRate: 4.5, // Medium-slow vibrato 177 vibratoDepth: 0.2 // Strong vibrato 178 }; 179 180 this.soundfontSettings['woodwind-clarinet'] = { 181 type: 'woodwind', 182 instrument: 'clarinet', 183 waveform: 'triangle', // Smooth, mellow 184 breathiness: 0.5, // Medium breathiness 185 attack: 0.03, 186 decay: 0.08, 187 sustain: 0.88, 188 release: 0.15, 189 vibratoRate: 5.5, // Medium vibrato rate 190 vibratoDepth: 0.12 // Medium vibrato 191 }; 192 193 this.soundfontSettings['woodwind-oboe'] = { 194 type: 'woodwind', 195 instrument: 'oboe', 196 waveform: 'square', // Reedy, distinctive 197 breathiness: 0.4, // Less breathy, more reedy 198 attack: 0.06, // Slower attack 199 decay: 0.15, 200 sustain: 0.72, 201 release: 0.28, // Longer release 202 vibratoRate: 7.5, // Fast vibrato 203 vibratoDepth: 0.16 // Strong vibrato 204 }; 205 206 this.soundfontSettings['woodwind-bassoon'] = { 207 type: 'woodwind', 208 instrument: 'bassoon', 209 waveform: 'triangle', // Deep, warm 210 breathiness: 0.6, // More breathy, reedy 211 attack: 0.1, // Slow attack 212 decay: 0.18, 213 sustain: 0.65, 214 release: 0.35, // Long release 215 vibratoRate: 3.5, // Slow vibrato 216 vibratoDepth: 0.1 // Subtle vibrato 217 }; 218 219 // Strings - made more distinct 220 this.soundfontSettings['string-violin'] = { 221 type: 'string', 222 instrument: 'violin', 223 waveform: 'sawtooth', // Bright, rich harmonics 224 bowPressure: 0.85, // High pressure, bright 225 attack: 0.005, // Very quick attack 226 decay: 0.03, 227 sustain: 0.95, // Very high sustain 228 release: 0.2, // Quick release 229 vibratoRate: 7, // Fast vibrato 230 vibratoDepth: 0.25 // Strong vibrato 231 }; 232 233 this.soundfontSettings['string-viola'] = { 234 type: 'string', 235 instrument: 'viola', 236 waveform: 'sawtooth', // Rich, warm 237 bowPressure: 0.7, // Medium-high pressure 238 attack: 0.015, 239 decay: 0.05, 240 sustain: 0.88, 241 release: 0.28, // Medium release 242 vibratoRate: 6, // Medium-fast vibrato 243 vibratoDepth: 0.22 // Strong vibrato 244 }; 245 246 this.soundfontSettings['string-cello'] = { 247 type: 'string', 248 instrument: 'cello', 249 waveform: 'triangle', // Warm, mellow 250 bowPressure: 0.55, // Lower pressure, warmer 251 attack: 0.025, 252 decay: 0.1, 253 sustain: 0.82, 254 release: 0.4, // Longer release 255 vibratoRate: 4.5, // Slower vibrato 256 vibratoDepth: 0.18 // Medium vibrato 257 }; 258 259 this.soundfontSettings['string-double-bass'] = { 260 type: 'string', 261 instrument: 'double-bass', 262 waveform: 'sine', // Deep, warm 263 bowPressure: 0.4, // Low pressure, very warm 264 attack: 0.08, // Slow attack 265 decay: 0.15, 266 sustain: 0.7, 267 release: 0.5, // Very long release 268 vibratoRate: 3, // Slow vibrato 269 vibratoDepth: 0.12 // Subtle vibrato 270 }; 271 272 // Vocal choral - made more distinct
273 const vowels = ['ah', 'oh', 'oo', 'ee']; 274 const genders = ['male', 'female', 'mixed']; 275 const vowelFormants = { 276 'ah': 0.0, 277 'oh': -0.15, 278 'oo': -0.3, 279 'ee': 0.2 280 }; 281 282 genders.forEach(gender => { 283 vowels.forEach(vowel => { 284 const key = `vocal-${gender}-${vowel}`; 285 const baseFormant = gender === 'male' ? -0.25 : (gender === 'female' ? 0.25 : 0); 286 const vowelFormant = vowelFormants[vowel] || 0; 287 288 this.soundfontSettings[key] = { 289 type: 'vocal', 290 gender: gender, 291 vowel: vowel, 292 formantShift: baseFormant + vowelFormant, 293 attack: 0.06, 294 decay: 0.12, 295 sustain: 0.88, 296 release: 0.35, 297 vibratoRate: gender === 'male' ? 4.5 : (gender === 'female' ? 5.5 : 5), 298 vibratoDepth: gender === 'male' ? 0.08 : (gender === 'female' ? 0.12 : 0.1) 299 }; 300 }); 301 }); 302 303 // Percussion 304 this.soundfontSettings['percussion-drums'] = { 305 type: 'percussion', 306 kit: 'standard' 307 }; 308 } 309 310 // Set the current soundfont 311 setSoundfont(soundfontName) { 312 this.currentSoundfont = soundfontName; 313 return this.soundfontSettings[soundfontName] || this.soundfontSettings.synthesizer; 314 } 315 316 // Get current soundfont settings 317 getCurrentSettings() { 318 return this.soundfontSettings[this.currentSoundfont] || this.soundfontSettings.synthesizer; 319 } 320 321 // Update a setting for the current soundfont 322 updateSetting(key, value) { 323 const settings = this.getCurrentSettings(); 324 if (settings) { 325 settings[key] = value; 326 } 327 } 328 329 // Get percussion sound name for a MIDI note 330 getPercussionSound(note) { 331 return this.percussionMap.get(note) || 'kick'; 332 } 333 334 // Check if current soundfont is percussion 335 isPercussion() { 336 const settings = this.getCurrentSettings(); 337 return settings && settings.type === 'percussion'; 338 } 339 340 // Get all available SF2 soundfonts 341 getAllSF2Soundfonts() { 342 if (!this.sf2Loader) { 343 return []; 344 } 345 return this.sf2Loader.getAllSF2Soundfonts(); 346 } 347 348 // Check if a soundfont is an SF2 file 349 isSF2Soundfont(soundfontName) { 350 if (!this.sf2Loader) { 351 return false; 352 } 353 // Check if it's in the predefined list 354 const sf2List = this.sf2Loader.getAllSF2Soundfonts(); 355 if (sf2List.some(sf => sf.displayName === soundfontName)) { 356 return true; 357 } 358 // Also check if it's a custom loaded soundfont 359 if (this.sf2Loader.loadedSoundfonts && this.sf2Loader.loadedSoundfonts.has(soundfontName)) { 360 return true; 361 } 362 return false; 363 } 364 365 // Load an SF2 soundfont 366 async loadSF2Soundfont(soundfontName) { 367 if (!this.sf2Loader) { 368 console.warn('SF2 loader not available'); 369 return false; 370 } 371 try { 372 await this.sf2Loader.loadSoundfont(soundfontName); 373 return true; 374 } catch (error) { 375 console.error(`Error loading SF2 soundfont ${soundfontName}:`, error); 376 return false; 377 } 378 } 379 380 // Play a note using SF2 soundfont 381 playSF2Note(soundfontName, midiNote, velocity = 127) { 382 if (!this.sf2Loader) { 383 return null; 384 } 385 return this.sf2Loader.playNote(soundfontName, midiNote, velocity); 386 } 387 388 // Stop a note using SF2 soundfont 389 stopSF2Note(soundfontName, midiNote) { 390 if (!this.sf2Loader) { 391 return; 392 } 393 this.sf2Loader.stopNote(soundfontName, midiNote); 394 } 395 396 // Check if SF2 soundfont is loaded 397 isSF2Loaded(soundfontName) { 398 if (!this.sf2Loader) { 399 return false; 400 } 401 return this.sf2Loader.isLoaded(soundfontName); 402 } 403} 404
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.