Ich versuche, die 3-Band-Wellenformanzeige von Rekordbox (niedrige, mittlere, hohe Frequenzen als feste, farbcodierte Formen) in JavaScript unter Verwendung von Leinwand zu replizieren. Ich habe bereits ein Python -Skript erstellt, das Wellenformdaten aus Audio -Dateien extrahiert und in 3 Bänder (niedrig, mittel, hoch) aufteilt und Min/Max -Beispielpaare pro Band in eine JSON -Datei speichert. Meine Herausforderung besteht darin, diese Bänder als feste, gefüllte Formen wie Rekordbox zu zeichnen, wo der Umschlag jedes Bandes eher ein glatter, kontinuierlicher Block als eine schnörkörige Linie ist.
Waveform Viewer
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background: #1a1a1a;
color: #e0e0e0;
}
.container {
max-width: 100%;
}
h1 {
margin: 0 0 20px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
background: #2c6e31;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #3c9341;
}
button:disabled {
background: #444;
color: #888;
cursor: not-allowed;
}
.waveform-container {
height: 300px;
background: #000;
border: 1px solid #555;
border-radius: 4px;
}
canvas {
display: block;
}
#time-info {
margin: 10px 0;
color: #aaa;
}
.file-input input[type="file"] {
background: #333;
color: #e0e0e0;
border: 1px solid #555;
padding: 5px;
border-radius: 4px;
}
#loading {
display: none;
margin-left: 10px;
color: #aaa;
}
.scroll-bar {
width: 100%;
height: 20px;
background: #333;
border: 1px solid #555;
border-radius: 4px;
}
.band-checkboxes {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.band-checkbox {
display: flex;
align-items: center;
padding: 5px 10px;
background: #333;
border-radius: 4px;
cursor: pointer;
}
.band-checkbox input {
margin-right: 5px;
}
.color-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 5px;
}
Waveform Viewer
Loading...
Zoom In
Zoom Out
Duration: 0s | View: 0s - 0s
class WaveformRenderer {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext("2d");
this.data = null;
this.zoom = 1;
this.offset = 0;
this.bands = {
low: { color: "#0055e2", visible: true }, // Blue, bottom
mid: { color: "#f2aa3c", visible: true }, // Orange, middle
high: { color: "#ffffff", visible: true }, // White, top
};
this.colors = { bg: "#000000", center: "#ccc" };
this.resize();
this.createCheckboxes();
window.addEventListener("resize", () => this.resize());
}
resize() {
this.canvas.width = this.canvas.parentElement.clientWidth;
this.canvas.height = 300;
this.render();
}
createCheckboxes() {
const container = document.getElementById("band-checkboxes");
for (const [name, { color, visible }] of Object.entries(this.bands)) {
const label = document.createElement("label");
label.className = "band-checkbox";
label.innerHTML = `
${name}
`;
label.querySelector("input").addEventListener("change", (e) => {
this.bands[name].visible = e.target.checked;
this.render();
});
container.appendChild(label);
}
}
async loadData(json) {
this.data = json;
for (const band of this.data.data.multiband.bands) {
band.samples = await this.decodeSamples(
band.samples,
band.sample_format,
this.data.bits_per_sample
);
}
this.render();
this.updateTime();
this.enableControls();
this.updateScroll();
}
async decodeSamples(samples, format, bits) {
const binary = atob(samples);
const bytes = new Uint8Array(
[...binary].map((ch) => ch.charCodeAt(0))
);
const view = new DataView(bytes.buffer);
const count = bytes.length / (bits === 8 ? 1 : 2);
const result = new Int16Array(count);
for (let i = 0; i < count; i++) {
result =
bits === 8 ? view.getInt8(i) * 256 : view.getInt16(i * 2, true);
}
return result;
}
zoomIn() {
if (this.zoom < 10) {
this.zoom *= 1.5;
this.adjustOffset();
this.render();
this.updateTime();
this.updateScroll();
}
}
zoomOut() {
if (this.zoom > 0.1) {
this.zoom /= 1.5;
this.adjustOffset();
this.render();
this.updateTime();
this.updateScroll();
}
}
adjustOffset() {
const visible = this.canvas.width / this.zoom;
const total =
this.data?.data.multiband.bands[0].samples.length / 2 || 0;
this.offset = Math.max(0, Math.min(this.offset, total - visible));
}
render() {
const { width, height } = this.canvas;
this.ctx.fillStyle = this.colors.bg;
this.ctx.fillRect(0, 0, width, height);
if (!this.data) return this.renderEmpty();
const center = height / 2;
this.ctx.beginPath();
this.ctx.moveTo(0, center);
this.ctx.lineTo(width, center);
this.ctx.strokeStyle = this.colors.center;
this.ctx.stroke();
// Render in z-index order: low, mid, high
["low", "mid", "high"].forEach((band) => {
if (this.bands[band].visible) this.renderBand(band, center);
});
}
renderBand(bandName, center) {
const band = this.data.data.multiband.bands.find(
(b) => b.name === bandName
);
if (!band) return;
const samples = band.samples;
const start = Math.floor(this.offset);
const end = Math.min(
Math.ceil(this.offset + this.canvas.width / this.zoom),
samples.length / 2
);
const scale = (this.canvas.height / 2) * 0.8;
this.ctx.beginPath();
this.ctx.moveTo(0, center);
for (let i = start; i < end; i++) {
const x = (i - this.offset) * this.zoom;
const max = center - (samples[i * 2 + 1] / 32768) * scale;
this.ctx.lineTo(x, max);
}
for (let i = end - 1; i >= start; i--) {
const x = (i - this.offset) * this.zoom;
const min = center - (samples[i * 2] / 32768) * scale;
this.ctx.lineTo(x, min);
}
this.ctx.closePath();
this.ctx.fillStyle = this.bands[bandName].color;
this.ctx.fill();
}
renderEmpty() {
this.ctx.fillStyle = "#666";
this.ctx.font = "14px Arial";
this.ctx.textAlign = "center";
this.ctx.fillText(
"Upload a waveform JSON file",
this.canvas.width / 2,
this.canvas.height / 2
);
}
updateTime() {
if (!this.data) return;
const { samples_per_pixel, sample_rate } = this.data;
const total =
((this.data.data.multiband.bands[0].samples.length / 2) *
samples_per_pixel) /
sample_rate;
const start = (this.offset * samples_per_pixel) / sample_rate;
const end = Math.min(
total,
start +
((this.canvas.width / this.zoom) * samples_per_pixel) /
sample_rate
);
document.getElementById(
"time-info"
).textContent = `Duration: ${total.toFixed(
2
)}s | View: ${start.toFixed(2)}s - ${end.toFixed(2)}s`;
}
enableControls() {
["zoom-in", "zoom-out", "scroll-bar"].forEach(
(id) => (document.getElementById(id).disabled = false)
);
}
updateScroll() {
const scroll = document.getElementById("scroll-bar");
const total =
this.data?.data.multiband.bands[0].samples.length / 2 || 0;
const visible = this.canvas.width / this.zoom;
scroll.max = Math.max(0, total - visible);
scroll.value = this.offset;
scroll.disabled = total {
const renderer = new WaveformRenderer("waveform");
document
.getElementById("zoom-in")
.addEventListener("click", () => renderer.zoomIn());
document
.getElementById("zoom-out")
.addEventListener("click", () => renderer.zoomOut());
document.getElementById("scroll-bar").addEventListener("input", (e) => {
renderer.offset = +e.target.value;
renderer.render();
renderer.updateTime();
});
document
.getElementById("file-input")
.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
document.getElementById("loading").style.display = "inline";
try {
const json = JSON.parse(await file.text());
if (json.type !== "multiband")
throw new Error("Only multiband waveforms supported");
await renderer.loadData(json);
} catch (err) {
alert("Error: " + err.message);
} finally {
document.getElementById("loading").style.display = "none";
}
});
});
< /code>
Mein Python -Skript gibt eine JSON -Datei wie folgt aus: < /p>
{
"type": "multiband",
"sample_rate": 44100,
"samples_per_pixel": 256,
"data": {
"multiband": {
"bands": [
{ "name": "low", "samples": ["base64 encoded min/max pairs"] },
{ "name": "low_mid", "samples": ["base64 encoded min/max pairs"] },
{ "name": "mid", "samples": ["base64 encoded min/max pairs"] },
{ "name": "high", "samples": ["base64 encoded min/max pairs"] }
]
}
}
}
< /code>
Was ich getan habe: < /strong> < /p>
Mein Python -Skript verwendet Filter, um das Audio in Bänder aufzuteilen, dann
berechnet Min /Max -Amplitudien pro Pixel (ähnlich der Wellenformanalyse
codes the Base. Daten in Arrays von min /max -Paaren
und rendern Sie sie auf einer Leinwand. Der obige Code zeichnet eine gefüllte
-Form unter Verwendung von Roh-min /max-Werten. Mein aktuelles Rendering zeigt zu viele Details (gezackte Kanten) und erfasst diesen sauberen, umschlagähnlichen Stil nicht vollständig. Speziell: Welche Techniken sollte ich verwenden, um die min /max -Daten in einem sauberen
-umschlag zu vereinfachen? Ästhetik (z. B. Umriss, füllen
Opazität, Overlay -Typ)? Der vollständige Code ist hier verfügbar. Https://github.com/gabrieljuliao/wavypy
Wenn Sie daran interessiert sind, ihn zu lösen, habe ich einen Ordner namens 3Band_issue im Repo erstellt. Es enthält Testdaten und eine HTML -Datei, die direkt im Browser geöffnet werden kann:
https://github.com/gabrieljuliao/wavypy ... band_issue
Danke, dass jede Leitlinie oder Code. wie:
Wie zeichne ich eine feste 3-Band-Wellenform wie Rekordbox mit Leinwand in JavaScript? ⇐ JavaScript
-
- Similar Topics
- Replies
- Views
- Last post
-
-
Wie kann ich den Wert aktualisieren, den ich mit Jetpack Compose zeichne?
by Anonymous » » in Android - 0 Replies
- 0 Views
-
Last post by Anonymous
-