Skip to content
Snippets Groups Projects
Commit 43872c7c authored by Sofiane Lasri's avatar Sofiane Lasri
Browse files

feat: add GUI interface and refactor radio player

- Added Tkinter-based GUI (radio_gui.py) for real-time radio monitoring
  * Displays current track, progress, pattern, and station info
  * Shows playback statistics and session duration
  * Includes volume control and station switching
- Refactored radio.py to integrate with GUI:
  * Added threading support for concurrent GUI and playback
  * Implemented track progress reporting
  * Added station switching functionality
- Reorganized configuration:
  * Deleted example.config.py (replaced by actual config.py)
  * Created proper config.py for production use
- Updated .gitignore to track config.py
- Added README.md with project description
- Enhanced defs.py with progress tracking and time formatting
  * Modified playSound functions to update GUI during playback
  * Added player reference management for volume control
- Added requirements.txt with dependencies
parent d8e28b8d
No related branches found
No related tags found
No related merge requests found
Pipeline #1101 canceled
.idea
\ No newline at end of file
config.py
\ No newline at end of file
README.md 0 → 100644
# Python GTA V Radios
Ce projet représente un script Python capable de reproduire fidèlement le comportement des stations de radio de GTA V, en utilisant les fichiers audio originaux du jeu. L'application recrée l'expérience radio réaliste du jeu, incluant la musique, les publicités, les actualités et l'identification des stations.
## Fonctionnalités
- **Simulation authentique** : Reproduit le comportement réaliste des radios GTA V
- **Stations multiples** : Support de plusieurs stations (Non-Stop-Pop, SilverLake, Funk, etc.)
- **Contenu varié** : Musique, publicités, actualités, identifiants de station
- **Séquençage intelligent** : Utilise des patterns configurables pour organiser le contenu
- **Gestion de la répétition** : Évite la répétition du contenu déjà joué
- **Contrôle du volume** : Volume ajustable pour tous les types de contenu
## Architecture
### Composants principaux
- **radio.py** : Point d'entrée principal et implémentation de la classe Radio
- Gère la logique de lecture des stations
- Traite les séquences de contenu basées sur des patterns
- Suit le contenu joué pour éviter les répétitions
- Sélectionne aléatoirement parmi les stations disponibles
- **defs.py** : Gestion des fichiers audio et utilitaires de lecture
- Découverte de fichiers via des patterns regex
- Lecture audio basée sur VLC avec contrôle du volume
- Fonctionnalité d'overlay intro/musique avec timing différé
- Filtrage des fichiers audio par extension
- **config.py** : Gestion de la configuration (copier depuis example.config.py)
- Patterns audio et probabilités
- Patterns regex de correspondance de fichiers
- Paramètres de lecture (volume, délais, extensions de fichiers)
## Organisation des fichiers audio
Le projet attend une structure de répertoires spécifique avec les fichiers audio organisés par station de radio :
```
├── non-stop-pop/ # Station Non-Stop-Pop
├── silverlake/ # Station SilverLake
├── funk/ # Station Funk
├── radio-adverts/ # Publicités partagées
└── radio-news/ # Actualités partagées
```
Chaque station contient des sous-répertoires catégorisés pour différents types de contenu.
## Types de contenu et patterns
- **IDs** : Clips d'identification de station
- **General** : Contenu général de station/commentaires DJ
- **Mono Solos** : Courts segments solo
- **Music** : Chansons complètes avec overlays d'intro optionnels
- **Ads** : Publicités depuis le pool partagé
- **News** : Segments d'actualités depuis le pool partagé
Le système utilise des patterns configurables pour séquencer le contenu de manière réaliste (ex: "ID, GENERAL, MUSIC" ou "AD, AD, NEWS").
## Installation et utilisation
### Prérequis
```bash
# Installer les dépendances
pip install -r requirements.txt
```
### Configuration
```bash
# Copier le fichier de configuration (requis avant la première exécution)
cp example.config.py config.py
```
Éditez `config.py` pour personnaliser :
- `debug` : Activer/désactiver la sortie de débogage
- `volume` : Volume audio par défaut (0-100)
- `adsProbability` : Chance de jouer des pubs/actualités entre les musiques
- `musicPatterns` : Patterns de séquence pour les segments musicaux
- `adsAndNewsPatterns` : Patterns de séquence pour les pubs/actualités
### Exécution
```bash
# Lancer la simulation radio
python radio.py
```
## Exigences des fichiers
- Python avec le module `vlc` pour la lecture audio
- Les fichiers audio doivent correspondre à `filesExtension` configuré (défaut : .wav)
- La structure des répertoires doit correspondre à l'organisation attendue
## Développement
Le projet suit une architecture modulaire permettant une extension facile avec de nouvelles stations et types de contenu. Les patterns de configuration permettent un contrôle fin du comportement de diffusion.
## Licence
Ce projet est destiné à un usage éducatif et personnel. Assurez-vous de respecter les droits d'auteur des fichiers audio utilisés.
File moved
......@@ -22,21 +22,54 @@ def getFilesByRegex(folderToScan, regex):
return audioFiles
def playSound(soundFile):
def playSound(soundFile, radio_instance=None):
if config.debug:
print("Playing: " + soundFile)
# We will play the sound file
player = vlc.MediaPlayer(soundFile)
instance = vlc.Instance()
player = instance.media_player_new()
media = instance.media_new(soundFile)
player.set_media(media)
player.audio_set_volume(config.volume)
player.play()
# We will wait for the sound to finish
# Store player reference in radio instance
if radio_instance:
radio_instance.current_player = player
# We will wait for the sound to finish and update progress
Ended = 6
current_state = player.get_state()
while current_state != Ended:
current_state = player.get_state()
# Update progress if we have a radio instance with GUI
if radio_instance and radio_instance.gui:
try:
length = player.get_length() / 1000 # Convert to seconds
position = player.get_time() / 1000 # Convert to seconds
if length > 0:
length_str = format_time(length)
position_str = format_time(position)
progress_str = f"{position_str} / {length_str}"
radio_instance.current_track_info['progress'] = progress_str
radio_instance.gui.update_current_track(radio_instance.current_track_info)
except:
pass # Ignore errors in progress tracking
time.sleep(0.1) # Small delay to prevent excessive updates
# Clear player reference
if radio_instance:
radio_instance.current_player = None
radio_instance.current_track_info['playing'] = False
if radio_instance.gui:
radio_instance.gui.update_current_track(radio_instance.current_track_info)
if config.debug:
print("Done playing: " + soundFile)
......@@ -54,7 +87,7 @@ def getMusicIntroFiles(filesPath, musicName):
return audioFiles
def playSoundWithDelayedSecondSound(firstSound, secondSound):
def playSoundWithDelayedSecondSound(firstSound, secondSound, radio_instance=None):
delay = random.randint(config.musicMinIntroDelay, config.musicMaxIntroDelay)
if config.debug:
......@@ -73,6 +106,10 @@ def playSoundWithDelayedSecondSound(firstSound, secondSound):
players[0].audio_set_volume(config.volume)
players[0].play()
# Store player reference in radio instance (use main music player)
if radio_instance:
radio_instance.current_player = players[0]
# We wait
time.sleep(delay)
......@@ -88,6 +125,7 @@ def playSoundWithDelayedSecondSound(firstSound, secondSound):
current_state = players[1].get_state()
while current_state != Ended:
current_state = players[1].get_state()
time.sleep(0.1)
if config.debug:
print("Done playing Intro: " + secondSound)
......@@ -100,5 +138,37 @@ def playSoundWithDelayedSecondSound(firstSound, secondSound):
while current_state != Ended:
current_state = players[0].get_state()
# Update progress during main song playback
if radio_instance and radio_instance.gui:
try:
length = players[0].get_length() / 1000 # Convert to seconds
position = players[0].get_time() / 1000 # Convert to seconds
if length > 0:
length_str = format_time(length)
position_str = format_time(position)
progress_str = f"{position_str} / {length_str}"
radio_instance.current_track_info['progress'] = progress_str
radio_instance.gui.update_current_track(radio_instance.current_track_info)
except:
pass # Ignore errors in progress tracking
time.sleep(0.1)
# Clear player reference
if radio_instance:
radio_instance.current_player = None
radio_instance.current_track_info['playing'] = False
if radio_instance.gui:
radio_instance.gui.update_current_track(radio_instance.current_track_info)
if config.debug:
print("Done playing Music: " + firstSound)
def format_time(seconds):
"""Format seconds into MM:SS format"""
minutes = int(seconds // 60)
seconds = int(seconds % 60)
return f"{minutes:02d}:{seconds:02d}"
......@@ -2,6 +2,7 @@ import os
import random
import config
import defs
import threading
class Radio:
......@@ -19,12 +20,30 @@ class Radio:
playedNews = []
playedMusics = []
playedMonoSolos = []
def __init__(self, radioName):
current_player = None
gui = None
current_track_info = {
'name': 'No track playing',
'type': 'NONE',
'progress': '0:00 / 0:00',
'playing': False,
'pattern': None
}
def __init__(self, radioName, gui_instance=None):
self.radioPath = os.getcwd() + "/" + radioName
self.adsPath = os.getcwd() + "/radio-adverts"
self.newsPath = os.getcwd() + "/radio-news"
self.radioName = radioName
self.gui = gui_instance
self.current_player = None
self.current_track_info = {
'name': 'No track playing',
'type': 'NONE',
'progress': '0:00 / 0:00',
'playing': False,
'pattern': None
}
print("Playing: " + self.radioName)
self.ids = defs.getFilesByRegex(self.radioPath, config.idPattern)
......@@ -56,21 +75,34 @@ class Radio:
if config.debug:
print("Pattern: " + str(pattern))
# Update pattern info
self.current_track_info['pattern'] = "".join(pattern)
if self.gui:
self.gui.update_current_track(self.current_track_info)
# We will now play the pattern
for item in pattern:
if item == "ID":
defs.playSound(random.choice(self.ids))
track = random.choice(self.ids)
self.update_track_info(track, "ID")
defs.playSound(track, self)
elif item == "GENERAL":
defs.playSound(random.choice(self.generals))
track = random.choice(self.generals)
self.update_track_info(track, "GENERAL")
defs.playSound(track, self)
elif item == "MONO_SOLO":
defs.playSound(random.choice(self.monoSolos))
track = random.choice(self.monoSolos)
self.update_track_info(track, "MONO_SOLO")
defs.playSound(track, self)
elif item == "MUSIC":
introFiles = defs.getMusicIntroFiles(self.radioPath, musicName)
if len(introFiles) > 0:
intro = random.choice(introFiles)
defs.playSoundWithDelayedSecondSound(music, intro)
self.update_track_info(music, "MUSIC (with intro)")
defs.playSoundWithDelayedSecondSound(music, intro, self)
else:
defs.playSound(music)
self.update_track_info(music, "MUSIC")
defs.playSound(music, self)
def playAdAndNews(self):
# We choose random pattern
......@@ -79,14 +111,25 @@ class Radio:
if config.debug:
print("Pattern: " + str(pattern))
# Update pattern info
self.current_track_info['pattern'] = "".join(pattern)
if self.gui:
self.gui.update_current_track(self.current_track_info)
# We will now play the pattern
for item in pattern:
if item == "AD":
defs.playSound(self.chooseRandomUnplayedTrack("ad"))
track = self.chooseRandomUnplayedTrack("ad")
self.update_track_info(track, "ADVERTISEMENT")
defs.playSound(track, self)
elif item == "NEWS":
defs.playSound(self.chooseRandomUnplayedTrack("news"))
track = self.chooseRandomUnplayedTrack("news")
self.update_track_info(track, "NEWS")
defs.playSound(track, self)
elif item == "MONO_SOLO":
defs.playSound(self.chooseRandomUnplayedTrack("monoSolo"))
track = self.chooseRandomUnplayedTrack("monoSolo")
self.update_track_info(track, "MONO_SOLO")
defs.playSound(track, self)
def chooseRandomUnplayedTrack(self, trackType):
if trackType == "music":
......@@ -126,13 +169,50 @@ class Radio:
return track
def update_track_info(self, track_path, track_type):
track_name = track_path.split("/")[-1].replace("_", " ").split(".")[0]
self.current_track_info['name'] = track_name
self.current_track_info['type'] = track_type
self.current_track_info['playing'] = True
if self.gui:
self.gui.update_current_track(self.current_track_info)
def switch_station(self, new_station):
self.radioName = new_station
self.radioPath = os.getcwd() + "/" + new_station
# Reload station data
self.ids = defs.getFilesByRegex(self.radioPath, config.idPattern)
self.monoSolos = defs.getFilesByRegex(self.radioPath, config.monoSoloPattern)
self.generals = defs.getFilesByRegex(self.radioPath, config.generalPattern)
self.musics = defs.getFilesByRegex(self.radioPath, config.musicPatten)
# Reset played lists for new station
self.playedMusics = []
self.playedMonoSolos = []
print(f"Switched to: {self.radioName}")
if __name__ == "__main__":
# Create GUI first
from radio_gui import RadioGUI
radiosStations = ["non-stop-pop", "silverlake", "funk"]
chosenRadio = random.choice(radiosStations)
try:
# Create radio instance with GUI
radio = Radio(chosenRadio)
radio.startRadio()
gui = RadioGUI(radio)
radio.gui = gui
# Start radio in separate thread
radio_thread = threading.Thread(target=radio.startRadio, daemon=True)
radio_thread.start()
# Run GUI in main thread
gui.run()
except KeyboardInterrupt:
print("Exiting...")
exit(0)
import tkinter as tk
from tkinter import ttk
import threading
import time
from datetime import datetime
class RadioGUI:
def __init__(self, radio_instance):
self.radio = radio_instance
self.window = tk.Tk()
self.window.title("GTA V Radio Simulator")
self.window.geometry("800x600")
self.window.configure(bg='#1a1a1a')
# Variables for tracking state
self.current_song = tk.StringVar(value="No song playing")
self.current_station = tk.StringVar(value=self.radio.radioName.upper())
self.current_pattern = tk.StringVar(value="Initializing...")
self.current_progress = tk.StringVar(value="0:00 / 0:00")
self.volume_var = tk.IntVar(value=50)
self.is_playing = tk.BooleanVar(value=False)
self.current_type = tk.StringVar(value="UNKNOWN")
self.played_tracks_count = tk.StringVar(value="0")
self.session_time = tk.StringVar(value="0:00:00")
# Start time for session tracking
self.session_start = datetime.now()
self.setup_ui()
self.start_update_thread()
def setup_ui(self):
# Main container
main_frame = tk.Frame(self.window, bg='#1a1a1a')
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# Title
title_label = tk.Label(main_frame, text="GTA V RADIO SIMULATOR",
font=('Arial', 20, 'bold'), fg='#00ff00', bg='#1a1a1a')
title_label.pack(pady=(0, 20))
# Station Info Frame
station_frame = tk.LabelFrame(main_frame, text="Station Info",
font=('Arial', 12, 'bold'), fg='#ffffff', bg='#2a2a2a')
station_frame.pack(fill=tk.X, pady=(0, 10))
tk.Label(station_frame, text="Current Station:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=0, column=0, sticky='w', padx=10, pady=5)
tk.Label(station_frame, textvariable=self.current_station, font=('Arial', 10),
fg='#00ff00', bg='#2a2a2a').grid(row=0, column=1, sticky='w', padx=10, pady=5)
tk.Label(station_frame, text="Session Time:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=1, column=0, sticky='w', padx=10, pady=5)
tk.Label(station_frame, textvariable=self.session_time, font=('Arial', 10),
fg='#00ff00', bg='#2a2a2a').grid(row=1, column=1, sticky='w', padx=10, pady=5)
# Now Playing Frame
playing_frame = tk.LabelFrame(main_frame, text="Now Playing",
font=('Arial', 12, 'bold'), fg='#ffffff', bg='#2a2a2a')
playing_frame.pack(fill=tk.X, pady=(0, 10))
tk.Label(playing_frame, text="Track:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=0, column=0, sticky='w', padx=10, pady=5)
tk.Label(playing_frame, textvariable=self.current_song, font=('Arial', 10),
fg='#ffff00', bg='#2a2a2a', wraplength=500).grid(row=0, column=1, sticky='w', padx=10, pady=5)
tk.Label(playing_frame, text="Type:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=1, column=0, sticky='w', padx=10, pady=5)
tk.Label(playing_frame, textvariable=self.current_type, font=('Arial', 10),
fg='#ff8800', bg='#2a2a2a').grid(row=1, column=1, sticky='w', padx=10, pady=5)
tk.Label(playing_frame, text="Progress:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=2, column=0, sticky='w', padx=10, pady=5)
tk.Label(playing_frame, textvariable=self.current_progress, font=('Arial', 10),
fg='#00ffff', bg='#2a2a2a').grid(row=2, column=1, sticky='w', padx=10, pady=5)
# Pattern Frame
pattern_frame = tk.LabelFrame(main_frame, text="Current Pattern",
font=('Arial', 12, 'bold'), fg='#ffffff', bg='#2a2a2a')
pattern_frame.pack(fill=tk.X, pady=(0, 10))
tk.Label(pattern_frame, textvariable=self.current_pattern, font=('Arial', 10),
fg='#ff00ff', bg='#2a2a2a', wraplength=700).pack(padx=10, pady=10)
# Statistics Frame
stats_frame = tk.LabelFrame(main_frame, text="Statistics",
font=('Arial', 12, 'bold'), fg='#ffffff', bg='#2a2a2a')
stats_frame.pack(fill=tk.X, pady=(0, 10))
stats_grid = tk.Frame(stats_frame, bg='#2a2a2a')
stats_grid.pack(fill=tk.X, padx=10, pady=10)
# Music stats
tk.Label(stats_grid, text="Played Music:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=0, column=0, sticky='w', padx=(0, 20))
tk.Label(stats_grid, text=f"{len(self.radio.playedMusics)}/{len(self.radio.musics)}",
font=('Arial', 10), fg='#00ff00', bg='#2a2a2a').grid(row=0, column=1, sticky='w')
# Ads stats
tk.Label(stats_grid, text="Played Ads:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=0, column=2, sticky='w', padx=(40, 20))
tk.Label(stats_grid, text=f"{len(self.radio.playedAds)}/{len(self.radio.ads)}",
font=('Arial', 10), fg='#00ff00', bg='#2a2a2a').grid(row=0, column=3, sticky='w')
# News stats
tk.Label(stats_grid, text="Played News:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=1, column=0, sticky='w', padx=(0, 20))
tk.Label(stats_grid, text=f"{len(self.radio.playedNews)}/{len(self.radio.news)}",
font=('Arial', 10), fg='#00ff00', bg='#2a2a2a').grid(row=1, column=1, sticky='w')
# Mono Solo stats
tk.Label(stats_grid, text="Played Mono Solos:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=1, column=2, sticky='w', padx=(40, 20))
tk.Label(stats_grid, text=f"{len(self.radio.playedMonoSolos)}/{len(self.radio.monoSolos)}",
font=('Arial', 10), fg='#00ff00', bg='#2a2a2a').grid(row=1, column=3, sticky='w')
# Control Frame
control_frame = tk.LabelFrame(main_frame, text="Controls",
font=('Arial', 12, 'bold'), fg='#ffffff', bg='#2a2a2a')
control_frame.pack(fill=tk.X, pady=(0, 10))
control_grid = tk.Frame(control_frame, bg='#2a2a2a')
control_grid.pack(fill=tk.X, padx=10, pady=10)
# Volume control
tk.Label(control_grid, text="Volume:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=0, column=0, sticky='w', padx=(0, 10))
volume_scale = tk.Scale(control_grid, from_=0, to=100, orient=tk.HORIZONTAL,
variable=self.volume_var, command=self.on_volume_change,
bg='#2a2a2a', fg='#ffffff', highlightbackground='#2a2a2a')
volume_scale.grid(row=0, column=1, sticky='ew', padx=(0, 20))
# Station buttons
tk.Label(control_grid, text="Switch Station:", font=('Arial', 10, 'bold'),
fg='#ffffff', bg='#2a2a2a').grid(row=0, column=2, sticky='w', padx=(20, 10))
station_buttons = tk.Frame(control_grid, bg='#2a2a2a')
station_buttons.grid(row=0, column=3, sticky='w')
stations = ["non-stop-pop", "silverlake", "funk"]
for i, station in enumerate(stations):
btn = tk.Button(station_buttons, text=station.replace('-', ' ').title(),
command=lambda s=station: self.switch_station(s),
bg='#444444', fg='#ffffff', font=('Arial', 8))
btn.pack(side=tk.LEFT, padx=2)
# Status Frame
status_frame = tk.Frame(main_frame, bg='#1a1a1a')
status_frame.pack(fill=tk.X)
self.status_label = tk.Label(status_frame, text="Status: Ready",
font=('Arial', 10), fg='#888888', bg='#1a1a1a')
self.status_label.pack(anchor='w')
def on_volume_change(self, value):
import config
config.volume = int(value)
if hasattr(self.radio, 'current_player') and self.radio.current_player:
self.radio.current_player.audio_set_volume(int(value))
def switch_station(self, station_name):
self.status_label.config(text=f"Status: Switching to {station_name}...")
self.radio.switch_station(station_name)
self.current_station.set(station_name.upper())
self.update_stats()
def update_stats(self):
# Update statistics in the GUI thread
def update():
for widget in self.window.winfo_children():
if isinstance(widget, tk.Frame):
for child in widget.winfo_children():
if isinstance(child, tk.LabelFrame) and child.cget('text') == 'Statistics':
stats_grid = child.winfo_children()[0]
# Update music stats
stats_grid.winfo_children()[1].config(
text=f"{len(self.radio.playedMusics)}/{len(self.radio.musics)}")
# Update ads stats
stats_grid.winfo_children()[3].config(
text=f"{len(self.radio.playedAds)}/{len(self.radio.ads)}")
# Update news stats
stats_grid.winfo_children()[5].config(
text=f"{len(self.radio.playedNews)}/{len(self.radio.news)}")
# Update mono solo stats
stats_grid.winfo_children()[7].config(
text=f"{len(self.radio.playedMonoSolos)}/{len(self.radio.monoSolos)}")
self.window.after(0, update)
def update_current_track(self, track_info):
self.current_song.set(track_info.get('name', 'Unknown'))
self.current_type.set(track_info.get('type', 'UNKNOWN'))
self.current_progress.set(track_info.get('progress', '0:00 / 0:00'))
self.is_playing.set(track_info.get('playing', False))
if track_info.get('pattern'):
self.current_pattern.set(f"Pattern: {track_info['pattern']}")
self.status_label.config(text=f"Status: Playing {track_info.get('type', 'Unknown')}")
self.update_stats()
def start_update_thread(self):
def update_session_time():
while True:
elapsed = datetime.now() - self.session_start
hours, remainder = divmod(int(elapsed.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
self.session_time.set(f"{hours:02d}:{minutes:02d}:{seconds:02d}")
time.sleep(1)
update_thread = threading.Thread(target=update_session_time, daemon=True)
update_thread.start()
def run(self):
self.window.mainloop()
def close(self):
self.window.quit()
self.window.destroy()
python-vlc>=3.0.18121
tkinter
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment