// SonicForge Studio SoundFont Player Service (function () { 'use strict'; const activeOscillators = {}; // Use the shared AudioContext from the main app (lazy init) let __gainNode = null; const getCtx = () => { if (typeof getAudioContext === 'function') { const ctx = getAudioContext(); if (!__gainNode) { __gainNode = ctx.createGain(); __gainNode.gain.value = 0.3; __gainNode.connect(ctx.destination); } return ctx; } if (!window.__sharedAudioCtx) { window.__sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (window.__sharedAudioCtx.state === 'suspended') { window.__sharedAudioCtx.resume(); } if (!__gainNode) { __gainNode = window.__sharedAudioCtx.createGain(); __gainNode.gain.value = 0.3; __gainNode.connect(window.__sharedAudioCtx.destination); } return window.__sharedAudioCtx; }; const SonicSF = { loadedFonts: {}, // Load SoundFont from URL → ArrayBuffer → store in memory loadSoundFont: async function (url) { if (this.loadedFonts[url]) return this.loadedFonts[url]; const resp = await fetch(url); if (!resp.ok) throw new Error('Failed to load SoundFont: ' + url); const buffer = await resp.arrayBuffer(); this.loadedFonts[url] = buffer; return buffer; }, playNote: function (note, velocity, durationMs, startTime, program, destinationNode) { const ctx = getCtx(); const freq = 440 * Math.pow(2, (note - 69) / 12); if (freq <= 0 || isNaN(freq)) return null; const osc = ctx.createOscillator(); const noteGain = ctx.createGain(); // Default settings let oscType = 'triangle'; let attackTime = 0.03; let decayTime = 0.1; let sustainLevel = 0.5; let releaseTime = 0.2; let volFactor = 0.25; const prog = program !== undefined ? parseInt(program) : 0; if (prog >= 0 && prog <= 7) { // Pianos oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; } else if (prog >= 8 && prog <= 15) { // Chromatic Perc oscType = 'sine'; decayTime = 0.1; sustainLevel = 0.0; releaseTime = 0.1; } else if (prog >= 16 && prog <= 23) { // Organs oscType = 'sine'; attackTime = 0.05; sustainLevel = 0.8; releaseTime = 0.1; } else if (prog >= 24 && prog <= 31) { // Guitars oscType = 'triangle'; decayTime = 0.4; sustainLevel = 0.2; releaseTime = 0.3; } else if (prog >= 32 && prog <= 39) { // Basses oscType = 'triangle'; attackTime = 0.02; decayTime = 0.2; sustainLevel = 0.6; releaseTime = 0.2; } else if (prog >= 40 && prog <= 47) { // Strings oscType = 'sawtooth'; attackTime = 0.15; sustainLevel = 0.8; releaseTime = 0.5; volFactor = 0.15; } else if (prog >= 48 && prog <= 55) { // Ensemble / Choir oscType = 'sawtooth'; attackTime = 0.2; sustainLevel = 0.8; releaseTime = 0.6; volFactor = 0.12; } else if (prog >= 56 && prog <= 63) { // Brass oscType = 'sawtooth'; attackTime = 0.08; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.15; } else if (prog >= 64 && prog <= 71) { // Reed oscType = 'square'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.2; volFactor = 0.15; } else if (prog >= 72 && prog <= 79) { // Pipe oscType = 'sine'; attackTime = 0.1; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.2; } else if (prog >= 80 && prog <= 119) { // Synth Lead/Pad/FX oscType = 'sawtooth'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.4; volFactor = 0.15; } osc.type = oscType; osc.frequency.value = freq; const startAt = startTime !== undefined ? startTime : ctx.currentTime; const durSec = durationMs / 1000; const vel = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8; const targetGain = vel * volFactor; // ADSR Envelope noteGain.gain.setValueAtTime(0, startAt); noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime); noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime); const releaseStart = startAt + Math.max(attackTime + decayTime, durSec); noteGain.gain.setValueAtTime(targetGain * sustainLevel, releaseStart); const rampEnd = Math.max(0.001, targetGain * sustainLevel * 0.01); noteGain.gain.exponentialRampToValueAtTime(rampEnd, releaseStart + releaseTime); osc.connect(noteGain); const dest = destinationNode || __gainNode || ctx.destination; noteGain.connect(dest); osc.start(startAt); const stopAt = releaseStart + releaseTime + 0.05; osc.stop(stopAt); const oscId = `${note}_${Date.now()}_${Math.random()}`; activeOscillators[oscId] = { osc, gain: noteGain }; // Clean up active oscillator reference after it stops setTimeout(() => { delete activeOscillators[oscId]; }, (stopAt - ctx.currentTime) * 1000 + 100); return osc; }, stopAll: function () { const ctx = getCtx(); Object.values(activeOscillators).forEach(entry => { try { if (entry.gain) { entry.gain.gain.cancelScheduledValues(ctx.currentTime); entry.gain.gain.setValueAtTime(entry.gain.gain.value || 1, ctx.currentTime); entry.gain.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.008); } if (entry.osc) entry.osc.stop(ctx.currentTime + 0.01); } catch (e) { } }); Object.keys(activeOscillators).forEach(k => delete activeOscillators[k]); }, // Save user SoundFont to IndexedDB via window.SonicStorage saveToIndexedDB: async function (name, arrayBuffer) { if (window.SonicStorage && window.SonicStorage.saveToIndexedDB) { await window.SonicStorage.saveToIndexedDB('soundfont_' + name, arrayBuffer); } }, // Load user SoundFont from IndexedDB loadFromIndexedDB: async function (name) { if (window.SonicStorage && window.SonicStorage.loadFromIndexedDB) { return await window.SonicStorage.loadFromIndexedDB('soundfont_' + name); } return null; } }; window.SonicSF = SonicSF; })();