HTML editor, controls and native messages
examples/tutorials/editor/index.html · 84 source lines
Read the chapters in source order. Select a numbered source line to expand its explanation; the line numbers and code match this snapshot exactly.
cargo run -p ravet-examples --bin midi-synth --locked -- --ui-test target/examples-ui-guideChapters
- Document and semantic theme lines 1–7
- Fit the controls and monitor panels lines 8–11
- Scale a single fixed design canvas lines 12–15
- Header, tabs and instrument controls lines 16–21
- Delay and member-expression panels lines 22–24
- Packet inspector and live monitors lines 25–29
- Install the local bridge and scale handler lines 30–37
- Build a reusable accessible knob lines 38–46
- Wire synth, delay and expression controls lines 47–59
- Track note ownership across UI interactions lines 60–63
- Release notes before mode, settings and stop actions lines 64–70
- Refresh UI from coherent native snapshots lines 71–84
Document and semantic theme
This local page is embedded in WebKit; it is not fetched from a remote website. Semantic CSS palette tokens control the instrument appearance. Rust injects shared web controls at the RAVET_CONTROLS placeholder before embedding.
1<!doctype html>
Select HTML5 parsing for the embedded document.
Link to line 12<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
Declare English content, UTF-8 text and a viewport that uses the native content width.
Link to line 23<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'">
Disable all resource loading by default; permit the local inline styles and scripts needed by this embedded page. This policy does not validate native messages.
Link to line 34<title>RAVET · Live examples</title>
Set the embedded document title to RAVET’s live examples.
Link to line 45<style>
Begin the local stylesheet; the following rules are embedded with the page.
Link to line 56:root{color-scheme:dark;font:13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ravet-foreground);background:var(--ravet-background)}*{box-sizing:border-box}body{margin:0;padding:20px 25px}button,input,select{font:inherit}button,select,input[type=number]{color:inherit;background:var(--ravet-raised);border:1px solid var(--ravet-border);border-radius:6px;padding:7px}button{cursor:pointer}button:hover{background:#423447}button:disabled{opacity:.4;cursor:default}button:focus-visible,input:focus-visible,select:focus-visible,[role=slider]:focus-visible{outline:2px solid var(--ravet-accent);outline-offset:3px}input[type=checkbox]{accent-color:var(--ravet-accent)}header{display:flex;justify-content:space-between;align-items:center}h1{font-size:26px;letter-spacing:-.6px;margin:4px 0}h2{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--ravet-accent);margin:0 0 12px}.brand{font-size:10px;letter-spacing:2.5px;color:var(--ravet-accent)}.subtitle,small,.hint{color:var(--ravet-muted);font-size:11px}.tabs{display:flex;gap:6px;margin:16px 0}.tabs button.active{background:var(--ravet-accent);border-color:var(--ravet-accent);color:var(--ravet-accent-text)}.panel{border:1px solid var(--ravet-border);background:var(--ravet-panel);border-radius:10px;padding:15px;margin-bottom:12px}.devices{display:grid;grid-template-columns:1fr;gap:16px}.field{display:grid;gap:7px;font-size:12px;color:var(--ravet-muted)}.field select{width:100%;min-width:0}.transport{display:flex;align-items:center;gap:8px;margin-top:20px}.primary{background:var(--ravet-accent);color:var(--ravet-accent-text);font-weight:650;border-color:var(--ravet-accent)}.primary:hover{background:#ffd294}.grid{display:grid;grid-template-columns:1fr 1.4fr;gap:12px}.oscillator{display:flex;align-items:center;gap:24px}.oscillator>.field{flex:1}.knob-row{display:flex;justify-content:space-around;gap:16px}.control{display:flex;flex-direction:column;align-items:center;gap:7px;min-width:90px}.control label{color:var(--ravet-foreground);font-size:11px}.control input[type=number]{width:82px;padding:4px;text-align:center;font-variant-numeric:tabular-nums}.dial{position:relative;width:64px;height:64px;border-radius:50%;background:conic-gradient(from 225deg,var(--ravet-accent) 0deg var(--sweep,0deg),var(--ravet-border) var(--sweep,0deg) 270deg,transparent 270deg);touch-action:none;user-select:none;cursor:ns-resize}.dial:before{content:"";position:absolute;inset:5px;border-radius:50%;background:linear-gradient(145deg,#52405a,#292031);border:1px solid #6b5675;box-shadow:0 2px 5px #0008}.indicator{position:absolute;inset:11px;transform:rotate(var(--angle,-135deg));pointer-events:none}.indicator:before{content:"";display:block;width:3px;height:13px;margin:0 auto;background:var(--ravet-foreground);border-radius:2px}.row{display:flex;align-items:center;justify-content:space-between;gap:12px}.adsr{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.keyboard{display:flex;gap:4px;margin-top:12px;touch-action:none;user-select:none}.key{flex:1;height:65px;min-width:0;border:0;background:#dbe8ea;color:#1c313b;padding:35px 2px 7px;font-size:11px;border-radius:4px;touch-action:none}.key.black{background:#263c47;color:#c6dde5;border:1px solid #47606c}.key.held{background:var(--ravet-accent);color:var(--ravet-accent-text);transform:translateY(2px)}.meters{display:grid;grid-template-columns:1fr 1fr;gap:18px}meter{width:100%;height:12px}.reading{font-size:10px;color:var(--ravet-muted);margin:4px 0 0;font-variant-numeric:tabular-nums}.voice-table{width:100%;border-collapse:collapse;font-size:11px;font-variant-numeric:tabular-nums}.voice-table th{color:var(--ravet-muted);font-weight:500;text-align:left;padding:5px}.voice-table td{padding:5px;border-top:1px solid var(--ravet-border)}.voice-empty{color:var(--ravet-muted);font-size:11px}.expression{display:grid;grid-template-columns:100px 1fr 1fr 1fr;gap:15px;align-items:center}.ump-fields{display:grid;grid-template-columns:repeat(3,1fr);gap:15px}.ump-fields input{width:100%}pre{font:12px ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;color:var(--ravet-accent);line-height:1.7;max-height:250px;overflow:auto}footer{display:flex;justify-content:space-between;gap:15px;color:var(--ravet-muted);font-size:11px;margin:12px 0 0}#status{color:var(--ravet-foreground);max-width:70%}.hidden{display:none!important}.hint{margin:9px 0 0;line-height:1.4}.connection{width:7px;height:7px;border-radius:50%;background:var(--ravet-border);display:inline-block;margin-right:6px}.connection.live{background:var(--ravet-accent)}dialog{color:inherit;background:var(--ravet-panel);border:1px solid #4d6873;border-radius:12px;padding:24px;width:470px;max-width:90vw;box-shadow:0 20px 90px #000a}dialog::backdrop{background:#050b10aa}dialog h2{font-size:14px;letter-spacing:.5px;text-transform:none;margin:0}dialog .row{margin-bottom:22px}#close-settings{font-size:18px;line-height:1;padding:3px 7px}.settings-note{margin:16px 0;color:var(--ravet-muted);line-height:1.5;font-size:12px}.top-actions{display:flex;gap:7px}.top-actions button{font-size:11px;padding:6px 9px}#voices-panel{padding:10px 15px}#voices-panel h2{margin-bottom:7px}#meters-panel{padding:10px 15px}#meters-panel h2{margin-bottom:7px}
Set the dark theme and semantic foreground/background tokens. This compact line also defines shared buttons, panels, tabs, knobs, keyboards, meters, tables and dialog styling. Knob motion uses CSS variables for sweep and indicator angle; touch-action:none prevents native scrolling from taking the drag.
Link to line 67
Blank line separating document or script sections.
Link to line 7Fit the controls and monitor panels
The compact layout keeps oscillator/filter/envelope controls, keyboard, meters and voice records visible at the default content size. The voice monitor scrolls inside its bounded panel. Effect mode uses larger delay knobs.
8/* Keep the complete instrument visible at the default and minimum preview size. */
Explain why the following overrides make the full instrument fit the default/minimum content size.
Link to line 89body{padding:14px 20px}h1{font-size:23px;margin:3px 0}.brand{font-size:9px}.tabs{margin:10px 0;gap:5px}.tabs button{padding:5px 9px}.panel{padding:11px;margin-bottom:9px}h2{margin-bottom:8px;font-size:10px}.grid{gap:9px}.control{gap:4px;min-width:72px}.control label{font-size:10px}.control input[type=number]{width:70px;padding:3px;font-size:11px}.dial{width:46px;height:46px}.dial:before{inset:4px}.indicator{inset:9px}.indicator:before{height:10px;width:2px}.adsr{gap:10px}.hint{font-size:10px;margin-top:6px}.oscillator{gap:14px}.knob-row{gap:9px}.keyboard{display:block;margin-top:9px;height:92px}.monitor-grid{display:grid;grid-template-columns:1fr 1.15fr;gap:9px}.monitor-grid>.panel{margin-bottom:0;height:78px}#voices{max-height:45px;overflow:auto}.voice-table th,.voice-table td{padding:3px}.voice-table{font-size:9px}#voices-panel,#meters-panel{padding:9px 11px}#voices-panel h2,#meters-panel h2{margin-bottom:6px}footer{margin-top:9px;font-size:10px}.expression{grid-template-columns:90px 1fr 1fr 1fr}body[data-mode=mpe] .hint{display:none}body[data-mode=mpe] .brand{display:none}body[data-mode=mpe] .keyboard{height:75px}
Reduce text, knob and panel dimensions; bound the voice monitor’s internal scroll area. MPE mode hides secondary hints and reduces keyboard height to keep its expression controls visible.
Link to line 910body[data-mode=effect] .monitor-grid{grid-template-columns:1fr}body[data-mode=effect] .dial{width:80px;height:80px}body[data-mode=effect] .indicator{inset:14px}body[data-mode=effect] .indicator:before{height:16px;width:3px}body[data-mode=effect] #effect-panel{padding:18px}
Use one meter panel column in effect mode and enlarge the delay knobs to 80 pixels. These are mode-specific overrides, not changes to control values.
Link to line 1011#meters-panel .meters{font-size:11px;line-height:1.1}#meters-panel meter{display:block;height:8px;margin-top:2px}#meters-panel .reading{font-size:9px;line-height:1.1;margin-top:2px}#voices{max-height:40px}
Compress meter labels/readings and cap the voice monitor height at 40 pixels.
Link to line 11Scale a single fixed design canvas
The native window locks its aspect ratio. A 900×780 element holds the complete design and is transformed uniformly to the actual viewport; body overflow is hidden. layoutExample performs the corresponding transform and centering.
12/* Scale one design canvas uniformly; the native window locks its aspect ratio. */
Document the uniform 900×780 canvas scaling used with the native aspect-ratio lock.
Link to line 1213html,body{width:100%;height:100%;overflow:hidden}body{padding:0}
Fill the viewport, hide outer scrolling and remove the body padding so the design canvas controls all positioning.
Link to line 1314#example-frame{position:fixed;left:0;top:0;width:900px;height:780px;padding:14px 20px;transform-origin:top left}
Fix the unscaled design at 900×780 pixels with top-left transform origin; JavaScript sets its scale and centered position.
Link to line 1415</style></head><body><main id="example-frame">
Close the styles/head, start the body and open the design-canvas main element.
Link to line 15Header, tabs and instrument controls
All notes off and Start/Stop are explicit actions. Four tabs select Synth/Effect/MPE/MIDI 2.0. Panel placeholders receive reusable knobs, the waveform Choice and PianoKeyboard rather than separate handwritten control implementations.
16<header><div><span class="brand">RAVET / INTERACTIVE EXAMPLES</span><h1 id="title">Subtractive synth</h1><span id="subtitle" class="subtitle">Oscillator → low-pass VCF → VCA · 8 voices</span></div><div class="top-actions"><button id="panic">All notes off</button><button id="main-stop" disabled>Stop audio</button></div></header>
Build title/subtitle elements and explicit All notes off and Start/Stop buttons. Stable IDs let snapshot updates and event handlers address them.
Link to line 1617<nav class="tabs" aria-label="Examples"><button data-mode="synth">Synth</button><button data-mode="effect">Effect</button><button data-mode="mpe">MPE</button><button data-mode="midi2">MIDI 2.0</button></nav>
Create four mode buttons with data-mode values used by the native mode action.
Link to line 1718
Blank line separating document or script sections.
Link to line 1819<section id="synth-panel"><div class="grid"><div class="panel"><h2>Oscillator / VCA</h2><div class="oscillator"><label class="field">Waveform<select id="wave"><option value="0">Sine</option><option value="1" selected>Saw · polyBLEP</option><option value="2">Square · polyBLEP</option><option value="3">Triangle</option></select></label><div id="vca-controls"></div></div></div><div class="panel"><h2>Low-pass VCF</h2><div id="vcf-controls" class="knob-row"></div></div></div>
Create the waveform selector with indices 0–3 and mount points for VCA and VCF knobs. The selected saw index matches the default Rust patch.
Link to line 1920<div class="panel"><h2>Shared ADSR · VCA + VCF</h2><div class="adsr" id="adsr-controls"></div><p class="hint">One envelope per voice drives amplitude and cutoff. ADSR edits apply to the next note.</p></div>
Provide the ADSR knob container and explain that one envelope controls amplitude/cutoff and edits latch at the next note.
Link to line 2021<div class="panel"><div class="row"><h2 style="margin:0">Keyboard · C3—C5</h2><span class="hint" style="margin:0">A W S E D F T G Y H U J K</span></div><div id="keyboard" class="keyboard" aria-label="Piano keyboard"></div><p class="hint">Hold a key or press a piano button. Release or leave the window to end notes.</p></div></section>
Provide the C3–C5 shared piano container and computer-key hint; the reusable PianoKeyboard will create the actual keys.
Link to line 21Delay and member-expression panels
Demo loop, ping-pong and bypass are independent controls. MPE member selection is limited to channels 1 and 2 for this on-screen keyboard; pressure/timbre widgets emit channel messages. This two-note UI policy is separate from the eight-voice synth capacity.
22<section id="effect-panel" class="panel hidden"><div class="row"><h2 style="margin:0">Stereo delay</h2><label><input type="checkbox" id="demo-loop" checked> Demo loop</label><label><input type="checkbox" id="ping-pong"> Ping-pong</label><label><input type="checkbox" id="bypass"> Bypass</label></div><div id="delay-controls" class="knob-row" style="margin-top:15px"></div><p class="hint">Time glides over 20 ms; feedback and mix smooth over 5 ms. Bypass plays dry while echoes continue internally. Demo loop plays repeating tone bursts. Turn it off to hear the input device selected in Audio/MIDI settings.</p></section>
Build the delay panel with Demo, Ping-pong and Bypass toggles plus its knob container. It starts hidden until the native snapshot selects effect mode.
Link to line 2223<section id="expression-panel" class="panel hidden"><h2>MPE · two lower-zone members</h2><div class="expression"><label class="field">Member<select id="member"><option value="1">1</option><option value="2">2</option></select></label><div id="bend-controls"></div><div id="pressure-controls"></div><div id="timbre-controls"></div></div><p class="hint">On-screen notes use members 1 and 2. Pressure raises VCA level; timbre opens the filter. A MIDI MPE controller can feed the same instrument.</p></section>
Build the MPE panel with member channels 1/2 and mount points for bend, pressure and timbre. Native MIDI can use the receiver’s broader policy independently of this on-screen two-member choice.
Link to line 2324
Blank line separating document or script sections.
Link to line 24Packet inspector and live monitors
Numeric HTML constraints describe group/channel/key and full-resolution fields but Rust validates again. The inspector displays packet words rather than routing them to audio. Voice and peak/RMS panels show returned snapshots; the status footer reports bridge/stream state.
25<section id="ump-panel" class="panel hidden"><h2>MIDI 2.0 · UMP packet inspector</h2><div class="ump-fields"><label class="field">Group · 0–15<input type="number" id="group" min="0" max="15" step="1" value="1"></label><label class="field">Channel · 0–15<input type="number" id="channel" min="0" max="15" step="1" value="2"></label><label class="field">Key · 0–127<input type="number" id="ump-key" min="0" max="127" step="1" value="60"></label><label class="field">Message<select id="kind"><option value="on">Note On</option><option value="off">Note Off</option><option value="bend">Per-note pitch bend</option><option value="pressure">Poly pressure</option></select></label><label class="field">Velocity · 16-bit<input type="number" id="velocity" min="0" max="65535" step="1" value="43981"></label><label class="field">Expression · 32-bit<input type="number" id="expression" min="0" max="4294967295" step="1" value="2435007847"></label></div><button id="encode" class="primary" style="margin-top:18px">Encode / decode packet</button><p class="hint">Full-resolution fields remain intact. This inspector shows codecs; it does not send MIDI 2.0 to the synth or a device.</p><pre id="packets">Choose fields and encode a packet.</pre></section>
Build the packet inspector: group/channel 0–15, key 0–127, velocity 0–65535 and expression 0–4294967295. Rust revalidates these fields; the button only requests codec inspection.
Link to line 2526<div class="monitor-grid"><section id="voices-panel" class="panel"><h2>Voices / expression</h2><div id="voices" class="voice-empty">No active notes</div></section><section id="meters-panel" class="panel"><h2>Live audio</h2><div class="meters"><div><label id="meter-label-0" for="meter-0">Output L</label><meter id="meter-0" min="0" max="1" value="0"></meter><div class="reading" id="reading-0">Peak −∞ · RMS −∞ dB</div></div><div><label id="meter-label-1" for="meter-1">Output R</label><meter id="meter-1" min="0" max="1" value="0"></meter><div class="reading" id="reading-1">Peak −∞ · RMS −∞ dB</div></div></div></section>
Build voice-state and stereo-output meter panels. Each meter has a label and a separate peak/RMS text reading for the accessible control wrapper.
Link to line 2627</div>
Close the monitor-grid wrapper opened by the preceding line.
Link to line 2728<footer><span id="status" role="status">Connecting…</span><span><i id="connection" class="connection"></i><span id="frames">Audio stopped</span></span></footer>
Create the live status region, connection indicator and rendered-frame display.
Link to line 2829</main>
Close the fixed design canvas before injected controls and scripts.
Link to line 29Install the local bridge and scale handler
The placeholder is replaced by shared-control code on the Rust side. ravetPost attempts the WebKit message handler and returns failure with status text if unavailable. action wraps explicit native requests; resize recomputes uniform scaling.
30<!-- RAVET_CONTROLS -->
Mark where embed_web_controls injects the shared controls/settings code before the Rust owner embeds this HTML.
Link to line 3031<script>
Begin the local example behavior script.
Link to line 3132const $=id=>document.getElementById(id);let mode='synth',running=false;
Define an ID lookup helper and initialize UI mode/running state. Actual native snapshots replace these defaults.
Link to line 3233function layoutExample(){const scale=Math.min(window.innerWidth/900,window.innerHeight/780),frame=$('example-frame');frame.style.transform=`scale(${scale})`;frame.style.left=`${(window.innerWidth-900*scale)/2}px`;frame.style.top=`${(window.innerHeight-780*scale)/2}px`;window.ravetExampleScale=scale;}
Compute the smaller width/900 or height/780 ratio, apply one uniform scale, center the resulting canvas and expose the scale for native layout tests.
Link to line 3334window.addEventListener('resize',layoutExample);layoutExample();
Recompute layout on every viewport resize and apply the initial layout immediately.
Link to line 3435window.ravetPost=message=>{try{window.webkit.messageHandlers.ravet.postMessage(message);return true}catch{$('status').textContent='Native bridge unavailable';return false}};
Send an object through the WebKit ravet message handler. Return true on bridge delivery; if the bridge is absent, display an error and return false. Delivery alone does not establish successful native application.
Link to line 3536const post=message=>window.ravetPost(message), action=(action,extra={})=>post({type:'action',action,...extra});
Wrap the replaceable ravetPost function and provide an action helper that adds type:action plus optional action fields.
Link to line 3637const synthControls={};synthControls[1]=new RavetControls.Choice($('wave'),1,post);
Create the control registry and bind waveform Choice to parameter ID 1 through the common message sender.
Link to line 37Build a reusable accessible knob
Create a label, focusable dial, indicator and numeric input with stable IDs/accessible names. Instantiate RavetControls.Knob with its physical bounds, step, default and optional logarithmic travel. The supplied send function determines its native message destination.
38function knob(parent,id,name,minimum,maximum,step,value,send=post,logarithmic=false,prefix='p'){
Define a knob factory accepting its container, stable ID, physical bounds, step/default, sender, travel mode and DOM-ID prefix.
Link to line 3839 const div=document.createElement('div');div.className='control';
Create the control wrapper used for the label, dial and numeric input.
Link to line 3940 const label=document.createElement('label');label.htmlFor=`${prefix}-${id}`;label.textContent=name;
Create its visible label, associate it with the dial ID and set the human-readable control name.
Link to line 4041 const dial=document.createElement('div');dial.className='dial';dial.id=`${prefix}-${id}`;dial.setAttribute('aria-label',name);
Create the dial element, assign its prefixed stable ID and give it the accessible name.
Link to line 4142 const indicator=document.createElement('span');indicator.className='indicator';dial.append(indicator);
Add the decorative indicator; shared Knob updates CSS angle/sweep rather than moving another control.
Link to line 4243 const number=document.createElement('input');number.type='number';number.id=`${prefix}-number-${id}`;number.setAttribute('aria-label',name+' value');
Create the numeric input with a distinct prefixed ID and an accessible name ending in value.
Link to line 4344 div.append(label,dial,number);$(parent).append(div);
Insert the label, dial and input into the wrapper, then mount that wrapper in the requested container.
Link to line 4445 return new RavetControls.Knob(dial,number,{id,minimum,maximum,step,value,logarithmic},send);
Construct the reusable Knob with physical configuration and the supplied message sender. Return it for later snapshot updates.
Link to line 4546}
Close the knob factory.
Link to line 46Wire synth, delay and expression controls
Synth control IDs match the nine-value patch. Delay IDs 1–5 match its physical patch; ID6 switches the demo source. MPE normalized bend becomes a 14-bit short message around center 8192; pressure uses channel pressure and timbre uses CC74. Meters use AccessibleMeter.
47synthControls[2]=knob('vca-controls',2,'VCA level',0,1,.01,.6);
Create ID 2 level: linear 0–1, step 0.01, default 0.6.
Link to line 4748synthControls[3]=knob('vcf-controls',3,'Cutoff · Hz',20,20000,1,800,post,true);
Create ID 3 cutoff: 20–20000 Hz, integer step, default 800 Hz and logarithmic dial travel.
Link to line 4849synthControls[4]=knob('vcf-controls',4,'Resonance · Q',.5,8,.01,.7);
Create ID 4 resonance Q: 0.5–8, step 0.01, default 0.7.
Link to line 4950synthControls[5]=knob('vcf-controls',5,'Env · semitones',0,72,1,36);
Create ID 5 positive envelope cutoff excursion: 0–72 semitones, default 36.
Link to line 5051for(const [id,name,min,max,step,value] of [[6,'Attack · ms',0,5000,1,10],[7,'Decay · ms',0,5000,1,150],[8,'Sustain',0,1,.01,.7],[9,'Release · ms',0,5000,1,300]])synthControls[id]=knob('adsr-controls',id,name,min,max,step,value);
Create ADSR IDs 6–9 from four records: attack/decay/release in milliseconds and sustain as a linear level. Their defaults mirror the physical Rust patch.
Link to line 5152const delayControls=[knob('delay-controls',1,'Time · ms',1,2000,1,375,post,true,'delay'),knob('delay-controls',2,'Feedback',0,.95,.01,.45,post,false,'delay'),knob('delay-controls',3,'Wet / dry',0,1,.01,.35,post,false,'delay')],bypass=new RavetControls.Toggle($('bypass'),4,post),pingPong=new RavetControls.Toggle($('ping-pong'),5,post),demoLoop=new RavetControls.Toggle($('demo-loop'),6,post);
Create the three continuous delay knobs with delay-prefixed DOM IDs. Bind bypass to ID 4, ping-pong to ID 5 and the example’s demo switch to ID 6.
Link to line 5253for(const [id,kind,name,min,max,step,value] of [[1,'bend','Bend · ±48 st',-1,1,.001,0],[2,'pressure','Pressure · VCA',0,127,1,0],[3,'timbre','Timbre · VCF',0,127,1,0]])knob(kind+'-controls',id,name,min,max,step,value,message=>{
Create member expression knobs. Bend uses normalized −1..1; pressure/timbre use integer 0..127. The custom sender converts these edits into short MIDI instead of processor parameter events.
Link to line 5354 if(!['edit','value'].includes(message.type))return true;
Acknowledge begin/end gesture messages without emitting MIDI; only actual value changes generate expression messages.
Link to line 5455 const channel=Number($('member').value),value=message.value;
Read the selected zero-based MPE member and the edited physical value.
Link to line 5556 if(kind==='bend'){const bend=Math.round(8192+value*(value<0?8192:8191));return midi([0xe0|channel,bend&127,bend>>7]);}
Map negative normalized bend over 8192 steps and positive bend over 8191 steps around center 8192. Encode the resulting 14-bit value as MIDI’s low/high seven-bit bytes on the selected channel.
Link to line 5657 return kind==='pressure'?midi([0xd0|channel,value]):midi([0xb0|channel,74,value]);
Send channel pressure with status 0xd0 or CC74 timbre with status 0xb0, combining the zero-based member channel into the status byte.
Link to line 5758},false,kind);
Close the knob factory.
Link to line 5859const meters=[0,1].map(i=>new RavetControls.AccessibleMeter($(`meter-${i}`),$(`reading-${i}`),{label:`Output ${i?'right':'left'}`}));
Wrap both output meter/readout pairs in AccessibleMeter with explicit left/right labels.
Link to line 59Track note ownership across UI interactions
held maps an interaction source to a key; notes tracks channel and reference count per key. Ignore note starts while stopped, settings are open or the same source already holds a note. Allocate two lower-zone members in MPE mode. Multiple sources holding the same key share one note until the last release.
60const held=new Map(),notes=new Map();let piano;
Create source-to-key and key-to-channel/reference-count maps; declare the shared piano object used during cancellation.
Link to line 6061function midi(bytes){return post({type:'midi',bytes})}
Wrap validated-by-Rust short-message bytes in a type:midi page request.
Link to line 6162function noteDown(key,source){if(!running||document.getElementById('io-settings')?.open||held.has(source))return false;let entry=notes.get(key);if(!entry){if(mode==='mpe'&¬es.size>=2)return false;const used=[...notes.values()].map(n=>n.channel);const channel=mode==='mpe'?(!used.includes(1)?1:2):0;entry={channel,count:0};notes.set(key,entry);midi([0x90|channel,key,100]);}entry.count++;held.set(source,key);return true;}
Refuse starts while stopped, settings are open or this source already owns a key. For a new key choose channel zero or a free MPE member, refuse a third distinct MPE key, send velocity-100 note-on, increment its shared reference count and remember the interaction source.
Link to line 6263function noteUp(source){const key=held.get(source);if(key===undefined)return;held.delete(source);const entry=notes.get(key);if(--entry.count===0){midi([0x80|entry.channel,key,64]);notes.delete(key);}}
Look up the interaction’s key, remove source ownership and decrement its reference count. Send velocity-64 note-off and remove the key only when its last source releases.
Link to line 63Release notes before mode, settings and stop actions
PianoKeyboard owns pointer/keyboard gestures. releaseAll clears both its gestures and the application maps. Tabs, settings-opening callback, stop and panic release notes before native actions. Packet encoding sends validated-by-Rust numeric fields as a separate message type.
64function releaseAll(){piano?.releaseAll();for(const source of [...held.keys()])noteUp(source);}
Cancel shared piano interactions, then release any remaining source entries from a copied key list so mutation cannot disrupt iteration.
Link to line 6465piano=new RavetControls.PianoKeyboard($('keyboard'),{firstNote:48,lastNote:72,onNoteOn:noteDown,onNoteOff:noteUp});
Create the shared keyboard for MIDI keys 48–72 inclusive and connect its on/off ownership callbacks.
Link to line 6566for(const b of document.querySelectorAll('[data-mode]'))b.onclick=()=>{releaseAll();action('mode',{mode:b.dataset.mode})};
Bind each mode tab to release current UI notes before requesting the corresponding native mode.
Link to line 6667window.ravetSettingsWillOpen=releaseAll;
Register note cancellation before the injected I/O settings popup opens.
Link to line 6768function stopAudio(){releaseAll();action('stop')}
Release on-screen notes first, then request native audio stop.
Link to line 6869$('main-stop').onclick=()=>{if(running)stopAudio();else window.ravetStartAudio()};$('panic').onclick=()=>{releaseAll();action('panic')};
Toggle between explicit stop and the injected settings/start action. All notes off releases UI ownership and sends the native panic action.
Link to line 6970$('encode').onclick=()=>post({type:'packet',group:Number($('group').value),channel:Number($('channel').value),key:Number($('ump-key').value),kind:$('kind').value,velocity:Number($('velocity').value),expression:Number($('expression').value)});
Read the inspector’s numeric/text fields into a type:packet request. Native parsing enforces integer bounds and constructs the typed UMP message.
Link to line 70Refresh UI from coherent native snapshots
ravetUpdate requires a values array, updates mode/running state and changes panel visibility. Reusable control update methods refresh displayed state without producing feedback edits. Enable the keyboard only for running synth/MPE, update output meters, reconstruct a text-only voice table and display the latest packet records.
71function text(id,value){if($(id).textContent!==value)$(id).textContent=value;}
Update textContent only when the display string changed, avoiding unnecessary DOM text replacements.
Link to line 7172window.ravetUpdate=state=>{
Expose the callback the native owner uses to refresh this page from its coherent presentation snapshot.
Link to line 7273 if(!state||!Array.isArray(state.values))return;const previous=mode;mode=state.mode;running=state.running;
Reject an absent snapshot or non-array values field, retain the previous mode for transition detection, and copy native mode/running state.
Link to line 7374 if(previous!==mode){releaseAll();}document.body.dataset.mode=mode;for(const b of document.querySelectorAll('[data-mode]'))b.classList.toggle('active',b.dataset.mode===mode);
Release UI-held notes on mode change, set the CSS mode attribute and update each tab’s active appearance.
Link to line 7475 const synth=mode==='synth'||mode==='mpe',effect=mode==='effect',ump=mode==='midi2';
Compute visibility categories: synth and MPE share instrument controls, effect uses delay, and MIDI 2.0 uses the inspector.
Link to line 7576 $('synth-panel').classList.toggle('hidden',!synth);$('effect-panel').classList.toggle('hidden',!effect);$('expression-panel').classList.toggle('hidden',mode!=='mpe');$('voices-panel').classList.toggle('hidden',!synth);$('ump-panel').classList.toggle('hidden',!ump);$('meters-panel').classList.toggle('hidden',ump);$('main-stop').classList.toggle('hidden',ump);$('panic').classList.toggle('hidden',!synth);
Set visibility for controls, voices, meters and actions from the active mode. The packet inspector hides audio actions because it does not synthesize.
Link to line 7677 text('title',{synth:'Subtractive synth',effect:'Stereo delay',mpe:'MPE synth',midi2:'MIDI 2.0 inspector'}[mode]);text('subtitle',ump?'Full-resolution fields → UMP words → decoded message':effect?'Live input or demo bursts → stereo / ping-pong delay → audio output':'Oscillator → low-pass VCF → VCA · 8 voices');
Choose mode-specific title and signal-flow subtitle using textContent.
Link to line 7778 for(let id=1;id<=9;id++)synthControls[id].update(state.values[id-1]);for(let i=0;i<3;i++)delayControls[i].update(state.delay[i]);bypass.update(state.delay[3]===1);pingPong.update(state.delay[4]===1);demoLoop.update(state.demo);
Refresh all physical controls from the snapshot using update methods, including delay toggles and demo state. These refreshes do not send edit feedback to Rust.
Link to line 7879 $('main-stop').disabled=false;text('main-stop',running?'Stop audio':'Start audio');piano.setEnabled(running&&(mode==='synth'||mode==='mpe'));piano.setActiveNotes(state.voices.filter(v=>v.phase!=='Released').map(v=>v.key));
Enable the main action, display its current Start/Stop label, enable piano interaction only for a running instrument, and highlight occupied non-released voice keys.
Link to line 7980 for(let i=0;i<2;i++)meters[i].update(state.meters[i+2]);text('status',state.status);text('frames',running?`${state.frames.toLocaleString()} frames`:'Audio stopped');$('connection').classList.toggle('live',running);
Update meters from output entries 2/3, refresh status/frame count and mark the connection indicator active while audio is running.
Link to line 8081 $('voices').replaceChildren();if(!state.voices.length)$('voices').textContent='No active notes';else{const table=document.createElement('table');table.className='voice-table';const head=table.createTHead().insertRow();for(const title of ['Channel','Key','State','Bend','Pressure','Timbre','ADSR']){const th=document.createElement('th');th.textContent=title;head.append(th)}for(const v of state.voices){const row=table.insertRow();for(const value of [v.channel,v.key,v.phase,v.bend.toFixed(1),v.pressure.toFixed(2),v.timbre.toFixed(2),v.envelope.toFixed(2)])row.insertCell().textContent=value;}$('voices').append(table);}
Rebuild the voice table from snapshot records with channel, key, phase, bend, pressure, timbre and ADSR level. Create text nodes with textContent, including the empty-state message.
Link to line 8182 if(state.packets.length)text('packets',state.packets.slice().reverse().join('\n\n'));window.ravetExampleReady=true;
Display packet history newest first without mutating the native snapshot array; set the ready flag consumed by hardware-free UI tests.
Link to line 8283};
Close the knob factory.
Link to line 8384</script></body></html>
Close the embedded behavior script, body and document.
Link to line 84