Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91812a3105 |
@@ -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