ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
Scan your choir music. Hear just your part. Free, on your phone, works offline.
Translation pending
EducationJavaScript · HTMLGitHub @pzfreoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Partsong: scan choral sheet music on your phone and play your own part (on-device OMR PWA)
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
A PWA for learning a choral part: photograph each page of a score, and the phone
reads the music, splits it into parts (Soprano/Alto/Tenor/Bass, plus e.g. a solo
line), and plays it back with your part loud and the others quiet.
Everything runs on the phone. There is no server.
Use it: https://partsong.app/ — open in Chrome on Android,
then Add to Home screen.Read the full README ↗A PWA for learning a choral part: photograph each page of a score, and the phone reads the music, splits it into parts (Soprano/Alto/Tenor/Bass, plus e.g. a solo line), and plays it back with your part loud and the others quiet.
Everything runs on the phone. There is no server.
Use it: https://partsong.app/ — open in Chrome on Android, then Add to Home screen.
omr/onnxruntime.py replaces the
onnxruntime package and forwards model calls to
onnxruntime-web (WebGPU for the segmentation and
encoder models when available, WASM for the decoder). Python blocks on those
calls via JSPI, so it needs Chrome/Edge 137+ (current Chrome on Android
is fine).omr-worker.js
run/keep); the service worker adds COOP/COEP (credentialless) headers,
which GitHub Pages can't, so the app is cross-origin isolated and the WASM
decoder runs on 4 threads; omr/runner.py sweeps homr's pairwise shape merge
by x-extent. The worker logs each page's time per model and in Python.
Measured and rejected: the decoder is already int8-quantized, and batching
staves changes results (its dynamic quantization scale spans the batch).score.js): each page is recognised separately. Bars are lined up
across a page's parts by position (homr occasionally splits a bar for one
part). Each staff is split into an upper and a lower voice using homr's own
per-note voice tags (upper/upper2, lower/lower2: stem-up and stem-down voices),
which omr/runner.py keeps in the MusicXML as comments because homr's writer
renumbers voices; scores read before that fall back to a pitch/onset guess.
Bars are grouped into systems.tests/drive-mark-every-staff.js).recognise(path, systems_json) in omr/runner.py). The bands are
kept with the score (systemHints) and used by later Read agains
(tests/drive-fix-systems.js).share.js, relay/): sharing uploads the score's PDF,
encrypted on the phone (AES-GCM), to a Cloudflare Worker + R2 relay, and
shares just the link https://partsong.app/#s=<id>.<key> (WhatsApp drops a
message's text when a file comes with it). Share part with score in a score's
settings is the same link share as the icon by its title. The key is only in the #fragment, which
browsers never send, so the relay can't read scores. Tapping the link fetches
and decrypts it and asks before adding, the same in Safari, Chrome or the
installed app (also when Partsong is already open: hashchange). If the
upload fails, the PDF is shared instead; if a link has expired (a year, by
the bucket's lifecycle rule) or the relay is down, the app says so and what
to do. tests/drive-relay.js (needs wrangler dev, see
its header). The relay only accepts uploads from partsong.app, up to 40 MB,
starting with the app's marker, and never overwrites.audio.js, Player.render): a score's settings can share
what Play plays (bars, repeats, your part or marked part, the others' volume,
sound, tempo, pitch) as an audio file, rendered offline in a few seconds:
AAC in an .m4a (WebCodecs AudioEncoder, muxed by mp4-muxer from jsdelivr,
~0.6 MB a minute) where the browser can encode AAC, else WAV.
tests/drive-share-audio.js.share.js): the share icon (by the score title, or on each
library row) makes one .partsong.pdf: a real PDF of the page photos (JPEG,
marked /PartsongPhoto n) that any phone opens, with the recognised MusicXML,
settings and marks attached as partsong-score.json (/PartsongScore,
deflated), and a last page saying how to open it in Partsong. It goes to the
share sheet (WhatsApp, Drive, email) or downloads; browsers only share a few
file types, and PDF is the one that's readable everywhere and sent unchanged.
Shares now normally go as a link (the PDF only when the relay can't take it);
Export PDF in a score's settings saves or sends the PDF itself, a copy that
doesn't expire (tests/drive-export-pdf.js).
Open PDF imports it (an ordinary PDF there is read as a new scan). Imports never
overwrite: if the score or its name is already in the library, it's added as
"Title (2)" etc. The installed app is also a share target (manifest
share_target → service worker inbox): shared score files are offered (and
wait in the inbox until answered, since Android can launch the app twice),
shared PDFs/photos start a new scan. Older .partsong.html files still open:
via Open PDF, sharing, or their page's button (which posts the full score
to the app); their link alone can't carry photos, so a score with photos isn't
added that way and the app says to share the file to Partsong instead.
Partsong is also registered to open PDFs (manifest file_handlers +
launchQueue: desktop Chrome/Edge, and Android where Chrome supports it), so
it appears under "Open with"; a score is offered, an ordinary PDF scanned
(tests/drive-open-with.js).
tests/drive-share.js, tests/drive-receive.js (with
tests/fixtures/legacy.partsong.html), tests/drive-share-target.js.tests/drive-search.js.repeatMarks, markRepeats, playOrder in score.js); the player plays a
list of score stretches in order (player.js), so a repeat plays twice and
follow-along jumps back. homr can miss a forward repeat at the start of a
line, so settings show each repeat's "goes back to bar" (saved per score),
plus a Play repeats switch. tests/drive-repeats.js (needs
testdata/tallis_0..2.musicxml, the sample as read).track() in app.js (sample,
read-ok/read-failed with page count, first play per opened score, share,
install, report). Never titles or music. Its script skips localhost.
tests/drive-analytics.js checks the events with GoatCounter stubbed.samples/tallis-if-ye-love-me.pdf,
Tallis's If ye love me (ed. K. Jaworski, CPDL licence), three pages: four
vocal staves plus a piano reduction.player.js): Web Audio; each note's volume and octave come from
the app at scheduling time; tempo, bar range, loop. Three synthesised sounds
(Sound: piano, organ, voice), remembered per device;
tests/drive-instrument.js renders each offline and returns them as WAV.<!-- imgpos --> comments), so playback highlights the current bar on your
own pages, with a playhead and a marker on your part's note, and auto-scrolls.
Tap a bar to start from there.pdf-pages.js): "Add images or PDF" renders each page with
pdf.js at ~300 dpi and treats it like a photo.orient.js): phones held flat over a page often save
it sideways. Sideways pages are detected from the staff lines and turned the
way a portrait photo would have been (using the stored pixels' EXIF tag);
each thumbnail also has a ↻ button.db.js): each score is saved on the device in IndexedDB with
its original photos, the recognised MusicXML and your settings (part, tempo,
mix), so it opens instantly without re-reading. MusicXML files can be opened
directly, too.scripts/fetch-homr.sh # pinned homr source -> vendor/, models -> models/
python3 -m http.server 8765 # then open http://localhost:8765
The service worker is skipped on localhost; use http://127.0.0.1:8765 to
exercise it. A phone needs HTTPS (camera, caches), so test there via the deployed
site.
Headless browser tests (put score images/MusicXML in the gitignored testdata/;
the orientation tests also need simulated camera photos cam_<page>_exif<tag>.jpg:
each page's pixels turned 90° anticlockwise, saved with EXIF orientation 1, 3 or 6):
node scripts/headless-test.mjs "http://localhost:8765/tests/omr-test.html?page=huron_1.png" out.json
node scripts/headless-test.mjs "http://localhost:8765/tests/score-test.html?set=huron&n=4" out.json 30
node scripts/headless-test.mjs "http://localhost:8765/tests/split-compare.html?set=huronv&n=4" out.json 30
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 600 tests/drive-app.js shot
node scripts/headless-test.mjs "http://127.0.0.1:8765/index.html" out.json 600 tests/drive-offline.js shot
node scripts/headless-test.mjs "http://localhost:8765/testdata/" out.json 400 tests/drive-library.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 100 tests/drive-follow.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 400 tests/drive-nav.js shot
node scripts/headless-test.mjs "http://localhost:8765/tests/orient-test.html" out.json 100
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 450 tests/drive-pdf.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 100 tests/drive-files.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 120 tests/drive-open-with.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 300 tests/drive-share-audio.js shot
(cd relay && npx wrangler dev --port 8787 --var "ORIGINS:http://localhost:8765" &)
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 300 tests/drive-relay.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 180 tests/drive-manual.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 100 tests/drive-lock.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 100 tests/drive-views.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 100 tests/drive-read-follow.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 180 tests/drive-share.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 100 tests/drive-library-share.js shot
CHROME_FLAGS=--disable-popup-blocking node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 180 tests/drive-receive.js shot
node scripts/headless-test.mjs "http://127.0.0.1:8765/testdata/" out.json 180 tests/drive-share-target.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 60 tests/drive-hidden.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 60 tests/drive-panel.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 60 tests/drive-copy.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 60 tests/drive-delete.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 60 tests/drive-report.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 40 tests/drive-about.js shot
# update banner + install card: stamped build on :8770, see the test header
SITE=<site-dir> node scripts/headless-test.mjs "http://localhost:8770/testdata/" out.json 180 tests/drive-update.js shot
node scripts/headless-test.mjs "http://localhost:8765/index.html" out.json 180 tests/drive-stale.js shot
python3 scripts/flaky-server.py 8768 --flaky & # breaks every 3rd model response
node scripts/headless-test.mjs "http://localhost:8768/tests/omr-test.html?page=huron_1.png" out.json 380
python3 scripts/flaky-server.py 8769 &
node scripts/headless-test.mjs "http://localhost:8769/testdata/" out.json 480 tests/drive-download-resume.js shot
python3 scripts/hang-server.py 8766 & # /__hang?on makes requests hang, like a weak signal
node scripts/headless-test.mjs "http://127.0.0.1:8766/index.html" out.json 120 tests/drive-slow-start.js shot
CPU_THROTTLE=4 node scripts/headless-test.mjs "http://localhost:8765/index.html?view=switch" out.json 180 tests/drive-read-timing.js shot
node scripts/headless-test.mjs "http://localhost:8765/testdata/" out.json 700 tests/drive-resume.js shot
node scripts/headless-test.mjs "http://localhost:8765/testdata/" out.json 400 tests/drive-resume-manual.js shot
.github/workflows/pages.yml fetches homr and the models and publishes to
GitHub Pages (custom domain partsong.app, set in the repo's Pages settings) on
every push to main. scripts/stamp-version.mjs adds ?v=<commit> to the app's
CSS/JS references so a deploy is never mixed with cached files from the last one (models aren't committed; GitHub release
downloads don't allow cross-origin fetches, so they're served with the site).
© 2026 Paul Fremantle. Partsong is free software under the GNU Affero General
Public License v3.0 (see LICENSE); it has to be, as it distributes homr. The
app's About page links here, which is the offer of source to its users.
Third-party components: homr (AGPL-3.0), whose models build on oemer (MIT) and Polyphonic-TrOMR (Apache-2.0); Pyodide (MPL-2.0) with NumPy (BSD-3-Clause), OpenCV (Apache-2.0) and Pillow (MIT-CMU); ONNX Runtime Web (MIT); pdf.js (Apache-2.0); OpenSheetMusicDisplay (BSD-3-Clause).