Fallback Tidal with search and ISRC matching

This commit is contained in:
afkarxyz
2025-11-26 07:32:26 +07:00
parent f346fbb6ba
commit 4241a591aa
7 changed files with 1197 additions and 178 deletions
+139 -125
View File
@@ -27,15 +27,15 @@ export function SpectrumVisualization({
const height = canvas.height;
// Calculate margins for labels
const marginLeft = 80;
const marginRight = 80;
const marginTop = 20;
const marginBottom = 50;
const marginLeft = 70; // More space for Frequency label
const marginRight = 70; // Space for color bar
const marginTop = 30; // More space at top
const marginBottom = 65; // More space at bottom for Time label
const plotWidth = width - marginLeft - marginRight;
const plotHeight = height - marginTop - marginBottom;
// Black background like Spek
// Black background
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
@@ -51,9 +51,11 @@ export function SpectrumVisualization({
plotHeight,
spectrumData
);
drawGrid(ctx, marginLeft, marginTop, plotWidth, plotHeight, nyquistFreq);
}
// Draw axes, labels, and color bar
drawAxesAndLabels(ctx, marginLeft, marginTop, plotWidth, plotHeight, nyquistFreq, duration, sampleRate);
drawColorBar(ctx, marginLeft + plotWidth + 15, marginTop, 20, plotHeight);
}, [sampleRate, bitsPerSample, duration, spectrumData]);
const drawRealSpectrum = (
@@ -70,40 +72,45 @@ export function SpectrumVisualization({
const freqBins = timeSlices[0].magnitudes.length;
const nyquistFreq = spectrum.max_freq;
// Find min/max dB values
let minDB = 0;
let maxDB = -120;
let maxDB = -200;
timeSlices.forEach((slice) => {
slice.magnitudes.forEach((db) => {
if (db > maxDB) maxDB = db;
if (db < minDB) minDB = db;
if (db < minDB && db > -200) minDB = db;
});
});
// Clamp range for better visualization
minDB = Math.max(minDB, maxDB - 90); // 90dB dynamic range
const dbRange = maxDB - minDB;
const sliceWidth = Math.ceil(width / timeSlices.length);
for (let t = 0; t < timeSlices.length; t++) {
const slice = timeSlices[t];
const xPos = x + (t / timeSlices.length) * width;
const sliceWidth = Math.max(1, width / timeSlices.length);
for (let f = 0; f < freqBins && f < slice.magnitudes.length; f++) {
const db = slice.magnitudes[f];
// Linear frequency scale like Spek
// Linear frequency scale
const freq = (f / freqBins) * nyquistFreq;
const freqRatio = freq / nyquistFreq;
const yPos = y + height - (freqRatio * height);
// Calculate next frequency bin position
// Calculate bin height
const nextFreq = ((f + 1) / freqBins) * nyquistFreq;
const nextFreqRatio = nextFreq / nyquistFreq;
const nextYPos = y + height - (nextFreqRatio * height);
const binHeight = Math.max(1, Math.abs(yPos - nextYPos) + 1);
const intensity = (db - minDB) / dbRange;
// Normalize intensity
const intensity = Math.max(0, Math.min(1, (db - minDB) / dbRange));
const color = getSpekColor(intensity);
ctx.fillStyle = color;
@@ -112,161 +119,168 @@ export function SpectrumVisualization({
}
};
// Vibrant color scheme like Spek - NGEJERENG!
const getSpekColor = (intensity: number): string => {
// Enhanced color scheme - better than Spek
if (intensity < 0.10) {
// Deep black to dark blue
const t = intensity / 0.10;
return `rgb(0, 0, ${Math.floor(t * 100)})`;
} else if (intensity < 0.25) {
// Dark blue to bright blue
const t = (intensity - 0.10) / 0.15;
return `rgb(0, ${Math.floor(t * 50)}, ${Math.floor(100 + t * 155)})`;
if (intensity < 0.08) {
// Black to deep blue
const t = intensity / 0.08;
return `rgb(0, 0, ${Math.floor(t * 80)})`;
} else if (intensity < 0.18) {
// Deep blue to bright blue
const t = (intensity - 0.08) / 0.10;
return `rgb(${Math.floor(t * 50)}, ${Math.floor(t * 30)}, ${Math.floor(80 + t * 175)})`;
} else if (intensity < 0.28) {
// Blue to magenta/purple
const t = (intensity - 0.18) / 0.10;
return `rgb(${Math.floor(50 + t * 150)}, ${Math.floor(30 - t * 30)}, ${Math.floor(255 - t * 55)})`;
} else if (intensity < 0.40) {
// Blue to cyan
const t = (intensity - 0.25) / 0.15;
return `rgb(0, ${Math.floor(50 + t * 205)}, 255)`;
} else if (intensity < 0.55) {
// Cyan to green
const t = (intensity - 0.40) / 0.15;
return `rgb(0, 255, ${Math.floor(255 - t * 200)})`;
} else if (intensity < 0.70) {
// Green to yellow
const t = (intensity - 0.55) / 0.15;
return `rgb(${Math.floor(t * 255)}, 255, ${Math.floor(55 - t * 55)})`;
} else if (intensity < 0.85) {
// Yellow to orange
const t = (intensity - 0.70) / 0.15;
return `rgb(255, ${Math.floor(255 - t * 100)}, 0)`;
// Magenta to bright red
const t = (intensity - 0.28) / 0.12;
return `rgb(${Math.floor(200 + t * 55)}, 0, ${Math.floor(200 - t * 200)})`;
} else if (intensity < 0.52) {
// Red to orange-red
const t = (intensity - 0.40) / 0.12;
return `rgb(255, ${Math.floor(t * 100)}, 0)`;
} else if (intensity < 0.65) {
// Orange-red to bright orange
const t = (intensity - 0.52) / 0.13;
return `rgb(255, ${Math.floor(100 + t * 80)}, 0)`;
} else if (intensity < 0.78) {
// Orange to yellow-orange
const t = (intensity - 0.65) / 0.13;
return `rgb(255, ${Math.floor(180 + t * 55)}, ${Math.floor(t * 30)})`;
} else if (intensity < 0.90) {
// Yellow-orange to bright yellow
const t = (intensity - 0.78) / 0.12;
return `rgb(255, ${Math.floor(235 + t * 20)}, ${Math.floor(30 + t * 100)})`;
} else {
// Orange to red
const t = (intensity - 0.85) / 0.15;
return `rgb(255, ${Math.floor(155 - t * 155)}, ${Math.floor(t * 30)})`;
// Yellow to white (hottest)
const t = (intensity - 0.90) / 0.10;
return `rgb(255, 255, ${Math.floor(130 + t * 125)})`;
}
};
const drawGrid = (
const drawAxesAndLabels = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
nyquistFreq: number
nyquistFreq: number,
duration: number,
sampleRate: number
) => {
// Enhanced grid lines
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)";
ctx.lineWidth = 1;
// Frequency labels on Y-axis
ctx.fillStyle = "#CCCCCC";
ctx.font = "12px Arial";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
// Dynamic frequency grid lines based on Nyquist frequency
const generateFreqLines = (maxFreq: number): number[] => {
if (maxFreq <= 24000) {
// Standard 44.1/48 kHz (Nyquist ~22/24 kHz)
return [1000, 2000, 5000, 10000, 15000, 20000];
} else if (maxFreq <= 48000) {
// 88.2/96 kHz (Nyquist ~44/48 kHz)
return [5000, 10000, 20000, 30000, 40000];
} else if (maxFreq <= 96000) {
// 176.4/192 kHz (Nyquist ~88/96 kHz)
return [10000, 20000, 40000, 60000, 80000];
} else {
// 352.8/384 kHz and higher (Nyquist ~176/192+ kHz)
return [20000, 40000, 80000, 120000, 160000];
}
};
const freqLines = generateFreqLines(nyquistFreq);
// Generate frequency labels based on Nyquist
const freqLabels = generateFreqLabels(nyquistFreq);
freqLines.forEach(freq => {
if (freq <= nyquistFreq) {
const freqRatio = freq / nyquistFreq;
const yPos = y + height - (freqRatio * height);
ctx.beginPath();
ctx.moveTo(x, yPos);
ctx.lineTo(x + width, yPos);
ctx.stroke();
}
});
// Vertical time grid lines
for (let i = 1; i < 10; i++) {
const xPos = x + (i / 10) * width;
ctx.beginPath();
ctx.moveTo(xPos, y);
ctx.lineTo(xPos, y + height);
ctx.stroke();
}
ctx.fillStyle = "rgba(220, 220, 220, 0.9)";
ctx.font = "11px Arial";
// Frequency labels - dynamic formatting
freqLines.forEach(freq => {
freqLabels.forEach(freq => {
if (freq <= nyquistFreq) {
const freqRatio = freq / nyquistFreq;
const yPos = y + height - (freqRatio * height);
const label = freq >= 1000 ? `${freq / 1000}k` : `${freq}`;
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(label, x - 6, yPos);
ctx.fillText(label, x - 8, yPos);
}
});
// Time labels
// "0" at bottom
ctx.fillText("0", x - 8, y + height);
// Time labels on X-axis
ctx.textAlign = "center";
ctx.textBaseline = "top";
for (let i = 0; i <= 10; i++) {
const timePos = x + (i / 10) * width;
const timeValue = (i / 10) * duration;
if (i % 2 === 0) {
ctx.fillText(timeValue.toFixed(1), timePos, y + height + 5);
}
const timeStep = getTimeStep(duration);
for (let t = 0; t <= duration; t += timeStep) {
const xPos = x + (t / duration) * width;
ctx.fillText(`${Math.round(t)}s`, xPos, y + height + 8);
}
// Axis titles
ctx.fillStyle = "#FFFFFF";
ctx.font = "bold 13px Arial";
ctx.shadowColor = "rgba(0, 0, 0, 0.8)";
ctx.shadowBlur = 4;
ctx.font = "13px Arial";
// Y-axis title: "Frequency (Hz)"
ctx.save();
ctx.translate(8, y + height / 2);
ctx.translate(12, y + height / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = "center";
ctx.fillText("Frequency (kHz)", 0, 0);
ctx.fillText("Frequency (Hz)", 0, 0);
ctx.restore();
// X-axis title: "Time (seconds)"
ctx.textAlign = "center";
ctx.fillText("Time (s)", x + width / 2, y + height + 26);
ctx.shadowBlur = 0;
ctx.fillText("Time (seconds)", x + width / 2, y + height + 35);
const boxGradient = ctx.createLinearGradient(x + width - 200, y + 5, x + width - 200, y + 68);
boxGradient.addColorStop(0, "rgba(0, 0, 0, 0.85)");
boxGradient.addColorStop(1, "rgba(0, 0, 0, 0.7)");
ctx.fillStyle = boxGradient;
ctx.fillRect(x + width - 200, y + 5, 190, 63);
ctx.strokeStyle = "rgba(255, 255, 255, 0.15)";
ctx.lineWidth = 1.5;
ctx.strokeRect(x + width - 200, y + 5, 190, 63);
// Sample rate info in top right
ctx.textAlign = "right";
ctx.fillStyle = "#CCCCCC";
ctx.font = "12px Arial";
ctx.fillText(`Sample Rate: ${sampleRate} Hz`, x + width - 5, y - 3);
};
const generateFreqLabels = (nyquistFreq: number): number[] => {
if (nyquistFreq <= 24000) {
return [2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000, 22000];
} else if (nyquistFreq <= 48000) {
return [5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000];
} else if (nyquistFreq <= 96000) {
return [10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000];
} else {
return [20000, 40000, 60000, 80000, 100000, 120000, 140000, 160000, 180000];
}
};
const getTimeStep = (duration: number): number => {
// Always use 30s intervals like the reference image
if (duration <= 60) return 15;
if (duration <= 120) return 30;
if (duration <= 300) return 30;
if (duration <= 600) return 60;
return 60;
};
const drawColorBar = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number
) => {
// Draw gradient color bar
for (let i = 0; i < height; i++) {
const intensity = 1 - (i / height); // Top is high, bottom is low
const color = getSpekColor(intensity);
ctx.fillStyle = color;
ctx.fillRect(x, y + i, width, 1);
}
// Border around color bar
ctx.strokeStyle = "#666666";
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
// Labels
ctx.fillStyle = "#FFFFFF";
ctx.font = "600 11px Arial";
ctx.font = "11px Arial";
ctx.textAlign = "left";
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
ctx.shadowBlur = 2;
ctx.fillText(`Sample Rate: ${(sampleRate / 1000).toFixed(1)} kHz`, x + width - 190, y + 20);
ctx.fillText(`Bit Depth: ${bitsPerSample}-bit`, x + width - 190, y + 36);
ctx.fillText(`Nyquist: ${(nyquistFreq / 1000).toFixed(1)} kHz`, x + width - 190, y + 52);
ctx.shadowBlur = 0;
ctx.textBaseline = "middle";
ctx.fillText("High", x + width + 5, y + 10);
ctx.fillText("Low", x + width + 5, y + height - 10);
};
return (
<div className="border border-white/10 rounded-lg overflow-hidden bg-black shadow-xl">
<canvas
ref={canvasRef}
width={1600}
height={800}
width={1200}
height={600}
className="w-full h-auto"
style={{ imageRendering: "auto" }}
/>