
Vous aussi vous en avez assez de ces musiques qui passent à la radio ? Vous aussi vous dites que ce serait quand même beaucoup plus stylé d’écouter Non-Stop Pop FM dans votre voiture ? Et bien sachez que vous êtes au bon endroit ! Dans cet article, nous verrons comment recréer la logique de base du système de radio de GTA V à partir des fichiers du jeu. 😀
Mais avant toute chose, ça va de soit mais il faut posséder le jeu. Sans lui vous ne pourrez pas réaliser notre simulateur, je n’inclurai pas les musiques et bandes sons dans le dépôt Git du projet. 
Pour réaliser ce projet, nous aurons besoin de plusieurs outils que vous devrez installer. Si vous souhaitez créer une version “embedded” du projet, que vous installerez par exemple sur une Raspberry Pi, les dépendances marquées d’un astérisque seront également à installer sur le système embarqué.
Note : Dans cet article je vais utiliser la version Legacy de GTA V, car c’est avec cette version que j’ai initialement créé ce script. C’est également avec cette version qu’est configuré mon OpenIV. Mais il n’y a normalement pas de problème concernant l’usage de la version Enhanced.
La première étape est d’extraire les fichiers du jeu. Pour ça allons utiliser OpenIV.
Une fois ouvert, OpenIV se présente sous la forme d’un gestionnaire de fichier. - Notez cette délicieuse apparence à la Windows 7. 
\x64\audio\sfx car c’est lui qui contient les fichiers audio qui nous intéresse.
Comme vous le constatez, le dossier sfx contient des archives au format propriétaire .rpf, qui contiennent eux, les fichiers sons du jeu. Chaque archive représente une catégorie de fichier son. On y retrouve par exemple les fichiers son des animaux, du scanner de la police, mais aussi et surtout les fichiers sons des radios. Ces derniers sont préfixés par le texte RADIO_. Pour les besoins de ce projet, nous aurons besoin d’extraire le contenu des archives suivantes :
Leur extraction est obligatoire. Notre script ne fera pas de vérification sur la présence de leur fichier son, seule la ceux de la station de radio seront vérifiés. D’ailleurs, vous pouvez choisir celles qui vous intéressent le plus. Le nombre importe peu. Pour ma part, je vais choisir Non-Stop Pop FM car c’est la plus complète (et ma préférée :D).

Lors de l’extraction, faites bien attention à choisir l’option “Export to WAV (.wav)”, et non pas “Extract” ou encore "Export to openFormat (.oac). Aussi, pensez bien à séparer les exports dans différents dossiers.
Ce qui est intéressant au travers de ces fichier extraits, c’est de voir la structure des dossiers. Si les archives des pubs et actualités n’ont rien de particulier, celles des stations de radio révèlent assez bien comment le jeu gère leur lecture.
Tout d’abord, chaque fichier son est placé dans un dossier du même nom, en lettres minuscules snake_case. On devine assez bien les dossiers représentants des musiques grâce à leur nom (adult_education, cooler_than_me, feel_good_inc, etc), et les jingle de la station grâce au prefix “id_”.
Mais ce n’est pas tout. Nous avons également un dossier “intro” qui contient des séquences audio avec le nom des musiques en lettres majuscules suivies d’un numéro (01, 02). Ces fichiers intro sont exactement ce qu’ils décrivent, c’est-à-dire les introduction du DJ de la station de radio à la musique qui va se jouer. Toutes les musiques n’ont pas obligatoirement une séquence d’introduction. D’ailleurs, cela varie beaucoup selon les stations de radio. On imagine assez bien que Non-Stop Pop a dû être une de celles qui reçu le plus d’attention.
Nous avons également les dossier “mono_solo_” qui représentent des séquences de monologue du DJ, “time” qui représente les annonces de l’heure sous forme de “good morning”, “good evening”. Puis nous avons le dossier “to” qui annonce l’arrivée des pubs et actualités, et enfin le dossier “general” qui comporte des courts monologues du DJ.
En résumé, voici ce que ça donne :
À partir de cette analyse, il est assez facile de déduire des règles regex qui vont nous permettre de détecter ces fichiers automatiquement.
((id)|(ID))_([0-9]){2}((mono)|(MONO))_((solo)|(SOLO))(general)|(GENERAL_([0-9]){2})Notez que ces regex sont volontairement permissives afin d’inclure les différentes casses. En ce qui concerne les musiques et leurs séquences d’introduction, c’est un peu plus compliqué car nous avons besoin de faire de la concaténation de variable. Comme pour les musiques on doit globalement chercher toutes les correspondances qui exclues les fichiers cités ci-dessus, cela donnerait ça : ^(?:(?!(" + idPattern + "|" + monoSoloPattern + "|" + generalPattern + "|(.*([A-Z])_([0-9]){2})|intro|to|time)).)+$. Très simplement, on inclue les regex précédentes dans celle-ci afin de les exclure de nos résultats de recherche.
Enfin, pour les introductions des musiques, nous avons besoin du nom de la musique en question. Donc on va dire REPLACEMEWITHMUSICTITLE_([0-9]){2}.
Pour l’instant, nous n’inclurons pas les séquences “time” et “to” à notre script. :)
Maintenant que nous connaissons la structure des fichiers, nous pouvons commencer à concevoir notre algorithme/script.
Tout d’abord, comme notre but est de reproduire le comportement d’une station de radio, nous devons intégrer la notion de paternes de lecture. Qu’est-ce que cela signifie ? Et bien, si nous nous contentons dons de lire aléatoirement les musiques récupérées, cela ne serait pas une radio mais une playlist. Nous ne pouvons pas non plus lire tous les fichiers de façon aléatoire car cela n’aurai aucun sens. Une bonne solution est de suivre des paternes de lecture prédéfinies qui seront eux, sélectionnés aléatoirement.
Pour garder les choses simples, nous allons créer 2 séries de paternes de lecture. Un pour les musiques et un autre pour les coupures publicitaires et d’actualité. Ainsi, nous pourrons plus facilement définir la portion de musique par rapport aux coupures. Voici ce que ça donne :
Paternes de musique :
Paternes de coupures :
Ce n’est pas une science exacte, ce sont des paternes que j’ai déduis avec l’habitude d’écouter les stations du jeu.
En plus des paternes de lecture, nous devons également intégrer la notion de variables dynamiques qui nous permettrons de rendre la lecture un peu plus naturelle. Un très bon exemple de variables dynamique est le délais qui se joue avant la lecture de l’introduction d’une musique par le DJ de la station. Nous pourrions utiliser des délais fixes, mais cela retirerai un peu du caractère réaliste de notre simulateur.
Ainsi, en termes de variables dynamiques à définir, nous aurons la liste suivante à prendre en compte :
En soit, les 3 premières variables ne sont pas dynamiques. Mais concernant le délais des intro, il sera lui dynamique car déterminé à partir des deux variables d’écart. Avec ces variables, nous aurons donc un suivi complet des lectures, évitant au maximum les répétitions. L’algorithme sera fait de sorte à ce que les musiques devront toutes être jouées avant de les répéter à nouveau.
Passons maintenant à la réalisation !
Je ne vais pas vous détailler le script ligne par ligne, d’abord car ce serait inintéressant à lire, mais aussi et surtout car c’est Noël et que j’ai chopé une flemmingite aigüe. 
Nous allons donc le survoler. Encore une fois, le projet est disponible sur GitHub, le lien est à la fin de cet article. :)
Afin de garder une structure relativement claire, nous allons découper notre script en 3 fichiers :
Comme précisé dans la section pré-requis de cet article, nous allons utiliser VLC comme backend pour jouer la musique.
Il s’agit là du script principal, c’est lui qui concentre la logique du système de radio. Nous avons basiquement 4 méthodes à l’intérieur :
import os
import random
from typing import Literal
import config
import defs
TrackType = Literal["music", "ad", "news", "monoSolo"]
class Radio:
radio_path: str = ""
ads_path: str = ""
news_path: str = ""
radio_name: str = ""
ids: list[str] = []
mono_solos: list[str] = []
generals: list[str] = []
musics: list[str] = []
ads: list[str] = []
news: list[str] = []
played_ads: list[str] = []
played_news: list[str] = []
played_musics: list[str] = []
played_mono_solos: list[str] = []
def __init__(self, radio_name: str) -> None:
self.radio_path = os.getcwd() + "/" + radio_name
self.ads_path = os.getcwd() + "/radio-adverts"
self.news_path = os.getcwd() + "/radio-news"
self.radio_name = radio_name
print("Playing: " + self.radio_name)
self.ids = defs.get_files_by_regex(self.radio_path, config.idPattern)
self.mono_solos = defs.get_files_by_regex(self.radio_path, config.monoSoloPattern)
self.generals = defs.get_files_by_regex(self.radio_path, config.generalPattern)
self.musics = defs.get_files_by_regex(self.radio_path, config.musicPatten)
self.ads = defs.get_files_by_regex(self.ads_path, ".*")
self.news = defs.get_files_by_regex(self.news_path, ".*")
def start_radio(self) -> None:
while True:
# Check if we will play an ad or news
if random.random() < config.adsProbability:
if config.debug:
print("Playing ads and news")
self.play_ad_and_news()
if config.debug:
print("Playing music")
self.play_music()
def play_music(self) -> None:
music: str = self.choose_random_unplayed_track("music")
music_name: str = music.split("/")[-1].split(".")[0]
# We choose random pattern
pattern: list[str] = random.choice(config.musicPatterns).split(", ")
if config.debug:
print("Pattern: " + str(pattern))
# We will now play the pattern
for item in pattern:
if item == "ID":
defs.play_sound(random.choice(self.ids))
elif item == "GENERAL":
defs.play_sound(random.choice(self.generals))
elif item == "MONO_SOLO":
defs.play_sound(random.choice(self.mono_solos))
elif item == "MUSIC":
intro_files: list[str] = defs.get_music_intro_files(self.radio_path, music_name)
if len(intro_files) > 0:
intro: str = random.choice(intro_files)
defs.play_sound_with_delayed_second_sound(music, intro)
else:
defs.play_sound(music)
def play_ad_and_news(self) -> None:
# We choose random pattern
pattern: list[str] = random.choice(config.adsAndNewsPatterns).split(", ")
if config.debug:
print("Pattern: " + str(pattern))
# We will now play the pattern
for item in pattern:
if item == "AD":
defs.play_sound(self.choose_random_unplayed_track("ad"))
elif item == "NEWS":
defs.play_sound(self.choose_random_unplayed_track("news"))
elif item == "MONO_SOLO":
defs.play_sound(self.choose_random_unplayed_track("monoSolo"))
def choose_random_unplayed_track(self, track_type: TrackType) -> str:
track_map: dict[TrackType, tuple[list[str], list[str]]] = {
"music": (self.musics, self.played_musics),
"ad": (self.ads, self.played_ads),
"news": (self.news, self.played_news),
"monoSolo": (self.mono_solos, self.played_mono_solos),
}
if track_type not in track_map:
raise Exception("Invalid track type")
tracks, played = track_map[track_type]
if len(played) == len(tracks):
played.clear()
track: str = random.choice(tracks)
while track in played:
track = random.choice(tracks)
played.append(track)
return track
try:
radio: Radio = Radio(config.radioStation)
radio.start_radio()
except KeyboardInterrupt:
print("Exiting...")
exit(0)
Ce script ce charge quant à lui de faire le lien entre notre classe Radio et le backend audio, ici VLC. L’idée est que si plus-tard je trouve une meilleur librairie pour jouer la musique, je n’aurais qu’à modifier ces méthodes. J’ai rencontré quelques difficultés concernant la lecture différée des introductions, notamment en souhaitant modifier le volume de la musique pour mieux entendre l’annonce du DJ.
Ainsi, nous avons 4 méthodes :
REPLACEMEWITHMUSICTITLE par le nom de notre musiqueimport os
import random
import re
import time
import vlc
import config
def get_files_by_regex(folder_to_scan: str, regex: str) -> list[str]:
# We will firstly scan for folders, and then for inside those folders
# for files. We will return a list of files that match the regex.
compiled_regex: re.Pattern[str] = re.compile(regex)
audio_files: list[str] = []
for folder in os.listdir(folder_to_scan):
folder_path: str = folder_to_scan + "/" + folder
if compiled_regex.match(folder) and os.path.isdir(folder_path):
# We will now scan the folder for files
for file in os.listdir(folder_path):
if file.endswith(config.filesExtension):
audio_files.append(folder_to_scan + "/" + folder + "/" + file)
return audio_files
def play_sound(sound_file: str) -> None:
if config.debug:
print("Playing: " + sound_file)
# We will play the sound file
player: vlc.MediaPlayer = vlc.MediaPlayer(sound_file)
player.audio_set_volume(config.volume)
player.play()
# We will wait for the sound to finish
ended: int = 6
current_state: int = player.get_state()
while current_state != ended:
current_state = player.get_state()
if config.debug:
print("Done playing: " + sound_file)
def get_music_intro_files(files_path: str, music_name: str) -> list[str]:
regex: re.Pattern[str] = re.compile(config.musicIntro.replace("REPLACEMEWITHMUSICTITLE", music_name.upper()))
audio_files: list[str] = []
files_path = files_path + "/" + config.introFolder
for file in os.listdir(files_path):
if regex.match(file):
if file.endswith(config.filesExtension):
audio_files.append(files_path + "/" + file)
return audio_files
def play_sound_with_delayed_second_sound(first_sound: str, second_sound: str) -> None:
delay: int = random.randint(config.musicMinIntroDelay, config.musicMaxIntroDelay)
if config.debug:
print("Playing: " + first_sound + " and " + second_sound + " with a delay of " + str(delay) + " seconds")
instances: list[vlc.Instance] = [vlc.Instance(), vlc.Instance()]
players: list[vlc.MediaPlayer] = [instances[0].media_player_new(), instances[1].media_player_new()]
# We set the first sound
players[0].set_media(instances[0].media_new(first_sound))
# We set the second sound
players[1].set_media(instances[1].media_new(second_sound))
# We will play the first sound
players[0].audio_set_volume(config.volume)
players[0].play()
# We wait
time.sleep(delay)
# We will play the second sound
players[1].play()
# We lower the volume of the first sound
# players[0].audio_set_volume(50)
# players[1].audio_set_volume(100)
# We will wait for the second sound to finish
ended: int = 6
current_state: int = players[1].get_state()
while current_state != ended:
current_state = players[1].get_state()
if config.debug:
print("Done playing Intro: " + second_sound)
# We will now raise the volume of the first sound
players[0].audio_set_volume(config.volume)
# We will wait for the first sound to finish
current_state = players[0].get_state()
while current_state != ended:
current_state = players[0].get_state()
if config.debug:
print("Done playing Music: " + first_sound)
Puis vient la configuration. C’est ici que nous définissons toutes nos variables (ou presque). :)
debug = False
filesExtension = ".wav"
musicMinIntroDelay = 5
musicMaxIntroDelay = 15
introFolder = "intro"
adsProbability = 0.25
volume = 75
# Radio station to play: "non-stop-pop", "silverlake", "funk", etc.
radioStation: str = "non-stop-pop"
# Regex
idPattern = "((id)|(ID))_([0-9]){2}"
monoSoloPattern = "((mono)|(MONO))_((solo)|(SOLO))"
generalPattern = "(general)|(GENERAL_([0-9]){2})"
musicPatten = "^(?:(?!(" + idPattern + "|" + monoSoloPattern + "|" + generalPattern + "|(.*([A-Z])_([0-9]){2})|intro|to|time)).)+$"
musicIntro = "REPLACEMEWITHMUSICTITLE_([0-9]){2}"
# Radio specific
musicPatterns = ["ID, GENERAL, MUSIC", "ID, MUSIC", "GENERAL, MUSIC",
"ID, GENERAL, MUSIC"]
adsAndNewsPatterns = ["AD, AD, NEWS", "AD, AD", "NEWS", "MONO_SOLO, AD", "AD, MONO_SOLO", "MONO_SOLO, NEWS",
"NEWS, MONO_SOLO"]
Voici ce que ça donne avec les musiques d’ajoutées. :D


Voilà tout pour ce projet !
Cela faisait un moment que je voulais le partager, depuis 2023 pour être exacte. L’idée de base était de créer un script que je pouvais installer sur une Raspberry Pi, que j’embarquerai dans ma voiture et brancherai à mon autoradio. Mais je me suis confronté à d’autres problèmes qui ont fait que j’ai très vite abandonné l’idée. Toutefois, je l’ai toujours gardé de côté et m’en suis servi de nombreuses fois pour écouter les radios de GTA V à la place Spotify. C’est… Original. 😂
Bien sûr, là le programme ne fonctionne qu’en ligne de commande, depuis le terminal. Mais vous pouvez très facilement lui construire une interface graphique, comme je l’ai fais l’année dernière avec Claude Code.

Ici par exemple, je lui avait demandé de me créer une interface me permettant de visualiser les musiques en cours de lecture, de changer de station de radio, de programmer les prochains paternes à jouer et d’exporter les stations de façon aléatoire, tout en respectant le système de paterne. C’est assez poussé, très cool à utiliser, mais peut-être un peu trop niche pour vous en parler ici. De plus, ce n’est plus vraiment ma réalisation puisque toute l’interface a été réalisée via Claude Code.
Sur ce, comme je ne vais pas publier d’article avant l’année prochaine, je vous souhaite de très bonnes fêtes de fin d’année, un Joyeux Noël et bonne nouvelle année 2026 ! 🎉
Dépôt Github : https://github.com/SofianeLasri/python-gtav-radios