Player Shoutcast/icecast
Publié : dim. 17/août/2025 3:55
Salut tous le monde je vous partage le code source d'un petit player qui permet d’écouter une radio shoutcast/icecast grâce à la bibliothèque BASS. L'exemple et normalement cross plateforme. Vous devrez télécharger la lib BASS et la placer à côté de l’exécutable pour faire fonctionner le code.
Pour windows je n'ai pas trouvé de version 64bits de la lib du coup j'ai utilisé PB 32bits pour compiler le code. Le player récupère le titre et la pochette via l'api itunes si elle est disponible. Je vous partage le code car c'est quelque chose que j'ai longtemp cherché à faire depuis que j'utilise PB. Ca me fait plaisirs de le partger.
Bonne utilisation
Pour windows je n'ai pas trouvé de version 64bits de la lib du coup j'ai utilisé PB 32bits pour compiler le code. Le player récupère le titre et la pochette via l'api itunes si elle est disponible. Je vous partage le code car c'est quelque chose que j'ai longtemp cherché à faire depuis que j'utilise PB. Ca me fait plaisirs de le partger.
Code : Tout sélectionner
; ------------------------------------------------------------
; By MetalOS
; Shoutcast Player – Cross‑Platform (Windows/macOS/Linux)
; PureBasic + BASS audio library (lecture réseau HTTP/ICY)
; - Lecture d'un flux Shoutcast/Icecast
; - Récupération du titre courant via /stats?json=1 ou 7.html
; - Recherche et affichage de la pochette (API iTunes)
; ------------------------------------------------------------
EnableExplicit
UsePNGImageDecoder()
UseJPEGImageDecoder()
UsePNGImageEncoder()
UseJPEGImageEncoder()
; --- Réseau ---
CompilerIf #PB_Compiler_Version < 600
If InitNetwork() = 0
MessageRequester("Erreur", "InitNetwork a échoué.")
End
EndIf
CompilerEndIf
; --- Sélection du nom de bibliothèque BASS selon l'OS ---
CompilerSelect #PB_Compiler_OS
CompilerCase #PB_OS_Windows
#BASS_LIB$ = "bass.dll"
CompilerCase #PB_OS_MacOS
#BASS_LIB$ = "libbass.dylib"
CompilerCase #PB_OS_Linux
#BASS_LIB$ = "libbass.so"
CompilerEndSelect
; --- Chargement dynamique de BASS ---
Global gLibBASS = 0
Prototype.i PB_BASS_Init(device.l, freq.l, flags.l, win.i, cls.i)
Prototype.i PB_BASS_Free()
Prototype.i PB_BASS_ErrorGetCode()
Prototype.i PB_BASS_StreamCreateURL(url.p-ascii, offset.l, flags.l, proc.i, user.i)
Prototype.i PB_BASS_StreamFree(handle.i)
Prototype.i PB_BASS_ChannelPlay(handle.i, restart.l)
Prototype.i PB_BASS_ChannelStop(handle.i)
Prototype.i PB_BASS_ChannelSetAttribute(handle.i, attrib.l, value.f)
Global BASS_Init.PB_BASS_Init
Global BASS_Free.PB_BASS_Free
Global BASS_ErrorGetCode.PB_BASS_ErrorGetCode
Global BASS_StreamCreateURL.PB_BASS_StreamCreateURL
Global BASS_StreamFree.PB_BASS_StreamFree
Global BASS_ChannelPlay.PB_BASS_ChannelPlay
Global BASS_ChannelStop.PB_BASS_ChannelStop
Global BASS_ChannelSetAttribute.PB_BASS_ChannelSetAttribute
Procedure.i LoadBASS()
Protected libPath$ = GetPathPart(ProgramFilename()) + #BASS_LIB$
If FileSize(libPath$) <= 0
libPath$ = #BASS_LIB$
EndIf
gLibBASS = OpenLibrary(#PB_Any, libPath$)
If gLibBASS = 0 : ProcedureReturn 0 : EndIf
BASS_Init = GetFunction(gLibBASS, "BASS_Init")
BASS_Free = GetFunction(gLibBASS, "BASS_Free")
BASS_ErrorGetCode = GetFunction(gLibBASS, "BASS_ErrorGetCode")
BASS_StreamCreateURL = GetFunction(gLibBASS, "BASS_StreamCreateURL")
BASS_StreamFree = GetFunction(gLibBASS, "BASS_StreamFree")
BASS_ChannelPlay = GetFunction(gLibBASS, "BASS_ChannelPlay")
BASS_ChannelStop = GetFunction(gLibBASS, "BASS_ChannelStop")
BASS_ChannelSetAttribute= GetFunction(gLibBASS, "BASS_ChannelSetAttribute")
If BASS_Init = 0 Or BASS_Free = 0 Or BASS_StreamCreateURL = 0 Or BASS_StreamFree = 0 Or BASS_ChannelPlay = 0 Or BASS_ChannelStop = 0 Or BASS_ChannelSetAttribute = 0
CloseLibrary(gLibBASS) : gLibBASS = 0 : ProcedureReturn 0
EndIf
ProcedureReturn 1
EndProcedure
; --- Constantes UI ---
#WIN = 0
#TIMER_META = 1
#G_URL_LABEL = 10
#G_URL = 11
#G_BTN_PLAY = 12
#G_BTN_STOP = 13
#G_VOL_LABEL = 14
#G_VOL = 15
#G_TITLE = 16
#G_COVER = 17
; --- Globals ---
Global gStreamURL$ = "http://65.108.105.26:7551/;stream/1" ; exemple Radio Bizzz
Global gBaseURL$, gLastTitle$ = "", gCoverImage = 0
Global gStreamHandle = 0
; --- Helpers ---
Procedure.s TrimAll(s$) : ProcedureReturn Trim(ReplaceString(ReplaceString(s$, #CRLF$, " "), #TAB$, " ")) : EndProcedure
Procedure.s ExtractBaseURL(url$)
Protected p, p2, base$
p = FindString(url$, "://", 1)
If p
p2 = FindString(url$, "/", p+3)
Else
p2 = FindString(url$, "/", 1)
EndIf
If p2 : base$ = Left(url$, p2-1) : Else : base$ = url$ : EndIf
ProcedureReturn base$
EndProcedure
Procedure.s SimpleURLEncode(s$)
Protected i, c, out$
For i=1 To Len(s$)
c = Asc(Mid(s$, i, 1))
Select c
Case 48 To 57, 65 To 90, 97 To 122 : out$ + Chr(c)
Case 32 : out$ + "+"
Default : out$ + "%" + RSet(Hex(c), 2, "0")
EndSelect
Next
ProcedureReturn out$
EndProcedure
Procedure.s GetJSONStringSafe(JSON, key$)
Protected root = JSONValue(JSON)
If root
Protected node = GetJSONMember(root, key$)
If node : ProcedureReturn GetJSONString(node) : EndIf
EndIf
ProcedureReturn ""
EndProcedure
; --- Récupération du titre courant depuis le serveur ---
Procedure.s FetchSongTitleFromStatsJSON(base$)
Protected *mem, txt$, j, root, title$, arr, el
*mem = ReceiveHTTPMemory(base$ + "/stats?json=1")
If *mem
txt$ = PeekS(*mem, -1, #PB_UTF8) : FreeMemory(*mem)
If txt$ <> ""
j = ParseJSON(#PB_Any, txt$)
If j
title$ = GetJSONStringSafe(j, "songtitle")
If title$ = "" : title$ = GetJSONStringSafe(j, "song") : EndIf
If title$ = ""
root = JSONValue(j)
arr = GetJSONMember(root, "streams")
If arr And JSONType(arr) = #PB_JSON_Array And JSONArraySize(arr) > 0
el = GetJSONElement(arr, 0)
title$ = GetJSONString(GetJSONMember(el, "songtitle"))
If title$ = "" : title$ = GetJSONString(GetJSONMember(el, "song")) : EndIf
EndIf
EndIf
FreeJSON(j)
EndIf
EndIf
EndIf
ProcedureReturn TrimAll(title$)
EndProcedure
Procedure.s FetchSongTitleFrom7(base$)
Protected *mem, txt$, title$, commas
*mem = ReceiveHTTPMemory(base$ + "/7.html")
If *mem
txt$ = PeekS(*mem, -1, #PB_UTF8) : FreeMemory(*mem)
If txt$ <> ""
commas = CountString(txt$, ",")
If commas >= 5 : title$ = StringField(txt$, 6, ",") : EndIf
If title$ = ""
*mem = ReceiveHTTPMemory(base$ + "/7.html?sid=1")
If *mem
txt$ = PeekS(*mem, -1, #PB_UTF8) : FreeMemory(*mem)
commas = CountString(txt$, ",")
If commas >= 5 : title$ = StringField(txt$, 6, ",") : EndIf
EndIf
EndIf
If title$ = ""
Protected pos = FindString(txt$, "StreamTitle='", 1)
If pos
Protected start = pos + Len("StreamTitle='")
Protected stop = FindString(txt$, "';", start)
If stop > start : title$ = Mid(txt$, start, stop-start) : EndIf
EndIf
EndIf
EndIf
EndIf
ProcedureReturn TrimAll(title$)
EndProcedure
Procedure.s GetCurrentTitle(base$)
Protected t$ = FetchSongTitleFromStatsJSON(base$)
If t$ = "" : t$ = FetchSongTitleFrom7(base$) : EndIf
ProcedureReturn t$
EndProcedure
; --- Téléchargement d'image ---
Procedure.i DownloadImageFromURL(url$)
Protected *mem = ReceiveHTTPMemory(url$)
If *mem
Protected img = CatchImage(#PB_Any, *mem, MemorySize(*mem))
FreeMemory(*mem)
ProcedureReturn img
EndIf
ProcedureReturn 0
EndProcedure
; --- Recherche de pochette via iTunes ---
Procedure.s FindArtworkURL(artist$, title$)
Protected term$ = SimpleURLEncode(artist$ + " " + title$)
Protected url$ = "https://itunes.apple.com/search?term=" + term$ + "&media=music&entity=song&limit=1"
Protected *mem = ReceiveHTTPMemory(url$)
If *mem
Protected txt$ = PeekS(*mem, -1, #PB_UTF8) : FreeMemory(*mem)
If txt$ <> ""
Protected j = ParseJSON(#PB_Any, txt$)
If j
Protected root = JSONValue(j)
Protected results = GetJSONMember(root, "results")
If results And JSONType(results) = #PB_JSON_Array And JSONArraySize(results) > 0
Protected item = GetJSONElement(results, 0)
Protected art$ = GetJSONString(GetJSONMember(item, "artworkUrl100"))
If art$ <> ""
art$ = ReplaceString(art$, "100x100", "600x600")
FreeJSON(j)
ProcedureReturn art$
EndIf
EndIf
FreeJSON(j)
EndIf
EndIf
EndIf
ProcedureReturn ""
EndProcedure
Procedure UpdateCoverFromTitle(title$)
Protected artist$, track$, dashPos
dashPos = FindString(title$, " - ", 1)
If dashPos
artist$ = Trim(Mid(title$, 1, dashPos-1))
track$ = Trim(Mid(title$, dashPos+3))
Else
track$ = title$
EndIf
If artist$ = "" And track$ = "" : ProcedureReturn : EndIf
Protected artURL$ = FindArtworkURL(artist$, track$)
If artURL$ <> ""
Protected img = DownloadImageFromURL(artURL$)
If img
If gCoverImage : FreeImage(gCoverImage) : gCoverImage = 0 : EndIf
gCoverImage = img
SetGadgetState(#G_COVER, ImageID(gCoverImage))
EndIf
EndIf
EndProcedure
; --- Métadonnées ---
Procedure UpdateMeta()
Protected t$ = GetCurrentTitle(gBaseURL$)
If t$ <> "" And t$ <> gLastTitle$
gLastTitle$ = t$
SetGadgetText(#G_TITLE, t$)
SetWindowTitle(#WIN, "Shoutcast Player - " + t$)
UpdateCoverFromTitle(t$)
EndIf
EndProcedure
; --- Audio : contrôle BASS ---
Procedure StartPlayback()
Protected url$ = GetGadgetText(#G_URL)
If url$ = "" : url$ = gStreamURL$ : SetGadgetText(#G_URL, url$) : EndIf
gBaseURL$ = ExtractBaseURL(url$)
If gStreamHandle
BASS_ChannelStop(gStreamHandle)
BASS_StreamFree(gStreamHandle)
gStreamHandle = 0
EndIf
gStreamHandle = BASS_StreamCreateURL(url$, 0, 0, 0, 0)
If gStreamHandle = 0
MessageRequester("Erreur", "Impossible d'ouvrir le flux (code " + Str(BASS_ErrorGetCode()) + ")")
ProcedureReturn
EndIf
BASS_ChannelPlay(gStreamHandle, 0)
; Volume initial selon le TrackBar
Protected vol.f = GetGadgetState(#G_VOL) / 100.0
If vol < 0 : vol = 0.7 : EndIf
; BASS_ATTRIB_VOL = 2 (valeur 0.0 à 1.0)
BASS_ChannelSetAttribute(gStreamHandle, 2, vol)
UpdateMeta()
EndProcedure
Procedure StopPlayback()
If gStreamHandle
BASS_ChannelStop(gStreamHandle)
BASS_StreamFree(gStreamHandle)
gStreamHandle = 0
EndIf
EndProcedure
; --- Programme principal ---
If LoadBASS() = 0
MessageRequester("BASS manquant", "La bibliothèque '" + #BASS_LIB$ + "' est introuvable. Place-la à côté de l'exécutable ou dans un chemin système.")
End
EndIf
If BASS_Init(-1, 44100, 0, 0, 0) = 0
MessageRequester("Erreur", "BASS_Init a échoué (code " + Str(BASS_ErrorGetCode()) + ")")
End
EndIf
If OpenWindow(#WIN, 0, 0, 520, 460, "Shoutcast Player (PureBasic · BASS)", #PB_Window_SystemMenu | #PB_Window_ScreenCentered | #PB_Window_MinimizeGadget)
TextGadget(#G_URL_LABEL, 10, 12, 60, 20, "Flux URL:")
StringGadget(#G_URL, 75, 10, 430, 22, gStreamURL$)
ButtonGadget(#G_BTN_PLAY, 10, 45, 80, 28, "Lire")
ButtonGadget(#G_BTN_STOP, 95, 45, 80, 28, "Stop")
TextGadget(#G_VOL_LABEL, 185, 50, 50, 20, "Volume")
TrackBarGadget(#G_VOL, 235, 50, 270, 22, 0, 100)
SetGadgetState(#G_VOL, 70)
TextGadget(#G_TITLE, 10, 85, 495, 24, "Titre en cours: (inconnu)")
ImageGadget(#G_COVER, 110, 120, 300, 300, 0, #PB_Image_Border)
AddWindowTimer(#WIN, #TIMER_META, 10000)
Repeat
Select WaitWindowEvent()
Case #PB_Event_CloseWindow
Break
Case #PB_Event_Gadget
Select EventGadget()
Case #G_BTN_PLAY
StartPlayback()
Case #G_BTN_STOP
StopPlayback()
Case #G_VOL
If gStreamHandle
BASS_ChannelSetAttribute(gStreamHandle, 2, GetGadgetState(#G_VOL)/100.0)
EndIf
EndSelect
Case #PB_Event_Timer
If EventTimer() = #TIMER_META
UpdateMeta()
EndIf
EndSelect
ForEver
If gCoverImage : FreeImage(gCoverImage) : EndIf
EndIf
If gStreamHandle : BASS_ChannelStop(gStreamHandle) : BASS_StreamFree(gStreamHandle) : EndIf
BASS_Free()
End