From e4936f0f5c85c99cb149c9c6b1d49f96ccd8b212 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Fri, 10 Apr 2026 11:47:04 +0200 Subject: [PATCH] added deezer integration --- android/AndroidManifest.xml | 1 + .../android/AndroidAudioPlayer.java | 67 ++++++++++++++ .../beatbattle/android/AndroidLauncher.java | 4 +- .../android/DeezerMusicService.java | 67 ++++++++++++++ .../java/group07/beatbattle/BeatBattle.java | 14 +++ .../group07/beatbattle/audio/AudioPlayer.java | 8 ++ .../controller/LobbyController.java | 89 ++++++++++++------- .../controller/RoundController.java | 4 + .../beatbattle/ecs/systems/AudioSystem.java | 24 +++-- .../beatbattle/service/MusicService.java | 5 ++ .../service/MusicServiceCallback.java | 9 ++ .../beatbattle/view/GameRoundView.java | 5 +- .../beatbattle/lwjgl3/Lwjgl3Launcher.java | 4 +- .../beatbattle/lwjgl3/MockMusicService.java | 28 ++++++ .../beatbattle/lwjgl3/NoOpAudioPlayer.java | 11 +++ 15 files changed, 296 insertions(+), 44 deletions(-) create mode 100644 android/src/main/java/group07/beatbattle/android/AndroidAudioPlayer.java create mode 100644 android/src/main/java/group07/beatbattle/android/DeezerMusicService.java create mode 100644 core/src/main/java/group07/beatbattle/audio/AudioPlayer.java create mode 100644 core/src/main/java/group07/beatbattle/service/MusicService.java create mode 100644 core/src/main/java/group07/beatbattle/service/MusicServiceCallback.java create mode 100644 lwjgl3/src/main/java/group07/beatbattle/lwjgl3/MockMusicService.java create mode 100644 lwjgl3/src/main/java/group07/beatbattle/lwjgl3/NoOpAudioPlayer.java diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 5ca6154..fcf25cd 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -1,6 +1,7 @@ + { + Gdx.app.error("AndroidAudioPlayer", "MediaPlayer error what=" + what + " extra=" + extra); + return false; + }); + mediaPlayer.prepareAsync(); + } catch (Exception e) { + Gdx.app.error("AndroidAudioPlayer", "Failed to play: " + url, e); + } + } + + @Override + public void stop() { + if (mediaPlayer != null) { + try { mediaPlayer.stop(); } catch (IllegalStateException ignored) {} + mediaPlayer.release(); + mediaPlayer = null; + } + } + + @Override + public void setMuted(boolean muted) { + this.muted = muted; + if (mediaPlayer != null) { + float vol = muted ? 0f : 1f; + mediaPlayer.setVolume(vol, vol); + } + } + + @Override + public void dispose() { + stop(); + } +} diff --git a/android/src/main/java/group07/beatbattle/android/AndroidLauncher.java b/android/src/main/java/group07/beatbattle/android/AndroidLauncher.java index af52a6f..efee2c9 100644 --- a/android/src/main/java/group07/beatbattle/android/AndroidLauncher.java +++ b/android/src/main/java/group07/beatbattle/android/AndroidLauncher.java @@ -23,6 +23,8 @@ protected void onCreate(Bundle savedInstanceState) { FirestoreSessionRepository sessionRepository = new FirestoreSessionRepository(firestore); FirebaseGateway firebaseGateway = new AndroidFirebaseGateway(sessionRepository); - initialize(new BeatBattle(firebaseGateway), configuration); + BeatBattle game = new BeatBattle(firebaseGateway); + game.setServices(new DeezerMusicService(), new AndroidAudioPlayer(this)); + initialize(game, configuration); } } diff --git a/android/src/main/java/group07/beatbattle/android/DeezerMusicService.java b/android/src/main/java/group07/beatbattle/android/DeezerMusicService.java new file mode 100644 index 0000000..1cd07d9 --- /dev/null +++ b/android/src/main/java/group07/beatbattle/android/DeezerMusicService.java @@ -0,0 +1,67 @@ +package group07.beatbattle.android; + +import com.badlogic.gdx.Gdx; + +import group07.beatbattle.model.Song; +import group07.beatbattle.service.MusicService; +import group07.beatbattle.service.MusicServiceCallback; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DeezerMusicService implements MusicService { + + private static final String CHART_URL = "https://api.deezer.com/chart/0/tracks?limit=50"; + + @Override + public void fetchTracks(int count, MusicServiceCallback callback) { + new Thread(() -> { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(CHART_URL).openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10_000); + conn.setReadTimeout(10_000); + + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) sb.append(line); + } + conn.disconnect(); + + JSONObject json = new JSONObject(sb.toString()); + JSONArray data = json.getJSONArray("data"); + + List songs = new ArrayList<>(); + for (int i = 0; i < data.length(); i++) { + JSONObject track = data.getJSONObject(i); + String id = String.valueOf(track.getLong("id")); + String title = track.getString("title"); + String artist = track.getJSONObject("artist").getString("name"); + String preview = track.optString("preview", ""); + String cover = track.getJSONObject("album").optString("cover_medium", ""); + if (!preview.isEmpty()) { + songs.add(new Song(id, title, artist, preview, cover)); + } + } + + Collections.shuffle(songs); + List result = new ArrayList<>(songs.subList(0, Math.min(count, songs.size()))); + + Gdx.app.postRunnable(() -> callback.onSuccess(result)); + + } catch (Exception e) { + Gdx.app.postRunnable(() -> callback.onFailure(e.getMessage())); + } + }).start(); + } +} diff --git a/core/src/main/java/group07/beatbattle/BeatBattle.java b/core/src/main/java/group07/beatbattle/BeatBattle.java index 12ec3d1..0421a32 100644 --- a/core/src/main/java/group07/beatbattle/BeatBattle.java +++ b/core/src/main/java/group07/beatbattle/BeatBattle.java @@ -5,9 +5,11 @@ import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; +import group07.beatbattle.audio.AudioPlayer; import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.controller.LobbyController; import group07.beatbattle.ecs.Engine; +import group07.beatbattle.service.MusicService; import group07.beatbattle.states.StartState; import group07.beatbattle.states.StateManager; import group07.beatbattle.ecs.systems.AudioSystem; @@ -28,6 +30,17 @@ public BeatBattle(FirebaseGateway firebaseGateway) { this.firebaseGateway = firebaseGateway; } + private MusicService musicService; + private AudioPlayer audioPlayer; + + public void setServices(MusicService musicService, AudioPlayer audioPlayer) { + this.musicService = musicService; + this.audioPlayer = audioPlayer; + } + + public MusicService getMusicService() { return musicService; } + public AudioPlayer getAudioPlayer() { return audioPlayer; } + @Override public void create() { montserratFont = loadFont("fonts/Montserrat-Regular.ttf", 54); @@ -37,6 +50,7 @@ public void create() { Engine engine = Engine.getInstance(); engine.addSystem(RoundSystem.getInstance()); engine.addSystem(AudioSystem.getInstance()); + AudioSystem.getInstance().setAudioPlayer(audioPlayer); LobbyController lobbyController = new LobbyController(this); StateManager.getInstance().setState(new StartState(this, lobbyController)); diff --git a/core/src/main/java/group07/beatbattle/audio/AudioPlayer.java b/core/src/main/java/group07/beatbattle/audio/AudioPlayer.java new file mode 100644 index 0000000..e62905c --- /dev/null +++ b/core/src/main/java/group07/beatbattle/audio/AudioPlayer.java @@ -0,0 +1,8 @@ +package group07.beatbattle.audio; + +public interface AudioPlayer { + void play(String url); + void stop(); + void setMuted(boolean muted); + void dispose(); +} diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index fe3780e..cb10c4f 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -7,9 +7,9 @@ import group07.beatbattle.model.GameSession; import group07.beatbattle.model.Player; import group07.beatbattle.model.Question; -import group07.beatbattle.model.SessionCreationResult; import group07.beatbattle.model.Song; import group07.beatbattle.model.services.LobbyService; +import group07.beatbattle.service.MusicServiceCallback; import group07.beatbattle.states.InRoundState; import group07.beatbattle.states.JoinCreateState; import group07.beatbattle.states.LobbyState; @@ -17,11 +17,16 @@ import group07.beatbattle.states.StateManager; import group07.beatbattle.view.JoinCreateView; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public class LobbyController { + private static final int OPTIONS_PER_Q = 4; + private final BeatBattle game; + private int numRounds = 5; // default, overwritten when host picks rounds public LobbyController(BeatBattle game) { this.game = game; @@ -42,6 +47,8 @@ public void onReady( int totalRounds, JoinCreateView view ) { + numRounds = totalRounds; + if (mode != GameMode.CREATE) { StateManager.getInstance().setState(new LobbyState(game, mode, this)); return; @@ -58,7 +65,7 @@ public void onReady( totalRounds, new LobbyService.CreateSessionCallback() { @Override - public void onSuccess(SessionCreationResult result) { + public void onSuccess(group07.beatbattle.model.SessionCreationResult result) { Gdx.app.postRunnable(() -> { view.setLoadingState(false); view.setStatusMessage(""); @@ -99,36 +106,58 @@ public void onBackFromLobby(GameMode mode) { } public void onStartGame() { - // TODO: replace mock session with real data from Firebase/Deezer - GameSession session = new GameSession("123456", "host1", 2); - - session.addPlayer(new Player("p1", "You")); - session.addPlayer(new Player("p2", "Oda")); - session.addPlayer(new Player("p3", "Lea")); - session.addPlayer(new Player("p4", "Milos")); - session.addPlayer(new Player("p5", "Emma")); - session.addPlayer(new Player("p6", "Lucas")); - session.addPlayer(new Player("p7", "Sofia")); - session.addPlayer(new Player("p8", "Noah")); - session.addPlayer(new Player("p9", "Mia")); + int tracksNeeded = numRounds + (OPTIONS_PER_Q - 1) * numRounds; + game.getMusicService().fetchTracks(tracksNeeded, new MusicServiceCallback() { + @Override + public void onSuccess(List songs) { + buildAndStartSession(songs); + } + + @Override + public void onFailure(String error) { + Gdx.app.error("LobbyController", "Deezer fetch failed: " + error); + } + }); + } + + private void buildAndStartSession(List songs) { + GameSession session = new GameSession("123456", "host1", numRounds); + + session.addPlayer(new Player("p1", "You")); + session.addPlayer(new Player("p2", "Oda")); + session.addPlayer(new Player("p3", "Lea")); + session.addPlayer(new Player("p4", "Milos")); + session.addPlayer(new Player("p5", "Emma")); + session.addPlayer(new Player("p6", "Lucas")); + session.addPlayer(new Player("p7", "Sofia")); + session.addPlayer(new Player("p8", "Noah")); + session.addPlayer(new Player("p9", "Mia")); session.addPlayer(new Player("p10", "Elias")); session.addPlayer(new Player("p11", "Nora")); - Song song1 = new Song("1", "Smells Like Teen Spirit", "Nirvana", "", ""); - session.addQuestion(new Question("q1", song1, Arrays.asList( - "Smells Like Teen Spirit - Nirvana", - "About a Girl - Nirvana", - "Come as You Are - Nirvana", - "Lithium - Nirvana" - ), 0)); - - Song song2 = new Song("2", "Billie Jean", "Michael Jackson", "", ""); - session.addQuestion(new Question("q2", song2, Arrays.asList( - "Billie Jean - Michael Jackson", - "Thriller - Michael Jackson", - "Beat It - Michael Jackson", - "Black or White - Michael Jackson" - ), 1)); + // Songs 0..numRounds-1 are correct answers; the rest are the decoy pool + List decoyPool = new ArrayList<>(songs.subList(numRounds, songs.size())); + + for (int q = 0; q < numRounds && q < songs.size(); q++) { + Song correct = songs.get(q); + + List options = new ArrayList<>(); + options.add(correct.getTitle() + " - " + correct.getArtist()); + + int decoysAdded = 0; + for (Song decoy : decoyPool) { + if (decoysAdded >= OPTIONS_PER_Q - 1) break; + String label = decoy.getTitle() + " - " + decoy.getArtist(); + if (!options.contains(label)) { + options.add(label); + decoysAdded++; + } + } + if (!decoyPool.isEmpty()) Collections.rotate(decoyPool, OPTIONS_PER_Q - 1); + + Collections.shuffle(options); + session.addQuestion(new Question("q" + (q + 1), correct, options, q)); + } RoundController roundController = new RoundController(game, session.getCurrentQuestion(), session); StateManager.getInstance().setState(new InRoundState(game, roundController)); diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index 8343375..45e65a0 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -5,6 +5,7 @@ import group07.beatbattle.ecs.components.TimerComponent; import group07.beatbattle.ecs.entities.Entity; import group07.beatbattle.ecs.entities.RoundFactory; +import group07.beatbattle.ecs.systems.AudioSystem; import group07.beatbattle.model.GameSession; import group07.beatbattle.model.Leaderboard; import group07.beatbattle.model.Player; @@ -34,6 +35,7 @@ public RoundController(BeatBattle game, Question question, GameSession session) question.getSong().getPreviewUrl() ); Engine.getInstance().addEntity(roundEntity); + AudioSystem.getInstance().play(roundEntity); } public Question getQuestion() { @@ -75,6 +77,7 @@ public void onRoundExpired() { } public void onLeaveSession() { + AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); } @@ -93,6 +96,7 @@ private void simulateMockPlayers() { private void transitionToLeaderboard() { simulateMockPlayers(); + AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); Leaderboard leaderboard = new Leaderboard(session.getPlayers()); LeaderboardController leaderboardController = new LeaderboardController(game, session, leaderboard); diff --git a/core/src/main/java/group07/beatbattle/ecs/systems/AudioSystem.java b/core/src/main/java/group07/beatbattle/ecs/systems/AudioSystem.java index a891b4f..015be42 100644 --- a/core/src/main/java/group07/beatbattle/ecs/systems/AudioSystem.java +++ b/core/src/main/java/group07/beatbattle/ecs/systems/AudioSystem.java @@ -1,13 +1,14 @@ package group07.beatbattle.ecs.systems; -import com.badlogic.gdx.audio.Music; +import group07.beatbattle.audio.AudioPlayer; import group07.beatbattle.ecs.components.AudioComponent; import group07.beatbattle.ecs.entities.Entity; + import java.util.List; public class AudioSystem extends EntitySystem { private static AudioSystem instance; - private Music currentMusic; + private AudioPlayer audioPlayer; private AudioSystem() {} @@ -16,28 +17,35 @@ public static AudioSystem getInstance() { return instance; } + public void setAudioPlayer(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + } + @Override public void update(List entities, float delta) { // playback is event-driven, triggered by play/stop calls } - public void play(Entity round, Music music) { + public void play(Entity round) { + if (audioPlayer == null) return; AudioComponent audio = round.getComponent(AudioComponent.class); if (audio == null) return; - if (currentMusic != null) currentMusic.stop(); - currentMusic = music; - currentMusic.play(); + audioPlayer.play(audio.previewUrl); audio.isPlaying = true; } public void stop(Entity round) { AudioComponent audio = round.getComponent(AudioComponent.class); if (audio == null) return; - if (currentMusic != null) currentMusic.stop(); + if (audioPlayer != null) audioPlayer.stop(); audio.isPlaying = false; } public void setMuted(boolean muted) { - if (currentMusic != null) currentMusic.setVolume(muted ? 0f : 1f); + if (audioPlayer != null) audioPlayer.setMuted(muted); + } + + public void dispose() { + if (audioPlayer != null) audioPlayer.dispose(); } } diff --git a/core/src/main/java/group07/beatbattle/service/MusicService.java b/core/src/main/java/group07/beatbattle/service/MusicService.java new file mode 100644 index 0000000..86dfbcf --- /dev/null +++ b/core/src/main/java/group07/beatbattle/service/MusicService.java @@ -0,0 +1,5 @@ +package group07.beatbattle.service; + +public interface MusicService { + void fetchTracks(int count, MusicServiceCallback callback); +} diff --git a/core/src/main/java/group07/beatbattle/service/MusicServiceCallback.java b/core/src/main/java/group07/beatbattle/service/MusicServiceCallback.java new file mode 100644 index 0000000..cbea090 --- /dev/null +++ b/core/src/main/java/group07/beatbattle/service/MusicServiceCallback.java @@ -0,0 +1,9 @@ +package group07.beatbattle.service; + +import group07.beatbattle.model.Song; +import java.util.List; + +public interface MusicServiceCallback { + void onSuccess(List songs); + void onFailure(String error); +} diff --git a/core/src/main/java/group07/beatbattle/view/GameRoundView.java b/core/src/main/java/group07/beatbattle/view/GameRoundView.java index 7558786..c273e8b 100644 --- a/core/src/main/java/group07/beatbattle/view/GameRoundView.java +++ b/core/src/main/java/group07/beatbattle/view/GameRoundView.java @@ -110,10 +110,7 @@ public void changed(ChangeEvent event, Actor actor) { infoStyle.font = game.getMontserratFont(); infoStyle.fontColor = Color.LIGHT_GRAY; - Label songLabel = new Label( - question.getSong().getTitle() + " — " + question.getSong().getArtist(), - infoStyle - ); + Label songLabel = new Label("Now Playing...", infoStyle); songLabel.setWrap(true); root.add(songLabel).fillX().center().padBottom(60f).row(); diff --git a/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/Lwjgl3Launcher.java b/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/Lwjgl3Launcher.java index 90dacac..cd624f7 100644 --- a/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/Lwjgl3Launcher.java +++ b/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/Lwjgl3Launcher.java @@ -13,7 +13,9 @@ public static void main(String[] args) { } private static Lwjgl3Application createApplication() { - return new Lwjgl3Application(new BeatBattle(new NoOpFirebaseGateway()), getDefaultConfiguration()); + BeatBattle game = new BeatBattle(new NoOpFirebaseGateway()); + game.setServices(new MockMusicService(), new NoOpAudioPlayer()); + return new Lwjgl3Application(game, getDefaultConfiguration()); } private static Lwjgl3ApplicationConfiguration getDefaultConfiguration() { diff --git a/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/MockMusicService.java b/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/MockMusicService.java new file mode 100644 index 0000000..c196dcf --- /dev/null +++ b/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/MockMusicService.java @@ -0,0 +1,28 @@ +package group07.beatbattle.lwjgl3; + +import group07.beatbattle.model.Song; +import group07.beatbattle.service.MusicService; +import group07.beatbattle.service.MusicServiceCallback; + +import java.util.ArrayList; +import java.util.List; + +/** Desktop stub — returns hardcoded songs so the game is playable without a network. */ +public class MockMusicService implements MusicService { + + @Override + public void fetchTracks(int count, MusicServiceCallback callback) { + List songs = new ArrayList<>(); + songs.add(new Song("1", "Smells Like Teen Spirit", "Nirvana", "", "")); + songs.add(new Song("2", "Billie Jean", "Michael Jackson", "", "")); + songs.add(new Song("3", "Bohemian Rhapsody", "Queen", "", "")); + songs.add(new Song("4", "Hotel California", "Eagles", "", "")); + songs.add(new Song("5", "Shape of You", "Ed Sheeran", "", "")); + songs.add(new Song("6", "Rolling in the Deep", "Adele", "", "")); + songs.add(new Song("7", "Blinding Lights", "The Weeknd", "", "")); + songs.add(new Song("8", "One", "Metallica", "", "")); + songs.add(new Song("9", "Stairway to Heaven", "Led Zeppelin", "", "")); + songs.add(new Song("10", "Lose Yourself", "Eminem", "", "")); + callback.onSuccess(songs.subList(0, Math.min(count, songs.size()))); + } +} diff --git a/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/NoOpAudioPlayer.java b/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/NoOpAudioPlayer.java new file mode 100644 index 0000000..55f3a9c --- /dev/null +++ b/lwjgl3/src/main/java/group07/beatbattle/lwjgl3/NoOpAudioPlayer.java @@ -0,0 +1,11 @@ +package group07.beatbattle.lwjgl3; + +import group07.beatbattle.audio.AudioPlayer; + +/** Desktop stub — no audio streaming support on desktop. */ +public class NoOpAudioPlayer implements AudioPlayer { + @Override public void play(String url) {} + @Override public void stop() {} + @Override public void setMuted(boolean muted) {} + @Override public void dispose() {} +}