diff --git a/pom.xml b/pom.xml
index 46199da..7cb3a2a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -120,6 +120,7 @@
languagesupport
3.4.1
+
com.googlecode.soundlibs
mp3spi
@@ -130,6 +131,20 @@
jflac-codec
1.5.2
+
+
+ org.bytedeco
+ ffmpeg
+ 6.1.1-1.5.10
+ windows-x86_64
+
+
+
+ org.bytedeco
+ javacv
+ 1.5.10
+
+
\ No newline at end of file
diff --git a/src/main/java/com/rdesk/Main.java b/src/main/java/com/rdesk/Main.java
index e97cb64..14a3559 100644
--- a/src/main/java/com/rdesk/Main.java
+++ b/src/main/java/com/rdesk/Main.java
@@ -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 {
diff --git a/src/main/java/com/rdesk/applications/entertainment/media/AudioEngine.java b/src/main/java/com/rdesk/applications/entertainment/media/AudioEngine.java
index 60e2762..c337091 100644
--- a/src/main/java/com/rdesk/applications/entertainment/media/AudioEngine.java
+++ b/src/main/java/com/rdesk/applications/entertainment/media/AudioEngine.java
@@ -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 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 c) { this.onPositionUpdate = c; }
- public void setFftListener(Consumer c) { this.fftListener = c; }
+ public void setOnTrackEnd(Runnable r) { this.onTrackEnd = r; }
+ public void setOnPositionUpdate(Consumer c) { this.onPositionUpdate = c; }
+ public void setFftListener(Consumer 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>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;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/rdesk/applications/entertainment/media/EqualizerPanel.java b/src/main/java/com/rdesk/applications/entertainment/media/EqualizerPanel.java
index 750d27b..c5bef54 100644
--- a/src/main/java/com/rdesk/applications/entertainment/media/EqualizerPanel.java
+++ b/src/main/java/com/rdesk/applications/entertainment/media/EqualizerPanel.java
@@ -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"},
diff --git a/src/main/java/com/rdesk/applications/entertainment/media/SpectrogramPanel.java b/src/main/java/com/rdesk/applications/entertainment/media/SpectrogramPanel.java
index b28ea89..3a1f5d2 100644
--- a/src/main/java/com/rdesk/applications/entertainment/media/SpectrogramPanel.java
+++ b/src/main/java/com/rdesk/applications/entertainment/media/SpectrogramPanel.java
@@ -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)));
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/rdesk/applications/entertainment/media/WavyPlayerPanel.java b/src/main/java/com/rdesk/applications/entertainment/media/WavyPlayerPanel.java
index 447bfb5..68c060e 100644
--- a/src/main/java/com/rdesk/applications/entertainment/media/WavyPlayerPanel.java
+++ b/src/main/java/com/rdesk/applications/entertainment/media/WavyPlayerPanel.java
@@ -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