Skip to content

Commit c300c7d

Browse files
committed
Regenerate portal for CPython audioif
1 parent 148da3d commit c300c7d

4 files changed

Lines changed: 119 additions & 15 deletions

File tree

audioif/index.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<!-- PYDEVICES-HEAD-TAGS: START -->
77
<title>PyDevices - audioif</title>
8-
<meta name="description" content="Native MicroPython usermods that keep CircuitPython import names and play/stop/pause/resume semantics: audiocore, audiomixer, synthio (including MidiTrack), effects, and audiomp3.">
8+
<meta name="description" content="An audio synthesis stack with CircuitPython import names and play/stop/pause/resume semantics. MicroPython firmware includes audiomp3; pydevices-audioif 0.0.1 provides audiocore, synthio, the mixer, and effects for CPython, Android, and Pyodide.">
99
<link rel="icon" type="image/svg+xml" href="/assets/img/logo.svg">
1010
<!-- PYDEVICES-HEAD-TAGS: END -->
1111
<script>try{if(localStorage.getItem('pydevices-theme')==='light'){document.documentElement.setAttribute('data-theme','light');}}catch(e){}</script>
@@ -25,14 +25,15 @@
2525
<div class="logo-badge product-mark" style="background: linear-gradient(135deg, var(--tier-1-amber), #d97706); color: #fff;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10v4M8 7v10M12 4v16M16 7v10M20 10v4"/></svg></div>
2626
<span class="eyebrow" style="color: var(--tier-1-amber);">audioif</span>
2727
</div>
28-
<h1>audioif — CircuitPython audio usermods for MicroPython.</h1>
29-
<p><code>audioif</code> Native MicroPython usermods that keep CircuitPython import names and play/stop/pause/resume semantics: audiocore, audiomixer, synthio (including MidiTrack), effects, and audiomp3.</p>
28+
<h1>audioif — CircuitPython-compatible audio for MicroPython and CPython.</h1>
29+
<p><code>audioif</code> An audio synthesis stack with CircuitPython import names and play/stop/pause/resume semantics. MicroPython firmware includes audiomp3; pydevices-audioif 0.0.1 provides audiocore, synthio, the mixer, and effects for CPython, Android, and Pyodide.</p>
3030
<div class="cta">
3131
<a class="btn primary" style="background: #24292e; color: #fff; border-color: #24292e;" href="https://github.com/PyDevices/audioif">
3232
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.9a3.4 3.4 0 0 0-1-2.6c3-.3 6-1.5 6-6.5a5 5 0 0 0-1.4-3.5 4.6 4.6 0 0 0-.1-3.5s-1.1-.3-3.5 1.3a12 12 0 0 0-6 0C6.6 1.7 5.5 2 5.5 2a4.6 4.6 0 0 0-.1 3.5A5 5 0 0 0 4 9c0 5 3 6.2 6 6.5a3.4 3.4 0 0 0-1 2.6V22"/></svg>
3333
GitHub Repo
3434
</a>
3535
<a class="btn" style="border-color: var(--tier-1-amber); color: var(--tier-1-amber);" href="https://github.com/PyDevices/audioif/blob/main/docs/porting-plan.md">Architecture</a>
36+
<a class="btn" style="border-color: var(--tier-1-amber); color: var(--tier-1-amber);" href="https://test.pypi.org/project/pydevices-audioif/">TestPyPI</a>
3637
<a class="btn" style="border-color: var(--tier-1-amber); color: var(--tier-1-amber);" href="https://github.com/PyDevices/pydevices/blob/main/docs/audio.md">Audio contract</a>
3738
<a class="btn" style="border-color: var(--tier-1-amber); color: var(--tier-1-amber);" href="https://pydevices.github.io/displayif/">displayif</a>
3839
</div>

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ <h3>displayif</h3>
8787
<span class="tag tag-tier-1">C Audio Usermod</span>
8888
</div>
8989
<h3>audioif</h3>
90-
<p>Native MicroPython usermods that keep CircuitPython import names and play/stop/pause/resume semantics: audiocore, audiomixer, synthio (including MidiTrack), effects, and audiomp3.</p>
90+
<p>An audio synthesis stack with CircuitPython import names and play/stop/pause/resume semantics. MicroPython firmware includes audiomp3; pydevices-audioif 0.0.1 provides audiocore, synthio, the mixer, and effects for CPython, Android, and Pyodide.</p>
9191
<span class="go">Visit site <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
9292
</a>
9393

vendor/micropython/micropython.mjs

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4543,8 +4543,20 @@ var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => {
45434543
canvas.__pydevicesInputInstalled = true;
45444544
canvas.tabIndex = canvas.tabIndex >= 0 ? canvas.tabIndex : 0;
45454545
const modifiers = (e) => ({altKey:e.altKey, ctrlKey:e.ctrlKey, metaKey:e.metaKey, shiftKey:e.shiftKey});
4546+
// Named and tracked on the element (rather than anonymous closures) so a
4547+
// future register on this same canvas - a new WASM instance after a VM
4548+
// restart, most likely - can strip them via pydevices_release_canvas
4549+
// before installing its own. Without that, events from a second
4550+
// instance would keep calling into the first instance's closures - and
4551+
// with them, its whole WASM heap - forever.
4552+
const listeners = [];
4553+
const on = (name, handler, opts) => {
4554+
canvas.addEventListener(name, handler, opts);
4555+
listeners.push([name, handler, opts]);
4556+
};
4557+
canvas.__pydevicesListeners = listeners;
45464558
for (const name of ['pointerdown', 'pointermove', 'pointerup', 'pointercancel']) {
4547-
canvas.addEventListener(name, (e) => {
4559+
on(name, (e) => {
45484560
const p = pydevices_relative_point(canvas, e);
45494561
pydevices_push_event(canvasId, Object.assign({type:name, x:p.x, y:p.y, movementX:e.movementX||0, movementY:e.movementY||0, button:e.button, buttons:e.buttons, pressure:e.pressure, pointerId:e.pointerId, pointerType:e.pointerType, primary:e.isPrimary}, modifiers(e)));
45504562
if (name === 'pointerdown') {
@@ -4554,19 +4566,19 @@ var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => {
45544566
e.preventDefault();
45554567
});
45564568
}
4557-
canvas.addEventListener('wheel', (e) => {
4569+
on('wheel', (e) => {
45584570
const p = pydevices_relative_point(canvas, e);
45594571
pydevices_push_event(canvasId, Object.assign({type:'wheel', x:p.x, y:p.y, deltaX:e.deltaX, deltaY:e.deltaY, deltaZ:e.deltaZ, deltaMode:e.deltaMode}, modifiers(e)));
45604572
e.preventDefault();
45614573
}, {passive:false});
45624574
for (const name of ['keydown', 'keyup']) {
4563-
canvas.addEventListener(name, (e) => {
4575+
on(name, (e) => {
45644576
pydevices_push_event(canvasId, Object.assign({type:name, key:e.key, code:e.code, repeat:e.repeat, location:e.location}, modifiers(e)));
45654577
if (e.key === 'Backspace' || e.key === 'Escape' || e.key === 'BrowserBack') e.preventDefault();
45664578
});
45674579
}
4568-
canvas.addEventListener('focus', () => pydevices_push_event(canvasId, {type:'focus'}));
4569-
canvas.addEventListener('blur', () => pydevices_push_event(canvasId, {type:'blur'}));
4580+
on('focus', () => pydevices_push_event(canvasId, {type:'focus'}));
4581+
on('blur', () => pydevices_push_event(canvasId, {type:'blur'}));
45704582
canvas.focus();
45714583
};
45724584

@@ -4602,9 +4614,65 @@ var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => {
46024614
state.animation = requestAnimationFrame(pydevices_paint);
46034615
};
46044616

4617+
4618+
var pydevices_release_canvas = (canvas) => {
4619+
for (const [name, handler, opts] of canvas.__pydevicesListeners || []) {
4620+
canvas.removeEventListener(name, handler, opts);
4621+
}
4622+
canvas.__pydevicesListeners = null;
4623+
canvas.__pydevicesInputInstalled = false;
4624+
};
46054625
var pydevices_export_host = () => {
46064626
if (Module.pydevicesBridge) return;
46074627
Module.pydevicesBridge = {
4628+
/*
4629+
* Releases everything this instance holds in the host page: the
4630+
* paint loop, the canvas listeners, pending timers, and any audio
4631+
* still playing or recording. Meant to run right before an embedder
4632+
* discards this Module for a fresh one - a VM restart, most likely -
4633+
* so the old instance's WASM heap (kept alive otherwise by nothing
4634+
* but its own rAF callback and DOM listeners) actually becomes
4635+
* garbage instead of accumulating on every restart, and so canvas
4636+
* input reaches whichever instance registers on it next rather than
4637+
* silently continuing to reach this one.
4638+
*
4639+
* Idempotent - calling it twice, or on an instance that never
4640+
* registered a display, does nothing harmful.
4641+
*/
4642+
shutdown: () => {
4643+
const state = pydevices_state;
4644+
if (state.animation) {
4645+
cancelAnimationFrame(state.animation);
4646+
state.animation = 0;
4647+
}
4648+
for (const display of state.displays.values()) {
4649+
if (display.canvas) pydevices_release_canvas(display.canvas);
4650+
}
4651+
state.displays.clear();
4652+
for (const timer of state.timers.values()) {
4653+
timer.periodic ? clearInterval(timer.handle) : clearTimeout(timer.handle);
4654+
}
4655+
state.timers.clear();
4656+
state.firedTimers.length = 0;
4657+
for (const source of state.audioSources) {
4658+
try { source.stop(); } catch (_) { /* already finished */ }
4659+
}
4660+
state.audioSources.clear();
4661+
state.audioQueuedBytes = 0;
4662+
state.inputFrames.length = 0;
4663+
if (state.processor) { try { state.processor.disconnect(); } catch (_) {} }
4664+
if (state.mediaNode) { try { state.mediaNode.disconnect(); } catch (_) {} }
4665+
if (state.mediaStream) { for (const track of state.mediaStream.getTracks()) track.stop(); }
4666+
state.processor = null;
4667+
state.mediaNode = null;
4668+
state.mediaStream = null;
4669+
state.microphoneEnabled = false;
4670+
if (state.audioContext) {
4671+
try { state.audioContext.close(); } catch (_) {}
4672+
state.audioContext = null;
4673+
}
4674+
state.audioEnabled = false;
4675+
},
46084676
enableAudio: async (microphone=false) => {
46094677
const state = pydevices_state;
46104678
const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
@@ -4698,8 +4766,16 @@ var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => {
46984766
pydevices_get_display(UTF8ToString(canvasPtr)).eventRing = ptr ? {ptr,capacity,stride} : null;
46994767
};
47004768

4769+
47014770
var _pydevices_host_reset = () => {
47024771
const state = pydevices_state;
4772+
if (state.animation) {
4773+
cancelAnimationFrame(state.animation);
4774+
state.animation = 0;
4775+
}
4776+
for (const display of state.displays.values()) {
4777+
if (display.canvas) pydevices_release_canvas(display.canvas);
4778+
}
47034779
state.displays.clear();
47044780
state.firedTimers.length = 0;
47054781
for (const timer of state.timers.values()) {
@@ -5694,6 +5770,7 @@ missingLibrarySymbols.forEach(missingLibrarySymbol)
56945770
'pydevices_push_event',
56955771
'pydevices_relative_point',
56965772
'pydevices_install_input',
5773+
'pydevices_release_canvas',
56975774
'pydevices_paint',
56985775
'pydevices_export_host',
56995776
'pydevices_decode_pcm',
@@ -5784,17 +5861,17 @@ var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_rest
57845861
var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc');
57855862
var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current');
57865863
var dynCall_iii = makeInvalidEarlyAccess('dynCall_iii');
5864+
var dynCall_iiii = makeInvalidEarlyAccess('dynCall_iiii');
5865+
var dynCall_iiiiiii = makeInvalidEarlyAccess('dynCall_iiiiiii');
57875866
var dynCall_ii = makeInvalidEarlyAccess('dynCall_ii');
57885867
var dynCall_iiiii = makeInvalidEarlyAccess('dynCall_iiiii');
57895868
var dynCall_viii = makeInvalidEarlyAccess('dynCall_viii');
57905869
var dynCall_iiiiii = makeInvalidEarlyAccess('dynCall_iiiiii');
5791-
var dynCall_iiii = makeInvalidEarlyAccess('dynCall_iiii');
57925870
var dynCall_di = makeInvalidEarlyAccess('dynCall_di');
57935871
var dynCall_vi = makeInvalidEarlyAccess('dynCall_vi');
57945872
var dynCall_vii = makeInvalidEarlyAccess('dynCall_vii');
57955873
var dynCall_viiii = makeInvalidEarlyAccess('dynCall_viiii');
57965874
var dynCall_i = makeInvalidEarlyAccess('dynCall_i');
5797-
var dynCall_iiiiiii = makeInvalidEarlyAccess('dynCall_iiiiiii');
57985875
var dynCall_v = makeInvalidEarlyAccess('dynCall_v');
57995876
var dynCall_jji = makeInvalidEarlyAccess('dynCall_jji');
58005877
var dynCall_viiiii = makeInvalidEarlyAccess('dynCall_viiiii');
@@ -5862,17 +5939,17 @@ function assignWasmExports(wasmExports) {
58625939
assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc');
58635940
assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current');
58645941
assert(typeof wasmExports['dynCall_iii'] != 'undefined', 'missing Wasm export: dynCall_iii');
5942+
assert(typeof wasmExports['dynCall_iiii'] != 'undefined', 'missing Wasm export: dynCall_iiii');
5943+
assert(typeof wasmExports['dynCall_iiiiiii'] != 'undefined', 'missing Wasm export: dynCall_iiiiiii');
58655944
assert(typeof wasmExports['dynCall_ii'] != 'undefined', 'missing Wasm export: dynCall_ii');
58665945
assert(typeof wasmExports['dynCall_iiiii'] != 'undefined', 'missing Wasm export: dynCall_iiiii');
58675946
assert(typeof wasmExports['dynCall_viii'] != 'undefined', 'missing Wasm export: dynCall_viii');
58685947
assert(typeof wasmExports['dynCall_iiiiii'] != 'undefined', 'missing Wasm export: dynCall_iiiiii');
5869-
assert(typeof wasmExports['dynCall_iiii'] != 'undefined', 'missing Wasm export: dynCall_iiii');
58705948
assert(typeof wasmExports['dynCall_di'] != 'undefined', 'missing Wasm export: dynCall_di');
58715949
assert(typeof wasmExports['dynCall_vi'] != 'undefined', 'missing Wasm export: dynCall_vi');
58725950
assert(typeof wasmExports['dynCall_vii'] != 'undefined', 'missing Wasm export: dynCall_vii');
58735951
assert(typeof wasmExports['dynCall_viiii'] != 'undefined', 'missing Wasm export: dynCall_viiii');
58745952
assert(typeof wasmExports['dynCall_i'] != 'undefined', 'missing Wasm export: dynCall_i');
5875-
assert(typeof wasmExports['dynCall_iiiiiii'] != 'undefined', 'missing Wasm export: dynCall_iiiiiii');
58765953
assert(typeof wasmExports['dynCall_v'] != 'undefined', 'missing Wasm export: dynCall_v');
58775954
assert(typeof wasmExports['dynCall_jji'] != 'undefined', 'missing Wasm export: dynCall_jji');
58785955
assert(typeof wasmExports['dynCall_viiiii'] != 'undefined', 'missing Wasm export: dynCall_viiiii');
@@ -5936,17 +6013,17 @@ function assignWasmExports(wasmExports) {
59366013
__emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc'];
59376014
_emscripten_stack_get_current = wasmExports['emscripten_stack_get_current'];
59386015
dynCall_iii = dynCalls['iii'] = createExportWrapper('dynCall_iii', 3);
6016+
dynCall_iiii = dynCalls['iiii'] = createExportWrapper('dynCall_iiii', 4);
6017+
dynCall_iiiiiii = dynCalls['iiiiiii'] = createExportWrapper('dynCall_iiiiiii', 7);
59396018
dynCall_ii = dynCalls['ii'] = createExportWrapper('dynCall_ii', 2);
59406019
dynCall_iiiii = dynCalls['iiiii'] = createExportWrapper('dynCall_iiiii', 5);
59416020
dynCall_viii = dynCalls['viii'] = createExportWrapper('dynCall_viii', 4);
59426021
dynCall_iiiiii = dynCalls['iiiiii'] = createExportWrapper('dynCall_iiiiii', 6);
5943-
dynCall_iiii = dynCalls['iiii'] = createExportWrapper('dynCall_iiii', 4);
59446022
dynCall_di = dynCalls['di'] = createExportWrapper('dynCall_di', 2);
59456023
dynCall_vi = dynCalls['vi'] = createExportWrapper('dynCall_vi', 2);
59466024
dynCall_vii = dynCalls['vii'] = createExportWrapper('dynCall_vii', 3);
59476025
dynCall_viiii = dynCalls['viiii'] = createExportWrapper('dynCall_viiii', 5);
59486026
dynCall_i = dynCalls['i'] = createExportWrapper('dynCall_i', 1);
5949-
dynCall_iiiiiii = dynCalls['iiiiiii'] = createExportWrapper('dynCall_iiiiiii', 7);
59506027
dynCall_v = dynCalls['v'] = createExportWrapper('dynCall_v', 1);
59516028
dynCall_jji = dynCalls['jji'] = createExportWrapper('dynCall_jji', 3);
59526029
dynCall_viiiii = dynCalls['viiiii'] = createExportWrapper('dynCall_viiiii', 6);
@@ -6927,9 +7004,20 @@ const py_proxy_handler = {
69277004
class PyProxyThenable {
69287005
constructor(ref) {
69297006
this._ref = ref;
7007+
this._generation = globalThis.proxy_js_generation;
69307008
}
69317009

69327010
then(resolve, reject) {
7011+
if (this._generation !== globalThis.proxy_js_generation) {
7012+
// The interpreter this generator belonged to is gone; its ref
7013+
// would resolve to an arbitrary object in the current one. The
7014+
// promise machinery calls this spontaneously, so settle the
7015+
// chain instead of throwing into it.
7016+
reject(
7017+
new Error("PyProxyThenable outlived its interpreter"),
7018+
);
7019+
return undefined;
7020+
}
69337021
const values = Module._malloc(3 * 3 * 4);
69347022
proxy_convert_js_to_mp_obj_jsside(resolve, values + 3 * 4);
69357023
proxy_convert_js_to_mp_obj_jsside(reject, values + 2 * 3 * 4);
@@ -7005,6 +7093,15 @@ class PythonError extends Error {
70057093
}
70067094

70077095
function proxy_js_init() {
7096+
// Proxy state lives on globalThis and is shared by every interpreter
7097+
// instance loaded from this module. Each (re)init starts a new
7098+
// generation; wrappers handed to the browser (setTimeout callbacks,
7099+
// event listeners, promise handlers) record the generation they were
7100+
// created in, so ones that outlive their interpreter go inert instead
7101+
// of calling into the current interpreter with a recycled index --
7102+
// which resolves to an arbitrary, unrelated object.
7103+
globalThis.proxy_js_generation =
7104+
(globalThis.proxy_js_generation || 0) + 1;
70087105
globalThis.proxy_js_ref = [globalThis, undefined];
70097106
globalThis.proxy_js_ref_next = PROXY_JS_REF_NUM_STATIC;
70107107
globalThis.proxy_js_ref_map = new Map();
@@ -7228,7 +7325,13 @@ function proxy_convert_mp_to_js_obj_jsside(value) {
72287325
// obj
72297326
const id = Module.getValue(value + 4, "i32");
72307327
if (kind === PROXY_KIND_MP_CALLABLE) {
7328+
const generation = globalThis.proxy_js_generation;
72317329
obj = (...args) => {
7330+
if (generation !== globalThis.proxy_js_generation) {
7331+
// The interpreter this callable belonged to is gone.
7332+
// Behave like a cleared timer: do nothing.
7333+
return undefined;
7334+
}
72327335
return proxy_call_python(id, args);
72337336
};
72347337
obj._ref = id;
4.54 KB
Binary file not shown.

0 commit comments

Comments
 (0)