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:
@@ -120,6 +120,7 @@
|
||||
<artifactId>languagesupport</artifactId>
|
||||
<version>3.4.1</version>
|
||||
</dependency>
|
||||
<!-- multimedia -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.soundlibs</groupId>
|
||||
<artifactId>mp3spi</artifactId>
|
||||
@@ -130,6 +131,20 @@
|
||||
<artifactId>jflac-codec</artifactId>
|
||||
<version>1.5.2</version>
|
||||
</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>
|
||||
|
||||
</project>
|
||||
@@ -5,7 +5,8 @@ import com.rdesk.kernel.BootProc;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
boolean dbg = true;
|
||||
boolean dbg = false;
|
||||
|
||||
if (!dbg) {
|
||||
Login.main(args);
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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 java.io.*;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.*;
|
||||
|
||||
@@ -9,20 +13,18 @@ public class AudioEngine {
|
||||
|
||||
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 totalFrames = 0;
|
||||
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 float[] EQ_FREQS = { 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 };
|
||||
private final float[] eqGainDb = new float[EQ_BANDS];
|
||||
private final float[][] bqCoeff = new float[EQ_BANDS][5];
|
||||
private final float[][][] bqState = new float[EQ_BANDS][2][2];
|
||||
|
||||
// fft
|
||||
private static final int FFT_SIZE = 4096;
|
||||
private Consumer<float[]> fftListener;
|
||||
private final float[] fftBuffer = new float[FFT_SIZE];
|
||||
@@ -40,17 +42,16 @@ public class AudioEngine {
|
||||
private double requestedSeekFraction = 0.0;
|
||||
private boolean seekPending = false;
|
||||
|
||||
// Public API
|
||||
public AudioEngine() { initEq(); }
|
||||
|
||||
public void setOnTrackEnd(Runnable r) { this.onTrackEnd = r; }
|
||||
public void setOnPositionUpdate(Consumer<Long> c) { this.onPositionUpdate = c; }
|
||||
public void setFftListener(Consumer<float[]> c) { this.fftListener = c; }
|
||||
public void setOnTrackEnd(Runnable r) { this.onTrackEnd = r; }
|
||||
public void setOnPositionUpdate(Consumer<Long> c) { this.onPositionUpdate = c; }
|
||||
public void setFftListener(Consumer<float[]> c) { this.fftListener = c; }
|
||||
|
||||
public State getState() { return state; }
|
||||
public long getPositionFrames() { return positionFrames; }
|
||||
public long getTotalFrames() { return totalFrames; }
|
||||
public AudioFormat getFormat() { return currentFormat; }
|
||||
public State getState() { return state; }
|
||||
public long getPositionFrames() { return positionFrames; }
|
||||
public long getTotalFrames() { return totalFrames; }
|
||||
public AudioFormat getFormat() { return currentFormat; }
|
||||
|
||||
public void setVolume(float v) {
|
||||
volume = Math.max(0f, Math.min(1f, v));
|
||||
@@ -108,9 +109,9 @@ public class AudioEngine {
|
||||
recomputeBiquad(band);
|
||||
}
|
||||
|
||||
public float getEqBand(int band) { return eqGainDb[band]; }
|
||||
public float[] getEqFrequencies() { return EQ_FREQS.clone(); }
|
||||
public static float[] getStaticEqFrequencies() { return EQ_FREQS.clone(); }
|
||||
public float getEqBand(int band) { return eqGainDb[band]; }
|
||||
public float[] getEqFrequencies() { return EQ_FREQS.clone(); }
|
||||
public static float[] getStaticEqFrequencies() { return EQ_FREQS.clone(); }
|
||||
|
||||
public void shutdown() { stop(); }
|
||||
|
||||
@@ -118,60 +119,49 @@ public class AudioEngine {
|
||||
currentTrack = track;
|
||||
state = State.PLAYING;
|
||||
|
||||
FFmpegFrameGrabber grabber = null;
|
||||
try {
|
||||
AudioInputStream rawStream = AudioSystem.getAudioInputStream(track.getFilePath().toFile());
|
||||
AudioFormat rawFmt = rawStream.getFormat();
|
||||
grabber = new FFmpegFrameGrabber(track.getFilePath().toFile());
|
||||
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,
|
||||
rawFmt.getSampleRate(),
|
||||
16,
|
||||
rawFmt.getChannels(),
|
||||
rawFmt.getChannels() * 2,
|
||||
rawFmt.getSampleRate(),
|
||||
false);
|
||||
sampleRate, 16, channels,
|
||||
frameSize, sampleRate, false);
|
||||
|
||||
AudioInputStream pcmStream;
|
||||
if (rawFmt.getEncoding() != AudioFormat.Encoding.PCM_SIGNED
|
||||
|| rawFmt.getSampleSizeInBits() != 16) {
|
||||
pcmStream = AudioSystem.getAudioInputStream(pcmFmt, rawStream);
|
||||
} 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;
|
||||
long durationMicros = grabber.getLengthInTime();
|
||||
if (durationMicros > 0) {
|
||||
long ffmpegSec = durationMicros / 1_000_000L;
|
||||
totalFrames = (long)(durationMicros / 1_000_000.0 * sampleRate);
|
||||
if (track.getDurationSeconds() == 0)
|
||||
track.setDurationSeconds((long)(pcmFrames / sampleRate));
|
||||
} else if (track.getDurationSeconds() > 0) {
|
||||
totalFrames = (long)(track.getDurationSeconds() * sampleRate);
|
||||
track.setDurationSeconds(ffmpegSec);
|
||||
} 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.open(pcmFmt, (int)(sampleRate * frameSize * 0.1f));
|
||||
dataLine.open(currentFormat, (int)(sampleRate * frameSize * 0.1f));
|
||||
dataLine.start();
|
||||
applyVolumeToLine(); // set initial volume
|
||||
applyVolumeToLine();
|
||||
|
||||
resetBiquadState();
|
||||
|
||||
final int BLOCK = 4096;
|
||||
byte[] rawBuf = new byte[BLOCK];
|
||||
float[] samples = new float[BLOCK / 2];
|
||||
|
||||
positionFrames = 0;
|
||||
|
||||
float[] samples = new float[8192];
|
||||
|
||||
while (!stopRequested) {
|
||||
// Pause
|
||||
|
||||
synchronized (this) {
|
||||
while (pauseRequested && !stopRequested) {
|
||||
try { wait(50); } catch (InterruptedException e) { break; }
|
||||
@@ -179,75 +169,69 @@ public class AudioEngine {
|
||||
}
|
||||
if (stopRequested) break;
|
||||
|
||||
// Seek
|
||||
if (seekPending) {
|
||||
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) {
|
||||
long targetFrame = (long)(requestedSeekFraction * totalFrames);
|
||||
long skip = targetFrame * frameSize;
|
||||
byte[] skipBuf = new byte[8192];
|
||||
long skipped = 0;
|
||||
while (skipped < skip && !stopRequested) {
|
||||
int toSkip = (int) Math.min(skipBuf.length, skip - skipped);
|
||||
int r = pcmStream.read(skipBuf, 0, toSkip);
|
||||
if (r < 0) break;
|
||||
skipped += r;
|
||||
}
|
||||
positionFrames = targetFrame;
|
||||
long targetMicros = (long)(requestedSeekFraction
|
||||
* (durationMicros > 0 ? durationMicros
|
||||
: track.getDurationSeconds() * 1_000_000L));
|
||||
grabber.setTimestamp(targetMicros, /* keyFrame */ true);
|
||||
positionFrames = (long)((targetMicros / 1_000_000.0) * sampleRate);
|
||||
}
|
||||
resetBiquadState();
|
||||
continue;
|
||||
}
|
||||
|
||||
int bytesRead = pcmStream.read(rawBuf);
|
||||
if (bytesRead <= 0) break;
|
||||
|
||||
// bytes to float
|
||||
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;
|
||||
Frame frame = grabber.grabSamples();
|
||||
if (frame == null || frame.samples == null || frame.samples.length == 0) {
|
||||
// End of stream
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
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++) {
|
||||
float s = Math.max(-1f, Math.min(1f, samples[i]));
|
||||
short sh = (short)(s * 32767f);
|
||||
rawBuf[i*2] = (byte)(sh & 0xFF);
|
||||
rawBuf[i*2+1] = (byte)((sh >> 8) & 0xFF);
|
||||
pcmBytes[i*2] = (byte)( sh & 0xFF);
|
||||
pcmBytes[i*2 + 1] = (byte)((sh >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
dataLine.write(rawBuf, 0, bytesRead);
|
||||
positionFrames += bytesRead / frameSize;
|
||||
dataLine.write(pcmBytes, 0, pcmBytes.length);
|
||||
|
||||
positionFrames += sampleCount / channels;
|
||||
if (onPositionUpdate != null) onPositionUpdate.accept(positionFrames);
|
||||
}
|
||||
|
||||
pcmStream.close();
|
||||
rawStream.close();
|
||||
if (!stopRequested) dataLine.drain();
|
||||
closeLine();
|
||||
grabber.stop();
|
||||
grabber.release();
|
||||
grabber = null;
|
||||
|
||||
state = State.STOPPED;
|
||||
if (!stopRequested && onTrackEnd != null) onTrackEnd.run();
|
||||
|
||||
} catch (UnsupportedAudioFileException e) {
|
||||
System.err.println("[Wavy] Unsupported format: " + track.getFilePath());
|
||||
state = State.STOPPED;
|
||||
} catch (LineUnavailableException | IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
System.err.println("[Wavy] Playback error for: " + track.getFilePath() + " — " + e.getMessage());
|
||||
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.isControlSupported(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 maxDb = 0f; // 0 dB is the unity gain
|
||||
float db = (volume == 0f) ? minDb : minDb + (maxDb - minDb) * volume;
|
||||
float maxDb = 0f;
|
||||
float db = (volume == 0f) ? minDb : minDb + (maxDb - minDb) * volume;
|
||||
gc.setValue(Math.max(minDb, Math.min(maxDb, db)));
|
||||
}
|
||||
}
|
||||
|
||||
// EQ
|
||||
private void initEq() {
|
||||
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; }
|
||||
}
|
||||
|
||||
// fast fourier transform ( algo: cooley turkey, with the radix 2 dit case )
|
||||
// links / references which may be helpful while reading through the code:
|
||||
// FFT (Cooley-Tukey radix-2 DIT — unchanged)
|
||||
// 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#The_radix-2_DIT_case
|
||||
|
||||
private void feedFft(float[] samples, int count, int channels) {
|
||||
if (fftListener == null) return;
|
||||
for (int i = 0; i < count; i += channels) {
|
||||
@@ -341,7 +324,6 @@ public class AudioEngine {
|
||||
for (int i = 0; i < n; i++)
|
||||
re[i] *= 0.5f * (1 - (float)Math.cos(2*Math.PI*i/(n-1)));
|
||||
// bit reversal
|
||||
int bits = (int)(Math.log(n)/Math.log(2));
|
||||
for (int j=1, i=0; j<n; j++) {
|
||||
int bit = n>>1;
|
||||
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;
|
||||
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-TODO: This is actually a really shitty idea? We dont distribute the User rsys folder! (Keep this)
|
||||
private static final String[][] PRESETS = {
|
||||
{"Flat", "0,0,0,0,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);
|
||||
|
||||
float[] bars = computeBars();
|
||||
int cellW = Math.max(1, w / BAR_COUNT);
|
||||
for (int b = 0; b < BAR_COUNT; b++) {
|
||||
int ci = (int)(bars[b] * (HEAT.length - 1));
|
||||
waterfallG.setColor(HEAT[ci]);
|
||||
waterfallG.fillRect(b * cellW, h - 1, cellW, 1);
|
||||
}
|
||||
if (BAR_COUNT * cellW < w) {
|
||||
waterfallG.setColor(HEAT[0]);
|
||||
waterfallG.fillRect(BAR_COUNT * cellW, h - 1, w - BAR_COUNT * cellW, 1);
|
||||
computeBars();
|
||||
|
||||
int fftLen = currentMag.length;
|
||||
float dbMin = -80f, dbMax = 0f;
|
||||
double logMin = Math.log10(20.0), logMax = Math.log10(22050.0);
|
||||
|
||||
int[] raster = new int[w];
|
||||
for (int px = 0; px < w; px++) {
|
||||
double freq = Math.pow(10, logMin + (logMax - logMin) * px / w);
|
||||
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) {
|
||||
@@ -193,14 +205,14 @@ public class SpectrogramPanel extends JPanel {
|
||||
float f = t - seg;
|
||||
Color a = stops[seg], b = stops[seg+1];
|
||||
map[i] = new Color(
|
||||
// hehe larp
|
||||
lerp(a.getRed(), b.getRed(), f),
|
||||
lerp(a.getGreen(), b.getGreen(), f),
|
||||
lerp(a.getBlue(), b.getBlue(), f));
|
||||
// hehe larp
|
||||
lerp(a.getRed(), b.getRed(), f),
|
||||
lerp(a.getGreen(), b.getGreen(), f),
|
||||
lerp(a.getBlue(), b.getBlue(), f));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
private static int lerp(int a, int b, float t) {
|
||||
return Math.max(0, Math.min(255, (int)(a + (b-a)*t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,17 +192,17 @@ public class WavyPlayerPanel extends JPanel {
|
||||
playlistModel.clear();
|
||||
lblStatus.setText("Scanning");
|
||||
library.scan(
|
||||
track -> SwingUtilities.invokeLater(() -> {
|
||||
playlistModel.addElement(track);
|
||||
lblStatus.setText(playlistModel.size() + " tracks found");
|
||||
}),
|
||||
() -> SwingUtilities.invokeLater(() -> {
|
||||
library.sortByArtistTitle();
|
||||
List<Track> sorted = library.getTracks();
|
||||
playlistModel.clear();
|
||||
for (Track t : sorted) playlistModel.addElement(t);
|
||||
lblStatus.setText(playlistModel.size() + " tracks.");
|
||||
})
|
||||
track -> SwingUtilities.invokeLater(() -> {
|
||||
playlistModel.addElement(track);
|
||||
lblStatus.setText(playlistModel.size() + " tracks found");
|
||||
}),
|
||||
() -> SwingUtilities.invokeLater(() -> {
|
||||
library.sortByArtistTitle();
|
||||
List<Track> sorted = library.getTracks();
|
||||
playlistModel.clear();
|
||||
for (Track t : sorted) playlistModel.addElement(t);
|
||||
lblStatus.setText(playlistModel.size() + " tracks.");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -271,7 +271,10 @@ public class WavyPlayerPanel extends JPanel {
|
||||
|
||||
if (totalFr > 0 && currentIndex >= 0) {
|
||||
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));
|
||||
@@ -283,30 +286,69 @@ public class WavyPlayerPanel extends JPanel {
|
||||
return String.format("%d:%02d", sec / 60, sec % 60);
|
||||
}
|
||||
|
||||
private class TrackCellRenderer extends DefaultListCellRenderer {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value,
|
||||
int index, boolean isSelected, boolean hasFocus) {
|
||||
Track t = (Track) value;
|
||||
String dur = t.getDurationSeconds() > 0 ? t.getDurationString() : "--:--";
|
||||
String html = String.format(
|
||||
"<html>" +
|
||||
"<b>%s</b>" +
|
||||
" " +
|
||||
"<small>%s - %s</small>" +
|
||||
" " +
|
||||
"<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 class TrackCellRenderer implements ListCellRenderer<Track> {
|
||||
|
||||
private final JPanel panel = new JPanel(new BorderLayout(0, 1));
|
||||
private final JLabel lblTop = new JLabel();
|
||||
private final JLabel lblBot = new JLabel();
|
||||
|
||||
TrackCellRenderer() {
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(3, 6, 3, 6));
|
||||
lblTop.setFont(lblTop.getFont().deriveFont(Font.BOLD, 12f));
|
||||
lblBot.setFont(lblBot.getFont().deriveFont(Font.PLAIN, 10f));
|
||||
panel.add(lblTop, BorderLayout.NORTH);
|
||||
panel.add(lblBot, BorderLayout.SOUTH);
|
||||
}
|
||||
private String esc(String s) {
|
||||
return s.replace("&","&").replace("<","<").replace(">",">");
|
||||
|
||||
@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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user