Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91812a3105 |
@@ -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>
|
||||||
@@ -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.*;
|
||||||
|
|
||||||
@@ -15,14 +19,12 @@ public class AudioEngine {
|
|||||||
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,7 +42,6 @@ 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; }
|
||||||
@@ -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 {
|
||||||
|
if (track.getDurationSeconds() > 0)
|
||||||
|
totalFrames = track.getDurationSeconds() * sampleRate;
|
||||||
|
else
|
||||||
totalFrames = 0;
|
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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Component getListCellRendererComponent(JList<?> list, Object value,
|
public Component getListCellRendererComponent(JList<? extends Track> list,
|
||||||
int index, boolean isSelected, boolean hasFocus) {
|
Track t, int index, boolean isSelected, boolean hasFocus) {
|
||||||
Track t = (Track) value;
|
|
||||||
|
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 dur = t.getDurationSeconds() > 0 ? t.getDurationString() : "--:--";
|
||||||
String html = String.format(
|
String artist = t.getArtist(), album = t.getAlbum();
|
||||||
"<html>" +
|
String sub = artist + " \u2013 " + album
|
||||||
"<b>%s</b>" +
|
+ " [" + t.getFormat() + "]"
|
||||||
" " +
|
+ " " + dur;
|
||||||
"<small>%s - %s</small>" +
|
lblBot.setText(sub);
|
||||||
" " +
|
|
||||||
"<font color=gray>[%s] %s</font>" +
|
return panel;
|
||||||
"</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("&","&").replace("<","<").replace(">",">");
|
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