Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c12bfcb4 | |||
| e50eff9122 | |||
| 677d09524e | |||
| 5c918109d5 |
+7
-1
@@ -7,7 +7,13 @@ import numpy as np
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.easyid3 import EasyID3
|
||||
|
||||
from font_manager import safe_title
|
||||
|
||||
def safe_title(title):
|
||||
# Deliberately not font_manager.safe_title: that module imports PyQt5 at load
|
||||
# time, and master_core is on the Qt-free web path (the `web` extra has no Qt).
|
||||
if not title:
|
||||
return title
|
||||
return title.encode('utf-8', errors='replace').decode('utf-8')
|
||||
|
||||
|
||||
def _try_mp3_tags(file_path):
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* The UA's [hidden] { display: none } loses to any class that sets display
|
||||
(e.g. .drop-overlay's flex), which left the overlay permanently covering the
|
||||
page. Make the attribute win everywhere. */
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
|
||||
+8
-8
@@ -100,7 +100,7 @@ async function api(path, options) {
|
||||
}
|
||||
|
||||
async function loadMetrics() {
|
||||
const data = await api('/api/metrics');
|
||||
const data = await api('api/metrics');
|
||||
state.metrics = data.metrics;
|
||||
state.metric = data.default;
|
||||
const select = el('metric-select');
|
||||
@@ -134,7 +134,7 @@ function renderFileList() {
|
||||
check.addEventListener('click', (ev) => ev.stopPropagation());
|
||||
check.addEventListener('change', async () => {
|
||||
const ids = collectOverlayFromDOM();
|
||||
await api('/api/overlay', {
|
||||
await api('api/overlay', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: ids }),
|
||||
@@ -159,7 +159,7 @@ function renderFileList() {
|
||||
remove.title = 'Forget this track (drops the analysis and removes it from browser storage)';
|
||||
remove.addEventListener('click', async (ev) => {
|
||||
ev.stopPropagation();
|
||||
await api(`/api/file?file_id=${encodeURIComponent(f.file_id)}`, { method: 'DELETE' });
|
||||
await api(`api/file?file_id=${encodeURIComponent(f.file_id)}`, { method: 'DELETE' });
|
||||
// The server only ever held numbers; the audio is ours to delete.
|
||||
await store.remove(f.file_id);
|
||||
releaseObjectUrl(f.file_id);
|
||||
@@ -871,7 +871,7 @@ async function refreshPlot() {
|
||||
if (state.yLog !== null) params.set('y_log', String(state.yLog));
|
||||
if (state.xLog !== null) params.set('x_log', String(state.xLog));
|
||||
|
||||
const payload = await api(`/api/plot?${params.toString()}`);
|
||||
const payload = await api(`api/plot?${params.toString()}`);
|
||||
state.payload = payload;
|
||||
el('plot-title').textContent = payload.datasets.length
|
||||
? `${payload.metric_name}${payload.datasets.length > 1 ? ` — ${payload.datasets.length} tracks` : ` — ${payload.datasets[0].label}`}`
|
||||
@@ -933,7 +933,7 @@ async function ingest(fileList) {
|
||||
}
|
||||
}
|
||||
|
||||
const s = await api('/api/session');
|
||||
const s = await api('api/session');
|
||||
state.files = s.files;
|
||||
state.overlay = s.overlay;
|
||||
if (!state.selected && state.files.length) state.selected = state.files[0].file_id;
|
||||
@@ -952,7 +952,7 @@ async function ingest(fileList) {
|
||||
* second round trip) and is recorded in `state.analysed`.
|
||||
*/
|
||||
async function analyseOne(fileId, name, metric = state.metric) {
|
||||
const url = `/api/analyse?` + new URLSearchParams({
|
||||
const url = `api/analyse?` + new URLSearchParams({
|
||||
file_id: fileId,
|
||||
name,
|
||||
metric: metric || state.metric || '',
|
||||
@@ -972,7 +972,7 @@ async function analyseOne(fileId, name, metric = state.metric) {
|
||||
function ensureOverlay(fileId) {
|
||||
if (!state.overlay.includes(fileId)) {
|
||||
state.overlay = [...state.overlay, fileId];
|
||||
api('/api/overlay', {
|
||||
api('api/overlay', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_ids: state.overlay }),
|
||||
@@ -1129,7 +1129,7 @@ async function boot() {
|
||||
}
|
||||
|
||||
await loadMetrics();
|
||||
const s = await api('/api/session');
|
||||
const s = await api('api/session');
|
||||
state.files = s.files;
|
||||
state.overlay = s.overlay;
|
||||
state.selected = state.files.length ? state.files[0].file_id : null;
|
||||
|
||||
+10
-28
@@ -165,41 +165,23 @@ export async function sweepIncomplete() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream stored audio to an endpoint as a raw body.
|
||||
* Send stored audio to an endpoint as a raw body.
|
||||
*
|
||||
* The body is sent as a stream (duplex: 'half') so a 200 MB file doesn't have to
|
||||
* be materialised as a second in-memory copy just to be uploaded.
|
||||
* The OPFS File is passed to fetch as-is: the browser reads a Blob body lazily
|
||||
* from its backing store, so a 200 MB master is never materialised in memory.
|
||||
* (A ReadableStream body with duplex: 'half' was used before, but Chrome only
|
||||
* allows streaming uploads over HTTP/2+, so any file past the old 8 MB cutoff
|
||||
* failed with "Failed to fetch" on HTTP/1.1 — e.g. http://localhost.)
|
||||
*/
|
||||
export async function send(fileId, url, {method = 'POST', onProgress} = {}) {
|
||||
const blob = await get(fileId);
|
||||
if (!blob) throw new Error('audio is no longer in browser storage — re-add the file');
|
||||
|
||||
// Small files go as a plain body: some proxies mishandle streaming duplex
|
||||
// requests, and below a few MB there is nothing to gain.
|
||||
if (blob.size < 8 * 1024 * 1024) {
|
||||
const buf = await blob.arrayBuffer();
|
||||
if (onProgress) onProgress(blob.size, blob.size);
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {'Content-Type': 'application/octet-stream'},
|
||||
body: buf,
|
||||
});
|
||||
}
|
||||
|
||||
let sent = 0;
|
||||
const body = blob.stream().pipeThrough(
|
||||
new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
sent += chunk.byteLength;
|
||||
if (onProgress) onProgress(sent, blob.size);
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
}),
|
||||
);
|
||||
return fetch(url, {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {'Content-Type': 'application/octet-stream'},
|
||||
body,
|
||||
duplex: 'half',
|
||||
body: blob,
|
||||
});
|
||||
if (onProgress) onProgress(blob.size, blob.size);
|
||||
return res;
|
||||
}
|
||||
|
||||
+4
-4
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Audio Mastering Analysis Toolkit</title>
|
||||
<link rel="stylesheet" href="/static/vendor/uPlot.min.css">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<link rel="stylesheet" href="static/vendor/uPlot.min.css">
|
||||
<link rel="stylesheet" href="static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -89,9 +89,9 @@
|
||||
|
||||
<div id="drop-overlay" class="drop-overlay" hidden><span>Drop to analyse</span></div>
|
||||
|
||||
<script src="/static/vendor/uPlot.iife.min.js"></script>
|
||||
<script src="static/vendor/uPlot.iife.min.js"></script>
|
||||
<!-- app.js is an ES module: it imports ./audio-store.js, which owns the
|
||||
browser-side copy of every uploaded file. -->
|
||||
<script type="module" src="/static/app.js"></script>
|
||||
<script type="module" src="static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user