fix: Wavy not being able to play FLAC, due too jflac

fix-details: Now using FFMpeg for anything audio related
dependency-add: ffmpeg (bytedeco) JavaCV (for ffmpeg; also by bytedeco)
soon-to-deprecate/remove: the old jflac and mp3spi dependencies, which are not needed anymore
add: a higher resolution waterfall diagram
TODO: Ship builds for Linux

notes: The Wavy Player is now much more stable. It still relies on the java sound API, but much less in terms of decoding any codecs. Which is now handled by FFMpeg instead of our own. Playback is still done through the java sound API tho!

---
7\ - rattatwinko 31/05/2026@19:11
This commit is contained in:
2026-05-31 19:11:51 +02:00
parent 372d2f450a
commit 91812a3105
6 changed files with 210 additions and 157 deletions
+15
View File
@@ -120,6 +120,7 @@
<artifactId>languagesupport</artifactId> <artifactId>languagesupport</artifactId>
<version>3.4.1</version> <version>3.4.1</version>
</dependency> </dependency>
<!-- multimedia -->
<dependency> <dependency>
<groupId>com.googlecode.soundlibs</groupId> <groupId>com.googlecode.soundlibs</groupId>
<artifactId>mp3spi</artifactId> <artifactId>mp3spi</artifactId>
@@ -130,6 +131,20 @@
<artifactId>jflac-codec</artifactId> <artifactId>jflac-codec</artifactId>
<version>1.5.2</version> <version>1.5.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>6.1.1-1.5.10</version>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.10</version>
</dependency>
</dependencies> </dependencies>
</project> </project>
+2 -1
View File
@@ -5,7 +5,8 @@ import com.rdesk.kernel.BootProc;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
boolean dbg = true; boolean dbg = false;
if (!dbg) { if (!dbg) {
Login.main(args); Login.main(args);
} else { } else {
@@ -1,7 +1,11 @@
package com.rdesk.applications.entertainment.media; package com.rdesk.applications.entertainment.media;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import java.io.*; import java.nio.ShortBuffer;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.*; import java.util.function.*;
@@ -9,20 +13,18 @@ public class AudioEngine {
public enum State { STOPPED, PLAYING, PAUSED } public enum State { STOPPED, PLAYING, PAUSED }
private volatile State state = State.STOPPED; private volatile State state = State.STOPPED;
private volatile long positionFrames = 0; private volatile long positionFrames = 0;
private volatile long totalFrames = 0; private volatile long totalFrames = 0;
private AudioFormat currentFormat; private AudioFormat currentFormat;
private volatile float volume = 0.8f; // 0..1 private volatile float volume = 0.8f; // 0..1
// eq
private static final int EQ_BANDS = 10; private static final int EQ_BANDS = 10;
private static final float[] EQ_FREQS = { 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 }; private static final float[] EQ_FREQS = { 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 };
private final float[] eqGainDb = new float[EQ_BANDS]; private final float[] eqGainDb = new float[EQ_BANDS];
private final float[][] bqCoeff = new float[EQ_BANDS][5]; private final float[][] bqCoeff = new float[EQ_BANDS][5];
private final float[][][] bqState = new float[EQ_BANDS][2][2]; private final float[][][] bqState = new float[EQ_BANDS][2][2];
// fft
private static final int FFT_SIZE = 4096; private static final int FFT_SIZE = 4096;
private Consumer<float[]> fftListener; private Consumer<float[]> fftListener;
private final float[] fftBuffer = new float[FFT_SIZE]; private final float[] fftBuffer = new float[FFT_SIZE];
@@ -40,17 +42,16 @@ public class AudioEngine {
private double requestedSeekFraction = 0.0; private double requestedSeekFraction = 0.0;
private boolean seekPending = false; private boolean seekPending = false;
// Public API
public AudioEngine() { initEq(); } public AudioEngine() { initEq(); }
public void setOnTrackEnd(Runnable r) { this.onTrackEnd = r; } public void setOnTrackEnd(Runnable r) { this.onTrackEnd = r; }
public void setOnPositionUpdate(Consumer<Long> c) { this.onPositionUpdate = c; } public void setOnPositionUpdate(Consumer<Long> c) { this.onPositionUpdate = c; }
public void setFftListener(Consumer<float[]> c) { this.fftListener = c; } public void setFftListener(Consumer<float[]> c) { this.fftListener = c; }
public State getState() { return state; } public State getState() { return state; }
public long getPositionFrames() { return positionFrames; } public long getPositionFrames() { return positionFrames; }
public long getTotalFrames() { return totalFrames; } public long getTotalFrames() { return totalFrames; }
public AudioFormat getFormat() { return currentFormat; } public AudioFormat getFormat() { return currentFormat; }
public void setVolume(float v) { public void setVolume(float v) {
volume = Math.max(0f, Math.min(1f, v)); volume = Math.max(0f, Math.min(1f, v));
@@ -108,9 +109,9 @@ public class AudioEngine {
recomputeBiquad(band); recomputeBiquad(band);
} }
public float getEqBand(int band) { return eqGainDb[band]; } public float getEqBand(int band) { return eqGainDb[band]; }
public float[] getEqFrequencies() { return EQ_FREQS.clone(); } public float[] getEqFrequencies() { return EQ_FREQS.clone(); }
public static float[] getStaticEqFrequencies() { return EQ_FREQS.clone(); } public static float[] getStaticEqFrequencies() { return EQ_FREQS.clone(); }
public void shutdown() { stop(); } public void shutdown() { stop(); }
@@ -118,60 +119,49 @@ public class AudioEngine {
currentTrack = track; currentTrack = track;
state = State.PLAYING; state = State.PLAYING;
FFmpegFrameGrabber grabber = null;
try { try {
AudioInputStream rawStream = AudioSystem.getAudioInputStream(track.getFilePath().toFile()); grabber = new FFmpegFrameGrabber(track.getFilePath().toFile());
AudioFormat rawFmt = rawStream.getFormat(); grabber.setSampleMode(FrameGrabber.SampleMode.SHORT); // always 16-bit signed interleaved
grabber.setOption("probesize", "5000000");
grabber.setOption("analyzeduration", "5000000");
grabber.start();
AudioFormat pcmFmt = new AudioFormat( final int sampleRate = grabber.getSampleRate();
final int channels = grabber.getAudioChannels();
final int frameSize = channels * 2; // 16-bit to 2 bytes per sample per channel
currentFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED, AudioFormat.Encoding.PCM_SIGNED,
rawFmt.getSampleRate(), sampleRate, 16, channels,
16, frameSize, sampleRate, false);
rawFmt.getChannels(),
rawFmt.getChannels() * 2,
rawFmt.getSampleRate(),
false);
AudioInputStream pcmStream; long durationMicros = grabber.getLengthInTime();
if (rawFmt.getEncoding() != AudioFormat.Encoding.PCM_SIGNED if (durationMicros > 0) {
|| rawFmt.getSampleSizeInBits() != 16) { long ffmpegSec = durationMicros / 1_000_000L;
pcmStream = AudioSystem.getAudioInputStream(pcmFmt, rawStream); totalFrames = (long)(durationMicros / 1_000_000.0 * sampleRate);
} else {
pcmStream = rawStream;
pcmFmt = rawFmt;
}
currentFormat = pcmFmt;
int frameSize = pcmFmt.getFrameSize();
float sampleRate = pcmFmt.getSampleRate();
int channels = pcmFmt.getChannels();
long pcmFrames = pcmStream.getFrameLength();
if (pcmFrames > 0 && pcmFrames != AudioSystem.NOT_SPECIFIED) {
totalFrames = pcmFrames;
if (track.getDurationSeconds() == 0) if (track.getDurationSeconds() == 0)
track.setDurationSeconds((long)(pcmFrames / sampleRate)); track.setDurationSeconds(ffmpegSec);
} else if (track.getDurationSeconds() > 0) {
totalFrames = (long)(track.getDurationSeconds() * sampleRate);
} else { } else {
totalFrames = 0; if (track.getDurationSeconds() > 0)
totalFrames = track.getDurationSeconds() * sampleRate;
else
totalFrames = 0;
} }
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, pcmFmt); DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, currentFormat);
dataLine = (SourceDataLine) AudioSystem.getLine(lineInfo); dataLine = (SourceDataLine) AudioSystem.getLine(lineInfo);
dataLine.open(pcmFmt, (int)(sampleRate * frameSize * 0.1f)); dataLine.open(currentFormat, (int)(sampleRate * frameSize * 0.1f));
dataLine.start(); dataLine.start();
applyVolumeToLine(); // set initial volume applyVolumeToLine();
resetBiquadState(); resetBiquadState();
final int BLOCK = 4096;
byte[] rawBuf = new byte[BLOCK];
float[] samples = new float[BLOCK / 2];
positionFrames = 0; positionFrames = 0;
float[] samples = new float[8192];
while (!stopRequested) { while (!stopRequested) {
// Pause
synchronized (this) { synchronized (this) {
while (pauseRequested && !stopRequested) { while (pauseRequested && !stopRequested) {
try { wait(50); } catch (InterruptedException e) { break; } try { wait(50); } catch (InterruptedException e) { break; }
@@ -179,75 +169,69 @@ public class AudioEngine {
} }
if (stopRequested) break; if (stopRequested) break;
// Seek
if (seekPending) { if (seekPending) {
seekPending = false; seekPending = false;
pcmStream.close();
rawStream.close();
rawStream = AudioSystem.getAudioInputStream(track.getFilePath().toFile());
if (rawFmt.getEncoding() != AudioFormat.Encoding.PCM_SIGNED
|| rawFmt.getSampleSizeInBits() != 16) {
pcmStream = AudioSystem.getAudioInputStream(pcmFmt, rawStream);
} else {
pcmStream = rawStream;
}
if (totalFrames > 0) { if (totalFrames > 0) {
long targetFrame = (long)(requestedSeekFraction * totalFrames); long targetMicros = (long)(requestedSeekFraction
long skip = targetFrame * frameSize; * (durationMicros > 0 ? durationMicros
byte[] skipBuf = new byte[8192]; : track.getDurationSeconds() * 1_000_000L));
long skipped = 0; grabber.setTimestamp(targetMicros, /* keyFrame */ true);
while (skipped < skip && !stopRequested) { positionFrames = (long)((targetMicros / 1_000_000.0) * sampleRate);
int toSkip = (int) Math.min(skipBuf.length, skip - skipped);
int r = pcmStream.read(skipBuf, 0, toSkip);
if (r < 0) break;
skipped += r;
}
positionFrames = targetFrame;
} }
resetBiquadState(); resetBiquadState();
continue; continue;
} }
int bytesRead = pcmStream.read(rawBuf); Frame frame = grabber.grabSamples();
if (bytesRead <= 0) break; if (frame == null || frame.samples == null || frame.samples.length == 0) {
// End of stream
// bytes to float break;
int sampleCount = bytesRead / 2;
for (int i = 0; i < sampleCount; i++) {
int lo = rawBuf[i*2] & 0xFF;
int hi = rawBuf[i*2+1];
samples[i] = (hi << 8 | lo) / 32768.0f;
} }
ShortBuffer sb = (ShortBuffer) frame.samples[0];
sb.rewind();
int sampleCount = sb.remaining();
// Grow float buffer if needed
if (samples.length < sampleCount) samples = new float[sampleCount];
// short to float (-1..1)
for (int i = 0; i < sampleCount; i++)
samples[i] = sb.get() / 32768.0f;
applyEq(samples, sampleCount, channels); applyEq(samples, sampleCount, channels);
feedFft(samples, sampleCount, channels); feedFft(samples, sampleCount, channels);
// other way around, float to bytes // float to short to bytes
byte[] pcmBytes = new byte[sampleCount * 2];
for (int i = 0; i < sampleCount; i++) { for (int i = 0; i < sampleCount; i++) {
float s = Math.max(-1f, Math.min(1f, samples[i])); float s = Math.max(-1f, Math.min(1f, samples[i]));
short sh = (short)(s * 32767f); short sh = (short)(s * 32767f);
rawBuf[i*2] = (byte)(sh & 0xFF); pcmBytes[i*2] = (byte)( sh & 0xFF);
rawBuf[i*2+1] = (byte)((sh >> 8) & 0xFF); pcmBytes[i*2 + 1] = (byte)((sh >> 8) & 0xFF);
} }
dataLine.write(rawBuf, 0, bytesRead); dataLine.write(pcmBytes, 0, pcmBytes.length);
positionFrames += bytesRead / frameSize;
positionFrames += sampleCount / channels;
if (onPositionUpdate != null) onPositionUpdate.accept(positionFrames); if (onPositionUpdate != null) onPositionUpdate.accept(positionFrames);
} }
pcmStream.close();
rawStream.close();
if (!stopRequested) dataLine.drain(); if (!stopRequested) dataLine.drain();
closeLine(); closeLine();
grabber.stop();
grabber.release();
grabber = null;
state = State.STOPPED; state = State.STOPPED;
if (!stopRequested && onTrackEnd != null) onTrackEnd.run(); if (!stopRequested && onTrackEnd != null) onTrackEnd.run();
} catch (UnsupportedAudioFileException e) { } catch (Exception e) {
System.err.println("[Wavy] Unsupported format: " + track.getFilePath()); System.err.println("[Wavy] Playback error for: " + track.getFilePath() + "" + e.getMessage());
state = State.STOPPED;
} catch (LineUnavailableException | IOException e) {
e.printStackTrace();
state = State.STOPPED; state = State.STOPPED;
if (grabber != null) {
try { grabber.stop(); grabber.release(); } catch (Exception ignored) {}
}
} }
} }
@@ -262,15 +246,13 @@ public class AudioEngine {
if (dataLine == null || !dataLine.isOpen()) return; if (dataLine == null || !dataLine.isOpen()) return;
if (dataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) { if (dataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
FloatControl gc = (FloatControl) dataLine.getControl(FloatControl.Type.MASTER_GAIN); FloatControl gc = (FloatControl) dataLine.getControl(FloatControl.Type.MASTER_GAIN);
// map 0..1 linearly to the controls min..max db range
float minDb = gc.getMinimum(); float minDb = gc.getMinimum();
float maxDb = 0f; // 0 dB is the unity gain float maxDb = 0f;
float db = (volume == 0f) ? minDb : minDb + (maxDb - minDb) * volume; float db = (volume == 0f) ? minDb : minDb + (maxDb - minDb) * volume;
gc.setValue(Math.max(minDb, Math.min(maxDb, db))); gc.setValue(Math.max(minDb, Math.min(maxDb, db)));
} }
} }
// EQ
private void initEq() { private void initEq() {
for (int b = 0; b < EQ_BANDS; b++) { eqGainDb[b] = 0f; recomputeBiquad(b); } for (int b = 0; b < EQ_BANDS; b++) { eqGainDb[b] = 0f; recomputeBiquad(b); }
} }
@@ -315,10 +297,11 @@ public class AudioEngine {
for (float[][] b : bqState) for (float[] c : b) { c[0]=0; c[1]=0; } for (float[][] b : bqState) for (float[] c : b) { c[0]=0; c[1]=0; }
} }
// fast fourier transform ( algo: cooley turkey, with the radix 2 dit case ) // FFT (Cooley-Tukey radix-2 DIT — unchanged)
// links / references which may be helpful while reading through the code: // Links / references which may be helpful while reading through the code:
// https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm // https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm
// https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#The_radix-2_DIT_case // https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#The_radix-2_DIT_case
private void feedFft(float[] samples, int count, int channels) { private void feedFft(float[] samples, int count, int channels) {
if (fftListener == null) return; if (fftListener == null) return;
for (int i = 0; i < count; i += channels) { for (int i = 0; i < count; i += channels) {
@@ -341,7 +324,6 @@ public class AudioEngine {
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
re[i] *= 0.5f * (1 - (float)Math.cos(2*Math.PI*i/(n-1))); re[i] *= 0.5f * (1 - (float)Math.cos(2*Math.PI*i/(n-1)));
// bit reversal // bit reversal
int bits = (int)(Math.log(n)/Math.log(2));
for (int j=1, i=0; j<n; j++) { for (int j=1, i=0; j<n; j++) {
int bit = n>>1; int bit = n>>1;
for (; (i&bit)!=0; bit>>=1) i^=bit; for (; (i&bit)!=0; bit>>=1) i^=bit;
@@ -372,4 +354,4 @@ public class AudioEngine {
mag[i] = (float)Math.sqrt(re[i]*re[i]+im[i]*im[i])/n; mag[i] = (float)Math.sqrt(re[i]*re[i]+im[i]*im[i])/n;
return mag; return mag;
} }
} }
@@ -11,6 +11,7 @@ public class EqualizerPanel extends JPanel {
}; };
// TODO: load from JSON in rsys directory (~/rsys/wavy_dat/eq/presets.json) // TODO: load from JSON in rsys directory (~/rsys/wavy_dat/eq/presets.json)
// TODO-TODO: This is actually a really shitty idea? We dont distribute the User rsys folder! (Keep this)
private static final String[][] PRESETS = { private static final String[][] PRESETS = {
{"Flat", "0,0,0,0,0,0,0,0,0,0"}, {"Flat", "0,0,0,0,0,0,0,0,0,0"},
{"Bass Boost", "8,6,4,2,0,0,0,0,0,0"}, {"Bass Boost", "8,6,4,2,0,0,0,0,0,0"},
@@ -98,17 +98,29 @@ public class SpectrogramPanel extends JPanel {
waterfallG.copyArea(0, 1, w, h - 1, 0, -1); waterfallG.copyArea(0, 1, w, h - 1, 0, -1);
float[] bars = computeBars(); computeBars();
int cellW = Math.max(1, w / BAR_COUNT);
for (int b = 0; b < BAR_COUNT; b++) { int fftLen = currentMag.length;
int ci = (int)(bars[b] * (HEAT.length - 1)); float dbMin = -80f, dbMax = 0f;
waterfallG.setColor(HEAT[ci]); double logMin = Math.log10(20.0), logMax = Math.log10(22050.0);
waterfallG.fillRect(b * cellW, h - 1, cellW, 1);
} int[] raster = new int[w];
if (BAR_COUNT * cellW < w) { for (int px = 0; px < w; px++) {
waterfallG.setColor(HEAT[0]); double freq = Math.pow(10, logMin + (logMax - logMin) * px / w);
waterfallG.fillRect(BAR_COUNT * cellW, h - 1, w - BAR_COUNT * cellW, 1); int bin = Math.min(fftLen - 1, (int)(freq / 22050.0 * fftLen));
int binL = Math.max(0, bin - 1);
int binR = Math.min(fftLen-1, bin + 1);
float mag = 0;
for (int k = binL; k <= binR; k++) mag += currentMag[k];
mag /= (binR - binL + 1);
float db = mag > 0 ? 20f * (float)Math.log10(mag) : dbMin;
float val = Math.max(0f, Math.min(1f, (db - dbMin) / (dbMax - dbMin)));
raster[px] = HEAT[(int)(val * (HEAT.length - 1))].getRGB();
} }
waterfallImg.setRGB(0, h - 1, w, 1, raster, 0, w);
} }
private void drawWaterfall(Graphics g, int x, int y, int w, int h) { private void drawWaterfall(Graphics g, int x, int y, int w, int h) {
@@ -193,14 +205,14 @@ public class SpectrogramPanel extends JPanel {
float f = t - seg; float f = t - seg;
Color a = stops[seg], b = stops[seg+1]; Color a = stops[seg], b = stops[seg+1];
map[i] = new Color( map[i] = new Color(
// hehe larp // hehe larp
lerp(a.getRed(), b.getRed(), f), lerp(a.getRed(), b.getRed(), f),
lerp(a.getGreen(), b.getGreen(), f), lerp(a.getGreen(), b.getGreen(), f),
lerp(a.getBlue(), b.getBlue(), f)); lerp(a.getBlue(), b.getBlue(), f));
} }
return map; return map;
} }
private static int lerp(int a, int b, float t) { private static int lerp(int a, int b, float t) {
return Math.max(0, Math.min(255, (int)(a + (b-a)*t))); return Math.max(0, Math.min(255, (int)(a + (b-a)*t)));
} }
} }
@@ -192,17 +192,17 @@ public class WavyPlayerPanel extends JPanel {
playlistModel.clear(); playlistModel.clear();
lblStatus.setText("Scanning"); lblStatus.setText("Scanning");
library.scan( library.scan(
track -> SwingUtilities.invokeLater(() -> { track -> SwingUtilities.invokeLater(() -> {
playlistModel.addElement(track); playlistModel.addElement(track);
lblStatus.setText(playlistModel.size() + " tracks found"); lblStatus.setText(playlistModel.size() + " tracks found");
}), }),
() -> SwingUtilities.invokeLater(() -> { () -> SwingUtilities.invokeLater(() -> {
library.sortByArtistTitle(); library.sortByArtistTitle();
List<Track> sorted = library.getTracks(); List<Track> sorted = library.getTracks();
playlistModel.clear(); playlistModel.clear();
for (Track t : sorted) playlistModel.addElement(t); for (Track t : sorted) playlistModel.addElement(t);
lblStatus.setText(playlistModel.size() + " tracks."); lblStatus.setText(playlistModel.size() + " tracks.");
}) })
); );
} }
@@ -271,7 +271,10 @@ public class WavyPlayerPanel extends JPanel {
if (totalFr > 0 && currentIndex >= 0) { if (totalFr > 0 && currentIndex >= 0) {
Track t = playlistModel.get(currentIndex); Track t = playlistModel.get(currentIndex);
if (t.getDurationSeconds() == 0) t.setDurationSeconds(totalSec); if (t.getDurationSeconds() == 0) {
t.setDurationSeconds(totalSec);
playlistView.repaint();
}
} }
lblTime.setText(fmt(posSec) + " / " + fmt(totalSec)); lblTime.setText(fmt(posSec) + " / " + fmt(totalSec));
@@ -283,30 +286,69 @@ public class WavyPlayerPanel extends JPanel {
return String.format("%d:%02d", sec / 60, sec % 60); return String.format("%d:%02d", sec / 60, sec % 60);
} }
private class TrackCellRenderer extends DefaultListCellRenderer { private class TrackCellRenderer implements ListCellRenderer<Track> {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, private final JPanel panel = new JPanel(new BorderLayout(0, 1));
int index, boolean isSelected, boolean hasFocus) { private final JLabel lblTop = new JLabel();
Track t = (Track) value; private final JLabel lblBot = new JLabel();
String dur = t.getDurationSeconds() > 0 ? t.getDurationString() : "--:--";
String html = String.format( TrackCellRenderer() {
"<html>" + panel.setBorder(BorderFactory.createEmptyBorder(3, 6, 3, 6));
"<b>%s</b>" + lblTop.setFont(lblTop.getFont().deriveFont(Font.BOLD, 12f));
" &nbsp; " + lblBot.setFont(lblBot.getFont().deriveFont(Font.PLAIN, 10f));
"<small>%s - %s</small>" + panel.add(lblTop, BorderLayout.NORTH);
" &nbsp; " + panel.add(lblBot, BorderLayout.SOUTH);
"<font color=gray>[%s] %s</font>" +
"</html>",
esc(t.getDisplayTitle()), esc(t.getArtist()), esc(t.getAlbum()),
t.getFormat(), dur);
JLabel lbl = (JLabel) super.getListCellRendererComponent(list, html, index, isSelected, hasFocus);
if (index == currentIndex)
lbl.setBorder(BorderFactory.createMatteBorder(0, 3, 0, 0,
UIManager.getColor("List.selectionBackground")));
return lbl;
} }
private String esc(String s) {
return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;"); @Override
public Component getListCellRendererComponent(JList<? extends Track> list,
Track t, int index, boolean isSelected, boolean hasFocus) {
Color bg = isSelected
? UIManager.getColor("List.selectionBackground")
: (index % 2 == 0
? list.getBackground()
: blend(list.getBackground(), UIManager.getColor("Panel.background"), 0.5f));
Color fg = isSelected
? UIManager.getColor("List.selectionForeground")
: list.getForeground();
Color fgDim = isSelected ? fg : blend(fg, bg, 0.45f);
panel.setBackground(bg);
lblTop.setForeground(fg);
lblBot.setForeground(fgDim);
if (index == currentIndex) {
panel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 3, 0, 0,
UIManager.getColor("List.selectionBackground")),
BorderFactory.createEmptyBorder(3, 4, 3, 6)));
} else {
panel.setBorder(BorderFactory.createEmptyBorder(3, 7, 3, 6));
}
lblTop.setText(t.getDisplayTitle());
String dur = t.getDurationSeconds() > 0 ? t.getDurationString() : "--:--";
String artist = t.getArtist(), album = t.getAlbum();
String sub = artist + " \u2013 " + album
+ " [" + t.getFormat() + "]"
+ " " + dur;
lblBot.setText(sub);
return panel;
}
private Color blend(Color a, Color b, float t) {
if (a == null) a = Color.WHITE;
if (b == null) b = Color.LIGHT_GRAY;
int r = (int)(a.getRed() + (b.getRed() - a.getRed()) * t);
int g = (int)(a.getGreen() + (b.getGreen() - a.getGreen()) * t);
int bl= (int)(a.getBlue() + (b.getBlue() - a.getBlue()) * t);
return new Color(
Math.max(0, Math.min(255, r)),
Math.max(0, Math.min(255, g)),
Math.max(0, Math.min(255, bl)));
} }
} }
} }