Crea un Reproductor de Música Animado con JavaFX
Aprende a integrar javafx.media para reproducir audio y dibujar un ecualizador visual dinámico usando AudioSpectrumListener, todo con una interfaz premium oscura y glassmorfismo.
JJ Arroyo
3 de marzo de 2026 • 8 min de lectura

En este tutorial te enseñaré cómo construir desde cero un reproductor de música funcional y estéticamente sorprendente utilizando JavaFX. No solo nos limitaremos a hacer que suene la música (que es bastante sencillo integrando el módulo de media), sino que construiremos un Ecualizador Visual (Spectrum Analyzer) que reacciona en tiempo real a las frecuencias de la canción que está sonando.
Todo esto, envuelto en una interfaz moderna con modo oscuro, botones circulares perfectos y toques de glassmorphism.
Requisitos Previos
Para este proyecto, es indispensable que cuentes con el módulo javafx.media en tu proyecto.
Si usas nuestro generador o compilas por línea de comandos, asegúrate de tenerlo incluido en el comando de ejecución y compilación:
javac --module-path "%JAVAFX_PATH%" --add-modules javafx.controls,javafx.fxml,javafx.media ...
java --module-path "%JAVAFX_PATH%" --add-modules javafx.controls,javafx.fxml,javafx.media ...
La Interfaz (FXML)
Nuestra interfaz está dividida estructuralmente usando un BorderPane. A la izquierda tendremos un VBox para cargar la carpeta y listar los archivos (ListView). Al centro, un Pane vacío donde dibujaremos programáticamente las barras del ecualizador. Abajo, los controles clásicos y un Slider.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<BorderPane styleClass="root-pane" stylesheets="@../css/style.css" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.reproductor_musica.controller.MainController">
<!-- Barra Lateral para Playlist -->
<left>
<VBox spacing="10" styleClass="sidebar">
<padding>
<Insets top="15" right="15" bottom="15" left="15"/>
</padding>
<Label text="Mi Música" styleClass="sidebar-title" />
<Button fx:id="btnLoadFolder" text="Cargar Carpeta" styleClass="btn-primary" onAction="#handleLoadFolder" maxWidth="Infinity" />
<ListView fx:id="playlistView" VBox.vgrow="ALWAYS" styleClass="playlist-view" />
</VBox>
</left>
<!-- Área Central para el Ecualizador Visual -->
<center>
<VBox alignment="CENTER" styleClass="center-pane">
<Label fx:id="songTitleLabel" text="No hay pista seleccionada" styleClass="song-title-label" />
<Pane fx:id="visualizerPane" VBox.vgrow="ALWAYS" />
</VBox>
</center>
<!-- Barra Inferior de Controles -->
<bottom>
<VBox styleClass="bottom-bar" spacing="10">
<padding>
<Insets top="15" right="20" bottom="15" left="20"/>
</padding>
<HBox spacing="10" alignment="CENTER">
<Label fx:id="lblCurrentTime" text="00:00" styleClass="time-label" />
<Slider fx:id="progressSlider" HBox.hgrow="ALWAYS" styleClass="progress-slider" />
<Label fx:id="lblTotalTime" text="00:00" styleClass="time-label" />
</HBox>
<HBox spacing="15" alignment="CENTER">
<Button fx:id="btnPrev" text="⏮" styleClass="btn-control" onAction="#handlePrev" />
<Button fx:id="btnPlayPause" text="▶" styleClass="btn-control, btn-play" onAction="#handlePlayPause" />
<Button fx:id="btnStop" text="⏹" styleClass="btn-control" onAction="#handleStop" />
<Button fx:id="btnNext" text="⏭" styleClass="btn-control" onAction="#handleNext" />
</HBox>
</VBox>
</bottom>
</BorderPane>
El CSS Premium (Modo Oscuro Glass)
El diseño oscuro utiliza gradientes radiales para crear profundidad detrás del ecualizador. Los botones se han forzado a ser círculos perfectos (-fx-background-radius: 20px o la mitad de su tamaño), y el deslizador o Slider está altamente personalizado.
.root-pane {
-fx-background-color: #121212;
-fx-font-family: 'Segoe UI', 'Inter', sans-serif;
}
.sidebar {
-fx-background-color: #1e1e1e;
-fx-pref-width: 320px;
-fx-border-color: #2c2c2c;
-fx-border-width: 0 1px 0 0;
}
.sidebar-title {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
}
.btn-primary {
-fx-background-color: #3b82f6;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-padding: 8px 12px;
-fx-background-radius: 8px;
-fx-cursor: hand;
}
.btn-primary:hover {
-fx-background-color: #2563eb;
}
.playlist-view {
-fx-background-color: transparent;
-fx-control-inner-background: transparent;
-fx-control-inner-background-alt: transparent;
}
.playlist-view .list-cell {
-fx-background-color: transparent;
-fx-text-fill: #d1d5db;
-fx-padding: 10px 12px;
-fx-font-size: 14px;
-fx-cursor: hand;
-fx-border-color: transparent transparent #2c2c2c transparent;
-fx-border-width: 0 0 1px 0;
}
.playlist-view .list-cell:filled:hover {
-fx-background-color: #2c2c2c;
-fx-text-fill: #ffffff;
}
.playlist-view .list-cell:filled:selected {
-fx-background-color: #3b82f6;
-fx-text-fill: #ffffff;
-fx-font-weight: bold;
}
.center-pane {
/* Gradiente radial para simular foco de luz tenue en el centro */
-fx-background-color: radial-gradient(
center 50% 50%,
radius 70%,
#2a2a35,
#121212
);
-fx-padding: 20px;
}
.song-title-label {
-fx-text-fill: #ffffff;
-fx-font-size: 24px;
-fx-font-weight: bold;
-fx-padding: 0 0 20px 0;
}
.bottom-bar {
-fx-background-color: rgba(30, 30, 30, 0.9);
-fx-border-color: #2c2c2c;
-fx-border-width: 1px 0 0 0;
}
.time-label {
-fx-text-fill: #9ca3af;
-fx-font-size: 12px;
}
/* Personalización profunda del Slider de tiempo */
.progress-slider {
-fx-padding: 5px 0;
}
.progress-slider .track {
-fx-background-color: #4b5563;
-fx-pref-height: 6px;
-fx-background-radius: 3px;
}
.progress-slider .thumb {
-fx-background-color: #3b82f6;
-fx-pref-width: 14px;
-fx-pref-height: 14px;
-fx-background-radius: 7px;
}
.progress-slider:hover .thumb {
-fx-effect: dropshadow(gaussian, rgba(59, 130, 246, 0.5), 10, 0, 0, 0);
}
/* Base de botones circulares y Play Button más grande */
.btn-control {
-fx-background-color: transparent;
-fx-text-fill: #d1d5db;
-fx-font-size: 18px;
-fx-cursor: hand;
-fx-min-width: 40px;
-fx-min-height: 40px;
-fx-max-width: 40px;
-fx-max-height: 40px;
-fx-background-radius: 20px;
-fx-alignment: center;
-fx-padding: 0;
}
.btn-control:hover {
-fx-background-color: #2c2c2c;
-fx-text-fill: #ffffff;
}
.btn-play {
-fx-font-size: 20px;
-fx-background-color: #3b82f6;
-fx-text-fill: white;
-fx-min-width: 50px;
-fx-min-height: 50px;
-fx-max-width: 50px;
-fx-max-height: 50px;
-fx-background-radius: 25px;
-fx-alignment: center;
}
.btn-play:hover {
-fx-background-color: #2563eb;
-fx-text-fill: white;
-fx-effect: dropshadow(gaussian, rgba(59, 130, 246, 0.4), 10, 0, 0, 0);
}
El Controlador: Magia con MediaPlayer y AudioSpectrumListener
El corazón del proyecto reside en utilizar MediaPlayer pasándole una URI.
Lo más deslumbrante de este controlador es que instanciamos 64 objetos Rectangle. Luego le pasamos al MediaPlayer una interfaz AudioSpectrumListener. Cada 0.04 segundos, JavaFX nos devuelve un arreglo (array) de magnitudes. Nosotros simplemente escalamos esos números negativos (-60db a 0db) en valores positivos de altura para nuestros rectángulos en pantalla.
MainController.java
package com.reproductor_musica.controller;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.Slider;
import javafx.scene.layout.Pane;
import javafx.scene.media.AudioSpectrumListener;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.DirectoryChooser;
import javafx.util.Duration;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class MainController {
@FXML private Button btnLoadFolder;
@FXML private ListView<String> playlistView;
@FXML private Label songTitleLabel;
@FXML private Pane visualizerPane;
@FXML private Label lblCurrentTime;
@FXML private Label lblTotalTime;
@FXML private Slider progressSlider;
@FXML private Button btnPlayPause;
// ... otros botones inyectados con @FXML (prev, next, stop)
private MediaPlayer mediaPlayer;
private List<File> songFiles = new ArrayList<>();
// Configuración del Ecualizador
private final int BANDS = 64;
private Rectangle[] rects;
private boolean isUserSeeking = false;
@FXML
public void initialize() {
setupVisualizer();
// 1. Escuchar cuando se hace click en una canción de la lista
playlistView.getSelectionModel().selectedIndexProperty().addListener((obs, oldVal, newVal) -> {
if (newVal != null && newVal.intValue() >= 0) playSong(newVal.intValue());
});
// 2. Controlar la barra de progreso (Slider) de la canción
progressSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
if (progressSlider.isValueChanging() || isUserSeeking) {
if (mediaPlayer != null) mediaPlayer.seek(Duration.seconds(newVal.doubleValue()));
}
});
progressSlider.setOnMousePressed(e -> isUserSeeking = true);
progressSlider.setOnMouseReleased(e -> isUserSeeking = false);
}
// Inicializa todos los rectángulos planos
private void setupVisualizer() {
rects = new Rectangle[BANDS];
visualizerPane.widthProperty().addListener((o, oldV, newV) -> updateVisualizerLayout());
visualizerPane.heightProperty().addListener((o, oldV, newV) -> updateVisualizerLayout());
for (int i = 0; i < BANDS; i++) {
Rectangle rect = new Rectangle();
rect.setFill(Color.web("#3b82f6"));
rect.setArcWidth(4); rect.setArcHeight(4);
rect.setHeight(5);
rects[i] = rect;
visualizerPane.getChildren().add(rect);
}
}
// Adaptar anchos de rectángulo dinámicamente si se alarga la ventana
private void updateVisualizerLayout() {
double width = visualizerPane.getWidth();
double height = visualizerPane.getHeight();
if (width <= 0 || height <= 0) return;
double bandWidth = width / BANDS;
double gap = bandWidth * 0.2;
double rectWidth = bandWidth - gap;
for (int i = 0; i < BANDS; i++) {
rects[i].setWidth(rectWidth);
rects[i].setX(i * bandWidth + gap/2);
rects[i].setY(height - rects[i].getHeight()); // Anclar abajo
}
}
// Funciones básicas de Carga de archivos
@FXML
protected void handleLoadFolder() {
DirectoryChooser directoryChooser = new DirectoryChooser();
File selectedDirectory = directoryChooser.showDialog(btnLoadFolder.getScene().getWindow());
if (selectedDirectory != null) {
File[] files = selectedDirectory.listFiles((d, name) -> name.toLowerCase().endsWith(".mp3") || name.toLowerCase().endsWith(".wav"));
if (files != null) {
songFiles.clear(); playlistView.getItems().clear();
for (File file : files) {
songFiles.add(file);
playlistView.getItems().add(file.getName());
}
}
}
}
private void playSong(int index) {
if (index < 0 || index >= songFiles.size()) return;
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.dispose(); // Liberar recursos previos
}
File file = songFiles.get(index);
songTitleLabel.setText(file.getName());
Media media = new Media(file.toURI().toString());
mediaPlayer = new MediaPlayer(media);
mediaPlayer.setOnReady(() -> {
Duration totalDuration = media.getDuration();
progressSlider.setMax(totalDuration.toSeconds());
lblTotalTime.setText(formatTime(totalDuration));
btnPlayPause.setText("⏸");
});
mediaPlayer.currentTimeProperty().addListener((obs, oldT, newT) -> {
if (!progressSlider.isValueChanging() && !isUserSeeking) {
progressSlider.setValue(newT.toSeconds());
}
lblCurrentTime.setText(formatTime(newT));
});
// Pasa automáticamente a la siguiente canción
mediaPlayer.setOnEndOfMedia(this::handleNext);
// --- LA MAGIA DEL ECUALIZADOR (Spectrum Listener) ---
mediaPlayer.setAudioSpectrumNumBands(BANDS);
mediaPlayer.setAudioSpectrumInterval(0.04);
mediaPlayer.setAudioSpectrumListener((timestamp, duration, magnitudes, phases) -> {
double height = visualizerPane.getHeight();
for (int i = 0; i < BANDS; i++) {
// Las magnitudes suelen ser negativas de -60 a 0. Las pasamos a positivo 0 a 60
float mag = magnitudes[i] + 60.0f;
if (mag < 0) mag = 0;
double finalHeight = height * (mag / 60.0);
if (finalHeight < 5) finalHeight = 5; // mínimo de barra visible
final int finalI = i;
final double finalH = finalHeight;
// Actualizar interfaz en el UI Thread
Platform.runLater(() -> {
rects[finalI].setHeight(finalH);
rects[finalI].setY(height - finalH);
// Colorear de Azul hacia el Cian/Verde según lo alta que esté la barra
double intensity = finalH / height;
Color c = Color.hsb(210 - (intensity * 100), 0.8, 1.0);
rects[finalI].setFill(c);
});
}
});
mediaPlayer.play();
}
@FXML protected void handlePlayPause() {
if (mediaPlayer == null) return;
if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) {
mediaPlayer.pause(); btnPlayPause.setText("▶");
} else {
mediaPlayer.play(); btnPlayPause.setText("⏸");
}
}
// Asumimos código autoexplicativo extra no mostrado para prev, stop, etc. y un utilitario formatTime.
}
Descarga el Código Completo
Si deseas estudiar el código de manera directa o usarlo de base para armar el próximo Spotify en Java, descarga el proyecto base completo armado con nuestro script dinámico:
¡A disfrutar de tu música con un toque técnico inolvidable!