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.mp3 import MP3
|
||||||
from mutagen.easyid3 import EasyID3
|
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):
|
def _try_mp3_tags(file_path):
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { 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 {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
+8
-8
@@ -100,7 +100,7 @@ async function api(path, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadMetrics() {
|
async function loadMetrics() {
|
||||||
const data = await api('/api/metrics');
|
const data = await api('api/metrics');
|
||||||
state.metrics = data.metrics;
|
state.metrics = data.metrics;
|
||||||
state.metric = data.default;
|
state.metric = data.default;
|
||||||
const select = el('metric-select');
|
const select = el('metric-select');
|
||||||
@@ -134,7 +134,7 @@ function renderFileList() {
|
|||||||
check.addEventListener('click', (ev) => ev.stopPropagation());
|
check.addEventListener('click', (ev) => ev.stopPropagation());
|
||||||
check.addEventListener('change', async () => {
|
check.addEventListener('change', async () => {
|
||||||
const ids = collectOverlayFromDOM();
|
const ids = collectOverlayFromDOM();
|
||||||
await api('/api/overlay', {
|
await api('api/overlay', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ file_ids: ids }),
|
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.title = 'Forget this track (drops the analysis and removes it from browser storage)';
|
||||||
remove.addEventListener('click', async (ev) => {
|
remove.addEventListener('click', async (ev) => {
|
||||||
ev.stopPropagation();
|
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.
|
// The server only ever held numbers; the audio is ours to delete.
|
||||||
await store.remove(f.file_id);
|
await store.remove(f.file_id);
|
||||||
releaseObjectUrl(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.yLog !== null) params.set('y_log', String(state.yLog));
|
||||||
if (state.xLog !== null) params.set('x_log', String(state.xLog));
|
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;
|
state.payload = payload;
|
||||||
el('plot-title').textContent = payload.datasets.length
|
el('plot-title').textContent = payload.datasets.length
|
||||||
? `${payload.metric_name}${payload.datasets.length > 1 ? ` — ${payload.datasets.length} tracks` : ` — ${payload.datasets[0].label}`}`
|
? `${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.files = s.files;
|
||||||
state.overlay = s.overlay;
|
state.overlay = s.overlay;
|
||||||
if (!state.selected && state.files.length) state.selected = state.files[0].file_id;
|
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`.
|
* second round trip) and is recorded in `state.analysed`.
|
||||||
*/
|
*/
|
||||||
async function analyseOne(fileId, name, metric = state.metric) {
|
async function analyseOne(fileId, name, metric = state.metric) {
|
||||||
const url = `/api/analyse?` + new URLSearchParams({
|
const url = `api/analyse?` + new URLSearchParams({
|
||||||
file_id: fileId,
|
file_id: fileId,
|
||||||
name,
|
name,
|
||||||
metric: metric || state.metric || '',
|
metric: metric || state.metric || '',
|
||||||
@@ -972,7 +972,7 @@ async function analyseOne(fileId, name, metric = state.metric) {
|
|||||||
function ensureOverlay(fileId) {
|
function ensureOverlay(fileId) {
|
||||||
if (!state.overlay.includes(fileId)) {
|
if (!state.overlay.includes(fileId)) {
|
||||||
state.overlay = [...state.overlay, fileId];
|
state.overlay = [...state.overlay, fileId];
|
||||||
api('/api/overlay', {
|
api('api/overlay', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ file_ids: state.overlay }),
|
body: JSON.stringify({ file_ids: state.overlay }),
|
||||||
@@ -1129,7 +1129,7 @@ async function boot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await loadMetrics();
|
await loadMetrics();
|
||||||
const s = await api('/api/session');
|
const s = await api('api/session');
|
||||||
state.files = s.files;
|
state.files = s.files;
|
||||||
state.overlay = s.overlay;
|
state.overlay = s.overlay;
|
||||||
state.selected = state.files.length ? state.files[0].file_id : null;
|
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
|
* The OPFS File is passed to fetch as-is: the browser reads a Blob body lazily
|
||||||
* be materialised as a second in-memory copy just to be uploaded.
|
* 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} = {}) {
|
export async function send(fileId, url, {method = 'POST', onProgress} = {}) {
|
||||||
const blob = await get(fileId);
|
const blob = await get(fileId);
|
||||||
if (!blob) throw new Error('audio is no longer in browser storage — re-add the file');
|
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
|
const res = await fetch(url, {
|
||||||
// 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, {
|
|
||||||
method,
|
method,
|
||||||
headers: {'Content-Type': 'application/octet-stream'},
|
headers: {'Content-Type': 'application/octet-stream'},
|
||||||
body,
|
body: blob,
|
||||||
duplex: 'half',
|
|
||||||
});
|
});
|
||||||
|
if (onProgress) onProgress(blob.size, blob.size);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Audio Mastering Analysis Toolkit</title>
|
<title>Audio Mastering Analysis Toolkit</title>
|
||||||
<link rel="stylesheet" href="/static/vendor/uPlot.min.css">
|
<link rel="stylesheet" href="static/vendor/uPlot.min.css">
|
||||||
<link rel="stylesheet" href="/static/app.css">
|
<link rel="stylesheet" href="static/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -89,9 +89,9 @@
|
|||||||
|
|
||||||
<div id="drop-overlay" class="drop-overlay" hidden><span>Drop to analyse</span></div>
|
<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
|
<!-- app.js is an ES module: it imports ./audio-store.js, which owns the
|
||||||
browser-side copy of every uploaded file. -->
|
browser-side copy of every uploaded file. -->
|
||||||
<script type="module" src="/static/app.js"></script>
|
<script type="module" src="static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user