diff --git a/CHANGELOG.md b/CHANGELOG.md index f02aa2f..69a354c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,19 @@ For installation and executable examples, see the [README](./README.md#getting-s - Replace the per-voice Butterworth IIR decimator (cutoff 0.30 output Fs) with cascaded polyphase linear-phase half-band FIR stages: eco 35 taps at 2×; standard 15 → 47 taps; high 11 → 15 → 47 taps. Substep calls only buffer their input; the cascade runs once per output frame. - Passband is flat to 0.40 output Fs (ripple below 0.001 dB for standard/high; formerly −11.5 dB at 0.35 Fs). Products folding below 0.40 Fs are rejected by at least 81 dB for standard/high and 63 dB for eco (standard formerly −52.3/−55.6 dB at 0.60/0.625 Fs). Between 0.50 and 0.60 Fs the transition band folds into 0.40–0.50 Fs, and far above Nyquist the former filter's bilinear zero was deeper; see [acoustic quality](./doc/acoustic-quality.md). -- Behavior change: output is delayed by a constant linear-phase 13.25 output frames for standard (8.5 eco, 13.875 high; about 0.28 ms at 48 kHz) instead of the IIR's frequency-dependent 2.7–3.3 frames. Each voice preallocates 158 state scalars instead of 8. +- Behavior change: output is delayed by a constant linear-phase 13.25 output frames for standard (8.5 eco, 13.875 high; about 0.28 ms at 48 kHz) instead of the IIR's frequency-dependent 2.7–3.3 frames. The decimator keeps 158 state scalars instead of 8 (per output channel since post-mix decimation, below). - Measured on this checkout's cloud container with `npm run benchmark` (median of four interleaved runs per side): standard 8 held voices 0.702 → 0.669 ms per 128-frame block, high 8 held voices 1.204 → 1.116 ms, eco unchanged within noise (0.434 → 0.435 ms). ### Band-limited square, saw and half-wave operators Square and saw operators now add two-sample polyBLEP residuals at their jumps, and half-wave operators add polyBLAMP residuals at their corners, sized from each operator's unmodulated phase step at the oversampled rate. In unit tests the folded-alias power of periodic frames drops by about 8–17 dB at the internal rate, before the decimator. Phase modulation makes the nominal step an approximation, so these shapes remain not alias-free. All non-sine periodic shapes now use the deterministic polynomial `fastSin` instead of `Math.sin`, and the shape helpers are split so the dispatcher stays inlinable. Measured on Node 22 with 16 held voices at 48 kHz (`standard`): square 1.74 → 0.98 ms/block, half 1.89 → 1.40 ms, saw 1.04 → 1.13 ms. Sine output and `periodicWaveform(theta, code)` without a step are unchanged apart from `fastSin` replacing `Math.sin` (~1e-10). Square/saw/half voices render differently near their edges. These timings are one Node process on one machine, not a browser AudioWorklet or device measurement. +### Post-mix decimation + +- Voices are now panned and summed to left/right at the oversampled rate, and one shared decimator per output channel runs after the mix, instead of one decimator per voice. A centered (dual-mono) mix filters once until the channel inputs differ. Measured against per-voice half-band FIR decimation with a 128-frame `Synth.render` loop at 48 kHz (median of three interleaved runs, one Node 22 process on one cloud x64 host): `standard` 8 voices 0.677 → 0.611 ms/block (−10%), 32 voices 2.772 → 2.477 ms (−11%); `high` 8 voices 1.115 → 0.996 ms (−11%), 32 voices 4.626 → 4.154 ms (−10%); `eco` 32 voices unchanged; a single voice costs about 0.01 ms/block more (mix-buffer overhead). Against the earlier per-voice Butterworth decimator the gains were 9–19%. +- Behavior change: note gain, expression, pan and steal fades now apply before the decimator, so a hard gain step is band-limited by it and rings through the half-band FIR (at most 26 output frames) instead of cutting instantly at the output rate. Ramps are within s·p·Σk|h[k]|/4 of the former output-rate product. +- Voices retire on their envelope end (all operator releases finished) and report `ended` without waiting for filter history; the shared decimator drains the tail; once nothing is sounding, history below the −96 dB floor snaps to exact zero. + ## v1.11.1 Package version 1.11.1, a GitHub release (not on npm). Performance patch over v1.11; no API, voice-format (still v7) or protocol changes. Output differs from v1.11 by about 1e-10 for sine operators. Security: scoped static review of the `src/core` change found no evidence-backed issue. diff --git a/README.md b/README.md index 9215dfb..7499c7c 100644 --- a/README.md +++ b/README.md @@ -502,7 +502,7 @@ The v1.10 checkout-only benchmark also reports scoped capacity candidates across `sound-quality` covers all three profiles and passed 2,505 lifecycle/headroom/determinism matrix cases plus 24 live-control cases in the recorded v1.8 verification. It also checks independent passband, THD, folded aliases and streaming references; these are controlled numerical gates, not arbitrary-FM or listening certification. `voice-quality` evaluates the curated preset bank and original synthetic DX7 recipes over register/velocity cells. Peak/RMS are raw dBFS, not LUFS or perceived loudness; host trims do not rewrite synthesis levels. See [acoustic-quality evidence](./doc/acoustic-quality.md#coverage-and-evidence) and [voice-quality notes](./doc/voice-quality.md). -Independent references cover Bessel-series high-index PM, contractive delayed feedback, conservative full-level feedback energy, and nested four-operator chain/branched/multicarrier synthesis. The per-voice 4× decimator is a polyphase linear-phase half-band FIR cascade, flat to 0.40 output Fs with at least 81 dB rejection of products that fold below that edge. Two 120-second streaming cases exercise held AM/PM and glide retargets. See [acoustic reference mathematics and limits](./doc/acoustic-quality.md); none certify arbitrary alias-free FM, perceptual quality or hardware fidelity. +Independent references cover Bessel-series high-index PM, contractive delayed feedback, conservative full-level feedback energy, and nested four-operator chain/branched/multicarrier synthesis. The shared post-mix 4× decimator is a polyphase linear-phase half-band FIR cascade, flat to 0.40 output Fs with at least 81 dB rejection of products that fold below that edge. Two 120-second streaming cases exercise held AM/PM and glide retargets. See [acoustic reference mathematics and limits](./doc/acoustic-quality.md); none certify arbitrary alias-free FM, perceptual quality or hardware fidelity. `browser-stress` exercises real AudioWorklet output, dense admissions/steals, controls, late rejection and queue cleanup under bounded main-thread contention. Timer gaps and diagnostic round trips are observations, **not** worklet CPU/GC/underrun/glitch measurements. Periodic analyser samples cannot prove uninterrupted audio. The Vite smoke additionally verifies an installed current tarball, non-root production deployment, retained license, CSP and negative asset/MIME cases. Run these separately from deadline benchmarks. diff --git a/dist/core/synth.js b/dist/core/synth.js index 5679362..8528688 100644 --- a/dist/core/synth.js +++ b/dist/core/synth.js @@ -1,2 +1,2 @@ -import{normalizeTuning as e,tuningFrequency as t}from"./tuning.js";import{lfoValue as r}from"./lfo.js";import{DECIMATOR_STATE_SIZE as a,createDecimatorCoefficients as s,decimateSample as o,decimatorPeak as i,qualityOversample as n}from"./decimator.js";const l=Object.getPrototypeOf(Float32Array.prototype),c=Object.getOwnPropertyDescriptor(l,"length").get,p=Object.getOwnPropertyDescriptor(l,Symbol.toStringTag).get,m=Float32Array.prototype.fill;class f extends Error{constructor(){super("All active voices have higher voicePriority"),this.name="VoiceAdmissionError"}}function h(e){if("number"!=typeof e||!Number.isInteger(e)||e<1||e>32)throw new RangeError("maxVoices must be an integer in 1..32");return e}function u(e){if("number"!=typeof e||!Number.isInteger(e)||e<0||e>127)throw new RangeError("voicePriority must be an integer in 0..127");return e}const d=Object.freeze({pitch:[-48,48],glide:[0,10],expression:[0,1],pan:[-1,1],modulation:[0,2],ramp:[0,10],feedback:[0,7],lfoRate:[0,20],amDepth:[0,1],pmDepth:[0,1200],gain:[0,1]});function y(e){if(!e||"object"!=typeof e||Array.isArray(e)||Object.getPrototypeOf(e)!==Object.prototype&&null!==Object.getPrototypeOf(e))throw new TypeError("controls must be a plain object");const t=Reflect.ownKeys(e);if(0===t.length)throw new TypeError("controls must not be empty");const r={};for(const a of t){if("string"!=typeof a||!["operatorLevels","operatorRatios","operatorFrequencies","operatorADSR"].includes(a)&&!Object.hasOwn(d,a))throw new TypeError("controls has an unknown field");const t=Object.getOwnPropertyDescriptor(e,a);if(!Object.hasOwn(t,"value"))throw new TypeError(`controls.${a} must be data`);const s=t.value;if("operatorLevels"===a||"operatorRatios"===a||"operatorFrequencies"===a||"operatorADSR"===a){if(!Array.isArray(s)||4!==s.length||5!==Reflect.ownKeys(s).length||Object.getPrototypeOf(s)!==Array.prototype)throw new TypeError(`${a} must be a dense four-element tuple`);const e=[];for(let t=0;t<4;t++){const r=Object.getOwnPropertyDescriptor(s,String(t));if(!r||!Object.hasOwn(r,"value"))throw new TypeError(`${a} must contain data`);const o=r.value;if("operatorADSR"===a){if(!o||"object"!=typeof o||Array.isArray(o)||Object.getPrototypeOf(o)!==Object.prototype&&null!==Object.getPrototypeOf(o)||4!==Reflect.ownKeys(o).length)throw new TypeError("operatorADSR requires full plain ADSR objects");const t={a:0,d:0,s:0,r:0};for(const e of["a","d","s","r"]){const r=Object.getOwnPropertyDescriptor(o,e);if(!r||!Object.hasOwn(r,"value"))throw new TypeError("operatorADSR must contain data");const a=r.value;if("number"!=typeof a||!Number.isFinite(a)||a<0||a>("s"===e?1:10))throw new RangeError("operatorADSR has an out-of-range value");t[e]=a}e.push(Object.freeze(t))}else{const t="operatorRatios"===a?.125:"operatorFrequencies"===a?1:0,r="operatorRatios"===a?32:"operatorFrequencies"===a?2e4:2;if(("operatorFrequencies"!==a||null!==o)&&("number"!=typeof o||!Number.isFinite(o)||or))throw new RangeError(`${a} must be finite and in ${t}..${r}`);e.push(o)}}Object.defineProperty(r,a,{value:Object.freeze(e),enumerable:!0});continue}const o=a,[i,n]=d[o];if("number"!=typeof s||!Number.isFinite(s)||sn)throw new RangeError(`controls.${a} must be finite and in ${i}..${n}`);r[o]=s}if(Object.hasOwn(r,"glide")&&!Object.hasOwn(r,"pitch"))throw new TypeError("glide requires pitch");if(Object.hasOwn(r,"ramp")&&!Reflect.ownKeys(r).some(e=>"ramp"!==e&&"pitch"!==e&&"glide"!==e&&"operatorADSR"!==e))throw new TypeError("ramp requires a rampable control");return Object.freeze(r)}import{ALGORITHMS as F}from"./algorithms.js";import{FLOOR_DB as g}from"./envelope.js";import{TAU as v,waveformCode as b,periodicWaveform as w,fastSin as T,NOISE_SEED as S,advanceNoise as R}from"./operator.js";import{preparedVoiceValue as x}from"../voices/normalize.js";import{normalizeVoice as A,prepareVoice as q}from"../voices/normalize.js";const D=.7,O=1/v,E=10**(g/20),j=Math.LN10/20,I=Object.freeze({velocity:1,pan:0,voicePriority:0}),V=e({}),M=["feedback","lfoRate","amDepth","pmDepth","gain"];function G(e){return e<=1?.5*e*2**-6*Math.PI:.5*2**(e-7)*Math.PI}function P(e,t,r=0){return Math.min(10,e*2**(-r*(t-60)/12))}function k(e){if("number"!=typeof e||!Number.isFinite(e)||e<0||e>1)throw new RangeError("mixGain must be finite and in 0..1")}function N(e){if(!e||"object"!=typeof e||Array.isArray(e)||Object.getPrototypeOf(e)!==Object.prototype&&null!==Object.getPrototypeOf(e))throw new TypeError("options must be a plain object");const t={};for(const r of Reflect.ownKeys(e)){if("mixGain"!==r&&"tuning"!==r&&"stealing"!==r&&"quality"!==r)throw new TypeError("options has an unknown field");const a=Object.getOwnPropertyDescriptor(e,r);if(!Object.hasOwn(a,"value"))throw new TypeError(`options.${r} must be data`);if("mixGain"===r)k(a.value),t.mixGain=a.value;else if("quality"===r)n(a.value),t.quality=a.value;else if("stealing"===r){if("oldest"!==a.value&&"release-first"!==a.value&&"quietest"!==a.value)throw new TypeError("stealing must be oldest, release-first or quietest");t.stealing=a.value}else t.tuning=a.value}return t}function L(e,t){return 0===e.pitchFrames||t>=e.pitchStart+e.pitchFrames?e.pitchTarget:e.pitchFrom+(e.pitchTarget-e.pitchFrom)*(t-e.pitchStart)/e.pitchFrames}function $(e,t,r,a,s){return 0===a||s>=r+a?t:e+(t-e)*(s-r)/a}function K(e,t){const r=e.voice.pitchEnvelope;return r?e.releaseTime>=0?0===r.r||t>=e.releaseTime+r.r?r.final:e.pitchRelease+(r.final-e.pitchRelease)*(t-e.releaseTime)/r.r:r.a>0&&t0&&t=e.expressionStart+e.expressionFrames&&(e.controlRamps&=-2)),2&t&&(e.pan=$(e.panFrom,e.panTarget,e.panStart,e.panFrames,r),r>=e.panStart+e.panFrames&&(e.controlRamps&=-3)),3&t&&U(e),4&t&&(e.modulation=$(e.modulationFrom,e.modulationTarget,e.modulationStart,e.modulationFrames,r),r>=e.modulationStart+e.modulationFrames&&(e.controlRamps&=-5),z(e)),8&t){for(let t=0;t<4;t++)e.operatorLevels[t]=$(e.operatorFrom[t],e.operatorTargets[t],e.operatorStart,e.operatorFrames,r);r>=e.operatorStart+e.operatorFrames&&(e.controlRamps&=-9)}if(16&t){let t=!1,a=!1;for(let s=0;s=e.ratioStart+e.ratioFrames&&(e.controlRamps&=-33)}}function U(e){const t=e.velocity*e.expression,r=(e.pan+1)*Math.PI/4;e.leftGain=1===e.pan?0:0===e.pan?t:t*Math.SQRT2*Math.cos(r),e.rightGain=-1===e.pan?0:0===e.pan?t:t*Math.SQRT2*Math.sin(r)}function Q(e,t){const r=e[t];for(let r=t+1;r0&&e0&&e=0){const a=e.releaseTimes[r],s=e.releaseStarts[r];return 0===a||t>=s+a?g:e.releaseDb[r]+(g-e.releaseDb[r])*((t-s)/a)}if(!e.envelopeEdited[r])return _(t,e.attackTimes[r],e.decayTimes[r],e.sustainDb[r]);const a=t-e.envelopeStart[r],s=e.attackTimes[r],o=e.decayTimes[r];if(s>0&&a0?0:e.envelopeFromDb[r];return o>0&&a=0){if(0===a||t>=e.releaseTime+a)return 0;const s=e.releaseDb[r]+(g-e.releaseDb[r])*((t-e.releaseTime)/a);return s<=g?0:Math.exp(s*j)}if(t>=e.attackTimes[r]+e.decayTimes[r])return e.sustainGain[r];if(0===t&&e.attackTimes[r]>0)return 0;const s=_(t,e.attackTimes[r],e.decayTimes[r],e.sustainDb[r]);return s<=g?0:Math.exp(s*j)}function H(e,t){const r=e.elapsed/t;let a=0;for(let t=0;t192e3)throw new RangeError("sampleRate must be finite and in 8000..192000");h(r);const i=N(o);this.quality=i.quality??"standard",this.oversample=n(this.quality),this.mixGain=i.mixGain??1,this.tuning=void 0===i.tuning?V:e(i.tuning),this.stealing=i.stealing??"oldest",this.sampleRate=t,this.maxVoices=r,this.voices=[],this.fades=[],this.freeVoices=Array.from({length:r+8+1},()=>{return e=this.oversample,{id:0,sequence:0,note:0,voicePriority:0,voice:null,graph:F[0],inputIndices:new Int8Array(8),carrierIndices:new Uint8Array(4),baseIncrements:new Float64Array(4),increments:new Float64Array(4),steps:new Float64Array(4),sustainDb:new Float64Array(4),sustainGain:new Float64Array(4),attackStep:new Float64Array(4),decayStep:new Float64Array(4),releaseStep:new Float64Array(4),attackTimes:new Float64Array(4),decayTimes:new Float64Array(4),releaseTimes:new Float64Array(4),operatorLevels:new Float64Array(4),operatorFrom:new Float64Array(4),operatorTargets:new Float64Array(4),operatorStart:0,operatorFrames:0,gains:new Float64Array(4*e),levels:new Float64Array(4),leftGain:0,rightGain:0,lastSample:0,fadeRemaining:0,lastFadeGain:1,carrierGain:0,phases:new Float64Array(4),values:new Float64Array(4),filters:new Float64Array(a),releaseDb:new Float64Array(4),previous:0,older:0,pitchRelease:0,feedbackScale:0,elapsed:0,releaseTime:-1,releaseEnd:-1,velocity:1,expression:1,pan:0,expressionFrom:1,expressionTarget:1,expressionStart:0,expressionFrames:0,panFrom:0,panTarget:0,panStart:0,panFrames:0,modulation:1,modulationFrom:1,modulationTarget:1,modulationStart:0,modulationFrames:0,controlRamps:0,modIndex:0,amDepth:0,pmDepth:0,pitchFrom:0,pitchTarget:0,pitchStart:0,pitchFrames:0,scalarValues:new Float64Array(M.length),scalarFrom:new Float64Array(M.length),scalarTargets:new Float64Array(M.length),scalarStarts:new Float64Array(M.length),scalarFrames:new Float64Array(M.length),ratios:new Float64Array(4),ratioFrom:new Float64Array(4),ratioTargets:new Float64Array(4),ratioStart:0,ratioFrames:0,frequencies:new Float64Array(4),frequencyFrom:new Float64Array(4),frequencyTargets:new Float64Array(4),frequencyStart:0,frequencyFrames:0,envelopeStart:new Float64Array(4),envelopeFromDb:new Float64Array(4),envelopeEdited:new Uint8Array(4),releaseStarts:new Float64Array(4),lfoTurns:0,lfoLive:!1,tunedFrequency:0,frequencyScales:new Float64Array(4),amGains:new Float64Array(4),pmFactors:new Float64Array(4),waveforms:new Uint8Array(4),noiseStates:new Uint32Array(4),noiseTurns:new Float64Array(4),noiseSteps:new Float64Array(4)};var e}),this.terminalIds=new Float64Array(r),this.terminalErrors=new Uint8Array(r),this.rendering=!1,this.fadeFrames=Math.ceil(.005*t),this.spillLeft=0,this.spillRight=0,this.spillRemaining=0,this.lastStolenId=null,this.onVoiceEnded=null,this.currentFrame=0,this.errorCount=0,this.nextId=1,this.subTimes=Float64Array.from({length:this.oversample},(e,r)=>r/(t*this.oversample)),this.sequence=0,this.decimatorCoefficients=s(t,this.quality)}setMixGain(e){k(e),this.mixGain=e}setTuning(t){const r=e(t);this.tuning=r;for(let e=0;ee)continue;const o=t<0?void 0:this.voices[t];let i=!o||s.voicePriority=0;e!==o.releaseTime>=0&&(i=e)}else"quietest"===this.stealing&&n!==r&&(i=n1)throw new RangeError(`${r} must be finite and in ${e}..1`);t[r]=s}}return t}(s),l=x(e);if("number"!=typeof r||!Number.isFinite(r)||r<0||r>127)throw new RangeError("note must be finite and in 0..127");if(void 0===a&&this.nextId>Number.MAX_SAFE_INTEGER)throw new RangeError("Note ID space exhausted");if(void 0!==a&&(!Number.isSafeInteger(a)||a<=0))throw new RangeError("id must be a positive safe integer");for(const e of this.voices)if(e.id===a)throw new RangeError("id already active");const c=this.voices.length>=this.maxVoices?this.stealingIndex(n):-1;if(this.voices.length>=this.maxVoices&&c<0)throw new f;void 0===a&&(a=this.nextId++),a>=this.nextId&&(this.nextId=a+1);const p=this.freeVoices.pop(),{baseIncrements:m,increments:h,sustainDb:d,sustainGain:y,steps:w,attackStep:T,decayStep:R,levels:A}=p,q=t(r,this.tuning),O=this.sampleRate*this.oversample;for(let e=0;e<4;e++){const t=l.ops[e];p.ratios[e]=p.ratioFrom[e]=p.ratioTargets[e]=t.ratio,p.frequencyTargets[e]=t.frequency??0,p.frequencies[e]=p.frequencyFrom[e]=t.frequency??q*t.ratio;const a=t.keyScale,s=a?Math.abs(r-a.breakpoint)/12*(r=this.maxVoices){const e=Q(this.voices,c);if(this.lastStolenId=e.id,8===this.fades.length){const e=Q(this.fades,0),t=e.lastFadeGain;this.spillLeft+=e.lastSample*e.leftGain*t,this.spillRight+=e.lastSample*e.rightGain*t,this.spillRemaining=this.fadeFrames,this.freeVoices.push(e)}e.fadeRemaining=this.fadeFrames,e.lastFadeGain=1,this.fades.push(e)}return this.voices.push(p),null!==this.lastStolenId&&this.ended(this.lastStolenId,"stolen"),a}noteOff(e){let t;for(const r of this.voices)if(r.id===e){t=r;break}return!(!t||t.releaseTime>=0||(this.releaseVoice(t),0))}releaseVoice(e){const t=e.elapsed/this.sampleRate;e.pitchRelease=K(e,t);let r=0;for(let a=0;a<4;a++){e.releaseDb[a]=X(e,t,a),e.releaseStarts[a]=t;const s=e.releaseTimes[a];r=Math.max(r,s),e.releaseStep[a]=0===s?0:10**((g-e.releaseDb[a])/(20*s*this.sampleRate*this.oversample))}e.releaseTime=t,e.releaseEnd=t+r}allNotesOff(){for(let e=0;e0;)this.freeVoices.push(this.fades.pop());this.spillLeft=this.spillRight=this.spillRemaining=0,this.lastStolenId=null,this.cancelVoices()}cancelVoices(){if(0===this.voices.length)return;const e=this.voices.pop(),t=e.id;this.freeVoices.push(e),this.cancelVoices(),this.ended(t,"cancelled")}updateNote(e,t){const r=y(t);let a;for(const t of this.voices)if(t.id===e){a=t;break}if(!a)return!1;0!==a.controlRamps&&C(a),void 0!==r.pitch&&(a.pitchFrom=L(a,a.elapsed),a.pitchTarget=r.pitch,a.pitchStart=a.elapsed,a.pitchFrames=(r.glide??0)*this.sampleRate);const s=(r.ramp??0)*this.sampleRate;if(void 0!==r.lfoRate&&!a.lfoLive){const e=a.voice.lfo,t="global"===e.sync?this.currentFrame:a.elapsed;a.lfoTurns=t/this.sampleRate*a.scalarValues[1]+(e.phase??0),a.lfoTurns-=Math.floor(a.lfoTurns),a.lfoLive=!0}for(let e=0;e=0&&(a.releaseDb[t]=s,a.releaseStarts[t]=e)}if(a.releaseTime>=0){a.releaseEnd=e;for(let t=0;t<4;t++)a.releaseEnd=Math.max(a.releaseEnd,e+a.releaseTimes[t])}}if(void 0!==r.expression&&(a.expressionFrom=a.expression,a.expressionTarget=r.expression,a.expressionStart=a.elapsed,a.expressionFrames=a.expression===r.expression?0:s,0===a.expressionFrames?(a.expression=r.expression,a.controlRamps&=-2):a.controlRamps|=1),void 0!==r.pan&&(a.panFrom=a.pan,a.panTarget=r.pan,a.panStart=a.elapsed,a.panFrames=a.pan===r.pan?0:s,0===a.panFrames?(a.pan=r.pan,a.controlRamps&=-3):a.controlRamps|=2),void 0===r.expression&&void 0===r.pan||U(a),void 0!==r.modulation&&(a.modulationFrom=a.modulation,a.modulationTarget=r.modulation,a.modulationStart=a.elapsed,a.modulationFrames=a.modulation===r.modulation?0:s,0===a.modulationFrames?(a.modulation=r.modulation,a.controlRamps&=-5):a.controlRamps|=4,z(a)),void 0!==r.operatorLevels){let e=!1;for(let t=0;t<4;t++)a.operatorFrom[t]=a.operatorLevels[t],a.operatorTargets[t]=r.operatorLevels[t],a.operatorFrom[t]!==a.operatorTargets[t]&&(e=!0),0===s&&(a.operatorLevels[t]=r.operatorLevels[t]);a.operatorStart=a.elapsed,a.operatorFrames=e?s:0,0===a.operatorFrames?a.controlRamps&=-9:a.controlRamps|=8}if(void 0!==r.pitch||void 0!==r.modulation){const e=2**(L(a,a.elapsed)/12);for(let t=0;t<4;t++)a.increments[t]=Math.min(.45*v,a.baseIncrements[t]*e),a.steps[t]=a.increments[t]}return!0}ended(e,t){if("function"==typeof this.onVoiceEnded)try{this.onVoiceEnded(e,t)}catch{this.errorCount++}}renderVoice(e){const{graph:t,inputIndices:a,carrierIndices:s,phases:i,values:n,filters:l,increments:c,steps:p,gains:m,levels:f}=e;0!==e.controlRamps&&C(e);const h=e.elapsed/this.sampleRate,u=e.releaseTime>=0&&h>=e.releaseEnd,d=e.voice.lfo,y=h>=(d.delay??0);let F=0;if(y&&(0!==e.amDepth||0!==e.pmDepth)){const t="global"===d.sync?this.currentFrame:e.elapsed,a=e.lfoLive?e.lfoTurns:t/this.sampleRate*d.rate+(d.phase??0);F=r((a-Math.floor(a))*v,d.waveform)}const g=y&&0!==e.amDepth?1-e.amDepth*(.5+.5*F):1,b=y&&0!==e.pmDepth?2**(e.pmDepth*F/1200):1;if(d.amTargets||d.pmTargets)for(let t=0;t<4;t++){const r=d.amTargets?.[t]??1,a=d.pmTargets?.[t]??1;e.amGains[t]=1===r?g:y?1-e.amDepth*r*(.5+.5*F):1,e.pmFactors[t]=1===a?b:y?2**(e.pmDepth*a*F/1200):1}const S=0!==e.pitchFrames,x=0!==e.ratioFrames||0!==e.frequencyFrames,A=S||void 0!==e.voice.pitchEnvelope||x;if(!A&&0!==e.pmDepth)for(let t=0;t<4;t++){const r=d.pmTargets?e.pmFactors[t]:b;p[t]=Math.min(.45*v,c[t]*r)}const q=!!(8&e.controlRamps);u||function(e,t,r){const{sustainGain:a,gains:s,attackStep:o,decayStep:i,releaseStep:n,attackTimes:l,decayTimes:c,releaseTimes:p}=e,m=r.length,f=t+r[m-1];for(let h=0;h<4;h++){if(e.envelopeEdited[h]){for(let a=0;a=0){if(0===y||t>=e.releaseTime+y){for(let e=0;e=u+d){for(let e=0;e0&&f=u&&f=0&&(s+=n[o]*j),l>=0&&(s+=n[l]*j);const c=i[r];n[r]=T(c+s)*m[t+r]*f[r]*V[r]*g;const h=c+p[r];i[r]=h=0&&(s+=n[o]*e.modIndex),l>=0&&(s+=n[l]*e.modIndex);const h=q?$(e.operatorFrom[t],e.operatorTargets[t],e.operatorStart,e.operatorFrames,c):e.operatorLevels[t],u=d.amTargets?e.amGains[t]:g,y=e.waveforms[t];let F;if(0===y)F=T(i[t]+s);else if(8===y){F=1&e.noiseStates[t]?1:-1;const r=e.noiseTurns[t]+e.noiseSteps[t],a=Math.floor(r);for(let r=0;r=e.pitchStart+e.pitchFrames){e.pitchFrames=0;const t=2**(e.pitchTarget/12);for(let r=0;r<4;r++)c[r]=Math.min(.45*v,e.baseIncrements[r]*t),p[r]=c[r]}return x&&(e.elapsed>=e.ratioStart+e.ratioFrames&&(e.ratioFrames=0),e.elapsed>=e.frequencyStart+e.frequencyFrames&&(e.frequencyFrames=0,e.controlRamps&=-65),0===e.ratioFrames&&0===e.frequencyFrames&&this.retuneVoice(e)),e.lastSample=D*e.scalarValues[4],e.lastSample}render(e,t,r=0,a){if(this.rendering)throw new Error("render cannot be called reentrantly");if("Float32Array"!==p.call(e)||"Float32Array"!==p.call(t))throw new RangeError("render requires native Float32Arrays");const s=c.call(e),o=c.call(t);if(!Number.isSafeInteger(r)||r<0)throw new RangeError("render requires an in-bounds offset and length");if(void 0===a&&(a=s-r),!Number.isSafeInteger(a)||a<0||r+a>s||r+a>o)throw new RangeError("render requires an in-bounds offset and length");if(0===this.voices.length&&0===this.fades.length&&0===this.spillRemaining)return m.call(e,0,r,r+a),t!==e&&m.call(t,0,r,r+a),void(this.currentFrame+=a);this.rendering=!0;try{for(let s=r,o=r+a;s=0&&(t.elapsed-1)/this.sampleRate>=t.releaseEnd?Math.max(n,i(t.filters)):1/0,n0){r+=this.spillLeft,a+=this.spillRight;const e=--this.spillRemaining;this.spillLeft*=e/(e+1),this.spillRight*=e/(e+1)}e[s]=Number.isFinite(r)?D*Math.tanh(r*this.mixGain/D):0,t[s]=Number.isFinite(a)?D*Math.tanh(a*this.mixGain/D):0,Number.isFinite(r)&&Number.isFinite(a)||this.errorCount++;for(let e=0;e32)throw new RangeError("maxVoices must be an integer in 1..32");return e}function u(e){if("number"!=typeof e||!Number.isInteger(e)||e<0||e>127)throw new RangeError("voicePriority must be an integer in 0..127");return e}const d=Object.freeze({pitch:[-48,48],glide:[0,10],expression:[0,1],pan:[-1,1],modulation:[0,2],ramp:[0,10],feedback:[0,7],lfoRate:[0,20],amDepth:[0,1],pmDepth:[0,1200],gain:[0,1]});function y(e){if(!e||"object"!=typeof e||Array.isArray(e)||Object.getPrototypeOf(e)!==Object.prototype&&null!==Object.getPrototypeOf(e))throw new TypeError("controls must be a plain object");const t=Reflect.ownKeys(e);if(0===t.length)throw new TypeError("controls must not be empty");const r={};for(const a of t){if("string"!=typeof a||!["operatorLevels","operatorRatios","operatorFrequencies","operatorADSR"].includes(a)&&!Object.hasOwn(d,a))throw new TypeError("controls has an unknown field");const t=Object.getOwnPropertyDescriptor(e,a);if(!Object.hasOwn(t,"value"))throw new TypeError(`controls.${a} must be data`);const s=t.value;if("operatorLevels"===a||"operatorRatios"===a||"operatorFrequencies"===a||"operatorADSR"===a){if(!Array.isArray(s)||4!==s.length||5!==Reflect.ownKeys(s).length||Object.getPrototypeOf(s)!==Array.prototype)throw new TypeError(`${a} must be a dense four-element tuple`);const e=[];for(let t=0;t<4;t++){const r=Object.getOwnPropertyDescriptor(s,String(t));if(!r||!Object.hasOwn(r,"value"))throw new TypeError(`${a} must contain data`);const o=r.value;if("operatorADSR"===a){if(!o||"object"!=typeof o||Array.isArray(o)||Object.getPrototypeOf(o)!==Object.prototype&&null!==Object.getPrototypeOf(o)||4!==Reflect.ownKeys(o).length)throw new TypeError("operatorADSR requires full plain ADSR objects");const t={a:0,d:0,s:0,r:0};for(const e of["a","d","s","r"]){const r=Object.getOwnPropertyDescriptor(o,e);if(!r||!Object.hasOwn(r,"value"))throw new TypeError("operatorADSR must contain data");const a=r.value;if("number"!=typeof a||!Number.isFinite(a)||a<0||a>("s"===e?1:10))throw new RangeError("operatorADSR has an out-of-range value");t[e]=a}e.push(Object.freeze(t))}else{const t="operatorRatios"===a?.125:"operatorFrequencies"===a?1:0,r="operatorRatios"===a?32:"operatorFrequencies"===a?2e4:2;if(("operatorFrequencies"!==a||null!==o)&&("number"!=typeof o||!Number.isFinite(o)||or))throw new RangeError(`${a} must be finite and in ${t}..${r}`);e.push(o)}}Object.defineProperty(r,a,{value:Object.freeze(e),enumerable:!0});continue}const o=a,[i,n]=d[o];if("number"!=typeof s||!Number.isFinite(s)||sn)throw new RangeError(`controls.${a} must be finite and in ${i}..${n}`);r[o]=s}if(Object.hasOwn(r,"glide")&&!Object.hasOwn(r,"pitch"))throw new TypeError("glide requires pitch");if(Object.hasOwn(r,"ramp")&&!Reflect.ownKeys(r).some(e=>"ramp"!==e&&"pitch"!==e&&"glide"!==e&&"operatorADSR"!==e))throw new TypeError("ramp requires a rampable control");return Object.freeze(r)}import{ALGORITHMS as g}from"./algorithms.js";import{FLOOR_DB as F}from"./envelope.js";import{TAU as v,waveformCode as b,periodicWaveform as w,fastSin as T,NOISE_SEED as S,advanceNoise as R}from"./operator.js";import{preparedVoiceValue as x}from"../voices/normalize.js";import{normalizeVoice as A,prepareVoice as q}from"../voices/normalize.js";const D=.7,O=1/v,E=10**(F/20),I=Math.LN10/20,j=Object.freeze({velocity:1,pan:0,voicePriority:0}),V=e({}),M=["feedback","lfoRate","amDepth","pmDepth","gain"];function P(e){return e<=1?.5*e*2**-6*Math.PI:.5*2**(e-7)*Math.PI}function G(e,t,r=0){return Math.min(10,e*2**(-r*(t-60)/12))}function k(e){if("number"!=typeof e||!Number.isFinite(e)||e<0||e>1)throw new RangeError("mixGain must be finite and in 0..1")}function L(e){if(!e||"object"!=typeof e||Array.isArray(e)||Object.getPrototypeOf(e)!==Object.prototype&&null!==Object.getPrototypeOf(e))throw new TypeError("options must be a plain object");const t={};for(const r of Reflect.ownKeys(e)){if("mixGain"!==r&&"tuning"!==r&&"stealing"!==r&&"quality"!==r)throw new TypeError("options has an unknown field");const a=Object.getOwnPropertyDescriptor(e,r);if(!Object.hasOwn(a,"value"))throw new TypeError(`options.${r} must be data`);if("mixGain"===r)k(a.value),t.mixGain=a.value;else if("quality"===r)n(a.value),t.quality=a.value;else if("stealing"===r){if("oldest"!==a.value&&"release-first"!==a.value&&"quietest"!==a.value)throw new TypeError("stealing must be oldest, release-first or quietest");t.stealing=a.value}else t.tuning=a.value}return t}function N(e,t){return 0===e.pitchFrames||t>=e.pitchStart+e.pitchFrames?e.pitchTarget:e.pitchFrom+(e.pitchTarget-e.pitchFrom)*(t-e.pitchStart)/e.pitchFrames}function $(e,t,r,a,s){return 0===a||s>=r+a?t:e+(t-e)*(s-r)/a}function K(e,t){const r=e.voice.pitchEnvelope;return r?e.releaseTime>=0?0===r.r||t>=e.releaseTime+r.r?r.final:e.pitchRelease+(r.final-e.pitchRelease)*(t-e.releaseTime)/r.r:r.a>0&&t0&&t=e.expressionStart+e.expressionFrames&&(e.controlRamps&=-2)),2&t&&(e.pan=$(e.panFrom,e.panTarget,e.panStart,e.panFrames,r),r>=e.panStart+e.panFrames&&(e.controlRamps&=-3)),3&t&&U(e),4&t&&(e.modulation=$(e.modulationFrom,e.modulationTarget,e.modulationStart,e.modulationFrames,r),r>=e.modulationStart+e.modulationFrames&&(e.controlRamps&=-5),z(e)),8&t){for(let t=0;t<4;t++)e.operatorLevels[t]=$(e.operatorFrom[t],e.operatorTargets[t],e.operatorStart,e.operatorFrames,r);r>=e.operatorStart+e.operatorFrames&&(e.controlRamps&=-9)}if(16&t){let t=!1,a=!1;for(let s=0;s=e.ratioStart+e.ratioFrames&&(e.controlRamps&=-33)}}function U(e){const t=e.velocity*e.expression,r=(e.pan+1)*Math.PI/4;e.leftGain=1===e.pan?0:0===e.pan?t:t*Math.SQRT2*Math.cos(r),e.rightGain=-1===e.pan?0:0===e.pan?t:t*Math.SQRT2*Math.sin(r)}function Q(e,t){const r=e[t];for(let r=t+1;r0&&e0&&e=0){const a=e.releaseTimes[r],s=e.releaseStarts[r];return 0===a||t>=s+a?F:e.releaseDb[r]+(F-e.releaseDb[r])*((t-s)/a)}if(!e.envelopeEdited[r])return _(t,e.attackTimes[r],e.decayTimes[r],e.sustainDb[r]);const a=t-e.envelopeStart[r],s=e.attackTimes[r],o=e.decayTimes[r];if(s>0&&a0?0:e.envelopeFromDb[r];return o>0&&a=0){if(0===a||t>=e.releaseTime+a)return 0;const s=e.releaseDb[r]+(F-e.releaseDb[r])*((t-e.releaseTime)/a);return s<=F?0:Math.exp(s*I)}if(t>=e.attackTimes[r]+e.decayTimes[r])return e.sustainGain[r];if(0===t&&e.attackTimes[r]>0)return 0;const s=_(t,e.attackTimes[r],e.decayTimes[r],e.sustainDb[r]);return s<=F?0:Math.exp(s*I)}function H(e,t){const r=e.elapsed/t;let a=0;for(let t=0;t=E)||(0!==r&&e.fill(0),0))}class W{constructor(t,r=8,o={}){if("number"!=typeof t||!Number.isFinite(t)||t<8e3||t>192e3)throw new RangeError("sampleRate must be finite and in 8000..192000");f(r);const i=L(o);this.quality=i.quality??"standard",this.oversample=n(this.quality),this.mixGain=i.mixGain??1,this.tuning=void 0===i.tuning?V:e(i.tuning),this.stealing=i.stealing??"oldest",this.sampleRate=t,this.maxVoices=r,this.voices=[],this.fades=[],this.freeVoices=Array.from({length:r+8+1},()=>{return e=this.oversample,{id:0,sequence:0,note:0,voicePriority:0,voice:null,graph:g[0],inputIndices:new Int8Array(8),carrierIndices:new Uint8Array(4),baseIncrements:new Float64Array(4),increments:new Float64Array(4),steps:new Float64Array(4),sustainDb:new Float64Array(4),sustainGain:new Float64Array(4),attackStep:new Float64Array(4),decayStep:new Float64Array(4),releaseStep:new Float64Array(4),attackTimes:new Float64Array(4),decayTimes:new Float64Array(4),releaseTimes:new Float64Array(4),operatorLevels:new Float64Array(4),operatorFrom:new Float64Array(4),operatorTargets:new Float64Array(4),operatorStart:0,operatorFrames:0,gains:new Float64Array(4*e),levels:new Float64Array(4),leftGain:0,rightGain:0,lastSample:0,fadeRemaining:0,lastFadeGain:1,carrierGain:0,phases:new Float64Array(4),values:new Float64Array(4),releaseDb:new Float64Array(4),previous:0,older:0,pitchRelease:0,feedbackScale:0,elapsed:0,releaseTime:-1,releaseEnd:-1,velocity:1,expression:1,pan:0,expressionFrom:1,expressionTarget:1,expressionStart:0,expressionFrames:0,panFrom:0,panTarget:0,panStart:0,panFrames:0,modulation:1,modulationFrom:1,modulationTarget:1,modulationStart:0,modulationFrames:0,controlRamps:0,modIndex:0,amDepth:0,pmDepth:0,pitchFrom:0,pitchTarget:0,pitchStart:0,pitchFrames:0,scalarValues:new Float64Array(M.length),scalarFrom:new Float64Array(M.length),scalarTargets:new Float64Array(M.length),scalarStarts:new Float64Array(M.length),scalarFrames:new Float64Array(M.length),ratios:new Float64Array(4),ratioFrom:new Float64Array(4),ratioTargets:new Float64Array(4),ratioStart:0,ratioFrames:0,frequencies:new Float64Array(4),frequencyFrom:new Float64Array(4),frequencyTargets:new Float64Array(4),frequencyStart:0,frequencyFrames:0,envelopeStart:new Float64Array(4),envelopeFromDb:new Float64Array(4),envelopeEdited:new Uint8Array(4),releaseStarts:new Float64Array(4),lfoTurns:0,lfoLive:!1,tunedFrequency:0,frequencyScales:new Float64Array(4),amGains:new Float64Array(4),pmFactors:new Float64Array(4),waveforms:new Uint8Array(4),noiseStates:new Uint32Array(4),noiseTurns:new Float64Array(4),noiseSteps:new Float64Array(4)};var e}),this.terminalIds=new Float64Array(r),this.terminalErrors=new Uint8Array(r),this.rendering=!1,this.fadeFrames=Math.ceil(.005*t),this.spillLeft=0,this.spillRight=0,this.spillRemaining=0,this.lastStolenId=null,this.onVoiceEnded=null,this.currentFrame=0,this.errorCount=0,this.nextId=1,this.subTimes=Float64Array.from({length:this.oversample},(e,r)=>r/(t*this.oversample)),this.sequence=0,this.decimatorCoefficients=s(t,this.quality),this.mixLeft=new Float64Array(this.oversample),this.mixRight=new Float64Array(this.oversample),this.filterLeft=new Float64Array(a),this.filterRight=new Float64Array(a),this.decimatorIdle=!0,this.decimatorMirrored=!0}setMixGain(e){k(e),this.mixGain=e}setTuning(t){const r=e(t);this.tuning=r;for(let e=0;ee)continue;const o=t<0?void 0:this.voices[t];let i=!o||s.voicePriority=0;e!==o.releaseTime>=0&&(i=e)}else"quietest"===this.stealing&&n!==r&&(i=n1)throw new RangeError(`${r} must be finite and in ${e}..1`);t[r]=s}}return t}(s),l=x(e);if("number"!=typeof r||!Number.isFinite(r)||r<0||r>127)throw new RangeError("note must be finite and in 0..127");if(void 0===a&&this.nextId>Number.MAX_SAFE_INTEGER)throw new RangeError("Note ID space exhausted");if(void 0!==a&&(!Number.isSafeInteger(a)||a<=0))throw new RangeError("id must be a positive safe integer");for(const e of this.voices)if(e.id===a)throw new RangeError("id already active");const c=this.voices.length>=this.maxVoices?this.stealingIndex(n):-1;if(this.voices.length>=this.maxVoices&&c<0)throw new h;void 0===a&&(a=this.nextId++),a>=this.nextId&&(this.nextId=a+1);const p=this.freeVoices.pop(),{baseIncrements:m,increments:f,sustainDb:d,sustainGain:y,steps:w,attackStep:T,decayStep:R,levels:A}=p,q=t(r,this.tuning),O=this.sampleRate*this.oversample;for(let e=0;e<4;e++){const t=l.ops[e];p.ratios[e]=p.ratioFrom[e]=p.ratioTargets[e]=t.ratio,p.frequencyTargets[e]=t.frequency??0,p.frequencies[e]=p.frequencyFrom[e]=t.frequency??q*t.ratio;const a=t.keyScale,s=a?Math.abs(r-a.breakpoint)/12*(r=this.maxVoices){const e=Q(this.voices,c);if(this.lastStolenId=e.id,8===this.fades.length){const e=Q(this.fades,0),t=e.lastFadeGain;this.spillLeft+=e.lastSample*e.leftGain*t,this.spillRight+=e.lastSample*e.rightGain*t,this.spillRemaining=this.fadeFrames,this.freeVoices.push(e)}e.fadeRemaining=this.fadeFrames,e.lastFadeGain=1,this.fades.push(e)}return this.voices.push(p),null!==this.lastStolenId&&this.ended(this.lastStolenId,"stolen"),a}noteOff(e){let t;for(const r of this.voices)if(r.id===e){t=r;break}return!(!t||t.releaseTime>=0||(this.releaseVoice(t),0))}releaseVoice(e){const t=e.elapsed/this.sampleRate;e.pitchRelease=K(e,t);let r=0;for(let a=0;a<4;a++){e.releaseDb[a]=X(e,t,a),e.releaseStarts[a]=t;const s=e.releaseTimes[a];r=Math.max(r,s),e.releaseStep[a]=0===s?0:10**((F-e.releaseDb[a])/(20*s*this.sampleRate*this.oversample))}e.releaseTime=t,e.releaseEnd=t+r}allNotesOff(){for(let e=0;e0;)this.freeVoices.push(this.fades.pop());this.spillLeft=this.spillRight=this.spillRemaining=0,this.resetDecimator(),this.lastStolenId=null,this.cancelVoices()}resetDecimator(){this.filterLeft.fill(0),this.filterRight.fill(0),this.decimatorIdle=!0,this.decimatorMirrored=!0}cancelVoices(){if(0===this.voices.length)return;const e=this.voices.pop(),t=e.id;this.freeVoices.push(e),this.cancelVoices(),this.ended(t,"cancelled")}updateNote(e,t){const r=y(t);let a;for(const t of this.voices)if(t.id===e){a=t;break}if(!a)return!1;0!==a.controlRamps&&C(a),void 0!==r.pitch&&(a.pitchFrom=N(a,a.elapsed),a.pitchTarget=r.pitch,a.pitchStart=a.elapsed,a.pitchFrames=(r.glide??0)*this.sampleRate);const s=(r.ramp??0)*this.sampleRate;if(void 0!==r.lfoRate&&!a.lfoLive){const e=a.voice.lfo,t="global"===e.sync?this.currentFrame:a.elapsed;a.lfoTurns=t/this.sampleRate*a.scalarValues[1]+(e.phase??0),a.lfoTurns-=Math.floor(a.lfoTurns),a.lfoLive=!0}for(let e=0;e=0&&(a.releaseDb[t]=s,a.releaseStarts[t]=e)}if(a.releaseTime>=0){a.releaseEnd=e;for(let t=0;t<4;t++)a.releaseEnd=Math.max(a.releaseEnd,e+a.releaseTimes[t])}}if(void 0!==r.expression&&(a.expressionFrom=a.expression,a.expressionTarget=r.expression,a.expressionStart=a.elapsed,a.expressionFrames=a.expression===r.expression?0:s,0===a.expressionFrames?(a.expression=r.expression,a.controlRamps&=-2):a.controlRamps|=1),void 0!==r.pan&&(a.panFrom=a.pan,a.panTarget=r.pan,a.panStart=a.elapsed,a.panFrames=a.pan===r.pan?0:s,0===a.panFrames?(a.pan=r.pan,a.controlRamps&=-3):a.controlRamps|=2),void 0===r.expression&&void 0===r.pan||U(a),void 0!==r.modulation&&(a.modulationFrom=a.modulation,a.modulationTarget=r.modulation,a.modulationStart=a.elapsed,a.modulationFrames=a.modulation===r.modulation?0:s,0===a.modulationFrames?(a.modulation=r.modulation,a.controlRamps&=-5):a.controlRamps|=4,z(a)),void 0!==r.operatorLevels){let e=!1;for(let t=0;t<4;t++)a.operatorFrom[t]=a.operatorLevels[t],a.operatorTargets[t]=r.operatorLevels[t],a.operatorFrom[t]!==a.operatorTargets[t]&&(e=!0),0===s&&(a.operatorLevels[t]=r.operatorLevels[t]);a.operatorStart=a.elapsed,a.operatorFrames=e?s:0,0===a.operatorFrames?a.controlRamps&=-9:a.controlRamps|=8}if(void 0!==r.pitch||void 0!==r.modulation){const e=2**(N(a,a.elapsed)/12);for(let t=0;t<4;t++)a.increments[t]=Math.min(.45*v,a.baseIncrements[t]*e),a.steps[t]=a.increments[t]}return!0}ended(e,t){if("function"==typeof this.onVoiceEnded)try{this.onVoiceEnded(e,t)}catch{this.errorCount++}}renderVoice(e,t=1){const{graph:a,inputIndices:s,carrierIndices:o,phases:i,values:n,increments:l,steps:c,gains:p,levels:m}=e;0!==e.controlRamps&&C(e);const h=e.scalarValues[4]*t,f=e.leftGain*h,u=e.rightGain*h,d=this.mixLeft,y=this.mixRight,g=e.elapsed/this.sampleRate,F=e.releaseTime>=0&&g>=e.releaseEnd,b=e.voice.lfo,S=g>=(b.delay??0);let x=0;if(S&&(0!==e.amDepth||0!==e.pmDepth)){const t="global"===b.sync?this.currentFrame:e.elapsed,a=e.lfoLive?e.lfoTurns:t/this.sampleRate*b.rate+(b.phase??0);x=r((a-Math.floor(a))*v,b.waveform)}const A=S&&0!==e.amDepth?1-e.amDepth*(.5+.5*x):1,q=S&&0!==e.pmDepth?2**(e.pmDepth*x/1200):1;if(b.amTargets||b.pmTargets)for(let t=0;t<4;t++){const r=b.amTargets?.[t]??1,a=b.pmTargets?.[t]??1;e.amGains[t]=1===r?A:S?1-e.amDepth*r*(.5+.5*x):1,e.pmFactors[t]=1===a?q:S?2**(e.pmDepth*a*x/1200):1}const D=0!==e.pitchFrames,E=0!==e.ratioFrames||0!==e.frequencyFrames,I=D||void 0!==e.voice.pitchEnvelope||E;if(!I&&0!==e.pmDepth)for(let t=0;t<4;t++){const r=b.pmTargets?e.pmFactors[t]:q;c[t]=Math.min(.45*v,l[t]*r)}const j=!!(8&e.controlRamps);F||function(e,t,r){const{sustainGain:a,gains:s,attackStep:o,decayStep:i,releaseStep:n,attackTimes:l,decayTimes:c,releaseTimes:p}=e,m=r.length,h=t+r[m-1];for(let f=0;f<4;f++){if(e.envelopeEdited[f]){for(let a=0;a=0){if(0===y||t>=e.releaseTime+y){for(let e=0;e=u+d){for(let e=0;e0&&h=u&&h=0&&(a+=n[o]*P),l>=0&&(a+=n[l]*P);const h=i[t];n[t]=T(h+a)*p[r+t]*m[t]*k[t]*A;const f=h+c[t];i[t]=f=0&&(o+=n[l]*e.modIndex),h>=0&&(o+=n[h]*e.modIndex);const f=j?$(e.operatorFrom[a],e.operatorTargets[a],e.operatorStart,e.operatorFrames,r):e.operatorLevels[a],u=b.amTargets?e.amGains[a]:A,d=e.waveforms[a];let y;if(0===d)y=T(i[a]+o);else if(8===d){y=1&e.noiseStates[a]?1:-1;const t=e.noiseTurns[a]+e.noiseSteps[a],r=Math.floor(t);for(let t=0;t=e.pitchStart+e.pitchFrames){e.pitchFrames=0;const t=2**(e.pitchTarget/12);for(let r=0;r<4;r++)l[r]=Math.min(.45*v,e.baseIncrements[r]*t),c[r]=l[r]}return E&&(e.elapsed>=e.ratioStart+e.ratioFrames&&(e.ratioFrames=0),e.elapsed>=e.frequencyStart+e.frequencyFrames&&(e.frequencyFrames=0,e.controlRamps&=-65),0===e.ratioFrames&&0===e.frequencyFrames&&this.retuneVoice(e)),e.lastSample=V*e.scalarValues[4],e.lastSample}render(e,t,r=0,a){if(this.rendering)throw new Error("render cannot be called reentrantly");if("Float32Array"!==p.call(e)||"Float32Array"!==p.call(t))throw new RangeError("render requires native Float32Arrays");const s=c.call(e),i=c.call(t);if(!Number.isSafeInteger(r)||r<0)throw new RangeError("render requires an in-bounds offset and length");if(void 0===a&&(a=s-r),!Number.isSafeInteger(a)||a<0||r+a>s||r+a>i)throw new RangeError("render requires an in-bounds offset and length");if(0===this.voices.length&&0===this.fades.length&&0===this.spillRemaining&&this.decimatorIdle)return m.call(e,0,r,r+a),t!==e&&m.call(t,0,r,r+a),void(this.currentFrame+=a);this.rendering=!0;const n=this.oversample,l=this.decimatorCoefficients,h=this.mixLeft,f=this.mixRight,u=this.filterLeft,d=this.filterRight;try{for(let s=r,i=r+a;s=0&&(t.elapsed-1)/this.sampleRate>=t.releaseEnd&&(this.terminalIds[r]=t.id,this.terminalErrors[r++]=0,this.freeVoices.push(Q(this.voices,e--))):(this.errorCount++,this.terminalIds[r]=t.id,this.terminalErrors[r++]=1,this.freeVoices.push(Q(this.voices,e--)))}for(let e=0;e0){for(let e=0;e number;\nconst typedArrayKind = Object.getOwnPropertyDescriptor(typedArrayPrototype, Symbol.toStringTag)!.get! as (this: unknown) => string | undefined;\nconst typedArrayFill = Float32Array.prototype.fill;\n\nexport type VoiceEndReason = 'stolen' | 'ended' | 'error' | 'cancelled';\nexport interface NoteOptions { velocity?: number; pan?: number; voicePriority?: number }\nexport interface NoteControls {\n pitch?: number; glide?: number; expression?: number; pan?: number; modulation?: number; ramp?: number;\n /** Independent per-note amplitude, 0..1 (default 1), multiplied with expression before mix saturation. */\n gain?: number;\n operatorLevels?: readonly [number, number, number, number];\n feedback?: number; lfoRate?: number; amDepth?: number; pmDepth?: number;\n operatorRatios?: readonly [number, number, number, number];\n /** A number enables fixed Hz; null restores the operator's live ratio. Pitch still applies. */\n operatorFrequencies?: readonly [number | null, number | null, number | null, number | null];\n operatorADSR?: readonly [ADSR, ADSR, ADSR, ADSR];\n}\nexport interface SynthOptions {\n mixGain?: number;\n tuning?: TuningOptions;\n stealing?: 'oldest' | 'release-first' | 'quietest';\n quality?: QualityProfile;\n}\n\n/** A valid note could not displace any higher-priority logical voice. */\nexport class VoiceAdmissionError extends Error {\n constructor() {\n super('All active voices have higher voicePriority');\n this.name = 'VoiceAdmissionError';\n }\n}\n\nexport function validateMaxVoices(value: unknown): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 32) {\n throw new RangeError('maxVoices must be an integer in 1..32');\n }\n return value;\n}\n\nexport function validateVoicePriority(value: unknown): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 127) {\n throw new RangeError('voicePriority must be an integer in 0..127');\n }\n return value;\n}\n\n/** Snapshot note admission controls without invoking inherited values/accessors. */\nfunction readNoteOptions(input: NoteOptions): Required {\n if (!input || typeof input !== 'object' || Array.isArray(input) ||\n (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)) {\n throw new TypeError('note options must be a plain data object');\n }\n const result = { velocity: 1, pan: 0, voicePriority: 0 };\n for (const key of Reflect.ownKeys(input)) {\n if (key !== 'velocity' && key !== 'pan' && key !== 'voicePriority') throw new TypeError('note options has an unknown field');\n const descriptor = Object.getOwnPropertyDescriptor(input, key)!;\n if (!Object.hasOwn(descriptor, 'value')) throw new TypeError(`note options.${key} must be data`);\n const value: unknown = descriptor.value;\n if (key === 'voicePriority') result.voicePriority = validateVoicePriority(value);\n else {\n const min = key === 'pan' ? -1 : 0;\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > 1) {\n throw new RangeError(`${key} must be finite and in ${min}..1`);\n }\n result[key] = value;\n }\n }\n return result;\n}\n\nconst CONTROL_LIMITS = Object.freeze({\n pitch: [-48, 48], glide: [0, 10], expression: [0, 1], pan: [-1, 1], modulation: [0, 2], ramp: [0, 10],\n feedback: [0, 7], lfoRate: [0, 20], amDepth: [0, 1], pmDepth: [0, 1200], gain: [0, 1],\n} as const);\n\n/** Copy strict own-data controls at the API/dispatch boundary without invoking getters. */\nexport function validateNoteControls(input: NoteControls): NoteControls {\n if (!input || typeof input !== 'object' || Array.isArray(input) ||\n (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)) {\n throw new TypeError('controls must be a plain object');\n }\n const keys = Reflect.ownKeys(input);\n if (keys.length === 0) throw new TypeError('controls must not be empty');\n const result: NoteControls = {};\n for (const key of keys) {\n if (typeof key !== 'string' || !['operatorLevels', 'operatorRatios', 'operatorFrequencies', 'operatorADSR'].includes(key) && !Object.hasOwn(CONTROL_LIMITS, key)) throw new TypeError('controls has an unknown field');\n const descriptor = Object.getOwnPropertyDescriptor(input, key)!;\n if (!Object.hasOwn(descriptor, 'value')) throw new TypeError(`controls.${key} must be data`);\n const value: unknown = descriptor.value;\n if (key === 'operatorLevels' || key === 'operatorRatios' || key === 'operatorFrequencies' || key === 'operatorADSR') {\n if (!Array.isArray(value) || value.length !== 4 || Reflect.ownKeys(value).length !== 5 ||\n Object.getPrototypeOf(value) !== Array.prototype) throw new TypeError(`${key} must be a dense four-element tuple`);\n const tuple: unknown[] = [];\n for (let op = 0; op < 4; op++) {\n const element = Object.getOwnPropertyDescriptor(value, String(op));\n if (!element || !Object.hasOwn(element, 'value')) throw new TypeError(`${key} must contain data`);\n const item: unknown = element.value;\n if (key === 'operatorADSR') {\n if (!item || typeof item !== 'object' || Array.isArray(item) ||\n (Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) ||\n Reflect.ownKeys(item).length !== 4) throw new TypeError('operatorADSR requires full plain ADSR objects');\n const adsr: ADSR = { a: 0, d: 0, s: 0, r: 0 };\n for (const field of ['a', 'd', 's', 'r'] as const) {\n const data = Object.getOwnPropertyDescriptor(item, field);\n if (!data || !Object.hasOwn(data, 'value')) throw new TypeError('operatorADSR must contain data');\n const number: unknown = data.value;\n if (typeof number !== 'number' || !Number.isFinite(number) || number < 0 || number > (field === 's' ? 1 : 10)) {\n throw new RangeError('operatorADSR has an out-of-range value');\n }\n adsr[field] = number;\n }\n tuple.push(Object.freeze(adsr));\n } else {\n const min = key === 'operatorRatios' ? 0.125 : key === 'operatorFrequencies' ? 1 : 0;\n const max = key === 'operatorRatios' ? 32 : key === 'operatorFrequencies' ? 20000 : 2;\n if (key !== 'operatorFrequencies' || item !== null) {\n if (typeof item !== 'number' || !Number.isFinite(item) || item < min || item > max) throw new RangeError(`${key} must be finite and in ${min}..${max}`);\n }\n tuple.push(item);\n }\n }\n Object.defineProperty(result, key, { value: Object.freeze(tuple), enumerable: true });\n continue;\n }\n const control = key as keyof typeof CONTROL_LIMITS;\n const [min, max] = CONTROL_LIMITS[control];\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {\n throw new RangeError(`controls.${key} must be finite and in ${min}..${max}`);\n }\n result[control] = value;\n }\n if (Object.hasOwn(result, 'glide') && !Object.hasOwn(result, 'pitch')) throw new TypeError('glide requires pitch');\n if (Object.hasOwn(result, 'ramp') && !Reflect.ownKeys(result).some(key => key !== 'ramp' && key !== 'pitch' && key !== 'glide' && key !== 'operatorADSR')) {\n throw new TypeError('ramp requires a rampable control');\n }\n return Object.freeze(result);\n}\n\ninterface ActiveVoice {\n id: number; sequence: number; note: number; voicePriority: number; voice: PreparedVoice; graph: AlgorithmGraph;\n inputIndices: Int8Array; carrierIndices: Uint8Array;\n baseIncrements: Float64Array; increments: Float64Array; steps: Float64Array; sustainDb: Float64Array; sustainGain: Float64Array;\n attackStep: Float64Array; decayStep: Float64Array; releaseStep: Float64Array; gains: Float64Array;\n attackTimes: Float64Array; decayTimes: Float64Array; releaseTimes: Float64Array;\n operatorLevels: Float64Array; operatorFrom: Float64Array; operatorTargets: Float64Array;\n operatorStart: number; operatorFrames: number;\n levels: Float64Array; leftGain: number; rightGain: number; lastSample: number; fadeRemaining: number;\n lastFadeGain?: number; carrierGain: number; phases: Float64Array; values: Float64Array; filters: Float64Array;\n releaseDb: Float64Array; previous: number; older: number; pitchRelease: number;\n feedbackScale: number; elapsed: number; releaseTime: number; releaseEnd: number;\n velocity: number; expression: number; pan: number;\n expressionFrom: number; expressionTarget: number; expressionStart: number; expressionFrames: number;\n panFrom: number; panTarget: number; panStart: number; panFrames: number;\n modulation: number; modulationFrom: number; modulationTarget: number; modulationStart: number; modulationFrames: number;\n controlRamps: number;\n modIndex: number; amDepth: number; pmDepth: number;\n pitchFrom: number; pitchTarget: number; pitchStart: number; pitchFrames: number;\n scalarValues: Float64Array; scalarFrom: Float64Array; scalarTargets: Float64Array; scalarStarts: Float64Array; scalarFrames: Float64Array;\n ratios: Float64Array; ratioFrom: Float64Array; ratioTargets: Float64Array; ratioStart: number; ratioFrames: number;\n frequencies: Float64Array; frequencyFrom: Float64Array; frequencyTargets: Float64Array; frequencyStart: number; frequencyFrames: number;\n envelopeStart: Float64Array; envelopeFromDb: Float64Array; envelopeEdited: Uint8Array; releaseStarts: Float64Array;\n lfoTurns: number; lfoLive: boolean;\n tunedFrequency: number;\n frequencyScales: Float64Array;\n amGains: Float64Array; pmFactors: Float64Array;\n waveforms: Uint8Array; noiseStates: Uint32Array; noiseTurns: Float64Array; noiseSteps: Float64Array;\n}\n\ninterface MutableCounters {\n currentFrame: number; errorCount: number; lastStolenId: number | null;\n}\n\nimport { ALGORITHMS } from './algorithms.js';\nimport { FLOOR_DB } from './envelope.js';\nimport { TAU, waveformCode, periodicWaveform, fastSin, NOISE_SEED, advanceNoise } from './operator.js';\nimport { preparedVoiceValue } from '../voices/normalize.js';\nexport { normalizeVoice, prepareVoice } from '../voices/normalize.js';\n\nconst HEADROOM = 0.7;\nconst DEFAULT_MAX_VOICES = 8;\nconst MAX_FADES = 8;\nconst INV_TAU = 1 / TAU;\nconst AMPLITUDE_FLOOR = 10 ** (FLOOR_DB / 20);\nconst DB_TO_LOG_GAIN = Math.LN10 / 20;\nconst DEFAULT_NOTE_OPTIONS: NoteOptions = Object.freeze({ velocity: 1, pan: 0, voicePriority: 0 });\nconst DEFAULT_TUNING = normalizeTuning({});\nconst EXPRESSION_RAMP = 1;\nconst PAN_RAMP = 2;\nconst MODULATION_RAMP = 4;\nconst OPERATOR_RAMP = 8;\nconst ENGINE_RAMP = 16;\nconst RATIO_RAMP = 32;\nconst FREQUENCY_RAMP = 64;\nconst SCALAR_FIELDS = ['feedback', 'lfoRate', 'amDepth', 'pmDepth', 'gain'] as const;\n\nfunction feedbackGain(value: number): number {\n return value <= 1 ? value * 0.5 * 2 ** -6 * Math.PI : 0.5 * 2 ** (value - 7) * Math.PI;\n}\n\n/** @internal Scale once per admission; low-note durations never exceed the voice's ten-second budget. */\nexport function operatorDuration(seconds: number, note: number, rateKeyScale = 0): number {\n return Math.min(10, seconds * 2 ** (-rateKeyScale * (note - 60) / 12));\n}\n\nfunction validateMixGain(gain: number): void {\n if (typeof gain !== 'number' || !Number.isFinite(gain) || gain < 0 || gain > 1) {\n throw new RangeError('mixGain must be finite and in 0..1');\n }\n}\n\n/** @internal Validate structural settings without allocating voice slots; tuning is validated separately by normalizeTuning. */\nexport function readSynthOptions(input: SynthOptions): SynthOptions {\n if (!input || typeof input !== 'object' || Array.isArray(input) ||\n (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)) {\n throw new TypeError('options must be a plain object');\n }\n const result: SynthOptions = {};\n for (const key of Reflect.ownKeys(input)) {\n if (key !== 'mixGain' && key !== 'tuning' && key !== 'stealing' && key !== 'quality') throw new TypeError('options has an unknown field');\n const descriptor = Object.getOwnPropertyDescriptor(input, key)!;\n if (!Object.hasOwn(descriptor, 'value')) throw new TypeError(`options.${key} must be data`);\n if (key === 'mixGain') {\n validateMixGain(descriptor.value);\n result.mixGain = descriptor.value;\n } else if (key === 'quality') {\n qualityOversample(descriptor.value);\n result.quality = descriptor.value;\n } else if (key === 'stealing') {\n if (descriptor.value !== 'oldest' && descriptor.value !== 'release-first' && descriptor.value !== 'quietest') {\n throw new TypeError('stealing must be oldest, release-first or quietest');\n }\n result.stealing = descriptor.value;\n } else {\n result.tuning = descriptor.value;\n }\n }\n return result;\n}\n\nfunction createVoiceSlot(oversample: number): ActiveVoice {\n return {\n id: 0, sequence: 0, note: 0, voicePriority: 0, voice: null!, graph: ALGORITHMS[0],\n inputIndices: new Int8Array(8), carrierIndices: new Uint8Array(4),\n baseIncrements: new Float64Array(4), increments: new Float64Array(4), steps: new Float64Array(4),\n sustainDb: new Float64Array(4), sustainGain: new Float64Array(4),\n attackStep: new Float64Array(4), decayStep: new Float64Array(4), releaseStep: new Float64Array(4),\n attackTimes: new Float64Array(4), decayTimes: new Float64Array(4), releaseTimes: new Float64Array(4),\n operatorLevels: new Float64Array(4), operatorFrom: new Float64Array(4), operatorTargets: new Float64Array(4),\n operatorStart: 0, operatorFrames: 0,\n gains: new Float64Array(4 * oversample), levels: new Float64Array(4),\n leftGain: 0, rightGain: 0, lastSample: 0, fadeRemaining: 0, lastFadeGain: 1, carrierGain: 0,\n phases: new Float64Array(4), values: new Float64Array(4), filters: new Float64Array(DECIMATOR_STATE_SIZE), releaseDb: new Float64Array(4),\n previous: 0, older: 0, pitchRelease: 0, feedbackScale: 0,\n elapsed: 0, releaseTime: -1, releaseEnd: -1, velocity: 1, expression: 1, pan: 0,\n expressionFrom: 1, expressionTarget: 1, expressionStart: 0, expressionFrames: 0,\n panFrom: 0, panTarget: 0, panStart: 0, panFrames: 0,\n modulation: 1, modulationFrom: 1, modulationTarget: 1, modulationStart: 0, modulationFrames: 0,\n controlRamps: 0,\n modIndex: 0, amDepth: 0, pmDepth: 0, pitchFrom: 0, pitchTarget: 0, pitchStart: 0, pitchFrames: 0,\n scalarValues: new Float64Array(SCALAR_FIELDS.length), scalarFrom: new Float64Array(SCALAR_FIELDS.length),\n scalarTargets: new Float64Array(SCALAR_FIELDS.length), scalarStarts: new Float64Array(SCALAR_FIELDS.length), scalarFrames: new Float64Array(SCALAR_FIELDS.length),\n ratios: new Float64Array(4), ratioFrom: new Float64Array(4), ratioTargets: new Float64Array(4), ratioStart: 0, ratioFrames: 0,\n frequencies: new Float64Array(4), frequencyFrom: new Float64Array(4), frequencyTargets: new Float64Array(4), frequencyStart: 0, frequencyFrames: 0,\n envelopeStart: new Float64Array(4), envelopeFromDb: new Float64Array(4), envelopeEdited: new Uint8Array(4), releaseStarts: new Float64Array(4),\n lfoTurns: 0, lfoLive: false,\n tunedFrequency: 0,\n frequencyScales: new Float64Array(4),\n amGains: new Float64Array(4), pmFactors: new Float64Array(4),\n waveforms: new Uint8Array(4), noiseStates: new Uint32Array(4), noiseTurns: new Float64Array(4), noiseSteps: new Float64Array(4),\n };\n}\n\nfunction pitchAt(active: ActiveVoice, frame: number): number {\n if (active.pitchFrames === 0 || frame >= active.pitchStart + active.pitchFrames) return active.pitchTarget;\n return active.pitchFrom + (active.pitchTarget - active.pitchFrom) * (frame - active.pitchStart) / active.pitchFrames;\n}\nfunction rampAt(from: number, target: number, start: number, frames: number, frame: number): number {\n if (frames === 0 || frame >= start + frames) return target;\n return from + (target - from) * (frame - start) / frames;\n}\n\nfunction pitchEnvelopeAt(active: ActiveVoice, time: number): number {\n const envelope = active.voice.pitchEnvelope;\n if (!envelope) return 0;\n if (active.releaseTime >= 0) {\n if (envelope.r === 0 || time >= active.releaseTime + envelope.r) return envelope.final;\n return active.pitchRelease + (envelope.final - active.pitchRelease) * (time - active.releaseTime) / envelope.r;\n }\n if (envelope.a > 0 && time < envelope.a) return envelope.initial + (envelope.peak - envelope.initial) * time / envelope.a;\n if (envelope.d > 0 && time < envelope.a + envelope.d) return envelope.peak + (envelope.sustain - envelope.peak) * (time - envelope.a) / envelope.d;\n return envelope.sustain;\n}\n\nfunction updateModulation(active: ActiveVoice): void {\n active.modIndex = active.voice.modIndex * active.modulation;\n active.amDepth = Math.min(1, active.scalarValues[2] * active.modulation);\n active.pmDepth = Math.min(1200, active.scalarValues[3] * active.modulation);\n if (active.pmDepth === 0) {\n for (let op = 0; op < 4; op++) active.steps[op] = active.increments[op];\n }\n}\n\nfunction advanceControls(active: ActiveVoice): void {\n const ramps = active.controlRamps;\n const frame = active.elapsed;\n if (ramps & EXPRESSION_RAMP) {\n active.expression = rampAt(active.expressionFrom, active.expressionTarget, active.expressionStart, active.expressionFrames, frame);\n if (frame >= active.expressionStart + active.expressionFrames) active.controlRamps &= ~EXPRESSION_RAMP;\n }\n if (ramps & PAN_RAMP) {\n active.pan = rampAt(active.panFrom, active.panTarget, active.panStart, active.panFrames, frame);\n if (frame >= active.panStart + active.panFrames) active.controlRamps &= ~PAN_RAMP;\n }\n if (ramps & (EXPRESSION_RAMP | PAN_RAMP)) updatePan(active);\n if (ramps & MODULATION_RAMP) {\n active.modulation = rampAt(active.modulationFrom, active.modulationTarget, active.modulationStart, active.modulationFrames, frame);\n if (frame >= active.modulationStart + active.modulationFrames) active.controlRamps &= ~MODULATION_RAMP;\n updateModulation(active);\n }\n if (ramps & OPERATOR_RAMP) {\n for (let op = 0; op < 4; op++) {\n active.operatorLevels[op] = rampAt(active.operatorFrom[op], active.operatorTargets[op], active.operatorStart, active.operatorFrames, frame);\n }\n if (frame >= active.operatorStart + active.operatorFrames) active.controlRamps &= ~OPERATOR_RAMP;\n }\n if (ramps & ENGINE_RAMP) {\n let pending = false, modulationChanged = false;\n for (let field = 0; field < SCALAR_FIELDS.length; field++) {\n if (active.scalarFrames[field] === 0) continue;\n active.scalarValues[field] = rampAt(active.scalarFrom[field], active.scalarTargets[field], active.scalarStarts[field], active.scalarFrames[field], frame);\n if (frame < active.scalarStarts[field] + active.scalarFrames[field]) pending = true;\n else active.scalarFrames[field] = 0;\n if (field === 0) active.feedbackScale = feedbackGain(active.scalarValues[0]);\n if (field === 2 || field === 3) modulationChanged = true;\n }\n if (modulationChanged) updateModulation(active);\n if (!pending) active.controlRamps &= ~ENGINE_RAMP;\n }\n if (ramps & RATIO_RAMP) {\n for (let op = 0; op < 4; op++) active.ratios[op] = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, frame);\n if (frame >= active.ratioStart + active.ratioFrames) active.controlRamps &= ~RATIO_RAMP;\n }\n}\n\nfunction updatePan(active: ActiveVoice): void {\n const gain = active.velocity * active.expression;\n const angle = (active.pan + 1) * Math.PI / 4;\n active.leftGain = active.pan === 1 ? 0 : active.pan === 0 ? gain : gain * Math.SQRT2 * Math.cos(angle);\n active.rightGain = active.pan === -1 ? 0 : active.pan === 0 ? gain : gain * Math.SQRT2 * Math.sin(angle);\n}\nfunction removeAt(items: T[], index: number): T {\n const removed = items[index];\n for (let i = index + 1; i < items.length; i++) items[i - 1] = items[i];\n items.pop();\n return removed;\n}\n\nfunction heldDb(time: number, attack: number, decay: number, sustainDb: number): number {\n if (attack > 0 && time < attack) return FLOOR_DB * (1 - time / attack);\n if (decay > 0 && time < attack + decay) return sustainDb * ((time - attack) / decay);\n return sustainDb;\n}\n\nfunction envelopeDbAt(active: ActiveVoice, time: number, op: number): number {\n if (active.releaseTime >= 0) {\n const release = active.releaseTimes[op], start = active.releaseStarts[op];\n if (release === 0 || time >= start + release) return FLOOR_DB;\n return active.releaseDb[op] + (FLOOR_DB - active.releaseDb[op]) * ((time - start) / release);\n }\n if (!active.envelopeEdited[op]) return heldDb(time, active.attackTimes[op], active.decayTimes[op], active.sustainDb[op]);\n const elapsed = time - active.envelopeStart[op], attack = active.attackTimes[op], decay = active.decayTimes[op];\n // A zero attack still preserves the boundary value: the new decay starts at\n // the interrupted dB instead of jumping to full scale.\n if (attack > 0 && elapsed < attack) return active.envelopeFromDb[op] * (1 - elapsed / attack);\n const from = attack > 0 ? 0 : active.envelopeFromDb[op];\n if (decay > 0 && elapsed < attack + decay) return from + (active.sustainDb[op] - from) * (elapsed - attack) / decay;\n return active.sustainDb[op];\n}\n\nfunction gainAt(active: ActiveVoice, time: number, op: number): number {\n if (active.envelopeEdited[op]) {\n const db = envelopeDbAt(active, time, op);\n return db <= FLOOR_DB ? 0 : Math.exp(db * DB_TO_LOG_GAIN);\n }\n const release = active.releaseTimes[op];\n if (active.releaseTime >= 0) {\n if (release === 0 || time >= active.releaseTime + release) return 0;\n const db = active.releaseDb[op] + (FLOOR_DB - active.releaseDb[op]) * ((time - active.releaseTime) / release);\n return db <= FLOOR_DB ? 0 : Math.exp(db * DB_TO_LOG_GAIN);\n }\n if (time >= active.attackTimes[op] + active.decayTimes[op]) return active.sustainGain[op];\n if (time === 0 && active.attackTimes[op] > 0) return 0;\n const db = heldDb(time, active.attackTimes[op], active.decayTimes[op], active.sustainDb[op]);\n return db <= FLOOR_DB ? 0 : Math.exp(db * DB_TO_LOG_GAIN);\n}\n\nfunction prepareGains(active: ActiveVoice, time: number, subTimes: Float64Array): void {\n const { sustainGain, gains, attackStep, decayStep, releaseStep, attackTimes, decayTimes, releaseTimes } = active;\n const oversample = subTimes.length;\n const lastTime = time + subTimes[oversample - 1];\n for (let op = 0; op < 4; op++) {\n if (active.envelopeEdited[op]) {\n for (let sub = 0; sub < oversample; sub++) gains[op + sub * 4] = gainAt(active, time + subTimes[sub], op);\n continue;\n }\n const attack = attackTimes[op], decay = decayTimes[op], release = releaseTimes[op];\n let step: number | undefined;\n if (active.releaseTime >= 0) {\n if (release === 0 || time >= active.releaseTime + release) {\n for (let sub = 0; sub < oversample; sub++) gains[op + sub * 4] = 0;\n continue;\n }\n if (lastTime < active.releaseTime + release) step = releaseStep[op];\n } else if (time >= attack + decay) {\n for (let sub = 0; sub < oversample; sub++) gains[op + sub * 4] = sustainGain[op];\n continue;\n } else if (time > 0 && lastTime < attack) {\n step = attackStep[op];\n } else if (time >= attack && lastTime < attack + decay) {\n step = decayStep[op];\n }\n // Re-anchor every output frame, so rounding cannot accumulate across time\n // or depend on render chunking. Boundary-crossing frames keep exact dB rules.\n if (step !== undefined) {\n let gain = gainAt(active, time, op);\n for (let sub = 0; sub < oversample; sub++) {\n gains[op + sub * 4] = gain;\n gain *= step;\n }\n } else {\n for (let sub = 0; sub < oversample; sub++) {\n gains[op + sub * 4] = gainAt(active, time + subTimes[sub], op);\n }\n }\n }\n}\n// Carrier-envelope energy avoids stealing a loud note merely at a zero crossing.\nfunction voiceAudibility(active: ActiveVoice, sampleRate: number): number {\n const time = active.elapsed / sampleRate;\n let gain = 0;\n for (let index = 0; index < active.graph.carriers.length; index++) {\n const op = active.graph.carriers[index];\n const level = active.controlRamps & OPERATOR_RAMP ?\n rampAt(active.operatorFrom[op], active.operatorTargets[op], active.operatorStart, active.operatorFrames, active.elapsed) : active.operatorLevels[op];\n gain += active.levels[op] * level * gainAt(active, time, op);\n }\n const expression = active.controlRamps & EXPRESSION_RAMP ?\n rampAt(active.expressionFrom, active.expressionTarget, active.expressionStart, active.expressionFrames, active.elapsed) :\n active.expression;\n const layerGain = rampAt(active.scalarFrom[4], active.scalarTargets[4], active.scalarStarts[4], active.scalarFrames[4], active.elapsed);\n return gain * active.carrierGain * active.velocity * expression * layerGain;\n}\n\n\nexport class Synth {\n declare readonly sampleRate: number;\n declare readonly maxVoices: number;\n declare readonly quality: QualityProfile;\n /** @internal */\n declare oversample: number;\n declare readonly currentFrame: number;\n declare readonly errorCount: number;\n declare readonly lastStolenId: number | null;\n declare onVoiceEnded?: ((id: number, reason: VoiceEndReason) => void) | null;\n /** @internal */\n declare voices: ActiveVoice[];\n /** @internal */\n declare fades: ActiveVoice[];\n /** @internal */\n declare fadeFrames: number;\n /** @internal */\n declare spillLeft: number;\n /** @internal */\n declare spillRight: number;\n /** @internal */\n declare spillRemaining: number;\n /** @internal */\n declare nextId: number;\n /** @internal */\n declare subTimes: Float64Array;\n /** @internal */\n declare sequence: number;\n /** @internal */\n declare decimatorCoefficients: Float64Array;\n /** @internal Bounded spare slots include one admission scratch slot. */\n declare freeVoices: ActiveVoice[];\n /** @internal Preallocated notifications are drained after each stable frame. */\n declare terminalIds: Float64Array;\n /** @internal */\n declare terminalErrors: Uint8Array;\n /** @internal */\n declare rendering: boolean;\n /** @internal */\n declare mixGain: number;\n /** @internal */\n declare tuning: NormalizedTuning;\n /** @internal */\n declare stealing: NonNullable;\n\n constructor(sampleRate: number, maxVoices = DEFAULT_MAX_VOICES, options: SynthOptions = {}) {\n if (typeof sampleRate !== 'number' || !Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {\n throw new RangeError('sampleRate must be finite and in 8000..192000');\n }\n validateMaxVoices(maxVoices);\n const settings = readSynthOptions(options);\n this.quality = settings.quality ?? 'standard';\n this.oversample = qualityOversample(this.quality);\n this.mixGain = settings.mixGain ?? 1;\n this.tuning = settings.tuning === undefined ? DEFAULT_TUNING : normalizeTuning(settings.tuning);\n this.stealing = settings.stealing ?? 'oldest';\n this.sampleRate = sampleRate;\n this.maxVoices = maxVoices;\n this.voices = [];\n this.fades = [];\n this.freeVoices = Array.from({ length: maxVoices + MAX_FADES + 1 }, () => createVoiceSlot(this.oversample));\n this.terminalIds = new Float64Array(maxVoices);\n this.terminalErrors = new Uint8Array(maxVoices);\n this.rendering = false;\n this.fadeFrames = Math.ceil(sampleRate * 0.005);\n this.spillLeft = 0;\n this.spillRight = 0;\n this.spillRemaining = 0;\n this.lastStolenId = null;\n this.onVoiceEnded = null;\n this.currentFrame = 0;\n this.errorCount = 0;\n this.nextId = 1;\n this.subTimes = Float64Array.from({ length: this.oversample }, (_, sub) => sub / (sampleRate * this.oversample));\n this.sequence = 0;\n this.decimatorCoefficients = createDecimatorCoefficients(sampleRate, this.quality);\n }\n setMixGain(gain: number): void {\n validateMixGain(gain);\n this.mixGain = gain;\n }\n\n setTuning(tuning: TuningOptions): void {\n const normalized = normalizeTuning(tuning);\n this.tuning = normalized;\n for (let index = 0; index < this.voices.length; index++) this.retuneVoice(this.voices[index]);\n for (let index = 0; index < this.fades.length; index++) this.retuneVoice(this.fades[index]);\n }\n\n /** @internal */\n retuneVoice(active: ActiveVoice): void {\n const frequency = tuningFrequency(active.note, this.tuning);\n active.tunedFrequency = frequency;\n const rate = this.sampleRate * this.oversample;\n const factor = 2 ** (pitchAt(active, active.elapsed) / 12);\n for (let op = 0; op < 4; op++) {\n const source = active.voice.ops[op];\n const ratio = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, active.elapsed);\n const target = active.frequencyTargets[op] || frequency * ratio;\n const hz = rampAt(active.frequencyFrom[op], target, active.frequencyStart, active.frequencyFrames, active.elapsed);\n active.baseIncrements[op] = TAU * hz * 2 ** (source.detune / 1200) / rate;\n active.increments[op] = Math.min(TAU * 0.45, active.baseIncrements[op] * factor);\n active.steps[op] = active.increments[op];\n }\n }\n\n /** @internal */\n stealingIndex(voicePriority: number): number {\n let chosen = -1;\n let quietest = 0;\n for (let index = 0; index < this.voices.length; index++) {\n const candidate = this.voices[index];\n if (candidate.voicePriority > voicePriority) continue;\n const current = chosen < 0 ? undefined : this.voices[chosen];\n let preferred = !current || candidate.voicePriority < current.voicePriority;\n let audibility = 0;\n if (this.stealing === 'quietest') audibility = voiceAudibility(candidate, this.sampleRate);\n if (current && candidate.voicePriority === current.voicePriority) {\n preferred = candidate.sequence < current.sequence;\n if (this.stealing === 'release-first') {\n const released = candidate.releaseTime >= 0, currentReleased = current.releaseTime >= 0;\n if (released !== currentReleased) preferred = released;\n } else if (this.stealing === 'quietest' && audibility !== quietest) preferred = audibility < quietest;\n }\n if (preferred) {\n chosen = index;\n quietest = audibility;\n }\n }\n return chosen;\n }\n\n\n noteOn(input: VoiceInput | PreparedVoice, note: number, id?: number, options: NoteOptions = DEFAULT_NOTE_OPTIONS): number {\n const { velocity, pan, voicePriority } = readNoteOptions(options);\n const voice = preparedVoiceValue(input);\n if (typeof note !== 'number' || !Number.isFinite(note) || note < 0 || note > 127) {\n throw new RangeError('note must be finite and in 0..127');\n }\n if (id === undefined && this.nextId > Number.MAX_SAFE_INTEGER) throw new RangeError('Note ID space exhausted');\n if (id !== undefined && (!Number.isSafeInteger(id) || id <= 0)) throw new RangeError('id must be a positive safe integer');\n for (const active of this.voices) if (active.id === id) throw new RangeError('id already active');\n const stolenIndex = this.voices.length >= this.maxVoices ? this.stealingIndex(voicePriority) : -1;\n if (this.voices.length >= this.maxVoices && stolenIndex < 0) throw new VoiceAdmissionError();\n if (id === undefined) id = this.nextId++;\n if (id >= this.nextId) this.nextId = id + 1;\n // Initialize the spare before changing admission state. No slot can be reused\n // while it is still active or fading, including during terminal callbacks.\n const active = this.freeVoices.pop()!;\n const { baseIncrements, increments, sustainDb, sustainGain, steps, attackStep, decayStep, levels } = active;\n const frequency = tuningFrequency(note, this.tuning);\n const rate = this.sampleRate * this.oversample;\n for (let i = 0; i < 4; i++) {\n const op = voice.ops[i];\n active.ratios[i] = active.ratioFrom[i] = active.ratioTargets[i] = op.ratio;\n active.frequencyTargets[i] = op.frequency ?? 0;\n active.frequencies[i] = active.frequencyFrom[i] = op.frequency ?? frequency * op.ratio;\n const scale = op.keyScale;\n const attenuation = scale ? Math.abs(note - scale.breakpoint) / 12 *\n (note < scale.breakpoint ? scale.leftDbPerOctave : scale.rightDbPerOctave) : 0;\n levels[i] = op.level * 10 ** (-(attenuation + (op.velocitySensitivity ?? 0) * (1 - velocity)) / 20);\n active.waveforms[i] = waveformCode(op.waveform);\n active.noiseSteps[i] = (op.noiseRate ?? 8000) / rate;\n const operatorFrequency = (op.frequency ?? frequency * op.ratio) * 2 ** (op.detune / 1200);\n active.frequencyScales[i] = TAU * 2 ** (op.detune / 1200) / rate;\n baseIncrements[i] = TAU * operatorFrequency / rate;\n // Retain the original pre-PM cap, but keep unclipped pitch bases so a\n // downward bend can bring high-ratio operators back into audible range.\n increments[i] = TAU * Math.min(rate * 0.45, operatorFrequency) / rate;\n sustainDb[i] = op.adsr.s === 0 ? FLOOR_DB : Math.max(FLOOR_DB, 20 * Math.log10(op.adsr.s));\n sustainGain[i] = sustainDb[i] <= FLOOR_DB ? 0 : 10 ** (sustainDb[i] / 20);\n steps[i] = Math.min(TAU * 0.45, increments[i]);\n const attack = active.attackTimes[i] = operatorDuration(op.adsr.a, note, op.rateKeyScale);\n const decay = active.decayTimes[i] = operatorDuration(op.adsr.d, note, op.rateKeyScale);\n active.releaseTimes[i] = operatorDuration(op.adsr.r, note, op.rateKeyScale);\n attackStep[i] = attack === 0 ? 1 : 10 ** (-FLOOR_DB / (20 * attack * rate));\n decayStep[i] = decay === 0 ? 1 : 10 ** (sustainDb[i] / (20 * decay * rate));\n }\n active.id = id;\n active.voicePriority = voicePriority;\n active.note = note;\n active.sequence = this.sequence++;\n active.voice = voice;\n active.graph = ALGORITHMS[voice.algorithm];\n // Cache the fixed topology in pooled typed storage; frozen graph-array\n // indexing otherwise repeats for every operator at every oversampled frame.\n for (let op = 0; op < 4; op++) {\n active.inputIndices[op * 2] = active.graph.inputs[op][0] ?? -1;\n active.inputIndices[op * 2 + 1] = active.graph.inputs[op][1] ?? -1;\n }\n active.carrierIndices.set(active.graph.carriers);\n active.velocity = velocity;\n active.expression = 1;\n active.pan = pan;\n active.expressionFrom = active.expressionTarget = 1;\n active.expressionStart = active.expressionFrames = 0;\n active.panFrom = active.panTarget = pan;\n active.panStart = active.panFrames = 0;\n active.modulation = active.modulationFrom = active.modulationTarget = 1;\n active.modulationStart = active.modulationFrames = active.controlRamps = 0;\n active.modIndex = voice.modIndex;\n active.amDepth = voice.lfo.amDepth;\n active.pmDepth = voice.lfo.pmDepth;\n active.pitchFrom = active.pitchTarget = active.pitchStart = active.pitchFrames = 0;\n active.operatorLevels.fill(1);\n active.operatorFrom.fill(1);\n active.operatorTargets.fill(1);\n active.operatorStart = active.operatorFrames = 0;\n active.tunedFrequency = frequency;\n active.ratioStart = active.ratioFrames = active.frequencyStart = active.frequencyFrames = 0;\n active.scalarValues[0] = voice.feedback;\n active.scalarValues[1] = voice.lfo.rate;\n active.scalarValues[2] = voice.lfo.amDepth;\n active.scalarValues[3] = voice.lfo.pmDepth;\n active.scalarValues[4] = 1;\n active.scalarFrom.set(active.scalarValues);\n active.scalarTargets.set(active.scalarValues);\n active.scalarStarts.fill(0);\n active.scalarFrames.fill(0);\n active.envelopeStart.fill(0);\n active.envelopeFromDb.fill(FLOOR_DB);\n active.envelopeEdited.fill(0);\n active.releaseStarts.fill(0);\n active.lfoTurns = 0;\n active.lfoLive = false;\n active.pitchRelease = 0;\n updatePan(active);\n active.lastSample = active.fadeRemaining = 0;\n active.lastFadeGain = 1;\n active.carrierGain = HEADROOM / active.graph.carriers.length;\n active.phases.fill(0);\n active.noiseStates.fill(NOISE_SEED);\n active.noiseTurns.fill(0);\n active.values.fill(0);\n active.filters.fill(0);\n active.releaseDb.fill(0);\n active.releaseStep.fill(0);\n active.gains.fill(0);\n active.previous = active.older = active.elapsed = 0;\n active.feedbackScale = feedbackGain(voice.feedback);\n active.releaseTime = active.releaseEnd = -1;\n (this as MutableCounters).lastStolenId = null;\n if (this.voices.length >= this.maxVoices) {\n const stolen = removeAt(this.voices, stolenIndex);\n (this as MutableCounters).lastStolenId = stolen.id;\n // Preserve the old DSP state. Exhausted tails collapse into the existing\n // bounded spill ramp, then recycle before callbacks can admit more notes.\n if (this.fades.length === MAX_FADES) {\n const retired = removeAt(this.fades, 0);\n const gain = retired.lastFadeGain!;\n this.spillLeft += retired.lastSample * retired.leftGain * gain;\n this.spillRight += retired.lastSample * retired.rightGain * gain;\n this.spillRemaining = this.fadeFrames;\n this.freeVoices.push(retired);\n }\n stolen.fadeRemaining = this.fadeFrames;\n stolen.lastFadeGain = 1;\n this.fades.push(stolen);\n }\n this.voices.push(active);\n // Notify only after admission: callbacks may start another note reentrantly.\n if (this.lastStolenId !== null) this.ended(this.lastStolenId, 'stolen');\n return id;\n }\n\n noteOff(id: number): boolean {\n let active: ActiveVoice | undefined;\n for (const item of this.voices) if (item.id === id) { active = item; break; }\n if (!active || active.releaseTime >= 0) return false;\n this.releaseVoice(active);\n return true;\n }\n\n /** @internal */\n releaseVoice(active: ActiveVoice): void {\n const time = active.elapsed / this.sampleRate;\n active.pitchRelease = pitchEnvelopeAt(active, time);\n let maxRelease = 0;\n for (let op = 0; op < 4; op++) {\n active.releaseDb[op] = envelopeDbAt(active, time, op);\n active.releaseStarts[op] = time;\n const release = active.releaseTimes[op];\n maxRelease = Math.max(maxRelease, release);\n active.releaseStep[op] = release === 0 ? 0 :\n 10 ** ((FLOOR_DB - active.releaseDb[op]) / (20 * release * this.sampleRate * this.oversample));\n }\n active.releaseTime = time;\n active.releaseEnd = time + maxRelease;\n }\n allNotesOff(): void {\n for (let index = 0; index < this.voices.length; index++) {\n const active = this.voices[index];\n if (active.releaseTime < 0) this.releaseVoice(active);\n }\n }\n\n panic(): void {\n while (this.fades.length > 0) this.freeVoices.push(this.fades.pop()!);\n this.spillLeft = this.spillRight = this.spillRemaining = 0;\n (this as MutableCounters).lastStolenId = null;\n this.cancelVoices();\n }\n\n /** @internal */\n cancelVoices(): void {\n if (this.voices.length === 0) return;\n const active = this.voices.pop()!;\n const id = active.id;\n this.freeVoices.push(active);\n // At most maxVoices (32) stack-local IDs survive recycling. Unlike shared\n // scratch storage, they cannot be overwritten by reentrant callbacks.\n this.cancelVoices();\n this.ended(id, 'cancelled');\n }\n\n\n updateNote(id: number, input: NoteControls): boolean {\n const controls = validateNoteControls(input);\n let active: ActiveVoice | undefined;\n for (const item of this.voices) if (item.id === id) { active = item; break; }\n if (!active) return false;\n if (active.controlRamps !== 0) advanceControls(active);\n if (controls.pitch !== undefined) {\n active.pitchFrom = pitchAt(active, active.elapsed);\n active.pitchTarget = controls.pitch;\n active.pitchStart = active.elapsed;\n active.pitchFrames = (controls.glide ?? 0) * this.sampleRate;\n }\n const frames = (controls.ramp ?? 0) * this.sampleRate;\n if (controls.lfoRate !== undefined && !active.lfoLive) {\n const lfo = active.voice.lfo;\n const clock = lfo.sync === 'global' ? this.currentFrame : active.elapsed;\n active.lfoTurns = clock / this.sampleRate * active.scalarValues[1] + (lfo.phase ?? 0);\n active.lfoTurns -= Math.floor(active.lfoTurns);\n active.lfoLive = true;\n }\n for (let field = 0; field < SCALAR_FIELDS.length; field++) {\n const value = controls[SCALAR_FIELDS[field]];\n if (value === undefined) continue;\n active.scalarFrom[field] = active.scalarValues[field];\n active.scalarTargets[field] = value;\n active.scalarStarts[field] = active.elapsed;\n active.scalarFrames[field] = active.scalarValues[field] === value ? 0 : frames;\n if (active.scalarFrames[field] === 0) {\n active.scalarValues[field] = value;\n if (field === 0) active.feedbackScale = feedbackGain(value);\n if (field === 2 || field === 3) updateModulation(active);\n }\n active.controlRamps |= ENGINE_RAMP;\n }\n if (active.controlRamps & ENGINE_RAMP) advanceControls(active);\n if (controls.operatorRatios !== undefined || controls.operatorFrequencies !== undefined) {\n // Sample ongoing pitch-independent Hz before changing either timeline.\n for (let op = 0; op < 4; op++) {\n const ratio = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, active.elapsed);\n const target = active.frequencyTargets[op] || active.tunedFrequency * ratio;\n active.frequencies[op] = rampAt(active.frequencyFrom[op], target, active.frequencyStart, active.frequencyFrames, active.elapsed);\n }\n }\n if (controls.operatorRatios !== undefined) {\n for (let op = 0; op < 4; op++) {\n active.ratioFrom[op] = active.ratios[op];\n active.ratioTargets[op] = controls.operatorRatios[op];\n if (frames === 0) active.ratios[op] = controls.operatorRatios[op];\n }\n active.ratioStart = active.elapsed;\n active.ratioFrames = frames;\n if (frames !== 0) active.controlRamps |= RATIO_RAMP;\n else active.controlRamps &= ~RATIO_RAMP;\n }\n if (controls.operatorFrequencies !== undefined) {\n for (let op = 0; op < 4; op++) {\n active.frequencyFrom[op] = active.frequencies[op];\n active.frequencyTargets[op] = controls.operatorFrequencies[op] ?? 0;\n }\n active.frequencyStart = active.elapsed;\n active.frequencyFrames = frames;\n if (frames !== 0) active.controlRamps |= FREQUENCY_RAMP;\n else active.controlRamps &= ~FREQUENCY_RAMP;\n }\n if (controls.operatorRatios !== undefined || controls.operatorFrequencies !== undefined) this.retuneVoice(active);\n if (controls.operatorADSR !== undefined) {\n const time = active.elapsed / this.sampleRate;\n for (let op = 0; op < 4; op++) {\n const db = envelopeDbAt(active, time, op), adsr = controls.operatorADSR[op];\n const keyScale = active.voice.ops[op].rateKeyScale;\n active.envelopeFromDb[op] = db;\n active.envelopeStart[op] = time;\n active.envelopeEdited[op] = 1;\n active.attackTimes[op] = operatorDuration(adsr.a, active.note, keyScale);\n active.decayTimes[op] = operatorDuration(adsr.d, active.note, keyScale);\n if (active.attackTimes[op] === 0 && active.decayTimes[op] === 0) active.decayTimes[op] = 1 / this.sampleRate;\n active.sustainDb[op] = adsr.s === 0 ? FLOOR_DB : Math.max(FLOOR_DB, 20 * Math.log10(adsr.s));\n active.sustainGain[op] = active.sustainDb[op] <= FLOOR_DB ? 0 : 10 ** (active.sustainDb[op] / 20);\n active.releaseTimes[op] = operatorDuration(adsr.r, active.note, keyScale);\n if (active.releaseTime >= 0) {\n active.releaseDb[op] = db;\n active.releaseStarts[op] = time;\n }\n }\n if (active.releaseTime >= 0) {\n active.releaseEnd = time;\n for (let op = 0; op < 4; op++) active.releaseEnd = Math.max(active.releaseEnd, time + active.releaseTimes[op]);\n }\n }\n if (controls.expression !== undefined) {\n active.expressionFrom = active.expression;\n active.expressionTarget = controls.expression;\n active.expressionStart = active.elapsed;\n active.expressionFrames = active.expression === controls.expression ? 0 : frames;\n if (active.expressionFrames === 0) {\n active.expression = controls.expression;\n active.controlRamps &= ~EXPRESSION_RAMP;\n } else active.controlRamps |= EXPRESSION_RAMP;\n }\n if (controls.pan !== undefined) {\n active.panFrom = active.pan;\n active.panTarget = controls.pan;\n active.panStart = active.elapsed;\n active.panFrames = active.pan === controls.pan ? 0 : frames;\n if (active.panFrames === 0) {\n active.pan = controls.pan;\n active.controlRamps &= ~PAN_RAMP;\n } else active.controlRamps |= PAN_RAMP;\n }\n if (controls.expression !== undefined || controls.pan !== undefined) updatePan(active);\n if (controls.modulation !== undefined) {\n active.modulationFrom = active.modulation;\n active.modulationTarget = controls.modulation;\n active.modulationStart = active.elapsed;\n active.modulationFrames = active.modulation === controls.modulation ? 0 : frames;\n if (active.modulationFrames === 0) {\n active.modulation = controls.modulation;\n active.controlRamps &= ~MODULATION_RAMP;\n } else active.controlRamps |= MODULATION_RAMP;\n updateModulation(active);\n }\n if (controls.operatorLevels !== undefined) {\n let changed = false;\n for (let op = 0; op < 4; op++) {\n active.operatorFrom[op] = active.operatorLevels[op];\n active.operatorTargets[op] = controls.operatorLevels[op];\n if (active.operatorFrom[op] !== active.operatorTargets[op]) changed = true;\n if (frames === 0) active.operatorLevels[op] = controls.operatorLevels[op];\n }\n active.operatorStart = active.elapsed;\n active.operatorFrames = changed ? frames : 0;\n if (active.operatorFrames === 0) active.controlRamps &= ~OPERATOR_RAMP;\n else active.controlRamps |= OPERATOR_RAMP;\n }\n // Reset a former PM/glide step even when modulation becomes zero.\n if (controls.pitch !== undefined || controls.modulation !== undefined) {\n const factor = 2 ** (pitchAt(active, active.elapsed) / 12);\n for (let op = 0; op < 4; op++) {\n active.increments[op] = Math.min(TAU * 0.45, active.baseIncrements[op] * factor);\n active.steps[op] = active.increments[op];\n }\n }\n return true;\n }\n\n /** @internal */\n ended(id: number, reason: VoiceEndReason): void {\n if (typeof this.onVoiceEnded === 'function') {\n try { this.onVoiceEnded(id, reason); } catch { (this as MutableCounters).errorCount++; }\n }\n }\n\n // One output frame, with no temporary arrays or objects in the hot path.\n /** @internal */\n renderVoice(active: ActiveVoice): number {\n const { graph, inputIndices, carrierIndices, phases, values, filters, increments, steps, gains, levels } = active;\n if (active.controlRamps !== 0) advanceControls(active);\n const time = active.elapsed / this.sampleRate;\n const finished = active.releaseTime >= 0 && time >= active.releaseEnd;\n const lfo = active.voice.lfo;\n const lfoEnabled = time >= (lfo.delay ?? 0);\n let tremolo = 0;\n if (lfoEnabled && (active.amDepth !== 0 || active.pmDepth !== 0)) {\n const clock = lfo.sync === 'global' ? this.currentFrame : active.elapsed;\n const turns = active.lfoLive ? active.lfoTurns : clock / this.sampleRate * lfo.rate + (lfo.phase ?? 0);\n tremolo = lfoValue((turns - Math.floor(turns)) * TAU, lfo.waveform);\n }\n const amGain = !lfoEnabled || active.amDepth === 0 ? 1 : 1 - active.amDepth * (0.5 + 0.5 * tremolo);\n const pmFactor = !lfoEnabled || active.pmDepth === 0 ? 1 : 2 ** (active.pmDepth * tremolo / 1200);\n if (lfo.amTargets || lfo.pmTargets) {\n for (let op = 0; op < 4; op++) {\n const am = lfo.amTargets?.[op] ?? 1, pm = lfo.pmTargets?.[op] ?? 1;\n active.amGains[op] = am === 1 ? amGain : !lfoEnabled ? 1 : 1 - active.amDepth * am * (0.5 + 0.5 * tremolo);\n active.pmFactors[op] = pm === 1 ? pmFactor : !lfoEnabled ? 1 : 2 ** (active.pmDepth * pm * tremolo / 1200);\n }\n }\n const gliding = active.pitchFrames !== 0;\n const frequencyRamping = active.ratioFrames !== 0 || active.frequencyFrames !== 0;\n const dynamicPitch = gliding || active.voice.pitchEnvelope !== undefined || frequencyRamping;\n if (!dynamicPitch && active.pmDepth !== 0) {\n for (let op = 0; op < 4; op++) {\n const factor = lfo.pmTargets ? active.pmFactors[op] : pmFactor;\n steps[op] = Math.min(TAU * 0.45, increments[op] * factor);\n }\n }\n const operatorRamping = (active.controlRamps & OPERATOR_RAMP) !== 0;\n if (!finished) prepareGains(active, time, this.subTimes);\n let output = 0;\n const oversample = this.oversample, modIndex = active.modIndex, feedbackScale = active.feedbackScale;\n const operatorLevels = active.operatorLevels, waveforms = active.waveforms;\n // All-sine voices with static operator levels and shared AM take the branch-light path below.\n const simple = waveforms[0] === 0 && waveforms[1] === 0 && waveforms[2] === 0 && waveforms[3] === 0 &&\n !operatorRamping && !lfo.amTargets;\n for (let sub = 0; sub < oversample; sub++) {\n const frame = active.elapsed + sub / this.oversample;\n if (dynamicPitch) {\n const factor = 2 ** (pitchAt(active, frame) / 12 + pitchEnvelopeAt(active, time + this.subTimes[sub]) / 1200);\n for (let op = 0; op < 4; op++) {\n if (frequencyRamping) {\n const ratio = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, frame);\n const target = active.frequencyTargets[op] || active.tunedFrequency * ratio;\n const hz = rampAt(active.frequencyFrom[op], target, active.frequencyStart, active.frequencyFrames, frame);\n active.baseIncrements[op] = hz * active.frequencyScales[op];\n }\n const pm = lfo.pmTargets ? active.pmFactors[op] : pmFactor;\n steps[op] = Math.min(TAU * 0.45, Math.min(TAU * 0.45, active.baseIncrements[op] * factor) * pm);\n }\n }\n let sample = 0;\n if (!finished) {\n if (simple) {\n const base = sub * 4;\n for (let op = 0; op < 4; op++) {\n let modulation = op === 0 ? (active.previous + active.older) * feedbackScale : 0;\n const first = inputIndices[op * 2], second = inputIndices[op * 2 + 1];\n if (first >= 0) modulation += values[first] * modIndex;\n if (second >= 0) modulation += values[second] * modIndex;\n const phase = phases[op];\n values[op] = fastSin(phase + modulation) * gains[base + op] * levels[op] * operatorLevels[op] * amGain;\n const next = phase + steps[op];\n phases[op] = next < TAU ? next : next - TAU;\n }\n } else {\n for (let op = 0; op < 4; op++) {\n let modulation = op === 0 ? (active.previous + active.older) * active.feedbackScale : 0;\n const first = inputIndices[op * 2], second = inputIndices[op * 2 + 1];\n if (first >= 0) modulation += values[first] * active.modIndex;\n if (second >= 0) modulation += values[second] * active.modIndex;\n const operatorLevel = operatorRamping ?\n rampAt(active.operatorFrom[op], active.operatorTargets[op], active.operatorStart, active.operatorFrames, frame) : active.operatorLevels[op];\n const am = lfo.amTargets ? active.amGains[op] : amGain;\n const code = active.waveforms[op];\n let oscillator: number;\n if (code === 0) oscillator = fastSin(phases[op] + modulation);\n else if (code === 8) {\n oscillator = (active.noiseStates[op] & 1) === 0 ? -1 : 1;\n const turns = active.noiseTurns[op] + active.noiseSteps[op];\n // At the minimum clock a 20 kHz hold rate needs at most two ticks.\n const ticks = Math.floor(turns);\n for (let tick = 0; tick < ticks; tick++) active.noiseStates[op] = advanceNoise(active.noiseStates[op]);\n active.noiseTurns[op] = turns - ticks;\n } else oscillator = periodicWaveform(phases[op] + modulation, code, steps[op] * INV_TAU);\n const value = oscillator * gains[op + sub * 4] * levels[op] * operatorLevel * am;\n values[op] = value;\n const phase = phases[op] + steps[op];\n const wrapped = phase < TAU ? phase : phase - TAU;\n phases[op] = wrapped;\n if (!Number.isFinite(value) || !Number.isFinite(wrapped)) return NaN;\n }\n }\n active.older = active.previous;\n active.previous = values[0];\n for (let j = 0; j < graph.carriers.length; j++) sample += values[carrierIndices[j]];\n sample *= active.carrierGain;\n // Non-finite operator state always reaches a carrier through modulation or the mix.\n if (!Number.isFinite(sample)) return NaN;\n }\n output = decimateSample(sample, filters, this.decimatorCoefficients);\n if (!Number.isFinite(output)) return NaN;\n }\n if (active.lfoLive) {\n active.lfoTurns += active.scalarValues[1] / this.sampleRate;\n active.lfoTurns -= Math.floor(active.lfoTurns);\n }\n active.elapsed++;\n if (gliding && active.elapsed >= active.pitchStart + active.pitchFrames) {\n active.pitchFrames = 0;\n const factor = 2 ** (active.pitchTarget / 12);\n for (let op = 0; op < 4; op++) {\n increments[op] = Math.min(TAU * 0.45, active.baseIncrements[op] * factor);\n steps[op] = increments[op];\n }\n }\n if (frequencyRamping) {\n if (active.elapsed >= active.ratioStart + active.ratioFrames) active.ratioFrames = 0;\n if (active.elapsed >= active.frequencyStart + active.frequencyFrames) {\n active.frequencyFrames = 0;\n active.controlRamps &= ~FREQUENCY_RAMP;\n }\n if (active.ratioFrames === 0 && active.frequencyFrames === 0) this.retuneVoice(active);\n }\n active.lastSample = output * active.scalarValues[4];\n return active.lastSample;\n }\n\n render(left: Float32Array, right: Float32Array, offset = 0, length?: number): void {\n if (this.rendering) throw new Error('render cannot be called reentrantly');\n if (typedArrayKind.call(left) !== 'Float32Array' || typedArrayKind.call(right) !== 'Float32Array') {\n throw new RangeError('render requires native Float32Arrays');\n }\n const leftLength = typedArrayLength.call(left), rightLength = typedArrayLength.call(right);\n if (!Number.isSafeInteger(offset) || offset < 0) {\n throw new RangeError('render requires an in-bounds offset and length');\n }\n if (length === undefined) length = leftLength - offset;\n if (!Number.isSafeInteger(length) || length < 0 ||\n offset + length > leftLength || offset + length > rightLength) {\n throw new RangeError('render requires an in-bounds offset and length');\n }\n if (this.voices.length === 0 && this.fades.length === 0 && this.spillRemaining === 0) {\n typedArrayFill.call(left, 0, offset, offset + length);\n if (right !== left) typedArrayFill.call(right, 0, offset, offset + length);\n (this as MutableCounters).currentFrame += length;\n return;\n }\n this.rendering = true;\n try {\n for (let frame = offset, end = offset + length; frame < end; frame++) {\n let mixedLeft = 0, mixedRight = 0, terminalCount = 0;\n for (let index = 0; index < this.voices.length; index++) {\n const active = this.voices[index];\n const sample = this.renderVoice(active);\n if (!Number.isFinite(sample)) {\n (this as MutableCounters).errorCount++;\n this.terminalIds[terminalCount] = active.id;\n this.terminalErrors[terminalCount++] = 1;\n this.freeVoices.push(removeAt(this.voices, index--));\n continue;\n }\n mixedLeft += sample * active.leftGain;\n mixedRight += sample * active.rightGain;\n let tailPeak = Math.abs(sample);\n if (active.releaseTime >= 0 && (active.elapsed - 1) / this.sampleRate >= active.releaseEnd) {\n tailPeak = Math.max(tailPeak, decimatorPeak(active.filters));\n } else tailPeak = Infinity;\n if (tailPeak < AMPLITUDE_FLOOR) {\n this.terminalIds[terminalCount] = active.id;\n this.terminalErrors[terminalCount++] = 0;\n this.freeVoices.push(removeAt(this.voices, index--));\n }\n }\n for (let index = 0; index < this.fades.length; index++) {\n const active = this.fades[index];\n const sample = this.renderVoice(active);\n if (!Number.isFinite(sample)) {\n (this as MutableCounters).errorCount++;\n this.freeVoices.push(removeAt(this.fades, index--));\n continue;\n }\n const gain = active.fadeRemaining / this.fadeFrames;\n active.lastFadeGain = gain;\n mixedLeft += sample * active.leftGain * gain;\n mixedRight += sample * active.rightGain * gain;\n if (--active.fadeRemaining === 0) this.freeVoices.push(removeAt(this.fades, index--));\n }\n if (this.spillRemaining > 0) {\n mixedLeft += this.spillLeft;\n mixedRight += this.spillRight;\n const next = --this.spillRemaining;\n this.spillLeft *= next / (next + 1);\n this.spillRight *= next / (next + 1);\n }\n // Saturate each stereo sum independently, preserving center dual mono.\n left[frame] = Number.isFinite(mixedLeft) ? HEADROOM * Math.tanh(mixedLeft * this.mixGain / HEADROOM) : 0;\n right[frame] = Number.isFinite(mixedRight) ? HEADROOM * Math.tanh(mixedRight * this.mixGain / HEADROOM) : 0;\n if (!Number.isFinite(mixedLeft) || !Number.isFinite(mixedRight)) (this as MutableCounters).errorCount++;\n // Callbacks may admit/release notes, but replacements first render on the\n // next frame. Live traversal would let a zero-release chain run forever.\n for (let index = 0; index < terminalCount; index++) {\n this.ended(this.terminalIds[index], this.terminalErrors[index] ? 'error' : 'ended');\n }\n (this as MutableCounters).currentFrame++;\n }\n } finally {\n this.rendering = false;\n }\n }\n}\n"],"mappings":"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","ignoreList":[]} +{"version":3,"file":"synth.js","names":["normalizeTuning","tuningFrequency","lfoValue","DECIMATOR_STATE_SIZE","createDecimatorCoefficients","decimateSample","decimatorPeak","qualityOversample","typedArrayPrototype","Object","getPrototypeOf","Float32Array","prototype","typedArrayLength","getOwnPropertyDescriptor","get","typedArrayKind","Symbol","toStringTag","typedArrayFill","fill","VoiceAdmissionError","Error","constructor","super","this","name","validateMaxVoices","value","Number","isInteger","RangeError","validateVoicePriority","CONTROL_LIMITS","freeze","pitch","glide","expression","pan","modulation","ramp","feedback","lfoRate","amDepth","pmDepth","gain","validateNoteControls","input","Array","isArray","TypeError","keys","Reflect","ownKeys","length","result","key","includes","hasOwn","descriptor","tuple","op","element","String","item","adsr","a","d","s","r","field","data","number","isFinite","push","min","max","defineProperty","enumerable","control","some","ALGORITHMS","FLOOR_DB","TAU","waveformCode","periodicWaveform","fastSin","NOISE_SEED","advanceNoise","preparedVoiceValue","normalizeVoice","prepareVoice","HEADROOM","INV_TAU","AMPLITUDE_FLOOR","DB_TO_LOG_GAIN","Math","LN10","DEFAULT_NOTE_OPTIONS","velocity","voicePriority","DEFAULT_TUNING","SCALAR_FIELDS","feedbackGain","PI","operatorDuration","seconds","note","rateKeyScale","validateMixGain","readSynthOptions","mixGain","quality","stealing","tuning","pitchAt","active","frame","pitchFrames","pitchStart","pitchTarget","pitchFrom","rampAt","from","target","start","frames","pitchEnvelopeAt","time","envelope","voice","pitchEnvelope","releaseTime","final","pitchRelease","initial","peak","sustain","updateModulation","modIndex","scalarValues","steps","increments","advanceControls","ramps","controlRamps","elapsed","expressionFrom","expressionTarget","expressionStart","expressionFrames","panFrom","panTarget","panStart","panFrames","updatePan","modulationFrom","modulationTarget","modulationStart","modulationFrames","operatorLevels","operatorFrom","operatorTargets","operatorStart","operatorFrames","pending","modulationChanged","scalarFrames","scalarFrom","scalarTargets","scalarStarts","feedbackScale","ratios","ratioFrom","ratioTargets","ratioStart","ratioFrames","angle","leftGain","SQRT2","cos","rightGain","sin","removeAt","items","index","removed","i","pop","heldDb","attack","decay","sustainDb","envelopeDbAt","release","releaseTimes","releaseStarts","releaseDb","envelopeEdited","attackTimes","decayTimes","envelopeStart","envelopeFromDb","gainAt","db","exp","sustainGain","voiceAudibility","sampleRate","graph","carriers","level","levels","layerGain","carrierGain","settleDecimator","state","clear","Synth","maxVoices","options","settings","oversample","voices","fades","freeVoices","createVoiceSlot","id","sequence","inputIndices","Int8Array","carrierIndices","Uint8Array","baseIncrements","Float64Array","attackStep","decayStep","releaseStep","gains","lastSample","fadeRemaining","lastFadeGain","phases","values","previous","older","releaseEnd","frequencies","frequencyFrom","frequencyTargets","frequencyStart","frequencyFrames","lfoTurns","lfoLive","tunedFrequency","frequencyScales","amGains","pmFactors","waveforms","noiseStates","Uint32Array","noiseTurns","noiseSteps","terminalIds","terminalErrors","rendering","fadeFrames","ceil","spillLeft","spillRight","spillRemaining","lastStolenId","onVoiceEnded","currentFrame","errorCount","nextId","subTimes","_","sub","decimatorCoefficients","mixLeft","mixRight","filterLeft","filterRight","decimatorIdle","decimatorMirrored","setMixGain","setTuning","normalized","retuneVoice","frequency","rate","factor","source","ops","ratio","hz","detune","stealingIndex","chosen","quietest","candidate","current","preferred","audibility","released","noteOn","readNoteOptions","MAX_SAFE_INTEGER","isSafeInteger","stolenIndex","scale","keyScale","attenuation","abs","breakpoint","leftDbPerOctave","rightDbPerOctave","velocitySensitivity","waveform","noiseRate","operatorFrequency","log10","algorithm","inputs","set","lfo","stolen","retired","ended","noteOff","releaseVoice","maxRelease","allNotesOff","panic","resetDecimator","cancelVoices","updateNote","controls","clock","sync","phase","floor","operatorRatios","operatorFrequencies","operatorADSR","changed","reason","renderVoice","fadeGain","voiceGain","panLeft","panRight","finished","lfoEnabled","delay","tremolo","turns","amGain","pmFactor","amTargets","pmTargets","am","pm","gliding","frequencyRamping","dynamicPitch","operatorRamping","lastTime","step","prepareGains","output","simple","sample","base","first","second","next","operatorLevel","code","oscillator","ticks","tick","wrapped","NaN","j","render","left","right","offset","call","leftLength","rightLength","coefficients","end","terminalCount","mixedLeft","mixedRight","silentLeft","silentRight","mirrored","inputLeft","inputRight","tanh","idleLeft","idleRight"],"sources":["../../src/core/synth.ts"],"sourcesContent":["import type { ADSR, PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { AlgorithmGraph } from './algorithms.js';\nimport { normalizeTuning, tuningFrequency } from './tuning.js';\nimport type { NormalizedTuning, TuningOptions } from './tuning.js';\nimport { lfoValue } from './lfo.js';\nimport { DECIMATOR_STATE_SIZE, createDecimatorCoefficients, decimateSample, decimatorPeak, qualityOversample } from './decimator.js';\nimport type { QualityProfile } from './decimator.js';\nexport type { QualityProfile } from './decimator.js';\n\nconst typedArrayPrototype: object = Object.getPrototypeOf(Float32Array.prototype);\nconst typedArrayLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, 'length')!.get! as (this: unknown) => number;\nconst typedArrayKind = Object.getOwnPropertyDescriptor(typedArrayPrototype, Symbol.toStringTag)!.get! as (this: unknown) => string | undefined;\nconst typedArrayFill = Float32Array.prototype.fill;\n\nexport type VoiceEndReason = 'stolen' | 'ended' | 'error' | 'cancelled';\nexport interface NoteOptions { velocity?: number; pan?: number; voicePriority?: number }\nexport interface NoteControls {\n pitch?: number; glide?: number; expression?: number; pan?: number; modulation?: number; ramp?: number;\n /** Independent per-note amplitude, 0..1 (default 1), multiplied with expression before mix saturation. */\n gain?: number;\n operatorLevels?: readonly [number, number, number, number];\n feedback?: number; lfoRate?: number; amDepth?: number; pmDepth?: number;\n operatorRatios?: readonly [number, number, number, number];\n /** A number enables fixed Hz; null restores the operator's live ratio. Pitch still applies. */\n operatorFrequencies?: readonly [number | null, number | null, number | null, number | null];\n operatorADSR?: readonly [ADSR, ADSR, ADSR, ADSR];\n}\nexport interface SynthOptions {\n mixGain?: number;\n tuning?: TuningOptions;\n stealing?: 'oldest' | 'release-first' | 'quietest';\n quality?: QualityProfile;\n}\n\n/** A valid note could not displace any higher-priority logical voice. */\nexport class VoiceAdmissionError extends Error {\n constructor() {\n super('All active voices have higher voicePriority');\n this.name = 'VoiceAdmissionError';\n }\n}\n\nexport function validateMaxVoices(value: unknown): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 32) {\n throw new RangeError('maxVoices must be an integer in 1..32');\n }\n return value;\n}\n\nexport function validateVoicePriority(value: unknown): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 127) {\n throw new RangeError('voicePriority must be an integer in 0..127');\n }\n return value;\n}\n\n/** Snapshot note admission controls without invoking inherited values/accessors. */\nfunction readNoteOptions(input: NoteOptions): Required {\n if (!input || typeof input !== 'object' || Array.isArray(input) ||\n (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)) {\n throw new TypeError('note options must be a plain data object');\n }\n const result = { velocity: 1, pan: 0, voicePriority: 0 };\n for (const key of Reflect.ownKeys(input)) {\n if (key !== 'velocity' && key !== 'pan' && key !== 'voicePriority') throw new TypeError('note options has an unknown field');\n const descriptor = Object.getOwnPropertyDescriptor(input, key)!;\n if (!Object.hasOwn(descriptor, 'value')) throw new TypeError(`note options.${key} must be data`);\n const value: unknown = descriptor.value;\n if (key === 'voicePriority') result.voicePriority = validateVoicePriority(value);\n else {\n const min = key === 'pan' ? -1 : 0;\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > 1) {\n throw new RangeError(`${key} must be finite and in ${min}..1`);\n }\n result[key] = value;\n }\n }\n return result;\n}\n\nconst CONTROL_LIMITS = Object.freeze({\n pitch: [-48, 48], glide: [0, 10], expression: [0, 1], pan: [-1, 1], modulation: [0, 2], ramp: [0, 10],\n feedback: [0, 7], lfoRate: [0, 20], amDepth: [0, 1], pmDepth: [0, 1200], gain: [0, 1],\n} as const);\n\n/** Copy strict own-data controls at the API/dispatch boundary without invoking getters. */\nexport function validateNoteControls(input: NoteControls): NoteControls {\n if (!input || typeof input !== 'object' || Array.isArray(input) ||\n (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)) {\n throw new TypeError('controls must be a plain object');\n }\n const keys = Reflect.ownKeys(input);\n if (keys.length === 0) throw new TypeError('controls must not be empty');\n const result: NoteControls = {};\n for (const key of keys) {\n if (typeof key !== 'string' || !['operatorLevels', 'operatorRatios', 'operatorFrequencies', 'operatorADSR'].includes(key) && !Object.hasOwn(CONTROL_LIMITS, key)) throw new TypeError('controls has an unknown field');\n const descriptor = Object.getOwnPropertyDescriptor(input, key)!;\n if (!Object.hasOwn(descriptor, 'value')) throw new TypeError(`controls.${key} must be data`);\n const value: unknown = descriptor.value;\n if (key === 'operatorLevels' || key === 'operatorRatios' || key === 'operatorFrequencies' || key === 'operatorADSR') {\n if (!Array.isArray(value) || value.length !== 4 || Reflect.ownKeys(value).length !== 5 ||\n Object.getPrototypeOf(value) !== Array.prototype) throw new TypeError(`${key} must be a dense four-element tuple`);\n const tuple: unknown[] = [];\n for (let op = 0; op < 4; op++) {\n const element = Object.getOwnPropertyDescriptor(value, String(op));\n if (!element || !Object.hasOwn(element, 'value')) throw new TypeError(`${key} must contain data`);\n const item: unknown = element.value;\n if (key === 'operatorADSR') {\n if (!item || typeof item !== 'object' || Array.isArray(item) ||\n (Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) ||\n Reflect.ownKeys(item).length !== 4) throw new TypeError('operatorADSR requires full plain ADSR objects');\n const adsr: ADSR = { a: 0, d: 0, s: 0, r: 0 };\n for (const field of ['a', 'd', 's', 'r'] as const) {\n const data = Object.getOwnPropertyDescriptor(item, field);\n if (!data || !Object.hasOwn(data, 'value')) throw new TypeError('operatorADSR must contain data');\n const number: unknown = data.value;\n if (typeof number !== 'number' || !Number.isFinite(number) || number < 0 || number > (field === 's' ? 1 : 10)) {\n throw new RangeError('operatorADSR has an out-of-range value');\n }\n adsr[field] = number;\n }\n tuple.push(Object.freeze(adsr));\n } else {\n const min = key === 'operatorRatios' ? 0.125 : key === 'operatorFrequencies' ? 1 : 0;\n const max = key === 'operatorRatios' ? 32 : key === 'operatorFrequencies' ? 20000 : 2;\n if (key !== 'operatorFrequencies' || item !== null) {\n if (typeof item !== 'number' || !Number.isFinite(item) || item < min || item > max) throw new RangeError(`${key} must be finite and in ${min}..${max}`);\n }\n tuple.push(item);\n }\n }\n Object.defineProperty(result, key, { value: Object.freeze(tuple), enumerable: true });\n continue;\n }\n const control = key as keyof typeof CONTROL_LIMITS;\n const [min, max] = CONTROL_LIMITS[control];\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {\n throw new RangeError(`controls.${key} must be finite and in ${min}..${max}`);\n }\n result[control] = value;\n }\n if (Object.hasOwn(result, 'glide') && !Object.hasOwn(result, 'pitch')) throw new TypeError('glide requires pitch');\n if (Object.hasOwn(result, 'ramp') && !Reflect.ownKeys(result).some(key => key !== 'ramp' && key !== 'pitch' && key !== 'glide' && key !== 'operatorADSR')) {\n throw new TypeError('ramp requires a rampable control');\n }\n return Object.freeze(result);\n}\n\ninterface ActiveVoice {\n id: number; sequence: number; note: number; voicePriority: number; voice: PreparedVoice; graph: AlgorithmGraph;\n inputIndices: Int8Array; carrierIndices: Uint8Array;\n baseIncrements: Float64Array; increments: Float64Array; steps: Float64Array; sustainDb: Float64Array; sustainGain: Float64Array;\n attackStep: Float64Array; decayStep: Float64Array; releaseStep: Float64Array; gains: Float64Array;\n attackTimes: Float64Array; decayTimes: Float64Array; releaseTimes: Float64Array;\n operatorLevels: Float64Array; operatorFrom: Float64Array; operatorTargets: Float64Array;\n operatorStart: number; operatorFrames: number;\n levels: Float64Array; leftGain: number; rightGain: number; lastSample: number; fadeRemaining: number;\n lastFadeGain?: number; carrierGain: number; phases: Float64Array; values: Float64Array;\n releaseDb: Float64Array; previous: number; older: number; pitchRelease: number;\n feedbackScale: number; elapsed: number; releaseTime: number; releaseEnd: number;\n velocity: number; expression: number; pan: number;\n expressionFrom: number; expressionTarget: number; expressionStart: number; expressionFrames: number;\n panFrom: number; panTarget: number; panStart: number; panFrames: number;\n modulation: number; modulationFrom: number; modulationTarget: number; modulationStart: number; modulationFrames: number;\n controlRamps: number;\n modIndex: number; amDepth: number; pmDepth: number;\n pitchFrom: number; pitchTarget: number; pitchStart: number; pitchFrames: number;\n scalarValues: Float64Array; scalarFrom: Float64Array; scalarTargets: Float64Array; scalarStarts: Float64Array; scalarFrames: Float64Array;\n ratios: Float64Array; ratioFrom: Float64Array; ratioTargets: Float64Array; ratioStart: number; ratioFrames: number;\n frequencies: Float64Array; frequencyFrom: Float64Array; frequencyTargets: Float64Array; frequencyStart: number; frequencyFrames: number;\n envelopeStart: Float64Array; envelopeFromDb: Float64Array; envelopeEdited: Uint8Array; releaseStarts: Float64Array;\n lfoTurns: number; lfoLive: boolean;\n tunedFrequency: number;\n frequencyScales: Float64Array;\n amGains: Float64Array; pmFactors: Float64Array;\n waveforms: Uint8Array; noiseStates: Uint32Array; noiseTurns: Float64Array; noiseSteps: Float64Array;\n}\n\ninterface MutableCounters {\n currentFrame: number; errorCount: number; lastStolenId: number | null;\n}\n\nimport { ALGORITHMS } from './algorithms.js';\nimport { FLOOR_DB } from './envelope.js';\nimport { TAU, waveformCode, periodicWaveform, fastSin, NOISE_SEED, advanceNoise } from './operator.js';\nimport { preparedVoiceValue } from '../voices/normalize.js';\nexport { normalizeVoice, prepareVoice } from '../voices/normalize.js';\n\nconst HEADROOM = 0.7;\nconst DEFAULT_MAX_VOICES = 8;\nconst MAX_FADES = 8;\nconst INV_TAU = 1 / TAU;\nconst AMPLITUDE_FLOOR = 10 ** (FLOOR_DB / 20);\nconst DB_TO_LOG_GAIN = Math.LN10 / 20;\nconst DEFAULT_NOTE_OPTIONS: NoteOptions = Object.freeze({ velocity: 1, pan: 0, voicePriority: 0 });\nconst DEFAULT_TUNING = normalizeTuning({});\nconst EXPRESSION_RAMP = 1;\nconst PAN_RAMP = 2;\nconst MODULATION_RAMP = 4;\nconst OPERATOR_RAMP = 8;\nconst ENGINE_RAMP = 16;\nconst RATIO_RAMP = 32;\nconst FREQUENCY_RAMP = 64;\nconst SCALAR_FIELDS = ['feedback', 'lfoRate', 'amDepth', 'pmDepth', 'gain'] as const;\n\nfunction feedbackGain(value: number): number {\n return value <= 1 ? value * 0.5 * 2 ** -6 * Math.PI : 0.5 * 2 ** (value - 7) * Math.PI;\n}\n\n/** @internal Scale once per admission; low-note durations never exceed the voice's ten-second budget. */\nexport function operatorDuration(seconds: number, note: number, rateKeyScale = 0): number {\n return Math.min(10, seconds * 2 ** (-rateKeyScale * (note - 60) / 12));\n}\n\nfunction validateMixGain(gain: number): void {\n if (typeof gain !== 'number' || !Number.isFinite(gain) || gain < 0 || gain > 1) {\n throw new RangeError('mixGain must be finite and in 0..1');\n }\n}\n\n/** @internal Validate structural settings without allocating voice slots; tuning is validated separately by normalizeTuning. */\nexport function readSynthOptions(input: SynthOptions): SynthOptions {\n if (!input || typeof input !== 'object' || Array.isArray(input) ||\n (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)) {\n throw new TypeError('options must be a plain object');\n }\n const result: SynthOptions = {};\n for (const key of Reflect.ownKeys(input)) {\n if (key !== 'mixGain' && key !== 'tuning' && key !== 'stealing' && key !== 'quality') throw new TypeError('options has an unknown field');\n const descriptor = Object.getOwnPropertyDescriptor(input, key)!;\n if (!Object.hasOwn(descriptor, 'value')) throw new TypeError(`options.${key} must be data`);\n if (key === 'mixGain') {\n validateMixGain(descriptor.value);\n result.mixGain = descriptor.value;\n } else if (key === 'quality') {\n qualityOversample(descriptor.value);\n result.quality = descriptor.value;\n } else if (key === 'stealing') {\n if (descriptor.value !== 'oldest' && descriptor.value !== 'release-first' && descriptor.value !== 'quietest') {\n throw new TypeError('stealing must be oldest, release-first or quietest');\n }\n result.stealing = descriptor.value;\n } else {\n result.tuning = descriptor.value;\n }\n }\n return result;\n}\n\nfunction createVoiceSlot(oversample: number): ActiveVoice {\n return {\n id: 0, sequence: 0, note: 0, voicePriority: 0, voice: null!, graph: ALGORITHMS[0],\n inputIndices: new Int8Array(8), carrierIndices: new Uint8Array(4),\n baseIncrements: new Float64Array(4), increments: new Float64Array(4), steps: new Float64Array(4),\n sustainDb: new Float64Array(4), sustainGain: new Float64Array(4),\n attackStep: new Float64Array(4), decayStep: new Float64Array(4), releaseStep: new Float64Array(4),\n attackTimes: new Float64Array(4), decayTimes: new Float64Array(4), releaseTimes: new Float64Array(4),\n operatorLevels: new Float64Array(4), operatorFrom: new Float64Array(4), operatorTargets: new Float64Array(4),\n operatorStart: 0, operatorFrames: 0,\n gains: new Float64Array(4 * oversample), levels: new Float64Array(4),\n leftGain: 0, rightGain: 0, lastSample: 0, fadeRemaining: 0, lastFadeGain: 1, carrierGain: 0,\n phases: new Float64Array(4), values: new Float64Array(4), releaseDb: new Float64Array(4),\n previous: 0, older: 0, pitchRelease: 0, feedbackScale: 0,\n elapsed: 0, releaseTime: -1, releaseEnd: -1, velocity: 1, expression: 1, pan: 0,\n expressionFrom: 1, expressionTarget: 1, expressionStart: 0, expressionFrames: 0,\n panFrom: 0, panTarget: 0, panStart: 0, panFrames: 0,\n modulation: 1, modulationFrom: 1, modulationTarget: 1, modulationStart: 0, modulationFrames: 0,\n controlRamps: 0,\n modIndex: 0, amDepth: 0, pmDepth: 0, pitchFrom: 0, pitchTarget: 0, pitchStart: 0, pitchFrames: 0,\n scalarValues: new Float64Array(SCALAR_FIELDS.length), scalarFrom: new Float64Array(SCALAR_FIELDS.length),\n scalarTargets: new Float64Array(SCALAR_FIELDS.length), scalarStarts: new Float64Array(SCALAR_FIELDS.length), scalarFrames: new Float64Array(SCALAR_FIELDS.length),\n ratios: new Float64Array(4), ratioFrom: new Float64Array(4), ratioTargets: new Float64Array(4), ratioStart: 0, ratioFrames: 0,\n frequencies: new Float64Array(4), frequencyFrom: new Float64Array(4), frequencyTargets: new Float64Array(4), frequencyStart: 0, frequencyFrames: 0,\n envelopeStart: new Float64Array(4), envelopeFromDb: new Float64Array(4), envelopeEdited: new Uint8Array(4), releaseStarts: new Float64Array(4),\n lfoTurns: 0, lfoLive: false,\n tunedFrequency: 0,\n frequencyScales: new Float64Array(4),\n amGains: new Float64Array(4), pmFactors: new Float64Array(4),\n waveforms: new Uint8Array(4), noiseStates: new Uint32Array(4), noiseTurns: new Float64Array(4), noiseSteps: new Float64Array(4),\n };\n}\n\nfunction pitchAt(active: ActiveVoice, frame: number): number {\n if (active.pitchFrames === 0 || frame >= active.pitchStart + active.pitchFrames) return active.pitchTarget;\n return active.pitchFrom + (active.pitchTarget - active.pitchFrom) * (frame - active.pitchStart) / active.pitchFrames;\n}\nfunction rampAt(from: number, target: number, start: number, frames: number, frame: number): number {\n if (frames === 0 || frame >= start + frames) return target;\n return from + (target - from) * (frame - start) / frames;\n}\n\nfunction pitchEnvelopeAt(active: ActiveVoice, time: number): number {\n const envelope = active.voice.pitchEnvelope;\n if (!envelope) return 0;\n if (active.releaseTime >= 0) {\n if (envelope.r === 0 || time >= active.releaseTime + envelope.r) return envelope.final;\n return active.pitchRelease + (envelope.final - active.pitchRelease) * (time - active.releaseTime) / envelope.r;\n }\n if (envelope.a > 0 && time < envelope.a) return envelope.initial + (envelope.peak - envelope.initial) * time / envelope.a;\n if (envelope.d > 0 && time < envelope.a + envelope.d) return envelope.peak + (envelope.sustain - envelope.peak) * (time - envelope.a) / envelope.d;\n return envelope.sustain;\n}\n\nfunction updateModulation(active: ActiveVoice): void {\n active.modIndex = active.voice.modIndex * active.modulation;\n active.amDepth = Math.min(1, active.scalarValues[2] * active.modulation);\n active.pmDepth = Math.min(1200, active.scalarValues[3] * active.modulation);\n if (active.pmDepth === 0) {\n for (let op = 0; op < 4; op++) active.steps[op] = active.increments[op];\n }\n}\n\nfunction advanceControls(active: ActiveVoice): void {\n const ramps = active.controlRamps;\n const frame = active.elapsed;\n if (ramps & EXPRESSION_RAMP) {\n active.expression = rampAt(active.expressionFrom, active.expressionTarget, active.expressionStart, active.expressionFrames, frame);\n if (frame >= active.expressionStart + active.expressionFrames) active.controlRamps &= ~EXPRESSION_RAMP;\n }\n if (ramps & PAN_RAMP) {\n active.pan = rampAt(active.panFrom, active.panTarget, active.panStart, active.panFrames, frame);\n if (frame >= active.panStart + active.panFrames) active.controlRamps &= ~PAN_RAMP;\n }\n if (ramps & (EXPRESSION_RAMP | PAN_RAMP)) updatePan(active);\n if (ramps & MODULATION_RAMP) {\n active.modulation = rampAt(active.modulationFrom, active.modulationTarget, active.modulationStart, active.modulationFrames, frame);\n if (frame >= active.modulationStart + active.modulationFrames) active.controlRamps &= ~MODULATION_RAMP;\n updateModulation(active);\n }\n if (ramps & OPERATOR_RAMP) {\n for (let op = 0; op < 4; op++) {\n active.operatorLevels[op] = rampAt(active.operatorFrom[op], active.operatorTargets[op], active.operatorStart, active.operatorFrames, frame);\n }\n if (frame >= active.operatorStart + active.operatorFrames) active.controlRamps &= ~OPERATOR_RAMP;\n }\n if (ramps & ENGINE_RAMP) {\n let pending = false, modulationChanged = false;\n for (let field = 0; field < SCALAR_FIELDS.length; field++) {\n if (active.scalarFrames[field] === 0) continue;\n active.scalarValues[field] = rampAt(active.scalarFrom[field], active.scalarTargets[field], active.scalarStarts[field], active.scalarFrames[field], frame);\n if (frame < active.scalarStarts[field] + active.scalarFrames[field]) pending = true;\n else active.scalarFrames[field] = 0;\n if (field === 0) active.feedbackScale = feedbackGain(active.scalarValues[0]);\n if (field === 2 || field === 3) modulationChanged = true;\n }\n if (modulationChanged) updateModulation(active);\n if (!pending) active.controlRamps &= ~ENGINE_RAMP;\n }\n if (ramps & RATIO_RAMP) {\n for (let op = 0; op < 4; op++) active.ratios[op] = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, frame);\n if (frame >= active.ratioStart + active.ratioFrames) active.controlRamps &= ~RATIO_RAMP;\n }\n}\n\nfunction updatePan(active: ActiveVoice): void {\n const gain = active.velocity * active.expression;\n const angle = (active.pan + 1) * Math.PI / 4;\n active.leftGain = active.pan === 1 ? 0 : active.pan === 0 ? gain : gain * Math.SQRT2 * Math.cos(angle);\n active.rightGain = active.pan === -1 ? 0 : active.pan === 0 ? gain : gain * Math.SQRT2 * Math.sin(angle);\n}\nfunction removeAt(items: T[], index: number): T {\n const removed = items[index];\n for (let i = index + 1; i < items.length; i++) items[i - 1] = items[i];\n items.pop();\n return removed;\n}\n\nfunction heldDb(time: number, attack: number, decay: number, sustainDb: number): number {\n if (attack > 0 && time < attack) return FLOOR_DB * (1 - time / attack);\n if (decay > 0 && time < attack + decay) return sustainDb * ((time - attack) / decay);\n return sustainDb;\n}\n\nfunction envelopeDbAt(active: ActiveVoice, time: number, op: number): number {\n if (active.releaseTime >= 0) {\n const release = active.releaseTimes[op], start = active.releaseStarts[op];\n if (release === 0 || time >= start + release) return FLOOR_DB;\n return active.releaseDb[op] + (FLOOR_DB - active.releaseDb[op]) * ((time - start) / release);\n }\n if (!active.envelopeEdited[op]) return heldDb(time, active.attackTimes[op], active.decayTimes[op], active.sustainDb[op]);\n const elapsed = time - active.envelopeStart[op], attack = active.attackTimes[op], decay = active.decayTimes[op];\n // A zero attack still preserves the boundary value: the new decay starts at\n // the interrupted dB instead of jumping to full scale.\n if (attack > 0 && elapsed < attack) return active.envelopeFromDb[op] * (1 - elapsed / attack);\n const from = attack > 0 ? 0 : active.envelopeFromDb[op];\n if (decay > 0 && elapsed < attack + decay) return from + (active.sustainDb[op] - from) * (elapsed - attack) / decay;\n return active.sustainDb[op];\n}\n\nfunction gainAt(active: ActiveVoice, time: number, op: number): number {\n if (active.envelopeEdited[op]) {\n const db = envelopeDbAt(active, time, op);\n return db <= FLOOR_DB ? 0 : Math.exp(db * DB_TO_LOG_GAIN);\n }\n const release = active.releaseTimes[op];\n if (active.releaseTime >= 0) {\n if (release === 0 || time >= active.releaseTime + release) return 0;\n const db = active.releaseDb[op] + (FLOOR_DB - active.releaseDb[op]) * ((time - active.releaseTime) / release);\n return db <= FLOOR_DB ? 0 : Math.exp(db * DB_TO_LOG_GAIN);\n }\n if (time >= active.attackTimes[op] + active.decayTimes[op]) return active.sustainGain[op];\n if (time === 0 && active.attackTimes[op] > 0) return 0;\n const db = heldDb(time, active.attackTimes[op], active.decayTimes[op], active.sustainDb[op]);\n return db <= FLOOR_DB ? 0 : Math.exp(db * DB_TO_LOG_GAIN);\n}\n\nfunction prepareGains(active: ActiveVoice, time: number, subTimes: Float64Array): void {\n const { sustainGain, gains, attackStep, decayStep, releaseStep, attackTimes, decayTimes, releaseTimes } = active;\n const oversample = subTimes.length;\n const lastTime = time + subTimes[oversample - 1];\n for (let op = 0; op < 4; op++) {\n if (active.envelopeEdited[op]) {\n for (let sub = 0; sub < oversample; sub++) gains[op + sub * 4] = gainAt(active, time + subTimes[sub], op);\n continue;\n }\n const attack = attackTimes[op], decay = decayTimes[op], release = releaseTimes[op];\n let step: number | undefined;\n if (active.releaseTime >= 0) {\n if (release === 0 || time >= active.releaseTime + release) {\n for (let sub = 0; sub < oversample; sub++) gains[op + sub * 4] = 0;\n continue;\n }\n if (lastTime < active.releaseTime + release) step = releaseStep[op];\n } else if (time >= attack + decay) {\n for (let sub = 0; sub < oversample; sub++) gains[op + sub * 4] = sustainGain[op];\n continue;\n } else if (time > 0 && lastTime < attack) {\n step = attackStep[op];\n } else if (time >= attack && lastTime < attack + decay) {\n step = decayStep[op];\n }\n // Re-anchor every output frame, so rounding cannot accumulate across time\n // or depend on render chunking. Boundary-crossing frames keep exact dB rules.\n if (step !== undefined) {\n let gain = gainAt(active, time, op);\n for (let sub = 0; sub < oversample; sub++) {\n gains[op + sub * 4] = gain;\n gain *= step;\n }\n } else {\n for (let sub = 0; sub < oversample; sub++) {\n gains[op + sub * 4] = gainAt(active, time + subTimes[sub], op);\n }\n }\n }\n}\n// Carrier-envelope energy avoids stealing a loud note merely at a zero crossing.\nfunction voiceAudibility(active: ActiveVoice, sampleRate: number): number {\n const time = active.elapsed / sampleRate;\n let gain = 0;\n for (let index = 0; index < active.graph.carriers.length; index++) {\n const op = active.graph.carriers[index];\n const level = active.controlRamps & OPERATOR_RAMP ?\n rampAt(active.operatorFrom[op], active.operatorTargets[op], active.operatorStart, active.operatorFrames, active.elapsed) : active.operatorLevels[op];\n gain += active.levels[op] * level * gainAt(active, time, op);\n }\n const expression = active.controlRamps & EXPRESSION_RAMP ?\n rampAt(active.expressionFrom, active.expressionTarget, active.expressionStart, active.expressionFrames, active.elapsed) :\n active.expression;\n const layerGain = rampAt(active.scalarFrom[4], active.scalarTargets[4], active.scalarStarts[4], active.scalarFrames[4], active.elapsed);\n return gain * active.carrierGain * active.velocity * expression * layerGain;\n}\n\n/** True when the buffered history is (now) exactly zero. With nothing left\n * sounding, sub-floor residue is cleared; a live silent voice instead flushes\n * the finite FIR to exact zeros so its envelope tail stays intact. */\nfunction settleDecimator(state: Float64Array, clear: boolean): boolean {\n const peak = decimatorPeak(state);\n if (peak !== 0 && (!clear || peak >= AMPLITUDE_FLOOR)) return false;\n if (peak !== 0) state.fill(0);\n return true;\n}\n\nexport class Synth {\n declare readonly sampleRate: number;\n declare readonly maxVoices: number;\n declare readonly quality: QualityProfile;\n /** @internal */\n declare oversample: number;\n declare readonly currentFrame: number;\n declare readonly errorCount: number;\n declare readonly lastStolenId: number | null;\n declare onVoiceEnded?: ((id: number, reason: VoiceEndReason) => void) | null;\n /** @internal */\n declare voices: ActiveVoice[];\n /** @internal */\n declare fades: ActiveVoice[];\n /** @internal */\n declare fadeFrames: number;\n /** @internal */\n declare spillLeft: number;\n /** @internal */\n declare spillRight: number;\n /** @internal */\n declare spillRemaining: number;\n /** @internal */\n declare nextId: number;\n /** @internal */\n declare subTimes: Float64Array;\n /** @internal */\n declare sequence: number;\n /** @internal */\n declare decimatorCoefficients: Float64Array;\n /** @internal Oversampled stereo mix for the current output frame. */\n declare mixLeft: Float64Array;\n /** @internal */\n declare mixRight: Float64Array;\n /** @internal Shared post-mix decimator state, one per output channel. */\n declare filterLeft: Float64Array;\n /** @internal */\n declare filterRight: Float64Array;\n /** @internal True once both decimator states have drained below the floor. */\n declare decimatorIdle: boolean;\n /** @internal filterRight is stale and logically equals filterLeft (dual mono). */\n declare decimatorMirrored: boolean;\n /** @internal Bounded spare slots include one admission scratch slot. */\n declare freeVoices: ActiveVoice[];\n /** @internal Preallocated notifications are drained after each stable frame. */\n declare terminalIds: Float64Array;\n /** @internal */\n declare terminalErrors: Uint8Array;\n /** @internal */\n declare rendering: boolean;\n /** @internal */\n declare mixGain: number;\n /** @internal */\n declare tuning: NormalizedTuning;\n /** @internal */\n declare stealing: NonNullable;\n\n constructor(sampleRate: number, maxVoices = DEFAULT_MAX_VOICES, options: SynthOptions = {}) {\n if (typeof sampleRate !== 'number' || !Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {\n throw new RangeError('sampleRate must be finite and in 8000..192000');\n }\n validateMaxVoices(maxVoices);\n const settings = readSynthOptions(options);\n this.quality = settings.quality ?? 'standard';\n this.oversample = qualityOversample(this.quality);\n this.mixGain = settings.mixGain ?? 1;\n this.tuning = settings.tuning === undefined ? DEFAULT_TUNING : normalizeTuning(settings.tuning);\n this.stealing = settings.stealing ?? 'oldest';\n this.sampleRate = sampleRate;\n this.maxVoices = maxVoices;\n this.voices = [];\n this.fades = [];\n this.freeVoices = Array.from({ length: maxVoices + MAX_FADES + 1 }, () => createVoiceSlot(this.oversample));\n this.terminalIds = new Float64Array(maxVoices);\n this.terminalErrors = new Uint8Array(maxVoices);\n this.rendering = false;\n this.fadeFrames = Math.ceil(sampleRate * 0.005);\n this.spillLeft = 0;\n this.spillRight = 0;\n this.spillRemaining = 0;\n this.lastStolenId = null;\n this.onVoiceEnded = null;\n this.currentFrame = 0;\n this.errorCount = 0;\n this.nextId = 1;\n this.subTimes = Float64Array.from({ length: this.oversample }, (_, sub) => sub / (sampleRate * this.oversample));\n this.sequence = 0;\n this.decimatorCoefficients = createDecimatorCoefficients(sampleRate, this.quality);\n this.mixLeft = new Float64Array(this.oversample);\n this.mixRight = new Float64Array(this.oversample);\n this.filterLeft = new Float64Array(DECIMATOR_STATE_SIZE);\n this.filterRight = new Float64Array(DECIMATOR_STATE_SIZE);\n this.decimatorIdle = true;\n this.decimatorMirrored = true;\n }\n setMixGain(gain: number): void {\n validateMixGain(gain);\n this.mixGain = gain;\n }\n\n setTuning(tuning: TuningOptions): void {\n const normalized = normalizeTuning(tuning);\n this.tuning = normalized;\n for (let index = 0; index < this.voices.length; index++) this.retuneVoice(this.voices[index]);\n for (let index = 0; index < this.fades.length; index++) this.retuneVoice(this.fades[index]);\n }\n\n /** @internal */\n retuneVoice(active: ActiveVoice): void {\n const frequency = tuningFrequency(active.note, this.tuning);\n active.tunedFrequency = frequency;\n const rate = this.sampleRate * this.oversample;\n const factor = 2 ** (pitchAt(active, active.elapsed) / 12);\n for (let op = 0; op < 4; op++) {\n const source = active.voice.ops[op];\n const ratio = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, active.elapsed);\n const target = active.frequencyTargets[op] || frequency * ratio;\n const hz = rampAt(active.frequencyFrom[op], target, active.frequencyStart, active.frequencyFrames, active.elapsed);\n active.baseIncrements[op] = TAU * hz * 2 ** (source.detune / 1200) / rate;\n active.increments[op] = Math.min(TAU * 0.45, active.baseIncrements[op] * factor);\n active.steps[op] = active.increments[op];\n }\n }\n\n /** @internal */\n stealingIndex(voicePriority: number): number {\n let chosen = -1;\n let quietest = 0;\n for (let index = 0; index < this.voices.length; index++) {\n const candidate = this.voices[index];\n if (candidate.voicePriority > voicePriority) continue;\n const current = chosen < 0 ? undefined : this.voices[chosen];\n let preferred = !current || candidate.voicePriority < current.voicePriority;\n let audibility = 0;\n if (this.stealing === 'quietest') audibility = voiceAudibility(candidate, this.sampleRate);\n if (current && candidate.voicePriority === current.voicePriority) {\n preferred = candidate.sequence < current.sequence;\n if (this.stealing === 'release-first') {\n const released = candidate.releaseTime >= 0, currentReleased = current.releaseTime >= 0;\n if (released !== currentReleased) preferred = released;\n } else if (this.stealing === 'quietest' && audibility !== quietest) preferred = audibility < quietest;\n }\n if (preferred) {\n chosen = index;\n quietest = audibility;\n }\n }\n return chosen;\n }\n\n\n noteOn(input: VoiceInput | PreparedVoice, note: number, id?: number, options: NoteOptions = DEFAULT_NOTE_OPTIONS): number {\n const { velocity, pan, voicePriority } = readNoteOptions(options);\n const voice = preparedVoiceValue(input);\n if (typeof note !== 'number' || !Number.isFinite(note) || note < 0 || note > 127) {\n throw new RangeError('note must be finite and in 0..127');\n }\n if (id === undefined && this.nextId > Number.MAX_SAFE_INTEGER) throw new RangeError('Note ID space exhausted');\n if (id !== undefined && (!Number.isSafeInteger(id) || id <= 0)) throw new RangeError('id must be a positive safe integer');\n for (const active of this.voices) if (active.id === id) throw new RangeError('id already active');\n const stolenIndex = this.voices.length >= this.maxVoices ? this.stealingIndex(voicePriority) : -1;\n if (this.voices.length >= this.maxVoices && stolenIndex < 0) throw new VoiceAdmissionError();\n if (id === undefined) id = this.nextId++;\n if (id >= this.nextId) this.nextId = id + 1;\n // Initialize the spare before changing admission state. No slot can be reused\n // while it is still active or fading, including during terminal callbacks.\n const active = this.freeVoices.pop()!;\n const { baseIncrements, increments, sustainDb, sustainGain, steps, attackStep, decayStep, levels } = active;\n const frequency = tuningFrequency(note, this.tuning);\n const rate = this.sampleRate * this.oversample;\n for (let i = 0; i < 4; i++) {\n const op = voice.ops[i];\n active.ratios[i] = active.ratioFrom[i] = active.ratioTargets[i] = op.ratio;\n active.frequencyTargets[i] = op.frequency ?? 0;\n active.frequencies[i] = active.frequencyFrom[i] = op.frequency ?? frequency * op.ratio;\n const scale = op.keyScale;\n const attenuation = scale ? Math.abs(note - scale.breakpoint) / 12 *\n (note < scale.breakpoint ? scale.leftDbPerOctave : scale.rightDbPerOctave) : 0;\n levels[i] = op.level * 10 ** (-(attenuation + (op.velocitySensitivity ?? 0) * (1 - velocity)) / 20);\n active.waveforms[i] = waveformCode(op.waveform);\n active.noiseSteps[i] = (op.noiseRate ?? 8000) / rate;\n const operatorFrequency = (op.frequency ?? frequency * op.ratio) * 2 ** (op.detune / 1200);\n active.frequencyScales[i] = TAU * 2 ** (op.detune / 1200) / rate;\n baseIncrements[i] = TAU * operatorFrequency / rate;\n // Retain the original pre-PM cap, but keep unclipped pitch bases so a\n // downward bend can bring high-ratio operators back into audible range.\n increments[i] = TAU * Math.min(rate * 0.45, operatorFrequency) / rate;\n sustainDb[i] = op.adsr.s === 0 ? FLOOR_DB : Math.max(FLOOR_DB, 20 * Math.log10(op.adsr.s));\n sustainGain[i] = sustainDb[i] <= FLOOR_DB ? 0 : 10 ** (sustainDb[i] / 20);\n steps[i] = Math.min(TAU * 0.45, increments[i]);\n const attack = active.attackTimes[i] = operatorDuration(op.adsr.a, note, op.rateKeyScale);\n const decay = active.decayTimes[i] = operatorDuration(op.adsr.d, note, op.rateKeyScale);\n active.releaseTimes[i] = operatorDuration(op.adsr.r, note, op.rateKeyScale);\n attackStep[i] = attack === 0 ? 1 : 10 ** (-FLOOR_DB / (20 * attack * rate));\n decayStep[i] = decay === 0 ? 1 : 10 ** (sustainDb[i] / (20 * decay * rate));\n }\n active.id = id;\n active.voicePriority = voicePriority;\n active.note = note;\n active.sequence = this.sequence++;\n active.voice = voice;\n active.graph = ALGORITHMS[voice.algorithm];\n // Cache the fixed topology in pooled typed storage; frozen graph-array\n // indexing otherwise repeats for every operator at every oversampled frame.\n for (let op = 0; op < 4; op++) {\n active.inputIndices[op * 2] = active.graph.inputs[op][0] ?? -1;\n active.inputIndices[op * 2 + 1] = active.graph.inputs[op][1] ?? -1;\n }\n active.carrierIndices.set(active.graph.carriers);\n active.velocity = velocity;\n active.expression = 1;\n active.pan = pan;\n active.expressionFrom = active.expressionTarget = 1;\n active.expressionStart = active.expressionFrames = 0;\n active.panFrom = active.panTarget = pan;\n active.panStart = active.panFrames = 0;\n active.modulation = active.modulationFrom = active.modulationTarget = 1;\n active.modulationStart = active.modulationFrames = active.controlRamps = 0;\n active.modIndex = voice.modIndex;\n active.amDepth = voice.lfo.amDepth;\n active.pmDepth = voice.lfo.pmDepth;\n active.pitchFrom = active.pitchTarget = active.pitchStart = active.pitchFrames = 0;\n active.operatorLevels.fill(1);\n active.operatorFrom.fill(1);\n active.operatorTargets.fill(1);\n active.operatorStart = active.operatorFrames = 0;\n active.tunedFrequency = frequency;\n active.ratioStart = active.ratioFrames = active.frequencyStart = active.frequencyFrames = 0;\n active.scalarValues[0] = voice.feedback;\n active.scalarValues[1] = voice.lfo.rate;\n active.scalarValues[2] = voice.lfo.amDepth;\n active.scalarValues[3] = voice.lfo.pmDepth;\n active.scalarValues[4] = 1;\n active.scalarFrom.set(active.scalarValues);\n active.scalarTargets.set(active.scalarValues);\n active.scalarStarts.fill(0);\n active.scalarFrames.fill(0);\n active.envelopeStart.fill(0);\n active.envelopeFromDb.fill(FLOOR_DB);\n active.envelopeEdited.fill(0);\n active.releaseStarts.fill(0);\n active.lfoTurns = 0;\n active.lfoLive = false;\n active.pitchRelease = 0;\n updatePan(active);\n active.lastSample = active.fadeRemaining = 0;\n active.lastFadeGain = 1;\n active.carrierGain = HEADROOM / active.graph.carriers.length;\n active.phases.fill(0);\n active.noiseStates.fill(NOISE_SEED);\n active.noiseTurns.fill(0);\n active.values.fill(0);\n active.releaseDb.fill(0);\n active.releaseStep.fill(0);\n active.gains.fill(0);\n active.previous = active.older = active.elapsed = 0;\n active.feedbackScale = feedbackGain(voice.feedback);\n active.releaseTime = active.releaseEnd = -1;\n (this as MutableCounters).lastStolenId = null;\n if (this.voices.length >= this.maxVoices) {\n const stolen = removeAt(this.voices, stolenIndex);\n (this as MutableCounters).lastStolenId = stolen.id;\n // Preserve the old DSP state. Exhausted tails collapse into the existing\n // bounded spill ramp, then recycle before callbacks can admit more notes.\n if (this.fades.length === MAX_FADES) {\n const retired = removeAt(this.fades, 0);\n const gain = retired.lastFadeGain!;\n this.spillLeft += retired.lastSample * retired.leftGain * gain;\n this.spillRight += retired.lastSample * retired.rightGain * gain;\n this.spillRemaining = this.fadeFrames;\n this.freeVoices.push(retired);\n }\n stolen.fadeRemaining = this.fadeFrames;\n stolen.lastFadeGain = 1;\n this.fades.push(stolen);\n }\n this.voices.push(active);\n // Notify only after admission: callbacks may start another note reentrantly.\n if (this.lastStolenId !== null) this.ended(this.lastStolenId, 'stolen');\n return id;\n }\n\n noteOff(id: number): boolean {\n let active: ActiveVoice | undefined;\n for (const item of this.voices) if (item.id === id) { active = item; break; }\n if (!active || active.releaseTime >= 0) return false;\n this.releaseVoice(active);\n return true;\n }\n\n /** @internal */\n releaseVoice(active: ActiveVoice): void {\n const time = active.elapsed / this.sampleRate;\n active.pitchRelease = pitchEnvelopeAt(active, time);\n let maxRelease = 0;\n for (let op = 0; op < 4; op++) {\n active.releaseDb[op] = envelopeDbAt(active, time, op);\n active.releaseStarts[op] = time;\n const release = active.releaseTimes[op];\n maxRelease = Math.max(maxRelease, release);\n active.releaseStep[op] = release === 0 ? 0 :\n 10 ** ((FLOOR_DB - active.releaseDb[op]) / (20 * release * this.sampleRate * this.oversample));\n }\n active.releaseTime = time;\n active.releaseEnd = time + maxRelease;\n }\n allNotesOff(): void {\n for (let index = 0; index < this.voices.length; index++) {\n const active = this.voices[index];\n if (active.releaseTime < 0) this.releaseVoice(active);\n }\n }\n\n panic(): void {\n while (this.fades.length > 0) this.freeVoices.push(this.fades.pop()!);\n this.spillLeft = this.spillRight = this.spillRemaining = 0;\n this.resetDecimator();\n (this as MutableCounters).lastStolenId = null;\n this.cancelVoices();\n }\n\n /** @internal Drop the shared decimator tail, as panic dropped per-voice filters. */\n resetDecimator(): void {\n this.filterLeft.fill(0);\n this.filterRight.fill(0);\n this.decimatorIdle = true;\n this.decimatorMirrored = true;\n }\n\n /** @internal */\n cancelVoices(): void {\n if (this.voices.length === 0) return;\n const active = this.voices.pop()!;\n const id = active.id;\n this.freeVoices.push(active);\n // At most maxVoices (32) stack-local IDs survive recycling. Unlike shared\n // scratch storage, they cannot be overwritten by reentrant callbacks.\n this.cancelVoices();\n this.ended(id, 'cancelled');\n }\n\n\n updateNote(id: number, input: NoteControls): boolean {\n const controls = validateNoteControls(input);\n let active: ActiveVoice | undefined;\n for (const item of this.voices) if (item.id === id) { active = item; break; }\n if (!active) return false;\n if (active.controlRamps !== 0) advanceControls(active);\n if (controls.pitch !== undefined) {\n active.pitchFrom = pitchAt(active, active.elapsed);\n active.pitchTarget = controls.pitch;\n active.pitchStart = active.elapsed;\n active.pitchFrames = (controls.glide ?? 0) * this.sampleRate;\n }\n const frames = (controls.ramp ?? 0) * this.sampleRate;\n if (controls.lfoRate !== undefined && !active.lfoLive) {\n const lfo = active.voice.lfo;\n const clock = lfo.sync === 'global' ? this.currentFrame : active.elapsed;\n active.lfoTurns = clock / this.sampleRate * active.scalarValues[1] + (lfo.phase ?? 0);\n active.lfoTurns -= Math.floor(active.lfoTurns);\n active.lfoLive = true;\n }\n for (let field = 0; field < SCALAR_FIELDS.length; field++) {\n const value = controls[SCALAR_FIELDS[field]];\n if (value === undefined) continue;\n active.scalarFrom[field] = active.scalarValues[field];\n active.scalarTargets[field] = value;\n active.scalarStarts[field] = active.elapsed;\n active.scalarFrames[field] = active.scalarValues[field] === value ? 0 : frames;\n if (active.scalarFrames[field] === 0) {\n active.scalarValues[field] = value;\n if (field === 0) active.feedbackScale = feedbackGain(value);\n if (field === 2 || field === 3) updateModulation(active);\n }\n active.controlRamps |= ENGINE_RAMP;\n }\n if (active.controlRamps & ENGINE_RAMP) advanceControls(active);\n if (controls.operatorRatios !== undefined || controls.operatorFrequencies !== undefined) {\n // Sample ongoing pitch-independent Hz before changing either timeline.\n for (let op = 0; op < 4; op++) {\n const ratio = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, active.elapsed);\n const target = active.frequencyTargets[op] || active.tunedFrequency * ratio;\n active.frequencies[op] = rampAt(active.frequencyFrom[op], target, active.frequencyStart, active.frequencyFrames, active.elapsed);\n }\n }\n if (controls.operatorRatios !== undefined) {\n for (let op = 0; op < 4; op++) {\n active.ratioFrom[op] = active.ratios[op];\n active.ratioTargets[op] = controls.operatorRatios[op];\n if (frames === 0) active.ratios[op] = controls.operatorRatios[op];\n }\n active.ratioStart = active.elapsed;\n active.ratioFrames = frames;\n if (frames !== 0) active.controlRamps |= RATIO_RAMP;\n else active.controlRamps &= ~RATIO_RAMP;\n }\n if (controls.operatorFrequencies !== undefined) {\n for (let op = 0; op < 4; op++) {\n active.frequencyFrom[op] = active.frequencies[op];\n active.frequencyTargets[op] = controls.operatorFrequencies[op] ?? 0;\n }\n active.frequencyStart = active.elapsed;\n active.frequencyFrames = frames;\n if (frames !== 0) active.controlRamps |= FREQUENCY_RAMP;\n else active.controlRamps &= ~FREQUENCY_RAMP;\n }\n if (controls.operatorRatios !== undefined || controls.operatorFrequencies !== undefined) this.retuneVoice(active);\n if (controls.operatorADSR !== undefined) {\n const time = active.elapsed / this.sampleRate;\n for (let op = 0; op < 4; op++) {\n const db = envelopeDbAt(active, time, op), adsr = controls.operatorADSR[op];\n const keyScale = active.voice.ops[op].rateKeyScale;\n active.envelopeFromDb[op] = db;\n active.envelopeStart[op] = time;\n active.envelopeEdited[op] = 1;\n active.attackTimes[op] = operatorDuration(adsr.a, active.note, keyScale);\n active.decayTimes[op] = operatorDuration(adsr.d, active.note, keyScale);\n if (active.attackTimes[op] === 0 && active.decayTimes[op] === 0) active.decayTimes[op] = 1 / this.sampleRate;\n active.sustainDb[op] = adsr.s === 0 ? FLOOR_DB : Math.max(FLOOR_DB, 20 * Math.log10(adsr.s));\n active.sustainGain[op] = active.sustainDb[op] <= FLOOR_DB ? 0 : 10 ** (active.sustainDb[op] / 20);\n active.releaseTimes[op] = operatorDuration(adsr.r, active.note, keyScale);\n if (active.releaseTime >= 0) {\n active.releaseDb[op] = db;\n active.releaseStarts[op] = time;\n }\n }\n if (active.releaseTime >= 0) {\n active.releaseEnd = time;\n for (let op = 0; op < 4; op++) active.releaseEnd = Math.max(active.releaseEnd, time + active.releaseTimes[op]);\n }\n }\n if (controls.expression !== undefined) {\n active.expressionFrom = active.expression;\n active.expressionTarget = controls.expression;\n active.expressionStart = active.elapsed;\n active.expressionFrames = active.expression === controls.expression ? 0 : frames;\n if (active.expressionFrames === 0) {\n active.expression = controls.expression;\n active.controlRamps &= ~EXPRESSION_RAMP;\n } else active.controlRamps |= EXPRESSION_RAMP;\n }\n if (controls.pan !== undefined) {\n active.panFrom = active.pan;\n active.panTarget = controls.pan;\n active.panStart = active.elapsed;\n active.panFrames = active.pan === controls.pan ? 0 : frames;\n if (active.panFrames === 0) {\n active.pan = controls.pan;\n active.controlRamps &= ~PAN_RAMP;\n } else active.controlRamps |= PAN_RAMP;\n }\n if (controls.expression !== undefined || controls.pan !== undefined) updatePan(active);\n if (controls.modulation !== undefined) {\n active.modulationFrom = active.modulation;\n active.modulationTarget = controls.modulation;\n active.modulationStart = active.elapsed;\n active.modulationFrames = active.modulation === controls.modulation ? 0 : frames;\n if (active.modulationFrames === 0) {\n active.modulation = controls.modulation;\n active.controlRamps &= ~MODULATION_RAMP;\n } else active.controlRamps |= MODULATION_RAMP;\n updateModulation(active);\n }\n if (controls.operatorLevels !== undefined) {\n let changed = false;\n for (let op = 0; op < 4; op++) {\n active.operatorFrom[op] = active.operatorLevels[op];\n active.operatorTargets[op] = controls.operatorLevels[op];\n if (active.operatorFrom[op] !== active.operatorTargets[op]) changed = true;\n if (frames === 0) active.operatorLevels[op] = controls.operatorLevels[op];\n }\n active.operatorStart = active.elapsed;\n active.operatorFrames = changed ? frames : 0;\n if (active.operatorFrames === 0) active.controlRamps &= ~OPERATOR_RAMP;\n else active.controlRamps |= OPERATOR_RAMP;\n }\n // Reset a former PM/glide step even when modulation becomes zero.\n if (controls.pitch !== undefined || controls.modulation !== undefined) {\n const factor = 2 ** (pitchAt(active, active.elapsed) / 12);\n for (let op = 0; op < 4; op++) {\n active.increments[op] = Math.min(TAU * 0.45, active.baseIncrements[op] * factor);\n active.steps[op] = active.increments[op];\n }\n }\n return true;\n }\n\n /** @internal */\n ended(id: number, reason: VoiceEndReason): void {\n if (typeof this.onVoiceEnded === 'function') {\n try { this.onVoiceEnded(id, reason); } catch { (this as MutableCounters).errorCount++; }\n }\n }\n\n // One output frame, with no temporary arrays or objects in the hot path.\n // Oversampled samples are panned into mixLeft/mixRight; the shared post-mix\n // decimator then runs once per channel instead of once per voice. Returns\n // the last undecimated sample (for steal spill) or NaN on corrupt state;\n // substeps accumulated before a NaN were finite and stay in the mix.\n /** @internal */\n renderVoice(active: ActiveVoice, fadeGain = 1): number {\n const { graph, inputIndices, carrierIndices, phases, values, increments, steps, gains, levels } = active;\n if (active.controlRamps !== 0) advanceControls(active);\n const voiceGain = active.scalarValues[4] * fadeGain;\n const panLeft = active.leftGain * voiceGain, panRight = active.rightGain * voiceGain;\n const mixLeft = this.mixLeft, mixRight = this.mixRight;\n const time = active.elapsed / this.sampleRate;\n const finished = active.releaseTime >= 0 && time >= active.releaseEnd;\n const lfo = active.voice.lfo;\n const lfoEnabled = time >= (lfo.delay ?? 0);\n let tremolo = 0;\n if (lfoEnabled && (active.amDepth !== 0 || active.pmDepth !== 0)) {\n const clock = lfo.sync === 'global' ? this.currentFrame : active.elapsed;\n const turns = active.lfoLive ? active.lfoTurns : clock / this.sampleRate * lfo.rate + (lfo.phase ?? 0);\n tremolo = lfoValue((turns - Math.floor(turns)) * TAU, lfo.waveform);\n }\n const amGain = !lfoEnabled || active.amDepth === 0 ? 1 : 1 - active.amDepth * (0.5 + 0.5 * tremolo);\n const pmFactor = !lfoEnabled || active.pmDepth === 0 ? 1 : 2 ** (active.pmDepth * tremolo / 1200);\n if (lfo.amTargets || lfo.pmTargets) {\n for (let op = 0; op < 4; op++) {\n const am = lfo.amTargets?.[op] ?? 1, pm = lfo.pmTargets?.[op] ?? 1;\n active.amGains[op] = am === 1 ? amGain : !lfoEnabled ? 1 : 1 - active.amDepth * am * (0.5 + 0.5 * tremolo);\n active.pmFactors[op] = pm === 1 ? pmFactor : !lfoEnabled ? 1 : 2 ** (active.pmDepth * pm * tremolo / 1200);\n }\n }\n const gliding = active.pitchFrames !== 0;\n const frequencyRamping = active.ratioFrames !== 0 || active.frequencyFrames !== 0;\n const dynamicPitch = gliding || active.voice.pitchEnvelope !== undefined || frequencyRamping;\n if (!dynamicPitch && active.pmDepth !== 0) {\n for (let op = 0; op < 4; op++) {\n const factor = lfo.pmTargets ? active.pmFactors[op] : pmFactor;\n steps[op] = Math.min(TAU * 0.45, increments[op] * factor);\n }\n }\n const operatorRamping = (active.controlRamps & OPERATOR_RAMP) !== 0;\n if (!finished) prepareGains(active, time, this.subTimes);\n let output = 0;\n const oversample = this.oversample, modIndex = active.modIndex, feedbackScale = active.feedbackScale;\n const operatorLevels = active.operatorLevels, waveforms = active.waveforms;\n // All-sine voices with static operator levels and shared AM take the branch-light path below.\n const simple = waveforms[0] === 0 && waveforms[1] === 0 && waveforms[2] === 0 && waveforms[3] === 0 &&\n !operatorRamping && !lfo.amTargets;\n for (let sub = 0; sub < oversample; sub++) {\n const frame = active.elapsed + sub / this.oversample;\n if (dynamicPitch) {\n const factor = 2 ** (pitchAt(active, frame) / 12 + pitchEnvelopeAt(active, time + this.subTimes[sub]) / 1200);\n for (let op = 0; op < 4; op++) {\n if (frequencyRamping) {\n const ratio = rampAt(active.ratioFrom[op], active.ratioTargets[op], active.ratioStart, active.ratioFrames, frame);\n const target = active.frequencyTargets[op] || active.tunedFrequency * ratio;\n const hz = rampAt(active.frequencyFrom[op], target, active.frequencyStart, active.frequencyFrames, frame);\n active.baseIncrements[op] = hz * active.frequencyScales[op];\n }\n const pm = lfo.pmTargets ? active.pmFactors[op] : pmFactor;\n steps[op] = Math.min(TAU * 0.45, Math.min(TAU * 0.45, active.baseIncrements[op] * factor) * pm);\n }\n }\n let sample = 0;\n if (!finished) {\n if (simple) {\n const base = sub * 4;\n for (let op = 0; op < 4; op++) {\n let modulation = op === 0 ? (active.previous + active.older) * feedbackScale : 0;\n const first = inputIndices[op * 2], second = inputIndices[op * 2 + 1];\n if (first >= 0) modulation += values[first] * modIndex;\n if (second >= 0) modulation += values[second] * modIndex;\n const phase = phases[op];\n values[op] = fastSin(phase + modulation) * gains[base + op] * levels[op] * operatorLevels[op] * amGain;\n const next = phase + steps[op];\n phases[op] = next < TAU ? next : next - TAU;\n }\n } else {\n for (let op = 0; op < 4; op++) {\n let modulation = op === 0 ? (active.previous + active.older) * active.feedbackScale : 0;\n const first = inputIndices[op * 2], second = inputIndices[op * 2 + 1];\n if (first >= 0) modulation += values[first] * active.modIndex;\n if (second >= 0) modulation += values[second] * active.modIndex;\n const operatorLevel = operatorRamping ?\n rampAt(active.operatorFrom[op], active.operatorTargets[op], active.operatorStart, active.operatorFrames, frame) : active.operatorLevels[op];\n const am = lfo.amTargets ? active.amGains[op] : amGain;\n const code = active.waveforms[op];\n let oscillator: number;\n if (code === 0) oscillator = fastSin(phases[op] + modulation);\n else if (code === 8) {\n oscillator = (active.noiseStates[op] & 1) === 0 ? -1 : 1;\n const turns = active.noiseTurns[op] + active.noiseSteps[op];\n // At the minimum clock a 20 kHz hold rate needs at most two ticks.\n const ticks = Math.floor(turns);\n for (let tick = 0; tick < ticks; tick++) active.noiseStates[op] = advanceNoise(active.noiseStates[op]);\n active.noiseTurns[op] = turns - ticks;\n } else oscillator = periodicWaveform(phases[op] + modulation, code, steps[op] * INV_TAU);\n const value = oscillator * gains[op + sub * 4] * levels[op] * operatorLevel * am;\n values[op] = value;\n const phase = phases[op] + steps[op];\n const wrapped = phase < TAU ? phase : phase - TAU;\n phases[op] = wrapped;\n if (!Number.isFinite(value) || !Number.isFinite(wrapped)) return NaN;\n }\n }\n active.older = active.previous;\n active.previous = values[0];\n for (let j = 0; j < graph.carriers.length; j++) sample += values[carrierIndices[j]];\n sample *= active.carrierGain;\n // Non-finite operator state always reaches a carrier through modulation or the mix.\n if (!Number.isFinite(sample)) return NaN;\n mixLeft[sub] += sample * panLeft;\n mixRight[sub] += sample * panRight;\n }\n output = sample;\n }\n if (active.lfoLive) {\n active.lfoTurns += active.scalarValues[1] / this.sampleRate;\n active.lfoTurns -= Math.floor(active.lfoTurns);\n }\n active.elapsed++;\n if (gliding && active.elapsed >= active.pitchStart + active.pitchFrames) {\n active.pitchFrames = 0;\n const factor = 2 ** (active.pitchTarget / 12);\n for (let op = 0; op < 4; op++) {\n increments[op] = Math.min(TAU * 0.45, active.baseIncrements[op] * factor);\n steps[op] = increments[op];\n }\n }\n if (frequencyRamping) {\n if (active.elapsed >= active.ratioStart + active.ratioFrames) active.ratioFrames = 0;\n if (active.elapsed >= active.frequencyStart + active.frequencyFrames) {\n active.frequencyFrames = 0;\n active.controlRamps &= ~FREQUENCY_RAMP;\n }\n if (active.ratioFrames === 0 && active.frequencyFrames === 0) this.retuneVoice(active);\n }\n active.lastSample = output * active.scalarValues[4];\n return active.lastSample;\n }\n\n render(left: Float32Array, right: Float32Array, offset = 0, length?: number): void {\n if (this.rendering) throw new Error('render cannot be called reentrantly');\n if (typedArrayKind.call(left) !== 'Float32Array' || typedArrayKind.call(right) !== 'Float32Array') {\n throw new RangeError('render requires native Float32Arrays');\n }\n const leftLength = typedArrayLength.call(left), rightLength = typedArrayLength.call(right);\n if (!Number.isSafeInteger(offset) || offset < 0) {\n throw new RangeError('render requires an in-bounds offset and length');\n }\n if (length === undefined) length = leftLength - offset;\n if (!Number.isSafeInteger(length) || length < 0 ||\n offset + length > leftLength || offset + length > rightLength) {\n throw new RangeError('render requires an in-bounds offset and length');\n }\n if (this.voices.length === 0 && this.fades.length === 0 && this.spillRemaining === 0 && this.decimatorIdle) {\n typedArrayFill.call(left, 0, offset, offset + length);\n if (right !== left) typedArrayFill.call(right, 0, offset, offset + length);\n (this as MutableCounters).currentFrame += length;\n return;\n }\n this.rendering = true;\n const oversample = this.oversample, coefficients = this.decimatorCoefficients;\n const mixLeft = this.mixLeft, mixRight = this.mixRight, filterLeft = this.filterLeft, filterRight = this.filterRight;\n try {\n for (let frame = offset, end = offset + length; frame < end; frame++) {\n let terminalCount = 0;\n mixLeft.fill(0);\n mixRight.fill(0);\n for (let index = 0; index < this.voices.length; index++) {\n const active = this.voices[index];\n const sample = this.renderVoice(active);\n if (!Number.isFinite(sample)) {\n (this as MutableCounters).errorCount++;\n this.terminalIds[terminalCount] = active.id;\n this.terminalErrors[terminalCount++] = 1;\n this.freeVoices.push(removeAt(this.voices, index--));\n continue;\n }\n // Envelope-based retirement: once every operator release has ended the\n // voice contributes silence; the shared decimator drains its tail.\n if (active.releaseTime >= 0 && (active.elapsed - 1) / this.sampleRate >= active.releaseEnd) {\n this.terminalIds[terminalCount] = active.id;\n this.terminalErrors[terminalCount++] = 0;\n this.freeVoices.push(removeAt(this.voices, index--));\n }\n }\n for (let index = 0; index < this.fades.length; index++) {\n const active = this.fades[index];\n const gain = active.fadeRemaining / this.fadeFrames;\n const sample = this.renderVoice(active, gain);\n if (!Number.isFinite(sample)) {\n (this as MutableCounters).errorCount++;\n this.freeVoices.push(removeAt(this.fades, index--));\n continue;\n }\n active.lastFadeGain = gain;\n if (--active.fadeRemaining === 0) this.freeVoices.push(removeAt(this.fades, index--));\n }\n if (this.spillRemaining > 0) {\n for (let sub = 0; sub < oversample; sub++) {\n mixLeft[sub] += this.spillLeft;\n mixRight[sub] += this.spillRight;\n }\n const next = --this.spillRemaining;\n this.spillLeft *= next / (next + 1);\n this.spillRight *= next / (next + 1);\n }\n let mixedLeft = 0, mixedRight = 0, silentLeft = true, silentRight = true;\n let mirrored = this.decimatorMirrored;\n for (let sub = 0; sub < oversample; sub++) {\n const inputLeft = mixLeft[sub], inputRight = mixRight[sub];\n if (inputLeft !== 0) silentLeft = false;\n if (inputRight !== 0) silentRight = false;\n // Identical channel histories give identical outputs: centered mixes\n // filter once until the inputs first differ.\n if (mirrored && inputLeft !== inputRight) {\n filterRight.set(filterLeft);\n mirrored = false;\n }\n mixedLeft = decimateSample(inputLeft, filterLeft, coefficients);\n mixedRight = mirrored ? mixedLeft : decimateSample(inputRight, filterRight, coefficients);\n }\n this.decimatorMirrored = mirrored;\n // Saturate each stereo sum independently, preserving center dual mono.\n left[frame] = Number.isFinite(mixedLeft) ? HEADROOM * Math.tanh(mixedLeft * this.mixGain / HEADROOM) : 0;\n right[frame] = Number.isFinite(mixedRight) ? HEADROOM * Math.tanh(mixedRight * this.mixGain / HEADROOM) : 0;\n if (!Number.isFinite(mixedLeft) || !Number.isFinite(mixedRight)) {\n (this as MutableCounters).errorCount++;\n this.resetDecimator();\n } else {\n // A silent channel goes idle once its FIR history is zero. After the\n // last source retires, history below the -96 dB floor snaps to zero.\n const clear = this.voices.length === 0 && this.fades.length === 0 && this.spillRemaining === 0;\n const idleLeft = silentLeft && settleDecimator(filterLeft, clear);\n const idleRight = mirrored ? idleLeft : silentRight && settleDecimator(filterRight, clear);\n this.decimatorIdle = idleLeft && idleRight;\n if (this.decimatorIdle) this.decimatorMirrored = true;\n }\n // Callbacks may admit/release notes, but replacements first render on the\n // next frame. Live traversal would let a zero-release chain run forever.\n for (let index = 0; index < terminalCount; index++) {\n this.ended(this.terminalIds[index], this.terminalErrors[index] ? 'error' : 'ended');\n }\n (this as MutableCounters).currentFrame++;\n }\n } finally {\n this.rendering = false;\n }\n }\n}\n"],"mappings":"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","ignoreList":[]} diff --git a/doc/acoustic-quality.html b/doc/acoustic-quality.html index 52b4cb5..d36f2ff 100644 --- a/doc/acoustic-quality.html +++ b/doc/acoustic-quality.html @@ -1,6 +1,6 @@ Acoustic references and decimation — OPM.js
OPM.js documentationOnline demos

Generated from Markdown source. Edit the source, not this HTML.

Acoustic references and decimation #

-

Every profile renders operators at an internal multiple of the output rate and decimates with a cascade of original linear-phase half-band FIR filters. Each 2:1 stage has a centre tap of 1/2 and every other tap zero, so it costs one multiply per unique coefficient pair, and it is evaluated in polyphase form: oversampled inputs are only buffered, and the whole cascade runs once per output frame on the last internal substep. The default standard profile uses four-times internal sampling: a 15-tap stage at 4× followed by a 47-tap stage at 2×. Passband is flat to 0.40 times output sample rate; products that would fold below that edge (from 0.60 Fs upward) are rejected by at least 81 dB. Each voice has 158 preallocated state scalars (counter/ring positions, the frame's substep inputs and doubled delay rings); there are no allocations. State resets on admission and all buffered history must drain to zero on release.

+

Every profile renders operators at an internal multiple of the output rate and decimates with a cascade of original linear-phase half-band FIR filters. Each 2:1 stage has a centre tap of 1/2 and every other tap zero, so it costs one multiply per unique coefficient pair, and it is evaluated in polyphase form: oversampled inputs are only buffered, and the whole cascade runs once per output frame on the last internal substep. The default standard profile uses four-times internal sampling: a 15-tap stage at 4× followed by a 47-tap stage at 2×. Passband is flat to 0.40 times output sample rate; products that would fold below that edge (from 0.60 Fs upward) are rejected by at least 81 dB. Voices are panned and summed to left/right at the internal rate, and one shared decimator per output channel runs after the mix, so filter work no longer scales with polyphony. Each channel has 158 preallocated state scalars (counter/ring positions, the frame's substep inputs and doubled delay rings); there are no allocations. A centered (dual-mono) mix filters once until the channel inputs first differ. Because note gain, expression, pan and steal fades apply before the decimator, a hard gain step is band-limited by it and rings for the FIR's length (at most 26 output frames, standard/high) plus its linear-phase delay instead of cutting at the output rate.

Before this change (v1.11.1 and earlier) the decimator was an eighth-order bilinear Butterworth IIR with cutoff 0.30 output Fs (fourth order for eco). Its tables are kept below for comparison.

Construction accepts quality: 'eco' | 'standard' | 'high' in SynthOptions, offline render options and OPM options. Quality cannot change on an active synth; OPM retains it across restart. maxVoices accepts 1–32, with default 8; larger budgets are opt-in; the profile tables and the stealing-fade ceiling (eight) are unchanged.

ProfileInternal rateHalf-band stages (taps)Design rejectionGroup delayTradeoff
eco2×3563 dB8.5 framesFewer oscillator substeps, earlier internal Nyquist and weaker rejection
standard (default)4×15 → 4781 dB13.25 framesAcceptance baseline
high8×11 → 15 → 4781 dB13.875 framesHigher internal Nyquist, twice standard oscillator substeps; not an arbitrary-patch alias-free guarantee
@@ -26,7 +26,7 @@

Coverage and evidence
ProfileMedian msp95 msp99 msWorst msp99 / deadlineMisses
Eco0.2123750.2316250.2797080.4531660.1050 / 2,000
Standard0.3803330.4237500.5092920.6079170.1910 / 2,000
High0.6807090.7473330.8199170.9811250.3070 / 2,000

All rows reported zero DSP errors and admission rejections. These are offline host wall-clock measurements, not AudioWorklet underrun counters, allocation measurements, listening judgments or physical-device acceptance. They do not replace the different historical v1.6 host results recorded in CHANGELOG.

-

Envelope-transition references compare the waveform through the actual voice-ended frame, then require exact retired silence. The implementation retires a voice once every buffered FIR input is below the state floor rather than waiting for exact zeros. A state floor is not a bound on each future output sample: the independent bound multiplies the floor by each stage's absolute tap sum.

+

Voices retire on their envelope: once every operator release has ended, the voice is freed and its ended notification fires without waiting for filter history. The shared decimator keeps draining that history; envelope-transition references compare the waveform through the frame where the channel's buffered FIR inputs all fall below the state floor, then require exact silence. The implementation clears that sub-floor history (only on a channel whose input is silent) rather than waiting for exact zeros. A state floor is not a bound on each future output sample: the independent bound multiplies the floor by each stage's absolute tap sum.

Conservative host capacity candidates #

Scope note (v1.11): the benchmark patches use sine operators only. Non-sine operator waveforms (half, abs, quarter, alternating, camel, square, saw) add a per-sample waveform evaluation and noise adds a bounded LFSR step, so patches that use them cost more per voice than the measured candidates indicate; the work stays bounded by the voice limit, but the capacity candidates below were not re-measured for those voices.

The v1.10 checkout adds this matrix/candidate output through the checkout-only maintenance command; it is not a packaged API or command. From the matching 1.10.0 source checkout, run:

diff --git a/doc/acoustic-quality.md b/doc/acoustic-quality.md index 0fca16b..157f43a 100644 --- a/doc/acoustic-quality.md +++ b/doc/acoustic-quality.md @@ -1,6 +1,6 @@ # Acoustic references and decimation -Every profile renders operators at an internal multiple of the output rate and decimates with a cascade of original **linear-phase half-band FIR** filters. Each 2:1 stage has a centre tap of 1/2 and every other tap zero, so it costs one multiply per unique coefficient pair, and it is evaluated in polyphase form: oversampled inputs are only buffered, and the whole cascade runs once per output frame on the last internal substep. The default **standard** profile uses four-times internal sampling: a 15-tap stage at 4× followed by a 47-tap stage at 2×. Passband is flat to **0.40 times output sample rate**; products that would fold below that edge (from 0.60 Fs upward) are rejected by at least 81 dB. Each voice has 158 preallocated state scalars (counter/ring positions, the frame's substep inputs and doubled delay rings); there are no allocations. State resets on admission and all buffered history must drain to zero on release. +Every profile renders operators at an internal multiple of the output rate and decimates with a cascade of original **linear-phase half-band FIR** filters. Each 2:1 stage has a centre tap of 1/2 and every other tap zero, so it costs one multiply per unique coefficient pair, and it is evaluated in polyphase form: oversampled inputs are only buffered, and the whole cascade runs once per output frame on the last internal substep. The default **standard** profile uses four-times internal sampling: a 15-tap stage at 4× followed by a 47-tap stage at 2×. Passband is flat to **0.40 times output sample rate**; products that would fold below that edge (from 0.60 Fs upward) are rejected by at least 81 dB. Voices are panned and summed to left/right at the internal rate, and one shared decimator per output channel runs after the mix, so filter work no longer scales with polyphony. Each channel has 158 preallocated state scalars (counter/ring positions, the frame's substep inputs and doubled delay rings); there are no allocations. A centered (dual-mono) mix filters once until the channel inputs first differ. Because note gain, expression, pan and steal fades apply before the decimator, a hard gain step is band-limited by it and rings for the FIR's length (at most 26 output frames, standard/high) plus its linear-phase delay instead of cutting at the output rate. Before this change (v1.11.1 and earlier) the decimator was an eighth-order bilinear Butterworth IIR with cutoff 0.30 output Fs (fourth order for eco). Its tables are kept below for comparison. @@ -76,7 +76,7 @@ Historical **1.8.0** checkout measurement (not the new capacity matrix): Apple M All rows reported zero DSP errors and admission rejections. These are offline host wall-clock measurements, not AudioWorklet underrun counters, allocation measurements, listening judgments or physical-device acceptance. They do not replace the different historical v1.6 host results recorded in CHANGELOG. -Envelope-transition references compare the waveform through the actual voice-ended frame, then require exact retired silence. The implementation retires a voice once every buffered FIR input is below the state floor rather than waiting for exact zeros. A state floor is not a bound on each future output sample: the independent bound multiplies the floor by each stage's absolute tap sum. +Voices retire on their envelope: once every operator release has ended, the voice is freed and its `ended` notification fires without waiting for filter history. The shared decimator keeps draining that history; envelope-transition references compare the waveform through the frame where the channel's buffered FIR inputs all fall below the state floor, then require exact silence. The implementation clears that sub-floor history (only on a channel whose input is silent) rather than waiting for exact zeros. A state floor is not a bound on each future output sample: the independent bound multiplies the floor by each stage's absolute tap sum. ## Conservative host capacity candidates diff --git a/doc/changelog.html b/doc/changelog.html index 9bbc9eb..7b9f1bd 100644 --- a/doc/changelog.html +++ b/doc/changelog.html @@ -1,11 +1,13 @@ -Changelog — OPM.js
OPM.js documentationOnline demos

Generated from Markdown source. Edit the source, not this HTML.

Changelog #

+Changelog — OPM.js
OPM.js documentationOnline demos

Generated from Markdown source. Edit the source, not this HTML.

Changelog #

For installation and executable examples, see the README and the English / 繁體中文 usage guides.

Unreleased #

Half-band FIR decimation #

-
  • Replace the per-voice Butterworth IIR decimator (cutoff 0.30 output Fs) with cascaded polyphase linear-phase half-band FIR stages: eco 35 taps at 2×; standard 15 → 47 taps; high 11 → 15 → 47 taps. Substep calls only buffer their input; the cascade runs once per output frame.

  • Passband is flat to 0.40 output Fs (ripple below 0.001 dB for standard/high; formerly −11.5 dB at 0.35 Fs). Products folding below 0.40 Fs are rejected by at least 81 dB for standard/high and 63 dB for eco (standard formerly −52.3/−55.6 dB at 0.60/0.625 Fs). Between 0.50 and 0.60 Fs the transition band folds into 0.40–0.50 Fs, and far above Nyquist the former filter's bilinear zero was deeper; see acoustic quality.

  • Behavior change: output is delayed by a constant linear-phase 13.25 output frames for standard (8.5 eco, 13.875 high; about 0.28 ms at 48 kHz) instead of the IIR's frequency-dependent 2.7–3.3 frames. Each voice preallocates 158 state scalars instead of 8.

  • Measured on this checkout's cloud container with npm run benchmark (median of four interleaved runs per side): standard 8 held voices 0.702 → 0.669 ms per 128-frame block, high 8 held voices 1.204 → 1.116 ms, eco unchanged within noise (0.434 → 0.435 ms).

+
  • Replace the per-voice Butterworth IIR decimator (cutoff 0.30 output Fs) with cascaded polyphase linear-phase half-band FIR stages: eco 35 taps at 2×; standard 15 → 47 taps; high 11 → 15 → 47 taps. Substep calls only buffer their input; the cascade runs once per output frame.

  • Passband is flat to 0.40 output Fs (ripple below 0.001 dB for standard/high; formerly −11.5 dB at 0.35 Fs). Products folding below 0.40 Fs are rejected by at least 81 dB for standard/high and 63 dB for eco (standard formerly −52.3/−55.6 dB at 0.60/0.625 Fs). Between 0.50 and 0.60 Fs the transition band folds into 0.40–0.50 Fs, and far above Nyquist the former filter's bilinear zero was deeper; see acoustic quality.

  • Behavior change: output is delayed by a constant linear-phase 13.25 output frames for standard (8.5 eco, 13.875 high; about 0.28 ms at 48 kHz) instead of the IIR's frequency-dependent 2.7–3.3 frames. The decimator keeps 158 state scalars instead of 8 (per output channel since post-mix decimation, below).

  • Measured on this checkout's cloud container with npm run benchmark (median of four interleaved runs per side): standard 8 held voices 0.702 → 0.669 ms per 128-frame block, high 8 held voices 1.204 → 1.116 ms, eco unchanged within noise (0.434 → 0.435 ms).

Band-limited square, saw and half-wave operators #

Square and saw operators now add two-sample polyBLEP residuals at their jumps, and half-wave operators add polyBLAMP residuals at their corners, sized from each operator's unmodulated phase step at the oversampled rate. In unit tests the folded-alias power of periodic frames drops by about 8–17 dB at the internal rate, before the decimator. Phase modulation makes the nominal step an approximation, so these shapes remain not alias-free. All non-sine periodic shapes now use the deterministic polynomial fastSin instead of Math.sin, and the shape helpers are split so the dispatcher stays inlinable. Measured on Node 22 with 16 held voices at 48 kHz (standard): square 1.74 → 0.98 ms/block, half 1.89 → 1.40 ms, saw 1.04 → 1.13 ms. Sine output and periodicWaveform(theta, code) without a step are unchanged apart from fastSin replacing Math.sin (~1e-10). Square/saw/half voices render differently near their edges. These timings are one Node process on one machine, not a browser AudioWorklet or device measurement.

+

Post-mix decimation #

+
  • Voices are now panned and summed to left/right at the oversampled rate, and one shared decimator per output channel runs after the mix, instead of one decimator per voice. A centered (dual-mono) mix filters once until the channel inputs differ. Measured against per-voice half-band FIR decimation with a 128-frame Synth.render loop at 48 kHz (median of three interleaved runs, one Node 22 process on one cloud x64 host): standard 8 voices 0.677 → 0.611 ms/block (−10%), 32 voices 2.772 → 2.477 ms (−11%); high 8 voices 1.115 → 0.996 ms (−11%), 32 voices 4.626 → 4.154 ms (−10%); eco 32 voices unchanged; a single voice costs about 0.01 ms/block more (mix-buffer overhead). Against the earlier per-voice Butterworth decimator the gains were 9–19%.

  • Behavior change: note gain, expression, pan and steal fades now apply before the decimator, so a hard gain step is band-limited by it and rings through the half-band FIR (at most 26 output frames) instead of cutting instantly at the output rate. Ramps are within s·p·Σk|h[k]|/4 of the former output-rate product.

  • Voices retire on their envelope end (all operator releases finished) and report ended without waiting for filter history; the shared decimator drains the tail; once nothing is sounding, history below the −96 dB floor snaps to exact zero.

v1.11.1 #

Package version 1.11.1, a GitHub release (not on npm). Performance patch over v1.11; no API, voice-format (still v7) or protocol changes. Output differs from v1.11 by about 1e-10 for sine operators. Security: scoped static review of the src/core change found no evidence-backed issue.

Faster sine operators #

diff --git a/doc/expressive-voices.html b/doc/expressive-voices.html index 1f09266..4f2a7d9 100644 --- a/doc/expressive-voices.html +++ b/doc/expressive-voices.html @@ -8,7 +8,7 @@

Patch fields Oscillator shapes and noise #

For θ = phase + modulation and fractional turns t = frac(θ / (2π)), the periodic shapes are:

ShapeFormula
sinesin(θ); the unchanged legacy fast path
halfmax(0, sin(θ))
absabs(sin(θ))
quarterabs(sin(θ)) when frac(θ/π) < 0.5, otherwise zero
alternatingsin(2θ) when t < 0.5, otherwise zero
camelabs(sin(2θ)) when t < 0.5, otherwise zero
square−1 for negative sine, +1 otherwise (including zero); polyBLEP-corrected in Synth
saw2 * frac(t + 0.5) - 1; rising, zero at θ = 0; polyBLEP-corrected in Synth
-

Phase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Non-sine shapes evaluate the same deterministic polynomial sine as sine operators. Synth smooths the square and saw jumps with two-sample polyBLEP residuals and the two half-wave corners with polyBLAMP residuals, sized from each operator's unmodulated phase step at the oversampled rate. This lowers folded aliases of unmodulated carriers (about 8–17 dB at the internal rate in the unit tests, before decimation); under phase modulation the nominal step is only an approximation. Quarter, alternating and camel remain naive, and held noise is never band-limited. None of these oscillators is alias-free, even with the per-voice oversampling/decimator.

+

Phase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Non-sine shapes evaluate the same deterministic polynomial sine as sine operators. Synth smooths the square and saw jumps with two-sample polyBLEP residuals and the two half-wave corners with polyBLAMP residuals, sized from each operator's unmodulated phase step at the oversampled rate. This lowers folded aliases of unmodulated carriers (about 8–17 dB at the internal rate in the unit tests, before decimation); under phase modulation the nominal step is only an approximation. Quarter, alternating and camel remain naive, and held noise is never band-limited. None of these oscillators is alias-free, even with oversampling and the post-mix decimator.

Noise is an original deterministic 17-bit maximal-length LFSR (x^17+x^3+1), seeded with 0x1ffff at every note admission, including pooled slot reuse. The low bit produces bipolar ±1 and advances on the noiseRate time-domain hold clock, independent of output sample rate. Identical admissions reproduce the sequence. Ratio, fixed frequency, detune, tuning, pitch envelopes, live pitch and modulator PM do not pitch noise; existing operatorRatios/operatorFrequencies controls remain valid but have no audible pitch effect on it. Envelope, key/rate scaling, velocity, level, AM and graph routing still apply; noise can be either a modulator or carrier.

This OPM-NE-inspired musical extension allows noise on any operator of any voice, unlike hardware restricted to the last operator of channel 8. It is not a Yamaha/YM2151 fidelity claim. Explicit v1–v6 reject operator waveform or noiseRate with a version-7 requirement; LegacyVoiceV6 preserves the prior shape. Prepared snapshots, bank JSON, content-keyed registration and worklet validation retain both new fields.

Pitch envelope #

diff --git a/doc/expressive-voices.md b/doc/expressive-voices.md index 2b46135..efd9cdb 100644 --- a/doc/expressive-voices.md +++ b/doc/expressive-voices.md @@ -39,7 +39,7 @@ For θ = phase + modulation and fractional turns `t = frac(θ / (2π))`, the per | square | −1 for negative sine, +1 otherwise (including zero); polyBLEP-corrected in Synth | | saw | `2 * frac(t + 0.5) - 1`; rising, zero at θ = 0; polyBLEP-corrected in Synth | -Phase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Non-sine shapes evaluate the same deterministic polynomial sine as sine operators. Synth smooths the square and saw jumps with two-sample polyBLEP residuals and the two half-wave corners with polyBLAMP residuals, sized from each operator's unmodulated phase step at the oversampled rate. This lowers folded aliases of unmodulated carriers (about 8–17 dB at the internal rate in the unit tests, before decimation); under phase modulation the nominal step is only an approximation. Quarter, alternating and camel remain naive, and held noise is never band-limited. None of these oscillators is alias-free, even with the per-voice oversampling/decimator. +Phase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Non-sine shapes evaluate the same deterministic polynomial sine as sine operators. Synth smooths the square and saw jumps with two-sample polyBLEP residuals and the two half-wave corners with polyBLAMP residuals, sized from each operator's unmodulated phase step at the oversampled rate. This lowers folded aliases of unmodulated carriers (about 8–17 dB at the internal rate in the unit tests, before decimation); under phase modulation the nominal step is only an approximation. Quarter, alternating and camel remain naive, and held noise is never band-limited. None of these oscillators is alias-free, even with oversampling and the post-mix decimator. Noise is an original deterministic 17-bit maximal-length LFSR (`x^17+x^3+1`), seeded with `0x1ffff` at every note admission, including pooled slot reuse. The low bit produces bipolar ±1 and advances on the `noiseRate` time-domain hold clock, independent of output sample rate. Identical admissions reproduce the sequence. Ratio, fixed frequency, detune, tuning, pitch envelopes, live pitch and modulator PM do not pitch noise; existing `operatorRatios`/`operatorFrequencies` controls remain valid but have no audible pitch effect on it. Envelope, key/rate scaling, velocity, level, AM and graph routing still apply; noise can be either a modulator or carrier. diff --git a/doc/readme.html b/doc/readme.html index 008f576..78425c0 100644 --- a/doc/readme.html +++ b/doc/readme.html @@ -249,7 +249,7 @@

Optimized distribution capacity guidance. Actual mobile/MIDI and human-listening acceptance still require original physical observations and listener findings, not automated PASS substitutions.

sound-quality covers all three profiles and passed 2,505 lifecycle/headroom/determinism matrix cases plus 24 live-control cases in the recorded v1.8 verification. It also checks independent passband, THD, folded aliases and streaming references; these are controlled numerical gates, not arbitrary-FM or listening certification. voice-quality evaluates the curated preset bank and original synthetic DX7 recipes over register/velocity cells. Peak/RMS are raw dBFS, not LUFS or perceived loudness; host trims do not rewrite synthesis levels. See acoustic-quality evidence and voice-quality notes.

-

Independent references cover Bessel-series high-index PM, contractive delayed feedback, conservative full-level feedback energy, and nested four-operator chain/branched/multicarrier synthesis. The per-voice 4× decimator is a polyphase linear-phase half-band FIR cascade, flat to 0.40 output Fs with at least 81 dB rejection of products that fold below that edge. Two 120-second streaming cases exercise held AM/PM and glide retargets. See acoustic reference mathematics and limits; none certify arbitrary alias-free FM, perceptual quality or hardware fidelity.

+

Independent references cover Bessel-series high-index PM, contractive delayed feedback, conservative full-level feedback energy, and nested four-operator chain/branched/multicarrier synthesis. The shared post-mix 4× decimator is a polyphase linear-phase half-band FIR cascade, flat to 0.40 output Fs with at least 81 dB rejection of products that fold below that edge. Two 120-second streaming cases exercise held AM/PM and glide retargets. See acoustic reference mathematics and limits; none certify arbitrary alias-free FM, perceptual quality or hardware fidelity.

browser-stress exercises real AudioWorklet output, dense admissions/steals, controls, late rejection and queue cleanup under bounded main-thread contention. Timer gaps and diagnostic round trips are observations, not worklet CPU/GC/underrun/glitch measurements. Periodic analyser samples cannot prove uninterrupted audio. The Vite smoke additionally verifies an installed current tarball, non-root production deployment, retained license, CSP and negative asset/MIME cases. Run these separately from deadline benchmarks.

Voice format #

A voice has four operators, an algorithm (0–7), and integer patch feedback (0–7). This is a canonical version 7 entry. Single objects may omit version, name, modIndex (4), and lfo (off/sine). Explicit versions 1–6 retain their original shapes and normalize to 7: v1 excludes key scaling, v1/2 exclude velocity sensitivity, v1–3 exclude LFO waveform, v1–4 exclude expressive v5 fields, v1–5 exclude v6 LFO targets, and v1–6 exclude operator waveform/noise rate.

diff --git a/doc/search-data.js b/doc/search-data.js index 62aef37..4cca8f4 100644 --- a/doc/search-data.js +++ b/doc/search-data.js @@ -1,2 +1,2 @@ // Generated documentation search data. -window.OPM_DOC_SEARCH = [{"title":"OPM.js","text":"OPM.js # OPM.js\n\nA 4-operator FM synthesis engine for the browser, inspired by the Yamaha YM2151 (OPM) sound chip.\n\nOPM.js recreates the classic 16-bit era FM sound — four-operator synthesis with a default eight-voice budget (configurable up to 32), multiple algorithms, feedback, and ADSR envelopes — as a lightweight, zero-runtime-dependency TypeScript engine powered by the Web Audio API, distributed as JavaScript ES modules.\n\n> Status: v1.11.1 (package 1.11.1) is the latest GitHub release, a performance patch over v1.11 (faster sine operators; no API or format changes). v1.11 added: voice format v7 with per-operator waveforms and held noise, optional stereo chorus/reverb, MIDI program-to-voice maps with RPN bend sensitivity, and approximate .opm patch import. Voice v1–v6 inputs remain accepted; runtime dependencies remain empty. The previous v1.10 release (package 1.10.0) uses voice format v6.\n>\n> GitHub release distribution and npm registry publication are separate. Use the release tarball or a source checkout; no npm publication is implied. See the compatibility policy.\n\nUsage guides: English · 繁體中文.\n\nOnline demos (HTTPS): opm.js-package.xyz. Click a playback button to grant audio startup; physical-device and listening acceptance remain separate from a working web page.\n\n## New in v1.11\n\nThese APIs are new in the v1.11 release and are not part of the v1.10 archive. Physical-device, MIDI-hardware and human-listening acceptance remain unverified.\n\n- Voice format v7: per-operator waveform (sine, half, abs, quarter, alternating, camel, square, saw, noise) and noiseRate; see Voice format and the expressive voice reference.\n- Three original noise-based percussion recipes (noise-snare, noise-hihat, explosion) in the bundled bank.\n- Optional stereo chorus and reverb, usable offline (createStereoEffects, applyEffects) and live (createEffects): audio buses.\n- MIDI programVoices/drumVoices maps, GM-family starter maps and RPN 0 bend sensitivity for files and the live adapter: MIDI files, MIDI performance.\n- importOPM/describeOPM for approximate conversion of VOPM/MXDRV-family .opm text patches: expressive voices.\n\n## New in v1.10\n\nThese APIs and examples are included in v1.10. Historical release archives remain unchanged. GitHub distribution does not imply npm publication or deployment. Source contributors run npm ci and npm run build; consumers can install the release tarball without a build toolchain.\n\n- Inert, bounded version-1 Arrangement parse/save/load through root/core APIs; authored definitions replay from beat 0, not DSP checkpoints: portable Arrangement projects.\n- Opt-in expressive Standard MIDI files with lossSummary; default controls: 'omit' keeps note-only behavior: MIDI files.\n- Workload-scoped measured capacity candidates, not universal device limits: acoustic quality.\n- Bounded local MIDI capture and physical-device/listening campaign readiness: MIDI performance. Physical/mobile/MIDI and human-listening acceptance remain unverified until genuine observations are supplied.\n- A cross-feature contract table connects replay, rendering, performance and host boundaries. Existing thirteen demos include two original songs, Play/Stop, score-project save/load and WAV export.\n- Static HTML documentation and a complete generated API reference are rebuilt after distribution emit and included under doc/ in locally built packages. Markdown remains the source of truth. Open doc/index.html locally or serve the whole doc/ directory; navigation and local search require no server or runtime dependencies.\n\nChoose a workflow: try the demos · install into an npm project · deploy in a browser · render in Node.js · API reference · troubleshooting.\n\n## About\n\nThe Yamaha YM2151 (OPM) powered a generation of arcade boards and the Sharp X68000, defining the sound of the mid-1980s with its 8-channel, 4-operator FM architecture. OPM.js brings that architecture to the browser with a TypeScript implementation and directly deployable JavaScript.\n\nOPM.js is a musically-accurate reimplementation, not a cycle-accurate hardware clone: envelope timing and modulation curves are tuned to sound correct rather than to reproduce silicon behaviour bit-for-bit.\n\n## Features\n\n- 4-operator FM synthesis with 8 connection algorithms and hardware-style feedback\n- Per-operator dB-domain ADSR, level/rate key scaling, velocity sensitivity and optional fixed-Hz oscillators\n- Per-voice pitch envelope and LFO with four AM / PM waveforms, delay, phase, note/global synchronization and per-operator depth targets\n- Configurable 1–32-voice polyphony (default 8; larger budgets are opt-in) with oldest, release-first or quietest stealing, per-note voice priority that protects melodies, and at most eight bounded ~5 ms stealing fades\n- Smooth live timbre controls — independent pitch, expression/pan/modulation/level, feedback, ratio/fixed-Hz and LFO ramps; editable ADSR anchored at current dB\n- Audio-clock scores and musical Transport — pause/resume, seek, loop, BPM/tempo maps with linear tempo ramps, beat/bar conversion, quantization and swing helpers\n- Adaptive music — looping layers and sections that switch on beat/bar boundaries while shared layers keep sounding\n- Part-scoped performance policies — sustain, poly/mono legato, last/high/low priority, per-key and per-part live controls, voice limits, and an optional user-granted Web MIDI adapter\n- Prepared immutable patches and pooled DSP state — reuse bounded voice slots instead of allocating typed state on every admission\n- Incremental PCM16/PCM24/Float32 WAV export with static Worker rendering, progress, cancellation and sink backpressure; approximate six-to-four-operator DX7 import\n- Pre-saturation mix gain and global tuning — reference A4 and interpolated 128-note cents offsets\n- DSP quality profiles — eco 2×, default standard 4× and high 8× oversampling with linear-phase half-band FIR decimation; bounded state and explicit CPU/aliasing tradeoffs\n- Atomic voice-bank replacement, removal and JSON export preserving queued/sounding patch snapshots\n- TypeScript declarations for browser, core, and voice-module exports\n- AudioWorklet-based DSP — synthesis runs off the main thread\n- Optional stereo chorus and reverb — the same pure DSP offline and in a separate opt-in AudioWorklet; no built-in mixer\n- Zero runtime dependencies, built on the Web Audio API\n- Works in the browser and Node.js (offline rendering)\n\n## Requirements\n\n- Modern browser with ES modules and AudioWorklet (served over HTTPS or localhost)\n- Node.js 22+ for offline rendering and tests\n\n## Getting started\n\n### Try the checkout\n\nObtain the v1.10 source archive or checkout to run the thirteen examples. HTML pages, Vite tooling and development programs are checkout-only, not included in the installable .tgz. Links to these resources below open their repository files; serve the corresponding local HTML to run them. Repository commands run in the directory containing package.json, src/, and scripts/ (called OPM.js in the examples).\n\nThe committed dist/ is ready to use; trying the demos needs no npm installation or build. With Python 3 available, run from that repository root:\n\nsh\npython3 -m http.server 8000\n\n\nOpen http://localhost:8000/index.html for the English example catalog:\n\n| Example | Demonstrates |\n| --- | --- |\n| Basic notes and voices | Presets, pitch, velocity, timed and held notes |\n| Melody and chord scheduling | Twinkle, Twinkle, Little Star, absolute timing, bounded lookahead and cancellation |\n| Live expression and per-voice LFO | Bend/glide a held note; change expression, pan and modulation; scheduled release |\n| Shared AudioContext and routing | Host GainNode, manual routing, diagnostics and borrowed-context ownership |\n| Offline synthesis and WAV | Stereo PCM, frame counts and PCM16 WAV without an AudioContext |\n| Advanced playground | Key scaling, suspend/resume, diagnostics and approximate DX7 import |\n| Preset and DX7 audition | Register/velocity grid, A/B comparison, raw peak/RMS reports and rendered WAV |\n| Scores, Transport and performance | Pause/seek/loop/tempo, sustain/mono priority, live controls, quality/bank tools, multi-format Worker export and physical-device observations |\n| Adaptive game music | Boundary-quantized section/layer switching, tempo ramp, swing, voice priority |\n| Playable instrument and Web MIDI | Mono lead + limited poly pad, per-part/per-key controls, optional user-granted MIDI |\n| Independent buses and stems | Three OPM instances on one AudioContext, separate gain/filter/echo, offline stems |\n| FM sound designer | Operators, algorithm graph, dB envelopes, selective LFO, live edits, patch JSON |\n\nClick a playback button to start audio. Stop the server with Ctrl+C when finished. Do not open the HTML through file://. Pages and runtime messages are English; TypeScript helpers live in demo/.\n\nIf dist/ is missing or you have changed src/ or demo/ TypeScript, regenerate it using the development commands first.\n\n### Install into an npm project\n\nDownload opm.js-1.10.0.tgz from the v1.10 release, then install it from your application's root:\n\nsh\nnpm install /actual/path/to/opm.js-1.10.0.tgz\n\n\nNo checkout or consumer build is required. The registry package name does not guarantee npm publication; do not substitute npm install opm.js or an assumed CDN URL. The v1.10 asset includes these synchronized guides; the original v1.8 tag and archive remain unchanged.\n\nTo build a local tarball instead, run from the OPM.js source checkout with Node.js 22+ and npm:\n\nsh\nnpm ci\nnpm pack\n\n\nnpm pack builds automatically and produces opm.js-1.10.0.tgz. To create a new application beside the checkout:\n\nsh\ncd ..\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install ../OPM.js/opm.js-1.10.0.tgz\n\n\nFor an existing application, run only the install command from its root, adjusting the tarball path. Consumers do not install the engine's development dependencies or need a build step. The installed package contains minified JS, .js.map source maps with embedded TypeScript, generated .d.ts declarations, demo scripts, usage documentation, and legal files—not HTML demo pages, separate source files, or build scripts.\n\nFor a bundled host, use the checkout-only installed-package Vite example: it preserves the complete engine/worklet/Worker tree and license, supports a non-root deployment base, and checks production CSP, MIME types and missing assets. Maintainers can follow the package verification and publication prerequisites; this checkout has no npm publishing workflow, and authentication/provenance must be established separately.\n\n### Use in a browser\n\nFrom the installed application's root, use the package's deployment CLI with a fresh destination:\n\nsh\nnpx --no-install opm-assets copy public/opm\n\n\nThe CLI copies the complete dist/ tree plus LICENSE, hashes the assets, and never overwrites a differing destination. Repeating an identical deployment reuses it. POSIX mkdir -p public/opm, cp -R nodemodules/opm.js/dist/. public/opm/ and cp nodemodules/opm.js/LICENSE public/opm/LICENSE remain an equivalent manual layout, but without the CLI's verification and overwrite protection.\n\nWithout npm, copy the checkout's complete dist/ contents and its LICENSE into your site's opm/ directory instead. The commands use a POSIX shell; copying those same files manually is equivalent.\n\nKeep this layout; individual worklets and their imported modules must not be moved independently:\n\ntext\npublic/\n index.html\n opm/\n api/\n core/\n voices/\n worklet/\n worker/\n tools/\n LICENSE\n\n\nSave this complete example as public/index.html:\n\nhtml\n\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>OPM.js example\u003c/title>\n\u003cbutton id=\"play\" type=\"button\">Play brass chord\u003c/button>\n\u003coutput id=\"status\" aria-live=\"polite\">Ready\u003c/output>\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });\n status.textContent = 'Playing brass chord';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\u003c/html>\n\n\nFrom the application root, serve that public directory:\n\nsh\npython3 -m http.server 8000 --directory public\n\n\nOpen http://localhost:8000/ and click Play brass chord. Production hosting must use HTTPS. The user gesture and awaited start() are required for browser audio playback.\n\nBare names such as import { OPM } from 'opm.js' do not resolve in plain browser HTML without an import map or a bundler. Even with a bundler, do not assume it copies the AudioWorklet and its dependencies: the static-copy layout above is the supported baseline. Import the served URL and deploy the complete directory together.\n\n### Render in Node.js\n\nIn the npm application's root, save this as render.mjs:\n\njs\nimport { Synth } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst sampleRate = 44100;\nconst synth = new Synth(sampleRate);\nconst id = synth.noteOn(brass, 60);\nconst left = new Float32Array(sampleRate);\nconst right = new Float32Array(sampleRate);\nsynth.render(left, right, 0, 22050);\nsynth.noteOff(id);\nsynth.render(left, right, 22050, 22050);\nconsole.log(left.some(sample => sample !== 0), synth.errorCount);\n\n\nRun node render.mjs; it prints true 0. The two buffers contain one second of PCM, including release after the first half-second. This does not play through Node's speakers or write a WAV file. Use .mjs or a project with \"type\": \"module\"; no CommonJS entry is provided.\n\nTo run the same script from a checkout instead, change the imports to ./dist/core/index.js and ./dist/voices/brass.js.\n\n## API reference\n\nThe following reference describes v1.10. Historical release archives remain unchanged. The compiler-generated reference includes every public root/core/voice/tool export and its full declarations; it is refreshed by npm run build. See the cross-feature contract table for IDs, lifecycle observations, ownership and persistence.\n\nAll times are in seconds unless stated otherwise. Browser and Node examples intentionally use different import styles:\n\n| Exports | Installed npm import | Browser URL in the layout above |\n| --- | --- | --- |\n| OPM, createLookaheadScheduler, playSequence, streamSequence, createTransport, createArrangement, createPerformance, renderSequenceInWorker | opm.js | ./opm/api/index.js |\n| Synth, renderNote, core helpers | opm.js/core | ./opm/core/index.js |\n| Score/Arrangement project parsers, serializers and compileBeatSequence | opm.js or opm.js/core | ./opm/core/index.js |\n| importMidiFile, exportMidiFile | opm.js/midi-file, root or core | ./opm/core/midi-file.js |\n| brass | opm.js/voices/brass.js | ./opm/voices/brass.js |\n| parseVoiceBank, validateVoice, schema helpers | opm.js/voices/schema.js | ./opm/voices/schema.js |\n| importDX7, describeDX7 | opm.js/voices/dx7.js | ./opm/voices/dx7.js |\n| importOPM, describeOPM | opm.js/voices/opm.js | ./opm/voices/opm.js |\n\n### Browser: OPM\n\n| Call | Contract |\n| --- | --- |\n| new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain = 1, tuning = {}, stealing = 'oldest', interruption = 'cancel', quality = 'standard', maxVoices = 8 } = {}) | Optional integer sample rate 8000–96000 Hz; voice limit 1–32 (default 8; each voice costs CPU). Quality and polyphony are immutable and survive node recreation. Borrow an existing AudioContext; OPM never closes or suspends it. Omit destination to use its destination, or use null for explicit routing. Optional workletUrl relocates the complete secure same-origin worklet module tree without relaxing CSP/MIME checks. |\n| await opm.start() / await opm.resume() | Initialize/resume the context and worklet from a user gesture; concurrent starts coalesce. Existing contexts are resumed after browser suspension. Await before playing. |\n| opm.connect(destination) / opm.disconnect(destination?) | Connect/disconnect the started worklet output; return opm. Omitted disconnect destination removes all output connections. Destination must belong to the same context. |\n| opm.loadVoice(name, voice) | Strictly validate/copy a voice; register/replace a name for future notes, at most 128 entries. Name: 1–64 ASCII letters, digits, _, or -. Works before start(). |\n| opm.replaceVoiceBank(source) | Atomically validate and replace the entire registry from bank JSON or a complete voice array. Maximum 128 entries/256 KiB JSON; a plain empty array clears it. Failure leaves the old bank unchanged. |\n| opm.removeVoice(name) / opm.exportVoiceBank() | Remove a lookup and return a boolean / export detached canonical named JSON, capped at 256 KiB. Existing queued/sounding notes keep their immutable patches. An empty bank exports []. |\n| opm.playNote({ voice = 'brass', note, time = 0, at, duration = null, velocity = 1, pan = 0, voicePriority = 0, late = 'start' }) | Return a positive safe-integer ID without wrapping. Finite fractional MIDI 0–127; use either relative time (0–60 seconds) or absolute nonnegative AudioContext at (at most 60 seconds ahead), never both. Duration (0,60] or null holds until stop; velocity 0–1; pan −1–1; voicePriority an integer 0–127 (higher cannot be stolen by lower; see audio buses). Requires start(). |\n| opm.stop(id, options = {}) | Return a command ID. Optional at schedules cancellation/release; ordinary stop retains release-tail automation. Immediate { cancelControls: true } also removes that ID's queued controls/off/onset. cancelControls cannot coexist with at, even when false. Unknown/already-released ordinary targets reject; release is not immediate mute. |\n| opm.updateNote(id, controls, { at } = {}) | Return a command ID. Apply immediately or at an absolute audio time. Controls before onset persist for that pending note; released notes remain controllable until they end. Invalid/inactive/queue-full commands report rejection. |\n| opm.allNotesOff() / opm.panic() | Return a command ID; both bypass a full scheduled-event queue. All-notes-off cancels pending onsets/automation and releases active gates naturally. Panic removes every active/release/steal tail immediately and emits a reset; routing and prepared patches survive. |\n| opm.setMixGain(gain) / opm.setTuning(tuning) | Require a started node and return a command ID. Replace the setting immediately; retain it across node recreation. They affect active and future voices without restarting envelopes/phases. |\n| opm.voices | Defensive ReadonlyMap of deeply frozen patches; mutate the registry only through its bank/load methods. |\n| await opm.getDiagnostics() | Return { type: 'diagnostics', requestId, activeVoices, pendingEvents, errors, rejectedNotes }. Requires running context; at most 64 outstanding requests. Suspend/close/processor failure rejects pending requests; resume before retrying. |\n| await opm.close() | Serialize disposal against initialization, disconnect the node, and close only an owned context. Emit a global reset: close; teardown/failure does not guarantee individual terminal note replies. A later start() recreates owned contexts; borrowed contexts remain usable. Cancel application timers separately. |\n| opm.subscribe(listener) | Multicast event subscription; returns an idempotent unsubscribe function. Listener exceptions are isolated. |\n| await opm.waitForCommand(commandId, { timeout, signal } = {}) | Cancellable admission acknowledgement; timeout default 5000 ms, integer 1–60000 ms; 64 pending waits/128 retained receipts. Rejection throws CommandRejectedError; timeout, abort and teardown reject. Acceptance is not scheduled execution/completion. |\n| await opm.dispose() | Terminal disposal, including listener/helper cleanup. Unlike restartable close(), a disposed instance cannot start again. |\n\nonEvent(event) receives { type: 'note', id, state, reason?, frame, time } with states accepted, started, released, ended, stolen, cancelled, or rejected; frame/time report actual admission, dispatch or completion boundaries. It also receives diagnostics, processor errors, { type: 'command', command, commandId?, id?, state: 'accepted' | 'rejected', reason?, frame, time }, { type: 'context', state, frame, time }, and { type: 'reset', reason, commandId?, frame, time }. Command-triggered resets carry their initiating ID independently of receipt-cache retention. Command acceptance means admission/immediate application, not guaranteed future execution; correlate by commandId. Reset reasons are close, failure, panic, and interruption: clear application gate bookkeeping on reset. Callback exceptions are isolated.\n\nAt most maxVoices logical voices (default 8, opt-in up to 32, including release tails) are active, with up to eight short fading remnants. stealing: 'oldest' is the default; 'release-first' chooses the oldest released voice before held voices; 'quietest' uses current carrier envelope × velocity × expression, not waveform zero crossings, with oldest ties. A full engine steals only among voices of equal or lower voicePriority (lowest first); if every voice outranks the new note it is refused with rejected reason priority, without changing anything else. The worklet bounds pending events and tracked note IDs to 256 each. Future timed notes use start/off events; held notes use a start event. Duplicate IDs and overflow reject before enqueueing; terminal events remove obsolete events. Schedule long pieces incrementally. More than eight voices and more buses cost CPU roughly linearly: measure on target devices.\n\nlate: 'start' preserves the full duration after the actual delayed start; late: 'drop' rejects a missed start with reason late, including worklet-delivery delays. Same-frame stops precede onsets, then controls. Scheduled stops/controls obey the same 60-second future horizon and 256-event bound.\n\nNoteControls accepts nonempty subsets of pitch (−48..48 semitones), glide (0..10 seconds, requires pitch), expression (0..1), pan (−1..1), modulation (0..2), operatorLevels (four 0..2 multipliers), feedback (continuous 0..7), lfoRate (0..20 Hz), amDepth (0..1), pmDepth (0..1200 cents), operatorRatios (four 0.125..32 values), operatorFrequencies (four 1..20000 Hz values or null to restore ratio mode), and operatorADSR (four complete ADSR objects). ramp (0..10 seconds) independently interpolates supplied rampable controls; glide remains linear in semitones. Omitted controls retain state and oscillators keep phase/feedback history. ADSR edits reanchor from current dB: held notes restart attack, released notes begin their new key-scaled release; zero release silences the source and drains the filter. ADSR is not a rampable control. Fixed-Hz controls still follow live pitch but not tuning-table transposition. There is no global setLFO(). See expressive controls and host integration.\n\nSince v1.9: gain is an independent 0–1 multiplier, default 1, using the same ramp range. It composes with expression rather than replacing it and does not restart phase or envelopes.\n\ncreateLookaheadScheduler(opm, callback, { onError, horizon = 0.2, interval = 0.025, maxNotes = 32 }) returns { running, start(), stop(), dispose() }. Its callback receives the half-open absolute window { from, to, maxNotes } and returns at most maxNotes notes with at in that window and a numeric duration. Horizon is 0.02–10 seconds; interval is 0.005–1 second and less than horizon; maxNotes is an integer 1–128. At most 128 gates remain outstanding. Call start() from a gesture and handle its rejected promise; callback/admission errors stop the scheduler and reach required onError. Timer stalls skip missed windows rather than bursting old notes. Stop/dispose cancel only its notes and never close OPM. See the checkout-only song demo and both usage guides for executable recipes.\n\nAny non-running context stops lookahead scheduling, as does a reset; onError reports it and restarting is explicit. Default interruption: 'cancel' clears existing notes and automation on suspension/interruption, preventing replay on resume. Opt-in 'preserve' retains direct-note gates/queued events across suspension, but does not automatically restart lookahead. Resume from a fresh user gesture. A closed borrowed context must be replaced by its host.\n\n### Offline: Synth and renderNote\n\n| Call or property | Contract |\n| --- | --- |\n| new Synth(sampleRate, maxVoices = 8, options = {}) | Required finite sample rate 8000–192000 Hz; integer voice limit 1–32. Options are { mixGain, tuning, stealing, quality }, identical to OPM. No Web Audio is required. |\n| prepareVoice(voice) | Strictly validate, detach and deeply freeze an opaque PreparedVoice once for repeated core admissions; forged lookalikes cannot bypass validation. |\n| synth.noteOn(voiceOrPrepared, note, id?, { velocity = 1, pan = 0, voicePriority = 0 } = {}) | Raw voices validate/copy each call; prepared patches reuse validated data. Finite fractional MIDI 0–127; explicit positive safe-integer ID unique among active notes; velocity 0–1; pan −1–1; voicePriority an integer 0–127. A full synth with no equal-or-lower-priority victim throws VoiceAdmissionError before changing state or consuming an ID. |\n| synth.noteOff(id) | Begin release at current rendered time; return true, or false for unknown/already-released IDs. |\n| synth.updateNote(id, controls) | Apply the same live controls; return true for an active/releasing note or false for an unknown/terminal ID. |\n| synth.allNotesOff() / synth.panic() | Release active gates naturally / immediately clear all logical voices, release/steal tails and spill. Panic notifies active notes with terminal reason cancelled. |\n| synth.setMixGain(gain) / synth.setTuning(tuning) | Replace mix drive / tuning without phase or envelope reset; return no value. |\n| synth.render(left, right, offset = 0, length = nativeLeftLength - offset) | Write native Float32Arrays; nonnegative safe-integer offset/length must fit both actual buffers. Shadow metadata/methods cannot enlarge work; proxies/forgeries and coercible nonnumbers reject. Split calls at offline events. Center pan is dual mono. |\n| synth.currentFrame, synth.errorCount, synth.lastStolenId | Cumulative frames, numerical-failure count, and stolen ID (null when the last successful note-on stole none). |\n| synth.onVoiceEnded = (id, reason) => … | Exactly-once terminal notification (stolen, ended, error, cancelled). Slots recycle before callbacks; replacements first render next frame. Recursive render() rejects. A stolen note's fade can outlive its notification. |\n| renderNote({ voice, note = 60, duration = 0.5, velocity = 1, pan = 0, voicePriority = 0, sampleRate = 44100, mixGain = 1, tuning = {}, stealing = 'oldest', quality = 'standard' }) | Return { samples, left, right, sampleRate, diagnostics: { errors } }, with samples === left. Complete voice required; finite note/duration/velocity/pan clamp to 0–127/0–30/0–1/−1–1. Integer sample rate 8000–96000; synth options and integer voicePriority 0–127 validate strictly. |\n| encodeWav({ left, right?, sampleRate, format = 'pcm16' }) | Return little-endian RIFF/WAVE Uint8Array: PCM16, PCM24 or Float32. Omit right for mono. Native Float32 channels must contain finite samples in −1–1 and match lengths. Invalid amplitudes reject rather than clip; integer rate 8000–192000, full-buffer budget 4,000,000 frames. |\n| createWavEncoder({ sampleRate, channels, format = 'pcm16', totalFrames }) | Incremental RIFF32 output: header() once, encode({ left, right? }) for 1–65536 frames per chunk, then finalize() at exactly the declared frame count. Channels: 1 or 2. Bounded encoded chunks and \u003c4 GiB files, independent of the full-buffer frame limit. |\n\nrenderNote() uses Synth for identical envelope timing, LFO, filtering, velocity, pan, and saturation. It retains ceil((duration + longestEffectiveRelease + 0.01) * sampleRate) frames, including note-dependent rate scaling, with a 4,000,000-frame budget.\n\nSynth preallocates maxVoices + 9 state slots: logical voices, eight possible fades and one admission scratch slot. Prepared admission allocates no new per-voice typed state. Browser snapshots use a content-keyed 128-slot LRU cache; validated replacement reuses a registration ID after saturation rather than falling back to inline transport. Queued/active notes retain their original immutable prepared snapshot. Raw browser inputs still validate freshly.\n\nPan uses a center-normalized constant-power law: left/right gains are sqrt(2) cos/sin((pan + 1) pi / 4) before saturation. Center preserves the previous per-channel gain; hard sides mute the opposite channel and boost the selected channel by sqrt(2). This does not promise constant loudness after polyphonic saturation.\n\nOther core exports: normalizeVoice(voice) makes a strict detached copy; envelopeAt(time, gate, adsr) evaluates the dB-domain envelope; ALGORITHMS contains eight immutable routing graphs; HEADROOM is 0.7, OVERSAMPLE is 4, and MAXRENDERSAMPLES is 4,000,000. sampleRateValue(value) validates an integer rate of 8000–96000. Schema helpers are described below.\n\n### Optional stereo effects\n\ncreateEffects(context: BaseAudioContext, options?: EffectsOptions): Promise\u003cOpmEffects> creates an unconnected stereo insert with input, output, ready, update(params), reset() and dispose(). It never resumes or closes the host context. Route opm.node after await opm.start() into fx.input, then connect fx.output to your host graph.\n\ncreateStereoEffects(sampleRate, options?) and applyEffects(left, right, sampleRate, params) are exported at root and opm.js/core. The engine processes native Float32 buffers in place with bounded preallocated delay state; the helper returns copies with a conservative tail and enforces the 4,000,000-frame budget. Options are { chorus?: { rate, depth, mix, feedback?, voices? }, reverb?: { size, damping, mix, preDelay?, width? }, order? }. Updates replace the configuration with smooth ramps; omitted sections and mix 0 preserve finite dry input bit-for-bit. Wet paths reserve at least 3 dB headroom using a linear crossfade. Ranges, gain law, memory bounds, routing and offline details.\n\n### Mix gain and tuning\n\nmixGain is finite 0–1 (default 1), applied before final tanh saturation; lowering a downstream GainNode cannot undo saturation distortion. Zero mutes PCM without stopping note progression. Host gain remains responsible for listening volume.\n\ntuning: { referenceHz = 440, offsets } replaces the complete global tuning. Reference A4 is 20–20000 Hz; omitted offsets mean zero, otherwise supply exactly 128 finite cents offsets in −4800..4800. Inputs are detached/frozen. Fractional MIDI interpolates neighboring offsets, with MIDI 127 using its own endpoint. Retuning preserves phase/envelopes and does not change the original note used for key scaling. normalizeTuning and tuningFrequency are also core exports.\n\n### Shared live/offline scores\n\nSequenceEvent is a note { type: 'note', id, time, duration, note, voice?, velocity?, pan?, voicePriority? }, explicit stop { type: 'stop', id, time }, or control { type: 'control', id, time, controls }. Times are score-relative seconds; note IDs are unique positive safe integers and commands must reference score notes. Omitted voice is brass; priority defaults to 0 and accepts integers 0–127. Whole-score validation/detachment precedes submission. Limits: 128 notes, 256 reserved slots (two per note plus explicit commands), and 60 seconds including gates/commands. Controls before onset apply at onset; same-frame stop precedes onset, then controls.\n\njs\nimport { playSequence } from 'opm.js';\nimport { renderSequence, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst events = [\n { type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },\n { type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },\n];\nconst voices = new Map([['lead', brass]]);\nconst offline = renderSequence(events, { voices, sampleRate: 48000, mixGain: 0.5, tuning: { referenceHz: 442 } });\nconst wav = encodeWav({ left: offline.left, right: offline.right, sampleRate: offline.sampleRate });\n// In a browser gesture, with an initialized OPM using the same gain/tuning:\n// opm.loadVoice('lead', brass);\n// const playback = playSequence(opm, events, { at: opm.context.currentTime + 0.05 });\n// playback.stop(); // idempotent; affects only that playback's original node\n\n\nplaySequence returns { ids: ReadonlyMap\u003cscoreId, noteId>, stop() }; acceptance still arrives through OPM events. It is not an atomic reservation against other callers filling the worklet queue. prepareSequence(events, { voices }) exposes the immutable resolved score; renderSequence(events, { voices, sampleRate, maxVoices = 8, mixGain, tuning, stealing, quality }) returns stereo PCM with conservative release-tail capacity, including live ADSR edits, and enforces 4,000,000 frames. maxVoices is an integer 1–32. Live/offline sample parity assumes matching settings, rate, frame origin and no competing notes. Longer scores use the separate chunked/streaming helpers.\n\n#### Long scores without aggregate PCM\n\nprepareLongSequence validates a separate maximum of 65,536 input events and 24 hours including gates/commands. estimateSequenceCapacity reports frame/PCM/chunk bytes and short-helper eligibility. renderSequenceChunks uses two reusable buffers; consume/copy each chunk's valid frames before advancing. cancel(), iterator return or an AbortSignal prevents further advancement; optional maxFrames caps cumulative work. Full-buffer rendering/encodeWav retain their limits; incremental createWavEncoder uses a separate RIFF32 file-size bound.\n\njs\nimport { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';\nconst score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];\nconst options = { sampleRate: 96000, chunkFrames: 4096 };\nconsole.log(estimateSequenceCapacity(score, options));\nconst render = renderSequenceChunks(score, options);\nlet frames = 0, energy = 0;\nfor (const chunk of render) {\n for (let i = 0; i \u003c chunk.frames; i++) energy += chunk.left[i] ** 2;\n frames += chunk.frames; // No aggregate PCM retained.\n}\nconsole.log(frames, energy, render.diagnostics.errors);\n\n\nstreamSequence(opm, score, { at, horizon, interval, maxSlots, signal, onError }) returns { running, ids, start(), pump(), stop(), dispose() }. It validates the whole bounded long score, then schedules note/control/stop windows using held gates and incremental releases, preserving cross-window ID mappings and only cancelling its own notes. Start from a gesture; context interruption/reset stops the stream and requires explicit restart. Shared worklet admission remains bounded and is not atomically reserved against unrelated callers. See streaming contracts, capacity and browser recipe and checkout-only example 08.\n\n### Portable score projects and MIDI files (introduced in v1.9)\n\nparseScoreProject(source: string | object) returns a canonical frozen ScoreProject with version: 1, beat events, normalized tempoMap, timeSignature, named complete voices, and synthesis settings (sampleRate, quality, maxVoices, mixGain, tuning, stealing). serializeScoreProject(project) emits bounded canonical JSON. Projects are limited to 8 MiB, 65,536 events and 128 voices (256 KiB total voice JSON); notes refer to stored voice names, with omitted voice requiring stored brass. Missing tempo/meter/settings normalize to 120 BPM, 4/4 and the normal synthesis defaults.\n\njs\nimport { parseScoreProject, serializeScoreProject, compileBeatSequence } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst project = parseScoreProject({\n version: 1,\n voices: { brass },\n events: [{ type: 'note', id: 1, beat: 0, duration: 2, note: 60 }],\n});\nconst seconds = compileBeatSequence(project.events, {\n tempoMap: project.tempoMap, voices: new Map(Object.entries(project.voices)),\n});\nconst saved = serializeScoreProject(project);\n\n\ncompileBeatSequence(events, { tempoMap?, bpm?, voices? }) validates beat events and returns second-based SequenceEvent[] for existing full/chunked/Worker rendering. Live Transport uses the original beat events and tempo map; load the project's named voices into OPM first. Keep the project's synthesis settings when choosing the live/offline engine. See score projects for complete recipes and bounds.\n\nimportMidiFile(bytes, options?) / exportMidiFile(events, options?) are exported from root, core and opm.js/midi-file. Import returns { events, tempoMap, timeSignature, warnings, lossSummary }; format 0/1 PPQN only, with strict byte/track/event budgets and malformed/truncated rejection. channelVoices/defaultVoice map channels to named FM voices; optional programVoices and drumVoices select voices at note onset. Default import applies sustain to durations, warns about unsupported data and rejects unclosed notes; explicit policies may reject unsupported data or close notes at the end. Default controls: 'omit' keeps note-only import and rejects control events/nonzero pan on export. Opt-in expressive conversion is described below; export still rejects unsupported controls, fractional note pitches, nonzero priority, ambiguous channel ownership and linear tempo ramps rather than silently losing them. See MIDI files for policy and loss details.\n\n### Portable Arrangement and expressive MIDI files (v1.10)\n\nThese additions are included in v1.10; the historical v1.9 archive remains unchanged.\n\nparseArrangementProject(source: string | object) / serializeArrangementProject(project) are available from root and core. The separate ArrangementProject format is version 1 and stores named layers, sections, initialSection, normalized tempoMap/timeSignature, named complete voices and the same synthesis settings as score projects. Limits are 8 MiB, 128 voices/256 KiB voice JSON, 16 layers, 32 sections, 256 beats per loop and 65,536 total events. Live and saved definitions share validation; score-project version 1 is unchanged.\n\njs\nimport { parseArrangementProject, serializeArrangementProject } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst arrangementProject = parseArrangementProject({\n version: 1, voices: { brass },\n layers: [{ name: 'lead', length: 4, gain: 0.6,\n events: [{ type: 'note', id: 1, voice: 'brass', beat: 0, duration: 1, note: 60 }] }],\n sections: [{ name: 'intro', layers: ['lead'] }], initialSection: 'intro',\n});\nconst arrangementJSON = serializeArrangementProject(arrangementProject);\n\n\nLoad stored voices into an engine constructed with the saved settings, then pass the project's layers, sections, initial section, tempo and meter to createArrangement. Parsing/loading does not start playback or restore phases/envelopes, pending changes or external routing. The checkout adaptive page supports project save/load; see the complete project replay recipes.\n\nBoth MIDI file adapters accept controls: 'preserve' | 'omit' (default 'omit', preserving the original note-only behavior) and an initial pitchBendRange of 0–48 semitones (default 2). Preserve-mode import and live MIDI now decode channel-local RPN 0 sensitivity, including cents, clamped to 48 semitones. Export still uses the explicit range and emits no RPN or program changes. Export accepts only channel-representable controls; incompatible overlap, unsupported controls/ramps and other losses reject instead of being guessed. Import returns frozen lossSummary groups: omissions, approximations and preservedControls; preserved counts refer to MIDI wire messages, not generated note-control fanout. Bank selection remains omitted. See expressive SMF policies and loss accounting.\n\nPreservation is of representable gate/control meaning, not archived MIDI bytes or complete live-performance fidelity. Channel expression after a closed gate reports the conservative release-tail-controls approximation because patch-dependent tails are unknown; unsupported: 'reject' rejects it. Export rejects after-gate controls.\n\nMidiVoiceMap = Readonly\u003cRecord\u003cnumber, string>> | ReadonlyMap\u003cnumber, string> maps integer 0–127 keys to named voices (at most 128 own-data entries). File import accepts programVoices?: MidiVoiceMap and drumVoices?: MidiVoiceMap; live createMidiAdapter(performance, access, options?) accepts programVoices?: MidiVoiceMap, but no drum map. Program changes affect later onsets only; unmapped file programs fall back to channel/default voices, while unmapped live programs are ignored. gmProgramVoices: Readonly\u003cRecord\u003cnumber, string>> and gmDrumVoices: Readonly\u003cRecord\u003cnumber, string>>, exported from root/core, are frozen artistic starter maps, not a GM sound-set claim. Supply a voice bank containing those names; missing voices use the normal unknown-voice errors.\n\nperformance.configurePart(part, options, policy?: { preserveNotes?: boolean }) can retain existing gates on voice changes with { preserveNotes: true }, as used by the live adapter. Default configuration still clears gates on voice/mode changes; mode changes always clear. RPN selectors CC100/101 cannot be custom-mapped; CC6/38 mappings apply only when RPN 0 is not selected. See live MIDI policies.\n\n\n### Musical Transport and performance policies\n\ncreateTransport(opm, beatScore, options) uses quarter-note beats for event positions and note durations. It provides start()/resume(), pause(), stop(), seek(beat), setTempo(bpm), setTempoMap(points), setLoop({ enabled, from, to }), pump() and dispose(), with defensive state/cursor/ID snapshots. BPM is 1–1000 (default 120); tempo maps start at beat 0 and contain at most 1024 increasing points. The AudioContext clock drives bounded scheduling; pause/seek/loop affect only owned notes. Restarting reconstructs scalar/ratio ramps and remaining durations but restarts envelopes/phases; unfinished fixed-Hz transitions restore the latest Hz/null policy immediately. This is musical seeking, not a DSP-state snapshot. See Transport contracts.\n\nIn the current checkout, TransportOptions.startupLead is a finite 0–10 seconds, default min(0.05, horizon / 2). Startup, resume, seek and clock reconstruction anchor in the future and keep musical position fixed during that lead, avoiding the historical cold-start beat-0 scheduling defect. Set 0 to opt out. late:'drop' still rejects genuinely late admissions; this is not a deadline guarantee. The historical v1.8.1 tarball lacks this fix.\n\nThe root and core export beatsToSeconds, secondsToBeats, normalizeTempoMap, beatToBarBeat and barBeatToBeat. Meter denominators are powers of two through 32; numerator is 1–32. Positions are bounded to 86400 quarter-note beats.\n\ncreatePerformance(opm, options) manages 1–16 parts (default 16), at most 128 physical keys and tracked gates system-wide. Configure a part's voice, poly/mono mode, legato and last/high/low priority; call noteOn(part, note, { velocity }) to obtain a physical key ID, noteOff(part, keyId), sustain(part, boolean), updatePart(part, { glide, pan, expression }), allNotesOff(part?), getPart(part) and dispose(). Equal-pitch keys remain distinct. Held keys take precedence over pedal-only keys. Mono legato preserves onset velocity/envelopes within ±48 semitones of the original onset; farther moves retrigger at the actual pitch. Reset/interruption clears helper ownership even under preserve policy; scoped cleanup never panics unrelated notes. This is a performance helper, not a MIDI driver. See performance recipe.\n\n### Worker WAV export and quality profiles\n\nrenderSequenceInWorker(score, { ...chunkedOptions, format, sink, onProgress, signal, workerUrl, startupTimeoutMs, phaseDiagnostics, onPhase }) returns a promise for { capacity, format, bytesWritten, diagnostics }. sink.write(bytes) may transfer ownership and return a promise: the static module Worker waits for acknowledgement before rendering the next encoded chunk. Optional close() runs on completion; abort(reason) handles rollback. Cancellation terminates/rejects without waiting for a hung sink; the host must invalidate its partial output. Progress reports { frames, totalFrames, bytesWritten, errors }. This helper is browser-only; pure chunk rendering/encoding also works in Node. Deploy the entire dist/ tree, including worker/render.js and its companions, with same-origin secure URLs and normal CSP/JavaScript MIME checks. See incremental/Worker sink recipes and checkout-only example 08; long-file export uses a direct file sink, not an aggregate Blob fallback.\n\nquality is accepted by OPM, Synth, renderNote, full/chunked sequence rendering and Worker options. standard uses 4× oversampling with a 15 → 47-tap half-band FIR cascade (flat to 0.40 output Fs, 81 dB rejection, 13.25-frame delay); eco uses 2× with a single 35-tap stage and weaker (63 dB) rejection, while high uses 8× with an 11 → 15 → 47-tap cascade at higher CPU cost. Profiles do not promise alias-free FM or universal realtime deadlines. See measured quality tradeoffs.\n\n### Adaptive music, tempo curves and grids\n\ncreateArrangement(opm, { layers, sections, initialSection, bpm?, tempoMap?, timeSignature? }) loops named layers on one global beat grid and switches sections or toggles layers at the first beat/bar boundary that is not earlier than the notes already admitted. Shared layers are one continuous schedule, so their sounding notes are not retriggered. TempoPoint accepts curve: 'linear' (BPM changes linearly per beat; closed-form logarithmic integral) and endBpm; quantizeBeat, swingBeat and swingBeatEvents shape grids. createTransport and createArrangement are musical restarts, not DSP checkpoints. Details, formulas and the exact pause/seek contract: adaptive music; try checkout-only example 09.\n\nSince v1.9: ArrangementLayer.gain is 0–1 (default 1). switchSection and setLayer accept fade in seconds (0–10, default 0), and setLayerGain(name, gain, { quantize?, fade? }) returns the committed beat. Shared layers continue without retriggering; layer gain multiplies independent note expression and includes owned release tails. Arrangement reserves authored gain controls for layer envelopes; use expression inside layer scores.\n\n### Expressive performance and Web MIDI\n\nperformance.updateKey(part, key, controls) and performance.updatePartNotes(part, controls) apply any NoteControls to one key or to a whole part; configurePart accepts voiceLimit (1–32, release tails count) and voicePriority. createMidiAdapter(performance, await requestMidiAccess(), options) maps channel-voice MIDI (notes, sustain, bend, wheel, volume/expression, pan, pressure, all-notes-off) from user-granted inputs to parts. Importing never requests access, SysEx is never requested, and disconnect or dispose() releases only the keys the adapter owns. See expressive performance and MIDI and checkout-only example 10.\n\nSince v1.9: MidiAdapterOptions.controllerMap accepts at most 128 MidiControllerMapping entries. Each names controller, scalar field (including gain) or tuple field plus zero-based operator, explicit min, max, ramp, and optional reset. Reserved CC64/120/121/123 cannot be remapped; duplicates reject. A mapping overrides that CC's ordinary default only. CC121 restores captured effective part defaults or the supplied reset; fixed-Hz reset:null restores ratio mode. performance.getPartControls(part) returns a detached frozen snapshot of effective part controls, not per-key overrides. See the MIDI guide for field bounds and reset behavior.\n\n### Worker diagnostics and deployment\n\nrenderSequenceInWorker accepts optional startupTimeoutMs (integer 1–2147483647 ms, module-ready watchdog only), phaseDiagnostics and onPhase; the Worker posts a versioned ready before any score is sent. There is no built-in overall deadline or automatic retry: bound the complete job with your own AbortSignal. See Worker diagnostics. From an installed application's root, run npx --no-install opm-assets copy \u003cnew-directory> (atomic, never overwrites a differing destination, hashes every file) and npx --no-install opm-assets check \u003cbase-url> (byte, status, MIME and nosniff check of served assets); see host integration. VERSION is exported from the package root.\n\n\n### Physical-device recovery workflow\n\nUse checkout-only example 08 over HTTPS on physical iOS/Android devices, following the acceptance workflow and support matrix. Record device/OS/browser/rate/policy, scenario observations and manual pass/fail/unverified results. Exercise both policies through lock/unlock, app switching, calls, headset/Bluetooth routes, battery saving, prolonged playback and main-thread stalls. Reports stay local and never certify untested devices. Desktop/headless checks and analyser peaks do not prove audible continuity or physical-phone recovery.\n\n### WAV files and DX7 import\n\nSave this as export.mjs in the installed application's root; node export.mjs writes a stereo file:\n\njs\nimport { writeFile } from 'node:fs/promises';\nimport { renderNote, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\nconst audio = renderNote({ voice: brass, note: 60, duration: 0.7, velocity: 0.8, pan: -0.3 });\nawait writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));\n\n\nimportDX7(bytes) from opm.js/voices/dx7.js returns complete normalized version 7 voices. It accepts exactly one standard framed/checksummed 163-byte single-voice or 4104-byte 32-voice bank SysEx message (Uint8Array), rejecting invalid framing, length, checksum, or non-7-bit data. describeDX7(bytes) returns per-voice {name,sourceAlgorithm,algorithm,selectedOperators,droppedOperators,warnings}; selected/dropped numbers refer to DX7 operators 1–6, source algorithms to 1–32, and converted algorithms to 0–7.\n\nThis remains an approximate six-to-four-operator conversion, not DX7 synthesis/emulation. Fixed-Hz operators are retained; operator velocity/rate scaling, pitch envelopes and LFO delay/sync are musical heuristics. Pitch-envelope stages are reduced; unsupported descending saw/sample-and-hold waveforms are substituted with warnings. Dropped operators/routing, oscillator sync, transpose, per-operator AM and envelope details still differ; the converter reduces selected operators' AM sensitivity to one shared depth, not selective v6 targets. Inspect descriptions and expressive conversion limits before loading. The checkout-only audition uses original synthetic fixtures, not copied patches or a six-operator reference engine.\n\nimportOPM(source) from opm.js/voices/opm.js accepts decimal VOPM/MXDRV-family .opm text (string or Latin-1 Uint8Array) and returns complete normalized v7 voices. describeOPM(source) returns {name,program,algorithm,operators,warnings} for each patch. Names are sanitized and deterministically de-duplicated; source program numbers are metadata, not an automatic MIDI map. Input is bounded to 262144 bytes, 128 patches and 512 bytes per line, with strict integer ranges and complete unique sections. This is an approximate conversion, not register-level YM2151 emulation: held decay, rate scaling, detune, feedback and LFO differ, PAN is ignored, and C2 noise is an OPM.js extension. See OPM conversion formulas and limits.\n\n## Optimized distribution\n\nEvery .js in dist/ has a matching .js.map and compiler-generated .d.ts, including thirteen bundled example entry points in the current checkout (twelve in historical v1.8.1). Engine modules preserve source paths; demo-only helpers are bundled into those entry points rather than shipped as separate modules or orphan declarations. Safe minification retains composed maps with embedded TypeScript; the empty type-only worklet-globals module has no source mappings. Deploy the entire matching tree; publishing maps exposes its sources. Do not edit generated files. Build/package smoke rejects missing map/declaration companions.\n\nEngine, worklet, demos, tests and development scripts use strict TypeScript. Following XYZ.js's approach, declarations derive from implementation. Node.js 22+ is required. tsconfig.json checks source/emits declarations; tsconfig.dev.json emits ignored .dev/. ESM imports retain .js specifiers. Use npm test, not bare node --test; npm run typecheck includes tools/tests/demos/public consumers. The current build produces thirteen dist/demo/ entry points plus static HTML/search under doc/; checkout index.html is the demo catalog. Copy the complete doc/ directory together with top-level Markdown/legal files when hosting generated guides.\n\nIn the repository checkout, not the installed npm package:\n\nsh\nnpm ci\nnpm run build\nnpm test\nnpm run benchmark\nnpm run typecheck\nnpm run security\nnpx --no-install playwright install --with-deps chromium\nnpm run browser-smoke -- chromium\nnpm run browser-stress -- chromium\nnpm run sound-quality\nnpm run voice-quality\nnpm run vite-smoke\n\n\nThese install pinned development tools and regenerate dist/; the build replaces that generated directory. Consumers need no build tools or runtime dependencies. npm pack also runs the build automatically. Terser uses up to ten safe compression passes, without unsafe floating-point transformations or property mangling.\n\nThe build does not produce JSON assets or gzip/Brotli sidecars. If desired, configure compression at the web host while retaining normal .js import URLs. Send the matching Content-Encoding and Vary: Accept-Encoding only for compressed responses, with a JavaScript MIME type. The basic Python server serves the ordinary .js modules directly.\n\nBenchmark results depend on the machine and JavaScript engine. CHANGELOG records observed verification, and SECURITY covers safe embedding and review requirements.\n\nQuality automation configures Node 22/24/26 and Chromium/Firefox/WebKit smoke scenarios with real AudioWorklet output; browser installation is a development-only prerequisite. Headless Linux Firefox additionally needs a running native audio server; CI starts PulseAudio with a null output sink (see the quality instructions). Configuration alone does not prove successful runs or all-platform coverage: observed checks belong in CHANGELOG. npm run security -- --package-smoke exercises installed exports/types. No fallback masks browser failure.\n\nThe real-time benchmark reports warmed 128-frame block p95/p99/worst and misses against 128/sampleRate seconds. Local defaults are report-only; optional OPMBENCHP99BUDGETRATIO / OPMBENCHWORSTBUDGETRATIO enforce host-specific budgets. Run deadline acceptance separately from CPU-heavy offline rendering; competing workloads can change its outcome. CI configures p99 ratio 1 at 48 kHz (2.667 ms), with worst/GC stalls report-only. Four-times oversampling/filtering and controlled spectral tests do not promise arbitrary alias-free FM or deadlines on every host.\n\nThe v1.10 checkout-only benchmark also reports scoped capacity candidates across quality/voice configurations with an explicit reserve criterion and zero measured misses. Candidates retain their host/runtime/version/patch/workload identity and all raw timing rows; they do not certify AudioWorklet continuity or physical devices and never auto-switch synthesis quality. See capacity guidance. Actual mobile/MIDI and human-listening acceptance still require original physical observations and listener findings, not automated PASS substitutions.\n\nsound-quality covers all three profiles and passed 2,505 lifecycle/headroom/determinism matrix cases plus 24 live-control cases in the recorded v1.8 verification. It also checks independent passband, THD, folded aliases and streaming references; these are controlled numerical gates, not arbitrary-FM or listening certification. voice-quality evaluates the curated preset bank and original synthetic DX7 recipes over register/velocity cells. Peak/RMS are raw dBFS, not LUFS or perceived loudness; host trims do not rewrite synthesis levels. See acoustic-quality evidence and voice-quality notes.\n\nIndependent references cover Bessel-series high-index PM, contractive delayed feedback, conservative full-level feedback energy, and nested four-operator chain/branched/multicarrier synthesis. The per-voice 4× decimator is a polyphase linear-phase half-band FIR cascade, flat to 0.40 output Fs with at least 81 dB rejection of products that fold below that edge. Two 120-second streaming cases exercise held AM/PM and glide retargets. See acoustic reference mathematics and limits; none certify arbitrary alias-free FM, perceptual quality or hardware fidelity.\n\nbrowser-stress exercises real AudioWorklet output, dense admissions/steals, controls, late rejection and queue cleanup under bounded main-thread contention. Timer gaps and diagnostic round trips are observations, not worklet CPU/GC/underrun/glitch measurements. Periodic analyser samples cannot prove uninterrupted audio. The Vite smoke additionally verifies an installed current tarball, non-root production deployment, retained license, CSP and negative asset/MIME cases. Run these separately from deadline benchmarks.\n\n## Voice format\n\nA voice has four operators, an algorithm (0–7), and integer patch feedback (0–7). This is a canonical version 7 entry. Single objects may omit version, name, modIndex (4), and lfo (off/sine). Explicit versions 1–6 retain their original shapes and normalize to 7: v1 excludes key scaling, v1/2 exclude velocity sensitivity, v1–3 exclude LFO waveform, v1–4 exclude expressive v5 fields, v1–5 exclude v6 LFO targets, and v1–6 exclude operator waveform/noise rate.\n\njson\n{\n \"version\": 7,\n \"name\": \"brass\",\n \"algorithm\": 4,\n \"feedback\": 3,\n \"modIndex\": 4,\n \"lfo\": { \"rate\": 5.2, \"amDepth\": 0, \"pmDepth\": 12, \"waveform\": \"sine\" },\n \"ops\": [\n { \"ratio\": 1.0, \"level\": 0.8, \"detune\": 0, \"velocitySensitivity\": 12,\n \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.6, \"r\": 0.1 },\n \"keyScale\": { \"breakpoint\": 60, \"leftDbPerOctave\": 0, \"rightDbPerOctave\": 6 } },\n { \"ratio\": 1.0, \"level\": 0.6, \"detune\": 3,\n \"adsr\": { \"a\": 0.01, \"d\": 0.15, \"s\": 0.5, \"r\": 0.1 } },\n { \"ratio\": 2.0, \"level\": 0.4, \"detune\": -3,\n \"adsr\": { \"a\": 0.02, \"d\": 0.3, \"s\": 0.4, \"r\": 0.15 } },\n { \"ratio\": 1.0, \"level\": 0.7, \"detune\": 0,\n \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.55, \"r\": 0.1 } }\n ]\n}\n\n\nFor OPM, Synth.noteOn(), and normalizeVoice(), algorithm, feedback, and exactly four complete operators are required; the optional metadata/defaults are described above. Unknown fields, accessors, sparse operator arrays, incorrect types, non-finite numbers, and out-of-range values are rejected.\n\n| Voice field | Allowed values |\n| --- | --- |\n| version, name | Version 7; legacy 1–6 accepted only with original fields; name: 1–64 ASCII letters, digits, _, or - |\n| algorithm, feedback | Integers 0–7 |\n| ratio, level, detune | 0.125–32, 0–1, −1200–1200 cents |\n| adsr.a, adsr.d, adsr.s, adsr.r | Attack/decay/release: 0–10 seconds; sustain: 0–1 |\n| modIndex | 0–16 |\n| lfo.rate, lfo.amDepth, lfo.pmDepth | 0–20 Hz, 0–1, 0–1200 cents |\n| lfo.waveform | sine (default), triangle, saw, square; v4+ shape only |\n| ops[i].keyScale | Optional; breakpoint: MIDI integer 0–127; leftDbPerOctave, rightDbPerOctave: 0–24 dB/octave |\n| ops[i].velocitySensitivity | Optional since v3: 0–48 dB attenuation at velocity zero; omitted is 0 |\n| ops[i].frequency | Optional v5 fixed frequency 1–20000 Hz; overrides note-derived ratio frequency, with detune/live pitch/PM still applied |\n| ops[i].rateKeyScale | Optional v5 0–4: ADSR times multiply by 2 * (-rateKeyScale (note - 60) / 12), clamped to 10 seconds; default 0 |\n| ops[i].waveform | Optional v7 sine (default), half, abs, quarter, alternating, camel, square, saw, noise |\n| ops[i].noiseRate | Optional v7 20–20000 Hz, default 8000; ignored unless waveform is noise |\n| pitchEnvelope | Optional v5 {a,d,r,initial,peak,sustain,final}; times 0–10 seconds, levels −4800..4800 cents, continuous linear-cents release |\n| lfo.delay, lfo.sync, lfo.phase | Optional v5 0–10 seconds, 'note'/'global' (default note), 0–1 turns (default 0); delay gates depth, not the phase clock |\n| lfo.amTargets, lfo.pmTargets | Optional v6 four-element depth factors 0–1; input booleans normalize to 0/1; absent targets mean all 1 |\n\nSee the expressive voice reference for pitch composition, fixed-frequency behavior and synchronization. Ratio remains required even when frequency is supplied. Preserve the source patch version when importing older banks; changing a version number alone does not add unsupported fields to its legacy shape.\n\nPeriodic shapes use phase plus modulation; sine retains the unchanged legacy fast path. Noise uses a deterministic 17-bit bipolar sample-and-hold sequence reset at every admission/slot reuse. Ratio, fixed Hz, detune, pitch and PM do not pitch noise; live ratio/frequency controls remain valid but have no audible pitch effect. Envelope, velocity, level, key scaling, AM and graph roles still apply. Noise on any operator is an OPM.js extension, not hardware-fidelity emulation. Square/saw use polyBLEP and half uses polyBLAMP edge correction, but these shapes and noise can still alias, especially under FM; see shape formulas and noise details. Original presets include noise-snare, noise-hihat and explosion.\n\n\nKey scaling attenuates operator level by 10 (-slope * abs(note - breakpoint) / 12 / 20) on the corresponding side; omitted scaling is flat. Velocity sensitivity additionally multiplies it by 10 (-sensitivity * (1 - velocity) / 20), changing carrier loudness and modulator brightness. Final note velocity still multiplies the output; default sensitivity 0 preserves legacy sound. Pan belongs to the note, not the voice.\n\n### Voice banks\n\nImport parseVoiceBank from opm.js/voices/schema.js in Node, or ./opm/voices/schema.js in the deployed browser example. It accepts a JSON string or array and returns Map\u003cname, voice> with frozen copies.\n\nA bank must be an array of 1–128 complete versioned voices; wrap individual JSON examples in [...]. Every bank entry requires version, name, algorithm, feedback, modIndex, lfo, and four complete operators; names must be unique. JSON strings are capped at 256 KiB in UTF-8. The schema module exports voiceSchema; the example bank module exports examples. Import examples from opm.js/voices/examples.js (Node) or ./opm/voices/examples.js (browser) and pass it to parseVoiceBank(examples). External JSON banks remain supported.\n\nopm.replaceVoiceBank() applies the bank's whole-input validation before swapping lookups. Only its plain empty-array input is a clearing operation; parseVoiceBank('[]') still rejects an empty bank. Removing/replacing a name does not alter immutable patches already queued or sounding.\n\nUnlike strict single-voice normalization, validateVoice(), parseVoiceBank(), and renderNote() clamp finite out-of-range numeric fields to the schema limits. Invalid types, non-finite values, unknown fields, unsupported versions, and invalid algorithm/feedback enums still fail. validateVoice() handles one complete voice; bounded() clamps a finite number; LIMITS, MAXBANKBYTES, and MAXBANKVOICES expose the bounds.\n\nThe guides include complete browser loader examples with HTTP-status checks and error handling. The engine does not fetch URLs: its string-size check runs only after your application has obtained the text. Bound untrusted downloads before buffering them; do not treat the bundled-example loader as a hardened external downloader.\n\n## Troubleshooting\n\n| Symptom | Action |\n| --- | --- |\n| npm returns E404 for opm.js | Use the local tarball installation recipe above; do not assume a registry release exists. |\n| AudioWorklet is not available, or file:// fails | Use a supporting browser over HTTPS/localhost. In Node, use Synth, not OPM.start(). |\n| Bare module specifier cannot be resolved | Use the browser's ./opm/... URLs and copied assets, not npm names in raw HTML. |\n| Processor/module 404 or JavaScript MIME error | Preserve the complete distribution layout; ensure asset URLs serve JS, not a single-page app's HTML fallback. |\n| No sound, or “Call start() before playNote()” | Await start() inside a click handler; inspect the displayed error and browser/device audio settings. |\n| Invalid note, duration, or unknown voice | Check finite fractional MIDI 0–127, duration (null or (0,60]), velocity/pan, and registered voice name. |\n| Notes disappear | Observe onEvent rejections/steals and getDiagnostics(); respect voice and queue bounds and schedule smaller batches. |\n| Bank import fails | Use an array of complete versioned voices with unique names, not a single partial voice object. |\n| Offline script makes no speaker sound/file | PCM buffers do not play themselves; use encodeWav and a file/download API to save them. |\n| Compressed assets fail to load | Import .js; configure content-encoding negotiation or serve the normal minified files. |\n\n\n## Architecture\n\n\n┌─────────────────────────────────────────────┐\n│ main thread │\n│ playNote() / control messages │\n└──────────────┬──────────────────────────────┘\n │ MessagePort\n┌──────────────▼──────────────────────────────┐\n│ AudioWorklet processor │\n│ voice allocation → operator graph │\n│ 4-op FM (8 algorithms, feedback) │\n│ ADSR + LFO → stereo out │\n└─────────────────────────────────────────────┘\n\n\n## Roadmap\n\n- [x] v1.0 — 4-operator FM engine, eight algorithms, feedback, ADSR, LFO, detune, browser API, and offline rendering\n- [x] v1.2 — key scaling, approximate DX7 SysEx voice import, and WAV export (regression tests and actual browser demo/import/download verified; see CHANGELOG for evidence and limits)\n- [x] v1.11 — voice v7 waveforms and noise, stereo chorus/reverb, MIDI program maps and RPN, .opm import (regression tests, a real-browser audio smoke, passing Node 22/24/26 and Chromium/Firefox/WebKit CI, and demo coverage; physical-device and human-listening acceptance are unverified)\n\n## Limitations\n\n- Not a register-level YM2151 emulator; VGM playback is out of scope\n- Bundled sounds are original curated recipes, not a comprehensive commercial bank; bring your own sounds.\n- Selectable 2×/4×/8× oversampling and half-band FIR decimation reduce controlled folded aliases; arbitrary FM can still alias, and the linear-phase filters add a constant delay (13.25 output frames for standard).\n- Numerical host trims are not perceptual loudness matching. DX7 conversion remains six-to-four-operator and intentionally lossy; no hardware-fidelity or all-device glitch-free guarantee is made.\n\n## License\n\nApache License 2.0\n\nCopyright 2026 OPM.js authors\n\nOPM.js is an independent project inspired by the architecture of the Yamaha YM2151. It is not affiliated with, endorsed by, or connected to Yamaha Corporation.\n","href":"./readme.html"},{"title":"OPM.js · OPM.js","text":"OPM.js OPM.js","href":"./readme.html#opmjs"},{"title":"OPM.js · New in v1.11","text":"OPM.js New in v1.11","href":"./readme.html#new-in-v111"},{"title":"OPM.js · New in v1.10","text":"OPM.js New in v1.10","href":"./readme.html#new-in-v110"},{"title":"OPM.js · About","text":"OPM.js About","href":"./readme.html#about"},{"title":"OPM.js · Features","text":"OPM.js Features","href":"./readme.html#features"},{"title":"OPM.js · Requirements","text":"OPM.js Requirements","href":"./readme.html#requirements"},{"title":"OPM.js · Getting started","text":"OPM.js Getting started","href":"./readme.html#getting-started"},{"title":"OPM.js · Try the checkout","text":"OPM.js Try the checkout","href":"./readme.html#try-the-checkout"},{"title":"OPM.js · Install into an npm project","text":"OPM.js Install into an npm project","href":"./readme.html#install-into-an-npm-project"},{"title":"OPM.js · Use in a browser","text":"OPM.js Use in a browser","href":"./readme.html#use-in-a-browser"},{"title":"OPM.js · Render in Node.js","text":"OPM.js Render in Node.js","href":"./readme.html#render-in-nodejs"},{"title":"OPM.js · API reference","text":"OPM.js API reference","href":"./readme.html#api-reference"},{"title":"OPM.js · Browser: OPM","text":"OPM.js Browser: OPM","href":"./readme.html#browser-opm"},{"title":"OPM.js · Offline: Synth and renderNote","text":"OPM.js Offline: Synth and renderNote","href":"./readme.html#offline-synth-and-rendernote"},{"title":"OPM.js · Optional stereo effects","text":"OPM.js Optional stereo effects","href":"./readme.html#optional-stereo-effects"},{"title":"OPM.js · Mix gain and tuning","text":"OPM.js Mix gain and tuning","href":"./readme.html#mix-gain-and-tuning"},{"title":"OPM.js · Shared live/offline scores","text":"OPM.js Shared live/offline scores","href":"./readme.html#shared-liveoffline-scores"},{"title":"OPM.js · Long scores without aggregate PCM","text":"OPM.js Long scores without aggregate PCM","href":"./readme.html#long-scores-without-aggregate-pcm"},{"title":"OPM.js · Portable score projects and MIDI files (introduced in v1.9)","text":"OPM.js Portable score projects and MIDI files (introduced in v1.9)","href":"./readme.html#portable-score-projects-and-midi-files-introduced-in-v19"},{"title":"OPM.js · Portable Arrangement and expressive MIDI files (v1.10)","text":"OPM.js Portable Arrangement and expressive MIDI files (v1.10)","href":"./readme.html#portable-arrangement-and-expressive-midi-files-v110"},{"title":"OPM.js · Musical Transport and performance policies","text":"OPM.js Musical Transport and performance policies","href":"./readme.html#musical-transport-and-performance-policies"},{"title":"OPM.js · Worker WAV export and quality profiles","text":"OPM.js Worker WAV export and quality profiles","href":"./readme.html#worker-wav-export-and-quality-profiles"},{"title":"OPM.js · Adaptive music, tempo curves and grids","text":"OPM.js Adaptive music, tempo curves and grids","href":"./readme.html#adaptive-music-tempo-curves-and-grids"},{"title":"OPM.js · Expressive performance and Web MIDI","text":"OPM.js Expressive performance and Web MIDI","href":"./readme.html#expressive-performance-and-web-midi"},{"title":"OPM.js · Worker diagnostics and deployment","text":"OPM.js Worker diagnostics and deployment","href":"./readme.html#worker-diagnostics-and-deployment"},{"title":"OPM.js · Physical-device recovery workflow","text":"OPM.js Physical-device recovery workflow","href":"./readme.html#physical-device-recovery-workflow"},{"title":"OPM.js · WAV files and DX7 import","text":"OPM.js WAV files and DX7 import","href":"./readme.html#wav-files-and-dx7-import"},{"title":"OPM.js · Optimized distribution","text":"OPM.js Optimized distribution","href":"./readme.html#optimized-distribution"},{"title":"OPM.js · Voice format","text":"OPM.js Voice format","href":"./readme.html#voice-format"},{"title":"OPM.js · Voice banks","text":"OPM.js Voice banks","href":"./readme.html#voice-banks"},{"title":"OPM.js · Troubleshooting","text":"OPM.js Troubleshooting","href":"./readme.html#troubleshooting"},{"title":"OPM.js · Architecture","text":"OPM.js Architecture","href":"./readme.html#architecture"},{"title":"OPM.js · Roadmap","text":"OPM.js Roadmap","href":"./readme.html#roadmap"},{"title":"OPM.js · Limitations","text":"OPM.js Limitations","href":"./readme.html#limitations"},{"title":"OPM.js · License","text":"OPM.js License","href":"./readme.html#license"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism # SECURITY.md — OPM.js Security Policy & Review Mechanism\n\nOPM.js is a client-side, four-operator FM synthesis library. It processes voice definitions, DX7 SysEx and PCM buffers, and runs real-time DSP in an AudioWorklet. It has no application server or authentication system and has zero runtime dependencies. The host application remains responsible for website security, untrusted downloads, resource budgets and safe audio presentation.\n\nFor installation, browser deployment, and API examples, start with the README or the English / Traditional Chinese usage guide.\n\nContents: Supported versions · Reporting a vulnerability · Safe embedding · Threat model · Security review mechanism · Non-goals\n\n## Supported versions\n\n| Version | Supported |\n| --- | --- |\n| 1.x | Supported security line; use the latest available patch |\n| \u003c 1.0 | Not supported |\n\nThe current checkout requires Node.js 22 or newer for Node usage and development. Browser applications require ES modules, AudioWorklet and a secure context (HTTPS or localhost). The Node requirement does not provide a browser sandbox or replace browser updates.\n\nUse a complete distribution from a known checkout or package, not individual modules from unrelated builds. Check CHANGELOG for observed verification; an advertised feature or configured CI job is not proof that every runtime or platform has passed.\n\n## Reporting a vulnerability\n\nReport exploitable problems privately to the repository maintainer. Use the GitHub private vulnerability reporting form when it is enabled. If a private reporting route is unavailable, request a private contact method without publishing exploit details, sensitive files or a proof of concept.\n\nInclude enough information to reproduce the issue safely:\n\n- The affected OPM.js version or commit, and whether the behavior uses src/, committed dist/, or an installed package.\n- Node/browser version, operating system, sample rate and relevant deployment or routing configuration.\n- A minimal input and sequence of API calls or worklet messages, including expected and observed behavior.\n- The apparent impact: code execution, validation bypass, excessive CPU/memory use, processor failure, unexpected audio, or data exposure.\n- Relevant errors, diagnostics and whether the issue reproduces with a freshly built, matching distribution.\n- Any proposed fix or mitigation, and whether the report may be credited publicly.\n\nDo not include credentials, private voice banks, personal recordings or unrelated user data. Use a synthetic input when possible. Minimize denial-of-service examples; do not test against other people's websites or production audio sessions.\n\nThe acknowledgement target is 72 hours, on a best-effort basis, not a guaranteed response or remediation deadline. Coordinate public disclosure with the maintainer so affected users can receive an actionable fix or mitigation. Ordinary usage questions may use public issues; exploitable details should remain private.\n\n## Safe embedding\n\n### Website deployment and browser permissions\n\n- Serve the complete, matching dist/ tree over HTTPS or localhost, preserving worklet and imported-module paths. A configurable worklet URL must stay same-origin, use HTTPS/loopback HTTP, contain no credentials/fragment, and retain CSP/MIME enforcement. Do not load through file://, and retain LICENSE.\n- The root index.html, examples/ pages, favicon.ico and this policy are public website assets. Publish the build-generated doc/ HTML, stylesheet and search assets together with the matching documentation sources.\n- The Python development server is for local checks, not a hardened public host. A production server should not expose .git/, .dev/, node_modules/, development tooling, credentials or directory listings.\n- Serve JavaScript with a JavaScript MIME type. Use X-Content-Type-Options: nosniff; do not return an application's HTML fallback for a missing worklet/module URL.\n- Apply a host-specific Content Security Policy that allows only reviewed module origins, restricts framing and base URLs, and disables unnecessary object embedding. Check the script and worklet/worker directives applicable to each supported browser.\n- The checkout examples use external module scripts and a local stylesheet. They need no unsafe-eval. Do not weaken CSP, TLS or browser permission/autoplay controls to suppress a deployment error.\n- WAV Blob URLs are data downloads, not executable modules. Do not broaden script origins simply because downloads use blob: URLs.\n- Only enable browser features that the host needs. Synthesis does not require microphone, camera or location access. Test playback, worklet loading and download behavior after changing hosting headers.\n\n### Voice banks and JavaScript objects\n\nUse loadVoice(), normalizeVoice(), validateVoice() or parseVoiceBank() at the appropriate documented boundary; do not send unchecked definitions straight into the DSP or raw worklet protocol.\n\n| Input | Boundary |\n| --- | --- |\n| Single-voice API | Required fields and exactly four complete operators; finite numeric values and documented ranges; unknown fields and accessors reject |\n| Voice-bank JSON string | At most 262,144 UTF-8 bytes (256 KiB), then JSON parsing and bank validation |\n| Voice-bank array | 1–128 complete versioned voices, unique validated names, and frozen normalized copies |\n| Legacy versions 1–6 | Original shapes only: v1 excludes key scaling, v1/2 exclude velocity sensitivity, v1–3 exclude waveform, v1–4 exclude expressive fields, v1–5 exclude per-operator LFO targets, v1–6 exclude operator waveform/noiseRate; all normalize to v7. noiseRate is 20–20000 Hz |\n| .opm text patches | importOPM/describeOPM: at most 262,144 bytes, 128 patches and 512 bytes per line; NUL bytes, unknown/duplicate/missing sections, non-decimal or out-of-range fields reject with a line number; results pass the normal v7 validation path |\n| MIDI program/drum maps | programVoices/drumVoices: plain data objects or native Maps, at most 128 entries, canonical integer keys 0–127 and valid voice names; accessors and extra Map properties reject. RPN 0 state is per channel and bounded to 0–48 semitones |\n| PreparedVoice | Immutable detached core snapshot, including nested pitch envelopes; only private identity membership grants trust, never a structural brand/copy |\n| Tuning / score / controls | A4 20–20000 Hz, 128 bounded cents offsets; strict dense four-element level/ratio/Hz/ADSR and LFO-target tuples; short scores retain 128-note/256-slot/60-second bounds; long scores have separate event/time/chunk limits and immutable own-data snapshots |\n| Atomic OPM bank replacement | Validate all entries before swapping ≤128 lookups; plain empty array clears, while JSON/standalone empty bank parsing rejects; exports use detached canonical names and ≤256 KiB UTF-8 |\n| Transport / performance | ≤1024 tempo points, quarter-note positions ≤86400; bounded scheduling density and scoped ID ownership; 1–16 parts, ≤128 physical keys/tracked gates with detached snapshots |\n\nSingle-voice APIs reject out-of-range fields. Bank validation clamps finite numeric fields to documented bounds, but still rejects malformed types, non-finite numbers and invalid version/algorithm/feedback values. Clamping is not a substitute for validation.\n\nKeep allowlisted, own-data copying at trust boundaries. Do not merge untrusted objects into prototypes, invoke getters to inspect them, or treat a valid voice name as authorization to construct a URL or filesystem path. Engine bank parsing does not load paths or fetch voice URLs.\n\nIndependent note gain and expression are both bounded to 0..1 and multiply before stereo mix saturation. Layer fades do not bypass control validation, note ownership, worklet queue limits or rejection reporting.\n\n### Score projects and Standard MIDI files\n\nVersioned score-project parsing accepts only the documented own-data schema, detached validated voices, bounded beat events and synthesis settings. Loading a project does not fetch assets, execute code or start playback. Compilation converts note gate endpoints through the complete tempo map; offline resource budgets still apply after conversion.\n\nThe separate version-1 Arrangement project uses the same own-data beat/voice/settings boundaries and one pure definition validator shared with live arrangements. Bounds include 8 MiB serialized input/output, 128 voices/256 KiB voice JSON, 16 layers, 32 sections and 65,536 total events. Parsing is inert; persisted musical definitions do not include live IDs, permissions, pending commands or DSP state. Host replay must explicitly load validated voices and start from a gesture.\n\nThe independent Standard MIDI file adapter validates native bytes, bounded chunks, PPQN timing and event framing. It does not provide a native MIDI driver, transmit SysEx or interpret uploaded bytes as executable content. Hosts must check file size before buffering, choose explicit channel-to-voice mappings, expose import warnings and reject unsupported export semantics instead of silently changing a score.\n\nOpt-in expressive SMF conversion separately bounds generated per-note fanout to 65,536 events. Frozen loss summaries separate omitted content, approximations and recognized source-message counts. Channel expression after a closed gate reports possible patch-dependent release-tail loss; strict unsupported policy rejects it. Export rejects incompatible channel ownership, unsupported controls and after-gate semantics rather than guessing. Explicit pitch-range policy is not RPN support.\n\n### External downloads and file uploads\n\nThe engine does not provide a voice-download API. Host loaders must validate the source and enforce an input budget before buffering or parsing. Engine checks cannot recover memory already consumed by an oversized download.\n\n- Check HTTP status and permitted origins before accepting a response; do not derive destinations from an untrusted voice name.\n- Treat declared MIME type, file extension and Content-Length as hints, not evidence that the payload is valid or bounded. Chunked and decoded/compressed responses still need a real byte budget.\n- Bound streamed response bytes and cancel an oversized transfer before assembling the complete buffer. Set host-appropriate cancellation and time limits.\n- Check a local file's size before arrayBuffer() or text decoding, then validate its actual contents. A picker selection is not a trust guarantee.\n- Do not insert filenames, imported descriptions or error messages into HTML. The examples display them as text; retain that separation in host UIs.\n- Apply application-level limits to repeated uploads, repeated parsing and simultaneous synth instances. A per-call limit is not a whole-page resource quota.\n\n### DX7 SysEx conversion\n\nimportDX7() and describeDX7() accept one Uint8Array containing a standard 163-byte single-voice or 4104-byte 32-voice bank message. Length, framing, checksum and seven-bit payload checks reject malformed input. The playground checks the maximum file size before buffering.\n\nThis is approximate six-to-four-operator voice conversion, not a DX7 emulator or an arbitrary SysEx interpreter. Conversion does not execute the payload. Review the returned descriptions and warnings before auditioning a patch; keep descriptions outside the strict voice schema.\n\n### PCM, offline rendering and WAV export\n\nencodeWav() encodes PCM; it does not decode/play uploaded audio. Own-data arguments are left, optional right, sampleRate and optional format (pcm16, pcm24, float32). Native Float32 samples must be finite in −1–1; stereo lengths match, with at most 4,000,000 frames per channel for this full-buffer convenience API.\n\nProject render results onto those fields rather than forwarding diagnostics or other metadata. Reject encoding/rendering errors visibly instead of substituting a fake result.\n\nCore rendering authenticates native Float32Array kind/length instead of overridable properties. Validate offset before deriving the default length; numeric coercion, proxies, forged channels and out-of-bounds ranges reject without advancing sound. Idle clearing uses the native fill method. Hosts still own the actual PCM storage and must not mutate it concurrently.\n\nOffline rendering allocates output buffers and executes synchronously. The host should limit duration, sample rate, envelope tails, concurrent requests and export frequency to suit its device and UI. Valid input may still consume significant time and memory; no universal deadline or whole-page memory guarantee is made.\n\nLong-score rendering and incremental createWavEncoder() use bounded reusable PCM/encoded chunks, not unlimited aggregate buffers. Consume borrowed PCM before advancing; declare an exact encoder frame count and finalize only after all frames. Encoder chunks are 1–65536 frames and RIFF32 files remain \u003c4 GiB; convenience rendering/encodeWav retain their 4,000,000-frame bound. Hosts must bound cumulative duration/work, retained bytes, concurrent renders and export frequency.\n\nrenderSequenceInWorker() validates before Worker allocation, transfers at most one unacknowledged encoded chunk and awaits sink writes before advancing. Capture intrinsic byte counts before handing ownership to a sink; storage workers may transfer/detach the buffers. Progress/abort callbacks are untrusted host behavior. Native AbortSignal cancellation terminates/rejects immediately without waiting for hung writes/abort/close. The sink owns invalidation and rollback; cancellation cannot retract persisted bytes. Acquire file sinks from trusted gestures, retain lifetime cancellation across picker/writable awaits, and clean up acquisition failures before starting a Worker. Never replace a long-file sink with unbounded Blob accumulation.\n\nCreate download URLs only for successfully encoded bytes. Replace/revoke obsolete Blob URLs and release them when the owning page or component is disposed. Treat filenames and rendered audio as potentially private application data.\n\n### AudioWorklet, scheduling and context ownership\n\n- Await start() in a user interaction and monitor onEvent or independent subscribe() listeners. A note ID is not admission; handle rejections, terminal states and processor errors. waitForCommand() is bounded, timed/cancellable and resolves admission only, not future execution; reset/close/failure invalidate pending waits.\n- The worklet bounds pending events and tracked note IDs to 256 each. Schedule bounded batches rather than flooding the port with distant-future events.\n- The optional effects worklet (worklet/fx-processor.js) has a separate strict own-data update/reset/close protocol. Chorus/reverb numbers and voice counts validate again before entering DSP; no command queue is retained and at most 32 commands are admitted per audio-frame boundary. Error replies are bounded and close remains available under flooding. MessagePort buffering before dispatch and many host-created instances remain host resource concerns.\n- Effects module loading uses the same secure same-origin URL rules and native CSP/MIME enforcement as synthesis; deploy the full matching tree. Effects never resume, suspend or close a borrowed context. Processor/decode failures surface through onEvent; dispose disconnects/stops only the insert.\n- Effects preallocate bounded seconds-scaled Float64 delay state (under 866 KiB at 192 kHz), sanitize NaN/Infinity PCM to zero, clamp internal state and flush tiny values. Offline tail-extended copies obey the existing 4,000,000-frame budget. Wet headroom is not a limiter for over-unity input or a host mix of many buses.\n- Each synth limits logical voices to configured 1–32 (default eight), with up to eight bounded stealing fades. Immutable eco/standard/high profiles change fixed internal sampling/filter work, not these bounds. These limits do not constrain an attacker creating many instances.\n- DSP state and terminal notification buffers are preallocated. Ended/error callbacks run after stable frame traversal; callback-admitted replacements start next frame, and recursive rendering rejects rather than extending a frame's work.\n- Named/prepared worklet registrations use 128 content-keyed LRU slots. Replacement revalidates before committing; queued/active notes own immutable old snapshots. Raw objects still validate freshly.\n- Absolute start/stop/control times are safely framed with a 60-second future horizon; stop precedes onset and controls. Command rejection is correlated by commandId; accepted means admission, not guaranteed future execution. Global allNotesOff/panic bypass full scheduled queues. Bound sequences and lookahead rather than flooding messages.\n- Immediate stop(id, { cancelControls: true }) removes only that ID's queued automation/onset/off before natural release; it cannot be combined with at. Musical Transport retains release-tail controls during ordinary playback but cancels its own future automation on pause/seek/loop rebuilding. Seeking is a musical restart, not an exact DSP snapshot.\n- Performance cleanup is part/helper scoped, not global panic. Repeated equal-pitch keys retain separate identities; stealing/reset cannot resurrect stale held/pedal state.\n- Handle rejected diagnostics promises and failed node initialization. Do not hide failure behind mock audio nodes or a fallback engine.\n- A shared AudioContext is borrowed: OPM disconnects its node but never closes/suspends the host context. close() is restartable; terminal dispose() clears owned helpers/subscriptions without taking ownership of a borrowed context. Hosts own downstream routing and permissions.\n- Clear host gate bookkeeping on global reset; close/failure need not reply with every terminal note. Default cancel removes old gates/automation on suspension/interruption; preserve is opt-in. Both policies stop lookahead and require gesture-driven explicit restart.\n- The recovery harness bounds local events/attempts and captures explicit device metadata, observations and manual scenario status. It uploads nothing. Automated desktop checks cannot certify physical phones or listening continuity; support-matrix cells without physical evidence remain unverified.\n- Provide obvious play/stop controls and begin at a comfortable, low host gain, especially with headphones. Finite samples and synthesis headroom do not guarantee safe listening volume.\n\n### Source maps, privacy and redistribution\n\nEvery distribution JS has a matching source map with embedded TypeScript and a compiler-generated declaration. Publishing maps intentionally makes the implementation source readable; minification is not secrecy. Never place credentials or private configuration in source that will be built or deployed.\n\nOPM.js does not provide an account, storage or telemetry service. Hosting providers and applications can still record request metadata or persist voices/audio. Document those host-specific behaviors separately and do not claim this library prevents data collection by the surrounding page.\n\nKeep each deployment's JS, maps and declarations from the same build. Review exact development dependency versions, build plugins, CI actions, package contents and license compatibility before changing the supply chain.\n\n## Threat model\n\n| Surface | Risk | Required controls |\n| --- | --- | --- |\n| External voice JSON or JavaScript objects | High | Type/shape/byte/count limits; finite bounds; own-data validation; no untrusted prototype merging |\n| Host download/upload and DOM code | High | Pre-buffer limits, permitted sources, text-only presentation and host-page XSS defenses |\n| Raw AudioWorklet messages | High | Strict message validation, bounded IDs/events and observable failure handling |\n| DX7 binary input and WAV PCM arguments | Medium | Bounded lengths, framing/checksum or sample validation; no native decoder |\n| Score-project JSON and Standard MIDI files | High | Byte/event/schema/framing limits, own-data options, detached validated voices and explicit unsupported-semantics handling |\n| DSP and offline rendering | Medium | Finite output, bounded per-instance work, duration/sample-rate budgets and error diagnostics |\n| Public scheduling API | Medium | Argument validation, admission handling, cancellation and bounded scheduling batches |\n| Build and deployment supply chain | Medium | Exact development pins, reviewed CI actions, complete matching assets and zero runtime dependencies |\n| Output audio and downloadable artifacts | Medium | User-controlled playback, conservative gain and host-managed privacy |\n\nAn AudioWorklet separates real-time processing from the main thread, but is not a security boundary against a compromised host page. Malicious host JavaScript can create nodes, spam messages, fetch data or manipulate the UI independently of OPM.js. Input validation and per-instance limits reduce accidental or adversarial input damage; they do not sandbox XSS or guarantee availability under arbitrary page load.\n\nThis policy addresses the library and checkout examples. Authentication, authorization, cookies, cross-origin access, backend storage, upload endpoints and rate limiting belong to the application deploying them.\n\n## Security review mechanism\n\n### 1. Review triggers (mandatory)\n\nA security review is required before:\n\n- [ ] Any release tag (vX.Y for zero patch versions, vX.Y.Z otherwise)\n- [ ] Merging a PR that touches: src/worklet/, src/core/, or voice parsing (src/voices/, src/api/ loaders)\n- [ ] Adding any dependency, build plugin, or CI action\n- [ ] Supporting a new voice-bank source (file import, URL, user paste)\n\n### 2. Review checklist\n\nA. Untrusted data (voice banks, config)\n- [ ] Voice JSON/key scaling, score projects, SMF/DX7 binary input, and WAV arguments validate type, shape, length, numeric bounds, and own-data properties before use\n- [ ] Explicit allowlisted copying; no spreading/merging untrusted objects into prototypes; no accessor invocation\n- [ ] Numeric bounds cover ADSR/live reanchoring, ratio/fixed-Hz controls, rate scaling, pitch envelope, LFO waveform/delay/sync/phase/targets and independent depth/rate, feedback, velocity/pan, levels and ramps; tuning/score/Transport/performance/encoder limits remain bounded and legacy shapes exclude later fields\n- [ ] Prepared identity cannot be forged, trusted patches stay deeply immutable, and public map snapshots cannot change stored patches\n- [ ] Application file/URL loaders bound sources, content type, and payload before buffering; engine does not fetch URLs\n- [ ] DX7 rejects invalid length/framing/checksum/seven-bit payload; conversion descriptions remain outside strict voice schema\n\nB. Worklet boundary\n- [ ] Every raw message is checked for strict own-data shape before reads, enqueueing, or rendering\n- [ ] No eval/Function execution or nonliteral dynamic imports in source/generated JS; configurable worklet/Worker URLs require secure same-origin assets and cannot weaken CSP/MIME validation\n- [ ] Duplicate IDs reject before enqueueing; tracked IDs/events remain bounded; ended/stolen/error notes remove obsolete off events\n- [ ] Held-note cancellation/release, bounded allNotesOff/panic, command admission/rejection, context/reset and processor failure propagate safely; teardown does not promise impossible per-note acknowledgements\n- [ ] Registration replacement, controls, tuning, score IDs/times and absolute scheduling validate own-data shape; queued snapshots, ordering and cleanup preserve bounds\n- [ ] Encoded Worker chunks require ordered acknowledgements, intrinsic byte accounting before sink ownership transfer, exact frame/file completion and termination on cancellation/failure; host sinks own rollback\n\nC. DSP loop safety\n- [ ] Output is finite: any NaN/Infinity in the render loop snaps to silence and increments an error counter (Synth.errorCount or renderNote()'s diagnostics.errors)\n- [ ] Fixed work ceiling: configured 1–32 logical voices (default eight) and at most eight fades, deterministic priority-aware admission/stealing, authenticated native output lengths and numeric ranges before work\n- [ ] Slot reset/recycling preserves identity and numerical state under callback admissions; terminal callbacks cannot replenish a frame's traversal or recursively render it\n- [ ] Determinism and chunk-independent output preserved; live/offline LFO, timing, filtering, stereo, velocity, and saturation parity\n- [ ] Deadline statistics exclude warmup and expose p95/p99/worst/misses without universal performance promises; spectral acceptance is controlled, not arbitrary FM alias-free proof\n\nD. Supply chain\n- [ ] Zero runtime dependencies still holds (npm ls --omit=dev is empty)\n- [ ] Dev dependencies and CI actions pinned to exact versions/commit SHAs\n- [ ] package.json files whitelist ships only intended dist/ assets, usage docs, and legal/project files\n- [ ] Standalone deployment retains the installed LICENSE alongside complete matching worklet/module assets; CSP/MIME/404 failures stay visible\n- [ ] Any npm publication requires separate maintainer authorization, protected approval, matching tag/version/commit/release and independent A/B/C/D/package evidence, plus a reviewed provenance-bearing publishing path; post-publication checks compare exact tarball bytes, validate source provenance and cryptographically verify signatures before registry-installed consumer acceptance. This checkout has no npm publishing workflow; local verification is not publication.\n\n### 3. Verification gates\n\nRun development commands from a source checkout after npm ci; installed npm packages do not contain the development tooling.\n\n- npm run build then npm test — source/distribution behavior, malformed-input, deterministic rendering, and spectral regression coverage\n- npm run typecheck — strict source, development tools/tests/demos, and NodeNext consumer against generated public declarations\n- npm run security -- --package-smoke — TypeScript compiler AST gate over source and Acorn gate over generated JS, exact development pins, zero runtime dependencies, isolated installed exports/type smoke\n- npm audit and npm audit --omit=dev — record actual tooling/runtime audit results; runtime dependency tree must be empty\n- npx --no-install playwright install --with-deps chromium then npm run browser-smoke -- chromium — real AudioWorklet smoke; Firefox/WebKit alternatives are configured separately\n- Headless Linux Firefox requires a running native audio server. CI starts PulseAudio and a null sink before real worklet smoke; it does not mock audio or relax browser permission/autoplay controls. Consult the usage guides for setup and stage diagnostics.\n- npm run benchmark — host-dependent 128-frame render deadlines, with optional explicit budget environment variables\n- npm run sound-quality / npm run voice-quality — numerical/register/velocity matrices and controlled spectral reports, not perceptual/hardware-fidelity proof\n- npm run browser-stress -- chromium — real bounded worklet contention/lifecycle/queue checks; timer/round-trip data are not CPU, GC or glitch counters\n- npm run vite-smoke — installed artifact, non-root production base, license, actual audio, CSP and negative MIME/404 scenarios; audit standalone tooling from examples/vite too\n- Follow publishing prerequisites before any authorized npm upload; source review cannot establish external environment protections, registry authentication or provenance\n\n### 4. Review record\n\nEach release notes in the CHANGELOG which checklist sections were exercised (A/B/C/D) and by whom. Checklist failures block release; waivers require a written reason in the CHANGELOG.\n\nFor v1.2, final A/B/C/D independent review and observed integration verification are recorded in CHANGELOG before release. Configured Node 22/24/26 and Chromium/Firefox/WebKit CI jobs are capabilities, not evidence of an external run. Missing browser binaries/host support or unrun CI must be reported, not represented as passing coverage.\n\nv1.5 feature-completion review: InputBoundaryReview covered A/B; DspSupplyReview covered C/D. The integration owner reproduced and fixed null mixGain/tuning acceptance and overridable render-buffer metadata, then also prevented offset coercion before default-length calculation. Runtime evidence and remaining registry/physical-device prerequisites are recorded separately in CHANGELOG; static review is not a claim of runtime coverage.\n\nFinalInputsReview (A/B) and FinalDspReview (C/D) rechecked the final hardened source and release controls, with no new evidence-backed findings. Their PASS applies to scoped static inspection only; they ran no build, payload, tests, audit or browser commands.\n\nv1.5 release review: Release15Inputs approved scoped static A/B inspection; Release15DspSupply approved C/D for GitHub distribution of package 1.5.0, with no evidence-backed blockers. Neither reviewer executed runtime gates or certified external npm settings. The integration owner's fresh package, native browser, numerical, audit and report-only benchmark evidence is recorded in CHANGELOG.\n\nv1.5 interruption-repair recheck: Release15Inputs initially found that deferred running notifications could leave deduplication state stale. Both initial and existing-node resume now synchronize successful state observation; repeated fully deferred transitions and pending diagnostics are covered by consumer regressions. Release15Inputs then approved A/B and Release15DspSupply approved C/D without remaining scoped static findings. The original Linux WebKit CI failure blocks publication until repaired-candidate verification; static approval is not a waiver or runtime certification.\n\nv1.6 release review: a security review found two command-waiter boundary issues (abort-signal state/listener shadowing and receipt-eviction handling around correlated panic resets). Both were fixed with regressions and rechecked natively against a borrowed context. No fresh independent A/B/C/D approval was obtained for v1.6; the maintainer explicitly authorized the GitHub release on the integration owner's runtime evidence, recorded as a waiver in CHANGELOG. npm publication still requires the independent review record described in doc/publishing.md.\n\nUnreleased seven-capability checkout review: SecurityDataReview approved scoped A, SecurityProtocolReview B, SecurityDSPReview C, and SecuritySupplyReview D static inspection. Initial findings about release-tail controls, sink-transferred buffer accounting and deferred file-picker/writable teardown were fixed and the affected source rechecked without remaining scoped findings. These reviewers executed no tests, builds, audits or browser commands; their PASS does not certify runtime behavior, deployment permissions, physical devices or registry protections. The integration owner's observed tests, package gates, native browser smoke and report-only CPU measurements are recorded separately in CHANGELOG. This is not a release approval or a new waiver.\n\nv1.7 release review: Release17Data approved scoped static A, Release17Protocol B, Release17DSP C, and Release17Supply D, with no evidence-backed blockers in the final seven-capability source and 1.7.0 release metadata. All four reviews were read-only and executed no runtime gates. Fresh isolated package/build/test/type/audit/quality checks and managed native Chromium AudioWorklet observations are recorded in CHANGELOG. Remote CI must pass before the release tag is published; its run URL and outcome belong in the GitHub release record. No waiver or npm authentication/provenance/external-protection certification is asserted.\n\nv1.8 review record: InputProtocolReview approved scoped static A/B and DspAssetsReview scoped static C/D after the bounded arrangement, MIDI own-data, finite tempo-slope and canonical asset-overlap fixes. Their static approval does not certify runtime or external npm settings. The release commit d793d69056c68a971ede1af988338412687227fd has an observed successful Node 22/24/26 and Chromium/Firefox/WebKit CI run; the separate review record and release package record retain the evidence and tarball digest. This is not registry provenance, physical-device/MIDI or listening acceptance.\n\nv1.8.1 documentation/package patch review: Patch181Inputs approved scoped static A/B and Patch181DspSupply scoped static C/D, with no evidence-backed blockers. Reviewers were read-only and ran no build, tests or external actions. The integration owner confirmed the source diff against v1.8 changes only src/version.ts; runtime validation, protocols and DSP are unchanged. Final regenerated assets, package gates and remote CI are separate release prerequisites, recorded in CHANGELOG and the GitHub release notes. No npm provenance, physical-device/MIDI or listening certification is asserted.\n\nUnreleased integration-quality review: FinalImprovementInputs approved scoped static A/B after the pedal-ownership and applied-workload evidence fixes. FinalImprovementDspSupply approved scoped static C/D specifically for an isolated immutable canonical candidate (47 runtime modules with 141 matching distribution assets), after generated-link validation and the DOM-free core/project/MIDI type boundary were hardened. No surviving scoped findings remain. Both reviewers were read-only and executed no runtime gates or publication. The integration owner separately observed 381 passing behavioral tests, type checks including installed DOM-free Node consumers, numerical/dependency/package checks and native Chromium UI/smoke/stress checks. Unexpected numbered generated-file copies were preserved outside the checkout; live-tree packaging remains blocked by their reappearance and is not covered by the isolated-candidate approval. Desktop automation and numerical analysis do not certify physical devices, physical MIDI or human listening. No release approval, npm authentication/provenance/registry protection, physical-device or listening certification is asserted.\n\nv1.9 release review: Release19Inputs approved scoped static A/B and Release19Supply scoped static C/D for an isolated canonical package 1.9.0 candidate, with no evidence-backed security findings. Reviewers ran no runtime gates. Numbered live-tree generated copies are not approved for packaging and must remain outside the candidate. GitHub publication requires exact clean-commit artifacts, passing remote Node/browser CI and an archive digest retained in the release record. Physical-device/MIDI, human listening and npm provenance remain unverified and are not claimed by this GitHub-only release.\n\nv1.9 deadline repair: Release19Supply rechecked and approved scoped static C/D topology-cache/pooling/render changes after the initial release candidate failed the Node 22 prepared-burst p99 gate. Fixed slot-local caches derive only from the existing validated graphs, overwrite all edges, preserve summation order and retain finite checks, bounded fades and reentrant callback behavior. The reviewer executed no checks; this is not a performance waiver. The repaired exact commit must pass the unchanged remote gates before tagging.\n\nUnreleased readiness review: ExpressionSecurity approved scoped static A/B for final expressive SMF boundaries, ownership and loss accounting; ArrangementCaptureSecurity approved scoped A/C/D for portable/shared arrangement validation, lifecycle, inert imports, pure core contracts and bounded text-only local MIDI capture. No evidence-backed findings remained. Reviewers executed no gates. The integration owner separately observed 403 passing tests, installed package/types/security/audit checks, 108 bit-identical baseline renders, scoped capacity measurements and native isolated Edge UI/worklet checks, recorded in CHANGELOG. Physical phones/controllers and genuine eighteen-preset listening originals remain blocked/unverified. This is not release/publication approval.\n\nv1.10 release review: Release110Inputs approved scoped static A/B and Release110DspSupply scoped static C/D for an immutable canonical package 1.10.0 candidate (48 runtime modules and 144 matching distribution assets), with no evidence-backed blockers. Both approved the final documentation/generated-HTML delta. Reviewers ran no runtime gates. The integration owner separately observed 403 behavioral tests, public/DOM-free core types, numerical matrices, zero-vulnerability development/runtime/Vite audits, isolated installed-package/asset/type gates, expressive MIDI-to-Arrangement PCM/WAV smoke and the unchanged isolated p99 deadline gate. Unexpected numbered live-tree generated copies are preserved and excluded. Publish only exact committed canonical assets after remote Node/browser CI passes; retain the commit, CI URL and archive digest in the GitHub release record. This is not npm provenance or physical-device/MIDI/listening certification.\n\nv1.11 review: SecInputsProtocol approved scoped static A/B (voice v7 validation, .opm parser, MIDI program/RPN boundaries, synth and effects worklet protocols, effects URL/lifecycle) with no evidence-backed vulnerability. SecDspSupply returned approved-with-fixes C/D with low/informational findings only: the deployment inventory now requires worklet/fx-processor.js (with a rejection test), the effects headroom documentation is qualified as a steady-state ceiling (a feedback decrease can briefly exceed 0.7 during its 20 ms ramp), the input table above now covers v7 and .opm, and capacity candidates are documented as sine-operator measurements. Reviewers ran no runtime gates. The integration owner separately observed the typecheck, the full behavioral suite, the npm run security gate, and a real Chromium AudioWorklet smoke of the noise and waveform voices through the effects insert. Physical devices, MIDI hardware and human listening remain unverified; this is not a release, npm publication or deployment record.\n\nv1.11.1 review: SecFastSin approved a scoped static review of the fastSin polynomial and all-sine render fast path (NaN/Infinity containment, |value| ≤ 1 bound, determinism, O(1) cost, state parity with the generic path) with no evidence-backed vulnerability. The reviewer ran no runtime gates; the integration owner ran tests, typecheck, the package-smoke security gates and a Chrome AudioWorklet smoke.\n\n## Non-goals\n\n- Sandboxing the host page (OPM.js runs in the page's trust domain; XSS protection is the embedder's job)\n- DRM / copy protection of voice banks\n- Remote code loading of any kind — never supported\n","href":"./security.html"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · SECURITY.md — OPM.js Security Policy & Review Mechanism","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism SECURITY.md — OPM.js Security Policy & Review Mechanism","href":"./security.html#securitymd--opmjs-security-policy--review-mechanism"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Supported versions","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Supported versions","href":"./security.html#supported-versions"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Reporting a vulnerability","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Reporting a vulnerability","href":"./security.html#reporting-a-vulnerability"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Safe embedding","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Safe embedding","href":"./security.html#safe-embedding"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Website deployment and browser permissions","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Website deployment and browser permissions","href":"./security.html#website-deployment-and-browser-permissions"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Voice banks and JavaScript objects","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Voice banks and JavaScript objects","href":"./security.html#voice-banks-and-javascript-objects"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Score projects and Standard MIDI files","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Score projects and Standard MIDI files","href":"./security.html#score-projects-and-standard-midi-files"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · External downloads and file uploads","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism External downloads and file uploads","href":"./security.html#external-downloads-and-file-uploads"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · DX7 SysEx conversion","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism DX7 SysEx conversion","href":"./security.html#dx7-sysex-conversion"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · PCM, offline rendering and WAV export","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism PCM, offline rendering and WAV export","href":"./security.html#pcm-offline-rendering-and-wav-export"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · AudioWorklet, scheduling and context ownership","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism AudioWorklet, scheduling and context ownership","href":"./security.html#audioworklet-scheduling-and-context-ownership"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Source maps, privacy and redistribution","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Source maps, privacy and redistribution","href":"./security.html#source-maps-privacy-and-redistribution"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Threat model","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Threat model","href":"./security.html#threat-model"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Security review mechanism","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Security review mechanism","href":"./security.html#security-review-mechanism"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 1. Review triggers (mandatory)","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 1. Review triggers (mandatory)","href":"./security.html#1-review-triggers-mandatory"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 2. Review checklist","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 2. Review checklist","href":"./security.html#2-review-checklist"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 3. Verification gates","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 3. Verification gates","href":"./security.html#3-verification-gates"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 4. Review record","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 4. Review record","href":"./security.html#4-review-record"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Non-goals","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Non-goals","href":"./security.html#non-goals"},{"title":"Changelog","text":"Changelog # Changelog\n\nFor installation and executable examples, see the README and the English / 繁體中文 usage guides.\n\n## Unreleased\n\n### Half-band FIR decimation\n\n- Replace the per-voice Butterworth IIR decimator (cutoff 0.30 output Fs) with cascaded polyphase linear-phase half-band FIR stages: eco 35 taps at 2×; standard 15 → 47 taps; high 11 → 15 → 47 taps. Substep calls only buffer their input; the cascade runs once per output frame.\n- Passband is flat to 0.40 output Fs (ripple below 0.001 dB for standard/high; formerly −11.5 dB at 0.35 Fs). Products folding below 0.40 Fs are rejected by at least 81 dB for standard/high and 63 dB for eco (standard formerly −52.3/−55.6 dB at 0.60/0.625 Fs). Between 0.50 and 0.60 Fs the transition band folds into 0.40–0.50 Fs, and far above Nyquist the former filter's bilinear zero was deeper; see acoustic quality.\n- Behavior change: output is delayed by a constant linear-phase 13.25 output frames for standard (8.5 eco, 13.875 high; about 0.28 ms at 48 kHz) instead of the IIR's frequency-dependent 2.7–3.3 frames. Each voice preallocates 158 state scalars instead of 8.\n- Measured on this checkout's cloud container with npm run benchmark (median of four interleaved runs per side): standard 8 held voices 0.702 → 0.669 ms per 128-frame block, high 8 held voices 1.204 → 1.116 ms, eco unchanged within noise (0.434 → 0.435 ms).\n\n### Band-limited square, saw and half-wave operators\n\nSquare and saw operators now add two-sample polyBLEP residuals at their jumps, and half-wave operators add polyBLAMP residuals at their corners, sized from each operator's unmodulated phase step at the oversampled rate. In unit tests the folded-alias power of periodic frames drops by about 8–17 dB at the internal rate, before the decimator. Phase modulation makes the nominal step an approximation, so these shapes remain not alias-free. All non-sine periodic shapes now use the deterministic polynomial fastSin instead of Math.sin, and the shape helpers are split so the dispatcher stays inlinable. Measured on Node 22 with 16 held voices at 48 kHz (standard): square 1.74 → 0.98 ms/block, half 1.89 → 1.40 ms, saw 1.04 → 1.13 ms. Sine output and periodicWaveform(theta, code) without a step are unchanged apart from fastSin replacing Math.sin (~1e-10). Square/saw/half voices render differently near their edges. These timings are one Node process on one machine, not a browser AudioWorklet or device measurement.\n\n## v1.11.1\n\nPackage version 1.11.1, a GitHub release (not on npm). Performance patch over v1.11; no API, voice-format (still v7) or protocol changes. Output differs from v1.11 by about 1e-10 for sine operators. Security: scoped static review of the src/core change found no evidence-backed issue.\n\n### Faster sine operators\n\nSine operators now use a deterministic polynomial fastSin (odd degree-13 after reduction to ±π/2) instead of Math.sin, and all-sine voices with static operator levels and shared AM take a branch-light render path. Measured on Node 26 with npm run benchmark: standard 8 held voices 0.265 → 0.215 ms/block (−19%), 32 voices 1.132 → 0.900 ms (−20%), high 32 voices 1.977 → 1.515 ms (−23%). fastSin is bounded by 1 with |error| ≤ 2.0e-10 against the exact sine (about −194 dB, below Float32 resolution) plus about 1e-16·|phase| from range reduction, and is pure arithmetic, so results no longer depend on each JavaScript engine's Math.sin rounding. Rendered samples differ from v1.11 at the ~1e-10 level; non-sine operator shapes, LFOs and effects still use Math.sin. These timings are one Node process on one machine, not a browser AudioWorklet or device measurement.\n\n## v1.11\n\nPackage version 1.11.0, published as a GitHub release (not on npm). Voice format v7 (per-operator waveforms and held noise; v1–v6 inputs still accepted), optional stereo effects, MIDI program maps with RPN bend sensitivity and approximate .opm import. Remote CI for the release content (commit 64ed642) passed on Node 22/24/26 and Chromium/Firefox/WebKit: https://github.com/JS-PACKAGE/OPM.js/actions/runs/37117509642. An isolated git archive build passed npm run security -- --package-smoke. Physical-device, MIDI-hardware and human-listening acceptance remain unverified, and GitHub distribution is separate from npm publication. The repository now lives at JS-PACKAGE/OPM.js; links to historical CI runs and review issues keep their originally recorded addresses.\n\n### Approximate OPM text import\n\n- Add bounded dependency-free importOPM/describeOPM for decimal VOPM/MXDRV-family text patches, returning normalized v7 voices and separate deterministic metadata/loss warnings.\n- Preserve all eight named operator graphs, raw key-on SLOT masking, TL attenuation, coarse detune and per-operator AM enable. Document musical envelope/DT1/key-scaling/LFO approximations, omitted second decay, ignored pan and extension-only C2 noise; no register-level emulation or fidelity claim.\n- Add original recipe, finite deterministic render, topology, parameter-table, naming and hostile-input regression tests.\n\n### MIDI voice selection and bend sensitivity\n\n- Add opt-in bounded program-to-named-FM voice maps to file and live adapters, plus file-only drum-note maps and frozen artistic GM-family/percussion starter maps. Note-on voice selection leaves sounding gates untouched; unmapped programs retain fallback/ignore policies. File loss summaries distinguish honored program changes from omissions.\n- Behavior change: preserve-mode file import and live MIDI now decode channel-local RPN 0 pitch-bend sensitivity, including cents, clamped to 48 semitones. Null/NRPN selection disables data entry; live reset restores the initial range. CC100/101 are reserved; CC6/38 custom mappings still apply when RPN 0 is inactive. Export emits neither program changes nor RPN.\n- Add optional configurePart preservation policy for future-onset patch changes without changing default clear-on-configuration behavior.\n\n### Voice v7 waveforms and held noise\n\n- Canonicalize legacy v1–v6 voices to v7 while preserving their original field restrictions and unchanged sine rendering.\n- Add per-operator sine/half/abs/quarter/alternating/camel/square/saw and deterministic 17-bit held-noise oscillators, with 20–20000 Hz noise clocks and pooled admission resets. Noise on any operator is an OPM.js extension, not a chip-fidelity claim; discontinuous shapes are not alias-free.\n- Preserve waveform/noise fields through immutable preparation, bank JSON and worklet registration; add original noise-snare, noise-hihat and explosion recipes with percussion metadata and designer controls.\n\n### Optional stereo effects\n\n- Add original pure-DSP fractional-delay chorus and damped-comb/all-pass stereo reverb, shared by offline createStereoEffects/tail-extending applyEffects and an opt-in dedicated createEffects AudioWorklet insert.\n- Preserve existing synthesis and dry PCM; validate bounded own-data parameters, smooth complete configuration replacements, normalize wet gain for headroom and sanitize malformed PCM without poisoning delay state.\n- Extend the independent-buses demo with live chorus/reverb toggles and matching offline stem effects; document routing, gain law, bounded memory/tails and the separate worklet security surface.\n\n### Release hygiene and verification\n\n- The deployment inventory now requires worklet/fx-processor.js; the independent review record is in SECURITY.md (A/B approved; C/D low and informational findings addressed: deployment inventory, steady-state effects headroom wording, v7/.opm input table, sine-only capacity scope).\n- Repository links now point at JS-PACKAGE/OPM.js; historical CI-run and review-issue links keep their recorded addresses.\n- Integration owner observed: strict typecheck (source, tests, demos, generated public and DOM-free core types), 438 passing behavioral tests, the zero-dependency/dynamic-code security gate, and a real Chromium AudioWorklet smoke in which noise-snare, explosion and a camel-waveform voice produced non-silent output through the effects insert while the context stayed running; the sound designer, buses and audition pages loaded without console errors. Not run: Firefox/WebKit, physical devices, MIDI hardware, human listening, benchmark capacity for non-sine voices, installed-package/Vite smoke and npm publication.\n\n## v1.10\n\nPackage version 1.10.0. Portable arrangements, expressive MIDI exchange and scoped acceptance tooling; voice format remains v6. Historical tags and archives remain immutable. GitHub distribution is separate from npm publication and site deployment.\n\n### Portable music and expressive file exchange\n\n- Add a separate bounded version-1 Arrangement project format for named looping layers, sections, initial section, tempo/meter, validated voices and synthesis settings. Share definition validation with live Arrangement playback; save/load reconstructs music, never DSP state or pending live commands.\n- Add opt-in MIDI-file expressive controls while preserving the default note-only conversion policy. Preserve supported channel/key expression under explicit bend-range policy and expose grouped omissions, approximations and preserved wire-message counts; reject unrepresentable export semantics.\n- Add host/workload-scoped benchmark capacity candidates across quality and fully occupied voice limits. Require explicit p99 reserve, zero observed misses and clean diagnostics; retain raw worst/misses and unchanged baseline CI budgets. Candidates do not certify browser/physical-device performance.\n- Consolidate public project/MIDI surfaces, ID/lifecycle/ownership contracts and the distinction between saved musical definitions, running sessions, numerical safety and human acceptance. Synchronize current-release resources while keeping maintenance tools and HTML examples checkout-only.\n- Reuse the mobile and audition harnesses for explicit current-version acceptance campaigns. Add bounded local physical-MIDI observation downloads to the instrument page, recording exact submitted/observed settings, text-only identifiers and separate manual judgments; desktop, active, stale-scope, abandoned and truncated runs remain unverified.\n\n### Acceptance boundaries\n\n- Physical iOS/Android interruption/route/long-play acceptance, real MIDI hardware/permission/hot-unplug behavior and the eighteen presets' genuine human listening findings remain blocked pending original physical/listener observations. Local UI, numerical rendering and injected MIDI checks must not promote those results or trim metadata.\n- This release does not certify physical devices, human listening, npm publication or deployment. Genuine original observations are required before support or trim metadata can be promoted.\n\n### Pre-release checkout verification\n\n- Integration owner: build, 403 behavioral tests, strict generated public and DOM-free core types, security AST/dependency gates, isolated installed package/asset CLI/type smoke, and development/runtime audits (zero reported vulnerabilities) passed.\n- 108 stereo renders (all eighteen bundled presets × eco/standard/high × 44100/48000 Hz, fixed note/velocity/pan/gate) remained bit-identical to the published checkout's committed distribution. This establishes those exercised defaults, not every possible score.\n- Expressive SMF export/import preserved gate-internal bend/expression/pan/modulation under explicit policy, reported no losses for that representable fixture, rendered 31680 finite stereo frames and encoded a 126764-byte WAV; after-gate export rejected.\n- Native isolated Microsoft Edge 154.0.4258.48 UI verified save → load without autoplay → explicit replay, edited gain/section persistence, arbitrary imported layer/section names, quantized switches with fades, literal HTML-looking capture metadata, desktop manual-pass remaining unverified and scope invalidation. No page errors were observed; screenshots were inspected. Automation is not hardware/listening evidence.\n- A separate native Edge boundary smoke confirmed active exports stay unverified, 270 synthetic host actions retain 256 observations with 16 counted drops, 24 finished runs disable further capture, and a deliberately deferred injected permission result after Dispose cannot resurrect the adapter/audio. These synthetic checks do not exercise a physical permission prompt/controller.\n- The existing browser smoke and stress bodies also passed using a throwaway launch override selecting isolated Edge (not mocked audio, no weakened gates): real stereo/pan analyser signal, suspend/resume/close ownership, 768 dense notes, bounded contention, expected late rejection and final zero active voices/pending events/DSP errors. The smoke fixture logged a forbidden resource request while its audio/lifecycle assertions passed; the separate example UI had no page errors.\n- The 29-row Apple M5 / darwin arm64 / Node v26.7.0 benchmark used 48000 Hz, 128 frames, 300 excluded warmup blocks and 2000 measured blocks per row. All rows retained clean diagnostics and zero observed misses. The fully occupied held-brass matrix selected eco/32, standard/32 and high/16 as highest sampled candidates under 50% p99 reserve; high/32 was ineligible. The complete matrix and its scope are in acoustic-quality.\n- ExpressionSecurity approved scoped static A/B, and ArrangementCaptureSecurity scoped A/C/D, with no evidence-backed findings. Reviewers ran no runtime gates. These reviews do not approve publication or certify hardware/listeners.\n- The pinned Playwright Chromium launcher initially failed because its cached macOS Framework was missing; forced reinstallation downloaded but timed out during installation. Managed browser sessions also detached. Native Edge supplied the observed UI checks; these are not successful runs of the pinned Chromium binary.\n\n### Release-candidate verification\n\n- Package 1.10.0 passes 403 behavioral tests, the 144-asset build and strict public/DOM-free core type checks. Development, runtime and standalone Vite tooling audits report zero vulnerabilities; the runtime dependency tree is empty.\n- An isolated canonical candidate passes parsed source/generated-code, installed-package, asset CLI and installed public/DOM-free Node type gates. The local archive verifier validates matching version, bounded safe paths and complete JS/map/declaration triplets; registry publication/provenance is not claimed.\n- An installed consumer exports an 83-byte expressive SMF, imports it with no reported omissions/approximations, saves/reloads an inert Arrangement project, renders 10,560 finite deterministic stereo frames and writes a 42,284-byte WAV. Default note-only conversion and after-gate control rejection are also exercised.\n- Numerical sound/preset quality commands pass. A separate isolated 29-row benchmark passes the unchanged p99 deadline ratio 1; host measurements do not certify physical-device continuity. The earlier concurrent report-only run is not a new capacity recommendation.\n- The live-tree package gate correctly rejects unexpected numbered generated copies that reappeared during verification. They are preserved and excluded: release artifacts must be rebuilt from the exact committed canonical tree in isolation.\n- Release110Inputs approved scoped static A/B, and Release110DspSupply scoped static C/D, for the immutable canonical 1.10.0 candidate without evidence-backed blockers. Both also approved the final documentation delta, including generated HTML and the Map-based score compilation recipe. Reviewers executed no runtime gates; passing remote Node/browser CI remains mandatory before tagging.\n\n\n## v1.9\n\nPackage version 1.9.0. Integration and musical workflows release; voice format remains v6. Historical releases remain immutable. GitHub distribution is separate from npm publication; no npm publication is performed.\n\n### Integration and musical workflows\n\n- Add bounded Transport startupLead for cold starts and reconstructed playback. The musical position stays at the requested beat during count-in; genuine late/drop errors remain visible.\n- Add independent per-note and Arrangement layer gain, quantized gain changes and section fades/crossfades, including existing notes and release tails without overwriting authored expression.\n- Add declarative MIDI CC mappings with scalar/operator targets and captured-baseline reset. Adapter-owned notes can be released under a held pedal without cancelling another input's keys or pedal state.\n- Add version-1 self-contained score projects, deterministic serialization and a shared tempo-integrating beat compiler for live/offline workflows.\n- Add the independent opm.js/midi-file SMF type 0/1 PPQN import/export adapter, with bounded parsing and explicit warnings or rejection for unsupported content.\n- Keep beat-event type ownership in the environment-independent core. Separate Node consumer checks compile core/project/MIDI declarations without DOM or Web Audio globals, including an installed-package check.\n- Add two original short pieces with play/stop, project save/load and Worker PCM16 WAV export. These are musical examples, not a listening-quality certification.\n\n### Documentation and acceptance\n\n- Generate navigable, locally searchable HTML guides and a full compiler-derived API reference, with prominent online-demo links. Markdown remains the documentation source.\n- Add condition-grouped device and human-listening evidence review tools. Conflicting, incomplete, automated and unassessed evidence does not become a physical-device or listening PASS.\n- Capture the actually applied gain/tuning after Play and after partial settings failures, rather than stale session defaults.\n\n### Observed verification and review boundaries\n\n- Local behavioral suite: 382 passing tests. Dependency audit: zero vulnerabilities; runtime dependency tree: empty. Numerical sound-quality checks cover 2,505 matrix rows and 24 live-control cases; preset-quality output is accepted.\n- Native Chromium AudioWorklet observations cover beat-0 startup, gain mute/resume without retrigger, pedal-owned MIDI cleanup, both original pieces and project round trips. The actual Arrangement UI produces signal → exact silence → signal for a solo pad layer and switches/fades sections across pause/resume. Project JSON and a valid Worker PCM16 WAV were downloaded to disk; the documented Node chunk-WAV recipe also produced a finite, correctly framed file.\n- Default-gain PCM remains bit-identical in 333 baseline comparison renders. Node CPU measurements are report-only host observations, not real-time or mobile support guarantees.\n- FinalImprovementInputs approved scoped static A/B; FinalImprovementDspSupply approved scoped static C/D for an isolated immutable canonical candidate after the pedal ownership, workload-recording, generated-link and DOM-free type-boundary fixes. Reviewers ran no runtime gates. Earlier numbered generated-file copies were preserved outside the checkout; release artifacts must be built from the exact clean release commit.\n- The isolated candidate passes the 141-asset build, all behavioral/type checks and installed-package/runtime/asset/DOM-free-Node declaration gates. Native Chromium 150 smoke and the bounded 768-note stress gate pass with zero DSP errors; the intentional late/drop remains counted. HTML search and declaration-anchor navigation also work from file:// at a 390-pixel viewport without page overflow.\n- Native matched/dry A/B previews produce finite signal, but numerical measurement leaves every human criterion unassessed and refuses unconfirmed/empty finding exports. The actual desktop device capture validates as 0/36 PASS and remains unverified; neither observation is physical iOS/Android, physical MIDI or human-listening evidence.\n- Physical-device captures and genuine human listening findings are still required before those acceptance statuses or trim recommendations can be promoted. This GitHub release does not imply deployment, npm publication or physical-device/listening certification.\n- Release19Inputs approved scoped static A/B; Release19Supply approved scoped static C/D for an isolated canonical 1.9.0 candidate, with no evidence-backed security findings. Neither reviewer executed runtime gates. The GitHub release record retains the exact release commit, observed remote CI outcome and archive SHA-256; npm publication is excluded.\n- Initial release candidate 43f32549a000acdae6dd5a83aa690f97e4c42575 was not tagged: its remote CI failed Node 22 prepared-burst p99 (3.131281 ms versus a 2.666667 ms deadline), while Node 24/26 and all three browser jobs passed. No waiver or budget relaxation was used.\n- Cache operator edges and carriers in preallocated voice-slot storage at admission, preserving topology order and avoiding repeated frozen-array indexing in oversampled rendering. Keep phase/sample finite checks; add cross-topology/profile recycling coverage. A 288-case before/after smoke is PCM bit-identical across qualities, rates, stealing, gain ramps and chunk boundaries. Local Node 22 prepared-burst p99 is 1.238708 ms with zero missed deadlines; the final remote release gate remains authoritative.\n\n## v1.8.1\n\nPackage version 1.8.1. Documentation and packaging patch; no runtime API, voice-format, DSP or scheduling behavior changes. The original v1.8 tag and archive remain immutable. GitHub release distribution is not npm registry publication.\n\n### Documentation and packaging\n\n- Ship the synchronized README, bilingual usage guides and topical integration/deployment guides in a separately versioned installable archive.\n- Correct public voice budgets, priority admission, grid-helper import locations, DX7 AM conversion limits, Worker startup/phase contracts and complete static-asset deployment instructions.\n- Replace links to package-excluded HTML/source/tooling with explicit checkout-only repository resources and provide runnable default-voice arrangement recipes.\n- Leave a one-beat count-in in Transport recipes and document cold-start beat-0 late:'drop' rejection. This patch does not fix or hide the runtime timing limitation.\n- Align package/lock/source VERSION, installation filenames and release-specific deployment paths to 1.8.1; regenerate compiler-owned distribution assets.\n\n### Security and observed release-candidate verification\n\n- Patch181Inputs approved scoped static A/B and Patch181DspSupply scoped static C/D, with no evidence-backed blockers. Both reviews were read-only and ran no runtime or external gates; no waiver is asserted.\n- An isolated 1.8.1 candidate passed build, all 310 behavioral tests, strict source/development/installed-public-contract type checks, source/generated AST security gates and installed-package rendering/WAV/types/CLI checks. Full and runtime audits reported zero vulnerabilities; runtime dependencies remain empty. Standalone Vite tooling audits also reported zero vulnerabilities.\n- Fresh sound-quality passed 2,505 matrix cases and 24 live-control cases with independent spectral/filter/streaming gates; voice-quality completed successfully. The benchmark ran with no acceptance budgets and remains report-only, not a host deadline or physical-device certification.\n- Fresh generated output differs from the preceding distribution only in the three VERSION JS/map/declaration companions; all other deployed assets are byte-identical. The source diff against v1.8 changes only src/version.ts.\n- Sandboxed, muted native Chromium 150 passed the existing compiled browser smoke, 768-note stress and installed-package Vite subpath/CSP/lifecycle/missing-asset/wrong-MIME assertions using a throwaway executable-launch adapter; test assertions and runtime code were unchanged. A fresh pinned Chromium download timed out after receiving the archive, so this is explicit native-executable evidence, not a PASS for that download or the default cached-browser command.\n- The exact installed archive resolved all eleven public import specifiers and rendered finite nonzero PCM with zero errors at 32 voices in eco/standard/high, protected high-priority notes, rejected lower priority and round-tripped a linear tempo map/grid. The actual asset CLI copied/reused 134 files and checked 133 served assets with matching bytes, MIME and nosniff.\n- Exact README/English/Chinese browser quick starts reported VERSION:1.8.1, finite nonzero PCM and no browser errors or note rejections; screenshots were inspected. Checkout documentation passed relative file/anchor checks and pinned repository-resource validation.\n- Release publication is gated on observed remote Node.js 22/24/26 and Chromium/Firefox/WebKit CI for the exact release commit. Its run URL/outcome and the final archive digest are retained in the GitHub release notes. npm registry/provenance, physical-device/MIDI behavior and human listening remain separate unverified prerequisites.\n\n## v1.8\n\nPackage version 1.8.0. GitHub release distribution and npm registry publication are separate operations; neither implies physical-device acceptance.\n\n### Engine and integration\n\n- Add priority-aware voice admission and opt-in 1–32 logical voices, retaining the default eight-voice engine and at most eight stealing fades.\n- Add beat-linear tempo ramps, quantization/swing helpers and adaptive looping layers with boundary-quantized section changes and scoped cleanup.\n- Add per-key/per-part expressive controls, per-part voice budgets and an optional, explicitly requested non-SysEx Web MIDI adapter.\n- Add Worker readiness, optional startup timeout and host-local phase diagnostics without weakening chunk acknowledgements or cancellation.\n- Add the dependency-free opm-assets deployment/check CLI, canonical filesystem overlap protection, and a bounded physical-device evidence review command.\n- Expand all-profile acoustic references, selective-LFO original presets, audition controls and local human-finding exports. Add adaptive, instrument, independent-bus/stem and FM-designer examples.\n\n### Security and observed verification\n\n- InputProtocolReview approved scoped static A/B and DspAssetsReview scoped static C/D after correcting arrangement expansion/terminal cleanup, MIDI allowlist own-data handling, nonfinite linear slopes and symlinked deployment overlap. Both reviews were read-only and ran no runtime gates; this is not release approval.\n- The slope and filesystem regressions were observed failing before correction. All 41 targeted arrangement, MIDI, Transport and asset-tool tests passed after correction.\n- Final hardened candidate verification passed 310 behavioral tests, build, strict source/development/public-contract types and source/generated security gates. Isolated installed-package rendering, declarations and the actual installed asset CLI passed. Full/runtime audits found zero vulnerabilities; runtime dependencies remain empty. The device-evidence CLI classified an empty synthetic capture as 36 unverified cells, without creating physical evidence.\n- Fresh sound-quality passed 2,505 matrix cases and 24 live-control cases across eco/standard/high, including its independent spectral/filter/streaming gates; voice-quality completed with accepted: true. These numerical results are not listening or hardware-fidelity verdicts.\n- Assistant-owned native Chromium exercised FM-designer hold/live feedback/release/export with one active Worklet voice and zero errors; instrument startup/cleanup; independent-bus playback and actual offline stem links; eco audition control-phrase measurement/WAV output; and adaptive boundary switching, tempo acceleration, pause/resume/stop with no browser errors. Screenshots were inspected. The audition rejected an unassessed human finding; no listening results were fabricated.\n- Native static Worker smoke completed PCM24 output through two writes (17,324 bytes), reported zero DSP errors and observed initializing/rendering/writing/closing/completed phases.\n- The repository Playwright Chromium smoke could not launch because its cached Chromium framework was missing. Managed Chromium observations above are separate evidence, not a pass for that command or cross-browser CI. Listening, physical MIDI/device behavior and publication remain unverified.\n- Remove the manual npm-publish.yml GitHub Actions workflow. Registry publication remains a separate maintainer action; online registry-verify still expects provenance from that historical workflow path.\n- Candidate preparation preserved unexpected duplicate generated files with 2 suffixes separately and excluded them from commits. This is not a build guarantee: the current build replaces the generated dist/ directory, so preserve any host/user files outside it before rebuilding.\n\n### GitHub release package and post-release documentation\n\n- The unchanged v1.8 tag points to d793d69056c68a971ede1af988338412687227fd. Its remote quality run passed Node.js 22/24/26 and Chromium/Firefox/WebKit; this later evidence does not turn the missing local Playwright binary above into a local command PASS.\n- Attach the previously missing opm.js-1.8.0.tgz, built in an isolated checkout of that exact tag. The 491,851-byte archive has SHA-256 bee862d5b3ccaa7c4e8ff959583873c78a8ca8a9f56f4d8b039e490e498e2587; build/type/security/local-artifact gates and exact installed-package rendering/asset CLI passed, and a downloaded copy matched byte-for-byte. The review issue is closed as a review record, not npm-publication certification.\n- Deep audit of the attached archive verified all eleven public import specifiers, 44 JS/map/declaration triplets, embedded sources, runtime/type import closure and the actual CLI's served asset bytes/MIME/nosniff. Native Chromium observed finite nonzero AudioWorklet output with zero errors in eco/standard/high at maxVoices:32; native static Worker output matched core WAV bytes in all three formats with transferred sink buffers and zero errors. These checks are not physical polyphony-budget, device/MIDI or listening acceptance.\n- Synchronize README, both usage guides, all topical guides, Vite guidance, project guidance and current publication/security instructions to v1.8 contracts. Document direct release-tarball installation, twelve checkout-only HTML examples, 1–32 logical voices/default eight, priority-aware admission, tempo/arrangement, expressive/MIDI and Worker diagnostics; make shipped-document links independent of excluded HTML/source/tooling.\n- Keep historical verification records and the original tag/tarball unchanged. Registry publication/provenance, physical-device/MIDI behavior and human listening remain separate unverified prerequisites.\n- Documentation execution caught incorrect grid-helper import claims and cold-start Transport beat-0 late rejection. Grid helpers are root exports, not core exports; runnable Transport recipes now leave a one-beat count-in and explicitly document the remaining late:'drop' timing limitation without changing runtime code.\n- Post-release documentation smoke passed isolated build/type/local-artifact gates, direct GitHub URL installation, all three Node quick-start scripts (true 0), installed asset copy/reuse and 133 served-asset checks, plus the installed-package Vite 8.3.2 production build. Sandboxed, muted native Chromium exercised the exact README/bilingual quick starts, both updated Transport/performance scripts and the complete default-voice arrangement recipe with finite nonzero PCM, zero DSP errors/rejections and no browser errors; screenshots were inspected. Native Worker diagnostics produced 8,684 PCM24 WAV bytes identical to core output and completed all phases. An installed 32-voice priority/tempo/grid smoke preserved protected voices, rejected lower priority and round-tripped a linear tempo map. These are scoped documentation/contract observations, not a fresh full-suite, registry, physical-device/MIDI or listening certification.\n\n## v1.7\n\nPackage version 1.7.0. GitHub release distribution and npm registry publication are separate operations; the historical v1.6 tag and artifact remain immutable.\n\n### Musical scheduling, rendering and expressive control\n\n- Add beat-based createTransport() with AudioContext-clock pause/resume, seek, loops, time signatures, BPM and bounded tempo maps. Restarts rebuild owned musical notes/automation, not an exact DSP snapshot; unrelated notes remain untouched. Preserve scheduled controls throughout release tails, including the final non-loop endpoint.\n- Add exact-frame incremental WAV encoding and PCM16/PCM24/Float32 output. Full-buffer encoding retains its existing frame limit; incremental encoding enforces RIFF32/sequence limits without retaining a complete WAV.\n- Add a static same-origin module Worker renderer with progress, cancellation and one acknowledged chunk at a time. Await sink writes for backpressure, preserve accounting when the sink transfers buffers, and invalidate output through the host's abort contract.\n- Add bounded part-scoped performance policies: independent physical key IDs, sustain, polyphonic/mono legato, last/high/low priority and scoped cleanup. This is not a MIDI driver.\n- Add independently ramped feedback, LFO rate/depths, operator ratios/fixed frequencies and live ADSR reanchoring without resetting oscillator phase. Canonical voice format v6 adds per-operator AM/PM targets; strict original v1–v5 inputs remain accepted.\n- Add immutable eco/standard/high quality profiles (2×/4×/8×); standard retains the previous default output. Publish measured CPU tradeoffs without arbitrary-FM alias-free, perceptual or physical-device claims.\n- Add atomic bank replacement, lookup removal and detached canonical JSON export. Validation failures leave the bank unchanged; queued/sounding patches retain their snapshots. An explicit empty input array clears the bank.\n- Integrate all seven capabilities into public exports/declarations, bilingual usage guides and the shared-score example. Abort file-export lifetime before awaiting picker/writable acquisition; wait for panic/release admission before allowing the next UI action.\n\n### Pre-release feature verification\n\n- Node.js 26.7.0: 272 behavioral tests, strict source/development/NodeNext-consumer types, generated build, AST/security gates and isolated installed-package render/WAV/types passed. Full tooling and runtime audits found zero vulnerabilities; the runtime dependency tree is empty. The broader offline sound-quality command passed 1,728 matrix cases and its independent spectrum/filter/four-operator/two-minute streaming gates.\n- Actual offline smoke rendered eco PCM16, standard PCM24 and high Float32 WAVs with zero DSP errors. The default profile retained bitwise PCM parity across all eight algorithms against the pre-change distribution. Real Worker-thread regressions cover transferred sink ownership, backpressure and cancellation; Transport regressions exercise actual worklet DSP release-tail controls.\n- The assistant-managed hidden Chromium 150 browser exercised trusted audio startup, nonzero analyser output in all three live quality profiles, smooth feedback/LFO controls, Transport pause/seek/tempo/resume/stop and an actual loop seam (seek 7.8 beats at 300 BPM, observed wrap to 0.33), mono/high-priority key/pedal controls, bank replace/remove/export and native static-Worker PCM24 preview under same-origin CSP and nosniff. The preview contained 43,680 frames / 262,124 bytes with zero DSP errors; an actual full-page screenshot was inspected. Browser observations are not listening or physical-device acceptance.\n- Native browser Worker smoke matched whole-core WAV bytes for eco PCM16, standard PCM24 and high Float32 while transferring every sink buffer; at most one write was active. Cancellation rejected with AbortError despite stalled write/abort promises. Wrong-MIME and missing Worker assets rejected without writing or closing the sink, with one abort each.\n- A throwaway same-origin application-world probe substituted native OPFS storage for the OS chooser, retaining the actual registered demo teardown callback and native Worker. Leaving during picker acquisition started no Worker/writable; leaving during writable acquisition started no Worker and aborted exactly once. A normal 60-second / 96-kHz PCM16 export wrote 23,101,484 native file bytes through 1,411 writes, closed once and reported zero DSP errors. Probe files were removed; this does not verify the operating-system chooser UI.\n- Report-only Apple M5 profile benchmark: eight voices/LFO, 48 kHz, 128-frame blocks, 300 excluded warmup and 2,000 measured blocks per profile. Eco/standard/high p99 was 0.280/0.509/0.820 ms, with zero deadline misses and zero DSP errors in each row. See acoustic evidence for exact timings and limits.\n- Independent scoped static review: SecurityDataReview A PASS, SecurityProtocolReview B PASS, SecurityDSPReview C PASS, SecuritySupplyReview D PASS after correcting release-tail control omission, transferred-buffer byte accounting and deferred file-acquisition teardown. Reviewers ran no runtime gates; integration evidence above is separate. No waiver or release authorization is asserted.\n- Unexpected duplicated generated assets failed an early package-map gate; original assets were preserved. v1.7 verification and packaging use a clean tracked-file checkout, excluding duplicate/untracked assets without deleting user files.\n- This feature-development record did not certify cross-browser CI or npm publication. Listening quality, physical iOS/Android interruption behavior, arbitrary-patch alias freedom and the operating-system file chooser remain unverified.\n\n### v1.7 release-candidate verification\n\n- Package/lock version 1.7.0: a fresh isolated checkout passed all 272 behavioral tests, source/development/NodeNext-consumer type checks, generated build, source/built AST security gates and installed-package render/WAV/types. Full tooling and runtime audits reported zero vulnerabilities; runtime dependencies remain empty. Both sound-quality and voice-quality commands passed, followed separately by the report-only benchmark.\n- Managed native Chromium 150 under same-origin CSP and nosniff exercised the built shared-score demo and 48-kHz AudioWorklet output in eco/standard/high profiles: each produced finite nonzero PCM and zero DSP errors. This is native runtime observation, not listening or physical-device acceptance.\n- Fresh independent read-only static release review: Release17Data A PASS, Release17Protocol B PASS, Release17DSP C PASS, Release17Supply D PASS. Reviewers executed no runtime gates or external publication actions.\n- The GitHub release is gated on observed remote Node 22/24/26 and Chromium/Firefox/WebKit quality/security jobs, including native stress and installed Vite deployment. The release notes record the actual run URL/outcome and verified tarball digest; configured jobs alone are not passing evidence. GitHub distribution does not publish to npm.\n\n## v1.6\n\nPackage version 1.6.0. GitHub release distribution and npm registry publication are separate operations; the v1.5 artifact remains immutable.\n\n### Acoustic quality, expression and adoption\n\n- Add original independent nested four-op chain/branched/multicarrier references alongside Bessel/feedback/long-stream coverage; replace the former four-pole decimator with a preallocated eighth-order Butterworth cascade. Passband, folded-product rejection, phase, tail and CPU tradeoffs are explicit; no arbitrary-FM alias-free or perceptual claim.\n- Canonicalize voice format v5 with fixed-Hz operators, rate key scaling, pitch envelopes and delayed/phase-selectable note/global LFO. Retain strict original v1–v4 input shapes. Add independent per-operator level multipliers/ramps and note-scaled offline release tails.\n- Preserve supported fixed-Hz/expressive DX7 source fields using documented musical heuristics and actionable substitution/loss warnings; six-to-four topology and hardware timing remain intentionally lossy.\n- Add bounded long-score preparation, capacity estimation, reusable chunked rendering and mixed note/control/stop streaming without removing short-score/worklet/WAV limits.\n- Add same-origin custom worklet assets, multicast subscriptions, timed/abortable command admission waits and terminal disposal alongside restartable close.\n- Expand original curated preset recipes and provenance/register/velocity metadata, numerical host trims and repeatable phrase/A/B audition without rewriting patches for loudness.\n- Expand physical-device recovery scenarios, bounded local observation/status exports and an explicit unverified iOS/Android support matrix.\n- Add post-publication registry byte/integrity/source-provenance/signature/installed-consumer verification to the manual npm workflow; local artifact validation is available without claiming registry publication.\n- Synchronize project guidance and public usage contracts. Physical-device/listening evidence and authorized first npm publication remain external prerequisites.\n\n### v1.6 verification record\n\n- Node.js 26.7.0: 198 behavioral tests, strict source/development/consumer types, source/generated AST gates, installed-package render/WAV/declarations passed; root full/runtime and Vite tooling audits found zero vulnerabilities, with an empty runtime dependency tree. A stale generated dist containing duplicated 2/ 3 suffix files failed the packaged-map gate; it was preserved outside the repository and rebuilt cleanly rather than deleted.\n- Native Chromium 153.0.8010.12 (48 kHz backend, muted) ran the audition UI (A/B measurement, matched-versus-dry reports, PCM16 WAV download, play/stop/dispose and fresh restart), a 60-second/96 kHz bounded render (5,775,360 frames, zero DSP errors) and a 390 px layout check; the event log is now height-limited so long reports do not widen the page. It also ran command-waiter admission, receipt eviction, native abort-signal handling, correlated panic reset, subscription cleanup and terminal disposal against a borrowed context. Worklet stress and the installed Vite subpath/CSP/MIME/404 smoke passed again; the native WebKit 26.6 AudioWorklet smoke passed.\n- Report-only Apple M5 benchmark (48 kHz, 128 frames, 2.667 ms deadline): the order-8 decimator costs more than the former filter. Eight voices with LFO had p99 at 1.575× the deadline and 79 misses; without LFO 1.429× and 45 misses; raw burst 3.323× and prepared burst 2.591×. Worst samples include scheduler/GC pauses (up to 120 ms), so this is a host-dependent tradeoff, not an underrun counter. Measure before enabling dense polyphony on tighter CPU budgets.\n- Not verified: listening quality, physical iOS/Android interruption behavior, locked Playwright 1.56.1, Firefox, Linux WebKit CI and any npm registry operation (npm whoami previously returned ENEEDAUTH). Desktop automation does not earn physical-device acceptance.\n- Waiver: v1.6 was released on GitHub at the maintainer's explicit request without a fresh independent A/B/C/D review (see SECURITY.md). It is not eligible for npm publication until that review record exists.\n\n## v1.5\n\nPackage version 1.5.0. GitHub release distribution and npm registry publication are separate operations; the historical v1.4 artifact remains immutable.\n\n### Complete expressive playback and lifecycle\n\n- Return correlated command IDs/accepted-or-rejected events for stop, controls, all-notes-off, panic, mix gain and tuning. Queue-full/inactive commands no longer fail silently; acceptance is admission, not a future-execution promise.\n- Add allNotesOff with natural release and queue-bypassing panic with immediate tail removal; add context/reset events and reliable host gate cleanup across close/failure/interruption.\n- Add independent optional 0–10-second expression/pan/modulation ramps, preserving immediate defaults and pitch-glide semantics.\n- Add 0–1 pre-tanh mix gain and phase-preserving global A4/128-note cents tuning; browser notes now accept fractional MIDI like offline notes.\n- Replace saturated patch-cache inline fallback with bounded content-key LRU registration replacement; queued/active voices retain original prepared snapshots.\n- Add bounded prepareSequence/renderSequence/playSequence with shared note/control/stop ordering, frame rounding, tuning and release tails.\n- Canonicalize voice format 4 with sine/triangle/saw/square LFO waveforms; retain versions 1/2/3 under old field rules. Add oldest/release-first/quietest stealing without instantaneous zero-crossing decisions.\n- Default context interruption to cancel; preserve is explicit. Non-running contexts/resets stop lookahead, with error reporting and gesture-driven restart rather than stale replay.\n- Observe context state before and after native resume rather than relying only on queued statechange notifications; cancel interrupted gates and diagnostics reliably without duplicate interruption resets.\n- Add example 08 with native playback, same-score WAV, gain/tuning/stealing controls, suspend/recovery/disposal, bounded local observation export and manually confirmed physical-device scenarios.\n\n### Independent numerical and security acceptance\n\n- Add index-16 Bessel PM spectral references at 44.1/48/96 kHz with separate brightness and alias bounds, a contractive delayed-feedback reference for feedback 7/level 0.25, and conservative full-level feedback energy bounds. Chaotic full-level feedback does not admit unique alias/harmonic separation; no all-patch or chip-fidelity claim.\n- Add two 120-second streaming scenarios for held AM/PM and repeated interrupted glides; verify continuity, deterministic rechunking and terminal silence.\n- InputBoundaryReview reviewed A/B; DspSupplyReview reviewed C/D. Reproduced/fixed null mixGain/tuning acceptance and shadowed native render metadata; also reject offset coercion before default-length calculation. Permanent regressions protect unchanged sound/frame state on rejected input.\n- Node.js 26.7.0: 157 behavioral tests and strict source/development/public-consumer types passed. Full/runtime npm audits found zero vulnerabilities; runtime dependencies remain empty.\n- All 1,728 sound-matrix cases and independent/long-stream gates passed. The conservative filter still reports −11.627 dB at 9.6 kHz/48 kHz; improving arbitrary upper-register brightness/aliasing is not claimed.\n- Owned sandboxed, explicitly muted Chromium 153.0.8010.12/Playwright 1.63.0 exercised real AudioWorklet signal, cancellation/preservation, routing/lifecycle and the feature UI. Actual backend rate was 24 kHz; all four waveform WAVs decoded as 21,840 stereo PCM16 frames/87,404 bytes. Fresh running-state analyser output confirmed preserve across all three stealing policies; no listening or underrun certification.\n- The local device report contained 184 bounded events with every physical-device scenario unconfirmed; nothing was uploaded. No devicectl/adb or real phone was available. npm whoami returned ENEEDAUTH during feature verification: authenticated first npm publication and registry-installed verification remain external prerequisites.\n- FinalInputsReview (A/B) and FinalDspReview (C/D) re-reviewed the completed fixes without remaining findings. Native Node installed-tarball runtime and consumer declaration gates passed against the clean 27-module distribution.\n- Executed the actual canonical JSON from README and both usage guides with finite nonzero PCM and zero errors. Both unmodified shared-score guide scripts rendered 13,440 frames at 24 kHz in native Chromium, produced analyser signal and closed their owned contexts.\n- Final native browser smoke passed. Stress exercised 768 dense notes in 32 batches: 769 started including the initial held note, 760 steals and one intentional late rejection; settled active/pending/error counts were zero. Installed-package Vite 8.3.2 production checks passed for non-root base, CSP, normal assets, missing worklet and incorrect MIME handling.\n- Report-only Apple M5/Node 26.7.0 benchmark (48 kHz, 128 frames, 2,000 measured blocks/scenario): eight voices with LFO had p99 2.2315 ms, worst 10.627416 ms and 16 deadline misses; without LFO had 6 misses, raw burst 5, prepared burst 0. No hard budgets were configured; scheduler/GC-sensitive timings are not an underrun counter or realtime guarantee.\n\n### v1.5 release verification\n\n- Security: Release15Inputs approved scoped static A/B review; Release15DspSupply approved C/D for this GitHub-only release. No evidence-backed release blockers remained. Reviewers ran no runtime commands; integration checks were performed separately.\n- Fresh package 1.5.0 verification on Node.js 26.7.0: 27 JS/map/declaration triplets, all 157 tests, strict source/development/consumer types, source/generated AST gates, installed render/WAV/types and empty runtime dependency tree passed. Root full/runtime and installed Vite tooling audits found zero vulnerabilities.\n- All 1,728 sound cases, independent PM/feedback bounds, two 120-second streaming scenarios and 108 preset/conversion cells passed again.\n- Owned sandboxed, explicitly muted Chromium 153.0.8010.12/Playwright 1.63.0 passed native smoke/stress and installed-package Vite 8.3.2 production subpath/CSP/MIME/404/license checks again. The actual backend was 24 kHz; stress started 769 notes, stole 760, intentionally rejected one late note and settled with zero active/pending notes and DSP errors. These local runs do not certify locked Playwright 1.56.1, Firefox/WebKit or physical-phone coverage.\n- Fresh Apple M5 report-only benchmark (300 warmup blocks excluded, 2,000 measured 128-frame blocks at 48 kHz): eight voices with LFO p99/worst 0.630/2.520 ms and zero misses; raw burst 2.295/12.621 ms and 10 misses; prepared burst 0.914/1.963 ms and zero misses. No hard budgets were configured; these observations are not a universal realtime or glitch-free guarantee.\n\n### WebKit interruption repair\n\n- Initial package 1.5.0 quality CI passed Node 22/24/26, Chromium and Firefox but failed Linux WebKit 26.0 interruption cancellation. Publication was paused; the failing assertion was not removed or waived.\n- Native suspend/resume promises can settle before queued statechange tasks. Synchronize observation around both initial and existing-node resumes, recording transitions before callbacks and deduplicating notifications.\n- Real-processor regressions cover deferred running/suspended notifications over consecutive cycles, both resume/start, cancel/preserve, actual PCM, queued/held cancellation exactly once and borrowed ownership. A separate consumer regression verifies outstanding diagnostics reject at recovery boundaries.\n- Release15Inputs re-reviewed A/B after fixing stale observer state; Release15DspSupply re-reviewed C/D. Both scoped static reviews passed without remaining findings; neither reviewer ran runtime gates.\n- Rebuilt 27-module distribution, all 159 behavioral tests, strict types, source/generated AST checks and installed-package render/WAV/declarations passed on Node.js 26.7.0.\n- Unmodified native smoke passed on owned WebKit 26.6 and sandboxed, explicitly muted Chromium 153.0.8010.12 with Playwright 1.63.0 on macOS arm64, using the repaired distribution and a muted audio graph. Both verified interruption reset, terminal cancellation and silent/empty recovery, with zero DSP errors; actual sample rate was 24 kHz. This is separate from locked Linux WebKit 26.0 acceptance.\n\n## v1.4\n\nPackage version 1.4.0. Distributed through the GitHub release tarball; npm registry publication is a separate operation.\n\n### Sound quality and realtime acceptance\n\n- Add a 1,728-case offline matrix across four sample rates, all eight algorithms, feedback extremes, registers, velocities and voice-stealing workloads. Check finite output, headroom, settled release, determinism and render-chunk independence.\n- Report controlled passband loss, harmonics 2–8 THD and two isolated ultrasonic folded aliases. These bounds are not hardware-fidelity, arbitrary-FM alias-free or perceptual-loudness claims.\n- Add native AudioWorklet stress with 768 dense notes, live controls, scheduled releases, stealing and bounded main-thread contention. Establish actual rendered output before testing a past deadline; a running main-thread clock alone is insufficient during processor startup.\n\n### Prepared patches and expressive notes\n\n- Add opaque immutable prepareVoice() snapshots and bounded reusable core voice state. Register/cache up to 128 patches per worklet, with validated inline transport beyond the cache; expose defensive read-only voice-bank snapshots.\n- Add Synth.updateNote() / OPM.updateNote() with relative pitch, linear-in-semitone glide, expression, pan and FM/LFO modulation controls, including scheduled and pending-note updates.\n- Make voice version 3 canonical, adding optional per-operator velocity sensitivity. Continue accepting version 1/2 under their original field rules; omitted sensitivity preserves legacy sound. DX7 velocity conversion is an explicitly approximate heuristic.\n- Bound terminal notifications after each frame's traversal and reject recursive rendering. Callback-admitted notes begin no earlier than the next frame, preventing callbacks from replenishing an unbounded render loop.\n\n### Audio-clock scheduling and deployment\n\n- Add absolute at, scheduled stop, explicit late-start/drop policies and actual lifecycle frame/time stamps. At equal frames, stop precedes onset, then controls; late-start retains the full gate.\n- Add bounded createLookaheadScheduler() with explicit error handling, restart/disposal and skipped missed windows. Update the finite song and live-control examples.\n- Add an installed-tarball Vite example with complete dist and LICENSE copying, non-root deployment, CSP and real MIME/404 acceptance. Add a manual npm trusted-publishing/provenance workflow gated by matching release metadata, security evidence and environment approval; no runtime dependencies are added.\n\n### Preset and DX7 audition\n\n- Add the seventh example: preset/original-synthetic-DX7 A/B audition, register/velocity grids, raw peak/RMS dBFS reports, bounded attenuation-only host trim and offline WAV download. Cover seven bundled presets plus five generated converter recipes, not copyrighted third-party banks or a six-operator reference renderer.\n- Keep the mobile report horizontally scrollable inside its container. Update README, both executable usage guides, deployment/publishing guidance and the security policy.\n\n### Feature verification before release\n\n- Security: AllFeaturesInputsReview approved scoped static A/B review; AllFeaturesDspSupplyReview approved C/D after the bounded-callback and deployed-LICENSE fixes. Runtime checks were performed separately by 語喵.\n- Node.js 26.7.0: all 108 behavioral tests, strict source/development/consumer types, source/generated AST security gates and installed-package render/WAV/types passed. Build output has 22 JS/map/declaration triplets. Root full/runtime audits and the installed Vite consumer audit found zero vulnerabilities; runtime dependencies remain empty.\n- All 1,728 sound cases and 108 preset/conversion cells passed. Thirty-two legacy stereo PCM cases were bit-identical to shipped tag v1.3, covering four rates, eight algorithms, LFO, polyphony, stealing and release. A 256-prepared-note/control/steal render allocated zero additional Float64 state arrays after construction; this is not a whole-JavaScript zero-allocation guarantee.\n- Owned isolated headless Chromium 153.0.8010.12 with Playwright 1.63.0, sandboxing and explicit audio muting passed native smoke and stress: 769 accepted/started notes, 760 steals, one intentional late rejection, peak 0.672, final active/pending counts zero and no DSP errors. This is not locked Playwright 1.56.1, Firefox/WebKit or current remote-CI coverage.\n- The same silent browser exercised both guides' absolute-control and eight-note lookahead recipes, restart/stop/disposal, live controls, a complete 24-second song and audition A/B/restart. The 390-pixel catalog/report stayed within the viewport. A persisted audition WAV decoded as 40,513 stereo PCM16 frames at 48 kHz (162,096 bytes).\n- Vite 8.3.2 production acceptance passed under /opm-example/: real signal/lifecycle, blocked inline CSP script, missing-worklet and wrong-MIME failures, and exact deployed LICENSE contents.\n- Apple M5 / Node 26 report-only benchmark excluded 300 warmup blocks and measured 2,000 blocks per scenario. Raw burst p99/worst were 1.038/1.402 ms; prepared burst p99/worst were 1.238/2.470 ms, with no missed 2.667 ms deadlines. Scheduler/GC variance and other hosts prevent a universal realtime guarantee.\n- npm registry publication is not part of this GitHub release. Local npm authentication was unavailable during feature verification; trusted-publisher/environment setup and release-specific approval/evidence remain prerequisites for the manual publishing workflow.\n\n### v1.4 release verification\n\n- Security: Release14Inputs approved static A/B review, including WAV/render and application file boundaries; Release14DspSupply approved static C/D review for this GitHub-only release. Neither reviewer executed runtime checks; no evidence-backed release blockers remained.\n- Fresh package 1.4.0 verification on Node.js 26.7.0: 22 JS/map/declaration triplets, all 108 tests, strict typechecks, source/generated AST gates, installed render/WAV/types, zero runtime dependencies and both root audits passed. All 1,728 sound cases and 108 preset/conversion cells passed again.\n- Owned sandboxed headless Chromium 153.0.8010.12 with Playwright 1.63.0 and explicit audio muting passed native smoke/stress and installed-package Vite production checks again: 769 accepted/started notes, 760 steals, one intentional late rejection, zero DSP errors and no final active/pending notes. Locked-browser and remote-CI results are separate evidence.\n- Fresh Apple M5 report-only benchmark: raw burst p99/worst 1.131/3.303 ms with one missed 2.667 ms deadline; prepared burst p99/worst 0.913/1.133 ms with zero misses. These host-dependent wall-clock observations are not a universal realtime guarantee.\n\n## v1.3.0\n\n### Release verification and security review\n\n- Security: independent reviewers Release13Inputs (A/B, including the v1.2.0 release diff) and Release13DspSupply (C/D, current source/build/package controls) approved scoped static reviews with no evidence-backed blockers. Runtime checks were executed by 語喵, not the reviewers.\n- Fresh local verification on Node.js 26.7.0: build produced 21 JS/map/declaration triplets; all 73 tests, strict source/development/consumer typechecks, AST security gates and both npm audits passed (zero vulnerabilities). The runtime dependency tree is empty. Installed v1.3.0 package rendering produced 1,440 frames and a 5,804-byte WAV; installed consumer types passed.\n- Real Chromium 153.0.8010.12 AudioWorklet smoke passed on macOS arm64 using the existing Playwright 1.63.0 launcher: stereo/pan, note lifecycle, routing, suspend/resume and context ownership passed with zero DSP errors. Locked Playwright 1.56.1 Chromium installation did not complete locally; this is not locked-browser coverage.\n- Observed pre-release quality CI for b177bed passed Node 22/24/26 and Chromium/Firefox/WebKit jobs; Pages deployment also passed. These runs precede the release metadata commit.\n- Apple M5 / Node 26 benchmark excluded 300 warmup blocks and measured 2,000 blocks per scenario. Burst p99 was 1.121 ms, worst 1.534 ms, with zero missed 2.667 ms deadlines. This report-only local measurement is not a universal realtime guarantee.\n\n### Detailed English website security policy\n\n- Expand the English security policy with deployment, upload/download limits, voice/DX7/PCM boundaries, scheduling, audio safety, privacy and private reporting guidance. Preserve mandatory security-review triggers and checklists.\n- Link the policy from the example catalog. Browser verification confirmed the link and HTTP 200 for the hosted policy.\n\n### Website favicon\n\n- Add an original 16/32/48-pixel favicon.ico and link it from the catalog and all six examples.\n- Browser decoding and visual inspection confirmed the 48-pixel image and all three ICO directory entries.\n\n### English example catalog\n\n- Turn the root index.html into a six-example English catalog. Add basic notes/voices, stereo/per-voice LFO, shared AudioContext routing and offline WAV examples. Move the song to examples/song.html and the advanced playground to examples/playground.html; remove the old demo/index.html route.\n- Keep all six TypeScript helpers in demo/ and build matching minified JS, embedded-source maps and genuine compiler-generated declarations. The build emits 21 JS/map/declaration triplets.\n- Browser smoke exercised English hold/release, pan/LFO settings, song start/stop, key scaling and real AudioWorklet diagnostics. Disposing and restarting OPM retained the running host context. The 390-pixel catalog had no horizontal overflow. Fix wrapped heading links so their entire title block is clickable; actual catalog navigation then started and released a note successfully.\n- Offline WAV smoke produced 11,246 stereo frames at 22,050 Hz (45,028 bytes), verified RIFF/WAVE, PCM16, sample rate and non-silent samples. The download action was exercised, but persisted browser-download files were not observed.\n- Verification: build, 73 behavioral tests, strict source/development/consumer typechecks, zero-runtime-dependency/dynamic-code gates and isolated installed-package rendering/WAV/types passed on Node.js 26.7.0. Remote CI and Firefox/WebKit were not run; full timed song completion was not verified by the browser automation.\n\n### Node.js 22 minimum\n\n- Align root lockfile metadata and support/development documentation with the upstream package requirement of Node.js >=22.0.0 and CI matrix of 22/24/26.\n- The maintainer reports Node.js 22 tests passing; this session's local checks ran on Node.js 26.7.0.\n\n### Complete JavaScript/map/declaration triplets\n\n- Preserve engine source-module paths instead of bundling into hashed chunks, allowing every emitted JS to retain genuine compiler-generated export declarations. Generate declarations for both demos as well; their export {}; accurately represents modules with no public exports.\n- Enforce a matching .js.map and .d.ts for every JS before replacing dist/, and in the isolated package gate. Remove obsolete chunk output and update deployment documentation.\n- Verification by 語喵: 17 JavaScript files, 17 matching source maps, and 17 declarations in both the rebuilt tree and package manifest. Embedded sources match original TypeScript. Build, 73 behavioral tests, strict source/development/consumer typechecks, AST/security checks, and installed-package rendering/WAV/types passed.\n- Real Chromium 153.0.8010.12 AudioWorklet smoke passed with preserved module imports on macOS arm64 using the existing matching Playwright 1.63.0 launcher; stereo, pan, note lifecycle, routing, suspend/resume, and context ownership passed with zero DSP errors. Remote CI and locked-browser runs were not performed.\n\n### Minification and TypeScript source maps\n\n- Minify every distribution JavaScript module, including both demos, through the shared safe Terser pipeline. Emit a matching .js.map and relative sourceMappingURL for each module; compose esbuild/Terser mappings back to original TypeScript with embedded source contents.\n- Include maps in the package whitelist and distribution allowlists. Keep .d.ts declarations and omit JSON assets/compressed sidecars. Publishing source maps exposes the embedded source, as documented in README and both usage guides.\n- Verification by 語喵: build, 73 behavioral tests, strict typechecks, AST/security gates, and isolated installed-package rendering/WAV/types passed. All 16 JS files have corresponding packaged maps; every embedded TypeScript source matches its source file exactly. A real invalid-voice exception under Node --enable-source-maps resolves to src/voices/normalize.ts:9:11, rather than generated chunk coordinates. Three re-export-only entry modules correctly have empty source mappings.\n- Visually exercised electric-piano hold/release/diagnostics and song start/stop after demo minification, with no page errors. Real Chromium 153.0.8010.12 AudioWorklet smoke passed on macOS arm64 using the existing matching Playwright 1.63.0 launcher, covering stereo, pan, lifecycle, routing, resume, and context ownership. Remote CI and locked-browser runs were not performed.\n\n### JavaScript-only distribution assets\n\n- Restrict dist/ to .js and generated .d.ts; remove JSON assets and gzip/Brotli sidecars from build output. Move canonical bundled data to typed modules: opm.js/voices/examples.js exports examples, and opm.js/voices/voice.schema.js exports voiceSchema. Remove the obsolete generic asset export; external JSON voice-bank input remains supported.\n- Update demo loading, package exports, consumer declarations, and both usage guides. The seven presets and schema were compared against their previous JSON data and match exactly.\n- Verification by 語喵: build, 73 behavioral tests, strict source/development/consumer typechecks, source/dist AST gates, and isolated installed-package rendering/WAV/types passed. The package smoke checks every packaged distribution file against the .js/.d.ts allowlist. Visually exercised electric-piano hold/release and diagnostics in the rebuilt demo; no page errors or DSP errors.\n- Real Chromium 153.0.8010.12 AudioWorklet smoke passed on macOS arm64 using the existing matching Playwright 1.63.0 launcher, covering stereo, pan, lifecycle, routing, resume, and context ownership. This is not remote CI or locked-browser evidence.\n- Security: 語喵 reviewed scoped A/B/C/D requirements: bundled assets contain only preserved literal data, bank validation still runs before demo registration, worklet/DSP boundaries and limits are unchanged, and no dependency or dynamic execution was added. This is a scoped author review, not an independent review.\n- Development/runtime audits reported zero vulnerabilities; the runtime dependency tree is empty.\n\n### TypeScript source cutover\n\n- Preserve published structural OPM/Synth class contracts by stripping implementation-only declaration members; retain the literal bank-size type and optional event callback. Installed consumer checks cover adapters and the literal limit. The test entrypoint enumerates filenames without shell globs and forwards Node runner options for Node 18/Windows compatibility (Windows execution not observed); diagnostics filtering and reporter selection were exercised on Node 18/26.\n- Migrate engine, AudioWorklet, voice parsers, demos, behavioral tests, and development tools to strict TypeScript, following XYZ.js's source/generated-declaration approach. Remove obsolete JavaScript source and handwritten declarations; keep public .js package/deployment URLs and zero runtime dependencies.\n- Generate declarations from implementation, compile development programs into ignored .dev/, and build external dist/demo/ modules for both HTML demos. Preserve npm and Node 18+ compatibility rather than adopting XYZ.js's newer platform requirement. CI invokes the compiled tooling through npm scripts.\n- Verification by 語喵: build, complete strict typecheck, 73 behavioral tests on Node 26.7.0 and Node 18.20.8, TypeScript-source/JavaScript-dist AST gates, installed ESM/WAV/type smoke, and development/runtime audits (zero vulnerabilities). All original behavioral test titles remain; the former 74 count included automatic discovery of the fixture-only module, now excluded by explicit *.test.js selection.\n- Released v1.2 DSP versus compiled TypeScript and rebuilt distribution: 248 cases, 4,504,320 compared channel samples, exact Float32 equality, plus matching WAV bytes. Covers algorithms, stereo pan, sample rates, and burst stealing; this is scoped parity evidence, not hardware fidelity.\n- Apple M5 / Node 26: unchanged 300-warmup/2,000-block benchmark passed all p99 budgets; burst p99 0.868 ms, zero missed deadlines against 2.667 ms. No universal deadline guarantee.\n- Real rebuilt AudioWorklet smoke passed Chromium 153.0.8010.12 and WebKit 26.6 on macOS arm64 using the existing matching Playwright 1.63.0 launcher (not the locked CI browser versions). Both HTML demos were visually checked and exercised for held/released notes, WAV generation action, and song start/stop. Browser download persistence was not verified. Firefox and remote CI were not run for this migration.\n- Security: independent TsSecurityReview approved static A/B/C/D review with no findings: runtime validation remains beneath erased types, bounded voices/messages/binary input and own-data checks remain, declaration fields do not add runtime initializers, tooling is exactly pinned, and runtime dependencies remain empty. Runtime checks above were executed by 語喵, not the reviewer.\n\n## v1.2.0\n\n### CI failure repairs\n\n- Inspect the first quality run: Node 18/22 passed functional gates but exceeded the unchanged 2.667 ms burst p99 budget (3.205/3.019 ms); Firefox timed out without stage diagnostics.\n- Reduce attack/decay/release envelope exponentiation within each 4x output frame. Re-anchor analytic gain every frame and retain direct evaluation at stage boundaries; validation, eight-voice/eight-fade bounds, workload, and deadline budget remain unchanged. Double-precision rounding can differ from the previous implementation; no universal byte-identity promise.\n- Provide Linux Firefox CI with a running native PulseAudio server and null output sink. Keep real AudioContext/AudioWorklet processing and trusted clicks; add stage, audio-clock/context, and request/error diagnostics rather than autoplay overrides or fake audio.\n- Verification: Node 18.20.8 and 22.23.3 each passed 74 tests, including analytic envelope boundaries and very short stages. Build, public types, AST/security and installed-package ESM/type smoke passed. On Apple M5, the unchanged 300-warmup/2,000-block benchmark passed every p99 gate; burst p99 was 1.499/1.478 ms with zero missed deadlines in those runs. These measurements do not prove the GitHub x64 hosts' performance.\n- Audio comparison covered 387 cases and 2,045,248 channel samples, including burst steals at 8–192 kHz: observed Float32 output matched the previous implementation exactly; source/distribution parity also passed the same cases.\n- Locked Playwright 1.56.1 / Firefox 142.0.1 passed real stereo, held/released notes, routing, suspend/resume, diagnostics and context ownership smoke against built assets on Linux arm64 with PulseAudio. The local host was Ubuntu 26.04 using Playwright's Ubuntu 24.04 arm64 binary override, not the GitHub Ubuntu 24.04 x64 runner. Chromium 153 and WebKit 26.6 smoke also passed on macOS using the existing matching Playwright 1.63.0 launcher. The repaired external CI run has not yet been observed.\n- Security: independent source reviewer CiRepairSecurity approved scoped A/B/C/D checks with no blockers: finite/bounded frame-local envelope computation, unchanged trust boundaries, and native CI audio setup. 語喵 executed the integration checks above; the reviewer did not execute runtime gates.\n\n### Final deadline repair\n\n- The follow-up CI run still exceeded the unchanged burst deadline on Node 22 (p99 3.043 ms, 31 misses). Replace hot-loop base-10 exponentiation with exp(db * ln(10) / 20) without changing envelope stages, re-anchoring, bounds, or the benchmark workload/budget.\n- Fresh verification passed 74 tests on Node 26 and Node 22.23.3, build, public types, AST/installed-package gates, and both npm audits (zero vulnerabilities). Apple M5 / Node 22 burst p99 was 1.009 ms with zero misses against the original 2.667 ms budget; external CI must independently confirm runner performance.\n- Independent reviewer ReleaseExpSecurity approved the final arithmetic change under A/B/C/D with no blockers (static review only). Real Chromium AudioWorklet smoke passed again against rebuilt assets using the cached Playwright 1.63 launcher; remote locked-browser gates remain authoritative.\n\n### Readiness and additive features\n\n- Add borrowed AudioContext ownership, resumable/coalesced initialization, serialized disposal, explicit output routing, held notes, velocity, and stereo pan.\n- Surface worklet note admission/lifecycle/rejection events, diagnostics, and processor errors. Bound tracked IDs/events, reject duplicates before enqueueing, and remove stale off events when notes terminate.\n- Preserve eight logical voices with bounded short steal fades and exactly-once terminal notifications. Unify offline rendering with the live Synth DSP path, including LFO, filtering, saturation, velocity, and pan.\n- Introduce version 2 voices with optional per-operator key scaling; retain legacy version 1 input without new fields. Expand the bundled example bank.\n- Add finite/bounded PCM16 mono/stereo WAV encoding and clean-room approximate six-to-four-operator DX7 SysEx import, with separate conversion descriptions/warnings.\n- Ship TypeScript declarations through conditional package exports and copy their dependent declarations intact during builds. Keep zero runtime dependencies.\n- Add development-only AST security/package gates, an installed-package type consumer, configurable 128-frame deadline statistics, spectral regression coverage, and configured Node/browser CI. Browser tooling is development-only; no fallback hides missing AudioWorklet support.\n- Update English and Traditional Chinese runnable installation, lifecycle, routing, rendering, import/export, and quality instructions; extend the browser demo with key scaling, DX7 preview, and WAV download.\n\n### Verification and release gate\n\n- Integration verification passed 73 tests, the distribution build, strict public-API TypeScript checks, source/built AST security gates, isolated tarball installation exercising ESM and TypeScript consumers, and full/runtime-only npm audits with zero vulnerabilities.\n- Source and minified distribution produced byte-identical stereo PCM and WAV bytes across 189 offline cases (7,944,138 stereo samples), plus 40 live stealing/render blocks.\n- Real Chromium, Firefox, and WebKit AudioWorklet smoke scenarios passed signal/stereo, routing, held-note lifecycle, suspend/resume, diagnostics, and context ownership checks. Local browser downloads for pinned Playwright 1.56.1 did not complete; the unchanged scenarios used an already cached matching Playwright 1.63.0 browser launcher. The pinned CI browser installation and external Node 18/22/24 CI jobs remain configured, not observed runs.\n- The actual demos produced finite audio for key scaling and the expanded presets, previewed an independently generated DX7 fixture, downloaded and decoded a stereo WAV (48 kHz, 39,384 frames), and reported no page errors or rejected notes. Root-demo stop behavior was reproduced and fixed.\n- On Apple M5 / Node 26.7.0, the isolated 48 kHz / 128-frame benchmark passed its p99 deadline budget after 300 warm-up and 2,000 measured blocks per scenario. Burst p99 was 1.347 ms; worst was 3.703 ms with 5 missed 2.667 ms deadlines. A concurrent large offline-render workload exceeded the p99 budget (2.789 ms, 23 misses). These host/load-specific measurements are not a hard real-time guarantee.\n\n### Security review\n\n語喵 coordinated integration verification. Independent source reviewer FinalSecurity approved A/B/C/D after remediation, with no remaining blockers; FinalConsumerReview approved the consumer/lifecycle/DSP review after the reentrant voice-admission fix. Reviewers did not independently execute the runtime gates above; no release tag, publication, or external CI run is claimed.\n\n- A — Untrusted data: approved own-data voice/key-scaling validation, bounded bank/DX7 parsing, framing/checksum/seven-bit checks, and WAV limits. Remediated forged typed-array metadata and inherited WAV-field reads using native kind/length intrinsics and own descriptors; samples are captured, validated, and encoded once. Boundary regressions passed.\n- B — Worklet boundary: approved malformed-message handling, pre-enqueue duplicate/capacity checks, held-note cancellation, lifecycle ID cleanup, diagnostics, and processor-error propagation. Late or repeated note-off is an idempotent no-op, avoiding false rejection after completion/stealing.\n- C — DSP safety: approved eight logical voices/eight bounded fading remnants, terminal callbacks, finite output, LFO/offline parity, stereo gain, key scaling, and deterministic rendering. Admit replacements before notifying stolen callbacks so reentrant admission cannot exceed the voice limit; regression and spectral tests passed. Deadline limitations are recorded above.\n- D — Supply chain: approved zero runtime dependencies, exact esbuild/Terser/Acorn/Playwright/TypeScript development pins and lockfile, pinned CI actions, package whitelist, and installed ESM/type exports. Generated-code/package gates and both npm audits passed.\n\n## v1.1.0\n\nRelease gate: 語喵 coordinated independent security reviewers ReleaseBoundary (A/B) and ReleaseDsp (C/D), both approving their assigned source/security slices with no blockers. Fresh verification passed all 37 tests, full and runtime-only npm audits (zero vulnerabilities), an empty runtime dependency tree, the distribution build, and an isolated v1.1.0 tarball installation exercising public exports, finite stereo PCM, note release, mono rendering, and bank parsing. The browser and differential-audio evidence below was obtained during the preceding optimization/documentation work; runtime code is unchanged since those checks.\n\n### Usage documentation\n\n- Expand README and both usage guides with checkout demos, local-tarball installation without assuming registry publication, complete static browser deployment, and standalone Node.js PCM examples.\n- Clarify lifecycle, scheduling and release, custom voices, bank loading, validation bounds, compressed delivery, and troubleshooting. Distinguish strict single-voice normalization from bank clamping, and document that renderNote() does not apply LFO.\n- Add safe-embedding guidance and usage links in the security/contributor documents. Correct the contributor API diagram and explain rebuilding dist/ before checking source changes in the demos.\n\nDocumentation verification: built and packed an isolated checkout, installed its tarball in a clean application, and executed five Node.js snippets and all three JSON voice examples. Chromium exercised seven extracted HTML examples with real AudioWorklet signal, including custom-voice early release and bundled-bank loading, with no observed browser/worklet errors. Runtime implementation is unchanged.\n\n### Optimization and compact distribution\n\n- Cache real-time sustain gains, routing, subsample offsets, and per-frame pitch steps; replace bounded phase modulo with subtraction and fill idle blocks directly.\n- Prepare offline envelopes once per operator, retaining dB-domain curves and floating-point operation order. Move voice normalization out of the DSP module so the browser facade no longer imports the synthesis implementation.\n- Batch worklet event consumption and compact cancelled events in place, preserving same-frame ordering and the 256-event ceiling. Add an audio regression covering partial consumption, cancellation, and queue refill.\n- Add ready-to-use minified ESM and JSON in dist/, with shared chunks, gzip level 9, and Brotli quality 11 alternatives. Preserve public exports and property names; retain readable sources. npm exports and both demos now use dist/.\n- Add pinned development-only build tools and a repeatable benchmark. The npm whitelist excludes precompressed alternatives, development tooling, tests, and source files to avoid redundant package payloads.\n\n### Measurements and verification\n\nLocal Node.js v26.7.0 measurements using the scripts/benchmark.js workload, three warmups and nine timed runs. Real-time cases render 0.5 seconds at 48 kHz in 128-frame blocks; the offline case includes release and filter tail. Times are machine-dependent.\n\n| Scenario | Original median | Optimized minified median |\n| --- | ---: | ---: |\n| Eight voices with LFO | 84.869 ms | 70.874 ms |\n| Offline note with release | 27.857 ms | 16.774 ms |\n\nOriginal runtime JS/JSON totaled 33,741 bytes. Minified assets total 20,319 bytes; the sums of separately compressed gzip/Brotli variants are 9,014/7,987 bytes. These are asset totals, not npm tarball sizes.\n\nVerification: 37 tests passed against source and the same 37 against minified modules; differential smoke covered 288 scenarios and 1,505,688 samples for each implementation against the original, with byte-identical audio and chunk-independent output. All compressed assets decoded byte-for-byte. Chromium exercised both demo controls and actual AudioWorklet output, including audible signal, finite samples, release to silence, and clean shutdown. An isolated npm-tarball consumer exercised public browser/offline/voice-bank imports without development dependencies.\n\n### Security review\n\nReviewed by 語喵 (AI assistant), with independent security and numerical reviewers, using SECURITY.md A–D. No changed-code blockers were found.\n\n- A — Untrusted data: normalization extraction retains prototype and own-data checks, explicit copying, numeric bounds, and rejection of accessors; voice-bank limits remain unchanged.\n- B — Worklet boundary: shape checks precede payload reads; queue ordering, cancellation, and capacity remain bounded. Source and generated JS scans found no dynamic execution sinks.\n- C — DSP safety: eight-voice ceiling, finite-output safeguards, four-operator/4× processing, headroom, and filter tails remain intact; source/minified differential and malformed-input checks passed.\n- D — Supply chain: runtime dependency tree is empty. esbuild 0.28.2 and Terser 5.51.2 are exact development pins with a lockfile; full and runtime-only npm audits reported zero vulnerabilities. The packed whitelist contains 19 intended assets/docs/legal/metadata entries. Minification disables unsafe math, pure-getter assumptions, and property mangling.\n\n## v1.0.0\n\nFirst stable release of the documented browser and offline-rendering APIs.\n\n- Four-operator FM synthesis with eight algorithms, feedback, ADSR, LFO, detune, and eight-voice polyphony.\n- Browser AudioWorklet API, offline rendering, versioned JSON voice format, and English/Traditional Chinese usage guides.\n- Browser test page with a complete Twinkle, Twinkle, Little Star melody.\n\n### Security review\n\nReviewed by 語喵 (AI assistant) for this release, using the A–D checklist in SECURITY.md:\n\n- A — Untrusted data: voice objects use explicit data-field allowlists and bounded numeric values; JSON voice banks are capped at 256 KiB and 128 voices. File/URL voice-bank loaders are not provided, so path traversal and content-type checks do not apply.\n- B — Worklet boundary: messages are shape-checked, invalid data is ignored, and pending events are capped. Source scan found no dynamic eval, new Function, or import() calls.\n- C — DSP safety: eight active voices maximum; non-finite output is silenced and counted. The deterministic rendering and malformed-input tests passed.\n- D — Supply chain: zero runtime dependencies; no dev dependencies or CI actions; npm audit --omit=dev reported zero vulnerabilities. The package dry run contains only the intended src/, usage docs, changelog, README, license, security policy, and package metadata.\n\nVerification: node --test (36 passing), npm ls --omit=dev --depth=0 (empty), npm audit --omit=dev (zero vulnerabilities), and npm pack --dry-run --json (18 intended entries).\n","href":"./changelog.html"},{"title":"Changelog · Changelog","text":"Changelog Changelog","href":"./changelog.html#changelog"},{"title":"Changelog · Unreleased","text":"Changelog Unreleased","href":"./changelog.html#unreleased"},{"title":"Changelog · Half-band FIR decimation","text":"Changelog Half-band FIR decimation","href":"./changelog.html#half-band-fir-decimation"},{"title":"Changelog · Band-limited square, saw and half-wave operators","text":"Changelog Band-limited square, saw and half-wave operators","href":"./changelog.html#band-limited-square-saw-and-half-wave-operators"},{"title":"Changelog · v1.11.1","text":"Changelog v1.11.1","href":"./changelog.html#v1111"},{"title":"Changelog · Faster sine operators","text":"Changelog Faster sine operators","href":"./changelog.html#faster-sine-operators"},{"title":"Changelog · v1.11","text":"Changelog v1.11","href":"./changelog.html#v111"},{"title":"Changelog · Approximate OPM text import","text":"Changelog Approximate OPM text import","href":"./changelog.html#approximate-opm-text-import"},{"title":"Changelog · MIDI voice selection and bend sensitivity","text":"Changelog MIDI voice selection and bend sensitivity","href":"./changelog.html#midi-voice-selection-and-bend-sensitivity"},{"title":"Changelog · Voice v7 waveforms and held noise","text":"Changelog Voice v7 waveforms and held noise","href":"./changelog.html#voice-v7-waveforms-and-held-noise"},{"title":"Changelog · Optional stereo effects","text":"Changelog Optional stereo effects","href":"./changelog.html#optional-stereo-effects"},{"title":"Changelog · Release hygiene and verification","text":"Changelog Release hygiene and verification","href":"./changelog.html#release-hygiene-and-verification"},{"title":"Changelog · v1.10","text":"Changelog v1.10","href":"./changelog.html#v110"},{"title":"Changelog · Portable music and expressive file exchange","text":"Changelog Portable music and expressive file exchange","href":"./changelog.html#portable-music-and-expressive-file-exchange"},{"title":"Changelog · Acceptance boundaries","text":"Changelog Acceptance boundaries","href":"./changelog.html#acceptance-boundaries"},{"title":"Changelog · Pre-release checkout verification","text":"Changelog Pre-release checkout verification","href":"./changelog.html#pre-release-checkout-verification"},{"title":"Changelog · Release-candidate verification","text":"Changelog Release-candidate verification","href":"./changelog.html#release-candidate-verification"},{"title":"Changelog · v1.9","text":"Changelog v1.9","href":"./changelog.html#v19"},{"title":"Changelog · Integration and musical workflows","text":"Changelog Integration and musical workflows","href":"./changelog.html#integration-and-musical-workflows"},{"title":"Changelog · Documentation and acceptance","text":"Changelog Documentation and acceptance","href":"./changelog.html#documentation-and-acceptance"},{"title":"Changelog · Observed verification and review boundaries","text":"Changelog Observed verification and review boundaries","href":"./changelog.html#observed-verification-and-review-boundaries"},{"title":"Changelog · v1.8.1","text":"Changelog v1.8.1","href":"./changelog.html#v181"},{"title":"Changelog · Documentation and packaging","text":"Changelog Documentation and packaging","href":"./changelog.html#documentation-and-packaging"},{"title":"Changelog · Security and observed release-candidate verification","text":"Changelog Security and observed release-candidate verification","href":"./changelog.html#security-and-observed-release-candidate-verification"},{"title":"Changelog · v1.8","text":"Changelog v1.8","href":"./changelog.html#v18"},{"title":"Changelog · Engine and integration","text":"Changelog Engine and integration","href":"./changelog.html#engine-and-integration"},{"title":"Changelog · Security and observed verification","text":"Changelog Security and observed verification","href":"./changelog.html#security-and-observed-verification"},{"title":"Changelog · GitHub release package and post-release documentation","text":"Changelog GitHub release package and post-release documentation","href":"./changelog.html#github-release-package-and-post-release-documentation"},{"title":"Changelog · v1.7","text":"Changelog v1.7","href":"./changelog.html#v17"},{"title":"Changelog · Musical scheduling, rendering and expressive control","text":"Changelog Musical scheduling, rendering and expressive control","href":"./changelog.html#musical-scheduling-rendering-and-expressive-control"},{"title":"Changelog · Pre-release feature verification","text":"Changelog Pre-release feature verification","href":"./changelog.html#pre-release-feature-verification"},{"title":"Changelog · v1.7 release-candidate verification","text":"Changelog v1.7 release-candidate verification","href":"./changelog.html#v17-release-candidate-verification"},{"title":"Changelog · v1.6","text":"Changelog v1.6","href":"./changelog.html#v16"},{"title":"Changelog · Acoustic quality, expression and adoption","text":"Changelog Acoustic quality, expression and adoption","href":"./changelog.html#acoustic-quality-expression-and-adoption"},{"title":"Changelog · v1.6 verification record","text":"Changelog v1.6 verification record","href":"./changelog.html#v16-verification-record"},{"title":"Changelog · v1.5","text":"Changelog v1.5","href":"./changelog.html#v15"},{"title":"Changelog · Complete expressive playback and lifecycle","text":"Changelog Complete expressive playback and lifecycle","href":"./changelog.html#complete-expressive-playback-and-lifecycle"},{"title":"Changelog · Independent numerical and security acceptance","text":"Changelog Independent numerical and security acceptance","href":"./changelog.html#independent-numerical-and-security-acceptance"},{"title":"Changelog · v1.5 release verification","text":"Changelog v1.5 release verification","href":"./changelog.html#v15-release-verification"},{"title":"Changelog · WebKit interruption repair","text":"Changelog WebKit interruption repair","href":"./changelog.html#webkit-interruption-repair"},{"title":"Changelog · v1.4","text":"Changelog v1.4","href":"./changelog.html#v14"},{"title":"Changelog · Sound quality and realtime acceptance","text":"Changelog Sound quality and realtime acceptance","href":"./changelog.html#sound-quality-and-realtime-acceptance"},{"title":"Changelog · Prepared patches and expressive notes","text":"Changelog Prepared patches and expressive notes","href":"./changelog.html#prepared-patches-and-expressive-notes"},{"title":"Changelog · Audio-clock scheduling and deployment","text":"Changelog Audio-clock scheduling and deployment","href":"./changelog.html#audio-clock-scheduling-and-deployment"},{"title":"Changelog · Preset and DX7 audition","text":"Changelog Preset and DX7 audition","href":"./changelog.html#preset-and-dx7-audition"},{"title":"Changelog · Feature verification before release","text":"Changelog Feature verification before release","href":"./changelog.html#feature-verification-before-release"},{"title":"Changelog · v1.4 release verification","text":"Changelog v1.4 release verification","href":"./changelog.html#v14-release-verification"},{"title":"Changelog · v1.3.0","text":"Changelog v1.3.0","href":"./changelog.html#v130"},{"title":"Changelog · Release verification and security review","text":"Changelog Release verification and security review","href":"./changelog.html#release-verification-and-security-review"},{"title":"Changelog · Detailed English website security policy","text":"Changelog Detailed English website security policy","href":"./changelog.html#detailed-english-website-security-policy"},{"title":"Changelog · Website favicon","text":"Changelog Website favicon","href":"./changelog.html#website-favicon"},{"title":"Changelog · English example catalog","text":"Changelog English example catalog","href":"./changelog.html#english-example-catalog"},{"title":"Changelog · Node.js 22 minimum","text":"Changelog Node.js 22 minimum","href":"./changelog.html#nodejs-22-minimum"},{"title":"Changelog · Complete JavaScript/map/declaration triplets","text":"Changelog Complete JavaScript/map/declaration triplets","href":"./changelog.html#complete-javascriptmapdeclaration-triplets"},{"title":"Changelog · Minification and TypeScript source maps","text":"Changelog Minification and TypeScript source maps","href":"./changelog.html#minification-and-typescript-source-maps"},{"title":"Changelog · JavaScript-only distribution assets","text":"Changelog JavaScript-only distribution assets","href":"./changelog.html#javascript-only-distribution-assets"},{"title":"Changelog · TypeScript source cutover","text":"Changelog TypeScript source cutover","href":"./changelog.html#typescript-source-cutover"},{"title":"Changelog · v1.2.0","text":"Changelog v1.2.0","href":"./changelog.html#v120"},{"title":"Changelog · CI failure repairs","text":"Changelog CI failure repairs","href":"./changelog.html#ci-failure-repairs"},{"title":"Changelog · Final deadline repair","text":"Changelog Final deadline repair","href":"./changelog.html#final-deadline-repair"},{"title":"Changelog · Readiness and additive features","text":"Changelog Readiness and additive features","href":"./changelog.html#readiness-and-additive-features"},{"title":"Changelog · Verification and release gate","text":"Changelog Verification and release gate","href":"./changelog.html#verification-and-release-gate"},{"title":"Changelog · Security review","text":"Changelog Security review","href":"./changelog.html#security-review"},{"title":"Changelog · v1.1.0","text":"Changelog v1.1.0","href":"./changelog.html#v110-1"},{"title":"Changelog · Usage documentation","text":"Changelog Usage documentation","href":"./changelog.html#usage-documentation"},{"title":"Changelog · Optimization and compact distribution","text":"Changelog Optimization and compact distribution","href":"./changelog.html#optimization-and-compact-distribution"},{"title":"Changelog · Measurements and verification","text":"Changelog Measurements and verification","href":"./changelog.html#measurements-and-verification"},{"title":"Changelog · Security review","text":"Changelog Security review","href":"./changelog.html#security-review-1"},{"title":"Changelog · v1.0.0","text":"Changelog v1.0.0","href":"./changelog.html#v100"},{"title":"Changelog · Security review","text":"Changelog Security review","href":"./changelog.html#security-review-2"},{"title":"Acoustic references and decimation","text":"Acoustic references and decimation # Acoustic references and decimation\n\nEvery profile renders operators at an internal multiple of the output rate and decimates with a cascade of original linear-phase half-band FIR filters. Each 2:1 stage has a centre tap of 1/2 and every other tap zero, so it costs one multiply per unique coefficient pair, and it is evaluated in polyphase form: oversampled inputs are only buffered, and the whole cascade runs once per output frame on the last internal substep. The default standard profile uses four-times internal sampling: a 15-tap stage at 4× followed by a 47-tap stage at 2×. Passband is flat to 0.40 times output sample rate; products that would fold below that edge (from 0.60 Fs upward) are rejected by at least 81 dB. Each voice has 158 preallocated state scalars (counter/ring positions, the frame's substep inputs and doubled delay rings); there are no allocations. State resets on admission and all buffered history must drain to zero on release.\n\nBefore this change (v1.11.1 and earlier) the decimator was an eighth-order bilinear Butterworth IIR with cutoff 0.30 output Fs (fourth order for eco). Its tables are kept below for comparison.\n\nConstruction accepts quality: 'eco' | 'standard' | 'high' in SynthOptions, offline render options and OPM options. Quality cannot change on an active synth; OPM retains it across restart. maxVoices accepts 1–32, with default 8; larger budgets are opt-in; the profile tables and the stealing-fade ceiling (eight) are unchanged.\n\n| Profile | Internal rate | Half-band stages (taps) | Design rejection | Group delay | Tradeoff |\n| --- | ---: | --- | ---: | ---: | --- |\n| eco | 2× | 35 | 63 dB | 8.5 frames | Fewer oscillator substeps, earlier internal Nyquist and weaker rejection |\n| standard (default) | 4× | 15 → 47 | 81 dB | 13.25 frames | Acceptance baseline |\n| high | 8× | 11 → 15 → 47 | 81 dB | 13.875 frames | Higher internal Nyquist, twice standard oscillator substeps; not an arbitrary-patch alias-free guarantee |\n\nCoefficients are equiripple (Parks–McClellan) designs using the standard half-band construction: a length-\\((N+1)/2\\) low-pass with band \\([0, 2f_p]\\) is interleaved with the 1/2 centre tap. Band edges, as fractions of each stage's input rate, are 0.20/0.30 for the final 2:1 stage (and eco), 0.10/0.40 for the 4×→2× stage and 0.05/0.45 for the 8×→4× stage. Passband ripple is below 0.001 dB for standard and high and 0.006 dB for eco.\n\n## Response and choice\n\nFor stage input rate \\(R\\) and unique taps \\(g_k\\), each stage's transfer is\n\n\\[\nHs(f)=e^{-i\\omega(2M-1)}\\Bigl(\\tfrac12+2\\sum{k=1}^{M} g_k\\cos\\bigl((2k-1)\\omega\\bigr)\\Bigr),\\qquad \\omega=2\\pi f/R,\n\\]\n\nand the profile's response is the product over its stages, evaluated at the internal rate so source products fold at the internal Nyquist before filtering. The test reference reads the published taps (the design contract) and evaluates this DTFT directly; it does not use the production polyphase rings. Fourier inversion supplies the impulse reference and envelope-transition convolution.\n\nAnalytical response in dB for the standard profile at 48 kHz (rounded; not observed listening results):\n\n| Frequency / output Fs | Former four-pole .20 Fs | Former Butterworth order-8 .30 Fs | Half-band FIR cascade |\n| --- | ---: | ---: | ---: |\n| .10 | -3.84 | approximately 0 | approximately 0 |\n| .20 | -11.90 | -0.006 | +0.001 |\n| .25 | -16.12 | -0.209 | -0.001 |\n| .35 | -23.92 | -11.51 | +0.001 |\n| .40 | — | -21.06 | -0.001 |\n| .45 | — | -29.86 | -0.70 |\n| .50 | -33.52 | -37.90 | -6.05 |\n| .60 | — | -52.29 | -81.39 |\n| .625 | -39.88 | -55.62 | -81.56 |\n| 1.125 | -55.93 | -112.88 | -92.75 |\n| 1.6 | — | -177.26 | -89.35 |\n\nThe FIR keeps the upper octave (e.g. 16.8 kHz at 48 kHz, formerly -11.5 dB) and rejects products that fold into the audible band by about 26–29 dB more than the Butterworth near output Nyquist. Two tradeoffs are deliberate: between 0.50 and 0.60 Fs the transition band lets products fold into 0.40–0.50 Fs (above the flat passband; above 19.2 kHz at 48 kHz but from 17.6 kHz at 44.1 kHz), and far above Nyquist the bilinear Butterworth's zero at the internal Nyquist gave deeper rejection than the FIR's flat 81–93 dB stopband. Neither is a guarantee against arbitrary FM-generated frequencies above the internal Nyquist limit.\n\n## Phase, tails and CPU tradeoffs\n\nThe cascade is linear phase: every frequency is delayed by the same constant, 13.25 output frames for standard (about 276 microseconds at 48 kHz; 8.5 frames for eco, 13.875 for high). The former IIR had a frequency-dependent delay of about 2.7–3.3 frames. Actual output is taken on the last internal substep, which adds a .75-frame phase advance relative to the first substep's clock. Neither value is total AudioContext/device latency. A note released with zero release time therefore still sounds for the filter's delay before its finite history drains.\n\nThe impulse response is finite and symmetric; it has pre- and post-ringing at the transition band rather than the IIR's one-sided ringing. Independent impulse integration gives approximately 1.758 for the absolute impulse sum and 93.16 internal samples for the absolute first moment. Full-feedback gates use these impulse-sum and absolute-moment bounds rather than assuming unit-bounded filter output. Stereo output saturation/headroom remains separate.\n\nWork per output frame is 20 multiplies for standard (twice 4 for the 4×→2× stage, 12 for the final stage) instead of 16 biquads (80 multiplies), 9 for eco and 32 for high. The per-substep call only stores its input, so the synth's voice loop inlines it. Realtime deadline acceptance must still be measured on each host, not inferred from operation counts.\n\n## Coverage and evidence\n\nThe checkout-only maintenance command npm run sound-quality (about four minutes on the development laptop) reports, for each of eco, standard and high:\n\n- Controlled synthesized passband/THD/folded aliases. Standard keeps the original 22.05/44.1/48/96 kHz grid; eco and high run 44.1/48/96 kHz. Measured passband loss is compared with the direct transfer above. In the half-band run at 48 kHz the measured and predicted losses agreed within 0.0001 dB at every checked frequency; at .35 Fs they were −0.0056 dB (eco) and +0.0009 dB (standard and high), where the former Butterworth lost 7.3–11.5 dB. THD of harmonics 2–8 was 0.0208 % for all three.\n- Folded ultrasonic products. Standard (−81.6 dB at .625 Fs, −92.8 dB at 1.125 Fs), high (−81.6 / −93.1 dB) and eco (−93.3 dB at .625 Fs; its 1.125 Fs source exceeds the oscillator's 0.45 internal-rate clamp) all stay below the 30 dB floor, and each is also gated by the direct transfer's prediction so a filter regression is caught even above the floor. The former Butterworth gave −55.6 dB (standard) and −37.4 dB (eco) at .625 Fs.\n- Independent high-index Bessel PM and contractive delayed-feedback references with separate in-band and folded-product budgets at 44.1/48/96 kHz; the worst Bessel coefficient error was 9.0e-10 (eco), 1.0e-9 (standard) and 1.0e-9 (high).\n- Explicit four-operator nested-chain, branched-leaf and two-carrier equations at 44.1/48/96 kHz. Unequal ratios, indices 1.5/4/8, upper/lower registers and independent ADSR attack/decay/held/interrupted-attack/release boundaries detect incorrect routing, modulation depths and carrier normalization. The reference derives envelopes directly in dB and filters by frequency-domain convolution, not by copying the engine loop.\n- A lifecycle/headroom/deterministic-chunk matrix: 2,505 cases over algorithms 0–7 × feedback 0/7 × MIDI 24/60/96 × velocity × polyphony 1/8/16, plus opt-in 32-voice boundary rows (32, 33 and 40 simultaneous notes with maxVoices 32). Every case renders whole and in 127-frame chunks and must be bit-identical, finite, within headroom, audible (velocity above zero), silent after release and report exactly one terminal event per note.\n- Live-control rows (24): for each profile and algorithm, glide, operatorRatios ramp, feedback ramp, operatorADSR, level/expression/pan ramp and fixed-Hz edits are applied at sample boundaries to a note carrying selective AM/PM LFO targets. Whole and chunked renders are bit-identical, finite, bounded and fully terminal. This proves stability, not smoothness to a listener.\n- Streaming: the standard profile keeps the two 120-second deterministic replays; eco and high render 12-second replays with held LFO, interrupted glides and settled-sine residual checks.\n\nThe standalone decimator comparison (actual filter magnitude, complex phase and impulse latency versus independent transfer mathematics and the former filter) remains standard-profile only. A standalone 48 kHz module smoke for that profile observed +0.00085/+0.00092 dB at .20/.35 Fs and -81.55868/-92.75183 dB for the .625/1.125 Fs folded products; decimated impulse energy delay was 12.5 output frames (13.25-frame group delay less the .75-frame substep advance) and its maximum difference from independent Fourier inversion was below 4e-16. No listening session, perceptual preference, arbitrary-patch alias freedom, chip fidelity, or physical-device CPU/underrun result is asserted here.\n\nThe checkout-only scripts/benchmark.ts retains the historical baseline gates and report-only quality/polyphony/multibus comparisons, and adds a full held-note matrix: 1, 4, 8, 16 and 32 voices for each of eco, standard and high. The matrix sets maxVoices equal to the number of held notes. See conservative host capacity candidates below and audio buses. Timing is not realtime acceptance merely because a separate baseline budget is configured; fewer arithmetic operations do not prove perceptual preference or physical-device stability.\n\nHistorical 1.8.0 checkout measurement (not the new capacity matrix): Apple M5, darwin arm64, Node.js 26.7.0, 48 kHz, 128-frame blocks (2.667 ms deadline), 300 excluded warmup blocks and 2,000 measured blocks per row. No explicit acceptance budgets were configured.\n\n| Profile | Median ms | p95 ms | p99 ms | Worst ms | p99 / deadline | Misses |\n| --- | ---: | ---: | ---: | ---: | ---: | ---: |\n| Eco | 0.212375 | 0.231625 | 0.279708 | 0.453166 | 0.105 | 0 / 2,000 |\n| Standard | 0.380333 | 0.423750 | 0.509292 | 0.607917 | 0.191 | 0 / 2,000 |\n| High | 0.680709 | 0.747333 | 0.819917 | 0.981125 | 0.307 | 0 / 2,000 |\n\nAll rows reported zero DSP errors and admission rejections. These are offline host wall-clock measurements, not AudioWorklet underrun counters, allocation measurements, listening judgments or physical-device acceptance. They do not replace the different historical v1.6 host results recorded in CHANGELOG.\n\nEnvelope-transition references compare the waveform through the actual voice-ended frame, then require exact retired silence. The implementation retires a voice once every buffered FIR input is below the state floor rather than waiting for exact zeros. A state floor is not a bound on each future output sample: the independent bound multiplies the floor by each stage's absolute tap sum.\n\n## Conservative host capacity candidates\n\nScope note (v1.11): the benchmark patches use sine operators only. Non-sine operator waveforms (half, abs, quarter, alternating, camel, square, saw) add a per-sample waveform evaluation and noise adds a bounded LFSR step, so patches that use them cost more per voice than the measured candidates indicate; the work stays bounded by the voice limit, but the capacity candidates below were not re-measured for those voices.\n\nThe v1.10 checkout adds this matrix/candidate output through the checkout-only maintenance command; it is not a packaged API or command. From the matching 1.10.0 source checkout, run:\n\nsh\nOPMBENCHCAPACITYP99RATIO=0.5 npm run benchmark\n\n\nThe capacity ratio defaults to 0.5 and may only be made stricter, in (0, 0.5]. This means retaining at least half of the nominal block deadline as observed p99 reserve, not reserving a guaranteed amount of real audio-thread time. A row is eligible only when:\n\n- Its observed p99/deadline ratio is at or below that threshold.\n- It has zero measured deadline misses.\n- Accumulated DSP/processor errors and admission rejections are zero, voice/event limits hold, held-note occupancy matches the authored workload, and the final PCM block of every engine is finite.\n- It is not the idle baseline.\n\nJSON results preserve every measured row, including ineligible rows, raw worst time, miss count, diagnosticIssues and capacity.reasons. capacityGuidance.candidates refer to eligible caseIds and repeat each row's exact workload, p99 reserve, worst time and misses. No candidate is chosen across unrelated workloads, no quality setting changes automatically, and an empty candidate list is a valid result.\n\nEach row records quality, per-engine maxVoices, engineCount, voicesPerEngine, patch ID, workload kind and whether it exercised core rendering or the real processor in a Node host shell. The report records VERSION, Node version, CPU/platform/architecture, rate, 128-frame block/deadline, excluded warmup, measured iterations, exact authored patches and note/dispatch recipes. A historical one-held-note row with maxVoices: 8 is evidence for one active note, not eight-note capacity. Matrix rows are fully occupied; burst rows include note creation, queue splitting and bounded stealing fades. Several-engine rows include sequential independent DSP renders, not browser routing/effects overhead.\n\nThe original six baseline scenarios remain the only timing-gated rows when OPMBENCHP99BUDGETRATIO or OPMBENCHWORSTBUDGETRATIO is set. Existing report-only comparisons and the new matrix remain report-only for these gates; diagnostic failures still cause a failing exit status. Capacity eligibility is independent: passing a configured CI budget does not make a row eligible, and an ineligible timing row alone does not fail an unbudgeted benchmark. Existing OPMBENCHSAMPLERATE, OPMBENCHBLOCKS and OPMBENCH_WARMUP settings control the experiment (defaults 48000, 2000 and 300).\n\nKeep the full report with the matching checkout/commit and record surrounding host load. Repeat under representative contention and sustained thermal load; row order, JIT, scheduler/GC pauses and a finite sample count affect the measurements. Worst time and misses must remain visible even when p99 looks good. The finite check samples the final block, not every output sample in the experiment; functional DSP tests provide separate coverage.\n\nTreat eligible rows as candidate host configurations requiring browser and physical-device verification, not portable voice limits, AudioWorklet underrun counters, mobile safety, or realtime certification. Re-run the application's exact voices, releases, automation, raw/prepared dispatch rate, number of engines and effects graph in its target browser. Then complete physical-device/manual acceptance for interruptions, sustained playback, audible glitches and actual MIDI hardware where relevant. Node timing cannot substitute for a real device or a human listener, and neither candidate eligibility nor CPU measurements establish a perceptual preference between quality profiles.\n\n### Observed checkout measurement\n\nOn Apple M5 / darwin arm64, Node v26.7.0, the pre-release package-1.9.0 checkout was measured at 48000 Hz, 128 frames (2.667 ms deadline), 300 warmup blocks excluded and 2000 measured blocks per row. No build, test suite or browser scenario ran concurrently; surrounding OS load was not controlled. This is one observation, not a measurement of package 1.10.0 or thermal/host-contention acceptance. All 29 rows retained clean diagnostics and zero observed misses. Matrix results:\n\n| Quality | Held voices / configured limit | p99 ms | Worst ms | Observed p99 reserve | Candidate |\n| --- | ---: | ---: | ---: | ---: | --- |\n| eco | 1 / 1 | 0.026 | 0.120 | 99.0% | yes |\n| eco | 4 / 4 | 0.090 | 0.173 | 96.6% | yes |\n| eco | 8 / 8 | 0.176 | 0.269 | 93.4% | yes |\n| eco | 16 / 16 | 0.418 | 0.850 | 84.3% | yes |\n| eco | 32 / 32 | 0.717 | 0.810 | 73.1% | yes |\n| standard | 1 / 1 | 0.042 | 0.063 | 98.4% | yes |\n| standard | 4 / 4 | 0.155 | 0.230 | 94.2% | yes |\n| standard | 8 / 8 | 0.329 | 0.377 | 87.7% | yes |\n| standard | 16 / 16 | 0.593 | 0.649 | 77.8% | yes |\n| standard | 32 / 32 | 1.196 | 1.753 | 55.1% | yes |\n| high | 1 / 1 | 0.073 | 0.104 | 97.3% | yes |\n| high | 4 / 4 | 0.258 | 0.324 | 90.3% | yes |\n| high | 8 / 8 | 0.503 | 0.609 | 81.1% | yes |\n| high | 16 / 16 | 1.044 | 1.253 | 60.9% | yes |\n| high | 32 / 32 | 2.126 | 2.477 | 20.3% | no |\n\nFor this exact held-brass workload, the highest sampled eligible configurations were eco/32, standard/32 and high/16; high/32 failed the 50% p99-reserve criterion despite zero observed misses. Start with the unchanged standard/eight default unless the application needs more voices; test the selected configuration under its actual graph/load rather than deploying the maximum sampled candidate blindly. Raw and prepared burst p99/worst were 0.683/2.162 ms and 0.625/0.676 ms respectively, both with zero misses; these are separate processor workloads, not proof that any 32-voice dispatch workload has the held-note result.\n","href":"./acoustic-quality.html"},{"title":"Acoustic references and decimation · Acoustic references and decimation","text":"Acoustic references and decimation Acoustic references and decimation","href":"./acoustic-quality.html#acoustic-references-and-decimation"},{"title":"Acoustic references and decimation · Response and choice","text":"Acoustic references and decimation Response and choice","href":"./acoustic-quality.html#response-and-choice"},{"title":"Acoustic references and decimation · Phase, tails and CPU tradeoffs","text":"Acoustic references and decimation Phase, tails and CPU tradeoffs","href":"./acoustic-quality.html#phase-tails-and-cpu-tradeoffs"},{"title":"Acoustic references and decimation · Coverage and evidence","text":"Acoustic references and decimation Coverage and evidence","href":"./acoustic-quality.html#coverage-and-evidence"},{"title":"Acoustic references and decimation · Conservative host capacity candidates","text":"Acoustic references and decimation Conservative host capacity candidates","href":"./acoustic-quality.html#conservative-host-capacity-candidates"},{"title":"Acoustic references and decimation · Observed checkout measurement","text":"Acoustic references and decimation Observed checkout measurement","href":"./acoustic-quality.html#observed-checkout-measurement"},{"title":"Adaptive music, tempo curves and the seek contract","text":"Adaptive music, tempo curves and the seek contract # Adaptive music, tempo curves and the seek contract\n\nThis guide covers createArrangement (looping layers and sections that switch on musical boundaries), tempo curves and grid helpers shared by createTransport, and the exact meaning of pause, seek and resume. The v1.10 example 09 (checkout-only) demonstrates the scheduler and portable arrangement save/load. The portable APIs ship in package 1.10.0; build and serve examples/adaptive.html from the matching source checkout to use the example.\n\n## Arrangement\n\nts\nimport { OPM, createArrangement } from 'opm.js';\n\nconst opm = new OPM({ maxVoices: 16 });\nconst arrangement = createArrangement(opm, {\n bpm: 96,\n initialSection: 'explore',\n layers: [\n { name: 'pad', length: 16, voicePriority: 20,\n events: [{ type: 'note', id: 1, beat: 0, duration: 16, note: 48, voice: 'brass' }] },\n { name: 'arp', length: 8, events: [\n { type: 'note', id: 2, beat: 0, duration: 0.5, note: 60 },\n { type: 'note', id: 3, beat: 1, duration: 0.5, note: 67 },\n ] },\n { name: 'lead', length: 8, voicePriority: 100, events: [\n { type: 'note', id: 4, beat: 0, duration: 2, note: 72 },\n ] },\n ],\n sections: [\n { name: 'explore', layers: ['pad', 'arp'] },\n { name: 'combat', layers: ['pad', 'lead'] },\n ],\n});\nawait arrangement.start(); // call from a user gesture\narrangement.switchSection('combat', { fade: 0.8 }); // crossfade; returns committed beat\narrangement.setLayer('arp', true, { quantize: 'beat', fade: 0.4 });\narrangement.setLayerGain('pad', 0.6, { quantize: 'beat', fade: 1 });\n\n\nIn a plain browser, import the served ./opm/api/index.js URL instead of the bare package name and invoke the startup/switch calls from a button handler after deploying the complete tree as in the quick start. This runnable recipe uses the default brass voice; register your own pad/lead patches with loadVoice before using their names. On host teardown, call arrangement.dispose() and await opm.dispose().\n\nRules that make switches musical:\n\n- One global beat grid. Every layer loops with its own length (quarter-note beats, at most 256), aligned to beat 0 of the arrangement. A layer that two sections share is one continuous schedule, so its sounding notes are not retriggered or stopped when other layers change.\n- Commit boundary. switchSection and setLayer commit at the first quantize boundary ('bar' default, 'beat', or a number of quarter notes) that is not earlier than the notes already admitted to the AudioContext clock. The returned value is that beat. With the default 0.2 s lookahead, a switch requested inside the final instants before a bar line lands on the following bar instead of rewriting notes that are already scheduled.\n- Removed layers. Notes that started before the boundary are released at the boundary (their release tail still sounds). Pass preserveNotes: true to let them finish naturally. Notes that would start at or after the boundary are never admitted.\n- Layer gain and crossfades. A layer's gain is 0..1 (default 1). setLayerGain(name, gain, { quantize?, fade? }) returns its committed beat; quantization follows section changes. fade is a linear seconds-based envelope in 0..10 (default 0), not a beat duration. Section/layer changes with a positive fade ramp removed layers to zero and new layers from zero to their current gain target; shared layers are untouched. Removed sustained gates release when the fade finishes unless preserveNotes is true; shorter gates may release naturally during the fade. Zero-fade removal retains the existing audible release-tail behavior; preserveNotes: true with zero fade leaves sounding gates unchanged.\n- Independent expression. Layer gain multiplies authored note expression exactly in DSP, including simultaneous ramps, new onsets midway through a fade and owned release tails. Interrupted gain ramps retarget from their boundary value. The arrangement reserves note-control gain; authored layer events must use expression instead. setLayerGain does not change unrelated layers, notes admitted by another helper, or the engine's global mix gain. Gain targets survive stop; stop restores the initial section.\n- Voice priority. A layer's voicePriority (0–127) is combined with each note's own value by taking the maximum. When the engine is full, lower-priority notes are refused instead of stealing a protected melody. Refusals are counted in snapshot.priorityDrops and do not stop the arrangement; any other rejection does stop it and calls onError.\n- Bounds. At most 16 layers, 32 sections and 65,536 events in total; at most 128 outstanding notes and 256 queued commands. Each lookahead window allows at most 32 loop repetitions per active layer, 65,536 candidate-note inspections and 256 command admissions, including gain controls. At most 256 retained gain changes and 256 pending section changes; excess capacity rejects mutation or stops scheduling through onError. Future automation streams incrementally; terminal notes discard their remaining commands. Loop lengths are 1/1024–256 quarter notes; note onsets lie inside the loop while durations/owned controls may cross a boundary. Names match [A-Za-z0-9_-]{1,64}; layer/section names cannot be duplicated, and every reference must exist. Repeated section members normalize to one membership in first-occurrence order. Options and events are copied as own data; accessors, sparse arrays and unknown fields are rejected.\n- Ownership. Only notes the arrangement admitted are stopped by pause(), stop() or dispose(). The shared OPM and its context are never closed.\n\nsnapshot reports state, position, bar/beat, the current and pending sections and layers, the tempo map and priorityDrops. setTempo(bpm) and setTempoMap(map) replace the tempo from the current beat on; notes that were already admitted keep the audio times they were admitted with (at most one lookahead window).\n\n## Save and reload an interactive arrangement\n\nKeep a definition separate from the live scheduler. parseArrangementProject / serializeArrangementProject, available since v1.10, store a version 1 ArrangementProject containing named complete voices, normalized synthesis settings, tempo/meter, layers (length, events, gain and priority), named sections, and initialSection. This is distinct from the unchanged ScoreProject v1. See the self-contained creation and live replay recipe.\n\nPortable parsing and live creation share a DOM-free validator; neither creates audio or fetches assets. After parsing, create new OPM({ ...project.settings }), clear its implicit brass registry with replaceVoiceBank([]), load each [name, voice] from project.voices, and pass { layers, sections, initialSection, tempoMap, timeSignature } to createArrangement. Only start() inside a user gesture plays it. Loading/replacing must dispose the old owned arrangement first; teardown the old owned synth without closing another component's borrowed context.\n\nThe local current adaptive page accepts arbitrary valid project names and rebuilds its section/layer/gain controls from them. Section switches become the saved initialSection; layer toggles edit that section's membership, and gain/tempo edits update the saved definition. Save definition → Load arrangement JSON → Start restores those requested targets and registered voices/settings, after which switching and fading still work. A pending boundary is saved as its requested target, not its transition timeline. A malformed or oversized file rejects visibly; uploads are bounded before reading. Loading and Reset demo stop previous owned playback and never auto-play. Reset demo discards the imported definition and applies the selected swing.\n\nThe adaptive page owns and closes its own context, with a separate 16% host monitoring gain. It preserves saved synthesis settings and reports unsupported context/sample-rate failures instead of replacing them. A component borrowing a host context must not close it.\n\nSaving is not a snapshot of a live Arrangement. It excludes beat position, queued changes, running fades, active gates, oscillator/envelope/LFO/filter state and release tails. Replay begins at beat 0; no exact DSP-state restore is claimed. Budgets are 8 MiB, 128 named voices / 256 KiB normalized bank, 1–16 layers, at most 32 sections and 65,536 total events. Default tempo/meter/settings are the same as score projects, gain defaults to 1 and priority to 0; authored note-control gain remains reserved.\n\n\n## Tempo curves\n\nTempoPoint is { beat, bpm, curve?, endBpm? }. Without curve (or with 'step') the tempo is constant until the next point, exactly as in 1.7. With curve: 'linear' the BPM changes linearly per beat to the next point's bpm, or to endBpm if given (endBpm is only valid with 'linear'; the last point cannot be linear).\n\nFor a segment starting at beat b0 with bpm0 and slope r = (bpm1 − bpm0) / (b1 − b0), the time to beat b is\n\n$$t(b) = \\int{b0}^{b}\\frac{60}{\\mathrm{bpm}0 + r\\,(x-b0)}\\,dx = \\frac{60}{r}\\ln\\left(1 + \\frac{r\\,(b-b0)}{\\mathrm{bpm}0}\\right)$$\n\n(60·Δb/bpm0 when r = 0), and the inverse is Δb = bpm0 · expm1(r·t / 60) / r. beatsToSeconds and secondsToBeats use these closed forms with log1p and expm1, so ramps convert exactly across segment boundaries. setTempo on a running transport or arrangement truncates any ramp that is under way at the current beat (its endBpm becomes the interpolated BPM) and inserts a new constant tempo there.\n\nLinear maps whose derived slope is not finite are rejected before admission, including subnormal beat intervals that would overflow the BPM-per-beat calculation.\n\n## Grid helpers\n\nImport these grid helpers from opm.js (or the deployed browser api/index.js), not opm.js/core. Beat/second conversion helpers are exported by both entry points.\n\n- quantizeBeat(beat, quantum = 1, mode = 'ceil') with modes floor | ceil | nearest | next (next is strictly later even on an exact boundary).\n- swingBeat(beat, subdivision = 0.5, ratio = 2/3) warps each pair of subdivisions; a ratio of 0.5 is straight and 2/3 a triplet feel. It is monotonic and maps pair boundaries to themselves.\n- swingBeatEvents(events, subdivision, ratio) swings note starts and ends, so adjacent gates keep their order.\n\n## What pause, seek and resume mean\n\ncreateTransport and createArrangement are musical restarts, not DSP checkpoints.\n\n| State | Preserved | Not preserved |\n| --- | --- | --- |\n| Musical position (beats), tempo map, loop | yes | |\n| Transport seek()/loop wrap into a sustained note | the note restarts at the target beat, with pitch, expression, gain, pan, modulation, ratios, feedback, LFO, level and ADSR values reconstructed from the score's controls and remaining ramps | oscillator phase, envelope stage and level, LFO phase, filter and feedback history, any release tail that was sounding |\n| Arrangement pause() / resume() | the beat, effective layer set and current layer gain envelopes; unfinished fades continue after resume | every owned note is released on pause; on resume only notes whose onset lies at or after the resume beat start, so a long pad that was mid-note re-enters at its next onset |\n| Notes admitted by the host outside the transport/arrangement | untouched | |\n\nAn exact snapshot of oscillator phases, envelope state and filters is deliberately not offered: it would couple the public API to private DSP state and make 1.x DSP changes breaking. If a game needs a continuous pad across a scene change, keep that layer in both sections (it is then never interrupted), or keep the pad in a separate engine whose lifetime you control.\nTransport start and every explicit reconstruction use a future audio anchor (startupLead seconds, default min(0.05, horizon / 2), finite 0..10). The musical cursor stays at the destination during this count-in; note durations and loop lengths are not stretched. Arrangement uses the same default lead and freezes its resume cursor during count-in. Lead is delivery headroom, not a guarantee: late/drop failures and missed windows remain errors.\n\n## Verified behavior\n\ntest/arrangement.test.ts in the v1.10 checkout renders through the real AudioWorklet processor. It checks that a pad shared by two sections is admitted once across a bar switch, that a removed layer is released at the exact boundary beat and that preserveNotes lets it finish, that a high-priority melody survives a one-voice engine without failing the arrangement, that a tempo change alters only unadmitted onsets, and that pause schedules nothing. test/transport.test.ts in that checkout checks the logarithmic integral and its inverse to 1e-9 beats. Arrangement-project tests additionally define immutable/canonical parse boundaries, malformed/budget rejection and identical real-worklet PCM after saved-definition replay with section switches and fades; these are automated invariants, not physical device or human listening acceptance.\n","href":"./adaptive-music.html"},{"title":"Adaptive music, tempo curves and the seek contract · Adaptive music, tempo curves and the seek contract","text":"Adaptive music, tempo curves and the seek contract Adaptive music, tempo curves and the seek contract","href":"./adaptive-music.html#adaptive-music-tempo-curves-and-the-seek-contract"},{"title":"Adaptive music, tempo curves and the seek contract · Arrangement","text":"Adaptive music, tempo curves and the seek contract Arrangement","href":"./adaptive-music.html#arrangement"},{"title":"Adaptive music, tempo curves and the seek contract · Save and reload an interactive arrangement","text":"Adaptive music, tempo curves and the seek contract Save and reload an interactive arrangement","href":"./adaptive-music.html#save-and-reload-an-interactive-arrangement"},{"title":"Adaptive music, tempo curves and the seek contract · Tempo curves","text":"Adaptive music, tempo curves and the seek contract Tempo curves","href":"./adaptive-music.html#tempo-curves"},{"title":"Adaptive music, tempo curves and the seek contract · Grid helpers","text":"Adaptive music, tempo curves and the seek contract Grid helpers","href":"./adaptive-music.html#grid-helpers"},{"title":"Adaptive music, tempo curves and the seek contract · What pause, seek and resume mean","text":"Adaptive music, tempo curves and the seek contract What pause, seek and resume mean","href":"./adaptive-music.html#what-pause-seek-and-resume-mean"},{"title":"Adaptive music, tempo curves and the seek contract · Verified behavior","text":"Adaptive music, tempo curves and the seek contract Verified behavior","href":"./adaptive-music.html#verified-behavior"},{"title":"Polyphony budgets, voice priority and independent buses","text":"Polyphony budgets, voice priority and independent buses # Polyphony budgets, voice priority and independent buses\n\nOPM.js has two ways to get more than eight simultaneous sounds, and they cost and sound different. Example 11 (v1.10 source, checkout-only) demonstrates independent buses; run examples/buses.html locally from the matching checkout.\n\n## One engine, more voices\n\nnew OPM({ maxVoices }) (and Synth, renderSequence, the Worker renderer) accepts 1–32 logical voices. The default stays 8. Everything else is unchanged:\n\n- Released notes still occupy a voice until their tail ends, so 32 voices means 32 sounding or releasing notes.\n- The stealing fade is still at most eight ~5 ms fades. DSP voice state is preallocated for maxVoices + 9 slots. Trusted prepared-patch admission reuses that state; raw patches require validation and snapshot allocation. This is not a global zero-allocation promise for host APIs or command dispatch.\n- One output tanh stage and one mixGain apply to the sum of all voices.\n\n### Voice priority\n\nEach note may carry voicePriority, an integer 0–127 (default 0), in playNote, score notes (SequenceEvent, beat events), Synth.noteOn options, renderNote and Performance parts.\n\nWhen the engine is full, an incoming note chooses its victim only among sounding voices with priority less than or equal to its own: the lowest priority first, then the engine's stealing policy (oldest, release-first, quietest) among equals. If every voice outranks it, the note is refused:\n\n- Synth.noteOn throws VoiceAdmissionError before changing any state or consuming an ID;\n- the AudioWorklet reports rejected with reason priority (not counted as a processor error);\n- offline score rendering skips the note and keeps going.\n\nUse it for protected melodies, bass lines and stingers; do not use it to hide an undersized maxVoices, since a protected sound can still fall silent if protected sounds alone fill the engine.\n\n### What it costs\n\nThe checkout-only maintenance command npm run benchmark renders 128-frame blocks of the real DSP. The v1.10 checkout adds exact workload metadata and conservative candidate configurations, using observed p99 ≤ half the block deadline by default, zero measured misses and clean diagnostics (criteria and report fields). Run the matching 1.10.0 source checkout; this command and its reports are not packaged APIs. The full matrix covers 1/4/8/16/32 held voices for every quality profile; raw/prepared eight-start-per-block bursts and two/four independent eight-voice engines remain separate measured workloads. This does not measure browser effects graphs or AudioWorklet underruns.\n\nThe historical 1.8.0 scaling run below was taken on an Apple M5 (Node 26.7.0, 48 kHz, 2.667 ms block deadline) on a laptop whose load average was about 24 from unrelated processes, so it is not a capacity recommendation and the CI budget gate was not applied. It illustrates how cost grows with sounding voices; these relative medians are not portable scaling constants.\n\n| Scenario (median block time ÷ deadline) | Median | Relative to 8 voices |\n| --- | --- | --- |\n| 8 voices, standard | 0.65 | 1.0× |\n| 16 voices, standard (opt-in) | 1.19 | 1.8× |\n| 32 voices, standard (opt-in) | 2.84 | 4.4× |\n| 32 voices, eco (opt-in) | 1.63 | 2.5× |\n| 8 voices, eco / high | 0.31 / 1.00 | 0.5× / 1.5× |\n| 2 engines × 8 voices | 1.29 | 2.0× |\n| 4 engines × 8 voices | 2.26 | 3.5× |\n\nUnder that load even the eight-voice case missed its block deadline at p99 (5.7× in this run; the median stayed at 0.65×). For contrast, the historical unloaded run of the same host class measured the eight-voice standard p99 at 0.19× the deadline (acoustic quality). Neither historical table selects a current capacity candidate. Measure again on the target host, retain all rows including worst times/misses and compare configurations within the same workload. An eligible steady eight-voice row does not establish eligibility for bursts, 32 voices, several buses, different patches or effects.\n\nUse capacityGuidance.candidates to identify exact quality/per-engine maxVoices/engineCount/voicesPerEngine combinations worth verifying, then run the matching application in the target browser and on physical devices under sustained load and interruptions. OPMBENCHCAPACITYP99RATIO defaults to 0.5 and can only tighten selection. The independent OPMBENCHP99BUDGETRATIO and OPMBENCHWORSTBUDGETRATIO retain the original six baseline CI timing gates; comparison/matrix rows remain report-only. No command automatically lowers quality or claims realtime certification. Physical mobile acceptance must be recorded separately; Node host results cannot supply it. Lower voices, shorter tails, fewer buses or eco are configurations to measure and audition, not guaranteed safe fallbacks.\n\n## Several engines on one AudioContext\n\nCreate one OPM per bus and give each the same borrowed context and its own destination node:\n\nts\nconst context = new AudioContext();\nconst lead = new OPM({ context, destination: leadGain, maxVoices: 6 });\nconst pad = new OPM({ context, destination: padFilter, maxVoices: 12 });\nawait Promise.all([lead.start(), pad.start()]); // from a user gesture\n\n\n- Each engine has its own voice budget, mixGain, quality profile and saturation. Web Audio then adds the buses linearly.\n- OPM disconnects only its own node and never closes or suspends a borrowed context. Close the context yourself after disposing every engine.\n- Each engine is a separate AudioWorklet processor; more buses mean more per-block overhead and more CPU (see the table). Share a Transport or Arrangement per engine; they own only their own notes.\n- Optional createEffects() supplies stereo chorus/reverb as a separate AudioWorklet insert; sidechains, metering and mixing remain host-owned. The package still ships no mixer.\n\n### Stems are not a monolithic mix\n\nA single engine saturates the sum of all its voices once. Several engines saturate each stem and then add them, so their sum is a different, equally valid mix. It is not bit-identical to rendering all notes in one engine, and it can exceed full scale if each stem is near its limit. The example's offline \"linear bus mix\" clips to ±1 and reports its peak; lower the stem gains for dense material.\n\nFor offline stems render each bus separately with renderSequence (or the Worker renderer), keeping the same sampleRate, maxVoices and mixGain you use live. A shared start frame keeps stems aligned.\n\n## Optional stereo chorus and reverb\n\nts\nimport { OPM, createEffects } from 'opm.js';\nconst context = new AudioContext();\nconst opm = new OPM({ context, destination: null });\nconst fx = await createEffects(context, {\n params: {\n chorus: { rate: 0.8, depth: 0.6, mix: 0.3 },\n reverb: { size: 0.6, damping: 0.5, mix: 0.25 },\n },\n onEvent: event => console.error(event.error),\n});\nawait opm.start(); // user gesture; starts/resumes synthesis\nopm.node!.connect(fx.input);\nfx.output.connect(context.destination);\n// Replaces the complete configuration, smoothly fading omitted sections to bypass:\nfx.update({ reverb: { size: 0.5, damping: 0.7, mix: 0.2 } });\n// On teardown: fx.dispose(); await opm.dispose(); then close your own context.\n\n\ncreateEffects(context, { workletUrl?, params?, onEvent? }) never starts, suspends or closes the borrowed BaseAudioContext, and never automatically connects its output. input and output are the same stereo-in/stereo-out node; ready: Promise\u003cvoid> resolves at creation, not playback. reset() clears state; dispose() is idempotent and stops the processor. Module URLs follow OPM's secure same-origin relocation rules; default is ../worklet/fx-processor.js relative to the API module. Deploy the entire matching module tree, with JavaScript MIME, CSP and nosniff intact.\n\nOffline, import createStereoEffects(sampleRate, options?) or applyEffects(left, right, sampleRate, params) from opm.js/core (also exported at root). The engine accepts integer 8000–192000 Hz. process(left, right, offset?, length?) edits native Float32 buffers in place with no temporary DSP allocation. applyEffects returns new stereo copies extended by ceil(tailSeconds sampleRate); source stereo lengths must match and the resulting length must not exceed 4,000,000 frames per channel*.\n\n| Section | Parameters |\n| --- | --- |\n| chorus | Required rate 0.05–10 Hz, depth 0–1, mix 0–1; optional feedback 0–0.7 (default 0), integer voices 1–4 (default 2) |\n| reverb | Required size, damping, mix 0–1; optional preDelay 0–0.1 seconds (default 0), width 0–1 (default 1) |\n| order | chorus-reverb (default) or reverb-chorus |\n\nOptions must be strict plain own-data objects: unknown fields, accessors, missing required fields and non-finite/out-of-range numbers reject. params and its sections are frozen snapshots. update() replaces all options with a 20 ms linear parameter ramp; order changes fade to dry before switching and fade back. reset() deterministically clears all delay lines, filters and LFO state and applies the target parameters immediately.\n\nThe original fractional-delay chorus uses a 10 ms base delay with up to ±8 ms sine modulation and phase-spread voices. The original seconds-scaled Schroeder network uses four parallel damped combs and two series all-pass diffusers per channel; it makes no Yamaha/YM2151 or other hardware-fidelity claim. size sets nominal comb T60 to 0.2 + 3 * size seconds; damping low-passes feedback. Width crossfades mono and stereo wet sound.\n\nThe gain law is linear dry/wet crossfade, dry (1 - mix) + wet mix, not equal-power summation. In steady state the wet output has a 0.7 ceiling for unity-bounded input (at least 3 dB headroom): chorus averages voices and compensates feedback by 1 - feedback; comb injection uses 1 - loopGain, and each all-pass is normalized by its worst-case gain of 3. The ceiling is a steady-state property: lowering chorus feedback through update() ramps over 20 ms while the delay line still holds content written at the old, higher feedback, so a resonant input can briefly exceed 0.7 (output stays finite and clamped). Dry output is not attenuated at mix 0. Omitted sections and exact zero mix are bit-transparent for finite input, including signed zero. Host sums can still clip; arbitrary over-unity input is not a mastering limiter, and float32 WAV export of effected audio should be checked or limited by the host.\n\nAll delay state is preallocated: delay samples are at most ceil(0.563 * sampleRate) + 32 Float64 values (plus 52 control/filter values), under 866 KiB at 192 kHz. Processing clamps internal values to ±1,000,000, replaces NaN/Infinity input with zero and flushes magnitudes below 1e-20; malformed PCM cannot permanently poison state. Finite dry bypass remains untouched. Output and reset are deterministic and processing is block-size independent.\n\ntailSeconds is a conservative −80 dB audible-tail bound, not a hard truncation: reverb reserves 0.3 + 3 * T60 seconds and chorus reserves a feedback-dependent bound based on its maximum delay. Cascaded bounds add; updates retain the largest previous bound until reset so old tails remain covered. The bounded worklet accepts at most 32 commands per audio frame boundary, stores no command queue, reports validation/rate failures and keeps close available during flooding. Pace host updates; do not flood the MessagePort.\n\n\n## Choosing\n\n| Need | Use |\n| --- | --- |\n| A melody that accompaniment must not steal | voicePriority on the melody, one engine |\n| More than eight sounding notes, one tonal group | maxVoices up to 32; measure CPU |\n| Separate effects, ducking or stems per instrument group | One engine per bus |\n| Reduce DSP work before target-device measurements | Fewer voices, shorter releases, fewer buses; audition eco |\n","href":"./audio-buses.html"},{"title":"Polyphony budgets, voice priority and independent buses · Polyphony budgets, voice priority and independent buses","text":"Polyphony budgets, voice priority and independent buses Polyphony budgets, voice priority and independent buses","href":"./audio-buses.html#polyphony-budgets-voice-priority-and-independent-buses"},{"title":"Polyphony budgets, voice priority and independent buses · One engine, more voices","text":"Polyphony budgets, voice priority and independent buses One engine, more voices","href":"./audio-buses.html#one-engine-more-voices"},{"title":"Polyphony budgets, voice priority and independent buses · Voice priority","text":"Polyphony budgets, voice priority and independent buses Voice priority","href":"./audio-buses.html#voice-priority"},{"title":"Polyphony budgets, voice priority and independent buses · What it costs","text":"Polyphony budgets, voice priority and independent buses What it costs","href":"./audio-buses.html#what-it-costs"},{"title":"Polyphony budgets, voice priority and independent buses · Several engines on one AudioContext","text":"Polyphony budgets, voice priority and independent buses Several engines on one AudioContext","href":"./audio-buses.html#several-engines-on-one-audiocontext"},{"title":"Polyphony budgets, voice priority and independent buses · Stems are not a monolithic mix","text":"Polyphony budgets, voice priority and independent buses Stems are not a monolithic mix","href":"./audio-buses.html#stems-are-not-a-monolithic-mix"},{"title":"Polyphony budgets, voice priority and independent buses · Optional stereo chorus and reverb","text":"Polyphony budgets, voice priority and independent buses Optional stereo chorus and reverb","href":"./audio-buses.html#optional-stereo-chorus-and-reverb"},{"title":"Polyphony budgets, voice priority and independent buses · Choosing","text":"Polyphony budgets, voice priority and independent buses Choosing","href":"./audio-buses.html#choosing"},{"title":"Compatibility policy","text":"Compatibility policy # Compatibility policy\n\nThis document states what OPM.js 1.x promises, what it does not, and how platform support is described. It is a maintenance policy, not a warranty. Evidence of what has actually been exercised lives in the CHANGELOG.\n\n## Versioning\n\nOPM.js follows semantic versioning for the surfaces below. Within 1.x:\n\n- Patch releases fix defects. They may change rendered PCM only to correct documented-incorrect behavior, and the CHANGELOG says so.\n- Minor releases add backward-compatible API, optional voice fields, new example pages and tooling. Default behavior is preserved: with default options, the same voice, notes, sample rate and quality profile render bit-identical PCM to the previous minor. Release 1.8.0 was checked against the 1.7.0 build over 279 renders (all bundled voices × three quality profiles × three registers × two sample rates, plus dense scores with all three stealing policies); see the CHANGELOG.\n- Major releases may remove deprecated API or change defaults. Deprecations are announced in the CHANGELOG at least one minor release before removal. No public API is currently deprecated.\n\n## Public surface\n\n| Surface | Promise |\n| --- | --- |\n| opm.js (browser API), opm.js/core (offline DSP), opm.js/midi-file (SMF adapter), opm.js/voices/schema.js, opm.js/voices/normalize.js, opm.js/voices/dx7.js, opm.js/voices/*.js (banks and metadata), opm.js/tools/assets.js | Documented exports, option names, event shapes and error classes are stable in 1.x. Added options are optional. Browser-only calls require their documented environment even when importing on Node is safe. |\n| opm-assets command | copy \u003cdestination> and check \u003cbase-url> keep their arguments and one-line JSON result; new subcommands may be added. |\n| Voice JSON | Canonical version 7. Versions 1–6 normalize to version 7 for all of 1.x with their original restrictions. See below. |\n| Score / Arrangement projects | Each project has its own explicit format version. Parsing is inert, bounded and detached; serialization saves musical definitions and synthesis settings, not DSP state, permissions, timers or external effects. Newer project formats may reject in older runtimes. |\n| dist/ file layout | Deploy the complete matching tree. Relative paths between modules, the AudioWorklet processor and the Worker are an implementation detail, but the tree is released as one unit. |\n| Generated declarations | Types describe the public API. Types marked @internal are stripped from declarations and may change at any time. |\n\nNot public: the AudioWorklet message protocol and Worker protocol (versioned internally, validated strictly, subject to change in any release), module-private helpers that are not exported from the entry points above, source-map contents, and the exact wording of error messages (match on error class/name, CommandRejectedError.event and event fields instead).\n\n## Voice format\n\n- Version 7 is canonical. Exported banks are always version 7.\n- Legacy versions 1–6 are accepted for the whole 1.x line with their original shapes: v1 excludes key scaling, v1/2 velocity sensitivity, v1–3 LFO waveform, v1–4 expressive fields, v1–5 per-operator LFO targets, and v1–6 operator waveform/noise rate.\n- A new voice field requires a new version. Validation rejects unknown fields, so a voice written for a newer minor version may fail to load in an older runtime. Forward compatibility is not promised; backward compatibility is.\n- Curated usage metadata (suggested register, velocity, polyphony, trim) is kept in src/voices/preset-metadata.ts (checkout-only), outside the voice schema, and is never part of the exported voice JSON.\n\n## Sound compatibility\n\nPCM is deterministic for identical inputs, sample rate and quality profile, independent of render chunking. It is not promised to be identical across minor versions when a new feature is used (for example voicePriority, or a 32-voice engine that no longer steals). Features that need new option values never change the output of calls that do not pass them.\n\nTwo facts matter when planning mixes:\n\n- The eco, standard (default) and high profiles intentionally sound slightly different. Switching profile is a sound change.\n- Every engine instance applies one output tanh stage. Splitting a mix across several OPM instances therefore does not reproduce a single-engine mix bit for bit. See audio buses.\n\n## Platform support\n\n| Platform | Statement |\n| --- | --- |\n| Node.js | 22 or newer for offline rendering, tests, tooling and the opm-assets command. |\n| Browsers | Need ES modules and AudioWorklet on a secure context (HTTPS or localhost). Module Workers are required only for Worker WAV export. Web MIDI is optional and feature-detected. |\n| Desktop CI | The repository's workflow runs real AudioWorklet smoke and stress against Chromium, Firefox and WebKit. A configured workflow is a capability; consult the release notes for runs that actually passed. |\n| iOS Safari, Android Chrome | Unverified on physical hardware. The mobile acceptance workflow and checkout-only npm run device-evidence exist so a maintainer can review real captures; none is supplied. |\n| Web MIDI | Availability depends on the browser and a user permission prompt. The package never requests access on import and never requests SysEx. |\n| Physical MIDI controllers | Unverified on physical hardware. Injected MIDI objects and desktop automation verify software paths, not a browser permission prompt, a real pedal or hot-unplug recovery. |\n| Preset listening quality | Unverified by human listeners. Numerical safety, RMS matching and automated playback do not establish musical preference, perceived loudness or a listening-approved trim. |\n\nSupport for a platform means \"no known defect and the documented checks were run\", never a certification for every device, OS version or audio route.\n\n## Resource bounds\n\nLimits (note and slot counts, bank size, tempo points, layer and section counts, held keys) are part of the validation contract. Raising a limit is a minor change; lowering one is a major change. CPU cost is not bounded by contract: maxVoices above 8, additional buses and the high profile all trade CPU for polyphony or fidelity. Measure on target devices.\n\nBenchmark capacity candidates describe only their recorded host, runtime, patch and workload. They are not portable device limits or an AudioWorklet underrun measurement. Quality and voice budgets never change automatically; choosing another quality is an explicit sound change. See measured capacity guidance and the cross-feature contracts.\n\n## Reporting breaks\n\nA change that makes a documented call stop working, or changes default PCM within a minor release, is a bug. Report it with the two versions, the call and a minimal score or voice. Security-relevant reports follow SECURITY.md.\n","href":"./compatibility.html"},{"title":"Compatibility policy · Compatibility policy","text":"Compatibility policy Compatibility policy","href":"./compatibility.html#compatibility-policy"},{"title":"Compatibility policy · Versioning","text":"Compatibility policy Versioning","href":"./compatibility.html#versioning"},{"title":"Compatibility policy · Public surface","text":"Compatibility policy Public surface","href":"./compatibility.html#public-surface"},{"title":"Compatibility policy · Voice format","text":"Compatibility policy Voice format","href":"./compatibility.html#voice-format"},{"title":"Compatibility policy · Sound compatibility","text":"Compatibility policy Sound compatibility","href":"./compatibility.html#sound-compatibility"},{"title":"Compatibility policy · Platform support","text":"Compatibility policy Platform support","href":"./compatibility.html#platform-support"},{"title":"Compatibility policy · Resource bounds","text":"Compatibility policy Resource bounds","href":"./compatibility.html#resource-bounds"},{"title":"Compatibility policy · Reporting breaks","text":"Compatibility policy Reporting breaks","href":"./compatibility.html#reporting-breaks"},{"title":"Voice v7 and expressive controls","text":"Voice v7 and expressive controls # Voice v7 and expressive controls\n\nVoice format version 7 is independent of the npm package version. normalizeVoice accepts an omitted version as v7; explicit v1–v6 retain their original field restrictions. Normalized, prepared, imported and bank voices emit v7. Canonical bank entries require version, name, algorithm, feedback, modIndex, lfo, and exactly four ops; the exported JSON schema describes that canonical shape, including numeric LFO target tuples.\n\nInputs must use plain objects and dense own-data operator/target arrays. Unknown fields, accessors, malformed enums, nonfinite numbers and unsupported versions are rejected without evaluating getters. normalizeVoice rejects out-of-range numbers; the bank-oriented validateVoice/parseVoiceBank clamp finite numeric values to the documented bounds. Integer selectors and MIDI breakpoints must still be integers. LFO target tuples are detached and frozen even in normalized voices. prepareVoice and bank validation return detached, deeply frozen snapshots, including the optional pitch envelope. A cloned prepared patch must be validated again; a copied shape does not confer trusted identity.\n\n## Patch fields\n\n| Field | Bounds / meaning |\n| --- | --- |\n| algorithm, feedback | Integers 0–7; algorithm selects the four-op graph. Feedback acts on operator zero. |\n| name | 1–64 ASCII letters, digits, _, -; optional for single-voice input. |\n| modIndex | 0–16 radians per unit modulator signal; single-input default 4. |\n| Operator ratio | 0.125–32; required even with fixed Hz. |\n| Operator level | 0–1 before envelope, key/velocity attenuation and live operator multiplier. |\n| Operator detune | −1200–1200 cents. |\n| Operator adsr | Required {a,d,s,r}: attack/decay/release 0–10 seconds, sustain gain 0–1. Ramps are linear in dB; the −96 dB floor becomes silence. |\n| Operator keyScale | Optional {breakpoint,leftDbPerOctave,rightDbPerOctave}: breakpoint integer MIDI 0–127, each attenuation 0–24 dB/octave. |\n| Operator velocitySensitivity | Optional 0–48 dB attenuation at velocity zero; default zero. Final note velocity gain remains independent. |\n| Operator frequency | Optional 1–20000 Hz, replacing tuned note frequency × ratio. Detune, live pitch/glide, pitch envelope and LFO pitch modulation still apply. Changing MIDI note or tuning does not transpose fixed Hz, but optional key/rate scaling still responds to the played note. |\n| Operator rateKeyScale | Optional 0–4; default zero. Each ADSR duration becomes min(10, seconds 2 (-rateKeyScale (note - 60) / 12)). Fractional notes interpolate continuously; zero durations stay zero. Durations are prepared at admission and do not follow live pitch bends or tuning changes. |\n| Operator waveform | Optional v7 sine (default), half, abs, quarter, alternating, camel, square, saw, noise. |\n| Operator noiseRate | Optional v7 20–20000 Hz, default 8000. Ignored unless waveform is noise. |\n\nExplicit v1 rejects key scaling and velocity sensitivity; v2 adds key scaling; v3 adds velocity sensitivity; v4 adds LFO waveform. All explicit v1–v4 reject frequency, rateKeyScale, pitchEnvelope, and LFO delay/sync/phase, rather than silently dropping them. V5 adds those expressive fields, represented by LegacyVoiceV5 and LegacyLFOV5. All explicit v1–v5 reject LFO amTargets/pmTargets. V6 adds those per-operator targets; leaving them absent preserves the legacy all-operator modulation behavior without adding default fields to snapshots.\n\n### Oscillator shapes and noise\n\nFor θ = phase + modulation and fractional turns t = frac(θ / (2π)), the periodic shapes are:\n\n| Shape | Formula |\n| --- | --- |\n| sine | sin(θ); the unchanged legacy fast path |\n| half | max(0, sin(θ)) |\n| abs | abs(sin(θ)) |\n| quarter | abs(sin(θ)) when frac(θ/π) \u003c 0.5, otherwise zero |\n| alternating | sin(2θ) when t \u003c 0.5, otherwise zero |\n| camel | abs(sin(2θ)) when t \u003c 0.5, otherwise zero |\n| square | −1 for negative sine, +1 otherwise (including zero); polyBLEP-corrected in Synth |\n| saw | 2 * frac(t + 0.5) - 1; rising, zero at θ = 0; polyBLEP-corrected in Synth |\n\nPhase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Non-sine shapes evaluate the same deterministic polynomial sine as sine operators. Synth smooths the square and saw jumps with two-sample polyBLEP residuals and the two half-wave corners with polyBLAMP residuals, sized from each operator's unmodulated phase step at the oversampled rate. This lowers folded aliases of unmodulated carriers (about 8–17 dB at the internal rate in the unit tests, before decimation); under phase modulation the nominal step is only an approximation. Quarter, alternating and camel remain naive, and held noise is never band-limited. None of these oscillators is alias-free, even with the per-voice oversampling/decimator.\n\nNoise is an original deterministic 17-bit maximal-length LFSR (x^17+x^3+1), seeded with 0x1ffff at every note admission, including pooled slot reuse. The low bit produces bipolar ±1 and advances on the noiseRate time-domain hold clock, independent of output sample rate. Identical admissions reproduce the sequence. Ratio, fixed frequency, detune, tuning, pitch envelopes, live pitch and modulator PM do not pitch noise; existing operatorRatios/operatorFrequencies controls remain valid but have no audible pitch effect on it. Envelope, key/rate scaling, velocity, level, AM and graph routing still apply; noise can be either a modulator or carrier.\n\nThis OPM-NE-inspired musical extension allows noise on any operator of any voice, unlike hardware restricted to the last operator of channel 8. It is not a Yamaha/YM2151 fidelity claim. Explicit v1–v6 reject operator waveform or noiseRate with a version-7 requirement; LegacyVoiceV6 preserves the prior shape. Prepared snapshots, bank JSON, content-keyed registration and worklet validation retain both new fields.\n\n\n### Pitch envelope\n\nOptional pitchEnvelope requires all seven own-data fields:\n\nts\npitchEnvelope: {\n a: 0.02, d: 0.15, r: 0.1,\n initial: -300, peak: 100, sustain: 0, final: -100\n}\n\n\na, d, r are 0–10 seconds. initial, peak, sustain, final are −4800–4800 cents. Held notes move linearly in cents initial → peak → sustain. A zero attack skips directly to peak; a zero decay skips directly to sustain. Release moves from the current cents value to final, even during attack/decay; zero release immediately selects final. Final pitch persists while operator releases remain audible. The pitch envelope does not prolong otherwise silent operators or filter tails.\n\nPitch cents combine multiplicatively with original operator Hz, detune, live semitone pitch/glide and LFO PM. Pitch-envelope and glide evaluation use deterministic synthesis substeps for the selected quality profile: standard 4× (default), eco 2×, high 8×. Splitting render into different buffer sizes does not change the timeline. Operator phase increments are limited to 45% of the internal sample rate before/after LFO PM, preserving the engine's existing high-frequency safety cap.\n\n### LFO\n\nlfo requires rate (0–20 Hz), amDepth (0–1) and pmDepth (0–1200 cents); single-input omission disables modulation. waveform defaults to sine, with triangle, rising saw, and square also supported.\n\n- delay: optional 0–10 seconds, default zero. Gates both AM and PM depth until that note's elapsed time reaches delay; the phase clock continues during delay. This is a depth gate, not a gradual fade-in.\n- sync: note (default) resets the clock at note admission. global uses Synth.currentFrame, including rendered silence, so separately admitted notes share a deterministic frame clock. Neither uses wall time. Panic does not rewind the global clock.\n- phase: optional 0–1 turns, default zero; 1 wraps to 0. At rate zero this can produce static AM/PM offset.\n- amTargets, pmTargets: optional readonly four-element tuples in operator order, each entry a finite 0–1 depth multiplier or a boolean (false → 0, true → 1). Normalization stores immutable numeric LFOTargets; LFOInput accepts LFOTargetsInput. Each omitted tuple acts as [1, 1, 1, 1], preserving the old sound. Zero disables that modulation for the selected operator; fractional weights reduce it independently of operator level. Dense own-data elements are required; getters, holes, extra fields and malformed values reject.\n\nLFO values are evaluated once per output frame. Sine and triangle start at zero rising, saw at −1, square at +1. For operator i, AM multiplies gain by 1 - amDepth amTargets[i] (0.5 + 0.5 waveformValue); PM multiplies pitch by 2 (pmDepth pmTargets[i] * waveformValue / 1200). Missing targets use 1 in these formulas. Delay, sync and phase remain shared by all four operators.\n\n## Live operator levels\n\nSynth.updateNote, host updateNote, scheduled controls, and sequence automation accept:\n\nts\n{ operatorLevels: [1, 0.4, 1.5, 0], ramp: 0.1 }\n\n\nThe readonly tuple must contain exactly four finite own-data numbers in 0–2. Each number multiplies its patch operator level after key/velocity scaling and before FM routing/feedback. All default to 1. A multiplier cannot revive a patch operator whose level is zero. Modulator levels change timbre; carrier levels change that carrier's contribution. Final expression/velocity and equal-power pan remain separate controls.\n\nramp is 0–10 seconds, default zero. The four multipliers ramp linearly and independently from their current values using the same supplied duration. An interrupted ramp starts from its current value, not the old target. Unrelated expression, pan, modulation or pitch updates do not cancel operator ramps. Validation detaches and freezes the tuple and its outer control record before scheduling. Recycled/stealing voice slots preserve current tails and fully reset controls for new admissions; render processing allocates no arrays or objects.\n\n## Live timbre and envelope controls\n\nThe same note-control APIs accept these optional fields independently:\n\n| Control | Bounds / meaning |\n| --- | --- |\n| feedback | Finite 0–7 continuous feedback selector. Between 0 and 1, gain interpolates from zero to the classic selector-1 gain; 1–7 follows the original exponential gain mapping. Patch feedback remains an integer. |\n| lfoRate, amDepth, pmDepth | 0–20 Hz, 0–1, 0–1200 cents respectively; update the admitted note's LFO independently. |\n| operatorRatios | Readonly four-element tuple of finite 0.125–32 ratios. Fixed-Hz operators retain these ratios but ignore them until restored to ratio mode. |\n| operatorFrequencies | Readonly four-element tuple, each entry finite 1–20000 Hz or null to restore tuned note × ratio. Fixed Hz ignores MIDI/tuning transposition; detune, live pitch, pitch envelope and LFO PM still apply. |\n| operatorADSR | Readonly four-element tuple of complete {a,d,s,r} objects using the patch ADSR bounds. Parameters are replaced, not interpolated by ramp. |\n\nramp gives feedback, LFO and frequency/ratio updates independent linear timelines. It must accompany a rampable scalar or tuple control; operatorADSR alone with ramp rejects. Interrupted updates start at the current value; omitted controls keep their existing timelines. Live lfoRate integrates phase without resetting the current phase, including interrupted rate ramps. Tuples and nested ADSRs are strict own-data snapshots detached and frozen before scheduling.\n\nUpdating a held operator's ADSR restarts attack from its current dB level, then decays to the new sustain. With both attack and decay zero, a one-frame bridge preserves continuity. An update during release starts from the current dB level and completes the new key-scaled release, bounded to ten seconds; zero release silences immediately. Envelope durations provide continuity rather than interpolating ADSR parameters with the control ramp.\n\n## DX7 import: retained data versus approximations\n\nimportDX7 remains a lossy six-to-four-operator conversion, not hardware emulation. Source format and behavior were checked against the Yamaha DX7 manual, printed pp. 13–17 and 30–31 and the DX7II packed VMEM layout, Add-11.\n\n- Fixed frequency is retained in Hz as 10 ** ((coarse & 3) + fine / 100), rather than a MIDI-60 ratio. Detune remains approximate.\n- Keyboard rate scaling maps source 0–7 to OPM 0–4. This is a continuous musical approximation, not Yamaha's grouped-key/rate transfer function.\n- Pitch envelope retains L4 → L1 → L3 → L4. L2 is omitted; R2/R3 durations are merged and capped at ten seconds. Levels interpolate piecewise linearly through source 0/50/99 = −4800/0/+4800 cents. Rates use the existing min(10, 10 2 * (-rate / 10)) seconds heuristic, not measured hardware timing. A fully neutral pitch envelope is omitted.\n- LFO delay maps source 0–99 to 0–10 seconds; sync on maps to note and sync off to deterministic global phase. Delay shaping and free-running/random hardware phase are not reproduced.\n- Triangle, rising saw, square and sine retain their waveform family. Descending saw becomes rising saw; sample-and-hold becomes sine. Both substitutions are explicitly reported by describeDX7.\n\nOperator selection, envelope/output response, topology, feedback placement, velocity and level scaling remain approximations. Oscillator phase carry, source transpose, positive keyboard scaling, per-operator AM and nonzero final amplitude-envelope levels are not preserved. describeDX7 reports these losses separately from the strict usable voice shape.\n\n## OPM text import\n\nimportOPM(source: string | Uint8Array): Voice[] and describeOPM(source: string | Uint8Array): OPMImportDescription[] live in opm.js/voices/opm.js. They convert decimal VOPM/MXDRV-family text patches into complete v7 voices through normal normalization. This is approximate conversion, not register-level YM2151 emulation, with no hardware-fidelity claim.\n\nFormat references: the published MiOPM text-layout header, VOPM unofficial manual and OPM application manual, especially figures 2.5–2.16. Only format/parameter documentation was used; no emulator or converter implementation was copied. The VOPM GUI multiplier description is doubled relative to hardware MUL: this importer conservatively uses raw hardware values. The published text example contains an out-of-range KS value; hardware KS 0–3 is enforced rather than guessing its meaning. No physical or listening comparison has been performed.\n\nEach patch starts with @:\u003cprogram 0..127> \u003cname> and must contain exactly one each of LFO:, CH:, M1:, C1:, M2:, C2:. Blank lines and lines starting with // after whitespace are ignored; CRLF is accepted. Section order is flexible; fields are decimal integers only:\n\ntext\nLFO: LFRQ AMD PMD WF NFRQ\nCH: PAN FL CON AMS PMS SLOT NE\nM1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN\nC1: (same eleven fields)\nM2: (same eleven fields)\nC2: (same eleven fields)\n\n\nRanges: LFRQ 0–255; AMD/PMD 0–127; WF 0–3; NFRQ 0–31; PAN byte 0–255 (ignored); FL/CON/PMS 0–7; AMS 0–3; NE/AMS-EN 0–1. SLOT is the raw key-on mask: only bits 3/4/5/6 (8/16/32/64) enable M1/C1/M2/C2, so all enabled is 120, not 15. Disabled operators get zero level with warnings. AR/D1R/D2R 0–31; RR/D1L/MUL 0–15; TL 0–127; KS/DT2 0–3; DT1 0–7. Duplicate/missing/unknown sections, malformed/out-of-range fields and NUL reject with RangeError and a line number. Empty files reject. Limits are 262144 input bytes, 128 patches and 512 bytes per line. Byte input is decoded explicitly as Latin-1, not UTF-8 or Windows-1252; strings use UTF-8 byte counts for bounds. Names become [a-zA-Z0-9-]{1,64}, falling back to OPM, with deterministic 2, _3, … suffixes and truncation.\n\n### Conversion formulas\n\n- Routing: CON 0–7 corresponds directly to OPM.js algorithms 0–7. File order M1/C1/M2/C2 is signal order, not register-slot order M1/M2/C1/C2. The manual's diagrams give: 0 M1→C1→M2→C2; 1 (M1+C1)→M2→C2; 2 C1→M2 then (M1+M2)→C2; 3 M1→C1 then (C1+M2)→C2; 4 two chains M1→C1 and M2→C2; 5 M1 modulates the other three; 6 M1→C1 plus independent M2/C2; 7 all carriers. Feedback stays on M1, with FL retained but the engine's musical strength curve.\n- Envelope: the original bounded heuristic T(rate) = min(10, 10 2 (-rate / 3)) seconds gives a=T(AR), d=T(D1R), r=T(2RR+1). Zero AR/D1R represents infinity and is clamped to ten seconds with a warning. RR's doubled-plus-one input-rate convention follows the manual; the times are not chip timings. Sustain is linear gain 10 * (-3D1L/20); D1L=15 uses −93 dB instead of −45 dB. The engine interpolates envelope levels in dB. D2R cannot be represented by ADSR: held-note second decay is omitted, D1L holds until release, and every nonzero D2R gets an operator-specific warning.\n- Level: level = 10 * (-0.75TL/20) for both carriers and modulators. Operator output amplitude also drives phase modulation, so this preserves TL attenuation without carrier-only renormalization. modIndex=4 retains the default four-radian modulation scale, not a hardware transfer curve.\n- Pitch/scaling: MUL 0→ratio 0.5; 1–15→their integer ratios. DT1 becomes fixed cents [0,1,2,3,0,-1,-2,-3], a small symmetric approximation to the chip's key-dependent table. DT2 adds [0,600,781,950] cents (manual figure 2.7). Their sum stays within ±1200. KS maps linearly to rateKeyScale=KS*4/3: octave-based duration scaling around MIDI 60, not hardware grouped-key rates.\n- LFO: fixed reference clock f=3579545 Hz; rate = f/232 * 2(LFRQ>>4) (1+(LFRQ&15)/16), fitting manual figure 2.16, capped at 20 Hz with a warning. PM sensitivity maxima are [0,5,10,20,50,100,400,700] cents; pmDepth=PMSmaxPMD/128. AM sensitivity maxima are [0,23.90625,47.8125,95.625] dB; amDepth=1-10*(-AMSmaxAMD/128/20). These endpoint-depth mappings approximate the engine's linear-gain/cents LFO, not the chip's attenuation/frequency transfer function. Per-operator AMS-EN becomes amTargets. WF 0/1/2 becomes saw/square/triangle; 3 (noise) substitutes deterministic sine with a warning. AM/PM waveform phase relationships and global phase differ from hardware; clock selection and free-running chip state are not retained.\n- Noise: NE replaces C2 (op4) with waveform:'noise'; its approximate rate is f/(32*(32-NFRQ)), capped to 20–20000 Hz with a clamp warning. The manual prints a noise-divider equation but does not spell out raw zero encoding: this conversion conservatively assumes an inverted 32-step countdown and always warns about that approximation when NE is on. It does not reproduce a chip noise spectrum. OPM.js noise on any channel is an extension: the hardware channel-8 restriction, LFSR and special noise envelope are not reproduced.\n\ndescribeOPM returns {name,program,algorithm,operators,warnings}. operators lists source labels in destination order. Warnings are deterministic, de-duplicated and bounded by the fixed four-operator conversion, including every approximation/substitution/clamp above. Program numbers are metadata only; wire imported names into a MIDI program map explicitly. Patches, not file programs, are de-duplicated by name.\n","href":"./expressive-voices.html"},{"title":"Voice v7 and expressive controls · Voice v7 and expressive controls","text":"Voice v7 and expressive controls Voice v7 and expressive controls","href":"./expressive-voices.html#voice-v7-and-expressive-controls"},{"title":"Voice v7 and expressive controls · Patch fields","text":"Voice v7 and expressive controls Patch fields","href":"./expressive-voices.html#patch-fields"},{"title":"Voice v7 and expressive controls · Oscillator shapes and noise","text":"Voice v7 and expressive controls Oscillator shapes and noise","href":"./expressive-voices.html#oscillator-shapes-and-noise"},{"title":"Voice v7 and expressive controls · Pitch envelope","text":"Voice v7 and expressive controls Pitch envelope","href":"./expressive-voices.html#pitch-envelope"},{"title":"Voice v7 and expressive controls · LFO","text":"Voice v7 and expressive controls LFO","href":"./expressive-voices.html#lfo"},{"title":"Voice v7 and expressive controls · Live operator levels","text":"Voice v7 and expressive controls Live operator levels","href":"./expressive-voices.html#live-operator-levels"},{"title":"Voice v7 and expressive controls · Live timbre and envelope controls","text":"Voice v7 and expressive controls Live timbre and envelope controls","href":"./expressive-voices.html#live-timbre-and-envelope-controls"},{"title":"Voice v7 and expressive controls · DX7 import: retained data versus approximations","text":"Voice v7 and expressive controls DX7 import: retained data versus approximations","href":"./expressive-voices.html#dx7-import-retained-data-versus-approximations"},{"title":"Voice v7 and expressive controls · OPM text import","text":"Voice v7 and expressive controls OPM text import","href":"./expressive-voices.html#opm-text-import"},{"title":"Voice v7 and expressive controls · Conversion formulas","text":"Voice v7 and expressive controls Conversion formulas","href":"./expressive-voices.html#conversion-formulas"},{"title":"Host integration","text":"Host integration # Host integration\n\n## Worklet deployment\n\nnew OPM({ workletUrl?: string | URL }) accepts an optional module path. Relative paths resolve against the page URL; a URL is copied when constructed. The default remains new URL('../worklet/processor.js', import.meta.url). A custom module must be same-origin HTTP(S), with no credentials or fragment. HTTPS and secure loopback HTTP are supported; insecure contexts are rejected before audio resources are allocated. start() rejects and emits an error event when initialization fails, including normal CSP, network and MIME failures. Validation does not bypass browser policy.\n\nDeploy the processor and its relative imports, not only one JavaScript file. Keep their installed layout; serve JavaScript as JavaScript and asset misses as 404, not SPA HTML. See the installed-package Vite example (checkout-only) for a non-root deployment with script-src 'self', worker-src 'self' and no blob/eval allowances.\n\nts\nimport { OPM } from 'opm.js';\n\nconst engine = new OPM({ workletUrl: '/assets/opm/worklet/processor.js' });\n// Invoke initialization/resume from a trusted user gesture.\nawait engine.start();\n\n\n## Worker WAV output and bounded sinks\n\nrenderSequenceInWorker(events, { sink, format?, onProgress?, signal?, workerUrl?, ...chunkOptions })\nrenders long scores in a static module Worker. Chunk options are the core\nrenderSequenceChunks options, including sampleRate, chunkFrames (1–65536,\ndefault4096), maxFrames, voice registry, mix/tuning/stealing and quality.\nformat is pcm16 (default), pcm24, or IEEE float32. Output is stereo RIFF32\nWAV; oversized RIFF files reject before Worker allocation. Score, options and\npatches are validated and detached first; there is no audio device/context.\n\nThe default module is new URL('../worker/render.js', import.meta.url).\nDeploy dist/worker/render.js and all its relative imports, maps and\ndeclarations from the matching distribution. An explicit workerUrl resolves\nagainst the page and must be same-origin HTTPS or secure loopback HTTP, with\nno credentials/fragment. The default must also remain same-origin.\nUse worker-src 'self'; no blob module, eval, dynamic executable source or\ncross-origin loading is used. CSP, MIME and network failures reject normally.\nImporting the browser API on Node/SSR is safe, but calling this helper requires\na browser with module Workers.\n\nA sink is an own-data object with write(Uint8Array): void | Promise\u003cvoid>,\noptional close() and optional abort(reason). The Worker transfers one byte\nchunk and waits until the sink write fulfills before advancing DSP. The sink owns\nthe byte buffer and may transfer/detach it (for example, to a storage Worker);\nbyte accounting captures its native size before invoking write. Neither thread\naccumulates a complete PCM buffer or WAV. The sink must actually drain to bounded\nstorage: retaining every chunk defeats the memory bound.\nonProgress({ frames, totalFrames, bytesWritten, errors }) runs after each\nsuccessful write, including the initial header (frames:0); frame counts are\ncumulative. The result contains capacity, format, bytesWritten and\ndiagnostics: { errors, processedEvents, renderedFrames }. Success means\nall bytes were written and close() fulfilled, not that a host file is audible.\n\nAbort, sink/progress exceptions and Worker failures terminate the Worker,\nremove listeners and reject. abort(reason) is invoked once, but its promise\nand an outstanding write/close are not awaited: a hung sink cannot hold\ncancellation hostage. Their later rejections are consumed. A sink must\ninvalidate in-flight writes and discard/roll back incomplete output; cancellation\ncannot retract bytes already persisted. abort must not finalize an incomplete\nfile. Use host-side cleanup when abort itself fails.\n\nFor an actual file export, request File System Access from a trusted gesture\nwhere it is supported. Feature-detect it; if unavailable, report that long-file\nexport is unsupported rather than aggregating a long WAV into a Blob:\n\njs\nimport { renderSequenceInWorker } from 'opm.js';\n\n// Run this handler directly from a user gesture.\nasync function exportScore(events, signal) {\n signal?.throwIfAborted();\n if (typeof window.showSaveFilePicker !== 'function') {\n throw new Error('Streaming file export requires File System Access');\n }\n let file, settled = false;\n const rollback = async reason => {\n if (!file || settled) return;\n settled = true;\n await file.abort(reason);\n };\n try {\n const handle = await window.showSaveFilePicker({\n suggestedName: 'sequence.wav',\n types: [{ description: 'WAV audio', accept: { 'audio/wav': ['.wav'] } }],\n });\n signal?.throwIfAborted();\n file = await handle.createWritable();\n signal?.throwIfAborted();\n return await renderSequenceInWorker(events, {\n sampleRate:48000, chunkFrames:4096, format:'pcm24', signal,\n workerUrl:'/audio/opm/worker/render.js',\n sink: {\n write(bytes) { return file.write(bytes); },\n async close() { await file.close(); settled = true; },\n abort: rollback,\n },\n });\n } catch (error) {\n await rollback(error).catch(cleanup => console.error('File rollback failed', cleanup));\n throw error;\n }\n}\n\n\nThe checkout demo's memory-only download is intentionally a small export:\npreflight a hard frame/byte budget, retain chunks only within that budget, and\nrevoke its old Blob URL on replacement/disposal. It is not a fallback for a long\nfile sink. Hosts also own cumulative work, concurrency and export-frequency limits.\n\nNode and browser hosts may use createWavEncoder({ sampleRate, channels:1|2,\nformat?, totalFrames }) from opm.js/core directly. Write header() exactly\nonce, then each encode({ left, right? }) result to a backpressured sink before\nconsuming the next borrowed PCM chunk; finally write finalize() (a RIFF\nalignment byte or empty array). Each call accepts1–65536 frames. The declared\nframe count is exact: overrun/underrun reject, and invalid chunks never advance\nframesEncoded. No audio/file bytes are retained by the encoder. Its getters\nare totalFrames, framesEncoded, byteLength and finished. Zero-frame WAVs\nare supported by this streaming API. The full-buffer encodeWav({ left,\nright?, sampleRate, format? }) keeps its original1–4,000,000-frame budget.\nAll channels must be native Float32Arrays of finite samples in−1–1; encoding\nrejects rather than clipping. PCM24 uses signed little-endian endpoints; float\nWAV uses format3 with a fact sample-count chunk.\n\n## Events, acknowledgements and ownership\n\nengine.subscribe(listener) returns an idempotent unsubscribe function. Each subscription is independent, even for the same listener; it coexists with onEvent and scheduler subscriptions. Exceptions and attempts to mutate frozen event records cannot disrupt other listeners or cleanup. Removing a listener during dispatch skips its remaining delivery; newly added listeners begin with the next event. Ordinary close() preserves subscriptions for a later start().\n\nCommands still synchronously return numeric IDs. waitForCommand(commandId, { timeout?, signal? }) waits only for the correlated admission acknowledgement, including a reply received before the method is called. The timeout is milliseconds: default 5000, integer range 1..60000. At most 64 pending waits and 128 command receipts are retained. Receipts without live waiters are evicted oldest-first; an unknown/expired ID rejects rather than waiting forever. Multiple waits for one ID are independent. Abort/timeout removes only that wait, not the admitted command or another waiter. A late acknowledgement can still be retrieved while its receipt is retained. Signals must be genuine AbortSignals: intrinsic state/reason and listener operations bypass shadow accessors and reject duck-typed objects.\n\nA rejected acknowledgement throws CommandRejectedError with its frozen event (reason, note ID and command ID). Pending waits reject on reset, close, interruption/suspension or processor failure. A command-triggered reset carries its initiating commandId; an ordered panic reset rejects earlier outstanding commands, but preserves that panic's own acknowledgement and commands posted after it, even after receipt eviction. Uncommanded interruption resets invalidate every outstanding receipt. A settled acknowledgement is historical admission evidence and is not revoked by a later reset. Accepted does not prove scheduled execution, gate release, release-tail completion or audible output. Use note lifecycle events and diagnostics for those distinct observations.\n\nts\nconst id = engine.playNote({ note: 60 });\nconst stop = engine.stop(id, { at: engine.context!.currentTime + 0.1 });\nconst acknowledgement = await engine.waitForCommand(stop, { timeout: 2000 });\n// acknowledgement.state === 'accepted': future stop was admitted, not completed.\n\n\nclose() disconnects OPM's node, closes its port and removes its context listener; it is restartable. dispose() is terminal and idempotent: it also removes subscriptions/onEvent, rejects outstanding waits and prevents restart/new subscriptions. Both close an owned AudioContext, but never close or suspend a borrowed context. The host owns its additional routing nodes and any context it supplied.\n\n## Cross-feature contracts\n\n| Identity / observation | Meaning | Not equivalent to |\n| --- | --- | --- |\n| Score note ID | Local musical identity used by note, control and stop events | A live OPM note ID; score IDs are remapped on admission |\n| OPM note ID | One live note's owned lifecycle | A physical key ID or a command ID |\n| Performance key ID | One physical key press, distinct even at equal pitch | Its current sounding gate; mono selection may reuse or replace a gate |\n| Command ID / accepted | Correlated command admission | Scheduled execution, audible onset or completed release |\n| Note started / released / ended | Audio-frame lifecycle transitions | Device-output or microphone-observed timing; released may retain a tail |\n| Arrangement committed beat | The scheduled musical change boundary | Immediate application; already admitted notes retain their times |\n| getDiagnostics() | Voice/queue counts, errors and rejected notes | CPU utilization, GC pauses, underrun counters or audible continuity |\n\nPersist authored music, not an audio session. Score projects store finite beat events; Arrangement projects store looping layers and named sections. Both retain validated named patches, tempo/meter and synthesis settings. They do not retain current DSP phases/envelopes, live key/pedal ownership, pending section commands, host effects/routing or MIDI permissions/controller mappings. Hosts save those additional application choices separately and explicitly.\n\nParsing or loading a file never grants permission or starts audio. Load the validated named voices, construct the intended engine with its immutable quality/voice budget, connect the host graph and start from a trusted gesture. Dispose the old helper and its owned engine when replacing a session; never close a context borrowed from another component. Treat new project definitions as musical starts, not seamless continuation.\n\nLive MIDI and MIDI files are different contracts. The live adapter applies channel/key policies to owned gates; an SMF conversion must make its expressive-controller policy explicit and expose every reported omission or approximation. A valid MIDI or project round trip does not prove hardware fidelity or lossless conversion of unsupported musical semantics. See MIDI files.\n\nFor reproducible sound retain the package version, normalized patch identity, sample rate, quality, voice budget, tuning, mix gain and stealing policy. Retain host/output gains and routing separately: downstream attenuation cannot undo the engine's saturation. Numerical output and measured host capacity remain separate from physical recovery and human listening acceptance.\n\n\n## Audio/output clock mapping and component cleanup\n\ngetOutputTimestamp() pairs an AudioContext contextTime (seconds at device output) with the corresponding performanceTime (milliseconds on the performance clock). It is not a pair of independent current-time samples. Map an audio event at audioTime to an estimated output time with:\n\nts\nconst stamp = context.getOutputTimestamp();\nconst estimatedOutputMs = stamp.performanceTime + (audioTime - stamp.contextTime) * 1000;\n\n\nDo not add baseLatency or outputLatency again: the timestamp already describes output. Timestamp support/precision varies, the initial pair may be zero, and the result is an estimate—not microphone-observed acoustic onset. Re-sample while running after resume/device changes. If unavailable, display audio-clock timing only rather than inventing output-clock precision.\n\nThe following host can be mounted beside a button/status element. unmount() removes DOM/OPM listeners, aborts outstanding waits, disposes OPM, disconnects host routing and closes the host-owned context. The asynchronous gesture handler checks teardown after every asynchronous startup boundary.\n\nts\nimport { OPM } from 'opm.js';\n\nconst button = document.querySelector\u003cHTMLButtonElement>('#play')!;\nconst status = document.querySelector\u003cHTMLElement>('#status')!;\nconst context = new AudioContext();\nconst output = context.createGain();\noutput.gain.value = 0.08;\noutput.connect(context.destination);\nconst engine = new OPM({ context, destination: output,\n workletUrl: '/assets/opm/worklet/processor.js' });\nconst lifetime = new AbortController();\nlet mounted = true;\nlet note: number | undefined;\nconst unsubscribe = engine.subscribe(event => {\n if (event.type === 'error') status.textContent = event.error.message;\n if (event.type === 'reset') note = undefined;\n if (event.type === 'note' && event.id === note) {\n status.textContent = event.state;\n if (['ended', 'stolen', 'cancelled', 'rejected'].includes(event.state)) note = undefined;\n if (event.state === 'started' && typeof context.getOutputTimestamp === 'function') {\n const stamp = context.getOutputTimestamp();\n if (stamp.performanceTime > 0 && Number.isFinite(stamp.contextTime) && Number.isFinite(stamp.performanceTime)) {\n const outputMs = stamp.performanceTime + (event.time - stamp.contextTime) * 1000;\n status.title = Estimated output onset: ${outputMs.toFixed(1)} ms;\n }\n }\n }\n});\nlet busy = false;\nconst onClick = async () => {\n if (!mounted || busy) return;\n busy = true;\n try {\n await context.resume(); // Called synchronously in the trusted gesture.\n if (!mounted) return;\n await engine.start();\n if (!mounted) return;\n if (note !== undefined) {\n await engine.waitForCommand(engine.stop(note), { signal: lifetime.signal });\n if (!mounted) return;\n }\n // A desired performance-clock target converted back to audio seconds.\n const desiredOutputMs = performance.now() + 150;\n let at = context.currentTime + 0.05;\n if (typeof context.getOutputTimestamp === 'function') {\n const stamp = context.getOutputTimestamp();\n if (stamp.performanceTime > 0 && Number.isFinite(stamp.contextTime) && Number.isFinite(stamp.performanceTime)) {\n at = Math.max(at, stamp.contextTime + (desiredOutputMs - stamp.performanceTime) / 1000);\n }\n }\n note = engine.playNote({ note: 60, at, duration: 1 });\n } catch (error) {\n if (mounted) status.textContent = error instanceof Error ? error.message : String(error);\n } finally { busy = false; }\n};\nbutton.addEventListener('click', onClick);\n\nlet teardown: Promise\u003cvoid> | undefined;\nfunction unmount(): Promise\u003cvoid> {\n if (teardown) return teardown;\n mounted = false;\n button.removeEventListener('click', onClick);\n lifetime.abort();\n unsubscribe();\n teardown = (async () => {\n try { await engine.dispose(); }\n finally {\n output.disconnect();\n if (context.state !== 'closed') await context.close();\n }\n })();\n return teardown;\n}\n\n\nFor a shared context owned elsewhere, omit its final context.close(); disconnect only the routing nodes this component owns. streamSequence and createLookaheadScheduler have their own dispose() methods: dispose each scheduler before its OPM instance so admission timers and live-note ownership are released.\n\n## Other bundlers and SSR hosts\n\nWebpack, Rollup, Parcel and static hosts use the same explicit static-asset contract without relying on unverified worklet-plugin APIs: deploy the installed package's entire dist/ tree plus the package-root LICENSE at a same-origin URL, preserving relative paths. This is a packaging recipe, not a claim that those other bundlers were exercised. Only the existing Vite production smoke provides bundler-specific runtime verification.\n\nThe package ships a dependency-free helper that does this safely: Run it from a project with OPM.js installed; --no-install prevents an accidental registry fetch.\n\nsh\nnpx --no-install opm-assets copy public/audio/opm-1.10.0 # atomic copy into a NEW directory\nnpx --no-install opm-assets check https://example.test/audio/opm-1.10.0/ # byte-for-byte check of what is served\n\n\ncopy (also import { copyAssets } from 'opm.js/tools/assets.js', Node only) inspects the installed release first: only .js, .js.map and .d.ts files under dist/, no symlinks, every .js with its map and declaration, the entry modules api/index.js, core/index.js, worklet/processor.js and worker/render.js, and a bounded size (4,096 files, 16 MiB). It stages a temporary directory beside the destination, copies and re-hashes every file, writes opm-assets.json (SHA-256 and size per file, plus LICENSE) and renames the staging directory into place. It never overwrites: a destination that already holds exactly this release is reported as reused: true; anything else is an error and left untouched. Use a fresh release-specific directory (or an atomic deployment switch) so removed old assets cannot mix with a new tree.\n\ncheck (or checkDeployment(url)) downloads each listed file from an HTTPS or loopback-HTTP base URL and compares status, a JavaScript MIME type, X-Content-Type-Options: nosniff (disable with requireNosniff: false) and the SHA-256 against the installed release. Redirects are errors, so an SPA fallback that returns HTML with status 200 fails on MIME or bytes. It cannot see your page's Content-Security-Policy and does not start an AudioWorklet or Worker: run the browser smoke or the Vite example (checkout-only) for that.\n\nConfigure new OPM({ workletUrl: '/audio/opm-1.10.0/worklet/processor.js' }) and, for Worker export, workerUrl: '/audio/opm-1.10.0/worker/render.js'. The worklet's own imports must remain deployed beside it and use valid JavaScript MIME types; no blob URL or relaxed CSP is needed. The host can bundle the browser-facing API normally or import /audio/opm-1.10.0/api/index.js externally.\n\nAn SSR module may import types safely, but instantiate/resume OPM only on the client, from a trusted user gesture. Keep module initialization free of window, document and AudioContext access on the server; dynamically import the runtime inside client-only code when required by the host. Dispose it during component teardown using the ownership recipe above.\n\n## Part-scoped performance\n\ncreatePerformance() supplies playing policies, not a MIDI driver. It requires\na started OPM and never owns its AudioContext. Keep returned physical key IDs\nuntil their matching key-up; repeated equal-pitch keys have separate identities.\n\njs\nimport { createPerformance } from 'opm.js';\n\n// opm is already started from the host's trusted input gesture.\nconst performance = createPerformance(opm, {\n parts: 16, maxKeys: 128, maxKeysPerPart: 16,\n onError: error => console.error(error),\n});\nperformance.configurePart(0, {\n voice: 'brass', mode: 'mono', legato: true, priority: 'high', glide: 0.02,\n pan: 0, expression: 0.8,\n});\nconst key = performance.noteOn(0, 60, { velocity: 0.7 }); // host key-down\nperformance.sustain(0, true); // pedal-down\nperformance.noteOff(0, key); // physical key-up\n// Later, in the pedal-up input handler:\n// performance.sustain(0, false);\n// performance.updatePart(0, { pan: -0.5, expression: 0.6, glide: 0.03 });\n// performance.allNotesOff(0); // only this part; no global panic\n// performance.dispose(); // release all helper-owned gates on unmount\n\n\nParts are 0-based, with 1–16 configured parts (default 16), at most 128 keys and\n128 tracked gates system-wide, including pending releases. Per-part limits share\nthat system budget; overflow rejects without evicting unrelated parts. A part\nmay use mode: 'poly' or 'mono', legato, and priority: 'last' | 'high' | 'low'.\nPhysically held keys take precedence over pedal-only keys; equal pitches choose\nthe newest identity. Mono legato preserves the original onset's envelope,\nvelocity and key/rate scaling. Its pitch offset is anchored to that onset:\nwithin ±48 semitones it reuses the gate, farther moves retrigger at the actual\npitch instead of clamping. Non-legato switching retriggers the patch.\n\ngetPart(part) returns a detached frozen snapshot. Stealing prunes the affected\ngate without later automatic readmission; reset clears state. Interruption\nreleases helper-owned notes even when direct OPM notes use preserve policy.\nDispose performance/Transport helpers before disposing their shared engine.\nTheir cleanup does not release unrelated host or Transport notes.\n\nRelease 1.8 adds updateKey, updatePartNotes, per-part voiceLimit/voicePriority and an optional Web MIDI adapter; see expressive performance and MIDI. Looping layers and boundary-quantized section switching are covered in adaptive music, and Worker startup and phase diagnostics in Worker diagnostics.\n","href":"./host-integration.html"},{"title":"Host integration · Host integration","text":"Host integration Host integration","href":"./host-integration.html#host-integration"},{"title":"Host integration · Worklet deployment","text":"Host integration Worklet deployment","href":"./host-integration.html#worklet-deployment"},{"title":"Host integration · Worker WAV output and bounded sinks","text":"Host integration Worker WAV output and bounded sinks","href":"./host-integration.html#worker-wav-output-and-bounded-sinks"},{"title":"Host integration · Events, acknowledgements and ownership","text":"Host integration Events, acknowledgements and ownership","href":"./host-integration.html#events-acknowledgements-and-ownership"},{"title":"Host integration · Cross-feature contracts","text":"Host integration Cross-feature contracts","href":"./host-integration.html#cross-feature-contracts"},{"title":"Host integration · Audio/output clock mapping and component cleanup","text":"Host integration Audio/output clock mapping and component cleanup","href":"./host-integration.html#audiooutput-clock-mapping-and-component-cleanup"},{"title":"Host integration · Other bundlers and SSR hosts","text":"Host integration Other bundlers and SSR hosts","href":"./host-integration.html#other-bundlers-and-ssr-hosts"},{"title":"Host integration · Part-scoped performance","text":"Host integration Part-scoped performance","href":"./host-integration.html#part-scoped-performance"},{"title":"Standard MIDI files","text":"Standard MIDI files # Standard MIDI files\n\nimportMidiFile and exportMidiFile are dependency-free binary adapters, available from opm.js/midi-file, opm.js/core, and the main API. They do not open MIDI devices, transmit SysEx, emulate a chip, or claim a General MIDI sound set.\n\nExpression conversion below is available since v1.10 in package 1.10.0. Controls remain opt-in, with default omission and explicit lossSummary; interactive examples are checkout-only and require the matching v1.10 source checkout.\n\n## Import\n\nts\nconst score = importMidiFile(bytes, {\n channelVoices: { 0: 'brass', 1: 'lead', 9: 'drum' }, // channels are zero-based\n defaultVoice: 'brass',\n unsupported: 'warn',\n unclosedNotes: 'reject',\n sustain: 'apply',\n controls: 'preserve', // explicit; default 'omit' retains legacy note-only conversion\n pitchBendRange: 2, // initial semitones, finite 0..48; RPN 0 may change it\n});\n// score.events: BeatSequenceEvent[] (positive unique note IDs plus per-note controls)\n// score.tempoMap: normalized quarter-note BPM steps, beginning at beat zero\n// score.timeSignature: one numerator/denominator\n// score.warnings: { code, message, count }[]; counts are aggregated by code\n// score.lossSummary: frozen { omissions, approximations, preservedControls }\n\n\nSupported input is SMF format 0 (one track) or format 1 (simultaneous tracks), with positive integer PPQN timing. All tracks are merged by absolute tick, then file track number, then event order. Channel state is shared across tracks. Repeated note-ons at the same channel/pitch are owned FIFO, independently of sustain; note-on velocity zero is a note-off. CC64 values 64..127 hold released keys until pedal-up. Sustain becomes longer note durations and produces sustain-applied warnings. sustain: 'reject' rejects any CC64 rather than approximating it.\n\nVoice selection is opt-in: programVoices?: MidiVoiceMap maps program numbers 0–127 to FM voice names, and drumVoices?: MidiVoiceMap maps drum notes 0–127 on zero-based channel 9. Each channel starts at program 0; Program Change updates it in merged tick/track/event order. Each note stores its onset voice, so changes never retarget held notes. Drum entries take precedence, then program entries, then channelVoices[channel], then defaultVoice (brass). Only the final default fallback warns. Omitting the new maps preserves legacy note output. Bank CC0/32 stays ignored with warnings. The caller supplies the corresponding voice registry to compilation/rendering; missing names produce the normal unknown-voice error. Preserve routing on export using distinct voice names and voiceChannels.\n\nMidiVoiceMap = Readonly\u003cRecord\u003cnumber, string>> | ReadonlyMap\u003cnumber, string> accepts plain own-data objects or native Maps, copied before use, at most 128 entries. Keys must be canonical integers 0–127; names match [a-zA-Z0-9_-]{1,64}. Accessors, inherited entries, symbols and custom Map properties/prototypes reject. gmProgramVoices and gmDrumVoices are frozen root/core exports: all 128 programs map by GM family to bundled FM names, with a partial percussion map. They are artistic starters, not a GM sound-set claim. Load the bank containing those names, including noise-snare/noise-hihat for the starter drum map, or edit the map for your bank.\n\nTempo defaults to 120 BPM until the first tempo event. Conflicting tempo events at the same tick use the last event in the documented merge order and warn. MIDI microseconds-per-quarter must produce BPM in 1..1000. Only one meter is representable: conflicting meters at beat zero or any actual meter change later reject. Repeating the same meter is accepted. Numerators are 1..32; denominators are 1, 2, 4, 8, 16 or 32.\n\nThe default controls: 'omit' is compatibility mode: bend, pressure and controllers other than CC64 are omitted with ignored-channel warnings. Opt in to controls: 'preserve' for:\n\n| MIDI source | Beat control meaning |\n| --- | --- |\n| Pitch bend | pitch, signed semitones using channel sensitivity, initially pitchBendRange (default 2) |\n| CC7 and CC11 | expression = (CC7 / 127) * (CC11 / 127); initial values are both 127 |\n| CC10 | pan = max(-1, (value - 64) / 63); initial center is 64 |\n| CC1 or channel pressure | Channel-default modulation = 1 + value / 127; last channel message wins, initial value is 1 |\n| Poly pressure | Same modulation scale, applied only to the newest still-pressed key of that channel/pitch |\n\nChannel updates visit every held or sustain-held gate in onset order, including distinct repeated pitches, and set the initial state of future notes. Poly pressure does not change future-note defaults or target sustain-only keys. Once assigned to a pressed key, its modulation override persists through sustain and takes precedence over later channel CC1/pressure defaults, matching the live Performance control merge. Pressure is LFO-depth scaling, not an invented force/amplitude unit. Each expressive note is followed by a same-beat initial control snapshot (pitch: 0, expression: 1, pan: 0, modulation: 1 unless the channel changed). This can contain zero expression without turning the note-on velocity into a release.\n\nThe adapter does not know synthesis patches' release-tail duration. Once a gate closes it is no longer a channel-owned control target. A subsequent channel-expression message on that channel produces the conservative release-tail-controls approximation warning, even if the unknown tail might already have ended. It still updates held/sustained gates and future-note defaults. unsupported: 'reject' rejects this possible loss. This is not complete live-performance/release-tail fidelity.\n\nPreserve-mode import decodes RPN 0 sensitivity: CC101=0 and CC100=0 select it, CC6 supplies semitones (clamped to 48), and optional CC38 supplies cents (clamped to 99). Their sum is clamped to 48 semitones; each channel starts from pitchBendRange. Only later bend messages use the changed scale. CC101=127/CC100=127 deselect; other RPN numbers and NRPN selection CC98/99 also disable sensitivity data entry. Selection messages are recognized; unselected CC6/38 remain omitted with warnings. Omit mode does not interpret RPN.\n\nUnmapped programs, bank selection, NRPN, unselected data-entry, other controllers/channel-mode messages, release velocity, text, key signatures, port/channel metadata, sequencer-specific metadata, SMPTE offsets, framed SysEx/escaped data, and header extensions remain omitted with warnings. Non-default MIDI metronome/thirty-second-note meter fields also warn. No SysEx is executed. unsupported: 'reject' rejects omissions, conflicting tempos, default voice substitution and possible release-tail-control loss; explicit sustain: 'apply' and unclosedNotes: 'close-at-end' remain accepted approximations.\n\n### Inspect losses, not just success\n\nwarnings remains the aggregated { code, message, count } API. lossSummary separates:\n\n- omissions: ignored metadata/channel/SysEx/header data, release velocity and meter-detail warnings.\n- approximations: default voice substitution, flattened sustain, conflicting tempo choice, explicit end-of-file closure and possible release-tail-control loss.\n- preservedControls: { kind, count } for pitch-bend, expression, pan, modulation, channel-pressure, poly-pressure, program-change and pitch-bend-range. Counts are accepted source messages, not generated per-note controls. A program change counts when its number has a program-map entry (even without a later note); otherwise ignored-program counts in omissions. pitch-bend-range counts CC100/101 selection and selected RPN-0 CC6/38. CC7/11 both count toward expression; initial snapshots do not count. Neutral/no-op messages count as recognized semantics, not archived bytes.\n\nWarning counts retain their original units: CC64 messages, closed gates for unclosed-notes, note onsets using a fallback voice, or source warnings/messages for other codes. Arrays, entries and summary are frozen and bounded by the fixed code/kind vocabularies. Expose both loss categories before rendering or saving an imported project. Sustain is an explicit gate-duration approximation, not a pedal-performance recording.\n\nUnmatched note-offs always reject. Held keys or sustained gates at the final end-of-track reject by default. unclosedNotes: 'close-at-end' explicitly closes them at the greatest track end tick and reports unclosed-notes; it cannot create a positive duration if the note starts at that tick. Zero-length gates reject. No invented extra tail is added.\n\nTrack lengths, channel data bytes, four-byte VLQs, running status, recognized fixed-length meta records, and SysEx/meta length framing are checked. Meta/SysEx cancel running status. Every declared track must contain a terminal end-of-track event with no bytes after it; undeclared chunks/trailing bytes reject. Format 2, SMPTE division and unframed system statuses reject. Input must be a native, non-shared Uint8Array; subarrays and Node Buffers are accepted without invoking caller metadata getters.\n\n## Export\n\nts\nconst bytes = exportMidiFile(score.events, {\n format: 1, // default 1; 0 is also supported\n ppqn: 480, // integer 1..32767\n tempoMap: score.tempoMap,\n timeSignature: score.timeSignature,\n voiceChannels: { brass: 0, lead: 1, drum: 9 },\n controls: 'preserve', // default 'omit' still rejects control events/nonzero pan\n pitchBendRange: 2,\n});\nconst reloaded = importMidiFile(bytes, {\n channelVoices: { 0: 'brass', 1: 'lead', 9: 'drum' },\n controls: 'preserve',\n pitchBendRange: 2,\n});\n\n\nFormat 0 combines conductor and musical events into one track. Format 1 writes one conductor track and one musical track per used channel, sorted by channel. Output is deterministic, uses explicit status bytes, writes a single initial meter, and puts note-offs before controls/onsets at equal ticks. Initial channel state is written before its note-on. Remaining expressive operations follow score event order at each rounded tick; contiguous equal-value per-note fanout is restored as one channel update. All tracks end at the same last note/tempo tick. Notes and explicit stop events are supported; a stop shortens its owned gate. Stops before onset, unknown IDs and duplicate note IDs reject. IDs are local score identities and are reassigned on import, not serialized into MIDI.\n\nOnly brass -> channel 0 is implicit. Other named voices require voiceChannels; embedded synthesis patches cannot be exported. Pitches must be integer 0..127. Positive normalized velocities are rounded to 1..127 (never velocity-zero note-offs). Beats and gate endpoints independently round to the nearest PPQN tick, which is monotone; gates that collapse to zero ticks reject. Same-channel/pitch gates must finish in onset order to preserve FIFO ownership; a later onset ending earlier than its predecessor rejects. MIDI has no note-instance IDs with which to recover that nesting.\n\nWithout controls: 'preserve', OPM control events and nonzero note pan reject, maintaining the previous note-only export contract. Expressive export accepts only pitch, expression, pan and modulation controls. Ramp/glide (even zero), gain, operator/LFO/feedback controls, nonzero priority, zero velocity, unmapped voices, and linear tempo curves reject rather than silently dropping musical data. Supply an explicitly discretized step tempo map if approximation is desired; this adapter does not choose that approximation. Tempo points colliding after tick rounding reject. BPM is rounded to the MIDI three-byte microseconds field; tempos too slow for that field reject.\n\nPitch rounds to 14-bit bend within the explicit range; pan and modulation round to seven-bit controller values, with modulation restricted to 1..2. Expression rounds to the nearest representable CC7×CC11 product (ties prefer the higher CC7 factor), preserving already representable products up to floating-point arithmetic. Export emits both factors, CC7 before CC11; intermediate same-tick product updates are possible, with the final product established before onset or the next sample. Channel-wide modulation is encoded as CC1; representable per-key differences are encoded as poly pressure. Original controller/pressure message classes, redundant messages, unused channel state and MIDI bytes are not archival data in a BeatSequence.\n\nChannel pitch/expression/pan changes must agree for every overlapping gate on that channel. Contiguous equal-valued fanout controls can establish that agreement; a private per-note change that would alter another gate rejects. Poly-pressure modulation can address only the newest sounding repeated-pitch gate. Put onset controls directly after their owned note at the same rounded tick. Incompatible overlapping initial states, older repeated-pitch pressure changes, unknown/before-onset controls, controls outside the gate, and controls rounded onto release reject. After-gate controls accepted by the synthesis core cannot be exported: patch-dependent release tails have no finite gate representation in this adapter. Use distinct voice/channel mappings for independent parts rather than letting export guess a channel allocation.\n\nThus file → score → file preserves representable gate/control meaning under the explicit export policy and quantization, not complete General MIDI playback, exact message identity or release-tail behavior. No program, RPN, sustain-controller or SysEx output is synthesized. If imported RPN changes sensitivity, choose an explicit export range containing every decoded pitch; the exporter never reconstructs source RPN or program routing.\n\n## Hard bounds\n\n- MAXMIDIFILE_BYTES: 16 MiB input/output.\n- MAXMIDIFILE_TRACKS: 128 imported tracks; export uses at most 17.\n- MAXMIDIFILE_EVENTS: 65,536 wire events, including omitted metadata/controllers and end-of-track.\n- Expressive fanout plus notes is separately limited to 65,536 generated beat events; a small wire file with many held notes and controllers can hit this bound. Export also checks its generated wire budget before growing musical tracks.\n- At most 1,024 normalized tempo points and 86,400 quarter-note beats.\n- Delta times must fit four-byte VLQ (0..268,435,455). Export rejects excessive silent gaps instead of inserting hidden events.\n- Beat event/options/mappings must be dense own-data arrays/plain objects; accessors and unknown fields reject. Import channel mappings allow 16 channels; export mappings allow 256 names. Program/drum maps additionally accept native Maps, bounded to 128 entries each.\n\nThe beat horizon is not a guarantee of renderability: compileBeatSequence and offline/live renderers apply their separate elapsed-time, polyphony, memory and scheduling limits. A dense file may need chunked offline rendering or live transport streaming rather than one worklet batch.\n\n## Runnable Node file → chunk WAV and MIDI reload\n\nAfter npm run build, save this as midi-to-wav.mjs in the repository root and run node midi-to-wav.mjs input.mid output.wav output.mid. It uses the built local package through its self-reference; no npm publication is required.\n\njs\nimport { readFile, writeFile, stat } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { importMidiFile, exportMidiFile, MAXMIDIFILE_BYTES } from 'opm.js/midi-file';\nimport { compileBeatSequence, renderSequenceChunks, createWavEncoder } from 'opm.js/core';\n\nconst [input, wavPath, midiPath] = process.argv.slice(2);\nif (!input || !wavPath || !midiPath) throw new Error('Pass input.mid output.wav output.mid');\nif ((await stat(input)).size > MAXMIDIFILE_BYTES) throw new Error('Input exceeds MIDI byte budget');\nconst midiPolicy = { controls: 'preserve', pitchBendRange: 2 };\nconst score = importMidiFile(await readFile(input), { ...midiPolicy, channelVoices: { 0: 'brass' } });\nfor (const warning of score.warnings) console.warn(warning.code, warning.count, warning.message);\nconsole.log('Import loss summary:', score.lossSummary);\nconst seconds = compileBeatSequence(score.events, { tempoMap: score.tempoMap });\nconst render = renderSequenceChunks(seconds, { sampleRate: 48000, chunkFrames: 4096 });\nconst encoder = createWavEncoder({\n sampleRate: render.capacity.sampleRate,\n channels: 2,\n format: 'pcm16',\n totalFrames: render.capacity.frames,\n});\nfunction* wavBytes() {\n try {\n yield encoder.header();\n // PCM buffers are reused; encode each chunk before advancing the iterator.\n for (const chunk of render) yield encoder.encode({ left: chunk.left, right: chunk.right });\n yield encoder.finalize();\n } finally {\n render.cancel();\n }\n}\nawait pipeline(Readable.from(wavBytes()), createWriteStream(wavPath));\nconst midi = exportMidiFile(score.events, {\n ...midiPolicy,\n tempoMap: score.tempoMap,\n timeSignature: score.timeSignature,\n voiceChannels: { brass: 0 },\n});\nawait writeFile(midiPath, midi);\nconst reloaded = importMidiFile(await readFile(midiPath), { ...midiPolicy, channelVoices: { 0: 'brass' } });\nconsole.log('Reloaded events:', reloaded.events.length, 'Rendered frames:', render.diagnostics.renderedFrames);\n\n\nThe example intentionally maps otherwise-unmapped channels to brass with visible warnings. Replace both mappings with distinct voice/channel routes and pass your voices Map to compilation/rendering for different instruments. Ambiguous expressive export rejects instead of guessing those routes. It demonstrates file conversion from the current checkout, not a claim of General MIDI fidelity, listening quality, or physical-device testing.\n","href":"./midi-files.html"},{"title":"Standard MIDI files · Standard MIDI files","text":"Standard MIDI files Standard MIDI files","href":"./midi-files.html#standard-midi-files"},{"title":"Standard MIDI files · Import","text":"Standard MIDI files Import","href":"./midi-files.html#import"},{"title":"Standard MIDI files · Inspect losses, not just success","text":"Standard MIDI files Inspect losses, not just success","href":"./midi-files.html#inspect-losses-not-just-success"},{"title":"Standard MIDI files · Export","text":"Standard MIDI files Export","href":"./midi-files.html#export"},{"title":"Standard MIDI files · Hard bounds","text":"Standard MIDI files Hard bounds","href":"./midi-files.html#hard-bounds"},{"title":"Standard MIDI files · Runnable Node file → chunk WAV and MIDI reload","text":"Standard MIDI files Runnable Node file → chunk WAV and MIDI reload","href":"./midi-files.html#runnable-node-file--chunk-wav-and-midi-reload"},{"title":"Expressive performance and optional Web MIDI","text":"Expressive performance and optional Web MIDI # Expressive performance and optional Web MIDI\n\ncreatePerformance(opm, options) gives a host part-scoped key policies (poly/mono, legato, sustain, last/high/low priority), per-key and per-part live controls, per-part voice budgets and priorities, and a separate optional Web MIDI adapter. Try example 10 (checkout-only).\n\n## Per-key and per-part controls\n\nts\nconst key = performance.noteOn(0, 64, { velocity: 0.8 }); // independent key identity, not a note ID\nperformance.updateKey(0, key, { feedback: 5, pitch: 0.3 }); // that key only\nperformance.updatePartNotes(0, { operatorRatios: [2, 1, 1, 1], ramp: 0.05 }); // every sounding note + future notes\n\n\nBoth calls take the full NoteControls object (pitch, glide, expression, pan, modulation, ramp, operator levels/ratios/fixed-Hz/ADSR, feedback and LFO controls). Input is copied as own data and validated before any note is touched.\n\n- updateKey returns false for an unknown or already-removed key and otherwise applies to that key's gate. A key that is not sounding in a mono part (a held key behind the selected one) only stores its controls; the shared gate is never modified by it. When the mono selection later moves to that key, its stored controls are applied together with the pitch offset.\n- updatePartNotes merges into the part's control state, updates every sounding gate of the part (release tails included) and is applied to notes started later. Pitch accumulates as part + key + legato offset and is validated against the engine's ±48 semitone control range.\n- Part pan and expression set through configurePart/updatePart remain the base values. Controls that you apply do not change those configured values.\n- getPartControls(part) returns a detached, frozen effective part-control snapshot: configured defaults resolved against the current named/custom voice, then merged with part controls. It excludes per-key overrides. Operator levels are multipliers, with neutral defaults [1, 1, 1, 1], not the patch's raw operator levels.\n- Mono legato retains its anchor rule: beyond ±48 semitones of the original gate the part restarts the note at the exact target pitch, which re-evaluates velocity, key scaling and rate scaling.\n- noteOff(part, key, options?: PerformanceNoteOffOptions) accepts only own-data force?: boolean. Ordinary note-off remains pedal-aware and returns false for an already-unheld key. { force: true } also releases an unheld pedal-latched key, bypassing sustain for that key alone; mono selection retargets normally. It leaves the pedal and unrelated keys unchanged.\n\n## Voice budgets and priority\n\nts\nperformance.configurePart(0, { voice: 'lead', mode: 'mono', legato: true, voicePriority: 100 });\nperformance.configurePart(1, { voice: 'strings', voiceLimit: 8, voicePriority: 10 });\n\n\n- voiceLimit (1–32) caps the part's own gates. Release tails count: a released note still owns its voice until the engine reports it ended, so noteOn throws a RangeError while the part is at its limit. A limit larger than the engine's maxVoices does not create voices.\n- voicePriority (0–127, default 0) is sent with every note the part admits. With a full engine a note can only displace voices of equal or lower priority, lowest first (then the engine's stealing policy). If every sounding voice has higher priority the note is refused, nothing else changes, and the part removes the key so no phantom held key remains.\n- This is unrelated to the key-selection priority: 'last' | 'high' | 'low'.\n- getPart() snapshots include voiceLimit and voicePriority.\n- configurePart(part, options, policy?: { preserveNotes?: boolean }) normally clears gates on voice/mode changes. { preserveNotes: true } retains sounding gates on voice-only changes, selecting the new patch for later admissions; mono legato restarts rather than reusing a gate with a different patch. Mode changes always clear keys.\n\n## Web MIDI adapter\n\nts\nimport { createPerformance, createMidiAdapter, requestMidiAccess } from 'opm.js';\n\nbutton.onclick = async () => { // an explicit user action\n const access = await requestMidiAccess(); // sysex: false; may show a permission prompt\n const midi = createMidiAdapter(performance, access, { parts: 2, pitchBendRange: 2 });\n // midi.snapshot, midi.releaseAll(), midi.dispose()\n};\n\n\nImporting the module never requests access. The adapter accepts any object that provides inputs and statechange events (MidiAccessLike), so tests and hosts can inject their own. It is not a MIDI driver: no SysEx, clock or device-specific behavior. Optional programVoices?: MidiVoiceMap maps programs 0–127 to named FM voices via configurePart with preserved notes; only later onsets use the new voice, unmapped programs are ignored. The map accepts a plain own-data object or native Map, at most 128 entries; keys are integers 0–127 and names match [a-zA-Z0-9_-]{1,64}. It is copied/validated before ports open. Unknown bank names report the normal performance unknown-voice error to onError. gmProgramVoices (root/core export) is an artistic family starter, not a GM sound set. No live drumVoices option is provided; configure percussion parts explicitly. Bank select remains ignored.\n\n| Message (channel 1–16 → part 0–15) | Effect |\n| --- | --- |\n| Note on / off (velocity 0 = off) | noteOn(part, note, { velocity: v / 127 }); note-off releases the oldest still-held key of that input, channel and pitch. Repeated equal pitches keep separate identities. |\n| CC64 | Sustain pedal for the part. The part is sustained while any adapter input holds its pedal. |\n| CC1, channel pressure, polyphonic pressure | LFO depth multiplier 1 + v/127 (1×–2×): never below the patch default. Polyphonic pressure uses updateKey; the others updatePartNotes. |\n| CC7, CC11 | Multiply into the part's expression. |\n| CC10 | Pan. |\n| Pitch bend | ± channel sensitivity, initially pitchBendRange (default 2, at most 48). |\n| CC101/100, then CC6/38 | RPN 0 selection and sensitivity in semitones/cents for this channel; affects later bends only. |\n| CC120 | Force-release all owned keys (including pedal-latched keys) for this input/channel; no global panic or changes to other inputs/host keys. |\n| CC123 | Pedal-aware note-off for physically held keys of this input/channel; latched keys remain owned for later forced cleanup. |\n| CC121 | Reset the adapter's controllers for the channel and release its pedal. |\n\nEverything else is counted in snapshot.ignoredMessages. Malformed packets (wrong length, data bytes above 127, unknown status) are ignored, never thrown into the page.\n\nRPN 0 requires CC101=0/CC100=0. CC6 semitones clamp to 48, optional CC38 cents clamp to 99, and the total clamps to 48 semitones. Each channel starts with the explicit range (default 2); state is shared across adapter inputs for that channel, like other part controllers. RPN null (CC101=127/CC100=127), other RPN numbers, or NRPN selectors CC98/99 deselect sensitivity data entry. CC121 restores the explicit range and null selection; dispose clears internal RPN state and removes listeners, without resetting host part controls. No SysEx is accepted.\n\nLifecycle: the adapter attaches to current and later inputs (optionally restricted with inputIds) and calls open() where needed. releaseAll() force-releases only adapter-owned keys, including earlier pedal-latched note-offs, without changing the pedal. Disconnect and dispose() force-release the affected input's owned keys and remove listeners, then remove its contribution to the shared pedal union. If the last adapter pedal contribution disappears, the part pedal turns off and may release host-played pedal-latched keys too: pedal/controller state is shared per part, not key-owned. Another input's pedal contribution remains effective.\n\nLimits are 32 inputs, 128 physically held keys and 256 ownership records. Physically released records stay available for pedal cleanup; stale records are pruned from Performance snapshots only at the ownership threshold or explicit forced cleanup. Excess admission is reported to onError and dropped, never silently overwriting ownership.\n\n## Custom CC mappings\n\nts\nimport type { MidiControllerMapping } from 'opm.js';\n\nconst controllerMap: readonly MidiControllerMapping[] = [\n { controller: 16, field: 'feedback', min: 0, max: 7, ramp: 0.05 },\n { controller: 17, field: 'operatorRatios', operator: 0, min: 0.5, max: 6, ramp: 0.05 },\n { controller: 18, field: 'operatorLevels', operator: 1, min: 0, max: 2, ramp: 0.05 },\n { controller: 19, field: 'lfoRate', min: 0, max: 20, ramp: 0.05, reset: 3 },\n { controller: 20, field: 'operatorFrequencies', operator: 2, min: 100, max: 2000, ramp: 0.05, reset: null },\n];\nconst midi = createMidiAdapter(performance, access, { parts: 2, controllerMap });\n\n\nEach entry requires controller, field, min, max and ramp. Operator fields additionally require a zero-based operator index 0–3; scalar fields prohibit it. CC value 0 maps exactly to min, 127 to max, and intermediate values interpolate linearly. Endpoints must be finite, ordered and inside the engine's bounds:\n\n| Field | Range |\n| --- | --- |\n| pitch | −48..48 semitones |\n| expression, gain | 0..1 |\n| pan | −1..1 |\n| modulation | 0..2 |\n| feedback | 0..7 |\n| lfoRate | 0..20 Hz |\n| amDepth | 0..1 |\n| pmDepth | 0..1200 cents |\n| operatorLevels | 0..2 multiplier |\n| operatorRatios | 0.125..32 |\n| operatorFrequencies | 1..20000 Hz |\n\nramp is an explicit duration 0–10 seconds (0 is immediate); pitch uses glide, other fields use the engine's control ramp. glide, ramp and operatorADSR are not mapping targets. Mappings update part defaults, sounding gates (including release tails) and future notes. A tuple mapping preserves the other three effective part operator elements; per-key tuple overrides retain the Performance merge policy and may override the whole tuple.\n\nA listed CC replaces that controller's ordinary action: mapping CC1 to feedback disables CC1's default modulation action, but channel/poly pressure still control modulation. CC64/100/101/120/121/123 cannot be remapped. RPN 0 data-entry CC6/38 takes precedence over custom mappings only while selected; otherwise mappings work normally, or unmapped data entry is ignored. NRPN selectors deselect RPN before any ordinary/custom action. Controllers must be integers 0–127; at most 128 entries are accepted, with no duplicate controller or target (same field/operator). Omitting controllerMap preserves ordinary CC behavior, except the newly supported RPN sensitivity.\n\nCC121 restores every mapped target for the channel. Optional reset defines that target's baseline and must be inside the field's engine bounds (not necessarily inside min/max). Only operatorFrequencies accepts reset: null, which restores ratio mode. Without reset, the baseline is captured from getPartControls when the adapter is created, so later patch/configuration changes do not change that reset value. Reset retains unrelated tuple elements and uses each mapping's ramp. Ordinary reset behavior and input-pedal ownership remain intact.\n\nThe adapter copies and validates the complete map before adding input/state listeners or opening ports. Plain own-data records and dense own-data arrays only: accessors, unknown fields, executable array overrides, sparse arrays, invalid ranges and duplicates throw synchronously. Later mutation of the caller's map has no effect. Errors while processing messages go to onError.\n\nExample 10 connects hardware knobs CC16–19 to feedback, operator 1 ratio, operator 2 level and LFO rate; the browser sliders use the same ranges and 50 ms ramps. Knobs follow the incoming channel, sliders the selected part. Physical MIDI devices and permission dialogs require manual verification on supported browsers.\n\n\n## Support and verification\n\nWeb MIDI exists in some browsers only and needs a secure context and user permission; feature-detect and handle rejection (requestMidiAccess rejects when unavailable). test/midi.test.ts (checkout-only) drives the adapter with an injected access object against the real worklet processor: channel routing, repeated pitches, pedal deferral, malformed packets, bend/wheel/volume ranges, hot-unplug releasing only one input's keys, and dispose. No physical keyboard or browser permission flow was exercised by the automated tests.\n\n## Original physical MIDI campaign (current 1.10.0 checkout)\n\nNo physical controller, permission-dialog result or human listening original is supplied here. Permission, connection, hot-unplug, sustain, CC and bend acceptance all remain unverified. Injected adapter tests and automated browser clicks do not change that status.\n\nThe bounded local observation export below is available in the v1.10 checkout-only example, not the installed engine archive. Build and serve the matching 1.10.0 source checkout to use the capture panel. No public export/protocol API was added.\n\n1. Build and serve the matching 1.10.0 checkout over HTTPS or localhost. Use a browser that actually exposes Web MIDI; unsupported browsers remain unverified for controller operation, not a fabricated pass. Start at comfortable low output volume and click Start / restart audio.\n2. In Local physical MIDI campaign, select the actual environment and enter device model/OS, exact browser version, controller model/firmware/USB or Bluetooth connection, and output route/device volume. Avoid serial numbers or personal identifiers. For desktop UI smoke choose Desktop / automation, even when exercising synthetic messages.\n3. Select a scenario and Begin local scenario capture. For permission, begin before Connect Web MIDI. Test a fresh real prompt's grant and denial as separate runs, using the browser's permission settings between runs when necessary. If permission was already granted or no prompt appeared, say so: that is not original grant/denial-dialog coverage.\n4. Follow the on-page instructions and the criteria below. Record observed behavior and unexercised steps, not only “works.” Check the physical-action and listening declarations only after actually doing both. Choose manual pass/fail/unverified, then Finish capture / release keys. Confirm silence after release tails. Repeat every scenario under the same exact declared conditions, exporting before changing browser/device/route.\n5. Export local MIDI observations before leaving or clearing. Review displayed and exported port IDs/names before sharing; browsers may expose identifying strings. Keep the untouched original privately; share a separately identified redacted copy if needed, never overwrite the original to fill missing results.\n\n| Scenario | Required original observations / manual criteria |\n| --- | --- |\n| Permission | Actual grant and denial outcomes, whether a prompt appeared, no SysEx request, visible denial error and usable host keyboard after denial; previously granted access must be identified. |\n| Connect | Physical channel 1 mono lead / channel 2 poly pad routing, repeated equal pitches, velocity-zero note-off, note release and no stuck keys. State unsupported features instead of inferring coverage. |\n| Hot-unplug | Unplug while holding notes and pedal, silence of the disconnected input after tails, reconnect without resurrected gates. Exercise a second controller if claiming input-isolation coverage; with one input leave that subclaim unverified. |\n| Sustain | Channel 2 chord held after physical note-off under CC64; pedal-up releases it; CC123 remains pedal-aware and CC120 forces adapter-owned cleanup. Check the shared part-pedal effect on host keys; do not claim pedal is key-owned. |\n| CC | Physical CC16–19 minimum/middle/maximum and audible held-note changes, CC121 restoration to adapter-creation baselines; CC1/7/10/11 and pressure when actually available. Note unavailable messages. Knobs do not move sliders; effective snapshots, not slider positions, describe applied state. |\n| Bend | Each routed channel's full down/up ±2-semitone range, return to center, fresh notes and released tails, other channel unaffected. Record actual controller centering; audible pitch judgment is manual, not a frequency measurement. |\n\nThe export is demo-local opm-local-midi-observations, version 1, not a public API protocol. It holds at most 24 runs and 256 timestamped page observations per run, with overflow counted; additional runs are refused until export/clear. Notes are capped at 1200 characters; four condition fields and input IDs/names at 160, user agent/errors at 240, and input enumeration at 32. Only 2–3-byte channel-voice packets are observed, never SysEx or arbitrary payloads. No network request, microphone/audio recording, persistent storage or JSON import/auto-trust path is added.\n\nEvery run carries exact VERSION, declared environment/device/browser/controller/route, bounded user agent, start/end/duration, beginning/ending actual sample rate, quality/maxVoices, fixed submitted mix/tuning/stealing/interruption settings, normalized loaded lead/strings patches, effective part states/controls, adapter snapshot and CC mappings. The profile is the applied instrument-midi-1 workload, not arbitrary presets or voices. Incoming MIDI packets, port changes, permission decisions/errors and host-control actions are page observations, never automatic pass/fail or proof of audible continuity.\n\nEngine construction and capture share one explicit immutable settings policy: standard quality, 16 voices, mix gain 0.35, oldest stealing, cancel interruption, and tuning { referenceHz: 440, offsets: [128 numeric zero cents entries] }. Exported quality/voice limits and context sample rate also come from the actual instance; normalized patches and effective part controls come from loaded snapshots. Begin/end settings are main-thread snapshots at capture boundaries, not sample-accurate audio-frame measurements. The ending snapshot is taken before Finish's part/adapter release or teardown cleanup; it can still contain held keys. Cleanup is a subsequent safety action, not evidence that the captured state was already silent. Exercise and record manual release/silence criteria during the active scenario if claiming that coverage.\n\nmanualJudgment remains separate from acceptanceStatus: active, abandoned, desktop/automation, non-physical, non-listened, changed-scope or overflowed runs are acceptance-unverified. Changing declared conditions, host-selected part, octave or glide invalidates the active scope and clears declarations; start/restart/dispose/page exit finishes it unverified. Intentional MIDI CC/bend changes are scenario actions, captured with effective controls, not a new static baseline. Begin/end settings must be reviewed alongside the event sequence. Finish releases the parts; teardown removes capture listeners and revokes download URLs. Reconfigure and recapture rather than relabeling an old run.\n\nReview the original JSON directly: check format/version, counts/truncation, finished timestamps, exact workload and route, manual declarations and notes; correlate packets/port transitions with claimed steps. A declared physical pass/fail is unauthenticated local evidence, not certified hardware acceptance. There is intentionally no additional public evidence validator. Unsupported or missing steps stay unverified even if a selector says pass; a maintainer must attach reviewed original references and exact scope before upgrading any compatibility claim.\n","href":"./midi-performance.html"},{"title":"Expressive performance and optional Web MIDI · Expressive performance and optional Web MIDI","text":"Expressive performance and optional Web MIDI Expressive performance and optional Web MIDI","href":"./midi-performance.html#expressive-performance-and-optional-web-midi"},{"title":"Expressive performance and optional Web MIDI · Per-key and per-part controls","text":"Expressive performance and optional Web MIDI Per-key and per-part controls","href":"./midi-performance.html#per-key-and-per-part-controls"},{"title":"Expressive performance and optional Web MIDI · Voice budgets and priority","text":"Expressive performance and optional Web MIDI Voice budgets and priority","href":"./midi-performance.html#voice-budgets-and-priority"},{"title":"Expressive performance and optional Web MIDI · Web MIDI adapter","text":"Expressive performance and optional Web MIDI Web MIDI adapter","href":"./midi-performance.html#web-midi-adapter"},{"title":"Expressive performance and optional Web MIDI · Custom CC mappings","text":"Expressive performance and optional Web MIDI Custom CC mappings","href":"./midi-performance.html#custom-cc-mappings"},{"title":"Expressive performance and optional Web MIDI · Support and verification","text":"Expressive performance and optional Web MIDI Support and verification","href":"./midi-performance.html#support-and-verification"},{"title":"Expressive performance and optional Web MIDI · Original physical MIDI campaign (current 1.10.0 checkout)","text":"Expressive performance and optional Web MIDI Original physical MIDI campaign (current 1.10.0 checkout)","href":"./midi-performance.html#original-physical-midi-campaign-current-1100-checkout"},{"title":"Physical-device audio recovery acceptance","text":"Physical-device audio recovery acceptance # Physical-device audio recovery acceptance\n\nExample 08 (checkout-only) is a repeatable manual capture runner, not a phone certification tool. Lifecycle events and analyser peaks cannot measure audible dropouts or prove AudioWorklet CPU deadlines. Desktop Chromium automation, emulated mobile user agents and host suspend() are not physical iOS/Android acceptance. The demo and maintenance workflows below require a repository checkout; they are absent from installed tarballs.\n\n## Evidence-backed support matrix\n\nNo physical-device capture is supplied with this repository. Every physical result below is unverified, for both interruption policies. API support and desktop smoke coverage are not hardware evidence. Only attach a result to an exact device model, OS/browser version, route, observed sample rate and tested policy; do not generalize it to an OS family.\n\n| Scenario | Physical iOS Safari, cancel / preserve | Physical Android Chrome, cancel / preserve |\n| --- | --- | --- |\n| Lock / unlock | unverified / unverified | unverified / unverified |\n| App switch / return | unverified / unverified | unverified / unverified |\n| Call / system interruption | unverified / unverified | unverified / unverified |\n| Bluetooth connect / disconnect | unverified / unverified | unverified / unverified |\n| Wired / USB headset route | unverified / unverified | unverified / unverified |\n| Low Power Mode / Battery Saver | unverified / unverified | unverified / unverified |\n| Long play, at least 10 minutes | unverified / unverified | unverified / unverified |\n| Explicit 2-second main-thread stall | unverified / unverified | unverified / unverified |\n| Dispose / recreate | unverified / unverified | unverified / unverified |\n\n## Prepare and repeat\n\n1. Build the current example (npm ci, npm run build) and serve the repository. A physical phone needs a reachable HTTPS origin trusted by that device; desktop localhost alone does not expose the server to a phone. Do not bypass certificate errors or use file://. No cloud upload service is required.\n2. Use the actual physical iOS Safari or Android Chrome browser, not desktop device emulation. Set comfortable speaker/headphone volume. Enter the exact model, OS version and browser version from the device settings. Do not put personal identifiers or phone numbers in notes.\n3. Select cancel, tap Start / resume, then select a scenario and Begin scenario capture. Begin clears old sound and submits one held note plus one onset due in five audio-clock seconds. Notes remain held until interruption policy clears them or you finish/panic. Metadata, scenario and policy are captured at begin; changing a route may not change the browser's fixed context sample rate.\n4. Follow the on-page scenario instructions. Record route/battery settings, interruption duration, whether the context actually stopped, and what you heard before tapping Start again. Add timestamped dropout/replay/stuck-gate/route markers during capture (or after returning, noting that the marker is retrospective).\n5. Tap Start / resume on return when needed. No timer resumes the context. Check recovery and manual pass criteria below. A route change or app switch may not interrupt the context on a particular device: record that fact and leave interruption-policy acceptance unverified, rather than inventing a suspension.\n6. Enter notes, check the manual observation declaration only if you actually performed/listened to this run, choose pass, fail or unverified, and Finish capture / silence. Finish submits panic. A short long-play run cannot earn pass: fewer than 600 seconds is forced unverified.\n7. Repeat the same scenario with preserve: Dispose synth, change policy, Start again, then begin a new capture. Test one scenario/policy at a time. Export before reloading/navigating away or after each policy batch. At most 24 recent captures are retained; export the first device's 18 captures before starting a second device. Repeated runs are separate records, not merged claims.\n\n## Manual pass / fail criteria\n\n- Cancel, when an actual context interruption was observed: prior held notes and pending events must not return after gesture recovery. Play a fresh score after recovery to confirm new admission. Unexpected replay, duplicate/stuck gates or inability to recover a still-valid context is fail.\n- Preserve, when an actual interruption was observed: direct held-note state can continue from the paused audio clock after gesture recovery, without duplicate gates. A still-pending direct onset can become due as that clock advances; this is not wall-clock catch-up/replay. Verify the held gate can be released and a fresh score admitted. Preserve does not promise audio while the OS suspends it, nor retention after dispose or a closed context.\n- Both policies: classify intentional OS silence separately from a dropout while audio should be running. No unexpected audible gaps, stuck/duplicate notes, or unbounded replay; fresh sound and All notes off/Panic must work after recovery. Report fail with subjective markers and notes if these expectations fail. Absence of an audible problem is a manual judgment, not a measured underrun count.\n- Routes and battery saver: record both route transitions and settings. Check fresh playback and silence controls after each transition. If a connector, call or power mode cannot be exercised, leave that scenario unverified. No permission prompts or OS route selectors are automated by the page.\n- Long play: listen for at least 10 minutes; record exact duration and foreground/background conditions. Finishing early forces a claimed pass to unverified. This limited session says nothing about all-day playback, thermal throttling or other devices.\n- Main-thread stall: explicitly press the 2-second stall button and listen. The UI should pause; audio must be judged independently. This intentional busy loop is bounded and never runs automatically.\n- Dispose / recreate: Dispose must silence old sound; Start creates a fresh synth using the still-host-owned context without resurrecting old gates. A closed context cannot be certified as recovered by a button; record the failure and reload to start a separate run.\n- Streaming score: interruption stops lookahead scheduling under either policy. Resuming the context must not restart the old stream; the demo's explicit stream button is the only restart. Direct-note preserve and stream restart are different contracts.\n\n## Local evidence format and handling\n\nExport local observations downloads opm-device-observations.json; the page does not upload, persist or record microphone/PCM audio. Exports use format opm-local-device-acceptance, version 1, and contain:\n\n- Per-run declared environment/model/OS/browser and bounded user agent, begin/end timestamps, actual begin/end context sample rates, fixed interruption policy and scenario, duration, notes and subjective markers.\n- New captures also snapshot the exact package version and beginning loadProfile: stimulus revision, normalized lead patch, actual OPM quality/maxVoices, submitted mix gain/tuning/stealing settings and host monitoring gain. This describes the initial workload, not everything a user might play later; retain event history and explain any extra workload/actions in notes.\n- Bounded engine/context/user-action observations with monotonic and wall timestamps. These are objective page observations, not computed pass/fail judgments.\n- Separate manualJudgment and acceptanceStatus. Desktop/automation, active/incomplete, absent, and non-listened captures are always acceptance-unverified. A physical pass/fail is only a user-declared manual result, not independently authenticated hardware evidence.\n- Coverage for all 36 combinations (two physical browser families × nine scenarios × two policies). It references the latest finished retained capture for each cell; absent cells are unverified. Use per-run metadata when interpreting the matrix, since cells do not assert every model/version is supported.\n- Limits and eviction counters: 24 runs, 256 observations/run, 64 subjective markers/run, 1200 note characters. Old observations/runs are evicted with counts; marker overflow is explicitly rejected. Strings in observations are capped at 240 characters, object depth at four, and keys/array entries at 32. Recent lifecycle text is bounded and rendered through textContent, never HTML.\n\nKeep exported JSON locally unless you intentionally choose to share it. Review notes and device identifiers before sharing. The runner deliberately has no JSON import or auto-trust path: opening a capture never upgrades it into a verified physical result. To review an artifact, validate its format/version, bounds and counters; check device/browser versions, sample rates, ended timestamp, declared environment/listening, judgment and notes, then correlate marker timestamps with retained events. Missing/truncated evidence should remain unverified. Preserve original files rather than editing them to fill missing results.\n\n## Reviewing exported evidence with npm run device-evidence\n\nsh\nnpm run device-evidence -- opm-device-observations.json [other-device.json …]\nnpm run device-evidence -- --require-complete ios.json android.json\nnpm run --silent device-evidence -- ios.json android.json > device-review.json\n\n\nThe command (Node 22+, no network) reviews all supplied files collectively, independently of their exported acceptanceStatus and coverage. A declared physical capture needs a finished, consistent start/end/duration, declared listening, unchanged policy, recorded begin/end sample rates, exact model/OS/browser strings and a compatible-looking user agent. Long play needs at least 600 seconds. Wall-clock jumps or elapsed-marker contradictions remain unverified rather than earning duration credit. The result then takes the human's manualJudgment; user agents and form entries do not authenticate hardware.\n\nRuns are grouped by exact environment, model, OS, browser, user agent, beginning/ending sample rates, policy, package version and canonical workload snapshot. Within each exact scope, the latest finished timestamp, not filename order or run ID, selects a scenario. Unfinished retries never replace finished captures. Equal-time conflicting judgments or contradictory content for the same scoped run identity stay unverified. Repeated originals add filename/run references, not coverage. The review includes original notes, markers and observations so a maintainer can investigate; every selected cell identifies its scope and source filename plus run ID and identity.\n\nPackage/workload combinations are separate campaigns. A complete campaign needs all nine scenarios in one exact scope per environment/policy: scenarios from different models, browser versions or sample rates are never stitched into one passing scope. If several scopes qualify, a complete one is selected, then the most recently finished; all scopes and failures remain in the report and selection is warned. One iOS export and one Android export, each with 18 compatible captures, can therefore satisfy all 36 cells together. Different versions/workloads cannot fill one another's gaps. The representative campaign prefers complete coverage and is explicitly identified; inspect every campaign, not only the aggregate counts.\n\n--require-complete exits 1 unless at least one recorded package/workload campaign has all 36 declared passing cells. Legacy version-1 exports without these new optional fields still parse and retain their declared per-run results, but unknown metadata cannot establish compatibility or strict completeness. Do not edit originals to invent a version/workload: retain and review them, then recapture with the current runner. No automatic migration can reconstruct an unrecorded workload.\n\nInputs are bounded to 16 regular files, 2 MiB each, 24 runs/file, 256 observations and 64 markers/run, 1,200 note characters, unique IDs within a file, valid counters/timestamps, known scenarios/policies, plausible sample rates and bounded own JSON data. Malformed structure is rejected; contradictory declared timing is marked unverified with reasons. A full device needs 18 captures, so export before the 24-run eviction limit. The stdout review is a separate artifact; inputs are never rewritten or uploaded.\n\nWhat this does not do: authenticate hardware, measure underruns, or replace reading markers, notes and truncated-event counters. Complete means “passing declared coverage under these exact recorded conditions,” never certification of iOS, Android or a phone family. The device evidence regression tests (checkout-only) use synthetic validator inputs, not physical results. To graduate the support matrix, supply original captures from actual iOS Safari and Android Chrome devices, have a maintainer review each scenario/policy and its exact conditions, and attach the reviewed artifacts and scope to the documented result. No physical devices or captures were supplied, so the matrix above remains unverified.\n\n## Desktop smoke workflow (not physical acceptance)\n\nAfter the integration build, open example 08 on desktop localhost. Choose Desktop / automation and actual desktop metadata, then:\n\n1. Export with no captures: all 36 coverage cells must be unverified.\n2. Start, begin lock/unlock, add a dropout marker with a literal HTML-looking note (for example \u003cb>heard gap\u003c/b>), export while active: acceptance remains unverified and text is not interpreted as HTML.\n3. Check manual observation and select pass, finish, export: manualJudgment can be pass but desktop acceptanceStatus and physical coverage remain unverified; the held sound is silenced.\n4. Start another run for main-thread stall, press the explicit stall control and finish unverified. Observe the bounded stall begin/end events. Repeat cancel/preserve only after Dispose; verify policy metadata follows the engine configuration.\n5. Select long play and finish before 10 minutes: it cannot earn physical pass. Exporting never sends a fetch/XHR; only Blob-download URLs are created. Refresh clears all captures.\n6. Exercise the separate mixed-event stream and 60-second/96-kHz bounded chunk renderer controls. They are desktop API smoke evidence only, not real-device long-play evidence.\n\nNo physical result should be added to the matrix without an actual manual capture and review. Automated smoke output must be labeled desktop-only.\n\n## Executable current-version campaign\n\nFor the matching v1.10 checkout (package 1.10.0), retain originals from one physical iOS Safari device and one physical Android Chrome device. Build and serve this source checkout for the checkout-only evidence page and commands. Each device needs the nine rows above under cancel and preserve: 18 runs per device / 36 cells, not one desktop run relabeled as a phone. Keep one exact version/workload scope per device/policy. Build-generated files and package version must match; retain the checkout revision separately with originals so equal package versions do not conceal different source.\n\nBefore each batch, Dispose and apply the intended profile/stealing/policy, tuning, mix gain and waveform, then Start and begin. Read the exported beginning loadProfile instead of assuming the visible selector is the applied engine. Do not change quality/policy/workload during a scenario or combine changed scopes to fill a campaign. Route-transition scenarios should describe before/after routes in markers and notes; unrelated route/battery/headphone or workload changes need a new separately scoped batch. Keep the same stimulus for the nine baseline scenarios; chord/control/stream experiments are supplemental evidence, not replacements for missing baseline cells.\n\nKeep an adjacent local campaign record listing exact checkout revision, filenames/run IDs, declared device/browser/OS, routes and volume, sample rates, profile/voice limits, policy, elapsed listening duration and each scenario's observed/absent interruption. Record real calls, connectors, Bluetooth transitions and battery modes only when available. For long play, listen continuously for at least 600 seconds and note foreground/background timing and thermal/battery conditions. Intentional OS silence and a real-time audible dropout are different findings. No phone, headset, call or power-mode access means that cell remains unverified; do not substitute desktop suspension or a timer.\n\nExport each 18-run device batch before the 24-run retention bound. Preserve original JSON and separately generated review output; use npm run device-evidence -- --require-complete ios.json android.json only as a declared-coverage review, then inspect exact scopes, markers, notes and event truncation. A successful review does not authenticate phones or replace human listening. A maintainer may update a matrix cell only with a reviewed original artifact reference and its exact tested conditions; changed versions/profiles/routes require new evidence.\n\nPhysical MIDI is a separate optional capability and campaign: use the instrument workflow, not this recovery runner. Browser Web MIDI absence is not a synthesis failure and a phone recovery capture is not proof of a keyboard, permission prompt or hot-unplug result.\n\nRemaining blocker: no physical iOS/Android phones, real interruption/route accessories, physical MIDI controller, or original human observations were supplied. All physical cells remain unverified. Code/UI automation can verify capture/export behavior only.\n","href":"./mobile-acceptance.html"},{"title":"Physical-device audio recovery acceptance · Physical-device audio recovery acceptance","text":"Physical-device audio recovery acceptance Physical-device audio recovery acceptance","href":"./mobile-acceptance.html#physical-device-audio-recovery-acceptance"},{"title":"Physical-device audio recovery acceptance · Evidence-backed support matrix","text":"Physical-device audio recovery acceptance Evidence-backed support matrix","href":"./mobile-acceptance.html#evidence-backed-support-matrix"},{"title":"Physical-device audio recovery acceptance · Prepare and repeat","text":"Physical-device audio recovery acceptance Prepare and repeat","href":"./mobile-acceptance.html#prepare-and-repeat"},{"title":"Physical-device audio recovery acceptance · Manual pass / fail criteria","text":"Physical-device audio recovery acceptance Manual pass / fail criteria","href":"./mobile-acceptance.html#manual-pass--fail-criteria"},{"title":"Physical-device audio recovery acceptance · Local evidence format and handling","text":"Physical-device audio recovery acceptance Local evidence format and handling","href":"./mobile-acceptance.html#local-evidence-format-and-handling"},{"title":"Physical-device audio recovery acceptance · Reviewing exported evidence with npm run device-evidence","text":"Physical-device audio recovery acceptance Reviewing exported evidence with npm run device-evidence","href":"./mobile-acceptance.html#reviewing-exported-evidence-with-npm-run-device-evidence"},{"title":"Physical-device audio recovery acceptance · Desktop smoke workflow (not physical acceptance)","text":"Physical-device audio recovery acceptance Desktop smoke workflow (not physical acceptance)","href":"./mobile-acceptance.html#desktop-smoke-workflow-not-physical-acceptance"},{"title":"Physical-device audio recovery acceptance · Executable current-version campaign","text":"Physical-device audio recovery acceptance Executable current-version campaign","href":"./mobile-acceptance.html#executable-current-version-campaign"},{"title":"Publication and package verification","text":"Publication and package verification # Publication and package verification\n\nGitHub v1.10 distribution targets package 1.10.0 with portable Arrangement projects, opt-in MIDI expression conversion and checkout-only capacity/campaign tooling; it does not publish to npm. The release location and installable opm.js-1.10.0.tgz are the publication targets, not proof that remote publication has completed. Once uploaded, install the downloaded archive from an application with npm install /actual/path/to/opm.js-1.10.0.tgz, without building or installing development tools. The release record must retain the actual reviewed commit, CI run and archive digest.\n\nThe historical v1.8 tag points to d793d69056c68a971ede1af988338412687227fd. Its 1.8.0 archive is 491,851 bytes, with SHA-256 bee862d5b3ccaa7c4e8ff959583873c78a8ca8a9f56f4d8b039e490e498e2587; it was built from that tag, installed/exercised, and downloaded back byte-for-byte. Its original documentation remains unchanged. The separately versioned 1.8.1 package includes the synchronized guides and its own regenerated version assets; do not confuse the two artifacts or their digests. Preserve existing tags and assets.\n\n## No automated npm publication\n\nThis repository does not ship a GitHub Actions workflow that publishes to npm. A GitHub release, tag, or passing CI job is not registry publication. An authorized maintainer must authenticate and publish separately; GitHub authentication does not grant npm authentication. Do not create a token or log credentials as part of local checks.\n\nTrusted publishing, npm ownership/2FA, environment reviewers and protected tags are external npm/GitHub settings. They are not configured by files in this checkout.\n\nregistry-verify's online provenance check still requires a published SLSA v1 attestation whose workflow identity is .github/workflows/npm-publish.yml, matching the removed workflow's intended identity. This is not evidence that any package was published by it. A package published another way will fail that check. A future publication needs an explicitly authorized provenance-bearing path and a matching reviewed verifier; do not bypass the check or treat local validation as registry acceptance.\n\n## Local and post-publication commands\n\nFrom a source checkout, validate a reviewed artifact without contacting npm or claiming registry availability:\n\nsh\nnpm run registry-verify -- --local /absolute/path/opm.js-1.10.0.tgz 1.10.0\n\n\nAfter publication, replace the SHA below with the actual reviewed release commit:\n\nsh\nnpm run registry-verify -- /absolute/path/opm.js-VERSION.tgz VERSION FULL40CHARACTERCOMMITSHA\n\n\nThe online command is read-only with respect to the registry: it cannot publish, configure trusted publishers or authenticate an account. Downloads have explicit byte/time budgets and are restricted to HTTPS registry.npmjs.org; its isolated installation is removed afterwards. It requires tar, npm with provenance verification support, and published SLSA v1 GitHub provenance whose workflow path is .github/workflows/npm-publish.yml. Retain its digest, file list and run URL in the public review record.\n\nFirst-package bootstrap, npm ownership/2FA, trusted-publisher configuration, GitHub environment reviewers/protected tags, publication authorization and actual registry verification remain external requirements. The public registry returned 404 for opm.js@1.8.0 during the package audit; registry integrity/signatures/provenance therefore remain unverified. Do not substitute a local PASS, stale release review, or manual unprovenanced bootstrap for a verified provenance-bearing publication. GitHub distribution of 1.8.0 does not establish npm publication. Run the security review and package gates against the exact candidate before any newly authorized release.\n","href":"./publishing.html"},{"title":"Publication and package verification · Publication and package verification","text":"Publication and package verification Publication and package verification","href":"./publishing.html#publication-and-package-verification"},{"title":"Publication and package verification · No automated npm publication","text":"Publication and package verification No automated npm publication","href":"./publishing.html#no-automated-npm-publication"},{"title":"Publication and package verification · Local and post-publication commands","text":"Publication and package verification Local and post-publication commands","href":"./publishing.html#local-and-post-publication-commands"},{"title":"Replayable score projects","text":"Replayable score projects # Replayable score projects\n\nAvailable since v1.9. A version 1 ScoreProject stores musical events, a tempo map, meter, named complete voice snapshots and synthesis settings in one self-contained JSON file. The pure APIs import from opm.js/core in Node without an AudioContext, DOM or Worker.\n\n## Create, save and load\n\nts\nimport { parseScoreProject, serializeScoreProject } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst project = parseScoreProject({\n version: 1,\n voices: { lead: brass },\n events: [\n { type: 'note', id: 1, voice: 'lead', note: 60, beat: 0, duration: 4 },\n { type: 'control', id: 1, beat: 2, controls: { expression: 0.6, ramp: 0.2 } },\n ],\n tempoMap: [{ beat: 0, bpm: 90, curve: 'linear' }, { beat: 4, bpm: 120 }],\n timeSignature: { numerator: 4, denominator: 4 },\n settings: { sampleRate: 44100, quality: 'standard', maxVoices: 8,\n mixGain: 0.35, stealing: 'release-first', tuning: { referenceHz: 440 } },\n});\nconst json = serializeScoreProject(project);\nconst replay = parseScoreProject(json);\n\n\nThe parser accepts a JSON string or a plain own-data object. version, events and voices are required. Omitted tempo, meter and settings normalize to 120 BPM, 4/4, 44100 Hz, standard, eight voices, mixGain 1, oldest stealing and 440 Hz with zero tuning offsets. Canonical output contains all settings and the 128-entry tuning table. Voice names are sorted, controls use stable key order, and saved defaults are explicit. Event array order is retained for deterministic simultaneous-event ordering.\n\nEvery note uses a stored voice name. Omitted note voice means brass, which must also be stored. Inline patches are rejected inside projects; standalone compileBeatSequence accepts them. Voice definitions normalize through the existing voice-version validator, including supported legacy patch inputs; project files themselves must specify version 1. No external bank or URL is referenced.\n\nReturned objects, arrays, voices, controls and settings are detached and frozen. Unknown fields, inherited fields, accessors, invalid versions, malformed meters/tempo maps, invalid controls/settings, duplicate note IDs and missing note/voice references throw. There is no prototype merge or invocation of JSON serialization hooks from caller data. Do not pass Proxies or executable objects as project data.\n\nBudgets: 8 MiB total serialized project, 128 stored voices with a 256 KiB normalized bank budget, 65,536 events and 86,400 quarter-note beats. Compiled seconds must fit the existing 24-hour long-sequence horizon. Array holes/extra properties are rejected. These budgets do not enlarge worklet queues or full-buffer rendering limits.\n\n## Portable Arrangement projects\n\nAvailable since v1.10, ArrangementProject is a separate version 1 definition, not an extension of ScoreProject v1. Existing score files keep their schema and replay behavior. The APIs parseArrangementProject(source: string | object) and serializeArrangementProject(project) ship in package 1.10.0 and are exported from both opm.js and opm.js/core; core parsing needs no DOM or Web Audio declarations. Interactive save/load examples remain checkout-only.\n\nts\nimport { parseArrangementProject, serializeArrangementProject } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst adaptive = parseArrangementProject({\n version: 1,\n voices: { pad: brass, lead: brass },\n settings: { sampleRate: 44100, maxVoices: 8, mixGain: 0.2 },\n tempoMap: [{ beat: 0, bpm: 96 }],\n timeSignature: { numerator: 3, denominator: 4 },\n layers: [\n { name: 'bed', length: 12, gain: 0.6, voicePriority: 20,\n events: [{ type: 'note', id: 1, beat: 0, duration: 12, note: 48, voice: 'pad' }] },\n { name: 'melody', length: 3, gain: 0.8, voicePriority: 100, events: [\n { type: 'note', id: 1, beat: 0, duration: 2, note: 72, voice: 'lead' },\n { type: 'control', id: 1, beat: 1, controls: { expression: 0.5, ramp: 0.2 } },\n ] },\n ],\n sections: [{ name: 'calm', layers: ['bed'] }, { name: 'battle', layers: ['bed', 'melody'] }],\n initialSection: 'calm',\n});\nconst saved = serializeArrangementProject(adaptive);\nconst restored = parseArrangementProject(saved);\n\n\nRequired fields are version, voices, layers, sections and initialSection. Tempo, meter and synthesis settings use exactly the score-project defaults; each layer normalizes gain to 1 and voicePriority to 0. Named voices, control-property order and all defaults are canonical; layer, section, member and event array order is retained. Every note references a stored voice; note IDs are unique within each layer, so separate layers may reuse IDs. Notes must start inside the layer's loop; durations and owned controls may extend beyond its end. Authored control gain is reserved for the layer and rejects; use expression.\n\nBoth live createArrangement and portable parsing use the same pure definition validator. They reject duplicate layer/section names, unknown references/fields, sparse arrays, accessors, invalid controls and out-of-range values. Repeated section members normalize to one membership in first-occurrence order, preserving existing live semantics. The returned layers, sections, events, voices, tempo/meter and settings are detached and deeply frozen. Projects allow 1–16 layers, at most 32 sections, 65,536 events in total, loop lengths 1/1024–256 quarter notes, priorities 0–127 and gains 0–1. The required initial section must exist (it may have no active layers). The 8-MiB project / 128-voice / 256-KiB normalized-bank budgets and beat/compiled-second horizons above also apply; the corresponding exported constants are MAXARRANGEMENTPROJECTBYTES and MAXARRANGEMENTPROJECTVOICES. Hosts must check upload/download bytes before buffering.\n\n### Load into the live API\n\nts\nimport { OPM, createArrangement } from 'opm.js';\n\n// Inside a browser click handler; dispose the previous owned arrangement/synth first.\nconst synth = new OPM({ ...restored.settings });\nsynth.replaceVoiceBank([]); // Remove implicit brass: a project may already store 128 names.\nfor (const [name, voice] of Object.entries(restored.voices)) synth.loadVoice(name, voice);\nconst music = createArrangement(synth, {\n layers: restored.layers, sections: restored.sections, initialSection: restored.initialSection,\n tempoMap: restored.tempoMap, timeSignature: restored.timeSignature,\n onError: error => console.error(error),\n});\nawait music.start(); // Parsing/loading alone never starts audio.\nmusic.switchSection('battle', { quantize: 'bar', fade: 0.8 });\nmusic.setLayerGain('bed', 0.4, { quantize: 'beat', fade: 0.5 });\n// When this host is finished:\nmusic.dispose();\nawait synth.dispose();\n\n\nUse the complete current built distribution for this example. A browser may choose a different actual sample rate, or reject a requested context/rate; surface that failure rather than silently substituting settings. On host teardown, dispose the old arrangement before replacing its owned synth; do not close a context borrowed from another component.\n\nThe saved object is an authored replay definition. It does not store the current musical cursor, scheduled changes, in-progress fades, note IDs, oscillator/envelope/LFO/filter history, or a DSP checkpoint. Loading starts the definition at beat 0 and its initialSection only after a user gesture. There is no serializer for live Arrangement objects; keep the authored definition separately and explicitly commit host edits into it.\n\nIn the current checkout, build and serve examples/adaptive.html: switch sections, toggle a layer, change a gain target or accelerate, then Save definition → Load arrangement JSON → Start. The page saves requested targets and the edited section definition (including edits whose musical boundary is still pending), not the pending transition itself. Loading disposes prior owned playback, rebuilds controls for arbitrary valid layer/section names, and stays stopped. Reset demo intentionally replaces the loaded definition and applies the selected swing without playing. Playback, subsequent switches and fades reuse the saved voices/settings; no code edits are needed.\n\nThe adaptive page owns its context and attenuates monitoring to 16% separately from the saved synthesis mixGain; saved settings are not weakened or silently rewritten for playback. Context/rate initialization failures remain visible.\n\n## Replay with Transport\n\nts\nimport { OPM, createTransport } from 'opm.js';\n\n// Create/resume this context inside a user gesture in a browser.\nconst context = new AudioContext({ sampleRate: replay.settings.sampleRate });\nconst monitor = context.createGain();\nmonitor.gain.value = 0.16;\nmonitor.connect(context.destination);\nawait context.resume();\nconst opm = new OPM({ ...replay.settings, context, destination: monitor });\nfor (const [name, voice] of Object.entries(replay.voices)) opm.loadVoice(name, voice);\nconst transport = createTransport(opm, replay.events, {\n tempoMap: replay.tempoMap,\n timeSignature: replay.timeSignature,\n onError: error => console.error(error),\n});\nawait transport.start();\n// Stop the owned score, then release the synth and the owned context when done.\ntransport.dispose();\nawait opm.close();\nmonitor.disconnect();\nawait context.close();\n\n\nTransport retains musical timing for restart/tempo edits. The actual browser sample rate may differ from the requested rate. Host monitoring attenuation is deliberately separate from saved synthesis mixGain.\n\n## Node: load → compile → chunk WAV\n\nts\nimport { readFile, open } from 'node:fs/promises';\nimport { parseScoreProject, compileBeatSequence, renderSequenceChunks,\n createWavEncoder } from 'opm.js/core';\n\nconst score = parseScoreProject(await readFile('piece.opm.json', 'utf8'));\nconst voices = new Map(Object.entries(score.voices));\nconst events = compileBeatSequence(score.events, { tempoMap: score.tempoMap, voices });\nconst chunks = renderSequenceChunks(events, {\n ...score.settings, voices, chunkFrames: 4096,\n maxFrames: score.settings.sampleRate * 120,\n});\nconst wav = createWavEncoder({ sampleRate: score.settings.sampleRate,\n channels: 2, format: 'pcm16', totalFrames: chunks.capacity.frames });\nconst file = await open('piece.wav', 'w');\ntry {\n // FileHandle.writeFile completes each byte chunk before the next render step.\n await file.writeFile(wav.header());\n for (const chunk of chunks) await file.writeFile(wav.encode({ left: chunk.left, right: chunk.right }));\n await file.writeFile(wav.finalize());\n} finally {\n chunks.cancel();\n await file.close();\n}\n\n\ncompileBeatSequence(events, { tempoMap?, bpm?, voices? }) returns validated second-based SequenceEvent[], preserving named voice references. Supply the same registry to the render consumer. Its default tempo is 120 BPM; tempoMap takes precedence over bpm. A gate's duration is the integrated end time minus integrated onset time, including all tempo ramps and steps it crosses. Control ramp/glide durations remain seconds, not beats. IDs and stable input ordering are preserved. The existing full-buffer renderSequence and browser renderSequenceInWorker consume the same compiled events/settings; existing queue/render capacity limits still apply.\n\nThe opm.js/core and opm.js/midi-file declarations can be consumed by Node TypeScript projects without DOM or Web Audio globals. This boundary is checked separately from the browser API declarations.\n\nChunk arrays are borrowed; encode/write each chunk before advancing. A failed write can leave a partial file: use a temporary output and rename only after successful finalization if atomic replacement is required. Rendering errors and device output are different evidence: deterministic WAV tests do not certify physical playback or subjective listening quality.\n\n## Original composition showcase\n\nOpen examples/showcase.html over localhost or HTTPS after building. Harbor at First Light is a D-major/B-minor 4/4 miniature with a glass lead, a middle countermelody and a slowing return. Lanterns on the Stair is an E-minor 3/4 miniature with a chromatic dominant and an open-ninth ending. Both melodies and arrangements are original to this demo, not copied songs.\n\nPlay/Stop uses the public Transport API and a low 16% host monitor gain. Render WAV streams PCM16 in a Worker, with a 90-second/32-MiB demo download budget. Save project writes canonical JSON; Load project validates before replacing the current score. Reload either saved composition, play it and render it again to exercise replay. Offline export preserves synthesis mixGain, not the extra host monitoring gain. Stop also cancels an active export. No physical iOS/Android or human listening outcome is asserted here.\n","href":"./score-projects.html"},{"title":"Replayable score projects · Replayable score projects","text":"Replayable score projects Replayable score projects","href":"./score-projects.html#replayable-score-projects"},{"title":"Replayable score projects · Create, save and load","text":"Replayable score projects Create, save and load","href":"./score-projects.html#create-save-and-load"},{"title":"Replayable score projects · Portable Arrangement projects","text":"Replayable score projects Portable Arrangement projects","href":"./score-projects.html#portable-arrangement-projects"},{"title":"Replayable score projects · Load into the live API","text":"Replayable score projects Load into the live API","href":"./score-projects.html#load-into-the-live-api"},{"title":"Replayable score projects · Replay with Transport","text":"Replayable score projects Replay with Transport","href":"./score-projects.html#replay-with-transport"},{"title":"Replayable score projects · Node: load → compile → chunk WAV","text":"Replayable score projects Node: load → compile → chunk WAV","href":"./score-projects.html#node-load--compile--chunk-wav"},{"title":"Replayable score projects · Original composition showcase","text":"Replayable score projects Original composition showcase","href":"./score-projects.html#original-composition-showcase"},{"title":"Sound design workflow","text":"Sound design workflow # Sound design workflow\n\nExample 12 (checkout-only) is a browser FM designer built only on the public API (OPM, playNote, updateNote). It is a demo helper, not part of the engine. Start it over HTTPS or localhost, click Start, then hold a note (button or Space) and edit while it sounds. Begin at a low device volume: host gain defaults to 0.15 and is capped at 0.3.\n\n## What you can edit\n\n| Area | Controls |\n| --- | --- |\n| Topology | Algorithm 0–7 with a live signal graph (arrows run from modulator to recipient; carriers reach the output), operator-1 feedback 0–7, modulation index |\n| Operators | Ratio, level, detune, waveform, noise hold rate (20–20000 Hz) and four-stage ADSR for each operator |\n| LFO | Rate, AM depth, PM cents, waveform, delay, note/global sync, phase, and a 0–1 AM and PM target weight per operator (the selective-LFO fields) |\n| Envelopes | A dB-versus-time plot of each operator for a chosen gate length. It evaluates the engine's own envelopeAt, so segments that are straight in dB are exponential in amplitude, and −96 dB is the engine floor. |\n| Starting points | Every bundled recipe, with its provenance, intended register, velocity range and host trim shown beside it |\n| Audio | DSP profile (eco / standard / high), note, velocity, host gain |\n\n## Live edits versus retriggers\n\nA held note is edited natively where the engine has a live control, and retriggered otherwise:\n\n| Edit | Behavior while a note is held |\n| --- | --- |\n| Operator ratio | operatorRatios ramp (20 ms) |\n| Any ADSR stage | operatorADSR, re-anchored at the current dB level |\n| Feedback | feedback ramp (20 ms) |\n| LFO rate, AM depth, PM depth | lfoRate, amDepth, pmDepth ramp |\n| Algorithm, selective target weights, level, detune, operator/LFO waveform, noise rate, delay/sync/phase, modulation index | The note is released and retriggered, because these are fixed when a note is admitted |\n| Name | No audio change |\n\nThe envelope plot shows a fresh note. Key-rate scaling, pitch envelopes, velocity and the LFO can alter an actual note, and a live ADSR edit starts from the sounding level, so the plot is a guide rather than an oscilloscope.\n\n## Patch JSON\n\nThe page exports and imports one canonical version 7 patch (at most 16 KiB). Import uses the strict single-voice path: unknown fields, accessors, non-finite numbers and out-of-range values are rejected visibly, and legacy versions 1–6 are normalized to version 7. The exported file can be passed straight to opm.playNote({ voice: patch, note: 60 }) or loaded with opm.loadVoice(name, patch). Nothing is uploaded; use the bank tools for many voices at once.\n\nChoose noise for an unpitched deterministic held-noise oscillator; noise rate is ignored for periodic shapes. Ratio/fixed-Hz/live pitch controls do not pitch noise, but envelopes, levels, velocity, key scaling and AM still apply. Noise can modulate or act as a carrier on any operator, an OPM.js extension rather than a hardware-fidelity claim. Square/saw (polyBLEP) and half (polyBLAMP) are edge-corrected, but they and noise are not alias-free despite per-voice oversampling. The recipe selector automatically includes original noise-snare, noise-hihat and explosion starting points; see oscillator formulas.\n\n\n## Designing with selective LFO\n\nBy default the LFO touches every operator. Set pmTargets / amTargets to confine it:\n\n- PM on a carrier wobbles the pitch you hear; PM on a modulator changes brightness without vibrato.\n- AM on a carrier is tremolo of that carrier; AM on a modulator moves sideband strength.\n- A weight of 0 leaves an operator untouched; fractional weights scale the depth for that operator.\n\ntidekeys, emberbass and orbit_pad in the voice quality guide are worked examples, and the audition page (checkout-only) can A/B them against other recipes with a held ratio/feedback/ADSR control phrase.\n\n## Limits\n\nThe designer previews one note at a time on the main OPM instance of the page. It does not predict polyphonic loudness; leave headroom when chords are played (see the suggested polyphony and trim in the usage table). No listening judgment is built in.\n","href":"./sound-design.html"},{"title":"Sound design workflow · Sound design workflow","text":"Sound design workflow Sound design workflow","href":"./sound-design.html#sound-design-workflow"},{"title":"Sound design workflow · What you can edit","text":"Sound design workflow What you can edit","href":"./sound-design.html#what-you-can-edit"},{"title":"Sound design workflow · Live edits versus retriggers","text":"Sound design workflow Live edits versus retriggers","href":"./sound-design.html#live-edits-versus-retriggers"},{"title":"Sound design workflow · Patch JSON","text":"Sound design workflow Patch JSON","href":"./sound-design.html#patch-json"},{"title":"Sound design workflow · Designing with selective LFO","text":"Sound design workflow Designing with selective LFO","href":"./sound-design.html#designing-with-selective-lfo"},{"title":"Sound design workflow · Limits","text":"Sound design workflow Limits","href":"./sound-design.html#limits"},{"title":"Bounded sequences","text":"Bounded sequences # Bounded sequences\n\nprepareSequence, renderSequence, and playSequence keep their convenience limits: 60 seconds, 128 notes, and 256 reserved scheduling slots (a note reserves onset and automatic release). Full-buffer rendering additionally caps output at 4,000,000 stereo frames. A 60-second score at96kHz therefore requires the chunk API, not a larger full-buffer budget.\n\n## Offline chunks\n\nImport prepareLongSequence, estimateSequenceCapacity, and renderSequenceChunks from opm.js/core. Long input is limited to 24 hours including note gates, and 65,536 input events. Generated automatic releases are additional bounded events. Note IDs are unique positive safe integers; controls/stops must reference a score note. Own-data dense arrays/plain objects only; accessors, unknown fields, malformed voices, controls, and oversized scores reject eagerly. Prepared patches and operator-level control tuples are detached immutable snapshots.\n\nts\nconst capacity = estimateSequenceCapacity(score, { sampleRate: 96000, chunkFrames: 4096 });\nconst render = renderSequenceChunks(score, {\n sampleRate: 96000, chunkFrames: 4096, maxFrames: capacity.frames, signal,\n});\nfor (const chunk of render) {\n // Consume synchronously before advancing, or await a sink in this ordinary for loop.\n await sink.write(chunk.left, chunk.right, chunk.offset);\n}\nconsole.log(render.diagnostics);\n\n\nNo aggregate PCM is allocated: two reusable Float32 buffers, chunkFrames each (integer1..65,536, default4,096), plus bounded score/event/synth state. left/right are borrowed until the next next(); copy if retention is necessary. The final chunk exposes only its valid frames. offset and frames describe contiguous stereo output. Iterator return()/breaking a loop and cancel() terminate without further advancement. An aborted native AbortSignal throws AbortError on the next advancement before rendering; later calls return done. Cancellation granularity is one chunk, not an individual sample.\n\nCapacity reports total frames/PCM bytes, reusable-buffer bytes, input-event/note counts, reserved slots, convenience eligibility, and explicit limits. It validates without rendering or allocating output PCM. Output includes ceil-rounded note gates, note-scaled operator release times, and a10ms tail margin. maxFrames is an optional hard cumulative-work budget checked eagerly; the absolute bound is the24-hour horizon plus maximum10-second release and10ms tail. Cumulative diagnostics expose rendered frames, consumed scheduling events (including cancelled events), and DSP errors. Concatenating chunks matches the same full-buffer render exactly. Ordering is stop→note→control at equal frames; controls preceding onset clamp to that onset, while an early/tied stop cancels its target.\n\nfullBufferAllowed includes the 4,000,000-frame and convenience score budgets. singleBatchAllowed includes the 60-second/128-note/256-slot limits independent of PCM size. streamAllowed reports default 0.2-second-window density preflight eligibility: peakWindowSlots ≤256 and peakWindowNotes ≤128, with origin zero at the estimated sample rate. It is not an admission guarantee: outstanding live IDs are also capped at runtime and unrelated worklet clients consume capacity. Nondefault horizons are preflighted by streamSequence itself. limits names these budgets as fullBufferFrames, batchNotes, batchSlots, longEvents, longSeconds, chunkFrames, streamHorizonSeconds, streamSlots, and streamNotes. Estimation allocates bounded score/queue snapshots but no Synth voice pool or PCM.\n\nThis is pull-based synchronous DSP. Yield between chunks or use a Worker for responsive applications. A24-hour maximum is a safety ceiling, not a promise of inexpensive rendering. Streaming an encoded file requires a sink/encoder that supports incremental output; encodeWav remains a full-buffer convenience helper.\n\n## Mixed real-time lookahead\n\nImport streamSequence from opm.js. It eagerly validates the complete long score before start() can initialize audio. Events use the same relative-time note/control/stop shapes as playSequence.\n\nts\nconst stream = streamSequence(opm, score, {\n horizon: 0.2, interval: 0.025, maxSlots: 256,\n onError: error => console.error(error),\n});\nawait stream.start(); // Invoke from an appropriate user gesture.\n// Later: stream.stop() or stream.dispose().\n\n\nThe optional absolute at is current time through60 seconds ahead when starting. Lookahead horizon is0.01..10 seconds; timer interval is0.001..horizon/2. maxSlots is1..256 and bounds owned queued submissions, not the whole shared worklet. Whole-score window density is preflighted (also against actual sample-rate frames on start). Each window and outstanding live/pending IDs are bounded; at most128 owned IDs remain outstanding. Other users of the shared worklet still consume its capacity, so observe rejection events/onError rather than assuming admission. Notes use held gates and separate automatically scheduled releases, allowing durations beyond60 seconds without enlarging the direct-note contract.\n\nScore→runtime IDs survive across windows; pre-onset controls wait for their target's admission and apply at onset. ids is a detached snapshot of live/pending IDs, not a historical admission report. Terminal note events prune IDs and queued bookkeeping. stop() cancels only this stream's pending/active IDs, never unrelated notes. Reset and context interruption/suspension terminate the stream, even when the host preserves unrelated direct-note state. Stop/dispose is idempotent and cannot command a replacement node. Start is one-shot; a cancelled score requires a fresh handle. AbortSignal cancels playback, including asynchronous startup. pump() can be called explicitly by a host clock; automatic timers remain enabled.\n\nA missed score event after a timer stall terminates via onError instead of silently replaying stale notes. Admission failures terminate and cancel owned IDs. No browser timer can guarantee uninterrupted physical-device playback; use the sequence example and physical acceptance scenarios for device evidence.\n\n## Restartable musical transport\n\ncreateTransport from opm.js adds a musical cursor without changing the one-shot sequence APIs. BeatSequenceEvent uses beat instead of time; note duration is also in quarter-note beats. Controls retain their existing second-based ramp/glide durations. The score is detached and validated eagerly, with at most65,536 events and86,400 beats including gates. IDs are unique positive safe integers, and stops/controls must reference a score note.\n\nts\nconst transport = createTransport(opm, [\n { type: 'note', id: 1, beat: 1, duration: 3, note: 60 },\n { type: 'control', id: 1, beat: 2, controls: { expression: 0.5, ramp: 0.1 } },\n], {\n tempoMap: [{ beat: 0, bpm: 120 }, { beat: 2, bpm: 90 }],\n timeSignature: { numerator: 4, denominator: 4 },\n loop: { enabled: true, from: 0, to: 4 },\n onError: error => console.error(error),\n});\nawait transport.start(); // Start/resume from a user gesture.\ntransport.pause(); // Preserve position; cancel owned gates/queued controls.\ntransport.seek(2); // Remain paused; reconstruct when explicitly resumed.\nawait transport.resume();\ntransport.setTempo(100); // Preserve current beat; replace future tempo points.\ntransport.setLoop({ enabled: false, from: 0, to: 4 });\nconsole.log(transport.position, transport.snapshot.musicalPosition);\ntransport.stop(); // Reset cursor to beat zero; handle remains restartable.\ntransport.dispose(); // Permanent helper teardown; does not close OPM.\n\n\nTempo defaults to120 BPM. BPM is finite in1..1,000; a supplied map contains1..1,024 own-data points, begins at beat zero, and increases strictly. Points default to step tempo; curve: 'linear' ramps BPM per beat to the next point’s BPM or an explicit endBpm (1..1,000). endBpm requires a linear curve, and the final point cannot be linear. Nonfinite derived slopes reject. beatsToSeconds integrates60/BPM across every boundary, using the logarithmic integral for ramps; secondsToBeats is its inverse. setTempoMap replaces the complete map at the current cursor, not at score zero. setTempo retains earlier points, truncates any active ramp at its interpolated current BPM, inserts a constant point at the current beat and removes later points. See tempo curves and grids. Edits while running cancel/rebuild owned scheduling immediately; edits while paused/stopped stay inactive. Edits during pending startup invalidate that attempt and require explicit start/resume again.\n\nbeatToBarBeat and barBeatToBeat use one-based bars and fractional one-based beats in a fixed signature. The numerator is1..32 and denominator is1,2,4,8,16 or32; for6/8, one displayed beat equals half a quarter-note beat. snapshot contains frozen position/meter/tempo/loop data and state (stopped, starting, running, paused, disposed). position is in quarter-note beats; running becomes true only after asynchronous audio startup. ids is a detached live/pending score→runtime map; overlapping loop tails may map a score ID to its newest occurrence, not every owned tail.\n\nSeek is bounded by the score end (or the enabled loop end, whichever is later). Loop intervals are finite with0≤from\u003cto≤86,400 and may contain silence beyond the score. Playback before the interval proceeds until to, then wraps to from. Starting/seeking at or beyond an enabled loop's to normalizes to from. Loop seams are scheduled on AudioContext frames, not timer callbacks. Every iteration has distinct runtime IDs; old gates terminate at to, and controls outside that iteration are not submitted. Natural release tails can overlap the next iteration.\n\nFor notes admitted in the current segment, controls tied to release or later in the release tail are still scheduled, matching sequence/direct-note semantics. This includes expression/timbre/ADSR edits and controls at the final non-looping score endpoint; score duration includes control timestamps independently of note gates. Loop control intervals remain half-open at to, so an endpoint control does not leak into the next iteration. Natural completion stops admission but retains already scheduled controls and owned release-tail IDs until their terminal events; it does not perform scoped teardown. A seek/resume destination after a note's gate has ended never resurrects that release tail.\n\nRestart semantics: seek, resume, tempo/loop edits and loop wraps reconstruct crossing held notes using their remaining musical gate and relevant controls. Linear pitch/expression/pan/modulation/operator-level/feedback/LFO-rate/AM-depth/PM-depth/operator-ratio ramps are evaluated at the destination and continue for their independent remaining seconds. Defaults come from the note's prepared patch. The latest fixed-Hz/null frequency policy is applied immediately at the destination, even if its original frequency ramp was unfinished; null uses the reconstructed live ratio and current engine tuning. This deliberately restarts that timbre transition rather than inventing intermediate Hz for a ratio-dependent endpoint. The latest operator ADSR edit is reanchored at the restarted onset. Phase, envelopes, LFO and pitch-envelope state restart; this is not a sample-exact DSP checkpoint or seamless sustain promise. An early/tied stop cancels its note; pre-onset controls clamp to onset. Pause/seek/edits use ID-scoped stop(id, { cancelControls: true }), removing owned future automation without global panic/allNotesOff or touching unrelated clients.\nIndependent note gain (0..1, default 1) is also reconstructed at seek/resume and continues any outstanding gain ramp; it multiplies expression without replacing it.\n\nLookahead bounds match streaming: horizon0.01..10 seconds (default0.2), interval0.001..horizon/2 (default min(0.025,horizon/2)), maxSlots integer1..256 (default256), and at most128 outstanding owned note IDs. Score/loop density is preflighted; runtime commands are additionally capped against the actual queue. Loops shorter than horizon/32 reject; at most32 wraps are generated per pump. Missed events or a pump gap longer than a horizon stop with onError, rather than silently retiming. pump() is available for host-driven observation; bounded automatic timers remain enabled. Position observes AudioContext time and never integrates timer drift.\n\nTransport start, resume and active seek/tempo/loop reconstruction anchor playback in the future by startupLead seconds: default min(0.05, horizon / 2), finite 0..10. The cursor remains at the requested beat during this count-in, then follows AudioContext time; durations are not stretched. Loop seams preserve the already-established audio clock and do not insert a lead on every wrap. startupLead: 0 opts out for hosts that already control delivery timing. Notes still use late: 'drop': headroom reduces cold-start beat-0 loss but does not suppress late/drop rejections or missed-window errors. Resolving start() is not confirmation of physical audible playback.\n\nReset, replacement/closed context, suspension and interruption stop this transport even under OPM's preserve policy. Returning the context to running does not resume it; explicitly call start/resume from a user gesture. Pause, stop, reset and dispose invalidate pending startup, so late promises cannot resurrect notes. Stop/dispose are idempotent. Disposed handles reject start/resume and mutation; no operation closes or globally resets the shared engine.\n","href":"./streaming-sequences.html"},{"title":"Bounded sequences · Bounded sequences","text":"Bounded sequences Bounded sequences","href":"./streaming-sequences.html#bounded-sequences"},{"title":"Bounded sequences · Offline chunks","text":"Bounded sequences Offline chunks","href":"./streaming-sequences.html#offline-chunks"},{"title":"Bounded sequences · Mixed real-time lookahead","text":"Bounded sequences Mixed real-time lookahead","href":"./streaming-sequences.html#mixed-real-time-lookahead"},{"title":"Bounded sequences · Restartable musical transport","text":"Bounded sequences Restartable musical transport","href":"./streaming-sequences.html#restartable-musical-transport"},{"title":"OPM.js usage guide (English)","text":"OPM.js usage guide (English) # OPM.js usage guide (English)\n\n繁體中文 · Project README\n\nGitHub release v1.10 (package 1.10.0) adds portable Arrangement projects, opt-in expressive MIDI files with loss summaries, workload-scoped capacity candidates and bounded local MIDI capture/campaign readiness. Existing v6 voices, expressive performance, 32-voice polyphony and Worker export remain supported. GitHub distribution does not imply npm registry publication.\n\nVoice-format versions are independent of package versions: the published v1.10 release uses canonical voice v6, while the current v1.11 checkout uses canonical voice v7 (per-operator waveform and noiseRate); the immutable v1.6 release used v5. Explicit legacy voice versions 1–6 retain their original input shapes and normalize to v7.\n\nRelease scope: portable Arrangement projects, opt-in expressive MIDI files and loss summaries, measured capacity candidates and the cross-feature contract table are included in v1.10. Physical/mobile/MIDI and human listening results remain unverified until genuine original observations are supplied.\n\nContents: Acquire and install · Browser quick start · Browser API · Node PCM · Voice format and banks · Compression · Troubleshooting\n\n## New in v1.11 (current checkout, unreleased)\n\n- Voice v7 waveforms and noise. Each operator accepts waveform (sine, half, abs, quarter, alternating, camel, square, saw, noise) and noiseRate (20–20000 Hz). Existing sine voices render identically; square/saw (polyBLEP) and half (polyBLAMP) are edge-corrected but, like noise, not alias-free. Original noise-snare, noise-hihat and explosion recipes join the bundled bank. See expressive voices.\n- Optional stereo effects. createStereoEffects/applyEffects (offline) and createEffects (a dedicated AudioWorklet insert) provide chorus and reverb; nothing is connected automatically. See audio buses.\n- MIDI program maps and RPN. programVoices/drumVoices map program changes and GM drum notes to named FM voices (opt-in; gmProgramVoices/gmDrumVoices are starter maps), and RPN 0 pitch-bend sensitivity is decoded in preserve-mode files and by the live adapter. See MIDI files and MIDI performance.\n- .opm import. importOPM/describeOPM from opm.js/voices/opm.js approximately convert VOPM/MXDRV-family text patches; this is not register-level emulation.\n\n## New in v1.10\n\nOnline demos over HTTPS need a playback click for audio startup. The features below are included in v1.10; historical releases remain unchanged. A GitHub release does not imply npm publication or deployment. Consumers can install the release tarball; source contributors run npm ci then npm run build.\n\n- parseArrangementProject(source: string | object) / serializeArrangementProject(project) are root/core APIs for a separate ArrangementProject version 1. Detached, deeply frozen own-data definitions store voices/settings, tempo/meter, layers, sections and the required initial section. Parsing is inert: loading does not start audio. Bounds include 8 MiB, 1–16 layers, at most 32 sections, 65,536 total events and 128 voices/256 KiB normalized voice JSON. Replay begins at beat 0; live cursor, transitions, fades and DSP state are not saved. See portable Arrangement projects.\n- importMidiFile(Uint8Array, options?) returns { events, tempoMap, timeSignature, warnings, lossSummary }; exportMidiFile(events, options?) returns a Uint8Array. Root/core and opm.js/midi-file expose both. Format 0/1 PPQN only. Default controls: 'omit' retains note-only import and rejects control events/nonzero pan on export. Opt-in controls: 'preserve' supports pitch, expression, pan and modulation under explicit pitch-bend-range and channel-ownership policies. lossSummary separates omissions, approximations and preserved source-control counts; sustain remains a gate-duration approximation. Export still rejects ramps/glides, gain/timbre controls, fractional note pitches, nonzero priority, ambiguous channel ownership and linear tempo ramps. No program-to-FM conversion or SysEx transmission occurs. See MIDI files and explicit policies.\n- Measured capacity candidates are scoped to the measured workload, runtime and machine, not universal supported-device budgets.\n- Bounded local MIDI capture and physical-device/listening campaign readiness help collect genuine original observations; they are not completed physical or human acceptance.\n- The cross-feature contract table documents replay, performance, rendering and host boundaries.\n\n### Existing APIs and examples (introduced in v1.9)\n\n- parseScoreProject(source: string | object) / serializeScoreProject(project) store canonical version-1 beat events, normalized tempo/meter, named complete voices and synthesis settings. Defaults are 120 BPM, 4/4, 44100 Hz, standard quality, eight voices, mix gain 1, A4 440 Hz and oldest stealing. Strict own-data snapshots reject unknown fields/accessors; limits are 8 MiB, 65,536 events and 128 voices/256 KiB voice JSON. compileBeatSequence(events, { tempoMap?, bpm?, voices? }) returns validated second-based SequenceEvent[] for offline and Worker rendering. Load stored voices into OPM before using beat events with Transport. See score projects.\n- TransportOptions.startupLead is 0–10 seconds, default min(0.05, horizon / 2): a future anchor protects beat-0 startup and reconstruction; musical position stays fixed through the lead. Setting 0 opts out; late drops and host stalls remain possible.\n- Arrangement layer gain is 0–1 (default 1). switchSection/setLayer accept fade seconds (0–10); setLayerGain(name, gain, { quantize?, fade? }) returns the committed beat. Shared layers are continuous; gains multiply independent note expression and affect owned tails/new onsets. Arrangement reserves authored gain controls; use expression for score dynamics. See adaptive music.\n- MidiAdapterOptions.controllerMap contains at most 128 mappings with controller, field, explicit min, max, ramp, optional reset, and a zero-based operator for operator tuple fields. Duplicate CC/targets and reserved CC64/120/121/123 reject. Scalar fields include independent NoteControls.gain (0–1, default 1). CC121 restores captured effective part defaults or explicit reset (operatorFrequencies allows reset null for ratio mode). performance.getPartControls(part) returns frozen effective part controls, not key overrides. See MIDI performance.\n- Thirteen demo scripts include two original songs with Play/Stop, score-project save/load and WAV export. The demo HTML remains checkout/website-only.\n\nnpm run build generates HTML guides and the full compiler-generated API reference after dist emit. Current local packages ship them under doc/; historical release packages are unchanged. Open doc/index.html directly or copy/serve the complete directory (plus top-level Markdown/legal files for source links). Search is local and keyboard accessible; no runtime dependencies or remote search service are needed. Markdown stays the source of truth.\n\n## Acquire and install\n\nNo source checkout or consumer build toolchain is required. With Node.js 22+ and npm, create a new application and install the attached GitHub v1.10 package directly:\n\nsh\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install https://github.com/JS-PACKAGE/OPM.js/releases/download/v1.10/opm.js-1.10.0.tgz\n\n\nFor an existing app, run only the install command in its root. The v1.10 tarball includes these synchronized guides; the original v1.8 tag and archive remain immutable. The public npm registry is not assumed to have opm.js.\n\nAlternatively, build a tarball from a repository checkout (checkout-only). From the OPM.js repository root, with Node.js 22+ and npm installed:\n\nsh\nnpm ci\nnpm pack\n\n\nnpm pack runs the package's prepack build and creates opm.js-1.10.0.tgz; do not separately build first. From that repository root, make a new sibling application (the repository directory must be named OPM.js for this relative path):\n\nsh\ncd ..\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install ../OPM.js/opm.js-1.10.0.tgz\n\n\nFor an existing app, run npm install /actual/path/to/opm.js-1.10.0.tgz in its root instead; npm init is unnecessary. Consumer apps need no development dependencies. The package contains minified .js modules, matching .js.map source maps with embedded TypeScript sources, generated .d.ts declarations, 13 demo entry scripts, and documentation/legal files, but not separate TypeScript source files, development scripts/tests, HTML pages, or the Vite example. Node uses opm.js/core and opm.js/voices/brass.js; browsers without an import map/bundler use served URLs.\n\n## Browser quick start\n\nFor the existing checkout demos, from the OPM.js repository root run:\n\nsh\npython3 -m http.server 8000\n\n\nOpen http://localhost:8000/index.html for 13 checkout-only HTML examples: notes, lookahead melody, live modulation, shared context, WAV, playground, audition, shared scores/interruption recovery, instrument/MIDI, sound design, audio buses, adaptive arrangement and original songs. Python 3 only serves local assets. Committed dist/ needs no installation/build; after source/helper changes run npm ci then npm run build. Use HTTPS or localhost with ES modules/AudioWorklet, never file://.\n\nTo create an app page, from the opm-app root created above, use the installed CLI to atomically copy the entire distribution and license into a new static directory:\n\nsh\nnpx --no-install opm-assets copy public/opm\n\n\nUse a fresh destination; the CLI never overwrites differing host files and reuses an identical existing deployment. For upgrades, use a release-specific path (for example public/audio/opm-1.10.0) and update page imports together. --no-install prevents automatic registry fetching. POSIX manual equivalent: create public/opm, copy nodemodules/opm.js/dist/. into it, and copy nodemodules/opm.js/LICENSE to public/opm/LICENSE.\n\nSave this complete page as opm-app/public/index.html:\n\nhtml\n\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>OPM.js example\u003c/title>\n\u003cbutton id=\"play\" type=\"button\">Play brass chord\u003c/button>\n\u003coutput id=\"status\" aria-live=\"polite\">Ready\u003c/output>\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });\n status.textContent = 'Playing brass chord';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\u003c/html>\n\n\nFrom the opm-app root, serve public/ and visit http://localhost:8000/; click the button to hear the chord:\n\nsh\npython3 -m http.server 8000 --directory public\n\n\nFor a production host (with JavaScript MIME types and X-Content-Type-Options: nosniff configured), verify deployed bytes from the installed opm-app root:\n\nsh\nnpx --no-install opm-assets check https://your-host.example/opm/\n\n\nUse your actual asset-base URL with its trailing slash; loopback HTTP is also supported. The CLI checks the complete installed release's bytes, HTTP status, MIME and nosniff, rejects redirects and uses a default 15-second overall deadline. Python's basic server lacks nosniff and is for the quick start, not this production check. The Node opm.js/tools API exposes checkDeployment(baseUrl,{timeoutMs,requireNosniff}) (integer deadline 1–60000 ms); disabling nosniff is only for development hosts. This does not certify CSP, native Worker/AudioWorklet startup, listening or device deadlines.\n\nWithout npm, instead copy the checkout's complete dist/ contents to site/opm/, copy its LICENSE to site/opm/LICENSE, save the same page as site/index.html, then from the directory containing site/ serve it with python3 -m http.server 8000 --directory site. Keep api/, core/, worklet/, worker/, and voices/ together, including companion modules, maps and declarations. Do not relocate just one worklet/Worker file. Modules preserve source paths instead of using hashed chunks. The browser imports the served ./opm/api/index.js URL, not the bare npm name. Static-copy deployment avoids relying on a bundler to discover either module graph.\n\nFor an installed-package Vite app, follow the standalone Vite guide (checkout-only): install the release tarball and copy the complete distribution plus license. Vite is an optional host development tool, not an OPM.js runtime dependency or consumer build requirement. Keep worklet assets out of SPA fallback rewrites, use JavaScript MIME types, and configure CSP for your module/worklet deployment; do not loosen production policy just to run an inline example. Publication prerequisites are documented in publishing; this guide does not imply a registry release or configured npm authentication.\n\n## Browser API and lifecycle\n\n| Call | Contract |\n| --- | --- |\n| new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain = 1, tuning = {}, stealing = 'oldest', interruption = 'cancel', quality = 'standard', maxVoices = 8 } = {}) | Requested integer rate 8000–96000 Hz; immutable quality 'eco'/'standard'/'high' and integer logical polyphony 1–32 (default 8) survive close/restart. The actual rate is opm.context.sampleRate; a borrowed context determines its own rate, and OPM never suspends/closes it. Omitted destination uses context.destination; null disables connection. workletUrl optionally relocates the complete same-origin worklet tree without weakening secure-context/CSP/MIME requirements. |\n| await opm.start() / await opm.resume() | Initialize/resume from a user gesture and await before playback. Concurrent starts coalesce; existing suspended contexts resume. |\n| opm.connect(destination) / opm.disconnect(destination?) | Connect/disconnect the started worklet; return the instance. Destination must belong to its context; omitted disconnect destination removes all connections. |\n| opm.loadVoice(name, voice) | Strictly validate/copy a voice under [a-zA-Z0-9_-]{1,64} in a registry bounded to 128 names. Works before startup; replacement affects future notes. |\n| opm.replaceVoiceBank(source) / opm.exportVoiceBank() / opm.removeVoice(name) | Validate the entire JSON string or readonly complete-voice array before replacing the registry; at most 128 entries and 256 KiB JSON. Plain [] clears, but JSON \"[]\" remains invalid under the parser's 1–128 contract. Export canonical named JSON (empty bank: []), bounded to 256 KiB. Removal returns boolean and does not kill sounding/queued frozen patches. |\n| opm.playNote({ voice = 'brass', note, time = 0, at, duration = null, velocity = 1, pan = 0, voicePriority = 0, late = 'start' }) | Finite fractional MIDI 0–127; relative delay 0–60 seconds or mutually exclusive absolute at. Duration (0,60] or null to hold; velocity 0–1; pan −1–1; integer voicePriority 0–127. Returns a positive safe-integer note ID, not admission acknowledgement. |\n| opm.stop(id, { at } = {}) | Return a command ID; cancel a pending onset/release a live note immediately or at at, preserving natural release-tail automation. {cancelControls:true} instead immediately cancels that ID's queued onset/control/off commands and releases it. cancelControls, even false, cannot combine with at. Inactive/already-released targets report command rejection without changing note state. |\n| opm.updateNote(id, controls, { at } = {}) | Return a command ID; pending controls apply at onset, and releasing notes remain controllable. Inactive/invalid/queue-full commands report rejection. |\n| opm.allNotesOff() / opm.panic() | Return a command ID; bypass full scheduled queues. Cancel pending events and release active gates / immediately silence all tails and emit reset. Cache and routing survive. |\n| opm.setMixGain(gain) / opm.setTuning(tuning) | Require startup, return a command ID, and retain settings across node recreation; no phase/envelope restart. |\n| await opm.getDiagnostics() | Resolve {type:'diagnostics',requestId,activeVoices,pendingEvents,errors,rejectedNotes}. Requires running context, at most 64 outstanding; suspend/close/processor failure rejects pending requests. Resume before retrying. |\n| await opm.close() | Serialize disposal against initialization, disconnect the node, close only an owned context. Startup after close recreates owned contexts; borrowed contexts survive. |\n| opm.subscribe(listener) | Subscribe independently; returns an idempotent unsubscribe function. Exceptions are isolated. |\n| await opm.waitForCommand(commandId, { timeout, signal } = {}) | Admission-only wait: default 5000 ms, integer 1–60000 ms, optional AbortSignal; 64 pending waits/128 retained receipts. Rejection throws CommandRejectedError; acceptance is not execution. |\n| await opm.dispose() | Terminal cleanup; cannot restart this instance. close() remains restartable. |\n\nonEvent receives note lifecycle states, diagnostics, processor errors, command acknowledgements {type:'command',command,commandId?,id?,state,reason?,frame,time}, context state changes and global resets. Command state is accepted or rejected; acceptance means admission/immediate application, not guaranteed future execution. Correlate command IDs and handle rejection. Clear held-note bookkeeping on reset (close, failure, panic, interruption); close/failure need not produce every per-note terminal reply. Frame/time are audio boundaries, not callback delivery timestamps. opm.voices is a defensive ReadonlyMap snapshot of frozen patches.\n\nAbsolute at must be finite, nonnegative, safely representable in sample frames, and at most 60 seconds ahead of opm.context.currentTime. Past times are allowed: default late:'start' starts at the first available frame with its full gate duration; late:'drop' rejects with reason late, including worklet delivery delays. At equal frames, stop precedes onset, then controls; a scheduled stop at/before onset cancels rather than briefly sounding the note.\n\nControls are a nonempty own-data object: pitch −48..48 semitones; glide 0..10 seconds requiring pitch; expression 0..1; pan −1..1; modulation 0..2 (AM capped at 1/PM 1200 cents); operatorLevels is four multipliers 0..2 over patch levels. Current checkout also accepts feedback 0..7, lfoRate 0..20 Hz, amDepth 0..1, pmDepth 0..1200 cents, four operatorRatios 0.125..32, four operatorFrequencies 1..20000 Hz or null to restore ratio mode, and four complete operatorADSR {a,d,s,r} objects. Ramp 0..10 seconds independently retargets supplied scalar/ratio/frequency controls from current values; glide remains independent and linear in semitones. Oscillator phase and feedback history survive controls. ADSR changes instead reanchor at current dB: held attack restarts, released notes begin a newly scaled release capped at 10 seconds, and zero release drains the filter immediately. Fixed Hz follows pitch controls but ignores tuning-table transposition. Unknown fields/accessors/nonfinite/out-of-range values reject. See expressive voices and host integration.\n\nSince v1.9: gain (0–1, default 1) is independent of expression and follows ramp without restarting phase/envelopes; arrangement layer fades use this separate multiplier.\n\nThe selected 1–32 logical voices (default 8) and at most eight independent short stealing fades are bounded separately. voicePriority is integer 0–127, default 0: stealing considers only voices whose priority is no higher than the incoming note, selects the lowest eligible priority first, then applies the stealing policy. Stealing defaults to oldest; release-first chooses oldest released before held, while quietest uses current carrier envelope × velocity × expression with oldest ties, not instantaneous sample amplitude. If all voices have higher priority, the browser reports note rejection reason priority; direct Synth.noteOn throws VoiceAdmissionError. Events/IDs each cap at 256. Future timed notes reserve two events; controls/stops also consume slots. Terminal states reclaim obsolete events. Prepared patches use 128 content-keyed LRU registrations; validated replacement reuses IDs safely while queued/active notes keep their original snapshot.\n\nFor customization and early release, replace only the module \u003cscript> in opm-app/public/index.html above, keeping its #play button and #status output:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { brass } from './opm/voices/brass.js';\n\n const opm = new OPM();\n const voice = structuredClone(brass);\n voice.lfo.rate = 5;\n voice.lfo.amDepth = 0.2;\n voice.ops[0].level = 0.6;\n opm.loadVoice('my-brass', voice);\n const play = document.querySelector('#play');\n play.textContent = 'Play custom brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const at = opm.context.currentTime + 0.1;\n const id = opm.playNote({ voice: 'my-brass', note: 64, at, velocity: 0.7, pan: -0.5 });\n opm.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8, pan: 0.5, modulation: 1.2 }, { at: at + 0.1 });\n opm.stop(id, { at: at + 0.3 });\n status.textContent = 'Playing custom brass note';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\nThis example schedules one held note 0.1 seconds ahead, changes it 0.1 seconds after onset, and begins release 0.3 seconds after onset, all on the audio clock (three queued events, well inside the horizon). When disposing the page's synthesizer, await opm.close(); another click can start it again.\n\n### Bounded lookahead melody\n\nReplace the quick-start page's module script with this block, keeping #play and #status. Each click schedules a finite eight-note phrase; the Stop button cancels/releases only this scheduler's notes:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createLookaheadScheduler } from './opm/api/index.js';\n const opm = new OPM();\n const status = document.querySelector('#status');\n const melody = [60, 64, 67, 72, 67, 64, 62, 60];\n let origin;\n let index = 0;\n const scheduler = createLookaheadScheduler(opm, ({ from, to, maxNotes }) => {\n origin ??= from + 0.05;\n const batch = [];\n while (index \u003c melody.length) {\n const at = origin + index * 0.25;\n if (at \u003c from) { index++; continue; } // Skip beats missed during a stall.\n if (at >= to || batch.length === maxNotes) break;\n batch.push({ note: melody[index++], at, duration: 0.18, velocity: 0.6, late: 'drop' });\n }\n return batch;\n }, { onError(error) { status.textContent = error.message; } });\n document.querySelector('#play').addEventListener('click', async () => {\n scheduler.stop();\n origin = undefined;\n index = 0;\n try {\n await scheduler.start();\n status.textContent = 'Scheduling eight notes';\n } catch (error) { status.textContent = error.message; }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop phrase';\n document.body.append(stop);\n stop.addEventListener('click', () => scheduler.stop());\n window.disposeOPM = async () => { scheduler.dispose(); await opm.close(); };\n\u003c/script>\n\n\nThe callback receives half-open absolute windows [from,to) and must synchronously return at most maxNotes notes, each with at inside that window and numeric duration (0,60]; held notes and relative time are not allowed. Defaults are horizon 0.2 seconds, interval 0.025 seconds, and 32 notes per batch. Configurable bounds are horizon 0.02–10 seconds (greater than interval), interval 0.005–1 second, and maxNotes 1–128. There are at most 128 outstanding scheduler gates; normal worklet queue limits still apply. Missed windows are skipped, not replayed in a burst. Required onError receives callback/timer failures, which stop scheduling. start() initializes/resumes from a user gesture; stop() permits restart, while dispose() is permanent. Neither closes OPM or stops unrelated notes. After the finite phrase the callback returns empty batches; stop/dispose the helper when it is no longer needed.\n\nAny non-running context or reset stops the helper and reaches onError; restart explicitly from a gesture. Default interruption:'cancel' clears old gates/automation on suspension/interruption; 'preserve' retains direct notes/queued events but never automatically restarts lookahead.\n\n### Shared context, output routing, and events\n\nFor a host-owned context, replace the quick-start page's module script with this block. It holds each note until the Stop button and leaves the host context alive on disposal:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n const context = new AudioContext();\n const gain = context.createGain();\n gain.gain.value = 0.3;\n gain.connect(context.destination);\n const status = document.querySelector('#status');\n const opm = new OPM({\n context, destination: null,\n onEvent(event) {\n if (event.type === 'reset') {\n id = undefined;\n if (event.reason === 'close' || event.reason === 'failure') connected = false;\n }\n status.textContent = event.type === 'error' ? event.error.message\n : event.type === 'note' ? ${event.id}: ${event.state}\n : event.type === 'diagnostics' ? Voices: ${event.activeVoices}\n : event.type === 'command' ? ${event.command}: ${event.state} ${event.reason ?? ''}\n : event.type === 'context' ? Context: ${event.state} : Reset: ${event.reason};\n }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop held note';\n document.body.append(stop);\n let id;\n let connected = false;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.resume();\n if (!connected) { opm.connect(gain); connected = true; }\n if (id !== undefined) opm.stop(id);\n id = opm.playNote({ note: 60, velocity: 0.7, pan: 0.5 });\n console.log(await opm.getDiagnostics());\n } catch (error) { status.textContent = error.message; }\n });\n stop.addEventListener('click', () => { if (id !== undefined) opm.stop(id); });\n window.disposeOPM = async () => {\n await opm.close();\n connected = false;\n id = undefined;\n gain.disconnect();\n // The host decides when to close context.\n };\n\u003c/script>\n\n\nCall await window.disposeOPM() when removing this app's audio UI. The host remains responsible for the shared context and downstream nodes. Avoid suspending a shared context merely to stop one synthesizer.\n\n### Shared scores, tuning and safe recovery\n\nmixGain (finite 0–1, default 1) scales the mix before tanh; host gain controls listening volume, not saturation drive. Tuning replaces the complete configuration: {referenceHz: 442, offsets} with reference A4 20–20000 Hz and optional exactly 128 finite cents offsets in −4800..4800 (omission means zero). Fractional notes interpolate offsets, including a safe MIDI-127 endpoint. Active retuning preserves phases/envelopes and original-note key scaling.\n\nReplace the quick-start module script with this shared live/offline score:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, playSequence } from './opm/api/index.js';\n import { renderSequence } from './opm/core/index.js';\n import { brass } from './opm/voices/brass.js';\n const settings = { mixGain: 0.5, tuning: { referenceHz: 442 }, stealing: 'release-first' };\n const opm = new OPM({ ...settings, interruption: 'cancel' });\n const score = [\n { type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },\n { type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },\n ];\n let playback;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.start();\n playback?.stop();\n opm.loadVoice('lead', brass);\n playback = playSequence(opm, score, { at: opm.context.currentTime + 0.05 });\n const audio = renderSequence(score, { ...settings, sampleRate: opm.context.sampleRate, voices: opm.voices });\n document.querySelector('#status').textContent = Rendered ${audio.left.length} frames; errors ${audio.diagnostics.errors};\n } catch (error) { document.querySelector('#status').textContent = error.message; }\n });\n window.disposeOPM = async () => { playback?.stop(); await opm.close(); };\n\u003c/script>\n\n\nScore times are relative seconds. Notes require unique positive IDs and finite durations; stop/control events reference those IDs. Full validation precedes posting. Limits are 128 notes, 256 reserved slots (two/note plus commands), 60 seconds including gates, and 4,000,000 offline frames. playSequence returns a defensive score-to-note ID map and idempotent stop; it cannot reserve capacity atomically against unrelated callers. renderSequence includes tails. Matching PCM requires equal settings/rate/frame origin and no competing notes; encode only its left/right/sampleRate fields for WAV.\n\nOn physical iOS/Android, follow mobile acceptance and the support matrix using example 08 (checkout-only) over HTTPS. Capture device/OS/browser/rate/policy, observations and manual pass/fail/unverified results for lock/app/call/route/battery/long-play/stall scenarios. Reports remain local. Resume from a gesture; borrowed contexts remain host-owned. Desktop/headless signal checks are not physical recovery or audible-continuity evidence.\n\n### Bounded long scores\n\nFor up to 24 hours and 65,536 input events, use prepareLongSequence, estimateSequenceCapacity and renderSequenceChunks from opm.js/core. The estimate distinguishes full-buffer/single-batch eligibility from default streaming-window density; it does not promise admission alongside other callers. Chunk buffers are borrowed until the next next(): consume them immediately, retaining a copy only when needed. Break, cancel() or AbortSignal stops advancement; maxFrames can cap cumulative work. The 4,000,000-frame full-buffer budget remains; incremental WAV bypasses that allocation limit, not RIFF32 size limits.\n\njs\nimport { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';\nconst score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];\nconst options = { sampleRate: 96000, chunkFrames: 4096 };\nconsole.log(estimateSequenceCapacity(score, options));\nlet frames = 0, energy = 0;\nconst render = renderSequenceChunks(score, options);\nfor (const chunk of render) {\n for (let i = 0; i \u003c chunk.frames; i++) energy += chunk.left[i] ** 2;\n frames += chunk.frames;\n}\nconsole.log(frames, energy, render.diagnostics.errors);\n\n\nFor browser playback, import streamSequence alongside OPM, construct const stream = streamSequence(opm, score, { onError: console.error }), and call await stream.start() in the play button's gesture. stop() cancels only that stream's notes; dispose() removes its listener and makes it terminal. Controls, explicit stops and automatic releases retain their score IDs across windows. Interruption/reset stops the stream under both policies: resume the context, then explicitly restart if desired. See defaults, density limits and the complete browser recipe.\n\n### Beat transport and performance policy\n\nReplace the quick-start module script, retaining #play and #status:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createTransport, createPerformance } from './opm/api/index.js';\n const opm = new OPM({ quality: 'standard', maxVoices: 8 });\n const transport = createTransport(opm, [\n { type: 'note', id: 1, beat: 1, duration: 1, note: 60 },\n { type: 'note', id: 2, beat: 2, duration: 1, note: 64 }\n ], { bpm: 120, onError: console.error });\n const performance = createPerformance(opm, { parts: 1 });\n performance.configurePart(0, {\n voice: 'brass', mode: 'mono', legato: true, priority: 'last',\n glide: 0.1, pan: 0, expression: 0.8\n });\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.start();\n await transport.start();\n const key = performance.noteOn(0, 67, { velocity: 0.7 });\n performance.noteOff(0, key);\n document.querySelector('#status').textContent = 'Transport started';\n } catch (error) { document.querySelector('#status').textContent = error.message; }\n });\n window.disposeOPM = async () => {\n transport.dispose(); performance.dispose(); await opm.close();\n };\n\u003c/script>\n\n\nTransport uses quarter-note beats (including note durations), the AudioContext clock, and only its own note IDs. It provides pause, resume, stop, seek, setTempo (current position onward), setTempoMap, setLoop, pump, dispose, and read-only state/position/snapshot/IDs. Default BPM is 120; valid BPM 1–1000, tempo maps start at beat 0 and ascend strictly (at most 1024 points), and positions cap at 86400 beats. Signature numerator is 1–32, denominator a power of two up to 32. Horizon is 0.01–10 seconds, interval 0.001–horizon/2, and maxSlots 1–256. Pause/seek/loop restart envelopes/phases rather than restoring DSP snapshots: scalar/ratio ramps resume with remaining durations; latest fixed-Hz/null policy applies immediately, even mid-ramp, and latest ADSR reanchors the new onset. Core exports beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, and normalizeTempoMap.\n\nThe recipe leaves a musical one-beat count-in. Current-checkout Transport additionally anchors in the future with startupLead (default min(0.05, horizon / 2) seconds) so a beat-0 onset is not submitted at the just-expired cold-start clock. The historical v1.8.1 archive lacks that fix. late:'drop' still rejects genuinely late admissions and stops the helper through onError; neither count-in nor startup lead guarantees deadlines under host stalls. start() resolving is not confirmation that every note sounded.\n\nTempo points support default curve:'step' or curve:'linear' BPM interpolation in beat space up to the next point; the last point cannot be linear. Optional endBpm requires a linear segment and can preserve its endpoint when editing a map. Beat/second conversion integrates and inverts ramps. Import grid helpers quantizeBeat (floor, ceil, nearest, strictly-later next), swingBeat and swingBeatEvents from the package root opm.js (served ./opm/api/index.js in plain browsers), not opm.js/core; swing transforms note positions, explicit controls/stops and automatic note ends coherently. See host integration for tempo/grid recipes.\n\nFor layered section-based music, createArrangement aligns looping layers to the global beat grid and commits section/layer changes at quantized beat/bar/numeric boundaries no earlier than already admitted notes. Shared layers keep sounding without retriggering when unrelated layers change. Layer voicePriority combines with note priority by taking the maximum; refused lower-priority notes increment snapshot priorityDrops instead of failing the arrangement. See adaptive music and arrangement for limits, transitions and a complete recipe.\n\nPerformance has 1–16 zero-based parts (default 16), system/per-part key bounds up to 128, independent physical-key IDs even at equal pitch, poly/mono modes, last/high/low key-selection priority, sustain and part controls. Part voiceLimit is 1–32; voicePriority is integer 0–127 and is distinct from key-selection priority. Use noteOff(part,key), sustain(part,on), updatePart(part,{glide,pan,expression}), updateKey(part,key,controls), updatePartNotes(part,controls), allNotesOff(part?), getPart(part), and dispose(). Per-key controls and part-wide defaults include expressive note controls and owned release tails. Held keys outrank pedal-only keys. Mono legato preserves the original gate/envelope/onset velocity/key scaling only within ±48 semitones of the original onset; beyond that it retriggers at the actual pitch. Steals prune keys without automatic readmission; interruption/reset clears helper state even with preserve mode. The performance helper itself is device-agnostic; optional createMidiAdapter/requestMidiAccess add bounded non-SysEx MIDI input. See MIDI and expressive performance, host integration, and example 08 (checkout-only).\n\n## Offline PCM with Node.js\n\nIn the npm-installed opm-app root, save the following as render.mjs:\n\njs\nimport { Synth, prepareVoice } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst sampleRate = 44100;\nconst synth = new Synth(sampleRate);\nconst patch = prepareVoice(brass);\nconst id = synth.noteOn(patch, 60);\nconst left = new Float32Array(sampleRate);\nconst right = new Float32Array(sampleRate);\nsynth.render(left, right, 0, 22050);\nsynth.updateNote(id, { pitch: 7, glide: 0.05, expression: 0.8 });\nsynth.noteOff(id);\nsynth.render(left, right, 22050, 22050);\nconsole.log(left.some(sample => sample !== 0), synth.errorCount);\n\n\nRun node render.mjs in opm-app; expected output is true 0. These are in-memory PCM buffers; center pan is dual mono, other positions are stereo. Checkout imports are ./dist/core/index.js and ./dist/voices/brass.js. Use .mjs or \"type\": \"module\"; there is no CommonJS entry.\n\nnew Synth(sampleRate, maxVoices = 8, {mixGain, tuning, stealing, quality = 'standard'} = {}) requires finite 8000–192000 Hz (not necessarily integer) and integer 1–32 logical voices, default 8, with at most eight independent stealing fades. noteOn(voiceOrPrepared,note,id?,{velocity=1,pan=0,voicePriority=0}={}) accepts fractional MIDI 0–127 with strict velocity/pan bounds, integer priority 0–127 and unique positive safe IDs; it throws VoiceAdmissionError if all active voices have higher priority. Prepare immutable snapshots once or validate raw voices each call; pooled state is bounded. updateNote/noteOff return booleans. allNotesOff releases naturally; panic clears all tails; setters preserve phase/envelopes. onVoiceEnded(id,reason) reports exactly one terminal stolen, ended, error or cancelled; callback replacements start next frame and recursive render rejects.\n\nrender(left,right,offset=0,length=nativeLeftLength-offset) requires native Float32Arrays and nonnegative safe-integer ranges fitting both actual buffers. Shadowed length/fill properties do not enlarge work; proxies/forgeries and numeric coercion reject. Inspect cumulative currentFrame/errorCount; split rendering at events and retain enough release frames.\n\nFor a simpler single-note render, save this standalone script as opm-app/render-note.mjs:\n\njs\nimport { renderNote } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst { samples, sampleRate, diagnostics } = renderNote({\n voice: brass,\n note: 60,\n duration: 0.1,\n sampleRate: 44100\n});\nconsole.log(samples.length, sampleRate, diagnostics.errors);\n\n\nRun node render-note.mjs in opm-app. For a checkout use ./dist/core/index.js and ./dist/voices/brass.js. renderNote({ voice, note=60, duration=0.5, velocity=1, pan=0, sampleRate=44100 }) returns {samples,left,right,sampleRate,diagnostics:{errors}}, where samples === left. It requires a complete schema voice. Finite voice numeric fields clamp (invalid versions/algorithm/feedback still reject); finite note/duration/velocity/pan clamp to 0–127/0–30/0–1/−1–1. Sample rate is an integer 8000–96000 Hz.\n\nrenderNote also accepts strict mixGain, tuning, stealing and quality options; whole-score, chunked and Worker rendering forward quality too. 'eco' uses 2× oversampling with a 35-tap half-band FIR, default 'standard' uses 4× with a 15 → 47-tap half-band cascade, and 'high' uses 8× with an 11 → 15 → 47-tap cascade. Profiles promise neither alias-free output nor CPU deadlines; measure on the target host. See acoustic quality.\n\nIt uses Synth's exact envelope, LFO, filter, velocity, pan, and saturation path. Length is ceil((duration + longestEffectiveRelease + 0.01) sampleRate) including rate-key-scaled release, with a 4,000,000-frame cap. Pan gains before saturation are sqrt(2)cos/sin((pan+1)*pi/4); saturation may change perceived balance.\n\nOther core exports: normalizeVoice returns a strict, detached voice copy; envelopeAt(time, gate, adsr) returns dB-derived amplitude with time/gate in seconds; ALGORITHMS is eight immutable routing graphs; HEADROOM = 0.7, OVERSAMPLE = 4, MAXRENDERSAMPLES = 4000000; and sampleRateValue validates integer 8000–96000 Hz. dist/voices/schema.js exports validateVoice(voice) (complete, frozen validation), parseVoiceBank(input), finite-number clamping helper bounded(value, min, max, label = 'number'), LIMITS, MAXBANKBYTES, and MAXBANKVOICES.\n\n## Voice format and banks\n\nA standalone voice.json (save in opm-app if using it as a local asset) can contain this complete schema voice. See the voiceSchema export in voice.schema.js and the examples export in examples.js. Four operators appear in signal order:\n\njson\n{\n \"version\": 6,\n \"name\": \"simple\",\n \"algorithm\": 7,\n \"feedback\": 0,\n \"modIndex\": 4,\n \"lfo\": { \"rate\": 0, \"amDepth\": 0, \"pmDepth\": 0, \"waveform\": \"sine\", \"amTargets\": [1, 1, 1, 1], \"pmTargets\": [1, 1, 1, 1] },\n \"ops\": [\n { \"ratio\": 1, \"level\": 0.8, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.6, \"r\": 0.2 }, \"keyScale\": { \"breakpoint\": 60, \"leftDbPerOctave\": 0, \"rightDbPerOctave\": 6 } },\n { \"ratio\": 2, \"level\": 0.4, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 3, \"level\": 0.3, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 4, \"level\": 0.2, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } }\n ]\n}\n\n\n| Field | Bounds and meaning |\n| --- | --- |\n| version, name | Current canonical 7; explicit 1–6 retain their original strict shapes. v1 excludes key scaling; v1/2 exclude velocity sensitivity; v1–3 exclude waveform; v1–4 exclude v5 fields; v1–5 exclude v6 targets; v1–6 exclude operator waveform/noiseRate. Names: 1–64 ASCII letters/digits/underscores/hyphens. |\n| algorithm, feedback | Integers 0–7. |\n| modIndex | Phase-modulation strength 0–16. |\n| ops | Exactly four operators, each with ratio 0.125–32, level 0–1, detune −1200–1200 cents, and complete adsr: a, d, r each 0–10 seconds; sustain s 0–1. |\n| lfo | rate 0–20 Hz, amDepth 0–1, pmDepth 0–1200 cents; waveform sine (default), triangle, saw or square. |\n| ops[i].keyScale | Optional: breakpoint is a MIDI integer 0–127; left/right slopes are 0–24 dB/octave. Omission is flat. |\n| ops[i].velocitySensitivity | Optional 0–48 dB attenuation at velocity 0; omission is 0. Operator gain is multiplied by 10 * (-sensitivity (1 - velocity) / 20). Final output still multiplies by velocity. Set sensitivity on modulators to vary brightness as well as volume. |\n| ops[i].frequency, ops[i].rateKeyScale | Optional v5 fixed 1–20000 Hz frequency (ratio remains required), rate scale 0–4; ADSR times multiply by 2 * (-scale (note - 60) / 12), capped at 10 seconds. |\n| ops[i].waveform, ops[i].noiseRate | Optional v7 oscillator shape sine (default), half, abs, quarter, alternating, camel, square, saw or noise; noiseRate is the 20–20000 Hz held-noise clock (default 8000), ignored unless the waveform is noise. A noise operator is not pitched. |\n| pitchEnvelope | Optional v5 {a,d,r,initial,peak,sustain,final}: 0–10-second times, −4800..4800-cent levels; continuous release from current pitch. |\n| lfo.delay, lfo.sync, lfo.phase | Optional v5 0–10 seconds, 'note'/'global', 0–1 turns; defaults 0/note/0. Delay gates depth, not phase progression. |\n| lfo.amTargets, lfo.pmTargets | Optional v6 four-element weights 0–1; boolean inputs normalize to numbers. Omission targets all four operators with weight 1. |\n\nSingle voices may omit version/name/modIndex/lfo; defaults are current shape, index 4 and off/sine. Current output and bundled presets canonicalize to 7; legacy versions cannot carry newer fields (including v6 targets in explicit v5 and waveform fields in explicit v6). Four complete operators remain required. Strict normalization rejects bounds violations; all paths reject wrong types/accessors/unknown fields/sparse arrays and detach inputs. Key scaling uses the original note; pan belongs to the note. See expressive voices.\n\nA complete bank JSON file must contain an array of 1–128 complete voices with unique names (wrap the standalone object above in [...]); it is not a single top-level voice object. parseVoiceBank(jsonStringOrArray) from opm.js/voices/schema.js for Node, or the served ./opm/voices/schema.js for browsers, returns a Map of names to frozen validated voices. JSON strings are capped at 256 KiB UTF-8; arrays are capped by voice count. Bank parsing and validateVoice clamp finite out-of-range numeric fields to schema limits, unlike strict single-voice normalization; invalid version/algorithm/feedback values, nonfinite values, wrong types, and unknown fields still fail.\n\nFor a working bank loader, replace the module \u003cscript> in opm-app/public/index.html from the browser quick start, keeping its #play button and #status output. The earlier copy command places the bank module at public/opm/voices/examples.js:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { parseVoiceBank } from './opm/voices/schema.js';\n import { examples } from './opm/voices/examples.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n play.textContent = 'Play bank brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const bank = parseVoiceBank(examples);\n for (const [name, voice] of bank) opm.loadVoice(name, voice);\n opm.playNote({ voice: 'brass', note: 60, duration: 0.7 });\n status.textContent = 'Playing bank brass note';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\nServe public/ as above and click the button. This example imports a trusted bundled module. External JSON banks remain supported by parseVoiceBank; for untrusted downloads, your host/application must restrict source, content type, and response size before reading the body. The engine does not provide a network download API.\n\n## WAV export and DX7 import\n\nIn opm-app, save export.mjs and run node export.mjs:\n\njs\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { renderNote, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\nimport { importDX7, describeDX7 } from 'opm.js/voices/dx7.js';\n\nconst audio = renderNote({ voice: brass, duration: 0.7, velocity: 0.8, pan: -0.5 });\nawait writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));\n// Optional: node export.mjs /path/to/your/voice.syx\nif (process.argv[2]) {\n const bytes = new Uint8Array(await readFile(process.argv[2]));\n const voices = importDX7(bytes);\n console.log(describeDX7(bytes));\n const imported = renderNote({ voice: voices[0], duration: 1 });\n await writeFile('imported.wav', encodeWav({ left: imported.left, right: imported.right, sampleRate: imported.sampleRate }));\n}\n\n\nCheckout imports use ./dist/core/index.js, ./dist/voices/brass.js, and ./dist/voices/dx7.js. encodeWav({left,right?,sampleRate,format?}) returns little-endian RIFF bytes in 'pcm16' (default), 'pcm24' or 'float32' format. Omit right for mono. Float32 arrays must contain finite samples in −1–1; out-of-range amplitudes reject rather than clip, including float32 output. Stereo lengths must match. Rate is integer 8000–192000 Hz; full-buffer budget is 4,000,000 frames. Pass only these own-data fields: a whole renderNote result has extra fields and is rejected. It only encodes; your application saves/downloads bytes.\n\n### Incremental WAV and browser Worker export\n\ncreateWavEncoder({sampleRate,channels:1|2,format?,totalFrames}) emits header() once, accepts 1–65536 frames per encode({left,right?}), then requires exact frame accounting in finalize() (which emits any RIFF padding). It retains no PCM/output chunks. RIFF32 must remain below 4 GiB, with at most 24 hours plus release budget; there is no full-buffer 4-million-frame limit.\n\nFor a small runnable encoder example in opm-app, save as incremental.mjs and run node incremental.mjs:\n\njs\nimport { open } from 'node:fs/promises';\nimport { createWavEncoder } from 'opm.js/core';\nconst encoder = createWavEncoder({\n sampleRate: 44100, channels: 1, format: 'pcm24', totalFrames: 44100\n});\nconst file = await open('silence.wav', 'w');\ntry {\n await file.writeFile(encoder.header());\n for (let remaining = 44100; remaining > 0;) {\n const frames = Math.min(4096, remaining);\n await file.writeFile(encoder.encode({ left: new Float32Array(frames) }));\n remaining -= frames;\n }\n await file.writeFile(encoder.finalize());\n} finally { await file.close(); }\n\n\nFor long browser files, root export renderSequenceInWorker(events,{...chunkOptions,quality,format,sink,signal,onProgress,startupTimeoutMs?,phaseDiagnostics?,onPhase?,workerUrl?}) returns {capacity,format,bytesWritten,diagnostics}. The sink implements write(bytes) and optionally close()/abort(reason); writes are awaited, with at most one encoded chunk unacknowledged. Progress reports {frames,totalFrames,bytesWritten,errors}. signal must be a genuine AbortSignal, including a cross-realm signal; an already-aborted signal cancels before rendering. Cancellation terminates/rejects immediately with AbortError without awaiting a hung write, close or abort; the sink must invalidate outstanding writes and own rollback. The helper is browser-only; core/offline helpers work in Node.\n\nstartupTimeoutMs is optional (no deadline by default), integer 1–2147483647 ms, and applies only until the module Worker reports ready; expiration rejects with TimeoutError, not a whole-render or sink deadline. onPhase synchronously observes initializing, rendering, writing, closing and terminal completed/cancelled/failed states with host-local elapsed timing and counters; an observer exception fails the render and invalidates the sink. phaseDiagnostics:true adds bounded phase totals to successful diagnostics.phases. These timings are not DSP benchmarks, network traces or realtime guarantees. See Worker diagnostics for interpretation and deadline/cancellation recipes.\n\nThe default static module is dist/worker/render.js; optional URLs must be same-origin HTTP(S), never blob/data/eval. Copy the entire distribution and serve JavaScript MIME types under suitable worker-src 'self' and script-src 'self' CSP. See streaming sequences for the complete cancellable file-sink recipe. Example 08 (checkout-only) offers format preview and long Worker export: long files require File System Access, without an aggregate-Blob fallback; short previews use an explicit 8 MiB memory budget.\n\ntext\npublic/opm/\n api/ browser API and helper companions\n core/ DSP, sequence, transport and WAV companions\n worklet/ realtime processor\n worker/ render.js and offline Worker companions\n voices/ schemas, presets and importers\n\n\n\n### DX7 conversion\n\nimportDX7(Uint8Array) accepts exactly one framed/checksummed 163-byte single voice or 4104-byte 32-voice bank, rejecting raw/concatenated/invalid seven-bit messages. v1.8 returns normalized version 6 voices (v1.6 historically returned v5). describeDX7 returns {name,sourceAlgorithm,algorithm,selectedOperators,droppedOperators,warnings}; original algorithms are 1–32, converted 0–7, with DX7 operator numbering 1–6.\n\nThis remains a lossy musical heuristic for six-to-four-operator conversion, not DX7 synthesis/emulation. Fixed-Hz operators are retained. Velocity/rate scaling, reduced pitch-envelope stages and LFO delay/sync are heuristic; unsupported descending saw/sample-and-hold waveforms produce substitutions and warnings. Reduced routing, oscillator sync, transpose, per-operator AM and envelope details remain lossy. Inspect descriptions and expressive conversion limits before auditioning. Browser hosts register converted patches with loadVoice; bound uploaded/downloaded bytes before buffering. See voice quality for original recipes and numerical host trims.\n\n## Quality and release acceptance\n\nMaintainer commands run in the checkout, after npm ci:\n\nAll authored programs use strict TypeScript. npm run compile emits ignored .dev/; npm test compiles/selects behavioral tests, unlike bare node --test. Current npm run build derives declarations, preserves module paths, builds thirteen demo entry scripts and then generates static HTML documentation from Markdown and emitted declarations. Historical v1.8.1 has twelve scripts. npm run typecheck covers authored programs/public consumers. Every distribution JS has a matching map/declaration; consumers need neither TypeScript nor build tools.\n\nsh\nnpm run build\nnpm test\nnpm run typecheck\nnpm run security -- --package-smoke\nnpx --no-install playwright install --with-deps chromium\nnpm run browser-smoke -- chromium\nnpm run benchmark\nnpm run sound-quality\nnpm run voice-quality\nnpm run browser-stress -- chromium\n\n\nUse firefox/webkit instead to install/exercise those browsers. CI configures Node 22/24/26 and all three browser engines; configuration alone does not prove successful runs. Observed versions/results belong in CHANGELOG. Browser acceptance observes real AudioWorklet signal, finite output, lifecycle/release, and deployment paths, not just fake contexts. Roadmap completion requires a tested feature, working demo sound/export, and README coverage.\n\nHeadless Linux Firefox also needs a running native audio server; browser library installation alone is insufficient. CI installs pulseaudio, runs pulseaudio --start --exit-idle-time=-1, loads pactl load-module module-null-sink sinkname=opmci, selects pactl set-default-sink opm_ci, and checks pactl info before smoke. A null sink discards speaker output but still runs the native audio clock and real worklet graph. Smoke logs identify the pending stage and context state if initialization/resume stalls.\n\nBenchmark output reports warmed 128-frame block p95/p99/worst and misses of 128/sampleRate seconds. OPMBENCHBLOCKS, OPMBENCHWARMUP, and OPMBENCHSAMPLERATE control workloads; optional p99/worst budget ratios enforce host-specific thresholds. Local defaults are report-only; CI configures OPMBENCHP99BUDGET_RATIO=1 at 48 kHz (2.667 ms), worst/GC stalls report-only. This is not a universal real-time guarantee. Spectral acceptance compares a 30 kHz oscillator folded to 18 kHz with an equal-level 1760 Hz control at 48 kHz (at least 30 dB attenuation), not arbitrary band-limited FM proof.\n\nSound-quality tools cover multiple sample rates, algorithms, feedback, pitches, velocities, and polyphony, with finite/deterministic output and controlled spectral/level measurements. Preset reports expose raw peak/RMS dBFS and recommended bounded host trim, not automatic loudness rewriting. Browser stress observes native messaging, signal, lifecycle, queue drainage, and elapsed main-thread gaps under contention; these are not measurements of AudioWorklet CPU, GC, or guaranteed glitch-free output. Controlled alias attenuation is not a claim that arbitrary FM is alias-free. Consult voice quality and publishing acceptance for scope and release prerequisites.\n\nIndependent acceptance includes Bessel PM, contractive/full-level feedback bounds, nested four-op chain/branched/multicarrier references and two 120-second streamed LFO/glide scenarios. The 4× polyphase half-band FIR decimator is flat to 0.40 output Fs and rejects products folding below it by at least 81 dB, at the cost of a constant 13.25-frame linear-phase delay. See acoustic mathematics and limitations. These are not all-patch, perceptual, physical-device or hardware-fidelity proof.\n\n## Compressed deployment\n\nEvery .js in dist/ has a corresponding .js.map and generated .d.ts, including demo scripts. Engine modules preserve source paths; no hashed chunks are generated. Demo declarations contain export {}; because those modules export no API. The build produces no JSON assets or compressed sidecars. Maps embed original TypeScript; publishing them exposes that source for debugging. Configure compression at the web host if needed, retaining ordinary .js import URLs. Compressed responses need the matching Content-Encoding (br or gzip), Vary: Accept-Encoding, and a JavaScript MIME type. Uncompressed responses must not be marked compressed. Python's basic HTTP server serves the ordinary modules directly. Deploy the entire matching dist/ module tree as one version. See the README's optimized-distribution notes.\n\n## Troubleshooting\n\n| Symptom | Action |\n| --- | --- |\n| file://, insecure-origin, or AudioWorklet unavailable | Serve over HTTPS or http://localhost, in a browser supporting ES modules and AudioWorklet. |\n| Bare opm.js browser import cannot resolve | Import a served ./opm/... URL after copying assets; a bare specifier needs a separately configured import map or bundler. |\n| Worklet/module 404, HTML returned instead of JS, or MIME error | Copy the whole dist/ tree; preserve relative paths and exclude assets from SPA HTML rewrites. Check the JavaScript MIME type. |\n| Autoplay blocked or playback before start | Invoke and await opm.start() inside a user click before playNote(). |\n| Invalid pitch, voice name, duration, or unknown voice | Check the browser API argument bounds and register custom names before playback. |\n| Scheduled notes disappear | Watch lifecycle rejections/steals and diagnostics; respect selected logical polyphony (1–32, default 8), voicePriority admission and bounded IDs/events. |\n| Node makes no speaker sound/file | PCM must be played/saved explicitly; use the WAV export recipe above. |\n| Processor failure | Handle onEvent errors and failed diagnostics; close/restart rather than bypassing validation or substituting a fallback engine. |\n| npm registry E404 | Install the GitHub release tarball or checkout-packed local tarball, not a presumed registry release. |\n| Bank parse fails | Supply an array of complete, uniquely named schema voices within the bank size/count limits. |\n| Compressed bytes look like gibberish | Set the correct encoding and MIME response headers, or serve the ordinary .js modules without compression. |\n","href":"./usage.en.html"},{"title":"OPM.js usage guide (English) · OPM.js usage guide (English)","text":"OPM.js usage guide (English) OPM.js usage guide (English)","href":"./usage.en.html#opmjs-usage-guide-english"},{"title":"OPM.js usage guide (English) · New in v1.11 (current checkout, unreleased)","text":"OPM.js usage guide (English) New in v1.11 (current checkout, unreleased)","href":"./usage.en.html#new-in-v111-current-checkout-unreleased"},{"title":"OPM.js usage guide (English) · New in v1.10","text":"OPM.js usage guide (English) New in v1.10","href":"./usage.en.html#new-in-v110"},{"title":"OPM.js usage guide (English) · Existing APIs and examples (introduced in v1.9)","text":"OPM.js usage guide (English) Existing APIs and examples (introduced in v1.9)","href":"./usage.en.html#existing-apis-and-examples-introduced-in-v19"},{"title":"OPM.js usage guide (English) · Acquire and install","text":"OPM.js usage guide (English) Acquire and install","href":"./usage.en.html#acquire-and-install"},{"title":"OPM.js usage guide (English) · Browser quick start","text":"OPM.js usage guide (English) Browser quick start","href":"./usage.en.html#browser-quick-start"},{"title":"OPM.js usage guide (English) · Browser API and lifecycle","text":"OPM.js usage guide (English) Browser API and lifecycle","href":"./usage.en.html#browser-api-and-lifecycle"},{"title":"OPM.js usage guide (English) · Bounded lookahead melody","text":"OPM.js usage guide (English) Bounded lookahead melody","href":"./usage.en.html#bounded-lookahead-melody"},{"title":"OPM.js usage guide (English) · Shared context, output routing, and events","text":"OPM.js usage guide (English) Shared context, output routing, and events","href":"./usage.en.html#shared-context-output-routing-and-events"},{"title":"OPM.js usage guide (English) · Shared scores, tuning and safe recovery","text":"OPM.js usage guide (English) Shared scores, tuning and safe recovery","href":"./usage.en.html#shared-scores-tuning-and-safe-recovery"},{"title":"OPM.js usage guide (English) · Bounded long scores","text":"OPM.js usage guide (English) Bounded long scores","href":"./usage.en.html#bounded-long-scores"},{"title":"OPM.js usage guide (English) · Beat transport and performance policy","text":"OPM.js usage guide (English) Beat transport and performance policy","href":"./usage.en.html#beat-transport-and-performance-policy"},{"title":"OPM.js usage guide (English) · Offline PCM with Node.js","text":"OPM.js usage guide (English) Offline PCM with Node.js","href":"./usage.en.html#offline-pcm-with-nodejs"},{"title":"OPM.js usage guide (English) · Voice format and banks","text":"OPM.js usage guide (English) Voice format and banks","href":"./usage.en.html#voice-format-and-banks"},{"title":"OPM.js usage guide (English) · WAV export and DX7 import","text":"OPM.js usage guide (English) WAV export and DX7 import","href":"./usage.en.html#wav-export-and-dx7-import"},{"title":"OPM.js usage guide (English) · Incremental WAV and browser Worker export","text":"OPM.js usage guide (English) Incremental WAV and browser Worker export","href":"./usage.en.html#incremental-wav-and-browser-worker-export"},{"title":"OPM.js usage guide (English) · DX7 conversion","text":"OPM.js usage guide (English) DX7 conversion","href":"./usage.en.html#dx7-conversion"},{"title":"OPM.js usage guide (English) · Quality and release acceptance","text":"OPM.js usage guide (English) Quality and release acceptance","href":"./usage.en.html#quality-and-release-acceptance"},{"title":"OPM.js usage guide (English) · Compressed deployment","text":"OPM.js usage guide (English) Compressed deployment","href":"./usage.en.html#compressed-deployment"},{"title":"OPM.js usage guide (English) · Troubleshooting","text":"OPM.js usage guide (English) Troubleshooting","href":"./usage.en.html#troubleshooting"},{"title":"OPM.js 使用說明(繁體中文)","text":"OPM.js 使用說明(繁體中文) # OPM.js 使用說明(繁體中文)\n\nEnglish · 專案 README · 原始碼儲存庫\n\nGitHub Release v1.10(套件 1.10.0)加入可攜 Arrangement 專案、選用的 MIDI 檔案表情保留與損失摘要、依工作負載限定的容量候選,以及有界本機 MIDI 擷取/驗收活動準備。保留 canonical v6 音色、表情控制、32 聲部與 Worker 匯出。GitHub 發佈不表示 npm 已上架。Node.js 需 22+;README 是公開契約。\n\n音色格式版本與套件版本獨立:已發佈的 v1.10 與歷史 v1.7 使用 canonical v6;目前 v1.11 checkout 使用 canonical v7(新增每個運算子的 waveform 與 noiseRate);不可變的歷史 v1.6 Release 使用 v5。明確指定的舊音色版本 1–6 仍保留各自原有輸入格式,並正規化為 v7。\n\n本次 Release 範圍:可攜 Arrangement 專案、選用的 MIDI 檔案表情保留與損失摘要、實測容量候選及跨功能契約總覽包含於 v1.10。實體手機/MIDI 與人工聽感仍須真正的原始觀察才能完成驗收,不能用自動播放冒充通過。\n\n- 安裝與範例頁面\n- 瀏覽器靜態部署\n- 瀏覽器 API 與自訂音色\n- Node.js 離線渲染\n- 音色格式與銀行載入\n- 壓縮部署\n- 疑難排解\n\n## v1.11 新功能(目前 checkout,尚未發佈)\n\n- 音色 v7 波形與雜訊。每個運算子可指定 waveform(sine、half、abs、quarter、alternating、camel、square、saw、noise)與 noiseRate(20–20000 Hz)。既有正弦音色輸出不變;square/saw(polyBLEP)與 half(polyBLAMP)有邊緣抗混疊修正,但與 noise 一樣不保證無混疊。內建音色庫新增原創的 noise-snare、noise-hihat、explosion。詳見表情音色參考。\n- 選用立體聲效果。createStereoEffects/applyEffects(離線)與 createEffects(專用 AudioWorklet insert)提供 chorus 與 reverb;不會自動接線。詳見音訊匯流排。\n- MIDI 程式對照與 RPN。programVoices/drumVoices 將 Program Change 與 GM 鼓音符對應到具名 FM 音色(需主動啟用;gmProgramVoices/gmDrumVoices 為起手對照表);保留模式的檔案匯入與即時轉接器也會解碼 RPN 0 的 pitch-bend 靈敏度。詳見 MIDI 檔案與 MIDI 演奏。\n- .opm 匯入。opm.js/voices/opm.js 的 importOPM/describeOPM 近似轉換 VOPM/MXDRV 系列文字音色;並非暫存器層級模擬。\n\n## v1.10 新功能\n\nHTTPS 線上範例需點擊播放按鈕才能啟動音訊。以下功能包含於 v1.10;歷史 Release 保持不變。GitHub Release 不表示 npm 上架或網站部署。使用端可直接安裝 Release tarball;原始碼貢獻者執行 npm ci、npm run build。\n\n- parseArrangementProject(source: string | object)/serializeArrangementProject(project)由 root/core 匯出,讀寫獨立的 ArrangementProject version 1。嚴格 own-data 驗證回傳獨立且深度 frozen 的定義,儲存 voices/settings、tempo/meter、layers、sections 與必要的 initialSection。解析/載入不啟動音訊;上限 8 MiB、1–16 layers、32 sections、合計 65,536 events、128 voices/256 KiB 正規化音色 JSON。重播從 beat 0 開始,不儲存即時游標、待執行轉換、淡入淡出或 DSP 狀態。詳見可攜 Arrangement 專案。\n- importMidiFile(Uint8Array, options?)回傳 { events, tempoMap, timeSignature, warnings, lossSummary };exportMidiFile(events, options?)回傳 Uint8Array,由 root、core 與 opm.js/midi-file匯出。僅支援 format 0/1 PPQN。預設 controls: 'omit'維持 note-only 匯入,匯出仍拒絕 control events/非零 pan;選用 controls: 'preserve'可依明確 pitchBendRange 與 channel ownership 政策保留 pitch、expression、pan、modulation。lossSummary區分遺漏、近似與已保留的來源控制訊息計數;sustain 仍是音符長度近似。匯出仍拒絕 ramp/glide、gain/音色控制、非整數音高、非零 priority、channel ownership 歧義與 linear tempo ramp。無 program-to-FM 轉換或 SysEx 傳送。詳見MIDI 檔案。\n- 實測容量候選僅適用於所測工作負載、瀏覽器與機器,不是所有裝置的通用容量保證。\n- 有界本機 MIDI 擷取與實體裝置/聽感活動準備協助收集真正原始觀察,不代表已完成實體或人工驗收。\n- 跨功能契約總覽整理重播、表情控制、渲染與 host 邊界。\n\n### 既有 API 與範例(於 v1.9 引入)\n\n- parseScoreProject(source: string | object)/serializeScoreProject(project)讀寫 canonical version 1 專案:拍點 events、正規化 tempoMap/timeSignature、具名完整 voices 與 synthesis settings。預設為 120 BPM、4/4、44100 Hz、standard、8 聲部、mixGain 1、A4 440 Hz、oldest;嚴格 own-data 驗證且回傳 frozen snapshot。上限 8 MiB、65,536 events、128 voices/256 KiB 音色 JSON。compileBeatSequence(events, { tempoMap?, bpm?, voices? })轉為驗證過的秒制 SequenceEvent[],供離線/Worker 渲染;Transport 直接使用拍點 events,先將具名音色載入 OPM。詳見樂譜專案。\n- TransportOptions.startupLead為 0–10 秒,預設 min(0.05, horizon / 2):啟動/恢復/重建以未來時刻為原點,提前量期間維持音樂位置;0 可取消提前量。這修正歷史 beat-0 冷啟動缺陷,但不保證主執行緒停頓下的 deadline。\n- Arrangement layer gain為 0–1(預設 1),switchSection/setLayer可指定 fade 0–10 秒;setLayerGain(name, gain, { quantize?, fade? })回傳提交拍點。共用 layer 不重新起音,gain 獨立乘上 expression,涵蓋自行擁有的 release tails 與新音符;layer score 的 gain controls 保留給 Arrangement,音樂力度請用 expression。詳見自適應音樂。\n- MidiAdapterOptions.controllerMap至多 128 項,指定 controller、field、明確 min/max/ramp、選用 reset;operator tuple field 另需零起算 operator。重複 CC/target 與 CC64/120/121/123 拒絕。Scalar field 包含獨立 NoteControls.gain(0–1、預設 1);CC121 恢復建立 adapter 時擷取的有效預設或 explicit reset,fixed-Hz 的 reset:null 回到 ratio mode。performance.getPartControls(part)回傳 frozen 有效聲部控制,不含個別 key override。詳見MIDI 表情控制。\n- 套件建置 13 個 demo scripts,包含兩首原創歌曲的播放/停止、專案存取與 WAV 匯出;demo HTML 仍僅供 checkout/網站使用。\n\n建置在 dist 輸出後產生 HTML 文件與完整編譯器產生的 API 參考,本機新建套件將它們放在 doc/;歷史套件不變。可直接開啟 doc/index.html,或連同頂層 Markdown/法律檔案部署整個 doc/。本機搜尋支援鍵盤,不需伺服器、遠端搜尋服務或 runtime dependencies;Markdown 仍是唯一文件來源。\n\n## 安裝與範例頁面\n\n需要 Node.js 22+ 與 npm。不需 checkout 或建置工具鏈即可在新專案安裝 GitHub Release 的附加套件:\n\nsh\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install https://github.com/JS-PACKAGE/OPM.js/releases/download/v1.10/opm.js-1.10.0.tgz\n\n\nv1.10 Release tarball 包含這份同步文件;原 v1.8 標籤與套件維持不變。若要從原始碼自行封裝,以下另一路徑先在 OPM.js 儲存庫根目錄 執行,npm pack 會自行執行建置(prepack),不必預先重複執行 npm run build:\n\nsh\nnpm ci\nnpm pack\n\n\n此 checkout 的套件版本產生 opm.js-1.10.0.tgz。以下從儲存庫根目錄建立同層的新專案 opm-app,假設 checkout 目錄名為 OPM.js;若名稱不同,請調整安裝指令中的路徑。若已用上面的 Release 路徑安裝,請跳過這段:\n\nsh\ncd ..\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install ../OPM.js/opm.js-1.10.0.tgz\n\n\n既有專案只需在該目錄以 Release URL 或 tarball 的實際路徑執行 npm install,不需 npm init。此流程不假設已上架 npm registry;使用端不需建置依賴。封裝含 dist 的最小化 .js 模組、內嵌 TypeScript 原始碼的 .js.map、.d.ts、13 個 demo entry scripts、文件與法律檔案,不含獨立 TypeScript 原始碼檔案、tests、開發 scripts、13 個 HTML 範例頁面或 Vite 範例;維護指令應在 checkout 執行。\n\n要直接試用儲存庫的範例,請在 OPM.js 儲存庫根目錄 啟動伺服器:\n\nsh\npython3 -m http.server 8000\n\n\n開啟 http://localhost:8000/index.html 查看 13 個範例:基本音符、前瞻排程、即時調變、共用 context、WAV、進階控制台、試聽、共用樂譜/中斷恢復、自適應音樂、表情樂器/MIDI、音色設計、音訊匯流排與原創歌曲。HTML 頁面僅供 checkout 使用,頁面訊息為英文。Python 3 只提供本機 HTTP;既有 dist 不需安裝建置,原始碼/helper 改動後才執行 npm ci、npm run build。需 ES modules/AudioWorklet 與 HTTPS 或 localhost,不能用 file://。\n\n## 瀏覽器靜態部署\n\n在已安裝套件的 opm-app 根目錄複製完整發佈樹與授權檔:\n\nsh\nmkdir -p public/opm\ncp -R node_modules/opm.js/dist/. public/opm/\ncp node_modules/opm.js/LICENSE public/opm/LICENSE\n\n\n將下列頁面存為 opm-app/public/index.html:\n\nhtml\n\u003c!doctype html>\n\u003chtml lang=\"zh-Hant\">\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>OPM.js example\u003c/title>\n\u003cbutton id=\"play\" type=\"button\">Play brass chord\u003c/button>\n\u003coutput id=\"status\" aria-live=\"polite\">Ready\u003c/output>\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });\n status.textContent = 'Playing brass chord';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\u003c/html>\n\n\n在 opm-app 根目錄執行:\n\nsh\npython3 -m http.server 8000 --directory public\n\n\n開啟 http://localhost:8000/,點擊按鈕播放。上述複製指令適用 POSIX shell;其他系統可手動複製相同檔案。若完全不使用 npm,亦可將 checkout 的完整 dist/ 內容複製到網站根目錄的 site/opm/,將 checkout 的 LICENSE 複製為 site/opm/LICENSE,把同一 HTML 存成 site/index.html,並以網站根目錄 site/ 提供服務。務必連同 api/、core/、worklet/、voices/ 一起保留,不能單獨重新命名或移動 worklet 及其相依模組。引擎保留來源模組路徑,不再產生 hashed chunks。純 HTML 無 import map 時不能直接解析 import 'opm.js';即便使用 bundler,也不能假定它會複製 worklet URL 的模組相依樹。上述靜態複製與站內相對 URL 不依賴 bundler。\n\nWorker 匯出需要完整同版本樹,包括 companions、maps 與 declarations:\n\ntext\npublic/opm/\n├── api/\n├── core/\n├── worklet/\n├── worker/render.js\n└── voices/\n\n\nWorker 為同源 HTTP(S) 靜態 module,不使用 blob/data/eval;正式 CSP 的 worker-src/script-src 應允許 'self',資產須有正確 JavaScript MIME。不要把 worker 請求改寫為 SPA HTML。\n\n使用 Vite 的獨立、子路徑部署範例見 examples/vite/README.md(僅供 checkout):它安裝 tarball 並複製完整 dist 與 LICENSE,不依賴 npm registry 是否已上架。正式站點須提供正確 JavaScript MIME、真正的資產 404,且不得把 worklet 請求改寫成 SPA HTML;設定 CSP 時應使用外部 module script 與適當來源政策,不要為了範例的行內 script 放寬正式站點防護。部署安全細節見 SECURITY.md。\n\n已安裝套件也提供 opm-assets,可取代手動複製。從 opm-app 根目錄使用尚不存在的目的目錄:\n\nsh\nnpx --no-install opm-assets copy public/opm-1.10.0\nnpx --no-install opm-assets check https://your-host.example/opm-1.10.0/\n\n\n第二行請換成實際部署 URL;本機可用 loopback HTTP。若採此目錄,頁面匯入路徑也須改為 ./opm-1.10.0/api/index.js。copy 原子複製完整 dist/LICENSE、逐檔雜湊並建立 manifest,絕不覆寫既有目錄;check 比對實際回應的狀態、JavaScript MIME、nosniff 與 SHA-256,拒絕 redirect/SPA fallback,但不驗證頁面 CSP 或啟動音訊。一般 Python HTTP server 不提供 nosniff,不符合預設 check;正式主機須設定此標頭。完整契約見宿主整合。\n\n## 瀏覽器 API 與自訂音色\n\n| API | 用法與限制 |\n| --- | --- |\n| new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain=1, tuning={}, stealing='oldest', interruption='cancel', quality='standard', maxVoices=8 }={}) | 整數 sampleRate 8000–96000 Hz;maxVoices 為整數 1–32,預設 8。quality/maxVoices 建構後不可變,close/重啟仍保留。借用 context 不會被關閉/暫停;省略 destination 接到其 destination,null 不自動接線。workletUrl 可改變同源完整 worklet 模組樹的位置,但不放寬安全來源、CSP 或 MIME 限制。 |\n| await opm.start()/await opm.resume() | 在使用者互動中啟動/恢復並等待,再播放。並行啟動會合併;既有 context 被瀏覽器暫停後可恢復。 |\n| opm.connect(destination)/opm.disconnect(destination?) | 對已啟動的輸出接線/斷線,回傳 opm;目的節點必須來自相同 context。省略斷線目的地會移除所有輸出連線。 |\n| opm.loadVoice(name, voice) | 啟動前即可驗證、複製並註冊音色;名稱為 1–64 個 ASCII 英文字母、數字、_ 或 -,registry 最多 128 筆。覆蓋只影響未來音符。 |\n| opm.voices | ReadonlyMap 的防禦性快照;音色深度凍結。更換音色須用 loadVoice,修改取得的 Map 不會改變引擎。 |\n| opm.replaceVoiceBank(bank)/opm.exportVoiceBank()/opm.removeVoice(name) | 完整驗證後原子替換;bank 是 JSON 字串或完整音色陣列,最多 128 筆/JSON 256 KiB。直接傳 [] 清空,但 JSON \"[]\" 依 parser 契約拒絕。export 回傳具名不可變音色的 canonical JSON(空銀行為 []),上限 256 KiB。remove 回傳 boolean,不終止正在播放/排隊的快照。 |\n| opm.playNote({ voice='brass', note, time=0, at, duration=null, velocity=1, pan=0, voicePriority=0, late='start' }) | 有限 MIDI 小數 0–127;相對 time 0–60 秒或互斥的絕對 at(最多提前 60 秒)。duration 為 (0,60] 或 null 持續音;力度 0–1、聲像 −1–1、voicePriority 為整數 0–127。回傳正安全整數音符 ID,不代表接收成功。 |\n| opm.stop(id,{at,cancelControls}={}) | 一般 stop 取消待開始音符/開始 release,保留自然尾音的 automation。cancelControls:true 立即取消此 ID 的排隊 onset/control/off 並 release;cancelControls 欄位即使為 false,也不能與 at 共用。未知、終止或已 release 目標會回報命令拒絕。 |\n| opm.updateNote(id,controls,{at}={}) | 回傳命令 ID;待開始控制保留到 onset,release 中仍可更新。無效、inactive 或 queue-full 命令會回報拒絕。 |\n| opm.allNotesOff()/opm.panic() | 回傳命令 ID,不受已滿排程佇列阻擋。前者取消待處理事件並自然 release;後者立即移除所有尾音並發出 reset,保留路由/快取。 |\n| opm.setMixGain(gain)/opm.setTuning(tuning) | 須先啟動,回傳命令 ID;設定保留到 node 重建,不重設 phase/包絡。 |\n| await opm.getDiagnostics() | 回傳 {type:'diagnostics',requestId,activeVoices,pendingEvents,errors,rejectedNotes};context 須在 running,最多 64 個未完成請求。暫停/關閉/processor 失敗會拒絕待回覆請求,請先 resume 再重試。 |\n| await opm.close() | 與初始化依序處理,斷開 node,只關閉自行建立的 context。再次啟動會重建自有 context;借用的 context 仍可由宿主使用。 |\n| opm.subscribe(listener) | 獨立事件訂閱,回傳冪等的取消訂閱函式;各回呼例外隔離。 |\n| await opm.waitForCommand(commandId,{timeout,signal}={}) | 僅等待 admission:預設 5000 ms、整數 1–60000 ms,可由 AbortSignal 取消;最多 64 pending waits/128 receipts。拒絕拋出 CommandRejectedError;accepted 不是執行完成。 |\n| await opm.dispose() | 終止式清理,不能重新啟動;close 仍可重新 start。 |\n\nonEvent 除 note/diagnostics/error,新增 {type:'command',command,commandId?,id?,state,reason?,frame,time}、context 狀態與全域 reset。命令 state 為 accepted/rejected;accepted 只表示接收或立即套用,不保證未來一定執行。以 commandId 對應拒絕;frame/time 為音訊邊界而非回呼送達時間。reset 原因為 close/failure/panic/interruption,收到時清除自有 gate 紀錄;close/failure 不保證每音符終止回覆。回呼例外隔離。\n\n遲到音符預設 late:'start':在可用的第一個影格開始,數值 duration 從實際開始保留完整 gate;late:'drop' 則以 reason:'late' 拒絕,包含訊息處理延遲造成的遲到。同影格依序處理 stop、onset、controls;onset 前收到的控制保留到開始。\n\ncontrols 為非空 own-data 物件:pitch −48..48 半音;glide 0..10 秒且須搭配 pitch;expression 0..1;pan −1..1;modulation 0..2(AM 上限 1、PM 1200 音分);operatorLevels 是四個 0..2 的原音色 level 倍率。新增 feedback 0..7、lfoRate 0..20 Hz、amDepth 0..1、pmDepth 0..1200 音分;operatorRatios 為四個 0.125..32,operatorFrequencies 為四個 1..20000 Hz 或 null(恢復 ratio 模式),operatorADSR 為四個完整 {a,d,s,r}。ramp 0..10 秒獨立平滑指定的 scalar/level/ratio/frequency 欄位,省略/零立即生效;glide 獨立以半音線性滑動。phase/回授歷史保留,但 ADSR 從當前 dB 重新錨定:held 音重啟 attack,released 音開始新縮放 release(最多 10 秒),零 release 立即進入 filter drain。固定 Hz 仍跟隨 pitch 控制,忽略 tuning table 的移調。未知欄位、存取器、非有限/超界值拒絕。詳細控制語意見表情音色,宿主時鐘/生命週期見宿主整合。\n\nv1.9 新增 gain(0–1、預設 1),獨立乘上 expression,依 ramp 平滑且不重啟 phase/包絡;Arrangement layer fade 使用這個獨立倍率。\n\n最多 maxVoices 個邏輯聲部(1–32,預設 8),另有至多八個獨立約 5 ms 搶音淡出;增加 maxVoices 不增加淡出數。滿額時只能搶走 voicePriority 不高於新音符的聲部,先選最低優先權,再依 stealing 政策決定。全部較高時回報音符拒絕 reason:'priority',不搶走既有音符。預設 oldest;release-first 優先最早 release,quietest 按 carrier 包絡 × 力度 × expression 比較,同分取最早,不以瞬間波形判定。事件/ID 各限 256,未來定時音符佔兩筆,控制/stop 也佔額度;終止會回收過期事件。預備音色採 128 槽 content-key LRU,重新驗證後替換/重用 ID,既有排程與聲部保留原快照。無 setLFO;速率/深度可用 updateNote,波形與目標屬於音色。\n\n要自訂 LFO、排程與提早停止,保留上方 public/index.html 的 #play 按鈕與 #status 輸出,將其原本的 \u003cscript type=\"module\">…\u003c/script> 完整替換為以下區塊;從 opm-app 根目錄照前節提供 public/:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { brass } from './opm/voices/brass.js';\n\n const opm = new OPM();\n const voice = structuredClone(brass);\n voice.lfo.rate = 5;\n voice.lfo.amDepth = 0.2;\n voice.ops[0].level = 0.6;\n opm.loadVoice('my-brass', voice);\n\n const play = document.querySelector('#play');\n play.textContent = 'Play custom brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const at = opm.context.currentTime + 0.1;\n const id = opm.playNote({ voice: 'my-brass', note: 64, at, velocity: 0.7, pan: -0.5 });\n opm.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8, pan: 0.5, modulation: 1.2 }, { at: at + 0.1 });\n opm.stop(id, { at: at + 0.3 });\n status.textContent = 'Scheduled custom brass note and controls';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\n上述 onset、控制與 release 皆由音訊時鐘執行,不靠頁面計時器停止;單次僅排三筆事件,且皆在 60 秒範圍內。完成使用後可在應用程式生命週期中 await opm.close(),下次要播放時再由使用者互動呼叫 start()。\n\n### 有界前瞻排程\n\n長曲用 createLookaheadScheduler 分批排程,而非一次塞滿 queue。將同一 opm-app/public/index.html 的 module script 完整替換如下,再從 opm-app 根目錄提供 public/。點擊播放會安排有限的八個音符;主執行緒停頓時跳過已錯過的窗口,不補塞過期音符:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createLookaheadScheduler } from './opm/api/index.js';\n const status = document.querySelector('#status');\n const opm = new OPM();\n const melody = [60, 64, 67, 72, 67, 64, 62, 60];\n let origin;\n const scheduler = createLookaheadScheduler(opm, ({ from, to, maxNotes }) => {\n origin ??= from + 0.1;\n return melody.flatMap((note, index) => {\n const at = origin + index * 0.3;\n return at >= from && at \u003c to\n ? [{ note, at, duration: 0.2, velocity: 0.6, late: 'drop' }]\n : [];\n }).slice(0, maxNotes);\n }, {\n horizon: 0.2, interval: 0.025, maxNotes: 8,\n onError(error) { status.textContent = error.message; }\n });\n const play = document.querySelector('#play');\n play.textContent = 'Play finite melody';\n play.addEventListener('click', async () => {\n try {\n scheduler.stop();\n origin = undefined;\n await scheduler.start();\n status.textContent = 'Scheduling eight notes';\n } catch (error) { status.textContent = error.message; }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop melody';\n document.body.append(stop);\n stop.addEventListener('click', () => scheduler.stop());\n window.disposeOPM = async () => {\n scheduler.dispose();\n await opm.close();\n };\n\u003c/script>\n\n\n回呼同步回傳陣列,每筆必須有窗口 [from,to) 內的絕對 at 與 (0,60] 數值 duration,不能用相對 time 或持續音。onError 必填;計時器/回呼失敗會停止排程並通知它。預設 horizon 為 0.2 秒(範圍 0.02–10),interval 為 0.025 秒(0.005–1),且 horizon 必須大於 interval;maxNotes 預設 32(整數 1–128),最多追蹤 128 個尚未關 gate 的自有音符。本例每個窗口至多一音,全部音符播放後回呼回傳空陣列,計時器仍運作直到 stop/dispose。兩者只取消/釋放自己的音符,不關閉 OPM;dispose 後不可重啟。移除介面時呼叫 await window.disposeOPM()。\n\n任何 non-running context 或 reset 都會停止前瞻排程並通知 onError,須由使用者互動明確重啟。預設 interruption:'cancel' 取消中斷前 gate/automation;preserve 只保留直接音符/佇列,不自動恢復前瞻計時器。\n\n\n### 共用 context 與輸出接線\n\n若宿主已有 AudioContext,可將快速範例的 module script 替換成以下區塊。按播放後音符持續到按停止;disposeOPM() 只清除本合成器,不會關閉宿主 context:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n const context = new AudioContext();\n const gain = context.createGain();\n gain.gain.value = 0.3;\n gain.connect(context.destination);\n const status = document.querySelector('#status');\n const opm = new OPM({\n context, destination: null,\n onEvent(event) {\n if (event.type === 'reset') {\n id = undefined;\n if (event.reason === 'close' || event.reason === 'failure') connected = false;\n }\n status.textContent = event.type === 'error' ? event.error.message\n : event.type === 'note' ? ${event.id}: ${event.state}\n : event.type === 'diagnostics' ? Voices: ${event.activeVoices}\n : event.type === 'command' ? ${event.command}: ${event.state} ${event.reason ?? ''}\n : event.type === 'context' ? Context: ${event.state} : Reset: ${event.reason};\n }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop held note';\n document.body.append(stop);\n let id;\n let connected = false;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.resume();\n if (!connected) { opm.connect(gain); connected = true; }\n if (id !== undefined) opm.stop(id);\n id = opm.playNote({ note: 60, velocity: 0.7, pan: 0.5 });\n console.log(await opm.getDiagnostics());\n } catch (error) { status.textContent = error.message; }\n });\n stop.addEventListener('click', () => { if (id !== undefined) opm.stop(id); });\n window.disposeOPM = async () => {\n await opm.close();\n connected = false;\n id = undefined;\n gain.disconnect();\n };\n\u003c/script>\n\n\n移除此應用程式的音訊介面時呼叫 await window.disposeOPM()。context/下游節點由宿主管理;不要為了停止單一合成器而暫停整個共用 context。\n\n### 共用樂譜、全域調音與安全恢復\n\nmixGain 是有限 0–1(預設 1),在 tanh 前縮放;下游 GainNode 只控制聆聽音量,無法還原已產生的飽和失真。tuning 替換完整設定:referenceHz(A4)20–20000 Hz,offsets 可省略表示零,否則為恰好 128 個 −4800..4800 的有限音分。小數 MIDI 線性插值相鄰偏移,127 使用自身端點。即時調音保留 phase/包絡與原音符 key scaling。\n\n保留 quick-start 的 play/status,將 module script 替換為:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, playSequence } from './opm/api/index.js';\n import { renderSequence } from './opm/core/index.js';\n import { brass } from './opm/voices/brass.js';\n const settings = { mixGain: 0.5, tuning: { referenceHz: 442 }, stealing: 'release-first' };\n const opm = new OPM({ ...settings, interruption: 'cancel' });\n const score = [\n { type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },\n { type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },\n ];\n let playback;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.start();\n playback?.stop();\n opm.loadVoice('lead', brass);\n playback = playSequence(opm, score, { at: opm.context.currentTime + 0.05 });\n const audio = renderSequence(score, { ...settings, sampleRate: opm.context.sampleRate, voices: opm.voices });\n document.querySelector('#status').textContent = Rendered ${audio.left.length} frames; errors ${audio.diagnostics.errors};\n } catch (error) { document.querySelector('#status').textContent = error.message; }\n });\n window.disposeOPM = async () => { playback?.stop(); await opm.close(); };\n\u003c/script>\n\n\n樂譜使用相對秒數;note 為唯一正 ID 與有限 duration,stop/control 指向同一樂譜音符。整份驗證後才派送;限 128 notes、256 保留槽(每音符兩槽加命令)、含 gate 的 60 秒及離線 4,000,000 影格。playSequence 回傳防禦性 scoreId→noteId Map 與冪等 stop,不會對其他 caller 原子保留 worklet 容量。renderSequence 包含尾音;相同 PCM 須設定/取樣率/相對影格原點一致且無其他競爭音符。WAV 僅傳 left/right/sampleRate。\n\n以 HTTPS 在實體 iOS/Android 使用第 08 範例(僅供 checkout),依行動裝置驗收流程與支援矩陣記錄裝置/OS/瀏覽器/取樣率/政策、觀察與手動 pass/fail/unverified 判定。涵蓋鎖屏、切換 app、來電、耳機/Bluetooth、節電、長播放與主執行緒停頓。報告僅留本機;桌面、headless 與 analyser 訊號不代表實機恢復或聽感不中斷。恢復須手勢;借用 context 由宿主管理。\n\n### 有界長樂譜\n\n24 小時/65,536 個輸入事件以內,改用 opm.js/core 的 prepareLongSequence、estimateSequenceCapacity 與 renderSequenceChunks。估算會分別列出整段 buffer/單批 eligibility 與預設串流視窗密度,並不保證和其他 caller 競爭時的 admission。每塊 PCM 是借用 buffer,下一次 next() 就會覆寫;立即消費,只有需要保留時才複製。break、cancel() 或 AbortSignal 停止前進,maxFrames 可限制累計工作量;整段 PCM/WAV 預算不變。\n\njs\nimport { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';\nconst score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];\nconst options = { sampleRate: 96000, chunkFrames: 4096 };\nconsole.log(estimateSequenceCapacity(score, options));\nlet frames = 0, energy = 0;\nconst render = renderSequenceChunks(score, options);\nfor (const chunk of render) {\n for (let i = 0; i \u003c chunk.frames; i++) energy += chunk.left[i] ** 2;\n frames += chunk.frames;\n}\nconsole.log(frames, energy, render.diagnostics.errors);\n\n\n瀏覽器播放時與 OPM 一起 import streamSequence,建立 const stream = streamSequence(opm, score, { onError: console.error }),再於播放按鈕手勢內 await stream.start()。stop() 僅取消此串流的音符;dispose() 移除 listener 並永久終止此實例。control、明確 stop 與自動 release 跨視窗保留 score ID 對應。兩種政策的 interruption/reset 都停止串流;先恢復 context,再由使用者明確重啟。完整瀏覽器配方、預設值與密度限制見串流契約。\n\n### 拍點 Transport 與演奏聲部\n\ncreateTransport、createArrangement、createPerformance、createMidiAdapter、requestMidiAccess、playSequence、streamSequence、renderSequenceInWorker 與套件版本常數 VERSION 由根入口 opm.js(靜態部署為 ./opm/api/index.js)匯出。Transport 事件以 beat 取代 time,note.duration 也以四分音符拍計算。提供 start/resume、pause/stop、seek、setTempo(從目前位置起)、setTempoMap、setLoop、pump、dispose 與 state/position/running/snapshot/ids。它使用 AudioContext 時鐘,只管理自有音符。\n\nbpm 預設 120、範圍 1–1000;tempoMap 從 beat 0 開始嚴格遞增,最多 1024 點,拍點上限 86400。timeSignature 分子 1–32,分母為不超過 32 的二次冪;horizon 0.01–10 秒、interval 0.001–horizon/2、maxSlots 1–256。pause/seek/loop 重啟包絡與 phase,不是 DSP snapshot;重建當前 scalar/ratio ramp 與剩餘時間,最新 fixed-Hz/null 政策立即套用(即使 frequency ramp 未完成),最新 ADSR 在重啟 onset 重新錨定。純核心亦匯出 beatsToSeconds/secondsToBeats/beatToBarBeat/barBeatToBeat/normalizeTempoMap。\n\n下方配方保留音樂上的一拍 count-in。目前 checkout 額外使用 startupLead(預設 min(0.05,horizon/2) 秒)將時鐘原點放在未來,避免 beat 0 冷啟動時提交到已過期時刻;歷史 v1.8.1 套件沒有這項修正。late:'drop' 仍會拒絕真正晚到的起音、停止 helper 並通知 onError。Count-in/startupLead 不保證宿主停頓下仍能準時;start() 完成也不是每個音符已發聲的確認。\n\ncreatePerformance(opm,{parts:16,maxKeys:128,maxKeysPerPart:128,onError}) 提供零起算 1–16 個聲部,各自 configurePart 設定 voice、poly/mono、legato、last/high/low 按鍵選擇 priority、glide 0–10 秒、pan ±1 與 expression 0–1;兩種 key 額度皆為 1–128。先在使用者手勢中 await opm.start(),再呼叫 noteOn;回傳實體 key ID,須用 noteOff(part,key) 釋放,同音高按鍵仍獨立。另有 updatePart、sustain、allNotesOff(part?)、getPart 與 dispose。實際 held keys 優先於踏板保留;mono legato 在原 onset ±48 半音內重用 gate/包絡/原力度與 key scaling,超出時以真實音高重觸發。被偷走的 key 會移除,不自動重入;中斷/reset 即使 preserve 也清除 helper 自有狀態。可直接執行的 Transport/Performance 配方見宿主整合與串流樂譜。\n\nv1.8 的 updateKey(part,key,controls) 對單一實體按鍵套用 NoteControls;未選取的 mono key 保留控制,不改動目前 gate。updatePartNotes(part,controls) 更新整個聲部的自有音符(含 release 尾音)與未來音符預設。configurePart 的 voiceLimit 為 1–32,自有 release 尾音也佔額度,滿額 noteOn 拋出 RangeError;它不增加引擎聲部數。voicePriority 為 0–127(預設 0),與 last/high/low 的按鍵選擇 priority 不同。\n\n選用的 createMidiAdapter(performance,await requestMidiAccess(),options) 將使用者授權的 MIDI inputs 映射到聲部,支援 note、sustain、bend、wheel、volume/expression、pan、pressure 與 all-notes-off。匯入模組不會要求權限,永不要求 SysEx;disconnect/dispose 只釋放 adapter 自有按鍵。詳細配方與限制見表情演奏與 MIDI及第 10 範例(僅供 checkout)。\n\n### 速度曲線、網格與分層編曲\n\nTempoPoint 為 {beat,bpm,curve?,endBpm?};省略 curve 或 'step' 為定速段,'linear' 使 BPM 隨拍點線性變化至下一點 bpm 或指定 endBpm(僅 linear 可用;末點不可 linear)。beatsToSeconds/secondsToBeats 使用閉式對數積分與反函式。quantizeBeat、swingBeat、swingBeatEvents 從根入口 opm.js 匯入(純瀏覽器使用部署的 ./opm/api/index.js),不是 opm.js/core 匯出;後者連同音符起點與終點一起套用 swing。\n\ncreateArrangement(opm,{layers,sections,initialSection,bpm?,tempoMap?,timeSignature?}) 在共用拍點網格上循環具名 layers;switchSection/setLayer 在不早於已接收音符的第一個量化邊界提交(預設 bar,也可 beat/拍數),回傳提交拍點。共用 layer 保持連續,不重觸發;移除 layer 預設在邊界 release,preserveNotes:true 可讓它自然結束。layer 與 note 的 voicePriority 取較大值,優先權拒絕計入 snapshot.priorityDrops 而不停止編曲,其他拒絕會停止並通知 onError。至多 16 layers、32 sections、65,536 events,每層循環長度至多 256 拍;只清理自有音符。Arrangement pause/resume 不還原跨越恢復點的持續音,只從後續 onset 繼續;它和 Transport 都不是 DSP checkpoint。公式、密度上限及可執行配方見自適應音樂與第 09 範例(僅供 checkout)。\n\n保留 quick-start 的按鈕/輸出並替換 module script,可直接試用兩種 helper:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createTransport, createPerformance } from './opm/api/index.js';\n const opm = new OPM();\n const transport = createTransport(opm, [\n { type: 'note', id: 1, beat: 1, duration: 1, note: 60 },\n { type: 'note', id: 2, beat: 2, duration: 1, note: 64 }\n ], { bpm: 120, onError: console.error });\n const performance = createPerformance(opm, { parts: 1 });\n performance.configurePart(0, {\n voice: 'brass', mode: 'mono', legato: true, priority: 'last', glide: 0.1\n });\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n transport.stop();\n await transport.start();\n const key = performance.noteOn(0, 67, { velocity: 0.7 });\n performance.sustain(0, true);\n performance.noteOff(0, key);\n document.querySelector('#status').textContent = 'Release pedal to stop held note';\n } catch (error) {\n document.querySelector('#status').textContent = error.message;\n }\n });\n const pedal = document.createElement('button');\n pedal.textContent = 'Release pedal';\n document.body.append(pedal);\n pedal.addEventListener('click', () => performance.sustain(0, false));\n window.disposeOPM = async () => {\n transport.dispose();\n performance.dispose();\n await opm.close();\n };\n\u003c/script>\n\n\n## Node.js 離線渲染\n\n在已安裝套件的 opm-app 根目錄建立 render.mjs:\n\njs\nimport { Synth, prepareVoice } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst sampleRate = 44100;\nconst synth = new Synth(sampleRate);\nconst prepared = prepareVoice(brass);\nconst id = synth.noteOn(prepared, 60);\nconst left = new Float32Array(sampleRate);\nconst right = new Float32Array(sampleRate);\nsynth.render(left, right, 0, 22050);\nsynth.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8 });\nsynth.noteOff(id);\nsynth.render(left, right, 22050, 22050);\nconsole.log(left.some(sample => sample !== 0), synth.errorCount);\n\n\n在同一目錄執行 node render.mjs,預期顯示 true 0。中央聲像為左右相同,其他位置產生立體聲;此例只產生記憶體 PCM,不會自動播放或寫檔。checkout 匯入改為 ./dist/core/index.js 與 ./dist/voices/brass.js。使用 .mjs 或 \"type\":\"module\",沒有 CommonJS 入口。\n\nnew Synth(sampleRate,maxVoices=8,{mixGain,tuning,stealing,quality='standard'}={}) 需要有限 8000–192000 Hz 與整數 1–32 聲部(預設 8);noteOn 接受有限小數 MIDI 0–127、嚴格力度/聲像、voicePriority 0–127(預設 0)及未占用正安全 ID。滿額且所有聲部優先權較高時拋出 VoiceAdmissionError,保留既有聲部。noteOff/updateNote 回傳 boolean;allNotesOff 自然 release,panic 移除所有尾音;設定 gain/tuning 不重設 phase/包絡。onVoiceEnded 每音符一次 stolen/ended/error/cancelled;lastStolenId 無搶音時為 null。\n\nprepareVoice(input) 一次嚴格驗證並建立不可變的可信任快照;不能用型別斷言或自製物件偽造。Synth.noteOn 可重用此快照,原始音色輸入則仍逐次驗證。核心預先配置 maxVoices + 9 個狀態槽(maxVoices 個邏輯聲部、八個淡出與一個入列暫存槽;預設 17 個),重用預備音色不需每音符配置 Float64 狀態陣列,但這不是整個 JavaScript/瀏覽器零配置保證。synth.updateNote(id, controls) 使用前述控制範圍,成功回傳 true,未知/已終止 ID 回傳 false。\n\nonVoiceEnded 可呼叫 noteOn/noteOff;回呼中新增的音符最早在下一影格渲染,不回頭改寫已計算的當前影格。不可在回呼中遞迴呼叫 render;引擎會拒絕。通知有固定工作上限,應保持回呼簡短。\n\nrender(left,right,offset=0,length=nativeLeftLength-offset) 需原生 Float32Array,非負安全整數範圍容納於兩個實際緩衝區。遮蔽 length/fill 不能增加工作,Proxy/偽造及需數值 coercion 的輸入拒絕。自行分段渲染/保留 release;currentFrame 為累計影格,errorCount 記錄數值失敗。\n\n若只需要單音的輔助渲染函式,在 opm-app 根目錄另存 render-note.mjs:\n\njs\nimport { renderNote } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst { samples, sampleRate, diagnostics } = renderNote({\n voice: brass,\n note: 60,\n duration: 0.1,\n sampleRate: 44100\n});\nconsole.log(samples.length, sampleRate, diagnostics.errors);\n\n\n執行 node render-note.mjs;checkout 改用 ./dist/... 匯入。renderNote({voice,note=60,duration=0.5,velocity=1,pan=0,sampleRate=44100}) 回傳 {samples,left,right,sampleRate,diagnostics:{errors}},samples === left。要求完整 Schema 音色;有限的音色數值會限制到範圍,但無效版本/algorithm/feedback 仍拒絕。有限 note/duration/velocity/pan 分別限制至 0–127/0–30 秒/0–1/−1–1;note 可為小數,sampleRate 必須是整數 8000–96000。\n\nrenderNote 也接受嚴格 mixGain/tuning/stealing/quality,與 renderSequence、分塊渲染及 Worker 共用。quality 為 eco(2×/35-tap 半帶 FIR)、standard(預設 4×/15 → 47-tap 半帶 FIR 串接)或 high(8×/11 → 15 → 47-tap);不是無混疊或即時期限保證。模式取捨見聲學品質,CPU 須在目標宿主測量。\n\n此函式與 Synth 使用相同包絡、LFO、濾波、力度、聲像及飽和路徑。長度為 ceil((duration+最長有效release+0.01)sampleRate),包含 note-dependent rate scaling,上限 4,000,000 影格。飽和前聲像增益為 sqrt(2)cos/sin((pan+1)*pi/4);飽和後不保證感知音量恆定。\n\n其他 opm.js/core 公開項目:normalizeVoice(嚴格驗證並產生獨立複本)、prepareVoice、NoteControls/PreparedVoice 型別、envelopeAt(time, gate, adsr)(以秒為單位的時間/gate 計算 dB 包絡對應振幅)、不可變的八種訊號圖 ALGORITHMS、HEADROOM = 0.7、OVERSAMPLE = 4、MAXRENDERSAMPLES = 4000000,以及整數 8000–96000 取樣率驗證器 sampleRateValue。\n\n## 音色格式與銀行載入\n\n參考 voice.schema.js 的 voiceSchema 與 examples.js 的 examples 匯出。下列為單一音色 JSON 物件,四個 ops 順序是訊號演算法的運算子順序;若要作為銀行,必須用 [ 和 ] 包成陣列:\n\njson\n{\n \"version\": 6,\n \"name\": \"simple\",\n \"algorithm\": 7,\n \"feedback\": 0,\n \"modIndex\": 4,\n \"lfo\": { \"rate\": 0, \"amDepth\": 0, \"pmDepth\": 0, \"waveform\": \"sine\", \"amTargets\": [1, 1, 1, 1], \"pmTargets\": [1, 1, 1, 1] },\n \"ops\": [\n { \"ratio\": 1, \"level\": 0.8, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.6, \"r\": 0.2 }, \"keyScale\": { \"breakpoint\": 60, \"leftDbPerOctave\": 0, \"rightDbPerOctave\": 6 } },\n { \"ratio\": 2, \"level\": 0.4, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 3, \"level\": 0.3, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 4, \"level\": 0.2, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } }\n ]\n}\n\n\n| 欄位 | 範圍 |\n| --- | --- |\n| version/name | 目前 canonical 版本 7;explicit 1–6 僅接受各自原有欄位。v1 無 keyScale,v1/2 無 velocitySensitivity,v1–3 無 waveform,v1–4 不接受 v5 欄位;v5 不接受 v6 targets;v1–6 不接受運算子 waveform/noiseRate。名稱為 1–64 ASCII 字母/數字/底線/連字號。 |\n| algorithm/feedback/modIndex | 前兩者是 0–7 的整數;調變強度 modIndex 是 0–16。 |\n| 四個 ops 各項 | 必須提供 ratio 0.125–32、level 0–1、detune -1200–1200 音分,以及 adsr 的 a、d、s、r;a/d/r 是 0–10 秒,s 是 0–1。 |\n| lfo | rate 0–20 Hz、amDepth 0–1、pmDepth 0–1200 音分;waveform 為 sine(預設)、triangle、saw 或 square。 |\n| ops[i].keyScale | 可省略表示平坦;breakpoint 為 MIDI 整數 0–127,leftDbPerOctave/rightDbPerOctave 為 0–24 dB/八度。 |\n| ops[i].velocitySensitivity | 從版本 3 起可選,0–48 dB,省略為 0;定義 velocity=0 時的運算子衰減。 |\n| ops[i].frequency/ops[i].rateKeyScale | v5 可選固定頻率 1–20000 Hz(ratio 仍必填)、速率鍵位縮放 0–4;ADSR 時間乘 2 * (-scale(note-60)/12) 並限制為最多 10 秒。 |\n| ops[i].waveform/ops[i].noiseRate | v7 可選的振盪波形:sine(預設)、half、abs、quarter、alternating、camel、square、saw、noise;noiseRate 為 20–20000 Hz 的保持式雜訊時脈(預設 8000),僅在波形為 noise 時有意義。雜訊運算子沒有音高。 |\n| pitchEnvelope | v5 可選 {a,d,r,initial,peak,sustain,final};時間 0–10 秒、音高 −4800..4800 音分;release 從目前音高連續開始。 |\n| lfo.delay/lfo.sync/lfo.phase | v5 可選 0–10 秒、note/global、0–1 圈,預設 0/note/0;delay 門控深度,不停止 phase 時鐘。 |\n| lfo.amTargets/lfo.pmTargets | v6 可選各四個 0–1 權重;boolean 輸入正規化為數字。省略為 [1,1,1,1],依運算子順序套用。 |\n\n單一音色可省略 version/name/lfo/modIndex,仍需 algorithm/feedback 與四個完整運算子。預設目前格式、modIndex 4、LFO off/sine,輸出統一版本 7;舊版 1–6 嚴格保留原始形狀,不得挾帶後來欄位。內建音色與 DX7 匯出也採 canonical v7。所有路徑拒絕錯誤型別、未知欄位、存取器及稀疏陣列,嚴格路徑另拒絕超界數字。鍵位縮放依原音符;pan 屬於音符。完整組合語意見表情音色參考。\n\nparseVoiceBank() 由 opm.js/voices/schema.js(瀏覽器靜態樹中為 ./opm/voices/schema.js)匯出,可接受 JSON 字串或音色陣列,要求 1–128 筆完整音色且名稱不重複,回傳 Map\u003cname, voice>,其音色為已驗證且凍結的完整物件。字串輸入最多 256 KiB(UTF-8);陣列輸入也有數量限制。和 validateVoice()、renderNote() 相同,此路徑將有限但超界的數值限制在範圍內,與嚴格的單音色 normalizeVoice() 不同;不合法的 version/algorithm/feedback、非有限數值、錯誤型別、未知欄位仍會拒絕。schema.js 也提供 validateVoice(voice)、有限數值邊界函式 bounded(value, min, max, label = 'number')、LIMITS、MAXBANKBYTES 與 MAXBANKVOICES。\n\n要載入已部署的可信任 examples.js 模組,保留先前 opm-app/public/index.html 的 #play 與 #status,將其原本 module script 完整替換為:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { parseVoiceBank } from './opm/voices/schema.js';\n import { examples } from './opm/voices/examples.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n play.textContent = 'Play bank brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const bank = parseVoiceBank(examples);\n for (const [name, voice] of bank) opm.loadVoice(name, voice);\n opm.playNote({ voice: 'brass', note: 60, duration: 0.7 });\n status.textContent = 'Playing bank brass note';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\n仍在 opm-app 根目錄以 python3 -m http.server 8000 --directory public 提供頁面。此例匯入套件所附的可信任模組;parseVoiceBank 仍支援外部 JSON 銀行。若接收不可信任的遠端下載,應由應用程式先限制來源、Content-Type 與下載大小,再讀取回應內容;合成引擎沒有內建網路下載 API。\n\n## WAV 匯出與 DX7 匯入\n\n在 opm-app 建立 export.mjs,執行 node export.mjs 會寫出立體聲 WAV:\n\njs\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { renderNote, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\nimport { importDX7, describeDX7 } from 'opm.js/voices/dx7.js';\nconst audio = renderNote({ voice: brass, duration: 0.7, velocity: 0.8, pan: -0.5 });\nawait writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));\n// 有檔案時:node export.mjs /實際路徑/voice.syx\nif (process.argv[2]) {\n const bytes = new Uint8Array(await readFile(process.argv[2]));\n const voices = importDX7(bytes);\n console.log(describeDX7(bytes));\n const imported = renderNote({ voice: voices[0], duration: 1 });\n await writeFile('imported.wav', encodeWav({ left: imported.left, right: imported.right, sampleRate: imported.sampleRate }));\n}\n\n\ncheckout 匯入改為 ./dist/core/index.js、./dist/voices/brass.js 與 ./dist/voices/dx7.js。encodeWav({left,right?,sampleRate,format='pcm16'}) 回傳小端序 RIFF 位元組,format 可為 pcm16/pcm24/float32;省略 right 為單聲道。所有格式的 Float32Array 樣本須有限且在 −1–1,超界拒絕而非削波;立體聲長度須相同。取樣率整數 8000–192000,上限 4,000,000 影格。只傳這些自有資料欄位:整個 renderNote 結果有額外欄位,會被拒絕。編碼不會自動寫檔;短預覽可使用有明確記憶體預算的 Blob,長檔案須直接寫 sink。\n\n增量 createWavEncoder({sampleRate,channels:1|2,format,totalFrames}) 由 opm.js/core 匯出:先寫一次 header(),每次 encode({left,right?}) 消費 1–65536 影格並回傳有界位元組,最後寫 finalize()(精確影格數與 RIFF padding)。它不保留完整 PCM/輸出,不受完整 buffer 的四百萬影格限制;仍受 RIFF32 大小與 24 小時加 release 預算限制。\n\n瀏覽器長檔案用 renderSequenceInWorker(score,{sampleRate,chunkFrames,quality,format,sink,signal,onProgress}),sink 提供 write(bytes) 與可選 close/abort,write 可回傳 Promise 施加 backpressure;最多一塊編碼資料未確認。progress 提供 frames/totalFrames/bytesWritten/errors,結果為 capacity/format/bytesWritten/diagnostics。AbortSignal 取消立即終止 Worker 並拒絕,不等待卡住的 write/abort;sink 負責回滾/失效化未完成寫入。此 helper 僅限瀏覽器,純核心/Node 離線工具仍可用。可指定同源靜態 workerUrl,預設為樹內 worker/render.js。增量 WAV 契約與可執行的 Worker file sink/取消配方見宿主整合,長 PCM 消費配方見串流樂譜。\n\nWorker 在模組載入/解析/求值後送出 {type:'ready',protocol:1},宿主收到有效 ready 才傳送樂譜。startupTimeoutMs 是選用整數 1–2,147,483,647 ms 的模組啟動 watchdog,僅從 Worker 建立量到 ready,逾時以 TimeoutError 拒絕並終止 Worker/abort sink,不限制渲染或慢速寫入。phaseDiagnostics:true 在成功結果的 diagnostics.phases 保留 initializing/rendering/writing/closing 與總時間;onPhase 同步通知轉換及 completed/cancelled/failed,回呼拋錯會使工作失敗。這些是宿主 wall-clock 時間,並非 DSP benchmark。\n\n沒有內建整體期限或自動重試;可用 signal:AbortSignal.timeout(300000) 對整份工作設五分鐘政策。取消不等待卡住的 write/abort;sink 仍須失效化已寫入的部分輸出。完整診斷、deadline 與回滾配方見Worker 診斷。\n\n第 08 範例(僅供 checkout) 提供真正 Transport/Performance、控制/品質/銀行/WAV 格式預覽與長檔案 Worker sink/取消。長檔案僅用 File System Access,不聚合成 Blob;短預覽有明確 8 MiB 記憶體預算。\n\nimportDX7(Uint8Array) 只接受單個框架/checksum 正確的 163-byte 單音色或 4104-byte/32 音色銀行;拒絕原始、串接或非 7-bit payload,輸出完整版本 6 音色。describeDX7 回傳 name/sourceAlgorithm/algorithm/selectedOperators/droppedOperators/warnings;原 algorithm 1–32、轉換後 0–7,operator 編號依 DX7 的 1–6。\n\n這仍是六運算子轉四運算子的有損音樂啟發式轉換,不是 DX7 合成/模擬。固定 Hz 運算子會保留;力度/速率鍵位縮放、減少階段的 pitch envelope 與 LFO delay/sync 採近似。下降 saw/sample-and-hold 波形以其他波形替代並附 warning;路由、oscillator sync、transpose 與包絡細節仍有損。轉換器將所選運算子的 AM sensitivity 簡化成共用深度,不保留逐運算子 AM,也不映射至選擇性 v6 amTargets。先看 descriptions、表情音色與轉換限制及音色品質再試聽;不可信任檔案/下載仍須先限制大小。\n\n## 品質與發佈驗收\n\n在 checkout 執行維護指令,不是在已安裝的套件中:\n\n所有程式皆使用 strict TypeScript;compile 輸出忽略追蹤的 .dev,npm test 編譯並選擇行為測試,不用裸 node --test。目前 build 由實作產生宣告、保留引擎路徑並建置 13 個 demo entry scripts,再由 Markdown 與輸出宣告產生靜態 HTML 文件;歷史 v1.8.1 為 12 個 scripts。每個 JS 配對 map/型別,typecheck 包含 source/tools/tests/demo/公開型別;使用端仍只需 JS,不需建置工具鏈。\n\nsh\nnpm ci\nnpm run build\nnpm test\nnpm run typecheck\nnpm run security -- --package-smoke\nnpx --no-install playwright install --with-deps chromium\nnpm run browser-smoke -- chromium\nnpm run benchmark\n\n\n可改用 firefox/webkit 安裝與測試其他引擎。CI 設定 Node 22/24/26 與三種瀏覽器,但設定本身不代表執行成功;實測版本與結果以 CHANGELOG 為準。瀏覽器驗收觀察真正 AudioWorklet 訊號、有限輸出、生命週期/release 與發佈路徑。發佈前置條件見發佈指南;本儲存庫沒有 npm publish workflow,本文件也不表示已設定 registry 授權或已執行 npm publish。\n\nHeadless Linux Firefox 還需要運作中的原生音訊服務,只安裝瀏覽器函式庫並不足夠。CI 安裝 pulseaudio,執行 pulseaudio --start --exit-idle-time=-1、pactl load-module module-null-sink sinkname=opmci、pactl set-default-sink opm_ci,並以 pactl info 確認就緒後才執行 smoke。Null sink 僅丟棄喇叭輸出,仍保留原生音訊時鐘與真正 worklet graph;若初始化/resume 卡住,log 會列出等待階段及 context 狀態。\n\n效能報告列出暖機後 128 影格區塊 p95/p99/最慢值與超過 128/sampleRate 秒的次數;OPMBENCHBLOCKS、OPMBENCHWARMUP、OPMBENCHSAMPLERATE 調整負載,p99/worst budget ratio 可設主機相關門檻。本機預設只報告;CI 設定 48 kHz 的 OPMBENCHP99BUDGET_RATIO=1(2.667 ms),最慢值/GC 停頓僅報告,非所有平台即時保證。頻譜驗收比較 48 kHz 下折疊到 18 kHz 的 30 kHz 振盪器與同強度 1760 Hz 控制,要求至少 30 dB 衰減,不能宣稱所有 FM 無混疊。\n\n另可在 checkout 執行 npm run sound-quality、npm run voice-quality、npm run browser-stress -- chromium 與 npm run vite-smoke -- chromium。品質矩陣涵蓋多取樣率、演算法、回授、音高、力度與多音;音色工具提供低/中/高音域與力度的 peak/RMS dBFS、建議宿主 trim 及 DX7 限制報告,並不自動重寫音色音量。詳細方法見音色品質指南。已觀察到的矩陣通過與瀏覽器有限訊號,只證明所測條件;主執行緒事件間隔不等於 worklet CPU/GC 時間,也不能證明所有平台零爆音、任意 FM 無混疊或全部音色等響度。\n\n獨立驗收含 Bessel PM、收斂/全強度回授能量界限、獨立四運算子串接/分支/多 carrier 參考,以及兩段 120 秒 LFO/glide 串流。4× polyphase 半帶 FIR decimator 的 passband 平坦至輸出取樣率 0.40 倍,折返到該頻率以下的成分至少衰減 81 dB,代價是固定 13.25 frame 的線性相位延遲;詳見聲學參考數學與限制。這些不是任意音色、聽感、實體裝置或硬體保真保證。\n\n## 壓縮部署\n\ndist/ 每個最小化 .js 都有對應 .js.map 與 TypeScript 自動產生的 .d.ts,包含 demo scripts。引擎保留來源模組路徑,不產生 hashed chunks;demo 無公開匯出,其宣告如實為 export {};。建置不產生 JSON 資產或壓縮側檔。Source maps 映射回並內嵌原始 TypeScript;發佈 maps 即公開這些原始碼供除錯。需要壓縮時由網站主機處理,程式仍匯入原本的 .js URL。伺服器送出壓縮內容時,須提供對應 Content-Encoding、Vary: Accept-Encoding 與 JavaScript MIME 類型;未壓縮回應不可標示為已壓縮。基本的 Python HTTP 伺服器可直接提供一般模組。整套 dist 須維持同一建置版本;詳見 README 的最佳化發佈。\n\n## 疑難排解\n\n| 現象 | 修正方式 |\n| --- | --- |\n| file://、非安全來源或 AudioWorklet 不可用 | 使用支援 AudioWorklet 的現代瀏覽器,由 HTTPS 或 localhost 提供整個頁面與檔案。 |\n| 瀏覽器無法解析 opm.js 裸套件名稱 | 使用複製的 ./opm/api/index.js 相對 URL;如自行設定 import map,也要確保 worklet 的完整模組樹可讀取。 |\n| worklet/模組 404,或拿到 HTML、MIME 類型錯誤 | 重新複製完整 dist 目錄並確認站點路徑;不要讓 SPA rewrite 將資產請求改送 index.html。 |\n| 尚未啟動就播放、瀏覽器自動播放被擋 | 在點擊事件內 await opm.start() 後才呼叫 playNote()。 |\n| 音高、名稱、延遲、時長或未知音色出錯 | 依瀏覽器 API檢查範圍,先註冊自訂音色。 |\n| 多音符遺失 | 檢查事件的 rejected/stolen 與診斷;注意 maxVoices 1–32(預設 8)、voicePriority 拒絕、聲部自有 voiceLimit 及有界事件/ID,分批排程。 |\n| Node 無聲、找不到 WAV | PCM 不會自動播放/寫檔;依上方 WAV 範例編碼並儲存。 |\n| Processor 失敗 | 處理 onEvent error 與拒絕的診斷請求,關閉/重啟;不要以不安全 fallback 掩蓋錯誤。 |\n| npm 回報 E404 | 不假設 registry 有此套件;依安裝步驟安裝 GitHub Release tarball 或 checkout 封裝的本機 tarball。 |\n| 音色銀行格式錯誤 | 必須是完整音色組成的陣列,並符合名稱、欄位、數量與大小限制;單一 JSON 物件須包入 [...]。 |\n| 壓縮回應顯示亂碼 | 設定正確的 Content-Encoding 與 MIME 類型,或直接提供未壓縮的 .js 模組。 |\n","href":"./usage.zh-TW.html"},{"title":"OPM.js 使用說明(繁體中文) · OPM.js 使用說明(繁體中文)","text":"OPM.js 使用說明(繁體中文) OPM.js 使用說明(繁體中文)","href":"./usage.zh-TW.html#opmjs-使用說明繁體中文"},{"title":"OPM.js 使用說明(繁體中文) · v1.11 新功能(目前 checkout,尚未發佈)","text":"OPM.js 使用說明(繁體中文) v1.11 新功能(目前 checkout,尚未發佈)","href":"./usage.zh-TW.html#v111-新功能目前-checkout尚未發佈"},{"title":"OPM.js 使用說明(繁體中文) · v1.10 新功能","text":"OPM.js 使用說明(繁體中文) v1.10 新功能","href":"./usage.zh-TW.html#v110-新功能"},{"title":"OPM.js 使用說明(繁體中文) · 既有 API 與範例(於 v1.9 引入)","text":"OPM.js 使用說明(繁體中文) 既有 API 與範例(於 v1.9 引入)","href":"./usage.zh-TW.html#既有-api-與範例於-v19-引入"},{"title":"OPM.js 使用說明(繁體中文) · 安裝與範例頁面","text":"OPM.js 使用說明(繁體中文) 安裝與範例頁面","href":"./usage.zh-TW.html#安裝與範例頁面"},{"title":"OPM.js 使用說明(繁體中文) · 瀏覽器靜態部署","text":"OPM.js 使用說明(繁體中文) 瀏覽器靜態部署","href":"./usage.zh-TW.html#瀏覽器靜態部署"},{"title":"OPM.js 使用說明(繁體中文) · 瀏覽器 API 與自訂音色","text":"OPM.js 使用說明(繁體中文) 瀏覽器 API 與自訂音色","href":"./usage.zh-TW.html#瀏覽器-api-與自訂音色"},{"title":"OPM.js 使用說明(繁體中文) · 有界前瞻排程","text":"OPM.js 使用說明(繁體中文) 有界前瞻排程","href":"./usage.zh-TW.html#有界前瞻排程"},{"title":"OPM.js 使用說明(繁體中文) · 共用 context 與輸出接線","text":"OPM.js 使用說明(繁體中文) 共用 context 與輸出接線","href":"./usage.zh-TW.html#共用-context-與輸出接線"},{"title":"OPM.js 使用說明(繁體中文) · 共用樂譜、全域調音與安全恢復","text":"OPM.js 使用說明(繁體中文) 共用樂譜、全域調音與安全恢復","href":"./usage.zh-TW.html#共用樂譜全域調音與安全恢復"},{"title":"OPM.js 使用說明(繁體中文) · 有界長樂譜","text":"OPM.js 使用說明(繁體中文) 有界長樂譜","href":"./usage.zh-TW.html#有界長樂譜"},{"title":"OPM.js 使用說明(繁體中文) · 拍點 Transport 與演奏聲部","text":"OPM.js 使用說明(繁體中文) 拍點 Transport 與演奏聲部","href":"./usage.zh-TW.html#拍點-transport-與演奏聲部"},{"title":"OPM.js 使用說明(繁體中文) · 速度曲線、網格與分層編曲","text":"OPM.js 使用說明(繁體中文) 速度曲線、網格與分層編曲","href":"./usage.zh-TW.html#速度曲線網格與分層編曲"},{"title":"OPM.js 使用說明(繁體中文) · Node.js 離線渲染","text":"OPM.js 使用說明(繁體中文) Node.js 離線渲染","href":"./usage.zh-TW.html#nodejs-離線渲染"},{"title":"OPM.js 使用說明(繁體中文) · 音色格式與銀行載入","text":"OPM.js 使用說明(繁體中文) 音色格式與銀行載入","href":"./usage.zh-TW.html#音色格式與銀行載入"},{"title":"OPM.js 使用說明(繁體中文) · WAV 匯出與 DX7 匯入","text":"OPM.js 使用說明(繁體中文) WAV 匯出與 DX7 匯入","href":"./usage.zh-TW.html#wav-匯出與-dx7-匯入"},{"title":"OPM.js 使用說明(繁體中文) · 品質與發佈驗收","text":"OPM.js 使用說明(繁體中文) 品質與發佈驗收","href":"./usage.zh-TW.html#品質與發佈驗收"},{"title":"OPM.js 使用說明(繁體中文) · 壓縮部署","text":"OPM.js 使用說明(繁體中文) 壓縮部署","href":"./usage.zh-TW.html#壓縮部署"},{"title":"OPM.js 使用說明(繁體中文) · 疑難排解","text":"OPM.js 使用說明(繁體中文) 疑難排解","href":"./usage.zh-TW.html#疑難排解"},{"title":"Preset and conversion acceptance","text":"Preset and conversion acceptance # Preset and conversion acceptance\n\nThe checkout-only maintenance command npm run voice-quality produces a fresh JSON report for the twenty-one bank voices and five original synthetic DX7 fixtures, each rendered under the eco, standard and high DSP profiles (69 accepted rows in the historical 1.8.0 run). Serve the current audition page (checkout-only) through the project's HTTP demo server for seeded A/B playback, profile selection, a held ratio/feedback/ADSR control phrase, local PCM16 WAV downloads and raw/matched numerical comparisons. No human acceptance is implied.\n\n## Bank, provenance and host trim\n\nThe existing seven recipes remain in examples; their synthesis parameters are unchanged, with current v7 format labels. Legacy v1–v6 inputs retain their original restrictions and normalize to v7; v1–v5 reject selective LFO targets and v1–v6 reject operator waveforms/noise rate. See the voice v7 guide for modulation, oscillator shapes and immutable snapshots. These original four-operator recipes are not converted emulator patches, downloaded SysEx, sampled recordings or claims of acoustic/hardware fidelity:\n\n| Addition | Intended MIDI register | Timbral design |\n| --- | --- | --- |\n| wood_mallet | 48–84 | Fast high-partial decay; higher keys shorten envelopes. |\n| glass_pluck | 48–84 | Velocity-sensitive glassy transient over a decaying body. |\n| hollow_reed | 48–76 | Even-ratio modulation of odd-partial carriers; delayed vibrato. |\n| slowairpad | 48–76 | Slow detuned onset, shallow pitch envelope, global LFO phase. |\n| bronze_plate | 48–72 | Noninteger PM ratios and unequal metallic decays. |\n| membrane_tom | 36–60 | Short body with a downward pitch-envelope transient. |\n| fixedhzchime | 48–84 | Additive 317/523/829/1237 Hz partials independent of key pitch. |\n| wire_kalimba | 48–84 | Key-tracking plucked body and fixed-Hz transient modulator. |\n| noise-snare | 36–84 | Short noise burst with fixed-Hz body tones. |\n| noise-hihat | 36–84 | High hold-rate noise and a brief quiet square partial. |\n| explosion | 36–84 | Low hold-rate noise, sub body and bright transient. |\n\nAll intended base velocities are 0.25–1. Phrase articulation reaches 0.1625; the numerical safety sweep also includes that soft boundary. Register labels express intended musical use, not a promise that notes outside them are invalid. Fixed-Hz operators ignore MIDI/tuning frequency but still respond to detune, pitch controls/envelope and LFO pitch modulation. Ratios remain present in the strict schema. Rate key scaling changes envelope times; none of these fields implement chip register behavior.\n\nMachine-readable presetMetadata in src/voices/preset-metadata.ts (checkout-only) records every bank recipe's provenance kind, source, Apache-2.0 license, non-emulator status, family, intended register/velocity, purpose and host trim. defaultBrassMetadata covers the separate default brass.js recipe, which differs from bank brass. Existing repository recipes are marked repository-recipe; additions are original-recipe. Metadata is deliberately outside strict voice objects: do not merge it into a patch submitted to validation.\n\nPackage wildcard exports expose the banks and metadata without additional runtime dependencies:\n\njs\nimport { examples } from 'opm.js/voices/examples.js';\nimport { originalPresets } from 'opm.js/voices/original.js';\nimport { presetMetadata } from 'opm.js/voices/preset-metadata.js';\n\n\nExisting demo selectors import examples, so the expanded bank appears through their existing list/load path. Operator levels remain timbral parameters; no output-level compensation was embedded into operators. Metadata recommends −6 dB additional host attenuation as a conservative starting point, not a measured perceptual preference. The fresh report computes separate safety trims and pair-specific energy matching. More voices/chords require extra host headroom.\n\n## Selective-LFO recipes and usage table\n\nThree recipes added in 1.8 use the per-operator amTargets/pmTargets tuples that the 1.7 bank did not exercise. They are original four-operator parameter recipes, not converted patches or recordings:\n\n| Recipe | Selective modulation | Playing notes |\n| --- | --- | --- |\n| tide_keys | AM only on the second carrier; PM on the first modulator and, lightly, the second pair's modulator, with no vibrato on either carrier | Short chords at medium velocity. Velocity-sensitive, key-scaled modulators soften hard high notes. Leave release headroom. |\n| ember_bass | AM on the two modulators (sideband strength, not the bass floor); PM only on the upper modulator | Monophonic lines at moderate velocity. The unmodulated fundamental/sub pair stays stable. |\n| orbit_pad | Shallow AM on the lower carrier, stronger AM on the upper pair; PM on modulators only; globally synchronized LFO | Let the 0.24–0.42 s attacks develop and tails clear before re-chording; use the lowest suggested polyphony the arrangement allows. |\n\nUsage metadata for every bank voice (presetMetadata, outside the strict voice schema). Intended register and velocity describe where a recipe is meant to be played, not a validity limit. Suggested polyphony and trim for the fifteen older recipes are conservative defaults (4 voices, −6 dB), not per-voice tuning; only the three 1.8 recipes carry specific values. Trims are a host starting point, never an operator-level change.\n\n| Voice | Family | MIDI | Velocity | Voices | Trim dB | Source |\n| --- | --- | --- | --- | --- | --- | --- |\n| bell | bell | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| brass | brass | 48–76 | 0.25–1 | 4 | −6 | examples.ts |\n| bass | bass | 36–60 | 0.25–1 | 4 | −6 | examples.ts |\n| electric_piano | keys | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| organ | organ | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| lead | lead | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| strings | strings | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| wood_mallet | mallet | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| glass_pluck | pluck | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| hollow_reed | reed | 48–76 | 0.25–1 | 4 | −6 | original.ts |\n| slowairpad | pad | 48–76 | 0.25–1 | 4 | −6 | original.ts |\n| bronze_plate | metallic | 48–72 | 0.25–1 | 4 | −6 | original.ts |\n| membrane_tom | percussion | 36–60 | 0.25–1 | 4 | −6 | original.ts |\n| fixedhzchime | inharmonic | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| wire_kalimba | pluck | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| tide_keys | keys | 48–84 | 0.25–0.9 | 4 | −9 | original.ts |\n| ember_bass | bass | 36–60 | 0.35–0.9 | 1 | −9 | original.ts |\n| orbit_pad | pad | 48–76 | 0.25–0.8 | 3 | −12 | original.ts |\n| noise-snare | percussion | 36–84 | 0.25–1 | 4 | −6 | original.ts |\n| noise-hihat | percussion | 36–84 | 0.25–1 | 4 | −6 | original.ts |\n| explosion | percussion | 36–84 | 0.25–1 | 4 | −6 | original.ts |\n\nThese numbers come from authoring intent plus the numerical sweeps below. Every metadata record has listeningStatus: 'unverified': no listener has endorsed a recipe.\n\n## Profiles and live-control phrases\n\nThe report runs every source through all three profiles at 48 kHz, adds the intended-register boundaries and velocity 0.1625 (the softest phrase articulation), and renders a live-control phrase: each note is held for 0.8 s while operatorRatios ramp at 0.12 s, feedback at 0.25 s, operatorADSR at 0.4 s and ratios/feedback return at 0.55 s. A row is accepted only if the render is finite, error-free, within HEADROOM and non-silent. This proves the control paths stay stable in the offline renderer; it is not a statement about how they sound.\n\n## Numerical measurement contract\n\nReports render centered stereo at 48 kHz with a 0.8-second gate plus note-scaled release/filter tail. The nine reference cells are MIDI 48/60/84 at velocities 0.25/0.6/1, plus each recipe's intended lower/middle/upper register. Output includes sample peak, whole-render RMS, gate RMS, crest factor, finite status, DSP errors and per-cell acceptance. RMS uses mean stereo-channel energy; it is unweighted, not LUFS and not perceptual equal loudness.\n\nEvery measured cell must be finite, error-free, peak ≤ HEADROOM + 0.000001 and gate RMS > 0.000001. Report safety trims are attenuation-only, bounded −24..0 dB, with single-note targets peak ≤ −12 dBFS and gate RMS ≤ −24 dBFS. A bounded safety suggestion cannot guarantee those targets for arbitrary future patches. The command exits unsuccessfully on rejection and validates synthetic Yamaha checksums and equivalent packed/single conversion.\n\ntest/preset-quality.test.ts (checkout-only) covers finite/headroom/non-silence across every semitone of the eight additions' intended registers at 22.05 kHz and soft/medium/hard velocities. It also checks fixed-Hz partial localization/key invariance and held-gate mallet decay versus pad buildup. These are behavioral numerical regressions, not object snapshots or artistic verdicts. The 48 kHz report samples register cells; neither report nor sweep certifies every velocity, sample rate, polyphonic combination or live control transition.\n\n## Repeatable listening workflow\n\n1. Choose A/B sources, register, base velocity, single/phrase material, host gain mode and seed. Default seed is 20261002. Record all choices; filenames include seed, material, gain mode, note and velocity. Phrase offsets are [0, 2, 4, 7, −5, −12], clamped to MIDI 48–84. The unsigned 32-bit LCG chooses each velocity multiplier (0.65/0.8/1) and gate (0.35/0.5/0.8 seconds). Changing seed changes articulation, not patch parameters.\n2. Start at low device volume. Measure, then compare A followed by B. Six-note phrases use identical padded slots for both sources, including full isolated tails. This gives equal whole-phrase measurement windows. Single-note matching uses the same 0.8-second gate window. Stop immediately cancels playing and queued buffers; Dispose closes the owned context and cleans URLs/nodes.\n3. In energy-matched mode, both sources are attenuated to the quieter of their measured energy levels, the −24 dBFS target, metadata-trim ceilings and the −12 dBFS peak constraints. No source is amplified. Report the applied host trims separately from raw DSP values. The shared 0.12 master gain (−18.4 dB) follows those trims. WAVs apply these exact same gains.\n4. Repeat with dry mode: only the common 0.12 master gain, no source trim. Repeat the nine-cell single-note grid (C3/C4/C6 × soft/medium/hard), and selected phrases at those settings. Comparisons never overlap source tails. For a recipe outside its intended register, treat that cell as a stress comparison, not its intended playing range.\n5. Record attack, held body, brightness, decay, release and any manual gain preference. Repeat after swapping A/B selection if assessing ordering effects. Differences in attack/crest/spectrum can make energy-matched sources sound unequally loud; manual preference is listening evidence only when an actual listener records it. Save matched and dry WAVs with settings for another listener.\n\nThe JSON report compares every source's middle-register seeded phrase against wood_mallet, recording dry master gains, matched host trims, raw measurements, common slot length and target. This is reproducible numerical preparation for listening, not a listening result. No subjective listening acceptance, hardware comparison or fidelity result is claimed here. No audio/patches are uploaded.\n\n### Recording a human finding\n\nThe audition page has a Record a human listening finding panel. After actually listening to both selected sources, enter an anonymous listener label, exact device/browser/OS and output route, then check the explicit listening confirmation. Choose A, B, no-preference or not-assessed for each criterion; at least one must be assessed. Changing settings clears confirmation, and recording requires a fresh declaration. Add subjective gain notes separately for the selected matched or dry comparison: describe what felt loud/comfortable and actual device settings, without inventing dB, LUFS or a calibration for a volume slider.\n\nA record stores the exact package version, A/B source names and SHA-256 normalized-patch identities, profile, material, seed, register, velocity, gain mode and phrase/control revisions. New records also store rendered/context sample rates, common master gain and actual A/B source-gain factors, separately from subjective notes. These are host gain factors, not perceptual loudness measurements. Records remain in page memory (at most 50) until Export findings JSON; no audio/finding is uploaded, and numerical reports never become human criteria.\n\nThis repository ships no recorded findings. Until someone records and reviews them, every recipe's listeningStatus stays unverified.\n\n### Reviewing findings and host-trim decisions\n\nsh\nnpm run listening-evidence -- listener-one.json listener-two.json\nnpm run --silent listening-evidence -- listener-one.json listener-two.json > listening-review.json\n\n\nThe Node 22+ checkout-only CLI validates the existing opm-listening-findings-1 / opm-listening-finding-1 exports. Limits are 16 regular files, 2 MiB/file, 1–50 findings/file, bounded listener/device/output/notes, exact semantic package versions, UTC timestamps, typed selection fields, SHA-256 patch identities and all seven known criteria. Empty exports, all-unassessed entries, malformed bounds/hashes and conflicting content for the same timestamp/listener/conditions are rejected. Existing exports without explicit confirmation or applied gains remain readable with provenance warnings; unknown gains stay unknown, never filled from today's metadata.\n\nThe separate stdout review preserves filenames/finding indices, declared listener/device/output, notes and exact conditions. Groups require the same ordered patch hashes, package version, DSP profile, material, register, velocity, seed, phrase/control revisions, gain mode and recorded playback gains/sample rates. Source aliases do not substitute for hashes; swapping A/B is a different group. Matched and dry findings never merge. Duplicate copies contribute source references only. Per-criterion assessed/A/B/no-preference/not-assessed counts reflect actual entries; declared listener labels are not authenticated unique people. A successful parse is not a musical-quality pass and the command never rewrites listeningStatus, hostTrimDb or patches.\n\nHash strings are declared identities: the current export does not embed patch snapshots, so the reviewer cannot recompute them or authenticate the listener. Correlate them with retained normalized patches or the exact versioned source when reviewing a trim proposal; a valid 64-digit hash alone is not proof of provenance.\n\nFor a host-trim decision, inspect exact matched and dry groups and original gain-comfort notes for the same hashes/profiles/registers/velocities. Keep energy matching and safety trims separate from perceptual judgments, document the intended register and polyphony/headroom, and retain current starting trims until a maintainer can justify a change under stated conditions. Preference for A alone does not establish a numeric trim. If testing a proposed trim, record the actual applied host gain and new findings, not an inferred device-volume unit; rerun numerical safety verification after an approved metadata change. Review artifacts support a human decision, never auto-green artistic acceptance or hardware fidelity.\n\nUse the audition's reproducible isolated notes, seeded phrases and live-control phrases for controlled comparisons, then listen in the original short songs Harbor at First Light and Lanterns on the Stair (checkout-only showcase) for arrangement context. Save the exact score project and route/profile/gain notes; short-song observations are supplemental and must not be mislabeled as the audition's single/phrase/control material. A maintainer may graduate a recipe's listening metadata only after real listeners supply reviewed originals tied to exact patch hashes and conditions, with the scope and artifact reference documented. No human findings or physical captures were supplied here; the existing unverified metadata is intentionally unchanged.\n\n### All-eighteen current-version campaign\n\nRun against the matching v1.10 checkout (package 1.10.0) and retain its exact revision alongside originals. Build and serve this source checkout for the checkout-only audition page and evidence commands. The eighteen rows in the usage table are the campaign checklist: no recipe inherits acceptance from a related family, a converted fixture, or another profile. Keep listeningStatus: 'unverified' and existing trims until genuine original findings have been reviewed; neither numerical safety nor a completed form is artistic endorsement.\n\nFor each recipe, repeat the following under eco, standard and high, keeping anonymous listener, exact device/OS/browser, route/device volume, seed 20261002 and reference source fixed. Use woodmallet as B, except use bell when A is woodmallet; record ordered patch hashes, not just names. A/B identity or reversed order does not inherit another comparison's result.\n\n1. Select Single note, recipe A/reference B, register C3/C4/C6 and each velocity 0.25/0.6/1: nine individual cells. Run each in energy-matched, then dry, listening to both sources and tails. Record a separate finding for each exact selected cell and gain mode. Compare nine single-note cells is a preview, not a nine-cell record: the finding panel stores only the currently selected register/velocity, so do not file the whole grid as one result.\n2. Select Seeded six-note phrase at middle/0.6; repeat matched and dry. Select Seeded phrase with held ratio, feedback and ADSR controls and repeat both modes. Describe attack/body/brightness/decay/release, control transitions and gain comfort where actually assessed; mark the others not-assessed. State stress cells outside the intended register/velocity rather than calling them intended-use defects.\n3. Check the actual-listening declaration only after hearing both sources at the selected conditions. Settings changes clear it; never reuse a declaration from another profile/patch/cell. Enter actual device volume and subjective gain notes without inventing slider-to-dB calibration. Retain applied source gains/master gain and rendered/context rates from the JSON separately from subjective impressions.\n4. This is 22 selected-condition findings per recipe/profile (18 singles plus four phrases), if each was genuinely assessed. Export each recipe/profile batch before the 50-finding limit, then clear. Keep original JSON and selected matched/dry WAVs plus the exact versioned normalized patch sources; the hashes are declared identities and do not embed patches. Do not duplicate findings to simulate more listeners or overwrite originals to fill coverage.\n5. Run the existing listening-evidence review on exported batches, then inspect original criteria, notes and exact-condition groups. Dry/matched, different gains/routes/profiles and reversed A/B remain separate. Have real listeners review the intended-use results and any failure or unassessed criterion; a parse or preference count is not an acceptance threshold.\n\nControlled audition does not cover chords, extended fatigue or arrangement context. Use existing pages for those separate original observations; retain a timestamped local companion record with checkout/version, normalized patch identities, device/browser/route/volume, applied profile/gains/sample rate, start/end/duration, actual notes/velocity/polyphony/score and human observations. It must explicitly say supplemental, not masquerade as opm-listening-finding-1 single/phrase/control material:\n\n- Chords: Playground selects every bank voice and plays MIDI 60/64/67 with 0.7-second gates at the chosen velocity/pan. Listen to repeated chords and full tails at soft/medium/hard settings, noting peak comfort and masking. That page's default standard-profile/8-voice engine and gains differ from audition; record them and do not call it matched A/B or all-profile coverage. Its separate initial default brass recipe differs from bank brass: for bank-brass chord acceptance use the exact bank patch in the score-project workflow rather than substituting the default. Higher/different polyphony requires its own exact score and headroom evidence.\n- Long play: on the existing basic page, select the exact bank recipe, note in its intended register and actual velocity; Hold for at least ten minutes, listen throughout, then Release and observe the tail. Record fatigue/brightness, audible gaps, route/battery/foreground conditions and real duration. This page's default standard profile/gain is not the audition's dry or matched playback; do not infer other profiles or thermal/all-day reliability. Repeat separately for each of the eighteen recipes; early or unattended runs stay unverified.\n- Live controls: the controlled audition phrase covers a reproducible offline ratio/feedback/ADSR transition, not physical real-time performance. Use the existing instrument's lead/strings parts and CC campaign for actual MIDI/live-control observations; do not claim it tested all eighteen recipes. For each other recipe use Sound design: select the recipe and profile, Start, Hold at a declared note/velocity, then adjust ratio/feedback/ADSR/LFO and Release. Retain exported beginning/ending patches, actual controls/ramps and route/host-gain settings with original observations. Quality changes dispose old audio; Start the new profile before recapturing. Retain retriggering edits separately from live updates and do not infer coverage from a similar patch.\n- Song context: listen to the existing original showcase songs, recording exact score/project, voice-to-part mapping, tempo, profile, applied gains/route and audible masking/tails. An unchanged song covers only the recipes actually used. To cover another recipe, preserve a separately named score project with that exact patch and document the changed part; do not claim the original song used all eighteen. Song findings are supplemental, not isolated-energy matches.\n\nHost-trim proposals require reviewed originals under matching hashes and stated register/polyphony, including both dry and matched gain-comfort findings and supplemental chord/song observations. Retain current trims when there is no numeric applied-gain experiment or justified scope; no guessed compensation in operators or metadata. A maintainer records the decision, scope and artifact references only after human review and numerical safety verification of an approved change.\n\nRemaining blocker: no human listeners or original findings, long-play/chord/song observations, or physical phones/controllers were supplied. Every preset's listening status and all physical acceptance remain unverified. UI automation cannot hear, certify hardware or authorize a trim change.\n\n\n## Original synthetic DX7 recipes\n\ndemo/audition-fixtures.ts (checkout-only) generates valid-checksum VCED singles and a VMEM bank from original recipes, using Yamaha DX7 manual pp. 30–31 and DX7II Add-11 layout tables. The fixtures exercise three paired carriers, six additive carriers, a fixed-frequency carrier, velocity-sensitive modulation and equivalent packed-bank conversion. The fixed carrier retains its approximately 263 Hz frequency in canonical v7 instead of a MIDI-60 ratio approximation; imports leave LFO operator targets and operator waveforms absent to preserve the established conversion sound.\n\nThe page and report include describeDX7() routing/loss warnings. Four slots cannot preserve all six-operator topologies; retained/dropped operators and approximate envelopes, levels, feedback, LFO, keyboard scaling and velocity transfer functions still matter. See the converter's current warnings for supported mappings rather than inferring that every source parameter is exact. There is no original six-operator DX7 reference renderer here: A/B compares OPM recipes and converted synthetic sources, not lossless conversion or DX7 hardware fidelity.\n","href":"./voice-quality.html"},{"title":"Preset and conversion acceptance · Preset and conversion acceptance","text":"Preset and conversion acceptance Preset and conversion acceptance","href":"./voice-quality.html#preset-and-conversion-acceptance"},{"title":"Preset and conversion acceptance · Bank, provenance and host trim","text":"Preset and conversion acceptance Bank, provenance and host trim","href":"./voice-quality.html#bank-provenance-and-host-trim"},{"title":"Preset and conversion acceptance · Selective-LFO recipes and usage table","text":"Preset and conversion acceptance Selective-LFO recipes and usage table","href":"./voice-quality.html#selective-lfo-recipes-and-usage-table"},{"title":"Preset and conversion acceptance · Profiles and live-control phrases","text":"Preset and conversion acceptance Profiles and live-control phrases","href":"./voice-quality.html#profiles-and-live-control-phrases"},{"title":"Preset and conversion acceptance · Numerical measurement contract","text":"Preset and conversion acceptance Numerical measurement contract","href":"./voice-quality.html#numerical-measurement-contract"},{"title":"Preset and conversion acceptance · Repeatable listening workflow","text":"Preset and conversion acceptance Repeatable listening workflow","href":"./voice-quality.html#repeatable-listening-workflow"},{"title":"Preset and conversion acceptance · Recording a human finding","text":"Preset and conversion acceptance Recording a human finding","href":"./voice-quality.html#recording-a-human-finding"},{"title":"Preset and conversion acceptance · Reviewing findings and host-trim decisions","text":"Preset and conversion acceptance Reviewing findings and host-trim decisions","href":"./voice-quality.html#reviewing-findings-and-host-trim-decisions"},{"title":"Preset and conversion acceptance · All-eighteen current-version campaign","text":"Preset and conversion acceptance All-eighteen current-version campaign","href":"./voice-quality.html#all-eighteen-current-version-campaign"},{"title":"Preset and conversion acceptance · Original synthetic DX7 recipes","text":"Preset and conversion acceptance Original synthetic DX7 recipes","href":"./voice-quality.html#original-synthetic-dx7-recipes"},{"title":"Worker render diagnostics, deadlines and rollback","text":"Worker render diagnostics, deadlines and rollback # Worker render diagnostics, deadlines and rollback\n\nrenderSequenceInWorker keeps the main thread free while a static module Worker renders a score and streams encoded WAV chunks to a host sink. This guide covers the startup handshake, optional phase diagnostics and how a host should bound time and clean up partial files. Core behavior (one unacknowledged chunk, exact byte accounting, cancellation without waiting for a hung sink) is unchanged; see host integration.\n\n## Startup handshake\n\nThe Worker posts { type: 'ready', protocol: 1 } after its module has been fetched, parsed and evaluated. The host sends the score only after a valid ready. The message is validated as strict own data; a second ready, a different protocol number or any chunk before ready fails the render, aborts the sink once and terminates the Worker.\n\nstartupTimeoutMs (optional, integer 1–2,147,483,647) starts when the Worker is constructed and stops at ready. If it expires the promise rejects with a TimeoutError DOMException, the Worker is terminated and sink.abort() is called. It never measures rendering, sink writes or close(), so a slow disk or a long score cannot trip it. It exists to tell \"the Worker module never started\" (bad workerUrl, blocked MIME, suspended environment) from \"rendering is slow\".\n\nts\nconst result = await renderSequenceInWorker(score, {\n sink, format: 'pcm24',\n startupTimeoutMs: 10_000, // module start only\n signal: AbortSignal.timeout(5 * 60_000), // your own whole-job deadline\n phaseDiagnostics: true,\n onPhase: status => console.debug(status.phase, status.phaseElapsedMs),\n});\nconsole.log(result.diagnostics.phases); // { status: 'completed', initializingMs, renderingMs, writingMs, closingMs, totalMs }\n\n\nThere is no built-in overall deadline and no automatic retry. A legitimately long render or a slow sink is indistinguishable from a stalled one without host knowledge, and a blind retry would re-send chunks to a sink that may already hold partial data. Use an AbortSignal for the policy that fits the app.\n\n## Phases\n\nonPhase(status) is called synchronously, for every transition, with a frozen { phase, elapsedMs, phaseElapsedMs, frames, totalFrames, bytesWritten, errors }:\n\n| Phase | Interval |\n| --- | --- |\n| initializing | from Worker construction until ready |\n| rendering | Worker compute: from ready/each acknowledgement until the next chunk arrives |\n| writing | from chunk arrival until the awaited sink.write() and onProgress finish |\n| closing | from the Worker's done until sink.close() resolves |\n| completed, cancelled, failed | terminal; reported once to onPhase |\n\nrendering and writing alternate once per chunk. With phaseDiagnostics: true the successful result carries four accumulated durations and the total in diagnostics.phases. Failed or cancelled renders reject; use onPhase to capture their terminal status. Timings are host monotonic wall-clock readings (performance.now()), not DSP benchmarks, and include scheduler and GC pauses. Neither option changes the bytes written.\n\nIf onPhase or onProgress throws, the render fails with that error exactly once; the first failure is preserved and the observer is not called again after the terminal phase.\n\n## Partial output and rollback\n\nThe sink owns its output. abort(reason) is called at most once, without being awaited, and the Worker is already terminated when it runs. It must make any in-flight write harmless and discard the partial file; cancellation cannot retract bytes already persisted. A robust File System Access sink:\n\nts\nasync function createFileSink(handle: FileSystemFileHandle): Promise\u003cWavSink> {\n const writable = await handle.createWritable(); // writes to a swap file until close()\n return {\n write: bytes => writable.write(bytes),\n close: () => writable.close(),\n abort: reason => writable.abort(reason), // discards the swap file: the target is untouched\n };\n}\n\n\nAcquire the handle from a trusted gesture, keep the cancellation signal alive across picker and createWritable() awaits, and clean up acquisition failures before starting a Worker. Never fall back to accumulating the whole file in a Blob.\n\n## Diagnosing a stalled start\n\n1. TimeoutError from startupTimeoutMs: fetch workerUrl directly. It must be same-origin, served as JavaScript with nosniff and a 200 status, next to the rest of the matching dist/ tree (npx --no-install opm-assets check \u003cbase-url> run from a project with OPM.js installed, verifies bytes, status and MIME without fetching a registry CLI).\n2. A Worker that reaches ready but never delivers a chunk: look at status.phase in onPhase. A long rendering phase with frames not advancing points at the score; a long writing phase points at the sink.\n3. Repeated, order-dependent startup failures in an automation harness are not evidence about the package. During the 1.7 verification a managed browser closed targets or missed Worker start deadlines when many Workers started back to back, while the same path passed in isolation. The cause was not established, which is why this release adds measurement (ready, phases) rather than an automatic retry.\n\n## Verified behavior\n\ntest/render-worker.test.ts (checkout-only) runs the real Worker module in a thread. It checks the phase sequence, that diagnostics do not change the bytes, that startupTimeoutMs rejects a Worker that never answers (no score is sent and the sink is aborted once) while a 30 ms-per-write sink succeeds under a 1 s watchdog, rejection of invalid or premature ready messages, and that a throwing observer fails the render once.\n","href":"./worker-diagnostics.html"},{"title":"Worker render diagnostics, deadlines and rollback · Worker render diagnostics, deadlines and rollback","text":"Worker render diagnostics, deadlines and rollback Worker render diagnostics, deadlines and rollback","href":"./worker-diagnostics.html#worker-render-diagnostics-deadlines-and-rollback"},{"title":"Worker render diagnostics, deadlines and rollback · Startup handshake","text":"Worker render diagnostics, deadlines and rollback Startup handshake","href":"./worker-diagnostics.html#startup-handshake"},{"title":"Worker render diagnostics, deadlines and rollback · Phases","text":"Worker render diagnostics, deadlines and rollback Phases","href":"./worker-diagnostics.html#phases"},{"title":"Worker render diagnostics, deadlines and rollback · Partial output and rollback","text":"Worker render diagnostics, deadlines and rollback Partial output and rollback","href":"./worker-diagnostics.html#partial-output-and-rollback"},{"title":"Worker render diagnostics, deadlines and rollback · Diagnosing a stalled start","text":"Worker render diagnostics, deadlines and rollback Diagnosing a stalled start","href":"./worker-diagnostics.html#diagnosing-a-stalled-start"},{"title":"Worker render diagnostics, deadlines and rollback · Verified behavior","text":"Worker render diagnostics, deadlines and rollback Verified behavior","href":"./worker-diagnostics.html#verified-behavior"},{"title":"opm.js · ADSR","text":"opm.js ADSR type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · applyEffects","text":"opm.js applyEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · Arrangement","text":"opm.js Arrangement type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementChangeOptions","text":"opm.js ArrangementChangeOptions type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementDefinition","text":"opm.js ArrangementDefinition type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js · ArrangementGainOptions","text":"opm.js ArrangementGainOptions type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementLayer","text":"opm.js ArrangementLayer type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js · ArrangementOptions","text":"opm.js ArrangementOptions type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementProject","text":"opm.js ArrangementProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · ArrangementSection","text":"opm.js ArrangementSection type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js · ArrangementSnapshot","text":"opm.js ArrangementSnapshot type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementState","text":"opm.js ArrangementState type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · BarBeat","text":"opm.js BarBeat type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · barBeatToBeat","text":"opm.js barBeatToBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · BeatQuantization","text":"opm.js BeatQuantization type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · BeatSequenceEvent","text":"opm.js BeatSequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · BeatSequenceOptions","text":"opm.js BeatSequenceOptions type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · beatsToSeconds","text":"opm.js beatsToSeconds value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · beatToBarBeat","text":"opm.js beatToBarBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · ChorusOptions","text":"opm.js ChorusOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · CommandEvent","text":"opm.js CommandEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · CommandName","text":"opm.js CommandName type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · CommandRejectedError","text":"opm.js CommandRejectedError value / type where applicable","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · CommandWaitOptions","text":"opm.js CommandWaitOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · compileBeatSequence","text":"opm.js compileBeatSequence value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · ContextEvent","text":"opm.js ContextEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · ContextState","text":"opm.js ContextState type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · createArrangement","text":"opm.js createArrangement value / type where applicable","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · createEffects","text":"opm.js createEffects value / type where applicable","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · createLookaheadScheduler","text":"opm.js createLookaheadScheduler value / type where applicable","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · createMidiAdapter","text":"opm.js createMidiAdapter value / type where applicable","href":"./api.html#declaration-apimididts"},{"title":"opm.js · createPerformance","text":"opm.js createPerformance value / type where applicable","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · createStereoEffects","text":"opm.js createStereoEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · createTransport","text":"opm.js createTransport value / type where applicable","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · DiagnosticsEvent","text":"opm.js DiagnosticsEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · EffectsEvent","text":"opm.js EffectsEvent type","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · EffectsOptions","text":"opm.js EffectsOptions type","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · ErrorEvent","text":"opm.js ErrorEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · exportMidiFile","text":"opm.js exportMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · FrozenOperator","text":"opm.js FrozenOperator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · FrozenVoice","text":"opm.js FrozenVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · gmDrumVoices","text":"opm.js gmDrumVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js · gmProgramVoices","text":"opm.js gmProgramVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js · importMidiFile","text":"opm.js importMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · KeyScale","text":"opm.js KeyScale type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyLFO","text":"opm.js LegacyLFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyLFOV5","text":"opm.js LegacyLFOV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV3","text":"opm.js LegacyVoiceV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV4","text":"opm.js LegacyVoiceV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV5","text":"opm.js LegacyVoiceV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV6","text":"opm.js LegacyVoiceV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFO","text":"opm.js LFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFOInput","text":"opm.js LFOInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFOTargets","text":"opm.js LFOTargets type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFOTargetsInput","text":"opm.js LFOTargetsInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LookaheadNote","text":"opm.js LookaheadNote type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · LookaheadOptions","text":"opm.js LookaheadOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · LookaheadScheduler","text":"opm.js LookaheadScheduler type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · LookaheadWindow","text":"opm.js LookaheadWindow type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · MAX_ARRANGEMENT_PROJECT_BYTES","text":"opm.js MAX_ARRANGEMENT_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MAX_ARRANGEMENT_PROJECT_VOICES","text":"opm.js MAX_ARRANGEMENT_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MAX_MIDI_FILE_BYTES","text":"opm.js MAX_MIDI_FILE_BYTES value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MAX_MIDI_FILE_EVENTS","text":"opm.js MAX_MIDI_FILE_EVENTS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MAX_MIDI_FILE_TRACKS","text":"opm.js MAX_MIDI_FILE_TRACKS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MAX_SCORE_PROJECT_BYTES","text":"opm.js MAX_SCORE_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MAX_SCORE_PROJECT_VOICES","text":"opm.js MAX_SCORE_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MidiAccessLike","text":"opm.js MidiAccessLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiAdapter","text":"opm.js MidiAdapter type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiAdapterOptions","text":"opm.js MidiAdapterOptions type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiAdapterSnapshot","text":"opm.js MidiAdapterSnapshot type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiControllerMapping","text":"opm.js MidiControllerMapping type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiExportOptions","text":"opm.js MidiExportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileControlKind","text":"opm.js MidiFileControlKind type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileLossSummary","text":"opm.js MidiFileLossSummary type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFilePreservedControl","text":"opm.js MidiFilePreservedControl type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileWarning","text":"opm.js MidiFileWarning type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileWarningCode","text":"opm.js MidiFileWarningCode type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiImportOptions","text":"opm.js MidiImportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiImportResult","text":"opm.js MidiImportResult type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiInputLike","text":"opm.js MidiInputLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiMessageEventLike","text":"opm.js MidiMessageEventLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiNavigatorLike","text":"opm.js MidiNavigatorLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiOperatorControl","text":"opm.js MidiOperatorControl type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiScalarControl","text":"opm.js MidiScalarControl type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiVoiceMap","text":"opm.js MidiVoiceMap type","href":"./api.html#declaration-coremidi-statedts"},{"title":"opm.js · MusicalTransport","text":"opm.js MusicalTransport type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · NormalizedTuning","text":"opm.js NormalizedTuning type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js · NormalizedVoice","text":"opm.js NormalizedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · normalizeTempoMap","text":"opm.js normalizeTempoMap value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · NoteControls","text":"opm.js NoteControls type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js · NoteEvent","text":"opm.js NoteEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · NoteState","text":"opm.js NoteState type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · Operator","text":"opm.js Operator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · OperatorWaveform","text":"opm.js OperatorWaveform type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · OPM","text":"opm.js OPM value / type where applicable","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · OpmEffects","text":"opm.js OpmEffects type","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · OPMEvent","text":"opm.js OPMEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · OPMOptions","text":"opm.js OPMOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · parseArrangementProject","text":"opm.js parseArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · parseScoreProject","text":"opm.js parseScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · Performance","text":"opm.js Performance type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceKeySnapshot","text":"opm.js PerformanceKeySnapshot type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceNoteOffOptions","text":"opm.js PerformanceNoteOffOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceNoteOptions","text":"opm.js PerformanceNoteOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceOptions","text":"opm.js PerformanceOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformancePartControls","text":"opm.js PerformancePartControls type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformancePartOptions","text":"opm.js PerformancePartOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformancePartSnapshot","text":"opm.js PerformancePartSnapshot type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PitchEnvelope","text":"opm.js PitchEnvelope type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · PlayNoteOptions","text":"opm.js PlayNoteOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · playSequence","text":"opm.js playSequence value / type where applicable","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · PlaySequenceOptions","text":"opm.js PlaySequenceOptions type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · PreparedVoice","text":"opm.js PreparedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · QualityProfile","text":"opm.js QualityProfile type","href":"./api.html#declaration-coredecimatordts"},{"title":"opm.js · quantizeBeat","text":"opm.js quantizeBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · renderSequenceInWorker","text":"opm.js renderSequenceInWorker value / type where applicable","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · requestMidiAccess","text":"opm.js requestMidiAccess value / type where applicable","href":"./api.html#declaration-apimididts"},{"title":"opm.js · ResetEvent","text":"opm.js ResetEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · ReverbOptions","text":"opm.js ReverbOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · ScheduledNoteOptions","text":"opm.js ScheduledNoteOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · ScoreProject","text":"opm.js ScoreProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · ScoreProjectSettings","text":"opm.js ScoreProjectSettings type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · secondsToBeats","text":"opm.js secondsToBeats value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · SequenceControlEvent","text":"opm.js SequenceControlEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequenceEvent","text":"opm.js SequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequenceNoteEvent","text":"opm.js SequenceNoteEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequencePlayback","text":"opm.js SequencePlayback type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · SequenceStopEvent","text":"opm.js SequenceStopEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequenceStream","text":"opm.js SequenceStream type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · SequenceStreamOptions","text":"opm.js SequenceStreamOptions type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · serializeArrangementProject","text":"opm.js serializeArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · serializeScoreProject","text":"opm.js serializeScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · StereoEffects","text":"opm.js StereoEffects type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · StereoEffectsOptions","text":"opm.js StereoEffectsOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · StopOptions","text":"opm.js StopOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · streamSequence","text":"opm.js streamSequence value / type where applicable","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · swingBeat","text":"opm.js swingBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · swingBeatEvents","text":"opm.js swingBeatEvents value / type where applicable","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · SynthOptions","text":"opm.js SynthOptions type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js · TempoPoint","text":"opm.js TempoPoint type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · TimeSignature","text":"opm.js TimeSignature type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · TransportLoop","text":"opm.js TransportLoop type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TransportOptions","text":"opm.js TransportOptions type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TransportSnapshot","text":"opm.js TransportSnapshot type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TransportState","text":"opm.js TransportState type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TuningOptions","text":"opm.js TuningOptions type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js · VERSION","text":"opm.js VERSION value / type where applicable","href":"./api.html#declaration-versiondts"},{"title":"opm.js · Voice","text":"opm.js Voice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · VoiceInput","text":"opm.js VoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · WavSink","text":"opm.js WavSink type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderOptions","text":"opm.js WorkerRenderOptions type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderPhase","text":"opm.js WorkerRenderPhase type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderPhaseDiagnostics","text":"opm.js WorkerRenderPhaseDiagnostics type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderPhaseStatus","text":"opm.js WorkerRenderPhaseStatus type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderProgress","text":"opm.js WorkerRenderProgress type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderResult","text":"opm.js WorkerRenderResult type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js/core · ADSR","text":"opm.js/core ADSR type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · ALGORITHMS","text":"opm.js/core ALGORITHMS value / type where applicable","href":"./api.html#declaration-corealgorithmsdts"},{"title":"opm.js/core · applyEffects","text":"opm.js/core applyEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · ArrangementDefinition","text":"opm.js/core ArrangementDefinition type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js/core · ArrangementLayer","text":"opm.js/core ArrangementLayer type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js/core · ArrangementProject","text":"opm.js/core ArrangementProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · ArrangementSection","text":"opm.js/core ArrangementSection type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js/core · BarBeat","text":"opm.js/core BarBeat type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · barBeatToBeat","text":"opm.js/core barBeatToBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · BeatSequenceEvent","text":"opm.js/core BeatSequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · BeatSequenceOptions","text":"opm.js/core BeatSequenceOptions type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · beatsToSeconds","text":"opm.js/core beatsToSeconds value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · beatToBarBeat","text":"opm.js/core beatToBarBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · ChorusOptions","text":"opm.js/core ChorusOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · ChunkedSequenceOptions","text":"opm.js/core ChunkedSequenceOptions type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · ChunkedSequenceRender","text":"opm.js/core ChunkedSequenceRender type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · compileBeatSequence","text":"opm.js/core compileBeatSequence value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · createStereoEffects","text":"opm.js/core createStereoEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · createWavEncoder","text":"opm.js/core createWavEncoder value / type where applicable","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · encodeWav","text":"opm.js/core encodeWav value / type where applicable","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · envelopeAt","text":"opm.js/core envelopeAt value / type where applicable","href":"./api.html#declaration-coreenvelopedts"},{"title":"opm.js/core · estimateSequenceCapacity","text":"opm.js/core estimateSequenceCapacity value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · exportMidiFile","text":"opm.js/core exportMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · FrozenVoice","text":"opm.js/core FrozenVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · gmDrumVoices","text":"opm.js/core gmDrumVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/core · gmProgramVoices","text":"opm.js/core gmProgramVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/core · HEADROOM","text":"opm.js/core HEADROOM value / type where applicable","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · importMidiFile","text":"opm.js/core importMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · KeyScale","text":"opm.js/core KeyScale type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyLFO","text":"opm.js/core LegacyLFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyLFOV5","text":"opm.js/core LegacyLFOV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoice","text":"opm.js/core LegacyVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV2","text":"opm.js/core LegacyVoiceV2 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV3","text":"opm.js/core LegacyVoiceV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV4","text":"opm.js/core LegacyVoiceV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV5","text":"opm.js/core LegacyVoiceV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV6","text":"opm.js/core LegacyVoiceV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFO","text":"opm.js/core LFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFOInput","text":"opm.js/core LFOInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFOTargets","text":"opm.js/core LFOTargets type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFOTargetsInput","text":"opm.js/core LFOTargetsInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · lfoValue","text":"opm.js/core lfoValue value / type where applicable","href":"./api.html#declaration-corelfodts"},{"title":"opm.js/core · MAX_ARRANGEMENT_PROJECT_BYTES","text":"opm.js/core MAX_ARRANGEMENT_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_ARRANGEMENT_PROJECT_VOICES","text":"opm.js/core MAX_ARRANGEMENT_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_LONG_SEQUENCE_EVENTS","text":"opm.js/core MAX_LONG_SEQUENCE_EVENTS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_LONG_SEQUENCE_SECONDS","text":"opm.js/core MAX_LONG_SEQUENCE_SECONDS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_MIDI_FILE_BYTES","text":"opm.js/core MAX_MIDI_FILE_BYTES value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MAX_MIDI_FILE_EVENTS","text":"opm.js/core MAX_MIDI_FILE_EVENTS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MAX_MIDI_FILE_TRACKS","text":"opm.js/core MAX_MIDI_FILE_TRACKS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MAX_RENDER_SAMPLES","text":"opm.js/core MAX_RENDER_SAMPLES value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SCORE_PROJECT_BYTES","text":"opm.js/core MAX_SCORE_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_SCORE_PROJECT_VOICES","text":"opm.js/core MAX_SCORE_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_SEQUENCE_CHUNK_FRAMES","text":"opm.js/core MAX_SEQUENCE_CHUNK_FRAMES value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SEQUENCE_NOTES","text":"opm.js/core MAX_SEQUENCE_NOTES value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SEQUENCE_SECONDS","text":"opm.js/core MAX_SEQUENCE_SECONDS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SEQUENCE_SLOTS","text":"opm.js/core MAX_SEQUENCE_SLOTS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MidiExportOptions","text":"opm.js/core MidiExportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileControlKind","text":"opm.js/core MidiFileControlKind type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileLossSummary","text":"opm.js/core MidiFileLossSummary type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFilePreservedControl","text":"opm.js/core MidiFilePreservedControl type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileWarning","text":"opm.js/core MidiFileWarning type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileWarningCode","text":"opm.js/core MidiFileWarningCode type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiImportOptions","text":"opm.js/core MidiImportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiImportResult","text":"opm.js/core MidiImportResult type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiVoiceMap","text":"opm.js/core MidiVoiceMap type","href":"./api.html#declaration-coremidi-statedts"},{"title":"opm.js/core · NormalizedTuning","text":"opm.js/core NormalizedTuning type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · normalizeTempoMap","text":"opm.js/core normalizeTempoMap value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · normalizeTuning","text":"opm.js/core normalizeTuning value / type where applicable","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · normalizeVoice","text":"opm.js/core normalizeVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/core · NoteControls","text":"opm.js/core NoteControls type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · NoteOptions","text":"opm.js/core NoteOptions type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · Operator","text":"opm.js/core Operator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · OperatorWaveform","text":"opm.js/core OperatorWaveform type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · OVERSAMPLE","text":"opm.js/core OVERSAMPLE value / type where applicable","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · parseArrangementProject","text":"opm.js/core parseArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · parseScoreProject","text":"opm.js/core parseScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · PitchEnvelope","text":"opm.js/core PitchEnvelope type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · PreparedSequenceEvent","text":"opm.js/core PreparedSequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · PreparedVoice","text":"opm.js/core PreparedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · prepareLongSequence","text":"opm.js/core prepareLongSequence value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · prepareSequence","text":"opm.js/core prepareSequence value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · prepareVoice","text":"opm.js/core prepareVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/core · QualityProfile","text":"opm.js/core QualityProfile type","href":"./api.html#declaration-coredecimatordts"},{"title":"opm.js/core · renderNote","text":"opm.js/core renderNote value / type where applicable","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · RenderNoteOptions","text":"opm.js/core RenderNoteOptions type","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · RenderResult","text":"opm.js/core RenderResult type","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · renderSequence","text":"opm.js/core renderSequence value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · renderSequenceChunks","text":"opm.js/core renderSequenceChunks value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · ReverbOptions","text":"opm.js/core ReverbOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · sampleRateValue","text":"opm.js/core sampleRateValue value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · ScoreProject","text":"opm.js/core ScoreProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · ScoreProjectSettings","text":"opm.js/core ScoreProjectSettings type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · secondsToBeats","text":"opm.js/core secondsToBeats value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · SequenceCapacity","text":"opm.js/core SequenceCapacity type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceChunk","text":"opm.js/core SequenceChunk type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceControlEvent","text":"opm.js/core SequenceControlEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceEvent","text":"opm.js/core SequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceNoteEvent","text":"opm.js/core SequenceNoteEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceOptions","text":"opm.js/core SequenceOptions type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceSnapshot","text":"opm.js/core SequenceSnapshot type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceStopEvent","text":"opm.js/core SequenceStopEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceVoices","text":"opm.js/core SequenceVoices type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · serializeArrangementProject","text":"opm.js/core serializeArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · serializeScoreProject","text":"opm.js/core serializeScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · StereoEffects","text":"opm.js/core StereoEffects type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · StereoEffectsOptions","text":"opm.js/core StereoEffectsOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · Synth","text":"opm.js/core Synth value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · SynthOptions","text":"opm.js/core SynthOptions type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · TempoPoint","text":"opm.js/core TempoPoint type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · TimeSignature","text":"opm.js/core TimeSignature type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · tuningFrequency","text":"opm.js/core tuningFrequency value / type where applicable","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · TuningOptions","text":"opm.js/core TuningOptions type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · validateMaxVoices","text":"opm.js/core validateMaxVoices value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · validateNoteControls","text":"opm.js/core validateNoteControls value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · validateVoicePriority","text":"opm.js/core validateVoicePriority value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · Voice","text":"opm.js/core Voice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · VoiceAdmissionError","text":"opm.js/core VoiceAdmissionError value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · VoiceEndReason","text":"opm.js/core VoiceEndReason type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · VoiceInput","text":"opm.js/core VoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · WavChunk","text":"opm.js/core WavChunk type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavEncoder","text":"opm.js/core WavEncoder type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavEncoderOptions","text":"opm.js/core WavEncoderOptions type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavFormat","text":"opm.js/core WavFormat type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavOptions","text":"opm.js/core WavOptions type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/midi-file · exportMidiFile","text":"opm.js/midi-file exportMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · importMidiFile","text":"opm.js/midi-file importMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MAX_MIDI_FILE_BYTES","text":"opm.js/midi-file MAX_MIDI_FILE_BYTES value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MAX_MIDI_FILE_EVENTS","text":"opm.js/midi-file MAX_MIDI_FILE_EVENTS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MAX_MIDI_FILE_TRACKS","text":"opm.js/midi-file MAX_MIDI_FILE_TRACKS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiExportOptions","text":"opm.js/midi-file MidiExportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileControlKind","text":"opm.js/midi-file MidiFileControlKind type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileLossSummary","text":"opm.js/midi-file MidiFileLossSummary type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFilePreservedControl","text":"opm.js/midi-file MidiFilePreservedControl type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileWarning","text":"opm.js/midi-file MidiFileWarning type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileWarningCode","text":"opm.js/midi-file MidiFileWarningCode type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiImportOptions","text":"opm.js/midi-file MidiImportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiImportResult","text":"opm.js/midi-file MidiImportResult type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/voices/schema.js · ADSR","text":"opm.js/voices/schema.js ADSR type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · Algorithm","text":"opm.js/voices/schema.js Algorithm type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · bounded","text":"opm.js/voices/schema.js bounded value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · CompleteVoiceInput","text":"opm.js/voices/schema.js CompleteVoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · FourOperators","text":"opm.js/voices/schema.js FourOperators type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · FrozenOperator","text":"opm.js/voices/schema.js FrozenOperator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · FrozenVoice","text":"opm.js/voices/schema.js FrozenVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · KeyScale","text":"opm.js/voices/schema.js KeyScale type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyLFO","text":"opm.js/voices/schema.js LegacyLFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyLFOV4","text":"opm.js/voices/schema.js LegacyLFOV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyLFOV5","text":"opm.js/voices/schema.js LegacyLFOV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperator","text":"opm.js/voices/schema.js LegacyOperator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperatorV2","text":"opm.js/voices/schema.js LegacyOperatorV2 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperatorV3","text":"opm.js/voices/schema.js LegacyOperatorV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperatorV6","text":"opm.js/voices/schema.js LegacyOperatorV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoice","text":"opm.js/voices/schema.js LegacyVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV2","text":"opm.js/voices/schema.js LegacyVoiceV2 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV3","text":"opm.js/voices/schema.js LegacyVoiceV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV4","text":"opm.js/voices/schema.js LegacyVoiceV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV5","text":"opm.js/voices/schema.js LegacyVoiceV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV6","text":"opm.js/voices/schema.js LegacyVoiceV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFO","text":"opm.js/voices/schema.js LFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFOInput","text":"opm.js/voices/schema.js LFOInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · lfoSync","text":"opm.js/voices/schema.js lfoSync value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFOTargets","text":"opm.js/voices/schema.js LFOTargets type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFOTargetsInput","text":"opm.js/voices/schema.js LFOTargetsInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · lfoWaveform","text":"opm.js/voices/schema.js lfoWaveform value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LIMITS","text":"opm.js/voices/schema.js LIMITS value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · MAX_BANK_BYTES","text":"opm.js/voices/schema.js MAX_BANK_BYTES value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · MAX_BANK_VOICES","text":"opm.js/voices/schema.js MAX_BANK_VOICES value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · NormalizedVoice","text":"opm.js/voices/schema.js NormalizedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · Operator","text":"opm.js/voices/schema.js Operator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · operatorWaveform","text":"opm.js/voices/schema.js operatorWaveform value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · OperatorWaveform","text":"opm.js/voices/schema.js OperatorWaveform type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · parseVoiceBank","text":"opm.js/voices/schema.js parseVoiceBank value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · PitchEnvelope","text":"opm.js/voices/schema.js PitchEnvelope type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · PreparedVoice","text":"opm.js/voices/schema.js PreparedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · prepareVoice","text":"opm.js/voices/schema.js prepareVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/voices/schema.js · validateVoice","text":"opm.js/voices/schema.js validateVoice value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · Voice","text":"opm.js/voices/schema.js Voice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · VoiceInput","text":"opm.js/voices/schema.js VoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/normalize.js · normalizeVoice","text":"opm.js/voices/normalize.js normalizeVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/voices/normalize.js · prepareVoice","text":"opm.js/voices/normalize.js prepareVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/voices/brass.js · brass","text":"opm.js/voices/brass.js brass value / type where applicable","href":"./api.html#declaration-voicesbrassdts"},{"title":"opm.js/voices/dx7.js · describeDX7","text":"opm.js/voices/dx7.js describeDX7 value / type where applicable","href":"./api.html#declaration-voicesdx7dts"},{"title":"opm.js/voices/dx7.js · DX7ImportDescription","text":"opm.js/voices/dx7.js DX7ImportDescription type","href":"./api.html#declaration-voicesdx7dts"},{"title":"opm.js/voices/dx7.js · importDX7","text":"opm.js/voices/dx7.js importDX7 value / type where applicable","href":"./api.html#declaration-voicesdx7dts"},{"title":"opm.js/voices/examples.js · examples","text":"opm.js/voices/examples.js examples value / type where applicable","href":"./api.html#declaration-voicesexamplesdts"},{"title":"opm.js/voices/gm-map.js · gmDrumVoices","text":"opm.js/voices/gm-map.js gmDrumVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/voices/gm-map.js · gmProgramVoices","text":"opm.js/voices/gm-map.js gmProgramVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/voices/opm.js · describeOPM","text":"opm.js/voices/opm.js describeOPM value / type where applicable","href":"./api.html#declaration-voicesopmdts"},{"title":"opm.js/voices/opm.js · importOPM","text":"opm.js/voices/opm.js importOPM value / type where applicable","href":"./api.html#declaration-voicesopmdts"},{"title":"opm.js/voices/opm.js · OPMImportDescription","text":"opm.js/voices/opm.js OPMImportDescription type","href":"./api.html#declaration-voicesopmdts"},{"title":"opm.js/voices/original.js · originalPresets","text":"opm.js/voices/original.js originalPresets value / type where applicable","href":"./api.html#declaration-voicesoriginaldts"},{"title":"opm.js/voices/preset-metadata.js · defaultBrassMetadata","text":"opm.js/voices/preset-metadata.js defaultBrassMetadata value / type where applicable","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"opm.js/voices/preset-metadata.js · presetMetadata","text":"opm.js/voices/preset-metadata.js presetMetadata value / type where applicable","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"opm.js/voices/preset-metadata.js · PresetMetadata","text":"opm.js/voices/preset-metadata.js PresetMetadata type","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"opm.js/voices/voice.schema.js · voiceSchema","text":"opm.js/voices/voice.schema.js voiceSchema value / type where applicable","href":"./api.html#declaration-voicesvoiceschemadts"},{"title":"opm.js/tools/assets.js · AssetDeployment","text":"opm.js/tools/assets.js AssetDeployment type","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · checkDeployment","text":"opm.js/tools/assets.js checkDeployment value / type where applicable","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · CheckDeploymentOptions","text":"opm.js/tools/assets.js CheckDeploymentOptions type","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · copyAssets","text":"opm.js/tools/assets.js copyAssets value / type where applicable","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · CopyAssetsOptions","text":"opm.js/tools/assets.js CopyAssetsOptions type","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · DeploymentCheck","text":"opm.js/tools/assets.js DeploymentCheck type","href":"./api.html#declaration-toolsassetsdts"},{"title":"Declarations · api/arrangement.d.ts","text":"import type { OPM } from './index.js';\nimport type { ArrangementDefinition } from '../core/arrangement-definition.js';\nexport type { ArrangementLayer, ArrangementSection } from '../core/arrangement-definition.js';\nimport type { BarBeat, TempoPoint } from '../core/transport.js';\nexport interface ArrangementOptions extends ArrangementDefinition {\n bpm?: number;\n horizon?: number;\n interval?: number;\n onError?: (error: Error) => void;\n}\nexport interface ArrangementChangeOptions {\n /** Boundary in quarter notes, or the meter-defined beat/bar; default bar. Never earlier than already admitted notes. */\n quantize?: 'beat' | 'bar' | number;\n /** Removed layers may finish naturally instead of being released at the boundary. Default false. */\n preserveNotes?: boolean;\n /** Linear gain fade in seconds, 0..10; default 0. Shared layers remain continuous. */\n fade?: number;\n}\nexport interface ArrangementGainOptions {\n quantize?: 'beat' | 'bar' | number;\n fade?: number;\n}\nexport type ArrangementState = 'stopped' | 'starting' | 'running' | 'paused' | 'disposed';\nexport interface ArrangementSnapshot {\n readonly state: ArrangementState;\n /** Global quarter-note position; a paused arrangement reports where it will resume. */\n readonly position: number;\n readonly musicalPosition: Readonly\u003cBarBeat>;\n readonly section: string;\n /** Layers effective at position. */\n readonly layers: readonly string[];\n /** Committed changes after the current position. */\n readonly pending: readonly Readonly\u003c{\n beat: number;\n section: string;\n layers: readonly string[];\n }>[];\n /** Notes refused by voice-priority admission; they are not an arrangement failure. */\n readonly priorityDrops: number;\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n}\nexport interface Arrangement {\n readonly state: ArrangementState;\n readonly snapshot: ArrangementSnapshot;\n start(): Promise\u003cvoid>;\n resume(): Promise\u003cvoid>;\n /** Releases owned notes. Resume restarts musically at the paused beat; no DSP checkpoint is restored. */\n pause(): void;\n /** Releases owned notes and rewinds to beat 0. */\n stop(): void;\n switchSection(name: string, options?: ArrangementChangeOptions): number;\n setLayer(name: string, enabled: boolean, options?: ArrangementChangeOptions): number;\n setLayerGain(name: string, gain: number, options?: ArrangementGainOptions): number;\n /** Replaces tempo from the current beat onward; notes already admitted keep their admitted audio times. */\n setTempo(bpm: number): void;\n setTempoMap(map: readonly TempoPoint[]): void;\n pump(): void;\n dispose(): void;\n}\n/**\n * Looping, quantized adaptive music for one OPM instance. Layers are aligned to the global beat grid, so a layer\n * shared by two sections is one continuous schedule: its sounding gates are neither retriggered nor stopped when\n * unrelated layers change. Changes are committed at a boundary no earlier than the notes already admitted.\n */\nexport declare function createArrangement(opm: OPM, options: ArrangementOptions): Arrangement;\n","href":"./api.html#declaration-apiarrangementdts"},{"title":"Declarations · api/fx.d.ts","text":"import type { StereoEffectsOptions } from '../core/fx.js';\nexport interface EffectsEvent {\n type: 'error';\n error: Error;\n}\nexport interface EffectsOptions {\n workletUrl?: string | URL;\n params?: StereoEffectsOptions;\n onEvent?: (event: EffectsEvent) => void;\n}\nexport interface OpmEffects {\n readonly input: AudioNode;\n readonly output: AudioNode;\n /** Resolves once the module/node have been created; context playback is host-owned. */\n readonly ready: Promise\u003cvoid>;\n update(params: StereoEffectsOptions): void;\n reset(): void;\n dispose(): void;\n}\n/** Optional stereo effect insert. Does not connect, resume, suspend or close the host context. */\nexport declare function createEffects(context: BaseAudioContext, options?: EffectsOptions): Promise\u003cOpmEffects>;\n","href":"./api.html#declaration-apifxdts"},{"title":"Declarations · api/index.d.ts","text":"import type { CompleteVoiceInput, NormalizedVoice, VoiceInput } from '../voices/schema.js';\nimport type { NoteControls, QualityProfile, SynthOptions } from '../core/synth.js';\nimport type { TuningOptions } from '../core/tuning.js';\nexport type { ADSR, LFO, LFOInput, LegacyLFO, LegacyLFOV5, LFOTargets, LFOTargetsInput, KeyScale, Operator, OperatorWaveform, Voice, VoiceInput, FrozenVoice, FrozenOperator, NormalizedVoice, PreparedVoice, PitchEnvelope, LegacyVoiceV3, LegacyVoiceV4, LegacyVoiceV5, LegacyVoiceV6 } from '../voices/schema.js';\nexport type { NoteControls, SynthOptions, QualityProfile } from '../core/synth.js';\nexport type { TuningOptions, NormalizedTuning } from '../core/tuning.js';\nexport { createEffects } from './fx.js';\nexport type { OpmEffects, EffectsOptions, EffectsEvent } from './fx.js';\nexport { createStereoEffects, applyEffects } from '../core/fx.js';\nexport type { StereoEffects, StereoEffectsOptions, ChorusOptions, ReverbOptions } from '../core/fx.js';\nexport { playSequence, streamSequence } from './sequence.js';\nexport type { PlaySequenceOptions, SequencePlayback, SequenceStreamOptions, SequenceStream } from './sequence.js';\nexport type { SequenceEvent, SequenceNoteEvent, SequenceStopEvent, SequenceControlEvent } from '../core/sequence.js';\nexport { createTransport, beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, normalizeTempoMap, quantizeBeat, swingBeat, swingBeatEvents } from './transport.js';\nexport type { BeatSequenceEvent, TransportLoop, TransportState, TransportOptions, TransportSnapshot, MusicalTransport, TempoPoint, TimeSignature, BarBeat, BeatQuantization } from './transport.js';\nexport { createArrangement } from './arrangement.js';\nexport type { Arrangement, ArrangementLayer, ArrangementSection, ArrangementOptions, ArrangementChangeOptions, ArrangementGainOptions, ArrangementState, ArrangementSnapshot } from './arrangement.js';\nexport { createMidiAdapter, requestMidiAccess } from './midi.js';\nexport type { MidiAdapter, MidiAdapterOptions, MidiAdapterSnapshot, MidiAccessLike, MidiInputLike, MidiMessageEventLike, MidiNavigatorLike, MidiControllerMapping, MidiScalarControl, MidiOperatorControl } from './midi.js';\nexport { importMidiFile, exportMidiFile, MAX_MIDI_FILE_BYTES, MAX_MIDI_FILE_TRACKS, MAX_MIDI_FILE_EVENTS } from '../core/midi-file.js';\nexport type { MidiFileWarningCode, MidiFileWarning, MidiImportOptions, MidiImportResult, MidiExportOptions, MidiFileControlKind, MidiFilePreservedControl, MidiFileLossSummary } from '../core/midi-file.js';\nexport type { MidiVoiceMap } from '../core/midi-state.js';\nexport { gmProgramVoices, gmDrumVoices } from '../voices/gm-map.js';\nexport { parseScoreProject, serializeScoreProject, compileBeatSequence, MAX_SCORE_PROJECT_BYTES, MAX_SCORE_PROJECT_VOICES, parseArrangementProject, serializeArrangementProject, MAX_ARRANGEMENT_PROJECT_BYTES, MAX_ARRANGEMENT_PROJECT_VOICES } from '../core/project.js';\nexport type { ScoreProject, ScoreProjectSettings, BeatSequenceOptions, ArrangementProject } from '../core/project.js';\nexport type { ArrangementDefinition } from '../core/arrangement-definition.js';\nexport { VERSION } from '../version.js';\nexport { createPerformance } from './performance.js';\nexport type { Performance, PerformanceOptions, PerformancePartOptions, PerformancePartControls, PerformanceNoteOptions, PerformanceNoteOffOptions, PerformanceKeySnapshot, PerformancePartSnapshot } from './performance.js';\nexport { renderSequenceInWorker } from './render-worker.js';\nexport type { WavSink, WorkerRenderOptions, WorkerRenderProgress, WorkerRenderResult, WorkerRenderPhase, WorkerRenderPhaseStatus, WorkerRenderPhaseDiagnostics } from './render-worker.js';\nexport type NoteState = 'accepted' | 'started' | 'released' | 'ended' | 'stolen' | 'cancelled' | 'rejected';\nexport interface NoteEvent {\n type: 'note';\n id: number;\n state: NoteState;\n reason?: string;\n /** Actual AudioContext sample frame and seconds at admission, dispatch or completion. */\n frame: number;\n time: number;\n}\nexport interface DiagnosticsEvent {\n type: 'diagnostics';\n requestId: number;\n activeVoices: number;\n pendingEvents: number;\n errors: number;\n rejectedNotes: number;\n}\nexport interface ErrorEvent {\n type: 'error';\n error: Error;\n}\nexport type CommandName = 'stop' | 'updateNote' | 'allNotesOff' | 'panic' | 'setMixGain' | 'setTuning';\nexport interface CommandEvent {\n type: 'command';\n command: CommandName;\n commandId?: number;\n id?: number;\n /** Accepted means admitted or immediately applied, not a future execution guarantee. */\n state: 'accepted' | 'rejected';\n reason?: string;\n frame: number;\n time: number;\n}\nexport type ContextState = AudioContextState | 'interrupted';\nexport interface ContextEvent {\n type: 'context';\n state: ContextState;\n frame: number;\n time: number;\n}\nexport interface ResetEvent {\n type: 'reset';\n reason: 'close' | 'failure' | 'panic' | 'interruption';\n frame: number;\n time: number;\n /** Initiating panic command, when available; independent of receipt-cache eviction. */\n commandId?: number;\n}\nexport type OPMEvent = NoteEvent | DiagnosticsEvent | CommandEvent | ContextEvent | ResetEvent | ErrorEvent;\nexport interface CommandWaitOptions {\n /** Milliseconds, default 5000; an integer in 1..60000. */\n timeout?: number;\n signal?: AbortSignal;\n}\n/** Admission rejection, not an audio execution/completion result. */\nexport declare class CommandRejectedError extends Error {\n readonly event: Readonly\u003cCommandEvent>;\n constructor(event: CommandEvent);\n}\nexport interface OPMOptions {\n sampleRate?: number;\n /** Borrowed context: OPM never closes or suspends it. */\n context?: AudioContext;\n /** Omit to connect to context.destination; null disables automatic connection. */\n destination?: AudioNode | null;\n /** Same-origin HTTPS (or secure loopback HTTP) module; default stays relative to this API module. */\n workletUrl?: string | URL;\n mixGain?: number;\n tuning?: TuningOptions;\n stealing?: SynthOptions['stealing'];\n /** Immutable synthesis profile; standard preserves the default sound. */\n quality?: QualityProfile;\n /** Logical polyphony, an integer in 1..32; default 8. Release tails occupy voices. */\n maxVoices?: number;\n /** Cancel all voices/events on interruption, or preserve direct-note state until resume. */\n interruption?: 'cancel' | 'preserve';\n onEvent?: (event: OPMEvent) => void;\n}\ninterface PlayNoteBase {\n voice?: string | VoiceInput;\n note: number;\n /** A late start keeps its full duration; drop rejects even if processing is delayed. */\n late?: 'start' | 'drop';\n /** null (the default) holds until stop; a number sets a duration before release. */\n duration?: number | null;\n velocity?: number;\n pan?: number;\n /** Admission importance, integer 0..127; larger values cannot be stolen by lower-priority notes. Default 0. */\n voicePriority?: number;\n}\nexport type PlayNoteOptions = PlayNoteBase & (\n/** Relative delay in seconds; cannot be combined with at. */\n{\n time?: number;\n at?: never;\n} | \n/** Absolute AudioContext seconds, at most 60 seconds ahead. Past times are allowed. */\n{\n at: number;\n time?: never;\n});\nexport interface ScheduledNoteOptions {\n /** Absolute AudioContext seconds. Stop without at is immediate, including cancellation. */\n at?: number;\n}\n/** Explicit cancellation is immediate-only; ordinary stops preserve tail automation. */\nexport type StopOptions = ScheduledNoteOptions & {\n cancelControls?: never;\n} | {\n at?: never;\n cancelControls?: boolean;\n};\n/** Browser-facing facade. Import Synth from ../core/synth.js for offline rendering. */\nexport declare class OPM {\n readonly sampleRate: number | undefined;\n get quality(): QualityProfile;\n get maxVoices(): number;\n /** Defensive map snapshot; loaded patches are deeply frozen. Use loadVoice to replace one. */\n get voices(): ReadonlyMap\u003cstring, NormalizedVoice>;\n readonly context: AudioContext | null;\n readonly node: AudioWorkletNode | null;\n onEvent?: (event: OPMEvent) => void;\n constructor(options?: OPMOptions);\n loadVoice(name: string, voice: VoiceInput): void;\n /** Atomically replaces the registry. Only an empty plain array (not JSON \"[]\") clears it. */\n replaceVoiceBank(source: string | readonly CompleteVoiceInput[]): void;\n removeVoice(name: string): boolean;\n /** Canonical versioned entries, detached from the immutable registry and UTF-8 bounded. */\n exportVoiceBank(): string;\n /** Independent subscription; close preserves it for restart, dispose removes it permanently. */\n subscribe(listener: (event: OPMEvent) => void): () => void;\n start(): Promise\u003cvoid>;\n resume(): Promise\u003cvoid>;\n connect(destination: AudioNode): this;\n disconnect(destination?: AudioNode): this;\n playNote(options: PlayNoteOptions): number;\n stop(id: number, options?: StopOptions): number;\n /** Pending updates apply at onset. At equal frames stop precedes onset, then controls. */\n updateNote(id: number, controls: NoteControls, options?: ScheduledNoteOptions): number;\n /** Cancel all pending events and release all active gates, preserving release tails. */\n allNotesOff(): number;\n /** Immediate silence, including release/stealing tails; routing and patch cache survive. */\n panic(): number;\n setMixGain(gain: number): number;\n setTuning(tuning: TuningOptions): number;\n /** Wait for admission only. At most 128 receipts and 64 live waits; live waits prevent eviction. */\n waitForCommand(commandId: number, options?: CommandWaitOptions): Promise\u003cCommandEvent>;\n getDiagnostics(): Promise\u003cDiagnosticsEvent>;\n close(): Promise\u003cvoid>;\n /** Permanently closes OPM and releases host callbacks; never closes a borrowed context. */\n dispose(): Promise\u003cvoid>;\n}\nexport interface LookaheadWindow {\n /** Half-open absolute AudioContext time window. Missed windows are skipped after a stall. */\n from: number;\n to: number;\n /** Maximum notes the callback may return in this batch. */\n maxNotes: number;\n}\nexport type LookaheadNote = PlayNoteOptions & {\n at: number;\n duration: number;\n};\nexport interface LookaheadOptions {\n /** Seconds, default 0.2; bounded to 0.02..10 and greater than interval. */\n horizon?: number;\n /** Seconds between pumps, default 0.025; bounded to 0.005..1. */\n interval?: number;\n /** Maximum batch size, default 32; bounded to 1..128. */\n maxNotes?: number;\n /** Timer/callback failures stop scheduling and are delivered here. */\n onError: (error: Error) => void;\n}\nexport interface LookaheadScheduler {\n readonly running: boolean;\n /** Call from a user gesture; initializes/resumes OPM, then fills the first window. */\n start(): Promise\u003cvoid>;\n /** Cancels pending notes and releases this scheduler's active gates, not other notes. */\n stop(): void;\n /** Stops and permanently prevents restart; does not close OPM. */\n dispose(): void;\n}\n/** Bounded timer-driven admission, not timer-driven note release. Audio frames own all gates. */\nexport declare function createLookaheadScheduler(opm: OPM, schedule: (window: LookaheadWindow) => readonly LookaheadNote[], options: LookaheadOptions): LookaheadScheduler;\n","href":"./api.html#declaration-apiindexdts"},{"title":"Declarations · api/midi.d.ts","text":"import type { Performance } from './performance.js';\nimport type { MidiVoiceMap } from '../core/midi-state.js';\nexport type MidiScalarControl = 'pitch' | 'expression' | 'gain' | 'pan' | 'modulation' | 'feedback' | 'lfoRate' | 'amDepth' | 'pmDepth';\nexport type MidiOperatorControl = 'operatorLevels' | 'operatorRatios' | 'operatorFrequencies';\ninterface MidiControllerRange {\n /** CC number 0..127, excluding RPN selection/pedal/reset/panic CC64/100/101/120/121/123. */\n controller: number;\n /** CC0 maps to min and CC127 to max; both endpoints must be valid engine controls. */\n min: number;\n max: number;\n /** Seconds 0..10; pitch uses glide, other fields use the engine control ramp. */\n ramp: number;\n}\n/** One CC targets one scalar or one operator element; no executable callbacks are accepted. */\nexport type MidiControllerMapping = MidiControllerRange & ({\n field: MidiScalarControl;\n reset?: number;\n} | {\n field: MidiOperatorControl;\n operator: 0 | 1 | 2 | 3;\n reset?: number | null;\n});\n/** Structural subset of Web MIDI, so hosts and tests can inject a real or simulated access object. */\nexport interface MidiMessageEventLike {\n readonly data: Uint8Array | null;\n}\nexport interface MidiInputLike {\n readonly id: string;\n readonly name?: string | null;\n readonly state?: string;\n readonly connection?: string;\n open?(): Promise\u003cunknown>;\n addEventListener(type: 'midimessage', listener: (event: MidiMessageEventLike) => void): void;\n removeEventListener(type: 'midimessage', listener: (event: MidiMessageEventLike) => void): void;\n}\nexport interface MidiAccessLike {\n readonly inputs: ReadonlyMap\u003cstring, MidiInputLike>;\n addEventListener(type: 'statechange', listener: () => void): void;\n removeEventListener(type: 'statechange', listener: () => void): void;\n}\nexport interface MidiNavigatorLike {\n requestMIDIAccess?(options?: {\n sysex?: boolean;\n software?: boolean;\n }): Promise\u003cMidiAccessLike>;\n}\nexport interface MidiAdapterOptions {\n /** Channels 1..16 map to zero-based Performance parts below this count; default 16. */\n parts?: number;\n /** Semitones for full pitch-bend deflection, 0..48; default 2. */\n pitchBendRange?: number;\n /** Opt-in program 0..127 to named voice selection for later note onsets. */\n programVoices?: MidiVoiceMap;\n /** Restrict to these stable port IDs; default every currently and subsequently connected input. */\n inputIds?: readonly string[];\n /** At most 128 own-data mappings. Ordinary CC defaults are overridden only for listed CCs. */\n controllerMap?: readonly MidiControllerMapping[];\n onError?: (error: Error) => void;\n}\nexport interface MidiAdapterSnapshot {\n readonly inputs: readonly string[];\n readonly heldKeys: number;\n /** Messages outside the supported channel-voice subset or malformed packets. */\n readonly ignoredMessages: number;\n readonly disposed: boolean;\n}\nexport interface MidiAdapter {\n readonly snapshot: MidiAdapterSnapshot;\n /** Releases only keys owned by this adapter; unrelated Performance parts and host notes stay untouched. */\n releaseAll(): void;\n dispose(): void;\n}\n/**\n * Request MIDI access without SysEx. Call from an explicit user action; browsers may show a permission prompt.\n * Importing this module never requests access.\n */\nexport declare function requestMidiAccess(host?: MidiNavigatorLike | undefined): Promise\u003cMidiAccessLike>;\n/**\n * Map channel-voice MIDI from user-granted inputs onto Performance parts. It is an adapter, not a MIDI driver:\n * no SysEx, clock or hardware-specific behavior is implemented.\n *\n * Note-off releases the oldest still-held key of the same input/channel/pitch. CC1, channel pressure and polyphonic\n * pressure all scale LFO depth 1x..2x (never below the patch default). CC7/CC11 multiply into part expression, CC10 pans,\n * CC64 is sustain, CC120/123 release only this adapter's keys for the channel, and CC121 resets its controllers.\n */\nexport declare function createMidiAdapter(performance: Performance, access: MidiAccessLike, options?: MidiAdapterOptions): MidiAdapter;\nexport {};\n","href":"./api.html#declaration-apimididts"},{"title":"Declarations · api/performance.d.ts","text":"import type { OPM } from './index.js';\nimport type { PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { NoteControls } from '../core/synth.js';\nexport interface PerformanceOptions {\n /** Zero-based part count, 1..16; default 16. */\n parts?: number;\n /** Total physical/pedal key records, 1..128; default 128. */\n maxKeys?: number;\n /** Per-part key records, 1..128; default 128, also subject to maxKeys. */\n maxKeysPerPart?: number;\n /** Event-driven cleanup failures; synchronous operation failures still throw. */\n onError?: (error: Error) => void;\n}\nexport interface PerformancePartControls {\n glide?: number;\n pan?: number;\n expression?: number;\n}\nexport interface PerformancePartOptions extends PerformancePartControls {\n voice: string | VoiceInput;\n mode: 'poly' | 'mono';\n legato: boolean;\n priority: 'last' | 'high' | 'low';\n /** Optional owned-voice budget, including audible release tails, 1..32. */\n voiceLimit?: number;\n /** Admission importance, 0..127; larger values protect against lower-priority notes. */\n voicePriority?: number;\n}\nexport interface PerformanceNoteOptions {\n velocity?: number;\n}\nexport interface PerformanceNoteOffOptions {\n force?: boolean;\n}\nexport interface PerformanceKeySnapshot {\n readonly key: number;\n readonly note: number;\n readonly velocity: number;\n readonly held: boolean;\n /** Only the currently sounding mono key owns its shared gate. */\n readonly gateId: number | null;\n}\nexport interface PerformancePartSnapshot {\n readonly voice: string | PreparedVoice;\n readonly mode: 'poly' | 'mono';\n readonly legato: boolean;\n readonly priority: 'last' | 'high' | 'low';\n readonly glide: number;\n readonly pan: number;\n readonly expression: number;\n readonly sustain: boolean;\n readonly voiceLimit: number | undefined;\n readonly voicePriority: number;\n readonly selectedKey: number | null;\n readonly keys: readonly PerformanceKeySnapshot[];\n}\nexport interface Performance {\n /** preserveNotes keeps existing gates when only the voice changes; default false. */\n configurePart(part: number, options: Partial\u003cPerformancePartOptions>, policy?: {\n preserveNotes?: boolean;\n }): void;\n updatePart(part: number, controls: PerformancePartControls): void;\n /** Update one physical key; inactive mono keys store controls without touching the selected gate. */\n updateKey(part: number, key: number, controls: NoteControls): boolean;\n /** Update owned sounding notes and defaults for future notes, including release tails. */\n updatePartNotes(part: number, controls: NoteControls): void;\n /** Independent key identity, not an OPM admission receipt. OPM must already be started. */\n noteOn(part: number, note: number, options?: PerformanceNoteOptions): number;\n noteOff(part: number, key: number, options?: PerformanceNoteOffOptions): boolean;\n sustain(part: number, on: boolean): void;\n allNotesOff(part?: number): void;\n getPart(part: number): PerformancePartSnapshot;\n /** Detached effective part defaults, including resolved voice operator controls (not per-key overrides). */\n getPartControls(part: number): Readonly\u003cNoteControls>;\n dispose(): void;\n}\n/** Device-agnostic key policy. No context, timers, MIDI driver or global panic is created. */\nexport declare function createPerformance(opm: OPM, options?: PerformanceOptions): Performance;\n","href":"./api.html#declaration-apiperformancedts"},{"title":"Declarations · api/render-worker.d.ts","text":"import type { ChunkedSequenceOptions, SequenceCapacity, SequenceEvent } from '../core/sequence.js';\nimport type { WavFormat } from '../core/wav.js';\nexport interface WavSink {\n /** Takes ownership of bytes; transferring/detaching their buffer is supported. */\n write(bytes: Uint8Array): Promise\u003cvoid> | void;\n close?(): Promise\u003cvoid> | void;\n /** Must invalidate outstanding writes. Called once on failure, without waiting for them. */\n abort?(reason: unknown): Promise\u003cvoid> | void;\n}\nexport interface WorkerRenderProgress {\n readonly frames: number;\n readonly totalFrames: number;\n readonly bytesWritten: number;\n readonly errors: number;\n}\nexport type WorkerRenderPhase = 'initializing' | 'rendering' | 'writing' | 'closing' | 'completed' | 'cancelled' | 'failed';\nexport interface WorkerRenderPhaseStatus {\n readonly phase: WorkerRenderPhase;\n /** Host-local monotonic wall time, not a DSP benchmark or network trace. */\n readonly elapsedMs: number;\n readonly phaseElapsedMs: number;\n readonly frames: number;\n readonly totalFrames: number;\n readonly bytesWritten: number;\n readonly errors: number;\n}\nexport interface WorkerRenderPhaseDiagnostics {\n readonly status: 'completed' | 'cancelled' | 'failed';\n readonly initializingMs: number;\n readonly renderingMs: number;\n readonly writingMs: number;\n readonly closingMs: number;\n readonly totalMs: number;\n}\nexport interface WorkerRenderOptions extends ChunkedSequenceOptions {\n format?: WavFormat;\n sink: WavSink;\n onProgress?: (progress: WorkerRenderProgress) => void;\n /** Optional module-ready deadline only; integer milliseconds in 1..2147483647. */\n startupTimeoutMs?: number;\n /** Retain four bounded host-local phase totals in the successful result. */\n phaseDiagnostics?: boolean;\n /** Synchronous observer. Throwing fails the render and invalidates the sink. */\n onPhase?: (status: WorkerRenderPhaseStatus) => void;\n /** Reviewed same-origin secure HTTP(S) static module, never a blob/data URL. */\n workerUrl?: string | URL;\n}\nexport interface WorkerRenderResult {\n readonly capacity: SequenceCapacity;\n readonly format: WavFormat;\n readonly bytesWritten: number;\n readonly diagnostics: Readonly\u003c{\n errors: number;\n processedEvents: number;\n renderedFrames: number;\n phases?: WorkerRenderPhaseDiagnostics;\n }>;\n}\n/** Browser-only, backpressured offline rendering; importing this module allocates nothing. */\nexport declare function renderSequenceInWorker(events: readonly SequenceEvent[], options: WorkerRenderOptions): Promise\u003cWorkerRenderResult>;\n","href":"./api.html#declaration-apirender-workerdts"},{"title":"Declarations · api/sequence.d.ts","text":"import type { OPM } from './index.js';\nimport type { SequenceEvent } from '../core/sequence.js';\nexport interface PlaySequenceOptions {\n /** Absolute AudioContext origin; omitted means currentTime at submission. */\n at?: number;\n}\nexport interface SequencePlayback {\n /** Score IDs mapped to submitted OPM IDs, not an acknowledgement of admission. */\n readonly ids: ReadonlyMap\u003cnumber, number>;\n /** Release started notes and immediately cancel pending onsets. Idempotent. */\n stop(): void;\n}\n/** Submit one bounded, wholly validated score; lifecycle acknowledgements remain OPM events. */\nexport declare function playSequence(opm: OPM, events: readonly SequenceEvent[], options?: PlaySequenceOptions): SequencePlayback;\nexport interface SequenceStreamOptions {\n /** Absolute origin, at most60 seconds ahead; defaults to currentTime at start. */\n at?: number;\n /** Lookahead seconds in0.01..10, default0.2. */\n horizon?: number;\n /** Timer interval seconds in0.001..horizon/2, default0.025 (or horizon/2). */\n interval?: number;\n /** Maximum own queued commands, integer1..256; default256. */\n maxSlots?: number;\n signal?: AbortSignal;\n onError?: (error: Error) => void;\n}\nexport interface SequenceStream {\n readonly running: boolean;\n /** Live/pending owned score IDs only; terminal IDs are pruned. Detached snapshot. */\n readonly ids: ReadonlyMap\u003cnumber, number>;\n /** One-shot start. stop/dispose/reset/interruption permanently cancel this score. */\n start(): Promise\u003cvoid>;\n /** Optional explicit pump for hosts driving their own clock; timers still run. */\n pump(): void;\n stop(): void;\n dispose(): void;\n}\n/**\n * Stream a fully validated long score in bounded mixed-event windows.\n * A missed onset/window or rejected command stops the stream instead of silently retiming.\n */\nexport declare function streamSequence(opm: OPM, events: readonly SequenceEvent[], options?: SequenceStreamOptions): SequenceStream;\n","href":"./api.html#declaration-apisequencedts"},{"title":"Declarations · api/transport.d.ts","text":"import type { OPM } from './index.js';\nimport type { BeatSequenceEvent } from '../core/sequence.js';\nimport type { TempoPoint, TimeSignature, BarBeat } from '../core/transport.js';\nexport { beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, normalizeTempoMap, quantizeBeat, swingBeat } from '../core/transport.js';\nexport type { TempoPoint, TimeSignature, BarBeat, BeatQuantization } from '../core/transport.js';\nexport type { BeatSequenceEvent } from '../core/sequence.js';\nexport interface TransportLoop {\n enabled: boolean;\n from: number;\n to: number;\n}\nexport type TransportState = 'stopped' | 'starting' | 'running' | 'paused' | 'disposed';\nexport interface TransportOptions {\n bpm?: number;\n tempoMap?: readonly TempoPoint[];\n timeSignature?: TimeSignature;\n loop?: TransportLoop;\n horizon?: number;\n interval?: number;\n /** Future audio anchor in seconds for start and reconstruction; default min(0.05, horizon / 2), range 0..10. */\n startupLead?: number;\n maxSlots?: number;\n onError?: (error: Error) => void;\n}\nexport interface TransportSnapshot {\n readonly position: number;\n readonly musicalPosition: Readonly\u003cBarBeat>;\n readonly state: TransportState;\n readonly running: boolean;\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n readonly loop: Readonly\u003cTransportLoop>;\n}\nexport interface MusicalTransport {\n readonly position: number;\n readonly running: boolean;\n readonly state: TransportState;\n readonly snapshot: TransportSnapshot;\n readonly ids: ReadonlyMap\u003cnumber, number>;\n start(): Promise\u003cvoid>;\n resume(): Promise\u003cvoid>;\n pause(): void;\n stop(): void;\n seek(beat: number): void;\n /** Replace the tempo from the current musical position onward; retain preceding points. */\n setTempo(bpm: number): void;\n setTempoMap(map: readonly TempoPoint[]): void;\n setLoop(loop: TransportLoop): void;\n pump(): void;\n dispose(): void;\n}\n/** Internal shared preparation: beat durations remain beats until admitted to the audio clock. */\nexport declare function prepareBeatEvents(opm: OPM, beatEvents: readonly BeatSequenceEvent[]): import(\"../core/sequence.js\").SequenceSnapshot;\n/** Swing note starts AND ends, so adjacent gates retain their musical ordering. */\nexport declare function swingBeatEvents(events: readonly BeatSequenceEvent[], subdivision?: number, ratio?: number): BeatSequenceEvent[];\n/** Restartable beat transport. Only owned IDs are ever stopped; the shared engine is not closed. */\nexport declare function createTransport(opm: OPM, beatEvents: readonly BeatSequenceEvent[], options?: TransportOptions): MusicalTransport;\n","href":"./api.html#declaration-apitransportdts"},{"title":"Declarations · core/algorithms.d.ts","text":"export interface AlgorithmGraph {\n readonly inputs: readonly (readonly number[])[];\n readonly carriers: readonly number[];\n}\nexport declare const ALGORITHMS: readonly [AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph];\nexport declare function feedbackPhase(previous: number, older: number, amount: number): number;\n","href":"./api.html#declaration-corealgorithmsdts"},{"title":"Declarations · core/arrangement-definition.d.ts","text":"import type { BeatSequenceEvent, SequenceSnapshot, SequenceVoices } from './sequence.js';\nimport type { TempoPoint, TimeSignature } from './transport.js';\nexport interface ArrangementLayer {\n name: string;\n /** Loop length in quarter notes, 1/1024 \u003c= length \u003c= 256; aligned to the global beat grid. */\n length: number;\n /** Note onsets lie inside [0, length). Durations and owned controls may cross a loop boundary. */\n events: readonly BeatSequenceEvent[];\n /** Admission importance 0..127, combined with each note's value by taking the maximum. */\n voicePriority?: number;\n /** Per-layer gain 0..1, default 1; independent of authored note expression. */\n gain?: number;\n}\nexport interface ArrangementSection {\n name: string;\n layers: readonly string[];\n}\n/** Musical definition shared by projects and live options, without scheduler callbacks or audio state. */\nexport interface ArrangementDefinition {\n readonly layers: readonly ArrangementLayer[];\n readonly sections: readonly ArrangementSection[];\n readonly initialSection: string;\n readonly tempoMap?: readonly TempoPoint[];\n readonly timeSignature?: TimeSignature;\n}\ninterface NormalizedArrangementDefinition extends ArrangementDefinition {\n readonly layers: readonly Readonly\u003cRequired\u003cArrangementLayer>>[];\n readonly sections: readonly Readonly\u003cArrangementSection>[];\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n}\nexport declare const MAX_ARRANGEMENT_LAYERS = 16;\nexport declare const MAX_ARRANGEMENT_SECTIONS = 32;\nexport declare const MAX_ARRANGEMENT_LAYER_LENGTH = 256;\nexport declare function arrangementName(value: unknown, label: string): string;\n/** One beat-event boundary for portable projects and live arrangements. */\nexport declare function prepareBeatSequence(events: readonly BeatSequenceEvent[], voices?: SequenceVoices): {\n events: readonly BeatSequenceEvent[];\n score: SequenceSnapshot;\n};\n/** Reject definitions that cannot be converted through their complete tempo map. */\nexport declare function validateBeatHorizon(events: readonly BeatSequenceEvent[], map: readonly Readonly\u003cTempoPoint>[]): void;\n/** Validate all references and resource bounds before a scheduler can own any notes. */\nexport declare function prepareArrangementDefinition(input: unknown, voices?: SequenceVoices): {\n definition: NormalizedArrangementDefinition;\n scores: readonly SequenceSnapshot[];\n};\nexport {};\n","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"Declarations · core/decimator.d.ts","text":"export type QualityProfile = 'eco' | 'standard' | 'high';\nexport declare const DECIMATOR_STATE_SIZE: number;\nexport declare function qualityOversample(quality: QualityProfile): number;\nexport declare function createDecimatorCoefficients(sampleRate: number, quality?: QualityProfile): Float64Array;\nexport declare function decimateSample(input: number, state: Float64Array, coefficients: Float64Array): number;\n/** Largest buffered sample magnitude, excluding counter/position bookkeeping. */\nexport declare function decimatorPeak(state: Float64Array): number;\n","href":"./api.html#declaration-coredecimatordts"},{"title":"Declarations · core/envelope.d.ts","text":"import type { ADSR } from '../voices/schema.js';\nexport interface PreparedEnvelope extends ADSR {\n sustainDB: number;\n sustainGain: number;\n decayEnd: number;\n releaseEnd: number;\n releaseDB: number;\n releaseSpanDB: number;\n}\nexport declare const FLOOR_DB = -96;\nexport declare function prepareEnvelope(a: number, d: number, s: number, r: number, gate: number): PreparedEnvelope;\nexport declare function preparedEnvelopeAt(time: number, gate: number, envelope: PreparedEnvelope): number;\nexport declare function envelopeAt(time: number, gate: number, adsr: ADSR): number;\n","href":"./api.html#declaration-coreenvelopedts"},{"title":"Declarations · core/fx.d.ts","text":"export interface ChorusOptions {\n rate: number;\n depth: number;\n mix: number;\n feedback?: number;\n voices?: number;\n}\nexport interface ReverbOptions {\n size: number;\n damping: number;\n mix: number;\n preDelay?: number;\n width?: number;\n}\nexport interface StereoEffectsOptions {\n chorus?: ChorusOptions;\n reverb?: ReverbOptions;\n order?: 'chorus-reverb' | 'reverb-chorus';\n}\nexport interface StereoEffects {\n process(left: Float32Array, right: Float32Array, offset?: number, length?: number): void;\n /** Replaces the complete configuration; omitted sections fade to bypass. */\n update(params: StereoEffectsOptions): void;\n reset(): void;\n readonly tailSeconds: number;\n readonly params: Readonly\u003cStereoEffectsOptions>;\n}\n/** Internal trust-boundary helper, also used by the worklet protocol. */\nexport declare function effectsData(input: unknown, keys: readonly string[], label: string): Record\u003cstring, unknown>;\nexport declare function normalizeEffectsOptions(input?: StereoEffectsOptions): Readonly\u003cStereoEffectsOptions>;\nexport declare function createStereoEffects(sampleRate: number, options?: StereoEffectsOptions): StereoEffects;\nexport declare function applyEffects(left: Float32Array, right: Float32Array, sampleRate: number, params: StereoEffectsOptions): {\n left: Float32Array;\n right: Float32Array;\n};\n","href":"./api.html#declaration-corefxdts"},{"title":"Declarations · core/index.d.ts","text":"import type { CompleteVoiceInput, FrozenVoice } from '../voices/schema.js';\nimport type { SynthOptions } from './synth.js';\nexport type { ADSR, PitchEnvelope, LFO, LFOInput, LegacyLFO, LegacyLFOV5, LFOTargets, LFOTargetsInput, KeyScale, Operator, OperatorWaveform, Voice, LegacyVoice, LegacyVoiceV2, LegacyVoiceV3, LegacyVoiceV4, LegacyVoiceV5, LegacyVoiceV6, VoiceInput, FrozenVoice, PreparedVoice } from '../voices/schema.js';\nexport type { NoteOptions, NoteControls, VoiceEndReason, SynthOptions, QualityProfile } from './synth.js';\nexport type { TuningOptions, NormalizedTuning } from './tuning.js';\nexport { createStereoEffects, applyEffects } from './fx.js';\nexport type { StereoEffects, StereoEffectsOptions, ChorusOptions, ReverbOptions } from './fx.js';\nexport { normalizeTuning, tuningFrequency } from './tuning.js';\nexport { lfoValue } from './lfo.js';\nexport type { BeatSequenceEvent, SequenceEvent, SequenceNoteEvent, SequenceStopEvent, SequenceControlEvent, SequenceVoices, SequenceOptions, PreparedSequenceEvent, SequenceSnapshot, SequenceCapacity, ChunkedSequenceOptions, SequenceChunk, ChunkedSequenceRender } from './sequence.js';\nexport { prepareSequence, renderSequence, prepareLongSequence, estimateSequenceCapacity, renderSequenceChunks, MAX_SEQUENCE_NOTES, MAX_SEQUENCE_SLOTS, MAX_SEQUENCE_SECONDS, MAX_RENDER_SAMPLES, MAX_LONG_SEQUENCE_SECONDS, MAX_LONG_SEQUENCE_EVENTS, MAX_SEQUENCE_CHUNK_FRAMES, sampleRateValue } from './sequence.js';\nexport type { WavOptions, WavFormat, WavEncoderOptions, WavEncoder, WavChunk } from './wav.js';\nexport type { TempoPoint, TimeSignature, BarBeat } from './transport.js';\nexport { beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, normalizeTempoMap } from './transport.js';\nexport { importMidiFile, exportMidiFile, MAX_MIDI_FILE_BYTES, MAX_MIDI_FILE_TRACKS, MAX_MIDI_FILE_EVENTS } from './midi-file.js';\nexport type { MidiFileWarningCode, MidiFileWarning, MidiImportOptions, MidiImportResult, MidiExportOptions, MidiFileControlKind, MidiFilePreservedControl, MidiFileLossSummary } from './midi-file.js';\nexport type { MidiVoiceMap } from './midi-state.js';\nexport { gmProgramVoices, gmDrumVoices } from '../voices/gm-map.js';\nexport { parseScoreProject, serializeScoreProject, compileBeatSequence, MAX_SCORE_PROJECT_BYTES, MAX_SCORE_PROJECT_VOICES, parseArrangementProject, serializeArrangementProject, MAX_ARRANGEMENT_PROJECT_BYTES, MAX_ARRANGEMENT_PROJECT_VOICES } from './project.js';\nexport type { ScoreProject, ScoreProjectSettings, BeatSequenceOptions, ArrangementProject } from './project.js';\nexport type { ArrangementLayer, ArrangementSection, ArrangementDefinition } from './arrangement-definition.js';\nexport interface RenderNoteOptions extends SynthOptions {\n voice: CompleteVoiceInput | FrozenVoice;\n note?: number;\n duration?: number;\n velocity?: number;\n pan?: number;\n voicePriority?: number;\n sampleRate?: number;\n}\nexport interface RenderResult {\n /** Same buffer as left, retained for compatibility. */\n samples: Float32Array;\n left: Float32Array;\n right: Float32Array;\n sampleRate: number;\n diagnostics: {\n errors: number;\n };\n}\nexport { envelopeAt } from './envelope.js';\nexport { ALGORITHMS } from './algorithms.js';\nexport { Synth, VoiceAdmissionError, normalizeVoice, prepareVoice, validateMaxVoices, validateNoteControls, validateVoicePriority } from './synth.js';\nexport { encodeWav, createWavEncoder } from './wav.js';\nexport declare const HEADROOM = 0.7;\nexport declare const OVERSAMPLE = 4;\nexport declare function renderNote(options: RenderNoteOptions): RenderResult;\n","href":"./api.html#declaration-coreindexdts"},{"title":"Declarations · core/lfo.d.ts","text":"import type { LFO } from '../voices/schema.js';\n/** Radian phase; sine and triangle start at zero rising, saw at -1, square at +1. */\nexport declare function lfoValue(phase: number, waveform: LFO['waveform']): number;\n","href":"./api.html#declaration-corelfodts"},{"title":"Declarations · core/midi-file.d.ts","text":"import type { BeatSequenceEvent } from './sequence.js';\nimport type { TempoPoint, TimeSignature } from './transport.js';\nimport type { MidiVoiceMap } from './midi-state.js';\nexport declare const MAX_MIDI_FILE_BYTES: number;\nexport declare const MAX_MIDI_FILE_TRACKS = 128;\n/** Counts all wire events, including skipped messages and end-of-track markers. */\nexport declare const MAX_MIDI_FILE_EVENTS = 65536;\nexport type MidiFileWarningCode = 'header-extension' | 'ignored-meta' | 'ignored-sysex' | 'ignored-channel' | 'ignored-program' | 'release-velocity' | 'default-voice' | 'sustain-applied' | 'tempo-conflict' | 'meter-detail' | 'unclosed-notes' | 'release-tail-controls';\nexport interface MidiFileWarning {\n code: MidiFileWarningCode;\n message: string;\n count: number;\n}\nexport type MidiFileControlKind = 'pitch-bend' | 'expression' | 'pan' | 'modulation' | 'channel-pressure' | 'poly-pressure' | 'program-change' | 'pitch-bend-range';\nexport interface MidiFilePreservedControl {\n kind: MidiFileControlKind;\n count: number;\n}\n/** Counts source messages, not the number of per-note events generated by fanout. */\nexport interface MidiFileLossSummary {\n omissions: readonly Readonly\u003cMidiFileWarning>[];\n approximations: readonly Readonly\u003cMidiFileWarning>[];\n preservedControls: readonly Readonly\u003cMidiFilePreservedControl>[];\n}\nexport interface MidiImportOptions {\n /** Zero-based MIDI channels, written as decimal object keys, to OPM voice names. */\n channelVoices?: Readonly\u003cRecord\u003cstring, string>>;\n defaultVoice?: string;\n programVoices?: MidiVoiceMap;\n drumVoices?: MidiVoiceMap;\n /** Unsupported musical/metadata content is reported, or rejects the entire file. */\n unsupported?: 'warn' | 'reject';\n /** Includes keys held by sustain at the final end-of-track tick. */\n unclosedNotes?: 'reject' | 'close-at-end';\n sustain?: 'apply' | 'reject';\n /** Legacy note-only conversion is the default. */\n controls?: 'preserve' | 'omit';\n /** Initial bend range; preserve mode honors RPN 0. Finite semitones in 0..48, default 2. */\n pitchBendRange?: number;\n}\nexport interface MidiImportResult {\n events: BeatSequenceEvent[];\n tempoMap: readonly Readonly\u003cTempoPoint>[];\n timeSignature: Readonly\u003cTimeSignature>;\n warnings: readonly Readonly\u003cMidiFileWarning>[];\n lossSummary: Readonly\u003cMidiFileLossSummary>;\n}\nexport interface MidiExportOptions {\n format?: 0 | 1;\n ppqn?: number;\n tempoMap?: readonly TempoPoint[];\n bpm?: number;\n timeSignature?: TimeSignature;\n /** Named voices to zero-based MIDI channels. Only brass -> channel 0 is implicit. */\n voiceChannels?: Readonly\u003cRecord\u003cstring, number>>;\n controls?: 'preserve' | 'omit';\n /** Must match the import/receiver policy; no RPN is emitted. */\n pitchBendRange?: number;\n}\n/** Strict SMF 0/1 PPQN adapter, not a General MIDI synthesizer or device driver. */\nexport declare function importMidiFile(source: Uint8Array, options?: MidiImportOptions): MidiImportResult;\n/** Deterministic SMF 0/1; expressive conversion is explicit and rejects ambiguous channel ownership. */\nexport declare function exportMidiFile(events: readonly BeatSequenceEvent[], options?: MidiExportOptions): Uint8Array;\n","href":"./api.html#declaration-coremidi-filedts"},{"title":"Declarations · core/midi-state.d.ts","text":"/** Program/drum note numbers 0..127 mapped to named FM voices. */\nexport type MidiVoiceMap = Readonly\u003cRecord\u003cnumber, string>> | ReadonlyMap\u003cnumber, string>;\nexport declare function midiVoiceName(value: unknown): string;\nexport declare function midiVoiceMap(input: unknown): ReadonlyMap\u003cnumber, string>;\nexport interface MidiRpnState {\n msb: number;\n lsb: number;\n semitones: number;\n cents: number;\n range: number;\n}\nexport declare function midiRpnState(range: number): MidiRpnState;\n/** Selection wins over CC mappings; data entry wins only while RPN 0 is selected. */\nexport declare function midiRpnControl(state: MidiRpnState, controller: number, value: number): boolean;\n","href":"./api.html#declaration-coremidi-statedts"},{"title":"Declarations · core/project.d.ts","text":"import type { PreparedVoice } from '../voices/schema.js';\nimport type { BeatSequenceEvent, SequenceEvent, SequenceVoices } from './sequence.js';\nimport type { ArrangementDefinition, ArrangementLayer, ArrangementSection } from './arrangement-definition.js';\nimport type { QualityProfile, SynthOptions } from './synth.js';\nimport type { NormalizedTuning } from './tuning.js';\nimport type { TempoPoint, TimeSignature } from './transport.js';\nexport declare const MAX_SCORE_PROJECT_BYTES: number;\nexport declare const MAX_SCORE_PROJECT_VOICES = 128;\nexport declare const MAX_ARRANGEMENT_PROJECT_BYTES: number;\nexport declare const MAX_ARRANGEMENT_PROJECT_VOICES = 128;\nexport interface BeatSequenceOptions {\n tempoMap?: readonly TempoPoint[];\n bpm?: number;\n voices?: SequenceVoices;\n}\nexport interface ScoreProjectSettings {\n readonly sampleRate: number;\n readonly quality: QualityProfile;\n readonly maxVoices: number;\n readonly mixGain: number;\n readonly tuning: NormalizedTuning;\n readonly stealing: NonNullable\u003cSynthOptions['stealing']>;\n}\n/** Version 1 is self-contained: every note names a patch stored in voices. */\nexport interface ScoreProject {\n readonly version: 1;\n readonly events: readonly BeatSequenceEvent[];\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n readonly voices: Readonly\u003cRecord\u003cstring, PreparedVoice>>;\n readonly settings: ScoreProjectSettings;\n}\n/** Version 1 stores a replayable definition; no live cursor, pending commands or DSP state. */\nexport interface ArrangementProject extends ArrangementDefinition {\n readonly version: 1;\n readonly layers: readonly Readonly\u003cRequired\u003cArrangementLayer>>[];\n readonly sections: readonly Readonly\u003cArrangementSection>[];\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n readonly voices: Readonly\u003cRecord\u003cstring, PreparedVoice>>;\n readonly settings: ScoreProjectSettings;\n}\n/** Compile musical gates by integrating both endpoints; control ramp/glide remain seconds. */\nexport declare function compileBeatSequence(events: readonly BeatSequenceEvent[], options?: BeatSequenceOptions): SequenceEvent[];\n/** Parse strict versioned own-data input, detach all nested data and freeze the snapshot. */\nexport declare function parseScoreProject(source: string | object): ScoreProject;\n/** Canonical compact JSON: stable field order, sorted patch names, normalized defaults. */\nexport declare function serializeScoreProject(project: ScoreProject): string;\n/** Parse a self-contained arrangement using the same definition boundary as createArrangement. */\nexport declare function parseArrangementProject(source: string | object): ArrangementProject;\n/** Canonical compact JSON for a definition, never a live Arrangement object. */\nexport declare function serializeArrangementProject(project: ArrangementProject): string;\n","href":"./api.html#declaration-coreprojectdts"},{"title":"Declarations · core/sequence.d.ts","text":"import type { PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { NoteControls, SynthOptions } from './synth.js';\nimport type { RenderResult } from './index.js';\nexport interface SequenceNoteEvent {\n type: 'note';\n id: number;\n time: number;\n duration: number;\n voice?: string | VoiceInput;\n note: number;\n velocity?: number;\n pan?: number;\n voicePriority?: number;\n}\nexport interface SequenceStopEvent {\n type: 'stop';\n id: number;\n time: number;\n}\nexport interface SequenceControlEvent {\n type: 'control';\n id: number;\n time: number;\n controls: NoteControls;\n}\nexport type SequenceEvent = SequenceNoteEvent | SequenceStopEvent | SequenceControlEvent;\nexport type BeatSequenceEvent = SequenceEvent extends infer E ? E extends SequenceEvent ? Omit\u003cE, 'time'> & {\n beat: number;\n} : never : never;\nexport type SequenceVoices = ReadonlyMap\u003cstring, VoiceInput>;\nexport interface SequenceOptions extends SynthOptions {\n voices?: SequenceVoices;\n sampleRate?: number;\n /** Logical polyphony, integer1..32; default8. Stolen fades remain bounded to eight. */\n maxVoices?: number;\n}\nexport type PreparedSequenceEvent = Readonly\u003cOmit\u003cSequenceNoteEvent, 'voice' | 'velocity' | 'pan' | 'voicePriority'> & {\n voice: PreparedVoice;\n velocity: number;\n pan: number;\n voicePriority: number;\n}> | Readonly\u003cSequenceStopEvent> | Readonly\u003cOmit\u003cSequenceControlEvent, 'controls'> & {\n controls: Readonly\u003cNoteControls>;\n}>;\nexport interface SequenceSnapshot {\n readonly events: readonly PreparedSequenceEvent[];\n readonly noteCount: number;\n /** Notes reserve both onset and automatic release, even if explicitly cancelled. */\n readonly reservedSlots: number;\n readonly endTime: number;\n}\nexport declare const MAX_SEQUENCE_NOTES = 128;\nexport declare const MAX_SEQUENCE_SLOTS = 256;\nexport declare const MAX_SEQUENCE_SECONDS = 60;\nexport declare const MAX_RENDER_SAMPLES = 4000000;\nexport declare const MAX_LONG_SEQUENCE_SECONDS: number;\nexport declare const MAX_LONG_SEQUENCE_EVENTS = 65536;\nexport declare const MAX_SEQUENCE_CHUNK_FRAMES = 65536;\nexport declare function sampleRateValue(value: number): number;\nexport declare function sequenceOwnData(input: unknown, allowed: readonly string[], required: readonly string[], label: string): Record\u003cstring, unknown>;\n/** Validate the entire score without invoking accessors, and detach every patch/control. */\nexport declare function prepareSequence(events: readonly SequenceEvent[], options?: Pick\u003cSequenceOptions, 'voices'>): SequenceSnapshot;\n/** Long scores have a separate input budget; this does not enlarge worklet queues. */\nexport declare function prepareLongSequence(events: readonly SequenceEvent[], options?: Pick\u003cSequenceOptions, 'voices'>): SequenceSnapshot;\nexport type SequenceFrameEvent = {\n frame: number;\n order: number;\n event: PreparedSequenceEvent;\n};\n/** Shared sample-frame ordering, including automatic releases and pending controls. */\nexport declare function sequenceFrameEvents(score: SequenceSnapshot, sampleRate: number, origin?: number): {\n queue: SequenceFrameEvent[];\n length: number;\n};\n/** Peak submissions in any half-open window; includes automatic releases. */\nexport declare function sequenceWindowCapacity(queue: readonly SequenceFrameEvent[], sampleRate: number, horizon: number): {\n peakWindowSlots: number;\n peakWindowNotes: number;\n};\nexport interface ChunkedSequenceOptions extends SequenceOptions {\n /** Integer 1..65536; default4096. Two buffers are reused for the entire render. */\n chunkFrames?: number;\n /** Optional hard cumulative frame budget, checked before allocation/advancement. */\n maxFrames?: number;\n signal?: AbortSignal;\n}\nexport interface SequenceCapacity {\n readonly sampleRate: number;\n readonly frames: number;\n readonly pcmBytes: number;\n readonly chunkFrames: number;\n readonly chunkBytes: number;\n readonly eventCount: number;\n readonly noteCount: number;\n readonly reservedSlots: number;\n readonly fullBufferAllowed: boolean;\n readonly singleBatchAllowed: boolean;\n /** Default0.2s-window preflight eligibility, not guaranteed live worklet admission. */\n readonly streamAllowed: boolean;\n readonly peakWindowSlots: number;\n readonly peakWindowNotes: number;\n readonly limits: Readonly\u003c{\n fullBufferFrames: number;\n batchNotes: number;\n batchSlots: number;\n longEvents: number;\n longSeconds: number;\n chunkFrames: number;\n streamHorizonSeconds: number;\n streamSlots: number;\n streamNotes: number;\n }>;\n}\nexport interface SequenceChunk {\n /** Borrowed arrays, overwritten by the next next(). Copy only if retention is required. */\n readonly left: Float32Array;\n readonly right: Float32Array;\n readonly offset: number;\n readonly frames: number;\n readonly sampleRate: number;\n readonly diagnostics: Readonly\u003c{\n errors: number;\n processedEvents: number;\n renderedFrames: number;\n }>;\n}\nexport interface ChunkedSequenceRender extends IterableIterator\u003cSequenceChunk> {\n readonly capacity: SequenceCapacity;\n readonly diagnostics: Readonly\u003c{\n errors: number;\n processedEvents: number;\n renderedFrames: number;\n }>;\n /** Idempotent. Subsequent next() returns done without advancing the synth. */\n cancel(): void;\n}\n/** Validate and estimate without allocating any PCM or advancing a synth. */\nexport declare function estimateSequenceCapacity(events: readonly SequenceEvent[], options?: ChunkedSequenceOptions): SequenceCapacity;\n/** Fully validate first; each next() advances at most chunkFrames, never allocates full PCM. */\nexport declare function renderSequenceChunks(events: readonly SequenceEvent[], options?: ChunkedSequenceOptions): ChunkedSequenceRender;\n/** Convenience full-buffer rendering retains the original score and allocation budgets. */\nexport declare function renderSequence(events: readonly SequenceEvent[], options?: SequenceOptions): RenderResult;\n","href":"./api.html#declaration-coresequencedts"},{"title":"Declarations · core/synth.d.ts","text":"import type { ADSR, PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { TuningOptions } from './tuning.js';\nimport type { QualityProfile } from './decimator.js';\nexport type { QualityProfile } from './decimator.js';\nexport type VoiceEndReason = 'stolen' | 'ended' | 'error' | 'cancelled';\nexport interface NoteOptions {\n velocity?: number;\n pan?: number;\n voicePriority?: number;\n}\nexport interface NoteControls {\n pitch?: number;\n glide?: number;\n expression?: number;\n pan?: number;\n modulation?: number;\n ramp?: number;\n /** Independent per-note amplitude, 0..1 (default 1), multiplied with expression before mix saturation. */\n gain?: number;\n operatorLevels?: readonly [number, number, number, number];\n feedback?: number;\n lfoRate?: number;\n amDepth?: number;\n pmDepth?: number;\n operatorRatios?: readonly [number, number, number, number];\n /** A number enables fixed Hz; null restores the operator's live ratio. Pitch still applies. */\n operatorFrequencies?: readonly [number | null, number | null, number | null, number | null];\n operatorADSR?: readonly [ADSR, ADSR, ADSR, ADSR];\n}\nexport interface SynthOptions {\n mixGain?: number;\n tuning?: TuningOptions;\n stealing?: 'oldest' | 'release-first' | 'quietest';\n quality?: QualityProfile;\n}\n/** A valid note could not displace any higher-priority logical voice. */\nexport declare class VoiceAdmissionError extends Error {\n constructor();\n}\nexport declare function validateMaxVoices(value: unknown): number;\nexport declare function validateVoicePriority(value: unknown): number;\n/** Copy strict own-data controls at the API/dispatch boundary without invoking getters. */\nexport declare function validateNoteControls(input: NoteControls): NoteControls;\nexport { normalizeVoice, prepareVoice } from '../voices/normalize.js';\nexport declare class Synth {\n readonly sampleRate: number;\n readonly maxVoices: number;\n readonly quality: QualityProfile;\n readonly currentFrame: number;\n readonly errorCount: number;\n readonly lastStolenId: number | null;\n onVoiceEnded?: ((id: number, reason: VoiceEndReason) => void) | null;\n constructor(sampleRate: number, maxVoices?: number, options?: SynthOptions);\n setMixGain(gain: number): void;\n setTuning(tuning: TuningOptions): void;\n noteOn(input: VoiceInput | PreparedVoice, note: number, id?: number, options?: NoteOptions): number;\n noteOff(id: number): boolean;\n allNotesOff(): void;\n panic(): void;\n updateNote(id: number, input: NoteControls): boolean;\n render(left: Float32Array, right: Float32Array, offset?: number, length?: number): void;\n}\n","href":"./api.html#declaration-coresynthdts"},{"title":"Declarations · core/transport.d.ts","text":"export interface TempoPoint {\n beat: number;\n bpm: number;\n curve?: 'step' | 'linear';\n endBpm?: number;\n}\nexport interface TimeSignature {\n numerator: number;\n denominator: number;\n}\n/** One-based bars and beats; beat may contain a fractional subdivision. */\nexport interface BarBeat {\n bar: number;\n beat: number;\n}\nexport declare const MAX_TRANSPORT_BEATS = 86400;\nexport declare const MAX_TEMPO_POINTS = 1024;\nexport declare function transportNumber(value: unknown, min: number, max: number, label: string): number;\n/** Read dense own-data arrays without executing element getters or iteration overrides. */\nexport declare function transportArray(input: unknown, max: number, label: string): unknown[];\n/** Strictly increasing quarter-note tempo; linear curves interpolate BPM in beat space. */\nexport declare function normalizeTempoMap(input?: readonly TempoPoint[], bpm?: number): readonly Readonly\u003cTempoPoint>[];\nexport declare function normalizeTimeSignature(input?: TimeSignature): Readonly\u003cTimeSignature>;\n/** BPM at a beat in an already validated map. */\nexport declare function tempoBPM(beat: number, map: readonly Readonly\u003cTempoPoint>[]): number;\n/** Insert a step without changing the elapsed portion of a linear ramp. */\nexport declare function replaceTempoFrom(beat: number, bpm: number, map: readonly Readonly\u003cTempoPoint>[]): readonly Readonly\u003cTempoPoint>[];\n/** Integral of 60/BPM across tempo boundaries. */\nexport declare function beatsToSeconds(beat: number, tempoMap?: readonly TempoPoint[]): number;\n/** Internal validated-map form avoids recopying a map on each scheduler tick. */\nexport declare function tempoSeconds(beat: number, map: readonly Readonly\u003cTempoPoint>[]): number;\nexport declare function secondsToBeats(seconds: number, tempoMap?: readonly TempoPoint[]): number;\nexport declare function tempoBeat(seconds: number, map: readonly Readonly\u003cTempoPoint>[]): number;\nexport declare function beatToBarBeat(beat: number, signature?: TimeSignature): Readonly\u003cBarBeat>;\nexport declare function barBeatToBeat(position: BarBeat, signature?: TimeSignature): number;\nexport type BeatQuantization = 'floor' | 'ceil' | 'nearest' | 'next';\n/** Grid in quarter-note beats; next is strictly later even at an exact boundary. */\nexport declare function quantizeBeat(beat: number, quantum?: number, mode?: BeatQuantization): number;\n/** Warp each pair of subdivisions; ratio .5 is straight, 2/3 is triplet swing. */\nexport declare function swingBeat(beat: number, subdivision?: number, ratio?: number): number;\n","href":"./api.html#declaration-coretransportdts"},{"title":"Declarations · core/tuning.d.ts","text":"export interface TuningOptions {\n referenceHz?: number;\n offsets?: readonly number[];\n}\nexport interface NormalizedTuning {\n readonly referenceHz: number;\n readonly offsets: readonly number[];\n}\n/** Validate own data only and detach the cents table from its caller. */\nexport declare function normalizeTuning(input: TuningOptions): NormalizedTuning;\n/** Fractional MIDI notes interpolate cents, not frequencies, between adjacent keys. */\nexport declare function tuningFrequency(note: number, tuning: NormalizedTuning): number;\n","href":"./api.html#declaration-coretuningdts"},{"title":"Declarations · core/wav.d.ts","text":"export type WavFormat = 'pcm16' | 'pcm24' | 'float32';\nexport interface WavOptions {\n left: Float32Array;\n right?: Float32Array;\n sampleRate: number;\n format?: WavFormat;\n}\nexport interface WavEncoderOptions {\n sampleRate: number;\n channels: 1 | 2;\n format?: WavFormat;\n totalFrames: number;\n}\nexport interface WavChunk {\n left: Float32Array;\n right?: Float32Array;\n}\nexport interface WavEncoder {\n readonly totalFrames: number;\n readonly framesEncoded: number;\n /** Entire file length, including the header and any RIFF alignment byte. */\n readonly byteLength: number;\n readonly finished: boolean;\n /** Emit exactly once, before encoding PCM. */\n header(): Uint8Array;\n /** Atomic accounting: invalid chunks never consume frames. Maximum65536 frames per call. */\n encode(chunk: WavChunk): Uint8Array;\n /** Require exact totalFrames; returns the RIFF alignment byte, or an empty array. */\n finalize(): Uint8Array;\n}\n/** Streaming RIFF32 output with known length; retains no PCM or emitted bytes. */\nexport declare function createWavEncoder(options: WavEncoderOptions): WavEncoder;\n/** Full-buffer WAV convenience export; defaultPCM16, at most4000000 frames. */\nexport declare function encodeWav(input: WavOptions): Uint8Array;\n","href":"./api.html#declaration-corewavdts"},{"title":"Declarations · tools/assets.d.ts","text":"export interface CopyAssetsOptions {\n destination: string;\n /** Advanced: deploy this installed opm.js package directory instead of the one containing this module. */\n packageRoot?: string;\n}\nexport interface AssetDeployment {\n readonly version: string;\n readonly destination: string;\n readonly files: number;\n readonly bytes: number;\n readonly reused: boolean;\n}\nexport interface CheckDeploymentOptions {\n /** Overall deadline, integer milliseconds in 1..60000; default 15000. */\n timeoutMs?: number;\n /** Require production X-Content-Type-Options: nosniff; default true. */\n requireNosniff?: boolean;\n /** Advanced: compare with this installed opm.js package directory instead of the one containing this module. */\n packageRoot?: string;\n}\nexport interface DeploymentCheck {\n readonly version: string;\n readonly baseUrl: string;\n readonly files: number;\n readonly bytes: number;\n readonly limitations: readonly string[];\n}\n/** Node-only. Atomically create a complete release-specific directory; never overwrite host files. */\nexport declare function copyAssets(options: CopyAssetsOptions): Promise\u003cAssetDeployment>;\n/** Fetch and hash the complete installed release; this does not certify page CSP or audible playback. */\nexport declare function checkDeployment(baseUrl: string | URL, options?: CheckDeploymentOptions): Promise\u003cDeploymentCheck>;\n","href":"./api.html#declaration-toolsassetsdts"},{"title":"Declarations · version.d.ts","text":"/** Package version; scripts/security-check.ts fails if it differs from package.json. */\nexport declare const VERSION = \"1.11.1\";\n","href":"./api.html#declaration-versiondts"},{"title":"Declarations · voices/brass.d.ts","text":"import type { Voice } from './schema.js';\nexport declare const brass: Voice;\n","href":"./api.html#declaration-voicesbrassdts"},{"title":"Declarations · voices/dx7.d.ts","text":"import type { Algorithm, Voice } from './schema.js';\nexport interface DX7ImportDescription {\n name: string;\n /** Original DX7 algorithm, 1..32. */\n sourceAlgorithm: number;\n algorithm: Algorithm;\n /** DX7 operator numbers 1..6 in converted OPM signal order. */\n selectedOperators: number[];\n droppedOperators: number[];\n warnings: string[];\n}\n/** Import exactly one standard DX7 single/bank dump as approximate four-op voices. */\nexport declare function importDX7(input: Uint8Array): Voice[];\n/** Report losses separately so imported voices retain the strict normal voice shape. */\nexport declare function describeDX7(input: Uint8Array): DX7ImportDescription[];\n","href":"./api.html#declaration-voicesdx7dts"},{"title":"Declarations · voices/examples.d.ts","text":"import type { Voice } from './schema.js';\nexport declare const examples: readonly Voice[];\n","href":"./api.html#declaration-voicesexamplesdts"},{"title":"Declarations · voices/gm-map.d.ts","text":"/** Artistic GM-family starter mappings, not a General MIDI sound set. */\nexport declare const gmProgramVoices: Readonly\u003cRecord\u003cnumber, string>>;\n/** Partial GM percussion starter; unmapped notes retain the caller fallback. */\nexport declare const gmDrumVoices: Readonly\u003cRecord\u003cnumber, string>>;\n","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"Declarations · voices/normalize.d.ts","text":"import type { NormalizedVoice, PreparedVoice, VoiceInput } from './schema.js';\nexport declare function normalizeVoice(source: VoiceInput): NormalizedVoice;\n/** Validate once and retain a deeply immutable, caller-independent snapshot. */\nexport declare function prepareVoice(input: VoiceInput): PreparedVoice;\n","href":"./api.html#declaration-voicesnormalizedts"},{"title":"Declarations · voices/opm.d.ts","text":"import type { Algorithm, Voice } from './schema.js';\nexport interface OPMImportDescription {\n name: string;\n program: number;\n algorithm: Algorithm;\n /** Source labels in destination operator order. */\n operators: readonly string[];\n warnings: string[];\n}\n/** Import bounded decimal VOPM/MXDRV-family text patches as approximate v7 voices. */\nexport declare function importOPM(source: string | Uint8Array): Voice[];\n/** Report source program metadata and bounded, de-duplicated conversion losses. */\nexport declare function describeOPM(source: string | Uint8Array): OPMImportDescription[];\n","href":"./api.html#declaration-voicesopmdts"},{"title":"Declarations · voices/original.d.ts","text":"import type { Voice } from './schema.js';\nexport declare const originalPresets: readonly Voice[];\n","href":"./api.html#declaration-voicesoriginaldts"},{"title":"Declarations · voices/preset-metadata.d.ts","text":"export interface PresetMetadata {\n readonly family: 'bell' | 'brass' | 'bass' | 'keys' | 'organ' | 'lead' | 'strings' | 'mallet' | 'pluck' | 'reed' | 'pad' | 'metallic' | 'percussion' | 'inharmonic';\n readonly provenance: {\n readonly kind: 'repository-recipe' | 'original-recipe';\n readonly source: string;\n readonly license: 'Apache-2.0';\n readonly copiedEmulatorPatch: false;\n };\n readonly intendedMidi: readonly [number, number];\n readonly intendedVelocity: readonly [number, number];\n /** Extra attenuation before the common audition gain; never an operator-level compensation. */\n readonly hostTrimDb: number;\n readonly purpose: string;\n readonly suggestedPolyphony: number;\n readonly designRationale: string;\n /** Authoring intent and numerical checks are not a listener verdict. */\n readonly listeningStatus: 'unverified';\n}\nexport declare const presetMetadata: Readonly\u003cRecord\u003cstring, PresetMetadata>>;\n/** The separate default brass export is not the bank brass recipe. */\nexport declare const defaultBrassMetadata: PresetMetadata;\n","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"Declarations · voices/schema.d.ts","text":"export type Algorithm = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\nexport interface ADSR {\n a: number;\n d: number;\n s: number;\n r: number;\n}\nexport type LFOTargets = readonly [number, number, number, number];\nexport type LFOTargetsInput = readonly [number | boolean, number | boolean, number | boolean, number | boolean];\nexport interface LegacyLFO {\n rate: number;\n amDepth: number;\n pmDepth: number;\n waveform?: never;\n delay?: never;\n sync?: never;\n phase?: never;\n amTargets?: never;\n pmTargets?: never;\n}\nexport interface LegacyLFOV4 extends Omit\u003cLegacyLFO, 'waveform'> {\n waveform: 'sine' | 'triangle' | 'saw' | 'square';\n}\nexport interface LegacyLFOV5 extends Omit\u003cLegacyLFOV4, 'delay' | 'sync' | 'phase'> {\n delay?: number;\n sync?: 'note' | 'global';\n phase?: number;\n}\nexport interface LFO extends Omit\u003cLegacyLFOV5, 'amTargets' | 'pmTargets'> {\n amTargets?: LFOTargets;\n pmTargets?: LFOTargets;\n}\nexport type LFOInput = Omit\u003cLFO, 'waveform' | 'amTargets' | 'pmTargets'> & {\n waveform?: LFO['waveform'];\n amTargets?: LFOTargetsInput;\n pmTargets?: LFOTargetsInput;\n};\nexport interface PitchEnvelope {\n a: number;\n d: number;\n r: number;\n initial: number;\n peak: number;\n sustain: number;\n final: number;\n}\nexport interface KeyScale {\n breakpoint: number;\n leftDbPerOctave: number;\n rightDbPerOctave: number;\n}\nexport type OperatorWaveform = 'sine' | 'half' | 'abs' | 'quarter' | 'alternating' | 'camel' | 'square' | 'saw' | 'noise';\nexport interface LegacyOperator {\n ratio: number;\n level: number;\n detune: number;\n adsr: ADSR;\n keyScale?: never;\n velocitySensitivity?: never;\n frequency?: never;\n rateKeyScale?: never;\n waveform?: never;\n noiseRate?: never;\n}\nexport interface LegacyOperatorV2 extends Omit\u003cLegacyOperator, 'keyScale'> {\n keyScale?: KeyScale;\n}\nexport interface LegacyOperatorV3 extends Omit\u003cLegacyOperatorV2, 'velocitySensitivity'> {\n velocitySensitivity?: number;\n}\nexport interface LegacyOperatorV6 extends Omit\u003cLegacyOperatorV3, 'frequency' | 'rateKeyScale'> {\n frequency?: number;\n rateKeyScale?: number;\n}\nexport interface Operator extends Omit\u003cLegacyOperatorV6, 'waveform' | 'noiseRate'> {\n waveform?: OperatorWaveform;\n noiseRate?: number;\n}\nexport type FourOperators\u003cT = Operator> = [T, T, T, T];\ninterface VoiceBase {\n name?: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex?: number;\n}\n/** Strict single-voice input; omitted version uses the current shape. */\nexport type VoiceInput = (VoiceBase & {\n version?: 7;\n lfo?: LFOInput;\n pitchEnvelope?: PitchEnvelope;\n ops: readonly [Operator, Operator, Operator, Operator];\n}) | (VoiceBase & {\n version: 6;\n lfo?: LFOInput;\n pitchEnvelope?: PitchEnvelope;\n ops: readonly [LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6];\n}) | (VoiceBase & {\n version: 5;\n lfo?: Omit\u003cLegacyLFOV5, 'waveform'> & {\n waveform?: LFO['waveform'];\n };\n pitchEnvelope?: PitchEnvelope;\n ops: readonly [LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6];\n}) | (VoiceBase & {\n version: 4;\n lfo?: Omit\u003cLegacyLFOV4, 'waveform'> & {\n waveform?: LFO['waveform'];\n };\n pitchEnvelope?: never;\n ops: readonly [LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3];\n}) | (VoiceBase & {\n version: 3;\n lfo?: LegacyLFO;\n pitchEnvelope?: never;\n ops: readonly [LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3];\n}) | (VoiceBase & {\n version: 2;\n lfo?: LegacyLFO;\n pitchEnvelope?: never;\n ops: readonly [LegacyOperatorV2, LegacyOperatorV2, LegacyOperatorV2, LegacyOperatorV2];\n}) | (VoiceBase & {\n version: 1;\n lfo?: LegacyLFO;\n pitchEnvelope?: never;\n ops: readonly [LegacyOperator, LegacyOperator, LegacyOperator, LegacyOperator];\n});\nexport interface Voice {\n version: 7;\n name: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex: number;\n lfo: LFO;\n pitchEnvelope?: PitchEnvelope;\n ops: FourOperators;\n}\nexport interface LegacyVoice {\n version: 1;\n name: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex: number;\n lfo: LegacyLFO;\n pitchEnvelope?: never;\n ops: FourOperators\u003cLegacyOperator>;\n}\nexport interface LegacyVoiceV2 extends Omit\u003cLegacyVoice, 'version' | 'ops'> {\n version: 2;\n ops: FourOperators\u003cLegacyOperatorV2>;\n}\nexport interface LegacyVoiceV3 extends Omit\u003cLegacyVoice, 'version' | 'ops'> {\n version: 3;\n ops: FourOperators\u003cLegacyOperatorV3>;\n}\nexport interface LegacyVoiceV4 extends Omit\u003cLegacyVoiceV3, 'version' | 'lfo'> {\n version: 4;\n lfo: LegacyLFOV4;\n}\nexport interface LegacyVoiceV6 extends Omit\u003cVoice, 'version' | 'ops'> {\n version: 6;\n ops: FourOperators\u003cLegacyOperatorV6>;\n}\nexport interface LegacyVoiceV5 extends Omit\u003cLegacyVoiceV6, 'version' | 'lfo'> {\n version: 5;\n lfo: LegacyLFOV5;\n}\nexport interface NormalizedVoice {\n version: 7;\n name?: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex: number;\n lfo: LFO;\n pitchEnvelope?: PitchEnvelope;\n ops: FourOperators;\n}\nexport type CompleteVoiceInput = Voice | LegacyVoice | LegacyVoiceV2 | LegacyVoiceV3 | LegacyVoiceV4 | LegacyVoiceV5 | LegacyVoiceV6;\nexport type FrozenVoice = Readonly\u003cOmit\u003cVoice, 'lfo' | 'ops' | 'pitchEnvelope'>> & {\n readonly lfo: Readonly\u003cLFO>;\n readonly pitchEnvelope?: Readonly\u003cPitchEnvelope>;\n readonly ops: readonly [FrozenOperator, FrozenOperator, FrozenOperator, FrozenOperator];\n};\nexport type FrozenOperator = Readonly\u003cOmit\u003cOperator, 'adsr' | 'keyScale'>> & {\n readonly adsr: Readonly\u003cADSR>;\n readonly keyScale?: Readonly\u003cKeyScale>;\n};\ndeclare const preparedVoiceBrand: unique symbol;\n/** Immutable validated snapshot. Only prepareVoice can create the trusted identity. */\nexport type PreparedVoice = Readonly\u003cOmit\u003cNormalizedVoice, 'lfo' | 'ops' | 'pitchEnvelope'>> & {\n readonly lfo: Readonly\u003cLFO>;\n readonly pitchEnvelope?: Readonly\u003cPitchEnvelope>;\n readonly ops: readonly [FrozenOperator, FrozenOperator, FrozenOperator, FrozenOperator];\n readonly [preparedVoiceBrand]: true;\n};\nexport { prepareVoice } from './normalize.js';\nexport declare const MAX_BANK_BYTES = 262144;\nexport declare const MAX_BANK_VOICES = 128;\ntype LimitKey = 'noiseRate' | 'ratio' | 'level' | 'detune' | 'velocitySensitivity' | 'frequency' | 'rateKeyScale' | 'a' | 'd' | 's' | 'r' | 'modIndex' | 'rate' | 'amDepth' | 'pmDepth' | 'delay' | 'phase' | 'initial' | 'peak' | 'sustain' | 'final' | 'breakpoint' | 'leftDbPerOctave' | 'rightDbPerOctave';\nexport declare const LIMITS: Readonly\u003cRecord\u003cLimitKey, readonly [number, number]>>;\nexport declare function bounded(value: unknown, min: number, max: number, label?: string): number;\nexport declare function lfoWaveform(value: unknown): LFO['waveform'];\nexport declare function lfoSync(value: unknown): NonNullable\u003cLFO['sync']>;\nexport declare function operatorWaveform(value: unknown): OperatorWaveform;\nexport declare function validateVoice(input: unknown): FrozenVoice;\nexport declare function parseVoiceBank(source: string | readonly unknown[]): Map\u003cstring, FrozenVoice>;\n","href":"./api.html#declaration-voicesschemadts"},{"title":"Declarations · voices/voice.schema.d.ts","text":"export declare const voiceSchema: {\n $schema: string;\n title: string;\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n version: {\n const: number;\n };\n name: {\n type: string;\n pattern: string;\n };\n algorithm: {\n type: string;\n minimum: number;\n maximum: number;\n };\n feedback: {\n type: string;\n minimum: number;\n maximum: number;\n };\n modIndex: {\n type: string;\n minimum: number;\n maximum: number;\n };\n pitchEnvelope: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n a: {\n type: string;\n minimum: number;\n maximum: number;\n };\n d: {\n type: string;\n minimum: number;\n maximum: number;\n };\n r: {\n type: string;\n minimum: number;\n maximum: number;\n };\n initial: {\n type: string;\n minimum: number;\n maximum: number;\n };\n peak: {\n type: string;\n minimum: number;\n maximum: number;\n };\n sustain: {\n type: string;\n minimum: number;\n maximum: number;\n };\n final: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n lfo: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n delay: {\n type: string;\n minimum: number;\n maximum: number;\n default: number;\n };\n sync: {\n type: string;\n enum: string[];\n default: string;\n };\n phase: {\n type: string;\n minimum: number;\n maximum: number;\n default: number;\n };\n amTargets: {\n type: string;\n minItems: number;\n maxItems: number;\n items: {\n type: string;\n minimum: number;\n maximum: number;\n };\n description: string;\n };\n pmTargets: {\n type: string;\n minItems: number;\n maxItems: number;\n items: {\n type: string;\n minimum: number;\n maximum: number;\n };\n description: string;\n };\n waveform: {\n type: string;\n enum: string[];\n };\n rate: {\n type: string;\n minimum: number;\n maximum: number;\n };\n amDepth: {\n type: string;\n minimum: number;\n maximum: number;\n };\n pmDepth: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n ops: {\n type: string;\n minItems: number;\n maxItems: number;\n items: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n waveform: {\n type: string;\n enum: string[];\n default: string;\n };\n noiseRate: {\n type: string;\n minimum: number;\n maximum: number;\n default: number;\n description: string;\n };\n frequency: {\n type: string;\n minimum: number;\n maximum: number;\n description: string;\n };\n rateKeyScale: {\n type: string;\n minimum: number;\n maximum: number;\n description: string;\n };\n velocitySensitivity: {\n type: string;\n minimum: number;\n maximum: number;\n description: string;\n };\n keyScale: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n breakpoint: {\n type: string;\n minimum: number;\n maximum: number;\n };\n leftDbPerOctave: {\n type: string;\n minimum: number;\n maximum: number;\n };\n rightDbPerOctave: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n ratio: {\n type: string;\n minimum: number;\n maximum: number;\n };\n level: {\n type: string;\n minimum: number;\n maximum: number;\n };\n detune: {\n type: string;\n minimum: number;\n maximum: number;\n };\n adsr: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n a: {\n type: string;\n minimum: number;\n maximum: number;\n };\n d: {\n type: string;\n minimum: number;\n maximum: number;\n };\n s: {\n type: string;\n minimum: number;\n maximum: number;\n };\n r: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n };\n };\n };\n };\n};\n","href":"./api.html#declaration-voicesvoiceschemadts"}]; +window.OPM_DOC_SEARCH = [{"title":"OPM.js","text":"OPM.js # OPM.js\n\nA 4-operator FM synthesis engine for the browser, inspired by the Yamaha YM2151 (OPM) sound chip.\n\nOPM.js recreates the classic 16-bit era FM sound — four-operator synthesis with a default eight-voice budget (configurable up to 32), multiple algorithms, feedback, and ADSR envelopes — as a lightweight, zero-runtime-dependency TypeScript engine powered by the Web Audio API, distributed as JavaScript ES modules.\n\n> Status: v1.11.1 (package 1.11.1) is the latest GitHub release, a performance patch over v1.11 (faster sine operators; no API or format changes). v1.11 added: voice format v7 with per-operator waveforms and held noise, optional stereo chorus/reverb, MIDI program-to-voice maps with RPN bend sensitivity, and approximate .opm patch import. Voice v1–v6 inputs remain accepted; runtime dependencies remain empty. The previous v1.10 release (package 1.10.0) uses voice format v6.\n>\n> GitHub release distribution and npm registry publication are separate. Use the release tarball or a source checkout; no npm publication is implied. See the compatibility policy.\n\nUsage guides: English · 繁體中文.\n\nOnline demos (HTTPS): opm.js-package.xyz. Click a playback button to grant audio startup; physical-device and listening acceptance remain separate from a working web page.\n\n## New in v1.11\n\nThese APIs are new in the v1.11 release and are not part of the v1.10 archive. Physical-device, MIDI-hardware and human-listening acceptance remain unverified.\n\n- Voice format v7: per-operator waveform (sine, half, abs, quarter, alternating, camel, square, saw, noise) and noiseRate; see Voice format and the expressive voice reference.\n- Three original noise-based percussion recipes (noise-snare, noise-hihat, explosion) in the bundled bank.\n- Optional stereo chorus and reverb, usable offline (createStereoEffects, applyEffects) and live (createEffects): audio buses.\n- MIDI programVoices/drumVoices maps, GM-family starter maps and RPN 0 bend sensitivity for files and the live adapter: MIDI files, MIDI performance.\n- importOPM/describeOPM for approximate conversion of VOPM/MXDRV-family .opm text patches: expressive voices.\n\n## New in v1.10\n\nThese APIs and examples are included in v1.10. Historical release archives remain unchanged. GitHub distribution does not imply npm publication or deployment. Source contributors run npm ci and npm run build; consumers can install the release tarball without a build toolchain.\n\n- Inert, bounded version-1 Arrangement parse/save/load through root/core APIs; authored definitions replay from beat 0, not DSP checkpoints: portable Arrangement projects.\n- Opt-in expressive Standard MIDI files with lossSummary; default controls: 'omit' keeps note-only behavior: MIDI files.\n- Workload-scoped measured capacity candidates, not universal device limits: acoustic quality.\n- Bounded local MIDI capture and physical-device/listening campaign readiness: MIDI performance. Physical/mobile/MIDI and human-listening acceptance remain unverified until genuine observations are supplied.\n- A cross-feature contract table connects replay, rendering, performance and host boundaries. Existing thirteen demos include two original songs, Play/Stop, score-project save/load and WAV export.\n- Static HTML documentation and a complete generated API reference are rebuilt after distribution emit and included under doc/ in locally built packages. Markdown remains the source of truth. Open doc/index.html locally or serve the whole doc/ directory; navigation and local search require no server or runtime dependencies.\n\nChoose a workflow: try the demos · install into an npm project · deploy in a browser · render in Node.js · API reference · troubleshooting.\n\n## About\n\nThe Yamaha YM2151 (OPM) powered a generation of arcade boards and the Sharp X68000, defining the sound of the mid-1980s with its 8-channel, 4-operator FM architecture. OPM.js brings that architecture to the browser with a TypeScript implementation and directly deployable JavaScript.\n\nOPM.js is a musically-accurate reimplementation, not a cycle-accurate hardware clone: envelope timing and modulation curves are tuned to sound correct rather than to reproduce silicon behaviour bit-for-bit.\n\n## Features\n\n- 4-operator FM synthesis with 8 connection algorithms and hardware-style feedback\n- Per-operator dB-domain ADSR, level/rate key scaling, velocity sensitivity and optional fixed-Hz oscillators\n- Per-voice pitch envelope and LFO with four AM / PM waveforms, delay, phase, note/global synchronization and per-operator depth targets\n- Configurable 1–32-voice polyphony (default 8; larger budgets are opt-in) with oldest, release-first or quietest stealing, per-note voice priority that protects melodies, and at most eight bounded ~5 ms stealing fades\n- Smooth live timbre controls — independent pitch, expression/pan/modulation/level, feedback, ratio/fixed-Hz and LFO ramps; editable ADSR anchored at current dB\n- Audio-clock scores and musical Transport — pause/resume, seek, loop, BPM/tempo maps with linear tempo ramps, beat/bar conversion, quantization and swing helpers\n- Adaptive music — looping layers and sections that switch on beat/bar boundaries while shared layers keep sounding\n- Part-scoped performance policies — sustain, poly/mono legato, last/high/low priority, per-key and per-part live controls, voice limits, and an optional user-granted Web MIDI adapter\n- Prepared immutable patches and pooled DSP state — reuse bounded voice slots instead of allocating typed state on every admission\n- Incremental PCM16/PCM24/Float32 WAV export with static Worker rendering, progress, cancellation and sink backpressure; approximate six-to-four-operator DX7 import\n- Pre-saturation mix gain and global tuning — reference A4 and interpolated 128-note cents offsets\n- DSP quality profiles — eco 2×, default standard 4× and high 8× oversampling with linear-phase half-band FIR decimation; bounded state and explicit CPU/aliasing tradeoffs\n- Atomic voice-bank replacement, removal and JSON export preserving queued/sounding patch snapshots\n- TypeScript declarations for browser, core, and voice-module exports\n- AudioWorklet-based DSP — synthesis runs off the main thread\n- Optional stereo chorus and reverb — the same pure DSP offline and in a separate opt-in AudioWorklet; no built-in mixer\n- Zero runtime dependencies, built on the Web Audio API\n- Works in the browser and Node.js (offline rendering)\n\n## Requirements\n\n- Modern browser with ES modules and AudioWorklet (served over HTTPS or localhost)\n- Node.js 22+ for offline rendering and tests\n\n## Getting started\n\n### Try the checkout\n\nObtain the v1.10 source archive or checkout to run the thirteen examples. HTML pages, Vite tooling and development programs are checkout-only, not included in the installable .tgz. Links to these resources below open their repository files; serve the corresponding local HTML to run them. Repository commands run in the directory containing package.json, src/, and scripts/ (called OPM.js in the examples).\n\nThe committed dist/ is ready to use; trying the demos needs no npm installation or build. With Python 3 available, run from that repository root:\n\nsh\npython3 -m http.server 8000\n\n\nOpen http://localhost:8000/index.html for the English example catalog:\n\n| Example | Demonstrates |\n| --- | --- |\n| Basic notes and voices | Presets, pitch, velocity, timed and held notes |\n| Melody and chord scheduling | Twinkle, Twinkle, Little Star, absolute timing, bounded lookahead and cancellation |\n| Live expression and per-voice LFO | Bend/glide a held note; change expression, pan and modulation; scheduled release |\n| Shared AudioContext and routing | Host GainNode, manual routing, diagnostics and borrowed-context ownership |\n| Offline synthesis and WAV | Stereo PCM, frame counts and PCM16 WAV without an AudioContext |\n| Advanced playground | Key scaling, suspend/resume, diagnostics and approximate DX7 import |\n| Preset and DX7 audition | Register/velocity grid, A/B comparison, raw peak/RMS reports and rendered WAV |\n| Scores, Transport and performance | Pause/seek/loop/tempo, sustain/mono priority, live controls, quality/bank tools, multi-format Worker export and physical-device observations |\n| Adaptive game music | Boundary-quantized section/layer switching, tempo ramp, swing, voice priority |\n| Playable instrument and Web MIDI | Mono lead + limited poly pad, per-part/per-key controls, optional user-granted MIDI |\n| Independent buses and stems | Three OPM instances on one AudioContext, separate gain/filter/echo, offline stems |\n| FM sound designer | Operators, algorithm graph, dB envelopes, selective LFO, live edits, patch JSON |\n\nClick a playback button to start audio. Stop the server with Ctrl+C when finished. Do not open the HTML through file://. Pages and runtime messages are English; TypeScript helpers live in demo/.\n\nIf dist/ is missing or you have changed src/ or demo/ TypeScript, regenerate it using the development commands first.\n\n### Install into an npm project\n\nDownload opm.js-1.10.0.tgz from the v1.10 release, then install it from your application's root:\n\nsh\nnpm install /actual/path/to/opm.js-1.10.0.tgz\n\n\nNo checkout or consumer build is required. The registry package name does not guarantee npm publication; do not substitute npm install opm.js or an assumed CDN URL. The v1.10 asset includes these synchronized guides; the original v1.8 tag and archive remain unchanged.\n\nTo build a local tarball instead, run from the OPM.js source checkout with Node.js 22+ and npm:\n\nsh\nnpm ci\nnpm pack\n\n\nnpm pack builds automatically and produces opm.js-1.10.0.tgz. To create a new application beside the checkout:\n\nsh\ncd ..\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install ../OPM.js/opm.js-1.10.0.tgz\n\n\nFor an existing application, run only the install command from its root, adjusting the tarball path. Consumers do not install the engine's development dependencies or need a build step. The installed package contains minified JS, .js.map source maps with embedded TypeScript, generated .d.ts declarations, demo scripts, usage documentation, and legal files—not HTML demo pages, separate source files, or build scripts.\n\nFor a bundled host, use the checkout-only installed-package Vite example: it preserves the complete engine/worklet/Worker tree and license, supports a non-root deployment base, and checks production CSP, MIME types and missing assets. Maintainers can follow the package verification and publication prerequisites; this checkout has no npm publishing workflow, and authentication/provenance must be established separately.\n\n### Use in a browser\n\nFrom the installed application's root, use the package's deployment CLI with a fresh destination:\n\nsh\nnpx --no-install opm-assets copy public/opm\n\n\nThe CLI copies the complete dist/ tree plus LICENSE, hashes the assets, and never overwrites a differing destination. Repeating an identical deployment reuses it. POSIX mkdir -p public/opm, cp -R nodemodules/opm.js/dist/. public/opm/ and cp nodemodules/opm.js/LICENSE public/opm/LICENSE remain an equivalent manual layout, but without the CLI's verification and overwrite protection.\n\nWithout npm, copy the checkout's complete dist/ contents and its LICENSE into your site's opm/ directory instead. The commands use a POSIX shell; copying those same files manually is equivalent.\n\nKeep this layout; individual worklets and their imported modules must not be moved independently:\n\ntext\npublic/\n index.html\n opm/\n api/\n core/\n voices/\n worklet/\n worker/\n tools/\n LICENSE\n\n\nSave this complete example as public/index.html:\n\nhtml\n\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>OPM.js example\u003c/title>\n\u003cbutton id=\"play\" type=\"button\">Play brass chord\u003c/button>\n\u003coutput id=\"status\" aria-live=\"polite\">Ready\u003c/output>\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });\n status.textContent = 'Playing brass chord';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\u003c/html>\n\n\nFrom the application root, serve that public directory:\n\nsh\npython3 -m http.server 8000 --directory public\n\n\nOpen http://localhost:8000/ and click Play brass chord. Production hosting must use HTTPS. The user gesture and awaited start() are required for browser audio playback.\n\nBare names such as import { OPM } from 'opm.js' do not resolve in plain browser HTML without an import map or a bundler. Even with a bundler, do not assume it copies the AudioWorklet and its dependencies: the static-copy layout above is the supported baseline. Import the served URL and deploy the complete directory together.\n\n### Render in Node.js\n\nIn the npm application's root, save this as render.mjs:\n\njs\nimport { Synth } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst sampleRate = 44100;\nconst synth = new Synth(sampleRate);\nconst id = synth.noteOn(brass, 60);\nconst left = new Float32Array(sampleRate);\nconst right = new Float32Array(sampleRate);\nsynth.render(left, right, 0, 22050);\nsynth.noteOff(id);\nsynth.render(left, right, 22050, 22050);\nconsole.log(left.some(sample => sample !== 0), synth.errorCount);\n\n\nRun node render.mjs; it prints true 0. The two buffers contain one second of PCM, including release after the first half-second. This does not play through Node's speakers or write a WAV file. Use .mjs or a project with \"type\": \"module\"; no CommonJS entry is provided.\n\nTo run the same script from a checkout instead, change the imports to ./dist/core/index.js and ./dist/voices/brass.js.\n\n## API reference\n\nThe following reference describes v1.10. Historical release archives remain unchanged. The compiler-generated reference includes every public root/core/voice/tool export and its full declarations; it is refreshed by npm run build. See the cross-feature contract table for IDs, lifecycle observations, ownership and persistence.\n\nAll times are in seconds unless stated otherwise. Browser and Node examples intentionally use different import styles:\n\n| Exports | Installed npm import | Browser URL in the layout above |\n| --- | --- | --- |\n| OPM, createLookaheadScheduler, playSequence, streamSequence, createTransport, createArrangement, createPerformance, renderSequenceInWorker | opm.js | ./opm/api/index.js |\n| Synth, renderNote, core helpers | opm.js/core | ./opm/core/index.js |\n| Score/Arrangement project parsers, serializers and compileBeatSequence | opm.js or opm.js/core | ./opm/core/index.js |\n| importMidiFile, exportMidiFile | opm.js/midi-file, root or core | ./opm/core/midi-file.js |\n| brass | opm.js/voices/brass.js | ./opm/voices/brass.js |\n| parseVoiceBank, validateVoice, schema helpers | opm.js/voices/schema.js | ./opm/voices/schema.js |\n| importDX7, describeDX7 | opm.js/voices/dx7.js | ./opm/voices/dx7.js |\n| importOPM, describeOPM | opm.js/voices/opm.js | ./opm/voices/opm.js |\n\n### Browser: OPM\n\n| Call | Contract |\n| --- | --- |\n| new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain = 1, tuning = {}, stealing = 'oldest', interruption = 'cancel', quality = 'standard', maxVoices = 8 } = {}) | Optional integer sample rate 8000–96000 Hz; voice limit 1–32 (default 8; each voice costs CPU). Quality and polyphony are immutable and survive node recreation. Borrow an existing AudioContext; OPM never closes or suspends it. Omit destination to use its destination, or use null for explicit routing. Optional workletUrl relocates the complete secure same-origin worklet module tree without relaxing CSP/MIME checks. |\n| await opm.start() / await opm.resume() | Initialize/resume the context and worklet from a user gesture; concurrent starts coalesce. Existing contexts are resumed after browser suspension. Await before playing. |\n| opm.connect(destination) / opm.disconnect(destination?) | Connect/disconnect the started worklet output; return opm. Omitted disconnect destination removes all output connections. Destination must belong to the same context. |\n| opm.loadVoice(name, voice) | Strictly validate/copy a voice; register/replace a name for future notes, at most 128 entries. Name: 1–64 ASCII letters, digits, _, or -. Works before start(). |\n| opm.replaceVoiceBank(source) | Atomically validate and replace the entire registry from bank JSON or a complete voice array. Maximum 128 entries/256 KiB JSON; a plain empty array clears it. Failure leaves the old bank unchanged. |\n| opm.removeVoice(name) / opm.exportVoiceBank() | Remove a lookup and return a boolean / export detached canonical named JSON, capped at 256 KiB. Existing queued/sounding notes keep their immutable patches. An empty bank exports []. |\n| opm.playNote({ voice = 'brass', note, time = 0, at, duration = null, velocity = 1, pan = 0, voicePriority = 0, late = 'start' }) | Return a positive safe-integer ID without wrapping. Finite fractional MIDI 0–127; use either relative time (0–60 seconds) or absolute nonnegative AudioContext at (at most 60 seconds ahead), never both. Duration (0,60] or null holds until stop; velocity 0–1; pan −1–1; voicePriority an integer 0–127 (higher cannot be stolen by lower; see audio buses). Requires start(). |\n| opm.stop(id, options = {}) | Return a command ID. Optional at schedules cancellation/release; ordinary stop retains release-tail automation. Immediate { cancelControls: true } also removes that ID's queued controls/off/onset. cancelControls cannot coexist with at, even when false. Unknown/already-released ordinary targets reject; release is not immediate mute. |\n| opm.updateNote(id, controls, { at } = {}) | Return a command ID. Apply immediately or at an absolute audio time. Controls before onset persist for that pending note; released notes remain controllable until they end. Invalid/inactive/queue-full commands report rejection. |\n| opm.allNotesOff() / opm.panic() | Return a command ID; both bypass a full scheduled-event queue. All-notes-off cancels pending onsets/automation and releases active gates naturally. Panic removes every active/release/steal tail immediately and emits a reset; routing and prepared patches survive. |\n| opm.setMixGain(gain) / opm.setTuning(tuning) | Require a started node and return a command ID. Replace the setting immediately; retain it across node recreation. They affect active and future voices without restarting envelopes/phases. |\n| opm.voices | Defensive ReadonlyMap of deeply frozen patches; mutate the registry only through its bank/load methods. |\n| await opm.getDiagnostics() | Return { type: 'diagnostics', requestId, activeVoices, pendingEvents, errors, rejectedNotes }. Requires running context; at most 64 outstanding requests. Suspend/close/processor failure rejects pending requests; resume before retrying. |\n| await opm.close() | Serialize disposal against initialization, disconnect the node, and close only an owned context. Emit a global reset: close; teardown/failure does not guarantee individual terminal note replies. A later start() recreates owned contexts; borrowed contexts remain usable. Cancel application timers separately. |\n| opm.subscribe(listener) | Multicast event subscription; returns an idempotent unsubscribe function. Listener exceptions are isolated. |\n| await opm.waitForCommand(commandId, { timeout, signal } = {}) | Cancellable admission acknowledgement; timeout default 5000 ms, integer 1–60000 ms; 64 pending waits/128 retained receipts. Rejection throws CommandRejectedError; timeout, abort and teardown reject. Acceptance is not scheduled execution/completion. |\n| await opm.dispose() | Terminal disposal, including listener/helper cleanup. Unlike restartable close(), a disposed instance cannot start again. |\n\nonEvent(event) receives { type: 'note', id, state, reason?, frame, time } with states accepted, started, released, ended, stolen, cancelled, or rejected; frame/time report actual admission, dispatch or completion boundaries. It also receives diagnostics, processor errors, { type: 'command', command, commandId?, id?, state: 'accepted' | 'rejected', reason?, frame, time }, { type: 'context', state, frame, time }, and { type: 'reset', reason, commandId?, frame, time }. Command-triggered resets carry their initiating ID independently of receipt-cache retention. Command acceptance means admission/immediate application, not guaranteed future execution; correlate by commandId. Reset reasons are close, failure, panic, and interruption: clear application gate bookkeeping on reset. Callback exceptions are isolated.\n\nAt most maxVoices logical voices (default 8, opt-in up to 32, including release tails) are active, with up to eight short fading remnants. stealing: 'oldest' is the default; 'release-first' chooses the oldest released voice before held voices; 'quietest' uses current carrier envelope × velocity × expression, not waveform zero crossings, with oldest ties. A full engine steals only among voices of equal or lower voicePriority (lowest first); if every voice outranks the new note it is refused with rejected reason priority, without changing anything else. The worklet bounds pending events and tracked note IDs to 256 each. Future timed notes use start/off events; held notes use a start event. Duplicate IDs and overflow reject before enqueueing; terminal events remove obsolete events. Schedule long pieces incrementally. More than eight voices and more buses cost CPU roughly linearly: measure on target devices.\n\nlate: 'start' preserves the full duration after the actual delayed start; late: 'drop' rejects a missed start with reason late, including worklet-delivery delays. Same-frame stops precede onsets, then controls. Scheduled stops/controls obey the same 60-second future horizon and 256-event bound.\n\nNoteControls accepts nonempty subsets of pitch (−48..48 semitones), glide (0..10 seconds, requires pitch), expression (0..1), pan (−1..1), modulation (0..2), operatorLevels (four 0..2 multipliers), feedback (continuous 0..7), lfoRate (0..20 Hz), amDepth (0..1), pmDepth (0..1200 cents), operatorRatios (four 0.125..32 values), operatorFrequencies (four 1..20000 Hz values or null to restore ratio mode), and operatorADSR (four complete ADSR objects). ramp (0..10 seconds) independently interpolates supplied rampable controls; glide remains linear in semitones. Omitted controls retain state and oscillators keep phase/feedback history. ADSR edits reanchor from current dB: held notes restart attack, released notes begin their new key-scaled release; zero release silences the source and drains the filter. ADSR is not a rampable control. Fixed-Hz controls still follow live pitch but not tuning-table transposition. There is no global setLFO(). See expressive controls and host integration.\n\nSince v1.9: gain is an independent 0–1 multiplier, default 1, using the same ramp range. It composes with expression rather than replacing it and does not restart phase or envelopes.\n\ncreateLookaheadScheduler(opm, callback, { onError, horizon = 0.2, interval = 0.025, maxNotes = 32 }) returns { running, start(), stop(), dispose() }. Its callback receives the half-open absolute window { from, to, maxNotes } and returns at most maxNotes notes with at in that window and a numeric duration. Horizon is 0.02–10 seconds; interval is 0.005–1 second and less than horizon; maxNotes is an integer 1–128. At most 128 gates remain outstanding. Call start() from a gesture and handle its rejected promise; callback/admission errors stop the scheduler and reach required onError. Timer stalls skip missed windows rather than bursting old notes. Stop/dispose cancel only its notes and never close OPM. See the checkout-only song demo and both usage guides for executable recipes.\n\nAny non-running context stops lookahead scheduling, as does a reset; onError reports it and restarting is explicit. Default interruption: 'cancel' clears existing notes and automation on suspension/interruption, preventing replay on resume. Opt-in 'preserve' retains direct-note gates/queued events across suspension, but does not automatically restart lookahead. Resume from a fresh user gesture. A closed borrowed context must be replaced by its host.\n\n### Offline: Synth and renderNote\n\n| Call or property | Contract |\n| --- | --- |\n| new Synth(sampleRate, maxVoices = 8, options = {}) | Required finite sample rate 8000–192000 Hz; integer voice limit 1–32. Options are { mixGain, tuning, stealing, quality }, identical to OPM. No Web Audio is required. |\n| prepareVoice(voice) | Strictly validate, detach and deeply freeze an opaque PreparedVoice once for repeated core admissions; forged lookalikes cannot bypass validation. |\n| synth.noteOn(voiceOrPrepared, note, id?, { velocity = 1, pan = 0, voicePriority = 0 } = {}) | Raw voices validate/copy each call; prepared patches reuse validated data. Finite fractional MIDI 0–127; explicit positive safe-integer ID unique among active notes; velocity 0–1; pan −1–1; voicePriority an integer 0–127. A full synth with no equal-or-lower-priority victim throws VoiceAdmissionError before changing state or consuming an ID. |\n| synth.noteOff(id) | Begin release at current rendered time; return true, or false for unknown/already-released IDs. |\n| synth.updateNote(id, controls) | Apply the same live controls; return true for an active/releasing note or false for an unknown/terminal ID. |\n| synth.allNotesOff() / synth.panic() | Release active gates naturally / immediately clear all logical voices, release/steal tails and spill. Panic notifies active notes with terminal reason cancelled. |\n| synth.setMixGain(gain) / synth.setTuning(tuning) | Replace mix drive / tuning without phase or envelope reset; return no value. |\n| synth.render(left, right, offset = 0, length = nativeLeftLength - offset) | Write native Float32Arrays; nonnegative safe-integer offset/length must fit both actual buffers. Shadow metadata/methods cannot enlarge work; proxies/forgeries and coercible nonnumbers reject. Split calls at offline events. Center pan is dual mono. |\n| synth.currentFrame, synth.errorCount, synth.lastStolenId | Cumulative frames, numerical-failure count, and stolen ID (null when the last successful note-on stole none). |\n| synth.onVoiceEnded = (id, reason) => … | Exactly-once terminal notification (stolen, ended, error, cancelled). Slots recycle before callbacks; replacements first render next frame. Recursive render() rejects. A stolen note's fade can outlive its notification. |\n| renderNote({ voice, note = 60, duration = 0.5, velocity = 1, pan = 0, voicePriority = 0, sampleRate = 44100, mixGain = 1, tuning = {}, stealing = 'oldest', quality = 'standard' }) | Return { samples, left, right, sampleRate, diagnostics: { errors } }, with samples === left. Complete voice required; finite note/duration/velocity/pan clamp to 0–127/0–30/0–1/−1–1. Integer sample rate 8000–96000; synth options and integer voicePriority 0–127 validate strictly. |\n| encodeWav({ left, right?, sampleRate, format = 'pcm16' }) | Return little-endian RIFF/WAVE Uint8Array: PCM16, PCM24 or Float32. Omit right for mono. Native Float32 channels must contain finite samples in −1–1 and match lengths. Invalid amplitudes reject rather than clip; integer rate 8000–192000, full-buffer budget 4,000,000 frames. |\n| createWavEncoder({ sampleRate, channels, format = 'pcm16', totalFrames }) | Incremental RIFF32 output: header() once, encode({ left, right? }) for 1–65536 frames per chunk, then finalize() at exactly the declared frame count. Channels: 1 or 2. Bounded encoded chunks and \u003c4 GiB files, independent of the full-buffer frame limit. |\n\nrenderNote() uses Synth for identical envelope timing, LFO, filtering, velocity, pan, and saturation. It retains ceil((duration + longestEffectiveRelease + 0.01) * sampleRate) frames, including note-dependent rate scaling, with a 4,000,000-frame budget.\n\nSynth preallocates maxVoices + 9 state slots: logical voices, eight possible fades and one admission scratch slot. Prepared admission allocates no new per-voice typed state. Browser snapshots use a content-keyed 128-slot LRU cache; validated replacement reuses a registration ID after saturation rather than falling back to inline transport. Queued/active notes retain their original immutable prepared snapshot. Raw browser inputs still validate freshly.\n\nPan uses a center-normalized constant-power law: left/right gains are sqrt(2) cos/sin((pan + 1) pi / 4) before saturation. Center preserves the previous per-channel gain; hard sides mute the opposite channel and boost the selected channel by sqrt(2). This does not promise constant loudness after polyphonic saturation.\n\nOther core exports: normalizeVoice(voice) makes a strict detached copy; envelopeAt(time, gate, adsr) evaluates the dB-domain envelope; ALGORITHMS contains eight immutable routing graphs; HEADROOM is 0.7, OVERSAMPLE is 4, and MAXRENDERSAMPLES is 4,000,000. sampleRateValue(value) validates an integer rate of 8000–96000. Schema helpers are described below.\n\n### Optional stereo effects\n\ncreateEffects(context: BaseAudioContext, options?: EffectsOptions): Promise\u003cOpmEffects> creates an unconnected stereo insert with input, output, ready, update(params), reset() and dispose(). It never resumes or closes the host context. Route opm.node after await opm.start() into fx.input, then connect fx.output to your host graph.\n\ncreateStereoEffects(sampleRate, options?) and applyEffects(left, right, sampleRate, params) are exported at root and opm.js/core. The engine processes native Float32 buffers in place with bounded preallocated delay state; the helper returns copies with a conservative tail and enforces the 4,000,000-frame budget. Options are { chorus?: { rate, depth, mix, feedback?, voices? }, reverb?: { size, damping, mix, preDelay?, width? }, order? }. Updates replace the configuration with smooth ramps; omitted sections and mix 0 preserve finite dry input bit-for-bit. Wet paths reserve at least 3 dB headroom using a linear crossfade. Ranges, gain law, memory bounds, routing and offline details.\n\n### Mix gain and tuning\n\nmixGain is finite 0–1 (default 1), applied before final tanh saturation; lowering a downstream GainNode cannot undo saturation distortion. Zero mutes PCM without stopping note progression. Host gain remains responsible for listening volume.\n\ntuning: { referenceHz = 440, offsets } replaces the complete global tuning. Reference A4 is 20–20000 Hz; omitted offsets mean zero, otherwise supply exactly 128 finite cents offsets in −4800..4800. Inputs are detached/frozen. Fractional MIDI interpolates neighboring offsets, with MIDI 127 using its own endpoint. Retuning preserves phase/envelopes and does not change the original note used for key scaling. normalizeTuning and tuningFrequency are also core exports.\n\n### Shared live/offline scores\n\nSequenceEvent is a note { type: 'note', id, time, duration, note, voice?, velocity?, pan?, voicePriority? }, explicit stop { type: 'stop', id, time }, or control { type: 'control', id, time, controls }. Times are score-relative seconds; note IDs are unique positive safe integers and commands must reference score notes. Omitted voice is brass; priority defaults to 0 and accepts integers 0–127. Whole-score validation/detachment precedes submission. Limits: 128 notes, 256 reserved slots (two per note plus explicit commands), and 60 seconds including gates/commands. Controls before onset apply at onset; same-frame stop precedes onset, then controls.\n\njs\nimport { playSequence } from 'opm.js';\nimport { renderSequence, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst events = [\n { type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },\n { type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },\n];\nconst voices = new Map([['lead', brass]]);\nconst offline = renderSequence(events, { voices, sampleRate: 48000, mixGain: 0.5, tuning: { referenceHz: 442 } });\nconst wav = encodeWav({ left: offline.left, right: offline.right, sampleRate: offline.sampleRate });\n// In a browser gesture, with an initialized OPM using the same gain/tuning:\n// opm.loadVoice('lead', brass);\n// const playback = playSequence(opm, events, { at: opm.context.currentTime + 0.05 });\n// playback.stop(); // idempotent; affects only that playback's original node\n\n\nplaySequence returns { ids: ReadonlyMap\u003cscoreId, noteId>, stop() }; acceptance still arrives through OPM events. It is not an atomic reservation against other callers filling the worklet queue. prepareSequence(events, { voices }) exposes the immutable resolved score; renderSequence(events, { voices, sampleRate, maxVoices = 8, mixGain, tuning, stealing, quality }) returns stereo PCM with conservative release-tail capacity, including live ADSR edits, and enforces 4,000,000 frames. maxVoices is an integer 1–32. Live/offline sample parity assumes matching settings, rate, frame origin and no competing notes. Longer scores use the separate chunked/streaming helpers.\n\n#### Long scores without aggregate PCM\n\nprepareLongSequence validates a separate maximum of 65,536 input events and 24 hours including gates/commands. estimateSequenceCapacity reports frame/PCM/chunk bytes and short-helper eligibility. renderSequenceChunks uses two reusable buffers; consume/copy each chunk's valid frames before advancing. cancel(), iterator return or an AbortSignal prevents further advancement; optional maxFrames caps cumulative work. Full-buffer rendering/encodeWav retain their limits; incremental createWavEncoder uses a separate RIFF32 file-size bound.\n\njs\nimport { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';\nconst score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];\nconst options = { sampleRate: 96000, chunkFrames: 4096 };\nconsole.log(estimateSequenceCapacity(score, options));\nconst render = renderSequenceChunks(score, options);\nlet frames = 0, energy = 0;\nfor (const chunk of render) {\n for (let i = 0; i \u003c chunk.frames; i++) energy += chunk.left[i] ** 2;\n frames += chunk.frames; // No aggregate PCM retained.\n}\nconsole.log(frames, energy, render.diagnostics.errors);\n\n\nstreamSequence(opm, score, { at, horizon, interval, maxSlots, signal, onError }) returns { running, ids, start(), pump(), stop(), dispose() }. It validates the whole bounded long score, then schedules note/control/stop windows using held gates and incremental releases, preserving cross-window ID mappings and only cancelling its own notes. Start from a gesture; context interruption/reset stops the stream and requires explicit restart. Shared worklet admission remains bounded and is not atomically reserved against unrelated callers. See streaming contracts, capacity and browser recipe and checkout-only example 08.\n\n### Portable score projects and MIDI files (introduced in v1.9)\n\nparseScoreProject(source: string | object) returns a canonical frozen ScoreProject with version: 1, beat events, normalized tempoMap, timeSignature, named complete voices, and synthesis settings (sampleRate, quality, maxVoices, mixGain, tuning, stealing). serializeScoreProject(project) emits bounded canonical JSON. Projects are limited to 8 MiB, 65,536 events and 128 voices (256 KiB total voice JSON); notes refer to stored voice names, with omitted voice requiring stored brass. Missing tempo/meter/settings normalize to 120 BPM, 4/4 and the normal synthesis defaults.\n\njs\nimport { parseScoreProject, serializeScoreProject, compileBeatSequence } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst project = parseScoreProject({\n version: 1,\n voices: { brass },\n events: [{ type: 'note', id: 1, beat: 0, duration: 2, note: 60 }],\n});\nconst seconds = compileBeatSequence(project.events, {\n tempoMap: project.tempoMap, voices: new Map(Object.entries(project.voices)),\n});\nconst saved = serializeScoreProject(project);\n\n\ncompileBeatSequence(events, { tempoMap?, bpm?, voices? }) validates beat events and returns second-based SequenceEvent[] for existing full/chunked/Worker rendering. Live Transport uses the original beat events and tempo map; load the project's named voices into OPM first. Keep the project's synthesis settings when choosing the live/offline engine. See score projects for complete recipes and bounds.\n\nimportMidiFile(bytes, options?) / exportMidiFile(events, options?) are exported from root, core and opm.js/midi-file. Import returns { events, tempoMap, timeSignature, warnings, lossSummary }; format 0/1 PPQN only, with strict byte/track/event budgets and malformed/truncated rejection. channelVoices/defaultVoice map channels to named FM voices; optional programVoices and drumVoices select voices at note onset. Default import applies sustain to durations, warns about unsupported data and rejects unclosed notes; explicit policies may reject unsupported data or close notes at the end. Default controls: 'omit' keeps note-only import and rejects control events/nonzero pan on export. Opt-in expressive conversion is described below; export still rejects unsupported controls, fractional note pitches, nonzero priority, ambiguous channel ownership and linear tempo ramps rather than silently losing them. See MIDI files for policy and loss details.\n\n### Portable Arrangement and expressive MIDI files (v1.10)\n\nThese additions are included in v1.10; the historical v1.9 archive remains unchanged.\n\nparseArrangementProject(source: string | object) / serializeArrangementProject(project) are available from root and core. The separate ArrangementProject format is version 1 and stores named layers, sections, initialSection, normalized tempoMap/timeSignature, named complete voices and the same synthesis settings as score projects. Limits are 8 MiB, 128 voices/256 KiB voice JSON, 16 layers, 32 sections, 256 beats per loop and 65,536 total events. Live and saved definitions share validation; score-project version 1 is unchanged.\n\njs\nimport { parseArrangementProject, serializeArrangementProject } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst arrangementProject = parseArrangementProject({\n version: 1, voices: { brass },\n layers: [{ name: 'lead', length: 4, gain: 0.6,\n events: [{ type: 'note', id: 1, voice: 'brass', beat: 0, duration: 1, note: 60 }] }],\n sections: [{ name: 'intro', layers: ['lead'] }], initialSection: 'intro',\n});\nconst arrangementJSON = serializeArrangementProject(arrangementProject);\n\n\nLoad stored voices into an engine constructed with the saved settings, then pass the project's layers, sections, initial section, tempo and meter to createArrangement. Parsing/loading does not start playback or restore phases/envelopes, pending changes or external routing. The checkout adaptive page supports project save/load; see the complete project replay recipes.\n\nBoth MIDI file adapters accept controls: 'preserve' | 'omit' (default 'omit', preserving the original note-only behavior) and an initial pitchBendRange of 0–48 semitones (default 2). Preserve-mode import and live MIDI now decode channel-local RPN 0 sensitivity, including cents, clamped to 48 semitones. Export still uses the explicit range and emits no RPN or program changes. Export accepts only channel-representable controls; incompatible overlap, unsupported controls/ramps and other losses reject instead of being guessed. Import returns frozen lossSummary groups: omissions, approximations and preservedControls; preserved counts refer to MIDI wire messages, not generated note-control fanout. Bank selection remains omitted. See expressive SMF policies and loss accounting.\n\nPreservation is of representable gate/control meaning, not archived MIDI bytes or complete live-performance fidelity. Channel expression after a closed gate reports the conservative release-tail-controls approximation because patch-dependent tails are unknown; unsupported: 'reject' rejects it. Export rejects after-gate controls.\n\nMidiVoiceMap = Readonly\u003cRecord\u003cnumber, string>> | ReadonlyMap\u003cnumber, string> maps integer 0–127 keys to named voices (at most 128 own-data entries). File import accepts programVoices?: MidiVoiceMap and drumVoices?: MidiVoiceMap; live createMidiAdapter(performance, access, options?) accepts programVoices?: MidiVoiceMap, but no drum map. Program changes affect later onsets only; unmapped file programs fall back to channel/default voices, while unmapped live programs are ignored. gmProgramVoices: Readonly\u003cRecord\u003cnumber, string>> and gmDrumVoices: Readonly\u003cRecord\u003cnumber, string>>, exported from root/core, are frozen artistic starter maps, not a GM sound-set claim. Supply a voice bank containing those names; missing voices use the normal unknown-voice errors.\n\nperformance.configurePart(part, options, policy?: { preserveNotes?: boolean }) can retain existing gates on voice changes with { preserveNotes: true }, as used by the live adapter. Default configuration still clears gates on voice/mode changes; mode changes always clear. RPN selectors CC100/101 cannot be custom-mapped; CC6/38 mappings apply only when RPN 0 is not selected. See live MIDI policies.\n\n\n### Musical Transport and performance policies\n\ncreateTransport(opm, beatScore, options) uses quarter-note beats for event positions and note durations. It provides start()/resume(), pause(), stop(), seek(beat), setTempo(bpm), setTempoMap(points), setLoop({ enabled, from, to }), pump() and dispose(), with defensive state/cursor/ID snapshots. BPM is 1–1000 (default 120); tempo maps start at beat 0 and contain at most 1024 increasing points. The AudioContext clock drives bounded scheduling; pause/seek/loop affect only owned notes. Restarting reconstructs scalar/ratio ramps and remaining durations but restarts envelopes/phases; unfinished fixed-Hz transitions restore the latest Hz/null policy immediately. This is musical seeking, not a DSP-state snapshot. See Transport contracts.\n\nIn the current checkout, TransportOptions.startupLead is a finite 0–10 seconds, default min(0.05, horizon / 2). Startup, resume, seek and clock reconstruction anchor in the future and keep musical position fixed during that lead, avoiding the historical cold-start beat-0 scheduling defect. Set 0 to opt out. late:'drop' still rejects genuinely late admissions; this is not a deadline guarantee. The historical v1.8.1 tarball lacks this fix.\n\nThe root and core export beatsToSeconds, secondsToBeats, normalizeTempoMap, beatToBarBeat and barBeatToBeat. Meter denominators are powers of two through 32; numerator is 1–32. Positions are bounded to 86400 quarter-note beats.\n\ncreatePerformance(opm, options) manages 1–16 parts (default 16), at most 128 physical keys and tracked gates system-wide. Configure a part's voice, poly/mono mode, legato and last/high/low priority; call noteOn(part, note, { velocity }) to obtain a physical key ID, noteOff(part, keyId), sustain(part, boolean), updatePart(part, { glide, pan, expression }), allNotesOff(part?), getPart(part) and dispose(). Equal-pitch keys remain distinct. Held keys take precedence over pedal-only keys. Mono legato preserves onset velocity/envelopes within ±48 semitones of the original onset; farther moves retrigger at the actual pitch. Reset/interruption clears helper ownership even under preserve policy; scoped cleanup never panics unrelated notes. This is a performance helper, not a MIDI driver. See performance recipe.\n\n### Worker WAV export and quality profiles\n\nrenderSequenceInWorker(score, { ...chunkedOptions, format, sink, onProgress, signal, workerUrl, startupTimeoutMs, phaseDiagnostics, onPhase }) returns a promise for { capacity, format, bytesWritten, diagnostics }. sink.write(bytes) may transfer ownership and return a promise: the static module Worker waits for acknowledgement before rendering the next encoded chunk. Optional close() runs on completion; abort(reason) handles rollback. Cancellation terminates/rejects without waiting for a hung sink; the host must invalidate its partial output. Progress reports { frames, totalFrames, bytesWritten, errors }. This helper is browser-only; pure chunk rendering/encoding also works in Node. Deploy the entire dist/ tree, including worker/render.js and its companions, with same-origin secure URLs and normal CSP/JavaScript MIME checks. See incremental/Worker sink recipes and checkout-only example 08; long-file export uses a direct file sink, not an aggregate Blob fallback.\n\nquality is accepted by OPM, Synth, renderNote, full/chunked sequence rendering and Worker options. standard uses 4× oversampling with a 15 → 47-tap half-band FIR cascade (flat to 0.40 output Fs, 81 dB rejection, 13.25-frame delay); eco uses 2× with a single 35-tap stage and weaker (63 dB) rejection, while high uses 8× with an 11 → 15 → 47-tap cascade at higher CPU cost. Profiles do not promise alias-free FM or universal realtime deadlines. See measured quality tradeoffs.\n\n### Adaptive music, tempo curves and grids\n\ncreateArrangement(opm, { layers, sections, initialSection, bpm?, tempoMap?, timeSignature? }) loops named layers on one global beat grid and switches sections or toggles layers at the first beat/bar boundary that is not earlier than the notes already admitted. Shared layers are one continuous schedule, so their sounding notes are not retriggered. TempoPoint accepts curve: 'linear' (BPM changes linearly per beat; closed-form logarithmic integral) and endBpm; quantizeBeat, swingBeat and swingBeatEvents shape grids. createTransport and createArrangement are musical restarts, not DSP checkpoints. Details, formulas and the exact pause/seek contract: adaptive music; try checkout-only example 09.\n\nSince v1.9: ArrangementLayer.gain is 0–1 (default 1). switchSection and setLayer accept fade in seconds (0–10, default 0), and setLayerGain(name, gain, { quantize?, fade? }) returns the committed beat. Shared layers continue without retriggering; layer gain multiplies independent note expression and includes owned release tails. Arrangement reserves authored gain controls for layer envelopes; use expression inside layer scores.\n\n### Expressive performance and Web MIDI\n\nperformance.updateKey(part, key, controls) and performance.updatePartNotes(part, controls) apply any NoteControls to one key or to a whole part; configurePart accepts voiceLimit (1–32, release tails count) and voicePriority. createMidiAdapter(performance, await requestMidiAccess(), options) maps channel-voice MIDI (notes, sustain, bend, wheel, volume/expression, pan, pressure, all-notes-off) from user-granted inputs to parts. Importing never requests access, SysEx is never requested, and disconnect or dispose() releases only the keys the adapter owns. See expressive performance and MIDI and checkout-only example 10.\n\nSince v1.9: MidiAdapterOptions.controllerMap accepts at most 128 MidiControllerMapping entries. Each names controller, scalar field (including gain) or tuple field plus zero-based operator, explicit min, max, ramp, and optional reset. Reserved CC64/120/121/123 cannot be remapped; duplicates reject. A mapping overrides that CC's ordinary default only. CC121 restores captured effective part defaults or the supplied reset; fixed-Hz reset:null restores ratio mode. performance.getPartControls(part) returns a detached frozen snapshot of effective part controls, not per-key overrides. See the MIDI guide for field bounds and reset behavior.\n\n### Worker diagnostics and deployment\n\nrenderSequenceInWorker accepts optional startupTimeoutMs (integer 1–2147483647 ms, module-ready watchdog only), phaseDiagnostics and onPhase; the Worker posts a versioned ready before any score is sent. There is no built-in overall deadline or automatic retry: bound the complete job with your own AbortSignal. See Worker diagnostics. From an installed application's root, run npx --no-install opm-assets copy \u003cnew-directory> (atomic, never overwrites a differing destination, hashes every file) and npx --no-install opm-assets check \u003cbase-url> (byte, status, MIME and nosniff check of served assets); see host integration. VERSION is exported from the package root.\n\n\n### Physical-device recovery workflow\n\nUse checkout-only example 08 over HTTPS on physical iOS/Android devices, following the acceptance workflow and support matrix. Record device/OS/browser/rate/policy, scenario observations and manual pass/fail/unverified results. Exercise both policies through lock/unlock, app switching, calls, headset/Bluetooth routes, battery saving, prolonged playback and main-thread stalls. Reports stay local and never certify untested devices. Desktop/headless checks and analyser peaks do not prove audible continuity or physical-phone recovery.\n\n### WAV files and DX7 import\n\nSave this as export.mjs in the installed application's root; node export.mjs writes a stereo file:\n\njs\nimport { writeFile } from 'node:fs/promises';\nimport { renderNote, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\nconst audio = renderNote({ voice: brass, note: 60, duration: 0.7, velocity: 0.8, pan: -0.3 });\nawait writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));\n\n\nimportDX7(bytes) from opm.js/voices/dx7.js returns complete normalized version 7 voices. It accepts exactly one standard framed/checksummed 163-byte single-voice or 4104-byte 32-voice bank SysEx message (Uint8Array), rejecting invalid framing, length, checksum, or non-7-bit data. describeDX7(bytes) returns per-voice {name,sourceAlgorithm,algorithm,selectedOperators,droppedOperators,warnings}; selected/dropped numbers refer to DX7 operators 1–6, source algorithms to 1–32, and converted algorithms to 0–7.\n\nThis remains an approximate six-to-four-operator conversion, not DX7 synthesis/emulation. Fixed-Hz operators are retained; operator velocity/rate scaling, pitch envelopes and LFO delay/sync are musical heuristics. Pitch-envelope stages are reduced; unsupported descending saw/sample-and-hold waveforms are substituted with warnings. Dropped operators/routing, oscillator sync, transpose, per-operator AM and envelope details still differ; the converter reduces selected operators' AM sensitivity to one shared depth, not selective v6 targets. Inspect descriptions and expressive conversion limits before loading. The checkout-only audition uses original synthetic fixtures, not copied patches or a six-operator reference engine.\n\nimportOPM(source) from opm.js/voices/opm.js accepts decimal VOPM/MXDRV-family .opm text (string or Latin-1 Uint8Array) and returns complete normalized v7 voices. describeOPM(source) returns {name,program,algorithm,operators,warnings} for each patch. Names are sanitized and deterministically de-duplicated; source program numbers are metadata, not an automatic MIDI map. Input is bounded to 262144 bytes, 128 patches and 512 bytes per line, with strict integer ranges and complete unique sections. This is an approximate conversion, not register-level YM2151 emulation: held decay, rate scaling, detune, feedback and LFO differ, PAN is ignored, and C2 noise is an OPM.js extension. See OPM conversion formulas and limits.\n\n## Optimized distribution\n\nEvery .js in dist/ has a matching .js.map and compiler-generated .d.ts, including thirteen bundled example entry points in the current checkout (twelve in historical v1.8.1). Engine modules preserve source paths; demo-only helpers are bundled into those entry points rather than shipped as separate modules or orphan declarations. Safe minification retains composed maps with embedded TypeScript; the empty type-only worklet-globals module has no source mappings. Deploy the entire matching tree; publishing maps exposes its sources. Do not edit generated files. Build/package smoke rejects missing map/declaration companions.\n\nEngine, worklet, demos, tests and development scripts use strict TypeScript. Following XYZ.js's approach, declarations derive from implementation. Node.js 22+ is required. tsconfig.json checks source/emits declarations; tsconfig.dev.json emits ignored .dev/. ESM imports retain .js specifiers. Use npm test, not bare node --test; npm run typecheck includes tools/tests/demos/public consumers. The current build produces thirteen dist/demo/ entry points plus static HTML/search under doc/; checkout index.html is the demo catalog. Copy the complete doc/ directory together with top-level Markdown/legal files when hosting generated guides.\n\nIn the repository checkout, not the installed npm package:\n\nsh\nnpm ci\nnpm run build\nnpm test\nnpm run benchmark\nnpm run typecheck\nnpm run security\nnpx --no-install playwright install --with-deps chromium\nnpm run browser-smoke -- chromium\nnpm run browser-stress -- chromium\nnpm run sound-quality\nnpm run voice-quality\nnpm run vite-smoke\n\n\nThese install pinned development tools and regenerate dist/; the build replaces that generated directory. Consumers need no build tools or runtime dependencies. npm pack also runs the build automatically. Terser uses up to ten safe compression passes, without unsafe floating-point transformations or property mangling.\n\nThe build does not produce JSON assets or gzip/Brotli sidecars. If desired, configure compression at the web host while retaining normal .js import URLs. Send the matching Content-Encoding and Vary: Accept-Encoding only for compressed responses, with a JavaScript MIME type. The basic Python server serves the ordinary .js modules directly.\n\nBenchmark results depend on the machine and JavaScript engine. CHANGELOG records observed verification, and SECURITY covers safe embedding and review requirements.\n\nQuality automation configures Node 22/24/26 and Chromium/Firefox/WebKit smoke scenarios with real AudioWorklet output; browser installation is a development-only prerequisite. Headless Linux Firefox additionally needs a running native audio server; CI starts PulseAudio with a null output sink (see the quality instructions). Configuration alone does not prove successful runs or all-platform coverage: observed checks belong in CHANGELOG. npm run security -- --package-smoke exercises installed exports/types. No fallback masks browser failure.\n\nThe real-time benchmark reports warmed 128-frame block p95/p99/worst and misses against 128/sampleRate seconds. Local defaults are report-only; optional OPMBENCHP99BUDGETRATIO / OPMBENCHWORSTBUDGETRATIO enforce host-specific budgets. Run deadline acceptance separately from CPU-heavy offline rendering; competing workloads can change its outcome. CI configures p99 ratio 1 at 48 kHz (2.667 ms), with worst/GC stalls report-only. Four-times oversampling/filtering and controlled spectral tests do not promise arbitrary alias-free FM or deadlines on every host.\n\nThe v1.10 checkout-only benchmark also reports scoped capacity candidates across quality/voice configurations with an explicit reserve criterion and zero measured misses. Candidates retain their host/runtime/version/patch/workload identity and all raw timing rows; they do not certify AudioWorklet continuity or physical devices and never auto-switch synthesis quality. See capacity guidance. Actual mobile/MIDI and human-listening acceptance still require original physical observations and listener findings, not automated PASS substitutions.\n\nsound-quality covers all three profiles and passed 2,505 lifecycle/headroom/determinism matrix cases plus 24 live-control cases in the recorded v1.8 verification. It also checks independent passband, THD, folded aliases and streaming references; these are controlled numerical gates, not arbitrary-FM or listening certification. voice-quality evaluates the curated preset bank and original synthetic DX7 recipes over register/velocity cells. Peak/RMS are raw dBFS, not LUFS or perceived loudness; host trims do not rewrite synthesis levels. See acoustic-quality evidence and voice-quality notes.\n\nIndependent references cover Bessel-series high-index PM, contractive delayed feedback, conservative full-level feedback energy, and nested four-operator chain/branched/multicarrier synthesis. The shared post-mix 4× decimator is a polyphase linear-phase half-band FIR cascade, flat to 0.40 output Fs with at least 81 dB rejection of products that fold below that edge. Two 120-second streaming cases exercise held AM/PM and glide retargets. See acoustic reference mathematics and limits; none certify arbitrary alias-free FM, perceptual quality or hardware fidelity.\n\nbrowser-stress exercises real AudioWorklet output, dense admissions/steals, controls, late rejection and queue cleanup under bounded main-thread contention. Timer gaps and diagnostic round trips are observations, not worklet CPU/GC/underrun/glitch measurements. Periodic analyser samples cannot prove uninterrupted audio. The Vite smoke additionally verifies an installed current tarball, non-root production deployment, retained license, CSP and negative asset/MIME cases. Run these separately from deadline benchmarks.\n\n## Voice format\n\nA voice has four operators, an algorithm (0–7), and integer patch feedback (0–7). This is a canonical version 7 entry. Single objects may omit version, name, modIndex (4), and lfo (off/sine). Explicit versions 1–6 retain their original shapes and normalize to 7: v1 excludes key scaling, v1/2 exclude velocity sensitivity, v1–3 exclude LFO waveform, v1–4 exclude expressive v5 fields, v1–5 exclude v6 LFO targets, and v1–6 exclude operator waveform/noise rate.\n\njson\n{\n \"version\": 7,\n \"name\": \"brass\",\n \"algorithm\": 4,\n \"feedback\": 3,\n \"modIndex\": 4,\n \"lfo\": { \"rate\": 5.2, \"amDepth\": 0, \"pmDepth\": 12, \"waveform\": \"sine\" },\n \"ops\": [\n { \"ratio\": 1.0, \"level\": 0.8, \"detune\": 0, \"velocitySensitivity\": 12,\n \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.6, \"r\": 0.1 },\n \"keyScale\": { \"breakpoint\": 60, \"leftDbPerOctave\": 0, \"rightDbPerOctave\": 6 } },\n { \"ratio\": 1.0, \"level\": 0.6, \"detune\": 3,\n \"adsr\": { \"a\": 0.01, \"d\": 0.15, \"s\": 0.5, \"r\": 0.1 } },\n { \"ratio\": 2.0, \"level\": 0.4, \"detune\": -3,\n \"adsr\": { \"a\": 0.02, \"d\": 0.3, \"s\": 0.4, \"r\": 0.15 } },\n { \"ratio\": 1.0, \"level\": 0.7, \"detune\": 0,\n \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.55, \"r\": 0.1 } }\n ]\n}\n\n\nFor OPM, Synth.noteOn(), and normalizeVoice(), algorithm, feedback, and exactly four complete operators are required; the optional metadata/defaults are described above. Unknown fields, accessors, sparse operator arrays, incorrect types, non-finite numbers, and out-of-range values are rejected.\n\n| Voice field | Allowed values |\n| --- | --- |\n| version, name | Version 7; legacy 1–6 accepted only with original fields; name: 1–64 ASCII letters, digits, _, or - |\n| algorithm, feedback | Integers 0–7 |\n| ratio, level, detune | 0.125–32, 0–1, −1200–1200 cents |\n| adsr.a, adsr.d, adsr.s, adsr.r | Attack/decay/release: 0–10 seconds; sustain: 0–1 |\n| modIndex | 0–16 |\n| lfo.rate, lfo.amDepth, lfo.pmDepth | 0–20 Hz, 0–1, 0–1200 cents |\n| lfo.waveform | sine (default), triangle, saw, square; v4+ shape only |\n| ops[i].keyScale | Optional; breakpoint: MIDI integer 0–127; leftDbPerOctave, rightDbPerOctave: 0–24 dB/octave |\n| ops[i].velocitySensitivity | Optional since v3: 0–48 dB attenuation at velocity zero; omitted is 0 |\n| ops[i].frequency | Optional v5 fixed frequency 1–20000 Hz; overrides note-derived ratio frequency, with detune/live pitch/PM still applied |\n| ops[i].rateKeyScale | Optional v5 0–4: ADSR times multiply by 2 * (-rateKeyScale (note - 60) / 12), clamped to 10 seconds; default 0 |\n| ops[i].waveform | Optional v7 sine (default), half, abs, quarter, alternating, camel, square, saw, noise |\n| ops[i].noiseRate | Optional v7 20–20000 Hz, default 8000; ignored unless waveform is noise |\n| pitchEnvelope | Optional v5 {a,d,r,initial,peak,sustain,final}; times 0–10 seconds, levels −4800..4800 cents, continuous linear-cents release |\n| lfo.delay, lfo.sync, lfo.phase | Optional v5 0–10 seconds, 'note'/'global' (default note), 0–1 turns (default 0); delay gates depth, not the phase clock |\n| lfo.amTargets, lfo.pmTargets | Optional v6 four-element depth factors 0–1; input booleans normalize to 0/1; absent targets mean all 1 |\n\nSee the expressive voice reference for pitch composition, fixed-frequency behavior and synchronization. Ratio remains required even when frequency is supplied. Preserve the source patch version when importing older banks; changing a version number alone does not add unsupported fields to its legacy shape.\n\nPeriodic shapes use phase plus modulation; sine retains the unchanged legacy fast path. Noise uses a deterministic 17-bit bipolar sample-and-hold sequence reset at every admission/slot reuse. Ratio, fixed Hz, detune, pitch and PM do not pitch noise; live ratio/frequency controls remain valid but have no audible pitch effect. Envelope, velocity, level, key scaling, AM and graph roles still apply. Noise on any operator is an OPM.js extension, not hardware-fidelity emulation. Square/saw use polyBLEP and half uses polyBLAMP edge correction, but these shapes and noise can still alias, especially under FM; see shape formulas and noise details. Original presets include noise-snare, noise-hihat and explosion.\n\n\nKey scaling attenuates operator level by 10 (-slope * abs(note - breakpoint) / 12 / 20) on the corresponding side; omitted scaling is flat. Velocity sensitivity additionally multiplies it by 10 (-sensitivity * (1 - velocity) / 20), changing carrier loudness and modulator brightness. Final note velocity still multiplies the output; default sensitivity 0 preserves legacy sound. Pan belongs to the note, not the voice.\n\n### Voice banks\n\nImport parseVoiceBank from opm.js/voices/schema.js in Node, or ./opm/voices/schema.js in the deployed browser example. It accepts a JSON string or array and returns Map\u003cname, voice> with frozen copies.\n\nA bank must be an array of 1–128 complete versioned voices; wrap individual JSON examples in [...]. Every bank entry requires version, name, algorithm, feedback, modIndex, lfo, and four complete operators; names must be unique. JSON strings are capped at 256 KiB in UTF-8. The schema module exports voiceSchema; the example bank module exports examples. Import examples from opm.js/voices/examples.js (Node) or ./opm/voices/examples.js (browser) and pass it to parseVoiceBank(examples). External JSON banks remain supported.\n\nopm.replaceVoiceBank() applies the bank's whole-input validation before swapping lookups. Only its plain empty-array input is a clearing operation; parseVoiceBank('[]') still rejects an empty bank. Removing/replacing a name does not alter immutable patches already queued or sounding.\n\nUnlike strict single-voice normalization, validateVoice(), parseVoiceBank(), and renderNote() clamp finite out-of-range numeric fields to the schema limits. Invalid types, non-finite values, unknown fields, unsupported versions, and invalid algorithm/feedback enums still fail. validateVoice() handles one complete voice; bounded() clamps a finite number; LIMITS, MAXBANKBYTES, and MAXBANKVOICES expose the bounds.\n\nThe guides include complete browser loader examples with HTTP-status checks and error handling. The engine does not fetch URLs: its string-size check runs only after your application has obtained the text. Bound untrusted downloads before buffering them; do not treat the bundled-example loader as a hardened external downloader.\n\n## Troubleshooting\n\n| Symptom | Action |\n| --- | --- |\n| npm returns E404 for opm.js | Use the local tarball installation recipe above; do not assume a registry release exists. |\n| AudioWorklet is not available, or file:// fails | Use a supporting browser over HTTPS/localhost. In Node, use Synth, not OPM.start(). |\n| Bare module specifier cannot be resolved | Use the browser's ./opm/... URLs and copied assets, not npm names in raw HTML. |\n| Processor/module 404 or JavaScript MIME error | Preserve the complete distribution layout; ensure asset URLs serve JS, not a single-page app's HTML fallback. |\n| No sound, or “Call start() before playNote()” | Await start() inside a click handler; inspect the displayed error and browser/device audio settings. |\n| Invalid note, duration, or unknown voice | Check finite fractional MIDI 0–127, duration (null or (0,60]), velocity/pan, and registered voice name. |\n| Notes disappear | Observe onEvent rejections/steals and getDiagnostics(); respect voice and queue bounds and schedule smaller batches. |\n| Bank import fails | Use an array of complete versioned voices with unique names, not a single partial voice object. |\n| Offline script makes no speaker sound/file | PCM buffers do not play themselves; use encodeWav and a file/download API to save them. |\n| Compressed assets fail to load | Import .js; configure content-encoding negotiation or serve the normal minified files. |\n\n\n## Architecture\n\n\n┌─────────────────────────────────────────────┐\n│ main thread │\n│ playNote() / control messages │\n└──────────────┬──────────────────────────────┘\n │ MessagePort\n┌──────────────▼──────────────────────────────┐\n│ AudioWorklet processor │\n│ voice allocation → operator graph │\n│ 4-op FM (8 algorithms, feedback) │\n│ ADSR + LFO → stereo out │\n└─────────────────────────────────────────────┘\n\n\n## Roadmap\n\n- [x] v1.0 — 4-operator FM engine, eight algorithms, feedback, ADSR, LFO, detune, browser API, and offline rendering\n- [x] v1.2 — key scaling, approximate DX7 SysEx voice import, and WAV export (regression tests and actual browser demo/import/download verified; see CHANGELOG for evidence and limits)\n- [x] v1.11 — voice v7 waveforms and noise, stereo chorus/reverb, MIDI program maps and RPN, .opm import (regression tests, a real-browser audio smoke, passing Node 22/24/26 and Chromium/Firefox/WebKit CI, and demo coverage; physical-device and human-listening acceptance are unverified)\n\n## Limitations\n\n- Not a register-level YM2151 emulator; VGM playback is out of scope\n- Bundled sounds are original curated recipes, not a comprehensive commercial bank; bring your own sounds.\n- Selectable 2×/4×/8× oversampling and half-band FIR decimation reduce controlled folded aliases; arbitrary FM can still alias, and the linear-phase filters add a constant delay (13.25 output frames for standard).\n- Numerical host trims are not perceptual loudness matching. DX7 conversion remains six-to-four-operator and intentionally lossy; no hardware-fidelity or all-device glitch-free guarantee is made.\n\n## License\n\nApache License 2.0\n\nCopyright 2026 OPM.js authors\n\nOPM.js is an independent project inspired by the architecture of the Yamaha YM2151. It is not affiliated with, endorsed by, or connected to Yamaha Corporation.\n","href":"./readme.html"},{"title":"OPM.js · OPM.js","text":"OPM.js OPM.js","href":"./readme.html#opmjs"},{"title":"OPM.js · New in v1.11","text":"OPM.js New in v1.11","href":"./readme.html#new-in-v111"},{"title":"OPM.js · New in v1.10","text":"OPM.js New in v1.10","href":"./readme.html#new-in-v110"},{"title":"OPM.js · About","text":"OPM.js About","href":"./readme.html#about"},{"title":"OPM.js · Features","text":"OPM.js Features","href":"./readme.html#features"},{"title":"OPM.js · Requirements","text":"OPM.js Requirements","href":"./readme.html#requirements"},{"title":"OPM.js · Getting started","text":"OPM.js Getting started","href":"./readme.html#getting-started"},{"title":"OPM.js · Try the checkout","text":"OPM.js Try the checkout","href":"./readme.html#try-the-checkout"},{"title":"OPM.js · Install into an npm project","text":"OPM.js Install into an npm project","href":"./readme.html#install-into-an-npm-project"},{"title":"OPM.js · Use in a browser","text":"OPM.js Use in a browser","href":"./readme.html#use-in-a-browser"},{"title":"OPM.js · Render in Node.js","text":"OPM.js Render in Node.js","href":"./readme.html#render-in-nodejs"},{"title":"OPM.js · API reference","text":"OPM.js API reference","href":"./readme.html#api-reference"},{"title":"OPM.js · Browser: OPM","text":"OPM.js Browser: OPM","href":"./readme.html#browser-opm"},{"title":"OPM.js · Offline: Synth and renderNote","text":"OPM.js Offline: Synth and renderNote","href":"./readme.html#offline-synth-and-rendernote"},{"title":"OPM.js · Optional stereo effects","text":"OPM.js Optional stereo effects","href":"./readme.html#optional-stereo-effects"},{"title":"OPM.js · Mix gain and tuning","text":"OPM.js Mix gain and tuning","href":"./readme.html#mix-gain-and-tuning"},{"title":"OPM.js · Shared live/offline scores","text":"OPM.js Shared live/offline scores","href":"./readme.html#shared-liveoffline-scores"},{"title":"OPM.js · Long scores without aggregate PCM","text":"OPM.js Long scores without aggregate PCM","href":"./readme.html#long-scores-without-aggregate-pcm"},{"title":"OPM.js · Portable score projects and MIDI files (introduced in v1.9)","text":"OPM.js Portable score projects and MIDI files (introduced in v1.9)","href":"./readme.html#portable-score-projects-and-midi-files-introduced-in-v19"},{"title":"OPM.js · Portable Arrangement and expressive MIDI files (v1.10)","text":"OPM.js Portable Arrangement and expressive MIDI files (v1.10)","href":"./readme.html#portable-arrangement-and-expressive-midi-files-v110"},{"title":"OPM.js · Musical Transport and performance policies","text":"OPM.js Musical Transport and performance policies","href":"./readme.html#musical-transport-and-performance-policies"},{"title":"OPM.js · Worker WAV export and quality profiles","text":"OPM.js Worker WAV export and quality profiles","href":"./readme.html#worker-wav-export-and-quality-profiles"},{"title":"OPM.js · Adaptive music, tempo curves and grids","text":"OPM.js Adaptive music, tempo curves and grids","href":"./readme.html#adaptive-music-tempo-curves-and-grids"},{"title":"OPM.js · Expressive performance and Web MIDI","text":"OPM.js Expressive performance and Web MIDI","href":"./readme.html#expressive-performance-and-web-midi"},{"title":"OPM.js · Worker diagnostics and deployment","text":"OPM.js Worker diagnostics and deployment","href":"./readme.html#worker-diagnostics-and-deployment"},{"title":"OPM.js · Physical-device recovery workflow","text":"OPM.js Physical-device recovery workflow","href":"./readme.html#physical-device-recovery-workflow"},{"title":"OPM.js · WAV files and DX7 import","text":"OPM.js WAV files and DX7 import","href":"./readme.html#wav-files-and-dx7-import"},{"title":"OPM.js · Optimized distribution","text":"OPM.js Optimized distribution","href":"./readme.html#optimized-distribution"},{"title":"OPM.js · Voice format","text":"OPM.js Voice format","href":"./readme.html#voice-format"},{"title":"OPM.js · Voice banks","text":"OPM.js Voice banks","href":"./readme.html#voice-banks"},{"title":"OPM.js · Troubleshooting","text":"OPM.js Troubleshooting","href":"./readme.html#troubleshooting"},{"title":"OPM.js · Architecture","text":"OPM.js Architecture","href":"./readme.html#architecture"},{"title":"OPM.js · Roadmap","text":"OPM.js Roadmap","href":"./readme.html#roadmap"},{"title":"OPM.js · Limitations","text":"OPM.js Limitations","href":"./readme.html#limitations"},{"title":"OPM.js · License","text":"OPM.js License","href":"./readme.html#license"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism # SECURITY.md — OPM.js Security Policy & Review Mechanism\n\nOPM.js is a client-side, four-operator FM synthesis library. It processes voice definitions, DX7 SysEx and PCM buffers, and runs real-time DSP in an AudioWorklet. It has no application server or authentication system and has zero runtime dependencies. The host application remains responsible for website security, untrusted downloads, resource budgets and safe audio presentation.\n\nFor installation, browser deployment, and API examples, start with the README or the English / Traditional Chinese usage guide.\n\nContents: Supported versions · Reporting a vulnerability · Safe embedding · Threat model · Security review mechanism · Non-goals\n\n## Supported versions\n\n| Version | Supported |\n| --- | --- |\n| 1.x | Supported security line; use the latest available patch |\n| \u003c 1.0 | Not supported |\n\nThe current checkout requires Node.js 22 or newer for Node usage and development. Browser applications require ES modules, AudioWorklet and a secure context (HTTPS or localhost). The Node requirement does not provide a browser sandbox or replace browser updates.\n\nUse a complete distribution from a known checkout or package, not individual modules from unrelated builds. Check CHANGELOG for observed verification; an advertised feature or configured CI job is not proof that every runtime or platform has passed.\n\n## Reporting a vulnerability\n\nReport exploitable problems privately to the repository maintainer. Use the GitHub private vulnerability reporting form when it is enabled. If a private reporting route is unavailable, request a private contact method without publishing exploit details, sensitive files or a proof of concept.\n\nInclude enough information to reproduce the issue safely:\n\n- The affected OPM.js version or commit, and whether the behavior uses src/, committed dist/, or an installed package.\n- Node/browser version, operating system, sample rate and relevant deployment or routing configuration.\n- A minimal input and sequence of API calls or worklet messages, including expected and observed behavior.\n- The apparent impact: code execution, validation bypass, excessive CPU/memory use, processor failure, unexpected audio, or data exposure.\n- Relevant errors, diagnostics and whether the issue reproduces with a freshly built, matching distribution.\n- Any proposed fix or mitigation, and whether the report may be credited publicly.\n\nDo not include credentials, private voice banks, personal recordings or unrelated user data. Use a synthetic input when possible. Minimize denial-of-service examples; do not test against other people's websites or production audio sessions.\n\nThe acknowledgement target is 72 hours, on a best-effort basis, not a guaranteed response or remediation deadline. Coordinate public disclosure with the maintainer so affected users can receive an actionable fix or mitigation. Ordinary usage questions may use public issues; exploitable details should remain private.\n\n## Safe embedding\n\n### Website deployment and browser permissions\n\n- Serve the complete, matching dist/ tree over HTTPS or localhost, preserving worklet and imported-module paths. A configurable worklet URL must stay same-origin, use HTTPS/loopback HTTP, contain no credentials/fragment, and retain CSP/MIME enforcement. Do not load through file://, and retain LICENSE.\n- The root index.html, examples/ pages, favicon.ico and this policy are public website assets. Publish the build-generated doc/ HTML, stylesheet and search assets together with the matching documentation sources.\n- The Python development server is for local checks, not a hardened public host. A production server should not expose .git/, .dev/, node_modules/, development tooling, credentials or directory listings.\n- Serve JavaScript with a JavaScript MIME type. Use X-Content-Type-Options: nosniff; do not return an application's HTML fallback for a missing worklet/module URL.\n- Apply a host-specific Content Security Policy that allows only reviewed module origins, restricts framing and base URLs, and disables unnecessary object embedding. Check the script and worklet/worker directives applicable to each supported browser.\n- The checkout examples use external module scripts and a local stylesheet. They need no unsafe-eval. Do not weaken CSP, TLS or browser permission/autoplay controls to suppress a deployment error.\n- WAV Blob URLs are data downloads, not executable modules. Do not broaden script origins simply because downloads use blob: URLs.\n- Only enable browser features that the host needs. Synthesis does not require microphone, camera or location access. Test playback, worklet loading and download behavior after changing hosting headers.\n\n### Voice banks and JavaScript objects\n\nUse loadVoice(), normalizeVoice(), validateVoice() or parseVoiceBank() at the appropriate documented boundary; do not send unchecked definitions straight into the DSP or raw worklet protocol.\n\n| Input | Boundary |\n| --- | --- |\n| Single-voice API | Required fields and exactly four complete operators; finite numeric values and documented ranges; unknown fields and accessors reject |\n| Voice-bank JSON string | At most 262,144 UTF-8 bytes (256 KiB), then JSON parsing and bank validation |\n| Voice-bank array | 1–128 complete versioned voices, unique validated names, and frozen normalized copies |\n| Legacy versions 1–6 | Original shapes only: v1 excludes key scaling, v1/2 exclude velocity sensitivity, v1–3 exclude waveform, v1–4 exclude expressive fields, v1–5 exclude per-operator LFO targets, v1–6 exclude operator waveform/noiseRate; all normalize to v7. noiseRate is 20–20000 Hz |\n| .opm text patches | importOPM/describeOPM: at most 262,144 bytes, 128 patches and 512 bytes per line; NUL bytes, unknown/duplicate/missing sections, non-decimal or out-of-range fields reject with a line number; results pass the normal v7 validation path |\n| MIDI program/drum maps | programVoices/drumVoices: plain data objects or native Maps, at most 128 entries, canonical integer keys 0–127 and valid voice names; accessors and extra Map properties reject. RPN 0 state is per channel and bounded to 0–48 semitones |\n| PreparedVoice | Immutable detached core snapshot, including nested pitch envelopes; only private identity membership grants trust, never a structural brand/copy |\n| Tuning / score / controls | A4 20–20000 Hz, 128 bounded cents offsets; strict dense four-element level/ratio/Hz/ADSR and LFO-target tuples; short scores retain 128-note/256-slot/60-second bounds; long scores have separate event/time/chunk limits and immutable own-data snapshots |\n| Atomic OPM bank replacement | Validate all entries before swapping ≤128 lookups; plain empty array clears, while JSON/standalone empty bank parsing rejects; exports use detached canonical names and ≤256 KiB UTF-8 |\n| Transport / performance | ≤1024 tempo points, quarter-note positions ≤86400; bounded scheduling density and scoped ID ownership; 1–16 parts, ≤128 physical keys/tracked gates with detached snapshots |\n\nSingle-voice APIs reject out-of-range fields. Bank validation clamps finite numeric fields to documented bounds, but still rejects malformed types, non-finite numbers and invalid version/algorithm/feedback values. Clamping is not a substitute for validation.\n\nKeep allowlisted, own-data copying at trust boundaries. Do not merge untrusted objects into prototypes, invoke getters to inspect them, or treat a valid voice name as authorization to construct a URL or filesystem path. Engine bank parsing does not load paths or fetch voice URLs.\n\nIndependent note gain and expression are both bounded to 0..1 and multiply before stereo mix saturation. Layer fades do not bypass control validation, note ownership, worklet queue limits or rejection reporting.\n\n### Score projects and Standard MIDI files\n\nVersioned score-project parsing accepts only the documented own-data schema, detached validated voices, bounded beat events and synthesis settings. Loading a project does not fetch assets, execute code or start playback. Compilation converts note gate endpoints through the complete tempo map; offline resource budgets still apply after conversion.\n\nThe separate version-1 Arrangement project uses the same own-data beat/voice/settings boundaries and one pure definition validator shared with live arrangements. Bounds include 8 MiB serialized input/output, 128 voices/256 KiB voice JSON, 16 layers, 32 sections and 65,536 total events. Parsing is inert; persisted musical definitions do not include live IDs, permissions, pending commands or DSP state. Host replay must explicitly load validated voices and start from a gesture.\n\nThe independent Standard MIDI file adapter validates native bytes, bounded chunks, PPQN timing and event framing. It does not provide a native MIDI driver, transmit SysEx or interpret uploaded bytes as executable content. Hosts must check file size before buffering, choose explicit channel-to-voice mappings, expose import warnings and reject unsupported export semantics instead of silently changing a score.\n\nOpt-in expressive SMF conversion separately bounds generated per-note fanout to 65,536 events. Frozen loss summaries separate omitted content, approximations and recognized source-message counts. Channel expression after a closed gate reports possible patch-dependent release-tail loss; strict unsupported policy rejects it. Export rejects incompatible channel ownership, unsupported controls and after-gate semantics rather than guessing. Explicit pitch-range policy is not RPN support.\n\n### External downloads and file uploads\n\nThe engine does not provide a voice-download API. Host loaders must validate the source and enforce an input budget before buffering or parsing. Engine checks cannot recover memory already consumed by an oversized download.\n\n- Check HTTP status and permitted origins before accepting a response; do not derive destinations from an untrusted voice name.\n- Treat declared MIME type, file extension and Content-Length as hints, not evidence that the payload is valid or bounded. Chunked and decoded/compressed responses still need a real byte budget.\n- Bound streamed response bytes and cancel an oversized transfer before assembling the complete buffer. Set host-appropriate cancellation and time limits.\n- Check a local file's size before arrayBuffer() or text decoding, then validate its actual contents. A picker selection is not a trust guarantee.\n- Do not insert filenames, imported descriptions or error messages into HTML. The examples display them as text; retain that separation in host UIs.\n- Apply application-level limits to repeated uploads, repeated parsing and simultaneous synth instances. A per-call limit is not a whole-page resource quota.\n\n### DX7 SysEx conversion\n\nimportDX7() and describeDX7() accept one Uint8Array containing a standard 163-byte single-voice or 4104-byte 32-voice bank message. Length, framing, checksum and seven-bit payload checks reject malformed input. The playground checks the maximum file size before buffering.\n\nThis is approximate six-to-four-operator voice conversion, not a DX7 emulator or an arbitrary SysEx interpreter. Conversion does not execute the payload. Review the returned descriptions and warnings before auditioning a patch; keep descriptions outside the strict voice schema.\n\n### PCM, offline rendering and WAV export\n\nencodeWav() encodes PCM; it does not decode/play uploaded audio. Own-data arguments are left, optional right, sampleRate and optional format (pcm16, pcm24, float32). Native Float32 samples must be finite in −1–1; stereo lengths match, with at most 4,000,000 frames per channel for this full-buffer convenience API.\n\nProject render results onto those fields rather than forwarding diagnostics or other metadata. Reject encoding/rendering errors visibly instead of substituting a fake result.\n\nCore rendering authenticates native Float32Array kind/length instead of overridable properties. Validate offset before deriving the default length; numeric coercion, proxies, forged channels and out-of-bounds ranges reject without advancing sound. Idle clearing uses the native fill method. Hosts still own the actual PCM storage and must not mutate it concurrently.\n\nOffline rendering allocates output buffers and executes synchronously. The host should limit duration, sample rate, envelope tails, concurrent requests and export frequency to suit its device and UI. Valid input may still consume significant time and memory; no universal deadline or whole-page memory guarantee is made.\n\nLong-score rendering and incremental createWavEncoder() use bounded reusable PCM/encoded chunks, not unlimited aggregate buffers. Consume borrowed PCM before advancing; declare an exact encoder frame count and finalize only after all frames. Encoder chunks are 1–65536 frames and RIFF32 files remain \u003c4 GiB; convenience rendering/encodeWav retain their 4,000,000-frame bound. Hosts must bound cumulative duration/work, retained bytes, concurrent renders and export frequency.\n\nrenderSequenceInWorker() validates before Worker allocation, transfers at most one unacknowledged encoded chunk and awaits sink writes before advancing. Capture intrinsic byte counts before handing ownership to a sink; storage workers may transfer/detach the buffers. Progress/abort callbacks are untrusted host behavior. Native AbortSignal cancellation terminates/rejects immediately without waiting for hung writes/abort/close. The sink owns invalidation and rollback; cancellation cannot retract persisted bytes. Acquire file sinks from trusted gestures, retain lifetime cancellation across picker/writable awaits, and clean up acquisition failures before starting a Worker. Never replace a long-file sink with unbounded Blob accumulation.\n\nCreate download URLs only for successfully encoded bytes. Replace/revoke obsolete Blob URLs and release them when the owning page or component is disposed. Treat filenames and rendered audio as potentially private application data.\n\n### AudioWorklet, scheduling and context ownership\n\n- Await start() in a user interaction and monitor onEvent or independent subscribe() listeners. A note ID is not admission; handle rejections, terminal states and processor errors. waitForCommand() is bounded, timed/cancellable and resolves admission only, not future execution; reset/close/failure invalidate pending waits.\n- The worklet bounds pending events and tracked note IDs to 256 each. Schedule bounded batches rather than flooding the port with distant-future events.\n- The optional effects worklet (worklet/fx-processor.js) has a separate strict own-data update/reset/close protocol. Chorus/reverb numbers and voice counts validate again before entering DSP; no command queue is retained and at most 32 commands are admitted per audio-frame boundary. Error replies are bounded and close remains available under flooding. MessagePort buffering before dispatch and many host-created instances remain host resource concerns.\n- Effects module loading uses the same secure same-origin URL rules and native CSP/MIME enforcement as synthesis; deploy the full matching tree. Effects never resume, suspend or close a borrowed context. Processor/decode failures surface through onEvent; dispose disconnects/stops only the insert.\n- Effects preallocate bounded seconds-scaled Float64 delay state (under 866 KiB at 192 kHz), sanitize NaN/Infinity PCM to zero, clamp internal state and flush tiny values. Offline tail-extended copies obey the existing 4,000,000-frame budget. Wet headroom is not a limiter for over-unity input or a host mix of many buses.\n- Each synth limits logical voices to configured 1–32 (default eight), with up to eight bounded stealing fades. Immutable eco/standard/high profiles change fixed internal sampling/filter work, not these bounds. These limits do not constrain an attacker creating many instances.\n- DSP state and terminal notification buffers are preallocated. Ended/error callbacks run after stable frame traversal; callback-admitted replacements start next frame, and recursive rendering rejects rather than extending a frame's work.\n- Named/prepared worklet registrations use 128 content-keyed LRU slots. Replacement revalidates before committing; queued/active notes own immutable old snapshots. Raw objects still validate freshly.\n- Absolute start/stop/control times are safely framed with a 60-second future horizon; stop precedes onset and controls. Command rejection is correlated by commandId; accepted means admission, not guaranteed future execution. Global allNotesOff/panic bypass full scheduled queues. Bound sequences and lookahead rather than flooding messages.\n- Immediate stop(id, { cancelControls: true }) removes only that ID's queued automation/onset/off before natural release; it cannot be combined with at. Musical Transport retains release-tail controls during ordinary playback but cancels its own future automation on pause/seek/loop rebuilding. Seeking is a musical restart, not an exact DSP snapshot.\n- Performance cleanup is part/helper scoped, not global panic. Repeated equal-pitch keys retain separate identities; stealing/reset cannot resurrect stale held/pedal state.\n- Handle rejected diagnostics promises and failed node initialization. Do not hide failure behind mock audio nodes or a fallback engine.\n- A shared AudioContext is borrowed: OPM disconnects its node but never closes/suspends the host context. close() is restartable; terminal dispose() clears owned helpers/subscriptions without taking ownership of a borrowed context. Hosts own downstream routing and permissions.\n- Clear host gate bookkeeping on global reset; close/failure need not reply with every terminal note. Default cancel removes old gates/automation on suspension/interruption; preserve is opt-in. Both policies stop lookahead and require gesture-driven explicit restart.\n- The recovery harness bounds local events/attempts and captures explicit device metadata, observations and manual scenario status. It uploads nothing. Automated desktop checks cannot certify physical phones or listening continuity; support-matrix cells without physical evidence remain unverified.\n- Provide obvious play/stop controls and begin at a comfortable, low host gain, especially with headphones. Finite samples and synthesis headroom do not guarantee safe listening volume.\n\n### Source maps, privacy and redistribution\n\nEvery distribution JS has a matching source map with embedded TypeScript and a compiler-generated declaration. Publishing maps intentionally makes the implementation source readable; minification is not secrecy. Never place credentials or private configuration in source that will be built or deployed.\n\nOPM.js does not provide an account, storage or telemetry service. Hosting providers and applications can still record request metadata or persist voices/audio. Document those host-specific behaviors separately and do not claim this library prevents data collection by the surrounding page.\n\nKeep each deployment's JS, maps and declarations from the same build. Review exact development dependency versions, build plugins, CI actions, package contents and license compatibility before changing the supply chain.\n\n## Threat model\n\n| Surface | Risk | Required controls |\n| --- | --- | --- |\n| External voice JSON or JavaScript objects | High | Type/shape/byte/count limits; finite bounds; own-data validation; no untrusted prototype merging |\n| Host download/upload and DOM code | High | Pre-buffer limits, permitted sources, text-only presentation and host-page XSS defenses |\n| Raw AudioWorklet messages | High | Strict message validation, bounded IDs/events and observable failure handling |\n| DX7 binary input and WAV PCM arguments | Medium | Bounded lengths, framing/checksum or sample validation; no native decoder |\n| Score-project JSON and Standard MIDI files | High | Byte/event/schema/framing limits, own-data options, detached validated voices and explicit unsupported-semantics handling |\n| DSP and offline rendering | Medium | Finite output, bounded per-instance work, duration/sample-rate budgets and error diagnostics |\n| Public scheduling API | Medium | Argument validation, admission handling, cancellation and bounded scheduling batches |\n| Build and deployment supply chain | Medium | Exact development pins, reviewed CI actions, complete matching assets and zero runtime dependencies |\n| Output audio and downloadable artifacts | Medium | User-controlled playback, conservative gain and host-managed privacy |\n\nAn AudioWorklet separates real-time processing from the main thread, but is not a security boundary against a compromised host page. Malicious host JavaScript can create nodes, spam messages, fetch data or manipulate the UI independently of OPM.js. Input validation and per-instance limits reduce accidental or adversarial input damage; they do not sandbox XSS or guarantee availability under arbitrary page load.\n\nThis policy addresses the library and checkout examples. Authentication, authorization, cookies, cross-origin access, backend storage, upload endpoints and rate limiting belong to the application deploying them.\n\n## Security review mechanism\n\n### 1. Review triggers (mandatory)\n\nA security review is required before:\n\n- [ ] Any release tag (vX.Y for zero patch versions, vX.Y.Z otherwise)\n- [ ] Merging a PR that touches: src/worklet/, src/core/, or voice parsing (src/voices/, src/api/ loaders)\n- [ ] Adding any dependency, build plugin, or CI action\n- [ ] Supporting a new voice-bank source (file import, URL, user paste)\n\n### 2. Review checklist\n\nA. Untrusted data (voice banks, config)\n- [ ] Voice JSON/key scaling, score projects, SMF/DX7 binary input, and WAV arguments validate type, shape, length, numeric bounds, and own-data properties before use\n- [ ] Explicit allowlisted copying; no spreading/merging untrusted objects into prototypes; no accessor invocation\n- [ ] Numeric bounds cover ADSR/live reanchoring, ratio/fixed-Hz controls, rate scaling, pitch envelope, LFO waveform/delay/sync/phase/targets and independent depth/rate, feedback, velocity/pan, levels and ramps; tuning/score/Transport/performance/encoder limits remain bounded and legacy shapes exclude later fields\n- [ ] Prepared identity cannot be forged, trusted patches stay deeply immutable, and public map snapshots cannot change stored patches\n- [ ] Application file/URL loaders bound sources, content type, and payload before buffering; engine does not fetch URLs\n- [ ] DX7 rejects invalid length/framing/checksum/seven-bit payload; conversion descriptions remain outside strict voice schema\n\nB. Worklet boundary\n- [ ] Every raw message is checked for strict own-data shape before reads, enqueueing, or rendering\n- [ ] No eval/Function execution or nonliteral dynamic imports in source/generated JS; configurable worklet/Worker URLs require secure same-origin assets and cannot weaken CSP/MIME validation\n- [ ] Duplicate IDs reject before enqueueing; tracked IDs/events remain bounded; ended/stolen/error notes remove obsolete off events\n- [ ] Held-note cancellation/release, bounded allNotesOff/panic, command admission/rejection, context/reset and processor failure propagate safely; teardown does not promise impossible per-note acknowledgements\n- [ ] Registration replacement, controls, tuning, score IDs/times and absolute scheduling validate own-data shape; queued snapshots, ordering and cleanup preserve bounds\n- [ ] Encoded Worker chunks require ordered acknowledgements, intrinsic byte accounting before sink ownership transfer, exact frame/file completion and termination on cancellation/failure; host sinks own rollback\n\nC. DSP loop safety\n- [ ] Output is finite: any NaN/Infinity in the render loop snaps to silence and increments an error counter (Synth.errorCount or renderNote()'s diagnostics.errors)\n- [ ] Fixed work ceiling: configured 1–32 logical voices (default eight) and at most eight fades, deterministic priority-aware admission/stealing, authenticated native output lengths and numeric ranges before work\n- [ ] Slot reset/recycling preserves identity and numerical state under callback admissions; terminal callbacks cannot replenish a frame's traversal or recursively render it\n- [ ] Determinism and chunk-independent output preserved; live/offline LFO, timing, filtering, stereo, velocity, and saturation parity\n- [ ] Deadline statistics exclude warmup and expose p95/p99/worst/misses without universal performance promises; spectral acceptance is controlled, not arbitrary FM alias-free proof\n\nD. Supply chain\n- [ ] Zero runtime dependencies still holds (npm ls --omit=dev is empty)\n- [ ] Dev dependencies and CI actions pinned to exact versions/commit SHAs\n- [ ] package.json files whitelist ships only intended dist/ assets, usage docs, and legal/project files\n- [ ] Standalone deployment retains the installed LICENSE alongside complete matching worklet/module assets; CSP/MIME/404 failures stay visible\n- [ ] Any npm publication requires separate maintainer authorization, protected approval, matching tag/version/commit/release and independent A/B/C/D/package evidence, plus a reviewed provenance-bearing publishing path; post-publication checks compare exact tarball bytes, validate source provenance and cryptographically verify signatures before registry-installed consumer acceptance. This checkout has no npm publishing workflow; local verification is not publication.\n\n### 3. Verification gates\n\nRun development commands from a source checkout after npm ci; installed npm packages do not contain the development tooling.\n\n- npm run build then npm test — source/distribution behavior, malformed-input, deterministic rendering, and spectral regression coverage\n- npm run typecheck — strict source, development tools/tests/demos, and NodeNext consumer against generated public declarations\n- npm run security -- --package-smoke — TypeScript compiler AST gate over source and Acorn gate over generated JS, exact development pins, zero runtime dependencies, isolated installed exports/type smoke\n- npm audit and npm audit --omit=dev — record actual tooling/runtime audit results; runtime dependency tree must be empty\n- npx --no-install playwright install --with-deps chromium then npm run browser-smoke -- chromium — real AudioWorklet smoke; Firefox/WebKit alternatives are configured separately\n- Headless Linux Firefox requires a running native audio server. CI starts PulseAudio and a null sink before real worklet smoke; it does not mock audio or relax browser permission/autoplay controls. Consult the usage guides for setup and stage diagnostics.\n- npm run benchmark — host-dependent 128-frame render deadlines, with optional explicit budget environment variables\n- npm run sound-quality / npm run voice-quality — numerical/register/velocity matrices and controlled spectral reports, not perceptual/hardware-fidelity proof\n- npm run browser-stress -- chromium — real bounded worklet contention/lifecycle/queue checks; timer/round-trip data are not CPU, GC or glitch counters\n- npm run vite-smoke — installed artifact, non-root production base, license, actual audio, CSP and negative MIME/404 scenarios; audit standalone tooling from examples/vite too\n- Follow publishing prerequisites before any authorized npm upload; source review cannot establish external environment protections, registry authentication or provenance\n\n### 4. Review record\n\nEach release notes in the CHANGELOG which checklist sections were exercised (A/B/C/D) and by whom. Checklist failures block release; waivers require a written reason in the CHANGELOG.\n\nFor v1.2, final A/B/C/D independent review and observed integration verification are recorded in CHANGELOG before release. Configured Node 22/24/26 and Chromium/Firefox/WebKit CI jobs are capabilities, not evidence of an external run. Missing browser binaries/host support or unrun CI must be reported, not represented as passing coverage.\n\nv1.5 feature-completion review: InputBoundaryReview covered A/B; DspSupplyReview covered C/D. The integration owner reproduced and fixed null mixGain/tuning acceptance and overridable render-buffer metadata, then also prevented offset coercion before default-length calculation. Runtime evidence and remaining registry/physical-device prerequisites are recorded separately in CHANGELOG; static review is not a claim of runtime coverage.\n\nFinalInputsReview (A/B) and FinalDspReview (C/D) rechecked the final hardened source and release controls, with no new evidence-backed findings. Their PASS applies to scoped static inspection only; they ran no build, payload, tests, audit or browser commands.\n\nv1.5 release review: Release15Inputs approved scoped static A/B inspection; Release15DspSupply approved C/D for GitHub distribution of package 1.5.0, with no evidence-backed blockers. Neither reviewer executed runtime gates or certified external npm settings. The integration owner's fresh package, native browser, numerical, audit and report-only benchmark evidence is recorded in CHANGELOG.\n\nv1.5 interruption-repair recheck: Release15Inputs initially found that deferred running notifications could leave deduplication state stale. Both initial and existing-node resume now synchronize successful state observation; repeated fully deferred transitions and pending diagnostics are covered by consumer regressions. Release15Inputs then approved A/B and Release15DspSupply approved C/D without remaining scoped static findings. The original Linux WebKit CI failure blocks publication until repaired-candidate verification; static approval is not a waiver or runtime certification.\n\nv1.6 release review: a security review found two command-waiter boundary issues (abort-signal state/listener shadowing and receipt-eviction handling around correlated panic resets). Both were fixed with regressions and rechecked natively against a borrowed context. No fresh independent A/B/C/D approval was obtained for v1.6; the maintainer explicitly authorized the GitHub release on the integration owner's runtime evidence, recorded as a waiver in CHANGELOG. npm publication still requires the independent review record described in doc/publishing.md.\n\nUnreleased seven-capability checkout review: SecurityDataReview approved scoped A, SecurityProtocolReview B, SecurityDSPReview C, and SecuritySupplyReview D static inspection. Initial findings about release-tail controls, sink-transferred buffer accounting and deferred file-picker/writable teardown were fixed and the affected source rechecked without remaining scoped findings. These reviewers executed no tests, builds, audits or browser commands; their PASS does not certify runtime behavior, deployment permissions, physical devices or registry protections. The integration owner's observed tests, package gates, native browser smoke and report-only CPU measurements are recorded separately in CHANGELOG. This is not a release approval or a new waiver.\n\nv1.7 release review: Release17Data approved scoped static A, Release17Protocol B, Release17DSP C, and Release17Supply D, with no evidence-backed blockers in the final seven-capability source and 1.7.0 release metadata. All four reviews were read-only and executed no runtime gates. Fresh isolated package/build/test/type/audit/quality checks and managed native Chromium AudioWorklet observations are recorded in CHANGELOG. Remote CI must pass before the release tag is published; its run URL and outcome belong in the GitHub release record. No waiver or npm authentication/provenance/external-protection certification is asserted.\n\nv1.8 review record: InputProtocolReview approved scoped static A/B and DspAssetsReview scoped static C/D after the bounded arrangement, MIDI own-data, finite tempo-slope and canonical asset-overlap fixes. Their static approval does not certify runtime or external npm settings. The release commit d793d69056c68a971ede1af988338412687227fd has an observed successful Node 22/24/26 and Chromium/Firefox/WebKit CI run; the separate review record and release package record retain the evidence and tarball digest. This is not registry provenance, physical-device/MIDI or listening acceptance.\n\nv1.8.1 documentation/package patch review: Patch181Inputs approved scoped static A/B and Patch181DspSupply scoped static C/D, with no evidence-backed blockers. Reviewers were read-only and ran no build, tests or external actions. The integration owner confirmed the source diff against v1.8 changes only src/version.ts; runtime validation, protocols and DSP are unchanged. Final regenerated assets, package gates and remote CI are separate release prerequisites, recorded in CHANGELOG and the GitHub release notes. No npm provenance, physical-device/MIDI or listening certification is asserted.\n\nUnreleased integration-quality review: FinalImprovementInputs approved scoped static A/B after the pedal-ownership and applied-workload evidence fixes. FinalImprovementDspSupply approved scoped static C/D specifically for an isolated immutable canonical candidate (47 runtime modules with 141 matching distribution assets), after generated-link validation and the DOM-free core/project/MIDI type boundary were hardened. No surviving scoped findings remain. Both reviewers were read-only and executed no runtime gates or publication. The integration owner separately observed 381 passing behavioral tests, type checks including installed DOM-free Node consumers, numerical/dependency/package checks and native Chromium UI/smoke/stress checks. Unexpected numbered generated-file copies were preserved outside the checkout; live-tree packaging remains blocked by their reappearance and is not covered by the isolated-candidate approval. Desktop automation and numerical analysis do not certify physical devices, physical MIDI or human listening. No release approval, npm authentication/provenance/registry protection, physical-device or listening certification is asserted.\n\nv1.9 release review: Release19Inputs approved scoped static A/B and Release19Supply scoped static C/D for an isolated canonical package 1.9.0 candidate, with no evidence-backed security findings. Reviewers ran no runtime gates. Numbered live-tree generated copies are not approved for packaging and must remain outside the candidate. GitHub publication requires exact clean-commit artifacts, passing remote Node/browser CI and an archive digest retained in the release record. Physical-device/MIDI, human listening and npm provenance remain unverified and are not claimed by this GitHub-only release.\n\nv1.9 deadline repair: Release19Supply rechecked and approved scoped static C/D topology-cache/pooling/render changes after the initial release candidate failed the Node 22 prepared-burst p99 gate. Fixed slot-local caches derive only from the existing validated graphs, overwrite all edges, preserve summation order and retain finite checks, bounded fades and reentrant callback behavior. The reviewer executed no checks; this is not a performance waiver. The repaired exact commit must pass the unchanged remote gates before tagging.\n\nUnreleased readiness review: ExpressionSecurity approved scoped static A/B for final expressive SMF boundaries, ownership and loss accounting; ArrangementCaptureSecurity approved scoped A/C/D for portable/shared arrangement validation, lifecycle, inert imports, pure core contracts and bounded text-only local MIDI capture. No evidence-backed findings remained. Reviewers executed no gates. The integration owner separately observed 403 passing tests, installed package/types/security/audit checks, 108 bit-identical baseline renders, scoped capacity measurements and native isolated Edge UI/worklet checks, recorded in CHANGELOG. Physical phones/controllers and genuine eighteen-preset listening originals remain blocked/unverified. This is not release/publication approval.\n\nv1.10 release review: Release110Inputs approved scoped static A/B and Release110DspSupply scoped static C/D for an immutable canonical package 1.10.0 candidate (48 runtime modules and 144 matching distribution assets), with no evidence-backed blockers. Both approved the final documentation/generated-HTML delta. Reviewers ran no runtime gates. The integration owner separately observed 403 behavioral tests, public/DOM-free core types, numerical matrices, zero-vulnerability development/runtime/Vite audits, isolated installed-package/asset/type gates, expressive MIDI-to-Arrangement PCM/WAV smoke and the unchanged isolated p99 deadline gate. Unexpected numbered live-tree generated copies are preserved and excluded. Publish only exact committed canonical assets after remote Node/browser CI passes; retain the commit, CI URL and archive digest in the GitHub release record. This is not npm provenance or physical-device/MIDI/listening certification.\n\nv1.11 review: SecInputsProtocol approved scoped static A/B (voice v7 validation, .opm parser, MIDI program/RPN boundaries, synth and effects worklet protocols, effects URL/lifecycle) with no evidence-backed vulnerability. SecDspSupply returned approved-with-fixes C/D with low/informational findings only: the deployment inventory now requires worklet/fx-processor.js (with a rejection test), the effects headroom documentation is qualified as a steady-state ceiling (a feedback decrease can briefly exceed 0.7 during its 20 ms ramp), the input table above now covers v7 and .opm, and capacity candidates are documented as sine-operator measurements. Reviewers ran no runtime gates. The integration owner separately observed the typecheck, the full behavioral suite, the npm run security gate, and a real Chromium AudioWorklet smoke of the noise and waveform voices through the effects insert. Physical devices, MIDI hardware and human listening remain unverified; this is not a release, npm publication or deployment record.\n\nv1.11.1 review: SecFastSin approved a scoped static review of the fastSin polynomial and all-sine render fast path (NaN/Infinity containment, |value| ≤ 1 bound, determinism, O(1) cost, state parity with the generic path) with no evidence-backed vulnerability. The reviewer ran no runtime gates; the integration owner ran tests, typecheck, the package-smoke security gates and a Chrome AudioWorklet smoke.\n\n## Non-goals\n\n- Sandboxing the host page (OPM.js runs in the page's trust domain; XSS protection is the embedder's job)\n- DRM / copy protection of voice banks\n- Remote code loading of any kind — never supported\n","href":"./security.html"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · SECURITY.md — OPM.js Security Policy & Review Mechanism","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism SECURITY.md — OPM.js Security Policy & Review Mechanism","href":"./security.html#securitymd--opmjs-security-policy--review-mechanism"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Supported versions","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Supported versions","href":"./security.html#supported-versions"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Reporting a vulnerability","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Reporting a vulnerability","href":"./security.html#reporting-a-vulnerability"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Safe embedding","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Safe embedding","href":"./security.html#safe-embedding"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Website deployment and browser permissions","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Website deployment and browser permissions","href":"./security.html#website-deployment-and-browser-permissions"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Voice banks and JavaScript objects","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Voice banks and JavaScript objects","href":"./security.html#voice-banks-and-javascript-objects"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Score projects and Standard MIDI files","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Score projects and Standard MIDI files","href":"./security.html#score-projects-and-standard-midi-files"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · External downloads and file uploads","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism External downloads and file uploads","href":"./security.html#external-downloads-and-file-uploads"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · DX7 SysEx conversion","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism DX7 SysEx conversion","href":"./security.html#dx7-sysex-conversion"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · PCM, offline rendering and WAV export","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism PCM, offline rendering and WAV export","href":"./security.html#pcm-offline-rendering-and-wav-export"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · AudioWorklet, scheduling and context ownership","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism AudioWorklet, scheduling and context ownership","href":"./security.html#audioworklet-scheduling-and-context-ownership"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Source maps, privacy and redistribution","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Source maps, privacy and redistribution","href":"./security.html#source-maps-privacy-and-redistribution"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Threat model","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Threat model","href":"./security.html#threat-model"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Security review mechanism","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Security review mechanism","href":"./security.html#security-review-mechanism"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 1. Review triggers (mandatory)","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 1. Review triggers (mandatory)","href":"./security.html#1-review-triggers-mandatory"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 2. Review checklist","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 2. Review checklist","href":"./security.html#2-review-checklist"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 3. Verification gates","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 3. Verification gates","href":"./security.html#3-verification-gates"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · 4. Review record","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism 4. Review record","href":"./security.html#4-review-record"},{"title":"SECURITY.md — OPM.js Security Policy & Review Mechanism · Non-goals","text":"SECURITY.md — OPM.js Security Policy & Review Mechanism Non-goals","href":"./security.html#non-goals"},{"title":"Changelog","text":"Changelog # Changelog\n\nFor installation and executable examples, see the README and the English / 繁體中文 usage guides.\n\n## Unreleased\n\n### Half-band FIR decimation\n\n- Replace the per-voice Butterworth IIR decimator (cutoff 0.30 output Fs) with cascaded polyphase linear-phase half-band FIR stages: eco 35 taps at 2×; standard 15 → 47 taps; high 11 → 15 → 47 taps. Substep calls only buffer their input; the cascade runs once per output frame.\n- Passband is flat to 0.40 output Fs (ripple below 0.001 dB for standard/high; formerly −11.5 dB at 0.35 Fs). Products folding below 0.40 Fs are rejected by at least 81 dB for standard/high and 63 dB for eco (standard formerly −52.3/−55.6 dB at 0.60/0.625 Fs). Between 0.50 and 0.60 Fs the transition band folds into 0.40–0.50 Fs, and far above Nyquist the former filter's bilinear zero was deeper; see acoustic quality.\n- Behavior change: output is delayed by a constant linear-phase 13.25 output frames for standard (8.5 eco, 13.875 high; about 0.28 ms at 48 kHz) instead of the IIR's frequency-dependent 2.7–3.3 frames. The decimator keeps 158 state scalars instead of 8 (per output channel since post-mix decimation, below).\n- Measured on this checkout's cloud container with npm run benchmark (median of four interleaved runs per side): standard 8 held voices 0.702 → 0.669 ms per 128-frame block, high 8 held voices 1.204 → 1.116 ms, eco unchanged within noise (0.434 → 0.435 ms).\n\n### Band-limited square, saw and half-wave operators\n\nSquare and saw operators now add two-sample polyBLEP residuals at their jumps, and half-wave operators add polyBLAMP residuals at their corners, sized from each operator's unmodulated phase step at the oversampled rate. In unit tests the folded-alias power of periodic frames drops by about 8–17 dB at the internal rate, before the decimator. Phase modulation makes the nominal step an approximation, so these shapes remain not alias-free. All non-sine periodic shapes now use the deterministic polynomial fastSin instead of Math.sin, and the shape helpers are split so the dispatcher stays inlinable. Measured on Node 22 with 16 held voices at 48 kHz (standard): square 1.74 → 0.98 ms/block, half 1.89 → 1.40 ms, saw 1.04 → 1.13 ms. Sine output and periodicWaveform(theta, code) without a step are unchanged apart from fastSin replacing Math.sin (~1e-10). Square/saw/half voices render differently near their edges. These timings are one Node process on one machine, not a browser AudioWorklet or device measurement.\n\n### Post-mix decimation\n\n- Voices are now panned and summed to left/right at the oversampled rate, and one shared decimator per output channel runs after the mix, instead of one decimator per voice. A centered (dual-mono) mix filters once until the channel inputs differ. Measured against per-voice half-band FIR decimation with a 128-frame Synth.render loop at 48 kHz (median of three interleaved runs, one Node 22 process on one cloud x64 host): standard 8 voices 0.677 → 0.611 ms/block (−10%), 32 voices 2.772 → 2.477 ms (−11%); high 8 voices 1.115 → 0.996 ms (−11%), 32 voices 4.626 → 4.154 ms (−10%); eco 32 voices unchanged; a single voice costs about 0.01 ms/block more (mix-buffer overhead). Against the earlier per-voice Butterworth decimator the gains were 9–19%.\n- Behavior change: note gain, expression, pan and steal fades now apply before the decimator, so a hard gain step is band-limited by it and rings through the half-band FIR (at most 26 output frames) instead of cutting instantly at the output rate. Ramps are within s·p·Σk|h[k]|/4 of the former output-rate product.\n- Voices retire on their envelope end (all operator releases finished) and report ended without waiting for filter history; the shared decimator drains the tail; once nothing is sounding, history below the −96 dB floor snaps to exact zero.\n\n## v1.11.1\n\nPackage version 1.11.1, a GitHub release (not on npm). Performance patch over v1.11; no API, voice-format (still v7) or protocol changes. Output differs from v1.11 by about 1e-10 for sine operators. Security: scoped static review of the src/core change found no evidence-backed issue.\n\n### Faster sine operators\n\nSine operators now use a deterministic polynomial fastSin (odd degree-13 after reduction to ±π/2) instead of Math.sin, and all-sine voices with static operator levels and shared AM take a branch-light render path. Measured on Node 26 with npm run benchmark: standard 8 held voices 0.265 → 0.215 ms/block (−19%), 32 voices 1.132 → 0.900 ms (−20%), high 32 voices 1.977 → 1.515 ms (−23%). fastSin is bounded by 1 with |error| ≤ 2.0e-10 against the exact sine (about −194 dB, below Float32 resolution) plus about 1e-16·|phase| from range reduction, and is pure arithmetic, so results no longer depend on each JavaScript engine's Math.sin rounding. Rendered samples differ from v1.11 at the ~1e-10 level; non-sine operator shapes, LFOs and effects still use Math.sin. These timings are one Node process on one machine, not a browser AudioWorklet or device measurement.\n\n## v1.11\n\nPackage version 1.11.0, published as a GitHub release (not on npm). Voice format v7 (per-operator waveforms and held noise; v1–v6 inputs still accepted), optional stereo effects, MIDI program maps with RPN bend sensitivity and approximate .opm import. Remote CI for the release content (commit 64ed642) passed on Node 22/24/26 and Chromium/Firefox/WebKit: https://github.com/JS-PACKAGE/OPM.js/actions/runs/37117509642. An isolated git archive build passed npm run security -- --package-smoke. Physical-device, MIDI-hardware and human-listening acceptance remain unverified, and GitHub distribution is separate from npm publication. The repository now lives at JS-PACKAGE/OPM.js; links to historical CI runs and review issues keep their originally recorded addresses.\n\n### Approximate OPM text import\n\n- Add bounded dependency-free importOPM/describeOPM for decimal VOPM/MXDRV-family text patches, returning normalized v7 voices and separate deterministic metadata/loss warnings.\n- Preserve all eight named operator graphs, raw key-on SLOT masking, TL attenuation, coarse detune and per-operator AM enable. Document musical envelope/DT1/key-scaling/LFO approximations, omitted second decay, ignored pan and extension-only C2 noise; no register-level emulation or fidelity claim.\n- Add original recipe, finite deterministic render, topology, parameter-table, naming and hostile-input regression tests.\n\n### MIDI voice selection and bend sensitivity\n\n- Add opt-in bounded program-to-named-FM voice maps to file and live adapters, plus file-only drum-note maps and frozen artistic GM-family/percussion starter maps. Note-on voice selection leaves sounding gates untouched; unmapped programs retain fallback/ignore policies. File loss summaries distinguish honored program changes from omissions.\n- Behavior change: preserve-mode file import and live MIDI now decode channel-local RPN 0 pitch-bend sensitivity, including cents, clamped to 48 semitones. Null/NRPN selection disables data entry; live reset restores the initial range. CC100/101 are reserved; CC6/38 custom mappings still apply when RPN 0 is inactive. Export emits neither program changes nor RPN.\n- Add optional configurePart preservation policy for future-onset patch changes without changing default clear-on-configuration behavior.\n\n### Voice v7 waveforms and held noise\n\n- Canonicalize legacy v1–v6 voices to v7 while preserving their original field restrictions and unchanged sine rendering.\n- Add per-operator sine/half/abs/quarter/alternating/camel/square/saw and deterministic 17-bit held-noise oscillators, with 20–20000 Hz noise clocks and pooled admission resets. Noise on any operator is an OPM.js extension, not a chip-fidelity claim; discontinuous shapes are not alias-free.\n- Preserve waveform/noise fields through immutable preparation, bank JSON and worklet registration; add original noise-snare, noise-hihat and explosion recipes with percussion metadata and designer controls.\n\n### Optional stereo effects\n\n- Add original pure-DSP fractional-delay chorus and damped-comb/all-pass stereo reverb, shared by offline createStereoEffects/tail-extending applyEffects and an opt-in dedicated createEffects AudioWorklet insert.\n- Preserve existing synthesis and dry PCM; validate bounded own-data parameters, smooth complete configuration replacements, normalize wet gain for headroom and sanitize malformed PCM without poisoning delay state.\n- Extend the independent-buses demo with live chorus/reverb toggles and matching offline stem effects; document routing, gain law, bounded memory/tails and the separate worklet security surface.\n\n### Release hygiene and verification\n\n- The deployment inventory now requires worklet/fx-processor.js; the independent review record is in SECURITY.md (A/B approved; C/D low and informational findings addressed: deployment inventory, steady-state effects headroom wording, v7/.opm input table, sine-only capacity scope).\n- Repository links now point at JS-PACKAGE/OPM.js; historical CI-run and review-issue links keep their recorded addresses.\n- Integration owner observed: strict typecheck (source, tests, demos, generated public and DOM-free core types), 438 passing behavioral tests, the zero-dependency/dynamic-code security gate, and a real Chromium AudioWorklet smoke in which noise-snare, explosion and a camel-waveform voice produced non-silent output through the effects insert while the context stayed running; the sound designer, buses and audition pages loaded without console errors. Not run: Firefox/WebKit, physical devices, MIDI hardware, human listening, benchmark capacity for non-sine voices, installed-package/Vite smoke and npm publication.\n\n## v1.10\n\nPackage version 1.10.0. Portable arrangements, expressive MIDI exchange and scoped acceptance tooling; voice format remains v6. Historical tags and archives remain immutable. GitHub distribution is separate from npm publication and site deployment.\n\n### Portable music and expressive file exchange\n\n- Add a separate bounded version-1 Arrangement project format for named looping layers, sections, initial section, tempo/meter, validated voices and synthesis settings. Share definition validation with live Arrangement playback; save/load reconstructs music, never DSP state or pending live commands.\n- Add opt-in MIDI-file expressive controls while preserving the default note-only conversion policy. Preserve supported channel/key expression under explicit bend-range policy and expose grouped omissions, approximations and preserved wire-message counts; reject unrepresentable export semantics.\n- Add host/workload-scoped benchmark capacity candidates across quality and fully occupied voice limits. Require explicit p99 reserve, zero observed misses and clean diagnostics; retain raw worst/misses and unchanged baseline CI budgets. Candidates do not certify browser/physical-device performance.\n- Consolidate public project/MIDI surfaces, ID/lifecycle/ownership contracts and the distinction between saved musical definitions, running sessions, numerical safety and human acceptance. Synchronize current-release resources while keeping maintenance tools and HTML examples checkout-only.\n- Reuse the mobile and audition harnesses for explicit current-version acceptance campaigns. Add bounded local physical-MIDI observation downloads to the instrument page, recording exact submitted/observed settings, text-only identifiers and separate manual judgments; desktop, active, stale-scope, abandoned and truncated runs remain unverified.\n\n### Acceptance boundaries\n\n- Physical iOS/Android interruption/route/long-play acceptance, real MIDI hardware/permission/hot-unplug behavior and the eighteen presets' genuine human listening findings remain blocked pending original physical/listener observations. Local UI, numerical rendering and injected MIDI checks must not promote those results or trim metadata.\n- This release does not certify physical devices, human listening, npm publication or deployment. Genuine original observations are required before support or trim metadata can be promoted.\n\n### Pre-release checkout verification\n\n- Integration owner: build, 403 behavioral tests, strict generated public and DOM-free core types, security AST/dependency gates, isolated installed package/asset CLI/type smoke, and development/runtime audits (zero reported vulnerabilities) passed.\n- 108 stereo renders (all eighteen bundled presets × eco/standard/high × 44100/48000 Hz, fixed note/velocity/pan/gate) remained bit-identical to the published checkout's committed distribution. This establishes those exercised defaults, not every possible score.\n- Expressive SMF export/import preserved gate-internal bend/expression/pan/modulation under explicit policy, reported no losses for that representable fixture, rendered 31680 finite stereo frames and encoded a 126764-byte WAV; after-gate export rejected.\n- Native isolated Microsoft Edge 154.0.4258.48 UI verified save → load without autoplay → explicit replay, edited gain/section persistence, arbitrary imported layer/section names, quantized switches with fades, literal HTML-looking capture metadata, desktop manual-pass remaining unverified and scope invalidation. No page errors were observed; screenshots were inspected. Automation is not hardware/listening evidence.\n- A separate native Edge boundary smoke confirmed active exports stay unverified, 270 synthetic host actions retain 256 observations with 16 counted drops, 24 finished runs disable further capture, and a deliberately deferred injected permission result after Dispose cannot resurrect the adapter/audio. These synthetic checks do not exercise a physical permission prompt/controller.\n- The existing browser smoke and stress bodies also passed using a throwaway launch override selecting isolated Edge (not mocked audio, no weakened gates): real stereo/pan analyser signal, suspend/resume/close ownership, 768 dense notes, bounded contention, expected late rejection and final zero active voices/pending events/DSP errors. The smoke fixture logged a forbidden resource request while its audio/lifecycle assertions passed; the separate example UI had no page errors.\n- The 29-row Apple M5 / darwin arm64 / Node v26.7.0 benchmark used 48000 Hz, 128 frames, 300 excluded warmup blocks and 2000 measured blocks per row. All rows retained clean diagnostics and zero observed misses. The fully occupied held-brass matrix selected eco/32, standard/32 and high/16 as highest sampled candidates under 50% p99 reserve; high/32 was ineligible. The complete matrix and its scope are in acoustic-quality.\n- ExpressionSecurity approved scoped static A/B, and ArrangementCaptureSecurity scoped A/C/D, with no evidence-backed findings. Reviewers ran no runtime gates. These reviews do not approve publication or certify hardware/listeners.\n- The pinned Playwright Chromium launcher initially failed because its cached macOS Framework was missing; forced reinstallation downloaded but timed out during installation. Managed browser sessions also detached. Native Edge supplied the observed UI checks; these are not successful runs of the pinned Chromium binary.\n\n### Release-candidate verification\n\n- Package 1.10.0 passes 403 behavioral tests, the 144-asset build and strict public/DOM-free core type checks. Development, runtime and standalone Vite tooling audits report zero vulnerabilities; the runtime dependency tree is empty.\n- An isolated canonical candidate passes parsed source/generated-code, installed-package, asset CLI and installed public/DOM-free Node type gates. The local archive verifier validates matching version, bounded safe paths and complete JS/map/declaration triplets; registry publication/provenance is not claimed.\n- An installed consumer exports an 83-byte expressive SMF, imports it with no reported omissions/approximations, saves/reloads an inert Arrangement project, renders 10,560 finite deterministic stereo frames and writes a 42,284-byte WAV. Default note-only conversion and after-gate control rejection are also exercised.\n- Numerical sound/preset quality commands pass. A separate isolated 29-row benchmark passes the unchanged p99 deadline ratio 1; host measurements do not certify physical-device continuity. The earlier concurrent report-only run is not a new capacity recommendation.\n- The live-tree package gate correctly rejects unexpected numbered generated copies that reappeared during verification. They are preserved and excluded: release artifacts must be rebuilt from the exact committed canonical tree in isolation.\n- Release110Inputs approved scoped static A/B, and Release110DspSupply scoped static C/D, for the immutable canonical 1.10.0 candidate without evidence-backed blockers. Both also approved the final documentation delta, including generated HTML and the Map-based score compilation recipe. Reviewers executed no runtime gates; passing remote Node/browser CI remains mandatory before tagging.\n\n\n## v1.9\n\nPackage version 1.9.0. Integration and musical workflows release; voice format remains v6. Historical releases remain immutable. GitHub distribution is separate from npm publication; no npm publication is performed.\n\n### Integration and musical workflows\n\n- Add bounded Transport startupLead for cold starts and reconstructed playback. The musical position stays at the requested beat during count-in; genuine late/drop errors remain visible.\n- Add independent per-note and Arrangement layer gain, quantized gain changes and section fades/crossfades, including existing notes and release tails without overwriting authored expression.\n- Add declarative MIDI CC mappings with scalar/operator targets and captured-baseline reset. Adapter-owned notes can be released under a held pedal without cancelling another input's keys or pedal state.\n- Add version-1 self-contained score projects, deterministic serialization and a shared tempo-integrating beat compiler for live/offline workflows.\n- Add the independent opm.js/midi-file SMF type 0/1 PPQN import/export adapter, with bounded parsing and explicit warnings or rejection for unsupported content.\n- Keep beat-event type ownership in the environment-independent core. Separate Node consumer checks compile core/project/MIDI declarations without DOM or Web Audio globals, including an installed-package check.\n- Add two original short pieces with play/stop, project save/load and Worker PCM16 WAV export. These are musical examples, not a listening-quality certification.\n\n### Documentation and acceptance\n\n- Generate navigable, locally searchable HTML guides and a full compiler-derived API reference, with prominent online-demo links. Markdown remains the documentation source.\n- Add condition-grouped device and human-listening evidence review tools. Conflicting, incomplete, automated and unassessed evidence does not become a physical-device or listening PASS.\n- Capture the actually applied gain/tuning after Play and after partial settings failures, rather than stale session defaults.\n\n### Observed verification and review boundaries\n\n- Local behavioral suite: 382 passing tests. Dependency audit: zero vulnerabilities; runtime dependency tree: empty. Numerical sound-quality checks cover 2,505 matrix rows and 24 live-control cases; preset-quality output is accepted.\n- Native Chromium AudioWorklet observations cover beat-0 startup, gain mute/resume without retrigger, pedal-owned MIDI cleanup, both original pieces and project round trips. The actual Arrangement UI produces signal → exact silence → signal for a solo pad layer and switches/fades sections across pause/resume. Project JSON and a valid Worker PCM16 WAV were downloaded to disk; the documented Node chunk-WAV recipe also produced a finite, correctly framed file.\n- Default-gain PCM remains bit-identical in 333 baseline comparison renders. Node CPU measurements are report-only host observations, not real-time or mobile support guarantees.\n- FinalImprovementInputs approved scoped static A/B; FinalImprovementDspSupply approved scoped static C/D for an isolated immutable canonical candidate after the pedal ownership, workload-recording, generated-link and DOM-free type-boundary fixes. Reviewers ran no runtime gates. Earlier numbered generated-file copies were preserved outside the checkout; release artifacts must be built from the exact clean release commit.\n- The isolated candidate passes the 141-asset build, all behavioral/type checks and installed-package/runtime/asset/DOM-free-Node declaration gates. Native Chromium 150 smoke and the bounded 768-note stress gate pass with zero DSP errors; the intentional late/drop remains counted. HTML search and declaration-anchor navigation also work from file:// at a 390-pixel viewport without page overflow.\n- Native matched/dry A/B previews produce finite signal, but numerical measurement leaves every human criterion unassessed and refuses unconfirmed/empty finding exports. The actual desktop device capture validates as 0/36 PASS and remains unverified; neither observation is physical iOS/Android, physical MIDI or human-listening evidence.\n- Physical-device captures and genuine human listening findings are still required before those acceptance statuses or trim recommendations can be promoted. This GitHub release does not imply deployment, npm publication or physical-device/listening certification.\n- Release19Inputs approved scoped static A/B; Release19Supply approved scoped static C/D for an isolated canonical 1.9.0 candidate, with no evidence-backed security findings. Neither reviewer executed runtime gates. The GitHub release record retains the exact release commit, observed remote CI outcome and archive SHA-256; npm publication is excluded.\n- Initial release candidate 43f32549a000acdae6dd5a83aa690f97e4c42575 was not tagged: its remote CI failed Node 22 prepared-burst p99 (3.131281 ms versus a 2.666667 ms deadline), while Node 24/26 and all three browser jobs passed. No waiver or budget relaxation was used.\n- Cache operator edges and carriers in preallocated voice-slot storage at admission, preserving topology order and avoiding repeated frozen-array indexing in oversampled rendering. Keep phase/sample finite checks; add cross-topology/profile recycling coverage. A 288-case before/after smoke is PCM bit-identical across qualities, rates, stealing, gain ramps and chunk boundaries. Local Node 22 prepared-burst p99 is 1.238708 ms with zero missed deadlines; the final remote release gate remains authoritative.\n\n## v1.8.1\n\nPackage version 1.8.1. Documentation and packaging patch; no runtime API, voice-format, DSP or scheduling behavior changes. The original v1.8 tag and archive remain immutable. GitHub release distribution is not npm registry publication.\n\n### Documentation and packaging\n\n- Ship the synchronized README, bilingual usage guides and topical integration/deployment guides in a separately versioned installable archive.\n- Correct public voice budgets, priority admission, grid-helper import locations, DX7 AM conversion limits, Worker startup/phase contracts and complete static-asset deployment instructions.\n- Replace links to package-excluded HTML/source/tooling with explicit checkout-only repository resources and provide runnable default-voice arrangement recipes.\n- Leave a one-beat count-in in Transport recipes and document cold-start beat-0 late:'drop' rejection. This patch does not fix or hide the runtime timing limitation.\n- Align package/lock/source VERSION, installation filenames and release-specific deployment paths to 1.8.1; regenerate compiler-owned distribution assets.\n\n### Security and observed release-candidate verification\n\n- Patch181Inputs approved scoped static A/B and Patch181DspSupply scoped static C/D, with no evidence-backed blockers. Both reviews were read-only and ran no runtime or external gates; no waiver is asserted.\n- An isolated 1.8.1 candidate passed build, all 310 behavioral tests, strict source/development/installed-public-contract type checks, source/generated AST security gates and installed-package rendering/WAV/types/CLI checks. Full and runtime audits reported zero vulnerabilities; runtime dependencies remain empty. Standalone Vite tooling audits also reported zero vulnerabilities.\n- Fresh sound-quality passed 2,505 matrix cases and 24 live-control cases with independent spectral/filter/streaming gates; voice-quality completed successfully. The benchmark ran with no acceptance budgets and remains report-only, not a host deadline or physical-device certification.\n- Fresh generated output differs from the preceding distribution only in the three VERSION JS/map/declaration companions; all other deployed assets are byte-identical. The source diff against v1.8 changes only src/version.ts.\n- Sandboxed, muted native Chromium 150 passed the existing compiled browser smoke, 768-note stress and installed-package Vite subpath/CSP/lifecycle/missing-asset/wrong-MIME assertions using a throwaway executable-launch adapter; test assertions and runtime code were unchanged. A fresh pinned Chromium download timed out after receiving the archive, so this is explicit native-executable evidence, not a PASS for that download or the default cached-browser command.\n- The exact installed archive resolved all eleven public import specifiers and rendered finite nonzero PCM with zero errors at 32 voices in eco/standard/high, protected high-priority notes, rejected lower priority and round-tripped a linear tempo map/grid. The actual asset CLI copied/reused 134 files and checked 133 served assets with matching bytes, MIME and nosniff.\n- Exact README/English/Chinese browser quick starts reported VERSION:1.8.1, finite nonzero PCM and no browser errors or note rejections; screenshots were inspected. Checkout documentation passed relative file/anchor checks and pinned repository-resource validation.\n- Release publication is gated on observed remote Node.js 22/24/26 and Chromium/Firefox/WebKit CI for the exact release commit. Its run URL/outcome and the final archive digest are retained in the GitHub release notes. npm registry/provenance, physical-device/MIDI behavior and human listening remain separate unverified prerequisites.\n\n## v1.8\n\nPackage version 1.8.0. GitHub release distribution and npm registry publication are separate operations; neither implies physical-device acceptance.\n\n### Engine and integration\n\n- Add priority-aware voice admission and opt-in 1–32 logical voices, retaining the default eight-voice engine and at most eight stealing fades.\n- Add beat-linear tempo ramps, quantization/swing helpers and adaptive looping layers with boundary-quantized section changes and scoped cleanup.\n- Add per-key/per-part expressive controls, per-part voice budgets and an optional, explicitly requested non-SysEx Web MIDI adapter.\n- Add Worker readiness, optional startup timeout and host-local phase diagnostics without weakening chunk acknowledgements or cancellation.\n- Add the dependency-free opm-assets deployment/check CLI, canonical filesystem overlap protection, and a bounded physical-device evidence review command.\n- Expand all-profile acoustic references, selective-LFO original presets, audition controls and local human-finding exports. Add adaptive, instrument, independent-bus/stem and FM-designer examples.\n\n### Security and observed verification\n\n- InputProtocolReview approved scoped static A/B and DspAssetsReview scoped static C/D after correcting arrangement expansion/terminal cleanup, MIDI allowlist own-data handling, nonfinite linear slopes and symlinked deployment overlap. Both reviews were read-only and ran no runtime gates; this is not release approval.\n- The slope and filesystem regressions were observed failing before correction. All 41 targeted arrangement, MIDI, Transport and asset-tool tests passed after correction.\n- Final hardened candidate verification passed 310 behavioral tests, build, strict source/development/public-contract types and source/generated security gates. Isolated installed-package rendering, declarations and the actual installed asset CLI passed. Full/runtime audits found zero vulnerabilities; runtime dependencies remain empty. The device-evidence CLI classified an empty synthetic capture as 36 unverified cells, without creating physical evidence.\n- Fresh sound-quality passed 2,505 matrix cases and 24 live-control cases across eco/standard/high, including its independent spectral/filter/streaming gates; voice-quality completed with accepted: true. These numerical results are not listening or hardware-fidelity verdicts.\n- Assistant-owned native Chromium exercised FM-designer hold/live feedback/release/export with one active Worklet voice and zero errors; instrument startup/cleanup; independent-bus playback and actual offline stem links; eco audition control-phrase measurement/WAV output; and adaptive boundary switching, tempo acceleration, pause/resume/stop with no browser errors. Screenshots were inspected. The audition rejected an unassessed human finding; no listening results were fabricated.\n- Native static Worker smoke completed PCM24 output through two writes (17,324 bytes), reported zero DSP errors and observed initializing/rendering/writing/closing/completed phases.\n- The repository Playwright Chromium smoke could not launch because its cached Chromium framework was missing. Managed Chromium observations above are separate evidence, not a pass for that command or cross-browser CI. Listening, physical MIDI/device behavior and publication remain unverified.\n- Remove the manual npm-publish.yml GitHub Actions workflow. Registry publication remains a separate maintainer action; online registry-verify still expects provenance from that historical workflow path.\n- Candidate preparation preserved unexpected duplicate generated files with 2 suffixes separately and excluded them from commits. This is not a build guarantee: the current build replaces the generated dist/ directory, so preserve any host/user files outside it before rebuilding.\n\n### GitHub release package and post-release documentation\n\n- The unchanged v1.8 tag points to d793d69056c68a971ede1af988338412687227fd. Its remote quality run passed Node.js 22/24/26 and Chromium/Firefox/WebKit; this later evidence does not turn the missing local Playwright binary above into a local command PASS.\n- Attach the previously missing opm.js-1.8.0.tgz, built in an isolated checkout of that exact tag. The 491,851-byte archive has SHA-256 bee862d5b3ccaa7c4e8ff959583873c78a8ca8a9f56f4d8b039e490e498e2587; build/type/security/local-artifact gates and exact installed-package rendering/asset CLI passed, and a downloaded copy matched byte-for-byte. The review issue is closed as a review record, not npm-publication certification.\n- Deep audit of the attached archive verified all eleven public import specifiers, 44 JS/map/declaration triplets, embedded sources, runtime/type import closure and the actual CLI's served asset bytes/MIME/nosniff. Native Chromium observed finite nonzero AudioWorklet output with zero errors in eco/standard/high at maxVoices:32; native static Worker output matched core WAV bytes in all three formats with transferred sink buffers and zero errors. These checks are not physical polyphony-budget, device/MIDI or listening acceptance.\n- Synchronize README, both usage guides, all topical guides, Vite guidance, project guidance and current publication/security instructions to v1.8 contracts. Document direct release-tarball installation, twelve checkout-only HTML examples, 1–32 logical voices/default eight, priority-aware admission, tempo/arrangement, expressive/MIDI and Worker diagnostics; make shipped-document links independent of excluded HTML/source/tooling.\n- Keep historical verification records and the original tag/tarball unchanged. Registry publication/provenance, physical-device/MIDI behavior and human listening remain separate unverified prerequisites.\n- Documentation execution caught incorrect grid-helper import claims and cold-start Transport beat-0 late rejection. Grid helpers are root exports, not core exports; runnable Transport recipes now leave a one-beat count-in and explicitly document the remaining late:'drop' timing limitation without changing runtime code.\n- Post-release documentation smoke passed isolated build/type/local-artifact gates, direct GitHub URL installation, all three Node quick-start scripts (true 0), installed asset copy/reuse and 133 served-asset checks, plus the installed-package Vite 8.3.2 production build. Sandboxed, muted native Chromium exercised the exact README/bilingual quick starts, both updated Transport/performance scripts and the complete default-voice arrangement recipe with finite nonzero PCM, zero DSP errors/rejections and no browser errors; screenshots were inspected. Native Worker diagnostics produced 8,684 PCM24 WAV bytes identical to core output and completed all phases. An installed 32-voice priority/tempo/grid smoke preserved protected voices, rejected lower priority and round-tripped a linear tempo map. These are scoped documentation/contract observations, not a fresh full-suite, registry, physical-device/MIDI or listening certification.\n\n## v1.7\n\nPackage version 1.7.0. GitHub release distribution and npm registry publication are separate operations; the historical v1.6 tag and artifact remain immutable.\n\n### Musical scheduling, rendering and expressive control\n\n- Add beat-based createTransport() with AudioContext-clock pause/resume, seek, loops, time signatures, BPM and bounded tempo maps. Restarts rebuild owned musical notes/automation, not an exact DSP snapshot; unrelated notes remain untouched. Preserve scheduled controls throughout release tails, including the final non-loop endpoint.\n- Add exact-frame incremental WAV encoding and PCM16/PCM24/Float32 output. Full-buffer encoding retains its existing frame limit; incremental encoding enforces RIFF32/sequence limits without retaining a complete WAV.\n- Add a static same-origin module Worker renderer with progress, cancellation and one acknowledged chunk at a time. Await sink writes for backpressure, preserve accounting when the sink transfers buffers, and invalidate output through the host's abort contract.\n- Add bounded part-scoped performance policies: independent physical key IDs, sustain, polyphonic/mono legato, last/high/low priority and scoped cleanup. This is not a MIDI driver.\n- Add independently ramped feedback, LFO rate/depths, operator ratios/fixed frequencies and live ADSR reanchoring without resetting oscillator phase. Canonical voice format v6 adds per-operator AM/PM targets; strict original v1–v5 inputs remain accepted.\n- Add immutable eco/standard/high quality profiles (2×/4×/8×); standard retains the previous default output. Publish measured CPU tradeoffs without arbitrary-FM alias-free, perceptual or physical-device claims.\n- Add atomic bank replacement, lookup removal and detached canonical JSON export. Validation failures leave the bank unchanged; queued/sounding patches retain their snapshots. An explicit empty input array clears the bank.\n- Integrate all seven capabilities into public exports/declarations, bilingual usage guides and the shared-score example. Abort file-export lifetime before awaiting picker/writable acquisition; wait for panic/release admission before allowing the next UI action.\n\n### Pre-release feature verification\n\n- Node.js 26.7.0: 272 behavioral tests, strict source/development/NodeNext-consumer types, generated build, AST/security gates and isolated installed-package render/WAV/types passed. Full tooling and runtime audits found zero vulnerabilities; the runtime dependency tree is empty. The broader offline sound-quality command passed 1,728 matrix cases and its independent spectrum/filter/four-operator/two-minute streaming gates.\n- Actual offline smoke rendered eco PCM16, standard PCM24 and high Float32 WAVs with zero DSP errors. The default profile retained bitwise PCM parity across all eight algorithms against the pre-change distribution. Real Worker-thread regressions cover transferred sink ownership, backpressure and cancellation; Transport regressions exercise actual worklet DSP release-tail controls.\n- The assistant-managed hidden Chromium 150 browser exercised trusted audio startup, nonzero analyser output in all three live quality profiles, smooth feedback/LFO controls, Transport pause/seek/tempo/resume/stop and an actual loop seam (seek 7.8 beats at 300 BPM, observed wrap to 0.33), mono/high-priority key/pedal controls, bank replace/remove/export and native static-Worker PCM24 preview under same-origin CSP and nosniff. The preview contained 43,680 frames / 262,124 bytes with zero DSP errors; an actual full-page screenshot was inspected. Browser observations are not listening or physical-device acceptance.\n- Native browser Worker smoke matched whole-core WAV bytes for eco PCM16, standard PCM24 and high Float32 while transferring every sink buffer; at most one write was active. Cancellation rejected with AbortError despite stalled write/abort promises. Wrong-MIME and missing Worker assets rejected without writing or closing the sink, with one abort each.\n- A throwaway same-origin application-world probe substituted native OPFS storage for the OS chooser, retaining the actual registered demo teardown callback and native Worker. Leaving during picker acquisition started no Worker/writable; leaving during writable acquisition started no Worker and aborted exactly once. A normal 60-second / 96-kHz PCM16 export wrote 23,101,484 native file bytes through 1,411 writes, closed once and reported zero DSP errors. Probe files were removed; this does not verify the operating-system chooser UI.\n- Report-only Apple M5 profile benchmark: eight voices/LFO, 48 kHz, 128-frame blocks, 300 excluded warmup and 2,000 measured blocks per profile. Eco/standard/high p99 was 0.280/0.509/0.820 ms, with zero deadline misses and zero DSP errors in each row. See acoustic evidence for exact timings and limits.\n- Independent scoped static review: SecurityDataReview A PASS, SecurityProtocolReview B PASS, SecurityDSPReview C PASS, SecuritySupplyReview D PASS after correcting release-tail control omission, transferred-buffer byte accounting and deferred file-acquisition teardown. Reviewers ran no runtime gates; integration evidence above is separate. No waiver or release authorization is asserted.\n- Unexpected duplicated generated assets failed an early package-map gate; original assets were preserved. v1.7 verification and packaging use a clean tracked-file checkout, excluding duplicate/untracked assets without deleting user files.\n- This feature-development record did not certify cross-browser CI or npm publication. Listening quality, physical iOS/Android interruption behavior, arbitrary-patch alias freedom and the operating-system file chooser remain unverified.\n\n### v1.7 release-candidate verification\n\n- Package/lock version 1.7.0: a fresh isolated checkout passed all 272 behavioral tests, source/development/NodeNext-consumer type checks, generated build, source/built AST security gates and installed-package render/WAV/types. Full tooling and runtime audits reported zero vulnerabilities; runtime dependencies remain empty. Both sound-quality and voice-quality commands passed, followed separately by the report-only benchmark.\n- Managed native Chromium 150 under same-origin CSP and nosniff exercised the built shared-score demo and 48-kHz AudioWorklet output in eco/standard/high profiles: each produced finite nonzero PCM and zero DSP errors. This is native runtime observation, not listening or physical-device acceptance.\n- Fresh independent read-only static release review: Release17Data A PASS, Release17Protocol B PASS, Release17DSP C PASS, Release17Supply D PASS. Reviewers executed no runtime gates or external publication actions.\n- The GitHub release is gated on observed remote Node 22/24/26 and Chromium/Firefox/WebKit quality/security jobs, including native stress and installed Vite deployment. The release notes record the actual run URL/outcome and verified tarball digest; configured jobs alone are not passing evidence. GitHub distribution does not publish to npm.\n\n## v1.6\n\nPackage version 1.6.0. GitHub release distribution and npm registry publication are separate operations; the v1.5 artifact remains immutable.\n\n### Acoustic quality, expression and adoption\n\n- Add original independent nested four-op chain/branched/multicarrier references alongside Bessel/feedback/long-stream coverage; replace the former four-pole decimator with a preallocated eighth-order Butterworth cascade. Passband, folded-product rejection, phase, tail and CPU tradeoffs are explicit; no arbitrary-FM alias-free or perceptual claim.\n- Canonicalize voice format v5 with fixed-Hz operators, rate key scaling, pitch envelopes and delayed/phase-selectable note/global LFO. Retain strict original v1–v4 input shapes. Add independent per-operator level multipliers/ramps and note-scaled offline release tails.\n- Preserve supported fixed-Hz/expressive DX7 source fields using documented musical heuristics and actionable substitution/loss warnings; six-to-four topology and hardware timing remain intentionally lossy.\n- Add bounded long-score preparation, capacity estimation, reusable chunked rendering and mixed note/control/stop streaming without removing short-score/worklet/WAV limits.\n- Add same-origin custom worklet assets, multicast subscriptions, timed/abortable command admission waits and terminal disposal alongside restartable close.\n- Expand original curated preset recipes and provenance/register/velocity metadata, numerical host trims and repeatable phrase/A/B audition without rewriting patches for loudness.\n- Expand physical-device recovery scenarios, bounded local observation/status exports and an explicit unverified iOS/Android support matrix.\n- Add post-publication registry byte/integrity/source-provenance/signature/installed-consumer verification to the manual npm workflow; local artifact validation is available without claiming registry publication.\n- Synchronize project guidance and public usage contracts. Physical-device/listening evidence and authorized first npm publication remain external prerequisites.\n\n### v1.6 verification record\n\n- Node.js 26.7.0: 198 behavioral tests, strict source/development/consumer types, source/generated AST gates, installed-package render/WAV/declarations passed; root full/runtime and Vite tooling audits found zero vulnerabilities, with an empty runtime dependency tree. A stale generated dist containing duplicated 2/ 3 suffix files failed the packaged-map gate; it was preserved outside the repository and rebuilt cleanly rather than deleted.\n- Native Chromium 153.0.8010.12 (48 kHz backend, muted) ran the audition UI (A/B measurement, matched-versus-dry reports, PCM16 WAV download, play/stop/dispose and fresh restart), a 60-second/96 kHz bounded render (5,775,360 frames, zero DSP errors) and a 390 px layout check; the event log is now height-limited so long reports do not widen the page. It also ran command-waiter admission, receipt eviction, native abort-signal handling, correlated panic reset, subscription cleanup and terminal disposal against a borrowed context. Worklet stress and the installed Vite subpath/CSP/MIME/404 smoke passed again; the native WebKit 26.6 AudioWorklet smoke passed.\n- Report-only Apple M5 benchmark (48 kHz, 128 frames, 2.667 ms deadline): the order-8 decimator costs more than the former filter. Eight voices with LFO had p99 at 1.575× the deadline and 79 misses; without LFO 1.429× and 45 misses; raw burst 3.323× and prepared burst 2.591×. Worst samples include scheduler/GC pauses (up to 120 ms), so this is a host-dependent tradeoff, not an underrun counter. Measure before enabling dense polyphony on tighter CPU budgets.\n- Not verified: listening quality, physical iOS/Android interruption behavior, locked Playwright 1.56.1, Firefox, Linux WebKit CI and any npm registry operation (npm whoami previously returned ENEEDAUTH). Desktop automation does not earn physical-device acceptance.\n- Waiver: v1.6 was released on GitHub at the maintainer's explicit request without a fresh independent A/B/C/D review (see SECURITY.md). It is not eligible for npm publication until that review record exists.\n\n## v1.5\n\nPackage version 1.5.0. GitHub release distribution and npm registry publication are separate operations; the historical v1.4 artifact remains immutable.\n\n### Complete expressive playback and lifecycle\n\n- Return correlated command IDs/accepted-or-rejected events for stop, controls, all-notes-off, panic, mix gain and tuning. Queue-full/inactive commands no longer fail silently; acceptance is admission, not a future-execution promise.\n- Add allNotesOff with natural release and queue-bypassing panic with immediate tail removal; add context/reset events and reliable host gate cleanup across close/failure/interruption.\n- Add independent optional 0–10-second expression/pan/modulation ramps, preserving immediate defaults and pitch-glide semantics.\n- Add 0–1 pre-tanh mix gain and phase-preserving global A4/128-note cents tuning; browser notes now accept fractional MIDI like offline notes.\n- Replace saturated patch-cache inline fallback with bounded content-key LRU registration replacement; queued/active voices retain original prepared snapshots.\n- Add bounded prepareSequence/renderSequence/playSequence with shared note/control/stop ordering, frame rounding, tuning and release tails.\n- Canonicalize voice format 4 with sine/triangle/saw/square LFO waveforms; retain versions 1/2/3 under old field rules. Add oldest/release-first/quietest stealing without instantaneous zero-crossing decisions.\n- Default context interruption to cancel; preserve is explicit. Non-running contexts/resets stop lookahead, with error reporting and gesture-driven restart rather than stale replay.\n- Observe context state before and after native resume rather than relying only on queued statechange notifications; cancel interrupted gates and diagnostics reliably without duplicate interruption resets.\n- Add example 08 with native playback, same-score WAV, gain/tuning/stealing controls, suspend/recovery/disposal, bounded local observation export and manually confirmed physical-device scenarios.\n\n### Independent numerical and security acceptance\n\n- Add index-16 Bessel PM spectral references at 44.1/48/96 kHz with separate brightness and alias bounds, a contractive delayed-feedback reference for feedback 7/level 0.25, and conservative full-level feedback energy bounds. Chaotic full-level feedback does not admit unique alias/harmonic separation; no all-patch or chip-fidelity claim.\n- Add two 120-second streaming scenarios for held AM/PM and repeated interrupted glides; verify continuity, deterministic rechunking and terminal silence.\n- InputBoundaryReview reviewed A/B; DspSupplyReview reviewed C/D. Reproduced/fixed null mixGain/tuning acceptance and shadowed native render metadata; also reject offset coercion before default-length calculation. Permanent regressions protect unchanged sound/frame state on rejected input.\n- Node.js 26.7.0: 157 behavioral tests and strict source/development/public-consumer types passed. Full/runtime npm audits found zero vulnerabilities; runtime dependencies remain empty.\n- All 1,728 sound-matrix cases and independent/long-stream gates passed. The conservative filter still reports −11.627 dB at 9.6 kHz/48 kHz; improving arbitrary upper-register brightness/aliasing is not claimed.\n- Owned sandboxed, explicitly muted Chromium 153.0.8010.12/Playwright 1.63.0 exercised real AudioWorklet signal, cancellation/preservation, routing/lifecycle and the feature UI. Actual backend rate was 24 kHz; all four waveform WAVs decoded as 21,840 stereo PCM16 frames/87,404 bytes. Fresh running-state analyser output confirmed preserve across all three stealing policies; no listening or underrun certification.\n- The local device report contained 184 bounded events with every physical-device scenario unconfirmed; nothing was uploaded. No devicectl/adb or real phone was available. npm whoami returned ENEEDAUTH during feature verification: authenticated first npm publication and registry-installed verification remain external prerequisites.\n- FinalInputsReview (A/B) and FinalDspReview (C/D) re-reviewed the completed fixes without remaining findings. Native Node installed-tarball runtime and consumer declaration gates passed against the clean 27-module distribution.\n- Executed the actual canonical JSON from README and both usage guides with finite nonzero PCM and zero errors. Both unmodified shared-score guide scripts rendered 13,440 frames at 24 kHz in native Chromium, produced analyser signal and closed their owned contexts.\n- Final native browser smoke passed. Stress exercised 768 dense notes in 32 batches: 769 started including the initial held note, 760 steals and one intentional late rejection; settled active/pending/error counts were zero. Installed-package Vite 8.3.2 production checks passed for non-root base, CSP, normal assets, missing worklet and incorrect MIME handling.\n- Report-only Apple M5/Node 26.7.0 benchmark (48 kHz, 128 frames, 2,000 measured blocks/scenario): eight voices with LFO had p99 2.2315 ms, worst 10.627416 ms and 16 deadline misses; without LFO had 6 misses, raw burst 5, prepared burst 0. No hard budgets were configured; scheduler/GC-sensitive timings are not an underrun counter or realtime guarantee.\n\n### v1.5 release verification\n\n- Security: Release15Inputs approved scoped static A/B review; Release15DspSupply approved C/D for this GitHub-only release. No evidence-backed release blockers remained. Reviewers ran no runtime commands; integration checks were performed separately.\n- Fresh package 1.5.0 verification on Node.js 26.7.0: 27 JS/map/declaration triplets, all 157 tests, strict source/development/consumer types, source/generated AST gates, installed render/WAV/types and empty runtime dependency tree passed. Root full/runtime and installed Vite tooling audits found zero vulnerabilities.\n- All 1,728 sound cases, independent PM/feedback bounds, two 120-second streaming scenarios and 108 preset/conversion cells passed again.\n- Owned sandboxed, explicitly muted Chromium 153.0.8010.12/Playwright 1.63.0 passed native smoke/stress and installed-package Vite 8.3.2 production subpath/CSP/MIME/404/license checks again. The actual backend was 24 kHz; stress started 769 notes, stole 760, intentionally rejected one late note and settled with zero active/pending notes and DSP errors. These local runs do not certify locked Playwright 1.56.1, Firefox/WebKit or physical-phone coverage.\n- Fresh Apple M5 report-only benchmark (300 warmup blocks excluded, 2,000 measured 128-frame blocks at 48 kHz): eight voices with LFO p99/worst 0.630/2.520 ms and zero misses; raw burst 2.295/12.621 ms and 10 misses; prepared burst 0.914/1.963 ms and zero misses. No hard budgets were configured; these observations are not a universal realtime or glitch-free guarantee.\n\n### WebKit interruption repair\n\n- Initial package 1.5.0 quality CI passed Node 22/24/26, Chromium and Firefox but failed Linux WebKit 26.0 interruption cancellation. Publication was paused; the failing assertion was not removed or waived.\n- Native suspend/resume promises can settle before queued statechange tasks. Synchronize observation around both initial and existing-node resumes, recording transitions before callbacks and deduplicating notifications.\n- Real-processor regressions cover deferred running/suspended notifications over consecutive cycles, both resume/start, cancel/preserve, actual PCM, queued/held cancellation exactly once and borrowed ownership. A separate consumer regression verifies outstanding diagnostics reject at recovery boundaries.\n- Release15Inputs re-reviewed A/B after fixing stale observer state; Release15DspSupply re-reviewed C/D. Both scoped static reviews passed without remaining findings; neither reviewer ran runtime gates.\n- Rebuilt 27-module distribution, all 159 behavioral tests, strict types, source/generated AST checks and installed-package render/WAV/declarations passed on Node.js 26.7.0.\n- Unmodified native smoke passed on owned WebKit 26.6 and sandboxed, explicitly muted Chromium 153.0.8010.12 with Playwright 1.63.0 on macOS arm64, using the repaired distribution and a muted audio graph. Both verified interruption reset, terminal cancellation and silent/empty recovery, with zero DSP errors; actual sample rate was 24 kHz. This is separate from locked Linux WebKit 26.0 acceptance.\n\n## v1.4\n\nPackage version 1.4.0. Distributed through the GitHub release tarball; npm registry publication is a separate operation.\n\n### Sound quality and realtime acceptance\n\n- Add a 1,728-case offline matrix across four sample rates, all eight algorithms, feedback extremes, registers, velocities and voice-stealing workloads. Check finite output, headroom, settled release, determinism and render-chunk independence.\n- Report controlled passband loss, harmonics 2–8 THD and two isolated ultrasonic folded aliases. These bounds are not hardware-fidelity, arbitrary-FM alias-free or perceptual-loudness claims.\n- Add native AudioWorklet stress with 768 dense notes, live controls, scheduled releases, stealing and bounded main-thread contention. Establish actual rendered output before testing a past deadline; a running main-thread clock alone is insufficient during processor startup.\n\n### Prepared patches and expressive notes\n\n- Add opaque immutable prepareVoice() snapshots and bounded reusable core voice state. Register/cache up to 128 patches per worklet, with validated inline transport beyond the cache; expose defensive read-only voice-bank snapshots.\n- Add Synth.updateNote() / OPM.updateNote() with relative pitch, linear-in-semitone glide, expression, pan and FM/LFO modulation controls, including scheduled and pending-note updates.\n- Make voice version 3 canonical, adding optional per-operator velocity sensitivity. Continue accepting version 1/2 under their original field rules; omitted sensitivity preserves legacy sound. DX7 velocity conversion is an explicitly approximate heuristic.\n- Bound terminal notifications after each frame's traversal and reject recursive rendering. Callback-admitted notes begin no earlier than the next frame, preventing callbacks from replenishing an unbounded render loop.\n\n### Audio-clock scheduling and deployment\n\n- Add absolute at, scheduled stop, explicit late-start/drop policies and actual lifecycle frame/time stamps. At equal frames, stop precedes onset, then controls; late-start retains the full gate.\n- Add bounded createLookaheadScheduler() with explicit error handling, restart/disposal and skipped missed windows. Update the finite song and live-control examples.\n- Add an installed-tarball Vite example with complete dist and LICENSE copying, non-root deployment, CSP and real MIME/404 acceptance. Add a manual npm trusted-publishing/provenance workflow gated by matching release metadata, security evidence and environment approval; no runtime dependencies are added.\n\n### Preset and DX7 audition\n\n- Add the seventh example: preset/original-synthetic-DX7 A/B audition, register/velocity grids, raw peak/RMS dBFS reports, bounded attenuation-only host trim and offline WAV download. Cover seven bundled presets plus five generated converter recipes, not copyrighted third-party banks or a six-operator reference renderer.\n- Keep the mobile report horizontally scrollable inside its container. Update README, both executable usage guides, deployment/publishing guidance and the security policy.\n\n### Feature verification before release\n\n- Security: AllFeaturesInputsReview approved scoped static A/B review; AllFeaturesDspSupplyReview approved C/D after the bounded-callback and deployed-LICENSE fixes. Runtime checks were performed separately by 語喵.\n- Node.js 26.7.0: all 108 behavioral tests, strict source/development/consumer types, source/generated AST security gates and installed-package render/WAV/types passed. Build output has 22 JS/map/declaration triplets. Root full/runtime audits and the installed Vite consumer audit found zero vulnerabilities; runtime dependencies remain empty.\n- All 1,728 sound cases and 108 preset/conversion cells passed. Thirty-two legacy stereo PCM cases were bit-identical to shipped tag v1.3, covering four rates, eight algorithms, LFO, polyphony, stealing and release. A 256-prepared-note/control/steal render allocated zero additional Float64 state arrays after construction; this is not a whole-JavaScript zero-allocation guarantee.\n- Owned isolated headless Chromium 153.0.8010.12 with Playwright 1.63.0, sandboxing and explicit audio muting passed native smoke and stress: 769 accepted/started notes, 760 steals, one intentional late rejection, peak 0.672, final active/pending counts zero and no DSP errors. This is not locked Playwright 1.56.1, Firefox/WebKit or current remote-CI coverage.\n- The same silent browser exercised both guides' absolute-control and eight-note lookahead recipes, restart/stop/disposal, live controls, a complete 24-second song and audition A/B/restart. The 390-pixel catalog/report stayed within the viewport. A persisted audition WAV decoded as 40,513 stereo PCM16 frames at 48 kHz (162,096 bytes).\n- Vite 8.3.2 production acceptance passed under /opm-example/: real signal/lifecycle, blocked inline CSP script, missing-worklet and wrong-MIME failures, and exact deployed LICENSE contents.\n- Apple M5 / Node 26 report-only benchmark excluded 300 warmup blocks and measured 2,000 blocks per scenario. Raw burst p99/worst were 1.038/1.402 ms; prepared burst p99/worst were 1.238/2.470 ms, with no missed 2.667 ms deadlines. Scheduler/GC variance and other hosts prevent a universal realtime guarantee.\n- npm registry publication is not part of this GitHub release. Local npm authentication was unavailable during feature verification; trusted-publisher/environment setup and release-specific approval/evidence remain prerequisites for the manual publishing workflow.\n\n### v1.4 release verification\n\n- Security: Release14Inputs approved static A/B review, including WAV/render and application file boundaries; Release14DspSupply approved static C/D review for this GitHub-only release. Neither reviewer executed runtime checks; no evidence-backed release blockers remained.\n- Fresh package 1.4.0 verification on Node.js 26.7.0: 22 JS/map/declaration triplets, all 108 tests, strict typechecks, source/generated AST gates, installed render/WAV/types, zero runtime dependencies and both root audits passed. All 1,728 sound cases and 108 preset/conversion cells passed again.\n- Owned sandboxed headless Chromium 153.0.8010.12 with Playwright 1.63.0 and explicit audio muting passed native smoke/stress and installed-package Vite production checks again: 769 accepted/started notes, 760 steals, one intentional late rejection, zero DSP errors and no final active/pending notes. Locked-browser and remote-CI results are separate evidence.\n- Fresh Apple M5 report-only benchmark: raw burst p99/worst 1.131/3.303 ms with one missed 2.667 ms deadline; prepared burst p99/worst 0.913/1.133 ms with zero misses. These host-dependent wall-clock observations are not a universal realtime guarantee.\n\n## v1.3.0\n\n### Release verification and security review\n\n- Security: independent reviewers Release13Inputs (A/B, including the v1.2.0 release diff) and Release13DspSupply (C/D, current source/build/package controls) approved scoped static reviews with no evidence-backed blockers. Runtime checks were executed by 語喵, not the reviewers.\n- Fresh local verification on Node.js 26.7.0: build produced 21 JS/map/declaration triplets; all 73 tests, strict source/development/consumer typechecks, AST security gates and both npm audits passed (zero vulnerabilities). The runtime dependency tree is empty. Installed v1.3.0 package rendering produced 1,440 frames and a 5,804-byte WAV; installed consumer types passed.\n- Real Chromium 153.0.8010.12 AudioWorklet smoke passed on macOS arm64 using the existing Playwright 1.63.0 launcher: stereo/pan, note lifecycle, routing, suspend/resume and context ownership passed with zero DSP errors. Locked Playwright 1.56.1 Chromium installation did not complete locally; this is not locked-browser coverage.\n- Observed pre-release quality CI for b177bed passed Node 22/24/26 and Chromium/Firefox/WebKit jobs; Pages deployment also passed. These runs precede the release metadata commit.\n- Apple M5 / Node 26 benchmark excluded 300 warmup blocks and measured 2,000 blocks per scenario. Burst p99 was 1.121 ms, worst 1.534 ms, with zero missed 2.667 ms deadlines. This report-only local measurement is not a universal realtime guarantee.\n\n### Detailed English website security policy\n\n- Expand the English security policy with deployment, upload/download limits, voice/DX7/PCM boundaries, scheduling, audio safety, privacy and private reporting guidance. Preserve mandatory security-review triggers and checklists.\n- Link the policy from the example catalog. Browser verification confirmed the link and HTTP 200 for the hosted policy.\n\n### Website favicon\n\n- Add an original 16/32/48-pixel favicon.ico and link it from the catalog and all six examples.\n- Browser decoding and visual inspection confirmed the 48-pixel image and all three ICO directory entries.\n\n### English example catalog\n\n- Turn the root index.html into a six-example English catalog. Add basic notes/voices, stereo/per-voice LFO, shared AudioContext routing and offline WAV examples. Move the song to examples/song.html and the advanced playground to examples/playground.html; remove the old demo/index.html route.\n- Keep all six TypeScript helpers in demo/ and build matching minified JS, embedded-source maps and genuine compiler-generated declarations. The build emits 21 JS/map/declaration triplets.\n- Browser smoke exercised English hold/release, pan/LFO settings, song start/stop, key scaling and real AudioWorklet diagnostics. Disposing and restarting OPM retained the running host context. The 390-pixel catalog had no horizontal overflow. Fix wrapped heading links so their entire title block is clickable; actual catalog navigation then started and released a note successfully.\n- Offline WAV smoke produced 11,246 stereo frames at 22,050 Hz (45,028 bytes), verified RIFF/WAVE, PCM16, sample rate and non-silent samples. The download action was exercised, but persisted browser-download files were not observed.\n- Verification: build, 73 behavioral tests, strict source/development/consumer typechecks, zero-runtime-dependency/dynamic-code gates and isolated installed-package rendering/WAV/types passed on Node.js 26.7.0. Remote CI and Firefox/WebKit were not run; full timed song completion was not verified by the browser automation.\n\n### Node.js 22 minimum\n\n- Align root lockfile metadata and support/development documentation with the upstream package requirement of Node.js >=22.0.0 and CI matrix of 22/24/26.\n- The maintainer reports Node.js 22 tests passing; this session's local checks ran on Node.js 26.7.0.\n\n### Complete JavaScript/map/declaration triplets\n\n- Preserve engine source-module paths instead of bundling into hashed chunks, allowing every emitted JS to retain genuine compiler-generated export declarations. Generate declarations for both demos as well; their export {}; accurately represents modules with no public exports.\n- Enforce a matching .js.map and .d.ts for every JS before replacing dist/, and in the isolated package gate. Remove obsolete chunk output and update deployment documentation.\n- Verification by 語喵: 17 JavaScript files, 17 matching source maps, and 17 declarations in both the rebuilt tree and package manifest. Embedded sources match original TypeScript. Build, 73 behavioral tests, strict source/development/consumer typechecks, AST/security checks, and installed-package rendering/WAV/types passed.\n- Real Chromium 153.0.8010.12 AudioWorklet smoke passed with preserved module imports on macOS arm64 using the existing matching Playwright 1.63.0 launcher; stereo, pan, note lifecycle, routing, suspend/resume, and context ownership passed with zero DSP errors. Remote CI and locked-browser runs were not performed.\n\n### Minification and TypeScript source maps\n\n- Minify every distribution JavaScript module, including both demos, through the shared safe Terser pipeline. Emit a matching .js.map and relative sourceMappingURL for each module; compose esbuild/Terser mappings back to original TypeScript with embedded source contents.\n- Include maps in the package whitelist and distribution allowlists. Keep .d.ts declarations and omit JSON assets/compressed sidecars. Publishing source maps exposes the embedded source, as documented in README and both usage guides.\n- Verification by 語喵: build, 73 behavioral tests, strict typechecks, AST/security gates, and isolated installed-package rendering/WAV/types passed. All 16 JS files have corresponding packaged maps; every embedded TypeScript source matches its source file exactly. A real invalid-voice exception under Node --enable-source-maps resolves to src/voices/normalize.ts:9:11, rather than generated chunk coordinates. Three re-export-only entry modules correctly have empty source mappings.\n- Visually exercised electric-piano hold/release/diagnostics and song start/stop after demo minification, with no page errors. Real Chromium 153.0.8010.12 AudioWorklet smoke passed on macOS arm64 using the existing matching Playwright 1.63.0 launcher, covering stereo, pan, lifecycle, routing, resume, and context ownership. Remote CI and locked-browser runs were not performed.\n\n### JavaScript-only distribution assets\n\n- Restrict dist/ to .js and generated .d.ts; remove JSON assets and gzip/Brotli sidecars from build output. Move canonical bundled data to typed modules: opm.js/voices/examples.js exports examples, and opm.js/voices/voice.schema.js exports voiceSchema. Remove the obsolete generic asset export; external JSON voice-bank input remains supported.\n- Update demo loading, package exports, consumer declarations, and both usage guides. The seven presets and schema were compared against their previous JSON data and match exactly.\n- Verification by 語喵: build, 73 behavioral tests, strict source/development/consumer typechecks, source/dist AST gates, and isolated installed-package rendering/WAV/types passed. The package smoke checks every packaged distribution file against the .js/.d.ts allowlist. Visually exercised electric-piano hold/release and diagnostics in the rebuilt demo; no page errors or DSP errors.\n- Real Chromium 153.0.8010.12 AudioWorklet smoke passed on macOS arm64 using the existing matching Playwright 1.63.0 launcher, covering stereo, pan, lifecycle, routing, resume, and context ownership. This is not remote CI or locked-browser evidence.\n- Security: 語喵 reviewed scoped A/B/C/D requirements: bundled assets contain only preserved literal data, bank validation still runs before demo registration, worklet/DSP boundaries and limits are unchanged, and no dependency or dynamic execution was added. This is a scoped author review, not an independent review.\n- Development/runtime audits reported zero vulnerabilities; the runtime dependency tree is empty.\n\n### TypeScript source cutover\n\n- Preserve published structural OPM/Synth class contracts by stripping implementation-only declaration members; retain the literal bank-size type and optional event callback. Installed consumer checks cover adapters and the literal limit. The test entrypoint enumerates filenames without shell globs and forwards Node runner options for Node 18/Windows compatibility (Windows execution not observed); diagnostics filtering and reporter selection were exercised on Node 18/26.\n- Migrate engine, AudioWorklet, voice parsers, demos, behavioral tests, and development tools to strict TypeScript, following XYZ.js's source/generated-declaration approach. Remove obsolete JavaScript source and handwritten declarations; keep public .js package/deployment URLs and zero runtime dependencies.\n- Generate declarations from implementation, compile development programs into ignored .dev/, and build external dist/demo/ modules for both HTML demos. Preserve npm and Node 18+ compatibility rather than adopting XYZ.js's newer platform requirement. CI invokes the compiled tooling through npm scripts.\n- Verification by 語喵: build, complete strict typecheck, 73 behavioral tests on Node 26.7.0 and Node 18.20.8, TypeScript-source/JavaScript-dist AST gates, installed ESM/WAV/type smoke, and development/runtime audits (zero vulnerabilities). All original behavioral test titles remain; the former 74 count included automatic discovery of the fixture-only module, now excluded by explicit *.test.js selection.\n- Released v1.2 DSP versus compiled TypeScript and rebuilt distribution: 248 cases, 4,504,320 compared channel samples, exact Float32 equality, plus matching WAV bytes. Covers algorithms, stereo pan, sample rates, and burst stealing; this is scoped parity evidence, not hardware fidelity.\n- Apple M5 / Node 26: unchanged 300-warmup/2,000-block benchmark passed all p99 budgets; burst p99 0.868 ms, zero missed deadlines against 2.667 ms. No universal deadline guarantee.\n- Real rebuilt AudioWorklet smoke passed Chromium 153.0.8010.12 and WebKit 26.6 on macOS arm64 using the existing matching Playwright 1.63.0 launcher (not the locked CI browser versions). Both HTML demos were visually checked and exercised for held/released notes, WAV generation action, and song start/stop. Browser download persistence was not verified. Firefox and remote CI were not run for this migration.\n- Security: independent TsSecurityReview approved static A/B/C/D review with no findings: runtime validation remains beneath erased types, bounded voices/messages/binary input and own-data checks remain, declaration fields do not add runtime initializers, tooling is exactly pinned, and runtime dependencies remain empty. Runtime checks above were executed by 語喵, not the reviewer.\n\n## v1.2.0\n\n### CI failure repairs\n\n- Inspect the first quality run: Node 18/22 passed functional gates but exceeded the unchanged 2.667 ms burst p99 budget (3.205/3.019 ms); Firefox timed out without stage diagnostics.\n- Reduce attack/decay/release envelope exponentiation within each 4x output frame. Re-anchor analytic gain every frame and retain direct evaluation at stage boundaries; validation, eight-voice/eight-fade bounds, workload, and deadline budget remain unchanged. Double-precision rounding can differ from the previous implementation; no universal byte-identity promise.\n- Provide Linux Firefox CI with a running native PulseAudio server and null output sink. Keep real AudioContext/AudioWorklet processing and trusted clicks; add stage, audio-clock/context, and request/error diagnostics rather than autoplay overrides or fake audio.\n- Verification: Node 18.20.8 and 22.23.3 each passed 74 tests, including analytic envelope boundaries and very short stages. Build, public types, AST/security and installed-package ESM/type smoke passed. On Apple M5, the unchanged 300-warmup/2,000-block benchmark passed every p99 gate; burst p99 was 1.499/1.478 ms with zero missed deadlines in those runs. These measurements do not prove the GitHub x64 hosts' performance.\n- Audio comparison covered 387 cases and 2,045,248 channel samples, including burst steals at 8–192 kHz: observed Float32 output matched the previous implementation exactly; source/distribution parity also passed the same cases.\n- Locked Playwright 1.56.1 / Firefox 142.0.1 passed real stereo, held/released notes, routing, suspend/resume, diagnostics and context ownership smoke against built assets on Linux arm64 with PulseAudio. The local host was Ubuntu 26.04 using Playwright's Ubuntu 24.04 arm64 binary override, not the GitHub Ubuntu 24.04 x64 runner. Chromium 153 and WebKit 26.6 smoke also passed on macOS using the existing matching Playwright 1.63.0 launcher. The repaired external CI run has not yet been observed.\n- Security: independent source reviewer CiRepairSecurity approved scoped A/B/C/D checks with no blockers: finite/bounded frame-local envelope computation, unchanged trust boundaries, and native CI audio setup. 語喵 executed the integration checks above; the reviewer did not execute runtime gates.\n\n### Final deadline repair\n\n- The follow-up CI run still exceeded the unchanged burst deadline on Node 22 (p99 3.043 ms, 31 misses). Replace hot-loop base-10 exponentiation with exp(db * ln(10) / 20) without changing envelope stages, re-anchoring, bounds, or the benchmark workload/budget.\n- Fresh verification passed 74 tests on Node 26 and Node 22.23.3, build, public types, AST/installed-package gates, and both npm audits (zero vulnerabilities). Apple M5 / Node 22 burst p99 was 1.009 ms with zero misses against the original 2.667 ms budget; external CI must independently confirm runner performance.\n- Independent reviewer ReleaseExpSecurity approved the final arithmetic change under A/B/C/D with no blockers (static review only). Real Chromium AudioWorklet smoke passed again against rebuilt assets using the cached Playwright 1.63 launcher; remote locked-browser gates remain authoritative.\n\n### Readiness and additive features\n\n- Add borrowed AudioContext ownership, resumable/coalesced initialization, serialized disposal, explicit output routing, held notes, velocity, and stereo pan.\n- Surface worklet note admission/lifecycle/rejection events, diagnostics, and processor errors. Bound tracked IDs/events, reject duplicates before enqueueing, and remove stale off events when notes terminate.\n- Preserve eight logical voices with bounded short steal fades and exactly-once terminal notifications. Unify offline rendering with the live Synth DSP path, including LFO, filtering, saturation, velocity, and pan.\n- Introduce version 2 voices with optional per-operator key scaling; retain legacy version 1 input without new fields. Expand the bundled example bank.\n- Add finite/bounded PCM16 mono/stereo WAV encoding and clean-room approximate six-to-four-operator DX7 SysEx import, with separate conversion descriptions/warnings.\n- Ship TypeScript declarations through conditional package exports and copy their dependent declarations intact during builds. Keep zero runtime dependencies.\n- Add development-only AST security/package gates, an installed-package type consumer, configurable 128-frame deadline statistics, spectral regression coverage, and configured Node/browser CI. Browser tooling is development-only; no fallback hides missing AudioWorklet support.\n- Update English and Traditional Chinese runnable installation, lifecycle, routing, rendering, import/export, and quality instructions; extend the browser demo with key scaling, DX7 preview, and WAV download.\n\n### Verification and release gate\n\n- Integration verification passed 73 tests, the distribution build, strict public-API TypeScript checks, source/built AST security gates, isolated tarball installation exercising ESM and TypeScript consumers, and full/runtime-only npm audits with zero vulnerabilities.\n- Source and minified distribution produced byte-identical stereo PCM and WAV bytes across 189 offline cases (7,944,138 stereo samples), plus 40 live stealing/render blocks.\n- Real Chromium, Firefox, and WebKit AudioWorklet smoke scenarios passed signal/stereo, routing, held-note lifecycle, suspend/resume, diagnostics, and context ownership checks. Local browser downloads for pinned Playwright 1.56.1 did not complete; the unchanged scenarios used an already cached matching Playwright 1.63.0 browser launcher. The pinned CI browser installation and external Node 18/22/24 CI jobs remain configured, not observed runs.\n- The actual demos produced finite audio for key scaling and the expanded presets, previewed an independently generated DX7 fixture, downloaded and decoded a stereo WAV (48 kHz, 39,384 frames), and reported no page errors or rejected notes. Root-demo stop behavior was reproduced and fixed.\n- On Apple M5 / Node 26.7.0, the isolated 48 kHz / 128-frame benchmark passed its p99 deadline budget after 300 warm-up and 2,000 measured blocks per scenario. Burst p99 was 1.347 ms; worst was 3.703 ms with 5 missed 2.667 ms deadlines. A concurrent large offline-render workload exceeded the p99 budget (2.789 ms, 23 misses). These host/load-specific measurements are not a hard real-time guarantee.\n\n### Security review\n\n語喵 coordinated integration verification. Independent source reviewer FinalSecurity approved A/B/C/D after remediation, with no remaining blockers; FinalConsumerReview approved the consumer/lifecycle/DSP review after the reentrant voice-admission fix. Reviewers did not independently execute the runtime gates above; no release tag, publication, or external CI run is claimed.\n\n- A — Untrusted data: approved own-data voice/key-scaling validation, bounded bank/DX7 parsing, framing/checksum/seven-bit checks, and WAV limits. Remediated forged typed-array metadata and inherited WAV-field reads using native kind/length intrinsics and own descriptors; samples are captured, validated, and encoded once. Boundary regressions passed.\n- B — Worklet boundary: approved malformed-message handling, pre-enqueue duplicate/capacity checks, held-note cancellation, lifecycle ID cleanup, diagnostics, and processor-error propagation. Late or repeated note-off is an idempotent no-op, avoiding false rejection after completion/stealing.\n- C — DSP safety: approved eight logical voices/eight bounded fading remnants, terminal callbacks, finite output, LFO/offline parity, stereo gain, key scaling, and deterministic rendering. Admit replacements before notifying stolen callbacks so reentrant admission cannot exceed the voice limit; regression and spectral tests passed. Deadline limitations are recorded above.\n- D — Supply chain: approved zero runtime dependencies, exact esbuild/Terser/Acorn/Playwright/TypeScript development pins and lockfile, pinned CI actions, package whitelist, and installed ESM/type exports. Generated-code/package gates and both npm audits passed.\n\n## v1.1.0\n\nRelease gate: 語喵 coordinated independent security reviewers ReleaseBoundary (A/B) and ReleaseDsp (C/D), both approving their assigned source/security slices with no blockers. Fresh verification passed all 37 tests, full and runtime-only npm audits (zero vulnerabilities), an empty runtime dependency tree, the distribution build, and an isolated v1.1.0 tarball installation exercising public exports, finite stereo PCM, note release, mono rendering, and bank parsing. The browser and differential-audio evidence below was obtained during the preceding optimization/documentation work; runtime code is unchanged since those checks.\n\n### Usage documentation\n\n- Expand README and both usage guides with checkout demos, local-tarball installation without assuming registry publication, complete static browser deployment, and standalone Node.js PCM examples.\n- Clarify lifecycle, scheduling and release, custom voices, bank loading, validation bounds, compressed delivery, and troubleshooting. Distinguish strict single-voice normalization from bank clamping, and document that renderNote() does not apply LFO.\n- Add safe-embedding guidance and usage links in the security/contributor documents. Correct the contributor API diagram and explain rebuilding dist/ before checking source changes in the demos.\n\nDocumentation verification: built and packed an isolated checkout, installed its tarball in a clean application, and executed five Node.js snippets and all three JSON voice examples. Chromium exercised seven extracted HTML examples with real AudioWorklet signal, including custom-voice early release and bundled-bank loading, with no observed browser/worklet errors. Runtime implementation is unchanged.\n\n### Optimization and compact distribution\n\n- Cache real-time sustain gains, routing, subsample offsets, and per-frame pitch steps; replace bounded phase modulo with subtraction and fill idle blocks directly.\n- Prepare offline envelopes once per operator, retaining dB-domain curves and floating-point operation order. Move voice normalization out of the DSP module so the browser facade no longer imports the synthesis implementation.\n- Batch worklet event consumption and compact cancelled events in place, preserving same-frame ordering and the 256-event ceiling. Add an audio regression covering partial consumption, cancellation, and queue refill.\n- Add ready-to-use minified ESM and JSON in dist/, with shared chunks, gzip level 9, and Brotli quality 11 alternatives. Preserve public exports and property names; retain readable sources. npm exports and both demos now use dist/.\n- Add pinned development-only build tools and a repeatable benchmark. The npm whitelist excludes precompressed alternatives, development tooling, tests, and source files to avoid redundant package payloads.\n\n### Measurements and verification\n\nLocal Node.js v26.7.0 measurements using the scripts/benchmark.js workload, three warmups and nine timed runs. Real-time cases render 0.5 seconds at 48 kHz in 128-frame blocks; the offline case includes release and filter tail. Times are machine-dependent.\n\n| Scenario | Original median | Optimized minified median |\n| --- | ---: | ---: |\n| Eight voices with LFO | 84.869 ms | 70.874 ms |\n| Offline note with release | 27.857 ms | 16.774 ms |\n\nOriginal runtime JS/JSON totaled 33,741 bytes. Minified assets total 20,319 bytes; the sums of separately compressed gzip/Brotli variants are 9,014/7,987 bytes. These are asset totals, not npm tarball sizes.\n\nVerification: 37 tests passed against source and the same 37 against minified modules; differential smoke covered 288 scenarios and 1,505,688 samples for each implementation against the original, with byte-identical audio and chunk-independent output. All compressed assets decoded byte-for-byte. Chromium exercised both demo controls and actual AudioWorklet output, including audible signal, finite samples, release to silence, and clean shutdown. An isolated npm-tarball consumer exercised public browser/offline/voice-bank imports without development dependencies.\n\n### Security review\n\nReviewed by 語喵 (AI assistant), with independent security and numerical reviewers, using SECURITY.md A–D. No changed-code blockers were found.\n\n- A — Untrusted data: normalization extraction retains prototype and own-data checks, explicit copying, numeric bounds, and rejection of accessors; voice-bank limits remain unchanged.\n- B — Worklet boundary: shape checks precede payload reads; queue ordering, cancellation, and capacity remain bounded. Source and generated JS scans found no dynamic execution sinks.\n- C — DSP safety: eight-voice ceiling, finite-output safeguards, four-operator/4× processing, headroom, and filter tails remain intact; source/minified differential and malformed-input checks passed.\n- D — Supply chain: runtime dependency tree is empty. esbuild 0.28.2 and Terser 5.51.2 are exact development pins with a lockfile; full and runtime-only npm audits reported zero vulnerabilities. The packed whitelist contains 19 intended assets/docs/legal/metadata entries. Minification disables unsafe math, pure-getter assumptions, and property mangling.\n\n## v1.0.0\n\nFirst stable release of the documented browser and offline-rendering APIs.\n\n- Four-operator FM synthesis with eight algorithms, feedback, ADSR, LFO, detune, and eight-voice polyphony.\n- Browser AudioWorklet API, offline rendering, versioned JSON voice format, and English/Traditional Chinese usage guides.\n- Browser test page with a complete Twinkle, Twinkle, Little Star melody.\n\n### Security review\n\nReviewed by 語喵 (AI assistant) for this release, using the A–D checklist in SECURITY.md:\n\n- A — Untrusted data: voice objects use explicit data-field allowlists and bounded numeric values; JSON voice banks are capped at 256 KiB and 128 voices. File/URL voice-bank loaders are not provided, so path traversal and content-type checks do not apply.\n- B — Worklet boundary: messages are shape-checked, invalid data is ignored, and pending events are capped. Source scan found no dynamic eval, new Function, or import() calls.\n- C — DSP safety: eight active voices maximum; non-finite output is silenced and counted. The deterministic rendering and malformed-input tests passed.\n- D — Supply chain: zero runtime dependencies; no dev dependencies or CI actions; npm audit --omit=dev reported zero vulnerabilities. The package dry run contains only the intended src/, usage docs, changelog, README, license, security policy, and package metadata.\n\nVerification: node --test (36 passing), npm ls --omit=dev --depth=0 (empty), npm audit --omit=dev (zero vulnerabilities), and npm pack --dry-run --json (18 intended entries).\n","href":"./changelog.html"},{"title":"Changelog · Changelog","text":"Changelog Changelog","href":"./changelog.html#changelog"},{"title":"Changelog · Unreleased","text":"Changelog Unreleased","href":"./changelog.html#unreleased"},{"title":"Changelog · Half-band FIR decimation","text":"Changelog Half-band FIR decimation","href":"./changelog.html#half-band-fir-decimation"},{"title":"Changelog · Band-limited square, saw and half-wave operators","text":"Changelog Band-limited square, saw and half-wave operators","href":"./changelog.html#band-limited-square-saw-and-half-wave-operators"},{"title":"Changelog · Post-mix decimation","text":"Changelog Post-mix decimation","href":"./changelog.html#post-mix-decimation"},{"title":"Changelog · v1.11.1","text":"Changelog v1.11.1","href":"./changelog.html#v1111"},{"title":"Changelog · Faster sine operators","text":"Changelog Faster sine operators","href":"./changelog.html#faster-sine-operators"},{"title":"Changelog · v1.11","text":"Changelog v1.11","href":"./changelog.html#v111"},{"title":"Changelog · Approximate OPM text import","text":"Changelog Approximate OPM text import","href":"./changelog.html#approximate-opm-text-import"},{"title":"Changelog · MIDI voice selection and bend sensitivity","text":"Changelog MIDI voice selection and bend sensitivity","href":"./changelog.html#midi-voice-selection-and-bend-sensitivity"},{"title":"Changelog · Voice v7 waveforms and held noise","text":"Changelog Voice v7 waveforms and held noise","href":"./changelog.html#voice-v7-waveforms-and-held-noise"},{"title":"Changelog · Optional stereo effects","text":"Changelog Optional stereo effects","href":"./changelog.html#optional-stereo-effects"},{"title":"Changelog · Release hygiene and verification","text":"Changelog Release hygiene and verification","href":"./changelog.html#release-hygiene-and-verification"},{"title":"Changelog · v1.10","text":"Changelog v1.10","href":"./changelog.html#v110"},{"title":"Changelog · Portable music and expressive file exchange","text":"Changelog Portable music and expressive file exchange","href":"./changelog.html#portable-music-and-expressive-file-exchange"},{"title":"Changelog · Acceptance boundaries","text":"Changelog Acceptance boundaries","href":"./changelog.html#acceptance-boundaries"},{"title":"Changelog · Pre-release checkout verification","text":"Changelog Pre-release checkout verification","href":"./changelog.html#pre-release-checkout-verification"},{"title":"Changelog · Release-candidate verification","text":"Changelog Release-candidate verification","href":"./changelog.html#release-candidate-verification"},{"title":"Changelog · v1.9","text":"Changelog v1.9","href":"./changelog.html#v19"},{"title":"Changelog · Integration and musical workflows","text":"Changelog Integration and musical workflows","href":"./changelog.html#integration-and-musical-workflows"},{"title":"Changelog · Documentation and acceptance","text":"Changelog Documentation and acceptance","href":"./changelog.html#documentation-and-acceptance"},{"title":"Changelog · Observed verification and review boundaries","text":"Changelog Observed verification and review boundaries","href":"./changelog.html#observed-verification-and-review-boundaries"},{"title":"Changelog · v1.8.1","text":"Changelog v1.8.1","href":"./changelog.html#v181"},{"title":"Changelog · Documentation and packaging","text":"Changelog Documentation and packaging","href":"./changelog.html#documentation-and-packaging"},{"title":"Changelog · Security and observed release-candidate verification","text":"Changelog Security and observed release-candidate verification","href":"./changelog.html#security-and-observed-release-candidate-verification"},{"title":"Changelog · v1.8","text":"Changelog v1.8","href":"./changelog.html#v18"},{"title":"Changelog · Engine and integration","text":"Changelog Engine and integration","href":"./changelog.html#engine-and-integration"},{"title":"Changelog · Security and observed verification","text":"Changelog Security and observed verification","href":"./changelog.html#security-and-observed-verification"},{"title":"Changelog · GitHub release package and post-release documentation","text":"Changelog GitHub release package and post-release documentation","href":"./changelog.html#github-release-package-and-post-release-documentation"},{"title":"Changelog · v1.7","text":"Changelog v1.7","href":"./changelog.html#v17"},{"title":"Changelog · Musical scheduling, rendering and expressive control","text":"Changelog Musical scheduling, rendering and expressive control","href":"./changelog.html#musical-scheduling-rendering-and-expressive-control"},{"title":"Changelog · Pre-release feature verification","text":"Changelog Pre-release feature verification","href":"./changelog.html#pre-release-feature-verification"},{"title":"Changelog · v1.7 release-candidate verification","text":"Changelog v1.7 release-candidate verification","href":"./changelog.html#v17-release-candidate-verification"},{"title":"Changelog · v1.6","text":"Changelog v1.6","href":"./changelog.html#v16"},{"title":"Changelog · Acoustic quality, expression and adoption","text":"Changelog Acoustic quality, expression and adoption","href":"./changelog.html#acoustic-quality-expression-and-adoption"},{"title":"Changelog · v1.6 verification record","text":"Changelog v1.6 verification record","href":"./changelog.html#v16-verification-record"},{"title":"Changelog · v1.5","text":"Changelog v1.5","href":"./changelog.html#v15"},{"title":"Changelog · Complete expressive playback and lifecycle","text":"Changelog Complete expressive playback and lifecycle","href":"./changelog.html#complete-expressive-playback-and-lifecycle"},{"title":"Changelog · Independent numerical and security acceptance","text":"Changelog Independent numerical and security acceptance","href":"./changelog.html#independent-numerical-and-security-acceptance"},{"title":"Changelog · v1.5 release verification","text":"Changelog v1.5 release verification","href":"./changelog.html#v15-release-verification"},{"title":"Changelog · WebKit interruption repair","text":"Changelog WebKit interruption repair","href":"./changelog.html#webkit-interruption-repair"},{"title":"Changelog · v1.4","text":"Changelog v1.4","href":"./changelog.html#v14"},{"title":"Changelog · Sound quality and realtime acceptance","text":"Changelog Sound quality and realtime acceptance","href":"./changelog.html#sound-quality-and-realtime-acceptance"},{"title":"Changelog · Prepared patches and expressive notes","text":"Changelog Prepared patches and expressive notes","href":"./changelog.html#prepared-patches-and-expressive-notes"},{"title":"Changelog · Audio-clock scheduling and deployment","text":"Changelog Audio-clock scheduling and deployment","href":"./changelog.html#audio-clock-scheduling-and-deployment"},{"title":"Changelog · Preset and DX7 audition","text":"Changelog Preset and DX7 audition","href":"./changelog.html#preset-and-dx7-audition"},{"title":"Changelog · Feature verification before release","text":"Changelog Feature verification before release","href":"./changelog.html#feature-verification-before-release"},{"title":"Changelog · v1.4 release verification","text":"Changelog v1.4 release verification","href":"./changelog.html#v14-release-verification"},{"title":"Changelog · v1.3.0","text":"Changelog v1.3.0","href":"./changelog.html#v130"},{"title":"Changelog · Release verification and security review","text":"Changelog Release verification and security review","href":"./changelog.html#release-verification-and-security-review"},{"title":"Changelog · Detailed English website security policy","text":"Changelog Detailed English website security policy","href":"./changelog.html#detailed-english-website-security-policy"},{"title":"Changelog · Website favicon","text":"Changelog Website favicon","href":"./changelog.html#website-favicon"},{"title":"Changelog · English example catalog","text":"Changelog English example catalog","href":"./changelog.html#english-example-catalog"},{"title":"Changelog · Node.js 22 minimum","text":"Changelog Node.js 22 minimum","href":"./changelog.html#nodejs-22-minimum"},{"title":"Changelog · Complete JavaScript/map/declaration triplets","text":"Changelog Complete JavaScript/map/declaration triplets","href":"./changelog.html#complete-javascriptmapdeclaration-triplets"},{"title":"Changelog · Minification and TypeScript source maps","text":"Changelog Minification and TypeScript source maps","href":"./changelog.html#minification-and-typescript-source-maps"},{"title":"Changelog · JavaScript-only distribution assets","text":"Changelog JavaScript-only distribution assets","href":"./changelog.html#javascript-only-distribution-assets"},{"title":"Changelog · TypeScript source cutover","text":"Changelog TypeScript source cutover","href":"./changelog.html#typescript-source-cutover"},{"title":"Changelog · v1.2.0","text":"Changelog v1.2.0","href":"./changelog.html#v120"},{"title":"Changelog · CI failure repairs","text":"Changelog CI failure repairs","href":"./changelog.html#ci-failure-repairs"},{"title":"Changelog · Final deadline repair","text":"Changelog Final deadline repair","href":"./changelog.html#final-deadline-repair"},{"title":"Changelog · Readiness and additive features","text":"Changelog Readiness and additive features","href":"./changelog.html#readiness-and-additive-features"},{"title":"Changelog · Verification and release gate","text":"Changelog Verification and release gate","href":"./changelog.html#verification-and-release-gate"},{"title":"Changelog · Security review","text":"Changelog Security review","href":"./changelog.html#security-review"},{"title":"Changelog · v1.1.0","text":"Changelog v1.1.0","href":"./changelog.html#v110-1"},{"title":"Changelog · Usage documentation","text":"Changelog Usage documentation","href":"./changelog.html#usage-documentation"},{"title":"Changelog · Optimization and compact distribution","text":"Changelog Optimization and compact distribution","href":"./changelog.html#optimization-and-compact-distribution"},{"title":"Changelog · Measurements and verification","text":"Changelog Measurements and verification","href":"./changelog.html#measurements-and-verification"},{"title":"Changelog · Security review","text":"Changelog Security review","href":"./changelog.html#security-review-1"},{"title":"Changelog · v1.0.0","text":"Changelog v1.0.0","href":"./changelog.html#v100"},{"title":"Changelog · Security review","text":"Changelog Security review","href":"./changelog.html#security-review-2"},{"title":"Acoustic references and decimation","text":"Acoustic references and decimation # Acoustic references and decimation\n\nEvery profile renders operators at an internal multiple of the output rate and decimates with a cascade of original linear-phase half-band FIR filters. Each 2:1 stage has a centre tap of 1/2 and every other tap zero, so it costs one multiply per unique coefficient pair, and it is evaluated in polyphase form: oversampled inputs are only buffered, and the whole cascade runs once per output frame on the last internal substep. The default standard profile uses four-times internal sampling: a 15-tap stage at 4× followed by a 47-tap stage at 2×. Passband is flat to 0.40 times output sample rate; products that would fold below that edge (from 0.60 Fs upward) are rejected by at least 81 dB. Voices are panned and summed to left/right at the internal rate, and one shared decimator per output channel runs after the mix, so filter work no longer scales with polyphony. Each channel has 158 preallocated state scalars (counter/ring positions, the frame's substep inputs and doubled delay rings); there are no allocations. A centered (dual-mono) mix filters once until the channel inputs first differ. Because note gain, expression, pan and steal fades apply before the decimator, a hard gain step is band-limited by it and rings for the FIR's length (at most 26 output frames, standard/high) plus its linear-phase delay instead of cutting at the output rate.\n\nBefore this change (v1.11.1 and earlier) the decimator was an eighth-order bilinear Butterworth IIR with cutoff 0.30 output Fs (fourth order for eco). Its tables are kept below for comparison.\n\nConstruction accepts quality: 'eco' | 'standard' | 'high' in SynthOptions, offline render options and OPM options. Quality cannot change on an active synth; OPM retains it across restart. maxVoices accepts 1–32, with default 8; larger budgets are opt-in; the profile tables and the stealing-fade ceiling (eight) are unchanged.\n\n| Profile | Internal rate | Half-band stages (taps) | Design rejection | Group delay | Tradeoff |\n| --- | ---: | --- | ---: | ---: | --- |\n| eco | 2× | 35 | 63 dB | 8.5 frames | Fewer oscillator substeps, earlier internal Nyquist and weaker rejection |\n| standard (default) | 4× | 15 → 47 | 81 dB | 13.25 frames | Acceptance baseline |\n| high | 8× | 11 → 15 → 47 | 81 dB | 13.875 frames | Higher internal Nyquist, twice standard oscillator substeps; not an arbitrary-patch alias-free guarantee |\n\nCoefficients are equiripple (Parks–McClellan) designs using the standard half-band construction: a length-\\((N+1)/2\\) low-pass with band \\([0, 2f_p]\\) is interleaved with the 1/2 centre tap. Band edges, as fractions of each stage's input rate, are 0.20/0.30 for the final 2:1 stage (and eco), 0.10/0.40 for the 4×→2× stage and 0.05/0.45 for the 8×→4× stage. Passband ripple is below 0.001 dB for standard and high and 0.006 dB for eco.\n\n## Response and choice\n\nFor stage input rate \\(R\\) and unique taps \\(g_k\\), each stage's transfer is\n\n\\[\nHs(f)=e^{-i\\omega(2M-1)}\\Bigl(\\tfrac12+2\\sum{k=1}^{M} g_k\\cos\\bigl((2k-1)\\omega\\bigr)\\Bigr),\\qquad \\omega=2\\pi f/R,\n\\]\n\nand the profile's response is the product over its stages, evaluated at the internal rate so source products fold at the internal Nyquist before filtering. The test reference reads the published taps (the design contract) and evaluates this DTFT directly; it does not use the production polyphase rings. Fourier inversion supplies the impulse reference and envelope-transition convolution.\n\nAnalytical response in dB for the standard profile at 48 kHz (rounded; not observed listening results):\n\n| Frequency / output Fs | Former four-pole .20 Fs | Former Butterworth order-8 .30 Fs | Half-band FIR cascade |\n| --- | ---: | ---: | ---: |\n| .10 | -3.84 | approximately 0 | approximately 0 |\n| .20 | -11.90 | -0.006 | +0.001 |\n| .25 | -16.12 | -0.209 | -0.001 |\n| .35 | -23.92 | -11.51 | +0.001 |\n| .40 | — | -21.06 | -0.001 |\n| .45 | — | -29.86 | -0.70 |\n| .50 | -33.52 | -37.90 | -6.05 |\n| .60 | — | -52.29 | -81.39 |\n| .625 | -39.88 | -55.62 | -81.56 |\n| 1.125 | -55.93 | -112.88 | -92.75 |\n| 1.6 | — | -177.26 | -89.35 |\n\nThe FIR keeps the upper octave (e.g. 16.8 kHz at 48 kHz, formerly -11.5 dB) and rejects products that fold into the audible band by about 26–29 dB more than the Butterworth near output Nyquist. Two tradeoffs are deliberate: between 0.50 and 0.60 Fs the transition band lets products fold into 0.40–0.50 Fs (above the flat passband; above 19.2 kHz at 48 kHz but from 17.6 kHz at 44.1 kHz), and far above Nyquist the bilinear Butterworth's zero at the internal Nyquist gave deeper rejection than the FIR's flat 81–93 dB stopband. Neither is a guarantee against arbitrary FM-generated frequencies above the internal Nyquist limit.\n\n## Phase, tails and CPU tradeoffs\n\nThe cascade is linear phase: every frequency is delayed by the same constant, 13.25 output frames for standard (about 276 microseconds at 48 kHz; 8.5 frames for eco, 13.875 for high). The former IIR had a frequency-dependent delay of about 2.7–3.3 frames. Actual output is taken on the last internal substep, which adds a .75-frame phase advance relative to the first substep's clock. Neither value is total AudioContext/device latency. A note released with zero release time therefore still sounds for the filter's delay before its finite history drains.\n\nThe impulse response is finite and symmetric; it has pre- and post-ringing at the transition band rather than the IIR's one-sided ringing. Independent impulse integration gives approximately 1.758 for the absolute impulse sum and 93.16 internal samples for the absolute first moment. Full-feedback gates use these impulse-sum and absolute-moment bounds rather than assuming unit-bounded filter output. Stereo output saturation/headroom remains separate.\n\nWork per output frame is 20 multiplies for standard (twice 4 for the 4×→2× stage, 12 for the final stage) instead of 16 biquads (80 multiplies), 9 for eco and 32 for high. The per-substep call only stores its input, so the synth's voice loop inlines it. Realtime deadline acceptance must still be measured on each host, not inferred from operation counts.\n\n## Coverage and evidence\n\nThe checkout-only maintenance command npm run sound-quality (about four minutes on the development laptop) reports, for each of eco, standard and high:\n\n- Controlled synthesized passband/THD/folded aliases. Standard keeps the original 22.05/44.1/48/96 kHz grid; eco and high run 44.1/48/96 kHz. Measured passband loss is compared with the direct transfer above. In the half-band run at 48 kHz the measured and predicted losses agreed within 0.0001 dB at every checked frequency; at .35 Fs they were −0.0056 dB (eco) and +0.0009 dB (standard and high), where the former Butterworth lost 7.3–11.5 dB. THD of harmonics 2–8 was 0.0208 % for all three.\n- Folded ultrasonic products. Standard (−81.6 dB at .625 Fs, −92.8 dB at 1.125 Fs), high (−81.6 / −93.1 dB) and eco (−93.3 dB at .625 Fs; its 1.125 Fs source exceeds the oscillator's 0.45 internal-rate clamp) all stay below the 30 dB floor, and each is also gated by the direct transfer's prediction so a filter regression is caught even above the floor. The former Butterworth gave −55.6 dB (standard) and −37.4 dB (eco) at .625 Fs.\n- Independent high-index Bessel PM and contractive delayed-feedback references with separate in-band and folded-product budgets at 44.1/48/96 kHz; the worst Bessel coefficient error was 9.0e-10 (eco), 1.0e-9 (standard) and 1.0e-9 (high).\n- Explicit four-operator nested-chain, branched-leaf and two-carrier equations at 44.1/48/96 kHz. Unequal ratios, indices 1.5/4/8, upper/lower registers and independent ADSR attack/decay/held/interrupted-attack/release boundaries detect incorrect routing, modulation depths and carrier normalization. The reference derives envelopes directly in dB and filters by frequency-domain convolution, not by copying the engine loop.\n- A lifecycle/headroom/deterministic-chunk matrix: 2,505 cases over algorithms 0–7 × feedback 0/7 × MIDI 24/60/96 × velocity × polyphony 1/8/16, plus opt-in 32-voice boundary rows (32, 33 and 40 simultaneous notes with maxVoices 32). Every case renders whole and in 127-frame chunks and must be bit-identical, finite, within headroom, audible (velocity above zero), silent after release and report exactly one terminal event per note.\n- Live-control rows (24): for each profile and algorithm, glide, operatorRatios ramp, feedback ramp, operatorADSR, level/expression/pan ramp and fixed-Hz edits are applied at sample boundaries to a note carrying selective AM/PM LFO targets. Whole and chunked renders are bit-identical, finite, bounded and fully terminal. This proves stability, not smoothness to a listener.\n- Streaming: the standard profile keeps the two 120-second deterministic replays; eco and high render 12-second replays with held LFO, interrupted glides and settled-sine residual checks.\n\nThe standalone decimator comparison (actual filter magnitude, complex phase and impulse latency versus independent transfer mathematics and the former filter) remains standard-profile only. A standalone 48 kHz module smoke for that profile observed +0.00085/+0.00092 dB at .20/.35 Fs and -81.55868/-92.75183 dB for the .625/1.125 Fs folded products; decimated impulse energy delay was 12.5 output frames (13.25-frame group delay less the .75-frame substep advance) and its maximum difference from independent Fourier inversion was below 4e-16. No listening session, perceptual preference, arbitrary-patch alias freedom, chip fidelity, or physical-device CPU/underrun result is asserted here.\n\nThe checkout-only scripts/benchmark.ts retains the historical baseline gates and report-only quality/polyphony/multibus comparisons, and adds a full held-note matrix: 1, 4, 8, 16 and 32 voices for each of eco, standard and high. The matrix sets maxVoices equal to the number of held notes. See conservative host capacity candidates below and audio buses. Timing is not realtime acceptance merely because a separate baseline budget is configured; fewer arithmetic operations do not prove perceptual preference or physical-device stability.\n\nHistorical 1.8.0 checkout measurement (not the new capacity matrix): Apple M5, darwin arm64, Node.js 26.7.0, 48 kHz, 128-frame blocks (2.667 ms deadline), 300 excluded warmup blocks and 2,000 measured blocks per row. No explicit acceptance budgets were configured.\n\n| Profile | Median ms | p95 ms | p99 ms | Worst ms | p99 / deadline | Misses |\n| --- | ---: | ---: | ---: | ---: | ---: | ---: |\n| Eco | 0.212375 | 0.231625 | 0.279708 | 0.453166 | 0.105 | 0 / 2,000 |\n| Standard | 0.380333 | 0.423750 | 0.509292 | 0.607917 | 0.191 | 0 / 2,000 |\n| High | 0.680709 | 0.747333 | 0.819917 | 0.981125 | 0.307 | 0 / 2,000 |\n\nAll rows reported zero DSP errors and admission rejections. These are offline host wall-clock measurements, not AudioWorklet underrun counters, allocation measurements, listening judgments or physical-device acceptance. They do not replace the different historical v1.6 host results recorded in CHANGELOG.\n\nVoices retire on their envelope: once every operator release has ended, the voice is freed and its ended notification fires without waiting for filter history. The shared decimator keeps draining that history; envelope-transition references compare the waveform through the frame where the channel's buffered FIR inputs all fall below the state floor, then require exact silence. The implementation clears that sub-floor history (only on a channel whose input is silent) rather than waiting for exact zeros. A state floor is not a bound on each future output sample: the independent bound multiplies the floor by each stage's absolute tap sum.\n\n## Conservative host capacity candidates\n\nScope note (v1.11): the benchmark patches use sine operators only. Non-sine operator waveforms (half, abs, quarter, alternating, camel, square, saw) add a per-sample waveform evaluation and noise adds a bounded LFSR step, so patches that use them cost more per voice than the measured candidates indicate; the work stays bounded by the voice limit, but the capacity candidates below were not re-measured for those voices.\n\nThe v1.10 checkout adds this matrix/candidate output through the checkout-only maintenance command; it is not a packaged API or command. From the matching 1.10.0 source checkout, run:\n\nsh\nOPMBENCHCAPACITYP99RATIO=0.5 npm run benchmark\n\n\nThe capacity ratio defaults to 0.5 and may only be made stricter, in (0, 0.5]. This means retaining at least half of the nominal block deadline as observed p99 reserve, not reserving a guaranteed amount of real audio-thread time. A row is eligible only when:\n\n- Its observed p99/deadline ratio is at or below that threshold.\n- It has zero measured deadline misses.\n- Accumulated DSP/processor errors and admission rejections are zero, voice/event limits hold, held-note occupancy matches the authored workload, and the final PCM block of every engine is finite.\n- It is not the idle baseline.\n\nJSON results preserve every measured row, including ineligible rows, raw worst time, miss count, diagnosticIssues and capacity.reasons. capacityGuidance.candidates refer to eligible caseIds and repeat each row's exact workload, p99 reserve, worst time and misses. No candidate is chosen across unrelated workloads, no quality setting changes automatically, and an empty candidate list is a valid result.\n\nEach row records quality, per-engine maxVoices, engineCount, voicesPerEngine, patch ID, workload kind and whether it exercised core rendering or the real processor in a Node host shell. The report records VERSION, Node version, CPU/platform/architecture, rate, 128-frame block/deadline, excluded warmup, measured iterations, exact authored patches and note/dispatch recipes. A historical one-held-note row with maxVoices: 8 is evidence for one active note, not eight-note capacity. Matrix rows are fully occupied; burst rows include note creation, queue splitting and bounded stealing fades. Several-engine rows include sequential independent DSP renders, not browser routing/effects overhead.\n\nThe original six baseline scenarios remain the only timing-gated rows when OPMBENCHP99BUDGETRATIO or OPMBENCHWORSTBUDGETRATIO is set. Existing report-only comparisons and the new matrix remain report-only for these gates; diagnostic failures still cause a failing exit status. Capacity eligibility is independent: passing a configured CI budget does not make a row eligible, and an ineligible timing row alone does not fail an unbudgeted benchmark. Existing OPMBENCHSAMPLERATE, OPMBENCHBLOCKS and OPMBENCH_WARMUP settings control the experiment (defaults 48000, 2000 and 300).\n\nKeep the full report with the matching checkout/commit and record surrounding host load. Repeat under representative contention and sustained thermal load; row order, JIT, scheduler/GC pauses and a finite sample count affect the measurements. Worst time and misses must remain visible even when p99 looks good. The finite check samples the final block, not every output sample in the experiment; functional DSP tests provide separate coverage.\n\nTreat eligible rows as candidate host configurations requiring browser and physical-device verification, not portable voice limits, AudioWorklet underrun counters, mobile safety, or realtime certification. Re-run the application's exact voices, releases, automation, raw/prepared dispatch rate, number of engines and effects graph in its target browser. Then complete physical-device/manual acceptance for interruptions, sustained playback, audible glitches and actual MIDI hardware where relevant. Node timing cannot substitute for a real device or a human listener, and neither candidate eligibility nor CPU measurements establish a perceptual preference between quality profiles.\n\n### Observed checkout measurement\n\nOn Apple M5 / darwin arm64, Node v26.7.0, the pre-release package-1.9.0 checkout was measured at 48000 Hz, 128 frames (2.667 ms deadline), 300 warmup blocks excluded and 2000 measured blocks per row. No build, test suite or browser scenario ran concurrently; surrounding OS load was not controlled. This is one observation, not a measurement of package 1.10.0 or thermal/host-contention acceptance. All 29 rows retained clean diagnostics and zero observed misses. Matrix results:\n\n| Quality | Held voices / configured limit | p99 ms | Worst ms | Observed p99 reserve | Candidate |\n| --- | ---: | ---: | ---: | ---: | --- |\n| eco | 1 / 1 | 0.026 | 0.120 | 99.0% | yes |\n| eco | 4 / 4 | 0.090 | 0.173 | 96.6% | yes |\n| eco | 8 / 8 | 0.176 | 0.269 | 93.4% | yes |\n| eco | 16 / 16 | 0.418 | 0.850 | 84.3% | yes |\n| eco | 32 / 32 | 0.717 | 0.810 | 73.1% | yes |\n| standard | 1 / 1 | 0.042 | 0.063 | 98.4% | yes |\n| standard | 4 / 4 | 0.155 | 0.230 | 94.2% | yes |\n| standard | 8 / 8 | 0.329 | 0.377 | 87.7% | yes |\n| standard | 16 / 16 | 0.593 | 0.649 | 77.8% | yes |\n| standard | 32 / 32 | 1.196 | 1.753 | 55.1% | yes |\n| high | 1 / 1 | 0.073 | 0.104 | 97.3% | yes |\n| high | 4 / 4 | 0.258 | 0.324 | 90.3% | yes |\n| high | 8 / 8 | 0.503 | 0.609 | 81.1% | yes |\n| high | 16 / 16 | 1.044 | 1.253 | 60.9% | yes |\n| high | 32 / 32 | 2.126 | 2.477 | 20.3% | no |\n\nFor this exact held-brass workload, the highest sampled eligible configurations were eco/32, standard/32 and high/16; high/32 failed the 50% p99-reserve criterion despite zero observed misses. Start with the unchanged standard/eight default unless the application needs more voices; test the selected configuration under its actual graph/load rather than deploying the maximum sampled candidate blindly. Raw and prepared burst p99/worst were 0.683/2.162 ms and 0.625/0.676 ms respectively, both with zero misses; these are separate processor workloads, not proof that any 32-voice dispatch workload has the held-note result.\n","href":"./acoustic-quality.html"},{"title":"Acoustic references and decimation · Acoustic references and decimation","text":"Acoustic references and decimation Acoustic references and decimation","href":"./acoustic-quality.html#acoustic-references-and-decimation"},{"title":"Acoustic references and decimation · Response and choice","text":"Acoustic references and decimation Response and choice","href":"./acoustic-quality.html#response-and-choice"},{"title":"Acoustic references and decimation · Phase, tails and CPU tradeoffs","text":"Acoustic references and decimation Phase, tails and CPU tradeoffs","href":"./acoustic-quality.html#phase-tails-and-cpu-tradeoffs"},{"title":"Acoustic references and decimation · Coverage and evidence","text":"Acoustic references and decimation Coverage and evidence","href":"./acoustic-quality.html#coverage-and-evidence"},{"title":"Acoustic references and decimation · Conservative host capacity candidates","text":"Acoustic references and decimation Conservative host capacity candidates","href":"./acoustic-quality.html#conservative-host-capacity-candidates"},{"title":"Acoustic references and decimation · Observed checkout measurement","text":"Acoustic references and decimation Observed checkout measurement","href":"./acoustic-quality.html#observed-checkout-measurement"},{"title":"Adaptive music, tempo curves and the seek contract","text":"Adaptive music, tempo curves and the seek contract # Adaptive music, tempo curves and the seek contract\n\nThis guide covers createArrangement (looping layers and sections that switch on musical boundaries), tempo curves and grid helpers shared by createTransport, and the exact meaning of pause, seek and resume. The v1.10 example 09 (checkout-only) demonstrates the scheduler and portable arrangement save/load. The portable APIs ship in package 1.10.0; build and serve examples/adaptive.html from the matching source checkout to use the example.\n\n## Arrangement\n\nts\nimport { OPM, createArrangement } from 'opm.js';\n\nconst opm = new OPM({ maxVoices: 16 });\nconst arrangement = createArrangement(opm, {\n bpm: 96,\n initialSection: 'explore',\n layers: [\n { name: 'pad', length: 16, voicePriority: 20,\n events: [{ type: 'note', id: 1, beat: 0, duration: 16, note: 48, voice: 'brass' }] },\n { name: 'arp', length: 8, events: [\n { type: 'note', id: 2, beat: 0, duration: 0.5, note: 60 },\n { type: 'note', id: 3, beat: 1, duration: 0.5, note: 67 },\n ] },\n { name: 'lead', length: 8, voicePriority: 100, events: [\n { type: 'note', id: 4, beat: 0, duration: 2, note: 72 },\n ] },\n ],\n sections: [\n { name: 'explore', layers: ['pad', 'arp'] },\n { name: 'combat', layers: ['pad', 'lead'] },\n ],\n});\nawait arrangement.start(); // call from a user gesture\narrangement.switchSection('combat', { fade: 0.8 }); // crossfade; returns committed beat\narrangement.setLayer('arp', true, { quantize: 'beat', fade: 0.4 });\narrangement.setLayerGain('pad', 0.6, { quantize: 'beat', fade: 1 });\n\n\nIn a plain browser, import the served ./opm/api/index.js URL instead of the bare package name and invoke the startup/switch calls from a button handler after deploying the complete tree as in the quick start. This runnable recipe uses the default brass voice; register your own pad/lead patches with loadVoice before using their names. On host teardown, call arrangement.dispose() and await opm.dispose().\n\nRules that make switches musical:\n\n- One global beat grid. Every layer loops with its own length (quarter-note beats, at most 256), aligned to beat 0 of the arrangement. A layer that two sections share is one continuous schedule, so its sounding notes are not retriggered or stopped when other layers change.\n- Commit boundary. switchSection and setLayer commit at the first quantize boundary ('bar' default, 'beat', or a number of quarter notes) that is not earlier than the notes already admitted to the AudioContext clock. The returned value is that beat. With the default 0.2 s lookahead, a switch requested inside the final instants before a bar line lands on the following bar instead of rewriting notes that are already scheduled.\n- Removed layers. Notes that started before the boundary are released at the boundary (their release tail still sounds). Pass preserveNotes: true to let them finish naturally. Notes that would start at or after the boundary are never admitted.\n- Layer gain and crossfades. A layer's gain is 0..1 (default 1). setLayerGain(name, gain, { quantize?, fade? }) returns its committed beat; quantization follows section changes. fade is a linear seconds-based envelope in 0..10 (default 0), not a beat duration. Section/layer changes with a positive fade ramp removed layers to zero and new layers from zero to their current gain target; shared layers are untouched. Removed sustained gates release when the fade finishes unless preserveNotes is true; shorter gates may release naturally during the fade. Zero-fade removal retains the existing audible release-tail behavior; preserveNotes: true with zero fade leaves sounding gates unchanged.\n- Independent expression. Layer gain multiplies authored note expression exactly in DSP, including simultaneous ramps, new onsets midway through a fade and owned release tails. Interrupted gain ramps retarget from their boundary value. The arrangement reserves note-control gain; authored layer events must use expression instead. setLayerGain does not change unrelated layers, notes admitted by another helper, or the engine's global mix gain. Gain targets survive stop; stop restores the initial section.\n- Voice priority. A layer's voicePriority (0–127) is combined with each note's own value by taking the maximum. When the engine is full, lower-priority notes are refused instead of stealing a protected melody. Refusals are counted in snapshot.priorityDrops and do not stop the arrangement; any other rejection does stop it and calls onError.\n- Bounds. At most 16 layers, 32 sections and 65,536 events in total; at most 128 outstanding notes and 256 queued commands. Each lookahead window allows at most 32 loop repetitions per active layer, 65,536 candidate-note inspections and 256 command admissions, including gain controls. At most 256 retained gain changes and 256 pending section changes; excess capacity rejects mutation or stops scheduling through onError. Future automation streams incrementally; terminal notes discard their remaining commands. Loop lengths are 1/1024–256 quarter notes; note onsets lie inside the loop while durations/owned controls may cross a boundary. Names match [A-Za-z0-9_-]{1,64}; layer/section names cannot be duplicated, and every reference must exist. Repeated section members normalize to one membership in first-occurrence order. Options and events are copied as own data; accessors, sparse arrays and unknown fields are rejected.\n- Ownership. Only notes the arrangement admitted are stopped by pause(), stop() or dispose(). The shared OPM and its context are never closed.\n\nsnapshot reports state, position, bar/beat, the current and pending sections and layers, the tempo map and priorityDrops. setTempo(bpm) and setTempoMap(map) replace the tempo from the current beat on; notes that were already admitted keep the audio times they were admitted with (at most one lookahead window).\n\n## Save and reload an interactive arrangement\n\nKeep a definition separate from the live scheduler. parseArrangementProject / serializeArrangementProject, available since v1.10, store a version 1 ArrangementProject containing named complete voices, normalized synthesis settings, tempo/meter, layers (length, events, gain and priority), named sections, and initialSection. This is distinct from the unchanged ScoreProject v1. See the self-contained creation and live replay recipe.\n\nPortable parsing and live creation share a DOM-free validator; neither creates audio or fetches assets. After parsing, create new OPM({ ...project.settings }), clear its implicit brass registry with replaceVoiceBank([]), load each [name, voice] from project.voices, and pass { layers, sections, initialSection, tempoMap, timeSignature } to createArrangement. Only start() inside a user gesture plays it. Loading/replacing must dispose the old owned arrangement first; teardown the old owned synth without closing another component's borrowed context.\n\nThe local current adaptive page accepts arbitrary valid project names and rebuilds its section/layer/gain controls from them. Section switches become the saved initialSection; layer toggles edit that section's membership, and gain/tempo edits update the saved definition. Save definition → Load arrangement JSON → Start restores those requested targets and registered voices/settings, after which switching and fading still work. A pending boundary is saved as its requested target, not its transition timeline. A malformed or oversized file rejects visibly; uploads are bounded before reading. Loading and Reset demo stop previous owned playback and never auto-play. Reset demo discards the imported definition and applies the selected swing.\n\nThe adaptive page owns and closes its own context, with a separate 16% host monitoring gain. It preserves saved synthesis settings and reports unsupported context/sample-rate failures instead of replacing them. A component borrowing a host context must not close it.\n\nSaving is not a snapshot of a live Arrangement. It excludes beat position, queued changes, running fades, active gates, oscillator/envelope/LFO/filter state and release tails. Replay begins at beat 0; no exact DSP-state restore is claimed. Budgets are 8 MiB, 128 named voices / 256 KiB normalized bank, 1–16 layers, at most 32 sections and 65,536 total events. Default tempo/meter/settings are the same as score projects, gain defaults to 1 and priority to 0; authored note-control gain remains reserved.\n\n\n## Tempo curves\n\nTempoPoint is { beat, bpm, curve?, endBpm? }. Without curve (or with 'step') the tempo is constant until the next point, exactly as in 1.7. With curve: 'linear' the BPM changes linearly per beat to the next point's bpm, or to endBpm if given (endBpm is only valid with 'linear'; the last point cannot be linear).\n\nFor a segment starting at beat b0 with bpm0 and slope r = (bpm1 − bpm0) / (b1 − b0), the time to beat b is\n\n$$t(b) = \\int{b0}^{b}\\frac{60}{\\mathrm{bpm}0 + r\\,(x-b0)}\\,dx = \\frac{60}{r}\\ln\\left(1 + \\frac{r\\,(b-b0)}{\\mathrm{bpm}0}\\right)$$\n\n(60·Δb/bpm0 when r = 0), and the inverse is Δb = bpm0 · expm1(r·t / 60) / r. beatsToSeconds and secondsToBeats use these closed forms with log1p and expm1, so ramps convert exactly across segment boundaries. setTempo on a running transport or arrangement truncates any ramp that is under way at the current beat (its endBpm becomes the interpolated BPM) and inserts a new constant tempo there.\n\nLinear maps whose derived slope is not finite are rejected before admission, including subnormal beat intervals that would overflow the BPM-per-beat calculation.\n\n## Grid helpers\n\nImport these grid helpers from opm.js (or the deployed browser api/index.js), not opm.js/core. Beat/second conversion helpers are exported by both entry points.\n\n- quantizeBeat(beat, quantum = 1, mode = 'ceil') with modes floor | ceil | nearest | next (next is strictly later even on an exact boundary).\n- swingBeat(beat, subdivision = 0.5, ratio = 2/3) warps each pair of subdivisions; a ratio of 0.5 is straight and 2/3 a triplet feel. It is monotonic and maps pair boundaries to themselves.\n- swingBeatEvents(events, subdivision, ratio) swings note starts and ends, so adjacent gates keep their order.\n\n## What pause, seek and resume mean\n\ncreateTransport and createArrangement are musical restarts, not DSP checkpoints.\n\n| State | Preserved | Not preserved |\n| --- | --- | --- |\n| Musical position (beats), tempo map, loop | yes | |\n| Transport seek()/loop wrap into a sustained note | the note restarts at the target beat, with pitch, expression, gain, pan, modulation, ratios, feedback, LFO, level and ADSR values reconstructed from the score's controls and remaining ramps | oscillator phase, envelope stage and level, LFO phase, filter and feedback history, any release tail that was sounding |\n| Arrangement pause() / resume() | the beat, effective layer set and current layer gain envelopes; unfinished fades continue after resume | every owned note is released on pause; on resume only notes whose onset lies at or after the resume beat start, so a long pad that was mid-note re-enters at its next onset |\n| Notes admitted by the host outside the transport/arrangement | untouched | |\n\nAn exact snapshot of oscillator phases, envelope state and filters is deliberately not offered: it would couple the public API to private DSP state and make 1.x DSP changes breaking. If a game needs a continuous pad across a scene change, keep that layer in both sections (it is then never interrupted), or keep the pad in a separate engine whose lifetime you control.\nTransport start and every explicit reconstruction use a future audio anchor (startupLead seconds, default min(0.05, horizon / 2), finite 0..10). The musical cursor stays at the destination during this count-in; note durations and loop lengths are not stretched. Arrangement uses the same default lead and freezes its resume cursor during count-in. Lead is delivery headroom, not a guarantee: late/drop failures and missed windows remain errors.\n\n## Verified behavior\n\ntest/arrangement.test.ts in the v1.10 checkout renders through the real AudioWorklet processor. It checks that a pad shared by two sections is admitted once across a bar switch, that a removed layer is released at the exact boundary beat and that preserveNotes lets it finish, that a high-priority melody survives a one-voice engine without failing the arrangement, that a tempo change alters only unadmitted onsets, and that pause schedules nothing. test/transport.test.ts in that checkout checks the logarithmic integral and its inverse to 1e-9 beats. Arrangement-project tests additionally define immutable/canonical parse boundaries, malformed/budget rejection and identical real-worklet PCM after saved-definition replay with section switches and fades; these are automated invariants, not physical device or human listening acceptance.\n","href":"./adaptive-music.html"},{"title":"Adaptive music, tempo curves and the seek contract · Adaptive music, tempo curves and the seek contract","text":"Adaptive music, tempo curves and the seek contract Adaptive music, tempo curves and the seek contract","href":"./adaptive-music.html#adaptive-music-tempo-curves-and-the-seek-contract"},{"title":"Adaptive music, tempo curves and the seek contract · Arrangement","text":"Adaptive music, tempo curves and the seek contract Arrangement","href":"./adaptive-music.html#arrangement"},{"title":"Adaptive music, tempo curves and the seek contract · Save and reload an interactive arrangement","text":"Adaptive music, tempo curves and the seek contract Save and reload an interactive arrangement","href":"./adaptive-music.html#save-and-reload-an-interactive-arrangement"},{"title":"Adaptive music, tempo curves and the seek contract · Tempo curves","text":"Adaptive music, tempo curves and the seek contract Tempo curves","href":"./adaptive-music.html#tempo-curves"},{"title":"Adaptive music, tempo curves and the seek contract · Grid helpers","text":"Adaptive music, tempo curves and the seek contract Grid helpers","href":"./adaptive-music.html#grid-helpers"},{"title":"Adaptive music, tempo curves and the seek contract · What pause, seek and resume mean","text":"Adaptive music, tempo curves and the seek contract What pause, seek and resume mean","href":"./adaptive-music.html#what-pause-seek-and-resume-mean"},{"title":"Adaptive music, tempo curves and the seek contract · Verified behavior","text":"Adaptive music, tempo curves and the seek contract Verified behavior","href":"./adaptive-music.html#verified-behavior"},{"title":"Polyphony budgets, voice priority and independent buses","text":"Polyphony budgets, voice priority and independent buses # Polyphony budgets, voice priority and independent buses\n\nOPM.js has two ways to get more than eight simultaneous sounds, and they cost and sound different. Example 11 (v1.10 source, checkout-only) demonstrates independent buses; run examples/buses.html locally from the matching checkout.\n\n## One engine, more voices\n\nnew OPM({ maxVoices }) (and Synth, renderSequence, the Worker renderer) accepts 1–32 logical voices. The default stays 8. Everything else is unchanged:\n\n- Released notes still occupy a voice until their tail ends, so 32 voices means 32 sounding or releasing notes.\n- The stealing fade is still at most eight ~5 ms fades. DSP voice state is preallocated for maxVoices + 9 slots. Trusted prepared-patch admission reuses that state; raw patches require validation and snapshot allocation. This is not a global zero-allocation promise for host APIs or command dispatch.\n- One output tanh stage and one mixGain apply to the sum of all voices.\n\n### Voice priority\n\nEach note may carry voicePriority, an integer 0–127 (default 0), in playNote, score notes (SequenceEvent, beat events), Synth.noteOn options, renderNote and Performance parts.\n\nWhen the engine is full, an incoming note chooses its victim only among sounding voices with priority less than or equal to its own: the lowest priority first, then the engine's stealing policy (oldest, release-first, quietest) among equals. If every voice outranks it, the note is refused:\n\n- Synth.noteOn throws VoiceAdmissionError before changing any state or consuming an ID;\n- the AudioWorklet reports rejected with reason priority (not counted as a processor error);\n- offline score rendering skips the note and keeps going.\n\nUse it for protected melodies, bass lines and stingers; do not use it to hide an undersized maxVoices, since a protected sound can still fall silent if protected sounds alone fill the engine.\n\n### What it costs\n\nThe checkout-only maintenance command npm run benchmark renders 128-frame blocks of the real DSP. The v1.10 checkout adds exact workload metadata and conservative candidate configurations, using observed p99 ≤ half the block deadline by default, zero measured misses and clean diagnostics (criteria and report fields). Run the matching 1.10.0 source checkout; this command and its reports are not packaged APIs. The full matrix covers 1/4/8/16/32 held voices for every quality profile; raw/prepared eight-start-per-block bursts and two/four independent eight-voice engines remain separate measured workloads. This does not measure browser effects graphs or AudioWorklet underruns.\n\nThe historical 1.8.0 scaling run below was taken on an Apple M5 (Node 26.7.0, 48 kHz, 2.667 ms block deadline) on a laptop whose load average was about 24 from unrelated processes, so it is not a capacity recommendation and the CI budget gate was not applied. It illustrates how cost grows with sounding voices; these relative medians are not portable scaling constants.\n\n| Scenario (median block time ÷ deadline) | Median | Relative to 8 voices |\n| --- | --- | --- |\n| 8 voices, standard | 0.65 | 1.0× |\n| 16 voices, standard (opt-in) | 1.19 | 1.8× |\n| 32 voices, standard (opt-in) | 2.84 | 4.4× |\n| 32 voices, eco (opt-in) | 1.63 | 2.5× |\n| 8 voices, eco / high | 0.31 / 1.00 | 0.5× / 1.5× |\n| 2 engines × 8 voices | 1.29 | 2.0× |\n| 4 engines × 8 voices | 2.26 | 3.5× |\n\nUnder that load even the eight-voice case missed its block deadline at p99 (5.7× in this run; the median stayed at 0.65×). For contrast, the historical unloaded run of the same host class measured the eight-voice standard p99 at 0.19× the deadline (acoustic quality). Neither historical table selects a current capacity candidate. Measure again on the target host, retain all rows including worst times/misses and compare configurations within the same workload. An eligible steady eight-voice row does not establish eligibility for bursts, 32 voices, several buses, different patches or effects.\n\nUse capacityGuidance.candidates to identify exact quality/per-engine maxVoices/engineCount/voicesPerEngine combinations worth verifying, then run the matching application in the target browser and on physical devices under sustained load and interruptions. OPMBENCHCAPACITYP99RATIO defaults to 0.5 and can only tighten selection. The independent OPMBENCHP99BUDGETRATIO and OPMBENCHWORSTBUDGETRATIO retain the original six baseline CI timing gates; comparison/matrix rows remain report-only. No command automatically lowers quality or claims realtime certification. Physical mobile acceptance must be recorded separately; Node host results cannot supply it. Lower voices, shorter tails, fewer buses or eco are configurations to measure and audition, not guaranteed safe fallbacks.\n\n## Several engines on one AudioContext\n\nCreate one OPM per bus and give each the same borrowed context and its own destination node:\n\nts\nconst context = new AudioContext();\nconst lead = new OPM({ context, destination: leadGain, maxVoices: 6 });\nconst pad = new OPM({ context, destination: padFilter, maxVoices: 12 });\nawait Promise.all([lead.start(), pad.start()]); // from a user gesture\n\n\n- Each engine has its own voice budget, mixGain, quality profile and saturation. Web Audio then adds the buses linearly.\n- OPM disconnects only its own node and never closes or suspends a borrowed context. Close the context yourself after disposing every engine.\n- Each engine is a separate AudioWorklet processor; more buses mean more per-block overhead and more CPU (see the table). Share a Transport or Arrangement per engine; they own only their own notes.\n- Optional createEffects() supplies stereo chorus/reverb as a separate AudioWorklet insert; sidechains, metering and mixing remain host-owned. The package still ships no mixer.\n\n### Stems are not a monolithic mix\n\nA single engine saturates the sum of all its voices once. Several engines saturate each stem and then add them, so their sum is a different, equally valid mix. It is not bit-identical to rendering all notes in one engine, and it can exceed full scale if each stem is near its limit. The example's offline \"linear bus mix\" clips to ±1 and reports its peak; lower the stem gains for dense material.\n\nFor offline stems render each bus separately with renderSequence (or the Worker renderer), keeping the same sampleRate, maxVoices and mixGain you use live. A shared start frame keeps stems aligned.\n\n## Optional stereo chorus and reverb\n\nts\nimport { OPM, createEffects } from 'opm.js';\nconst context = new AudioContext();\nconst opm = new OPM({ context, destination: null });\nconst fx = await createEffects(context, {\n params: {\n chorus: { rate: 0.8, depth: 0.6, mix: 0.3 },\n reverb: { size: 0.6, damping: 0.5, mix: 0.25 },\n },\n onEvent: event => console.error(event.error),\n});\nawait opm.start(); // user gesture; starts/resumes synthesis\nopm.node!.connect(fx.input);\nfx.output.connect(context.destination);\n// Replaces the complete configuration, smoothly fading omitted sections to bypass:\nfx.update({ reverb: { size: 0.5, damping: 0.7, mix: 0.2 } });\n// On teardown: fx.dispose(); await opm.dispose(); then close your own context.\n\n\ncreateEffects(context, { workletUrl?, params?, onEvent? }) never starts, suspends or closes the borrowed BaseAudioContext, and never automatically connects its output. input and output are the same stereo-in/stereo-out node; ready: Promise\u003cvoid> resolves at creation, not playback. reset() clears state; dispose() is idempotent and stops the processor. Module URLs follow OPM's secure same-origin relocation rules; default is ../worklet/fx-processor.js relative to the API module. Deploy the entire matching module tree, with JavaScript MIME, CSP and nosniff intact.\n\nOffline, import createStereoEffects(sampleRate, options?) or applyEffects(left, right, sampleRate, params) from opm.js/core (also exported at root). The engine accepts integer 8000–192000 Hz. process(left, right, offset?, length?) edits native Float32 buffers in place with no temporary DSP allocation. applyEffects returns new stereo copies extended by ceil(tailSeconds sampleRate); source stereo lengths must match and the resulting length must not exceed 4,000,000 frames per channel*.\n\n| Section | Parameters |\n| --- | --- |\n| chorus | Required rate 0.05–10 Hz, depth 0–1, mix 0–1; optional feedback 0–0.7 (default 0), integer voices 1–4 (default 2) |\n| reverb | Required size, damping, mix 0–1; optional preDelay 0–0.1 seconds (default 0), width 0–1 (default 1) |\n| order | chorus-reverb (default) or reverb-chorus |\n\nOptions must be strict plain own-data objects: unknown fields, accessors, missing required fields and non-finite/out-of-range numbers reject. params and its sections are frozen snapshots. update() replaces all options with a 20 ms linear parameter ramp; order changes fade to dry before switching and fade back. reset() deterministically clears all delay lines, filters and LFO state and applies the target parameters immediately.\n\nThe original fractional-delay chorus uses a 10 ms base delay with up to ±8 ms sine modulation and phase-spread voices. The original seconds-scaled Schroeder network uses four parallel damped combs and two series all-pass diffusers per channel; it makes no Yamaha/YM2151 or other hardware-fidelity claim. size sets nominal comb T60 to 0.2 + 3 * size seconds; damping low-passes feedback. Width crossfades mono and stereo wet sound.\n\nThe gain law is linear dry/wet crossfade, dry (1 - mix) + wet mix, not equal-power summation. In steady state the wet output has a 0.7 ceiling for unity-bounded input (at least 3 dB headroom): chorus averages voices and compensates feedback by 1 - feedback; comb injection uses 1 - loopGain, and each all-pass is normalized by its worst-case gain of 3. The ceiling is a steady-state property: lowering chorus feedback through update() ramps over 20 ms while the delay line still holds content written at the old, higher feedback, so a resonant input can briefly exceed 0.7 (output stays finite and clamped). Dry output is not attenuated at mix 0. Omitted sections and exact zero mix are bit-transparent for finite input, including signed zero. Host sums can still clip; arbitrary over-unity input is not a mastering limiter, and float32 WAV export of effected audio should be checked or limited by the host.\n\nAll delay state is preallocated: delay samples are at most ceil(0.563 * sampleRate) + 32 Float64 values (plus 52 control/filter values), under 866 KiB at 192 kHz. Processing clamps internal values to ±1,000,000, replaces NaN/Infinity input with zero and flushes magnitudes below 1e-20; malformed PCM cannot permanently poison state. Finite dry bypass remains untouched. Output and reset are deterministic and processing is block-size independent.\n\ntailSeconds is a conservative −80 dB audible-tail bound, not a hard truncation: reverb reserves 0.3 + 3 * T60 seconds and chorus reserves a feedback-dependent bound based on its maximum delay. Cascaded bounds add; updates retain the largest previous bound until reset so old tails remain covered. The bounded worklet accepts at most 32 commands per audio frame boundary, stores no command queue, reports validation/rate failures and keeps close available during flooding. Pace host updates; do not flood the MessagePort.\n\n\n## Choosing\n\n| Need | Use |\n| --- | --- |\n| A melody that accompaniment must not steal | voicePriority on the melody, one engine |\n| More than eight sounding notes, one tonal group | maxVoices up to 32; measure CPU |\n| Separate effects, ducking or stems per instrument group | One engine per bus |\n| Reduce DSP work before target-device measurements | Fewer voices, shorter releases, fewer buses; audition eco |\n","href":"./audio-buses.html"},{"title":"Polyphony budgets, voice priority and independent buses · Polyphony budgets, voice priority and independent buses","text":"Polyphony budgets, voice priority and independent buses Polyphony budgets, voice priority and independent buses","href":"./audio-buses.html#polyphony-budgets-voice-priority-and-independent-buses"},{"title":"Polyphony budgets, voice priority and independent buses · One engine, more voices","text":"Polyphony budgets, voice priority and independent buses One engine, more voices","href":"./audio-buses.html#one-engine-more-voices"},{"title":"Polyphony budgets, voice priority and independent buses · Voice priority","text":"Polyphony budgets, voice priority and independent buses Voice priority","href":"./audio-buses.html#voice-priority"},{"title":"Polyphony budgets, voice priority and independent buses · What it costs","text":"Polyphony budgets, voice priority and independent buses What it costs","href":"./audio-buses.html#what-it-costs"},{"title":"Polyphony budgets, voice priority and independent buses · Several engines on one AudioContext","text":"Polyphony budgets, voice priority and independent buses Several engines on one AudioContext","href":"./audio-buses.html#several-engines-on-one-audiocontext"},{"title":"Polyphony budgets, voice priority and independent buses · Stems are not a monolithic mix","text":"Polyphony budgets, voice priority and independent buses Stems are not a monolithic mix","href":"./audio-buses.html#stems-are-not-a-monolithic-mix"},{"title":"Polyphony budgets, voice priority and independent buses · Optional stereo chorus and reverb","text":"Polyphony budgets, voice priority and independent buses Optional stereo chorus and reverb","href":"./audio-buses.html#optional-stereo-chorus-and-reverb"},{"title":"Polyphony budgets, voice priority and independent buses · Choosing","text":"Polyphony budgets, voice priority and independent buses Choosing","href":"./audio-buses.html#choosing"},{"title":"Compatibility policy","text":"Compatibility policy # Compatibility policy\n\nThis document states what OPM.js 1.x promises, what it does not, and how platform support is described. It is a maintenance policy, not a warranty. Evidence of what has actually been exercised lives in the CHANGELOG.\n\n## Versioning\n\nOPM.js follows semantic versioning for the surfaces below. Within 1.x:\n\n- Patch releases fix defects. They may change rendered PCM only to correct documented-incorrect behavior, and the CHANGELOG says so.\n- Minor releases add backward-compatible API, optional voice fields, new example pages and tooling. Default behavior is preserved: with default options, the same voice, notes, sample rate and quality profile render bit-identical PCM to the previous minor. Release 1.8.0 was checked against the 1.7.0 build over 279 renders (all bundled voices × three quality profiles × three registers × two sample rates, plus dense scores with all three stealing policies); see the CHANGELOG.\n- Major releases may remove deprecated API or change defaults. Deprecations are announced in the CHANGELOG at least one minor release before removal. No public API is currently deprecated.\n\n## Public surface\n\n| Surface | Promise |\n| --- | --- |\n| opm.js (browser API), opm.js/core (offline DSP), opm.js/midi-file (SMF adapter), opm.js/voices/schema.js, opm.js/voices/normalize.js, opm.js/voices/dx7.js, opm.js/voices/*.js (banks and metadata), opm.js/tools/assets.js | Documented exports, option names, event shapes and error classes are stable in 1.x. Added options are optional. Browser-only calls require their documented environment even when importing on Node is safe. |\n| opm-assets command | copy \u003cdestination> and check \u003cbase-url> keep their arguments and one-line JSON result; new subcommands may be added. |\n| Voice JSON | Canonical version 7. Versions 1–6 normalize to version 7 for all of 1.x with their original restrictions. See below. |\n| Score / Arrangement projects | Each project has its own explicit format version. Parsing is inert, bounded and detached; serialization saves musical definitions and synthesis settings, not DSP state, permissions, timers or external effects. Newer project formats may reject in older runtimes. |\n| dist/ file layout | Deploy the complete matching tree. Relative paths between modules, the AudioWorklet processor and the Worker are an implementation detail, but the tree is released as one unit. |\n| Generated declarations | Types describe the public API. Types marked @internal are stripped from declarations and may change at any time. |\n\nNot public: the AudioWorklet message protocol and Worker protocol (versioned internally, validated strictly, subject to change in any release), module-private helpers that are not exported from the entry points above, source-map contents, and the exact wording of error messages (match on error class/name, CommandRejectedError.event and event fields instead).\n\n## Voice format\n\n- Version 7 is canonical. Exported banks are always version 7.\n- Legacy versions 1–6 are accepted for the whole 1.x line with their original shapes: v1 excludes key scaling, v1/2 velocity sensitivity, v1–3 LFO waveform, v1–4 expressive fields, v1–5 per-operator LFO targets, and v1–6 operator waveform/noise rate.\n- A new voice field requires a new version. Validation rejects unknown fields, so a voice written for a newer minor version may fail to load in an older runtime. Forward compatibility is not promised; backward compatibility is.\n- Curated usage metadata (suggested register, velocity, polyphony, trim) is kept in src/voices/preset-metadata.ts (checkout-only), outside the voice schema, and is never part of the exported voice JSON.\n\n## Sound compatibility\n\nPCM is deterministic for identical inputs, sample rate and quality profile, independent of render chunking. It is not promised to be identical across minor versions when a new feature is used (for example voicePriority, or a 32-voice engine that no longer steals). Features that need new option values never change the output of calls that do not pass them.\n\nTwo facts matter when planning mixes:\n\n- The eco, standard (default) and high profiles intentionally sound slightly different. Switching profile is a sound change.\n- Every engine instance applies one output tanh stage. Splitting a mix across several OPM instances therefore does not reproduce a single-engine mix bit for bit. See audio buses.\n\n## Platform support\n\n| Platform | Statement |\n| --- | --- |\n| Node.js | 22 or newer for offline rendering, tests, tooling and the opm-assets command. |\n| Browsers | Need ES modules and AudioWorklet on a secure context (HTTPS or localhost). Module Workers are required only for Worker WAV export. Web MIDI is optional and feature-detected. |\n| Desktop CI | The repository's workflow runs real AudioWorklet smoke and stress against Chromium, Firefox and WebKit. A configured workflow is a capability; consult the release notes for runs that actually passed. |\n| iOS Safari, Android Chrome | Unverified on physical hardware. The mobile acceptance workflow and checkout-only npm run device-evidence exist so a maintainer can review real captures; none is supplied. |\n| Web MIDI | Availability depends on the browser and a user permission prompt. The package never requests access on import and never requests SysEx. |\n| Physical MIDI controllers | Unverified on physical hardware. Injected MIDI objects and desktop automation verify software paths, not a browser permission prompt, a real pedal or hot-unplug recovery. |\n| Preset listening quality | Unverified by human listeners. Numerical safety, RMS matching and automated playback do not establish musical preference, perceived loudness or a listening-approved trim. |\n\nSupport for a platform means \"no known defect and the documented checks were run\", never a certification for every device, OS version or audio route.\n\n## Resource bounds\n\nLimits (note and slot counts, bank size, tempo points, layer and section counts, held keys) are part of the validation contract. Raising a limit is a minor change; lowering one is a major change. CPU cost is not bounded by contract: maxVoices above 8, additional buses and the high profile all trade CPU for polyphony or fidelity. Measure on target devices.\n\nBenchmark capacity candidates describe only their recorded host, runtime, patch and workload. They are not portable device limits or an AudioWorklet underrun measurement. Quality and voice budgets never change automatically; choosing another quality is an explicit sound change. See measured capacity guidance and the cross-feature contracts.\n\n## Reporting breaks\n\nA change that makes a documented call stop working, or changes default PCM within a minor release, is a bug. Report it with the two versions, the call and a minimal score or voice. Security-relevant reports follow SECURITY.md.\n","href":"./compatibility.html"},{"title":"Compatibility policy · Compatibility policy","text":"Compatibility policy Compatibility policy","href":"./compatibility.html#compatibility-policy"},{"title":"Compatibility policy · Versioning","text":"Compatibility policy Versioning","href":"./compatibility.html#versioning"},{"title":"Compatibility policy · Public surface","text":"Compatibility policy Public surface","href":"./compatibility.html#public-surface"},{"title":"Compatibility policy · Voice format","text":"Compatibility policy Voice format","href":"./compatibility.html#voice-format"},{"title":"Compatibility policy · Sound compatibility","text":"Compatibility policy Sound compatibility","href":"./compatibility.html#sound-compatibility"},{"title":"Compatibility policy · Platform support","text":"Compatibility policy Platform support","href":"./compatibility.html#platform-support"},{"title":"Compatibility policy · Resource bounds","text":"Compatibility policy Resource bounds","href":"./compatibility.html#resource-bounds"},{"title":"Compatibility policy · Reporting breaks","text":"Compatibility policy Reporting breaks","href":"./compatibility.html#reporting-breaks"},{"title":"Voice v7 and expressive controls","text":"Voice v7 and expressive controls # Voice v7 and expressive controls\n\nVoice format version 7 is independent of the npm package version. normalizeVoice accepts an omitted version as v7; explicit v1–v6 retain their original field restrictions. Normalized, prepared, imported and bank voices emit v7. Canonical bank entries require version, name, algorithm, feedback, modIndex, lfo, and exactly four ops; the exported JSON schema describes that canonical shape, including numeric LFO target tuples.\n\nInputs must use plain objects and dense own-data operator/target arrays. Unknown fields, accessors, malformed enums, nonfinite numbers and unsupported versions are rejected without evaluating getters. normalizeVoice rejects out-of-range numbers; the bank-oriented validateVoice/parseVoiceBank clamp finite numeric values to the documented bounds. Integer selectors and MIDI breakpoints must still be integers. LFO target tuples are detached and frozen even in normalized voices. prepareVoice and bank validation return detached, deeply frozen snapshots, including the optional pitch envelope. A cloned prepared patch must be validated again; a copied shape does not confer trusted identity.\n\n## Patch fields\n\n| Field | Bounds / meaning |\n| --- | --- |\n| algorithm, feedback | Integers 0–7; algorithm selects the four-op graph. Feedback acts on operator zero. |\n| name | 1–64 ASCII letters, digits, _, -; optional for single-voice input. |\n| modIndex | 0–16 radians per unit modulator signal; single-input default 4. |\n| Operator ratio | 0.125–32; required even with fixed Hz. |\n| Operator level | 0–1 before envelope, key/velocity attenuation and live operator multiplier. |\n| Operator detune | −1200–1200 cents. |\n| Operator adsr | Required {a,d,s,r}: attack/decay/release 0–10 seconds, sustain gain 0–1. Ramps are linear in dB; the −96 dB floor becomes silence. |\n| Operator keyScale | Optional {breakpoint,leftDbPerOctave,rightDbPerOctave}: breakpoint integer MIDI 0–127, each attenuation 0–24 dB/octave. |\n| Operator velocitySensitivity | Optional 0–48 dB attenuation at velocity zero; default zero. Final note velocity gain remains independent. |\n| Operator frequency | Optional 1–20000 Hz, replacing tuned note frequency × ratio. Detune, live pitch/glide, pitch envelope and LFO pitch modulation still apply. Changing MIDI note or tuning does not transpose fixed Hz, but optional key/rate scaling still responds to the played note. |\n| Operator rateKeyScale | Optional 0–4; default zero. Each ADSR duration becomes min(10, seconds 2 (-rateKeyScale (note - 60) / 12)). Fractional notes interpolate continuously; zero durations stay zero. Durations are prepared at admission and do not follow live pitch bends or tuning changes. |\n| Operator waveform | Optional v7 sine (default), half, abs, quarter, alternating, camel, square, saw, noise. |\n| Operator noiseRate | Optional v7 20–20000 Hz, default 8000. Ignored unless waveform is noise. |\n\nExplicit v1 rejects key scaling and velocity sensitivity; v2 adds key scaling; v3 adds velocity sensitivity; v4 adds LFO waveform. All explicit v1–v4 reject frequency, rateKeyScale, pitchEnvelope, and LFO delay/sync/phase, rather than silently dropping them. V5 adds those expressive fields, represented by LegacyVoiceV5 and LegacyLFOV5. All explicit v1–v5 reject LFO amTargets/pmTargets. V6 adds those per-operator targets; leaving them absent preserves the legacy all-operator modulation behavior without adding default fields to snapshots.\n\n### Oscillator shapes and noise\n\nFor θ = phase + modulation and fractional turns t = frac(θ / (2π)), the periodic shapes are:\n\n| Shape | Formula |\n| --- | --- |\n| sine | sin(θ); the unchanged legacy fast path |\n| half | max(0, sin(θ)) |\n| abs | abs(sin(θ)) |\n| quarter | abs(sin(θ)) when frac(θ/π) \u003c 0.5, otherwise zero |\n| alternating | sin(2θ) when t \u003c 0.5, otherwise zero |\n| camel | abs(sin(2θ)) when t \u003c 0.5, otherwise zero |\n| square | −1 for negative sine, +1 otherwise (including zero); polyBLEP-corrected in Synth |\n| saw | 2 * frac(t + 0.5) - 1; rising, zero at θ = 0; polyBLEP-corrected in Synth |\n\nPhase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Non-sine shapes evaluate the same deterministic polynomial sine as sine operators. Synth smooths the square and saw jumps with two-sample polyBLEP residuals and the two half-wave corners with polyBLAMP residuals, sized from each operator's unmodulated phase step at the oversampled rate. This lowers folded aliases of unmodulated carriers (about 8–17 dB at the internal rate in the unit tests, before decimation); under phase modulation the nominal step is only an approximation. Quarter, alternating and camel remain naive, and held noise is never band-limited. None of these oscillators is alias-free, even with oversampling and the post-mix decimator.\n\nNoise is an original deterministic 17-bit maximal-length LFSR (x^17+x^3+1), seeded with 0x1ffff at every note admission, including pooled slot reuse. The low bit produces bipolar ±1 and advances on the noiseRate time-domain hold clock, independent of output sample rate. Identical admissions reproduce the sequence. Ratio, fixed frequency, detune, tuning, pitch envelopes, live pitch and modulator PM do not pitch noise; existing operatorRatios/operatorFrequencies controls remain valid but have no audible pitch effect on it. Envelope, key/rate scaling, velocity, level, AM and graph routing still apply; noise can be either a modulator or carrier.\n\nThis OPM-NE-inspired musical extension allows noise on any operator of any voice, unlike hardware restricted to the last operator of channel 8. It is not a Yamaha/YM2151 fidelity claim. Explicit v1–v6 reject operator waveform or noiseRate with a version-7 requirement; LegacyVoiceV6 preserves the prior shape. Prepared snapshots, bank JSON, content-keyed registration and worklet validation retain both new fields.\n\n\n### Pitch envelope\n\nOptional pitchEnvelope requires all seven own-data fields:\n\nts\npitchEnvelope: {\n a: 0.02, d: 0.15, r: 0.1,\n initial: -300, peak: 100, sustain: 0, final: -100\n}\n\n\na, d, r are 0–10 seconds. initial, peak, sustain, final are −4800–4800 cents. Held notes move linearly in cents initial → peak → sustain. A zero attack skips directly to peak; a zero decay skips directly to sustain. Release moves from the current cents value to final, even during attack/decay; zero release immediately selects final. Final pitch persists while operator releases remain audible. The pitch envelope does not prolong otherwise silent operators or filter tails.\n\nPitch cents combine multiplicatively with original operator Hz, detune, live semitone pitch/glide and LFO PM. Pitch-envelope and glide evaluation use deterministic synthesis substeps for the selected quality profile: standard 4× (default), eco 2×, high 8×. Splitting render into different buffer sizes does not change the timeline. Operator phase increments are limited to 45% of the internal sample rate before/after LFO PM, preserving the engine's existing high-frequency safety cap.\n\n### LFO\n\nlfo requires rate (0–20 Hz), amDepth (0–1) and pmDepth (0–1200 cents); single-input omission disables modulation. waveform defaults to sine, with triangle, rising saw, and square also supported.\n\n- delay: optional 0–10 seconds, default zero. Gates both AM and PM depth until that note's elapsed time reaches delay; the phase clock continues during delay. This is a depth gate, not a gradual fade-in.\n- sync: note (default) resets the clock at note admission. global uses Synth.currentFrame, including rendered silence, so separately admitted notes share a deterministic frame clock. Neither uses wall time. Panic does not rewind the global clock.\n- phase: optional 0–1 turns, default zero; 1 wraps to 0. At rate zero this can produce static AM/PM offset.\n- amTargets, pmTargets: optional readonly four-element tuples in operator order, each entry a finite 0–1 depth multiplier or a boolean (false → 0, true → 1). Normalization stores immutable numeric LFOTargets; LFOInput accepts LFOTargetsInput. Each omitted tuple acts as [1, 1, 1, 1], preserving the old sound. Zero disables that modulation for the selected operator; fractional weights reduce it independently of operator level. Dense own-data elements are required; getters, holes, extra fields and malformed values reject.\n\nLFO values are evaluated once per output frame. Sine and triangle start at zero rising, saw at −1, square at +1. For operator i, AM multiplies gain by 1 - amDepth amTargets[i] (0.5 + 0.5 waveformValue); PM multiplies pitch by 2 (pmDepth pmTargets[i] * waveformValue / 1200). Missing targets use 1 in these formulas. Delay, sync and phase remain shared by all four operators.\n\n## Live operator levels\n\nSynth.updateNote, host updateNote, scheduled controls, and sequence automation accept:\n\nts\n{ operatorLevels: [1, 0.4, 1.5, 0], ramp: 0.1 }\n\n\nThe readonly tuple must contain exactly four finite own-data numbers in 0–2. Each number multiplies its patch operator level after key/velocity scaling and before FM routing/feedback. All default to 1. A multiplier cannot revive a patch operator whose level is zero. Modulator levels change timbre; carrier levels change that carrier's contribution. Final expression/velocity and equal-power pan remain separate controls.\n\nramp is 0–10 seconds, default zero. The four multipliers ramp linearly and independently from their current values using the same supplied duration. An interrupted ramp starts from its current value, not the old target. Unrelated expression, pan, modulation or pitch updates do not cancel operator ramps. Validation detaches and freezes the tuple and its outer control record before scheduling. Recycled/stealing voice slots preserve current tails and fully reset controls for new admissions; render processing allocates no arrays or objects.\n\n## Live timbre and envelope controls\n\nThe same note-control APIs accept these optional fields independently:\n\n| Control | Bounds / meaning |\n| --- | --- |\n| feedback | Finite 0–7 continuous feedback selector. Between 0 and 1, gain interpolates from zero to the classic selector-1 gain; 1–7 follows the original exponential gain mapping. Patch feedback remains an integer. |\n| lfoRate, amDepth, pmDepth | 0–20 Hz, 0–1, 0–1200 cents respectively; update the admitted note's LFO independently. |\n| operatorRatios | Readonly four-element tuple of finite 0.125–32 ratios. Fixed-Hz operators retain these ratios but ignore them until restored to ratio mode. |\n| operatorFrequencies | Readonly four-element tuple, each entry finite 1–20000 Hz or null to restore tuned note × ratio. Fixed Hz ignores MIDI/tuning transposition; detune, live pitch, pitch envelope and LFO PM still apply. |\n| operatorADSR | Readonly four-element tuple of complete {a,d,s,r} objects using the patch ADSR bounds. Parameters are replaced, not interpolated by ramp. |\n\nramp gives feedback, LFO and frequency/ratio updates independent linear timelines. It must accompany a rampable scalar or tuple control; operatorADSR alone with ramp rejects. Interrupted updates start at the current value; omitted controls keep their existing timelines. Live lfoRate integrates phase without resetting the current phase, including interrupted rate ramps. Tuples and nested ADSRs are strict own-data snapshots detached and frozen before scheduling.\n\nUpdating a held operator's ADSR restarts attack from its current dB level, then decays to the new sustain. With both attack and decay zero, a one-frame bridge preserves continuity. An update during release starts from the current dB level and completes the new key-scaled release, bounded to ten seconds; zero release silences immediately. Envelope durations provide continuity rather than interpolating ADSR parameters with the control ramp.\n\n## DX7 import: retained data versus approximations\n\nimportDX7 remains a lossy six-to-four-operator conversion, not hardware emulation. Source format and behavior were checked against the Yamaha DX7 manual, printed pp. 13–17 and 30–31 and the DX7II packed VMEM layout, Add-11.\n\n- Fixed frequency is retained in Hz as 10 ** ((coarse & 3) + fine / 100), rather than a MIDI-60 ratio. Detune remains approximate.\n- Keyboard rate scaling maps source 0–7 to OPM 0–4. This is a continuous musical approximation, not Yamaha's grouped-key/rate transfer function.\n- Pitch envelope retains L4 → L1 → L3 → L4. L2 is omitted; R2/R3 durations are merged and capped at ten seconds. Levels interpolate piecewise linearly through source 0/50/99 = −4800/0/+4800 cents. Rates use the existing min(10, 10 2 * (-rate / 10)) seconds heuristic, not measured hardware timing. A fully neutral pitch envelope is omitted.\n- LFO delay maps source 0–99 to 0–10 seconds; sync on maps to note and sync off to deterministic global phase. Delay shaping and free-running/random hardware phase are not reproduced.\n- Triangle, rising saw, square and sine retain their waveform family. Descending saw becomes rising saw; sample-and-hold becomes sine. Both substitutions are explicitly reported by describeDX7.\n\nOperator selection, envelope/output response, topology, feedback placement, velocity and level scaling remain approximations. Oscillator phase carry, source transpose, positive keyboard scaling, per-operator AM and nonzero final amplitude-envelope levels are not preserved. describeDX7 reports these losses separately from the strict usable voice shape.\n\n## OPM text import\n\nimportOPM(source: string | Uint8Array): Voice[] and describeOPM(source: string | Uint8Array): OPMImportDescription[] live in opm.js/voices/opm.js. They convert decimal VOPM/MXDRV-family text patches into complete v7 voices through normal normalization. This is approximate conversion, not register-level YM2151 emulation, with no hardware-fidelity claim.\n\nFormat references: the published MiOPM text-layout header, VOPM unofficial manual and OPM application manual, especially figures 2.5–2.16. Only format/parameter documentation was used; no emulator or converter implementation was copied. The VOPM GUI multiplier description is doubled relative to hardware MUL: this importer conservatively uses raw hardware values. The published text example contains an out-of-range KS value; hardware KS 0–3 is enforced rather than guessing its meaning. No physical or listening comparison has been performed.\n\nEach patch starts with @:\u003cprogram 0..127> \u003cname> and must contain exactly one each of LFO:, CH:, M1:, C1:, M2:, C2:. Blank lines and lines starting with // after whitespace are ignored; CRLF is accepted. Section order is flexible; fields are decimal integers only:\n\ntext\nLFO: LFRQ AMD PMD WF NFRQ\nCH: PAN FL CON AMS PMS SLOT NE\nM1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN\nC1: (same eleven fields)\nM2: (same eleven fields)\nC2: (same eleven fields)\n\n\nRanges: LFRQ 0–255; AMD/PMD 0–127; WF 0–3; NFRQ 0–31; PAN byte 0–255 (ignored); FL/CON/PMS 0–7; AMS 0–3; NE/AMS-EN 0–1. SLOT is the raw key-on mask: only bits 3/4/5/6 (8/16/32/64) enable M1/C1/M2/C2, so all enabled is 120, not 15. Disabled operators get zero level with warnings. AR/D1R/D2R 0–31; RR/D1L/MUL 0–15; TL 0–127; KS/DT2 0–3; DT1 0–7. Duplicate/missing/unknown sections, malformed/out-of-range fields and NUL reject with RangeError and a line number. Empty files reject. Limits are 262144 input bytes, 128 patches and 512 bytes per line. Byte input is decoded explicitly as Latin-1, not UTF-8 or Windows-1252; strings use UTF-8 byte counts for bounds. Names become [a-zA-Z0-9-]{1,64}, falling back to OPM, with deterministic 2, _3, … suffixes and truncation.\n\n### Conversion formulas\n\n- Routing: CON 0–7 corresponds directly to OPM.js algorithms 0–7. File order M1/C1/M2/C2 is signal order, not register-slot order M1/M2/C1/C2. The manual's diagrams give: 0 M1→C1→M2→C2; 1 (M1+C1)→M2→C2; 2 C1→M2 then (M1+M2)→C2; 3 M1→C1 then (C1+M2)→C2; 4 two chains M1→C1 and M2→C2; 5 M1 modulates the other three; 6 M1→C1 plus independent M2/C2; 7 all carriers. Feedback stays on M1, with FL retained but the engine's musical strength curve.\n- Envelope: the original bounded heuristic T(rate) = min(10, 10 2 (-rate / 3)) seconds gives a=T(AR), d=T(D1R), r=T(2RR+1). Zero AR/D1R represents infinity and is clamped to ten seconds with a warning. RR's doubled-plus-one input-rate convention follows the manual; the times are not chip timings. Sustain is linear gain 10 * (-3D1L/20); D1L=15 uses −93 dB instead of −45 dB. The engine interpolates envelope levels in dB. D2R cannot be represented by ADSR: held-note second decay is omitted, D1L holds until release, and every nonzero D2R gets an operator-specific warning.\n- Level: level = 10 * (-0.75TL/20) for both carriers and modulators. Operator output amplitude also drives phase modulation, so this preserves TL attenuation without carrier-only renormalization. modIndex=4 retains the default four-radian modulation scale, not a hardware transfer curve.\n- Pitch/scaling: MUL 0→ratio 0.5; 1–15→their integer ratios. DT1 becomes fixed cents [0,1,2,3,0,-1,-2,-3], a small symmetric approximation to the chip's key-dependent table. DT2 adds [0,600,781,950] cents (manual figure 2.7). Their sum stays within ±1200. KS maps linearly to rateKeyScale=KS*4/3: octave-based duration scaling around MIDI 60, not hardware grouped-key rates.\n- LFO: fixed reference clock f=3579545 Hz; rate = f/232 * 2(LFRQ>>4) (1+(LFRQ&15)/16), fitting manual figure 2.16, capped at 20 Hz with a warning. PM sensitivity maxima are [0,5,10,20,50,100,400,700] cents; pmDepth=PMSmaxPMD/128. AM sensitivity maxima are [0,23.90625,47.8125,95.625] dB; amDepth=1-10*(-AMSmaxAMD/128/20). These endpoint-depth mappings approximate the engine's linear-gain/cents LFO, not the chip's attenuation/frequency transfer function. Per-operator AMS-EN becomes amTargets. WF 0/1/2 becomes saw/square/triangle; 3 (noise) substitutes deterministic sine with a warning. AM/PM waveform phase relationships and global phase differ from hardware; clock selection and free-running chip state are not retained.\n- Noise: NE replaces C2 (op4) with waveform:'noise'; its approximate rate is f/(32*(32-NFRQ)), capped to 20–20000 Hz with a clamp warning. The manual prints a noise-divider equation but does not spell out raw zero encoding: this conversion conservatively assumes an inverted 32-step countdown and always warns about that approximation when NE is on. It does not reproduce a chip noise spectrum. OPM.js noise on any channel is an extension: the hardware channel-8 restriction, LFSR and special noise envelope are not reproduced.\n\ndescribeOPM returns {name,program,algorithm,operators,warnings}. operators lists source labels in destination order. Warnings are deterministic, de-duplicated and bounded by the fixed four-operator conversion, including every approximation/substitution/clamp above. Program numbers are metadata only; wire imported names into a MIDI program map explicitly. Patches, not file programs, are de-duplicated by name.\n","href":"./expressive-voices.html"},{"title":"Voice v7 and expressive controls · Voice v7 and expressive controls","text":"Voice v7 and expressive controls Voice v7 and expressive controls","href":"./expressive-voices.html#voice-v7-and-expressive-controls"},{"title":"Voice v7 and expressive controls · Patch fields","text":"Voice v7 and expressive controls Patch fields","href":"./expressive-voices.html#patch-fields"},{"title":"Voice v7 and expressive controls · Oscillator shapes and noise","text":"Voice v7 and expressive controls Oscillator shapes and noise","href":"./expressive-voices.html#oscillator-shapes-and-noise"},{"title":"Voice v7 and expressive controls · Pitch envelope","text":"Voice v7 and expressive controls Pitch envelope","href":"./expressive-voices.html#pitch-envelope"},{"title":"Voice v7 and expressive controls · LFO","text":"Voice v7 and expressive controls LFO","href":"./expressive-voices.html#lfo"},{"title":"Voice v7 and expressive controls · Live operator levels","text":"Voice v7 and expressive controls Live operator levels","href":"./expressive-voices.html#live-operator-levels"},{"title":"Voice v7 and expressive controls · Live timbre and envelope controls","text":"Voice v7 and expressive controls Live timbre and envelope controls","href":"./expressive-voices.html#live-timbre-and-envelope-controls"},{"title":"Voice v7 and expressive controls · DX7 import: retained data versus approximations","text":"Voice v7 and expressive controls DX7 import: retained data versus approximations","href":"./expressive-voices.html#dx7-import-retained-data-versus-approximations"},{"title":"Voice v7 and expressive controls · OPM text import","text":"Voice v7 and expressive controls OPM text import","href":"./expressive-voices.html#opm-text-import"},{"title":"Voice v7 and expressive controls · Conversion formulas","text":"Voice v7 and expressive controls Conversion formulas","href":"./expressive-voices.html#conversion-formulas"},{"title":"Host integration","text":"Host integration # Host integration\n\n## Worklet deployment\n\nnew OPM({ workletUrl?: string | URL }) accepts an optional module path. Relative paths resolve against the page URL; a URL is copied when constructed. The default remains new URL('../worklet/processor.js', import.meta.url). A custom module must be same-origin HTTP(S), with no credentials or fragment. HTTPS and secure loopback HTTP are supported; insecure contexts are rejected before audio resources are allocated. start() rejects and emits an error event when initialization fails, including normal CSP, network and MIME failures. Validation does not bypass browser policy.\n\nDeploy the processor and its relative imports, not only one JavaScript file. Keep their installed layout; serve JavaScript as JavaScript and asset misses as 404, not SPA HTML. See the installed-package Vite example (checkout-only) for a non-root deployment with script-src 'self', worker-src 'self' and no blob/eval allowances.\n\nts\nimport { OPM } from 'opm.js';\n\nconst engine = new OPM({ workletUrl: '/assets/opm/worklet/processor.js' });\n// Invoke initialization/resume from a trusted user gesture.\nawait engine.start();\n\n\n## Worker WAV output and bounded sinks\n\nrenderSequenceInWorker(events, { sink, format?, onProgress?, signal?, workerUrl?, ...chunkOptions })\nrenders long scores in a static module Worker. Chunk options are the core\nrenderSequenceChunks options, including sampleRate, chunkFrames (1–65536,\ndefault4096), maxFrames, voice registry, mix/tuning/stealing and quality.\nformat is pcm16 (default), pcm24, or IEEE float32. Output is stereo RIFF32\nWAV; oversized RIFF files reject before Worker allocation. Score, options and\npatches are validated and detached first; there is no audio device/context.\n\nThe default module is new URL('../worker/render.js', import.meta.url).\nDeploy dist/worker/render.js and all its relative imports, maps and\ndeclarations from the matching distribution. An explicit workerUrl resolves\nagainst the page and must be same-origin HTTPS or secure loopback HTTP, with\nno credentials/fragment. The default must also remain same-origin.\nUse worker-src 'self'; no blob module, eval, dynamic executable source or\ncross-origin loading is used. CSP, MIME and network failures reject normally.\nImporting the browser API on Node/SSR is safe, but calling this helper requires\na browser with module Workers.\n\nA sink is an own-data object with write(Uint8Array): void | Promise\u003cvoid>,\noptional close() and optional abort(reason). The Worker transfers one byte\nchunk and waits until the sink write fulfills before advancing DSP. The sink owns\nthe byte buffer and may transfer/detach it (for example, to a storage Worker);\nbyte accounting captures its native size before invoking write. Neither thread\naccumulates a complete PCM buffer or WAV. The sink must actually drain to bounded\nstorage: retaining every chunk defeats the memory bound.\nonProgress({ frames, totalFrames, bytesWritten, errors }) runs after each\nsuccessful write, including the initial header (frames:0); frame counts are\ncumulative. The result contains capacity, format, bytesWritten and\ndiagnostics: { errors, processedEvents, renderedFrames }. Success means\nall bytes were written and close() fulfilled, not that a host file is audible.\n\nAbort, sink/progress exceptions and Worker failures terminate the Worker,\nremove listeners and reject. abort(reason) is invoked once, but its promise\nand an outstanding write/close are not awaited: a hung sink cannot hold\ncancellation hostage. Their later rejections are consumed. A sink must\ninvalidate in-flight writes and discard/roll back incomplete output; cancellation\ncannot retract bytes already persisted. abort must not finalize an incomplete\nfile. Use host-side cleanup when abort itself fails.\n\nFor an actual file export, request File System Access from a trusted gesture\nwhere it is supported. Feature-detect it; if unavailable, report that long-file\nexport is unsupported rather than aggregating a long WAV into a Blob:\n\njs\nimport { renderSequenceInWorker } from 'opm.js';\n\n// Run this handler directly from a user gesture.\nasync function exportScore(events, signal) {\n signal?.throwIfAborted();\n if (typeof window.showSaveFilePicker !== 'function') {\n throw new Error('Streaming file export requires File System Access');\n }\n let file, settled = false;\n const rollback = async reason => {\n if (!file || settled) return;\n settled = true;\n await file.abort(reason);\n };\n try {\n const handle = await window.showSaveFilePicker({\n suggestedName: 'sequence.wav',\n types: [{ description: 'WAV audio', accept: { 'audio/wav': ['.wav'] } }],\n });\n signal?.throwIfAborted();\n file = await handle.createWritable();\n signal?.throwIfAborted();\n return await renderSequenceInWorker(events, {\n sampleRate:48000, chunkFrames:4096, format:'pcm24', signal,\n workerUrl:'/audio/opm/worker/render.js',\n sink: {\n write(bytes) { return file.write(bytes); },\n async close() { await file.close(); settled = true; },\n abort: rollback,\n },\n });\n } catch (error) {\n await rollback(error).catch(cleanup => console.error('File rollback failed', cleanup));\n throw error;\n }\n}\n\n\nThe checkout demo's memory-only download is intentionally a small export:\npreflight a hard frame/byte budget, retain chunks only within that budget, and\nrevoke its old Blob URL on replacement/disposal. It is not a fallback for a long\nfile sink. Hosts also own cumulative work, concurrency and export-frequency limits.\n\nNode and browser hosts may use createWavEncoder({ sampleRate, channels:1|2,\nformat?, totalFrames }) from opm.js/core directly. Write header() exactly\nonce, then each encode({ left, right? }) result to a backpressured sink before\nconsuming the next borrowed PCM chunk; finally write finalize() (a RIFF\nalignment byte or empty array). Each call accepts1–65536 frames. The declared\nframe count is exact: overrun/underrun reject, and invalid chunks never advance\nframesEncoded. No audio/file bytes are retained by the encoder. Its getters\nare totalFrames, framesEncoded, byteLength and finished. Zero-frame WAVs\nare supported by this streaming API. The full-buffer encodeWav({ left,\nright?, sampleRate, format? }) keeps its original1–4,000,000-frame budget.\nAll channels must be native Float32Arrays of finite samples in−1–1; encoding\nrejects rather than clipping. PCM24 uses signed little-endian endpoints; float\nWAV uses format3 with a fact sample-count chunk.\n\n## Events, acknowledgements and ownership\n\nengine.subscribe(listener) returns an idempotent unsubscribe function. Each subscription is independent, even for the same listener; it coexists with onEvent and scheduler subscriptions. Exceptions and attempts to mutate frozen event records cannot disrupt other listeners or cleanup. Removing a listener during dispatch skips its remaining delivery; newly added listeners begin with the next event. Ordinary close() preserves subscriptions for a later start().\n\nCommands still synchronously return numeric IDs. waitForCommand(commandId, { timeout?, signal? }) waits only for the correlated admission acknowledgement, including a reply received before the method is called. The timeout is milliseconds: default 5000, integer range 1..60000. At most 64 pending waits and 128 command receipts are retained. Receipts without live waiters are evicted oldest-first; an unknown/expired ID rejects rather than waiting forever. Multiple waits for one ID are independent. Abort/timeout removes only that wait, not the admitted command or another waiter. A late acknowledgement can still be retrieved while its receipt is retained. Signals must be genuine AbortSignals: intrinsic state/reason and listener operations bypass shadow accessors and reject duck-typed objects.\n\nA rejected acknowledgement throws CommandRejectedError with its frozen event (reason, note ID and command ID). Pending waits reject on reset, close, interruption/suspension or processor failure. A command-triggered reset carries its initiating commandId; an ordered panic reset rejects earlier outstanding commands, but preserves that panic's own acknowledgement and commands posted after it, even after receipt eviction. Uncommanded interruption resets invalidate every outstanding receipt. A settled acknowledgement is historical admission evidence and is not revoked by a later reset. Accepted does not prove scheduled execution, gate release, release-tail completion or audible output. Use note lifecycle events and diagnostics for those distinct observations.\n\nts\nconst id = engine.playNote({ note: 60 });\nconst stop = engine.stop(id, { at: engine.context!.currentTime + 0.1 });\nconst acknowledgement = await engine.waitForCommand(stop, { timeout: 2000 });\n// acknowledgement.state === 'accepted': future stop was admitted, not completed.\n\n\nclose() disconnects OPM's node, closes its port and removes its context listener; it is restartable. dispose() is terminal and idempotent: it also removes subscriptions/onEvent, rejects outstanding waits and prevents restart/new subscriptions. Both close an owned AudioContext, but never close or suspend a borrowed context. The host owns its additional routing nodes and any context it supplied.\n\n## Cross-feature contracts\n\n| Identity / observation | Meaning | Not equivalent to |\n| --- | --- | --- |\n| Score note ID | Local musical identity used by note, control and stop events | A live OPM note ID; score IDs are remapped on admission |\n| OPM note ID | One live note's owned lifecycle | A physical key ID or a command ID |\n| Performance key ID | One physical key press, distinct even at equal pitch | Its current sounding gate; mono selection may reuse or replace a gate |\n| Command ID / accepted | Correlated command admission | Scheduled execution, audible onset or completed release |\n| Note started / released / ended | Audio-frame lifecycle transitions | Device-output or microphone-observed timing; released may retain a tail |\n| Arrangement committed beat | The scheduled musical change boundary | Immediate application; already admitted notes retain their times |\n| getDiagnostics() | Voice/queue counts, errors and rejected notes | CPU utilization, GC pauses, underrun counters or audible continuity |\n\nPersist authored music, not an audio session. Score projects store finite beat events; Arrangement projects store looping layers and named sections. Both retain validated named patches, tempo/meter and synthesis settings. They do not retain current DSP phases/envelopes, live key/pedal ownership, pending section commands, host effects/routing or MIDI permissions/controller mappings. Hosts save those additional application choices separately and explicitly.\n\nParsing or loading a file never grants permission or starts audio. Load the validated named voices, construct the intended engine with its immutable quality/voice budget, connect the host graph and start from a trusted gesture. Dispose the old helper and its owned engine when replacing a session; never close a context borrowed from another component. Treat new project definitions as musical starts, not seamless continuation.\n\nLive MIDI and MIDI files are different contracts. The live adapter applies channel/key policies to owned gates; an SMF conversion must make its expressive-controller policy explicit and expose every reported omission or approximation. A valid MIDI or project round trip does not prove hardware fidelity or lossless conversion of unsupported musical semantics. See MIDI files.\n\nFor reproducible sound retain the package version, normalized patch identity, sample rate, quality, voice budget, tuning, mix gain and stealing policy. Retain host/output gains and routing separately: downstream attenuation cannot undo the engine's saturation. Numerical output and measured host capacity remain separate from physical recovery and human listening acceptance.\n\n\n## Audio/output clock mapping and component cleanup\n\ngetOutputTimestamp() pairs an AudioContext contextTime (seconds at device output) with the corresponding performanceTime (milliseconds on the performance clock). It is not a pair of independent current-time samples. Map an audio event at audioTime to an estimated output time with:\n\nts\nconst stamp = context.getOutputTimestamp();\nconst estimatedOutputMs = stamp.performanceTime + (audioTime - stamp.contextTime) * 1000;\n\n\nDo not add baseLatency or outputLatency again: the timestamp already describes output. Timestamp support/precision varies, the initial pair may be zero, and the result is an estimate—not microphone-observed acoustic onset. Re-sample while running after resume/device changes. If unavailable, display audio-clock timing only rather than inventing output-clock precision.\n\nThe following host can be mounted beside a button/status element. unmount() removes DOM/OPM listeners, aborts outstanding waits, disposes OPM, disconnects host routing and closes the host-owned context. The asynchronous gesture handler checks teardown after every asynchronous startup boundary.\n\nts\nimport { OPM } from 'opm.js';\n\nconst button = document.querySelector\u003cHTMLButtonElement>('#play')!;\nconst status = document.querySelector\u003cHTMLElement>('#status')!;\nconst context = new AudioContext();\nconst output = context.createGain();\noutput.gain.value = 0.08;\noutput.connect(context.destination);\nconst engine = new OPM({ context, destination: output,\n workletUrl: '/assets/opm/worklet/processor.js' });\nconst lifetime = new AbortController();\nlet mounted = true;\nlet note: number | undefined;\nconst unsubscribe = engine.subscribe(event => {\n if (event.type === 'error') status.textContent = event.error.message;\n if (event.type === 'reset') note = undefined;\n if (event.type === 'note' && event.id === note) {\n status.textContent = event.state;\n if (['ended', 'stolen', 'cancelled', 'rejected'].includes(event.state)) note = undefined;\n if (event.state === 'started' && typeof context.getOutputTimestamp === 'function') {\n const stamp = context.getOutputTimestamp();\n if (stamp.performanceTime > 0 && Number.isFinite(stamp.contextTime) && Number.isFinite(stamp.performanceTime)) {\n const outputMs = stamp.performanceTime + (event.time - stamp.contextTime) * 1000;\n status.title = Estimated output onset: ${outputMs.toFixed(1)} ms;\n }\n }\n }\n});\nlet busy = false;\nconst onClick = async () => {\n if (!mounted || busy) return;\n busy = true;\n try {\n await context.resume(); // Called synchronously in the trusted gesture.\n if (!mounted) return;\n await engine.start();\n if (!mounted) return;\n if (note !== undefined) {\n await engine.waitForCommand(engine.stop(note), { signal: lifetime.signal });\n if (!mounted) return;\n }\n // A desired performance-clock target converted back to audio seconds.\n const desiredOutputMs = performance.now() + 150;\n let at = context.currentTime + 0.05;\n if (typeof context.getOutputTimestamp === 'function') {\n const stamp = context.getOutputTimestamp();\n if (stamp.performanceTime > 0 && Number.isFinite(stamp.contextTime) && Number.isFinite(stamp.performanceTime)) {\n at = Math.max(at, stamp.contextTime + (desiredOutputMs - stamp.performanceTime) / 1000);\n }\n }\n note = engine.playNote({ note: 60, at, duration: 1 });\n } catch (error) {\n if (mounted) status.textContent = error instanceof Error ? error.message : String(error);\n } finally { busy = false; }\n};\nbutton.addEventListener('click', onClick);\n\nlet teardown: Promise\u003cvoid> | undefined;\nfunction unmount(): Promise\u003cvoid> {\n if (teardown) return teardown;\n mounted = false;\n button.removeEventListener('click', onClick);\n lifetime.abort();\n unsubscribe();\n teardown = (async () => {\n try { await engine.dispose(); }\n finally {\n output.disconnect();\n if (context.state !== 'closed') await context.close();\n }\n })();\n return teardown;\n}\n\n\nFor a shared context owned elsewhere, omit its final context.close(); disconnect only the routing nodes this component owns. streamSequence and createLookaheadScheduler have their own dispose() methods: dispose each scheduler before its OPM instance so admission timers and live-note ownership are released.\n\n## Other bundlers and SSR hosts\n\nWebpack, Rollup, Parcel and static hosts use the same explicit static-asset contract without relying on unverified worklet-plugin APIs: deploy the installed package's entire dist/ tree plus the package-root LICENSE at a same-origin URL, preserving relative paths. This is a packaging recipe, not a claim that those other bundlers were exercised. Only the existing Vite production smoke provides bundler-specific runtime verification.\n\nThe package ships a dependency-free helper that does this safely: Run it from a project with OPM.js installed; --no-install prevents an accidental registry fetch.\n\nsh\nnpx --no-install opm-assets copy public/audio/opm-1.10.0 # atomic copy into a NEW directory\nnpx --no-install opm-assets check https://example.test/audio/opm-1.10.0/ # byte-for-byte check of what is served\n\n\ncopy (also import { copyAssets } from 'opm.js/tools/assets.js', Node only) inspects the installed release first: only .js, .js.map and .d.ts files under dist/, no symlinks, every .js with its map and declaration, the entry modules api/index.js, core/index.js, worklet/processor.js and worker/render.js, and a bounded size (4,096 files, 16 MiB). It stages a temporary directory beside the destination, copies and re-hashes every file, writes opm-assets.json (SHA-256 and size per file, plus LICENSE) and renames the staging directory into place. It never overwrites: a destination that already holds exactly this release is reported as reused: true; anything else is an error and left untouched. Use a fresh release-specific directory (or an atomic deployment switch) so removed old assets cannot mix with a new tree.\n\ncheck (or checkDeployment(url)) downloads each listed file from an HTTPS or loopback-HTTP base URL and compares status, a JavaScript MIME type, X-Content-Type-Options: nosniff (disable with requireNosniff: false) and the SHA-256 against the installed release. Redirects are errors, so an SPA fallback that returns HTML with status 200 fails on MIME or bytes. It cannot see your page's Content-Security-Policy and does not start an AudioWorklet or Worker: run the browser smoke or the Vite example (checkout-only) for that.\n\nConfigure new OPM({ workletUrl: '/audio/opm-1.10.0/worklet/processor.js' }) and, for Worker export, workerUrl: '/audio/opm-1.10.0/worker/render.js'. The worklet's own imports must remain deployed beside it and use valid JavaScript MIME types; no blob URL or relaxed CSP is needed. The host can bundle the browser-facing API normally or import /audio/opm-1.10.0/api/index.js externally.\n\nAn SSR module may import types safely, but instantiate/resume OPM only on the client, from a trusted user gesture. Keep module initialization free of window, document and AudioContext access on the server; dynamically import the runtime inside client-only code when required by the host. Dispose it during component teardown using the ownership recipe above.\n\n## Part-scoped performance\n\ncreatePerformance() supplies playing policies, not a MIDI driver. It requires\na started OPM and never owns its AudioContext. Keep returned physical key IDs\nuntil their matching key-up; repeated equal-pitch keys have separate identities.\n\njs\nimport { createPerformance } from 'opm.js';\n\n// opm is already started from the host's trusted input gesture.\nconst performance = createPerformance(opm, {\n parts: 16, maxKeys: 128, maxKeysPerPart: 16,\n onError: error => console.error(error),\n});\nperformance.configurePart(0, {\n voice: 'brass', mode: 'mono', legato: true, priority: 'high', glide: 0.02,\n pan: 0, expression: 0.8,\n});\nconst key = performance.noteOn(0, 60, { velocity: 0.7 }); // host key-down\nperformance.sustain(0, true); // pedal-down\nperformance.noteOff(0, key); // physical key-up\n// Later, in the pedal-up input handler:\n// performance.sustain(0, false);\n// performance.updatePart(0, { pan: -0.5, expression: 0.6, glide: 0.03 });\n// performance.allNotesOff(0); // only this part; no global panic\n// performance.dispose(); // release all helper-owned gates on unmount\n\n\nParts are 0-based, with 1–16 configured parts (default 16), at most 128 keys and\n128 tracked gates system-wide, including pending releases. Per-part limits share\nthat system budget; overflow rejects without evicting unrelated parts. A part\nmay use mode: 'poly' or 'mono', legato, and priority: 'last' | 'high' | 'low'.\nPhysically held keys take precedence over pedal-only keys; equal pitches choose\nthe newest identity. Mono legato preserves the original onset's envelope,\nvelocity and key/rate scaling. Its pitch offset is anchored to that onset:\nwithin ±48 semitones it reuses the gate, farther moves retrigger at the actual\npitch instead of clamping. Non-legato switching retriggers the patch.\n\ngetPart(part) returns a detached frozen snapshot. Stealing prunes the affected\ngate without later automatic readmission; reset clears state. Interruption\nreleases helper-owned notes even when direct OPM notes use preserve policy.\nDispose performance/Transport helpers before disposing their shared engine.\nTheir cleanup does not release unrelated host or Transport notes.\n\nRelease 1.8 adds updateKey, updatePartNotes, per-part voiceLimit/voicePriority and an optional Web MIDI adapter; see expressive performance and MIDI. Looping layers and boundary-quantized section switching are covered in adaptive music, and Worker startup and phase diagnostics in Worker diagnostics.\n","href":"./host-integration.html"},{"title":"Host integration · Host integration","text":"Host integration Host integration","href":"./host-integration.html#host-integration"},{"title":"Host integration · Worklet deployment","text":"Host integration Worklet deployment","href":"./host-integration.html#worklet-deployment"},{"title":"Host integration · Worker WAV output and bounded sinks","text":"Host integration Worker WAV output and bounded sinks","href":"./host-integration.html#worker-wav-output-and-bounded-sinks"},{"title":"Host integration · Events, acknowledgements and ownership","text":"Host integration Events, acknowledgements and ownership","href":"./host-integration.html#events-acknowledgements-and-ownership"},{"title":"Host integration · Cross-feature contracts","text":"Host integration Cross-feature contracts","href":"./host-integration.html#cross-feature-contracts"},{"title":"Host integration · Audio/output clock mapping and component cleanup","text":"Host integration Audio/output clock mapping and component cleanup","href":"./host-integration.html#audiooutput-clock-mapping-and-component-cleanup"},{"title":"Host integration · Other bundlers and SSR hosts","text":"Host integration Other bundlers and SSR hosts","href":"./host-integration.html#other-bundlers-and-ssr-hosts"},{"title":"Host integration · Part-scoped performance","text":"Host integration Part-scoped performance","href":"./host-integration.html#part-scoped-performance"},{"title":"Standard MIDI files","text":"Standard MIDI files # Standard MIDI files\n\nimportMidiFile and exportMidiFile are dependency-free binary adapters, available from opm.js/midi-file, opm.js/core, and the main API. They do not open MIDI devices, transmit SysEx, emulate a chip, or claim a General MIDI sound set.\n\nExpression conversion below is available since v1.10 in package 1.10.0. Controls remain opt-in, with default omission and explicit lossSummary; interactive examples are checkout-only and require the matching v1.10 source checkout.\n\n## Import\n\nts\nconst score = importMidiFile(bytes, {\n channelVoices: { 0: 'brass', 1: 'lead', 9: 'drum' }, // channels are zero-based\n defaultVoice: 'brass',\n unsupported: 'warn',\n unclosedNotes: 'reject',\n sustain: 'apply',\n controls: 'preserve', // explicit; default 'omit' retains legacy note-only conversion\n pitchBendRange: 2, // initial semitones, finite 0..48; RPN 0 may change it\n});\n// score.events: BeatSequenceEvent[] (positive unique note IDs plus per-note controls)\n// score.tempoMap: normalized quarter-note BPM steps, beginning at beat zero\n// score.timeSignature: one numerator/denominator\n// score.warnings: { code, message, count }[]; counts are aggregated by code\n// score.lossSummary: frozen { omissions, approximations, preservedControls }\n\n\nSupported input is SMF format 0 (one track) or format 1 (simultaneous tracks), with positive integer PPQN timing. All tracks are merged by absolute tick, then file track number, then event order. Channel state is shared across tracks. Repeated note-ons at the same channel/pitch are owned FIFO, independently of sustain; note-on velocity zero is a note-off. CC64 values 64..127 hold released keys until pedal-up. Sustain becomes longer note durations and produces sustain-applied warnings. sustain: 'reject' rejects any CC64 rather than approximating it.\n\nVoice selection is opt-in: programVoices?: MidiVoiceMap maps program numbers 0–127 to FM voice names, and drumVoices?: MidiVoiceMap maps drum notes 0–127 on zero-based channel 9. Each channel starts at program 0; Program Change updates it in merged tick/track/event order. Each note stores its onset voice, so changes never retarget held notes. Drum entries take precedence, then program entries, then channelVoices[channel], then defaultVoice (brass). Only the final default fallback warns. Omitting the new maps preserves legacy note output. Bank CC0/32 stays ignored with warnings. The caller supplies the corresponding voice registry to compilation/rendering; missing names produce the normal unknown-voice error. Preserve routing on export using distinct voice names and voiceChannels.\n\nMidiVoiceMap = Readonly\u003cRecord\u003cnumber, string>> | ReadonlyMap\u003cnumber, string> accepts plain own-data objects or native Maps, copied before use, at most 128 entries. Keys must be canonical integers 0–127; names match [a-zA-Z0-9_-]{1,64}. Accessors, inherited entries, symbols and custom Map properties/prototypes reject. gmProgramVoices and gmDrumVoices are frozen root/core exports: all 128 programs map by GM family to bundled FM names, with a partial percussion map. They are artistic starters, not a GM sound-set claim. Load the bank containing those names, including noise-snare/noise-hihat for the starter drum map, or edit the map for your bank.\n\nTempo defaults to 120 BPM until the first tempo event. Conflicting tempo events at the same tick use the last event in the documented merge order and warn. MIDI microseconds-per-quarter must produce BPM in 1..1000. Only one meter is representable: conflicting meters at beat zero or any actual meter change later reject. Repeating the same meter is accepted. Numerators are 1..32; denominators are 1, 2, 4, 8, 16 or 32.\n\nThe default controls: 'omit' is compatibility mode: bend, pressure and controllers other than CC64 are omitted with ignored-channel warnings. Opt in to controls: 'preserve' for:\n\n| MIDI source | Beat control meaning |\n| --- | --- |\n| Pitch bend | pitch, signed semitones using channel sensitivity, initially pitchBendRange (default 2) |\n| CC7 and CC11 | expression = (CC7 / 127) * (CC11 / 127); initial values are both 127 |\n| CC10 | pan = max(-1, (value - 64) / 63); initial center is 64 |\n| CC1 or channel pressure | Channel-default modulation = 1 + value / 127; last channel message wins, initial value is 1 |\n| Poly pressure | Same modulation scale, applied only to the newest still-pressed key of that channel/pitch |\n\nChannel updates visit every held or sustain-held gate in onset order, including distinct repeated pitches, and set the initial state of future notes. Poly pressure does not change future-note defaults or target sustain-only keys. Once assigned to a pressed key, its modulation override persists through sustain and takes precedence over later channel CC1/pressure defaults, matching the live Performance control merge. Pressure is LFO-depth scaling, not an invented force/amplitude unit. Each expressive note is followed by a same-beat initial control snapshot (pitch: 0, expression: 1, pan: 0, modulation: 1 unless the channel changed). This can contain zero expression without turning the note-on velocity into a release.\n\nThe adapter does not know synthesis patches' release-tail duration. Once a gate closes it is no longer a channel-owned control target. A subsequent channel-expression message on that channel produces the conservative release-tail-controls approximation warning, even if the unknown tail might already have ended. It still updates held/sustained gates and future-note defaults. unsupported: 'reject' rejects this possible loss. This is not complete live-performance/release-tail fidelity.\n\nPreserve-mode import decodes RPN 0 sensitivity: CC101=0 and CC100=0 select it, CC6 supplies semitones (clamped to 48), and optional CC38 supplies cents (clamped to 99). Their sum is clamped to 48 semitones; each channel starts from pitchBendRange. Only later bend messages use the changed scale. CC101=127/CC100=127 deselect; other RPN numbers and NRPN selection CC98/99 also disable sensitivity data entry. Selection messages are recognized; unselected CC6/38 remain omitted with warnings. Omit mode does not interpret RPN.\n\nUnmapped programs, bank selection, NRPN, unselected data-entry, other controllers/channel-mode messages, release velocity, text, key signatures, port/channel metadata, sequencer-specific metadata, SMPTE offsets, framed SysEx/escaped data, and header extensions remain omitted with warnings. Non-default MIDI metronome/thirty-second-note meter fields also warn. No SysEx is executed. unsupported: 'reject' rejects omissions, conflicting tempos, default voice substitution and possible release-tail-control loss; explicit sustain: 'apply' and unclosedNotes: 'close-at-end' remain accepted approximations.\n\n### Inspect losses, not just success\n\nwarnings remains the aggregated { code, message, count } API. lossSummary separates:\n\n- omissions: ignored metadata/channel/SysEx/header data, release velocity and meter-detail warnings.\n- approximations: default voice substitution, flattened sustain, conflicting tempo choice, explicit end-of-file closure and possible release-tail-control loss.\n- preservedControls: { kind, count } for pitch-bend, expression, pan, modulation, channel-pressure, poly-pressure, program-change and pitch-bend-range. Counts are accepted source messages, not generated per-note controls. A program change counts when its number has a program-map entry (even without a later note); otherwise ignored-program counts in omissions. pitch-bend-range counts CC100/101 selection and selected RPN-0 CC6/38. CC7/11 both count toward expression; initial snapshots do not count. Neutral/no-op messages count as recognized semantics, not archived bytes.\n\nWarning counts retain their original units: CC64 messages, closed gates for unclosed-notes, note onsets using a fallback voice, or source warnings/messages for other codes. Arrays, entries and summary are frozen and bounded by the fixed code/kind vocabularies. Expose both loss categories before rendering or saving an imported project. Sustain is an explicit gate-duration approximation, not a pedal-performance recording.\n\nUnmatched note-offs always reject. Held keys or sustained gates at the final end-of-track reject by default. unclosedNotes: 'close-at-end' explicitly closes them at the greatest track end tick and reports unclosed-notes; it cannot create a positive duration if the note starts at that tick. Zero-length gates reject. No invented extra tail is added.\n\nTrack lengths, channel data bytes, four-byte VLQs, running status, recognized fixed-length meta records, and SysEx/meta length framing are checked. Meta/SysEx cancel running status. Every declared track must contain a terminal end-of-track event with no bytes after it; undeclared chunks/trailing bytes reject. Format 2, SMPTE division and unframed system statuses reject. Input must be a native, non-shared Uint8Array; subarrays and Node Buffers are accepted without invoking caller metadata getters.\n\n## Export\n\nts\nconst bytes = exportMidiFile(score.events, {\n format: 1, // default 1; 0 is also supported\n ppqn: 480, // integer 1..32767\n tempoMap: score.tempoMap,\n timeSignature: score.timeSignature,\n voiceChannels: { brass: 0, lead: 1, drum: 9 },\n controls: 'preserve', // default 'omit' still rejects control events/nonzero pan\n pitchBendRange: 2,\n});\nconst reloaded = importMidiFile(bytes, {\n channelVoices: { 0: 'brass', 1: 'lead', 9: 'drum' },\n controls: 'preserve',\n pitchBendRange: 2,\n});\n\n\nFormat 0 combines conductor and musical events into one track. Format 1 writes one conductor track and one musical track per used channel, sorted by channel. Output is deterministic, uses explicit status bytes, writes a single initial meter, and puts note-offs before controls/onsets at equal ticks. Initial channel state is written before its note-on. Remaining expressive operations follow score event order at each rounded tick; contiguous equal-value per-note fanout is restored as one channel update. All tracks end at the same last note/tempo tick. Notes and explicit stop events are supported; a stop shortens its owned gate. Stops before onset, unknown IDs and duplicate note IDs reject. IDs are local score identities and are reassigned on import, not serialized into MIDI.\n\nOnly brass -> channel 0 is implicit. Other named voices require voiceChannels; embedded synthesis patches cannot be exported. Pitches must be integer 0..127. Positive normalized velocities are rounded to 1..127 (never velocity-zero note-offs). Beats and gate endpoints independently round to the nearest PPQN tick, which is monotone; gates that collapse to zero ticks reject. Same-channel/pitch gates must finish in onset order to preserve FIFO ownership; a later onset ending earlier than its predecessor rejects. MIDI has no note-instance IDs with which to recover that nesting.\n\nWithout controls: 'preserve', OPM control events and nonzero note pan reject, maintaining the previous note-only export contract. Expressive export accepts only pitch, expression, pan and modulation controls. Ramp/glide (even zero), gain, operator/LFO/feedback controls, nonzero priority, zero velocity, unmapped voices, and linear tempo curves reject rather than silently dropping musical data. Supply an explicitly discretized step tempo map if approximation is desired; this adapter does not choose that approximation. Tempo points colliding after tick rounding reject. BPM is rounded to the MIDI three-byte microseconds field; tempos too slow for that field reject.\n\nPitch rounds to 14-bit bend within the explicit range; pan and modulation round to seven-bit controller values, with modulation restricted to 1..2. Expression rounds to the nearest representable CC7×CC11 product (ties prefer the higher CC7 factor), preserving already representable products up to floating-point arithmetic. Export emits both factors, CC7 before CC11; intermediate same-tick product updates are possible, with the final product established before onset or the next sample. Channel-wide modulation is encoded as CC1; representable per-key differences are encoded as poly pressure. Original controller/pressure message classes, redundant messages, unused channel state and MIDI bytes are not archival data in a BeatSequence.\n\nChannel pitch/expression/pan changes must agree for every overlapping gate on that channel. Contiguous equal-valued fanout controls can establish that agreement; a private per-note change that would alter another gate rejects. Poly-pressure modulation can address only the newest sounding repeated-pitch gate. Put onset controls directly after their owned note at the same rounded tick. Incompatible overlapping initial states, older repeated-pitch pressure changes, unknown/before-onset controls, controls outside the gate, and controls rounded onto release reject. After-gate controls accepted by the synthesis core cannot be exported: patch-dependent release tails have no finite gate representation in this adapter. Use distinct voice/channel mappings for independent parts rather than letting export guess a channel allocation.\n\nThus file → score → file preserves representable gate/control meaning under the explicit export policy and quantization, not complete General MIDI playback, exact message identity or release-tail behavior. No program, RPN, sustain-controller or SysEx output is synthesized. If imported RPN changes sensitivity, choose an explicit export range containing every decoded pitch; the exporter never reconstructs source RPN or program routing.\n\n## Hard bounds\n\n- MAXMIDIFILE_BYTES: 16 MiB input/output.\n- MAXMIDIFILE_TRACKS: 128 imported tracks; export uses at most 17.\n- MAXMIDIFILE_EVENTS: 65,536 wire events, including omitted metadata/controllers and end-of-track.\n- Expressive fanout plus notes is separately limited to 65,536 generated beat events; a small wire file with many held notes and controllers can hit this bound. Export also checks its generated wire budget before growing musical tracks.\n- At most 1,024 normalized tempo points and 86,400 quarter-note beats.\n- Delta times must fit four-byte VLQ (0..268,435,455). Export rejects excessive silent gaps instead of inserting hidden events.\n- Beat event/options/mappings must be dense own-data arrays/plain objects; accessors and unknown fields reject. Import channel mappings allow 16 channels; export mappings allow 256 names. Program/drum maps additionally accept native Maps, bounded to 128 entries each.\n\nThe beat horizon is not a guarantee of renderability: compileBeatSequence and offline/live renderers apply their separate elapsed-time, polyphony, memory and scheduling limits. A dense file may need chunked offline rendering or live transport streaming rather than one worklet batch.\n\n## Runnable Node file → chunk WAV and MIDI reload\n\nAfter npm run build, save this as midi-to-wav.mjs in the repository root and run node midi-to-wav.mjs input.mid output.wav output.mid. It uses the built local package through its self-reference; no npm publication is required.\n\njs\nimport { readFile, writeFile, stat } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { importMidiFile, exportMidiFile, MAXMIDIFILE_BYTES } from 'opm.js/midi-file';\nimport { compileBeatSequence, renderSequenceChunks, createWavEncoder } from 'opm.js/core';\n\nconst [input, wavPath, midiPath] = process.argv.slice(2);\nif (!input || !wavPath || !midiPath) throw new Error('Pass input.mid output.wav output.mid');\nif ((await stat(input)).size > MAXMIDIFILE_BYTES) throw new Error('Input exceeds MIDI byte budget');\nconst midiPolicy = { controls: 'preserve', pitchBendRange: 2 };\nconst score = importMidiFile(await readFile(input), { ...midiPolicy, channelVoices: { 0: 'brass' } });\nfor (const warning of score.warnings) console.warn(warning.code, warning.count, warning.message);\nconsole.log('Import loss summary:', score.lossSummary);\nconst seconds = compileBeatSequence(score.events, { tempoMap: score.tempoMap });\nconst render = renderSequenceChunks(seconds, { sampleRate: 48000, chunkFrames: 4096 });\nconst encoder = createWavEncoder({\n sampleRate: render.capacity.sampleRate,\n channels: 2,\n format: 'pcm16',\n totalFrames: render.capacity.frames,\n});\nfunction* wavBytes() {\n try {\n yield encoder.header();\n // PCM buffers are reused; encode each chunk before advancing the iterator.\n for (const chunk of render) yield encoder.encode({ left: chunk.left, right: chunk.right });\n yield encoder.finalize();\n } finally {\n render.cancel();\n }\n}\nawait pipeline(Readable.from(wavBytes()), createWriteStream(wavPath));\nconst midi = exportMidiFile(score.events, {\n ...midiPolicy,\n tempoMap: score.tempoMap,\n timeSignature: score.timeSignature,\n voiceChannels: { brass: 0 },\n});\nawait writeFile(midiPath, midi);\nconst reloaded = importMidiFile(await readFile(midiPath), { ...midiPolicy, channelVoices: { 0: 'brass' } });\nconsole.log('Reloaded events:', reloaded.events.length, 'Rendered frames:', render.diagnostics.renderedFrames);\n\n\nThe example intentionally maps otherwise-unmapped channels to brass with visible warnings. Replace both mappings with distinct voice/channel routes and pass your voices Map to compilation/rendering for different instruments. Ambiguous expressive export rejects instead of guessing those routes. It demonstrates file conversion from the current checkout, not a claim of General MIDI fidelity, listening quality, or physical-device testing.\n","href":"./midi-files.html"},{"title":"Standard MIDI files · Standard MIDI files","text":"Standard MIDI files Standard MIDI files","href":"./midi-files.html#standard-midi-files"},{"title":"Standard MIDI files · Import","text":"Standard MIDI files Import","href":"./midi-files.html#import"},{"title":"Standard MIDI files · Inspect losses, not just success","text":"Standard MIDI files Inspect losses, not just success","href":"./midi-files.html#inspect-losses-not-just-success"},{"title":"Standard MIDI files · Export","text":"Standard MIDI files Export","href":"./midi-files.html#export"},{"title":"Standard MIDI files · Hard bounds","text":"Standard MIDI files Hard bounds","href":"./midi-files.html#hard-bounds"},{"title":"Standard MIDI files · Runnable Node file → chunk WAV and MIDI reload","text":"Standard MIDI files Runnable Node file → chunk WAV and MIDI reload","href":"./midi-files.html#runnable-node-file--chunk-wav-and-midi-reload"},{"title":"Expressive performance and optional Web MIDI","text":"Expressive performance and optional Web MIDI # Expressive performance and optional Web MIDI\n\ncreatePerformance(opm, options) gives a host part-scoped key policies (poly/mono, legato, sustain, last/high/low priority), per-key and per-part live controls, per-part voice budgets and priorities, and a separate optional Web MIDI adapter. Try example 10 (checkout-only).\n\n## Per-key and per-part controls\n\nts\nconst key = performance.noteOn(0, 64, { velocity: 0.8 }); // independent key identity, not a note ID\nperformance.updateKey(0, key, { feedback: 5, pitch: 0.3 }); // that key only\nperformance.updatePartNotes(0, { operatorRatios: [2, 1, 1, 1], ramp: 0.05 }); // every sounding note + future notes\n\n\nBoth calls take the full NoteControls object (pitch, glide, expression, pan, modulation, ramp, operator levels/ratios/fixed-Hz/ADSR, feedback and LFO controls). Input is copied as own data and validated before any note is touched.\n\n- updateKey returns false for an unknown or already-removed key and otherwise applies to that key's gate. A key that is not sounding in a mono part (a held key behind the selected one) only stores its controls; the shared gate is never modified by it. When the mono selection later moves to that key, its stored controls are applied together with the pitch offset.\n- updatePartNotes merges into the part's control state, updates every sounding gate of the part (release tails included) and is applied to notes started later. Pitch accumulates as part + key + legato offset and is validated against the engine's ±48 semitone control range.\n- Part pan and expression set through configurePart/updatePart remain the base values. Controls that you apply do not change those configured values.\n- getPartControls(part) returns a detached, frozen effective part-control snapshot: configured defaults resolved against the current named/custom voice, then merged with part controls. It excludes per-key overrides. Operator levels are multipliers, with neutral defaults [1, 1, 1, 1], not the patch's raw operator levels.\n- Mono legato retains its anchor rule: beyond ±48 semitones of the original gate the part restarts the note at the exact target pitch, which re-evaluates velocity, key scaling and rate scaling.\n- noteOff(part, key, options?: PerformanceNoteOffOptions) accepts only own-data force?: boolean. Ordinary note-off remains pedal-aware and returns false for an already-unheld key. { force: true } also releases an unheld pedal-latched key, bypassing sustain for that key alone; mono selection retargets normally. It leaves the pedal and unrelated keys unchanged.\n\n## Voice budgets and priority\n\nts\nperformance.configurePart(0, { voice: 'lead', mode: 'mono', legato: true, voicePriority: 100 });\nperformance.configurePart(1, { voice: 'strings', voiceLimit: 8, voicePriority: 10 });\n\n\n- voiceLimit (1–32) caps the part's own gates. Release tails count: a released note still owns its voice until the engine reports it ended, so noteOn throws a RangeError while the part is at its limit. A limit larger than the engine's maxVoices does not create voices.\n- voicePriority (0–127, default 0) is sent with every note the part admits. With a full engine a note can only displace voices of equal or lower priority, lowest first (then the engine's stealing policy). If every sounding voice has higher priority the note is refused, nothing else changes, and the part removes the key so no phantom held key remains.\n- This is unrelated to the key-selection priority: 'last' | 'high' | 'low'.\n- getPart() snapshots include voiceLimit and voicePriority.\n- configurePart(part, options, policy?: { preserveNotes?: boolean }) normally clears gates on voice/mode changes. { preserveNotes: true } retains sounding gates on voice-only changes, selecting the new patch for later admissions; mono legato restarts rather than reusing a gate with a different patch. Mode changes always clear keys.\n\n## Web MIDI adapter\n\nts\nimport { createPerformance, createMidiAdapter, requestMidiAccess } from 'opm.js';\n\nbutton.onclick = async () => { // an explicit user action\n const access = await requestMidiAccess(); // sysex: false; may show a permission prompt\n const midi = createMidiAdapter(performance, access, { parts: 2, pitchBendRange: 2 });\n // midi.snapshot, midi.releaseAll(), midi.dispose()\n};\n\n\nImporting the module never requests access. The adapter accepts any object that provides inputs and statechange events (MidiAccessLike), so tests and hosts can inject their own. It is not a MIDI driver: no SysEx, clock or device-specific behavior. Optional programVoices?: MidiVoiceMap maps programs 0–127 to named FM voices via configurePart with preserved notes; only later onsets use the new voice, unmapped programs are ignored. The map accepts a plain own-data object or native Map, at most 128 entries; keys are integers 0–127 and names match [a-zA-Z0-9_-]{1,64}. It is copied/validated before ports open. Unknown bank names report the normal performance unknown-voice error to onError. gmProgramVoices (root/core export) is an artistic family starter, not a GM sound set. No live drumVoices option is provided; configure percussion parts explicitly. Bank select remains ignored.\n\n| Message (channel 1–16 → part 0–15) | Effect |\n| --- | --- |\n| Note on / off (velocity 0 = off) | noteOn(part, note, { velocity: v / 127 }); note-off releases the oldest still-held key of that input, channel and pitch. Repeated equal pitches keep separate identities. |\n| CC64 | Sustain pedal for the part. The part is sustained while any adapter input holds its pedal. |\n| CC1, channel pressure, polyphonic pressure | LFO depth multiplier 1 + v/127 (1×–2×): never below the patch default. Polyphonic pressure uses updateKey; the others updatePartNotes. |\n| CC7, CC11 | Multiply into the part's expression. |\n| CC10 | Pan. |\n| Pitch bend | ± channel sensitivity, initially pitchBendRange (default 2, at most 48). |\n| CC101/100, then CC6/38 | RPN 0 selection and sensitivity in semitones/cents for this channel; affects later bends only. |\n| CC120 | Force-release all owned keys (including pedal-latched keys) for this input/channel; no global panic or changes to other inputs/host keys. |\n| CC123 | Pedal-aware note-off for physically held keys of this input/channel; latched keys remain owned for later forced cleanup. |\n| CC121 | Reset the adapter's controllers for the channel and release its pedal. |\n\nEverything else is counted in snapshot.ignoredMessages. Malformed packets (wrong length, data bytes above 127, unknown status) are ignored, never thrown into the page.\n\nRPN 0 requires CC101=0/CC100=0. CC6 semitones clamp to 48, optional CC38 cents clamp to 99, and the total clamps to 48 semitones. Each channel starts with the explicit range (default 2); state is shared across adapter inputs for that channel, like other part controllers. RPN null (CC101=127/CC100=127), other RPN numbers, or NRPN selectors CC98/99 deselect sensitivity data entry. CC121 restores the explicit range and null selection; dispose clears internal RPN state and removes listeners, without resetting host part controls. No SysEx is accepted.\n\nLifecycle: the adapter attaches to current and later inputs (optionally restricted with inputIds) and calls open() where needed. releaseAll() force-releases only adapter-owned keys, including earlier pedal-latched note-offs, without changing the pedal. Disconnect and dispose() force-release the affected input's owned keys and remove listeners, then remove its contribution to the shared pedal union. If the last adapter pedal contribution disappears, the part pedal turns off and may release host-played pedal-latched keys too: pedal/controller state is shared per part, not key-owned. Another input's pedal contribution remains effective.\n\nLimits are 32 inputs, 128 physically held keys and 256 ownership records. Physically released records stay available for pedal cleanup; stale records are pruned from Performance snapshots only at the ownership threshold or explicit forced cleanup. Excess admission is reported to onError and dropped, never silently overwriting ownership.\n\n## Custom CC mappings\n\nts\nimport type { MidiControllerMapping } from 'opm.js';\n\nconst controllerMap: readonly MidiControllerMapping[] = [\n { controller: 16, field: 'feedback', min: 0, max: 7, ramp: 0.05 },\n { controller: 17, field: 'operatorRatios', operator: 0, min: 0.5, max: 6, ramp: 0.05 },\n { controller: 18, field: 'operatorLevels', operator: 1, min: 0, max: 2, ramp: 0.05 },\n { controller: 19, field: 'lfoRate', min: 0, max: 20, ramp: 0.05, reset: 3 },\n { controller: 20, field: 'operatorFrequencies', operator: 2, min: 100, max: 2000, ramp: 0.05, reset: null },\n];\nconst midi = createMidiAdapter(performance, access, { parts: 2, controllerMap });\n\n\nEach entry requires controller, field, min, max and ramp. Operator fields additionally require a zero-based operator index 0–3; scalar fields prohibit it. CC value 0 maps exactly to min, 127 to max, and intermediate values interpolate linearly. Endpoints must be finite, ordered and inside the engine's bounds:\n\n| Field | Range |\n| --- | --- |\n| pitch | −48..48 semitones |\n| expression, gain | 0..1 |\n| pan | −1..1 |\n| modulation | 0..2 |\n| feedback | 0..7 |\n| lfoRate | 0..20 Hz |\n| amDepth | 0..1 |\n| pmDepth | 0..1200 cents |\n| operatorLevels | 0..2 multiplier |\n| operatorRatios | 0.125..32 |\n| operatorFrequencies | 1..20000 Hz |\n\nramp is an explicit duration 0–10 seconds (0 is immediate); pitch uses glide, other fields use the engine's control ramp. glide, ramp and operatorADSR are not mapping targets. Mappings update part defaults, sounding gates (including release tails) and future notes. A tuple mapping preserves the other three effective part operator elements; per-key tuple overrides retain the Performance merge policy and may override the whole tuple.\n\nA listed CC replaces that controller's ordinary action: mapping CC1 to feedback disables CC1's default modulation action, but channel/poly pressure still control modulation. CC64/100/101/120/121/123 cannot be remapped. RPN 0 data-entry CC6/38 takes precedence over custom mappings only while selected; otherwise mappings work normally, or unmapped data entry is ignored. NRPN selectors deselect RPN before any ordinary/custom action. Controllers must be integers 0–127; at most 128 entries are accepted, with no duplicate controller or target (same field/operator). Omitting controllerMap preserves ordinary CC behavior, except the newly supported RPN sensitivity.\n\nCC121 restores every mapped target for the channel. Optional reset defines that target's baseline and must be inside the field's engine bounds (not necessarily inside min/max). Only operatorFrequencies accepts reset: null, which restores ratio mode. Without reset, the baseline is captured from getPartControls when the adapter is created, so later patch/configuration changes do not change that reset value. Reset retains unrelated tuple elements and uses each mapping's ramp. Ordinary reset behavior and input-pedal ownership remain intact.\n\nThe adapter copies and validates the complete map before adding input/state listeners or opening ports. Plain own-data records and dense own-data arrays only: accessors, unknown fields, executable array overrides, sparse arrays, invalid ranges and duplicates throw synchronously. Later mutation of the caller's map has no effect. Errors while processing messages go to onError.\n\nExample 10 connects hardware knobs CC16–19 to feedback, operator 1 ratio, operator 2 level and LFO rate; the browser sliders use the same ranges and 50 ms ramps. Knobs follow the incoming channel, sliders the selected part. Physical MIDI devices and permission dialogs require manual verification on supported browsers.\n\n\n## Support and verification\n\nWeb MIDI exists in some browsers only and needs a secure context and user permission; feature-detect and handle rejection (requestMidiAccess rejects when unavailable). test/midi.test.ts (checkout-only) drives the adapter with an injected access object against the real worklet processor: channel routing, repeated pitches, pedal deferral, malformed packets, bend/wheel/volume ranges, hot-unplug releasing only one input's keys, and dispose. No physical keyboard or browser permission flow was exercised by the automated tests.\n\n## Original physical MIDI campaign (current 1.10.0 checkout)\n\nNo physical controller, permission-dialog result or human listening original is supplied here. Permission, connection, hot-unplug, sustain, CC and bend acceptance all remain unverified. Injected adapter tests and automated browser clicks do not change that status.\n\nThe bounded local observation export below is available in the v1.10 checkout-only example, not the installed engine archive. Build and serve the matching 1.10.0 source checkout to use the capture panel. No public export/protocol API was added.\n\n1. Build and serve the matching 1.10.0 checkout over HTTPS or localhost. Use a browser that actually exposes Web MIDI; unsupported browsers remain unverified for controller operation, not a fabricated pass. Start at comfortable low output volume and click Start / restart audio.\n2. In Local physical MIDI campaign, select the actual environment and enter device model/OS, exact browser version, controller model/firmware/USB or Bluetooth connection, and output route/device volume. Avoid serial numbers or personal identifiers. For desktop UI smoke choose Desktop / automation, even when exercising synthetic messages.\n3. Select a scenario and Begin local scenario capture. For permission, begin before Connect Web MIDI. Test a fresh real prompt's grant and denial as separate runs, using the browser's permission settings between runs when necessary. If permission was already granted or no prompt appeared, say so: that is not original grant/denial-dialog coverage.\n4. Follow the on-page instructions and the criteria below. Record observed behavior and unexercised steps, not only “works.” Check the physical-action and listening declarations only after actually doing both. Choose manual pass/fail/unverified, then Finish capture / release keys. Confirm silence after release tails. Repeat every scenario under the same exact declared conditions, exporting before changing browser/device/route.\n5. Export local MIDI observations before leaving or clearing. Review displayed and exported port IDs/names before sharing; browsers may expose identifying strings. Keep the untouched original privately; share a separately identified redacted copy if needed, never overwrite the original to fill missing results.\n\n| Scenario | Required original observations / manual criteria |\n| --- | --- |\n| Permission | Actual grant and denial outcomes, whether a prompt appeared, no SysEx request, visible denial error and usable host keyboard after denial; previously granted access must be identified. |\n| Connect | Physical channel 1 mono lead / channel 2 poly pad routing, repeated equal pitches, velocity-zero note-off, note release and no stuck keys. State unsupported features instead of inferring coverage. |\n| Hot-unplug | Unplug while holding notes and pedal, silence of the disconnected input after tails, reconnect without resurrected gates. Exercise a second controller if claiming input-isolation coverage; with one input leave that subclaim unverified. |\n| Sustain | Channel 2 chord held after physical note-off under CC64; pedal-up releases it; CC123 remains pedal-aware and CC120 forces adapter-owned cleanup. Check the shared part-pedal effect on host keys; do not claim pedal is key-owned. |\n| CC | Physical CC16–19 minimum/middle/maximum and audible held-note changes, CC121 restoration to adapter-creation baselines; CC1/7/10/11 and pressure when actually available. Note unavailable messages. Knobs do not move sliders; effective snapshots, not slider positions, describe applied state. |\n| Bend | Each routed channel's full down/up ±2-semitone range, return to center, fresh notes and released tails, other channel unaffected. Record actual controller centering; audible pitch judgment is manual, not a frequency measurement. |\n\nThe export is demo-local opm-local-midi-observations, version 1, not a public API protocol. It holds at most 24 runs and 256 timestamped page observations per run, with overflow counted; additional runs are refused until export/clear. Notes are capped at 1200 characters; four condition fields and input IDs/names at 160, user agent/errors at 240, and input enumeration at 32. Only 2–3-byte channel-voice packets are observed, never SysEx or arbitrary payloads. No network request, microphone/audio recording, persistent storage or JSON import/auto-trust path is added.\n\nEvery run carries exact VERSION, declared environment/device/browser/controller/route, bounded user agent, start/end/duration, beginning/ending actual sample rate, quality/maxVoices, fixed submitted mix/tuning/stealing/interruption settings, normalized loaded lead/strings patches, effective part states/controls, adapter snapshot and CC mappings. The profile is the applied instrument-midi-1 workload, not arbitrary presets or voices. Incoming MIDI packets, port changes, permission decisions/errors and host-control actions are page observations, never automatic pass/fail or proof of audible continuity.\n\nEngine construction and capture share one explicit immutable settings policy: standard quality, 16 voices, mix gain 0.35, oldest stealing, cancel interruption, and tuning { referenceHz: 440, offsets: [128 numeric zero cents entries] }. Exported quality/voice limits and context sample rate also come from the actual instance; normalized patches and effective part controls come from loaded snapshots. Begin/end settings are main-thread snapshots at capture boundaries, not sample-accurate audio-frame measurements. The ending snapshot is taken before Finish's part/adapter release or teardown cleanup; it can still contain held keys. Cleanup is a subsequent safety action, not evidence that the captured state was already silent. Exercise and record manual release/silence criteria during the active scenario if claiming that coverage.\n\nmanualJudgment remains separate from acceptanceStatus: active, abandoned, desktop/automation, non-physical, non-listened, changed-scope or overflowed runs are acceptance-unverified. Changing declared conditions, host-selected part, octave or glide invalidates the active scope and clears declarations; start/restart/dispose/page exit finishes it unverified. Intentional MIDI CC/bend changes are scenario actions, captured with effective controls, not a new static baseline. Begin/end settings must be reviewed alongside the event sequence. Finish releases the parts; teardown removes capture listeners and revokes download URLs. Reconfigure and recapture rather than relabeling an old run.\n\nReview the original JSON directly: check format/version, counts/truncation, finished timestamps, exact workload and route, manual declarations and notes; correlate packets/port transitions with claimed steps. A declared physical pass/fail is unauthenticated local evidence, not certified hardware acceptance. There is intentionally no additional public evidence validator. Unsupported or missing steps stay unverified even if a selector says pass; a maintainer must attach reviewed original references and exact scope before upgrading any compatibility claim.\n","href":"./midi-performance.html"},{"title":"Expressive performance and optional Web MIDI · Expressive performance and optional Web MIDI","text":"Expressive performance and optional Web MIDI Expressive performance and optional Web MIDI","href":"./midi-performance.html#expressive-performance-and-optional-web-midi"},{"title":"Expressive performance and optional Web MIDI · Per-key and per-part controls","text":"Expressive performance and optional Web MIDI Per-key and per-part controls","href":"./midi-performance.html#per-key-and-per-part-controls"},{"title":"Expressive performance and optional Web MIDI · Voice budgets and priority","text":"Expressive performance and optional Web MIDI Voice budgets and priority","href":"./midi-performance.html#voice-budgets-and-priority"},{"title":"Expressive performance and optional Web MIDI · Web MIDI adapter","text":"Expressive performance and optional Web MIDI Web MIDI adapter","href":"./midi-performance.html#web-midi-adapter"},{"title":"Expressive performance and optional Web MIDI · Custom CC mappings","text":"Expressive performance and optional Web MIDI Custom CC mappings","href":"./midi-performance.html#custom-cc-mappings"},{"title":"Expressive performance and optional Web MIDI · Support and verification","text":"Expressive performance and optional Web MIDI Support and verification","href":"./midi-performance.html#support-and-verification"},{"title":"Expressive performance and optional Web MIDI · Original physical MIDI campaign (current 1.10.0 checkout)","text":"Expressive performance and optional Web MIDI Original physical MIDI campaign (current 1.10.0 checkout)","href":"./midi-performance.html#original-physical-midi-campaign-current-1100-checkout"},{"title":"Physical-device audio recovery acceptance","text":"Physical-device audio recovery acceptance # Physical-device audio recovery acceptance\n\nExample 08 (checkout-only) is a repeatable manual capture runner, not a phone certification tool. Lifecycle events and analyser peaks cannot measure audible dropouts or prove AudioWorklet CPU deadlines. Desktop Chromium automation, emulated mobile user agents and host suspend() are not physical iOS/Android acceptance. The demo and maintenance workflows below require a repository checkout; they are absent from installed tarballs.\n\n## Evidence-backed support matrix\n\nNo physical-device capture is supplied with this repository. Every physical result below is unverified, for both interruption policies. API support and desktop smoke coverage are not hardware evidence. Only attach a result to an exact device model, OS/browser version, route, observed sample rate and tested policy; do not generalize it to an OS family.\n\n| Scenario | Physical iOS Safari, cancel / preserve | Physical Android Chrome, cancel / preserve |\n| --- | --- | --- |\n| Lock / unlock | unverified / unverified | unverified / unverified |\n| App switch / return | unverified / unverified | unverified / unverified |\n| Call / system interruption | unverified / unverified | unverified / unverified |\n| Bluetooth connect / disconnect | unverified / unverified | unverified / unverified |\n| Wired / USB headset route | unverified / unverified | unverified / unverified |\n| Low Power Mode / Battery Saver | unverified / unverified | unverified / unverified |\n| Long play, at least 10 minutes | unverified / unverified | unverified / unverified |\n| Explicit 2-second main-thread stall | unverified / unverified | unverified / unverified |\n| Dispose / recreate | unverified / unverified | unverified / unverified |\n\n## Prepare and repeat\n\n1. Build the current example (npm ci, npm run build) and serve the repository. A physical phone needs a reachable HTTPS origin trusted by that device; desktop localhost alone does not expose the server to a phone. Do not bypass certificate errors or use file://. No cloud upload service is required.\n2. Use the actual physical iOS Safari or Android Chrome browser, not desktop device emulation. Set comfortable speaker/headphone volume. Enter the exact model, OS version and browser version from the device settings. Do not put personal identifiers or phone numbers in notes.\n3. Select cancel, tap Start / resume, then select a scenario and Begin scenario capture. Begin clears old sound and submits one held note plus one onset due in five audio-clock seconds. Notes remain held until interruption policy clears them or you finish/panic. Metadata, scenario and policy are captured at begin; changing a route may not change the browser's fixed context sample rate.\n4. Follow the on-page scenario instructions. Record route/battery settings, interruption duration, whether the context actually stopped, and what you heard before tapping Start again. Add timestamped dropout/replay/stuck-gate/route markers during capture (or after returning, noting that the marker is retrospective).\n5. Tap Start / resume on return when needed. No timer resumes the context. Check recovery and manual pass criteria below. A route change or app switch may not interrupt the context on a particular device: record that fact and leave interruption-policy acceptance unverified, rather than inventing a suspension.\n6. Enter notes, check the manual observation declaration only if you actually performed/listened to this run, choose pass, fail or unverified, and Finish capture / silence. Finish submits panic. A short long-play run cannot earn pass: fewer than 600 seconds is forced unverified.\n7. Repeat the same scenario with preserve: Dispose synth, change policy, Start again, then begin a new capture. Test one scenario/policy at a time. Export before reloading/navigating away or after each policy batch. At most 24 recent captures are retained; export the first device's 18 captures before starting a second device. Repeated runs are separate records, not merged claims.\n\n## Manual pass / fail criteria\n\n- Cancel, when an actual context interruption was observed: prior held notes and pending events must not return after gesture recovery. Play a fresh score after recovery to confirm new admission. Unexpected replay, duplicate/stuck gates or inability to recover a still-valid context is fail.\n- Preserve, when an actual interruption was observed: direct held-note state can continue from the paused audio clock after gesture recovery, without duplicate gates. A still-pending direct onset can become due as that clock advances; this is not wall-clock catch-up/replay. Verify the held gate can be released and a fresh score admitted. Preserve does not promise audio while the OS suspends it, nor retention after dispose or a closed context.\n- Both policies: classify intentional OS silence separately from a dropout while audio should be running. No unexpected audible gaps, stuck/duplicate notes, or unbounded replay; fresh sound and All notes off/Panic must work after recovery. Report fail with subjective markers and notes if these expectations fail. Absence of an audible problem is a manual judgment, not a measured underrun count.\n- Routes and battery saver: record both route transitions and settings. Check fresh playback and silence controls after each transition. If a connector, call or power mode cannot be exercised, leave that scenario unverified. No permission prompts or OS route selectors are automated by the page.\n- Long play: listen for at least 10 minutes; record exact duration and foreground/background conditions. Finishing early forces a claimed pass to unverified. This limited session says nothing about all-day playback, thermal throttling or other devices.\n- Main-thread stall: explicitly press the 2-second stall button and listen. The UI should pause; audio must be judged independently. This intentional busy loop is bounded and never runs automatically.\n- Dispose / recreate: Dispose must silence old sound; Start creates a fresh synth using the still-host-owned context without resurrecting old gates. A closed context cannot be certified as recovered by a button; record the failure and reload to start a separate run.\n- Streaming score: interruption stops lookahead scheduling under either policy. Resuming the context must not restart the old stream; the demo's explicit stream button is the only restart. Direct-note preserve and stream restart are different contracts.\n\n## Local evidence format and handling\n\nExport local observations downloads opm-device-observations.json; the page does not upload, persist or record microphone/PCM audio. Exports use format opm-local-device-acceptance, version 1, and contain:\n\n- Per-run declared environment/model/OS/browser and bounded user agent, begin/end timestamps, actual begin/end context sample rates, fixed interruption policy and scenario, duration, notes and subjective markers.\n- New captures also snapshot the exact package version and beginning loadProfile: stimulus revision, normalized lead patch, actual OPM quality/maxVoices, submitted mix gain/tuning/stealing settings and host monitoring gain. This describes the initial workload, not everything a user might play later; retain event history and explain any extra workload/actions in notes.\n- Bounded engine/context/user-action observations with monotonic and wall timestamps. These are objective page observations, not computed pass/fail judgments.\n- Separate manualJudgment and acceptanceStatus. Desktop/automation, active/incomplete, absent, and non-listened captures are always acceptance-unverified. A physical pass/fail is only a user-declared manual result, not independently authenticated hardware evidence.\n- Coverage for all 36 combinations (two physical browser families × nine scenarios × two policies). It references the latest finished retained capture for each cell; absent cells are unverified. Use per-run metadata when interpreting the matrix, since cells do not assert every model/version is supported.\n- Limits and eviction counters: 24 runs, 256 observations/run, 64 subjective markers/run, 1200 note characters. Old observations/runs are evicted with counts; marker overflow is explicitly rejected. Strings in observations are capped at 240 characters, object depth at four, and keys/array entries at 32. Recent lifecycle text is bounded and rendered through textContent, never HTML.\n\nKeep exported JSON locally unless you intentionally choose to share it. Review notes and device identifiers before sharing. The runner deliberately has no JSON import or auto-trust path: opening a capture never upgrades it into a verified physical result. To review an artifact, validate its format/version, bounds and counters; check device/browser versions, sample rates, ended timestamp, declared environment/listening, judgment and notes, then correlate marker timestamps with retained events. Missing/truncated evidence should remain unverified. Preserve original files rather than editing them to fill missing results.\n\n## Reviewing exported evidence with npm run device-evidence\n\nsh\nnpm run device-evidence -- opm-device-observations.json [other-device.json …]\nnpm run device-evidence -- --require-complete ios.json android.json\nnpm run --silent device-evidence -- ios.json android.json > device-review.json\n\n\nThe command (Node 22+, no network) reviews all supplied files collectively, independently of their exported acceptanceStatus and coverage. A declared physical capture needs a finished, consistent start/end/duration, declared listening, unchanged policy, recorded begin/end sample rates, exact model/OS/browser strings and a compatible-looking user agent. Long play needs at least 600 seconds. Wall-clock jumps or elapsed-marker contradictions remain unverified rather than earning duration credit. The result then takes the human's manualJudgment; user agents and form entries do not authenticate hardware.\n\nRuns are grouped by exact environment, model, OS, browser, user agent, beginning/ending sample rates, policy, package version and canonical workload snapshot. Within each exact scope, the latest finished timestamp, not filename order or run ID, selects a scenario. Unfinished retries never replace finished captures. Equal-time conflicting judgments or contradictory content for the same scoped run identity stay unverified. Repeated originals add filename/run references, not coverage. The review includes original notes, markers and observations so a maintainer can investigate; every selected cell identifies its scope and source filename plus run ID and identity.\n\nPackage/workload combinations are separate campaigns. A complete campaign needs all nine scenarios in one exact scope per environment/policy: scenarios from different models, browser versions or sample rates are never stitched into one passing scope. If several scopes qualify, a complete one is selected, then the most recently finished; all scopes and failures remain in the report and selection is warned. One iOS export and one Android export, each with 18 compatible captures, can therefore satisfy all 36 cells together. Different versions/workloads cannot fill one another's gaps. The representative campaign prefers complete coverage and is explicitly identified; inspect every campaign, not only the aggregate counts.\n\n--require-complete exits 1 unless at least one recorded package/workload campaign has all 36 declared passing cells. Legacy version-1 exports without these new optional fields still parse and retain their declared per-run results, but unknown metadata cannot establish compatibility or strict completeness. Do not edit originals to invent a version/workload: retain and review them, then recapture with the current runner. No automatic migration can reconstruct an unrecorded workload.\n\nInputs are bounded to 16 regular files, 2 MiB each, 24 runs/file, 256 observations and 64 markers/run, 1,200 note characters, unique IDs within a file, valid counters/timestamps, known scenarios/policies, plausible sample rates and bounded own JSON data. Malformed structure is rejected; contradictory declared timing is marked unverified with reasons. A full device needs 18 captures, so export before the 24-run eviction limit. The stdout review is a separate artifact; inputs are never rewritten or uploaded.\n\nWhat this does not do: authenticate hardware, measure underruns, or replace reading markers, notes and truncated-event counters. Complete means “passing declared coverage under these exact recorded conditions,” never certification of iOS, Android or a phone family. The device evidence regression tests (checkout-only) use synthetic validator inputs, not physical results. To graduate the support matrix, supply original captures from actual iOS Safari and Android Chrome devices, have a maintainer review each scenario/policy and its exact conditions, and attach the reviewed artifacts and scope to the documented result. No physical devices or captures were supplied, so the matrix above remains unverified.\n\n## Desktop smoke workflow (not physical acceptance)\n\nAfter the integration build, open example 08 on desktop localhost. Choose Desktop / automation and actual desktop metadata, then:\n\n1. Export with no captures: all 36 coverage cells must be unverified.\n2. Start, begin lock/unlock, add a dropout marker with a literal HTML-looking note (for example \u003cb>heard gap\u003c/b>), export while active: acceptance remains unverified and text is not interpreted as HTML.\n3. Check manual observation and select pass, finish, export: manualJudgment can be pass but desktop acceptanceStatus and physical coverage remain unverified; the held sound is silenced.\n4. Start another run for main-thread stall, press the explicit stall control and finish unverified. Observe the bounded stall begin/end events. Repeat cancel/preserve only after Dispose; verify policy metadata follows the engine configuration.\n5. Select long play and finish before 10 minutes: it cannot earn physical pass. Exporting never sends a fetch/XHR; only Blob-download URLs are created. Refresh clears all captures.\n6. Exercise the separate mixed-event stream and 60-second/96-kHz bounded chunk renderer controls. They are desktop API smoke evidence only, not real-device long-play evidence.\n\nNo physical result should be added to the matrix without an actual manual capture and review. Automated smoke output must be labeled desktop-only.\n\n## Executable current-version campaign\n\nFor the matching v1.10 checkout (package 1.10.0), retain originals from one physical iOS Safari device and one physical Android Chrome device. Build and serve this source checkout for the checkout-only evidence page and commands. Each device needs the nine rows above under cancel and preserve: 18 runs per device / 36 cells, not one desktop run relabeled as a phone. Keep one exact version/workload scope per device/policy. Build-generated files and package version must match; retain the checkout revision separately with originals so equal package versions do not conceal different source.\n\nBefore each batch, Dispose and apply the intended profile/stealing/policy, tuning, mix gain and waveform, then Start and begin. Read the exported beginning loadProfile instead of assuming the visible selector is the applied engine. Do not change quality/policy/workload during a scenario or combine changed scopes to fill a campaign. Route-transition scenarios should describe before/after routes in markers and notes; unrelated route/battery/headphone or workload changes need a new separately scoped batch. Keep the same stimulus for the nine baseline scenarios; chord/control/stream experiments are supplemental evidence, not replacements for missing baseline cells.\n\nKeep an adjacent local campaign record listing exact checkout revision, filenames/run IDs, declared device/browser/OS, routes and volume, sample rates, profile/voice limits, policy, elapsed listening duration and each scenario's observed/absent interruption. Record real calls, connectors, Bluetooth transitions and battery modes only when available. For long play, listen continuously for at least 600 seconds and note foreground/background timing and thermal/battery conditions. Intentional OS silence and a real-time audible dropout are different findings. No phone, headset, call or power-mode access means that cell remains unverified; do not substitute desktop suspension or a timer.\n\nExport each 18-run device batch before the 24-run retention bound. Preserve original JSON and separately generated review output; use npm run device-evidence -- --require-complete ios.json android.json only as a declared-coverage review, then inspect exact scopes, markers, notes and event truncation. A successful review does not authenticate phones or replace human listening. A maintainer may update a matrix cell only with a reviewed original artifact reference and its exact tested conditions; changed versions/profiles/routes require new evidence.\n\nPhysical MIDI is a separate optional capability and campaign: use the instrument workflow, not this recovery runner. Browser Web MIDI absence is not a synthesis failure and a phone recovery capture is not proof of a keyboard, permission prompt or hot-unplug result.\n\nRemaining blocker: no physical iOS/Android phones, real interruption/route accessories, physical MIDI controller, or original human observations were supplied. All physical cells remain unverified. Code/UI automation can verify capture/export behavior only.\n","href":"./mobile-acceptance.html"},{"title":"Physical-device audio recovery acceptance · Physical-device audio recovery acceptance","text":"Physical-device audio recovery acceptance Physical-device audio recovery acceptance","href":"./mobile-acceptance.html#physical-device-audio-recovery-acceptance"},{"title":"Physical-device audio recovery acceptance · Evidence-backed support matrix","text":"Physical-device audio recovery acceptance Evidence-backed support matrix","href":"./mobile-acceptance.html#evidence-backed-support-matrix"},{"title":"Physical-device audio recovery acceptance · Prepare and repeat","text":"Physical-device audio recovery acceptance Prepare and repeat","href":"./mobile-acceptance.html#prepare-and-repeat"},{"title":"Physical-device audio recovery acceptance · Manual pass / fail criteria","text":"Physical-device audio recovery acceptance Manual pass / fail criteria","href":"./mobile-acceptance.html#manual-pass--fail-criteria"},{"title":"Physical-device audio recovery acceptance · Local evidence format and handling","text":"Physical-device audio recovery acceptance Local evidence format and handling","href":"./mobile-acceptance.html#local-evidence-format-and-handling"},{"title":"Physical-device audio recovery acceptance · Reviewing exported evidence with npm run device-evidence","text":"Physical-device audio recovery acceptance Reviewing exported evidence with npm run device-evidence","href":"./mobile-acceptance.html#reviewing-exported-evidence-with-npm-run-device-evidence"},{"title":"Physical-device audio recovery acceptance · Desktop smoke workflow (not physical acceptance)","text":"Physical-device audio recovery acceptance Desktop smoke workflow (not physical acceptance)","href":"./mobile-acceptance.html#desktop-smoke-workflow-not-physical-acceptance"},{"title":"Physical-device audio recovery acceptance · Executable current-version campaign","text":"Physical-device audio recovery acceptance Executable current-version campaign","href":"./mobile-acceptance.html#executable-current-version-campaign"},{"title":"Publication and package verification","text":"Publication and package verification # Publication and package verification\n\nGitHub v1.10 distribution targets package 1.10.0 with portable Arrangement projects, opt-in MIDI expression conversion and checkout-only capacity/campaign tooling; it does not publish to npm. The release location and installable opm.js-1.10.0.tgz are the publication targets, not proof that remote publication has completed. Once uploaded, install the downloaded archive from an application with npm install /actual/path/to/opm.js-1.10.0.tgz, without building or installing development tools. The release record must retain the actual reviewed commit, CI run and archive digest.\n\nThe historical v1.8 tag points to d793d69056c68a971ede1af988338412687227fd. Its 1.8.0 archive is 491,851 bytes, with SHA-256 bee862d5b3ccaa7c4e8ff959583873c78a8ca8a9f56f4d8b039e490e498e2587; it was built from that tag, installed/exercised, and downloaded back byte-for-byte. Its original documentation remains unchanged. The separately versioned 1.8.1 package includes the synchronized guides and its own regenerated version assets; do not confuse the two artifacts or their digests. Preserve existing tags and assets.\n\n## No automated npm publication\n\nThis repository does not ship a GitHub Actions workflow that publishes to npm. A GitHub release, tag, or passing CI job is not registry publication. An authorized maintainer must authenticate and publish separately; GitHub authentication does not grant npm authentication. Do not create a token or log credentials as part of local checks.\n\nTrusted publishing, npm ownership/2FA, environment reviewers and protected tags are external npm/GitHub settings. They are not configured by files in this checkout.\n\nregistry-verify's online provenance check still requires a published SLSA v1 attestation whose workflow identity is .github/workflows/npm-publish.yml, matching the removed workflow's intended identity. This is not evidence that any package was published by it. A package published another way will fail that check. A future publication needs an explicitly authorized provenance-bearing path and a matching reviewed verifier; do not bypass the check or treat local validation as registry acceptance.\n\n## Local and post-publication commands\n\nFrom a source checkout, validate a reviewed artifact without contacting npm or claiming registry availability:\n\nsh\nnpm run registry-verify -- --local /absolute/path/opm.js-1.10.0.tgz 1.10.0\n\n\nAfter publication, replace the SHA below with the actual reviewed release commit:\n\nsh\nnpm run registry-verify -- /absolute/path/opm.js-VERSION.tgz VERSION FULL40CHARACTERCOMMITSHA\n\n\nThe online command is read-only with respect to the registry: it cannot publish, configure trusted publishers or authenticate an account. Downloads have explicit byte/time budgets and are restricted to HTTPS registry.npmjs.org; its isolated installation is removed afterwards. It requires tar, npm with provenance verification support, and published SLSA v1 GitHub provenance whose workflow path is .github/workflows/npm-publish.yml. Retain its digest, file list and run URL in the public review record.\n\nFirst-package bootstrap, npm ownership/2FA, trusted-publisher configuration, GitHub environment reviewers/protected tags, publication authorization and actual registry verification remain external requirements. The public registry returned 404 for opm.js@1.8.0 during the package audit; registry integrity/signatures/provenance therefore remain unverified. Do not substitute a local PASS, stale release review, or manual unprovenanced bootstrap for a verified provenance-bearing publication. GitHub distribution of 1.8.0 does not establish npm publication. Run the security review and package gates against the exact candidate before any newly authorized release.\n","href":"./publishing.html"},{"title":"Publication and package verification · Publication and package verification","text":"Publication and package verification Publication and package verification","href":"./publishing.html#publication-and-package-verification"},{"title":"Publication and package verification · No automated npm publication","text":"Publication and package verification No automated npm publication","href":"./publishing.html#no-automated-npm-publication"},{"title":"Publication and package verification · Local and post-publication commands","text":"Publication and package verification Local and post-publication commands","href":"./publishing.html#local-and-post-publication-commands"},{"title":"Replayable score projects","text":"Replayable score projects # Replayable score projects\n\nAvailable since v1.9. A version 1 ScoreProject stores musical events, a tempo map, meter, named complete voice snapshots and synthesis settings in one self-contained JSON file. The pure APIs import from opm.js/core in Node without an AudioContext, DOM or Worker.\n\n## Create, save and load\n\nts\nimport { parseScoreProject, serializeScoreProject } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst project = parseScoreProject({\n version: 1,\n voices: { lead: brass },\n events: [\n { type: 'note', id: 1, voice: 'lead', note: 60, beat: 0, duration: 4 },\n { type: 'control', id: 1, beat: 2, controls: { expression: 0.6, ramp: 0.2 } },\n ],\n tempoMap: [{ beat: 0, bpm: 90, curve: 'linear' }, { beat: 4, bpm: 120 }],\n timeSignature: { numerator: 4, denominator: 4 },\n settings: { sampleRate: 44100, quality: 'standard', maxVoices: 8,\n mixGain: 0.35, stealing: 'release-first', tuning: { referenceHz: 440 } },\n});\nconst json = serializeScoreProject(project);\nconst replay = parseScoreProject(json);\n\n\nThe parser accepts a JSON string or a plain own-data object. version, events and voices are required. Omitted tempo, meter and settings normalize to 120 BPM, 4/4, 44100 Hz, standard, eight voices, mixGain 1, oldest stealing and 440 Hz with zero tuning offsets. Canonical output contains all settings and the 128-entry tuning table. Voice names are sorted, controls use stable key order, and saved defaults are explicit. Event array order is retained for deterministic simultaneous-event ordering.\n\nEvery note uses a stored voice name. Omitted note voice means brass, which must also be stored. Inline patches are rejected inside projects; standalone compileBeatSequence accepts them. Voice definitions normalize through the existing voice-version validator, including supported legacy patch inputs; project files themselves must specify version 1. No external bank or URL is referenced.\n\nReturned objects, arrays, voices, controls and settings are detached and frozen. Unknown fields, inherited fields, accessors, invalid versions, malformed meters/tempo maps, invalid controls/settings, duplicate note IDs and missing note/voice references throw. There is no prototype merge or invocation of JSON serialization hooks from caller data. Do not pass Proxies or executable objects as project data.\n\nBudgets: 8 MiB total serialized project, 128 stored voices with a 256 KiB normalized bank budget, 65,536 events and 86,400 quarter-note beats. Compiled seconds must fit the existing 24-hour long-sequence horizon. Array holes/extra properties are rejected. These budgets do not enlarge worklet queues or full-buffer rendering limits.\n\n## Portable Arrangement projects\n\nAvailable since v1.10, ArrangementProject is a separate version 1 definition, not an extension of ScoreProject v1. Existing score files keep their schema and replay behavior. The APIs parseArrangementProject(source: string | object) and serializeArrangementProject(project) ship in package 1.10.0 and are exported from both opm.js and opm.js/core; core parsing needs no DOM or Web Audio declarations. Interactive save/load examples remain checkout-only.\n\nts\nimport { parseArrangementProject, serializeArrangementProject } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst adaptive = parseArrangementProject({\n version: 1,\n voices: { pad: brass, lead: brass },\n settings: { sampleRate: 44100, maxVoices: 8, mixGain: 0.2 },\n tempoMap: [{ beat: 0, bpm: 96 }],\n timeSignature: { numerator: 3, denominator: 4 },\n layers: [\n { name: 'bed', length: 12, gain: 0.6, voicePriority: 20,\n events: [{ type: 'note', id: 1, beat: 0, duration: 12, note: 48, voice: 'pad' }] },\n { name: 'melody', length: 3, gain: 0.8, voicePriority: 100, events: [\n { type: 'note', id: 1, beat: 0, duration: 2, note: 72, voice: 'lead' },\n { type: 'control', id: 1, beat: 1, controls: { expression: 0.5, ramp: 0.2 } },\n ] },\n ],\n sections: [{ name: 'calm', layers: ['bed'] }, { name: 'battle', layers: ['bed', 'melody'] }],\n initialSection: 'calm',\n});\nconst saved = serializeArrangementProject(adaptive);\nconst restored = parseArrangementProject(saved);\n\n\nRequired fields are version, voices, layers, sections and initialSection. Tempo, meter and synthesis settings use exactly the score-project defaults; each layer normalizes gain to 1 and voicePriority to 0. Named voices, control-property order and all defaults are canonical; layer, section, member and event array order is retained. Every note references a stored voice; note IDs are unique within each layer, so separate layers may reuse IDs. Notes must start inside the layer's loop; durations and owned controls may extend beyond its end. Authored control gain is reserved for the layer and rejects; use expression.\n\nBoth live createArrangement and portable parsing use the same pure definition validator. They reject duplicate layer/section names, unknown references/fields, sparse arrays, accessors, invalid controls and out-of-range values. Repeated section members normalize to one membership in first-occurrence order, preserving existing live semantics. The returned layers, sections, events, voices, tempo/meter and settings are detached and deeply frozen. Projects allow 1–16 layers, at most 32 sections, 65,536 events in total, loop lengths 1/1024–256 quarter notes, priorities 0–127 and gains 0–1. The required initial section must exist (it may have no active layers). The 8-MiB project / 128-voice / 256-KiB normalized-bank budgets and beat/compiled-second horizons above also apply; the corresponding exported constants are MAXARRANGEMENTPROJECTBYTES and MAXARRANGEMENTPROJECTVOICES. Hosts must check upload/download bytes before buffering.\n\n### Load into the live API\n\nts\nimport { OPM, createArrangement } from 'opm.js';\n\n// Inside a browser click handler; dispose the previous owned arrangement/synth first.\nconst synth = new OPM({ ...restored.settings });\nsynth.replaceVoiceBank([]); // Remove implicit brass: a project may already store 128 names.\nfor (const [name, voice] of Object.entries(restored.voices)) synth.loadVoice(name, voice);\nconst music = createArrangement(synth, {\n layers: restored.layers, sections: restored.sections, initialSection: restored.initialSection,\n tempoMap: restored.tempoMap, timeSignature: restored.timeSignature,\n onError: error => console.error(error),\n});\nawait music.start(); // Parsing/loading alone never starts audio.\nmusic.switchSection('battle', { quantize: 'bar', fade: 0.8 });\nmusic.setLayerGain('bed', 0.4, { quantize: 'beat', fade: 0.5 });\n// When this host is finished:\nmusic.dispose();\nawait synth.dispose();\n\n\nUse the complete current built distribution for this example. A browser may choose a different actual sample rate, or reject a requested context/rate; surface that failure rather than silently substituting settings. On host teardown, dispose the old arrangement before replacing its owned synth; do not close a context borrowed from another component.\n\nThe saved object is an authored replay definition. It does not store the current musical cursor, scheduled changes, in-progress fades, note IDs, oscillator/envelope/LFO/filter history, or a DSP checkpoint. Loading starts the definition at beat 0 and its initialSection only after a user gesture. There is no serializer for live Arrangement objects; keep the authored definition separately and explicitly commit host edits into it.\n\nIn the current checkout, build and serve examples/adaptive.html: switch sections, toggle a layer, change a gain target or accelerate, then Save definition → Load arrangement JSON → Start. The page saves requested targets and the edited section definition (including edits whose musical boundary is still pending), not the pending transition itself. Loading disposes prior owned playback, rebuilds controls for arbitrary valid layer/section names, and stays stopped. Reset demo intentionally replaces the loaded definition and applies the selected swing without playing. Playback, subsequent switches and fades reuse the saved voices/settings; no code edits are needed.\n\nThe adaptive page owns its context and attenuates monitoring to 16% separately from the saved synthesis mixGain; saved settings are not weakened or silently rewritten for playback. Context/rate initialization failures remain visible.\n\n## Replay with Transport\n\nts\nimport { OPM, createTransport } from 'opm.js';\n\n// Create/resume this context inside a user gesture in a browser.\nconst context = new AudioContext({ sampleRate: replay.settings.sampleRate });\nconst monitor = context.createGain();\nmonitor.gain.value = 0.16;\nmonitor.connect(context.destination);\nawait context.resume();\nconst opm = new OPM({ ...replay.settings, context, destination: monitor });\nfor (const [name, voice] of Object.entries(replay.voices)) opm.loadVoice(name, voice);\nconst transport = createTransport(opm, replay.events, {\n tempoMap: replay.tempoMap,\n timeSignature: replay.timeSignature,\n onError: error => console.error(error),\n});\nawait transport.start();\n// Stop the owned score, then release the synth and the owned context when done.\ntransport.dispose();\nawait opm.close();\nmonitor.disconnect();\nawait context.close();\n\n\nTransport retains musical timing for restart/tempo edits. The actual browser sample rate may differ from the requested rate. Host monitoring attenuation is deliberately separate from saved synthesis mixGain.\n\n## Node: load → compile → chunk WAV\n\nts\nimport { readFile, open } from 'node:fs/promises';\nimport { parseScoreProject, compileBeatSequence, renderSequenceChunks,\n createWavEncoder } from 'opm.js/core';\n\nconst score = parseScoreProject(await readFile('piece.opm.json', 'utf8'));\nconst voices = new Map(Object.entries(score.voices));\nconst events = compileBeatSequence(score.events, { tempoMap: score.tempoMap, voices });\nconst chunks = renderSequenceChunks(events, {\n ...score.settings, voices, chunkFrames: 4096,\n maxFrames: score.settings.sampleRate * 120,\n});\nconst wav = createWavEncoder({ sampleRate: score.settings.sampleRate,\n channels: 2, format: 'pcm16', totalFrames: chunks.capacity.frames });\nconst file = await open('piece.wav', 'w');\ntry {\n // FileHandle.writeFile completes each byte chunk before the next render step.\n await file.writeFile(wav.header());\n for (const chunk of chunks) await file.writeFile(wav.encode({ left: chunk.left, right: chunk.right }));\n await file.writeFile(wav.finalize());\n} finally {\n chunks.cancel();\n await file.close();\n}\n\n\ncompileBeatSequence(events, { tempoMap?, bpm?, voices? }) returns validated second-based SequenceEvent[], preserving named voice references. Supply the same registry to the render consumer. Its default tempo is 120 BPM; tempoMap takes precedence over bpm. A gate's duration is the integrated end time minus integrated onset time, including all tempo ramps and steps it crosses. Control ramp/glide durations remain seconds, not beats. IDs and stable input ordering are preserved. The existing full-buffer renderSequence and browser renderSequenceInWorker consume the same compiled events/settings; existing queue/render capacity limits still apply.\n\nThe opm.js/core and opm.js/midi-file declarations can be consumed by Node TypeScript projects without DOM or Web Audio globals. This boundary is checked separately from the browser API declarations.\n\nChunk arrays are borrowed; encode/write each chunk before advancing. A failed write can leave a partial file: use a temporary output and rename only after successful finalization if atomic replacement is required. Rendering errors and device output are different evidence: deterministic WAV tests do not certify physical playback or subjective listening quality.\n\n## Original composition showcase\n\nOpen examples/showcase.html over localhost or HTTPS after building. Harbor at First Light is a D-major/B-minor 4/4 miniature with a glass lead, a middle countermelody and a slowing return. Lanterns on the Stair is an E-minor 3/4 miniature with a chromatic dominant and an open-ninth ending. Both melodies and arrangements are original to this demo, not copied songs.\n\nPlay/Stop uses the public Transport API and a low 16% host monitor gain. Render WAV streams PCM16 in a Worker, with a 90-second/32-MiB demo download budget. Save project writes canonical JSON; Load project validates before replacing the current score. Reload either saved composition, play it and render it again to exercise replay. Offline export preserves synthesis mixGain, not the extra host monitoring gain. Stop also cancels an active export. No physical iOS/Android or human listening outcome is asserted here.\n","href":"./score-projects.html"},{"title":"Replayable score projects · Replayable score projects","text":"Replayable score projects Replayable score projects","href":"./score-projects.html#replayable-score-projects"},{"title":"Replayable score projects · Create, save and load","text":"Replayable score projects Create, save and load","href":"./score-projects.html#create-save-and-load"},{"title":"Replayable score projects · Portable Arrangement projects","text":"Replayable score projects Portable Arrangement projects","href":"./score-projects.html#portable-arrangement-projects"},{"title":"Replayable score projects · Load into the live API","text":"Replayable score projects Load into the live API","href":"./score-projects.html#load-into-the-live-api"},{"title":"Replayable score projects · Replay with Transport","text":"Replayable score projects Replay with Transport","href":"./score-projects.html#replay-with-transport"},{"title":"Replayable score projects · Node: load → compile → chunk WAV","text":"Replayable score projects Node: load → compile → chunk WAV","href":"./score-projects.html#node-load--compile--chunk-wav"},{"title":"Replayable score projects · Original composition showcase","text":"Replayable score projects Original composition showcase","href":"./score-projects.html#original-composition-showcase"},{"title":"Sound design workflow","text":"Sound design workflow # Sound design workflow\n\nExample 12 (checkout-only) is a browser FM designer built only on the public API (OPM, playNote, updateNote). It is a demo helper, not part of the engine. Start it over HTTPS or localhost, click Start, then hold a note (button or Space) and edit while it sounds. Begin at a low device volume: host gain defaults to 0.15 and is capped at 0.3.\n\n## What you can edit\n\n| Area | Controls |\n| --- | --- |\n| Topology | Algorithm 0–7 with a live signal graph (arrows run from modulator to recipient; carriers reach the output), operator-1 feedback 0–7, modulation index |\n| Operators | Ratio, level, detune, waveform, noise hold rate (20–20000 Hz) and four-stage ADSR for each operator |\n| LFO | Rate, AM depth, PM cents, waveform, delay, note/global sync, phase, and a 0–1 AM and PM target weight per operator (the selective-LFO fields) |\n| Envelopes | A dB-versus-time plot of each operator for a chosen gate length. It evaluates the engine's own envelopeAt, so segments that are straight in dB are exponential in amplitude, and −96 dB is the engine floor. |\n| Starting points | Every bundled recipe, with its provenance, intended register, velocity range and host trim shown beside it |\n| Audio | DSP profile (eco / standard / high), note, velocity, host gain |\n\n## Live edits versus retriggers\n\nA held note is edited natively where the engine has a live control, and retriggered otherwise:\n\n| Edit | Behavior while a note is held |\n| --- | --- |\n| Operator ratio | operatorRatios ramp (20 ms) |\n| Any ADSR stage | operatorADSR, re-anchored at the current dB level |\n| Feedback | feedback ramp (20 ms) |\n| LFO rate, AM depth, PM depth | lfoRate, amDepth, pmDepth ramp |\n| Algorithm, selective target weights, level, detune, operator/LFO waveform, noise rate, delay/sync/phase, modulation index | The note is released and retriggered, because these are fixed when a note is admitted |\n| Name | No audio change |\n\nThe envelope plot shows a fresh note. Key-rate scaling, pitch envelopes, velocity and the LFO can alter an actual note, and a live ADSR edit starts from the sounding level, so the plot is a guide rather than an oscilloscope.\n\n## Patch JSON\n\nThe page exports and imports one canonical version 7 patch (at most 16 KiB). Import uses the strict single-voice path: unknown fields, accessors, non-finite numbers and out-of-range values are rejected visibly, and legacy versions 1–6 are normalized to version 7. The exported file can be passed straight to opm.playNote({ voice: patch, note: 60 }) or loaded with opm.loadVoice(name, patch). Nothing is uploaded; use the bank tools for many voices at once.\n\nChoose noise for an unpitched deterministic held-noise oscillator; noise rate is ignored for periodic shapes. Ratio/fixed-Hz/live pitch controls do not pitch noise, but envelopes, levels, velocity, key scaling and AM still apply. Noise can modulate or act as a carrier on any operator, an OPM.js extension rather than a hardware-fidelity claim. Square/saw (polyBLEP) and half (polyBLAMP) are edge-corrected, but they and noise are not alias-free despite per-voice oversampling. The recipe selector automatically includes original noise-snare, noise-hihat and explosion starting points; see oscillator formulas.\n\n\n## Designing with selective LFO\n\nBy default the LFO touches every operator. Set pmTargets / amTargets to confine it:\n\n- PM on a carrier wobbles the pitch you hear; PM on a modulator changes brightness without vibrato.\n- AM on a carrier is tremolo of that carrier; AM on a modulator moves sideband strength.\n- A weight of 0 leaves an operator untouched; fractional weights scale the depth for that operator.\n\ntidekeys, emberbass and orbit_pad in the voice quality guide are worked examples, and the audition page (checkout-only) can A/B them against other recipes with a held ratio/feedback/ADSR control phrase.\n\n## Limits\n\nThe designer previews one note at a time on the main OPM instance of the page. It does not predict polyphonic loudness; leave headroom when chords are played (see the suggested polyphony and trim in the usage table). No listening judgment is built in.\n","href":"./sound-design.html"},{"title":"Sound design workflow · Sound design workflow","text":"Sound design workflow Sound design workflow","href":"./sound-design.html#sound-design-workflow"},{"title":"Sound design workflow · What you can edit","text":"Sound design workflow What you can edit","href":"./sound-design.html#what-you-can-edit"},{"title":"Sound design workflow · Live edits versus retriggers","text":"Sound design workflow Live edits versus retriggers","href":"./sound-design.html#live-edits-versus-retriggers"},{"title":"Sound design workflow · Patch JSON","text":"Sound design workflow Patch JSON","href":"./sound-design.html#patch-json"},{"title":"Sound design workflow · Designing with selective LFO","text":"Sound design workflow Designing with selective LFO","href":"./sound-design.html#designing-with-selective-lfo"},{"title":"Sound design workflow · Limits","text":"Sound design workflow Limits","href":"./sound-design.html#limits"},{"title":"Bounded sequences","text":"Bounded sequences # Bounded sequences\n\nprepareSequence, renderSequence, and playSequence keep their convenience limits: 60 seconds, 128 notes, and 256 reserved scheduling slots (a note reserves onset and automatic release). Full-buffer rendering additionally caps output at 4,000,000 stereo frames. A 60-second score at96kHz therefore requires the chunk API, not a larger full-buffer budget.\n\n## Offline chunks\n\nImport prepareLongSequence, estimateSequenceCapacity, and renderSequenceChunks from opm.js/core. Long input is limited to 24 hours including note gates, and 65,536 input events. Generated automatic releases are additional bounded events. Note IDs are unique positive safe integers; controls/stops must reference a score note. Own-data dense arrays/plain objects only; accessors, unknown fields, malformed voices, controls, and oversized scores reject eagerly. Prepared patches and operator-level control tuples are detached immutable snapshots.\n\nts\nconst capacity = estimateSequenceCapacity(score, { sampleRate: 96000, chunkFrames: 4096 });\nconst render = renderSequenceChunks(score, {\n sampleRate: 96000, chunkFrames: 4096, maxFrames: capacity.frames, signal,\n});\nfor (const chunk of render) {\n // Consume synchronously before advancing, or await a sink in this ordinary for loop.\n await sink.write(chunk.left, chunk.right, chunk.offset);\n}\nconsole.log(render.diagnostics);\n\n\nNo aggregate PCM is allocated: two reusable Float32 buffers, chunkFrames each (integer1..65,536, default4,096), plus bounded score/event/synth state. left/right are borrowed until the next next(); copy if retention is necessary. The final chunk exposes only its valid frames. offset and frames describe contiguous stereo output. Iterator return()/breaking a loop and cancel() terminate without further advancement. An aborted native AbortSignal throws AbortError on the next advancement before rendering; later calls return done. Cancellation granularity is one chunk, not an individual sample.\n\nCapacity reports total frames/PCM bytes, reusable-buffer bytes, input-event/note counts, reserved slots, convenience eligibility, and explicit limits. It validates without rendering or allocating output PCM. Output includes ceil-rounded note gates, note-scaled operator release times, and a10ms tail margin. maxFrames is an optional hard cumulative-work budget checked eagerly; the absolute bound is the24-hour horizon plus maximum10-second release and10ms tail. Cumulative diagnostics expose rendered frames, consumed scheduling events (including cancelled events), and DSP errors. Concatenating chunks matches the same full-buffer render exactly. Ordering is stop→note→control at equal frames; controls preceding onset clamp to that onset, while an early/tied stop cancels its target.\n\nfullBufferAllowed includes the 4,000,000-frame and convenience score budgets. singleBatchAllowed includes the 60-second/128-note/256-slot limits independent of PCM size. streamAllowed reports default 0.2-second-window density preflight eligibility: peakWindowSlots ≤256 and peakWindowNotes ≤128, with origin zero at the estimated sample rate. It is not an admission guarantee: outstanding live IDs are also capped at runtime and unrelated worklet clients consume capacity. Nondefault horizons are preflighted by streamSequence itself. limits names these budgets as fullBufferFrames, batchNotes, batchSlots, longEvents, longSeconds, chunkFrames, streamHorizonSeconds, streamSlots, and streamNotes. Estimation allocates bounded score/queue snapshots but no Synth voice pool or PCM.\n\nThis is pull-based synchronous DSP. Yield between chunks or use a Worker for responsive applications. A24-hour maximum is a safety ceiling, not a promise of inexpensive rendering. Streaming an encoded file requires a sink/encoder that supports incremental output; encodeWav remains a full-buffer convenience helper.\n\n## Mixed real-time lookahead\n\nImport streamSequence from opm.js. It eagerly validates the complete long score before start() can initialize audio. Events use the same relative-time note/control/stop shapes as playSequence.\n\nts\nconst stream = streamSequence(opm, score, {\n horizon: 0.2, interval: 0.025, maxSlots: 256,\n onError: error => console.error(error),\n});\nawait stream.start(); // Invoke from an appropriate user gesture.\n// Later: stream.stop() or stream.dispose().\n\n\nThe optional absolute at is current time through60 seconds ahead when starting. Lookahead horizon is0.01..10 seconds; timer interval is0.001..horizon/2. maxSlots is1..256 and bounds owned queued submissions, not the whole shared worklet. Whole-score window density is preflighted (also against actual sample-rate frames on start). Each window and outstanding live/pending IDs are bounded; at most128 owned IDs remain outstanding. Other users of the shared worklet still consume its capacity, so observe rejection events/onError rather than assuming admission. Notes use held gates and separate automatically scheduled releases, allowing durations beyond60 seconds without enlarging the direct-note contract.\n\nScore→runtime IDs survive across windows; pre-onset controls wait for their target's admission and apply at onset. ids is a detached snapshot of live/pending IDs, not a historical admission report. Terminal note events prune IDs and queued bookkeeping. stop() cancels only this stream's pending/active IDs, never unrelated notes. Reset and context interruption/suspension terminate the stream, even when the host preserves unrelated direct-note state. Stop/dispose is idempotent and cannot command a replacement node. Start is one-shot; a cancelled score requires a fresh handle. AbortSignal cancels playback, including asynchronous startup. pump() can be called explicitly by a host clock; automatic timers remain enabled.\n\nA missed score event after a timer stall terminates via onError instead of silently replaying stale notes. Admission failures terminate and cancel owned IDs. No browser timer can guarantee uninterrupted physical-device playback; use the sequence example and physical acceptance scenarios for device evidence.\n\n## Restartable musical transport\n\ncreateTransport from opm.js adds a musical cursor without changing the one-shot sequence APIs. BeatSequenceEvent uses beat instead of time; note duration is also in quarter-note beats. Controls retain their existing second-based ramp/glide durations. The score is detached and validated eagerly, with at most65,536 events and86,400 beats including gates. IDs are unique positive safe integers, and stops/controls must reference a score note.\n\nts\nconst transport = createTransport(opm, [\n { type: 'note', id: 1, beat: 1, duration: 3, note: 60 },\n { type: 'control', id: 1, beat: 2, controls: { expression: 0.5, ramp: 0.1 } },\n], {\n tempoMap: [{ beat: 0, bpm: 120 }, { beat: 2, bpm: 90 }],\n timeSignature: { numerator: 4, denominator: 4 },\n loop: { enabled: true, from: 0, to: 4 },\n onError: error => console.error(error),\n});\nawait transport.start(); // Start/resume from a user gesture.\ntransport.pause(); // Preserve position; cancel owned gates/queued controls.\ntransport.seek(2); // Remain paused; reconstruct when explicitly resumed.\nawait transport.resume();\ntransport.setTempo(100); // Preserve current beat; replace future tempo points.\ntransport.setLoop({ enabled: false, from: 0, to: 4 });\nconsole.log(transport.position, transport.snapshot.musicalPosition);\ntransport.stop(); // Reset cursor to beat zero; handle remains restartable.\ntransport.dispose(); // Permanent helper teardown; does not close OPM.\n\n\nTempo defaults to120 BPM. BPM is finite in1..1,000; a supplied map contains1..1,024 own-data points, begins at beat zero, and increases strictly. Points default to step tempo; curve: 'linear' ramps BPM per beat to the next point’s BPM or an explicit endBpm (1..1,000). endBpm requires a linear curve, and the final point cannot be linear. Nonfinite derived slopes reject. beatsToSeconds integrates60/BPM across every boundary, using the logarithmic integral for ramps; secondsToBeats is its inverse. setTempoMap replaces the complete map at the current cursor, not at score zero. setTempo retains earlier points, truncates any active ramp at its interpolated current BPM, inserts a constant point at the current beat and removes later points. See tempo curves and grids. Edits while running cancel/rebuild owned scheduling immediately; edits while paused/stopped stay inactive. Edits during pending startup invalidate that attempt and require explicit start/resume again.\n\nbeatToBarBeat and barBeatToBeat use one-based bars and fractional one-based beats in a fixed signature. The numerator is1..32 and denominator is1,2,4,8,16 or32; for6/8, one displayed beat equals half a quarter-note beat. snapshot contains frozen position/meter/tempo/loop data and state (stopped, starting, running, paused, disposed). position is in quarter-note beats; running becomes true only after asynchronous audio startup. ids is a detached live/pending score→runtime map; overlapping loop tails may map a score ID to its newest occurrence, not every owned tail.\n\nSeek is bounded by the score end (or the enabled loop end, whichever is later). Loop intervals are finite with0≤from\u003cto≤86,400 and may contain silence beyond the score. Playback before the interval proceeds until to, then wraps to from. Starting/seeking at or beyond an enabled loop's to normalizes to from. Loop seams are scheduled on AudioContext frames, not timer callbacks. Every iteration has distinct runtime IDs; old gates terminate at to, and controls outside that iteration are not submitted. Natural release tails can overlap the next iteration.\n\nFor notes admitted in the current segment, controls tied to release or later in the release tail are still scheduled, matching sequence/direct-note semantics. This includes expression/timbre/ADSR edits and controls at the final non-looping score endpoint; score duration includes control timestamps independently of note gates. Loop control intervals remain half-open at to, so an endpoint control does not leak into the next iteration. Natural completion stops admission but retains already scheduled controls and owned release-tail IDs until their terminal events; it does not perform scoped teardown. A seek/resume destination after a note's gate has ended never resurrects that release tail.\n\nRestart semantics: seek, resume, tempo/loop edits and loop wraps reconstruct crossing held notes using their remaining musical gate and relevant controls. Linear pitch/expression/pan/modulation/operator-level/feedback/LFO-rate/AM-depth/PM-depth/operator-ratio ramps are evaluated at the destination and continue for their independent remaining seconds. Defaults come from the note's prepared patch. The latest fixed-Hz/null frequency policy is applied immediately at the destination, even if its original frequency ramp was unfinished; null uses the reconstructed live ratio and current engine tuning. This deliberately restarts that timbre transition rather than inventing intermediate Hz for a ratio-dependent endpoint. The latest operator ADSR edit is reanchored at the restarted onset. Phase, envelopes, LFO and pitch-envelope state restart; this is not a sample-exact DSP checkpoint or seamless sustain promise. An early/tied stop cancels its note; pre-onset controls clamp to onset. Pause/seek/edits use ID-scoped stop(id, { cancelControls: true }), removing owned future automation without global panic/allNotesOff or touching unrelated clients.\nIndependent note gain (0..1, default 1) is also reconstructed at seek/resume and continues any outstanding gain ramp; it multiplies expression without replacing it.\n\nLookahead bounds match streaming: horizon0.01..10 seconds (default0.2), interval0.001..horizon/2 (default min(0.025,horizon/2)), maxSlots integer1..256 (default256), and at most128 outstanding owned note IDs. Score/loop density is preflighted; runtime commands are additionally capped against the actual queue. Loops shorter than horizon/32 reject; at most32 wraps are generated per pump. Missed events or a pump gap longer than a horizon stop with onError, rather than silently retiming. pump() is available for host-driven observation; bounded automatic timers remain enabled. Position observes AudioContext time and never integrates timer drift.\n\nTransport start, resume and active seek/tempo/loop reconstruction anchor playback in the future by startupLead seconds: default min(0.05, horizon / 2), finite 0..10. The cursor remains at the requested beat during this count-in, then follows AudioContext time; durations are not stretched. Loop seams preserve the already-established audio clock and do not insert a lead on every wrap. startupLead: 0 opts out for hosts that already control delivery timing. Notes still use late: 'drop': headroom reduces cold-start beat-0 loss but does not suppress late/drop rejections or missed-window errors. Resolving start() is not confirmation of physical audible playback.\n\nReset, replacement/closed context, suspension and interruption stop this transport even under OPM's preserve policy. Returning the context to running does not resume it; explicitly call start/resume from a user gesture. Pause, stop, reset and dispose invalidate pending startup, so late promises cannot resurrect notes. Stop/dispose are idempotent. Disposed handles reject start/resume and mutation; no operation closes or globally resets the shared engine.\n","href":"./streaming-sequences.html"},{"title":"Bounded sequences · Bounded sequences","text":"Bounded sequences Bounded sequences","href":"./streaming-sequences.html#bounded-sequences"},{"title":"Bounded sequences · Offline chunks","text":"Bounded sequences Offline chunks","href":"./streaming-sequences.html#offline-chunks"},{"title":"Bounded sequences · Mixed real-time lookahead","text":"Bounded sequences Mixed real-time lookahead","href":"./streaming-sequences.html#mixed-real-time-lookahead"},{"title":"Bounded sequences · Restartable musical transport","text":"Bounded sequences Restartable musical transport","href":"./streaming-sequences.html#restartable-musical-transport"},{"title":"OPM.js usage guide (English)","text":"OPM.js usage guide (English) # OPM.js usage guide (English)\n\n繁體中文 · Project README\n\nGitHub release v1.10 (package 1.10.0) adds portable Arrangement projects, opt-in expressive MIDI files with loss summaries, workload-scoped capacity candidates and bounded local MIDI capture/campaign readiness. Existing v6 voices, expressive performance, 32-voice polyphony and Worker export remain supported. GitHub distribution does not imply npm registry publication.\n\nVoice-format versions are independent of package versions: the published v1.10 release uses canonical voice v6, while the current v1.11 checkout uses canonical voice v7 (per-operator waveform and noiseRate); the immutable v1.6 release used v5. Explicit legacy voice versions 1–6 retain their original input shapes and normalize to v7.\n\nRelease scope: portable Arrangement projects, opt-in expressive MIDI files and loss summaries, measured capacity candidates and the cross-feature contract table are included in v1.10. Physical/mobile/MIDI and human listening results remain unverified until genuine original observations are supplied.\n\nContents: Acquire and install · Browser quick start · Browser API · Node PCM · Voice format and banks · Compression · Troubleshooting\n\n## New in v1.11 (current checkout, unreleased)\n\n- Voice v7 waveforms and noise. Each operator accepts waveform (sine, half, abs, quarter, alternating, camel, square, saw, noise) and noiseRate (20–20000 Hz). Existing sine voices render identically; square/saw (polyBLEP) and half (polyBLAMP) are edge-corrected but, like noise, not alias-free. Original noise-snare, noise-hihat and explosion recipes join the bundled bank. See expressive voices.\n- Optional stereo effects. createStereoEffects/applyEffects (offline) and createEffects (a dedicated AudioWorklet insert) provide chorus and reverb; nothing is connected automatically. See audio buses.\n- MIDI program maps and RPN. programVoices/drumVoices map program changes and GM drum notes to named FM voices (opt-in; gmProgramVoices/gmDrumVoices are starter maps), and RPN 0 pitch-bend sensitivity is decoded in preserve-mode files and by the live adapter. See MIDI files and MIDI performance.\n- .opm import. importOPM/describeOPM from opm.js/voices/opm.js approximately convert VOPM/MXDRV-family text patches; this is not register-level emulation.\n\n## New in v1.10\n\nOnline demos over HTTPS need a playback click for audio startup. The features below are included in v1.10; historical releases remain unchanged. A GitHub release does not imply npm publication or deployment. Consumers can install the release tarball; source contributors run npm ci then npm run build.\n\n- parseArrangementProject(source: string | object) / serializeArrangementProject(project) are root/core APIs for a separate ArrangementProject version 1. Detached, deeply frozen own-data definitions store voices/settings, tempo/meter, layers, sections and the required initial section. Parsing is inert: loading does not start audio. Bounds include 8 MiB, 1–16 layers, at most 32 sections, 65,536 total events and 128 voices/256 KiB normalized voice JSON. Replay begins at beat 0; live cursor, transitions, fades and DSP state are not saved. See portable Arrangement projects.\n- importMidiFile(Uint8Array, options?) returns { events, tempoMap, timeSignature, warnings, lossSummary }; exportMidiFile(events, options?) returns a Uint8Array. Root/core and opm.js/midi-file expose both. Format 0/1 PPQN only. Default controls: 'omit' retains note-only import and rejects control events/nonzero pan on export. Opt-in controls: 'preserve' supports pitch, expression, pan and modulation under explicit pitch-bend-range and channel-ownership policies. lossSummary separates omissions, approximations and preserved source-control counts; sustain remains a gate-duration approximation. Export still rejects ramps/glides, gain/timbre controls, fractional note pitches, nonzero priority, ambiguous channel ownership and linear tempo ramps. No program-to-FM conversion or SysEx transmission occurs. See MIDI files and explicit policies.\n- Measured capacity candidates are scoped to the measured workload, runtime and machine, not universal supported-device budgets.\n- Bounded local MIDI capture and physical-device/listening campaign readiness help collect genuine original observations; they are not completed physical or human acceptance.\n- The cross-feature contract table documents replay, performance, rendering and host boundaries.\n\n### Existing APIs and examples (introduced in v1.9)\n\n- parseScoreProject(source: string | object) / serializeScoreProject(project) store canonical version-1 beat events, normalized tempo/meter, named complete voices and synthesis settings. Defaults are 120 BPM, 4/4, 44100 Hz, standard quality, eight voices, mix gain 1, A4 440 Hz and oldest stealing. Strict own-data snapshots reject unknown fields/accessors; limits are 8 MiB, 65,536 events and 128 voices/256 KiB voice JSON. compileBeatSequence(events, { tempoMap?, bpm?, voices? }) returns validated second-based SequenceEvent[] for offline and Worker rendering. Load stored voices into OPM before using beat events with Transport. See score projects.\n- TransportOptions.startupLead is 0–10 seconds, default min(0.05, horizon / 2): a future anchor protects beat-0 startup and reconstruction; musical position stays fixed through the lead. Setting 0 opts out; late drops and host stalls remain possible.\n- Arrangement layer gain is 0–1 (default 1). switchSection/setLayer accept fade seconds (0–10); setLayerGain(name, gain, { quantize?, fade? }) returns the committed beat. Shared layers are continuous; gains multiply independent note expression and affect owned tails/new onsets. Arrangement reserves authored gain controls; use expression for score dynamics. See adaptive music.\n- MidiAdapterOptions.controllerMap contains at most 128 mappings with controller, field, explicit min, max, ramp, optional reset, and a zero-based operator for operator tuple fields. Duplicate CC/targets and reserved CC64/120/121/123 reject. Scalar fields include independent NoteControls.gain (0–1, default 1). CC121 restores captured effective part defaults or explicit reset (operatorFrequencies allows reset null for ratio mode). performance.getPartControls(part) returns frozen effective part controls, not key overrides. See MIDI performance.\n- Thirteen demo scripts include two original songs with Play/Stop, score-project save/load and WAV export. The demo HTML remains checkout/website-only.\n\nnpm run build generates HTML guides and the full compiler-generated API reference after dist emit. Current local packages ship them under doc/; historical release packages are unchanged. Open doc/index.html directly or copy/serve the complete directory (plus top-level Markdown/legal files for source links). Search is local and keyboard accessible; no runtime dependencies or remote search service are needed. Markdown stays the source of truth.\n\n## Acquire and install\n\nNo source checkout or consumer build toolchain is required. With Node.js 22+ and npm, create a new application and install the attached GitHub v1.10 package directly:\n\nsh\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install https://github.com/JS-PACKAGE/OPM.js/releases/download/v1.10/opm.js-1.10.0.tgz\n\n\nFor an existing app, run only the install command in its root. The v1.10 tarball includes these synchronized guides; the original v1.8 tag and archive remain immutable. The public npm registry is not assumed to have opm.js.\n\nAlternatively, build a tarball from a repository checkout (checkout-only). From the OPM.js repository root, with Node.js 22+ and npm installed:\n\nsh\nnpm ci\nnpm pack\n\n\nnpm pack runs the package's prepack build and creates opm.js-1.10.0.tgz; do not separately build first. From that repository root, make a new sibling application (the repository directory must be named OPM.js for this relative path):\n\nsh\ncd ..\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install ../OPM.js/opm.js-1.10.0.tgz\n\n\nFor an existing app, run npm install /actual/path/to/opm.js-1.10.0.tgz in its root instead; npm init is unnecessary. Consumer apps need no development dependencies. The package contains minified .js modules, matching .js.map source maps with embedded TypeScript sources, generated .d.ts declarations, 13 demo entry scripts, and documentation/legal files, but not separate TypeScript source files, development scripts/tests, HTML pages, or the Vite example. Node uses opm.js/core and opm.js/voices/brass.js; browsers without an import map/bundler use served URLs.\n\n## Browser quick start\n\nFor the existing checkout demos, from the OPM.js repository root run:\n\nsh\npython3 -m http.server 8000\n\n\nOpen http://localhost:8000/index.html for 13 checkout-only HTML examples: notes, lookahead melody, live modulation, shared context, WAV, playground, audition, shared scores/interruption recovery, instrument/MIDI, sound design, audio buses, adaptive arrangement and original songs. Python 3 only serves local assets. Committed dist/ needs no installation/build; after source/helper changes run npm ci then npm run build. Use HTTPS or localhost with ES modules/AudioWorklet, never file://.\n\nTo create an app page, from the opm-app root created above, use the installed CLI to atomically copy the entire distribution and license into a new static directory:\n\nsh\nnpx --no-install opm-assets copy public/opm\n\n\nUse a fresh destination; the CLI never overwrites differing host files and reuses an identical existing deployment. For upgrades, use a release-specific path (for example public/audio/opm-1.10.0) and update page imports together. --no-install prevents automatic registry fetching. POSIX manual equivalent: create public/opm, copy nodemodules/opm.js/dist/. into it, and copy nodemodules/opm.js/LICENSE to public/opm/LICENSE.\n\nSave this complete page as opm-app/public/index.html:\n\nhtml\n\u003c!doctype html>\n\u003chtml lang=\"en\">\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>OPM.js example\u003c/title>\n\u003cbutton id=\"play\" type=\"button\">Play brass chord\u003c/button>\n\u003coutput id=\"status\" aria-live=\"polite\">Ready\u003c/output>\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });\n status.textContent = 'Playing brass chord';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\u003c/html>\n\n\nFrom the opm-app root, serve public/ and visit http://localhost:8000/; click the button to hear the chord:\n\nsh\npython3 -m http.server 8000 --directory public\n\n\nFor a production host (with JavaScript MIME types and X-Content-Type-Options: nosniff configured), verify deployed bytes from the installed opm-app root:\n\nsh\nnpx --no-install opm-assets check https://your-host.example/opm/\n\n\nUse your actual asset-base URL with its trailing slash; loopback HTTP is also supported. The CLI checks the complete installed release's bytes, HTTP status, MIME and nosniff, rejects redirects and uses a default 15-second overall deadline. Python's basic server lacks nosniff and is for the quick start, not this production check. The Node opm.js/tools API exposes checkDeployment(baseUrl,{timeoutMs,requireNosniff}) (integer deadline 1–60000 ms); disabling nosniff is only for development hosts. This does not certify CSP, native Worker/AudioWorklet startup, listening or device deadlines.\n\nWithout npm, instead copy the checkout's complete dist/ contents to site/opm/, copy its LICENSE to site/opm/LICENSE, save the same page as site/index.html, then from the directory containing site/ serve it with python3 -m http.server 8000 --directory site. Keep api/, core/, worklet/, worker/, and voices/ together, including companion modules, maps and declarations. Do not relocate just one worklet/Worker file. Modules preserve source paths instead of using hashed chunks. The browser imports the served ./opm/api/index.js URL, not the bare npm name. Static-copy deployment avoids relying on a bundler to discover either module graph.\n\nFor an installed-package Vite app, follow the standalone Vite guide (checkout-only): install the release tarball and copy the complete distribution plus license. Vite is an optional host development tool, not an OPM.js runtime dependency or consumer build requirement. Keep worklet assets out of SPA fallback rewrites, use JavaScript MIME types, and configure CSP for your module/worklet deployment; do not loosen production policy just to run an inline example. Publication prerequisites are documented in publishing; this guide does not imply a registry release or configured npm authentication.\n\n## Browser API and lifecycle\n\n| Call | Contract |\n| --- | --- |\n| new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain = 1, tuning = {}, stealing = 'oldest', interruption = 'cancel', quality = 'standard', maxVoices = 8 } = {}) | Requested integer rate 8000–96000 Hz; immutable quality 'eco'/'standard'/'high' and integer logical polyphony 1–32 (default 8) survive close/restart. The actual rate is opm.context.sampleRate; a borrowed context determines its own rate, and OPM never suspends/closes it. Omitted destination uses context.destination; null disables connection. workletUrl optionally relocates the complete same-origin worklet tree without weakening secure-context/CSP/MIME requirements. |\n| await opm.start() / await opm.resume() | Initialize/resume from a user gesture and await before playback. Concurrent starts coalesce; existing suspended contexts resume. |\n| opm.connect(destination) / opm.disconnect(destination?) | Connect/disconnect the started worklet; return the instance. Destination must belong to its context; omitted disconnect destination removes all connections. |\n| opm.loadVoice(name, voice) | Strictly validate/copy a voice under [a-zA-Z0-9_-]{1,64} in a registry bounded to 128 names. Works before startup; replacement affects future notes. |\n| opm.replaceVoiceBank(source) / opm.exportVoiceBank() / opm.removeVoice(name) | Validate the entire JSON string or readonly complete-voice array before replacing the registry; at most 128 entries and 256 KiB JSON. Plain [] clears, but JSON \"[]\" remains invalid under the parser's 1–128 contract. Export canonical named JSON (empty bank: []), bounded to 256 KiB. Removal returns boolean and does not kill sounding/queued frozen patches. |\n| opm.playNote({ voice = 'brass', note, time = 0, at, duration = null, velocity = 1, pan = 0, voicePriority = 0, late = 'start' }) | Finite fractional MIDI 0–127; relative delay 0–60 seconds or mutually exclusive absolute at. Duration (0,60] or null to hold; velocity 0–1; pan −1–1; integer voicePriority 0–127. Returns a positive safe-integer note ID, not admission acknowledgement. |\n| opm.stop(id, { at } = {}) | Return a command ID; cancel a pending onset/release a live note immediately or at at, preserving natural release-tail automation. {cancelControls:true} instead immediately cancels that ID's queued onset/control/off commands and releases it. cancelControls, even false, cannot combine with at. Inactive/already-released targets report command rejection without changing note state. |\n| opm.updateNote(id, controls, { at } = {}) | Return a command ID; pending controls apply at onset, and releasing notes remain controllable. Inactive/invalid/queue-full commands report rejection. |\n| opm.allNotesOff() / opm.panic() | Return a command ID; bypass full scheduled queues. Cancel pending events and release active gates / immediately silence all tails and emit reset. Cache and routing survive. |\n| opm.setMixGain(gain) / opm.setTuning(tuning) | Require startup, return a command ID, and retain settings across node recreation; no phase/envelope restart. |\n| await opm.getDiagnostics() | Resolve {type:'diagnostics',requestId,activeVoices,pendingEvents,errors,rejectedNotes}. Requires running context, at most 64 outstanding; suspend/close/processor failure rejects pending requests. Resume before retrying. |\n| await opm.close() | Serialize disposal against initialization, disconnect the node, close only an owned context. Startup after close recreates owned contexts; borrowed contexts survive. |\n| opm.subscribe(listener) | Subscribe independently; returns an idempotent unsubscribe function. Exceptions are isolated. |\n| await opm.waitForCommand(commandId, { timeout, signal } = {}) | Admission-only wait: default 5000 ms, integer 1–60000 ms, optional AbortSignal; 64 pending waits/128 retained receipts. Rejection throws CommandRejectedError; acceptance is not execution. |\n| await opm.dispose() | Terminal cleanup; cannot restart this instance. close() remains restartable. |\n\nonEvent receives note lifecycle states, diagnostics, processor errors, command acknowledgements {type:'command',command,commandId?,id?,state,reason?,frame,time}, context state changes and global resets. Command state is accepted or rejected; acceptance means admission/immediate application, not guaranteed future execution. Correlate command IDs and handle rejection. Clear held-note bookkeeping on reset (close, failure, panic, interruption); close/failure need not produce every per-note terminal reply. Frame/time are audio boundaries, not callback delivery timestamps. opm.voices is a defensive ReadonlyMap snapshot of frozen patches.\n\nAbsolute at must be finite, nonnegative, safely representable in sample frames, and at most 60 seconds ahead of opm.context.currentTime. Past times are allowed: default late:'start' starts at the first available frame with its full gate duration; late:'drop' rejects with reason late, including worklet delivery delays. At equal frames, stop precedes onset, then controls; a scheduled stop at/before onset cancels rather than briefly sounding the note.\n\nControls are a nonempty own-data object: pitch −48..48 semitones; glide 0..10 seconds requiring pitch; expression 0..1; pan −1..1; modulation 0..2 (AM capped at 1/PM 1200 cents); operatorLevels is four multipliers 0..2 over patch levels. Current checkout also accepts feedback 0..7, lfoRate 0..20 Hz, amDepth 0..1, pmDepth 0..1200 cents, four operatorRatios 0.125..32, four operatorFrequencies 1..20000 Hz or null to restore ratio mode, and four complete operatorADSR {a,d,s,r} objects. Ramp 0..10 seconds independently retargets supplied scalar/ratio/frequency controls from current values; glide remains independent and linear in semitones. Oscillator phase and feedback history survive controls. ADSR changes instead reanchor at current dB: held attack restarts, released notes begin a newly scaled release capped at 10 seconds, and zero release drains the filter immediately. Fixed Hz follows pitch controls but ignores tuning-table transposition. Unknown fields/accessors/nonfinite/out-of-range values reject. See expressive voices and host integration.\n\nSince v1.9: gain (0–1, default 1) is independent of expression and follows ramp without restarting phase/envelopes; arrangement layer fades use this separate multiplier.\n\nThe selected 1–32 logical voices (default 8) and at most eight independent short stealing fades are bounded separately. voicePriority is integer 0–127, default 0: stealing considers only voices whose priority is no higher than the incoming note, selects the lowest eligible priority first, then applies the stealing policy. Stealing defaults to oldest; release-first chooses oldest released before held, while quietest uses current carrier envelope × velocity × expression with oldest ties, not instantaneous sample amplitude. If all voices have higher priority, the browser reports note rejection reason priority; direct Synth.noteOn throws VoiceAdmissionError. Events/IDs each cap at 256. Future timed notes reserve two events; controls/stops also consume slots. Terminal states reclaim obsolete events. Prepared patches use 128 content-keyed LRU registrations; validated replacement reuses IDs safely while queued/active notes keep their original snapshot.\n\nFor customization and early release, replace only the module \u003cscript> in opm-app/public/index.html above, keeping its #play button and #status output:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { brass } from './opm/voices/brass.js';\n\n const opm = new OPM();\n const voice = structuredClone(brass);\n voice.lfo.rate = 5;\n voice.lfo.amDepth = 0.2;\n voice.ops[0].level = 0.6;\n opm.loadVoice('my-brass', voice);\n const play = document.querySelector('#play');\n play.textContent = 'Play custom brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const at = opm.context.currentTime + 0.1;\n const id = opm.playNote({ voice: 'my-brass', note: 64, at, velocity: 0.7, pan: -0.5 });\n opm.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8, pan: 0.5, modulation: 1.2 }, { at: at + 0.1 });\n opm.stop(id, { at: at + 0.3 });\n status.textContent = 'Playing custom brass note';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\nThis example schedules one held note 0.1 seconds ahead, changes it 0.1 seconds after onset, and begins release 0.3 seconds after onset, all on the audio clock (three queued events, well inside the horizon). When disposing the page's synthesizer, await opm.close(); another click can start it again.\n\n### Bounded lookahead melody\n\nReplace the quick-start page's module script with this block, keeping #play and #status. Each click schedules a finite eight-note phrase; the Stop button cancels/releases only this scheduler's notes:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createLookaheadScheduler } from './opm/api/index.js';\n const opm = new OPM();\n const status = document.querySelector('#status');\n const melody = [60, 64, 67, 72, 67, 64, 62, 60];\n let origin;\n let index = 0;\n const scheduler = createLookaheadScheduler(opm, ({ from, to, maxNotes }) => {\n origin ??= from + 0.05;\n const batch = [];\n while (index \u003c melody.length) {\n const at = origin + index * 0.25;\n if (at \u003c from) { index++; continue; } // Skip beats missed during a stall.\n if (at >= to || batch.length === maxNotes) break;\n batch.push({ note: melody[index++], at, duration: 0.18, velocity: 0.6, late: 'drop' });\n }\n return batch;\n }, { onError(error) { status.textContent = error.message; } });\n document.querySelector('#play').addEventListener('click', async () => {\n scheduler.stop();\n origin = undefined;\n index = 0;\n try {\n await scheduler.start();\n status.textContent = 'Scheduling eight notes';\n } catch (error) { status.textContent = error.message; }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop phrase';\n document.body.append(stop);\n stop.addEventListener('click', () => scheduler.stop());\n window.disposeOPM = async () => { scheduler.dispose(); await opm.close(); };\n\u003c/script>\n\n\nThe callback receives half-open absolute windows [from,to) and must synchronously return at most maxNotes notes, each with at inside that window and numeric duration (0,60]; held notes and relative time are not allowed. Defaults are horizon 0.2 seconds, interval 0.025 seconds, and 32 notes per batch. Configurable bounds are horizon 0.02–10 seconds (greater than interval), interval 0.005–1 second, and maxNotes 1–128. There are at most 128 outstanding scheduler gates; normal worklet queue limits still apply. Missed windows are skipped, not replayed in a burst. Required onError receives callback/timer failures, which stop scheduling. start() initializes/resumes from a user gesture; stop() permits restart, while dispose() is permanent. Neither closes OPM or stops unrelated notes. After the finite phrase the callback returns empty batches; stop/dispose the helper when it is no longer needed.\n\nAny non-running context or reset stops the helper and reaches onError; restart explicitly from a gesture. Default interruption:'cancel' clears old gates/automation on suspension/interruption; 'preserve' retains direct notes/queued events but never automatically restarts lookahead.\n\n### Shared context, output routing, and events\n\nFor a host-owned context, replace the quick-start page's module script with this block. It holds each note until the Stop button and leaves the host context alive on disposal:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n const context = new AudioContext();\n const gain = context.createGain();\n gain.gain.value = 0.3;\n gain.connect(context.destination);\n const status = document.querySelector('#status');\n const opm = new OPM({\n context, destination: null,\n onEvent(event) {\n if (event.type === 'reset') {\n id = undefined;\n if (event.reason === 'close' || event.reason === 'failure') connected = false;\n }\n status.textContent = event.type === 'error' ? event.error.message\n : event.type === 'note' ? ${event.id}: ${event.state}\n : event.type === 'diagnostics' ? Voices: ${event.activeVoices}\n : event.type === 'command' ? ${event.command}: ${event.state} ${event.reason ?? ''}\n : event.type === 'context' ? Context: ${event.state} : Reset: ${event.reason};\n }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop held note';\n document.body.append(stop);\n let id;\n let connected = false;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.resume();\n if (!connected) { opm.connect(gain); connected = true; }\n if (id !== undefined) opm.stop(id);\n id = opm.playNote({ note: 60, velocity: 0.7, pan: 0.5 });\n console.log(await opm.getDiagnostics());\n } catch (error) { status.textContent = error.message; }\n });\n stop.addEventListener('click', () => { if (id !== undefined) opm.stop(id); });\n window.disposeOPM = async () => {\n await opm.close();\n connected = false;\n id = undefined;\n gain.disconnect();\n // The host decides when to close context.\n };\n\u003c/script>\n\n\nCall await window.disposeOPM() when removing this app's audio UI. The host remains responsible for the shared context and downstream nodes. Avoid suspending a shared context merely to stop one synthesizer.\n\n### Shared scores, tuning and safe recovery\n\nmixGain (finite 0–1, default 1) scales the mix before tanh; host gain controls listening volume, not saturation drive. Tuning replaces the complete configuration: {referenceHz: 442, offsets} with reference A4 20–20000 Hz and optional exactly 128 finite cents offsets in −4800..4800 (omission means zero). Fractional notes interpolate offsets, including a safe MIDI-127 endpoint. Active retuning preserves phases/envelopes and original-note key scaling.\n\nReplace the quick-start module script with this shared live/offline score:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, playSequence } from './opm/api/index.js';\n import { renderSequence } from './opm/core/index.js';\n import { brass } from './opm/voices/brass.js';\n const settings = { mixGain: 0.5, tuning: { referenceHz: 442 }, stealing: 'release-first' };\n const opm = new OPM({ ...settings, interruption: 'cancel' });\n const score = [\n { type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },\n { type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },\n ];\n let playback;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.start();\n playback?.stop();\n opm.loadVoice('lead', brass);\n playback = playSequence(opm, score, { at: opm.context.currentTime + 0.05 });\n const audio = renderSequence(score, { ...settings, sampleRate: opm.context.sampleRate, voices: opm.voices });\n document.querySelector('#status').textContent = Rendered ${audio.left.length} frames; errors ${audio.diagnostics.errors};\n } catch (error) { document.querySelector('#status').textContent = error.message; }\n });\n window.disposeOPM = async () => { playback?.stop(); await opm.close(); };\n\u003c/script>\n\n\nScore times are relative seconds. Notes require unique positive IDs and finite durations; stop/control events reference those IDs. Full validation precedes posting. Limits are 128 notes, 256 reserved slots (two/note plus commands), 60 seconds including gates, and 4,000,000 offline frames. playSequence returns a defensive score-to-note ID map and idempotent stop; it cannot reserve capacity atomically against unrelated callers. renderSequence includes tails. Matching PCM requires equal settings/rate/frame origin and no competing notes; encode only its left/right/sampleRate fields for WAV.\n\nOn physical iOS/Android, follow mobile acceptance and the support matrix using example 08 (checkout-only) over HTTPS. Capture device/OS/browser/rate/policy, observations and manual pass/fail/unverified results for lock/app/call/route/battery/long-play/stall scenarios. Reports remain local. Resume from a gesture; borrowed contexts remain host-owned. Desktop/headless signal checks are not physical recovery or audible-continuity evidence.\n\n### Bounded long scores\n\nFor up to 24 hours and 65,536 input events, use prepareLongSequence, estimateSequenceCapacity and renderSequenceChunks from opm.js/core. The estimate distinguishes full-buffer/single-batch eligibility from default streaming-window density; it does not promise admission alongside other callers. Chunk buffers are borrowed until the next next(): consume them immediately, retaining a copy only when needed. Break, cancel() or AbortSignal stops advancement; maxFrames can cap cumulative work. The 4,000,000-frame full-buffer budget remains; incremental WAV bypasses that allocation limit, not RIFF32 size limits.\n\njs\nimport { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';\nconst score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];\nconst options = { sampleRate: 96000, chunkFrames: 4096 };\nconsole.log(estimateSequenceCapacity(score, options));\nlet frames = 0, energy = 0;\nconst render = renderSequenceChunks(score, options);\nfor (const chunk of render) {\n for (let i = 0; i \u003c chunk.frames; i++) energy += chunk.left[i] ** 2;\n frames += chunk.frames;\n}\nconsole.log(frames, energy, render.diagnostics.errors);\n\n\nFor browser playback, import streamSequence alongside OPM, construct const stream = streamSequence(opm, score, { onError: console.error }), and call await stream.start() in the play button's gesture. stop() cancels only that stream's notes; dispose() removes its listener and makes it terminal. Controls, explicit stops and automatic releases retain their score IDs across windows. Interruption/reset stops the stream under both policies: resume the context, then explicitly restart if desired. See defaults, density limits and the complete browser recipe.\n\n### Beat transport and performance policy\n\nReplace the quick-start module script, retaining #play and #status:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createTransport, createPerformance } from './opm/api/index.js';\n const opm = new OPM({ quality: 'standard', maxVoices: 8 });\n const transport = createTransport(opm, [\n { type: 'note', id: 1, beat: 1, duration: 1, note: 60 },\n { type: 'note', id: 2, beat: 2, duration: 1, note: 64 }\n ], { bpm: 120, onError: console.error });\n const performance = createPerformance(opm, { parts: 1 });\n performance.configurePart(0, {\n voice: 'brass', mode: 'mono', legato: true, priority: 'last',\n glide: 0.1, pan: 0, expression: 0.8\n });\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.start();\n await transport.start();\n const key = performance.noteOn(0, 67, { velocity: 0.7 });\n performance.noteOff(0, key);\n document.querySelector('#status').textContent = 'Transport started';\n } catch (error) { document.querySelector('#status').textContent = error.message; }\n });\n window.disposeOPM = async () => {\n transport.dispose(); performance.dispose(); await opm.close();\n };\n\u003c/script>\n\n\nTransport uses quarter-note beats (including note durations), the AudioContext clock, and only its own note IDs. It provides pause, resume, stop, seek, setTempo (current position onward), setTempoMap, setLoop, pump, dispose, and read-only state/position/snapshot/IDs. Default BPM is 120; valid BPM 1–1000, tempo maps start at beat 0 and ascend strictly (at most 1024 points), and positions cap at 86400 beats. Signature numerator is 1–32, denominator a power of two up to 32. Horizon is 0.01–10 seconds, interval 0.001–horizon/2, and maxSlots 1–256. Pause/seek/loop restart envelopes/phases rather than restoring DSP snapshots: scalar/ratio ramps resume with remaining durations; latest fixed-Hz/null policy applies immediately, even mid-ramp, and latest ADSR reanchors the new onset. Core exports beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, and normalizeTempoMap.\n\nThe recipe leaves a musical one-beat count-in. Current-checkout Transport additionally anchors in the future with startupLead (default min(0.05, horizon / 2) seconds) so a beat-0 onset is not submitted at the just-expired cold-start clock. The historical v1.8.1 archive lacks that fix. late:'drop' still rejects genuinely late admissions and stops the helper through onError; neither count-in nor startup lead guarantees deadlines under host stalls. start() resolving is not confirmation that every note sounded.\n\nTempo points support default curve:'step' or curve:'linear' BPM interpolation in beat space up to the next point; the last point cannot be linear. Optional endBpm requires a linear segment and can preserve its endpoint when editing a map. Beat/second conversion integrates and inverts ramps. Import grid helpers quantizeBeat (floor, ceil, nearest, strictly-later next), swingBeat and swingBeatEvents from the package root opm.js (served ./opm/api/index.js in plain browsers), not opm.js/core; swing transforms note positions, explicit controls/stops and automatic note ends coherently. See host integration for tempo/grid recipes.\n\nFor layered section-based music, createArrangement aligns looping layers to the global beat grid and commits section/layer changes at quantized beat/bar/numeric boundaries no earlier than already admitted notes. Shared layers keep sounding without retriggering when unrelated layers change. Layer voicePriority combines with note priority by taking the maximum; refused lower-priority notes increment snapshot priorityDrops instead of failing the arrangement. See adaptive music and arrangement for limits, transitions and a complete recipe.\n\nPerformance has 1–16 zero-based parts (default 16), system/per-part key bounds up to 128, independent physical-key IDs even at equal pitch, poly/mono modes, last/high/low key-selection priority, sustain and part controls. Part voiceLimit is 1–32; voicePriority is integer 0–127 and is distinct from key-selection priority. Use noteOff(part,key), sustain(part,on), updatePart(part,{glide,pan,expression}), updateKey(part,key,controls), updatePartNotes(part,controls), allNotesOff(part?), getPart(part), and dispose(). Per-key controls and part-wide defaults include expressive note controls and owned release tails. Held keys outrank pedal-only keys. Mono legato preserves the original gate/envelope/onset velocity/key scaling only within ±48 semitones of the original onset; beyond that it retriggers at the actual pitch. Steals prune keys without automatic readmission; interruption/reset clears helper state even with preserve mode. The performance helper itself is device-agnostic; optional createMidiAdapter/requestMidiAccess add bounded non-SysEx MIDI input. See MIDI and expressive performance, host integration, and example 08 (checkout-only).\n\n## Offline PCM with Node.js\n\nIn the npm-installed opm-app root, save the following as render.mjs:\n\njs\nimport { Synth, prepareVoice } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst sampleRate = 44100;\nconst synth = new Synth(sampleRate);\nconst patch = prepareVoice(brass);\nconst id = synth.noteOn(patch, 60);\nconst left = new Float32Array(sampleRate);\nconst right = new Float32Array(sampleRate);\nsynth.render(left, right, 0, 22050);\nsynth.updateNote(id, { pitch: 7, glide: 0.05, expression: 0.8 });\nsynth.noteOff(id);\nsynth.render(left, right, 22050, 22050);\nconsole.log(left.some(sample => sample !== 0), synth.errorCount);\n\n\nRun node render.mjs in opm-app; expected output is true 0. These are in-memory PCM buffers; center pan is dual mono, other positions are stereo. Checkout imports are ./dist/core/index.js and ./dist/voices/brass.js. Use .mjs or \"type\": \"module\"; there is no CommonJS entry.\n\nnew Synth(sampleRate, maxVoices = 8, {mixGain, tuning, stealing, quality = 'standard'} = {}) requires finite 8000–192000 Hz (not necessarily integer) and integer 1–32 logical voices, default 8, with at most eight independent stealing fades. noteOn(voiceOrPrepared,note,id?,{velocity=1,pan=0,voicePriority=0}={}) accepts fractional MIDI 0–127 with strict velocity/pan bounds, integer priority 0–127 and unique positive safe IDs; it throws VoiceAdmissionError if all active voices have higher priority. Prepare immutable snapshots once or validate raw voices each call; pooled state is bounded. updateNote/noteOff return booleans. allNotesOff releases naturally; panic clears all tails; setters preserve phase/envelopes. onVoiceEnded(id,reason) reports exactly one terminal stolen, ended, error or cancelled; callback replacements start next frame and recursive render rejects.\n\nrender(left,right,offset=0,length=nativeLeftLength-offset) requires native Float32Arrays and nonnegative safe-integer ranges fitting both actual buffers. Shadowed length/fill properties do not enlarge work; proxies/forgeries and numeric coercion reject. Inspect cumulative currentFrame/errorCount; split rendering at events and retain enough release frames.\n\nFor a simpler single-note render, save this standalone script as opm-app/render-note.mjs:\n\njs\nimport { renderNote } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst { samples, sampleRate, diagnostics } = renderNote({\n voice: brass,\n note: 60,\n duration: 0.1,\n sampleRate: 44100\n});\nconsole.log(samples.length, sampleRate, diagnostics.errors);\n\n\nRun node render-note.mjs in opm-app. For a checkout use ./dist/core/index.js and ./dist/voices/brass.js. renderNote({ voice, note=60, duration=0.5, velocity=1, pan=0, sampleRate=44100 }) returns {samples,left,right,sampleRate,diagnostics:{errors}}, where samples === left. It requires a complete schema voice. Finite voice numeric fields clamp (invalid versions/algorithm/feedback still reject); finite note/duration/velocity/pan clamp to 0–127/0–30/0–1/−1–1. Sample rate is an integer 8000–96000 Hz.\n\nrenderNote also accepts strict mixGain, tuning, stealing and quality options; whole-score, chunked and Worker rendering forward quality too. 'eco' uses 2× oversampling with a 35-tap half-band FIR, default 'standard' uses 4× with a 15 → 47-tap half-band cascade, and 'high' uses 8× with an 11 → 15 → 47-tap cascade. Profiles promise neither alias-free output nor CPU deadlines; measure on the target host. See acoustic quality.\n\nIt uses Synth's exact envelope, LFO, filter, velocity, pan, and saturation path. Length is ceil((duration + longestEffectiveRelease + 0.01) sampleRate) including rate-key-scaled release, with a 4,000,000-frame cap. Pan gains before saturation are sqrt(2)cos/sin((pan+1)*pi/4); saturation may change perceived balance.\n\nOther core exports: normalizeVoice returns a strict, detached voice copy; envelopeAt(time, gate, adsr) returns dB-derived amplitude with time/gate in seconds; ALGORITHMS is eight immutable routing graphs; HEADROOM = 0.7, OVERSAMPLE = 4, MAXRENDERSAMPLES = 4000000; and sampleRateValue validates integer 8000–96000 Hz. dist/voices/schema.js exports validateVoice(voice) (complete, frozen validation), parseVoiceBank(input), finite-number clamping helper bounded(value, min, max, label = 'number'), LIMITS, MAXBANKBYTES, and MAXBANKVOICES.\n\n## Voice format and banks\n\nA standalone voice.json (save in opm-app if using it as a local asset) can contain this complete schema voice. See the voiceSchema export in voice.schema.js and the examples export in examples.js. Four operators appear in signal order:\n\njson\n{\n \"version\": 6,\n \"name\": \"simple\",\n \"algorithm\": 7,\n \"feedback\": 0,\n \"modIndex\": 4,\n \"lfo\": { \"rate\": 0, \"amDepth\": 0, \"pmDepth\": 0, \"waveform\": \"sine\", \"amTargets\": [1, 1, 1, 1], \"pmTargets\": [1, 1, 1, 1] },\n \"ops\": [\n { \"ratio\": 1, \"level\": 0.8, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.6, \"r\": 0.2 }, \"keyScale\": { \"breakpoint\": 60, \"leftDbPerOctave\": 0, \"rightDbPerOctave\": 6 } },\n { \"ratio\": 2, \"level\": 0.4, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 3, \"level\": 0.3, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 4, \"level\": 0.2, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } }\n ]\n}\n\n\n| Field | Bounds and meaning |\n| --- | --- |\n| version, name | Current canonical 7; explicit 1–6 retain their original strict shapes. v1 excludes key scaling; v1/2 exclude velocity sensitivity; v1–3 exclude waveform; v1–4 exclude v5 fields; v1–5 exclude v6 targets; v1–6 exclude operator waveform/noiseRate. Names: 1–64 ASCII letters/digits/underscores/hyphens. |\n| algorithm, feedback | Integers 0–7. |\n| modIndex | Phase-modulation strength 0–16. |\n| ops | Exactly four operators, each with ratio 0.125–32, level 0–1, detune −1200–1200 cents, and complete adsr: a, d, r each 0–10 seconds; sustain s 0–1. |\n| lfo | rate 0–20 Hz, amDepth 0–1, pmDepth 0–1200 cents; waveform sine (default), triangle, saw or square. |\n| ops[i].keyScale | Optional: breakpoint is a MIDI integer 0–127; left/right slopes are 0–24 dB/octave. Omission is flat. |\n| ops[i].velocitySensitivity | Optional 0–48 dB attenuation at velocity 0; omission is 0. Operator gain is multiplied by 10 * (-sensitivity (1 - velocity) / 20). Final output still multiplies by velocity. Set sensitivity on modulators to vary brightness as well as volume. |\n| ops[i].frequency, ops[i].rateKeyScale | Optional v5 fixed 1–20000 Hz frequency (ratio remains required), rate scale 0–4; ADSR times multiply by 2 * (-scale (note - 60) / 12), capped at 10 seconds. |\n| ops[i].waveform, ops[i].noiseRate | Optional v7 oscillator shape sine (default), half, abs, quarter, alternating, camel, square, saw or noise; noiseRate is the 20–20000 Hz held-noise clock (default 8000), ignored unless the waveform is noise. A noise operator is not pitched. |\n| pitchEnvelope | Optional v5 {a,d,r,initial,peak,sustain,final}: 0–10-second times, −4800..4800-cent levels; continuous release from current pitch. |\n| lfo.delay, lfo.sync, lfo.phase | Optional v5 0–10 seconds, 'note'/'global', 0–1 turns; defaults 0/note/0. Delay gates depth, not phase progression. |\n| lfo.amTargets, lfo.pmTargets | Optional v6 four-element weights 0–1; boolean inputs normalize to numbers. Omission targets all four operators with weight 1. |\n\nSingle voices may omit version/name/modIndex/lfo; defaults are current shape, index 4 and off/sine. Current output and bundled presets canonicalize to 7; legacy versions cannot carry newer fields (including v6 targets in explicit v5 and waveform fields in explicit v6). Four complete operators remain required. Strict normalization rejects bounds violations; all paths reject wrong types/accessors/unknown fields/sparse arrays and detach inputs. Key scaling uses the original note; pan belongs to the note. See expressive voices.\n\nA complete bank JSON file must contain an array of 1–128 complete voices with unique names (wrap the standalone object above in [...]); it is not a single top-level voice object. parseVoiceBank(jsonStringOrArray) from opm.js/voices/schema.js for Node, or the served ./opm/voices/schema.js for browsers, returns a Map of names to frozen validated voices. JSON strings are capped at 256 KiB UTF-8; arrays are capped by voice count. Bank parsing and validateVoice clamp finite out-of-range numeric fields to schema limits, unlike strict single-voice normalization; invalid version/algorithm/feedback values, nonfinite values, wrong types, and unknown fields still fail.\n\nFor a working bank loader, replace the module \u003cscript> in opm-app/public/index.html from the browser quick start, keeping its #play button and #status output. The earlier copy command places the bank module at public/opm/voices/examples.js:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { parseVoiceBank } from './opm/voices/schema.js';\n import { examples } from './opm/voices/examples.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n play.textContent = 'Play bank brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const bank = parseVoiceBank(examples);\n for (const [name, voice] of bank) opm.loadVoice(name, voice);\n opm.playNote({ voice: 'brass', note: 60, duration: 0.7 });\n status.textContent = 'Playing bank brass note';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\nServe public/ as above and click the button. This example imports a trusted bundled module. External JSON banks remain supported by parseVoiceBank; for untrusted downloads, your host/application must restrict source, content type, and response size before reading the body. The engine does not provide a network download API.\n\n## WAV export and DX7 import\n\nIn opm-app, save export.mjs and run node export.mjs:\n\njs\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { renderNote, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\nimport { importDX7, describeDX7 } from 'opm.js/voices/dx7.js';\n\nconst audio = renderNote({ voice: brass, duration: 0.7, velocity: 0.8, pan: -0.5 });\nawait writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));\n// Optional: node export.mjs /path/to/your/voice.syx\nif (process.argv[2]) {\n const bytes = new Uint8Array(await readFile(process.argv[2]));\n const voices = importDX7(bytes);\n console.log(describeDX7(bytes));\n const imported = renderNote({ voice: voices[0], duration: 1 });\n await writeFile('imported.wav', encodeWav({ left: imported.left, right: imported.right, sampleRate: imported.sampleRate }));\n}\n\n\nCheckout imports use ./dist/core/index.js, ./dist/voices/brass.js, and ./dist/voices/dx7.js. encodeWav({left,right?,sampleRate,format?}) returns little-endian RIFF bytes in 'pcm16' (default), 'pcm24' or 'float32' format. Omit right for mono. Float32 arrays must contain finite samples in −1–1; out-of-range amplitudes reject rather than clip, including float32 output. Stereo lengths must match. Rate is integer 8000–192000 Hz; full-buffer budget is 4,000,000 frames. Pass only these own-data fields: a whole renderNote result has extra fields and is rejected. It only encodes; your application saves/downloads bytes.\n\n### Incremental WAV and browser Worker export\n\ncreateWavEncoder({sampleRate,channels:1|2,format?,totalFrames}) emits header() once, accepts 1–65536 frames per encode({left,right?}), then requires exact frame accounting in finalize() (which emits any RIFF padding). It retains no PCM/output chunks. RIFF32 must remain below 4 GiB, with at most 24 hours plus release budget; there is no full-buffer 4-million-frame limit.\n\nFor a small runnable encoder example in opm-app, save as incremental.mjs and run node incremental.mjs:\n\njs\nimport { open } from 'node:fs/promises';\nimport { createWavEncoder } from 'opm.js/core';\nconst encoder = createWavEncoder({\n sampleRate: 44100, channels: 1, format: 'pcm24', totalFrames: 44100\n});\nconst file = await open('silence.wav', 'w');\ntry {\n await file.writeFile(encoder.header());\n for (let remaining = 44100; remaining > 0;) {\n const frames = Math.min(4096, remaining);\n await file.writeFile(encoder.encode({ left: new Float32Array(frames) }));\n remaining -= frames;\n }\n await file.writeFile(encoder.finalize());\n} finally { await file.close(); }\n\n\nFor long browser files, root export renderSequenceInWorker(events,{...chunkOptions,quality,format,sink,signal,onProgress,startupTimeoutMs?,phaseDiagnostics?,onPhase?,workerUrl?}) returns {capacity,format,bytesWritten,diagnostics}. The sink implements write(bytes) and optionally close()/abort(reason); writes are awaited, with at most one encoded chunk unacknowledged. Progress reports {frames,totalFrames,bytesWritten,errors}. signal must be a genuine AbortSignal, including a cross-realm signal; an already-aborted signal cancels before rendering. Cancellation terminates/rejects immediately with AbortError without awaiting a hung write, close or abort; the sink must invalidate outstanding writes and own rollback. The helper is browser-only; core/offline helpers work in Node.\n\nstartupTimeoutMs is optional (no deadline by default), integer 1–2147483647 ms, and applies only until the module Worker reports ready; expiration rejects with TimeoutError, not a whole-render or sink deadline. onPhase synchronously observes initializing, rendering, writing, closing and terminal completed/cancelled/failed states with host-local elapsed timing and counters; an observer exception fails the render and invalidates the sink. phaseDiagnostics:true adds bounded phase totals to successful diagnostics.phases. These timings are not DSP benchmarks, network traces or realtime guarantees. See Worker diagnostics for interpretation and deadline/cancellation recipes.\n\nThe default static module is dist/worker/render.js; optional URLs must be same-origin HTTP(S), never blob/data/eval. Copy the entire distribution and serve JavaScript MIME types under suitable worker-src 'self' and script-src 'self' CSP. See streaming sequences for the complete cancellable file-sink recipe. Example 08 (checkout-only) offers format preview and long Worker export: long files require File System Access, without an aggregate-Blob fallback; short previews use an explicit 8 MiB memory budget.\n\ntext\npublic/opm/\n api/ browser API and helper companions\n core/ DSP, sequence, transport and WAV companions\n worklet/ realtime processor\n worker/ render.js and offline Worker companions\n voices/ schemas, presets and importers\n\n\n\n### DX7 conversion\n\nimportDX7(Uint8Array) accepts exactly one framed/checksummed 163-byte single voice or 4104-byte 32-voice bank, rejecting raw/concatenated/invalid seven-bit messages. v1.8 returns normalized version 6 voices (v1.6 historically returned v5). describeDX7 returns {name,sourceAlgorithm,algorithm,selectedOperators,droppedOperators,warnings}; original algorithms are 1–32, converted 0–7, with DX7 operator numbering 1–6.\n\nThis remains a lossy musical heuristic for six-to-four-operator conversion, not DX7 synthesis/emulation. Fixed-Hz operators are retained. Velocity/rate scaling, reduced pitch-envelope stages and LFO delay/sync are heuristic; unsupported descending saw/sample-and-hold waveforms produce substitutions and warnings. Reduced routing, oscillator sync, transpose, per-operator AM and envelope details remain lossy. Inspect descriptions and expressive conversion limits before auditioning. Browser hosts register converted patches with loadVoice; bound uploaded/downloaded bytes before buffering. See voice quality for original recipes and numerical host trims.\n\n## Quality and release acceptance\n\nMaintainer commands run in the checkout, after npm ci:\n\nAll authored programs use strict TypeScript. npm run compile emits ignored .dev/; npm test compiles/selects behavioral tests, unlike bare node --test. Current npm run build derives declarations, preserves module paths, builds thirteen demo entry scripts and then generates static HTML documentation from Markdown and emitted declarations. Historical v1.8.1 has twelve scripts. npm run typecheck covers authored programs/public consumers. Every distribution JS has a matching map/declaration; consumers need neither TypeScript nor build tools.\n\nsh\nnpm run build\nnpm test\nnpm run typecheck\nnpm run security -- --package-smoke\nnpx --no-install playwright install --with-deps chromium\nnpm run browser-smoke -- chromium\nnpm run benchmark\nnpm run sound-quality\nnpm run voice-quality\nnpm run browser-stress -- chromium\n\n\nUse firefox/webkit instead to install/exercise those browsers. CI configures Node 22/24/26 and all three browser engines; configuration alone does not prove successful runs. Observed versions/results belong in CHANGELOG. Browser acceptance observes real AudioWorklet signal, finite output, lifecycle/release, and deployment paths, not just fake contexts. Roadmap completion requires a tested feature, working demo sound/export, and README coverage.\n\nHeadless Linux Firefox also needs a running native audio server; browser library installation alone is insufficient. CI installs pulseaudio, runs pulseaudio --start --exit-idle-time=-1, loads pactl load-module module-null-sink sinkname=opmci, selects pactl set-default-sink opm_ci, and checks pactl info before smoke. A null sink discards speaker output but still runs the native audio clock and real worklet graph. Smoke logs identify the pending stage and context state if initialization/resume stalls.\n\nBenchmark output reports warmed 128-frame block p95/p99/worst and misses of 128/sampleRate seconds. OPMBENCHBLOCKS, OPMBENCHWARMUP, and OPMBENCHSAMPLERATE control workloads; optional p99/worst budget ratios enforce host-specific thresholds. Local defaults are report-only; CI configures OPMBENCHP99BUDGET_RATIO=1 at 48 kHz (2.667 ms), worst/GC stalls report-only. This is not a universal real-time guarantee. Spectral acceptance compares a 30 kHz oscillator folded to 18 kHz with an equal-level 1760 Hz control at 48 kHz (at least 30 dB attenuation), not arbitrary band-limited FM proof.\n\nSound-quality tools cover multiple sample rates, algorithms, feedback, pitches, velocities, and polyphony, with finite/deterministic output and controlled spectral/level measurements. Preset reports expose raw peak/RMS dBFS and recommended bounded host trim, not automatic loudness rewriting. Browser stress observes native messaging, signal, lifecycle, queue drainage, and elapsed main-thread gaps under contention; these are not measurements of AudioWorklet CPU, GC, or guaranteed glitch-free output. Controlled alias attenuation is not a claim that arbitrary FM is alias-free. Consult voice quality and publishing acceptance for scope and release prerequisites.\n\nIndependent acceptance includes Bessel PM, contractive/full-level feedback bounds, nested four-op chain/branched/multicarrier references and two 120-second streamed LFO/glide scenarios. The 4× polyphase half-band FIR decimator is flat to 0.40 output Fs and rejects products folding below it by at least 81 dB, at the cost of a constant 13.25-frame linear-phase delay. See acoustic mathematics and limitations. These are not all-patch, perceptual, physical-device or hardware-fidelity proof.\n\n## Compressed deployment\n\nEvery .js in dist/ has a corresponding .js.map and generated .d.ts, including demo scripts. Engine modules preserve source paths; no hashed chunks are generated. Demo declarations contain export {}; because those modules export no API. The build produces no JSON assets or compressed sidecars. Maps embed original TypeScript; publishing them exposes that source for debugging. Configure compression at the web host if needed, retaining ordinary .js import URLs. Compressed responses need the matching Content-Encoding (br or gzip), Vary: Accept-Encoding, and a JavaScript MIME type. Uncompressed responses must not be marked compressed. Python's basic HTTP server serves the ordinary modules directly. Deploy the entire matching dist/ module tree as one version. See the README's optimized-distribution notes.\n\n## Troubleshooting\n\n| Symptom | Action |\n| --- | --- |\n| file://, insecure-origin, or AudioWorklet unavailable | Serve over HTTPS or http://localhost, in a browser supporting ES modules and AudioWorklet. |\n| Bare opm.js browser import cannot resolve | Import a served ./opm/... URL after copying assets; a bare specifier needs a separately configured import map or bundler. |\n| Worklet/module 404, HTML returned instead of JS, or MIME error | Copy the whole dist/ tree; preserve relative paths and exclude assets from SPA HTML rewrites. Check the JavaScript MIME type. |\n| Autoplay blocked or playback before start | Invoke and await opm.start() inside a user click before playNote(). |\n| Invalid pitch, voice name, duration, or unknown voice | Check the browser API argument bounds and register custom names before playback. |\n| Scheduled notes disappear | Watch lifecycle rejections/steals and diagnostics; respect selected logical polyphony (1–32, default 8), voicePriority admission and bounded IDs/events. |\n| Node makes no speaker sound/file | PCM must be played/saved explicitly; use the WAV export recipe above. |\n| Processor failure | Handle onEvent errors and failed diagnostics; close/restart rather than bypassing validation or substituting a fallback engine. |\n| npm registry E404 | Install the GitHub release tarball or checkout-packed local tarball, not a presumed registry release. |\n| Bank parse fails | Supply an array of complete, uniquely named schema voices within the bank size/count limits. |\n| Compressed bytes look like gibberish | Set the correct encoding and MIME response headers, or serve the ordinary .js modules without compression. |\n","href":"./usage.en.html"},{"title":"OPM.js usage guide (English) · OPM.js usage guide (English)","text":"OPM.js usage guide (English) OPM.js usage guide (English)","href":"./usage.en.html#opmjs-usage-guide-english"},{"title":"OPM.js usage guide (English) · New in v1.11 (current checkout, unreleased)","text":"OPM.js usage guide (English) New in v1.11 (current checkout, unreleased)","href":"./usage.en.html#new-in-v111-current-checkout-unreleased"},{"title":"OPM.js usage guide (English) · New in v1.10","text":"OPM.js usage guide (English) New in v1.10","href":"./usage.en.html#new-in-v110"},{"title":"OPM.js usage guide (English) · Existing APIs and examples (introduced in v1.9)","text":"OPM.js usage guide (English) Existing APIs and examples (introduced in v1.9)","href":"./usage.en.html#existing-apis-and-examples-introduced-in-v19"},{"title":"OPM.js usage guide (English) · Acquire and install","text":"OPM.js usage guide (English) Acquire and install","href":"./usage.en.html#acquire-and-install"},{"title":"OPM.js usage guide (English) · Browser quick start","text":"OPM.js usage guide (English) Browser quick start","href":"./usage.en.html#browser-quick-start"},{"title":"OPM.js usage guide (English) · Browser API and lifecycle","text":"OPM.js usage guide (English) Browser API and lifecycle","href":"./usage.en.html#browser-api-and-lifecycle"},{"title":"OPM.js usage guide (English) · Bounded lookahead melody","text":"OPM.js usage guide (English) Bounded lookahead melody","href":"./usage.en.html#bounded-lookahead-melody"},{"title":"OPM.js usage guide (English) · Shared context, output routing, and events","text":"OPM.js usage guide (English) Shared context, output routing, and events","href":"./usage.en.html#shared-context-output-routing-and-events"},{"title":"OPM.js usage guide (English) · Shared scores, tuning and safe recovery","text":"OPM.js usage guide (English) Shared scores, tuning and safe recovery","href":"./usage.en.html#shared-scores-tuning-and-safe-recovery"},{"title":"OPM.js usage guide (English) · Bounded long scores","text":"OPM.js usage guide (English) Bounded long scores","href":"./usage.en.html#bounded-long-scores"},{"title":"OPM.js usage guide (English) · Beat transport and performance policy","text":"OPM.js usage guide (English) Beat transport and performance policy","href":"./usage.en.html#beat-transport-and-performance-policy"},{"title":"OPM.js usage guide (English) · Offline PCM with Node.js","text":"OPM.js usage guide (English) Offline PCM with Node.js","href":"./usage.en.html#offline-pcm-with-nodejs"},{"title":"OPM.js usage guide (English) · Voice format and banks","text":"OPM.js usage guide (English) Voice format and banks","href":"./usage.en.html#voice-format-and-banks"},{"title":"OPM.js usage guide (English) · WAV export and DX7 import","text":"OPM.js usage guide (English) WAV export and DX7 import","href":"./usage.en.html#wav-export-and-dx7-import"},{"title":"OPM.js usage guide (English) · Incremental WAV and browser Worker export","text":"OPM.js usage guide (English) Incremental WAV and browser Worker export","href":"./usage.en.html#incremental-wav-and-browser-worker-export"},{"title":"OPM.js usage guide (English) · DX7 conversion","text":"OPM.js usage guide (English) DX7 conversion","href":"./usage.en.html#dx7-conversion"},{"title":"OPM.js usage guide (English) · Quality and release acceptance","text":"OPM.js usage guide (English) Quality and release acceptance","href":"./usage.en.html#quality-and-release-acceptance"},{"title":"OPM.js usage guide (English) · Compressed deployment","text":"OPM.js usage guide (English) Compressed deployment","href":"./usage.en.html#compressed-deployment"},{"title":"OPM.js usage guide (English) · Troubleshooting","text":"OPM.js usage guide (English) Troubleshooting","href":"./usage.en.html#troubleshooting"},{"title":"OPM.js 使用說明(繁體中文)","text":"OPM.js 使用說明(繁體中文) # OPM.js 使用說明(繁體中文)\n\nEnglish · 專案 README · 原始碼儲存庫\n\nGitHub Release v1.10(套件 1.10.0)加入可攜 Arrangement 專案、選用的 MIDI 檔案表情保留與損失摘要、依工作負載限定的容量候選,以及有界本機 MIDI 擷取/驗收活動準備。保留 canonical v6 音色、表情控制、32 聲部與 Worker 匯出。GitHub 發佈不表示 npm 已上架。Node.js 需 22+;README 是公開契約。\n\n音色格式版本與套件版本獨立:已發佈的 v1.10 與歷史 v1.7 使用 canonical v6;目前 v1.11 checkout 使用 canonical v7(新增每個運算子的 waveform 與 noiseRate);不可變的歷史 v1.6 Release 使用 v5。明確指定的舊音色版本 1–6 仍保留各自原有輸入格式,並正規化為 v7。\n\n本次 Release 範圍:可攜 Arrangement 專案、選用的 MIDI 檔案表情保留與損失摘要、實測容量候選及跨功能契約總覽包含於 v1.10。實體手機/MIDI 與人工聽感仍須真正的原始觀察才能完成驗收,不能用自動播放冒充通過。\n\n- 安裝與範例頁面\n- 瀏覽器靜態部署\n- 瀏覽器 API 與自訂音色\n- Node.js 離線渲染\n- 音色格式與銀行載入\n- 壓縮部署\n- 疑難排解\n\n## v1.11 新功能(目前 checkout,尚未發佈)\n\n- 音色 v7 波形與雜訊。每個運算子可指定 waveform(sine、half、abs、quarter、alternating、camel、square、saw、noise)與 noiseRate(20–20000 Hz)。既有正弦音色輸出不變;square/saw(polyBLEP)與 half(polyBLAMP)有邊緣抗混疊修正,但與 noise 一樣不保證無混疊。內建音色庫新增原創的 noise-snare、noise-hihat、explosion。詳見表情音色參考。\n- 選用立體聲效果。createStereoEffects/applyEffects(離線)與 createEffects(專用 AudioWorklet insert)提供 chorus 與 reverb;不會自動接線。詳見音訊匯流排。\n- MIDI 程式對照與 RPN。programVoices/drumVoices 將 Program Change 與 GM 鼓音符對應到具名 FM 音色(需主動啟用;gmProgramVoices/gmDrumVoices 為起手對照表);保留模式的檔案匯入與即時轉接器也會解碼 RPN 0 的 pitch-bend 靈敏度。詳見 MIDI 檔案與 MIDI 演奏。\n- .opm 匯入。opm.js/voices/opm.js 的 importOPM/describeOPM 近似轉換 VOPM/MXDRV 系列文字音色;並非暫存器層級模擬。\n\n## v1.10 新功能\n\nHTTPS 線上範例需點擊播放按鈕才能啟動音訊。以下功能包含於 v1.10;歷史 Release 保持不變。GitHub Release 不表示 npm 上架或網站部署。使用端可直接安裝 Release tarball;原始碼貢獻者執行 npm ci、npm run build。\n\n- parseArrangementProject(source: string | object)/serializeArrangementProject(project)由 root/core 匯出,讀寫獨立的 ArrangementProject version 1。嚴格 own-data 驗證回傳獨立且深度 frozen 的定義,儲存 voices/settings、tempo/meter、layers、sections 與必要的 initialSection。解析/載入不啟動音訊;上限 8 MiB、1–16 layers、32 sections、合計 65,536 events、128 voices/256 KiB 正規化音色 JSON。重播從 beat 0 開始,不儲存即時游標、待執行轉換、淡入淡出或 DSP 狀態。詳見可攜 Arrangement 專案。\n- importMidiFile(Uint8Array, options?)回傳 { events, tempoMap, timeSignature, warnings, lossSummary };exportMidiFile(events, options?)回傳 Uint8Array,由 root、core 與 opm.js/midi-file匯出。僅支援 format 0/1 PPQN。預設 controls: 'omit'維持 note-only 匯入,匯出仍拒絕 control events/非零 pan;選用 controls: 'preserve'可依明確 pitchBendRange 與 channel ownership 政策保留 pitch、expression、pan、modulation。lossSummary區分遺漏、近似與已保留的來源控制訊息計數;sustain 仍是音符長度近似。匯出仍拒絕 ramp/glide、gain/音色控制、非整數音高、非零 priority、channel ownership 歧義與 linear tempo ramp。無 program-to-FM 轉換或 SysEx 傳送。詳見MIDI 檔案。\n- 實測容量候選僅適用於所測工作負載、瀏覽器與機器,不是所有裝置的通用容量保證。\n- 有界本機 MIDI 擷取與實體裝置/聽感活動準備協助收集真正原始觀察,不代表已完成實體或人工驗收。\n- 跨功能契約總覽整理重播、表情控制、渲染與 host 邊界。\n\n### 既有 API 與範例(於 v1.9 引入)\n\n- parseScoreProject(source: string | object)/serializeScoreProject(project)讀寫 canonical version 1 專案:拍點 events、正規化 tempoMap/timeSignature、具名完整 voices 與 synthesis settings。預設為 120 BPM、4/4、44100 Hz、standard、8 聲部、mixGain 1、A4 440 Hz、oldest;嚴格 own-data 驗證且回傳 frozen snapshot。上限 8 MiB、65,536 events、128 voices/256 KiB 音色 JSON。compileBeatSequence(events, { tempoMap?, bpm?, voices? })轉為驗證過的秒制 SequenceEvent[],供離線/Worker 渲染;Transport 直接使用拍點 events,先將具名音色載入 OPM。詳見樂譜專案。\n- TransportOptions.startupLead為 0–10 秒,預設 min(0.05, horizon / 2):啟動/恢復/重建以未來時刻為原點,提前量期間維持音樂位置;0 可取消提前量。這修正歷史 beat-0 冷啟動缺陷,但不保證主執行緒停頓下的 deadline。\n- Arrangement layer gain為 0–1(預設 1),switchSection/setLayer可指定 fade 0–10 秒;setLayerGain(name, gain, { quantize?, fade? })回傳提交拍點。共用 layer 不重新起音,gain 獨立乘上 expression,涵蓋自行擁有的 release tails 與新音符;layer score 的 gain controls 保留給 Arrangement,音樂力度請用 expression。詳見自適應音樂。\n- MidiAdapterOptions.controllerMap至多 128 項,指定 controller、field、明確 min/max/ramp、選用 reset;operator tuple field 另需零起算 operator。重複 CC/target 與 CC64/120/121/123 拒絕。Scalar field 包含獨立 NoteControls.gain(0–1、預設 1);CC121 恢復建立 adapter 時擷取的有效預設或 explicit reset,fixed-Hz 的 reset:null 回到 ratio mode。performance.getPartControls(part)回傳 frozen 有效聲部控制,不含個別 key override。詳見MIDI 表情控制。\n- 套件建置 13 個 demo scripts,包含兩首原創歌曲的播放/停止、專案存取與 WAV 匯出;demo HTML 仍僅供 checkout/網站使用。\n\n建置在 dist 輸出後產生 HTML 文件與完整編譯器產生的 API 參考,本機新建套件將它們放在 doc/;歷史套件不變。可直接開啟 doc/index.html,或連同頂層 Markdown/法律檔案部署整個 doc/。本機搜尋支援鍵盤,不需伺服器、遠端搜尋服務或 runtime dependencies;Markdown 仍是唯一文件來源。\n\n## 安裝與範例頁面\n\n需要 Node.js 22+ 與 npm。不需 checkout 或建置工具鏈即可在新專案安裝 GitHub Release 的附加套件:\n\nsh\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install https://github.com/JS-PACKAGE/OPM.js/releases/download/v1.10/opm.js-1.10.0.tgz\n\n\nv1.10 Release tarball 包含這份同步文件;原 v1.8 標籤與套件維持不變。若要從原始碼自行封裝,以下另一路徑先在 OPM.js 儲存庫根目錄 執行,npm pack 會自行執行建置(prepack),不必預先重複執行 npm run build:\n\nsh\nnpm ci\nnpm pack\n\n\n此 checkout 的套件版本產生 opm.js-1.10.0.tgz。以下從儲存庫根目錄建立同層的新專案 opm-app,假設 checkout 目錄名為 OPM.js;若名稱不同,請調整安裝指令中的路徑。若已用上面的 Release 路徑安裝,請跳過這段:\n\nsh\ncd ..\nmkdir opm-app\ncd opm-app\nnpm init -y\nnpm install ../OPM.js/opm.js-1.10.0.tgz\n\n\n既有專案只需在該目錄以 Release URL 或 tarball 的實際路徑執行 npm install,不需 npm init。此流程不假設已上架 npm registry;使用端不需建置依賴。封裝含 dist 的最小化 .js 模組、內嵌 TypeScript 原始碼的 .js.map、.d.ts、13 個 demo entry scripts、文件與法律檔案,不含獨立 TypeScript 原始碼檔案、tests、開發 scripts、13 個 HTML 範例頁面或 Vite 範例;維護指令應在 checkout 執行。\n\n要直接試用儲存庫的範例,請在 OPM.js 儲存庫根目錄 啟動伺服器:\n\nsh\npython3 -m http.server 8000\n\n\n開啟 http://localhost:8000/index.html 查看 13 個範例:基本音符、前瞻排程、即時調變、共用 context、WAV、進階控制台、試聽、共用樂譜/中斷恢復、自適應音樂、表情樂器/MIDI、音色設計、音訊匯流排與原創歌曲。HTML 頁面僅供 checkout 使用,頁面訊息為英文。Python 3 只提供本機 HTTP;既有 dist 不需安裝建置,原始碼/helper 改動後才執行 npm ci、npm run build。需 ES modules/AudioWorklet 與 HTTPS 或 localhost,不能用 file://。\n\n## 瀏覽器靜態部署\n\n在已安裝套件的 opm-app 根目錄複製完整發佈樹與授權檔:\n\nsh\nmkdir -p public/opm\ncp -R node_modules/opm.js/dist/. public/opm/\ncp node_modules/opm.js/LICENSE public/opm/LICENSE\n\n\n將下列頁面存為 opm-app/public/index.html:\n\nhtml\n\u003c!doctype html>\n\u003chtml lang=\"zh-Hant\">\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>OPM.js example\u003c/title>\n\u003cbutton id=\"play\" type=\"button\">Play brass chord\u003c/button>\n\u003coutput id=\"status\" aria-live=\"polite\">Ready\u003c/output>\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });\n status.textContent = 'Playing brass chord';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\u003c/html>\n\n\n在 opm-app 根目錄執行:\n\nsh\npython3 -m http.server 8000 --directory public\n\n\n開啟 http://localhost:8000/,點擊按鈕播放。上述複製指令適用 POSIX shell;其他系統可手動複製相同檔案。若完全不使用 npm,亦可將 checkout 的完整 dist/ 內容複製到網站根目錄的 site/opm/,將 checkout 的 LICENSE 複製為 site/opm/LICENSE,把同一 HTML 存成 site/index.html,並以網站根目錄 site/ 提供服務。務必連同 api/、core/、worklet/、voices/ 一起保留,不能單獨重新命名或移動 worklet 及其相依模組。引擎保留來源模組路徑,不再產生 hashed chunks。純 HTML 無 import map 時不能直接解析 import 'opm.js';即便使用 bundler,也不能假定它會複製 worklet URL 的模組相依樹。上述靜態複製與站內相對 URL 不依賴 bundler。\n\nWorker 匯出需要完整同版本樹,包括 companions、maps 與 declarations:\n\ntext\npublic/opm/\n├── api/\n├── core/\n├── worklet/\n├── worker/render.js\n└── voices/\n\n\nWorker 為同源 HTTP(S) 靜態 module,不使用 blob/data/eval;正式 CSP 的 worker-src/script-src 應允許 'self',資產須有正確 JavaScript MIME。不要把 worker 請求改寫為 SPA HTML。\n\n使用 Vite 的獨立、子路徑部署範例見 examples/vite/README.md(僅供 checkout):它安裝 tarball 並複製完整 dist 與 LICENSE,不依賴 npm registry 是否已上架。正式站點須提供正確 JavaScript MIME、真正的資產 404,且不得把 worklet 請求改寫成 SPA HTML;設定 CSP 時應使用外部 module script 與適當來源政策,不要為了範例的行內 script 放寬正式站點防護。部署安全細節見 SECURITY.md。\n\n已安裝套件也提供 opm-assets,可取代手動複製。從 opm-app 根目錄使用尚不存在的目的目錄:\n\nsh\nnpx --no-install opm-assets copy public/opm-1.10.0\nnpx --no-install opm-assets check https://your-host.example/opm-1.10.0/\n\n\n第二行請換成實際部署 URL;本機可用 loopback HTTP。若採此目錄,頁面匯入路徑也須改為 ./opm-1.10.0/api/index.js。copy 原子複製完整 dist/LICENSE、逐檔雜湊並建立 manifest,絕不覆寫既有目錄;check 比對實際回應的狀態、JavaScript MIME、nosniff 與 SHA-256,拒絕 redirect/SPA fallback,但不驗證頁面 CSP 或啟動音訊。一般 Python HTTP server 不提供 nosniff,不符合預設 check;正式主機須設定此標頭。完整契約見宿主整合。\n\n## 瀏覽器 API 與自訂音色\n\n| API | 用法與限制 |\n| --- | --- |\n| new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain=1, tuning={}, stealing='oldest', interruption='cancel', quality='standard', maxVoices=8 }={}) | 整數 sampleRate 8000–96000 Hz;maxVoices 為整數 1–32,預設 8。quality/maxVoices 建構後不可變,close/重啟仍保留。借用 context 不會被關閉/暫停;省略 destination 接到其 destination,null 不自動接線。workletUrl 可改變同源完整 worklet 模組樹的位置,但不放寬安全來源、CSP 或 MIME 限制。 |\n| await opm.start()/await opm.resume() | 在使用者互動中啟動/恢復並等待,再播放。並行啟動會合併;既有 context 被瀏覽器暫停後可恢復。 |\n| opm.connect(destination)/opm.disconnect(destination?) | 對已啟動的輸出接線/斷線,回傳 opm;目的節點必須來自相同 context。省略斷線目的地會移除所有輸出連線。 |\n| opm.loadVoice(name, voice) | 啟動前即可驗證、複製並註冊音色;名稱為 1–64 個 ASCII 英文字母、數字、_ 或 -,registry 最多 128 筆。覆蓋只影響未來音符。 |\n| opm.voices | ReadonlyMap 的防禦性快照;音色深度凍結。更換音色須用 loadVoice,修改取得的 Map 不會改變引擎。 |\n| opm.replaceVoiceBank(bank)/opm.exportVoiceBank()/opm.removeVoice(name) | 完整驗證後原子替換;bank 是 JSON 字串或完整音色陣列,最多 128 筆/JSON 256 KiB。直接傳 [] 清空,但 JSON \"[]\" 依 parser 契約拒絕。export 回傳具名不可變音色的 canonical JSON(空銀行為 []),上限 256 KiB。remove 回傳 boolean,不終止正在播放/排隊的快照。 |\n| opm.playNote({ voice='brass', note, time=0, at, duration=null, velocity=1, pan=0, voicePriority=0, late='start' }) | 有限 MIDI 小數 0–127;相對 time 0–60 秒或互斥的絕對 at(最多提前 60 秒)。duration 為 (0,60] 或 null 持續音;力度 0–1、聲像 −1–1、voicePriority 為整數 0–127。回傳正安全整數音符 ID,不代表接收成功。 |\n| opm.stop(id,{at,cancelControls}={}) | 一般 stop 取消待開始音符/開始 release,保留自然尾音的 automation。cancelControls:true 立即取消此 ID 的排隊 onset/control/off 並 release;cancelControls 欄位即使為 false,也不能與 at 共用。未知、終止或已 release 目標會回報命令拒絕。 |\n| opm.updateNote(id,controls,{at}={}) | 回傳命令 ID;待開始控制保留到 onset,release 中仍可更新。無效、inactive 或 queue-full 命令會回報拒絕。 |\n| opm.allNotesOff()/opm.panic() | 回傳命令 ID,不受已滿排程佇列阻擋。前者取消待處理事件並自然 release;後者立即移除所有尾音並發出 reset,保留路由/快取。 |\n| opm.setMixGain(gain)/opm.setTuning(tuning) | 須先啟動,回傳命令 ID;設定保留到 node 重建,不重設 phase/包絡。 |\n| await opm.getDiagnostics() | 回傳 {type:'diagnostics',requestId,activeVoices,pendingEvents,errors,rejectedNotes};context 須在 running,最多 64 個未完成請求。暫停/關閉/processor 失敗會拒絕待回覆請求,請先 resume 再重試。 |\n| await opm.close() | 與初始化依序處理,斷開 node,只關閉自行建立的 context。再次啟動會重建自有 context;借用的 context 仍可由宿主使用。 |\n| opm.subscribe(listener) | 獨立事件訂閱,回傳冪等的取消訂閱函式;各回呼例外隔離。 |\n| await opm.waitForCommand(commandId,{timeout,signal}={}) | 僅等待 admission:預設 5000 ms、整數 1–60000 ms,可由 AbortSignal 取消;最多 64 pending waits/128 receipts。拒絕拋出 CommandRejectedError;accepted 不是執行完成。 |\n| await opm.dispose() | 終止式清理,不能重新啟動;close 仍可重新 start。 |\n\nonEvent 除 note/diagnostics/error,新增 {type:'command',command,commandId?,id?,state,reason?,frame,time}、context 狀態與全域 reset。命令 state 為 accepted/rejected;accepted 只表示接收或立即套用,不保證未來一定執行。以 commandId 對應拒絕;frame/time 為音訊邊界而非回呼送達時間。reset 原因為 close/failure/panic/interruption,收到時清除自有 gate 紀錄;close/failure 不保證每音符終止回覆。回呼例外隔離。\n\n遲到音符預設 late:'start':在可用的第一個影格開始,數值 duration 從實際開始保留完整 gate;late:'drop' 則以 reason:'late' 拒絕,包含訊息處理延遲造成的遲到。同影格依序處理 stop、onset、controls;onset 前收到的控制保留到開始。\n\ncontrols 為非空 own-data 物件:pitch −48..48 半音;glide 0..10 秒且須搭配 pitch;expression 0..1;pan −1..1;modulation 0..2(AM 上限 1、PM 1200 音分);operatorLevels 是四個 0..2 的原音色 level 倍率。新增 feedback 0..7、lfoRate 0..20 Hz、amDepth 0..1、pmDepth 0..1200 音分;operatorRatios 為四個 0.125..32,operatorFrequencies 為四個 1..20000 Hz 或 null(恢復 ratio 模式),operatorADSR 為四個完整 {a,d,s,r}。ramp 0..10 秒獨立平滑指定的 scalar/level/ratio/frequency 欄位,省略/零立即生效;glide 獨立以半音線性滑動。phase/回授歷史保留,但 ADSR 從當前 dB 重新錨定:held 音重啟 attack,released 音開始新縮放 release(最多 10 秒),零 release 立即進入 filter drain。固定 Hz 仍跟隨 pitch 控制,忽略 tuning table 的移調。未知欄位、存取器、非有限/超界值拒絕。詳細控制語意見表情音色,宿主時鐘/生命週期見宿主整合。\n\nv1.9 新增 gain(0–1、預設 1),獨立乘上 expression,依 ramp 平滑且不重啟 phase/包絡;Arrangement layer fade 使用這個獨立倍率。\n\n最多 maxVoices 個邏輯聲部(1–32,預設 8),另有至多八個獨立約 5 ms 搶音淡出;增加 maxVoices 不增加淡出數。滿額時只能搶走 voicePriority 不高於新音符的聲部,先選最低優先權,再依 stealing 政策決定。全部較高時回報音符拒絕 reason:'priority',不搶走既有音符。預設 oldest;release-first 優先最早 release,quietest 按 carrier 包絡 × 力度 × expression 比較,同分取最早,不以瞬間波形判定。事件/ID 各限 256,未來定時音符佔兩筆,控制/stop 也佔額度;終止會回收過期事件。預備音色採 128 槽 content-key LRU,重新驗證後替換/重用 ID,既有排程與聲部保留原快照。無 setLFO;速率/深度可用 updateNote,波形與目標屬於音色。\n\n要自訂 LFO、排程與提早停止,保留上方 public/index.html 的 #play 按鈕與 #status 輸出,將其原本的 \u003cscript type=\"module\">…\u003c/script> 完整替換為以下區塊;從 opm-app 根目錄照前節提供 public/:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { brass } from './opm/voices/brass.js';\n\n const opm = new OPM();\n const voice = structuredClone(brass);\n voice.lfo.rate = 5;\n voice.lfo.amDepth = 0.2;\n voice.ops[0].level = 0.6;\n opm.loadVoice('my-brass', voice);\n\n const play = document.querySelector('#play');\n play.textContent = 'Play custom brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const at = opm.context.currentTime + 0.1;\n const id = opm.playNote({ voice: 'my-brass', note: 64, at, velocity: 0.7, pan: -0.5 });\n opm.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8, pan: 0.5, modulation: 1.2 }, { at: at + 0.1 });\n opm.stop(id, { at: at + 0.3 });\n status.textContent = 'Scheduled custom brass note and controls';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\n上述 onset、控制與 release 皆由音訊時鐘執行,不靠頁面計時器停止;單次僅排三筆事件,且皆在 60 秒範圍內。完成使用後可在應用程式生命週期中 await opm.close(),下次要播放時再由使用者互動呼叫 start()。\n\n### 有界前瞻排程\n\n長曲用 createLookaheadScheduler 分批排程,而非一次塞滿 queue。將同一 opm-app/public/index.html 的 module script 完整替換如下,再從 opm-app 根目錄提供 public/。點擊播放會安排有限的八個音符;主執行緒停頓時跳過已錯過的窗口,不補塞過期音符:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createLookaheadScheduler } from './opm/api/index.js';\n const status = document.querySelector('#status');\n const opm = new OPM();\n const melody = [60, 64, 67, 72, 67, 64, 62, 60];\n let origin;\n const scheduler = createLookaheadScheduler(opm, ({ from, to, maxNotes }) => {\n origin ??= from + 0.1;\n return melody.flatMap((note, index) => {\n const at = origin + index * 0.3;\n return at >= from && at \u003c to\n ? [{ note, at, duration: 0.2, velocity: 0.6, late: 'drop' }]\n : [];\n }).slice(0, maxNotes);\n }, {\n horizon: 0.2, interval: 0.025, maxNotes: 8,\n onError(error) { status.textContent = error.message; }\n });\n const play = document.querySelector('#play');\n play.textContent = 'Play finite melody';\n play.addEventListener('click', async () => {\n try {\n scheduler.stop();\n origin = undefined;\n await scheduler.start();\n status.textContent = 'Scheduling eight notes';\n } catch (error) { status.textContent = error.message; }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop melody';\n document.body.append(stop);\n stop.addEventListener('click', () => scheduler.stop());\n window.disposeOPM = async () => {\n scheduler.dispose();\n await opm.close();\n };\n\u003c/script>\n\n\n回呼同步回傳陣列,每筆必須有窗口 [from,to) 內的絕對 at 與 (0,60] 數值 duration,不能用相對 time 或持續音。onError 必填;計時器/回呼失敗會停止排程並通知它。預設 horizon 為 0.2 秒(範圍 0.02–10),interval 為 0.025 秒(0.005–1),且 horizon 必須大於 interval;maxNotes 預設 32(整數 1–128),最多追蹤 128 個尚未關 gate 的自有音符。本例每個窗口至多一音,全部音符播放後回呼回傳空陣列,計時器仍運作直到 stop/dispose。兩者只取消/釋放自己的音符,不關閉 OPM;dispose 後不可重啟。移除介面時呼叫 await window.disposeOPM()。\n\n任何 non-running context 或 reset 都會停止前瞻排程並通知 onError,須由使用者互動明確重啟。預設 interruption:'cancel' 取消中斷前 gate/automation;preserve 只保留直接音符/佇列,不自動恢復前瞻計時器。\n\n\n### 共用 context 與輸出接線\n\n若宿主已有 AudioContext,可將快速範例的 module script 替換成以下區塊。按播放後音符持續到按停止;disposeOPM() 只清除本合成器,不會關閉宿主 context:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n const context = new AudioContext();\n const gain = context.createGain();\n gain.gain.value = 0.3;\n gain.connect(context.destination);\n const status = document.querySelector('#status');\n const opm = new OPM({\n context, destination: null,\n onEvent(event) {\n if (event.type === 'reset') {\n id = undefined;\n if (event.reason === 'close' || event.reason === 'failure') connected = false;\n }\n status.textContent = event.type === 'error' ? event.error.message\n : event.type === 'note' ? ${event.id}: ${event.state}\n : event.type === 'diagnostics' ? Voices: ${event.activeVoices}\n : event.type === 'command' ? ${event.command}: ${event.state} ${event.reason ?? ''}\n : event.type === 'context' ? Context: ${event.state} : Reset: ${event.reason};\n }\n });\n const stop = document.createElement('button');\n stop.textContent = 'Stop held note';\n document.body.append(stop);\n let id;\n let connected = false;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.resume();\n if (!connected) { opm.connect(gain); connected = true; }\n if (id !== undefined) opm.stop(id);\n id = opm.playNote({ note: 60, velocity: 0.7, pan: 0.5 });\n console.log(await opm.getDiagnostics());\n } catch (error) { status.textContent = error.message; }\n });\n stop.addEventListener('click', () => { if (id !== undefined) opm.stop(id); });\n window.disposeOPM = async () => {\n await opm.close();\n connected = false;\n id = undefined;\n gain.disconnect();\n };\n\u003c/script>\n\n\n移除此應用程式的音訊介面時呼叫 await window.disposeOPM()。context/下游節點由宿主管理;不要為了停止單一合成器而暫停整個共用 context。\n\n### 共用樂譜、全域調音與安全恢復\n\nmixGain 是有限 0–1(預設 1),在 tanh 前縮放;下游 GainNode 只控制聆聽音量,無法還原已產生的飽和失真。tuning 替換完整設定:referenceHz(A4)20–20000 Hz,offsets 可省略表示零,否則為恰好 128 個 −4800..4800 的有限音分。小數 MIDI 線性插值相鄰偏移,127 使用自身端點。即時調音保留 phase/包絡與原音符 key scaling。\n\n保留 quick-start 的 play/status,將 module script 替換為:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, playSequence } from './opm/api/index.js';\n import { renderSequence } from './opm/core/index.js';\n import { brass } from './opm/voices/brass.js';\n const settings = { mixGain: 0.5, tuning: { referenceHz: 442 }, stealing: 'release-first' };\n const opm = new OPM({ ...settings, interruption: 'cancel' });\n const score = [\n { type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },\n { type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },\n ];\n let playback;\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n await opm.start();\n playback?.stop();\n opm.loadVoice('lead', brass);\n playback = playSequence(opm, score, { at: opm.context.currentTime + 0.05 });\n const audio = renderSequence(score, { ...settings, sampleRate: opm.context.sampleRate, voices: opm.voices });\n document.querySelector('#status').textContent = Rendered ${audio.left.length} frames; errors ${audio.diagnostics.errors};\n } catch (error) { document.querySelector('#status').textContent = error.message; }\n });\n window.disposeOPM = async () => { playback?.stop(); await opm.close(); };\n\u003c/script>\n\n\n樂譜使用相對秒數;note 為唯一正 ID 與有限 duration,stop/control 指向同一樂譜音符。整份驗證後才派送;限 128 notes、256 保留槽(每音符兩槽加命令)、含 gate 的 60 秒及離線 4,000,000 影格。playSequence 回傳防禦性 scoreId→noteId Map 與冪等 stop,不會對其他 caller 原子保留 worklet 容量。renderSequence 包含尾音;相同 PCM 須設定/取樣率/相對影格原點一致且無其他競爭音符。WAV 僅傳 left/right/sampleRate。\n\n以 HTTPS 在實體 iOS/Android 使用第 08 範例(僅供 checkout),依行動裝置驗收流程與支援矩陣記錄裝置/OS/瀏覽器/取樣率/政策、觀察與手動 pass/fail/unverified 判定。涵蓋鎖屏、切換 app、來電、耳機/Bluetooth、節電、長播放與主執行緒停頓。報告僅留本機;桌面、headless 與 analyser 訊號不代表實機恢復或聽感不中斷。恢復須手勢;借用 context 由宿主管理。\n\n### 有界長樂譜\n\n24 小時/65,536 個輸入事件以內,改用 opm.js/core 的 prepareLongSequence、estimateSequenceCapacity 與 renderSequenceChunks。估算會分別列出整段 buffer/單批 eligibility 與預設串流視窗密度,並不保證和其他 caller 競爭時的 admission。每塊 PCM 是借用 buffer,下一次 next() 就會覆寫;立即消費,只有需要保留時才複製。break、cancel() 或 AbortSignal 停止前進,maxFrames 可限制累計工作量;整段 PCM/WAV 預算不變。\n\njs\nimport { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';\nconst score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];\nconst options = { sampleRate: 96000, chunkFrames: 4096 };\nconsole.log(estimateSequenceCapacity(score, options));\nlet frames = 0, energy = 0;\nconst render = renderSequenceChunks(score, options);\nfor (const chunk of render) {\n for (let i = 0; i \u003c chunk.frames; i++) energy += chunk.left[i] ** 2;\n frames += chunk.frames;\n}\nconsole.log(frames, energy, render.diagnostics.errors);\n\n\n瀏覽器播放時與 OPM 一起 import streamSequence,建立 const stream = streamSequence(opm, score, { onError: console.error }),再於播放按鈕手勢內 await stream.start()。stop() 僅取消此串流的音符;dispose() 移除 listener 並永久終止此實例。control、明確 stop 與自動 release 跨視窗保留 score ID 對應。兩種政策的 interruption/reset 都停止串流;先恢復 context,再由使用者明確重啟。完整瀏覽器配方、預設值與密度限制見串流契約。\n\n### 拍點 Transport 與演奏聲部\n\ncreateTransport、createArrangement、createPerformance、createMidiAdapter、requestMidiAccess、playSequence、streamSequence、renderSequenceInWorker 與套件版本常數 VERSION 由根入口 opm.js(靜態部署為 ./opm/api/index.js)匯出。Transport 事件以 beat 取代 time,note.duration 也以四分音符拍計算。提供 start/resume、pause/stop、seek、setTempo(從目前位置起)、setTempoMap、setLoop、pump、dispose 與 state/position/running/snapshot/ids。它使用 AudioContext 時鐘,只管理自有音符。\n\nbpm 預設 120、範圍 1–1000;tempoMap 從 beat 0 開始嚴格遞增,最多 1024 點,拍點上限 86400。timeSignature 分子 1–32,分母為不超過 32 的二次冪;horizon 0.01–10 秒、interval 0.001–horizon/2、maxSlots 1–256。pause/seek/loop 重啟包絡與 phase,不是 DSP snapshot;重建當前 scalar/ratio ramp 與剩餘時間,最新 fixed-Hz/null 政策立即套用(即使 frequency ramp 未完成),最新 ADSR 在重啟 onset 重新錨定。純核心亦匯出 beatsToSeconds/secondsToBeats/beatToBarBeat/barBeatToBeat/normalizeTempoMap。\n\n下方配方保留音樂上的一拍 count-in。目前 checkout 額外使用 startupLead(預設 min(0.05,horizon/2) 秒)將時鐘原點放在未來,避免 beat 0 冷啟動時提交到已過期時刻;歷史 v1.8.1 套件沒有這項修正。late:'drop' 仍會拒絕真正晚到的起音、停止 helper 並通知 onError。Count-in/startupLead 不保證宿主停頓下仍能準時;start() 完成也不是每個音符已發聲的確認。\n\ncreatePerformance(opm,{parts:16,maxKeys:128,maxKeysPerPart:128,onError}) 提供零起算 1–16 個聲部,各自 configurePart 設定 voice、poly/mono、legato、last/high/low 按鍵選擇 priority、glide 0–10 秒、pan ±1 與 expression 0–1;兩種 key 額度皆為 1–128。先在使用者手勢中 await opm.start(),再呼叫 noteOn;回傳實體 key ID,須用 noteOff(part,key) 釋放,同音高按鍵仍獨立。另有 updatePart、sustain、allNotesOff(part?)、getPart 與 dispose。實際 held keys 優先於踏板保留;mono legato 在原 onset ±48 半音內重用 gate/包絡/原力度與 key scaling,超出時以真實音高重觸發。被偷走的 key 會移除,不自動重入;中斷/reset 即使 preserve 也清除 helper 自有狀態。可直接執行的 Transport/Performance 配方見宿主整合與串流樂譜。\n\nv1.8 的 updateKey(part,key,controls) 對單一實體按鍵套用 NoteControls;未選取的 mono key 保留控制,不改動目前 gate。updatePartNotes(part,controls) 更新整個聲部的自有音符(含 release 尾音)與未來音符預設。configurePart 的 voiceLimit 為 1–32,自有 release 尾音也佔額度,滿額 noteOn 拋出 RangeError;它不增加引擎聲部數。voicePriority 為 0–127(預設 0),與 last/high/low 的按鍵選擇 priority 不同。\n\n選用的 createMidiAdapter(performance,await requestMidiAccess(),options) 將使用者授權的 MIDI inputs 映射到聲部,支援 note、sustain、bend、wheel、volume/expression、pan、pressure 與 all-notes-off。匯入模組不會要求權限,永不要求 SysEx;disconnect/dispose 只釋放 adapter 自有按鍵。詳細配方與限制見表情演奏與 MIDI及第 10 範例(僅供 checkout)。\n\n### 速度曲線、網格與分層編曲\n\nTempoPoint 為 {beat,bpm,curve?,endBpm?};省略 curve 或 'step' 為定速段,'linear' 使 BPM 隨拍點線性變化至下一點 bpm 或指定 endBpm(僅 linear 可用;末點不可 linear)。beatsToSeconds/secondsToBeats 使用閉式對數積分與反函式。quantizeBeat、swingBeat、swingBeatEvents 從根入口 opm.js 匯入(純瀏覽器使用部署的 ./opm/api/index.js),不是 opm.js/core 匯出;後者連同音符起點與終點一起套用 swing。\n\ncreateArrangement(opm,{layers,sections,initialSection,bpm?,tempoMap?,timeSignature?}) 在共用拍點網格上循環具名 layers;switchSection/setLayer 在不早於已接收音符的第一個量化邊界提交(預設 bar,也可 beat/拍數),回傳提交拍點。共用 layer 保持連續,不重觸發;移除 layer 預設在邊界 release,preserveNotes:true 可讓它自然結束。layer 與 note 的 voicePriority 取較大值,優先權拒絕計入 snapshot.priorityDrops 而不停止編曲,其他拒絕會停止並通知 onError。至多 16 layers、32 sections、65,536 events,每層循環長度至多 256 拍;只清理自有音符。Arrangement pause/resume 不還原跨越恢復點的持續音,只從後續 onset 繼續;它和 Transport 都不是 DSP checkpoint。公式、密度上限及可執行配方見自適應音樂與第 09 範例(僅供 checkout)。\n\n保留 quick-start 的按鈕/輸出並替換 module script,可直接試用兩種 helper:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM, createTransport, createPerformance } from './opm/api/index.js';\n const opm = new OPM();\n const transport = createTransport(opm, [\n { type: 'note', id: 1, beat: 1, duration: 1, note: 60 },\n { type: 'note', id: 2, beat: 2, duration: 1, note: 64 }\n ], { bpm: 120, onError: console.error });\n const performance = createPerformance(opm, { parts: 1 });\n performance.configurePart(0, {\n voice: 'brass', mode: 'mono', legato: true, priority: 'last', glide: 0.1\n });\n document.querySelector('#play').addEventListener('click', async () => {\n try {\n transport.stop();\n await transport.start();\n const key = performance.noteOn(0, 67, { velocity: 0.7 });\n performance.sustain(0, true);\n performance.noteOff(0, key);\n document.querySelector('#status').textContent = 'Release pedal to stop held note';\n } catch (error) {\n document.querySelector('#status').textContent = error.message;\n }\n });\n const pedal = document.createElement('button');\n pedal.textContent = 'Release pedal';\n document.body.append(pedal);\n pedal.addEventListener('click', () => performance.sustain(0, false));\n window.disposeOPM = async () => {\n transport.dispose();\n performance.dispose();\n await opm.close();\n };\n\u003c/script>\n\n\n## Node.js 離線渲染\n\n在已安裝套件的 opm-app 根目錄建立 render.mjs:\n\njs\nimport { Synth, prepareVoice } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst sampleRate = 44100;\nconst synth = new Synth(sampleRate);\nconst prepared = prepareVoice(brass);\nconst id = synth.noteOn(prepared, 60);\nconst left = new Float32Array(sampleRate);\nconst right = new Float32Array(sampleRate);\nsynth.render(left, right, 0, 22050);\nsynth.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8 });\nsynth.noteOff(id);\nsynth.render(left, right, 22050, 22050);\nconsole.log(left.some(sample => sample !== 0), synth.errorCount);\n\n\n在同一目錄執行 node render.mjs,預期顯示 true 0。中央聲像為左右相同,其他位置產生立體聲;此例只產生記憶體 PCM,不會自動播放或寫檔。checkout 匯入改為 ./dist/core/index.js 與 ./dist/voices/brass.js。使用 .mjs 或 \"type\":\"module\",沒有 CommonJS 入口。\n\nnew Synth(sampleRate,maxVoices=8,{mixGain,tuning,stealing,quality='standard'}={}) 需要有限 8000–192000 Hz 與整數 1–32 聲部(預設 8);noteOn 接受有限小數 MIDI 0–127、嚴格力度/聲像、voicePriority 0–127(預設 0)及未占用正安全 ID。滿額且所有聲部優先權較高時拋出 VoiceAdmissionError,保留既有聲部。noteOff/updateNote 回傳 boolean;allNotesOff 自然 release,panic 移除所有尾音;設定 gain/tuning 不重設 phase/包絡。onVoiceEnded 每音符一次 stolen/ended/error/cancelled;lastStolenId 無搶音時為 null。\n\nprepareVoice(input) 一次嚴格驗證並建立不可變的可信任快照;不能用型別斷言或自製物件偽造。Synth.noteOn 可重用此快照,原始音色輸入則仍逐次驗證。核心預先配置 maxVoices + 9 個狀態槽(maxVoices 個邏輯聲部、八個淡出與一個入列暫存槽;預設 17 個),重用預備音色不需每音符配置 Float64 狀態陣列,但這不是整個 JavaScript/瀏覽器零配置保證。synth.updateNote(id, controls) 使用前述控制範圍,成功回傳 true,未知/已終止 ID 回傳 false。\n\nonVoiceEnded 可呼叫 noteOn/noteOff;回呼中新增的音符最早在下一影格渲染,不回頭改寫已計算的當前影格。不可在回呼中遞迴呼叫 render;引擎會拒絕。通知有固定工作上限,應保持回呼簡短。\n\nrender(left,right,offset=0,length=nativeLeftLength-offset) 需原生 Float32Array,非負安全整數範圍容納於兩個實際緩衝區。遮蔽 length/fill 不能增加工作,Proxy/偽造及需數值 coercion 的輸入拒絕。自行分段渲染/保留 release;currentFrame 為累計影格,errorCount 記錄數值失敗。\n\n若只需要單音的輔助渲染函式,在 opm-app 根目錄另存 render-note.mjs:\n\njs\nimport { renderNote } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\n\nconst { samples, sampleRate, diagnostics } = renderNote({\n voice: brass,\n note: 60,\n duration: 0.1,\n sampleRate: 44100\n});\nconsole.log(samples.length, sampleRate, diagnostics.errors);\n\n\n執行 node render-note.mjs;checkout 改用 ./dist/... 匯入。renderNote({voice,note=60,duration=0.5,velocity=1,pan=0,sampleRate=44100}) 回傳 {samples,left,right,sampleRate,diagnostics:{errors}},samples === left。要求完整 Schema 音色;有限的音色數值會限制到範圍,但無效版本/algorithm/feedback 仍拒絕。有限 note/duration/velocity/pan 分別限制至 0–127/0–30 秒/0–1/−1–1;note 可為小數,sampleRate 必須是整數 8000–96000。\n\nrenderNote 也接受嚴格 mixGain/tuning/stealing/quality,與 renderSequence、分塊渲染及 Worker 共用。quality 為 eco(2×/35-tap 半帶 FIR)、standard(預設 4×/15 → 47-tap 半帶 FIR 串接)或 high(8×/11 → 15 → 47-tap);不是無混疊或即時期限保證。模式取捨見聲學品質,CPU 須在目標宿主測量。\n\n此函式與 Synth 使用相同包絡、LFO、濾波、力度、聲像及飽和路徑。長度為 ceil((duration+最長有效release+0.01)sampleRate),包含 note-dependent rate scaling,上限 4,000,000 影格。飽和前聲像增益為 sqrt(2)cos/sin((pan+1)*pi/4);飽和後不保證感知音量恆定。\n\n其他 opm.js/core 公開項目:normalizeVoice(嚴格驗證並產生獨立複本)、prepareVoice、NoteControls/PreparedVoice 型別、envelopeAt(time, gate, adsr)(以秒為單位的時間/gate 計算 dB 包絡對應振幅)、不可變的八種訊號圖 ALGORITHMS、HEADROOM = 0.7、OVERSAMPLE = 4、MAXRENDERSAMPLES = 4000000,以及整數 8000–96000 取樣率驗證器 sampleRateValue。\n\n## 音色格式與銀行載入\n\n參考 voice.schema.js 的 voiceSchema 與 examples.js 的 examples 匯出。下列為單一音色 JSON 物件,四個 ops 順序是訊號演算法的運算子順序;若要作為銀行,必須用 [ 和 ] 包成陣列:\n\njson\n{\n \"version\": 6,\n \"name\": \"simple\",\n \"algorithm\": 7,\n \"feedback\": 0,\n \"modIndex\": 4,\n \"lfo\": { \"rate\": 0, \"amDepth\": 0, \"pmDepth\": 0, \"waveform\": \"sine\", \"amTargets\": [1, 1, 1, 1], \"pmTargets\": [1, 1, 1, 1] },\n \"ops\": [\n { \"ratio\": 1, \"level\": 0.8, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.6, \"r\": 0.2 }, \"keyScale\": { \"breakpoint\": 60, \"leftDbPerOctave\": 0, \"rightDbPerOctave\": 6 } },\n { \"ratio\": 2, \"level\": 0.4, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 3, \"level\": 0.3, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } },\n { \"ratio\": 4, \"level\": 0.2, \"detune\": 0, \"adsr\": { \"a\": 0.01, \"d\": 0.2, \"s\": 0.5, \"r\": 0.2 } }\n ]\n}\n\n\n| 欄位 | 範圍 |\n| --- | --- |\n| version/name | 目前 canonical 版本 7;explicit 1–6 僅接受各自原有欄位。v1 無 keyScale,v1/2 無 velocitySensitivity,v1–3 無 waveform,v1–4 不接受 v5 欄位;v5 不接受 v6 targets;v1–6 不接受運算子 waveform/noiseRate。名稱為 1–64 ASCII 字母/數字/底線/連字號。 |\n| algorithm/feedback/modIndex | 前兩者是 0–7 的整數;調變強度 modIndex 是 0–16。 |\n| 四個 ops 各項 | 必須提供 ratio 0.125–32、level 0–1、detune -1200–1200 音分,以及 adsr 的 a、d、s、r;a/d/r 是 0–10 秒,s 是 0–1。 |\n| lfo | rate 0–20 Hz、amDepth 0–1、pmDepth 0–1200 音分;waveform 為 sine(預設)、triangle、saw 或 square。 |\n| ops[i].keyScale | 可省略表示平坦;breakpoint 為 MIDI 整數 0–127,leftDbPerOctave/rightDbPerOctave 為 0–24 dB/八度。 |\n| ops[i].velocitySensitivity | 從版本 3 起可選,0–48 dB,省略為 0;定義 velocity=0 時的運算子衰減。 |\n| ops[i].frequency/ops[i].rateKeyScale | v5 可選固定頻率 1–20000 Hz(ratio 仍必填)、速率鍵位縮放 0–4;ADSR 時間乘 2 * (-scale(note-60)/12) 並限制為最多 10 秒。 |\n| ops[i].waveform/ops[i].noiseRate | v7 可選的振盪波形:sine(預設)、half、abs、quarter、alternating、camel、square、saw、noise;noiseRate 為 20–20000 Hz 的保持式雜訊時脈(預設 8000),僅在波形為 noise 時有意義。雜訊運算子沒有音高。 |\n| pitchEnvelope | v5 可選 {a,d,r,initial,peak,sustain,final};時間 0–10 秒、音高 −4800..4800 音分;release 從目前音高連續開始。 |\n| lfo.delay/lfo.sync/lfo.phase | v5 可選 0–10 秒、note/global、0–1 圈,預設 0/note/0;delay 門控深度,不停止 phase 時鐘。 |\n| lfo.amTargets/lfo.pmTargets | v6 可選各四個 0–1 權重;boolean 輸入正規化為數字。省略為 [1,1,1,1],依運算子順序套用。 |\n\n單一音色可省略 version/name/lfo/modIndex,仍需 algorithm/feedback 與四個完整運算子。預設目前格式、modIndex 4、LFO off/sine,輸出統一版本 7;舊版 1–6 嚴格保留原始形狀,不得挾帶後來欄位。內建音色與 DX7 匯出也採 canonical v7。所有路徑拒絕錯誤型別、未知欄位、存取器及稀疏陣列,嚴格路徑另拒絕超界數字。鍵位縮放依原音符;pan 屬於音符。完整組合語意見表情音色參考。\n\nparseVoiceBank() 由 opm.js/voices/schema.js(瀏覽器靜態樹中為 ./opm/voices/schema.js)匯出,可接受 JSON 字串或音色陣列,要求 1–128 筆完整音色且名稱不重複,回傳 Map\u003cname, voice>,其音色為已驗證且凍結的完整物件。字串輸入最多 256 KiB(UTF-8);陣列輸入也有數量限制。和 validateVoice()、renderNote() 相同,此路徑將有限但超界的數值限制在範圍內,與嚴格的單音色 normalizeVoice() 不同;不合法的 version/algorithm/feedback、非有限數值、錯誤型別、未知欄位仍會拒絕。schema.js 也提供 validateVoice(voice)、有限數值邊界函式 bounded(value, min, max, label = 'number')、LIMITS、MAXBANKBYTES 與 MAXBANKVOICES。\n\n要載入已部署的可信任 examples.js 模組,保留先前 opm-app/public/index.html 的 #play 與 #status,將其原本 module script 完整替換為:\n\nhtml\n\u003cscript type=\"module\">\n import { OPM } from './opm/api/index.js';\n import { parseVoiceBank } from './opm/voices/schema.js';\n import { examples } from './opm/voices/examples.js';\n\n const opm = new OPM();\n const play = document.querySelector('#play');\n play.textContent = 'Play bank brass note';\n const status = document.querySelector('#status');\n play.addEventListener('click', async () => {\n play.disabled = true;\n try {\n await opm.start();\n const bank = parseVoiceBank(examples);\n for (const [name, voice] of bank) opm.loadVoice(name, voice);\n opm.playNote({ voice: 'brass', note: 60, duration: 0.7 });\n status.textContent = 'Playing bank brass note';\n } catch (error) {\n status.textContent = error.message;\n } finally {\n play.disabled = false;\n }\n });\n\u003c/script>\n\n\n仍在 opm-app 根目錄以 python3 -m http.server 8000 --directory public 提供頁面。此例匯入套件所附的可信任模組;parseVoiceBank 仍支援外部 JSON 銀行。若接收不可信任的遠端下載,應由應用程式先限制來源、Content-Type 與下載大小,再讀取回應內容;合成引擎沒有內建網路下載 API。\n\n## WAV 匯出與 DX7 匯入\n\n在 opm-app 建立 export.mjs,執行 node export.mjs 會寫出立體聲 WAV:\n\njs\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { renderNote, encodeWav } from 'opm.js/core';\nimport { brass } from 'opm.js/voices/brass.js';\nimport { importDX7, describeDX7 } from 'opm.js/voices/dx7.js';\nconst audio = renderNote({ voice: brass, duration: 0.7, velocity: 0.8, pan: -0.5 });\nawait writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));\n// 有檔案時:node export.mjs /實際路徑/voice.syx\nif (process.argv[2]) {\n const bytes = new Uint8Array(await readFile(process.argv[2]));\n const voices = importDX7(bytes);\n console.log(describeDX7(bytes));\n const imported = renderNote({ voice: voices[0], duration: 1 });\n await writeFile('imported.wav', encodeWav({ left: imported.left, right: imported.right, sampleRate: imported.sampleRate }));\n}\n\n\ncheckout 匯入改為 ./dist/core/index.js、./dist/voices/brass.js 與 ./dist/voices/dx7.js。encodeWav({left,right?,sampleRate,format='pcm16'}) 回傳小端序 RIFF 位元組,format 可為 pcm16/pcm24/float32;省略 right 為單聲道。所有格式的 Float32Array 樣本須有限且在 −1–1,超界拒絕而非削波;立體聲長度須相同。取樣率整數 8000–192000,上限 4,000,000 影格。只傳這些自有資料欄位:整個 renderNote 結果有額外欄位,會被拒絕。編碼不會自動寫檔;短預覽可使用有明確記憶體預算的 Blob,長檔案須直接寫 sink。\n\n增量 createWavEncoder({sampleRate,channels:1|2,format,totalFrames}) 由 opm.js/core 匯出:先寫一次 header(),每次 encode({left,right?}) 消費 1–65536 影格並回傳有界位元組,最後寫 finalize()(精確影格數與 RIFF padding)。它不保留完整 PCM/輸出,不受完整 buffer 的四百萬影格限制;仍受 RIFF32 大小與 24 小時加 release 預算限制。\n\n瀏覽器長檔案用 renderSequenceInWorker(score,{sampleRate,chunkFrames,quality,format,sink,signal,onProgress}),sink 提供 write(bytes) 與可選 close/abort,write 可回傳 Promise 施加 backpressure;最多一塊編碼資料未確認。progress 提供 frames/totalFrames/bytesWritten/errors,結果為 capacity/format/bytesWritten/diagnostics。AbortSignal 取消立即終止 Worker 並拒絕,不等待卡住的 write/abort;sink 負責回滾/失效化未完成寫入。此 helper 僅限瀏覽器,純核心/Node 離線工具仍可用。可指定同源靜態 workerUrl,預設為樹內 worker/render.js。增量 WAV 契約與可執行的 Worker file sink/取消配方見宿主整合,長 PCM 消費配方見串流樂譜。\n\nWorker 在模組載入/解析/求值後送出 {type:'ready',protocol:1},宿主收到有效 ready 才傳送樂譜。startupTimeoutMs 是選用整數 1–2,147,483,647 ms 的模組啟動 watchdog,僅從 Worker 建立量到 ready,逾時以 TimeoutError 拒絕並終止 Worker/abort sink,不限制渲染或慢速寫入。phaseDiagnostics:true 在成功結果的 diagnostics.phases 保留 initializing/rendering/writing/closing 與總時間;onPhase 同步通知轉換及 completed/cancelled/failed,回呼拋錯會使工作失敗。這些是宿主 wall-clock 時間,並非 DSP benchmark。\n\n沒有內建整體期限或自動重試;可用 signal:AbortSignal.timeout(300000) 對整份工作設五分鐘政策。取消不等待卡住的 write/abort;sink 仍須失效化已寫入的部分輸出。完整診斷、deadline 與回滾配方見Worker 診斷。\n\n第 08 範例(僅供 checkout) 提供真正 Transport/Performance、控制/品質/銀行/WAV 格式預覽與長檔案 Worker sink/取消。長檔案僅用 File System Access,不聚合成 Blob;短預覽有明確 8 MiB 記憶體預算。\n\nimportDX7(Uint8Array) 只接受單個框架/checksum 正確的 163-byte 單音色或 4104-byte/32 音色銀行;拒絕原始、串接或非 7-bit payload,輸出完整版本 6 音色。describeDX7 回傳 name/sourceAlgorithm/algorithm/selectedOperators/droppedOperators/warnings;原 algorithm 1–32、轉換後 0–7,operator 編號依 DX7 的 1–6。\n\n這仍是六運算子轉四運算子的有損音樂啟發式轉換,不是 DX7 合成/模擬。固定 Hz 運算子會保留;力度/速率鍵位縮放、減少階段的 pitch envelope 與 LFO delay/sync 採近似。下降 saw/sample-and-hold 波形以其他波形替代並附 warning;路由、oscillator sync、transpose 與包絡細節仍有損。轉換器將所選運算子的 AM sensitivity 簡化成共用深度,不保留逐運算子 AM,也不映射至選擇性 v6 amTargets。先看 descriptions、表情音色與轉換限制及音色品質再試聽;不可信任檔案/下載仍須先限制大小。\n\n## 品質與發佈驗收\n\n在 checkout 執行維護指令,不是在已安裝的套件中:\n\n所有程式皆使用 strict TypeScript;compile 輸出忽略追蹤的 .dev,npm test 編譯並選擇行為測試,不用裸 node --test。目前 build 由實作產生宣告、保留引擎路徑並建置 13 個 demo entry scripts,再由 Markdown 與輸出宣告產生靜態 HTML 文件;歷史 v1.8.1 為 12 個 scripts。每個 JS 配對 map/型別,typecheck 包含 source/tools/tests/demo/公開型別;使用端仍只需 JS,不需建置工具鏈。\n\nsh\nnpm ci\nnpm run build\nnpm test\nnpm run typecheck\nnpm run security -- --package-smoke\nnpx --no-install playwright install --with-deps chromium\nnpm run browser-smoke -- chromium\nnpm run benchmark\n\n\n可改用 firefox/webkit 安裝與測試其他引擎。CI 設定 Node 22/24/26 與三種瀏覽器,但設定本身不代表執行成功;實測版本與結果以 CHANGELOG 為準。瀏覽器驗收觀察真正 AudioWorklet 訊號、有限輸出、生命週期/release 與發佈路徑。發佈前置條件見發佈指南;本儲存庫沒有 npm publish workflow,本文件也不表示已設定 registry 授權或已執行 npm publish。\n\nHeadless Linux Firefox 還需要運作中的原生音訊服務,只安裝瀏覽器函式庫並不足夠。CI 安裝 pulseaudio,執行 pulseaudio --start --exit-idle-time=-1、pactl load-module module-null-sink sinkname=opmci、pactl set-default-sink opm_ci,並以 pactl info 確認就緒後才執行 smoke。Null sink 僅丟棄喇叭輸出,仍保留原生音訊時鐘與真正 worklet graph;若初始化/resume 卡住,log 會列出等待階段及 context 狀態。\n\n效能報告列出暖機後 128 影格區塊 p95/p99/最慢值與超過 128/sampleRate 秒的次數;OPMBENCHBLOCKS、OPMBENCHWARMUP、OPMBENCHSAMPLERATE 調整負載,p99/worst budget ratio 可設主機相關門檻。本機預設只報告;CI 設定 48 kHz 的 OPMBENCHP99BUDGET_RATIO=1(2.667 ms),最慢值/GC 停頓僅報告,非所有平台即時保證。頻譜驗收比較 48 kHz 下折疊到 18 kHz 的 30 kHz 振盪器與同強度 1760 Hz 控制,要求至少 30 dB 衰減,不能宣稱所有 FM 無混疊。\n\n另可在 checkout 執行 npm run sound-quality、npm run voice-quality、npm run browser-stress -- chromium 與 npm run vite-smoke -- chromium。品質矩陣涵蓋多取樣率、演算法、回授、音高、力度與多音;音色工具提供低/中/高音域與力度的 peak/RMS dBFS、建議宿主 trim 及 DX7 限制報告,並不自動重寫音色音量。詳細方法見音色品質指南。已觀察到的矩陣通過與瀏覽器有限訊號,只證明所測條件;主執行緒事件間隔不等於 worklet CPU/GC 時間,也不能證明所有平台零爆音、任意 FM 無混疊或全部音色等響度。\n\n獨立驗收含 Bessel PM、收斂/全強度回授能量界限、獨立四運算子串接/分支/多 carrier 參考,以及兩段 120 秒 LFO/glide 串流。4× polyphase 半帶 FIR decimator 的 passband 平坦至輸出取樣率 0.40 倍,折返到該頻率以下的成分至少衰減 81 dB,代價是固定 13.25 frame 的線性相位延遲;詳見聲學參考數學與限制。這些不是任意音色、聽感、實體裝置或硬體保真保證。\n\n## 壓縮部署\n\ndist/ 每個最小化 .js 都有對應 .js.map 與 TypeScript 自動產生的 .d.ts,包含 demo scripts。引擎保留來源模組路徑,不產生 hashed chunks;demo 無公開匯出,其宣告如實為 export {};。建置不產生 JSON 資產或壓縮側檔。Source maps 映射回並內嵌原始 TypeScript;發佈 maps 即公開這些原始碼供除錯。需要壓縮時由網站主機處理,程式仍匯入原本的 .js URL。伺服器送出壓縮內容時,須提供對應 Content-Encoding、Vary: Accept-Encoding 與 JavaScript MIME 類型;未壓縮回應不可標示為已壓縮。基本的 Python HTTP 伺服器可直接提供一般模組。整套 dist 須維持同一建置版本;詳見 README 的最佳化發佈。\n\n## 疑難排解\n\n| 現象 | 修正方式 |\n| --- | --- |\n| file://、非安全來源或 AudioWorklet 不可用 | 使用支援 AudioWorklet 的現代瀏覽器,由 HTTPS 或 localhost 提供整個頁面與檔案。 |\n| 瀏覽器無法解析 opm.js 裸套件名稱 | 使用複製的 ./opm/api/index.js 相對 URL;如自行設定 import map,也要確保 worklet 的完整模組樹可讀取。 |\n| worklet/模組 404,或拿到 HTML、MIME 類型錯誤 | 重新複製完整 dist 目錄並確認站點路徑;不要讓 SPA rewrite 將資產請求改送 index.html。 |\n| 尚未啟動就播放、瀏覽器自動播放被擋 | 在點擊事件內 await opm.start() 後才呼叫 playNote()。 |\n| 音高、名稱、延遲、時長或未知音色出錯 | 依瀏覽器 API檢查範圍,先註冊自訂音色。 |\n| 多音符遺失 | 檢查事件的 rejected/stolen 與診斷;注意 maxVoices 1–32(預設 8)、voicePriority 拒絕、聲部自有 voiceLimit 及有界事件/ID,分批排程。 |\n| Node 無聲、找不到 WAV | PCM 不會自動播放/寫檔;依上方 WAV 範例編碼並儲存。 |\n| Processor 失敗 | 處理 onEvent error 與拒絕的診斷請求,關閉/重啟;不要以不安全 fallback 掩蓋錯誤。 |\n| npm 回報 E404 | 不假設 registry 有此套件;依安裝步驟安裝 GitHub Release tarball 或 checkout 封裝的本機 tarball。 |\n| 音色銀行格式錯誤 | 必須是完整音色組成的陣列,並符合名稱、欄位、數量與大小限制;單一 JSON 物件須包入 [...]。 |\n| 壓縮回應顯示亂碼 | 設定正確的 Content-Encoding 與 MIME 類型,或直接提供未壓縮的 .js 模組。 |\n","href":"./usage.zh-TW.html"},{"title":"OPM.js 使用說明(繁體中文) · OPM.js 使用說明(繁體中文)","text":"OPM.js 使用說明(繁體中文) OPM.js 使用說明(繁體中文)","href":"./usage.zh-TW.html#opmjs-使用說明繁體中文"},{"title":"OPM.js 使用說明(繁體中文) · v1.11 新功能(目前 checkout,尚未發佈)","text":"OPM.js 使用說明(繁體中文) v1.11 新功能(目前 checkout,尚未發佈)","href":"./usage.zh-TW.html#v111-新功能目前-checkout尚未發佈"},{"title":"OPM.js 使用說明(繁體中文) · v1.10 新功能","text":"OPM.js 使用說明(繁體中文) v1.10 新功能","href":"./usage.zh-TW.html#v110-新功能"},{"title":"OPM.js 使用說明(繁體中文) · 既有 API 與範例(於 v1.9 引入)","text":"OPM.js 使用說明(繁體中文) 既有 API 與範例(於 v1.9 引入)","href":"./usage.zh-TW.html#既有-api-與範例於-v19-引入"},{"title":"OPM.js 使用說明(繁體中文) · 安裝與範例頁面","text":"OPM.js 使用說明(繁體中文) 安裝與範例頁面","href":"./usage.zh-TW.html#安裝與範例頁面"},{"title":"OPM.js 使用說明(繁體中文) · 瀏覽器靜態部署","text":"OPM.js 使用說明(繁體中文) 瀏覽器靜態部署","href":"./usage.zh-TW.html#瀏覽器靜態部署"},{"title":"OPM.js 使用說明(繁體中文) · 瀏覽器 API 與自訂音色","text":"OPM.js 使用說明(繁體中文) 瀏覽器 API 與自訂音色","href":"./usage.zh-TW.html#瀏覽器-api-與自訂音色"},{"title":"OPM.js 使用說明(繁體中文) · 有界前瞻排程","text":"OPM.js 使用說明(繁體中文) 有界前瞻排程","href":"./usage.zh-TW.html#有界前瞻排程"},{"title":"OPM.js 使用說明(繁體中文) · 共用 context 與輸出接線","text":"OPM.js 使用說明(繁體中文) 共用 context 與輸出接線","href":"./usage.zh-TW.html#共用-context-與輸出接線"},{"title":"OPM.js 使用說明(繁體中文) · 共用樂譜、全域調音與安全恢復","text":"OPM.js 使用說明(繁體中文) 共用樂譜、全域調音與安全恢復","href":"./usage.zh-TW.html#共用樂譜全域調音與安全恢復"},{"title":"OPM.js 使用說明(繁體中文) · 有界長樂譜","text":"OPM.js 使用說明(繁體中文) 有界長樂譜","href":"./usage.zh-TW.html#有界長樂譜"},{"title":"OPM.js 使用說明(繁體中文) · 拍點 Transport 與演奏聲部","text":"OPM.js 使用說明(繁體中文) 拍點 Transport 與演奏聲部","href":"./usage.zh-TW.html#拍點-transport-與演奏聲部"},{"title":"OPM.js 使用說明(繁體中文) · 速度曲線、網格與分層編曲","text":"OPM.js 使用說明(繁體中文) 速度曲線、網格與分層編曲","href":"./usage.zh-TW.html#速度曲線網格與分層編曲"},{"title":"OPM.js 使用說明(繁體中文) · Node.js 離線渲染","text":"OPM.js 使用說明(繁體中文) Node.js 離線渲染","href":"./usage.zh-TW.html#nodejs-離線渲染"},{"title":"OPM.js 使用說明(繁體中文) · 音色格式與銀行載入","text":"OPM.js 使用說明(繁體中文) 音色格式與銀行載入","href":"./usage.zh-TW.html#音色格式與銀行載入"},{"title":"OPM.js 使用說明(繁體中文) · WAV 匯出與 DX7 匯入","text":"OPM.js 使用說明(繁體中文) WAV 匯出與 DX7 匯入","href":"./usage.zh-TW.html#wav-匯出與-dx7-匯入"},{"title":"OPM.js 使用說明(繁體中文) · 品質與發佈驗收","text":"OPM.js 使用說明(繁體中文) 品質與發佈驗收","href":"./usage.zh-TW.html#品質與發佈驗收"},{"title":"OPM.js 使用說明(繁體中文) · 壓縮部署","text":"OPM.js 使用說明(繁體中文) 壓縮部署","href":"./usage.zh-TW.html#壓縮部署"},{"title":"OPM.js 使用說明(繁體中文) · 疑難排解","text":"OPM.js 使用說明(繁體中文) 疑難排解","href":"./usage.zh-TW.html#疑難排解"},{"title":"Preset and conversion acceptance","text":"Preset and conversion acceptance # Preset and conversion acceptance\n\nThe checkout-only maintenance command npm run voice-quality produces a fresh JSON report for the twenty-one bank voices and five original synthetic DX7 fixtures, each rendered under the eco, standard and high DSP profiles (69 accepted rows in the historical 1.8.0 run). Serve the current audition page (checkout-only) through the project's HTTP demo server for seeded A/B playback, profile selection, a held ratio/feedback/ADSR control phrase, local PCM16 WAV downloads and raw/matched numerical comparisons. No human acceptance is implied.\n\n## Bank, provenance and host trim\n\nThe existing seven recipes remain in examples; their synthesis parameters are unchanged, with current v7 format labels. Legacy v1–v6 inputs retain their original restrictions and normalize to v7; v1–v5 reject selective LFO targets and v1–v6 reject operator waveforms/noise rate. See the voice v7 guide for modulation, oscillator shapes and immutable snapshots. These original four-operator recipes are not converted emulator patches, downloaded SysEx, sampled recordings or claims of acoustic/hardware fidelity:\n\n| Addition | Intended MIDI register | Timbral design |\n| --- | --- | --- |\n| wood_mallet | 48–84 | Fast high-partial decay; higher keys shorten envelopes. |\n| glass_pluck | 48–84 | Velocity-sensitive glassy transient over a decaying body. |\n| hollow_reed | 48–76 | Even-ratio modulation of odd-partial carriers; delayed vibrato. |\n| slowairpad | 48–76 | Slow detuned onset, shallow pitch envelope, global LFO phase. |\n| bronze_plate | 48–72 | Noninteger PM ratios and unequal metallic decays. |\n| membrane_tom | 36–60 | Short body with a downward pitch-envelope transient. |\n| fixedhzchime | 48–84 | Additive 317/523/829/1237 Hz partials independent of key pitch. |\n| wire_kalimba | 48–84 | Key-tracking plucked body and fixed-Hz transient modulator. |\n| noise-snare | 36–84 | Short noise burst with fixed-Hz body tones. |\n| noise-hihat | 36–84 | High hold-rate noise and a brief quiet square partial. |\n| explosion | 36–84 | Low hold-rate noise, sub body and bright transient. |\n\nAll intended base velocities are 0.25–1. Phrase articulation reaches 0.1625; the numerical safety sweep also includes that soft boundary. Register labels express intended musical use, not a promise that notes outside them are invalid. Fixed-Hz operators ignore MIDI/tuning frequency but still respond to detune, pitch controls/envelope and LFO pitch modulation. Ratios remain present in the strict schema. Rate key scaling changes envelope times; none of these fields implement chip register behavior.\n\nMachine-readable presetMetadata in src/voices/preset-metadata.ts (checkout-only) records every bank recipe's provenance kind, source, Apache-2.0 license, non-emulator status, family, intended register/velocity, purpose and host trim. defaultBrassMetadata covers the separate default brass.js recipe, which differs from bank brass. Existing repository recipes are marked repository-recipe; additions are original-recipe. Metadata is deliberately outside strict voice objects: do not merge it into a patch submitted to validation.\n\nPackage wildcard exports expose the banks and metadata without additional runtime dependencies:\n\njs\nimport { examples } from 'opm.js/voices/examples.js';\nimport { originalPresets } from 'opm.js/voices/original.js';\nimport { presetMetadata } from 'opm.js/voices/preset-metadata.js';\n\n\nExisting demo selectors import examples, so the expanded bank appears through their existing list/load path. Operator levels remain timbral parameters; no output-level compensation was embedded into operators. Metadata recommends −6 dB additional host attenuation as a conservative starting point, not a measured perceptual preference. The fresh report computes separate safety trims and pair-specific energy matching. More voices/chords require extra host headroom.\n\n## Selective-LFO recipes and usage table\n\nThree recipes added in 1.8 use the per-operator amTargets/pmTargets tuples that the 1.7 bank did not exercise. They are original four-operator parameter recipes, not converted patches or recordings:\n\n| Recipe | Selective modulation | Playing notes |\n| --- | --- | --- |\n| tide_keys | AM only on the second carrier; PM on the first modulator and, lightly, the second pair's modulator, with no vibrato on either carrier | Short chords at medium velocity. Velocity-sensitive, key-scaled modulators soften hard high notes. Leave release headroom. |\n| ember_bass | AM on the two modulators (sideband strength, not the bass floor); PM only on the upper modulator | Monophonic lines at moderate velocity. The unmodulated fundamental/sub pair stays stable. |\n| orbit_pad | Shallow AM on the lower carrier, stronger AM on the upper pair; PM on modulators only; globally synchronized LFO | Let the 0.24–0.42 s attacks develop and tails clear before re-chording; use the lowest suggested polyphony the arrangement allows. |\n\nUsage metadata for every bank voice (presetMetadata, outside the strict voice schema). Intended register and velocity describe where a recipe is meant to be played, not a validity limit. Suggested polyphony and trim for the fifteen older recipes are conservative defaults (4 voices, −6 dB), not per-voice tuning; only the three 1.8 recipes carry specific values. Trims are a host starting point, never an operator-level change.\n\n| Voice | Family | MIDI | Velocity | Voices | Trim dB | Source |\n| --- | --- | --- | --- | --- | --- | --- |\n| bell | bell | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| brass | brass | 48–76 | 0.25–1 | 4 | −6 | examples.ts |\n| bass | bass | 36–60 | 0.25–1 | 4 | −6 | examples.ts |\n| electric_piano | keys | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| organ | organ | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| lead | lead | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| strings | strings | 48–84 | 0.25–1 | 4 | −6 | examples.ts |\n| wood_mallet | mallet | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| glass_pluck | pluck | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| hollow_reed | reed | 48–76 | 0.25–1 | 4 | −6 | original.ts |\n| slowairpad | pad | 48–76 | 0.25–1 | 4 | −6 | original.ts |\n| bronze_plate | metallic | 48–72 | 0.25–1 | 4 | −6 | original.ts |\n| membrane_tom | percussion | 36–60 | 0.25–1 | 4 | −6 | original.ts |\n| fixedhzchime | inharmonic | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| wire_kalimba | pluck | 48–84 | 0.25–1 | 4 | −6 | original.ts |\n| tide_keys | keys | 48–84 | 0.25–0.9 | 4 | −9 | original.ts |\n| ember_bass | bass | 36–60 | 0.35–0.9 | 1 | −9 | original.ts |\n| orbit_pad | pad | 48–76 | 0.25–0.8 | 3 | −12 | original.ts |\n| noise-snare | percussion | 36–84 | 0.25–1 | 4 | −6 | original.ts |\n| noise-hihat | percussion | 36–84 | 0.25–1 | 4 | −6 | original.ts |\n| explosion | percussion | 36–84 | 0.25–1 | 4 | −6 | original.ts |\n\nThese numbers come from authoring intent plus the numerical sweeps below. Every metadata record has listeningStatus: 'unverified': no listener has endorsed a recipe.\n\n## Profiles and live-control phrases\n\nThe report runs every source through all three profiles at 48 kHz, adds the intended-register boundaries and velocity 0.1625 (the softest phrase articulation), and renders a live-control phrase: each note is held for 0.8 s while operatorRatios ramp at 0.12 s, feedback at 0.25 s, operatorADSR at 0.4 s and ratios/feedback return at 0.55 s. A row is accepted only if the render is finite, error-free, within HEADROOM and non-silent. This proves the control paths stay stable in the offline renderer; it is not a statement about how they sound.\n\n## Numerical measurement contract\n\nReports render centered stereo at 48 kHz with a 0.8-second gate plus note-scaled release/filter tail. The nine reference cells are MIDI 48/60/84 at velocities 0.25/0.6/1, plus each recipe's intended lower/middle/upper register. Output includes sample peak, whole-render RMS, gate RMS, crest factor, finite status, DSP errors and per-cell acceptance. RMS uses mean stereo-channel energy; it is unweighted, not LUFS and not perceptual equal loudness.\n\nEvery measured cell must be finite, error-free, peak ≤ HEADROOM + 0.000001 and gate RMS > 0.000001. Report safety trims are attenuation-only, bounded −24..0 dB, with single-note targets peak ≤ −12 dBFS and gate RMS ≤ −24 dBFS. A bounded safety suggestion cannot guarantee those targets for arbitrary future patches. The command exits unsuccessfully on rejection and validates synthetic Yamaha checksums and equivalent packed/single conversion.\n\ntest/preset-quality.test.ts (checkout-only) covers finite/headroom/non-silence across every semitone of the eight additions' intended registers at 22.05 kHz and soft/medium/hard velocities. It also checks fixed-Hz partial localization/key invariance and held-gate mallet decay versus pad buildup. These are behavioral numerical regressions, not object snapshots or artistic verdicts. The 48 kHz report samples register cells; neither report nor sweep certifies every velocity, sample rate, polyphonic combination or live control transition.\n\n## Repeatable listening workflow\n\n1. Choose A/B sources, register, base velocity, single/phrase material, host gain mode and seed. Default seed is 20261002. Record all choices; filenames include seed, material, gain mode, note and velocity. Phrase offsets are [0, 2, 4, 7, −5, −12], clamped to MIDI 48–84. The unsigned 32-bit LCG chooses each velocity multiplier (0.65/0.8/1) and gate (0.35/0.5/0.8 seconds). Changing seed changes articulation, not patch parameters.\n2. Start at low device volume. Measure, then compare A followed by B. Six-note phrases use identical padded slots for both sources, including full isolated tails. This gives equal whole-phrase measurement windows. Single-note matching uses the same 0.8-second gate window. Stop immediately cancels playing and queued buffers; Dispose closes the owned context and cleans URLs/nodes.\n3. In energy-matched mode, both sources are attenuated to the quieter of their measured energy levels, the −24 dBFS target, metadata-trim ceilings and the −12 dBFS peak constraints. No source is amplified. Report the applied host trims separately from raw DSP values. The shared 0.12 master gain (−18.4 dB) follows those trims. WAVs apply these exact same gains.\n4. Repeat with dry mode: only the common 0.12 master gain, no source trim. Repeat the nine-cell single-note grid (C3/C4/C6 × soft/medium/hard), and selected phrases at those settings. Comparisons never overlap source tails. For a recipe outside its intended register, treat that cell as a stress comparison, not its intended playing range.\n5. Record attack, held body, brightness, decay, release and any manual gain preference. Repeat after swapping A/B selection if assessing ordering effects. Differences in attack/crest/spectrum can make energy-matched sources sound unequally loud; manual preference is listening evidence only when an actual listener records it. Save matched and dry WAVs with settings for another listener.\n\nThe JSON report compares every source's middle-register seeded phrase against wood_mallet, recording dry master gains, matched host trims, raw measurements, common slot length and target. This is reproducible numerical preparation for listening, not a listening result. No subjective listening acceptance, hardware comparison or fidelity result is claimed here. No audio/patches are uploaded.\n\n### Recording a human finding\n\nThe audition page has a Record a human listening finding panel. After actually listening to both selected sources, enter an anonymous listener label, exact device/browser/OS and output route, then check the explicit listening confirmation. Choose A, B, no-preference or not-assessed for each criterion; at least one must be assessed. Changing settings clears confirmation, and recording requires a fresh declaration. Add subjective gain notes separately for the selected matched or dry comparison: describe what felt loud/comfortable and actual device settings, without inventing dB, LUFS or a calibration for a volume slider.\n\nA record stores the exact package version, A/B source names and SHA-256 normalized-patch identities, profile, material, seed, register, velocity, gain mode and phrase/control revisions. New records also store rendered/context sample rates, common master gain and actual A/B source-gain factors, separately from subjective notes. These are host gain factors, not perceptual loudness measurements. Records remain in page memory (at most 50) until Export findings JSON; no audio/finding is uploaded, and numerical reports never become human criteria.\n\nThis repository ships no recorded findings. Until someone records and reviews them, every recipe's listeningStatus stays unverified.\n\n### Reviewing findings and host-trim decisions\n\nsh\nnpm run listening-evidence -- listener-one.json listener-two.json\nnpm run --silent listening-evidence -- listener-one.json listener-two.json > listening-review.json\n\n\nThe Node 22+ checkout-only CLI validates the existing opm-listening-findings-1 / opm-listening-finding-1 exports. Limits are 16 regular files, 2 MiB/file, 1–50 findings/file, bounded listener/device/output/notes, exact semantic package versions, UTC timestamps, typed selection fields, SHA-256 patch identities and all seven known criteria. Empty exports, all-unassessed entries, malformed bounds/hashes and conflicting content for the same timestamp/listener/conditions are rejected. Existing exports without explicit confirmation or applied gains remain readable with provenance warnings; unknown gains stay unknown, never filled from today's metadata.\n\nThe separate stdout review preserves filenames/finding indices, declared listener/device/output, notes and exact conditions. Groups require the same ordered patch hashes, package version, DSP profile, material, register, velocity, seed, phrase/control revisions, gain mode and recorded playback gains/sample rates. Source aliases do not substitute for hashes; swapping A/B is a different group. Matched and dry findings never merge. Duplicate copies contribute source references only. Per-criterion assessed/A/B/no-preference/not-assessed counts reflect actual entries; declared listener labels are not authenticated unique people. A successful parse is not a musical-quality pass and the command never rewrites listeningStatus, hostTrimDb or patches.\n\nHash strings are declared identities: the current export does not embed patch snapshots, so the reviewer cannot recompute them or authenticate the listener. Correlate them with retained normalized patches or the exact versioned source when reviewing a trim proposal; a valid 64-digit hash alone is not proof of provenance.\n\nFor a host-trim decision, inspect exact matched and dry groups and original gain-comfort notes for the same hashes/profiles/registers/velocities. Keep energy matching and safety trims separate from perceptual judgments, document the intended register and polyphony/headroom, and retain current starting trims until a maintainer can justify a change under stated conditions. Preference for A alone does not establish a numeric trim. If testing a proposed trim, record the actual applied host gain and new findings, not an inferred device-volume unit; rerun numerical safety verification after an approved metadata change. Review artifacts support a human decision, never auto-green artistic acceptance or hardware fidelity.\n\nUse the audition's reproducible isolated notes, seeded phrases and live-control phrases for controlled comparisons, then listen in the original short songs Harbor at First Light and Lanterns on the Stair (checkout-only showcase) for arrangement context. Save the exact score project and route/profile/gain notes; short-song observations are supplemental and must not be mislabeled as the audition's single/phrase/control material. A maintainer may graduate a recipe's listening metadata only after real listeners supply reviewed originals tied to exact patch hashes and conditions, with the scope and artifact reference documented. No human findings or physical captures were supplied here; the existing unverified metadata is intentionally unchanged.\n\n### All-eighteen current-version campaign\n\nRun against the matching v1.10 checkout (package 1.10.0) and retain its exact revision alongside originals. Build and serve this source checkout for the checkout-only audition page and evidence commands. The eighteen rows in the usage table are the campaign checklist: no recipe inherits acceptance from a related family, a converted fixture, or another profile. Keep listeningStatus: 'unverified' and existing trims until genuine original findings have been reviewed; neither numerical safety nor a completed form is artistic endorsement.\n\nFor each recipe, repeat the following under eco, standard and high, keeping anonymous listener, exact device/OS/browser, route/device volume, seed 20261002 and reference source fixed. Use woodmallet as B, except use bell when A is woodmallet; record ordered patch hashes, not just names. A/B identity or reversed order does not inherit another comparison's result.\n\n1. Select Single note, recipe A/reference B, register C3/C4/C6 and each velocity 0.25/0.6/1: nine individual cells. Run each in energy-matched, then dry, listening to both sources and tails. Record a separate finding for each exact selected cell and gain mode. Compare nine single-note cells is a preview, not a nine-cell record: the finding panel stores only the currently selected register/velocity, so do not file the whole grid as one result.\n2. Select Seeded six-note phrase at middle/0.6; repeat matched and dry. Select Seeded phrase with held ratio, feedback and ADSR controls and repeat both modes. Describe attack/body/brightness/decay/release, control transitions and gain comfort where actually assessed; mark the others not-assessed. State stress cells outside the intended register/velocity rather than calling them intended-use defects.\n3. Check the actual-listening declaration only after hearing both sources at the selected conditions. Settings changes clear it; never reuse a declaration from another profile/patch/cell. Enter actual device volume and subjective gain notes without inventing slider-to-dB calibration. Retain applied source gains/master gain and rendered/context rates from the JSON separately from subjective impressions.\n4. This is 22 selected-condition findings per recipe/profile (18 singles plus four phrases), if each was genuinely assessed. Export each recipe/profile batch before the 50-finding limit, then clear. Keep original JSON and selected matched/dry WAVs plus the exact versioned normalized patch sources; the hashes are declared identities and do not embed patches. Do not duplicate findings to simulate more listeners or overwrite originals to fill coverage.\n5. Run the existing listening-evidence review on exported batches, then inspect original criteria, notes and exact-condition groups. Dry/matched, different gains/routes/profiles and reversed A/B remain separate. Have real listeners review the intended-use results and any failure or unassessed criterion; a parse or preference count is not an acceptance threshold.\n\nControlled audition does not cover chords, extended fatigue or arrangement context. Use existing pages for those separate original observations; retain a timestamped local companion record with checkout/version, normalized patch identities, device/browser/route/volume, applied profile/gains/sample rate, start/end/duration, actual notes/velocity/polyphony/score and human observations. It must explicitly say supplemental, not masquerade as opm-listening-finding-1 single/phrase/control material:\n\n- Chords: Playground selects every bank voice and plays MIDI 60/64/67 with 0.7-second gates at the chosen velocity/pan. Listen to repeated chords and full tails at soft/medium/hard settings, noting peak comfort and masking. That page's default standard-profile/8-voice engine and gains differ from audition; record them and do not call it matched A/B or all-profile coverage. Its separate initial default brass recipe differs from bank brass: for bank-brass chord acceptance use the exact bank patch in the score-project workflow rather than substituting the default. Higher/different polyphony requires its own exact score and headroom evidence.\n- Long play: on the existing basic page, select the exact bank recipe, note in its intended register and actual velocity; Hold for at least ten minutes, listen throughout, then Release and observe the tail. Record fatigue/brightness, audible gaps, route/battery/foreground conditions and real duration. This page's default standard profile/gain is not the audition's dry or matched playback; do not infer other profiles or thermal/all-day reliability. Repeat separately for each of the eighteen recipes; early or unattended runs stay unverified.\n- Live controls: the controlled audition phrase covers a reproducible offline ratio/feedback/ADSR transition, not physical real-time performance. Use the existing instrument's lead/strings parts and CC campaign for actual MIDI/live-control observations; do not claim it tested all eighteen recipes. For each other recipe use Sound design: select the recipe and profile, Start, Hold at a declared note/velocity, then adjust ratio/feedback/ADSR/LFO and Release. Retain exported beginning/ending patches, actual controls/ramps and route/host-gain settings with original observations. Quality changes dispose old audio; Start the new profile before recapturing. Retain retriggering edits separately from live updates and do not infer coverage from a similar patch.\n- Song context: listen to the existing original showcase songs, recording exact score/project, voice-to-part mapping, tempo, profile, applied gains/route and audible masking/tails. An unchanged song covers only the recipes actually used. To cover another recipe, preserve a separately named score project with that exact patch and document the changed part; do not claim the original song used all eighteen. Song findings are supplemental, not isolated-energy matches.\n\nHost-trim proposals require reviewed originals under matching hashes and stated register/polyphony, including both dry and matched gain-comfort findings and supplemental chord/song observations. Retain current trims when there is no numeric applied-gain experiment or justified scope; no guessed compensation in operators or metadata. A maintainer records the decision, scope and artifact references only after human review and numerical safety verification of an approved change.\n\nRemaining blocker: no human listeners or original findings, long-play/chord/song observations, or physical phones/controllers were supplied. Every preset's listening status and all physical acceptance remain unverified. UI automation cannot hear, certify hardware or authorize a trim change.\n\n\n## Original synthetic DX7 recipes\n\ndemo/audition-fixtures.ts (checkout-only) generates valid-checksum VCED singles and a VMEM bank from original recipes, using Yamaha DX7 manual pp. 30–31 and DX7II Add-11 layout tables. The fixtures exercise three paired carriers, six additive carriers, a fixed-frequency carrier, velocity-sensitive modulation and equivalent packed-bank conversion. The fixed carrier retains its approximately 263 Hz frequency in canonical v7 instead of a MIDI-60 ratio approximation; imports leave LFO operator targets and operator waveforms absent to preserve the established conversion sound.\n\nThe page and report include describeDX7() routing/loss warnings. Four slots cannot preserve all six-operator topologies; retained/dropped operators and approximate envelopes, levels, feedback, LFO, keyboard scaling and velocity transfer functions still matter. See the converter's current warnings for supported mappings rather than inferring that every source parameter is exact. There is no original six-operator DX7 reference renderer here: A/B compares OPM recipes and converted synthetic sources, not lossless conversion or DX7 hardware fidelity.\n","href":"./voice-quality.html"},{"title":"Preset and conversion acceptance · Preset and conversion acceptance","text":"Preset and conversion acceptance Preset and conversion acceptance","href":"./voice-quality.html#preset-and-conversion-acceptance"},{"title":"Preset and conversion acceptance · Bank, provenance and host trim","text":"Preset and conversion acceptance Bank, provenance and host trim","href":"./voice-quality.html#bank-provenance-and-host-trim"},{"title":"Preset and conversion acceptance · Selective-LFO recipes and usage table","text":"Preset and conversion acceptance Selective-LFO recipes and usage table","href":"./voice-quality.html#selective-lfo-recipes-and-usage-table"},{"title":"Preset and conversion acceptance · Profiles and live-control phrases","text":"Preset and conversion acceptance Profiles and live-control phrases","href":"./voice-quality.html#profiles-and-live-control-phrases"},{"title":"Preset and conversion acceptance · Numerical measurement contract","text":"Preset and conversion acceptance Numerical measurement contract","href":"./voice-quality.html#numerical-measurement-contract"},{"title":"Preset and conversion acceptance · Repeatable listening workflow","text":"Preset and conversion acceptance Repeatable listening workflow","href":"./voice-quality.html#repeatable-listening-workflow"},{"title":"Preset and conversion acceptance · Recording a human finding","text":"Preset and conversion acceptance Recording a human finding","href":"./voice-quality.html#recording-a-human-finding"},{"title":"Preset and conversion acceptance · Reviewing findings and host-trim decisions","text":"Preset and conversion acceptance Reviewing findings and host-trim decisions","href":"./voice-quality.html#reviewing-findings-and-host-trim-decisions"},{"title":"Preset and conversion acceptance · All-eighteen current-version campaign","text":"Preset and conversion acceptance All-eighteen current-version campaign","href":"./voice-quality.html#all-eighteen-current-version-campaign"},{"title":"Preset and conversion acceptance · Original synthetic DX7 recipes","text":"Preset and conversion acceptance Original synthetic DX7 recipes","href":"./voice-quality.html#original-synthetic-dx7-recipes"},{"title":"Worker render diagnostics, deadlines and rollback","text":"Worker render diagnostics, deadlines and rollback # Worker render diagnostics, deadlines and rollback\n\nrenderSequenceInWorker keeps the main thread free while a static module Worker renders a score and streams encoded WAV chunks to a host sink. This guide covers the startup handshake, optional phase diagnostics and how a host should bound time and clean up partial files. Core behavior (one unacknowledged chunk, exact byte accounting, cancellation without waiting for a hung sink) is unchanged; see host integration.\n\n## Startup handshake\n\nThe Worker posts { type: 'ready', protocol: 1 } after its module has been fetched, parsed and evaluated. The host sends the score only after a valid ready. The message is validated as strict own data; a second ready, a different protocol number or any chunk before ready fails the render, aborts the sink once and terminates the Worker.\n\nstartupTimeoutMs (optional, integer 1–2,147,483,647) starts when the Worker is constructed and stops at ready. If it expires the promise rejects with a TimeoutError DOMException, the Worker is terminated and sink.abort() is called. It never measures rendering, sink writes or close(), so a slow disk or a long score cannot trip it. It exists to tell \"the Worker module never started\" (bad workerUrl, blocked MIME, suspended environment) from \"rendering is slow\".\n\nts\nconst result = await renderSequenceInWorker(score, {\n sink, format: 'pcm24',\n startupTimeoutMs: 10_000, // module start only\n signal: AbortSignal.timeout(5 * 60_000), // your own whole-job deadline\n phaseDiagnostics: true,\n onPhase: status => console.debug(status.phase, status.phaseElapsedMs),\n});\nconsole.log(result.diagnostics.phases); // { status: 'completed', initializingMs, renderingMs, writingMs, closingMs, totalMs }\n\n\nThere is no built-in overall deadline and no automatic retry. A legitimately long render or a slow sink is indistinguishable from a stalled one without host knowledge, and a blind retry would re-send chunks to a sink that may already hold partial data. Use an AbortSignal for the policy that fits the app.\n\n## Phases\n\nonPhase(status) is called synchronously, for every transition, with a frozen { phase, elapsedMs, phaseElapsedMs, frames, totalFrames, bytesWritten, errors }:\n\n| Phase | Interval |\n| --- | --- |\n| initializing | from Worker construction until ready |\n| rendering | Worker compute: from ready/each acknowledgement until the next chunk arrives |\n| writing | from chunk arrival until the awaited sink.write() and onProgress finish |\n| closing | from the Worker's done until sink.close() resolves |\n| completed, cancelled, failed | terminal; reported once to onPhase |\n\nrendering and writing alternate once per chunk. With phaseDiagnostics: true the successful result carries four accumulated durations and the total in diagnostics.phases. Failed or cancelled renders reject; use onPhase to capture their terminal status. Timings are host monotonic wall-clock readings (performance.now()), not DSP benchmarks, and include scheduler and GC pauses. Neither option changes the bytes written.\n\nIf onPhase or onProgress throws, the render fails with that error exactly once; the first failure is preserved and the observer is not called again after the terminal phase.\n\n## Partial output and rollback\n\nThe sink owns its output. abort(reason) is called at most once, without being awaited, and the Worker is already terminated when it runs. It must make any in-flight write harmless and discard the partial file; cancellation cannot retract bytes already persisted. A robust File System Access sink:\n\nts\nasync function createFileSink(handle: FileSystemFileHandle): Promise\u003cWavSink> {\n const writable = await handle.createWritable(); // writes to a swap file until close()\n return {\n write: bytes => writable.write(bytes),\n close: () => writable.close(),\n abort: reason => writable.abort(reason), // discards the swap file: the target is untouched\n };\n}\n\n\nAcquire the handle from a trusted gesture, keep the cancellation signal alive across picker and createWritable() awaits, and clean up acquisition failures before starting a Worker. Never fall back to accumulating the whole file in a Blob.\n\n## Diagnosing a stalled start\n\n1. TimeoutError from startupTimeoutMs: fetch workerUrl directly. It must be same-origin, served as JavaScript with nosniff and a 200 status, next to the rest of the matching dist/ tree (npx --no-install opm-assets check \u003cbase-url> run from a project with OPM.js installed, verifies bytes, status and MIME without fetching a registry CLI).\n2. A Worker that reaches ready but never delivers a chunk: look at status.phase in onPhase. A long rendering phase with frames not advancing points at the score; a long writing phase points at the sink.\n3. Repeated, order-dependent startup failures in an automation harness are not evidence about the package. During the 1.7 verification a managed browser closed targets or missed Worker start deadlines when many Workers started back to back, while the same path passed in isolation. The cause was not established, which is why this release adds measurement (ready, phases) rather than an automatic retry.\n\n## Verified behavior\n\ntest/render-worker.test.ts (checkout-only) runs the real Worker module in a thread. It checks the phase sequence, that diagnostics do not change the bytes, that startupTimeoutMs rejects a Worker that never answers (no score is sent and the sink is aborted once) while a 30 ms-per-write sink succeeds under a 1 s watchdog, rejection of invalid or premature ready messages, and that a throwing observer fails the render once.\n","href":"./worker-diagnostics.html"},{"title":"Worker render diagnostics, deadlines and rollback · Worker render diagnostics, deadlines and rollback","text":"Worker render diagnostics, deadlines and rollback Worker render diagnostics, deadlines and rollback","href":"./worker-diagnostics.html#worker-render-diagnostics-deadlines-and-rollback"},{"title":"Worker render diagnostics, deadlines and rollback · Startup handshake","text":"Worker render diagnostics, deadlines and rollback Startup handshake","href":"./worker-diagnostics.html#startup-handshake"},{"title":"Worker render diagnostics, deadlines and rollback · Phases","text":"Worker render diagnostics, deadlines and rollback Phases","href":"./worker-diagnostics.html#phases"},{"title":"Worker render diagnostics, deadlines and rollback · Partial output and rollback","text":"Worker render diagnostics, deadlines and rollback Partial output and rollback","href":"./worker-diagnostics.html#partial-output-and-rollback"},{"title":"Worker render diagnostics, deadlines and rollback · Diagnosing a stalled start","text":"Worker render diagnostics, deadlines and rollback Diagnosing a stalled start","href":"./worker-diagnostics.html#diagnosing-a-stalled-start"},{"title":"Worker render diagnostics, deadlines and rollback · Verified behavior","text":"Worker render diagnostics, deadlines and rollback Verified behavior","href":"./worker-diagnostics.html#verified-behavior"},{"title":"opm.js · ADSR","text":"opm.js ADSR type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · applyEffects","text":"opm.js applyEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · Arrangement","text":"opm.js Arrangement type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementChangeOptions","text":"opm.js ArrangementChangeOptions type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementDefinition","text":"opm.js ArrangementDefinition type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js · ArrangementGainOptions","text":"opm.js ArrangementGainOptions type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementLayer","text":"opm.js ArrangementLayer type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js · ArrangementOptions","text":"opm.js ArrangementOptions type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementProject","text":"opm.js ArrangementProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · ArrangementSection","text":"opm.js ArrangementSection type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js · ArrangementSnapshot","text":"opm.js ArrangementSnapshot type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · ArrangementState","text":"opm.js ArrangementState type","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · BarBeat","text":"opm.js BarBeat type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · barBeatToBeat","text":"opm.js barBeatToBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · BeatQuantization","text":"opm.js BeatQuantization type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · BeatSequenceEvent","text":"opm.js BeatSequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · BeatSequenceOptions","text":"opm.js BeatSequenceOptions type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · beatsToSeconds","text":"opm.js beatsToSeconds value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · beatToBarBeat","text":"opm.js beatToBarBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · ChorusOptions","text":"opm.js ChorusOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · CommandEvent","text":"opm.js CommandEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · CommandName","text":"opm.js CommandName type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · CommandRejectedError","text":"opm.js CommandRejectedError value / type where applicable","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · CommandWaitOptions","text":"opm.js CommandWaitOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · compileBeatSequence","text":"opm.js compileBeatSequence value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · ContextEvent","text":"opm.js ContextEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · ContextState","text":"opm.js ContextState type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · createArrangement","text":"opm.js createArrangement value / type where applicable","href":"./api.html#declaration-apiarrangementdts"},{"title":"opm.js · createEffects","text":"opm.js createEffects value / type where applicable","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · createLookaheadScheduler","text":"opm.js createLookaheadScheduler value / type where applicable","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · createMidiAdapter","text":"opm.js createMidiAdapter value / type where applicable","href":"./api.html#declaration-apimididts"},{"title":"opm.js · createPerformance","text":"opm.js createPerformance value / type where applicable","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · createStereoEffects","text":"opm.js createStereoEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · createTransport","text":"opm.js createTransport value / type where applicable","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · DiagnosticsEvent","text":"opm.js DiagnosticsEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · EffectsEvent","text":"opm.js EffectsEvent type","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · EffectsOptions","text":"opm.js EffectsOptions type","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · ErrorEvent","text":"opm.js ErrorEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · exportMidiFile","text":"opm.js exportMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · FrozenOperator","text":"opm.js FrozenOperator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · FrozenVoice","text":"opm.js FrozenVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · gmDrumVoices","text":"opm.js gmDrumVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js · gmProgramVoices","text":"opm.js gmProgramVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js · importMidiFile","text":"opm.js importMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · KeyScale","text":"opm.js KeyScale type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyLFO","text":"opm.js LegacyLFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyLFOV5","text":"opm.js LegacyLFOV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV3","text":"opm.js LegacyVoiceV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV4","text":"opm.js LegacyVoiceV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV5","text":"opm.js LegacyVoiceV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LegacyVoiceV6","text":"opm.js LegacyVoiceV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFO","text":"opm.js LFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFOInput","text":"opm.js LFOInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFOTargets","text":"opm.js LFOTargets type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LFOTargetsInput","text":"opm.js LFOTargetsInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · LookaheadNote","text":"opm.js LookaheadNote type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · LookaheadOptions","text":"opm.js LookaheadOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · LookaheadScheduler","text":"opm.js LookaheadScheduler type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · LookaheadWindow","text":"opm.js LookaheadWindow type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · MAX_ARRANGEMENT_PROJECT_BYTES","text":"opm.js MAX_ARRANGEMENT_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MAX_ARRANGEMENT_PROJECT_VOICES","text":"opm.js MAX_ARRANGEMENT_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MAX_MIDI_FILE_BYTES","text":"opm.js MAX_MIDI_FILE_BYTES value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MAX_MIDI_FILE_EVENTS","text":"opm.js MAX_MIDI_FILE_EVENTS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MAX_MIDI_FILE_TRACKS","text":"opm.js MAX_MIDI_FILE_TRACKS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MAX_SCORE_PROJECT_BYTES","text":"opm.js MAX_SCORE_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MAX_SCORE_PROJECT_VOICES","text":"opm.js MAX_SCORE_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · MidiAccessLike","text":"opm.js MidiAccessLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiAdapter","text":"opm.js MidiAdapter type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiAdapterOptions","text":"opm.js MidiAdapterOptions type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiAdapterSnapshot","text":"opm.js MidiAdapterSnapshot type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiControllerMapping","text":"opm.js MidiControllerMapping type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiExportOptions","text":"opm.js MidiExportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileControlKind","text":"opm.js MidiFileControlKind type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileLossSummary","text":"opm.js MidiFileLossSummary type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFilePreservedControl","text":"opm.js MidiFilePreservedControl type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileWarning","text":"opm.js MidiFileWarning type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiFileWarningCode","text":"opm.js MidiFileWarningCode type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiImportOptions","text":"opm.js MidiImportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiImportResult","text":"opm.js MidiImportResult type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js · MidiInputLike","text":"opm.js MidiInputLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiMessageEventLike","text":"opm.js MidiMessageEventLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiNavigatorLike","text":"opm.js MidiNavigatorLike type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiOperatorControl","text":"opm.js MidiOperatorControl type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiScalarControl","text":"opm.js MidiScalarControl type","href":"./api.html#declaration-apimididts"},{"title":"opm.js · MidiVoiceMap","text":"opm.js MidiVoiceMap type","href":"./api.html#declaration-coremidi-statedts"},{"title":"opm.js · MusicalTransport","text":"opm.js MusicalTransport type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · NormalizedTuning","text":"opm.js NormalizedTuning type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js · NormalizedVoice","text":"opm.js NormalizedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · normalizeTempoMap","text":"opm.js normalizeTempoMap value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · NoteControls","text":"opm.js NoteControls type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js · NoteEvent","text":"opm.js NoteEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · NoteState","text":"opm.js NoteState type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · Operator","text":"opm.js Operator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · OperatorWaveform","text":"opm.js OperatorWaveform type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · OPM","text":"opm.js OPM value / type where applicable","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · OpmEffects","text":"opm.js OpmEffects type","href":"./api.html#declaration-apifxdts"},{"title":"opm.js · OPMEvent","text":"opm.js OPMEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · OPMOptions","text":"opm.js OPMOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · parseArrangementProject","text":"opm.js parseArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · parseScoreProject","text":"opm.js parseScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · Performance","text":"opm.js Performance type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceKeySnapshot","text":"opm.js PerformanceKeySnapshot type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceNoteOffOptions","text":"opm.js PerformanceNoteOffOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceNoteOptions","text":"opm.js PerformanceNoteOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformanceOptions","text":"opm.js PerformanceOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformancePartControls","text":"opm.js PerformancePartControls type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformancePartOptions","text":"opm.js PerformancePartOptions type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PerformancePartSnapshot","text":"opm.js PerformancePartSnapshot type","href":"./api.html#declaration-apiperformancedts"},{"title":"opm.js · PitchEnvelope","text":"opm.js PitchEnvelope type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · PlayNoteOptions","text":"opm.js PlayNoteOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · playSequence","text":"opm.js playSequence value / type where applicable","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · PlaySequenceOptions","text":"opm.js PlaySequenceOptions type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · PreparedVoice","text":"opm.js PreparedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · QualityProfile","text":"opm.js QualityProfile type","href":"./api.html#declaration-coredecimatordts"},{"title":"opm.js · quantizeBeat","text":"opm.js quantizeBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · renderSequenceInWorker","text":"opm.js renderSequenceInWorker value / type where applicable","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · requestMidiAccess","text":"opm.js requestMidiAccess value / type where applicable","href":"./api.html#declaration-apimididts"},{"title":"opm.js · ResetEvent","text":"opm.js ResetEvent type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · ReverbOptions","text":"opm.js ReverbOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · ScheduledNoteOptions","text":"opm.js ScheduledNoteOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · ScoreProject","text":"opm.js ScoreProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · ScoreProjectSettings","text":"opm.js ScoreProjectSettings type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · secondsToBeats","text":"opm.js secondsToBeats value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · SequenceControlEvent","text":"opm.js SequenceControlEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequenceEvent","text":"opm.js SequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequenceNoteEvent","text":"opm.js SequenceNoteEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequencePlayback","text":"opm.js SequencePlayback type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · SequenceStopEvent","text":"opm.js SequenceStopEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js · SequenceStream","text":"opm.js SequenceStream type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · SequenceStreamOptions","text":"opm.js SequenceStreamOptions type","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · serializeArrangementProject","text":"opm.js serializeArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · serializeScoreProject","text":"opm.js serializeScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js · StereoEffects","text":"opm.js StereoEffects type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · StereoEffectsOptions","text":"opm.js StereoEffectsOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js · StopOptions","text":"opm.js StopOptions type","href":"./api.html#declaration-apiindexdts"},{"title":"opm.js · streamSequence","text":"opm.js streamSequence value / type where applicable","href":"./api.html#declaration-apisequencedts"},{"title":"opm.js · swingBeat","text":"opm.js swingBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · swingBeatEvents","text":"opm.js swingBeatEvents value / type where applicable","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · SynthOptions","text":"opm.js SynthOptions type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js · TempoPoint","text":"opm.js TempoPoint type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · TimeSignature","text":"opm.js TimeSignature type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js · TransportLoop","text":"opm.js TransportLoop type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TransportOptions","text":"opm.js TransportOptions type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TransportSnapshot","text":"opm.js TransportSnapshot type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TransportState","text":"opm.js TransportState type","href":"./api.html#declaration-apitransportdts"},{"title":"opm.js · TuningOptions","text":"opm.js TuningOptions type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js · VERSION","text":"opm.js VERSION value / type where applicable","href":"./api.html#declaration-versiondts"},{"title":"opm.js · Voice","text":"opm.js Voice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · VoiceInput","text":"opm.js VoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js · WavSink","text":"opm.js WavSink type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderOptions","text":"opm.js WorkerRenderOptions type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderPhase","text":"opm.js WorkerRenderPhase type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderPhaseDiagnostics","text":"opm.js WorkerRenderPhaseDiagnostics type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderPhaseStatus","text":"opm.js WorkerRenderPhaseStatus type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderProgress","text":"opm.js WorkerRenderProgress type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js · WorkerRenderResult","text":"opm.js WorkerRenderResult type","href":"./api.html#declaration-apirender-workerdts"},{"title":"opm.js/core · ADSR","text":"opm.js/core ADSR type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · ALGORITHMS","text":"opm.js/core ALGORITHMS value / type where applicable","href":"./api.html#declaration-corealgorithmsdts"},{"title":"opm.js/core · applyEffects","text":"opm.js/core applyEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · ArrangementDefinition","text":"opm.js/core ArrangementDefinition type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js/core · ArrangementLayer","text":"opm.js/core ArrangementLayer type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js/core · ArrangementProject","text":"opm.js/core ArrangementProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · ArrangementSection","text":"opm.js/core ArrangementSection type","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"opm.js/core · BarBeat","text":"opm.js/core BarBeat type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · barBeatToBeat","text":"opm.js/core barBeatToBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · BeatSequenceEvent","text":"opm.js/core BeatSequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · BeatSequenceOptions","text":"opm.js/core BeatSequenceOptions type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · beatsToSeconds","text":"opm.js/core beatsToSeconds value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · beatToBarBeat","text":"opm.js/core beatToBarBeat value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · ChorusOptions","text":"opm.js/core ChorusOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · ChunkedSequenceOptions","text":"opm.js/core ChunkedSequenceOptions type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · ChunkedSequenceRender","text":"opm.js/core ChunkedSequenceRender type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · compileBeatSequence","text":"opm.js/core compileBeatSequence value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · createStereoEffects","text":"opm.js/core createStereoEffects value / type where applicable","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · createWavEncoder","text":"opm.js/core createWavEncoder value / type where applicable","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · encodeWav","text":"opm.js/core encodeWav value / type where applicable","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · envelopeAt","text":"opm.js/core envelopeAt value / type where applicable","href":"./api.html#declaration-coreenvelopedts"},{"title":"opm.js/core · estimateSequenceCapacity","text":"opm.js/core estimateSequenceCapacity value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · exportMidiFile","text":"opm.js/core exportMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · FrozenVoice","text":"opm.js/core FrozenVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · gmDrumVoices","text":"opm.js/core gmDrumVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/core · gmProgramVoices","text":"opm.js/core gmProgramVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/core · HEADROOM","text":"opm.js/core HEADROOM value / type where applicable","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · importMidiFile","text":"opm.js/core importMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · KeyScale","text":"opm.js/core KeyScale type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyLFO","text":"opm.js/core LegacyLFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyLFOV5","text":"opm.js/core LegacyLFOV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoice","text":"opm.js/core LegacyVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV2","text":"opm.js/core LegacyVoiceV2 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV3","text":"opm.js/core LegacyVoiceV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV4","text":"opm.js/core LegacyVoiceV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV5","text":"opm.js/core LegacyVoiceV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LegacyVoiceV6","text":"opm.js/core LegacyVoiceV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFO","text":"opm.js/core LFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFOInput","text":"opm.js/core LFOInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFOTargets","text":"opm.js/core LFOTargets type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · LFOTargetsInput","text":"opm.js/core LFOTargetsInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · lfoValue","text":"opm.js/core lfoValue value / type where applicable","href":"./api.html#declaration-corelfodts"},{"title":"opm.js/core · MAX_ARRANGEMENT_PROJECT_BYTES","text":"opm.js/core MAX_ARRANGEMENT_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_ARRANGEMENT_PROJECT_VOICES","text":"opm.js/core MAX_ARRANGEMENT_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_LONG_SEQUENCE_EVENTS","text":"opm.js/core MAX_LONG_SEQUENCE_EVENTS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_LONG_SEQUENCE_SECONDS","text":"opm.js/core MAX_LONG_SEQUENCE_SECONDS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_MIDI_FILE_BYTES","text":"opm.js/core MAX_MIDI_FILE_BYTES value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MAX_MIDI_FILE_EVENTS","text":"opm.js/core MAX_MIDI_FILE_EVENTS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MAX_MIDI_FILE_TRACKS","text":"opm.js/core MAX_MIDI_FILE_TRACKS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MAX_RENDER_SAMPLES","text":"opm.js/core MAX_RENDER_SAMPLES value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SCORE_PROJECT_BYTES","text":"opm.js/core MAX_SCORE_PROJECT_BYTES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_SCORE_PROJECT_VOICES","text":"opm.js/core MAX_SCORE_PROJECT_VOICES value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · MAX_SEQUENCE_CHUNK_FRAMES","text":"opm.js/core MAX_SEQUENCE_CHUNK_FRAMES value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SEQUENCE_NOTES","text":"opm.js/core MAX_SEQUENCE_NOTES value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SEQUENCE_SECONDS","text":"opm.js/core MAX_SEQUENCE_SECONDS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MAX_SEQUENCE_SLOTS","text":"opm.js/core MAX_SEQUENCE_SLOTS value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · MidiExportOptions","text":"opm.js/core MidiExportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileControlKind","text":"opm.js/core MidiFileControlKind type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileLossSummary","text":"opm.js/core MidiFileLossSummary type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFilePreservedControl","text":"opm.js/core MidiFilePreservedControl type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileWarning","text":"opm.js/core MidiFileWarning type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiFileWarningCode","text":"opm.js/core MidiFileWarningCode type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiImportOptions","text":"opm.js/core MidiImportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiImportResult","text":"opm.js/core MidiImportResult type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/core · MidiVoiceMap","text":"opm.js/core MidiVoiceMap type","href":"./api.html#declaration-coremidi-statedts"},{"title":"opm.js/core · NormalizedTuning","text":"opm.js/core NormalizedTuning type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · normalizeTempoMap","text":"opm.js/core normalizeTempoMap value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · normalizeTuning","text":"opm.js/core normalizeTuning value / type where applicable","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · normalizeVoice","text":"opm.js/core normalizeVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/core · NoteControls","text":"opm.js/core NoteControls type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · NoteOptions","text":"opm.js/core NoteOptions type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · Operator","text":"opm.js/core Operator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · OperatorWaveform","text":"opm.js/core OperatorWaveform type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · OVERSAMPLE","text":"opm.js/core OVERSAMPLE value / type where applicable","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · parseArrangementProject","text":"opm.js/core parseArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · parseScoreProject","text":"opm.js/core parseScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · PitchEnvelope","text":"opm.js/core PitchEnvelope type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · PreparedSequenceEvent","text":"opm.js/core PreparedSequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · PreparedVoice","text":"opm.js/core PreparedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · prepareLongSequence","text":"opm.js/core prepareLongSequence value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · prepareSequence","text":"opm.js/core prepareSequence value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · prepareVoice","text":"opm.js/core prepareVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/core · QualityProfile","text":"opm.js/core QualityProfile type","href":"./api.html#declaration-coredecimatordts"},{"title":"opm.js/core · renderNote","text":"opm.js/core renderNote value / type where applicable","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · RenderNoteOptions","text":"opm.js/core RenderNoteOptions type","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · RenderResult","text":"opm.js/core RenderResult type","href":"./api.html#declaration-coreindexdts"},{"title":"opm.js/core · renderSequence","text":"opm.js/core renderSequence value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · renderSequenceChunks","text":"opm.js/core renderSequenceChunks value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · ReverbOptions","text":"opm.js/core ReverbOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · sampleRateValue","text":"opm.js/core sampleRateValue value / type where applicable","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · ScoreProject","text":"opm.js/core ScoreProject type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · ScoreProjectSettings","text":"opm.js/core ScoreProjectSettings type","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · secondsToBeats","text":"opm.js/core secondsToBeats value / type where applicable","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · SequenceCapacity","text":"opm.js/core SequenceCapacity type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceChunk","text":"opm.js/core SequenceChunk type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceControlEvent","text":"opm.js/core SequenceControlEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceEvent","text":"opm.js/core SequenceEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceNoteEvent","text":"opm.js/core SequenceNoteEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceOptions","text":"opm.js/core SequenceOptions type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceSnapshot","text":"opm.js/core SequenceSnapshot type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceStopEvent","text":"opm.js/core SequenceStopEvent type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · SequenceVoices","text":"opm.js/core SequenceVoices type","href":"./api.html#declaration-coresequencedts"},{"title":"opm.js/core · serializeArrangementProject","text":"opm.js/core serializeArrangementProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · serializeScoreProject","text":"opm.js/core serializeScoreProject value / type where applicable","href":"./api.html#declaration-coreprojectdts"},{"title":"opm.js/core · StereoEffects","text":"opm.js/core StereoEffects type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · StereoEffectsOptions","text":"opm.js/core StereoEffectsOptions type","href":"./api.html#declaration-corefxdts"},{"title":"opm.js/core · Synth","text":"opm.js/core Synth value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · SynthOptions","text":"opm.js/core SynthOptions type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · TempoPoint","text":"opm.js/core TempoPoint type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · TimeSignature","text":"opm.js/core TimeSignature type","href":"./api.html#declaration-coretransportdts"},{"title":"opm.js/core · tuningFrequency","text":"opm.js/core tuningFrequency value / type where applicable","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · TuningOptions","text":"opm.js/core TuningOptions type","href":"./api.html#declaration-coretuningdts"},{"title":"opm.js/core · validateMaxVoices","text":"opm.js/core validateMaxVoices value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · validateNoteControls","text":"opm.js/core validateNoteControls value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · validateVoicePriority","text":"opm.js/core validateVoicePriority value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · Voice","text":"opm.js/core Voice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · VoiceAdmissionError","text":"opm.js/core VoiceAdmissionError value / type where applicable","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · VoiceEndReason","text":"opm.js/core VoiceEndReason type","href":"./api.html#declaration-coresynthdts"},{"title":"opm.js/core · VoiceInput","text":"opm.js/core VoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/core · WavChunk","text":"opm.js/core WavChunk type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavEncoder","text":"opm.js/core WavEncoder type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavEncoderOptions","text":"opm.js/core WavEncoderOptions type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavFormat","text":"opm.js/core WavFormat type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/core · WavOptions","text":"opm.js/core WavOptions type","href":"./api.html#declaration-corewavdts"},{"title":"opm.js/midi-file · exportMidiFile","text":"opm.js/midi-file exportMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · importMidiFile","text":"opm.js/midi-file importMidiFile value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MAX_MIDI_FILE_BYTES","text":"opm.js/midi-file MAX_MIDI_FILE_BYTES value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MAX_MIDI_FILE_EVENTS","text":"opm.js/midi-file MAX_MIDI_FILE_EVENTS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MAX_MIDI_FILE_TRACKS","text":"opm.js/midi-file MAX_MIDI_FILE_TRACKS value / type where applicable","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiExportOptions","text":"opm.js/midi-file MidiExportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileControlKind","text":"opm.js/midi-file MidiFileControlKind type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileLossSummary","text":"opm.js/midi-file MidiFileLossSummary type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFilePreservedControl","text":"opm.js/midi-file MidiFilePreservedControl type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileWarning","text":"opm.js/midi-file MidiFileWarning type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiFileWarningCode","text":"opm.js/midi-file MidiFileWarningCode type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiImportOptions","text":"opm.js/midi-file MidiImportOptions type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/midi-file · MidiImportResult","text":"opm.js/midi-file MidiImportResult type","href":"./api.html#declaration-coremidi-filedts"},{"title":"opm.js/voices/schema.js · ADSR","text":"opm.js/voices/schema.js ADSR type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · Algorithm","text":"opm.js/voices/schema.js Algorithm type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · bounded","text":"opm.js/voices/schema.js bounded value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · CompleteVoiceInput","text":"opm.js/voices/schema.js CompleteVoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · FourOperators","text":"opm.js/voices/schema.js FourOperators type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · FrozenOperator","text":"opm.js/voices/schema.js FrozenOperator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · FrozenVoice","text":"opm.js/voices/schema.js FrozenVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · KeyScale","text":"opm.js/voices/schema.js KeyScale type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyLFO","text":"opm.js/voices/schema.js LegacyLFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyLFOV4","text":"opm.js/voices/schema.js LegacyLFOV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyLFOV5","text":"opm.js/voices/schema.js LegacyLFOV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperator","text":"opm.js/voices/schema.js LegacyOperator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperatorV2","text":"opm.js/voices/schema.js LegacyOperatorV2 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperatorV3","text":"opm.js/voices/schema.js LegacyOperatorV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyOperatorV6","text":"opm.js/voices/schema.js LegacyOperatorV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoice","text":"opm.js/voices/schema.js LegacyVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV2","text":"opm.js/voices/schema.js LegacyVoiceV2 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV3","text":"opm.js/voices/schema.js LegacyVoiceV3 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV4","text":"opm.js/voices/schema.js LegacyVoiceV4 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV5","text":"opm.js/voices/schema.js LegacyVoiceV5 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LegacyVoiceV6","text":"opm.js/voices/schema.js LegacyVoiceV6 type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFO","text":"opm.js/voices/schema.js LFO type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFOInput","text":"opm.js/voices/schema.js LFOInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · lfoSync","text":"opm.js/voices/schema.js lfoSync value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFOTargets","text":"opm.js/voices/schema.js LFOTargets type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LFOTargetsInput","text":"opm.js/voices/schema.js LFOTargetsInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · lfoWaveform","text":"opm.js/voices/schema.js lfoWaveform value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · LIMITS","text":"opm.js/voices/schema.js LIMITS value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · MAX_BANK_BYTES","text":"opm.js/voices/schema.js MAX_BANK_BYTES value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · MAX_BANK_VOICES","text":"opm.js/voices/schema.js MAX_BANK_VOICES value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · NormalizedVoice","text":"opm.js/voices/schema.js NormalizedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · Operator","text":"opm.js/voices/schema.js Operator type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · operatorWaveform","text":"opm.js/voices/schema.js operatorWaveform value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · OperatorWaveform","text":"opm.js/voices/schema.js OperatorWaveform type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · parseVoiceBank","text":"opm.js/voices/schema.js parseVoiceBank value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · PitchEnvelope","text":"opm.js/voices/schema.js PitchEnvelope type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · PreparedVoice","text":"opm.js/voices/schema.js PreparedVoice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · prepareVoice","text":"opm.js/voices/schema.js prepareVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/voices/schema.js · validateVoice","text":"opm.js/voices/schema.js validateVoice value / type where applicable","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · Voice","text":"opm.js/voices/schema.js Voice type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/schema.js · VoiceInput","text":"opm.js/voices/schema.js VoiceInput type","href":"./api.html#declaration-voicesschemadts"},{"title":"opm.js/voices/normalize.js · normalizeVoice","text":"opm.js/voices/normalize.js normalizeVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/voices/normalize.js · prepareVoice","text":"opm.js/voices/normalize.js prepareVoice value / type where applicable","href":"./api.html#declaration-voicesnormalizedts"},{"title":"opm.js/voices/brass.js · brass","text":"opm.js/voices/brass.js brass value / type where applicable","href":"./api.html#declaration-voicesbrassdts"},{"title":"opm.js/voices/dx7.js · describeDX7","text":"opm.js/voices/dx7.js describeDX7 value / type where applicable","href":"./api.html#declaration-voicesdx7dts"},{"title":"opm.js/voices/dx7.js · DX7ImportDescription","text":"opm.js/voices/dx7.js DX7ImportDescription type","href":"./api.html#declaration-voicesdx7dts"},{"title":"opm.js/voices/dx7.js · importDX7","text":"opm.js/voices/dx7.js importDX7 value / type where applicable","href":"./api.html#declaration-voicesdx7dts"},{"title":"opm.js/voices/examples.js · examples","text":"opm.js/voices/examples.js examples value / type where applicable","href":"./api.html#declaration-voicesexamplesdts"},{"title":"opm.js/voices/gm-map.js · gmDrumVoices","text":"opm.js/voices/gm-map.js gmDrumVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/voices/gm-map.js · gmProgramVoices","text":"opm.js/voices/gm-map.js gmProgramVoices value / type where applicable","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"opm.js/voices/opm.js · describeOPM","text":"opm.js/voices/opm.js describeOPM value / type where applicable","href":"./api.html#declaration-voicesopmdts"},{"title":"opm.js/voices/opm.js · importOPM","text":"opm.js/voices/opm.js importOPM value / type where applicable","href":"./api.html#declaration-voicesopmdts"},{"title":"opm.js/voices/opm.js · OPMImportDescription","text":"opm.js/voices/opm.js OPMImportDescription type","href":"./api.html#declaration-voicesopmdts"},{"title":"opm.js/voices/original.js · originalPresets","text":"opm.js/voices/original.js originalPresets value / type where applicable","href":"./api.html#declaration-voicesoriginaldts"},{"title":"opm.js/voices/preset-metadata.js · defaultBrassMetadata","text":"opm.js/voices/preset-metadata.js defaultBrassMetadata value / type where applicable","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"opm.js/voices/preset-metadata.js · presetMetadata","text":"opm.js/voices/preset-metadata.js presetMetadata value / type where applicable","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"opm.js/voices/preset-metadata.js · PresetMetadata","text":"opm.js/voices/preset-metadata.js PresetMetadata type","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"opm.js/voices/voice.schema.js · voiceSchema","text":"opm.js/voices/voice.schema.js voiceSchema value / type where applicable","href":"./api.html#declaration-voicesvoiceschemadts"},{"title":"opm.js/tools/assets.js · AssetDeployment","text":"opm.js/tools/assets.js AssetDeployment type","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · checkDeployment","text":"opm.js/tools/assets.js checkDeployment value / type where applicable","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · CheckDeploymentOptions","text":"opm.js/tools/assets.js CheckDeploymentOptions type","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · copyAssets","text":"opm.js/tools/assets.js copyAssets value / type where applicable","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · CopyAssetsOptions","text":"opm.js/tools/assets.js CopyAssetsOptions type","href":"./api.html#declaration-toolsassetsdts"},{"title":"opm.js/tools/assets.js · DeploymentCheck","text":"opm.js/tools/assets.js DeploymentCheck type","href":"./api.html#declaration-toolsassetsdts"},{"title":"Declarations · api/arrangement.d.ts","text":"import type { OPM } from './index.js';\nimport type { ArrangementDefinition } from '../core/arrangement-definition.js';\nexport type { ArrangementLayer, ArrangementSection } from '../core/arrangement-definition.js';\nimport type { BarBeat, TempoPoint } from '../core/transport.js';\nexport interface ArrangementOptions extends ArrangementDefinition {\n bpm?: number;\n horizon?: number;\n interval?: number;\n onError?: (error: Error) => void;\n}\nexport interface ArrangementChangeOptions {\n /** Boundary in quarter notes, or the meter-defined beat/bar; default bar. Never earlier than already admitted notes. */\n quantize?: 'beat' | 'bar' | number;\n /** Removed layers may finish naturally instead of being released at the boundary. Default false. */\n preserveNotes?: boolean;\n /** Linear gain fade in seconds, 0..10; default 0. Shared layers remain continuous. */\n fade?: number;\n}\nexport interface ArrangementGainOptions {\n quantize?: 'beat' | 'bar' | number;\n fade?: number;\n}\nexport type ArrangementState = 'stopped' | 'starting' | 'running' | 'paused' | 'disposed';\nexport interface ArrangementSnapshot {\n readonly state: ArrangementState;\n /** Global quarter-note position; a paused arrangement reports where it will resume. */\n readonly position: number;\n readonly musicalPosition: Readonly\u003cBarBeat>;\n readonly section: string;\n /** Layers effective at position. */\n readonly layers: readonly string[];\n /** Committed changes after the current position. */\n readonly pending: readonly Readonly\u003c{\n beat: number;\n section: string;\n layers: readonly string[];\n }>[];\n /** Notes refused by voice-priority admission; they are not an arrangement failure. */\n readonly priorityDrops: number;\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n}\nexport interface Arrangement {\n readonly state: ArrangementState;\n readonly snapshot: ArrangementSnapshot;\n start(): Promise\u003cvoid>;\n resume(): Promise\u003cvoid>;\n /** Releases owned notes. Resume restarts musically at the paused beat; no DSP checkpoint is restored. */\n pause(): void;\n /** Releases owned notes and rewinds to beat 0. */\n stop(): void;\n switchSection(name: string, options?: ArrangementChangeOptions): number;\n setLayer(name: string, enabled: boolean, options?: ArrangementChangeOptions): number;\n setLayerGain(name: string, gain: number, options?: ArrangementGainOptions): number;\n /** Replaces tempo from the current beat onward; notes already admitted keep their admitted audio times. */\n setTempo(bpm: number): void;\n setTempoMap(map: readonly TempoPoint[]): void;\n pump(): void;\n dispose(): void;\n}\n/**\n * Looping, quantized adaptive music for one OPM instance. Layers are aligned to the global beat grid, so a layer\n * shared by two sections is one continuous schedule: its sounding gates are neither retriggered nor stopped when\n * unrelated layers change. Changes are committed at a boundary no earlier than the notes already admitted.\n */\nexport declare function createArrangement(opm: OPM, options: ArrangementOptions): Arrangement;\n","href":"./api.html#declaration-apiarrangementdts"},{"title":"Declarations · api/fx.d.ts","text":"import type { StereoEffectsOptions } from '../core/fx.js';\nexport interface EffectsEvent {\n type: 'error';\n error: Error;\n}\nexport interface EffectsOptions {\n workletUrl?: string | URL;\n params?: StereoEffectsOptions;\n onEvent?: (event: EffectsEvent) => void;\n}\nexport interface OpmEffects {\n readonly input: AudioNode;\n readonly output: AudioNode;\n /** Resolves once the module/node have been created; context playback is host-owned. */\n readonly ready: Promise\u003cvoid>;\n update(params: StereoEffectsOptions): void;\n reset(): void;\n dispose(): void;\n}\n/** Optional stereo effect insert. Does not connect, resume, suspend or close the host context. */\nexport declare function createEffects(context: BaseAudioContext, options?: EffectsOptions): Promise\u003cOpmEffects>;\n","href":"./api.html#declaration-apifxdts"},{"title":"Declarations · api/index.d.ts","text":"import type { CompleteVoiceInput, NormalizedVoice, VoiceInput } from '../voices/schema.js';\nimport type { NoteControls, QualityProfile, SynthOptions } from '../core/synth.js';\nimport type { TuningOptions } from '../core/tuning.js';\nexport type { ADSR, LFO, LFOInput, LegacyLFO, LegacyLFOV5, LFOTargets, LFOTargetsInput, KeyScale, Operator, OperatorWaveform, Voice, VoiceInput, FrozenVoice, FrozenOperator, NormalizedVoice, PreparedVoice, PitchEnvelope, LegacyVoiceV3, LegacyVoiceV4, LegacyVoiceV5, LegacyVoiceV6 } from '../voices/schema.js';\nexport type { NoteControls, SynthOptions, QualityProfile } from '../core/synth.js';\nexport type { TuningOptions, NormalizedTuning } from '../core/tuning.js';\nexport { createEffects } from './fx.js';\nexport type { OpmEffects, EffectsOptions, EffectsEvent } from './fx.js';\nexport { createStereoEffects, applyEffects } from '../core/fx.js';\nexport type { StereoEffects, StereoEffectsOptions, ChorusOptions, ReverbOptions } from '../core/fx.js';\nexport { playSequence, streamSequence } from './sequence.js';\nexport type { PlaySequenceOptions, SequencePlayback, SequenceStreamOptions, SequenceStream } from './sequence.js';\nexport type { SequenceEvent, SequenceNoteEvent, SequenceStopEvent, SequenceControlEvent } from '../core/sequence.js';\nexport { createTransport, beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, normalizeTempoMap, quantizeBeat, swingBeat, swingBeatEvents } from './transport.js';\nexport type { BeatSequenceEvent, TransportLoop, TransportState, TransportOptions, TransportSnapshot, MusicalTransport, TempoPoint, TimeSignature, BarBeat, BeatQuantization } from './transport.js';\nexport { createArrangement } from './arrangement.js';\nexport type { Arrangement, ArrangementLayer, ArrangementSection, ArrangementOptions, ArrangementChangeOptions, ArrangementGainOptions, ArrangementState, ArrangementSnapshot } from './arrangement.js';\nexport { createMidiAdapter, requestMidiAccess } from './midi.js';\nexport type { MidiAdapter, MidiAdapterOptions, MidiAdapterSnapshot, MidiAccessLike, MidiInputLike, MidiMessageEventLike, MidiNavigatorLike, MidiControllerMapping, MidiScalarControl, MidiOperatorControl } from './midi.js';\nexport { importMidiFile, exportMidiFile, MAX_MIDI_FILE_BYTES, MAX_MIDI_FILE_TRACKS, MAX_MIDI_FILE_EVENTS } from '../core/midi-file.js';\nexport type { MidiFileWarningCode, MidiFileWarning, MidiImportOptions, MidiImportResult, MidiExportOptions, MidiFileControlKind, MidiFilePreservedControl, MidiFileLossSummary } from '../core/midi-file.js';\nexport type { MidiVoiceMap } from '../core/midi-state.js';\nexport { gmProgramVoices, gmDrumVoices } from '../voices/gm-map.js';\nexport { parseScoreProject, serializeScoreProject, compileBeatSequence, MAX_SCORE_PROJECT_BYTES, MAX_SCORE_PROJECT_VOICES, parseArrangementProject, serializeArrangementProject, MAX_ARRANGEMENT_PROJECT_BYTES, MAX_ARRANGEMENT_PROJECT_VOICES } from '../core/project.js';\nexport type { ScoreProject, ScoreProjectSettings, BeatSequenceOptions, ArrangementProject } from '../core/project.js';\nexport type { ArrangementDefinition } from '../core/arrangement-definition.js';\nexport { VERSION } from '../version.js';\nexport { createPerformance } from './performance.js';\nexport type { Performance, PerformanceOptions, PerformancePartOptions, PerformancePartControls, PerformanceNoteOptions, PerformanceNoteOffOptions, PerformanceKeySnapshot, PerformancePartSnapshot } from './performance.js';\nexport { renderSequenceInWorker } from './render-worker.js';\nexport type { WavSink, WorkerRenderOptions, WorkerRenderProgress, WorkerRenderResult, WorkerRenderPhase, WorkerRenderPhaseStatus, WorkerRenderPhaseDiagnostics } from './render-worker.js';\nexport type NoteState = 'accepted' | 'started' | 'released' | 'ended' | 'stolen' | 'cancelled' | 'rejected';\nexport interface NoteEvent {\n type: 'note';\n id: number;\n state: NoteState;\n reason?: string;\n /** Actual AudioContext sample frame and seconds at admission, dispatch or completion. */\n frame: number;\n time: number;\n}\nexport interface DiagnosticsEvent {\n type: 'diagnostics';\n requestId: number;\n activeVoices: number;\n pendingEvents: number;\n errors: number;\n rejectedNotes: number;\n}\nexport interface ErrorEvent {\n type: 'error';\n error: Error;\n}\nexport type CommandName = 'stop' | 'updateNote' | 'allNotesOff' | 'panic' | 'setMixGain' | 'setTuning';\nexport interface CommandEvent {\n type: 'command';\n command: CommandName;\n commandId?: number;\n id?: number;\n /** Accepted means admitted or immediately applied, not a future execution guarantee. */\n state: 'accepted' | 'rejected';\n reason?: string;\n frame: number;\n time: number;\n}\nexport type ContextState = AudioContextState | 'interrupted';\nexport interface ContextEvent {\n type: 'context';\n state: ContextState;\n frame: number;\n time: number;\n}\nexport interface ResetEvent {\n type: 'reset';\n reason: 'close' | 'failure' | 'panic' | 'interruption';\n frame: number;\n time: number;\n /** Initiating panic command, when available; independent of receipt-cache eviction. */\n commandId?: number;\n}\nexport type OPMEvent = NoteEvent | DiagnosticsEvent | CommandEvent | ContextEvent | ResetEvent | ErrorEvent;\nexport interface CommandWaitOptions {\n /** Milliseconds, default 5000; an integer in 1..60000. */\n timeout?: number;\n signal?: AbortSignal;\n}\n/** Admission rejection, not an audio execution/completion result. */\nexport declare class CommandRejectedError extends Error {\n readonly event: Readonly\u003cCommandEvent>;\n constructor(event: CommandEvent);\n}\nexport interface OPMOptions {\n sampleRate?: number;\n /** Borrowed context: OPM never closes or suspends it. */\n context?: AudioContext;\n /** Omit to connect to context.destination; null disables automatic connection. */\n destination?: AudioNode | null;\n /** Same-origin HTTPS (or secure loopback HTTP) module; default stays relative to this API module. */\n workletUrl?: string | URL;\n mixGain?: number;\n tuning?: TuningOptions;\n stealing?: SynthOptions['stealing'];\n /** Immutable synthesis profile; standard preserves the default sound. */\n quality?: QualityProfile;\n /** Logical polyphony, an integer in 1..32; default 8. Release tails occupy voices. */\n maxVoices?: number;\n /** Cancel all voices/events on interruption, or preserve direct-note state until resume. */\n interruption?: 'cancel' | 'preserve';\n onEvent?: (event: OPMEvent) => void;\n}\ninterface PlayNoteBase {\n voice?: string | VoiceInput;\n note: number;\n /** A late start keeps its full duration; drop rejects even if processing is delayed. */\n late?: 'start' | 'drop';\n /** null (the default) holds until stop; a number sets a duration before release. */\n duration?: number | null;\n velocity?: number;\n pan?: number;\n /** Admission importance, integer 0..127; larger values cannot be stolen by lower-priority notes. Default 0. */\n voicePriority?: number;\n}\nexport type PlayNoteOptions = PlayNoteBase & (\n/** Relative delay in seconds; cannot be combined with at. */\n{\n time?: number;\n at?: never;\n} | \n/** Absolute AudioContext seconds, at most 60 seconds ahead. Past times are allowed. */\n{\n at: number;\n time?: never;\n});\nexport interface ScheduledNoteOptions {\n /** Absolute AudioContext seconds. Stop without at is immediate, including cancellation. */\n at?: number;\n}\n/** Explicit cancellation is immediate-only; ordinary stops preserve tail automation. */\nexport type StopOptions = ScheduledNoteOptions & {\n cancelControls?: never;\n} | {\n at?: never;\n cancelControls?: boolean;\n};\n/** Browser-facing facade. Import Synth from ../core/synth.js for offline rendering. */\nexport declare class OPM {\n readonly sampleRate: number | undefined;\n get quality(): QualityProfile;\n get maxVoices(): number;\n /** Defensive map snapshot; loaded patches are deeply frozen. Use loadVoice to replace one. */\n get voices(): ReadonlyMap\u003cstring, NormalizedVoice>;\n readonly context: AudioContext | null;\n readonly node: AudioWorkletNode | null;\n onEvent?: (event: OPMEvent) => void;\n constructor(options?: OPMOptions);\n loadVoice(name: string, voice: VoiceInput): void;\n /** Atomically replaces the registry. Only an empty plain array (not JSON \"[]\") clears it. */\n replaceVoiceBank(source: string | readonly CompleteVoiceInput[]): void;\n removeVoice(name: string): boolean;\n /** Canonical versioned entries, detached from the immutable registry and UTF-8 bounded. */\n exportVoiceBank(): string;\n /** Independent subscription; close preserves it for restart, dispose removes it permanently. */\n subscribe(listener: (event: OPMEvent) => void): () => void;\n start(): Promise\u003cvoid>;\n resume(): Promise\u003cvoid>;\n connect(destination: AudioNode): this;\n disconnect(destination?: AudioNode): this;\n playNote(options: PlayNoteOptions): number;\n stop(id: number, options?: StopOptions): number;\n /** Pending updates apply at onset. At equal frames stop precedes onset, then controls. */\n updateNote(id: number, controls: NoteControls, options?: ScheduledNoteOptions): number;\n /** Cancel all pending events and release all active gates, preserving release tails. */\n allNotesOff(): number;\n /** Immediate silence, including release/stealing tails; routing and patch cache survive. */\n panic(): number;\n setMixGain(gain: number): number;\n setTuning(tuning: TuningOptions): number;\n /** Wait for admission only. At most 128 receipts and 64 live waits; live waits prevent eviction. */\n waitForCommand(commandId: number, options?: CommandWaitOptions): Promise\u003cCommandEvent>;\n getDiagnostics(): Promise\u003cDiagnosticsEvent>;\n close(): Promise\u003cvoid>;\n /** Permanently closes OPM and releases host callbacks; never closes a borrowed context. */\n dispose(): Promise\u003cvoid>;\n}\nexport interface LookaheadWindow {\n /** Half-open absolute AudioContext time window. Missed windows are skipped after a stall. */\n from: number;\n to: number;\n /** Maximum notes the callback may return in this batch. */\n maxNotes: number;\n}\nexport type LookaheadNote = PlayNoteOptions & {\n at: number;\n duration: number;\n};\nexport interface LookaheadOptions {\n /** Seconds, default 0.2; bounded to 0.02..10 and greater than interval. */\n horizon?: number;\n /** Seconds between pumps, default 0.025; bounded to 0.005..1. */\n interval?: number;\n /** Maximum batch size, default 32; bounded to 1..128. */\n maxNotes?: number;\n /** Timer/callback failures stop scheduling and are delivered here. */\n onError: (error: Error) => void;\n}\nexport interface LookaheadScheduler {\n readonly running: boolean;\n /** Call from a user gesture; initializes/resumes OPM, then fills the first window. */\n start(): Promise\u003cvoid>;\n /** Cancels pending notes and releases this scheduler's active gates, not other notes. */\n stop(): void;\n /** Stops and permanently prevents restart; does not close OPM. */\n dispose(): void;\n}\n/** Bounded timer-driven admission, not timer-driven note release. Audio frames own all gates. */\nexport declare function createLookaheadScheduler(opm: OPM, schedule: (window: LookaheadWindow) => readonly LookaheadNote[], options: LookaheadOptions): LookaheadScheduler;\n","href":"./api.html#declaration-apiindexdts"},{"title":"Declarations · api/midi.d.ts","text":"import type { Performance } from './performance.js';\nimport type { MidiVoiceMap } from '../core/midi-state.js';\nexport type MidiScalarControl = 'pitch' | 'expression' | 'gain' | 'pan' | 'modulation' | 'feedback' | 'lfoRate' | 'amDepth' | 'pmDepth';\nexport type MidiOperatorControl = 'operatorLevels' | 'operatorRatios' | 'operatorFrequencies';\ninterface MidiControllerRange {\n /** CC number 0..127, excluding RPN selection/pedal/reset/panic CC64/100/101/120/121/123. */\n controller: number;\n /** CC0 maps to min and CC127 to max; both endpoints must be valid engine controls. */\n min: number;\n max: number;\n /** Seconds 0..10; pitch uses glide, other fields use the engine control ramp. */\n ramp: number;\n}\n/** One CC targets one scalar or one operator element; no executable callbacks are accepted. */\nexport type MidiControllerMapping = MidiControllerRange & ({\n field: MidiScalarControl;\n reset?: number;\n} | {\n field: MidiOperatorControl;\n operator: 0 | 1 | 2 | 3;\n reset?: number | null;\n});\n/** Structural subset of Web MIDI, so hosts and tests can inject a real or simulated access object. */\nexport interface MidiMessageEventLike {\n readonly data: Uint8Array | null;\n}\nexport interface MidiInputLike {\n readonly id: string;\n readonly name?: string | null;\n readonly state?: string;\n readonly connection?: string;\n open?(): Promise\u003cunknown>;\n addEventListener(type: 'midimessage', listener: (event: MidiMessageEventLike) => void): void;\n removeEventListener(type: 'midimessage', listener: (event: MidiMessageEventLike) => void): void;\n}\nexport interface MidiAccessLike {\n readonly inputs: ReadonlyMap\u003cstring, MidiInputLike>;\n addEventListener(type: 'statechange', listener: () => void): void;\n removeEventListener(type: 'statechange', listener: () => void): void;\n}\nexport interface MidiNavigatorLike {\n requestMIDIAccess?(options?: {\n sysex?: boolean;\n software?: boolean;\n }): Promise\u003cMidiAccessLike>;\n}\nexport interface MidiAdapterOptions {\n /** Channels 1..16 map to zero-based Performance parts below this count; default 16. */\n parts?: number;\n /** Semitones for full pitch-bend deflection, 0..48; default 2. */\n pitchBendRange?: number;\n /** Opt-in program 0..127 to named voice selection for later note onsets. */\n programVoices?: MidiVoiceMap;\n /** Restrict to these stable port IDs; default every currently and subsequently connected input. */\n inputIds?: readonly string[];\n /** At most 128 own-data mappings. Ordinary CC defaults are overridden only for listed CCs. */\n controllerMap?: readonly MidiControllerMapping[];\n onError?: (error: Error) => void;\n}\nexport interface MidiAdapterSnapshot {\n readonly inputs: readonly string[];\n readonly heldKeys: number;\n /** Messages outside the supported channel-voice subset or malformed packets. */\n readonly ignoredMessages: number;\n readonly disposed: boolean;\n}\nexport interface MidiAdapter {\n readonly snapshot: MidiAdapterSnapshot;\n /** Releases only keys owned by this adapter; unrelated Performance parts and host notes stay untouched. */\n releaseAll(): void;\n dispose(): void;\n}\n/**\n * Request MIDI access without SysEx. Call from an explicit user action; browsers may show a permission prompt.\n * Importing this module never requests access.\n */\nexport declare function requestMidiAccess(host?: MidiNavigatorLike | undefined): Promise\u003cMidiAccessLike>;\n/**\n * Map channel-voice MIDI from user-granted inputs onto Performance parts. It is an adapter, not a MIDI driver:\n * no SysEx, clock or hardware-specific behavior is implemented.\n *\n * Note-off releases the oldest still-held key of the same input/channel/pitch. CC1, channel pressure and polyphonic\n * pressure all scale LFO depth 1x..2x (never below the patch default). CC7/CC11 multiply into part expression, CC10 pans,\n * CC64 is sustain, CC120/123 release only this adapter's keys for the channel, and CC121 resets its controllers.\n */\nexport declare function createMidiAdapter(performance: Performance, access: MidiAccessLike, options?: MidiAdapterOptions): MidiAdapter;\nexport {};\n","href":"./api.html#declaration-apimididts"},{"title":"Declarations · api/performance.d.ts","text":"import type { OPM } from './index.js';\nimport type { PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { NoteControls } from '../core/synth.js';\nexport interface PerformanceOptions {\n /** Zero-based part count, 1..16; default 16. */\n parts?: number;\n /** Total physical/pedal key records, 1..128; default 128. */\n maxKeys?: number;\n /** Per-part key records, 1..128; default 128, also subject to maxKeys. */\n maxKeysPerPart?: number;\n /** Event-driven cleanup failures; synchronous operation failures still throw. */\n onError?: (error: Error) => void;\n}\nexport interface PerformancePartControls {\n glide?: number;\n pan?: number;\n expression?: number;\n}\nexport interface PerformancePartOptions extends PerformancePartControls {\n voice: string | VoiceInput;\n mode: 'poly' | 'mono';\n legato: boolean;\n priority: 'last' | 'high' | 'low';\n /** Optional owned-voice budget, including audible release tails, 1..32. */\n voiceLimit?: number;\n /** Admission importance, 0..127; larger values protect against lower-priority notes. */\n voicePriority?: number;\n}\nexport interface PerformanceNoteOptions {\n velocity?: number;\n}\nexport interface PerformanceNoteOffOptions {\n force?: boolean;\n}\nexport interface PerformanceKeySnapshot {\n readonly key: number;\n readonly note: number;\n readonly velocity: number;\n readonly held: boolean;\n /** Only the currently sounding mono key owns its shared gate. */\n readonly gateId: number | null;\n}\nexport interface PerformancePartSnapshot {\n readonly voice: string | PreparedVoice;\n readonly mode: 'poly' | 'mono';\n readonly legato: boolean;\n readonly priority: 'last' | 'high' | 'low';\n readonly glide: number;\n readonly pan: number;\n readonly expression: number;\n readonly sustain: boolean;\n readonly voiceLimit: number | undefined;\n readonly voicePriority: number;\n readonly selectedKey: number | null;\n readonly keys: readonly PerformanceKeySnapshot[];\n}\nexport interface Performance {\n /** preserveNotes keeps existing gates when only the voice changes; default false. */\n configurePart(part: number, options: Partial\u003cPerformancePartOptions>, policy?: {\n preserveNotes?: boolean;\n }): void;\n updatePart(part: number, controls: PerformancePartControls): void;\n /** Update one physical key; inactive mono keys store controls without touching the selected gate. */\n updateKey(part: number, key: number, controls: NoteControls): boolean;\n /** Update owned sounding notes and defaults for future notes, including release tails. */\n updatePartNotes(part: number, controls: NoteControls): void;\n /** Independent key identity, not an OPM admission receipt. OPM must already be started. */\n noteOn(part: number, note: number, options?: PerformanceNoteOptions): number;\n noteOff(part: number, key: number, options?: PerformanceNoteOffOptions): boolean;\n sustain(part: number, on: boolean): void;\n allNotesOff(part?: number): void;\n getPart(part: number): PerformancePartSnapshot;\n /** Detached effective part defaults, including resolved voice operator controls (not per-key overrides). */\n getPartControls(part: number): Readonly\u003cNoteControls>;\n dispose(): void;\n}\n/** Device-agnostic key policy. No context, timers, MIDI driver or global panic is created. */\nexport declare function createPerformance(opm: OPM, options?: PerformanceOptions): Performance;\n","href":"./api.html#declaration-apiperformancedts"},{"title":"Declarations · api/render-worker.d.ts","text":"import type { ChunkedSequenceOptions, SequenceCapacity, SequenceEvent } from '../core/sequence.js';\nimport type { WavFormat } from '../core/wav.js';\nexport interface WavSink {\n /** Takes ownership of bytes; transferring/detaching their buffer is supported. */\n write(bytes: Uint8Array): Promise\u003cvoid> | void;\n close?(): Promise\u003cvoid> | void;\n /** Must invalidate outstanding writes. Called once on failure, without waiting for them. */\n abort?(reason: unknown): Promise\u003cvoid> | void;\n}\nexport interface WorkerRenderProgress {\n readonly frames: number;\n readonly totalFrames: number;\n readonly bytesWritten: number;\n readonly errors: number;\n}\nexport type WorkerRenderPhase = 'initializing' | 'rendering' | 'writing' | 'closing' | 'completed' | 'cancelled' | 'failed';\nexport interface WorkerRenderPhaseStatus {\n readonly phase: WorkerRenderPhase;\n /** Host-local monotonic wall time, not a DSP benchmark or network trace. */\n readonly elapsedMs: number;\n readonly phaseElapsedMs: number;\n readonly frames: number;\n readonly totalFrames: number;\n readonly bytesWritten: number;\n readonly errors: number;\n}\nexport interface WorkerRenderPhaseDiagnostics {\n readonly status: 'completed' | 'cancelled' | 'failed';\n readonly initializingMs: number;\n readonly renderingMs: number;\n readonly writingMs: number;\n readonly closingMs: number;\n readonly totalMs: number;\n}\nexport interface WorkerRenderOptions extends ChunkedSequenceOptions {\n format?: WavFormat;\n sink: WavSink;\n onProgress?: (progress: WorkerRenderProgress) => void;\n /** Optional module-ready deadline only; integer milliseconds in 1..2147483647. */\n startupTimeoutMs?: number;\n /** Retain four bounded host-local phase totals in the successful result. */\n phaseDiagnostics?: boolean;\n /** Synchronous observer. Throwing fails the render and invalidates the sink. */\n onPhase?: (status: WorkerRenderPhaseStatus) => void;\n /** Reviewed same-origin secure HTTP(S) static module, never a blob/data URL. */\n workerUrl?: string | URL;\n}\nexport interface WorkerRenderResult {\n readonly capacity: SequenceCapacity;\n readonly format: WavFormat;\n readonly bytesWritten: number;\n readonly diagnostics: Readonly\u003c{\n errors: number;\n processedEvents: number;\n renderedFrames: number;\n phases?: WorkerRenderPhaseDiagnostics;\n }>;\n}\n/** Browser-only, backpressured offline rendering; importing this module allocates nothing. */\nexport declare function renderSequenceInWorker(events: readonly SequenceEvent[], options: WorkerRenderOptions): Promise\u003cWorkerRenderResult>;\n","href":"./api.html#declaration-apirender-workerdts"},{"title":"Declarations · api/sequence.d.ts","text":"import type { OPM } from './index.js';\nimport type { SequenceEvent } from '../core/sequence.js';\nexport interface PlaySequenceOptions {\n /** Absolute AudioContext origin; omitted means currentTime at submission. */\n at?: number;\n}\nexport interface SequencePlayback {\n /** Score IDs mapped to submitted OPM IDs, not an acknowledgement of admission. */\n readonly ids: ReadonlyMap\u003cnumber, number>;\n /** Release started notes and immediately cancel pending onsets. Idempotent. */\n stop(): void;\n}\n/** Submit one bounded, wholly validated score; lifecycle acknowledgements remain OPM events. */\nexport declare function playSequence(opm: OPM, events: readonly SequenceEvent[], options?: PlaySequenceOptions): SequencePlayback;\nexport interface SequenceStreamOptions {\n /** Absolute origin, at most60 seconds ahead; defaults to currentTime at start. */\n at?: number;\n /** Lookahead seconds in0.01..10, default0.2. */\n horizon?: number;\n /** Timer interval seconds in0.001..horizon/2, default0.025 (or horizon/2). */\n interval?: number;\n /** Maximum own queued commands, integer1..256; default256. */\n maxSlots?: number;\n signal?: AbortSignal;\n onError?: (error: Error) => void;\n}\nexport interface SequenceStream {\n readonly running: boolean;\n /** Live/pending owned score IDs only; terminal IDs are pruned. Detached snapshot. */\n readonly ids: ReadonlyMap\u003cnumber, number>;\n /** One-shot start. stop/dispose/reset/interruption permanently cancel this score. */\n start(): Promise\u003cvoid>;\n /** Optional explicit pump for hosts driving their own clock; timers still run. */\n pump(): void;\n stop(): void;\n dispose(): void;\n}\n/**\n * Stream a fully validated long score in bounded mixed-event windows.\n * A missed onset/window or rejected command stops the stream instead of silently retiming.\n */\nexport declare function streamSequence(opm: OPM, events: readonly SequenceEvent[], options?: SequenceStreamOptions): SequenceStream;\n","href":"./api.html#declaration-apisequencedts"},{"title":"Declarations · api/transport.d.ts","text":"import type { OPM } from './index.js';\nimport type { BeatSequenceEvent } from '../core/sequence.js';\nimport type { TempoPoint, TimeSignature, BarBeat } from '../core/transport.js';\nexport { beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, normalizeTempoMap, quantizeBeat, swingBeat } from '../core/transport.js';\nexport type { TempoPoint, TimeSignature, BarBeat, BeatQuantization } from '../core/transport.js';\nexport type { BeatSequenceEvent } from '../core/sequence.js';\nexport interface TransportLoop {\n enabled: boolean;\n from: number;\n to: number;\n}\nexport type TransportState = 'stopped' | 'starting' | 'running' | 'paused' | 'disposed';\nexport interface TransportOptions {\n bpm?: number;\n tempoMap?: readonly TempoPoint[];\n timeSignature?: TimeSignature;\n loop?: TransportLoop;\n horizon?: number;\n interval?: number;\n /** Future audio anchor in seconds for start and reconstruction; default min(0.05, horizon / 2), range 0..10. */\n startupLead?: number;\n maxSlots?: number;\n onError?: (error: Error) => void;\n}\nexport interface TransportSnapshot {\n readonly position: number;\n readonly musicalPosition: Readonly\u003cBarBeat>;\n readonly state: TransportState;\n readonly running: boolean;\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n readonly loop: Readonly\u003cTransportLoop>;\n}\nexport interface MusicalTransport {\n readonly position: number;\n readonly running: boolean;\n readonly state: TransportState;\n readonly snapshot: TransportSnapshot;\n readonly ids: ReadonlyMap\u003cnumber, number>;\n start(): Promise\u003cvoid>;\n resume(): Promise\u003cvoid>;\n pause(): void;\n stop(): void;\n seek(beat: number): void;\n /** Replace the tempo from the current musical position onward; retain preceding points. */\n setTempo(bpm: number): void;\n setTempoMap(map: readonly TempoPoint[]): void;\n setLoop(loop: TransportLoop): void;\n pump(): void;\n dispose(): void;\n}\n/** Internal shared preparation: beat durations remain beats until admitted to the audio clock. */\nexport declare function prepareBeatEvents(opm: OPM, beatEvents: readonly BeatSequenceEvent[]): import(\"../core/sequence.js\").SequenceSnapshot;\n/** Swing note starts AND ends, so adjacent gates retain their musical ordering. */\nexport declare function swingBeatEvents(events: readonly BeatSequenceEvent[], subdivision?: number, ratio?: number): BeatSequenceEvent[];\n/** Restartable beat transport. Only owned IDs are ever stopped; the shared engine is not closed. */\nexport declare function createTransport(opm: OPM, beatEvents: readonly BeatSequenceEvent[], options?: TransportOptions): MusicalTransport;\n","href":"./api.html#declaration-apitransportdts"},{"title":"Declarations · core/algorithms.d.ts","text":"export interface AlgorithmGraph {\n readonly inputs: readonly (readonly number[])[];\n readonly carriers: readonly number[];\n}\nexport declare const ALGORITHMS: readonly [AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph, AlgorithmGraph];\nexport declare function feedbackPhase(previous: number, older: number, amount: number): number;\n","href":"./api.html#declaration-corealgorithmsdts"},{"title":"Declarations · core/arrangement-definition.d.ts","text":"import type { BeatSequenceEvent, SequenceSnapshot, SequenceVoices } from './sequence.js';\nimport type { TempoPoint, TimeSignature } from './transport.js';\nexport interface ArrangementLayer {\n name: string;\n /** Loop length in quarter notes, 1/1024 \u003c= length \u003c= 256; aligned to the global beat grid. */\n length: number;\n /** Note onsets lie inside [0, length). Durations and owned controls may cross a loop boundary. */\n events: readonly BeatSequenceEvent[];\n /** Admission importance 0..127, combined with each note's value by taking the maximum. */\n voicePriority?: number;\n /** Per-layer gain 0..1, default 1; independent of authored note expression. */\n gain?: number;\n}\nexport interface ArrangementSection {\n name: string;\n layers: readonly string[];\n}\n/** Musical definition shared by projects and live options, without scheduler callbacks or audio state. */\nexport interface ArrangementDefinition {\n readonly layers: readonly ArrangementLayer[];\n readonly sections: readonly ArrangementSection[];\n readonly initialSection: string;\n readonly tempoMap?: readonly TempoPoint[];\n readonly timeSignature?: TimeSignature;\n}\ninterface NormalizedArrangementDefinition extends ArrangementDefinition {\n readonly layers: readonly Readonly\u003cRequired\u003cArrangementLayer>>[];\n readonly sections: readonly Readonly\u003cArrangementSection>[];\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n}\nexport declare const MAX_ARRANGEMENT_LAYERS = 16;\nexport declare const MAX_ARRANGEMENT_SECTIONS = 32;\nexport declare const MAX_ARRANGEMENT_LAYER_LENGTH = 256;\nexport declare function arrangementName(value: unknown, label: string): string;\n/** One beat-event boundary for portable projects and live arrangements. */\nexport declare function prepareBeatSequence(events: readonly BeatSequenceEvent[], voices?: SequenceVoices): {\n events: readonly BeatSequenceEvent[];\n score: SequenceSnapshot;\n};\n/** Reject definitions that cannot be converted through their complete tempo map. */\nexport declare function validateBeatHorizon(events: readonly BeatSequenceEvent[], map: readonly Readonly\u003cTempoPoint>[]): void;\n/** Validate all references and resource bounds before a scheduler can own any notes. */\nexport declare function prepareArrangementDefinition(input: unknown, voices?: SequenceVoices): {\n definition: NormalizedArrangementDefinition;\n scores: readonly SequenceSnapshot[];\n};\nexport {};\n","href":"./api.html#declaration-corearrangement-definitiondts"},{"title":"Declarations · core/decimator.d.ts","text":"export type QualityProfile = 'eco' | 'standard' | 'high';\nexport declare const DECIMATOR_STATE_SIZE: number;\nexport declare function qualityOversample(quality: QualityProfile): number;\nexport declare function createDecimatorCoefficients(sampleRate: number, quality?: QualityProfile): Float64Array;\nexport declare function decimateSample(input: number, state: Float64Array, coefficients: Float64Array): number;\n/** Largest buffered sample magnitude, excluding counter/position bookkeeping. */\nexport declare function decimatorPeak(state: Float64Array): number;\n","href":"./api.html#declaration-coredecimatordts"},{"title":"Declarations · core/envelope.d.ts","text":"import type { ADSR } from '../voices/schema.js';\nexport interface PreparedEnvelope extends ADSR {\n sustainDB: number;\n sustainGain: number;\n decayEnd: number;\n releaseEnd: number;\n releaseDB: number;\n releaseSpanDB: number;\n}\nexport declare const FLOOR_DB = -96;\nexport declare function prepareEnvelope(a: number, d: number, s: number, r: number, gate: number): PreparedEnvelope;\nexport declare function preparedEnvelopeAt(time: number, gate: number, envelope: PreparedEnvelope): number;\nexport declare function envelopeAt(time: number, gate: number, adsr: ADSR): number;\n","href":"./api.html#declaration-coreenvelopedts"},{"title":"Declarations · core/fx.d.ts","text":"export interface ChorusOptions {\n rate: number;\n depth: number;\n mix: number;\n feedback?: number;\n voices?: number;\n}\nexport interface ReverbOptions {\n size: number;\n damping: number;\n mix: number;\n preDelay?: number;\n width?: number;\n}\nexport interface StereoEffectsOptions {\n chorus?: ChorusOptions;\n reverb?: ReverbOptions;\n order?: 'chorus-reverb' | 'reverb-chorus';\n}\nexport interface StereoEffects {\n process(left: Float32Array, right: Float32Array, offset?: number, length?: number): void;\n /** Replaces the complete configuration; omitted sections fade to bypass. */\n update(params: StereoEffectsOptions): void;\n reset(): void;\n readonly tailSeconds: number;\n readonly params: Readonly\u003cStereoEffectsOptions>;\n}\n/** Internal trust-boundary helper, also used by the worklet protocol. */\nexport declare function effectsData(input: unknown, keys: readonly string[], label: string): Record\u003cstring, unknown>;\nexport declare function normalizeEffectsOptions(input?: StereoEffectsOptions): Readonly\u003cStereoEffectsOptions>;\nexport declare function createStereoEffects(sampleRate: number, options?: StereoEffectsOptions): StereoEffects;\nexport declare function applyEffects(left: Float32Array, right: Float32Array, sampleRate: number, params: StereoEffectsOptions): {\n left: Float32Array;\n right: Float32Array;\n};\n","href":"./api.html#declaration-corefxdts"},{"title":"Declarations · core/index.d.ts","text":"import type { CompleteVoiceInput, FrozenVoice } from '../voices/schema.js';\nimport type { SynthOptions } from './synth.js';\nexport type { ADSR, PitchEnvelope, LFO, LFOInput, LegacyLFO, LegacyLFOV5, LFOTargets, LFOTargetsInput, KeyScale, Operator, OperatorWaveform, Voice, LegacyVoice, LegacyVoiceV2, LegacyVoiceV3, LegacyVoiceV4, LegacyVoiceV5, LegacyVoiceV6, VoiceInput, FrozenVoice, PreparedVoice } from '../voices/schema.js';\nexport type { NoteOptions, NoteControls, VoiceEndReason, SynthOptions, QualityProfile } from './synth.js';\nexport type { TuningOptions, NormalizedTuning } from './tuning.js';\nexport { createStereoEffects, applyEffects } from './fx.js';\nexport type { StereoEffects, StereoEffectsOptions, ChorusOptions, ReverbOptions } from './fx.js';\nexport { normalizeTuning, tuningFrequency } from './tuning.js';\nexport { lfoValue } from './lfo.js';\nexport type { BeatSequenceEvent, SequenceEvent, SequenceNoteEvent, SequenceStopEvent, SequenceControlEvent, SequenceVoices, SequenceOptions, PreparedSequenceEvent, SequenceSnapshot, SequenceCapacity, ChunkedSequenceOptions, SequenceChunk, ChunkedSequenceRender } from './sequence.js';\nexport { prepareSequence, renderSequence, prepareLongSequence, estimateSequenceCapacity, renderSequenceChunks, MAX_SEQUENCE_NOTES, MAX_SEQUENCE_SLOTS, MAX_SEQUENCE_SECONDS, MAX_RENDER_SAMPLES, MAX_LONG_SEQUENCE_SECONDS, MAX_LONG_SEQUENCE_EVENTS, MAX_SEQUENCE_CHUNK_FRAMES, sampleRateValue } from './sequence.js';\nexport type { WavOptions, WavFormat, WavEncoderOptions, WavEncoder, WavChunk } from './wav.js';\nexport type { TempoPoint, TimeSignature, BarBeat } from './transport.js';\nexport { beatsToSeconds, secondsToBeats, beatToBarBeat, barBeatToBeat, normalizeTempoMap } from './transport.js';\nexport { importMidiFile, exportMidiFile, MAX_MIDI_FILE_BYTES, MAX_MIDI_FILE_TRACKS, MAX_MIDI_FILE_EVENTS } from './midi-file.js';\nexport type { MidiFileWarningCode, MidiFileWarning, MidiImportOptions, MidiImportResult, MidiExportOptions, MidiFileControlKind, MidiFilePreservedControl, MidiFileLossSummary } from './midi-file.js';\nexport type { MidiVoiceMap } from './midi-state.js';\nexport { gmProgramVoices, gmDrumVoices } from '../voices/gm-map.js';\nexport { parseScoreProject, serializeScoreProject, compileBeatSequence, MAX_SCORE_PROJECT_BYTES, MAX_SCORE_PROJECT_VOICES, parseArrangementProject, serializeArrangementProject, MAX_ARRANGEMENT_PROJECT_BYTES, MAX_ARRANGEMENT_PROJECT_VOICES } from './project.js';\nexport type { ScoreProject, ScoreProjectSettings, BeatSequenceOptions, ArrangementProject } from './project.js';\nexport type { ArrangementLayer, ArrangementSection, ArrangementDefinition } from './arrangement-definition.js';\nexport interface RenderNoteOptions extends SynthOptions {\n voice: CompleteVoiceInput | FrozenVoice;\n note?: number;\n duration?: number;\n velocity?: number;\n pan?: number;\n voicePriority?: number;\n sampleRate?: number;\n}\nexport interface RenderResult {\n /** Same buffer as left, retained for compatibility. */\n samples: Float32Array;\n left: Float32Array;\n right: Float32Array;\n sampleRate: number;\n diagnostics: {\n errors: number;\n };\n}\nexport { envelopeAt } from './envelope.js';\nexport { ALGORITHMS } from './algorithms.js';\nexport { Synth, VoiceAdmissionError, normalizeVoice, prepareVoice, validateMaxVoices, validateNoteControls, validateVoicePriority } from './synth.js';\nexport { encodeWav, createWavEncoder } from './wav.js';\nexport declare const HEADROOM = 0.7;\nexport declare const OVERSAMPLE = 4;\nexport declare function renderNote(options: RenderNoteOptions): RenderResult;\n","href":"./api.html#declaration-coreindexdts"},{"title":"Declarations · core/lfo.d.ts","text":"import type { LFO } from '../voices/schema.js';\n/** Radian phase; sine and triangle start at zero rising, saw at -1, square at +1. */\nexport declare function lfoValue(phase: number, waveform: LFO['waveform']): number;\n","href":"./api.html#declaration-corelfodts"},{"title":"Declarations · core/midi-file.d.ts","text":"import type { BeatSequenceEvent } from './sequence.js';\nimport type { TempoPoint, TimeSignature } from './transport.js';\nimport type { MidiVoiceMap } from './midi-state.js';\nexport declare const MAX_MIDI_FILE_BYTES: number;\nexport declare const MAX_MIDI_FILE_TRACKS = 128;\n/** Counts all wire events, including skipped messages and end-of-track markers. */\nexport declare const MAX_MIDI_FILE_EVENTS = 65536;\nexport type MidiFileWarningCode = 'header-extension' | 'ignored-meta' | 'ignored-sysex' | 'ignored-channel' | 'ignored-program' | 'release-velocity' | 'default-voice' | 'sustain-applied' | 'tempo-conflict' | 'meter-detail' | 'unclosed-notes' | 'release-tail-controls';\nexport interface MidiFileWarning {\n code: MidiFileWarningCode;\n message: string;\n count: number;\n}\nexport type MidiFileControlKind = 'pitch-bend' | 'expression' | 'pan' | 'modulation' | 'channel-pressure' | 'poly-pressure' | 'program-change' | 'pitch-bend-range';\nexport interface MidiFilePreservedControl {\n kind: MidiFileControlKind;\n count: number;\n}\n/** Counts source messages, not the number of per-note events generated by fanout. */\nexport interface MidiFileLossSummary {\n omissions: readonly Readonly\u003cMidiFileWarning>[];\n approximations: readonly Readonly\u003cMidiFileWarning>[];\n preservedControls: readonly Readonly\u003cMidiFilePreservedControl>[];\n}\nexport interface MidiImportOptions {\n /** Zero-based MIDI channels, written as decimal object keys, to OPM voice names. */\n channelVoices?: Readonly\u003cRecord\u003cstring, string>>;\n defaultVoice?: string;\n programVoices?: MidiVoiceMap;\n drumVoices?: MidiVoiceMap;\n /** Unsupported musical/metadata content is reported, or rejects the entire file. */\n unsupported?: 'warn' | 'reject';\n /** Includes keys held by sustain at the final end-of-track tick. */\n unclosedNotes?: 'reject' | 'close-at-end';\n sustain?: 'apply' | 'reject';\n /** Legacy note-only conversion is the default. */\n controls?: 'preserve' | 'omit';\n /** Initial bend range; preserve mode honors RPN 0. Finite semitones in 0..48, default 2. */\n pitchBendRange?: number;\n}\nexport interface MidiImportResult {\n events: BeatSequenceEvent[];\n tempoMap: readonly Readonly\u003cTempoPoint>[];\n timeSignature: Readonly\u003cTimeSignature>;\n warnings: readonly Readonly\u003cMidiFileWarning>[];\n lossSummary: Readonly\u003cMidiFileLossSummary>;\n}\nexport interface MidiExportOptions {\n format?: 0 | 1;\n ppqn?: number;\n tempoMap?: readonly TempoPoint[];\n bpm?: number;\n timeSignature?: TimeSignature;\n /** Named voices to zero-based MIDI channels. Only brass -> channel 0 is implicit. */\n voiceChannels?: Readonly\u003cRecord\u003cstring, number>>;\n controls?: 'preserve' | 'omit';\n /** Must match the import/receiver policy; no RPN is emitted. */\n pitchBendRange?: number;\n}\n/** Strict SMF 0/1 PPQN adapter, not a General MIDI synthesizer or device driver. */\nexport declare function importMidiFile(source: Uint8Array, options?: MidiImportOptions): MidiImportResult;\n/** Deterministic SMF 0/1; expressive conversion is explicit and rejects ambiguous channel ownership. */\nexport declare function exportMidiFile(events: readonly BeatSequenceEvent[], options?: MidiExportOptions): Uint8Array;\n","href":"./api.html#declaration-coremidi-filedts"},{"title":"Declarations · core/midi-state.d.ts","text":"/** Program/drum note numbers 0..127 mapped to named FM voices. */\nexport type MidiVoiceMap = Readonly\u003cRecord\u003cnumber, string>> | ReadonlyMap\u003cnumber, string>;\nexport declare function midiVoiceName(value: unknown): string;\nexport declare function midiVoiceMap(input: unknown): ReadonlyMap\u003cnumber, string>;\nexport interface MidiRpnState {\n msb: number;\n lsb: number;\n semitones: number;\n cents: number;\n range: number;\n}\nexport declare function midiRpnState(range: number): MidiRpnState;\n/** Selection wins over CC mappings; data entry wins only while RPN 0 is selected. */\nexport declare function midiRpnControl(state: MidiRpnState, controller: number, value: number): boolean;\n","href":"./api.html#declaration-coremidi-statedts"},{"title":"Declarations · core/project.d.ts","text":"import type { PreparedVoice } from '../voices/schema.js';\nimport type { BeatSequenceEvent, SequenceEvent, SequenceVoices } from './sequence.js';\nimport type { ArrangementDefinition, ArrangementLayer, ArrangementSection } from './arrangement-definition.js';\nimport type { QualityProfile, SynthOptions } from './synth.js';\nimport type { NormalizedTuning } from './tuning.js';\nimport type { TempoPoint, TimeSignature } from './transport.js';\nexport declare const MAX_SCORE_PROJECT_BYTES: number;\nexport declare const MAX_SCORE_PROJECT_VOICES = 128;\nexport declare const MAX_ARRANGEMENT_PROJECT_BYTES: number;\nexport declare const MAX_ARRANGEMENT_PROJECT_VOICES = 128;\nexport interface BeatSequenceOptions {\n tempoMap?: readonly TempoPoint[];\n bpm?: number;\n voices?: SequenceVoices;\n}\nexport interface ScoreProjectSettings {\n readonly sampleRate: number;\n readonly quality: QualityProfile;\n readonly maxVoices: number;\n readonly mixGain: number;\n readonly tuning: NormalizedTuning;\n readonly stealing: NonNullable\u003cSynthOptions['stealing']>;\n}\n/** Version 1 is self-contained: every note names a patch stored in voices. */\nexport interface ScoreProject {\n readonly version: 1;\n readonly events: readonly BeatSequenceEvent[];\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n readonly voices: Readonly\u003cRecord\u003cstring, PreparedVoice>>;\n readonly settings: ScoreProjectSettings;\n}\n/** Version 1 stores a replayable definition; no live cursor, pending commands or DSP state. */\nexport interface ArrangementProject extends ArrangementDefinition {\n readonly version: 1;\n readonly layers: readonly Readonly\u003cRequired\u003cArrangementLayer>>[];\n readonly sections: readonly Readonly\u003cArrangementSection>[];\n readonly tempoMap: readonly Readonly\u003cTempoPoint>[];\n readonly timeSignature: Readonly\u003cTimeSignature>;\n readonly voices: Readonly\u003cRecord\u003cstring, PreparedVoice>>;\n readonly settings: ScoreProjectSettings;\n}\n/** Compile musical gates by integrating both endpoints; control ramp/glide remain seconds. */\nexport declare function compileBeatSequence(events: readonly BeatSequenceEvent[], options?: BeatSequenceOptions): SequenceEvent[];\n/** Parse strict versioned own-data input, detach all nested data and freeze the snapshot. */\nexport declare function parseScoreProject(source: string | object): ScoreProject;\n/** Canonical compact JSON: stable field order, sorted patch names, normalized defaults. */\nexport declare function serializeScoreProject(project: ScoreProject): string;\n/** Parse a self-contained arrangement using the same definition boundary as createArrangement. */\nexport declare function parseArrangementProject(source: string | object): ArrangementProject;\n/** Canonical compact JSON for a definition, never a live Arrangement object. */\nexport declare function serializeArrangementProject(project: ArrangementProject): string;\n","href":"./api.html#declaration-coreprojectdts"},{"title":"Declarations · core/sequence.d.ts","text":"import type { PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { NoteControls, SynthOptions } from './synth.js';\nimport type { RenderResult } from './index.js';\nexport interface SequenceNoteEvent {\n type: 'note';\n id: number;\n time: number;\n duration: number;\n voice?: string | VoiceInput;\n note: number;\n velocity?: number;\n pan?: number;\n voicePriority?: number;\n}\nexport interface SequenceStopEvent {\n type: 'stop';\n id: number;\n time: number;\n}\nexport interface SequenceControlEvent {\n type: 'control';\n id: number;\n time: number;\n controls: NoteControls;\n}\nexport type SequenceEvent = SequenceNoteEvent | SequenceStopEvent | SequenceControlEvent;\nexport type BeatSequenceEvent = SequenceEvent extends infer E ? E extends SequenceEvent ? Omit\u003cE, 'time'> & {\n beat: number;\n} : never : never;\nexport type SequenceVoices = ReadonlyMap\u003cstring, VoiceInput>;\nexport interface SequenceOptions extends SynthOptions {\n voices?: SequenceVoices;\n sampleRate?: number;\n /** Logical polyphony, integer1..32; default8. Stolen fades remain bounded to eight. */\n maxVoices?: number;\n}\nexport type PreparedSequenceEvent = Readonly\u003cOmit\u003cSequenceNoteEvent, 'voice' | 'velocity' | 'pan' | 'voicePriority'> & {\n voice: PreparedVoice;\n velocity: number;\n pan: number;\n voicePriority: number;\n}> | Readonly\u003cSequenceStopEvent> | Readonly\u003cOmit\u003cSequenceControlEvent, 'controls'> & {\n controls: Readonly\u003cNoteControls>;\n}>;\nexport interface SequenceSnapshot {\n readonly events: readonly PreparedSequenceEvent[];\n readonly noteCount: number;\n /** Notes reserve both onset and automatic release, even if explicitly cancelled. */\n readonly reservedSlots: number;\n readonly endTime: number;\n}\nexport declare const MAX_SEQUENCE_NOTES = 128;\nexport declare const MAX_SEQUENCE_SLOTS = 256;\nexport declare const MAX_SEQUENCE_SECONDS = 60;\nexport declare const MAX_RENDER_SAMPLES = 4000000;\nexport declare const MAX_LONG_SEQUENCE_SECONDS: number;\nexport declare const MAX_LONG_SEQUENCE_EVENTS = 65536;\nexport declare const MAX_SEQUENCE_CHUNK_FRAMES = 65536;\nexport declare function sampleRateValue(value: number): number;\nexport declare function sequenceOwnData(input: unknown, allowed: readonly string[], required: readonly string[], label: string): Record\u003cstring, unknown>;\n/** Validate the entire score without invoking accessors, and detach every patch/control. */\nexport declare function prepareSequence(events: readonly SequenceEvent[], options?: Pick\u003cSequenceOptions, 'voices'>): SequenceSnapshot;\n/** Long scores have a separate input budget; this does not enlarge worklet queues. */\nexport declare function prepareLongSequence(events: readonly SequenceEvent[], options?: Pick\u003cSequenceOptions, 'voices'>): SequenceSnapshot;\nexport type SequenceFrameEvent = {\n frame: number;\n order: number;\n event: PreparedSequenceEvent;\n};\n/** Shared sample-frame ordering, including automatic releases and pending controls. */\nexport declare function sequenceFrameEvents(score: SequenceSnapshot, sampleRate: number, origin?: number): {\n queue: SequenceFrameEvent[];\n length: number;\n};\n/** Peak submissions in any half-open window; includes automatic releases. */\nexport declare function sequenceWindowCapacity(queue: readonly SequenceFrameEvent[], sampleRate: number, horizon: number): {\n peakWindowSlots: number;\n peakWindowNotes: number;\n};\nexport interface ChunkedSequenceOptions extends SequenceOptions {\n /** Integer 1..65536; default4096. Two buffers are reused for the entire render. */\n chunkFrames?: number;\n /** Optional hard cumulative frame budget, checked before allocation/advancement. */\n maxFrames?: number;\n signal?: AbortSignal;\n}\nexport interface SequenceCapacity {\n readonly sampleRate: number;\n readonly frames: number;\n readonly pcmBytes: number;\n readonly chunkFrames: number;\n readonly chunkBytes: number;\n readonly eventCount: number;\n readonly noteCount: number;\n readonly reservedSlots: number;\n readonly fullBufferAllowed: boolean;\n readonly singleBatchAllowed: boolean;\n /** Default0.2s-window preflight eligibility, not guaranteed live worklet admission. */\n readonly streamAllowed: boolean;\n readonly peakWindowSlots: number;\n readonly peakWindowNotes: number;\n readonly limits: Readonly\u003c{\n fullBufferFrames: number;\n batchNotes: number;\n batchSlots: number;\n longEvents: number;\n longSeconds: number;\n chunkFrames: number;\n streamHorizonSeconds: number;\n streamSlots: number;\n streamNotes: number;\n }>;\n}\nexport interface SequenceChunk {\n /** Borrowed arrays, overwritten by the next next(). Copy only if retention is required. */\n readonly left: Float32Array;\n readonly right: Float32Array;\n readonly offset: number;\n readonly frames: number;\n readonly sampleRate: number;\n readonly diagnostics: Readonly\u003c{\n errors: number;\n processedEvents: number;\n renderedFrames: number;\n }>;\n}\nexport interface ChunkedSequenceRender extends IterableIterator\u003cSequenceChunk> {\n readonly capacity: SequenceCapacity;\n readonly diagnostics: Readonly\u003c{\n errors: number;\n processedEvents: number;\n renderedFrames: number;\n }>;\n /** Idempotent. Subsequent next() returns done without advancing the synth. */\n cancel(): void;\n}\n/** Validate and estimate without allocating any PCM or advancing a synth. */\nexport declare function estimateSequenceCapacity(events: readonly SequenceEvent[], options?: ChunkedSequenceOptions): SequenceCapacity;\n/** Fully validate first; each next() advances at most chunkFrames, never allocates full PCM. */\nexport declare function renderSequenceChunks(events: readonly SequenceEvent[], options?: ChunkedSequenceOptions): ChunkedSequenceRender;\n/** Convenience full-buffer rendering retains the original score and allocation budgets. */\nexport declare function renderSequence(events: readonly SequenceEvent[], options?: SequenceOptions): RenderResult;\n","href":"./api.html#declaration-coresequencedts"},{"title":"Declarations · core/synth.d.ts","text":"import type { ADSR, PreparedVoice, VoiceInput } from '../voices/schema.js';\nimport type { TuningOptions } from './tuning.js';\nimport type { QualityProfile } from './decimator.js';\nexport type { QualityProfile } from './decimator.js';\nexport type VoiceEndReason = 'stolen' | 'ended' | 'error' | 'cancelled';\nexport interface NoteOptions {\n velocity?: number;\n pan?: number;\n voicePriority?: number;\n}\nexport interface NoteControls {\n pitch?: number;\n glide?: number;\n expression?: number;\n pan?: number;\n modulation?: number;\n ramp?: number;\n /** Independent per-note amplitude, 0..1 (default 1), multiplied with expression before mix saturation. */\n gain?: number;\n operatorLevels?: readonly [number, number, number, number];\n feedback?: number;\n lfoRate?: number;\n amDepth?: number;\n pmDepth?: number;\n operatorRatios?: readonly [number, number, number, number];\n /** A number enables fixed Hz; null restores the operator's live ratio. Pitch still applies. */\n operatorFrequencies?: readonly [number | null, number | null, number | null, number | null];\n operatorADSR?: readonly [ADSR, ADSR, ADSR, ADSR];\n}\nexport interface SynthOptions {\n mixGain?: number;\n tuning?: TuningOptions;\n stealing?: 'oldest' | 'release-first' | 'quietest';\n quality?: QualityProfile;\n}\n/** A valid note could not displace any higher-priority logical voice. */\nexport declare class VoiceAdmissionError extends Error {\n constructor();\n}\nexport declare function validateMaxVoices(value: unknown): number;\nexport declare function validateVoicePriority(value: unknown): number;\n/** Copy strict own-data controls at the API/dispatch boundary without invoking getters. */\nexport declare function validateNoteControls(input: NoteControls): NoteControls;\nexport { normalizeVoice, prepareVoice } from '../voices/normalize.js';\nexport declare class Synth {\n readonly sampleRate: number;\n readonly maxVoices: number;\n readonly quality: QualityProfile;\n readonly currentFrame: number;\n readonly errorCount: number;\n readonly lastStolenId: number | null;\n onVoiceEnded?: ((id: number, reason: VoiceEndReason) => void) | null;\n constructor(sampleRate: number, maxVoices?: number, options?: SynthOptions);\n setMixGain(gain: number): void;\n setTuning(tuning: TuningOptions): void;\n noteOn(input: VoiceInput | PreparedVoice, note: number, id?: number, options?: NoteOptions): number;\n noteOff(id: number): boolean;\n allNotesOff(): void;\n panic(): void;\n updateNote(id: number, input: NoteControls): boolean;\n render(left: Float32Array, right: Float32Array, offset?: number, length?: number): void;\n}\n","href":"./api.html#declaration-coresynthdts"},{"title":"Declarations · core/transport.d.ts","text":"export interface TempoPoint {\n beat: number;\n bpm: number;\n curve?: 'step' | 'linear';\n endBpm?: number;\n}\nexport interface TimeSignature {\n numerator: number;\n denominator: number;\n}\n/** One-based bars and beats; beat may contain a fractional subdivision. */\nexport interface BarBeat {\n bar: number;\n beat: number;\n}\nexport declare const MAX_TRANSPORT_BEATS = 86400;\nexport declare const MAX_TEMPO_POINTS = 1024;\nexport declare function transportNumber(value: unknown, min: number, max: number, label: string): number;\n/** Read dense own-data arrays without executing element getters or iteration overrides. */\nexport declare function transportArray(input: unknown, max: number, label: string): unknown[];\n/** Strictly increasing quarter-note tempo; linear curves interpolate BPM in beat space. */\nexport declare function normalizeTempoMap(input?: readonly TempoPoint[], bpm?: number): readonly Readonly\u003cTempoPoint>[];\nexport declare function normalizeTimeSignature(input?: TimeSignature): Readonly\u003cTimeSignature>;\n/** BPM at a beat in an already validated map. */\nexport declare function tempoBPM(beat: number, map: readonly Readonly\u003cTempoPoint>[]): number;\n/** Insert a step without changing the elapsed portion of a linear ramp. */\nexport declare function replaceTempoFrom(beat: number, bpm: number, map: readonly Readonly\u003cTempoPoint>[]): readonly Readonly\u003cTempoPoint>[];\n/** Integral of 60/BPM across tempo boundaries. */\nexport declare function beatsToSeconds(beat: number, tempoMap?: readonly TempoPoint[]): number;\n/** Internal validated-map form avoids recopying a map on each scheduler tick. */\nexport declare function tempoSeconds(beat: number, map: readonly Readonly\u003cTempoPoint>[]): number;\nexport declare function secondsToBeats(seconds: number, tempoMap?: readonly TempoPoint[]): number;\nexport declare function tempoBeat(seconds: number, map: readonly Readonly\u003cTempoPoint>[]): number;\nexport declare function beatToBarBeat(beat: number, signature?: TimeSignature): Readonly\u003cBarBeat>;\nexport declare function barBeatToBeat(position: BarBeat, signature?: TimeSignature): number;\nexport type BeatQuantization = 'floor' | 'ceil' | 'nearest' | 'next';\n/** Grid in quarter-note beats; next is strictly later even at an exact boundary. */\nexport declare function quantizeBeat(beat: number, quantum?: number, mode?: BeatQuantization): number;\n/** Warp each pair of subdivisions; ratio .5 is straight, 2/3 is triplet swing. */\nexport declare function swingBeat(beat: number, subdivision?: number, ratio?: number): number;\n","href":"./api.html#declaration-coretransportdts"},{"title":"Declarations · core/tuning.d.ts","text":"export interface TuningOptions {\n referenceHz?: number;\n offsets?: readonly number[];\n}\nexport interface NormalizedTuning {\n readonly referenceHz: number;\n readonly offsets: readonly number[];\n}\n/** Validate own data only and detach the cents table from its caller. */\nexport declare function normalizeTuning(input: TuningOptions): NormalizedTuning;\n/** Fractional MIDI notes interpolate cents, not frequencies, between adjacent keys. */\nexport declare function tuningFrequency(note: number, tuning: NormalizedTuning): number;\n","href":"./api.html#declaration-coretuningdts"},{"title":"Declarations · core/wav.d.ts","text":"export type WavFormat = 'pcm16' | 'pcm24' | 'float32';\nexport interface WavOptions {\n left: Float32Array;\n right?: Float32Array;\n sampleRate: number;\n format?: WavFormat;\n}\nexport interface WavEncoderOptions {\n sampleRate: number;\n channels: 1 | 2;\n format?: WavFormat;\n totalFrames: number;\n}\nexport interface WavChunk {\n left: Float32Array;\n right?: Float32Array;\n}\nexport interface WavEncoder {\n readonly totalFrames: number;\n readonly framesEncoded: number;\n /** Entire file length, including the header and any RIFF alignment byte. */\n readonly byteLength: number;\n readonly finished: boolean;\n /** Emit exactly once, before encoding PCM. */\n header(): Uint8Array;\n /** Atomic accounting: invalid chunks never consume frames. Maximum65536 frames per call. */\n encode(chunk: WavChunk): Uint8Array;\n /** Require exact totalFrames; returns the RIFF alignment byte, or an empty array. */\n finalize(): Uint8Array;\n}\n/** Streaming RIFF32 output with known length; retains no PCM or emitted bytes. */\nexport declare function createWavEncoder(options: WavEncoderOptions): WavEncoder;\n/** Full-buffer WAV convenience export; defaultPCM16, at most4000000 frames. */\nexport declare function encodeWav(input: WavOptions): Uint8Array;\n","href":"./api.html#declaration-corewavdts"},{"title":"Declarations · tools/assets.d.ts","text":"export interface CopyAssetsOptions {\n destination: string;\n /** Advanced: deploy this installed opm.js package directory instead of the one containing this module. */\n packageRoot?: string;\n}\nexport interface AssetDeployment {\n readonly version: string;\n readonly destination: string;\n readonly files: number;\n readonly bytes: number;\n readonly reused: boolean;\n}\nexport interface CheckDeploymentOptions {\n /** Overall deadline, integer milliseconds in 1..60000; default 15000. */\n timeoutMs?: number;\n /** Require production X-Content-Type-Options: nosniff; default true. */\n requireNosniff?: boolean;\n /** Advanced: compare with this installed opm.js package directory instead of the one containing this module. */\n packageRoot?: string;\n}\nexport interface DeploymentCheck {\n readonly version: string;\n readonly baseUrl: string;\n readonly files: number;\n readonly bytes: number;\n readonly limitations: readonly string[];\n}\n/** Node-only. Atomically create a complete release-specific directory; never overwrite host files. */\nexport declare function copyAssets(options: CopyAssetsOptions): Promise\u003cAssetDeployment>;\n/** Fetch and hash the complete installed release; this does not certify page CSP or audible playback. */\nexport declare function checkDeployment(baseUrl: string | URL, options?: CheckDeploymentOptions): Promise\u003cDeploymentCheck>;\n","href":"./api.html#declaration-toolsassetsdts"},{"title":"Declarations · version.d.ts","text":"/** Package version; scripts/security-check.ts fails if it differs from package.json. */\nexport declare const VERSION = \"1.11.1\";\n","href":"./api.html#declaration-versiondts"},{"title":"Declarations · voices/brass.d.ts","text":"import type { Voice } from './schema.js';\nexport declare const brass: Voice;\n","href":"./api.html#declaration-voicesbrassdts"},{"title":"Declarations · voices/dx7.d.ts","text":"import type { Algorithm, Voice } from './schema.js';\nexport interface DX7ImportDescription {\n name: string;\n /** Original DX7 algorithm, 1..32. */\n sourceAlgorithm: number;\n algorithm: Algorithm;\n /** DX7 operator numbers 1..6 in converted OPM signal order. */\n selectedOperators: number[];\n droppedOperators: number[];\n warnings: string[];\n}\n/** Import exactly one standard DX7 single/bank dump as approximate four-op voices. */\nexport declare function importDX7(input: Uint8Array): Voice[];\n/** Report losses separately so imported voices retain the strict normal voice shape. */\nexport declare function describeDX7(input: Uint8Array): DX7ImportDescription[];\n","href":"./api.html#declaration-voicesdx7dts"},{"title":"Declarations · voices/examples.d.ts","text":"import type { Voice } from './schema.js';\nexport declare const examples: readonly Voice[];\n","href":"./api.html#declaration-voicesexamplesdts"},{"title":"Declarations · voices/gm-map.d.ts","text":"/** Artistic GM-family starter mappings, not a General MIDI sound set. */\nexport declare const gmProgramVoices: Readonly\u003cRecord\u003cnumber, string>>;\n/** Partial GM percussion starter; unmapped notes retain the caller fallback. */\nexport declare const gmDrumVoices: Readonly\u003cRecord\u003cnumber, string>>;\n","href":"./api.html#declaration-voicesgm-mapdts"},{"title":"Declarations · voices/normalize.d.ts","text":"import type { NormalizedVoice, PreparedVoice, VoiceInput } from './schema.js';\nexport declare function normalizeVoice(source: VoiceInput): NormalizedVoice;\n/** Validate once and retain a deeply immutable, caller-independent snapshot. */\nexport declare function prepareVoice(input: VoiceInput): PreparedVoice;\n","href":"./api.html#declaration-voicesnormalizedts"},{"title":"Declarations · voices/opm.d.ts","text":"import type { Algorithm, Voice } from './schema.js';\nexport interface OPMImportDescription {\n name: string;\n program: number;\n algorithm: Algorithm;\n /** Source labels in destination operator order. */\n operators: readonly string[];\n warnings: string[];\n}\n/** Import bounded decimal VOPM/MXDRV-family text patches as approximate v7 voices. */\nexport declare function importOPM(source: string | Uint8Array): Voice[];\n/** Report source program metadata and bounded, de-duplicated conversion losses. */\nexport declare function describeOPM(source: string | Uint8Array): OPMImportDescription[];\n","href":"./api.html#declaration-voicesopmdts"},{"title":"Declarations · voices/original.d.ts","text":"import type { Voice } from './schema.js';\nexport declare const originalPresets: readonly Voice[];\n","href":"./api.html#declaration-voicesoriginaldts"},{"title":"Declarations · voices/preset-metadata.d.ts","text":"export interface PresetMetadata {\n readonly family: 'bell' | 'brass' | 'bass' | 'keys' | 'organ' | 'lead' | 'strings' | 'mallet' | 'pluck' | 'reed' | 'pad' | 'metallic' | 'percussion' | 'inharmonic';\n readonly provenance: {\n readonly kind: 'repository-recipe' | 'original-recipe';\n readonly source: string;\n readonly license: 'Apache-2.0';\n readonly copiedEmulatorPatch: false;\n };\n readonly intendedMidi: readonly [number, number];\n readonly intendedVelocity: readonly [number, number];\n /** Extra attenuation before the common audition gain; never an operator-level compensation. */\n readonly hostTrimDb: number;\n readonly purpose: string;\n readonly suggestedPolyphony: number;\n readonly designRationale: string;\n /** Authoring intent and numerical checks are not a listener verdict. */\n readonly listeningStatus: 'unverified';\n}\nexport declare const presetMetadata: Readonly\u003cRecord\u003cstring, PresetMetadata>>;\n/** The separate default brass export is not the bank brass recipe. */\nexport declare const defaultBrassMetadata: PresetMetadata;\n","href":"./api.html#declaration-voicespreset-metadatadts"},{"title":"Declarations · voices/schema.d.ts","text":"export type Algorithm = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\nexport interface ADSR {\n a: number;\n d: number;\n s: number;\n r: number;\n}\nexport type LFOTargets = readonly [number, number, number, number];\nexport type LFOTargetsInput = readonly [number | boolean, number | boolean, number | boolean, number | boolean];\nexport interface LegacyLFO {\n rate: number;\n amDepth: number;\n pmDepth: number;\n waveform?: never;\n delay?: never;\n sync?: never;\n phase?: never;\n amTargets?: never;\n pmTargets?: never;\n}\nexport interface LegacyLFOV4 extends Omit\u003cLegacyLFO, 'waveform'> {\n waveform: 'sine' | 'triangle' | 'saw' | 'square';\n}\nexport interface LegacyLFOV5 extends Omit\u003cLegacyLFOV4, 'delay' | 'sync' | 'phase'> {\n delay?: number;\n sync?: 'note' | 'global';\n phase?: number;\n}\nexport interface LFO extends Omit\u003cLegacyLFOV5, 'amTargets' | 'pmTargets'> {\n amTargets?: LFOTargets;\n pmTargets?: LFOTargets;\n}\nexport type LFOInput = Omit\u003cLFO, 'waveform' | 'amTargets' | 'pmTargets'> & {\n waveform?: LFO['waveform'];\n amTargets?: LFOTargetsInput;\n pmTargets?: LFOTargetsInput;\n};\nexport interface PitchEnvelope {\n a: number;\n d: number;\n r: number;\n initial: number;\n peak: number;\n sustain: number;\n final: number;\n}\nexport interface KeyScale {\n breakpoint: number;\n leftDbPerOctave: number;\n rightDbPerOctave: number;\n}\nexport type OperatorWaveform = 'sine' | 'half' | 'abs' | 'quarter' | 'alternating' | 'camel' | 'square' | 'saw' | 'noise';\nexport interface LegacyOperator {\n ratio: number;\n level: number;\n detune: number;\n adsr: ADSR;\n keyScale?: never;\n velocitySensitivity?: never;\n frequency?: never;\n rateKeyScale?: never;\n waveform?: never;\n noiseRate?: never;\n}\nexport interface LegacyOperatorV2 extends Omit\u003cLegacyOperator, 'keyScale'> {\n keyScale?: KeyScale;\n}\nexport interface LegacyOperatorV3 extends Omit\u003cLegacyOperatorV2, 'velocitySensitivity'> {\n velocitySensitivity?: number;\n}\nexport interface LegacyOperatorV6 extends Omit\u003cLegacyOperatorV3, 'frequency' | 'rateKeyScale'> {\n frequency?: number;\n rateKeyScale?: number;\n}\nexport interface Operator extends Omit\u003cLegacyOperatorV6, 'waveform' | 'noiseRate'> {\n waveform?: OperatorWaveform;\n noiseRate?: number;\n}\nexport type FourOperators\u003cT = Operator> = [T, T, T, T];\ninterface VoiceBase {\n name?: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex?: number;\n}\n/** Strict single-voice input; omitted version uses the current shape. */\nexport type VoiceInput = (VoiceBase & {\n version?: 7;\n lfo?: LFOInput;\n pitchEnvelope?: PitchEnvelope;\n ops: readonly [Operator, Operator, Operator, Operator];\n}) | (VoiceBase & {\n version: 6;\n lfo?: LFOInput;\n pitchEnvelope?: PitchEnvelope;\n ops: readonly [LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6];\n}) | (VoiceBase & {\n version: 5;\n lfo?: Omit\u003cLegacyLFOV5, 'waveform'> & {\n waveform?: LFO['waveform'];\n };\n pitchEnvelope?: PitchEnvelope;\n ops: readonly [LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6, LegacyOperatorV6];\n}) | (VoiceBase & {\n version: 4;\n lfo?: Omit\u003cLegacyLFOV4, 'waveform'> & {\n waveform?: LFO['waveform'];\n };\n pitchEnvelope?: never;\n ops: readonly [LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3];\n}) | (VoiceBase & {\n version: 3;\n lfo?: LegacyLFO;\n pitchEnvelope?: never;\n ops: readonly [LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3, LegacyOperatorV3];\n}) | (VoiceBase & {\n version: 2;\n lfo?: LegacyLFO;\n pitchEnvelope?: never;\n ops: readonly [LegacyOperatorV2, LegacyOperatorV2, LegacyOperatorV2, LegacyOperatorV2];\n}) | (VoiceBase & {\n version: 1;\n lfo?: LegacyLFO;\n pitchEnvelope?: never;\n ops: readonly [LegacyOperator, LegacyOperator, LegacyOperator, LegacyOperator];\n});\nexport interface Voice {\n version: 7;\n name: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex: number;\n lfo: LFO;\n pitchEnvelope?: PitchEnvelope;\n ops: FourOperators;\n}\nexport interface LegacyVoice {\n version: 1;\n name: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex: number;\n lfo: LegacyLFO;\n pitchEnvelope?: never;\n ops: FourOperators\u003cLegacyOperator>;\n}\nexport interface LegacyVoiceV2 extends Omit\u003cLegacyVoice, 'version' | 'ops'> {\n version: 2;\n ops: FourOperators\u003cLegacyOperatorV2>;\n}\nexport interface LegacyVoiceV3 extends Omit\u003cLegacyVoice, 'version' | 'ops'> {\n version: 3;\n ops: FourOperators\u003cLegacyOperatorV3>;\n}\nexport interface LegacyVoiceV4 extends Omit\u003cLegacyVoiceV3, 'version' | 'lfo'> {\n version: 4;\n lfo: LegacyLFOV4;\n}\nexport interface LegacyVoiceV6 extends Omit\u003cVoice, 'version' | 'ops'> {\n version: 6;\n ops: FourOperators\u003cLegacyOperatorV6>;\n}\nexport interface LegacyVoiceV5 extends Omit\u003cLegacyVoiceV6, 'version' | 'lfo'> {\n version: 5;\n lfo: LegacyLFOV5;\n}\nexport interface NormalizedVoice {\n version: 7;\n name?: string;\n algorithm: Algorithm;\n feedback: Algorithm;\n modIndex: number;\n lfo: LFO;\n pitchEnvelope?: PitchEnvelope;\n ops: FourOperators;\n}\nexport type CompleteVoiceInput = Voice | LegacyVoice | LegacyVoiceV2 | LegacyVoiceV3 | LegacyVoiceV4 | LegacyVoiceV5 | LegacyVoiceV6;\nexport type FrozenVoice = Readonly\u003cOmit\u003cVoice, 'lfo' | 'ops' | 'pitchEnvelope'>> & {\n readonly lfo: Readonly\u003cLFO>;\n readonly pitchEnvelope?: Readonly\u003cPitchEnvelope>;\n readonly ops: readonly [FrozenOperator, FrozenOperator, FrozenOperator, FrozenOperator];\n};\nexport type FrozenOperator = Readonly\u003cOmit\u003cOperator, 'adsr' | 'keyScale'>> & {\n readonly adsr: Readonly\u003cADSR>;\n readonly keyScale?: Readonly\u003cKeyScale>;\n};\ndeclare const preparedVoiceBrand: unique symbol;\n/** Immutable validated snapshot. Only prepareVoice can create the trusted identity. */\nexport type PreparedVoice = Readonly\u003cOmit\u003cNormalizedVoice, 'lfo' | 'ops' | 'pitchEnvelope'>> & {\n readonly lfo: Readonly\u003cLFO>;\n readonly pitchEnvelope?: Readonly\u003cPitchEnvelope>;\n readonly ops: readonly [FrozenOperator, FrozenOperator, FrozenOperator, FrozenOperator];\n readonly [preparedVoiceBrand]: true;\n};\nexport { prepareVoice } from './normalize.js';\nexport declare const MAX_BANK_BYTES = 262144;\nexport declare const MAX_BANK_VOICES = 128;\ntype LimitKey = 'noiseRate' | 'ratio' | 'level' | 'detune' | 'velocitySensitivity' | 'frequency' | 'rateKeyScale' | 'a' | 'd' | 's' | 'r' | 'modIndex' | 'rate' | 'amDepth' | 'pmDepth' | 'delay' | 'phase' | 'initial' | 'peak' | 'sustain' | 'final' | 'breakpoint' | 'leftDbPerOctave' | 'rightDbPerOctave';\nexport declare const LIMITS: Readonly\u003cRecord\u003cLimitKey, readonly [number, number]>>;\nexport declare function bounded(value: unknown, min: number, max: number, label?: string): number;\nexport declare function lfoWaveform(value: unknown): LFO['waveform'];\nexport declare function lfoSync(value: unknown): NonNullable\u003cLFO['sync']>;\nexport declare function operatorWaveform(value: unknown): OperatorWaveform;\nexport declare function validateVoice(input: unknown): FrozenVoice;\nexport declare function parseVoiceBank(source: string | readonly unknown[]): Map\u003cstring, FrozenVoice>;\n","href":"./api.html#declaration-voicesschemadts"},{"title":"Declarations · voices/voice.schema.d.ts","text":"export declare const voiceSchema: {\n $schema: string;\n title: string;\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n version: {\n const: number;\n };\n name: {\n type: string;\n pattern: string;\n };\n algorithm: {\n type: string;\n minimum: number;\n maximum: number;\n };\n feedback: {\n type: string;\n minimum: number;\n maximum: number;\n };\n modIndex: {\n type: string;\n minimum: number;\n maximum: number;\n };\n pitchEnvelope: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n a: {\n type: string;\n minimum: number;\n maximum: number;\n };\n d: {\n type: string;\n minimum: number;\n maximum: number;\n };\n r: {\n type: string;\n minimum: number;\n maximum: number;\n };\n initial: {\n type: string;\n minimum: number;\n maximum: number;\n };\n peak: {\n type: string;\n minimum: number;\n maximum: number;\n };\n sustain: {\n type: string;\n minimum: number;\n maximum: number;\n };\n final: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n lfo: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n delay: {\n type: string;\n minimum: number;\n maximum: number;\n default: number;\n };\n sync: {\n type: string;\n enum: string[];\n default: string;\n };\n phase: {\n type: string;\n minimum: number;\n maximum: number;\n default: number;\n };\n amTargets: {\n type: string;\n minItems: number;\n maxItems: number;\n items: {\n type: string;\n minimum: number;\n maximum: number;\n };\n description: string;\n };\n pmTargets: {\n type: string;\n minItems: number;\n maxItems: number;\n items: {\n type: string;\n minimum: number;\n maximum: number;\n };\n description: string;\n };\n waveform: {\n type: string;\n enum: string[];\n };\n rate: {\n type: string;\n minimum: number;\n maximum: number;\n };\n amDepth: {\n type: string;\n minimum: number;\n maximum: number;\n };\n pmDepth: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n ops: {\n type: string;\n minItems: number;\n maxItems: number;\n items: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n waveform: {\n type: string;\n enum: string[];\n default: string;\n };\n noiseRate: {\n type: string;\n minimum: number;\n maximum: number;\n default: number;\n description: string;\n };\n frequency: {\n type: string;\n minimum: number;\n maximum: number;\n description: string;\n };\n rateKeyScale: {\n type: string;\n minimum: number;\n maximum: number;\n description: string;\n };\n velocitySensitivity: {\n type: string;\n minimum: number;\n maximum: number;\n description: string;\n };\n keyScale: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n breakpoint: {\n type: string;\n minimum: number;\n maximum: number;\n };\n leftDbPerOctave: {\n type: string;\n minimum: number;\n maximum: number;\n };\n rightDbPerOctave: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n ratio: {\n type: string;\n minimum: number;\n maximum: number;\n };\n level: {\n type: string;\n minimum: number;\n maximum: number;\n };\n detune: {\n type: string;\n minimum: number;\n maximum: number;\n };\n adsr: {\n type: string;\n additionalProperties: boolean;\n required: string[];\n properties: {\n a: {\n type: string;\n minimum: number;\n maximum: number;\n };\n d: {\n type: string;\n minimum: number;\n maximum: number;\n };\n s: {\n type: string;\n minimum: number;\n maximum: number;\n };\n r: {\n type: string;\n minimum: number;\n maximum: number;\n };\n };\n };\n };\n };\n };\n };\n};\n","href":"./api.html#declaration-voicesvoiceschemadts"}]; diff --git a/src/core/synth.ts b/src/core/synth.ts index 9f1adae..7fa1190 100644 --- a/src/core/synth.ts +++ b/src/core/synth.ts @@ -155,7 +155,7 @@ interface ActiveVoice { operatorLevels: Float64Array; operatorFrom: Float64Array; operatorTargets: Float64Array; operatorStart: number; operatorFrames: number; levels: Float64Array; leftGain: number; rightGain: number; lastSample: number; fadeRemaining: number; - lastFadeGain?: number; carrierGain: number; phases: Float64Array; values: Float64Array; filters: Float64Array; + lastFadeGain?: number; carrierGain: number; phases: Float64Array; values: Float64Array; releaseDb: Float64Array; previous: number; older: number; pitchRelease: number; feedbackScale: number; elapsed: number; releaseTime: number; releaseEnd: number; velocity: number; expression: number; pan: number; @@ -259,7 +259,7 @@ function createVoiceSlot(oversample: number): ActiveVoice { operatorStart: 0, operatorFrames: 0, gains: new Float64Array(4 * oversample), levels: new Float64Array(4), leftGain: 0, rightGain: 0, lastSample: 0, fadeRemaining: 0, lastFadeGain: 1, carrierGain: 0, - phases: new Float64Array(4), values: new Float64Array(4), filters: new Float64Array(DECIMATOR_STATE_SIZE), releaseDb: new Float64Array(4), + phases: new Float64Array(4), values: new Float64Array(4), releaseDb: new Float64Array(4), previous: 0, older: 0, pitchRelease: 0, feedbackScale: 0, elapsed: 0, releaseTime: -1, releaseEnd: -1, velocity: 1, expression: 1, pan: 0, expressionFrom: 1, expressionTarget: 1, expressionStart: 0, expressionFrames: 0, @@ -461,6 +461,15 @@ function voiceAudibility(active: ActiveVoice, sampleRate: number): number { return gain * active.carrierGain * active.velocity * expression * layerGain; } +/** True when the buffered history is (now) exactly zero. With nothing left + * sounding, sub-floor residue is cleared; a live silent voice instead flushes + * the finite FIR to exact zeros so its envelope tail stays intact. */ +function settleDecimator(state: Float64Array, clear: boolean): boolean { + const peak = decimatorPeak(state); + if (peak !== 0 && (!clear || peak >= AMPLITUDE_FLOOR)) return false; + if (peak !== 0) state.fill(0); + return true; +} export class Synth { declare readonly sampleRate: number; @@ -492,6 +501,18 @@ export class Synth { declare sequence: number; /** @internal */ declare decimatorCoefficients: Float64Array; + /** @internal Oversampled stereo mix for the current output frame. */ + declare mixLeft: Float64Array; + /** @internal */ + declare mixRight: Float64Array; + /** @internal Shared post-mix decimator state, one per output channel. */ + declare filterLeft: Float64Array; + /** @internal */ + declare filterRight: Float64Array; + /** @internal True once both decimator states have drained below the floor. */ + declare decimatorIdle: boolean; + /** @internal filterRight is stale and logically equals filterLeft (dual mono). */ + declare decimatorMirrored: boolean; /** @internal Bounded spare slots include one admission scratch slot. */ declare freeVoices: ActiveVoice[]; /** @internal Preallocated notifications are drained after each stable frame. */ @@ -538,6 +559,12 @@ export class Synth { this.subTimes = Float64Array.from({ length: this.oversample }, (_, sub) => sub / (sampleRate * this.oversample)); this.sequence = 0; this.decimatorCoefficients = createDecimatorCoefficients(sampleRate, this.quality); + this.mixLeft = new Float64Array(this.oversample); + this.mixRight = new Float64Array(this.oversample); + this.filterLeft = new Float64Array(DECIMATOR_STATE_SIZE); + this.filterRight = new Float64Array(DECIMATOR_STATE_SIZE); + this.decimatorIdle = true; + this.decimatorMirrored = true; } setMixGain(gain: number): void { validateMixGain(gain); @@ -696,7 +723,6 @@ export class Synth { active.noiseStates.fill(NOISE_SEED); active.noiseTurns.fill(0); active.values.fill(0); - active.filters.fill(0); active.releaseDb.fill(0); active.releaseStep.fill(0); active.gains.fill(0); @@ -761,10 +787,19 @@ export class Synth { panic(): void { while (this.fades.length > 0) this.freeVoices.push(this.fades.pop()!); this.spillLeft = this.spillRight = this.spillRemaining = 0; + this.resetDecimator(); (this as MutableCounters).lastStolenId = null; this.cancelVoices(); } + /** @internal Drop the shared decimator tail, as panic dropped per-voice filters. */ + resetDecimator(): void { + this.filterLeft.fill(0); + this.filterRight.fill(0); + this.decimatorIdle = true; + this.decimatorMirrored = true; + } + /** @internal */ cancelVoices(): void { if (this.voices.length === 0) return; @@ -931,10 +966,17 @@ export class Synth { } // One output frame, with no temporary arrays or objects in the hot path. + // Oversampled samples are panned into mixLeft/mixRight; the shared post-mix + // decimator then runs once per channel instead of once per voice. Returns + // the last undecimated sample (for steal spill) or NaN on corrupt state; + // substeps accumulated before a NaN were finite and stay in the mix. /** @internal */ - renderVoice(active: ActiveVoice): number { - const { graph, inputIndices, carrierIndices, phases, values, filters, increments, steps, gains, levels } = active; + renderVoice(active: ActiveVoice, fadeGain = 1): number { + const { graph, inputIndices, carrierIndices, phases, values, increments, steps, gains, levels } = active; if (active.controlRamps !== 0) advanceControls(active); + const voiceGain = active.scalarValues[4] * fadeGain; + const panLeft = active.leftGain * voiceGain, panRight = active.rightGain * voiceGain; + const mixLeft = this.mixLeft, mixRight = this.mixRight; const time = active.elapsed / this.sampleRate; const finished = active.releaseTime >= 0 && time >= active.releaseEnd; const lfo = active.voice.lfo; @@ -1034,9 +1076,10 @@ export class Synth { sample *= active.carrierGain; // Non-finite operator state always reaches a carrier through modulation or the mix. if (!Number.isFinite(sample)) return NaN; + mixLeft[sub] += sample * panLeft; + mixRight[sub] += sample * panRight; } - output = decimateSample(sample, filters, this.decimatorCoefficients); - if (!Number.isFinite(output)) return NaN; + output = sample; } if (active.lfoLive) { active.lfoTurns += active.scalarValues[1] / this.sampleRate; @@ -1077,16 +1120,20 @@ export class Synth { offset + length > leftLength || offset + length > rightLength) { throw new RangeError('render requires an in-bounds offset and length'); } - if (this.voices.length === 0 && this.fades.length === 0 && this.spillRemaining === 0) { + if (this.voices.length === 0 && this.fades.length === 0 && this.spillRemaining === 0 && this.decimatorIdle) { typedArrayFill.call(left, 0, offset, offset + length); if (right !== left) typedArrayFill.call(right, 0, offset, offset + length); (this as MutableCounters).currentFrame += length; return; } this.rendering = true; + const oversample = this.oversample, coefficients = this.decimatorCoefficients; + const mixLeft = this.mixLeft, mixRight = this.mixRight, filterLeft = this.filterLeft, filterRight = this.filterRight; try { for (let frame = offset, end = offset + length; frame < end; frame++) { - let mixedLeft = 0, mixedRight = 0, terminalCount = 0; + let terminalCount = 0; + mixLeft.fill(0); + mixRight.fill(0); for (let index = 0; index < this.voices.length; index++) { const active = this.voices[index]; const sample = this.renderVoice(active); @@ -1097,13 +1144,9 @@ export class Synth { this.freeVoices.push(removeAt(this.voices, index--)); continue; } - mixedLeft += sample * active.leftGain; - mixedRight += sample * active.rightGain; - let tailPeak = Math.abs(sample); + // Envelope-based retirement: once every operator release has ended the + // voice contributes silence; the shared decimator drains its tail. if (active.releaseTime >= 0 && (active.elapsed - 1) / this.sampleRate >= active.releaseEnd) { - tailPeak = Math.max(tailPeak, decimatorPeak(active.filters)); - } else tailPeak = Infinity; - if (tailPeak < AMPLITUDE_FLOOR) { this.terminalIds[terminalCount] = active.id; this.terminalErrors[terminalCount++] = 0; this.freeVoices.push(removeAt(this.voices, index--)); @@ -1111,29 +1154,56 @@ export class Synth { } for (let index = 0; index < this.fades.length; index++) { const active = this.fades[index]; - const sample = this.renderVoice(active); + const gain = active.fadeRemaining / this.fadeFrames; + const sample = this.renderVoice(active, gain); if (!Number.isFinite(sample)) { (this as MutableCounters).errorCount++; this.freeVoices.push(removeAt(this.fades, index--)); continue; } - const gain = active.fadeRemaining / this.fadeFrames; active.lastFadeGain = gain; - mixedLeft += sample * active.leftGain * gain; - mixedRight += sample * active.rightGain * gain; if (--active.fadeRemaining === 0) this.freeVoices.push(removeAt(this.fades, index--)); } if (this.spillRemaining > 0) { - mixedLeft += this.spillLeft; - mixedRight += this.spillRight; + for (let sub = 0; sub < oversample; sub++) { + mixLeft[sub] += this.spillLeft; + mixRight[sub] += this.spillRight; + } const next = --this.spillRemaining; this.spillLeft *= next / (next + 1); this.spillRight *= next / (next + 1); } + let mixedLeft = 0, mixedRight = 0, silentLeft = true, silentRight = true; + let mirrored = this.decimatorMirrored; + for (let sub = 0; sub < oversample; sub++) { + const inputLeft = mixLeft[sub], inputRight = mixRight[sub]; + if (inputLeft !== 0) silentLeft = false; + if (inputRight !== 0) silentRight = false; + // Identical channel histories give identical outputs: centered mixes + // filter once until the inputs first differ. + if (mirrored && inputLeft !== inputRight) { + filterRight.set(filterLeft); + mirrored = false; + } + mixedLeft = decimateSample(inputLeft, filterLeft, coefficients); + mixedRight = mirrored ? mixedLeft : decimateSample(inputRight, filterRight, coefficients); + } + this.decimatorMirrored = mirrored; // Saturate each stereo sum independently, preserving center dual mono. left[frame] = Number.isFinite(mixedLeft) ? HEADROOM * Math.tanh(mixedLeft * this.mixGain / HEADROOM) : 0; right[frame] = Number.isFinite(mixedRight) ? HEADROOM * Math.tanh(mixedRight * this.mixGain / HEADROOM) : 0; - if (!Number.isFinite(mixedLeft) || !Number.isFinite(mixedRight)) (this as MutableCounters).errorCount++; + if (!Number.isFinite(mixedLeft) || !Number.isFinite(mixedRight)) { + (this as MutableCounters).errorCount++; + this.resetDecimator(); + } else { + // A silent channel goes idle once its FIR history is zero. After the + // last source retires, history below the -96 dB floor snaps to zero. + const clear = this.voices.length === 0 && this.fades.length === 0 && this.spillRemaining === 0; + const idleLeft = silentLeft && settleDecimator(filterLeft, clear); + const idleRight = mirrored ? idleLeft : silentRight && settleDecimator(filterRight, clear); + this.decimatorIdle = idleLeft && idleRight; + if (this.decimatorIdle) this.decimatorMirrored = true; + } // Callbacks may admit/release notes, but replacements first render on the // next frame. Live traversal would let a zero-release chain run forever. for (let index = 0; index < terminalCount; index++) { diff --git a/test/api.test.ts b/test/api.test.ts index e75b918..6935b5e 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -88,6 +88,8 @@ function render(opm: OPM, length = 128) { } function audible(samples: Float32Array) { return samples.some(sample => Math.abs(sample) > 1e-6); } +// A hard gain step rings through the shared post-mix decimator before it snaps to zero. +const DECIMATOR_SETTLE_FRAMES = 64; test('public held gates, stereo options, scheduling and lifecycle diagnostics render through the worklet', async () => { const events: OPMEvent[] = []; @@ -194,7 +196,7 @@ test('absolute scheduling, pending controls and same-frame cancellation affect r [['accepted', 0], ['started', 32], ['released', 96]]); for (const event of events) if (event.type === 'note') assert.equal(event.time, event.frame / sampleRate); opm.updateNote(id, { expression: 0 }); - assert.ok(render(opm).right.every(sample => sample === 0), 'released tails still accept controls'); + assert.ok(render(opm).right.subarray(DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0), 'released tails still accept controls'); for (let block = 0; block < 40; block++) render(opm); assert.equal((await opm.getDiagnostics()).pendingEvents, 0); } finally { await opm.close(); } @@ -872,8 +874,8 @@ test('explicit stop cancellation removes only its own automation and leaves a na const tail = render(opm); assert.ok(audible(tail.left), 'cancellation releases instead of silencing immediately'); const after = render(opm); - assert.equal(audible(after.left), cancelControls); - assert.ok(after.right.every(sample => Math.abs(sample) < 1e-10), 'another gate keeps its own automation'); + assert.equal(audible(after.left.subarray(DECIMATOR_SETTLE_FRAMES)), cancelControls); + assert.ok(after.right.subarray(DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0), 'another gate keeps its own automation'); opm.panic(); const future = opm.playNote({ note: 69, time: 1 }); opm.updateNote(future, { expression: 0 }, { at: 2 }); @@ -901,6 +903,6 @@ test('malformed stop cancellation cannot remove controls or release its note', a assert.equal(reads, 0); assert.equal((await opm.getDiagnostics()).pendingEvents, 1); assert.ok(audible(render(opm).left)); - assert.ok(render(opm).left.every(sample => sample === 0)); + assert.ok(render(opm).left.subarray(DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0)); } finally { await opm.dispose(); } }); diff --git a/test/core.test.ts b/test/core.test.ts index 9b9a1ee..0756ac4 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -158,7 +158,7 @@ test('voice ceiling steals the oldest note and preserves headroom with eight voi test('nonfinite DSP state snaps to silence and increments the error count', () => { const synth = new Synth(16000); synth.noteOn(voice(), 60); - synth.voices[0].filters.fill(Infinity); + synth.voices[0].phases[0] = NaN; assert.ok(audio(synth, 128).every(value => value === 0)); assert.equal(synth.errorCount, 1); assert.equal(synth.voices.length, 0); @@ -201,3 +201,48 @@ test('native buffer bounds ignore shadow metadata and reject forged channels wit assert.ok(right.subarray(0).every(value => value === 0)); assert.equal(idle.currentFrame, 6); }); + +test('voices share one linear post-mix decimator per channel, including pan changes after a centered start', () => { + const HEADROOM = 0.7; + const stereo = (synth: Synth, length: number) => { + const left = new Float32Array(length), right = new Float32Array(length); + synth.render(left, right); + return { left, right }; + }; + for (const quality of ['eco', 'standard', 'high'] as const) { + const both = new Synth(16000, 8, { quality, mixGain: 1 }); + const first = new Synth(16000, 8, { quality }), second = new Synth(16000, 8, { quality }); + for (const synth of [both, first]) synth.noteOn(voice(), 60, 1); + for (const synth of [both, second]) synth.noteOn(voice({ algorithm: 4 }), 67, 2); + const centered = stereo(both, 200); + assert.deepEqual(centered.left, centered.right, 'a centered mix stays exact dual mono'); + stereo(first, 200); stereo(second, 200); + for (const synth of [both, first]) synth.updateNote(1, { pan: -0.6, ramp: 0.01 }); + for (const synth of [both, second]) synth.updateNote(2, { pan: 0.8 }); + const mixed = stereo(both, 600), a = stereo(first, 600), b = stereo(second, 600); + assert.ok(mixed.left.some((value, index) => value !== mixed.right[index])); + // The decimator is linear, so the pre-saturation mix equals the per-voice sum. + for (const channel of ['left', 'right'] as const) { + for (let i = 0; i < 600; i++) { + const sum = Math.atanh(a[channel][i] / HEADROOM) + Math.atanh(b[channel][i] / HEADROOM); + assert.ok(Math.abs(Math.atanh(mixed[channel][i] / HEADROOM) - sum) < 1e-6, `${quality} ${channel} ${i}`); + } + } + } +}); + +test('envelope-ended voices retire at once while the shared decimator drains to exact idle silence', () => { + const synth = new Synth(16000); + let endedFrame = -1; + synth.onVoiceEnded = () => { endedFrame = synth.currentFrame; }; + synth.noteOn(voice({ ops: Array.from({ length: 4 }, (_, i) => ({ ratio: i + 1, level: 0.5, detune: 0, + adsr: { a: 0, d: 0, s: 1, r: 0 } })) }), 60, 1); + audio(synth, 100); + synth.noteOff(1); + const tail = audio(synth, 200); + assert.equal(endedFrame, 100, 'zero release ends on its envelope, not after filter drain'); + assert.equal(synth.voices.length, 0); + assert.ok(tail.subarray(1, 8).some(value => value !== 0), 'decimator history continues after retirement'); + assert.ok(tail.subarray(64).every(value => value === 0)); + assert.equal(synth.decimatorIdle, true); +}); diff --git a/test/dsp-controls.test.ts b/test/dsp-controls.test.ts index 9c9d0cf..eef3642 100644 --- a/test/dsp-controls.test.ts +++ b/test/dsp-controls.test.ts @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { HEADROOM, Synth } from '../src/core/index.js'; import type { NoteControls, Voice } from '../src/core/index.js'; +import { filterImpulseBounds } from './decimator-reference.js'; function tone(): Voice { return { version: 7, name: 'dsp-controls', algorithm: 7, feedback: 0, modIndex: 0, @@ -26,6 +27,15 @@ function near(actual: Float32Array, expected: Float32Array, tolerance = 2e-7): v } } +// Note gains apply before the shared post-mix decimator. With per-frame gain +// slope s and a raw peak p, |filter(g·x) − g·filter(x)| ≤ s·p·Σk|h[k]|/4 (k in +// substeps). tone() has one audible carrier of four, so p = HEADROOM / 4. +function rampTolerance(slope: number): number { + return 2e-7 + slope * HEADROOM / 4 * filterImpulseBounds().absoluteFirstMoment / 4; +} +// A hard gain step rings through the decimator before the channel snaps to zero. +const DECIMATOR_SETTLE_FRAMES = 64; + function peak(samples: Float32Array): number { let result = 0; for (const sample of samples) result = Math.max(result, Math.abs(sample)); @@ -53,6 +63,8 @@ test('expression and pan ramps retarget continuously and keep independent timeli const actual = audio(synth, segments[segment]); assert.deepEqual(actual, audio(chunked, segments[segment], 37)); const unchanged = audio(reference, segments[segment]); + // Expression slope ≤ 5e-4/frame; pan gain slope ≤ √2·π/4 · 1.25/1600. + const tolerance = rampTolerance(5e-4 * Math.SQRT2 + Math.SQRT2 * Math.PI / 4 * 1.25 / 1600); for (let frame = 0; frame < segments[segment]; frame++) { const time = elapsed + frame; const expression = time < 400 ? 1 - 0.8 * time / 1600 : @@ -65,8 +77,8 @@ test('expression and pan ramps retarget continuously and keep independent timeli const rightGain = pan === -1 ? 0 : Math.SQRT2 * Math.sin(angle); const expectedLeft = HEADROOM * Math.tanh(dry * expression * leftGain / HEADROOM); const expectedRight = HEADROOM * Math.tanh(dry * expression * rightGain / HEADROOM); - assert.ok(Math.abs(actual.left[frame] - expectedLeft) < 2e-7, `left timeline at ${time}`); - assert.ok(Math.abs(actual.right[frame] - expectedRight) < 2e-7, `right timeline at ${time}`); + assert.ok(Math.abs(actual.left[frame] - expectedLeft) < tolerance, `left timeline at ${time}`); + assert.ok(Math.abs(actual.right[frame] - expectedRight) < tolerance, `right timeline at ${time}`); } elapsed += segments[segment]; } @@ -232,21 +244,26 @@ test('independent note gain composes exact ramps with expression and survives mu const actual = audio(synth, frames); assert.deepEqual(actual, audio(chunked, frames, 37)); const dry = audio(reference, frames); + // Gain slope ≤ 1/1600 per frame plus expression slope 0.5/3200. + const tolerance = rampTolerance(1 / 1600 + 0.5 / 3200); for (let frame = 0; frame < frames; frame++) { const time = elapsed + frame; const gain = time < 400 ? 1 - time / 1600 : time < 1200 ? 0.75 - 0.5 * (time - 400) / 800 : 0.25; const expression = 1 - 0.5 * Math.min(time / 3200, 1); const source = HEADROOM * Math.atanh(dry.left[frame] / HEADROOM); const expected = HEADROOM * Math.tanh(source * gain * expression / HEADROOM); - assert.ok(Math.abs(actual.left[frame] - expected) < 2e-7, `gain × expression at ${time}`); + assert.ok(Math.abs(actual.left[frame] - expected) < tolerance, `gain × expression at ${time}`); } elapsed += frames; } synth.updateNote(1, { gain: 0 }); - assert.ok(audio(synth, 128).left.every(sample => sample === 0)); + assert.ok(audio(synth, 128).left.subarray(DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0)); audio(reference, 128); synth.updateNote(1, { gain: 1, expression: 1 }); - assert.deepEqual(audio(synth, 128), audio(reference, 128), 'mute does not reset oscillator or envelope state'); + const unmuted = audio(synth, 128), continued = audio(reference, 128); + // Only the decimator restarts from silence; oscillator and envelope state continue. + near(unmuted.left.subarray(DECIMATOR_SETTLE_FRAMES), continued.left.subarray(DECIMATOR_SETTLE_FRAMES)); + near(unmuted.right.subarray(DECIMATOR_SETTLE_FRAMES), continued.right.subarray(DECIMATOR_SETTLE_FRAMES)); }); test('quietest stealing includes independent gain and rejects invalid gain without changing audio', () => { diff --git a/test/dsp-features.test.ts b/test/dsp-features.test.ts index 75ff133..a52af10 100644 --- a/test/dsp-features.test.ts +++ b/test/dsp-features.test.ts @@ -22,17 +22,20 @@ function rms(values: Float32Array) { return Math.sqrt(sum / values.length); } +// The 40-frame fade (5 ms at 8 kHz) then rings through the shared post-mix decimator. +const DECIMATOR_SETTLE_FRAMES = 64; + test('steals preserve the waveform at the boundary and fade without resetting phase', () => { const synth = new Synth(8000, 1), reference = new Synth(8000, 1); synth.noteOn(voice(), 60, 1); reference.noteOn(voice(), 60, 1); audio(synth, 173); audio(reference, 173); synth.noteOn(voice(), 64, 2, { velocity: 0 }); - const tail = audio(synth, 50).left; + const tail = audio(synth, 40 + DECIMATOR_SETTLE_FRAMES + 10).left; const continuation = audio(reference, 50).left; assert.equal(tail[0], continuation[0], 'first stolen sample retains complete DSP state'); assert.ok(rms(tail.subarray(20, 40)) < rms(continuation.subarray(20, 40))); - assert.ok(tail.subarray(40).every(sample => sample === 0)); + assert.ok(tail.subarray(40 + DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0)); assert.equal(synth.noteOff(1), false); }); @@ -52,8 +55,8 @@ test('exhausted tail pool carries nonzero sound through rapid steals with bounde const expected = audio(reference, 1).left[0]; assert.ok(Math.abs(expected) > 0.02, 'exhaustion occurs during audible output'); assert.ok(Math.abs(first - expected) < 0.01, 'collapsed tails must not abruptly disappear'); - const tail = audio(synth, 50).left; - assert.ok(tail.subarray(40).every(sample => sample === 0)); + const tail = audio(synth, 40 + DECIMATOR_SETTLE_FRAMES + 10).left; + assert.ok(tail.subarray(40 + DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0)); assert.equal(synth.fades.length, 0); }); @@ -65,13 +68,13 @@ test('logical terminal callbacks occur exactly once, including reused stolen IDs audio(synth, 50); synth.noteOn(voice(), 64, 2); assert.equal(synth.lastStolenId, 1); - synth.fades[0].filters.fill(Infinity); + synth.fades[0].phases[0] = NaN; audio(synth, 1); synth.noteOff(2); audio(synth, 400); synth.noteOn(voice(), 60, 1); assert.equal(synth.lastStolenId, null); - synth.voices[0].filters.fill(Infinity); + synth.voices[0].phases[0] = NaN; audio(synth, 200); assert.deepEqual(events, [[1, 'stolen'], [2, 'ended'], [1, 'error']]); assert.equal(synth.errorCount, 2); diff --git a/test/engine-v6.test.ts b/test/engine-v6.test.ts index 96521b6..040fdcd 100644 --- a/test/engine-v6.test.ts +++ b/test/engine-v6.test.ts @@ -181,11 +181,15 @@ test('zero attack/decay updates bridge the current dB for one frame and zero rel } } const expected = reference(source); - assert.ok(endedFrame >= 400 && endedFrame < 550, 'zero release drains filter state before retirement'); - near(result.subarray(0, endedFrame + 1), expected.subarray(0, endedFrame + 1)); + assert.equal(endedFrame, 400, 'zero release retires on its envelope end without waiting for filter history'); + // The shared post-mix decimator keeps draining the retired voice's history + // until its state falls below the floor, then snaps to exact silence. + let settled = endedFrame + 1; + while (settled < frames && result.subarray(settled).some(value => value !== 0)) settled++; + assert.ok(settled > endedFrame + 1 && settled < 550, 'filter history drains after retirement'); + near(result.subarray(0, settled), expected.subarray(0, settled)); const tailBound = filterStateTailBound(); - assert.ok(expected.subarray(endedFrame + 1).every(value => Math.abs(value) < tailBound)); - assert.ok(result.subarray(endedFrame + 1).every(value => value === 0)); + assert.ok(expected.subarray(settled).every(value => Math.abs(value) < tailBound)); }); test('independent feedback and LFO ramp interruption matches instantaneous continuous controls across chunks', () => { diff --git a/test/expression.test.ts b/test/expression.test.ts index 6e19391..f1fdef4 100644 --- a/test/expression.test.ts +++ b/test/expression.test.ts @@ -24,6 +24,9 @@ function rms(values: Float32Array): number { return Math.sqrt(sum / values.length); } +// A hard gain step rings through the shared post-mix decimator before it snaps to zero. +const DECIMATOR_SETTLE_FRAMES = 64; + test('prepared voices are deeply immutable snapshots and forged copies cannot bypass validation', () => { const input = tone(); input.ops[0].keyScale = { breakpoint: 60, leftDbPerOctave: 3, rightDbPerOctave: 6 }; @@ -72,7 +75,7 @@ test('prepared admissions never allocate typed state and remain bounded under re assert.equal(new Set([...synth.voices, ...synth.fades, ...synth.freeVoices]).size, capacity); } const invalidId = synth.voices[0].id; - synth.voices[0].filters.fill(Infinity); + synth.voices[0].phases[0] = NaN; synth.render(left, right); assert.equal(terminal.get(invalidId), 'error'); assert.ok(synth.voices.some(active => active.id === 1001)); @@ -103,18 +106,21 @@ test('pitch stays relative to the original note and immediate controls preserve synth.updateNote(id, controls); controls.expression = 1; const reduced = audio(synth, 256), unchanged = audio(reference, 256); - assert.ok(reduced.left.every(value => value === 0)); - for (let i = 0; i < reduced.right.length; i++) { + assert.ok(reduced.left.subarray(DECIMATOR_SETTLE_FRAMES).every(value => value === 0)); + for (let i = DECIMATOR_SETTLE_FRAMES; i < reduced.right.length; i++) { if (Math.abs(unchanged.right[i]) < 0.001) continue; const ratio = Math.atanh(reduced.right[i] / HEADROOM) / Math.atanh(unchanged.right[i] / HEADROOM); const expected = 0.25 / Math.sin((0.2 + 1) * Math.PI / 4); assert.ok(Math.abs(ratio - expected) < 0.00001); } synth.updateNote(id, { expression: 0, pan: -1 }); - assert.ok(audio(synth, 256).left.every(value => value === 0)); + assert.ok(audio(synth, 256).left.subarray(DECIMATOR_SETTLE_FRAMES).every(value => value === 0)); synth.updateNote(id, { expression: 1, pan: 0.2 }); audio(reference, 256); - assert.deepEqual(audio(synth, 256), audio(reference, 256)); + const restored = audio(synth, 256), continued = audio(reference, 256); + for (const channel of ['left', 'right'] as const) { + for (let i = DECIMATOR_SETTLE_FRAMES; i < 256; i++) assert.ok(Math.abs(restored[channel][i] - continued[channel][i]) < 2e-7); + } }); test('downward pitch bends recover the true operator frequency after an initially clipped high ratio', () => { @@ -270,7 +276,7 @@ test('terminal callback replacements first render on the next frame and cannot r let callbacks = 0; const admit = () => { const id = synth.noteOn(prepared, 60); - if (corrupt) synth.voices[0].filters.fill(Infinity); + if (corrupt) synth.voices[0].phases[0] = NaN; else synth.noteOff(id); }; synth.onVoiceEnded = (_id, reason) => { diff --git a/test/sequence.test.ts b/test/sequence.test.ts index 908cfc9..40b7c42 100644 --- a/test/sequence.test.ts +++ b/test/sequence.test.ts @@ -16,6 +16,9 @@ function toneVoice(release = 0.006): Voice { const sampleRate = 8000; +// A hard gain step rings through the shared post-mix decimator before it snaps to zero. +const DECIMATOR_SETTLE_FRAMES = 64; + test('a short fractional-note chord has exact onsets, ceil gates, release tails and deterministic stereo WAV audio', () => { const voice = toneVoice(); const events: SequenceEvent[] = [ @@ -61,7 +64,7 @@ test('pending controls clamp to onset and expression automation still applies du assert.ok(result.right.subarray(0, 48).every(value => value === 0)); assert.ok(result.right.subarray(48, 112).some(value => Math.abs(value) > 0.01)); assert.ok(result.right.subarray(112, 120).some(value => Math.abs(value) > 1e-6)); - assert.ok(result.right.subarray(120).every(value => value === 0)); + assert.ok(result.right.subarray(120 + DECIMATOR_SETTLE_FRAMES).every(value => value === 0)); }); test('stops before or tied with an onset cancel only their targeted notes regardless of input ordering', () => { @@ -85,7 +88,9 @@ test('the ninth simultaneous note steals the oldest with a bounded fade instead const stolen = renderSequence(events, { sampleRate }); const surviving = renderSequence(events.slice(1), { sampleRate }); assert.notDeepEqual(stolen.left.subarray(0, 40), surviving.left.subarray(0, 40)); - assert.deepEqual(stolen.left.subarray(40), surviving.left.subarray(40)); + // After the 40-frame fade, only decimator history of the stolen note remains. + const settled = 40 + DECIMATOR_SETTLE_FRAMES; + assert.ok(stolen.left.subarray(settled).every((value, index) => Math.abs(value - surviving.left[settled + index]) < 2e-7)); assert.ok(stolen.left.subarray(-32).every(value => value === 0)); }); diff --git a/test/worklet.test.ts b/test/worklet.test.ts index 8ee2e9f..1633d3e 100644 --- a/test/worklet.test.ts +++ b/test/worklet.test.ts @@ -76,6 +76,9 @@ function diagnostics(processor: TestProcessor) { return processor.messages.at(-1)!; } +// A hard gain step rings through the shared post-mix decimator before it snaps to zero. +const DECIMATOR_SETTLE_FRAMES = 64; + test('a stream longer than the pending cap reclaims stolen offs and retains only live note IDs', () => { Object.assign(globalThis, { currentFrame: 0 }); const processor = new Processor(); @@ -313,7 +316,7 @@ test('raw controls and scheduling fields use own data and reject malformed recor assert.ok(block(processor, 128).some(sample => sample !== 0)); assert.equal(processor.messages.filter(message => message.state === 'released').length, 0); processor.receive({ type: 'updateNote', id: 1, controls: { expression: 0 }, at: 256 / sampleRate }); - assert.ok(block(processor, 256).every(sample => sample === 0)); + assert.ok(block(processor, 256).subarray(DECIMATOR_SETTLE_FRAMES).every(sample => sample === 0)); }); test('full control queues report a rejected held-note stop and immediate recovery still ends the voice', () => {