BWF Analyser: browser page and macOS app

Reads and edits BWF metadata for production sound. One source tree builds a
single self-contained page and a native Tauri app with a Rust audio engine and
WAV writer. Around 370 checks across seven test suites.

First commit of the existing state, so that from here every change can be
seen and undone.
This commit is contained in:
2026-08-17 22:50:39 +08:00
commit 2d0b7fe8b5
50 changed files with 42383 additions and 0 deletions
+404
View File
@@ -0,0 +1,404 @@
<div id="bwfa-app-1" class="bwfa-scope bwfa-app" data-bwfa-app aria-label="BWF Metadata Analyser">
<div class="bwfa-header">
<h2 class="bwfa-title">BWF Metadata Analyser</h2>
<p class="bwfa-privacy-note">
<span class="badge badge-success" aria-hidden="true">Local only</span>
Your files are read directly in this browser tab and never leave your computer.
</p>
</div>
<div class="bwfa-dropzone" data-bwfa-dropzone tabindex="0" role="button"
aria-label="Select or drop a folder or files of BWF/WAV recordings">
<p class="bwfa-dropzone-text" data-bwfa-dropzone-text></p>
<div class="bwfa-dropzone-actions">
<button type="button" class="btn btn-primary" data-bwfa-select-folder>Select Folder</button>
<button type="button" class="btn btn-secondary" data-bwfa-select-files>Select Files</button>
<button type="button" class="btn btn-outline" data-bwfa-clear disabled>Clear</button>
</div>
<p class="bwfa-dropzone-subnote">
On iPhone/iPad, if the folder picker won't let you select a folder you've navigated into, use "Select Files" and choose the recordings directly instead &mdash; this is a known iOS quirk with folder pickers in general, not specific to this tool.
</p>
<input
type="file"
class="bwfa-visually-hidden"
data-bwfa-file-input
webkitdirectory
directory
multiple
accept=".wav,.bwf,.broadcastwave"
/>
<input
type="file"
class="bwfa-visually-hidden"
data-bwfa-files-input
multiple
accept=".wav,.bwf,.broadcastwave"
/>
</div>
<div class="bwfa-edit-entry" data-bwfa-edit-entry hidden>
<div class="bwfa-edit-divider"><span>or</span></div>
<div class="bwfa-launch-logo" aria-hidden="true"></div>
<button type="button" class="btn btn-outline" data-bwfa-edit-folder>Edit Metadata in a Folder&hellip;</button>
<span class="bwfa-current-folder" data-bwfa-current-folder hidden></span>
<p class="bwfa-dropzone-subnote">
This opens files for editing rather than just viewing. Saving a change writes it directly back to the original file on your disk &mdash; there is no undo, and no automatic backup is made. Available in Chromium-based browsers only (Chrome, Edge, Opera, Arc); Firefox and Safari, including iPhone/iPad, cannot write to local files from a webpage at all.
</p>
</div>
<p class="bwfa-fs-warning" data-bwfa-fs-warning>
Your browser blocked the folder-write permission because this page was opened straight from disk
(a <code>file://</code> address), where writing to local files isn't allowed. Reading, playback and
export all still work. To edit metadata, serve this folder over http instead: run
<code>python3 -m http.server</code> in the folder holding this file, then open
<code>http://localhost:8000/</code>.
</p>
<div class="bwfa-status" data-bwfa-status role="status" aria-live="polite"></div>
<div class="bwfa-progress progress" data-bwfa-progress-wrap hidden>
<div class="progress-bar" data-bwfa-progress-bar style="width:0%"></div>
</div>
<div class="bwfa-results" data-bwfa-results hidden>
<div class="bwfa-toolbar">
<div class="form-group bwfa-search-group">
<input
type="search"
class="form-control"
data-bwfa-search
placeholder="Filter by filename, scene, take&hellip;"
/>
</div>
<div class="bwfa-export-actions">
<!-- Columns moved to the cogwheel in the table's first header
cell. The button stays here, hidden, because the analyser
wires a listener to it at startup. -->
<div class="dropdown bwfa-legacy-export" data-bwfa-columns-dropdown>
<button type="button" class="btn btn-outline" data-bwfa-columns-toggle>Columns</button>
</div>
<button type="button" class="btn btn-outline" data-bwfa-bulk-edit-toggle hidden>Bulk Edit</button>
<button type="button" class="btn btn-outline" data-bwfa-report-open>Sound Report</button>
<!-- The three buttons the report modal replaced. Kept in the DOM,
hidden by CSS: the analyser wires its export pipeline to
them at startup, and the modal drives them rather than
duplicating any of it. -->
<button type="button" class="btn btn-outline bwfa-legacy-export" data-bwfa-export-fields-toggle>Export Fields</button>
<button type="button" class="btn btn-secondary bwfa-legacy-export" data-bwfa-export-csv>Export CSV</button>
<button type="button" class="btn btn-secondary bwfa-legacy-export" data-bwfa-export-pdf>Export PDF</button>
</div>
</div>
<div class="bwfa-editing-note" data-bwfa-editing-note hidden>
<span class="badge badge-warning">Editing</span>
Changes save directly to the original files on disk.
</div>
<div class="bwfa-bulk-edit-panel" data-bwfa-bulk-edit-panel hidden>
<div class="bwfa-bulk-edit-header">
<h4>Bulk Edit</h4>
<p data-bwfa-bulk-edit-count></p>
</div>
<div class="bwfa-bulk-edit-fields" data-bwfa-bulk-edit-fields></div>
<div class="bwfa-bulk-tracks" data-bwfa-bulk-tracks hidden></div>
<div class="bwfa-bulk-edit-actions">
<div class="bwfa-bulk-edit-progress" data-bwfa-bulk-edit-progress hidden></div>
<button type="button" class="btn btn-outline" data-bwfa-bulk-edit-cancel>Cancel</button>
<button type="button" class="btn btn-primary" data-bwfa-bulk-edit-apply disabled>Apply</button>
</div>
</div>
<div class="bwfa-player" data-bwfa-player hidden>
<div class="bwfa-player-label">
<span class="badge badge-info" data-bwfa-player-badge>Now playing</span>
<span class="bwfa-player-filename" data-bwfa-player-filename></span>
<button type="button" class="btn btn-sm btn-outline bwfa-player-edit"
data-bwfa-player-edit>Edit</button>
<span class="bwfa-player-decoding" data-bwfa-player-decoding hidden>Decoding&hellip;</span>
<button type="button" class="bwfa-round-btn bwfa-mixer-open"
data-bwfa-mixer-open aria-label="Mixer">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
<circle cx="8" cy="8" r="5.6" fill="none" stroke="currentColor"
stroke-width="1.4"/>
<path d="M8 3.6 V 8" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round"/>
</svg>
</button>
<button type="button" class="bwfa-round-btn bwfa-spectro-open"
data-bwfa-spectro-open aria-label="Spectrogram">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
<rect x="1" y="9" width="2" height="5" rx="0.6"/>
<rect x="4.5" y="5" width="2" height="9" rx="0.6"/>
<rect x="8" y="2" width="2" height="12" rx="0.6"/>
<rect x="11.5" y="6.5" width="2" height="7.5" rx="0.6"/>
</svg>
</button>
</div>
<div class="modal-backdrop bwfa-mixer-backdrop" data-bwfa-mixer-backdrop hidden></div>
<div class="modal bwfa-mixer-modal" data-bwfa-mixer hidden>
<div class="modal-dialog bwfa-mixer-dialog" role="dialog" aria-modal="true"
aria-label="Mixer">
<button type="button" class="modal-close" data-bwfa-mixer-close
aria-label="Close">&times;</button>
<div class="modal-header">
<div class="bwfa-modal-title-group">
<h3 class="modal-title">Mixer</h3>
<span class="bwfa-modal-filename" data-bwfa-mixer-filename></span>
</div>
</div>
<div class="modal-body">
<div class="bwfa-mixer-strips" data-bwfa-mixer-strips></div>
<div class="bwfa-player-transport bwfa-mixer-transport">
<button type="button" class="btn btn-sm btn-primary bwfa-player-playpause"
data-bwfa-mixer-playpause><span class="bwfa-btn-label"
data-bwfa-label>Play</span></button>
<span class="bwfa-player-time" data-bwfa-mixer-elapsed>00:00:00</span>
<canvas class="bwfa-player-waveform" data-bwfa-mixer-waveform
width="700" height="56"
aria-label="Waveform — click to seek"></canvas>
<span class="bwfa-player-time" data-bwfa-mixer-duration>00:00:00</span>
</div>
<p class="bwfa-mixer-note">This mix is for listening only. Exports and
reports ignore it.</p>
</div>
<div class="modal-footer">
<div class="bwfa-modal-nav bwfa-modal-nav-back">
<button type="button" class="btn btn-sm btn-outline"
data-bwfa-mixer-prev aria-label="Previous file">&#8249;</button>
<span class="bwfa-modal-nav-name" data-bwfa-mixer-prev-name></span>
</div>
<div class="bwfa-modal-nav bwfa-modal-nav-on">
<span class="bwfa-modal-nav-name" data-bwfa-mixer-next-name></span>
<button type="button" class="btn btn-sm btn-outline"
data-bwfa-mixer-next aria-label="Next file">&#8250;</button>
</div>
<button type="button" class="btn btn-outline" data-bwfa-mixer-close>Close</button>
</div>
</div>
</div>
<div class="modal-backdrop bwfa-spectro-backdrop" data-bwfa-spectro-backdrop hidden></div>
<div class="modal bwfa-spectro-modal" data-bwfa-spectro hidden>
<div class="modal-dialog bwfa-spectro-dialog" role="dialog" aria-modal="true"
aria-label="Spectrogram">
<button type="button" class="modal-close" data-bwfa-spectro-close
aria-label="Close">&times;</button>
<div class="bwfa-spectro-head">
<h4 data-bwfa-spectro-title>Spectrogram</h4>
<p data-bwfa-spectro-note></p>
<button type="button" class="btn btn-sm btn-outline bwfa-spectro-save"
data-bwfa-spectro-save>Save Image</button>
</div>
<div class="bwfa-spectro-plot">
<div class="bwfa-spectro-scale" data-bwfa-spectro-scale></div>
<canvas class="bwfa-spectro-canvas" data-bwfa-spectro-canvas></canvas>
</div>
</div>
</div>
<div class="bwfa-player-transport">
<button type="button" class="btn btn-sm btn-primary bwfa-player-playpause" data-bwfa-player-playpause><span class="bwfa-btn-label" data-bwfa-label>Play</span></button>
<span class="bwfa-player-time" data-bwfa-player-elapsed>00:00:00</span>
<canvas class="bwfa-player-waveform" data-bwfa-player-waveform width="700" height="56"
aria-label="Waveform — click to seek"></canvas>
<span class="bwfa-player-time" data-bwfa-player-duration>00:00:00</span>
</div>
<div class="bwfa-channel-row" data-bwfa-channel-row>
<span class="bwfa-channel-hint">Click a channel to mute it, double-click to solo it:</span>
<div class="chip-group bwfa-channel-chips" data-bwfa-channel-chips></div>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover bwfa-table">
<thead data-bwfa-table-head></thead>
<tbody data-bwfa-table-body></tbody>
</table>
</div>
</div>
<div class="bwfa-empty" data-bwfa-empty>No files analysed yet.</div>
<!-- Which columns the table shows. Opened by the cogwheel above the Play
column, since that's where you are when you want it. -->
<div class="modal-backdrop" data-bwfa-columns-backdrop hidden></div>
<div class="modal" data-bwfa-columns-modal role="dialog" aria-modal="true"
aria-labelledby="bwfa-app-1-columns-title" hidden>
<div class="modal-dialog bwfa-columns-dialog">
<div class="modal-header">
<h3 class="modal-title" id="bwfa-app-1-columns-title">Settings</h3>
<button type="button" class="modal-close" data-bwfa-columns-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<details class="bwfa-settings-section" data-bwfa-settings="appearance">
<summary>Appearance</summary>
<div class="bwfa-settings-body">
<div class="bwfa-choice-list">
<label class="bwfa-choice">
<input type="radio" name="bwfa-app-1-theme" value="auto" checked data-bwfa-theme-mode>
<span>Match the system<small>Follow macOS as it changes through the day</small></span>
</label>
<label class="bwfa-choice">
<input type="radio" name="bwfa-app-1-theme" value="light" data-bwfa-theme-mode>
<span>Light<small>Paper white, whatever the system is doing</small></span>
</label>
<label class="bwfa-choice">
<input type="radio" name="bwfa-app-1-theme" value="dark" data-bwfa-theme-mode>
<span>Dark<small>Anthracite, for a night shoot or a dark suite</small></span>
</label>
</div>
</div>
</details>
<details class="bwfa-settings-section" data-bwfa-settings="playback">
<summary>When a file finishes</summary>
<div class="bwfa-settings-body">
<div class="bwfa-choice-list">
<label class="bwfa-choice">
<input type="radio" name="bwfa-app-1-playback-mode" value="stop" checked
data-bwfa-playback-mode>
<span>Stop<small>Hold it in the player, ready to play again</small></span>
</label>
<label class="bwfa-choice">
<input type="radio" name="bwfa-app-1-playback-mode" value="next" data-bwfa-playback-mode>
<span>Play the next file<small>Work down the list as it's sorted and filtered</small></span>
</label>
<label class="bwfa-choice">
<input type="radio" name="bwfa-app-1-playback-mode" value="repeat" data-bwfa-playback-mode>
<span>Repeat it<small>Loop the same file until you stop it</small></span>
</label>
</div>
<div class="bwfa-settings-action" data-bwfa-audio-reset-row>
<button type="button" class="btn btn-sm btn-outline" data-bwfa-audio-reset>Restart the app</button>
<small>If a file plays with no sound at all, this rebuilds the audio path
and reloads the window — the same thing quitting and reopening does,
without the quitting. The folder comes back with it.</small>
</div>
</div>
</details>
<details class="bwfa-settings-section" data-bwfa-settings="columns">
<summary>Columns in the table</summary>
<div class="bwfa-settings-body">
<div class="bwfa-columns-menu" data-bwfa-columns-menu></div>
</div>
</details>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bwfa-columns-close>Done</button>
</div>
</div>
</div>
<!-- The sound report: who it's for, what format it lands in, and which
fields it carries. One field list for both formats, because a sound
report is a sound report whichever way it's written. -->
<div class="modal-backdrop" data-bwfa-export-backdrop hidden></div>
<div class="modal" data-bwfa-export-modal role="dialog" aria-modal="true"
aria-labelledby="bwfa-app-1-export-title" hidden>
<div class="modal-dialog bwfa-export-dialog">
<div class="modal-header">
<div class="bwfa-modal-title-group">
<h3 class="modal-title" id="bwfa-app-1-export-title">Sound Report</h3>
<span class="bwfa-modal-filename" data-bwfa-report-subject></span>
</div>
<button type="button" class="modal-close" data-bwfa-export-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<div class="bwfa-report-details">
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-company">Production company</label>
<input type="text" class="form-control" id="bwfa-app-1-report-company"
data-bwfa-report-field="company" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-project">Project / show name</label>
<input type="text" class="form-control" id="bwfa-app-1-report-project"
data-bwfa-report-field="project" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-director">Director</label>
<input type="text" class="form-control" id="bwfa-app-1-report-director"
data-bwfa-report-field="director" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-mixer">Sound mixer</label>
<input type="text" class="form-control" id="bwfa-app-1-report-mixer"
data-bwfa-report-field="mixer" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-phone">Mixer phone</label>
<input type="text" class="form-control" id="bwfa-app-1-report-phone"
data-bwfa-report-field="phone" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-email">Mixer email</label>
<input type="text" class="form-control" id="bwfa-app-1-report-email"
data-bwfa-report-field="email" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-note">Note</label>
<input type="text" class="form-control" id="bwfa-app-1-report-note"
data-bwfa-report-field="note" autocomplete="off">
</div>
<div class="bwfa-report-row">
<label class="form-label" for="bwfa-app-1-report-format">Output format</label>
<select class="form-select" id="bwfa-app-1-report-format" data-bwfa-report-format>
<option value="pdf" selected>PDF (sound report)</option>
<option value="csv">CSV (spreadsheet)</option>
</select>
</div>
</div>
<div class="bwfa-report-fields-head">
<h4>Fields in the report</h4>
<span class="bwfa-report-count" data-bwfa-export-count></span>
<div class="bwfa-export-actions-row">
<button type="button" class="btn btn-sm btn-outline" data-bwfa-export-all>Select all</button>
<button type="button" class="btn btn-sm btn-outline" data-bwfa-export-none>Select none</button>
<button type="button" class="btn btn-sm btn-outline" data-bwfa-export-defaults>Restore defaults</button>
</div>
</div>
<div class="bwfa-export-fields" data-bwfa-export-fields></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" data-bwfa-export-close>Cancel</button>
<button type="button" class="btn btn-primary" data-bwfa-report-create>Create Report</button>
</div>
</div>
</div>
<div class="modal-backdrop" data-bwfa-modal-backdrop hidden></div>
<div
class="modal"
data-bwfa-modal
role="dialog"
aria-modal="true"
aria-labelledby="bwfa-app-1-modal-title"
hidden
>
<div class="modal-dialog bwfa-modal-dialog">
<div class="modal-header">
<div class="bwfa-modal-title-group">
<h3 class="modal-title" id="bwfa-app-1-modal-title">File metadata</h3>
<span class="bwfa-modal-filename" data-bwfa-modal-filename></span>
</div>
<button type="button" class="modal-close" data-bwfa-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body" data-bwfa-modal-body></div>
<div class="modal-footer">
<div class="bwfa-modal-nav bwfa-modal-nav-back">
<button type="button" class="btn btn-sm btn-outline" data-bwfa-modal-prev
aria-label="Previous file">&#8249;</button>
<span class="bwfa-modal-nav-name" data-bwfa-modal-prev-name></span>
</div>
<div class="bwfa-modal-nav bwfa-modal-nav-on">
<span class="bwfa-modal-nav-name" data-bwfa-modal-next-name></span>
<button type="button" class="btn btn-sm btn-outline" data-bwfa-modal-next
aria-label="Next file">&#8250;</button>
</div>
<button type="button" class="btn btn-primary" data-bwfa-modal-save hidden disabled>Save</button>
<button type="button" class="btn btn-secondary" data-bwfa-modal-play>Play</button>
<button type="button" class="btn btn-outline" data-bwfa-modal-close>Close</button>
</div>
</div>
</div>
</div>