midiwire 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ (function(C,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(C=typeof globalThis<"u"?globalThis:C||self,f(C.MIDIControls={}))})(this,(function(C){"use strict";class f{constructor(E,n="[data-midi-cc]"){this.controller=E,this.selector=n,this.observer=null}bindAll(){document.querySelectorAll(this.selector==="[data-midi-cc]"?"[data-midi-cc], [data-midi-msb][data-midi-lsb]":this.selector).forEach(n=>{if(n.hasAttribute("data-midi-bound"))return;const s=this._parseAttributes(n);s&&(this.controller.bind(n,s),n.setAttribute("data-midi-bound","true"))})}enableAutoBinding(){if(this.observer)return;const E=this.selector==="[data-midi-cc]"?"[data-midi-cc], [data-midi-msb][data-midi-lsb]":this.selector;this.observer=new MutationObserver(n=>{n.forEach(s=>{s.addedNodes.forEach(_=>{if(_.nodeType===Node.ELEMENT_NODE){if(_.matches?.(E)){const i=this._parseAttributes(_);i&&!_.hasAttribute("data-midi-bound")&&(this.controller.bind(_,i),_.setAttribute("data-midi-bound","true"))}_.querySelectorAll&&_.querySelectorAll(E).forEach(e=>{if(!e.hasAttribute("data-midi-bound")){const r=this._parseAttributes(e);r&&(this.controller.bind(e,r),e.setAttribute("data-midi-bound","true"))}})}}),s.removedNodes.forEach(_=>{_.nodeType===Node.ELEMENT_NODE&&(_.hasAttribute?.("data-midi-bound")&&this.controller.unbind(_),_.querySelectorAll&&_.querySelectorAll("[data-midi-bound]").forEach(e=>{this.controller.unbind(e)}))})})}),this.observer.observe(document.body,{childList:!0,subtree:!0})}disableAutoBinding(){this.observer&&(this.observer.disconnect(),this.observer=null)}_parseAttributes(E){const n=parseInt(E.dataset.midiMsb,10),s=parseInt(E.dataset.midiLsb,10);if(!Number.isNaN(n)&&!Number.isNaN(s)&&n>=0&&n<=127&&s>=0&&s<=127){const i=parseInt(E.dataset.midiCc,10);return!Number.isNaN(i)&&i>=0&&i<=127&&console.warn(`Element has both 7-bit (data-midi-cc="${i}") and 14-bit (data-midi-msb="${n}" data-midi-lsb="${s}") CC attributes. 14-bit takes precedence.`,E),{msb:n,lsb:s,is14Bit:!0,channel:parseInt(E.dataset.midiChannel,10)||void 0,min:parseFloat(E.getAttribute("min"))||0,max:parseFloat(E.getAttribute("max"))||127,invert:E.dataset.midiInvert==="true",label:E.dataset.midiLabel}}const _=parseInt(E.dataset.midiCc,10);return!Number.isNaN(_)&&_>=0&&_<=127?{cc:_,channel:parseInt(E.dataset.midiChannel,10)||void 0,min:parseFloat(E.getAttribute("min"))||0,max:parseFloat(E.getAttribute("max"))||127,invert:E.dataset.midiInvert==="true",label:E.dataset.midiLabel}:((E.dataset.midiCc!==void 0||E.dataset.midiMsb!==void 0&&E.dataset.midiLsb!==void 0)&&console.warn("Invalid MIDI configuration on element:",E),null)}destroy(){this.disableAutoBinding()}}class M extends Error{constructor(E,n){super(E),this.name="MIDIError",this.code=n,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}}class p extends M{constructor(E,n){super(E,"MIDI_ACCESS_ERROR"),this.name="MIDIAccessError",this.reason=n}}class g extends M{constructor(E){super(E,"MIDI_CONNECTION_ERROR"),this.name="MIDIConnectionError"}}class U extends M{constructor(E,n,s){super(E,"MIDI_DEVICE_ERROR"),this.name="MIDIDeviceError",this.deviceType=n,this.deviceId=s}}class D extends M{constructor(E,n){super(E,"MIDI_VALIDATION_ERROR"),this.name="MIDIValidationError",this.validationType=n}}class H extends Error{constructor(E,n){super(E),this.name="DX7Error",this.code=n,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}}class G extends H{constructor(E,n,s){super(E,"DX7_PARSE_ERROR"),this.name="DX7ParseError",this.parseType=n,this.offset=s}}class d extends H{constructor(E,n,s){super(E,"DX7_VALIDATION_ERROR"),this.name="DX7ValidationError",this.validationType=n,this.value=s}}function O(a,E,n){return Math.max(E,Math.min(n,a))}function y(a,E,n,s=!1){const _=(a-E)/(n-E),e=(s?1-_:_)*127;return O(Math.round(e),0,127)}function q(a,E,n,s=!1){let _=O(a,0,127)/127;return s&&(_=1-_),E+_*(n-E)}function Q(a){const E={C:0,"C#":1,DB:1,D:2,"D#":3,EB:3,E:4,F:5,"F#":6,GB:6,G:7,"G#":8,AB:8,A:9,"A#":10,BB:10,B:11},n=a.match(/^([A-G][#b]?)(-?\d+)$/i);if(!n)throw new D(`Invalid note name: ${a}`,"note",a);const[,s,_]=n,i=E[s.toUpperCase()];if(i===void 0)throw new D(`Invalid note: ${s}`,"note",s);const e=(parseInt(_,10)+1)*12+i;return O(e,0,127)}function j(a,E=!1){const _=E?["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"]:["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],i=Math.floor(a/12)-1;return`${_[a%12]}${i}`}function J(a){const E=69+12*Math.log2(a/440);return O(Math.round(E),0,127)}function k(a){return 440*2**((a-69)/12)}function V(a){return{0:"Bank Select",1:"Modulation",2:"Breath Controller",4:"Foot Controller",5:"Portamento Time",7:"Volume",8:"Balance",10:"Pan",11:"Expression",64:"Sustain Pedal",65:"Portamento",66:"Sostenuto",67:"Soft Pedal",68:"Legato",71:"Resonance",72:"Release Time",73:"Attack Time",74:"Cutoff",75:"Decay Time",76:"Vibrato Rate",77:"Vibrato Depth",78:"Vibrato Delay",84:"Portamento Control",91:"Reverb",92:"Tremolo",93:"Chorus",94:"Detune",95:"Phaser",120:"All Sound Off",121:"Reset All Controllers",123:"All Notes Off"}[a]||`CC ${a}`}function v(a){const E=O(Math.round(a),0,16383);return{msb:E>>7&127,lsb:E&127}}function B(a,E){return O(a,0,127)<<7|O(E,0,127)}function w(a,E,n,s=!1){const _=(a-E)/(n-E),e=(s?1-_:_)*16383;return v(e)}function X(a,E,n,s,_=!1){let e=B(a,E)/16383;return _&&(e=1-e),n+e*(s-n)}class b{constructor(){this.events=new Map}on(E,n){return this.events.has(E)||this.events.set(E,[]),this.events.get(E).push(n),()=>this.off(E,n)}once(E,n){const s=(..._)=>{n(..._),this.off(E,s)};this.on(E,s)}off(E,n){if(!this.events.has(E))return;const s=this.events.get(E),_=s.indexOf(n);_>-1&&s.splice(_,1),s.length===0&&this.events.delete(E)}emit(E,n){if(!this.events.has(E))return;[...this.events.get(E)].forEach(_=>{try{_(n)}catch(i){console.error(`Error in event handler for "${E}":`,i)}})}removeAllListeners(E){E?this.events.delete(E):this.events.clear()}}const K={DEVICE_CHANGE:"device-change",INPUT_DEVICE_CONNECTED:"input-device-connected",INPUT_DEVICE_DISCONNECTED:"input-device-disconnected",OUTPUT_DEVICE_CONNECTED:"output-device-connected",OUTPUT_DEVICE_DISCONNECTED:"output-device-disconnected"};class Y extends b{constructor(E={}){super(),this.options={sysex:!1,...E},this.midiAccess=null,this.output=null,this.input=null}async requestAccess(){if(!navigator.requestMIDIAccess)throw new p("Web MIDI API is not supported in this browser","unsupported");try{this.midiAccess=await navigator.requestMIDIAccess({sysex:this.options.sysex}),this.midiAccess.onstatechange=E=>{const n=E.port,s=E.port.state;this.emit(K.DEVICE_CHANGE,{port:n,state:s,type:n.type,device:{id:n.id,name:n.name,manufacturer:n.manufacturer||"Unknown"}}),s==="disconnected"?n.type==="input"?(this.emit(K.INPUT_DEVICE_DISCONNECTED,{device:n}),this.input&&this.input.id===n.id&&(this.input=null)):n.type==="output"&&(this.emit(K.OUTPUT_DEVICE_DISCONNECTED,{device:n}),this.output&&this.output.id===n.id&&(this.output=null)):s==="connected"&&(n.type==="input"?this.emit(K.INPUT_DEVICE_CONNECTED,{device:n}):n.type==="output"&&this.emit(K.OUTPUT_DEVICE_CONNECTED,{device:n}))}}catch(E){throw E.name==="SecurityError"?new p("MIDI access denied. SysEx requires user permission.","denied"):new p(`Failed to get MIDI access: ${E.message}`,"failed")}}getOutputs(){if(!this.midiAccess)return[];const E=[];return this.midiAccess.outputs.forEach(n=>{n.state==="connected"&&E.push({id:n.id,name:n.name,manufacturer:n.manufacturer||"Unknown"})}),E}getInputs(){if(!this.midiAccess)return[];const E=[];return this.midiAccess.inputs.forEach(n=>{n.state==="connected"&&E.push({id:n.id,name:n.name,manufacturer:n.manufacturer||"Unknown"})}),E}async connect(E){if(!this.midiAccess)throw new g("MIDI access not initialized. Call requestAccess() first.");const n=Array.from(this.midiAccess.outputs.values());if(n.length===0)throw new U("No MIDI output devices available","output");if(E===void 0){this.output=n[0];return}if(typeof E=="number"){if(E<0||E>=n.length)throw new U(`Output index ${E} out of range (0-${n.length-1})`,"output",E);this.output=n[E];return}if(this.output=n.find(s=>s.name===E||s.id===E),!this.output){const s=n.map(_=>_.name).join(", ");throw new U(`MIDI output "${E}" not found. Available: ${s}`,"output",E)}}async connectInput(E,n){if(!this.midiAccess)throw new g("MIDI access not initialized. Call requestAccess() first.");if(typeof n!="function")throw new D("onMessage callback must be a function","callback");const s=Array.from(this.midiAccess.inputs.values());if(s.length===0)throw new U("No MIDI input devices available","input");if(this.input&&(this.input.onmidimessage=null),E===void 0)this.input=s[0];else if(typeof E=="number"){if(E<0||E>=s.length)throw new U(`Input index ${E} out of range (0-${s.length-1})`,"input",E);this.input=s[E]}else if(this.input=s.find(_=>_.name===E||_.id===E),!this.input){const _=s.map(i=>i.name).join(", ");throw new U(`MIDI input "${E}" not found. Available: ${_}`,"input",E)}this.input.onmidimessage=_=>{n(_)}}send(E,n=null){if(!this.output){console.warn("No MIDI output connected. Call connect() first.");return}try{const s=new Uint8Array(E);n===null?this.output.send(s):this.output.send(s,n)}catch(s){console.error("Failed to send MIDI message:",s)}}sendSysEx(E,n=!1){if(!this.options.sysex){console.warn("SysEx not enabled. Initialize with sysex: true");return}let s;n?s=[240,...E,247]:s=E,this.send(s)}disconnect(){this.input&&(this.input.onmidimessage=null,this.input=null),this.output=null}isConnected(){return this.output!==null}getCurrentOutput(){return this.output?{id:this.output.id,name:this.output.name,manufacturer:this.output.manufacturer||"Unknown"}:null}getCurrentInput(){return this.input?{id:this.input.id,name:this.input.name,manufacturer:this.input.manufacturer||"Unknown"}:null}}const o={READY:"ready",ERROR:"error",CC_SEND:"cc-send",CC_RECV:"cc-recv",NOTE_ON_SEND:"note-on-send",NOTE_ON_RECV:"note-on-recv",NOTE_OFF_SEND:"note-off-send",NOTE_OFF_RECV:"note-off-recv",SYSEX_SEND:"sysex-send",SYSEX_RECV:"sysex-recv",OUTPUT_CHANGED:"output-changed",INPUT_CONNECTED:"input-connected",DESTROYED:"destroyed",MIDI_MSG:"midi-msg",PATCH_SAVED:"patch-saved",PATCH_LOADED:"patch-loaded",PATCH_DELETED:"patch-deleted"};class x extends b{constructor(E={}){super(),this.options={channel:1,autoConnect:!0,sysex:!1,...E},this.connection=null,this.bindings=new Map,this.state=new Map,this.initialized=!1}async initialize(){if(this.initialized){console.warn("MIDI Controller already initialized");return}try{this.connection=new Y({sysex:this.options.sysex}),await this.connection.requestAccess(),this.options.autoConnect&&await this.connection.connect(this.options.output),this.options.input!==void 0&&await this.connectInput(this.options.input),this.initialized=!0,this.emit(o.READY,this),this.options.onReady?.(this)}catch(E){throw this.emit(o.ERROR,E),this.options.onError?.(E),E}}async connectInput(E){await this.connection.connectInput(E,n=>{this._handleMIDIMessage(n)}),this.emit(o.INPUT_CONNECTED,this.connection.getCurrentInput())}sendCC(E,n,s=this.options.channel){if(!this.initialized){console.warn("MIDI not initialized. Call initialize() first.");return}E=O(Math.round(E),0,127),n=O(Math.round(n),0,127),s=O(Math.round(s),1,16);const _=176+(s-1);this.connection.send([_,E,n]);const i=`${s}:${E}`;this.state.set(i,n),this.emit(o.CC_SEND,{cc:E,value:n,channel:s})}sendSysEx(E,n=!1){if(!this.initialized){console.warn("MIDI not initialized. Call initialize() first.");return}if(!this.options.sysex){console.warn("SysEx not enabled. Initialize with sysex: true");return}this.connection.sendSysEx(E,n),this.emit(o.SYSEX_SEND,{data:E,includeWrapper:n})}sendNoteOn(E,n=64,s=this.options.channel){if(!this.initialized)return;E=O(Math.round(E),0,127),n=O(Math.round(n),0,127),s=O(Math.round(s),1,16);const _=144+(s-1);this.connection.send([_,E,n]),this.emit(o.NOTE_ON_SEND,{note:E,velocity:n,channel:s})}sendNoteOff(E,n=this.options.channel,s=0){if(!this.initialized)return;E=O(Math.round(E),0,127),s=O(Math.round(s),0,127),n=O(Math.round(n),1,16);const _=144+(n-1);this.connection.send([_,E,s]),this.emit(o.NOTE_OFF_SEND,{note:E,channel:n,velocity:s})}bind(E,n,s={}){if(!E)return console.warn("Cannot bind: element is null or undefined"),()=>{};const _=this._createBinding(E,n,s);return this.bindings.set(E,_),this.initialized&&this.connection?.isConnected()&&_.handler({target:E}),()=>this.unbind(E)}unbind(E){const n=this.bindings.get(E);n&&(n.destroy(),this.bindings.delete(E))}getCC(E,n=this.options.channel){const s=`${n}:${E}`;return this.state.get(s)}getOutputs(){return this.connection?.getOutputs()||[]}getInputs(){return this.connection?.getInputs()||[]}async setOutput(E){await this.connection.connect(E),this.emit(o.OUTPUT_CHANGED,this.connection.getCurrentOutput())}getCurrentOutput(){return this.connection?.getCurrentOutput()||null}getCurrentInput(){return this.connection?.getCurrentInput()||null}destroy(){for(const E of this.bindings.values())E.destroy();this.bindings.clear(),this.state.clear(),this.connection?.disconnect(),this.initialized=!1,this.emit(o.DESTROYED),this.removeAllListeners()}_handleMIDIMessage(E){const[n,s,_]=E.data,i=n&240,e=(n&15)+1;if(n===240){this.emit(o.SYSEX_RECV,{data:Array.from(E.data),timestamp:E.midiwire});return}if(i===176){const r=`${e}:${s}`;this.state.set(r,_),this.emit(o.CC_RECV,{cc:s,value:_,channel:e});return}if(i===144&&_>0){this.emit(o.NOTE_ON_RECV,{note:s,velocity:_,channel:e});return}if(i===128||i===144&&_===0){this.emit(o.NOTE_OFF_RECV,{note:s,channel:e});return}this.emit(o.MIDI_MSG,{status:n,data:[s,_],channel:e,timestamp:E.midiwire})}_createBinding(E,n,s={}){const{min:_=parseFloat(E.getAttribute("min"))||0,max:i=parseFloat(E.getAttribute("max"))||127,channel:e,invert:r=!1,onInput:T=void 0}=n,{debounce:N=0}=s,A={...n,min:_,max:i,invert:r,onInput:T};if(e!==void 0&&(A.channel=e),n.is14Bit){const{msb:I,lsb:R}=n,L=F=>{const z=parseFloat(F.target.value);if(Number.isNaN(z))return;const{msb:St,lsb:lt}=w(z,_,i,r),W=e||this.options.channel;this.sendCC(I,St,W),this.sendCC(R,lt,W)};let h=null;const m=N>0?F=>{h&&clearTimeout(h),h=setTimeout(()=>{L(F),h=null},N)}:L;return E.addEventListener("input",m),E.addEventListener("change",m),{element:E,config:A,handler:L,destroy:()=>{h&&clearTimeout(h),E.removeEventListener("input",m),E.removeEventListener("change",m)}}}const{cc:P}=n,c=I=>{const R=parseFloat(I.target.value);if(Number.isNaN(R))return;const L=y(R,_,i,r),h=e===void 0?this.options.channel:e;this.sendCC(P,L,h)};let l=null;const S=N>0?I=>{l&&clearTimeout(l),l=setTimeout(()=>{c(I),l=null},N)}:c;return E.addEventListener("input",S),E.addEventListener("change",S),{element:E,config:A,handler:c,destroy:()=>{l&&clearTimeout(l),E.removeEventListener("input",S),E.removeEventListener("change",S)}}}getPatch(E="Unnamed Patch"){const n={name:E,device:this.getCurrentOutput()?.name||null,timestamp:new Date().toISOString(),version:"1.0",channels:{},settings:{}};for(const[s,_]of this.state.entries()){const[i,e]=s.split(":").map(Number);n.channels[i]||(n.channels[i]={ccs:{},notes:{}}),n.channels[i].ccs[e]=_}for(const[s,_]of this.bindings.entries()){const{config:i}=_;if(i.cc){const e=`cc${i.cc}`;n.settings[e]={min:i.min,max:i.max,invert:i.invert||!1,is14Bit:i.is14Bit||!1,label:s.getAttribute?.("data-midi-label")||null,elementId:s.id||null}}}return n}async setPatch(E){if(!E||!E.channels)throw new D("Invalid patch format","patch");const n=E.version||"1.0";n==="1.0"?await this._applyPatchV1(E):(console.warn(`Unknown patch version: ${n}. Attempting to apply as v1.0`),await this._applyPatchV1(E)),this.emit(o.PATCH_LOADED,{patch:E})}async _applyPatchV1(E){for(const[n,s]of Object.entries(E.channels)){const _=parseInt(n,10);if(s.ccs)for(const[i,e]of Object.entries(s.ccs)){const r=parseInt(i,10);this.sendCC(r,e,_)}if(s.notes)for(const[i,e]of Object.entries(s.notes)){const r=parseInt(i,10);e>0?this.sendNoteOn(r,e,_):this.sendNoteOff(r,_)}}if(E.settings)for(const[n,s]of Object.entries(E.settings))for(const[_,i]of this.bindings.entries())i.config.cc?.toString()===n.replace("cc","")&&(_.min!==void 0&&s.min!==void 0&&(_.min=String(s.min)),_.max!==void 0&&s.max!==void 0&&(_.max=String(s.max)));for(const[n,s]of this.bindings.entries()){const{config:_}=s;if(_.cc!==void 0){const i=_.channel||this.options.channel,e=E.channels[i];if(e?.ccs){const r=e.ccs[_.cc];if(r!==void 0){const T=_.min!==void 0?_.min:parseFloat(n.getAttribute?.("min"))||0,N=_.max!==void 0?_.max:parseFloat(n.getAttribute?.("max"))||127,A=_.invert||!1;let P;A?P=N-r/127*(N-T):P=T+r/127*(N-T),_.onInput&&typeof _.onInput=="function"?_.onInput(P):(n.value=P,n.dispatchEvent(new Event("input",{bubbles:!0})))}}}}}savePatch(E,n=null){const s=n||this.getPatch(E),_=`midiwire_patch_${E}`;try{return localStorage.setItem(_,JSON.stringify(s)),this.emit(o.PATCH_SAVED,{name:E,patch:s}),_}catch(i){throw console.error("Failed to save patch:",i),i}}loadPatch(E){const n=`midiwire_patch_${E}`;try{const s=localStorage.getItem(n);if(!s)return null;const _=JSON.parse(s);return this.emit(o.PATCH_LOADED,{name:E,patch:_}),_}catch(s){return console.error("Failed to load patch:",s),null}}deletePatch(E){const n=`midiwire_patch_${E}`;try{return localStorage.removeItem(n),this.emit(o.PATCH_DELETED,{name:E}),!0}catch(s){return console.error("Failed to delete patch:",s),!1}}listPatches(){const E=[];try{for(let n=0;n<localStorage.length;n++){const s=localStorage.key(n);if(s?.startsWith("midiwire_patch_")){const _=s.replace("midiwire_patch_",""),i=this.loadPatch(_);i&&E.push({name:_,patch:i})}}}catch(n){console.error("Failed to list patches:",n)}return E.sort((n,s)=>n.name.localeCompare(s.name))}}class Z{constructor(E={}){this.midi=E.midiController||null,this.onStatusUpdate=E.onStatusUpdate||(()=>{}),this.onConnectionUpdate=E.onConnectionUpdate||(()=>{}),this.channel=E.channel||1,this.currentDevice=null,this.isConnecting=!1}setMIDI(E){this.midi=E}setupDeviceListeners(E){this.midi?.connection&&(this.midi.connection.on(K.OUTPUT_DEVICE_CONNECTED,({device:n})=>{this.updateStatus(`Device connected: ${n.name}`,"connected"),E&&E()}),this.midi.connection.on(K.OUTPUT_DEVICE_DISCONNECTED,({device:n})=>{this.updateStatus(`Device disconnected: ${n.name}`,"error"),this.currentDevice&&n.name===this.currentDevice.name&&(this.currentDevice=null,this.updateConnectionStatus()),E&&E()}))}updateStatus(E,n=""){this.onStatusUpdate(E,n)}updateConnectionStatus(){this.onConnectionUpdate(this.currentDevice,this.midi)}getOutputDevices(){return this.midi?.connection?this.midi.connection.getOutputs():[]}isDeviceConnected(E){return this.midi?.connection?this.midi.connection.getOutputs().some(s=>s.name===E):!1}connectDeviceSelection(E,n){!E||!this.midi||E.addEventListener("change",async s=>{const _=s.target.value;if(!_){this.currentDevice&&this.midi&&(this.midi.connection.disconnect(),this.currentDevice=null,this.updateStatus("Disconnected"),this.updateConnectionStatus());return}if(!this.isConnecting){this.isConnecting=!0;try{await this.midi.setOutput(parseInt(_,10)),this.currentDevice=this.midi.getCurrentOutput(),this.updateConnectionStatus(),n&&await n(this.midi,this.currentDevice)}catch(i){this.updateStatus(`Connection failed: ${i.message}`,"error")}finally{this.isConnecting=!1}}})}connectChannelSelection(E){!E||!this.midi||E.addEventListener("change",n=>{this.midi&&(this.midi.options.channel=parseInt(n.target.value,10),this.updateConnectionStatus())})}populateDeviceList(E,n){if(!E)return;const s=this.getOutputDevices();if(s.length>0){if(E.innerHTML='<option value="">Select a device</option>'+s.map((_,i)=>`<option value="${i}">${_.name}</option>`).join(""),this.currentDevice){const _=s.findIndex(i=>i.name===this.currentDevice.name);_!==-1?E.value=_.toString():(E.value="",this.currentDevice=null,this.updateConnectionStatus())}else E.value="";this.currentDevice||this.updateStatus("Select a MIDI device")}else E.innerHTML='<option value="">No MIDI devices found</option>',this.updateStatus("No MIDI devices available","error");n&&n()}}class t{static PACKED_SIZE=128;static PACKED_OP_SIZE=17;static NUM_OPERATORS=6;static PACKED_OP_EG_RATE_1=0;static PACKED_OP_EG_RATE_2=1;static PACKED_OP_EG_RATE_3=2;static PACKED_OP_EG_RATE_4=3;static PACKED_OP_EG_LEVEL_1=4;static PACKED_OP_EG_LEVEL_2=5;static PACKED_OP_EG_LEVEL_3=6;static PACKED_OP_EG_LEVEL_4=7;static PACKED_OP_BREAK_POINT=8;static PACKED_OP_L_SCALE_DEPTH=9;static PACKED_OP_R_SCALE_DEPTH=10;static PACKED_OP_CURVES=11;static PACKED_OP_RATE_SCALING=12;static PACKED_OP_MOD_SENS=13;static PACKED_OP_OUTPUT_LEVEL=14;static PACKED_OP_MODE_FREQ=15;static PACKED_OP_DETUNE_FINE=16;static PACKED_PITCH_EG_RATE_1=102;static PACKED_PITCH_EG_RATE_2=103;static PACKED_PITCH_EG_RATE_3=104;static PACKED_PITCH_EG_RATE_4=105;static PACKED_PITCH_EG_LEVEL_1=106;static PACKED_PITCH_EG_LEVEL_2=107;static PACKED_PITCH_EG_LEVEL_3=108;static PACKED_PITCH_EG_LEVEL_4=109;static OFFSET_ALGORITHM=110;static OFFSET_FEEDBACK=111;static OFFSET_LFO_SPEED=112;static OFFSET_LFO_DELAY=113;static OFFSET_LFO_PM_DEPTH=114;static OFFSET_LFO_AM_DEPTH=115;static OFFSET_LFO_SYNC_WAVE=116;static OFFSET_TRANSPOSE=117;static OFFSET_AMP_MOD_SENS=118;static OFFSET_EG_BIAS_SENS=119;static PACKED_NAME_START=118;static NAME_LENGTH=10;static UNPACKED_SIZE=169;static UNPACKED_OP_SIZE=23;static UNPACKED_OP_EG_RATE_1=0;static UNPACKED_OP_EG_RATE_2=1;static UNPACKED_OP_EG_RATE_3=2;static UNPACKED_OP_EG_RATE_4=3;static UNPACKED_OP_EG_LEVEL_1=4;static UNPACKED_OP_EG_LEVEL_2=5;static UNPACKED_OP_EG_LEVEL_3=6;static UNPACKED_OP_EG_LEVEL_4=7;static UNPACKED_OP_BREAK_POINT=8;static UNPACKED_OP_L_SCALE_DEPTH=9;static UNPACKED_OP_R_SCALE_DEPTH=10;static UNPACKED_OP_L_CURVE=11;static UNPACKED_OP_R_CURVE=12;static UNPACKED_OP_RATE_SCALING=13;static UNPACKED_OP_DETUNE=14;static UNPACKED_OP_AMP_MOD_SENS=15;static UNPACKED_OP_OUTPUT_LEVEL=16;static UNPACKED_OP_MODE=17;static UNPACKED_OP_KEY_VEL_SENS=18;static UNPACKED_OP_FREQ_COARSE=19;static UNPACKED_OP_OSC_DETUNE=20;static UNPACKED_OP_FREQ_FINE=21;static UNPACKED_PITCH_EG_RATE_1=138;static UNPACKED_PITCH_EG_RATE_2=139;static UNPACKED_PITCH_EG_RATE_3=140;static UNPACKED_PITCH_EG_RATE_4=141;static UNPACKED_PITCH_EG_LEVEL_1=142;static UNPACKED_PITCH_EG_LEVEL_2=143;static UNPACKED_PITCH_EG_LEVEL_3=144;static UNPACKED_PITCH_EG_LEVEL_4=145;static UNPACKED_ALGORITHM=146;static UNPACKED_FEEDBACK=147;static UNPACKED_OSC_SYNC=148;static UNPACKED_LFO_SPEED=149;static UNPACKED_LFO_DELAY=150;static UNPACKED_LFO_PM_DEPTH=151;static UNPACKED_LFO_AM_DEPTH=152;static UNPACKED_LFO_KEY_SYNC=153;static UNPACKED_LFO_WAVE=154;static UNPACKED_LFO_PM_SENS=155;static UNPACKED_AMP_MOD_SENS=156;static UNPACKED_TRANSPOSE=157;static UNPACKED_EG_BIAS_SENS=158;static UNPACKED_NAME_START=159;static VCED_SIZE=163;static VCED_HEADER_SIZE=6;static VCED_DATA_SIZE=155;static VCED_SYSEX_START=240;static VCED_YAMAHA_ID=67;static VCED_SUB_STATUS=0;static VCED_FORMAT_SINGLE=0;static VCED_BYTE_COUNT_MSB=1;static VCED_BYTE_COUNT_LSB=27;static VCED_SYSEX_END=247;static MASK_7BIT=127;static MASK_2BIT=3;static MASK_3BIT=7;static MASK_4BIT=15;static MASK_5BIT=31;static MASK_1BIT=1;static TRANSPOSE_CENTER=24;static CHAR_YEN=92;static CHAR_ARROW_RIGHT=126;static CHAR_ARROW_LEFT=127;static CHAR_REPLACEMENT_Y=89;static CHAR_REPLACEMENT_GT=62;static CHAR_REPLACEMENT_LT=60;static CHAR_SPACE=32;static CHAR_MIN_PRINTABLE=32;static CHAR_MAX_PRINTABLE=126;static DEFAULT_EG_RATE=99;static DEFAULT_EG_LEVEL_MAX=99;static DEFAULT_EG_LEVEL_MIN=0;static DEFAULT_BREAK_POINT=60;static DEFAULT_OUTPUT_LEVEL=99;static DEFAULT_PITCH_EG_LEVEL=50;static DEFAULT_LFO_SPEED=35;static DEFAULT_LFO_PM_SENS=3;static DEFAULT_ALGORITHM=0;static DEFAULT_FEEDBACK=0;static MIDI_OCTAVE_OFFSET=-2;static MIDI_BREAK_POINT_OFFSET=21;constructor(E,n=0){if(E.length!==t.PACKED_SIZE)throw new d(`Invalid voice data length: expected ${t.PACKED_SIZE} bytes, got ${E.length}`,"length",E.length);this.index=n,this.data=new Uint8Array(E),this.name=this._extractName()}_extractName(){const E=this.data.subarray(t.PACKED_NAME_START,t.PACKED_NAME_START+t.NAME_LENGTH);return Array.from(E).map(s=>{let _=s&t.MASK_7BIT;return _===t.CHAR_YEN&&(_=t.CHAR_REPLACEMENT_Y),_===t.CHAR_ARROW_RIGHT&&(_=t.CHAR_REPLACEMENT_GT),_===t.CHAR_ARROW_LEFT&&(_=t.CHAR_REPLACEMENT_LT),(_<t.CHAR_MIN_PRINTABLE||_>t.CHAR_MAX_PRINTABLE)&&(_=t.CHAR_SPACE),String.fromCharCode(_)}).join("").trim()}getParameter(E){if(E<0||E>=t.PACKED_SIZE)throw new d(`Parameter offset out of range: ${E} (must be 0-${t.PACKED_SIZE-1})`,"offset",E);return this.data[E]&t.MASK_7BIT}getUnpackedParameter(E){if(E<0||E>=t.UNPACKED_SIZE)throw new d(`Unpacked parameter offset out of range: ${E} (must be 0-${t.UNPACKED_SIZE-1})`,"offset",E);return this.unpack()[E]&t.MASK_7BIT}setParameter(E,n){if(E<0||E>=t.PACKED_SIZE)throw new d(`Parameter offset out of range: ${E} (must be 0-${t.PACKED_SIZE-1})`,"offset",E);this.data[E]=n&t.MASK_7BIT,E>=t.PACKED_NAME_START&&E<t.PACKED_NAME_START+t.NAME_LENGTH&&(this.name=this._extractName())}unpack(){const E=this.data,n=new Uint8Array(t.UNPACKED_SIZE);for(let i=0;i<t.NUM_OPERATORS;i++){const e=(t.NUM_OPERATORS-1-i)*t.PACKED_OP_SIZE,r=i*t.UNPACKED_OP_SIZE;n[r+t.UNPACKED_OP_EG_RATE_1]=E[e+t.PACKED_OP_EG_RATE_1]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_RATE_2]=E[e+t.PACKED_OP_EG_RATE_2]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_RATE_3]=E[e+t.PACKED_OP_EG_RATE_3]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_RATE_4]=E[e+t.PACKED_OP_EG_RATE_4]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_LEVEL_1]=E[e+t.PACKED_OP_EG_LEVEL_1]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_LEVEL_2]=E[e+t.PACKED_OP_EG_LEVEL_2]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_LEVEL_3]=E[e+t.PACKED_OP_EG_LEVEL_3]&t.MASK_7BIT,n[r+t.UNPACKED_OP_EG_LEVEL_4]=E[e+t.PACKED_OP_EG_LEVEL_4]&t.MASK_7BIT,n[r+t.UNPACKED_OP_BREAK_POINT]=E[e+t.PACKED_OP_BREAK_POINT]&t.MASK_7BIT,n[r+t.UNPACKED_OP_L_SCALE_DEPTH]=E[e+t.PACKED_OP_L_SCALE_DEPTH]&t.MASK_7BIT,n[r+t.UNPACKED_OP_R_SCALE_DEPTH]=E[e+t.PACKED_OP_R_SCALE_DEPTH]&t.MASK_7BIT;const T=E[e+t.PACKED_OP_CURVES]&t.MASK_7BIT;n[r+t.UNPACKED_OP_L_CURVE]=T&t.MASK_2BIT,n[r+t.UNPACKED_OP_R_CURVE]=T>>2&t.MASK_2BIT;const N=E[e+t.PACKED_OP_RATE_SCALING]&t.MASK_7BIT;n[r+t.UNPACKED_OP_RATE_SCALING]=N&t.MASK_3BIT,n[r+t.UNPACKED_OP_DETUNE]=N>>3&t.MASK_4BIT;const A=E[e+t.PACKED_OP_MOD_SENS]&t.MASK_7BIT;n[r+t.UNPACKED_OP_AMP_MOD_SENS]=A&t.MASK_2BIT,n[r+t.UNPACKED_OP_KEY_VEL_SENS]=A>>2&t.MASK_3BIT,n[r+t.UNPACKED_OP_OUTPUT_LEVEL]=E[e+t.PACKED_OP_OUTPUT_LEVEL]&t.MASK_7BIT;const P=E[e+t.PACKED_OP_MODE_FREQ]&t.MASK_7BIT;n[r+t.UNPACKED_OP_MODE]=P&t.MASK_1BIT,n[r+t.UNPACKED_OP_FREQ_COARSE]=P>>1&t.MASK_5BIT;const c=E[e+t.PACKED_OP_DETUNE_FINE]&t.MASK_7BIT;n[r+t.UNPACKED_OP_OSC_DETUNE]=c&t.MASK_3BIT,n[r+t.UNPACKED_OP_FREQ_FINE]=c>>3&t.MASK_4BIT}n[t.UNPACKED_PITCH_EG_RATE_1]=E[t.PACKED_PITCH_EG_RATE_1]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_RATE_2]=E[t.PACKED_PITCH_EG_RATE_2]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_RATE_3]=E[t.PACKED_PITCH_EG_RATE_3]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_RATE_4]=E[t.PACKED_PITCH_EG_RATE_4]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_LEVEL_1]=E[t.PACKED_PITCH_EG_LEVEL_1]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_LEVEL_2]=E[t.PACKED_PITCH_EG_LEVEL_2]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_LEVEL_3]=E[t.PACKED_PITCH_EG_LEVEL_3]&t.MASK_7BIT,n[t.UNPACKED_PITCH_EG_LEVEL_4]=E[t.PACKED_PITCH_EG_LEVEL_4]&t.MASK_7BIT,n[t.UNPACKED_ALGORITHM]=E[t.OFFSET_ALGORITHM]&t.MASK_5BIT;const s=E[t.OFFSET_FEEDBACK]&t.MASK_7BIT;n[t.UNPACKED_FEEDBACK]=s&t.MASK_3BIT,n[t.UNPACKED_OSC_SYNC]=s>>3&t.MASK_1BIT,n[t.UNPACKED_LFO_SPEED]=E[t.OFFSET_LFO_SPEED]&t.MASK_7BIT,n[t.UNPACKED_LFO_DELAY]=E[t.OFFSET_LFO_DELAY]&t.MASK_7BIT,n[t.UNPACKED_LFO_PM_DEPTH]=E[t.OFFSET_LFO_PM_DEPTH]&t.MASK_7BIT,n[t.UNPACKED_LFO_AM_DEPTH]=E[t.OFFSET_LFO_AM_DEPTH]&t.MASK_7BIT;const _=E[t.OFFSET_LFO_SYNC_WAVE]&t.MASK_7BIT;n[t.UNPACKED_LFO_KEY_SYNC]=_&t.MASK_1BIT,n[t.UNPACKED_LFO_WAVE]=_>>1&t.MASK_3BIT,n[t.UNPACKED_LFO_PM_SENS]=_>>4&t.MASK_3BIT,n[t.UNPACKED_AMP_MOD_SENS]=E[t.OFFSET_AMP_MOD_SENS]&t.MASK_7BIT,n[t.UNPACKED_TRANSPOSE]=E[t.OFFSET_TRANSPOSE]&t.MASK_7BIT,n[t.UNPACKED_EG_BIAS_SENS]=E[t.OFFSET_EG_BIAS_SENS]&t.MASK_7BIT;for(let i=0;i<t.NAME_LENGTH;i++)n[t.UNPACKED_NAME_START+i]=E[t.PACKED_NAME_START+i]&t.MASK_7BIT;return n}static pack(E){if(E.length!==t.UNPACKED_SIZE)throw new d(`Invalid unpacked data length: expected ${t.UNPACKED_SIZE} bytes, got ${E.length}`,"length",E.length);const n=new Uint8Array(t.PACKED_SIZE);for(let T=0;T<t.NUM_OPERATORS;T++){const N=T*t.UNPACKED_OP_SIZE,A=(t.NUM_OPERATORS-1-T)*t.PACKED_OP_SIZE;n[A+t.PACKED_OP_EG_RATE_1]=E[N+t.UNPACKED_OP_EG_RATE_1],n[A+t.PACKED_OP_EG_RATE_2]=E[N+t.UNPACKED_OP_EG_RATE_2],n[A+t.PACKED_OP_EG_RATE_3]=E[N+t.UNPACKED_OP_EG_RATE_3],n[A+t.PACKED_OP_EG_RATE_4]=E[N+t.UNPACKED_OP_EG_RATE_4],n[A+t.PACKED_OP_EG_LEVEL_1]=E[N+t.UNPACKED_OP_EG_LEVEL_1],n[A+t.PACKED_OP_EG_LEVEL_2]=E[N+t.UNPACKED_OP_EG_LEVEL_2],n[A+t.PACKED_OP_EG_LEVEL_3]=E[N+t.UNPACKED_OP_EG_LEVEL_3],n[A+t.PACKED_OP_EG_LEVEL_4]=E[N+t.UNPACKED_OP_EG_LEVEL_4],n[A+t.PACKED_OP_BREAK_POINT]=E[N+t.UNPACKED_OP_BREAK_POINT],n[A+t.PACKED_OP_L_SCALE_DEPTH]=E[N+t.UNPACKED_OP_L_SCALE_DEPTH],n[A+t.PACKED_OP_R_SCALE_DEPTH]=E[N+t.UNPACKED_OP_R_SCALE_DEPTH];const P=E[N+t.UNPACKED_OP_L_CURVE]&t.MASK_2BIT,c=E[N+t.UNPACKED_OP_R_CURVE]&t.MASK_2BIT;n[A+t.PACKED_OP_CURVES]=P|c<<2;const l=E[N+t.UNPACKED_OP_RATE_SCALING]&t.MASK_3BIT,S=E[N+t.UNPACKED_OP_DETUNE]&t.MASK_4BIT;n[A+t.PACKED_OP_RATE_SCALING]=l|S<<3;const I=E[N+t.UNPACKED_OP_AMP_MOD_SENS]&t.MASK_2BIT,R=E[N+t.UNPACKED_OP_KEY_VEL_SENS]&t.MASK_3BIT;n[A+t.PACKED_OP_MOD_SENS]=I|R<<2,n[A+t.PACKED_OP_OUTPUT_LEVEL]=E[N+t.UNPACKED_OP_OUTPUT_LEVEL];const L=E[N+t.UNPACKED_OP_MODE]&t.MASK_1BIT,h=E[N+t.UNPACKED_OP_FREQ_COARSE]&t.MASK_5BIT;n[A+t.PACKED_OP_MODE_FREQ]=L|h<<1;const m=E[N+t.UNPACKED_OP_OSC_DETUNE]&t.MASK_3BIT,F=E[N+t.UNPACKED_OP_FREQ_FINE]&t.MASK_4BIT;n[A+t.PACKED_OP_DETUNE_FINE]=m|F<<3}n[t.PACKED_PITCH_EG_RATE_1]=E[t.UNPACKED_PITCH_EG_RATE_1],n[t.PACKED_PITCH_EG_RATE_2]=E[t.UNPACKED_PITCH_EG_RATE_2],n[t.PACKED_PITCH_EG_RATE_3]=E[t.UNPACKED_PITCH_EG_RATE_3],n[t.PACKED_PITCH_EG_RATE_4]=E[t.UNPACKED_PITCH_EG_RATE_4],n[t.PACKED_PITCH_EG_LEVEL_1]=E[t.UNPACKED_PITCH_EG_LEVEL_1],n[t.PACKED_PITCH_EG_LEVEL_2]=E[t.UNPACKED_PITCH_EG_LEVEL_2],n[t.PACKED_PITCH_EG_LEVEL_3]=E[t.UNPACKED_PITCH_EG_LEVEL_3],n[t.PACKED_PITCH_EG_LEVEL_4]=E[t.UNPACKED_PITCH_EG_LEVEL_4],n[t.OFFSET_ALGORITHM]=E[t.UNPACKED_ALGORITHM];const s=E[t.UNPACKED_FEEDBACK]&t.MASK_3BIT,_=E[t.UNPACKED_OSC_SYNC]&t.MASK_1BIT;n[t.OFFSET_FEEDBACK]=s|_<<3,n[t.OFFSET_LFO_SPEED]=E[t.UNPACKED_LFO_SPEED],n[t.OFFSET_LFO_DELAY]=E[t.UNPACKED_LFO_DELAY],n[t.OFFSET_LFO_PM_DEPTH]=E[t.UNPACKED_LFO_PM_DEPTH],n[t.OFFSET_LFO_AM_DEPTH]=E[t.UNPACKED_LFO_AM_DEPTH];const i=E[t.UNPACKED_LFO_KEY_SYNC]&t.MASK_1BIT,e=E[t.UNPACKED_LFO_WAVE]&t.MASK_3BIT,r=E[t.UNPACKED_LFO_PM_SENS]&t.MASK_3BIT;n[t.OFFSET_LFO_SYNC_WAVE]=i|e<<1|r<<4,n[t.OFFSET_AMP_MOD_SENS]=E[t.UNPACKED_AMP_MOD_SENS],n[t.OFFSET_TRANSPOSE]=E[t.UNPACKED_TRANSPOSE],n[t.OFFSET_EG_BIAS_SENS]=E[t.UNPACKED_EG_BIAS_SENS];for(let T=0;T<t.NAME_LENGTH;T++)n[t.PACKED_NAME_START+T]=E[t.UNPACKED_NAME_START+T];return n}static createDefault(E=0){const n=new Uint8Array(t.UNPACKED_SIZE);for(let i=0;i<t.NUM_OPERATORS;i++){const e=i*t.UNPACKED_OP_SIZE;n[e+t.UNPACKED_OP_EG_RATE_1]=t.DEFAULT_EG_RATE,n[e+t.UNPACKED_OP_EG_RATE_2]=t.DEFAULT_EG_RATE,n[e+t.UNPACKED_OP_EG_RATE_3]=t.DEFAULT_EG_RATE,n[e+t.UNPACKED_OP_EG_RATE_4]=t.DEFAULT_EG_RATE,n[e+t.UNPACKED_OP_EG_LEVEL_1]=t.DEFAULT_EG_LEVEL_MAX,n[e+t.UNPACKED_OP_EG_LEVEL_2]=t.DEFAULT_EG_LEVEL_MAX,n[e+t.UNPACKED_OP_EG_LEVEL_3]=t.DEFAULT_EG_LEVEL_MAX,n[e+t.UNPACKED_OP_EG_LEVEL_4]=t.DEFAULT_EG_LEVEL_MIN,n[e+t.UNPACKED_OP_BREAK_POINT]=t.DEFAULT_BREAK_POINT,n[e+t.UNPACKED_OP_L_SCALE_DEPTH]=0,n[e+t.UNPACKED_OP_R_SCALE_DEPTH]=0,n[e+t.UNPACKED_OP_L_CURVE]=0,n[e+t.UNPACKED_OP_R_CURVE]=0,n[e+t.UNPACKED_OP_RATE_SCALING]=0,n[e+t.UNPACKED_OP_AMP_MOD_SENS]=0,n[e+t.UNPACKED_OP_KEY_VEL_SENS]=0,n[e+t.UNPACKED_OP_OUTPUT_LEVEL]=t.DEFAULT_OUTPUT_LEVEL,n[e+t.UNPACKED_OP_MODE]=0,n[e+t.UNPACKED_OP_FREQ_COARSE]=0,n[e+t.UNPACKED_OP_OSC_DETUNE]=0,n[e+t.UNPACKED_OP_FREQ_FINE]=0}n[t.UNPACKED_PITCH_EG_RATE_1]=t.DEFAULT_EG_RATE,n[t.UNPACKED_PITCH_EG_RATE_2]=t.DEFAULT_EG_RATE,n[t.UNPACKED_PITCH_EG_RATE_3]=t.DEFAULT_EG_RATE,n[t.UNPACKED_PITCH_EG_RATE_4]=t.DEFAULT_EG_RATE,n[t.UNPACKED_PITCH_EG_LEVEL_1]=t.DEFAULT_PITCH_EG_LEVEL,n[t.UNPACKED_PITCH_EG_LEVEL_2]=t.DEFAULT_PITCH_EG_LEVEL,n[t.UNPACKED_PITCH_EG_LEVEL_3]=t.DEFAULT_PITCH_EG_LEVEL,n[t.UNPACKED_PITCH_EG_LEVEL_4]=t.DEFAULT_PITCH_EG_LEVEL,n[t.UNPACKED_ALGORITHM]=t.DEFAULT_ALGORITHM,n[t.UNPACKED_FEEDBACK]=t.DEFAULT_FEEDBACK,n[t.UNPACKED_OSC_SYNC]=0,n[t.UNPACKED_LFO_SPEED]=t.DEFAULT_LFO_SPEED,n[t.UNPACKED_LFO_DELAY]=0,n[t.UNPACKED_LFO_PM_DEPTH]=0,n[t.UNPACKED_LFO_AM_DEPTH]=0,n[t.UNPACKED_LFO_KEY_SYNC]=0,n[t.UNPACKED_LFO_WAVE]=0,n[t.UNPACKED_LFO_PM_SENS]=t.DEFAULT_LFO_PM_SENS,n[t.UNPACKED_AMP_MOD_SENS]=0,n[t.UNPACKED_TRANSPOSE]=t.TRANSPOSE_CENTER,n[t.UNPACKED_EG_BIAS_SENS]=0;const s="Init Voice";for(let i=0;i<t.NAME_LENGTH;i++)n[t.UNPACKED_NAME_START+i]=i<s.length?s.charCodeAt(i):t.CHAR_SPACE;const _=t.pack(n);return new t(_,E)}static fromUnpacked(E,n=0){const s=t.pack(E);return new t(s,n)}static async fromFile(E){return new Promise((n,s)=>{const _=new FileReader;_.onload=i=>{try{const e=new Uint8Array(i.target.result);if(e[0]!==t.VCED_SYSEX_START||e[1]!==t.VCED_YAMAHA_ID||e[2]!==t.VCED_SUB_STATUS||e[3]!==t.VCED_FORMAT_SINGLE||e[4]!==t.VCED_BYTE_COUNT_MSB||e[5]!==t.VCED_BYTE_COUNT_LSB)throw new G("Invalid VCED header","header",0);const r=e.subarray(t.VCED_HEADER_SIZE,t.VCED_HEADER_SIZE+t.VCED_DATA_SIZE),T=e[t.VCED_HEADER_SIZE+t.VCED_DATA_SIZE],N=u._calculateChecksum(r,t.VCED_DATA_SIZE);T!==N&&console.warn(`DX7 VCED checksum mismatch (expected ${N.toString(16)}, got ${T.toString(16)}). This is common with vintage SysEx files.`);const A=new Uint8Array(t.UNPACKED_SIZE);let P=0;for(let l=0;l<t.NUM_OPERATORS;l++){const S=(t.NUM_OPERATORS-1-l)*t.UNPACKED_OP_SIZE;A[S+t.UNPACKED_OP_EG_RATE_1]=r[P++],A[S+t.UNPACKED_OP_EG_RATE_2]=r[P++],A[S+t.UNPACKED_OP_EG_RATE_3]=r[P++],A[S+t.UNPACKED_OP_EG_RATE_4]=r[P++],A[S+t.UNPACKED_OP_EG_LEVEL_1]=r[P++],A[S+t.UNPACKED_OP_EG_LEVEL_2]=r[P++],A[S+t.UNPACKED_OP_EG_LEVEL_3]=r[P++],A[S+t.UNPACKED_OP_EG_LEVEL_4]=r[P++],A[S+t.UNPACKED_OP_BREAK_POINT]=r[P++],A[S+t.UNPACKED_OP_L_SCALE_DEPTH]=r[P++],A[S+t.UNPACKED_OP_R_SCALE_DEPTH]=r[P++],A[S+t.UNPACKED_OP_L_CURVE]=r[P++],A[S+t.UNPACKED_OP_R_CURVE]=r[P++],A[S+t.UNPACKED_OP_RATE_SCALING]=r[P++],A[S+t.UNPACKED_OP_DETUNE]=r[P++];const I=r[P++];A[S+t.UNPACKED_OP_AMP_MOD_SENS]=I&t.MASK_2BIT,A[S+t.UNPACKED_OP_KEY_VEL_SENS]=I>>2&t.MASK_3BIT,A[S+t.UNPACKED_OP_OUTPUT_LEVEL]=r[P++],A[S+t.UNPACKED_OP_MODE]=r[P++],A[S+t.UNPACKED_OP_FREQ_COARSE]=r[P++],A[S+t.UNPACKED_OP_FREQ_FINE]=r[P++],A[S+t.UNPACKED_OP_OSC_DETUNE]=r[P++]}A[t.UNPACKED_PITCH_EG_RATE_1]=r[P++],A[t.UNPACKED_PITCH_EG_RATE_2]=r[P++],A[t.UNPACKED_PITCH_EG_RATE_3]=r[P++],A[t.UNPACKED_PITCH_EG_RATE_4]=r[P++],A[t.UNPACKED_PITCH_EG_LEVEL_1]=r[P++],A[t.UNPACKED_PITCH_EG_LEVEL_2]=r[P++],A[t.UNPACKED_PITCH_EG_LEVEL_3]=r[P++],A[t.UNPACKED_PITCH_EG_LEVEL_4]=r[P++],A[t.UNPACKED_ALGORITHM]=r[P++],A[t.UNPACKED_FEEDBACK]=r[P++],A[t.UNPACKED_OSC_SYNC]=r[P++],A[t.UNPACKED_LFO_SPEED]=r[P++],A[t.UNPACKED_LFO_DELAY]=r[P++],A[t.UNPACKED_LFO_PM_DEPTH]=r[P++],A[t.UNPACKED_LFO_AM_DEPTH]=r[P++],A[t.UNPACKED_LFO_KEY_SYNC]=r[P++],A[t.UNPACKED_LFO_WAVE]=r[P++],A[t.UNPACKED_LFO_PM_SENS]=r[P++],A[t.UNPACKED_TRANSPOSE]=r[P++];for(let l=0;l<t.NAME_LENGTH;l++)A[t.UNPACKED_NAME_START+l]=r[P++];const c=t.pack(A);n(new t(c,0))}catch(e){s(e)}},_.onerror=()=>s(new Error("Failed to read file")),_.readAsArrayBuffer(E)})}toSysEx(){const E=this.unpack(),n=new Uint8Array(t.VCED_SIZE);let s=0;n[s++]=t.VCED_SYSEX_START,n[s++]=t.VCED_YAMAHA_ID,n[s++]=t.VCED_SUB_STATUS,n[s++]=t.VCED_FORMAT_SINGLE,n[s++]=t.VCED_BYTE_COUNT_MSB,n[s++]=t.VCED_BYTE_COUNT_LSB;for(let i=t.NUM_OPERATORS-1;i>=0;i--){const e=i*t.UNPACKED_OP_SIZE;n[s++]=E[e+t.UNPACKED_OP_EG_RATE_1],n[s++]=E[e+t.UNPACKED_OP_EG_RATE_2],n[s++]=E[e+t.UNPACKED_OP_EG_RATE_3],n[s++]=E[e+t.UNPACKED_OP_EG_RATE_4],n[s++]=E[e+t.UNPACKED_OP_EG_LEVEL_1],n[s++]=E[e+t.UNPACKED_OP_EG_LEVEL_2],n[s++]=E[e+t.UNPACKED_OP_EG_LEVEL_3],n[s++]=E[e+t.UNPACKED_OP_EG_LEVEL_4],n[s++]=E[e+t.UNPACKED_OP_BREAK_POINT],n[s++]=E[e+t.UNPACKED_OP_L_SCALE_DEPTH],n[s++]=E[e+t.UNPACKED_OP_R_SCALE_DEPTH],n[s++]=E[e+t.UNPACKED_OP_L_CURVE],n[s++]=E[e+t.UNPACKED_OP_R_CURVE],n[s++]=E[e+t.UNPACKED_OP_RATE_SCALING],n[s++]=E[e+t.UNPACKED_OP_DETUNE];const r=E[e+t.UNPACKED_OP_AMP_MOD_SENS]&t.MASK_2BIT,T=E[e+t.UNPACKED_OP_KEY_VEL_SENS]&t.MASK_3BIT;n[s++]=r|T<<2,n[s++]=E[e+t.UNPACKED_OP_OUTPUT_LEVEL],n[s++]=E[e+t.UNPACKED_OP_MODE],n[s++]=E[e+t.UNPACKED_OP_FREQ_COARSE],n[s++]=E[e+t.UNPACKED_OP_OSC_DETUNE],n[s++]=E[e+t.UNPACKED_OP_FREQ_FINE]}n[s++]=E[t.UNPACKED_PITCH_EG_RATE_1],n[s++]=E[t.UNPACKED_PITCH_EG_RATE_2],n[s++]=E[t.UNPACKED_PITCH_EG_RATE_3],n[s++]=E[t.UNPACKED_PITCH_EG_RATE_4],n[s++]=E[t.UNPACKED_PITCH_EG_LEVEL_1],n[s++]=E[t.UNPACKED_PITCH_EG_LEVEL_2],n[s++]=E[t.UNPACKED_PITCH_EG_LEVEL_3],n[s++]=E[t.UNPACKED_PITCH_EG_LEVEL_4],n[s++]=E[t.UNPACKED_ALGORITHM],n[s++]=E[t.UNPACKED_FEEDBACK],n[s++]=E[t.UNPACKED_OSC_SYNC],n[s++]=E[t.UNPACKED_LFO_SPEED],n[s++]=E[t.UNPACKED_LFO_DELAY],n[s++]=E[t.UNPACKED_LFO_PM_DEPTH],n[s++]=E[t.UNPACKED_LFO_AM_DEPTH],n[s++]=E[t.UNPACKED_LFO_KEY_SYNC],n[s++]=E[t.UNPACKED_LFO_WAVE],n[s++]=E[t.UNPACKED_LFO_PM_SENS],n[s++]=E[t.UNPACKED_TRANSPOSE];for(let i=0;i<t.NAME_LENGTH;i++)n[s++]=E[t.UNPACKED_NAME_START+i];const _=n.subarray(t.VCED_HEADER_SIZE,t.VCED_HEADER_SIZE+t.VCED_DATA_SIZE);return n[s++]=u._calculateChecksum(_,t.VCED_DATA_SIZE),n[s++]=t.VCED_SYSEX_END,n}toJSON(){const E=this.unpack(),n=[],s=e=>["-LN","-EX","+EX","+LN"][e]||"UNKNOWN",_=e=>["TRIANGLE","SAW DOWN","SAW UP","SQUARE","SINE","SAMPLE & HOLD"][e]||"UNKNOWN",i=e=>{const r=["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],T=Math.floor(e/12)+t.MIDI_OCTAVE_OFFSET;return`${r[e%12]}${T}`};for(let e=0;e<t.NUM_OPERATORS;e++){const r=e*t.UNPACKED_OP_SIZE,T=E[r+t.UNPACKED_OP_MODE]===0?"RATIO":"FIXED";n.push({id:e+1,osc:{detune:E[r+t.UNPACKED_OP_OSC_DETUNE],freq:{coarse:E[r+t.UNPACKED_OP_FREQ_COARSE],fine:E[r+t.UNPACKED_OP_FREQ_FINE],mode:T}},eg:{rates:[E[r+t.UNPACKED_OP_EG_RATE_1],E[r+t.UNPACKED_OP_EG_RATE_2],E[r+t.UNPACKED_OP_EG_RATE_3],E[r+t.UNPACKED_OP_EG_RATE_4]],levels:[E[r+t.UNPACKED_OP_EG_LEVEL_1],E[r+t.UNPACKED_OP_EG_LEVEL_2],E[r+t.UNPACKED_OP_EG_LEVEL_3],E[r+t.UNPACKED_OP_EG_LEVEL_4]]},key:{velocity:E[r+t.UNPACKED_OP_KEY_VEL_SENS],scaling:E[r+t.UNPACKED_OP_RATE_SCALING],breakPoint:i(E[r+t.UNPACKED_OP_BREAK_POINT]+t.MIDI_BREAK_POINT_OFFSET)},output:{level:E[r+t.UNPACKED_OP_OUTPUT_LEVEL],ampModSens:E[r+t.UNPACKED_OP_AMP_MOD_SENS]},scale:{left:{depth:E[r+t.UNPACKED_OP_L_SCALE_DEPTH],curve:s(E[r+t.UNPACKED_OP_L_CURVE])},right:{depth:E[r+t.UNPACKED_OP_R_SCALE_DEPTH],curve:s(E[r+t.UNPACKED_OP_R_CURVE])}}})}return{name:this.name||"(Empty)",operators:n,pitchEG:{rates:[E[t.UNPACKED_PITCH_EG_RATE_1],E[t.UNPACKED_PITCH_EG_RATE_2],E[t.UNPACKED_PITCH_EG_RATE_3],E[t.UNPACKED_PITCH_EG_RATE_4]],levels:[E[t.UNPACKED_PITCH_EG_LEVEL_1],E[t.UNPACKED_PITCH_EG_LEVEL_2],E[t.UNPACKED_PITCH_EG_LEVEL_3],E[t.UNPACKED_PITCH_EG_LEVEL_4]]},lfo:{speed:E[t.UNPACKED_LFO_SPEED],delay:E[t.UNPACKED_LFO_DELAY],pmDepth:E[t.UNPACKED_LFO_PM_DEPTH],amDepth:E[t.UNPACKED_LFO_AM_DEPTH],keySync:E[t.UNPACKED_LFO_KEY_SYNC]===1,wave:_(E[t.UNPACKED_LFO_WAVE])},global:{algorithm:E[t.UNPACKED_ALGORITHM]+1,feedback:E[t.UNPACKED_FEEDBACK],oscKeySync:E[t.UNPACKED_OSC_SYNC]===1,pitchModSens:E[t.UNPACKED_LFO_PM_SENS],transpose:E[t.UNPACKED_TRANSPOSE]-t.TRANSPOSE_CENTER}}}}class u{static SYSEX_START=240;static SYSEX_END=247;static SYSEX_YAMAHA_ID=67;static SYSEX_SUB_STATUS=0;static SYSEX_FORMAT_32_VOICES=9;static SYSEX_BYTE_COUNT_MSB=32;static SYSEX_BYTE_COUNT_LSB=0;static SYSEX_HEADER=[u.SYSEX_START,u.SYSEX_YAMAHA_ID,u.SYSEX_SUB_STATUS,u.SYSEX_FORMAT_32_VOICES,u.SYSEX_BYTE_COUNT_MSB,u.SYSEX_BYTE_COUNT_LSB];static SYSEX_HEADER_SIZE=6;static VOICE_DATA_SIZE=4096;static SYSEX_SIZE=4104;static VOICE_SIZE=128;static NUM_VOICES=32;static CHECKSUM_MODULO=128;static MASK_7BIT=127;constructor(E,n=""){if(this.voices=new Array(u.NUM_VOICES),this.name=n,E)this._load(E);else for(let s=0;s<u.NUM_VOICES;s++)this.voices[s]=t.createDefault(s)}static _calculateChecksum(E,n){let s=0;for(let _=0;_<n;_++)s+=E[_];return u.CHECKSUM_MODULO-s%u.CHECKSUM_MODULO&u.MASK_7BIT}_load(E){const n=E instanceof Uint8Array?E:new Uint8Array(E);let s,_=0;if(n[0]===u.SYSEX_START){const e=n.subarray(0,u.SYSEX_HEADER_SIZE),r=u.SYSEX_HEADER;for(let T=0;T<u.SYSEX_HEADER_SIZE;T++)if(e[T]!==r[T])throw new G(`Invalid SysEx header at position ${T}: expected ${r[T].toString(16)}, got ${e[T].toString(16)}`,"header",T);s=n.subarray(u.SYSEX_HEADER_SIZE,u.SYSEX_HEADER_SIZE+u.VOICE_DATA_SIZE),_=u.SYSEX_HEADER_SIZE}else if(n.length===u.VOICE_DATA_SIZE)s=n;else throw new d(`Invalid data length: expected ${u.VOICE_DATA_SIZE} or ${u.SYSEX_SIZE} bytes, got ${n.length}`,"length",n.length);if(s.length!==u.VOICE_DATA_SIZE)throw new d(`Invalid voice data length: expected ${u.VOICE_DATA_SIZE} bytes, got ${s.length}`,"length",s.length);const i=u.SYSEX_HEADER_SIZE+u.VOICE_DATA_SIZE;if(_>0&&n.length>=i+1){const e=n[i],r=u._calculateChecksum(s,u.VOICE_DATA_SIZE);e!==r&&console.warn(`DX7 checksum mismatch (expected ${r.toString(16)}, got ${e.toString(16)}). This is common with vintage SysEx files and the data is likely still valid.`)}this.voices=new Array(u.NUM_VOICES);for(let e=0;e<u.NUM_VOICES;e++){const r=e*u.VOICE_SIZE,T=s.subarray(r,r+u.VOICE_SIZE);this.voices[e]=new t(T,e)}}replaceVoice(E,n){if(E<0||E>=u.NUM_VOICES)throw new d(`Invalid voice index: ${E}`,"index",E);const s=new Uint8Array(n.data);this.voices[E]=new t(s,E)}addVoice(E){for(let n=0;n<this.voices.length;n++){const s=this.voices[n];if(s.name===""||s.name==="Init Voice")return this.replaceVoice(n,E),n}return-1}getVoices(){return this.voices}getVoice(E){return E<0||E>=this.voices.length?null:this.voices[E]}getVoiceNames(){return this.voices.map(E=>E.name)}findVoiceByName(E){const n=E.toLowerCase();return this.voices.find(s=>s.name.toLowerCase().includes(n))||null}static async fromFile(E){return new Promise((n,s)=>{const _=new FileReader;_.onload=async i=>{try{const e=E.name||"",r=new Uint8Array(i.target.result);if(r[0]===u.SYSEX_START&&r[3]===t.VCED_FORMAT_SINGLE)s(new G("This is a single voice file. Use DX7Voice.fromFile() instead.","format",3));else{const T=e.replace(/\.[^/.]+$/,""),N=new u(i.target.result,T);n(N)}}catch(e){s(e)}},_.onerror=()=>s(new Error("Failed to read file")),_.readAsArrayBuffer(E)})}toSysEx(){const E=new Uint8Array(u.SYSEX_SIZE);let n=0;u.SYSEX_HEADER.forEach(_=>{E[n++]=_});for(const _ of this.voices)for(let i=0;i<u.VOICE_SIZE;i++)E[n++]=_.data[i];const s=E.subarray(u.SYSEX_HEADER_SIZE,u.SYSEX_HEADER_SIZE+u.VOICE_DATA_SIZE);return E[n++]=u._calculateChecksum(s,u.VOICE_DATA_SIZE),E[n++]=u.SYSEX_END,E}toJSON(){const E=this.voices.map((n,s)=>{const _=n.toJSON();return{index:s+1,..._}});return{version:"1.0",name:this.name||"",voices:E}}}function tt(a){return a[0]!==240||a[a.length-1]!==247?null:{manufacturerId:a[1],payload:a.slice(2,-1),raw:a}}function Et(a,E){return[240,a,...E,247]}function nt(a){return a.length>=2&&a[0]===240&&a[a.length-1]===247}function st(a){const E=[];for(let n=0;n<a.length;n+=7){const s=a.slice(n,n+7);let _=0;const i=[];for(let e=0;e<s.length;e++){const r=s[e];r&128&&(_|=1<<e),i.push(r&127)}E.push(_,...i)}return E}function _t(a){const E=[];for(let n=0;n<a.length;n+=8){const s=a[n],_=Math.min(7,a.length-n-1);for(let i=0;i<_;i++){let e=a[n+1+i];s&1<<i&&(e|=128),E.push(e)}}return E}function et(a){return Number.isInteger(a)&&a>=1&&a<=16}function rt(a){return Number.isInteger(a)&&a>=0&&a<=127}function it(a){return Number.isInteger(a)&&a>=0&&a<=31}function at(a){return Number.isInteger(a)&&a>=0&&a<=127}function At(a){return Number.isInteger(a)&&a>=0&&a<=127}function Pt(a){return Number.isInteger(a)&&a>=0&&a<=127}function Ct(a){return Number.isInteger(a)&&a>=0&&a<=127}function ut(a){return Number.isInteger(a)&&a>=0&&a<=16383}function Tt(a,E){return Number.isInteger(a)&&a>=0&&a<=127&&Number.isInteger(E)&&E>=0&&E<=127}async function $(a={}){const E=new x(a);await E.initialize();const n=a.selector||"[data-midi-cc]";{const s=new f(E,n);s.bindAll(),a.watchDOM&&s.enableAutoBinding(),E._binder=s}return E}async function Nt(a={}){const{onStatusUpdate:E,onConnectionUpdate:n,channel:s,output:_,sysex:i,onReady:e,onError:r,selector:T,watchDOM:N,...A}=a,c=await $({autoConnect:!1,sysex:i,channel:s||1,selector:T||"[data-midi-cc]",watchDOM:N,onError:r,...A}),l=new Z({midiController:c,onStatusUpdate:E||(()=>{}),onConnectionUpdate:n||(()=>{}),channel:s||1});if(_)try{await c.setOutput(_),l.currentDevice=c.getCurrentOutput(),l.updateConnectionStatus()}catch(S){r?r(S):console.error("Failed to connect to MIDI device:",S.message)}return e&&e(c,l),l}C.CONN=K,C.CONNECTION_EVENTS=K,C.CONTROLLER_EVENTS=o,C.CTRL=o,C.DX7Bank=u,C.DX7Error=H,C.DX7ParseError=G,C.DX7ValidationError=d,C.DX7Voice=t,C.DataAttributeBinder=f,C.EventEmitter=b,C.MIDIAccessError=p,C.MIDIConnection=Y,C.MIDIConnectionError=g,C.MIDIController=x,C.MIDIDeviceError=U,C.MIDIDeviceManager=Z,C.MIDIError=M,C.MIDIValidationError=D,C.clamp=O,C.createMIDIController=$,C.createMIDIDeviceManager=Nt,C.createSysEx=Et,C.decode14BitValue=B,C.decode7Bit=_t,C.denormalize14BitValue=X,C.denormalizeValue=q,C.encode14BitValue=v,C.encode7Bit=st,C.frequencyToNote=J,C.getCCName=V,C.isSysEx=nt,C.isValid14BitCC=it,C.isValidCC=rt,C.isValidChannel=et,C.isValidMIDIValue=at,C.isValidNote=At,C.isValidPitchBend=ut,C.isValidPitchBendBytes=Tt,C.isValidProgramChange=Ct,C.isValidVelocity=Pt,C.normalize14BitValue=w,C.normalizeValue=y,C.noteNameToNumber=Q,C.noteNumberToName=j,C.noteToFrequency=k,C.parseSysEx=tt,Object.defineProperty(C,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "midiwire",
3
+ "version": "0.1.0",
4
+ "description": "Declarative JavaScript library for browser-based MIDI control",
5
+ "type": "module",
6
+ "main": "./dist/midiwire.umd.js",
7
+ "module": "./dist/midiwire.es.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/midiwire.es.js",
11
+ "require": "./dist/midiwire.umd.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "dev": "vite",
20
+ "build": "vite build",
21
+ "preview": "vite preview",
22
+ "test": "vitest",
23
+ "test:ui": "vitest --ui",
24
+ "test:coverage": "vitest run --coverage",
25
+ "lint": "biome check --write"
26
+ },
27
+ "keywords": [
28
+ "midiwire",
29
+ "midi",
30
+ "webmidi",
31
+ "wire",
32
+ "controller",
33
+ "synth",
34
+ "synthesizer",
35
+ "cc",
36
+ "control-change",
37
+ "sysex",
38
+ "audio",
39
+ "music"
40
+ ],
41
+ "author": "",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/alexferl/midiwire.git"
46
+ },
47
+ "devDependencies": {
48
+ "@biomejs/biome": "2.3.11",
49
+ "@vitest/coverage-v8": "4.0.17",
50
+ "@vitest/ui": "4.0.17",
51
+ "jsdom": "27.4.0",
52
+ "vite": "7.3.1",
53
+ "vitest": "4.0.17"
54
+ },
55
+ "engines": {
56
+ "node": ">=20.0.0"
57
+ }
58
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Declarative MIDI binding system using HTML data attributes.
3
+ * Features:
4
+ * - Auto-discovery: Scans DOM for data-midi-* attributes
5
+ * - 7-bit CC: data-midi-cc="74"
6
+ * - 14-bit CC: data-midi-msb="74" data-midi-lsb="75"
7
+ * - Custom ranges: min/max attributes or data-midi-min/max
8
+ * - Channel override: data-midi-channel="2"
9
+ * - Value inversion: data-midi-invert="true"
10
+ * - Debouncing: data-midi-debounce="100"
11
+ * - Auto-binding: Watch DOM for dynamically added elements
12
+ * - Labeling: data-midi-label="Filter Cutoff"
13
+ *
14
+ * Enables zero-JavaScript MIDI controller creation with pure HTML.
15
+ */
16
+ export class DataAttributeBinder {
17
+ /**
18
+ * @param {import("../core/MIDIController.js").MIDIController} controller
19
+ * @param {string} selector - CSS selector for elements to bind
20
+ */
21
+ constructor(controller, selector = "[data-midi-cc]") {
22
+ this.controller = controller
23
+ this.selector = selector
24
+ this.observer = null
25
+ }
26
+
27
+ /**
28
+ * Bind all matching elements in the document
29
+ */
30
+ bindAll() {
31
+ // Support both 7-bit CC (data-midi-cc) and 14-bit CC (data-midi-msb + data-midi-lsb)
32
+ const elements = document.querySelectorAll(
33
+ this.selector === "[data-midi-cc]"
34
+ ? "[data-midi-cc], [data-midi-msb][data-midi-lsb]"
35
+ : this.selector,
36
+ )
37
+
38
+ elements.forEach((element) => {
39
+ // Skip if already bound
40
+ if (element.hasAttribute("data-midi-bound")) return
41
+
42
+ const config = this._parseAttributes(element)
43
+ if (config) {
44
+ this.controller.bind(element, config)
45
+ element.setAttribute("data-midi-bound", "true")
46
+ }
47
+ })
48
+ }
49
+
50
+ /**
51
+ * Watch for dynamically added elements and auto-bind them
52
+ */
53
+ enableAutoBinding() {
54
+ if (this.observer) return
55
+
56
+ // Support both 7-bit CC (data-midi-cc) and 14-bit CC (data-midi-msb + data-midi-lsb)
57
+ const selector =
58
+ this.selector === "[data-midi-cc]"
59
+ ? "[data-midi-cc], [data-midi-msb][data-midi-lsb]"
60
+ : this.selector
61
+
62
+ this.observer = new MutationObserver((mutations) => {
63
+ mutations.forEach((mutation) => {
64
+ // Handle added nodes
65
+ mutation.addedNodes.forEach((node) => {
66
+ if (node.nodeType === Node.ELEMENT_NODE) {
67
+ // Check the node itself
68
+ if (node.matches?.(selector)) {
69
+ const config = this._parseAttributes(node)
70
+ if (config && !node.hasAttribute("data-midi-bound")) {
71
+ this.controller.bind(node, config)
72
+ node.setAttribute("data-midi-bound", "true")
73
+ }
74
+ }
75
+
76
+ // Check children
77
+ if (node.querySelectorAll) {
78
+ const children = node.querySelectorAll(selector)
79
+ children.forEach((child) => {
80
+ if (!child.hasAttribute("data-midi-bound")) {
81
+ const config = this._parseAttributes(child)
82
+ if (config) {
83
+ this.controller.bind(child, config)
84
+ child.setAttribute("data-midi-bound", "true")
85
+ }
86
+ }
87
+ })
88
+ }
89
+ }
90
+ })
91
+
92
+ // Handle removed nodes
93
+ mutation.removedNodes.forEach((node) => {
94
+ if (node.nodeType === Node.ELEMENT_NODE) {
95
+ // Unbind removed elements
96
+ if (node.hasAttribute?.("data-midi-bound")) {
97
+ this.controller.unbind(node)
98
+ }
99
+
100
+ // Check children recursively
101
+ if (node.querySelectorAll) {
102
+ const boundChildren = node.querySelectorAll("[data-midi-bound]")
103
+ boundChildren.forEach((child) => {
104
+ this.controller.unbind(child)
105
+ })
106
+ }
107
+ }
108
+ })
109
+ })
110
+ })
111
+
112
+ this.observer.observe(document.body, {
113
+ childList: true,
114
+ subtree: true,
115
+ })
116
+ }
117
+
118
+ /**
119
+ * Stop watching for new elements
120
+ */
121
+ disableAutoBinding() {
122
+ if (this.observer) {
123
+ this.observer.disconnect()
124
+ this.observer = null
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Parse MIDI config from data attributes
130
+ * @param {HTMLElement} element
131
+ * @returns {Object|null}
132
+ * @private
133
+ */
134
+ _parseAttributes(element) {
135
+ // Check for 14-bit CC (MSB + LSB)
136
+ const msb = parseInt(element.dataset.midiMsb, 10)
137
+ const lsb = parseInt(element.dataset.midiLsb, 10)
138
+
139
+ if (
140
+ !Number.isNaN(msb) &&
141
+ !Number.isNaN(lsb) &&
142
+ msb >= 0 &&
143
+ msb <= 127 &&
144
+ lsb >= 0 &&
145
+ lsb <= 127
146
+ ) {
147
+ // Check if 7-bit CC is also present
148
+ const cc = parseInt(element.dataset.midiCc, 10)
149
+ if (!Number.isNaN(cc) && cc >= 0 && cc <= 127) {
150
+ console.warn(
151
+ `Element has both 7-bit (data-midi-cc="${cc}") and 14-bit (data-midi-msb="${msb}" data-midi-lsb="${lsb}") CC attributes. 14-bit takes precedence.`,
152
+ element,
153
+ )
154
+ }
155
+
156
+ // Valid 14-bit CC
157
+ return {
158
+ msb,
159
+ lsb,
160
+ is14Bit: true,
161
+ channel: parseInt(element.dataset.midiChannel, 10) || undefined,
162
+ min: parseFloat(element.getAttribute("min")) || 0,
163
+ max: parseFloat(element.getAttribute("max")) || 127,
164
+ invert: element.dataset.midiInvert === "true",
165
+ label: element.dataset.midiLabel,
166
+ }
167
+ }
168
+
169
+ // Fallback to 7-bit CC
170
+ const cc = parseInt(element.dataset.midiCc, 10)
171
+ if (!Number.isNaN(cc) && cc >= 0 && cc <= 127) {
172
+ return {
173
+ cc,
174
+ channel: parseInt(element.dataset.midiChannel, 10) || undefined,
175
+ min: parseFloat(element.getAttribute("min")) || 0,
176
+ max: parseFloat(element.getAttribute("max")) || 127,
177
+ invert: element.dataset.midiInvert === "true",
178
+ label: element.dataset.midiLabel,
179
+ }
180
+ }
181
+
182
+ // Invalid configuration
183
+ if (
184
+ element.dataset.midiCc !== undefined ||
185
+ (element.dataset.midiMsb !== undefined && element.dataset.midiLsb !== undefined)
186
+ ) {
187
+ console.warn(`Invalid MIDI configuration on element:`, element)
188
+ }
189
+ return null
190
+ }
191
+
192
+ /**
193
+ * Clean up
194
+ */
195
+ destroy() {
196
+ this.disableAutoBinding()
197
+ }
198
+ }