5 Commits

Author SHA1 Message Date
rattatwinko 372d2f450a add : Wavy the Music player!
dependency-add: mp3spi, jflac (WAVE can be played directly by the JVM)
fix : the "This will end all running Tasks!" in Exit.java, cause we have "sessions" now

TODO: Implement EQ Preset loading from JSON for Wavy ; Implement playback of more codecs. Maybe Use FFMpeg.

---
7\ - rattatwinko 30/05/26@23:10
2026-05-30 23:10:35 +02:00
rattatwinko f160b07bf4 experimenting with sessions and stuff like that.
Changes:
+ Login Panel
+ Some Kernel Booting stuff
2026-05-27 07:21:30 +02:00
rattatwinko 307062eb35 refactor some kernel stuff, keyboard loading for windows not supported anymore. too much of a hassle.
add:
kernel error/info logging
ability to load the keymap from json (onboot) (also store it)
remove:
windows keyboard settings (too shitty to maintain)
2026-05-23 22:25:47 +02:00
rattatwinko 72d5df2c70 add some rsys utils (mostly just linux modified) 2026-05-21 17:05:59 +02:00
rattatwinko 19f9f1f3a6 xkcd application ; fatjar packaging 2026-05-17 22:10:25 +02:00
26 changed files with 2985 additions and 96 deletions
+39 -1
View File
@@ -16,6 +16,7 @@
<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
@@ -24,6 +25,34 @@
<mainClass>com.rdesk.Main</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.rdesk.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -91,7 +120,16 @@
<artifactId>languagesupport</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>mp3spi</artifactId>
<version>1.9.5.4</version>
</dependency>
<dependency>
<groupId>org.jflac</groupId>
<artifactId>jflac-codec</artifactId>
<version>1.5.2</version>
</dependency>
</dependencies>
</project>
+6 -86
View File
@@ -1,95 +1,15 @@
package com.rdesk;
import com.rdesk.applications.productivity.Calculator;
import com.rdesk.applications.productivity.browsing.TabbedBrowser;
import com.rdesk.applications.productivity.code.CodeDesk;
import com.rdesk.applications.system.Exit;
import com.rdesk.applications.productivity.Notes;
import com.rdesk.applications.system.TaskManager;
import com.rdesk.applications.system.filehaj.FileExplorerApp;
import com.rdesk.kernel.*;
import com.rdesk.kernel.launcher.AppLauncher;
import javax.swing.*;
import com.rdesk.applications.system.login.Login;
import com.rdesk.kernel.BootProc;
public class Main {
private static final boolean RUN_IN_FS = true;
private static final boolean USE_SYSDEFAULT_LAF = false;
private static final boolean USE_FLATLAF = false;
public static void main(String[] args) {
SwingUtilities.invokeLater(Main::boot);
}
private static void boot() {
Kernel kernel = Kernel.get();
kernel.boot();
JFrame frame = new JFrame("rDesk");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
kernel.shutdown();
}
});
if (USE_FLATLAF) {
try {
UIManager.setLookAndFeel(
"com.formdev.flatlaf.themes.FlatMacLightLaf"
);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(
null,
ex.getMessage() + "@USE_FLATLAF"
);
}
}
if (USE_SYSDEFAULT_LAF) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(
null,
ex.getMessage() + "@USE_SYSDEFAULT_LAF"
);
}
}
if (RUN_IN_FS) {
frame.setUndecorated(true);
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
boolean dbg = true;
if (!dbg) {
Login.main(args);
} else {
frame.setSize(1200, 800);
frame.setLocationRelativeTo(null);
BootProc.boot();
}
JDesktopPane desktop = new JDesktopPane();
kernel.windows().attachDesktop(desktop);
AppLauncher launcher = new AppLauncher(kernel);
registerSystemApps(launcher);
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, launcher, desktop);
split.setDividerSize(0);
split.setEnabled(false);
frame.setContentPane(split);
frame.setVisible(true);
}
private static void registerSystemApps(AppLauncher launcher) {
launcher.addApp(new Notes(), "Productivity");
launcher.addApp(new TabbedBrowser(), "Productivity");
launcher.addApp(new CodeDesk(), "Productivity");
launcher.addApp(new Calculator(), "Productivity");
launcher.addApp(new Exit(), "System");
launcher.addApp(new TaskManager(), "System");
launcher.addApp(new FileExplorerApp(), "System");
}
}
@@ -0,0 +1,27 @@
package com.rdesk.applications.entertainment.comic;
public class ComicData {
private int num;
private String title;
private String img;
private String alt;
// Gson requires default constructor
public ComicData() {}
public int getNum() {
return num;
}
public String getTitle() {
return title;
}
public String getImg() {
return img;
}
public String getAlt() {
return alt;
}
}
@@ -0,0 +1,58 @@
package com.rdesk.applications.entertainment.comic;
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ComicFetcher {
private static final String BASE_URL = "https://xkcd.com/";
private static final String INFO_URL = BASE_URL + "info.0.json";
private static final String COMIC_URL_TEMPLATE = BASE_URL + "%d/info.0.json";
private final HttpClient httpClient;
private final Gson gson;
public ComicFetcher() {
this.httpClient = HttpClient.newHttpClient();
this.gson = new Gson();
}
public int fetchLatestComicNumber() throws IOException, InterruptedException {
String json = fetchJson(INFO_URL);
LatestInfo info = gson.fromJson(json, LatestInfo.class);
if (info == null || info.num == 0) {
throw new IOException("api response error");
}
return info.num;
}
public ComicData fetchComicData(int num) throws IOException, InterruptedException {
String url = String.format(COMIC_URL_TEMPLATE, num);
String json = fetchJson(url);
ComicData comic = gson.fromJson(json, ComicData.class);
if (comic == null || comic.getImg() == null) {
throw new IOException("error , idk bro #" + num);
}
return comic;
}
private String fetchJson(String urlString) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(urlString))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + " for " + urlString);
}
return response.body();
}
private static class LatestInfo {
int num;
}
}
@@ -0,0 +1,54 @@
package com.rdesk.applications.entertainment.comic;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
import javax.swing.*;
import java.awt.*;
public class Xkcd implements DesktopApp {
@Override
public String getId() {
return "xkcd";
}
@Override
public String getName() {
return "XKCD";
}
@Override
public JInternalFrame createWindow(Kernel kernel) {
JInternalFrame frame = new JInternalFrame(
"XKCD",
true, true, true, true
);
frame.setSize(800, 700);
frame.setLayout(new BorderLayout(5, 5));
JPanel topPanel = new JPanel(new BorderLayout(5, 0));
JButton refreshButton = new JButton("Random Comic");
JLabel titleLabel = new JLabel(" ", SwingConstants.CENTER);
titleLabel.setFont(new Font("SansSerif", Font.BOLD, 14));
topPanel.add(refreshButton, BorderLayout.WEST);
topPanel.add(titleLabel, BorderLayout.CENTER);
frame.add(topPanel, BorderLayout.NORTH);
ZoomableImagePanel imagePanel = new ZoomableImagePanel();
frame.add(imagePanel, BorderLayout.CENTER);
JTextArea altTextArea = new JTextArea(4, 40);
altTextArea.setEditable(false);
altTextArea.setLineWrap(true);
altTextArea.setWrapStyleWord(true);
altTextArea.setFont(new Font("SansSerif", Font.PLAIN, 12));
JScrollPane altScrollPane = new JScrollPane(altTextArea);
frame.add(altScrollPane, BorderLayout.SOUTH);
new XkcdController(refreshButton, titleLabel, altTextArea, imagePanel).loadRandomComic();
frame.setVisible(true);
return frame;
}
}
@@ -0,0 +1,97 @@
package com.rdesk.applications.entertainment.comic;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.util.concurrent.ExecutionException;
public class XkcdController {
private final JButton refreshButton;
private final JLabel titleLabel;
private final JTextArea altTextArea;
private final ZoomableImagePanel imagePanel;
private final ComicFetcher fetcher;
public XkcdController(JButton refreshButton, JLabel titleLabel,
JTextArea altTextArea, ZoomableImagePanel imagePanel) {
this.refreshButton = refreshButton;
this.titleLabel = titleLabel;
this.altTextArea = altTextArea;
this.imagePanel = imagePanel;
this.fetcher = new ComicFetcher();
refreshButton.addActionListener(e -> loadRandomComic());
}
public void loadRandomComic() {
setLoadingState(true);
SwingWorker<ComicData, Void> worker = new SwingWorker<>() {
@Override
protected ComicData doInBackground() throws Exception {
int latest = fetcher.fetchLatestComicNumber();
int randomNum = (int) (Math.random() * latest) + 1;
return fetcher.fetchComicData(randomNum);
}
@Override
protected void done() {
setLoadingState(false);
try {
ComicData comic = get();
if (comic != null) {
displayComic(comic);
} else {
showError("cant load comic");
}
} catch (InterruptedException | ExecutionException e) {
showError("Error: " + e.getCause().getMessage());
}
}
};
worker.execute();
}
private void displayComic(ComicData comic) {
titleLabel.setText(String.format("#%d: %s", comic.getNum(), comic.getTitle()));
altTextArea.setText(comic.getAlt());
SwingWorker<BufferedImage, Void> imageWorker = new SwingWorker<>() {
@Override
protected BufferedImage doInBackground() throws Exception {
URL url = new URL(comic.getImg());
return javax.imageio.ImageIO.read(url);
}
@Override
protected void done() {
try {
BufferedImage img = get();
imagePanel.setImage(img);
} catch (Exception e) {
showError("failed to load image: " + e.getMessage());
imagePanel.clearImage();
}
}
};
imageWorker.execute();
}
private void setLoadingState(boolean loading) {
SwingUtilities.invokeLater(() -> {
refreshButton.setEnabled(!loading);
if (loading) {
titleLabel.setText("fetching image");
altTextArea.setText("");
imagePanel.clearImage();
}
});
}
private void showError(String message) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
titleLabel.setText(" ");
altTextArea.setText("");
});
}
}
@@ -0,0 +1,136 @@
package com.rdesk.applications.entertainment.comic;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
public class ZoomableImagePanel extends JPanel {
private BufferedImage originalImage;
private double zoom = 1.0;
private Point dragStart;
private Point viewportStart;
private final JViewport viewport;
private static final double ZOOM_STEP = 0.1;
private static final double MIN_ZOOM = 0.2;
private static final double MAX_ZOOM = 3.0;
public ZoomableImagePanel() {
setLayout(new BorderLayout());
JLabel imageLabel = new JLabel("", SwingConstants.CENTER);
JScrollPane scrollPane = new JScrollPane(imageLabel);
add(scrollPane, BorderLayout.CENTER);
this.viewport = scrollPane.getViewport();
imageLabel.addMouseWheelListener(this::onMouseWheel);
DragHandler dragHandler = new DragHandler(imageLabel);
imageLabel.addMouseListener(dragHandler);
imageLabel.addMouseMotionListener(dragHandler);
}
public void setImage(BufferedImage image) {
this.originalImage = image;
this.zoom = 1.0;
updateDisplay();
SwingUtilities.invokeLater(() -> viewport.setViewPosition(new Point(0, 0)));
}
public void clearImage() {
this.originalImage = null;
removeImageFromLabel();
}
private void updateDisplay() {
if (originalImage == null) {
removeImageFromLabel();
return;
}
JLabel label = getImageLabel();
int newWidth = (int) (originalImage.getWidth() * zoom);
int newHeight = (int) (originalImage.getHeight() * zoom);
Image scaled = originalImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
label.setIcon(new ImageIcon(scaled));
label.setText("");
label.setPreferredSize(new Dimension(newWidth, newHeight));
revalidate();
repaint();
}
private void onMouseWheel(MouseWheelEvent e) {
if (originalImage == null) return;
double oldZoom = zoom;
double newZoom = zoom + (e.getWheelRotation() < 0 ? ZOOM_STEP : -ZOOM_STEP);
newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));
if (newZoom == zoom) return;
Point mouseOnScreen = e.getPoint();
Point viewPos = viewport.getViewPosition();
double xRatio = (viewPos.x + mouseOnScreen.x) / (double) viewport.getExtentSize().width;
double yRatio = (viewPos.y + mouseOnScreen.y) / (double) viewport.getExtentSize().height;
zoom = newZoom;
updateDisplay();
Dimension contentSize = getImageLabel().getPreferredSize();
Dimension viewSize = viewport.getExtentSize();
int newX = (int) (contentSize.width * xRatio - viewSize.width / 2.0);
int newY = (int) (contentSize.height * yRatio - viewSize.height / 2.0);
newX = Math.max(0, Math.min(newX, contentSize.width - viewSize.width));
newY = Math.max(0, Math.min(newY, contentSize.height - viewSize.height));
viewport.setViewPosition(new Point(newX, newY));
}
private JLabel getImageLabel() {
return (JLabel) ((JScrollPane) getComponent(0)).getViewport().getView();
}
private void removeImageFromLabel() {
JLabel label = getImageLabel();
label.setIcon(null);
label.setText("Loading");
label.setPreferredSize(null);
revalidate();
}
private class DragHandler extends MouseAdapter {
private final JLabel label;
DragHandler(JLabel label) {
this.label = label;
}
@Override
public void mousePressed(MouseEvent e) {
if (!SwingUtilities.isLeftMouseButton(e)) return;
dragStart = e.getPoint();
viewportStart = viewport.getViewPosition();
label.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
@Override
public void mouseReleased(MouseEvent e) {
if (!SwingUtilities.isLeftMouseButton(e)) return;
dragStart = null;
viewportStart = null;
label.setCursor(Cursor.getDefaultCursor());
}
@Override
public void mouseDragged(MouseEvent e) {
if (dragStart == null || viewportStart == null) return;
int deltaX = dragStart.x - e.getX();
int deltaY = dragStart.y - e.getY();
Point newPos = new Point(viewportStart.x + deltaX, viewportStart.y + deltaY);
Dimension contentSize = label.getPreferredSize();
Dimension viewSize = viewport.getExtentSize();
newPos.x = Math.max(0, Math.min(newPos.x, contentSize.width - viewSize.width));
newPos.y = Math.max(0, Math.min(newPos.y, contentSize.height - viewSize.height));
viewport.setViewPosition(newPos);
}
}
}
@@ -0,0 +1,375 @@
package com.rdesk.applications.entertainment.media;
import javax.sound.sampled.*;
import java.io.*;
import java.util.concurrent.*;
import java.util.function.*;
public class AudioEngine {
public enum State { STOPPED, PLAYING, PAUSED }
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
// 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];
private int fftPos = 0;
private ExecutorService playerThread;
private volatile boolean stopRequested = false;
private volatile boolean pauseRequested = false;
private Runnable onTrackEnd;
private Consumer<Long> onPositionUpdate;
private SourceDataLine dataLine;
private Track currentTrack = null;
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 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));
applyVolumeToLine();
}
public void play(Track track) {
stop();
playerThread = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "wavy-audio");
t.setDaemon(true);
t.setPriority(Thread.MAX_PRIORITY - 1);
return t;
});
stopRequested = false;
pauseRequested = false;
playerThread.submit(() -> playbackLoop(track));
}
public void togglePause() {
if (state == State.PLAYING) {
pauseRequested = true;
state = State.PAUSED;
} else if (state == State.PAUSED) {
pauseRequested = false;
state = State.PLAYING;
synchronized (this) { notifyAll(); }
}
}
public void stop() {
stopRequested = true;
pauseRequested = false;
synchronized (this) { notifyAll(); }
if (playerThread != null) {
playerThread.shutdownNow();
try { playerThread.awaitTermination(2, TimeUnit.SECONDS); }
catch (InterruptedException ignored) {}
playerThread = null;
}
closeLine();
state = State.STOPPED;
positionFrames = 0;
}
public void seekFraction(double fraction) {
if (currentTrack == null) return;
requestedSeekFraction = Math.max(0.0, Math.min(1.0, fraction));
seekPending = true;
}
public void setEqBand(int band, float db) {
if (band < 0 || band >= EQ_BANDS) return;
eqGainDb[band] = db;
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 void shutdown() { stop(); }
private void playbackLoop(Track track) {
currentTrack = track;
state = State.PLAYING;
try {
AudioInputStream rawStream = AudioSystem.getAudioInputStream(track.getFilePath().toFile());
AudioFormat rawFmt = rawStream.getFormat();
AudioFormat pcmFmt = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
rawFmt.getSampleRate(),
16,
rawFmt.getChannels(),
rawFmt.getChannels() * 2,
rawFmt.getSampleRate(),
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;
if (track.getDurationSeconds() == 0)
track.setDurationSeconds((long)(pcmFrames / sampleRate));
} else if (track.getDurationSeconds() > 0) {
totalFrames = (long)(track.getDurationSeconds() * sampleRate);
} else {
totalFrames = 0;
}
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, pcmFmt);
dataLine = (SourceDataLine) AudioSystem.getLine(lineInfo);
dataLine.open(pcmFmt, (int)(sampleRate * frameSize * 0.1f));
dataLine.start();
applyVolumeToLine(); // set initial volume
resetBiquadState();
final int BLOCK = 4096;
byte[] rawBuf = new byte[BLOCK];
float[] samples = new float[BLOCK / 2];
positionFrames = 0;
while (!stopRequested) {
// Pause
synchronized (this) {
while (pauseRequested && !stopRequested) {
try { wait(50); } catch (InterruptedException e) { break; }
}
}
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;
}
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;
}
applyEq(samples, sampleCount, channels);
feedFft(samples, sampleCount, channels);
// other way around, float to bytes
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);
}
dataLine.write(rawBuf, 0, bytesRead);
positionFrames += bytesRead / frameSize;
if (onPositionUpdate != null) onPositionUpdate.accept(positionFrames);
}
pcmStream.close();
rawStream.close();
if (!stopRequested) dataLine.drain();
closeLine();
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();
state = State.STOPPED;
}
}
private void closeLine() {
if (dataLine != null) {
if (dataLine.isOpen()) { dataLine.stop(); dataLine.close(); }
dataLine = null;
}
}
private void applyVolumeToLine() {
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;
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); }
}
// math heavy shit, this code may burn your eyes out.
// it works, but i dont like how it works
private void recomputeBiquad(int band) {
float sampleRate = (currentFormat != null) ? currentFormat.getSampleRate() : 44100f;
float f0 = EQ_FREQS[band];
float dBgain= eqGainDb[band];
float Q = 1.41421356f;
float A = (float) Math.pow(10.0, dBgain / 40.0);
double w0 = 2.0 * Math.PI * f0 / sampleRate;
double cosW = Math.cos(w0);
double sinW = Math.sin(w0);
double alpha= sinW / (2.0 * Q);
double b0 = 1 + alpha*A, b1 = -2*cosW, b2 = 1 - alpha*A;
double a0 = 1 + alpha/A, a1 = -2*cosW, a2 = 1 - alpha/A;
bqCoeff[band][0] = (float)(b0/a0); bqCoeff[band][1] = (float)(b1/a0);
bqCoeff[band][2] = (float)(b2/a0); bqCoeff[band][3] = (float)(a1/a0);
bqCoeff[band][4] = (float)(a2/a0);
}
private void applyEq(float[] samples, int count, int channels) {
for (int band = 0; band < EQ_BANDS; band++) {
if (Math.abs(eqGainDb[band]) < 0.01f) continue;
float b0=bqCoeff[band][0], b1=bqCoeff[band][1], b2=bqCoeff[band][2];
float a1=bqCoeff[band][3], a2=bqCoeff[band][4];
for (int i = 0; i < count; i++) {
int ch = i % channels;
float x = samples[i];
float z1 = bqState[band][ch][0], z2 = bqState[band][ch][1];
float y = b0*x + z1;
bqState[band][ch][0] = b1*x - a1*y + z2;
bqState[band][ch][1] = b2*x - a2*y;
samples[i] = y;
}
}
}
private void resetBiquadState() {
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:
// 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) {
float mono = 0;
for (int c = 0; c < channels; c++) mono += samples[i+c];
mono /= channels;
fftBuffer[fftPos++] = mono;
if (fftPos == FFT_SIZE) {
fftListener.accept(computeFFT(fftBuffer));
System.arraycopy(fftBuffer, FFT_SIZE/2, fftBuffer, 0, FFT_SIZE/2);
fftPos = FFT_SIZE/2;
}
}
}
static float[] computeFFT(float[] x) {
int n = x.length;
float[] re = x.clone(), im = new float[n];
// hann window
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;
i^=bit;
if (j<i) {
float t=re[j]; re[j]=re[i]; re[i]=t;
t=im[j]; im[j]=im[i]; im[i]=t;
}
}
// cooley turkey algorithm
for (int len=2; len<=n; len<<=1) {
double angle=-2*Math.PI/len;
float wRe=(float)Math.cos(angle), wIm=(float)Math.sin(angle);
for (int i=0; i<n; i+=len) {
float cRe=1, cIm=0;
for (int j=0; j<len/2; j++) {
float uRe=re[i+j], uIm=im[i+j];
float vRe=re[i+j+len/2]*cRe - im[i+j+len/2]*cIm;
float vIm=re[i+j+len/2]*cIm + im[i+j+len/2]*cRe;
re[i+j]=uRe+vRe; im[i+j]=uIm+vIm;
re[i+j+len/2]=uRe-vRe; im[i+j+len/2]=uIm-vIm;
float nRe=cRe*wRe-cIm*wIm; cIm=cRe*wIm+cIm*wRe; cRe=nRe;
}
}
}
float[] mag = new float[n/2];
for (int i=0; i<n/2; i++)
mag[i] = (float)Math.sqrt(re[i]*re[i]+im[i]*im[i])/n;
return mag;
}
}
@@ -0,0 +1,54 @@
package com.rdesk.applications.entertainment.media;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
public class CoverArtPanel extends JPanel {
private BufferedImage coverImage = null;
private String trackTitle = "No track";
private String artistName = "";
public CoverArtPanel() {
setBorder(BorderFactory.createLoweredBevelBorder());
setPreferredSize(new Dimension(130, 130));
}
public void setTrack(Track track) {
coverImage = track != null ? track.getCoverArt() : null;
trackTitle = track != null ? track.getDisplayTitle() : "No track";
artistName = track != null ? track.getArtist() : "";
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int w = getWidth(), h = getHeight();
if (coverImage != null) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
double sx = (double) w / coverImage.getWidth();
double sy = (double) h / coverImage.getHeight();
double scale = Math.max(sx, sy);
int dw = (int)(coverImage.getWidth() * scale);
int dh = (int)(coverImage.getHeight() * scale);
int dx = (w - dw) / 2, dy = (h - dh) / 2;
g2.drawImage(coverImage, dx, dy, dw, dh, null);
} else {
g.setColor(UIManager.getColor("Panel.background"));
g.fillRect(0, 0, w, h);
g.setColor(UIManager.getColor("Label.foreground"));
g.setFont(g.getFont().deriveFont(Font.BOLD, 10f));
FontMetrics fm = g.getFontMetrics();
String line1 = trackTitle.length() > 20 ? trackTitle.substring(0,18)+"" : trackTitle;
String line2 = artistName.length() > 20 ? artistName.substring(0,18)+"" : artistName;
g.drawString(line1, (w - fm.stringWidth(line1))/2, h/2 - fm.getHeight()/2 + fm.getAscent());
g.setFont(g.getFont().deriveFont(Font.PLAIN, 9f));
fm = g.getFontMetrics();
g.drawString(line2, (w - fm.stringWidth(line2))/2, h/2 + fm.getAscent() + 2);
}
}
}
@@ -0,0 +1,170 @@
package com.rdesk.applications.entertainment.media;
import javax.swing.*;
import java.awt.*;
public class EqualizerPanel extends JPanel {
private static final int BANDS = 10;
private static final String[] FREQ_LABELS = {
"32Hz","64Hz","125Hz","250Hz","500Hz","1kHz","2kHz","4kHz","8kHz","16kHz"
};
// TODO: load from JSON in rsys directory (~/rsys/wavy_dat/eq/presets.json)
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"},
{"Treble Boost", "0,0,0,0,0,0,2,4,6,8"},
{"V-Shape", "6,4,2,-1,-3,-3,-1,2,4,6"},
{"Rock", "4,3,1,0,-1,0,2,3,3,2"},
{"Jazz", "3,2,1,2,0,-1,-1,0,1,2"},
{"Classical", "0,0,0,0,0,0,-2,-3,-3,-2"},
{"Electronic", "5,3,0,-1,-3,0,1,2,4,5"},
};
private final AudioEngine engine;
private final JSlider[] sliders = new JSlider[BANDS];
private final JLabel[] dbLabels = new JLabel[BANDS];
private final FreqCurvePanel curve;
public EqualizerPanel(AudioEngine engine) {
this.engine = engine;
setLayout(new BorderLayout(4, 4));
setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
curve = new FreqCurvePanel();
curve.setPreferredSize(new Dimension(0, 80));
add(buildTopBar(), BorderLayout.NORTH);
add(curve, BorderLayout.CENTER);
add(buildSliders(), BorderLayout.SOUTH);
}
private JPanel buildTopBar() {
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
p.add(new JLabel("Preset:"));
JComboBox<String> presetBox = new JComboBox<>();
for (String[] pr : PRESETS) presetBox.addItem(pr[0]);
presetBox.addActionListener(e -> applyPreset(presetBox.getSelectedIndex()));
p.add(presetBox);
JButton reset = new JButton("Reset");
reset.addActionListener(e -> applyPreset(0));
p.add(reset);
return p;
}
private JPanel buildSliders() {
JPanel p = new JPanel(new GridLayout(1, BANDS, 2, 0));
for (int i = 0; i < BANDS; i++) {
final int band = i;
JPanel col = new JPanel();
col.setLayout(new BoxLayout(col, BoxLayout.Y_AXIS));
JLabel dbLbl = new JLabel("0 dB", SwingConstants.CENTER);
dbLbl.setFont(dbLbl.getFont().deriveFont(9f));
dbLbl.setAlignmentX(0.5f);
dbLabels[i] = dbLbl;
JSlider sl = new JSlider(JSlider.VERTICAL, -120, 120, 0);
sl.setPaintTicks(true);
sl.setMajorTickSpacing(60);
sl.setMinorTickSpacing(20);
sl.setSnapToTicks(false);
sl.setAlignmentX(0.5f);
sliders[i] = sl;
sl.addChangeListener(e -> {
float db = sl.getValue() / 10.0f;
engine.setEqBand(band, db);
dbLabels[band].setText(String.format("%+.0f dB", db));
curve.repaint();
});
JLabel freqLbl = new JLabel(FREQ_LABELS[i], SwingConstants.CENTER);
freqLbl.setFont(freqLbl.getFont().deriveFont(8f));
freqLbl.setAlignmentX(0.5f);
col.add(dbLbl);
col.add(sl);
col.add(freqLbl);
p.add(col);
}
return p;
}
private void applyPreset(int idx) {
if (idx < 0 || idx >= PRESETS.length) return;
String[] vals = PRESETS[idx][1].split(",");
for (int i = 0; i < BANDS && i < vals.length; i++) {
try {
float db = Float.parseFloat(vals[i].trim());
sliders[i].setValue(Math.round(db * 10));
} catch (NumberFormatException ignored) {}
}
}
private class FreqCurvePanel extends JPanel {
FreqCurvePanel() {
setBorder(BorderFactory.createTitledBorder("Frequency Response"));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth(), h = getHeight();
Insets ins = getInsets();
int x0 = ins.left + 2, y0 = ins.top + 2;
int cw = w - ins.left - ins.right - 4;
int ch = h - ins.top - ins.bottom - 4;
if (cw <= 0 || ch <= 0) { g2.dispose(); return; }
float midY = y0 + ch / 2.0f;
g2.setColor(Color.LIGHT_GRAY);
g2.drawLine(x0, (int)midY, x0 + cw, (int)midY);
float logMin = (float)Math.log10(20.0);
float logMax = (float)Math.log10(20000.0);
int pts = cw;
int[] xp = new int[pts], yp = new int[pts];
for (int i = 0; i < pts; i++) {
float f = (float)Math.pow(10, logMin + (logMax - logMin) * i / pts);
double totalDb = 0;
for (int b = 0; b < BANDS; b++) {
float db = sliders[b].getValue() / 10.0f;
if (Math.abs(db) < 0.01f) continue;
float f0 = AudioEngine.getStaticEqFrequencies()[b];
float Q = 1.41421356f;
double ratio = f / f0;
double lrSq = (ratio - 1.0/ratio) * (ratio - 1.0/ratio);
double denom = 1 + lrSq / (Q * Q * (ratio + 1.0/ratio) * (ratio + 1.0/ratio));
totalDb += db * denom;
}
float py = midY - (float)(totalDb / 12.0 * ch / 2 * 0.9);
xp[i] = x0 + i;
yp[i] = Math.max(y0, Math.min(y0 + ch, (int)py));
}
g2.setColor(getForeground());
g2.setStroke(new BasicStroke(1.5f));
for (int i = 1; i < pts; i++) {
g2.drawLine(xp[i-1], yp[i-1], xp[i], yp[i]);
}
// Band markers ; i like blue (like Boris Johnson)!
g2.setColor(Color.BLUE);
for (int b = 0; b < BANDS; b++) {
float f0 = AudioEngine.getStaticEqFrequencies()[b];
float db = sliders[b].getValue() / 10.0f;
float nx = x0 + (float)((Math.log10(f0) - logMin) / (logMax - logMin) * cw);
float ny = midY - (db / 12.0f * ch / 2 * 0.9f);
ny = Math.max(y0, Math.min(y0 + ch, ny));
g2.fillOval((int)nx - 3, (int)ny - 3, 6, 6);
}
g2.dispose();
}
}
}
@@ -0,0 +1,296 @@
package com.rdesk.applications.entertainment.media;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class MetadataReader {
public static void populate(Track track) {
String fmt = track.getFormat();
try {
switch (fmt) {
case "MP3" -> readID3v2(track);
case "FLAC" -> readFLAC(track);
case "WAV" -> readWAV(track);
}
} catch (Exception ignored) {/* do jackshit */}
}
// MP3 / ID3v2
private static void readID3v2(Track track) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(track.getFilePath().toFile(), "r")) {
byte[] header = new byte[10];
if (raf.read(header) < 10) return;
if (header[0] != 'I' || header[1] != 'D' || header[2] != '3') return;
int majorVer = header[3] & 0xFF;
if (majorVer < 3) return;
// syncsafe size
long tagSize = ((header[6] & 0x7F) << 21)
| ((header[7] & 0x7F) << 14)
| ((header[8] & 0x7F) << 7)
| (header[9] & 0x7F);
byte[] tagData = new byte[(int) tagSize];
raf.readFully(tagData);
int pos = 0;
while (pos + 10 <= tagData.length) {
String frameId = new String(tagData, pos, 4, StandardCharsets.ISO_8859_1);
if (frameId.charAt(0) == '\0') break; // padding
int frameSize;
if (majorVer == 4) {
frameSize = ((tagData[pos+4] & 0x7F) << 21)
| ((tagData[pos+5] & 0x7F) << 14)
| ((tagData[pos+6] & 0x7F) << 7)
| (tagData[pos+7] & 0x7F);
} else {
frameSize = ((tagData[pos+4] & 0xFF) << 24)
| ((tagData[pos+5] & 0xFF) << 16)
| ((tagData[pos+6] & 0xFF) << 8)
| (tagData[pos+7] & 0xFF);
}
pos += 10; // skip frame header
if (frameSize <= 0 || pos + frameSize > tagData.length) break;
byte[] frameData = new byte[frameSize];
System.arraycopy(tagData, pos, frameData, 0, frameSize);
pos += frameSize;
switch (frameId) {
case "TIT2" -> track.setTitle(decodeTextFrame(frameData));
case "TPE1" -> track.setArtist(decodeTextFrame(frameData));
case "TALB" -> track.setAlbum(decodeTextFrame(frameData));
case "TDRC", "TYER" -> {
String yr = decodeTextFrame(frameData);
if (yr.length() >= 4) {
try { track.setYear(Integer.parseInt(yr.substring(0, 4))); } catch (NumberFormatException ignored) {}
}
}
case "TRCK" -> {
String trck = decodeTextFrame(frameData);
int slash = trck.indexOf('/');
try { track.setTrackNumber(Integer.parseInt(slash >= 0 ? trck.substring(0, slash) : trck)); }
catch (NumberFormatException ignored) {}
}
case "APIC" -> track.setCoverArt(decodeAPIC(frameData));
}
}
}
}
private static String decodeTextFrame(byte[] data) {
if (data.length == 0) return "";
int enc = data[0] & 0xFF;
int start = 1;
// Strip BOM for UTF-16
// vv terrible code vv
return switch (enc) {
case 0 -> new String(data, start, data.length - start, StandardCharsets.ISO_8859_1).trim();
case 1 -> {
// UTF-16 with BOM
if (data.length - start >= 2 && data[start] == (byte)0xFF && data[start+1] == (byte)0xFE)
yield new String(data, start+2, data.length - start - 2, StandardCharsets.UTF_16LE).trim();
if (data.length - start >= 2 && data[start] == (byte)0xFE && data[start+1] == (byte)0xFF)
yield new String(data, start+2, data.length - start - 2, StandardCharsets.UTF_16BE).trim();
yield new String(data, start, data.length - start, StandardCharsets.UTF_16).trim();
}
case 2 -> new String(data, start, data.length - start, StandardCharsets.UTF_16BE).trim();
case 3 -> new String(data, start, data.length - start, StandardCharsets.UTF_8).trim();
default -> new String(data, start, data.length - start, StandardCharsets.ISO_8859_1).trim();
};
}
private static BufferedImage decodeAPIC(byte[] data) {
if (data.length < 4) return null;
int enc = data[0] & 0xFF;
int pos = 1;
while (pos < data.length && data[pos] != 0) pos++;
pos++;
if (pos >= data.length) return null;
pos++;
if (enc == 1 || enc == 2) {
while (pos + 1 < data.length && !(data[pos] == 0 && data[pos+1] == 0)) pos++;
pos += 2;
} else {
while (pos < data.length && data[pos] != 0) pos++;
pos++;
}
if (pos >= data.length) return null;
try {
return ImageIO.read(new ByteArrayInputStream(data, pos, data.length - pos));
} catch (IOException e) { return null; }
}
// FLAC
private static void readFLAC(Track track) throws IOException {
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(track.getFilePath().toFile())))) {
byte[] magic = new byte[4];
dis.readFully(magic);
if (magic[0] != 'f' || magic[1] != 'L' || magic[2] != 'a' || magic[3] != 'C') return;
boolean last = false;
while (!last) {
int blockHeader = dis.readUnsignedByte();
last = (blockHeader & 0x80) != 0;
int blockType = blockHeader & 0x7F;
int blockLen = (dis.readUnsignedByte() << 16)
| (dis.readUnsignedByte() << 8)
| dis.readUnsignedByte();
byte[] blockData = new byte[blockLen];
dis.readFully(blockData);
if (blockType == 4) { // VORBIS_COMMENT
parseVorbisComment(track, blockData);
} else if (blockType == 6) { // PICTURE
BufferedImage img = parseFLACPicture(blockData);
if (img != null) track.setCoverArt(img);
} else if (blockType == 0) { // STREAMINFO ; duration
if (blockLen >= 18) {
long totalSamples = ((long)(blockData[13] & 0x0F) << 32)
| (((long)(blockData[14] & 0xFF)) << 24)
| (((long)(blockData[15] & 0xFF)) << 16)
| (((long)(blockData[16] & 0xFF)) << 8)
| ((long)(blockData[17] & 0xFF));
int sampleRate = ((blockData[10] & 0xFF) << 12)
| ((blockData[11] & 0xFF) << 4)
| ((blockData[12] & 0xFF) >> 4);
if (sampleRate > 0)
track.setDurationSeconds(totalSamples / sampleRate);
}
}
}
}
}
private static void parseVorbisComment(Track track, byte[] data) {
try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) {
int vendorLen = readLE32(dis);
dis.skipBytes(vendorLen);
int count = readLE32(dis);
for (int i = 0; i < count; i++) {
int len = readLE32(dis);
byte[] cb = new byte[len];
dis.readFully(cb);
String comment = new String(cb, StandardCharsets.UTF_8);
int eq = comment.indexOf('=');
if (eq < 0) continue;
String k = comment.substring(0, eq).toUpperCase();
String v = comment.substring(eq + 1);
switch (k) {
case "TITLE" -> track.setTitle(v);
case "ARTIST" -> track.setArtist(v);
case "ALBUM" -> track.setAlbum(v);
case "DATE" -> { try { track.setYear(Integer.parseInt(v.substring(0, Math.min(4, v.length())))); } catch (NumberFormatException ignored) {} }
case "TRACKNUMBER" -> { try { track.setTrackNumber(Integer.parseInt(v)); } catch (NumberFormatException ignored) {} }
}
}
} catch (IOException ignored) {}
}
private static BufferedImage parseFLACPicture(byte[] data) {
try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) {
int pictureType = dis.readInt();
int mimeLen = dis.readInt();
dis.skipBytes(mimeLen);
int descLen = dis.readInt();
dis.skipBytes(descLen);
dis.skipBytes(16); // width, height, depth, index
int picLen = dis.readInt();
byte[] picData = new byte[picLen];
dis.readFully(picData);
return ImageIO.read(new ByteArrayInputStream(picData));
} catch (IOException e) { return null; }
}
// WAV
private static void readWAV(Track track) throws IOException {
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(track.getFilePath().toFile())))) {
byte[] riff = new byte[4];
dis.readFully(riff);
if (!new String(riff, StandardCharsets.US_ASCII).equals("RIFF")) return;
readLE32(dis); // total size
byte[] wave = new byte[4];
dis.readFully(wave);
if (!new String(wave, StandardCharsets.US_ASCII).equals("WAVE")) return;
int sampleRate = 0;
long dataSize = 0;
int blockAlign = 0;
while (dis.available() >= 8) {
byte[] chunkId = new byte[4];
dis.readFully(chunkId);
long chunkSize = Integer.toUnsignedLong(readLE32(dis));
String id = new String(chunkId, StandardCharsets.US_ASCII);
if (id.equals("fmt ")) {
dis.skipBytes(2); // audio format
dis.skipBytes(2); // channels
sampleRate = readLE32(dis);
dis.skipBytes(4); // byte rate
blockAlign = dis.readShort();
dis.skipBytes(2); // bits per sample
long skip = chunkSize - 16;
if (skip > 0) dis.skipBytes((int) skip);
} else if (id.equals("data")) {
dataSize = chunkSize;
dis.skipBytes((int) Math.min(chunkSize, Integer.MAX_VALUE));
} else if (id.equals("LIST")) {
byte[] listType = new byte[4];
dis.readFully(listType);
String lt = new String(listType, StandardCharsets.US_ASCII);
if (lt.equals("INFO")) {
long remaining = chunkSize - 4;
while (remaining >= 8) {
byte[] subId = new byte[4];
dis.readFully(subId);
int subLen = readLE32(dis);
remaining -= 8;
byte[] subData = new byte[subLen];
dis.readFully(subData);
remaining -= subLen;
if (subLen % 2 != 0 && remaining > 0) { dis.skipBytes(1); remaining--; }
String subKey = new String(subId, StandardCharsets.US_ASCII);
String val = new String(subData, StandardCharsets.ISO_8859_1).replace("\0", "").trim();
switch (subKey) {
case "INAM" -> track.setTitle(val);
case "IART" -> track.setArtist(val);
case "IPRD" -> track.setAlbum(val);
case "ICRD" -> { try { track.setYear(Integer.parseInt(val.substring(0, Math.min(4, val.length())))); } catch (NumberFormatException ignored) {} }
case "ITRK" -> { try { track.setTrackNumber(Integer.parseInt(val)); } catch (NumberFormatException ignored) {} }
}
}
} else {
long skip = chunkSize - 4;
if (skip > 0) dis.skipBytes((int) skip);
}
} else {
if (chunkSize > 0) dis.skipBytes((int) Math.min(chunkSize, Integer.MAX_VALUE));
}
}
if (sampleRate > 0 && blockAlign > 0 && dataSize > 0) {
track.setDurationSeconds(dataSize / (long)((long) sampleRate * blockAlign));
}
}
}
private static int readLE32(DataInputStream dis) throws IOException {
int b0 = dis.readUnsignedByte();
int b1 = dis.readUnsignedByte();
int b2 = dis.readUnsignedByte();
int b3 = dis.readUnsignedByte();
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
}
}
@@ -0,0 +1,92 @@
package com.rdesk.applications.entertainment.media;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
public class MusicLibrary {
private static final Set<String> SUPPORTED_EXTENSIONS =
Set.of(".mp3", ".flac", ".wav");
private final List<Track> tracks = new ArrayList<>();
private Path rootPath;
private ExecutorService scanExecutor;
public MusicLibrary(Path rootPath) {
this.rootPath = rootPath;
}
public Path getRootPath() { return rootPath; }
public void setRootPath(Path rootPath) {
this.rootPath = rootPath;
}
public List<Track> getTracks() {
return Collections.unmodifiableList(tracks);
}
public void scan(Consumer<Track> onProgress, Runnable onComplete) {
if (scanExecutor != null && !scanExecutor.isShutdown()) {
scanExecutor.shutdownNow();
}
scanExecutor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "wavy-library-scan");
t.setDaemon(true);
return t;
});
tracks.clear();
scanExecutor.submit(() -> {
try {
if (!Files.isDirectory(rootPath)) {
onComplete.run();
return;
}
Files.walkFileTree(rootPath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
String name = file.getFileName().toString().toLowerCase();
boolean supported = SUPPORTED_EXTENSIONS.stream().anyMatch(name::endsWith);
if (supported) {
Track track = new Track(file);
MetadataReader.populate(track);
synchronized (tracks) {
tracks.add(track);
}
onProgress.accept(track);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE; // skip unreadable files
}
});
} catch (IOException e) {
e.printStackTrace();
} finally {
onComplete.run();
}
});
}
public void sortByArtistTitle() {
synchronized (tracks) {
tracks.sort(Comparator
.comparing(Track::getArtist, String.CASE_INSENSITIVE_ORDER)
.thenComparing(Track::getTitle, String.CASE_INSENSITIVE_ORDER));
}
}
public void shutdown() {
if (scanExecutor != null) scanExecutor.shutdownNow();
}
}
@@ -0,0 +1,67 @@
package com.rdesk.applications.entertainment.media;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class SourceConfig {
private static final Path CONFIG_DIR = Path.of(System.getProperty("user.home"), "rsys");
private static final Path CONFIG_FILE = CONFIG_DIR.resolve("wavy_sources.json");
public static Path loadMusicRoot() {
if (Files.exists(CONFIG_FILE)) {
try {
String json = Files.readString(CONFIG_FILE, StandardCharsets.UTF_8);
String path = extractJsonString(json, "musicRoot");
if (path != null && !path.isBlank()) {
Path p = Path.of(path);
if (Files.isDirectory(p)) return p;
}
} catch (IOException ignored) {}
}
return defaultMusicRoot();
}
public static void saveMusicRoot(Path root) throws IOException {
Files.createDirectories(CONFIG_DIR);
String json = "{\n \"musicRoot\": \"" + escape(root.toString()) + "\"\n}\n";
Files.writeString(CONFIG_FILE, json, StandardCharsets.UTF_8);
}
private static Path defaultMusicRoot() {
String os = System.getProperty("os.name", "").toLowerCase();
if (os.contains("win")) {
String home = System.getProperty("user.home");
return Path.of(home, "Music");
}
return Path.of(System.getProperty("user.home"), "Music");
}
private static String extractJsonString(String json, String key) {
String needle = "\"" + key + "\"";
int ki = json.indexOf(needle);
if (ki < 0) return null;
int colon = json.indexOf(':', ki + needle.length());
if (colon < 0) return null;
int open = json.indexOf('"', colon + 1);
if (open < 0) return null;
int close = open + 1;
while (close < json.length()) {
char c = json.charAt(close);
if (c == '\\') { close += 2; continue; }
if (c == '"') break;
close++;
}
if (close >= json.length()) return null;
return unescape(json.substring(open + 1, close));
}
private static String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
private static String unescape(String s) {
return s.replace("\\\\", "\\").replace("\\\"", "\"").replace("\\/", "/");
}
}
@@ -0,0 +1,206 @@
package com.rdesk.applications.entertainment.media;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.ArrayBlockingQueue;
public class SpectrogramPanel extends JPanel {
private static final int BAR_COUNT = 96;
private static final Color[] HEAT = buildHeatMap();
private BufferedImage waterfallImg = null;
private Graphics2D waterfallG = null;
private float[] currentMag = new float[8192];
private float[] barSmoothed = new float[BAR_COUNT];
private float[] barPeak = new float[BAR_COUNT];
private int[] peakTimer = new int[BAR_COUNT];
private final ArrayBlockingQueue<float[]> fftQueue = new ArrayBlockingQueue<>(16);
private final Timer renderTimer;
public SpectrogramPanel() {
setBorder(BorderFactory.createTitledBorder("Spectrogram"));
setPreferredSize(new Dimension(400, 220));
setMinimumSize(new Dimension(100, 80));
renderTimer = new Timer(16, e -> {
float[] mag;
boolean updated = false;
while ((mag = fftQueue.poll()) != null) {
currentMag = mag;
scrollWaterfall();
updated = true;
}
if (updated) repaint();
});
renderTimer.start();
}
public void receiveFft(float[] magnitudes) {
fftQueue.offer(magnitudes.clone());
}
public void reset() {
currentMag = new float[2048];
java.util.Arrays.fill(barSmoothed, 0f);
java.util.Arrays.fill(barPeak, 0f);
// Clear waterfall image
if (waterfallG != null) {
waterfallG.setBackground(getBackground());
Insets ins = getInsets();
int wh = (getHeight() - ins.top - ins.bottom) / 2;
waterfallG.clearRect(0, 0, waterfallImg.getWidth(), Math.max(1, wh));
}
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Insets ins = getInsets();
int x0 = ins.left, y0 = ins.top;
int w = getWidth() - ins.left - ins.right;
int h = getHeight() - ins.top - ins.bottom;
if (w <= 4 || h <= 4) return;
int waterfallH = h / 2;
int barH = h - waterfallH;
ensureWaterfallImage(w, waterfallH);
drawWaterfall(g, x0, y0, w, waterfallH);
drawSpectrum (g, x0, y0 + waterfallH, w, barH);
}
private void ensureWaterfallImage(int w, int h) {
if (waterfallImg == null || waterfallImg.getWidth() != w || waterfallImg.getHeight() != h) {
BufferedImage newImg = new BufferedImage(w, Math.max(1,h), BufferedImage.TYPE_INT_RGB);
Graphics2D ng = newImg.createGraphics();
Color bg = getBackground();
ng.setColor(bg != null ? bg : Color.WHITE);
ng.fillRect(0, 0, w, Math.max(1,h));
if (waterfallImg != null && waterfallG != null) {
ng.drawImage(waterfallImg, 0, 0, null);
waterfallG.dispose();
}
waterfallImg = newImg;
waterfallG = ng;
}
}
private void scrollWaterfall() {
if (waterfallImg == null) return;
int w = waterfallImg.getWidth(), h = waterfallImg.getHeight();
if (h < 2) return;
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);
}
}
private void drawWaterfall(Graphics g, int x, int y, int w, int h) {
if (waterfallImg != null)
g.drawImage(waterfallImg, x, y, w, h, null);
g.setColor(Color.GRAY);
g.drawLine(x, y + h - 1, x + w, y + h - 1);
}
private void drawSpectrum(Graphics g, int x, int y, int w, int h) {
float[] bars = barSmoothed; // already updated by scrollWaterfall
int barW = Math.max(1, w / BAR_COUNT);
int gap = Math.max(1, barW / 5);
for (int i = 0; i < BAR_COUNT; i++) {
float val = bars[i];
int bx = x + i * barW;
int bh = (int)(val * (h - 12));
int ci = (int)(val * (HEAT.length - 1));
g.setColor(HEAT[ci]);
g.fillRect(bx + gap/2, y + h - 12 - bh, barW - gap, bh);
if (barPeak[i] > 0.01f) {
int ph = (int)(barPeak[i] * (h - 12));
g.setColor(Color.WHITE);
g.fillRect(bx + gap/2, y + h - 12 - ph - 1, barW - gap, 2);
}
}
g.setColor(getForeground());
g.setFont(g.getFont().deriveFont(8f));
FontMetrics fm = g.getFontMetrics();
String[] lbl = {"32","125","500","2k","8k","16k"};
double logMin = Math.log10(20.0), logMax = Math.log10(22050.0);
float[] freqs = {32,125,500,2000,8000,16000};
for (int i = 0; i < lbl.length; i++) {
double nx = (Math.log10(freqs[i]) - logMin) / (logMax - logMin) * w;
g.drawString(lbl[i], x + (int)nx, y + h - 1);
}
}
private float[] computeBars() {
int fftLen = currentMag.length;
float[] bars = new float[BAR_COUNT];
double logMin = Math.log10(20.0), logMax = Math.log10(22050.0);
for (int b = 0; b < BAR_COUNT; b++) {
double f1 = Math.pow(10, logMin + (logMax - logMin) * b / BAR_COUNT);
double f2 = Math.pow(10, logMin + (logMax - logMin) * (b + 1) / BAR_COUNT);
int bin1 = Math.max(0, Math.min(fftLen-1, (int)(f1/22050.0*fftLen)));
int bin2 = Math.max(bin1, Math.min(fftLen-1, (int)(f2/22050.0*fftLen)));
float sum = 0; int cnt = 0;
for (int k = bin1; k <= bin2; k++) { sum += currentMag[k]; cnt++; }
bars[b] = cnt > 0 ? sum / cnt : 0;
}
float dbMin = -80, dbMax = 0;
for (int b = 0; b < BAR_COUNT; b++) {
float db = bars[b] > 0 ? 20*(float)Math.log10(bars[b]) : dbMin;
bars[b] = Math.max(0, Math.min(1, (db - dbMin) / (dbMax - dbMin)));
}
for (int b = 0; b < BAR_COUNT; b++) {
barSmoothed[b] = barSmoothed[b]*0.65f + bars[b]*0.35f;
if (barSmoothed[b] > barPeak[b]) { barPeak[b] = barSmoothed[b]; peakTimer[b] = 25; }
else if (peakTimer[b] > 0) peakTimer[b]--;
else barPeak[b] = Math.max(0, barPeak[b] - 0.006f);
}
return barSmoothed;
}
private static Color[] buildHeatMap() {
int N = 256;
Color[] map = new Color[N];
Color[] stops = { Color.BLACK, new Color(0,0,180), Color.CYAN, Color.YELLOW, Color.RED };
int segs = stops.length - 1;
for (int i = 0; i < N; i++) {
float t = (float)i/(N-1) * segs;
int seg = Math.min((int)t, segs-1);
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));
}
return map;
}
private static int lerp(int a, int b, float t) {
return Math.max(0, Math.min(255, (int)(a + (b-a)*t)));
}
}
@@ -0,0 +1,64 @@
package com.rdesk.applications.entertainment.media;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
public class Track {
private final Path filePath;
private String title;
private String artist;
private String album;
private int year;
private int trackNumber;
private long durationSeconds;
private BufferedImage coverArt; // null if not embedded
private final String format; // "MP3", "FLAC", "WAV"
public Track(Path filePath) {
this.filePath = filePath;
String name = filePath.getFileName().toString();
int dot = name.lastIndexOf('.');
this.format = dot >= 0 ? name.substring(dot + 1).toUpperCase() : "UNKNOWN";
this.title = dot >= 0 ? name.substring(0, dot) : name;
this.artist = "Unknown Artist";
this.album = "Unknown Album";
this.year = 0;
this.trackNumber = 0;
this.durationSeconds = 0;
this.coverArt = null;
}
public Path getFilePath() { return filePath; }
public String getTitle() { return title; }
public String getArtist() { return artist; }
public String getAlbum() { return album; }
public int getYear() { return year; }
public int getTrackNumber() { return trackNumber; }
public long getDurationSeconds() { return durationSeconds; }
public BufferedImage getCoverArt() { return coverArt; }
public String getFormat() { return format; }
public String getDisplayTitle() {
return title != null && !title.isBlank() ? title : filePath.getFileName().toString();
}
public String getDurationString() {
long m = durationSeconds / 60;
long s = durationSeconds % 60;
return String.format("%d:%02d", m, s);
}
public void setTitle(String title) { this.title = title; }
public void setArtist(String artist) { this.artist = artist; }
public void setAlbum(String album) { this.album = album; }
public void setYear(int year) { this.year = year; }
public void setTrackNumber(int trackNumber) { this.trackNumber = trackNumber; }
public void setDurationSeconds(long dur) { this.durationSeconds = dur; }
public void setCoverArt(BufferedImage coverArt) { this.coverArt = coverArt; }
@Override
public String toString() {
return String.format("%s — %s", artist, title);
}
}
@@ -0,0 +1,90 @@
package com.rdesk.applications.entertainment.media;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
import javax.swing.*;
import java.nio.file.Path;
public class Wavy implements DesktopApp {
@Override public String getId() { return "wavy-player"; }
@Override public String getName() { return "Wavy"; }
@Override
public JInternalFrame createWindow(Kernel kernel) {
JInternalFrame frame = new JInternalFrame(
"Wavy", true, true, true, true);
frame.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
Path musicRoot = SourceConfig.loadMusicRoot();
MusicLibrary library = new MusicLibrary(musicRoot);
WavyPlayerPanel player = new WavyPlayerPanel(library);
player.hookFrameClose(frame);
frame.setJMenuBar(buildMenuBar(frame, library, player));
frame.setContentPane(player);
frame.pack();
frame.setSize(700, 780);
frame.setVisible(true);
player.reloadLibrary();
return frame;
}
private JMenuBar buildMenuBar(JInternalFrame frame, MusicLibrary library,
WavyPlayerPanel player) {
JMenuBar bar = new JMenuBar();
JMenu file = new JMenu("File");
JMenuItem chooseLocation = new JMenuItem("Choose Music Location…");
chooseLocation.setAccelerator(KeyStroke.getKeyStroke("control O"));
chooseLocation.addActionListener(e -> {
JFileChooser fc = new JFileChooser(library.getRootPath().toFile());
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fc.setDialogTitle("Select Music Folder");
fc.setApproveButtonText("Use This Folder");
if (fc.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
Path chosen = fc.getSelectedFile().toPath();
library.setRootPath(chosen);
try { SourceConfig.saveMusicRoot(chosen); }
catch (Exception ex) {
JOptionPane.showMessageDialog(frame,
"Could not save music location:\n" + ex.getMessage(),
"Save Error", JOptionPane.WARNING_MESSAGE);
}
frame.setTitle("Wavy - " + chosen.getFileName());
player.reloadLibrary();
}
});
file.add(chooseLocation);
JMenuItem refresh = new JMenuItem("Refresh Library");
refresh.setAccelerator(KeyStroke.getKeyStroke("F5"));
refresh.addActionListener(e -> player.reloadLibrary());
file.add(refresh);
file.addSeparator();
JMenuItem close = new JMenuItem("Close");
close.addActionListener(e -> frame.dispose());
file.add(close);
JMenu help = new JMenu("Help");
JMenuItem about = new JMenuItem("About Wavy");
about.addActionListener(e -> JOptionPane.showMessageDialog(frame, """
Wavy is the music player for rDesk
it supports MP3 FLAC and WAVE Files (soon to be extended)
Wavy features a 10 band equalizer with some presets, shuffle,
repeat, and a library path which persists between start / stops!
""", "About Wavy", JOptionPane.INFORMATION_MESSAGE));
help.add(about);
bar.add(file);
bar.add(help);
return bar;
}
}
@@ -0,0 +1,312 @@
package com.rdesk.applications.entertainment.media;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
public class WavyPlayerPanel extends JPanel {
private final AudioEngine engine;
private final MusicLibrary library;
private final CoverArtPanel coverPanel;
private final SpectrogramPanel spectrogram;
private final EqualizerPanel equalizer;
// Transport controls
private final JLabel lblTitle = new JLabel("No track selected");
private final JLabel lblArtist = new JLabel(" ");
private final JLabel lblAlbum = new JLabel(" ");
private final JLabel lblTime = new JLabel("0:00 / 0:00");
private final JSlider seekBar = new JSlider(0, 1000, 0);
private final JButton btnPrev = new JButton("<");
private final JButton btnPlay = new JButton("Play");
private final JButton btnStop = new JButton("Stop");
private final JButton btnNext = new JButton(">");
private final JCheckBox chkShuffle = new JCheckBox("Shuffle");
private final JCheckBox chkRepeat = new JCheckBox("Repeat");
private final JSlider volSlider;
// playlist
private final DefaultListModel<Track> playlistModel = new DefaultListModel<>();
private final JList<Track> playlistView = new JList<>(playlistModel);
private final JLabel lblStatus = new JLabel("No library loaded.");
// state
private int currentIndex = -1;
private boolean seekDragging = false;
private long currentDurationSec = 0;
private final Timer uiTimer;
public WavyPlayerPanel(MusicLibrary library) {
this.library = library;
this.engine = new AudioEngine();
this.coverPanel = new CoverArtPanel();
this.spectrogram = new SpectrogramPanel();
this.equalizer = new EqualizerPanel(engine);
volSlider = new JSlider(0, 100, 80);
setLayout(new BorderLayout(0, 0));
setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
engine.setOnTrackEnd(() -> SwingUtilities.invokeLater(this::playNext));
engine.setFftListener(mag -> {
spectrogram.receiveFft(mag);
});
engine.setOnPositionUpdate(pos -> {
if (!seekDragging) SwingUtilities.invokeLater(() -> updateSeekBar(pos));
});
buildUI();
uiTimer = new Timer(500, e -> updateTimeLabel());
uiTimer.start();
}
public void hookFrameClose(JInternalFrame frame) {
frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() {
@Override public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) {
engine.shutdown();
library.shutdown();
}
@Override public void internalFrameClosed(javax.swing.event.InternalFrameEvent e) {
engine.shutdown();
library.shutdown();
}
});
}
private void buildUI() {
add(buildNowPlayingPanel(), BorderLayout.NORTH);
add(spectrogram, BorderLayout.CENTER);
add(buildBottomTabs(), BorderLayout.SOUTH);
}
private JPanel buildNowPlayingPanel() {
JPanel panel = new JPanel(new BorderLayout(8, 0));
panel.setBorder(BorderFactory.createTitledBorder("Now Playing"));
coverPanel.setPreferredSize(new Dimension(130, 130));
coverPanel.setMinimumSize(new Dimension(60, 60));
panel.add(coverPanel, BorderLayout.WEST);
JPanel right = new JPanel(new BorderLayout(0, 4));
right.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
JPanel infoPanel = new JPanel(new GridLayout(3, 1, 0, 1));
lblTitle.setFont(lblTitle.getFont().deriveFont(Font.BOLD, 13f));
infoPanel.add(lblTitle);
infoPanel.add(lblArtist);
infoPanel.add(lblAlbum);
right.add(infoPanel, BorderLayout.NORTH);
JPanel seekPanel = new JPanel(new BorderLayout(4, 0));
seekBar.setPaintTicks(false);
seekBar.setPaintLabels(false);
seekBar.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) { seekDragging = true; }
public void mouseReleased(MouseEvent e) {
seekDragging = false;
engine.seekFraction(seekBar.getValue() / (double) seekBar.getMaximum());
}
});
seekPanel.add(seekBar, BorderLayout.CENTER);
seekPanel.add(lblTime, BorderLayout.EAST);
right.add(seekPanel, BorderLayout.CENTER);
right.add(buildTransportRow(), BorderLayout.SOUTH);
panel.add(right, BorderLayout.CENTER);
return panel;
}
private JPanel buildTransportRow() {
JPanel row = new JPanel(new BorderLayout(0, 0));
JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 0));
btns.add(btnPrev);
btns.add(btnPlay);
btns.add(btnStop);
btns.add(btnNext);
btns.add(new JSeparator(JSeparator.VERTICAL) {{ setPreferredSize(new Dimension(4, 24)); }});
btns.add(chkShuffle);
btns.add(chkRepeat);
row.add(btns, BorderLayout.WEST);
JPanel vol = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
vol.add(new JLabel("Volume:"));
volSlider.setPreferredSize(new Dimension(90, volSlider.getPreferredSize().height));
volSlider.setToolTipText("Volume");
volSlider.addChangeListener(e -> engine.setVolume(volSlider.getValue() / 100f));
vol.add(volSlider);
row.add(vol, BorderLayout.EAST);
btnPlay.addActionListener(e -> togglePlayPause());
btnStop.addActionListener(e -> {
engine.stop();
btnPlay.setText("Play");
});
btnPrev.addActionListener(e -> playPrev());
btnNext.addActionListener(e -> playNext());
return row;
}
private JTabbedPane buildBottomTabs() {
JTabbedPane tabs = new JTabbedPane();
// Playlist
JPanel playlistPanel = new JPanel(new BorderLayout(0, 2));
playlistPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
playlistView.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
playlistView.setCellRenderer(new TrackCellRenderer());
playlistView.setFixedCellHeight(38);
playlistView.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int idx = playlistView.locationToIndex(e.getPoint());
if (idx >= 0) playTrack(idx);
}
}
});
JScrollPane scroll = new JScrollPane(playlistView);
scroll.setPreferredSize(new Dimension(0, 220));
playlistPanel.add(scroll, BorderLayout.CENTER);
playlistPanel.add(lblStatus, BorderLayout.SOUTH);
tabs.addTab("Playlist", playlistPanel);
tabs.addTab("Equalizer", equalizer);
return tabs;
}
public void reloadLibrary() {
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.");
})
);
}
public AudioEngine getEngine() { return engine; }
private void togglePlayPause() {
if (engine.getState() == AudioEngine.State.STOPPED) {
if (currentIndex < 0 && playlistModel.size() > 0) playTrack(0);
else if (currentIndex >= 0) playTrack(currentIndex);
} else {
engine.togglePause();
boolean paused = engine.getState() == AudioEngine.State.PAUSED;
btnPlay.setText(paused ? "Play" : "Pause");
}
}
private void playTrack(int index) {
if (index < 0 || index >= playlistModel.size()) return;
currentIndex = index;
Track track = playlistModel.get(index);
engine.play(track);
updateTrackInfo(track);
currentDurationSec = track.getDurationSeconds();
playlistView.setSelectedIndex(index);
playlistView.ensureIndexIsVisible(index);
btnPlay.setText("Pause");
spectrogram.reset();
playlistView.repaint();
}
private void playNext() {
if (chkRepeat.isSelected() && currentIndex >= 0) { playTrack(currentIndex); return; }
int next = chkShuffle.isSelected()
? (int)(Math.random() * playlistModel.size())
: (currentIndex + 1) % Math.max(1, playlistModel.size());
playTrack(next);
}
private void playPrev() {
if (currentIndex <= 0) return;
playTrack(currentIndex - 1);
}
private void updateTrackInfo(Track track) {
lblTitle.setText(track.getDisplayTitle());
lblArtist.setText(track.getArtist());
lblAlbum.setText(track.getAlbum() + (track.getYear() > 0 ? " (" + track.getYear() + ")" : ""));
coverPanel.setTrack(track);
seekBar.setValue(0);
}
private void updateSeekBar(long posFrames) {
long total = engine.getTotalFrames();
if (total > 0) seekBar.setValue((int)(posFrames * 1000L / total));
}
private void updateTimeLabel() {
float sr = engine.getFormat() != null ? engine.getFormat().getSampleRate() : 44100f;
long posFr = engine.getPositionFrames();
long totalFr = engine.getTotalFrames();
long posSec = (long)(posFr / sr);
long totalSec = totalFr > 0
? (long)(totalFr / sr)
: (currentIndex >= 0 ? playlistModel.get(currentIndex).getDurationSeconds() : 0);
if (totalFr > 0 && currentIndex >= 0) {
Track t = playlistModel.get(currentIndex);
if (t.getDurationSeconds() == 0) t.setDurationSeconds(totalSec);
}
lblTime.setText(fmt(posSec) + " / " + fmt(totalSec));
if (engine.getState() == AudioEngine.State.STOPPED) btnPlay.setText("Play");
}
private static String fmt(long sec) {
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>" +
" &nbsp; " +
"<small>%s - %s</small>" +
" &nbsp; " +
"<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;");
}
}
}
@@ -1,5 +1,6 @@
package com.rdesk.applications.system;
import com.rdesk.applications.system.login.Login;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
@@ -22,7 +23,7 @@ public class Exit implements DesktopApp {
@Override
public JInternalFrame createWindow(Kernel kernel) {
JInternalFrame frame = new JInternalFrame("System Controls", true, true);
frame.setSize(350, 180);
frame.setSize(420, 240);
frame.setResizable(false);
JPanel mainPanel = new JPanel(new GridBagLayout());
@@ -30,7 +31,7 @@ public class Exit implements DesktopApp {
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5);
gbc.insets = new Insets(6, 6, 6, 6);
JLabel label = new JLabel("Choose an action:");
label.setFont(label.getFont().deriveFont(Font.BOLD));
@@ -38,8 +39,11 @@ public class Exit implements DesktopApp {
JButton shutdownButton = new JButton("Shutdown rDesk");
JButton shutdownOS = new JButton("Shutdown Host OS");
JButton logoffButton = new JButton("Log off");
shutdownButton.setFocusPainted(false);
shutdownButton.addActionListener(e -> confirmAndShutdown(kernel, frame));
shutdownOS.setFocusPainted(false);
shutdownOS.addActionListener(e -> {
try {
@@ -48,8 +52,37 @@ public class Exit implements DesktopApp {
throw new RuntimeException(ex);
}
});
logoffButton.setFocusPainted(false);
logoffButton.addActionListener(e -> {
int choice = JOptionPane.showConfirmDialog(
frame,
"Log off and return to the login screen?",
"Confirm Log off",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
);
if (choice != JOptionPane.YES_OPTION) return;
SwingUtilities.invokeLater(() -> {
try {
Login.main(null);
} catch (Exception ex) {
JOptionPane.showMessageDialog(frame,
"Failed to launch login screen: " + ex.getMessage(),
"Error",
JOptionPane.ERROR_MESSAGE);
return;
}
kernel.logoff();
});
});
mainPanel.add(shutdownButton, gbc);
mainPanel.add(shutdownOS, gbc);
mainPanel.add(logoffButton, gbc);
JButton cancelButton = new JButton("Cancel");
cancelButton.setFocusPainted(false);
@@ -93,7 +126,7 @@ public class Exit implements DesktopApp {
if (choice == JOptionPane.YES_OPTION) {
frame.dispose();
performShutdownCmd(); // cause else the call wouldnt be called
performShutdownCmd();
kernel.shutdown();
}
}
@@ -107,7 +140,7 @@ public class Exit implements DesktopApp {
JOptionPane.WARNING_MESSAGE
);
if (choice == JOptionPane.YES_OPTION) {
frame.dispose(); // close the confirmation window first
frame.dispose();
kernel.shutdown();
}
}
@@ -0,0 +1,126 @@
package com.rdesk.applications.system.login;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.awt.Window;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
public class Handler {
private static final String FILENAME = System.getProperty("user.home") + File.separator + "rsys" + File.separator + "rsys_passwdstore.json";
private static final SecureRandom RNG = new SecureRandom();
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final int SALT_LEN = 16;
private static final int KEY_LEN = 256; // bits
private static final int ITERATIONS = 200_000;
private static final String ALGO = "PBKDF2WithHmacSHA256";
public static boolean storeExists() {
return new File(FILENAME).exists();
}
public static UserRecord load() throws Exception {
File f = new File(FILENAME);
if (!f.exists()) return null;
try (FileReader fr = new FileReader(f)) {
return GSON.fromJson(fr, UserRecord.class);
}
}
public static void save(UserRecord u) throws Exception {
try (FileWriter fw = new FileWriter(FILENAME)) {
GSON.toJson(u, fw);
}
}
public static UserRecord createUser(String username, char[] password) throws Exception {
byte[] salt = new byte[SALT_LEN];
RNG.nextBytes(salt);
byte[] hash = pbkdf2(password, salt, ITERATIONS, KEY_LEN);
UserRecord u = new UserRecord();
u.username = username;
u.saltB64 = Base64.getEncoder().encodeToString(salt);
u.hashB64 = Base64.getEncoder().encodeToString(hash);
u.iterations = ITERATIONS;
return u;
}
public static boolean verifyPassword(char[] password, UserRecord stored) throws Exception {
byte[] salt = Base64.getDecoder().decode(stored.saltB64);
byte[] expected = Base64.getDecoder().decode(stored.hashB64);
byte[] computed = pbkdf2(password, salt, stored.iterations, expected.length * 8);
return slowEquals(expected, computed);
}
public static boolean verifyAndClose(Window window, char[] password, UserRecord stored) throws Exception {
try {
boolean ok = verifyPassword(password, stored);
if (ok && window != null) {
window.dispose();
}
return ok;
} finally {
clearPassword(password);
}
}
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int keyLengthBits) throws Exception {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLengthBits);
SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGO);
return skf.generateSecret(spec).getEncoded();
}
private static boolean slowEquals(byte[] a, byte[] b) {
if (a == null || b == null || a.length != b.length) return false;
int diff = 0;
for (int i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
return diff == 0;
}
public static void clearPassword(char[] p) {
if (p == null) return;
Arrays.fill(p, (char) 0);
}
public static String[] listUsers() {
File f = new File(FILENAME);
if (!f.exists()) return new String[0];
try (FileReader fr = new FileReader(f)) {
JsonElement root = JsonParser.parseReader(fr);
List<String> users = new ArrayList<>();
if (root.isJsonArray()) {
JsonArray arr = root.getAsJsonArray();
for (JsonElement el : arr) {
try {
UserRecord ur = GSON.fromJson(el, UserRecord.class);
if (ur != null && ur.username != null) users.add(ur.username);
} catch (Throwable ignore) { /* skip malformed entry */ }
}
} else if (root.isJsonObject()) {
try {
UserRecord ur = GSON.fromJson(root, UserRecord.class);
if (ur != null && ur.username != null) users.add(ur.username);
} catch (Throwable ignore) { /* malformed object -> no users */ }
}
return users.toArray(new String[0]);
} catch (Exception e) {
return new String[0];
}
}
}
@@ -0,0 +1,267 @@
package com.rdesk.applications.system.login;
import com.rdesk.kernel.BootMgr;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.util.logging.Logger;
public class Login {
private JFrame frame;
private JComboBox<String> userCombo;
private JTextField userFieldFallback;
private JPasswordField passField;
private JLabel statusLabel;
private UserRecord stored;
private JButton loginBtn;
private JButton closeBtn;
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
new Login().start();
} catch (Exception e) {
JOptionPane.showMessageDialog(null, e);
System.exit(1);
}
});
}
private void start() throws Exception {
log("start(): checking store existence");
if (!Handler.storeExists()) {
showCreateUserDialog();
}
stored = Handler.load();
log("start(): loaded user record: " + (stored != null ? stored.username : "null"));
buildAndShowGui();
}
private void buildAndShowGui() {
log("buildAndShowGui(): building GUI");
frame = new JFrame("rDesk Login");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
Image icon = createImageFromSwingIcon(UIManager.getIcon("FileView.computerIcon"));
if (icon != null) frame.setIconImage(icon);
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(6, 6, 6, 6);
c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL;
int row = 0;
if (icon != null) {
JLabel iconLabel = new JLabel(new ImageIcon(icon.getScaledInstance(64, 64, Image.SCALE_SMOOTH)));
GridBagConstraints ic = (GridBagConstraints) c.clone();
ic.gridx = 0;
ic.gridy = row;
ic.gridheight = 2;
ic.anchor = GridBagConstraints.CENTER;
panel.add(iconLabel, ic);
}
int colOffset = (icon != null) ? 1 : 0;
c.gridx = colOffset;
c.gridy = row;
c.gridwidth = 1;
panel.add(new JLabel("Username:"), c);
c.gridx = colOffset + 1;
c.gridy = row++;
c.weightx = 1.0;
String[] users = fetchUserList();
if (users.length > 0) {
userCombo = new JComboBox<>(users);
userCombo.setEditable(false);
userCombo.setPreferredSize(new Dimension(160, 24));
panel.add(userCombo, c);
} else {
userFieldFallback = new JTextField(16);
userFieldFallback.setPreferredSize(new Dimension(160, 24));
panel.add(userFieldFallback, c);
}
c.gridx = colOffset;
c.gridy = row;
c.weightx = 0;
panel.add(new JLabel("Password:"), c);
c.gridx = colOffset + 1;
c.gridy = row++;
c.weightx = 1.0;
passField = new JPasswordField(16);
passField.setPreferredSize(new Dimension(160, 24));
panel.add(passField, c);
closeBtn = new JButton("Close");
closeBtn.addActionListener(e -> frame.dispose());
loginBtn = new JButton("Login");
loginBtn.addActionListener(e -> onLogin());
c.gridx = colOffset;
c.gridy = row;
c.gridwidth = 1;
c.weightx = 0.5;
c.fill = GridBagConstraints.HORIZONTAL;
panel.add(closeBtn, c);
c.gridx = colOffset + 1;
c.gridy = row++;
c.weightx = 0.5;
panel.add(loginBtn, c);
statusLabel = new JLabel(" ");
statusLabel.setForeground(Color.BLUE);
c.gridx = colOffset;
c.gridy = row;
c.gridwidth = 2;
c.weightx = 1.0;
c.fill = GridBagConstraints.HORIZONTAL;
panel.add(statusLabel, c);
frame.getContentPane().add(panel);
frame.getRootPane().setDefaultButton(loginBtn);
passField.addActionListener(e -> onLogin());
if (userFieldFallback != null) userFieldFallback.addActionListener(e -> onLogin());
if (userCombo != null) {
userCombo.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("ENTER"), "login");
userCombo.getActionMap().put("login", new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { onLogin(); }
});
}
frame.pack();
Dimension total = frame.getContentPane().getSize();
int gap = 12;
int buttonWidth = Math.max(90, (total.width - gap) / 2);
closeBtn.setPreferredSize(new Dimension(buttonWidth, 28));
loginBtn.setPreferredSize(new Dimension(buttonWidth, 28));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
log("buildAndShowGui(): GUI visible");
}
private Image createImageFromSwingIcon(Icon icon) {
if (icon == null) return null;
int w = Math.max(1, icon.getIconWidth());
int h = Math.max(1, icon.getIconHeight());
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
try {
icon.paintIcon(null, g, 0, 0);
} finally {
g.dispose();
}
return img;
}
private void onLogin() {
String userAttempt = (userCombo != null)
? String.valueOf(userCombo.getSelectedItem()).trim()
: userFieldFallback.getText().trim();
char[] passAttempt = passField.getPassword();
log("onLogin(): attempt for user='" + userAttempt + "'");
if (stored == null) {
statusLabel.setText("No user record found.");
Handler.clearPassword(passAttempt);
return;
}
if (!stored.username.equals(userAttempt)) {
statusLabel.setText("Invalid username or password.");
Handler.clearPassword(passAttempt);
return;
}
try {
boolean ok = Handler.verifyAndClose(frame, passAttempt, stored);
if (ok) {
log("onLogin(): authentication successful for user='" + userAttempt + "'");
statusLabel.setText(" ");
SwingUtilities.invokeLater(() -> {
new SwingWorker<Void, Void>() {
@Override protected Void doInBackground() {
try {
BootMgr.launchOrShow();
log("[Login]: BootManager.launchOrShow() completed");
} catch (Exception e) {
log("onLogin(): BootManager error: " + e.getMessage());
}
return null;
}
}.execute();
});
} else {
statusLabel.setText("Invalid username or password.");
log("onLogin(): password verification failed for user='" + userAttempt + "'");
}
} catch (Exception ex) {
ex.printStackTrace();
statusLabel.setText("Error during verification.");
log("onLogin(): exception during verification: " + ex.getMessage());
} finally {
Handler.clearPassword(passAttempt);
passField.setText("");
}
}
private void showCreateUserDialog() throws Exception {
log("showCreateUserDialog(): creating user dialog");
JPanel p = new JPanel(new GridLayout(3,2,6,6));
JTextField user = new JTextField(16);
JPasswordField pass = new JPasswordField(16);
JPasswordField pass2 = new JPasswordField(16);
p.add(new JLabel("Username:")); p.add(user);
p.add(new JLabel("Password:")); p.add(pass);
p.add(new JLabel("Confirm:")); p.add(pass2);
int res = JOptionPane.showConfirmDialog(null, p, "Create User", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (res != JOptionPane.OK_OPTION) {
log("showCreateUserDialog(): user cancelled creation, exiting");
System.exit(0);
}
if (!String.valueOf(pass.getPassword()).equals(String.valueOf(pass2.getPassword()))) {
JOptionPane.showMessageDialog(null, "Passwords do not match.", "Error", JOptionPane.ERROR_MESSAGE);
log("showCreateUserDialog(): passwords did not match");
showCreateUserDialog();
return;
}
UserRecord u = Handler.createUser(user.getText().trim(), pass.getPassword());
Handler.save(u);
log("showCreateUserDialog(): created and saved user '" + u.username + "'");
JOptionPane.showMessageDialog(null, "User created.", "Success", JOptionPane.INFORMATION_MESSAGE);
}
private String[] fetchUserList() {
try {
log("fetchUserList(): attempting to retrieve user list from Handler");
String[] users = Handler.listUsers();
if (users == null || users.length == 0) {
if (stored != null) {
return new String[]{stored.username};
}
return new String[0];
}
return users;
} catch (Exception t) {
if (stored != null) return new String[]{stored.username};
return new String[0];
}
}
private void log(String msg) {
System.out.println("[Login]: " + msg);
}
}
@@ -0,0 +1,10 @@
package com.rdesk.applications.system.login;
import com.google.gson.annotations.SerializedName;
public class UserRecord {
@SerializedName("username") public String username;
@SerializedName("salt_b64") public String saltB64;
@SerializedName("hash_b64") public String hashB64;
@SerializedName("iterations") public int iterations;
}
@@ -0,0 +1,119 @@
package com.rdesk.applications.system.rsys;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
import com.rdesk.kernel.rsys.KbInterface;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class KbChanger implements DesktopApp {
private static final String[] LAYOUTS = {
"us", "uk", "de", "fr", "es", "it", "ru", "jp", "br", "cz"
};
@Override
public String getId() {
return "kbchanger";
}
@Override
public String getName() {
return "KbChanger";
}
@Override
public JInternalFrame createWindow(Kernel kernel) {
JInternalFrame frame = new JInternalFrame(
"Keyboard Layout",
true, // resizable
true, // closable
true, // maximizable
true // iconifiable
);
frame.setSize(420, 200);
frame.setLayout(new BorderLayout(8, 8));
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
JPanel top = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 12));
top.add(new JLabel("Layout:"));
JComboBox<String> layoutCombo = new JComboBox<>(LAYOUTS);
layoutCombo.setEditable(false);
top.add(layoutCombo);
JButton applyBtn = new JButton("Apply");
top.add(applyBtn);
frame.add(top, BorderLayout.NORTH);
JPanel center = new JPanel(new BorderLayout(6, 6));
JLabel hint = new JLabel("Type here to test the selected layout:");
center.add(hint, BorderLayout.NORTH);
JTextField testField = new JTextField();
center.add(testField, BorderLayout.CENTER);
JLabel status = new JLabel("Last applied: (none)");
status.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
center.add(status, BorderLayout.SOUTH);
frame.add(center, BorderLayout.CENTER);
KbInterface kb = new KbInterface();
try {
Path configPath = Paths.get(System.getProperty("user.home"), "rsys", "rsys_kblayout.json");
if (Files.exists(configPath)) {
try (Reader reader = Files.newBufferedReader(configPath)) {
JsonObject config = JsonParser.parseReader(reader).getAsJsonObject();
if (config.has("kblayout")) {
String savedLayout = config.get("kblayout").getAsString();
layoutCombo.setSelectedItem(savedLayout);
status.setText("Last applied: " + savedLayout);
kb.setKbLayout(savedLayout);
}
}
}
} catch (Exception ex) {
System.err.println("Failed to read existing kb layout config: " + ex.getMessage());
}
applyBtn.addActionListener((ActionEvent e) -> {
String selected = (String) layoutCombo.getSelectedItem();
if (selected == null || selected.trim().isEmpty()) {
JOptionPane.showMessageDialog(frame, "Please select a layout.", "Keyboard Layout", JOptionPane.WARNING_MESSAGE);
return;
}
kb.setKbLayout(selected);
kb.saveLayout(selected);
status.setText("Last applied: " + selected);
SwingUtilities.invokeLater(testField::requestFocusInWindow);
});
testField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
applyBtn.doClick();
}
}
});
frame.setVisible(true);
return frame;
}
}
@@ -0,0 +1,40 @@
package com.rdesk.kernel;
import javax.swing.*;
import java.awt.*;
public final class BootMgr {
private static JFrame desktopFrame;
private BootMgr() {}
public static synchronized void launchOrShow() {
if (desktopFrame == null) {
JFrame f = BootProc.boot();
if (f != null) desktopFrame = f;
else {
desktopFrame = new JFrame();
desktopFrame.setContentPane(f);
desktopFrame.pack();
}
desktopFrame.setVisible(true);
desktopFrame.toFront();
} else {
if (!desktopFrame.isVisible()) desktopFrame.setVisible(true);
desktopFrame.toFront();
desktopFrame.requestFocus();
}
}
public static synchronized void hideDesktopOnLogout() {
if (desktopFrame != null) {
desktopFrame.setVisible(false);
}
}
public static synchronized void disposeDesktop() {
if (desktopFrame != null) {
desktopFrame.dispose();
desktopFrame = null;
}
}
}
@@ -0,0 +1,111 @@
package com.rdesk.kernel;
import com.rdesk.applications.entertainment.comic.Xkcd;
import com.rdesk.applications.entertainment.media.Wavy;
import com.rdesk.applications.productivity.Calculator;
import com.rdesk.applications.productivity.Notes;
import com.rdesk.applications.productivity.browsing.TabbedBrowser;
import com.rdesk.applications.productivity.code.CodeDesk;
import com.rdesk.applications.system.Exit;
import com.rdesk.applications.system.TaskManager;
import com.rdesk.applications.system.filehaj.FileExplorerApp;
import com.rdesk.applications.system.rsys.KbChanger;
import com.rdesk.kernel.launcher.AppLauncher;
import javax.swing.*;
public class BootProc {
private static final boolean RUN_IN_FS = true;
private static final boolean USE_SYSDEFAULT_LAF = false;
private static final boolean USE_FLATLAF = false;
private static final boolean loadSettingsKernelFlag = true;
private static JFrame mainFrame;
private static JDesktopPane desktopPane;
public static synchronized JFrame boot() {
if (mainFrame != null) {
if (!mainFrame.isVisible()) mainFrame.setVisible(true);
mainFrame.toFront();
mainFrame.requestFocus();
return mainFrame;
}
Kernel kernel = Kernel.get();
kernel.initSettings(loadSettingsKernelFlag);
kernel.boot();
mainFrame = new JFrame("rDesk");
mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
mainFrame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
kernel.shutdown();
}
});
if (USE_FLATLAF) {
try {
UIManager.setLookAndFeel("com.formdev.flatlaf.themes.FlatMacLightLaf");
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(null, ex.getMessage() + "@USE_FLATLAF");
}
}
if (USE_SYSDEFAULT_LAF) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(null, ex.getMessage() + "@USE_SYSDEFAULT_LAF");
}
}
if (RUN_IN_FS) {
mainFrame.setUndecorated(true);
mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
} else {
mainFrame.setSize(1200, 800);
mainFrame.setLocationRelativeTo(null);
}
desktopPane = new JDesktopPane();
kernel.windows().attachDesktop(desktopPane);
kernel.attachFrame(mainFrame);
AppLauncher launcher = new AppLauncher(kernel);
registerSystemApps(launcher);
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, launcher, desktopPane);
split.setDividerSize(0);
split.setEnabled(false);
mainFrame.setContentPane(split);
mainFrame.setVisible(true);
return mainFrame;
}
public static synchronized JFrame getMainFrame() {
return mainFrame;
}
public static synchronized JDesktopPane getDesktopPane() {
return desktopPane;
}
private static void registerSystemApps(AppLauncher launcher) {
// productivity
launcher.addApp(new Notes(), "Productivity");
launcher.addApp(new TabbedBrowser(), "Productivity");
launcher.addApp(new CodeDesk(), "Productivity");
launcher.addApp(new Calculator(), "Productivity");
// entertainment
launcher.addApp(new Xkcd(), "Entertainment");
launcher.addApp(new Wavy(), "Entertainment");
// System apps
launcher.addApp(new Exit(), "System");
launcher.addApp(new TaskManager(), "System");
launcher.addApp(new FileExplorerApp(), "System");
launcher.addApp(new KbChanger(), "System");
}
}
+38 -3
View File
@@ -1,11 +1,17 @@
package com.rdesk.kernel;
import com.rdesk.kernel.launcher.AppLauncher;
import com.rdesk.kernel.rsys.KbInterface;
import javax.swing.*;
public class Kernel {
private static volatile Kernel instance;
// set to true in production builds
private boolean initSettings = true;
private final AppManager appManager;
private final WindowManager windowManager;
private final EventBus eventBus;
@@ -35,18 +41,41 @@ public class Kernel {
public void boot() {
running = true;
System.out.println("[Kernel] Booting...");
kInfo("Booting");
eventBus.emit("kernel.boot");
// load keyboard layout:
if (initSettings)
initRSysSettings();
}
private void initRSysSettings() {
try {
kInfo("loading system config for : " + (String) System.getProperty("os.name"));
KbInterface kbInterface = new KbInterface();
kbInterface.loadLayout();
kInfo("loaded kbLayout");
} catch (Exception e) {
kErr("Failed to initialize system settings ; error message: " + e.getMessage());
}
}
public void attachFrame(JFrame frame) {
this.desktopFrame = frame;
}
public void logoff() {
if (running) SwingUtilities.invokeLater(() -> {
desktopFrame.setVisible(false);
});
}
public void shutdown() {
if (!running) return;
System.out.println("[Kernel] Shutdown started");
kInfo("Shutting down");
running = false;
appManager.stopAll();
@@ -57,10 +86,16 @@ public class Kernel {
}
eventBus.emit("kernel.shutdown");
System.out.println("[Kernel] Shutdown complete");
kInfo("Shut down Kernel");
System.exit(0);
}
// kernel info string printers
private void kErr(String errmsg) { System.err.println("[KERNEL FAULT]: " + errmsg); }
private void kInfo(String infomsg) { System.out.println("[Kernel]: " + infomsg); }
// public kernel api
public void initSettings(boolean flag) { initSettings = flag; }
public AppManager apps() { return appManager; }
public WindowManager windows() { return windowManager; }
public EventBus events() { return eventBus; }
@@ -0,0 +1,92 @@
package com.rdesk.kernel.rsys;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.swing.*;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class KbInterface {
/* DISCLAIMER:
* This is a class which is though to be for the rsys operating system,
* it only supports the keyboard set option for x11, macos and windows isnt supported
*/
private static class KbConfig {
String kblayout;
public KbConfig(String kblayout) {
this.kblayout = kblayout;
}
}
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
public void setKbLayout(String kbLayout) {
String os = System.getProperty("os.name").toLowerCase();
try {
if (os.contains("nix") || os.contains("nux") || os.contains("aix") || os.contains("linux")) {
setLinuxLayout(kbLayout); // actual work!
} else if (os.contains("mac")) {
logErr("mac is too shitty for this");
} else {
logErr("what are you running?: " + os);
}
} catch (Exception ex) {
showError("The keyboard layout could not be set " + ex.getMessage());
}
}
public void loadLayout() {
Path configPath = getConfigPath();
if (!Files.exists(configPath)) {
return;
}
try (Reader reader = Files.newBufferedReader(configPath)) {
KbConfig config = gson.fromJson(reader, KbConfig.class);
if (config != null && config.kblayout != null && !config.kblayout.isBlank()) {
setKbLayout(config.kblayout);
}
} catch (Exception ex) {
logErr("Failed to load layout configuration: " + ex.getMessage());
}
}
public void saveLayout(String kbLayout) {
Path configPath = getConfigPath();
try {
Files.createDirectories(configPath.getParent());
try (Writer writer = Files.newBufferedWriter(configPath)) {
gson.toJson(new KbConfig(kbLayout), writer);
}
} catch (Exception ex) {
logErr("Failed to save layout configuration: " + ex.getMessage());
}
}
private Path getConfigPath() {
return Paths.get(System.getProperty("user.home"), "rsys", "rsys_kblayout.json");
}
private void setLinuxLayout(String kbLayout) throws Exception {
new ProcessBuilder("setxkbmap", kbLayout)
.inheritIO()
.start()
.waitFor();
}
private void logErr(String message) { System.err.println("[KbInterface.java]: " + (String) message); }
private void showError(String message) {
JOptionPane.showMessageDialog(
null,
message,
"Keyboard Layout",
JOptionPane.ERROR_MESSAGE
);
}
}