Skip to content

Repository files navigation

gcradio

An internet radio player for the Nintendo GameCube. Point it at an Icecast or SHOUTcast stream — MP3, AAC or Opus, over HTTP or HTTPS — and the console plays it, showing the track title as it changes.

Written in C for devkitPPC/libogc2. One source tree builds two players:

  • the console build (make) — everything drawn in the libogc text console, 1.1 MB, the smallest and longest-tested of the two;
  • the GX build (make GUI=1) — a two-panel interface drawn by the GPU on a render thread of its own, 2.2 MB.

About 1.7 MB of RAM out of the console's 24 MB, either way.

The GX interface, with the station list on the left and the player on the right — the list scrolls, and the station playing stays marked while you move the highlight elsewhere:

┌──────────────────────┬─────────────────────────────────────────┐
│ STATIONS      1/25   │  NightRide FM (Opus)                    │
│ ──────────────────── │  Nightride - Tokyo Rose                 │
│ ▎NightRide FM (Opus) │ ─────────────────────────────────────── │
│  Chillsynth (Opus)   │  Opus   251 kbps   48000 Hz   stereo    │
│  Datawave (Opus)     │  00:41:07                               │
│  Spacesynth (Opus)   │                                         │
│  Darksynth (Opus)    │  ▆█▇▅▆▄▅▃▄▃▃▂▃▂▂▁▂▁▁▁▁▁░░  spectrum     │
│  Horrorsynth (Opus)  │  ████████████████░░░░░░░░  L            │
│  Rekt (Opus)         │  ███████████████░░░░░░░░░  R            │
│  EBSM (Opus)         │ ─────────────────────────────────────── │
│  Synthetic FM 256k   │  streaming                     vol 200  │
│  Synthwave (laut.fm) │  net  ███░░░░░  audio  ███████░         │
│  Nectarine 192k     ▌│  20134 KB   64211 frames   attempts 1   │
│                      │  stalls 0   dropouts 0   rate changes 0 │
└──────────────────────┴─────────────────────────────────────────┘
  UP/DOWN  volume        Z  stations        START  quit

The bars are a real 2048-point FFT — see "Features" — and the two meters below them are the peak level of each channel. The downward slope is what music actually looks like, not a decoration.

The console build, which shows the same information as text:

gcradio - HTTP audio streaming on GameCube

Station : [1/1] Nectarine Demoscene Radio
IP : 192.168.1.56    DNS : 192.168.1.1
Stream : http://nectarine.from-de.com:80/necta192

State   : connected       host -> 88.198.25.215
Title   : Syphus - Turbochip
Format  : 44100 Hz / 192 kbps / 2 ch
Buffers : network  15%  audio  98%
Recv    : 3696 KB / 5947 frames
Volume  : 200
Audio   : playing   dropouts 0

UP/DOWN : volume   Z : stations   START : quit

Features

Audio

  • MP3, AAC and Opus. The format comes from the server's Content-Type and, when that is missing or wrong, from the bytes themselves. A stream in a format this build cannot decode says so on screen rather than falling silent.
  • HE-AAC handled correctly: the sample rate is taken from the decoder, not from the ADTS header, which advertises the core rate and would play SBR content at half speed.
  • Ogg demuxing written for this project rather than pulled from libogg, because the pull-style API would have had to block the loop that reads the controller.
  • ICY titles, de-interleaved and resynchronised with the audio — the title appears when the track does, not eleven seconds early. Ogg streams carry their titles in-band instead, and those are read too.
  • Volume with auto-repeat, and a playing-time counter that only advances while sound is actually going out, so it stays comparable with the dropout count.

Network

  • HTTP and HTTPS. TLS 1.2 through a cross-built Mbed TLS, with the trust store read from the SD card so an expired root can be replaced without rebuilding.
  • Built-in DNS resolver — libogc ships none on the GameCube.
  • HTTP redirects followed, including the single-use tokens that many stations hand out; a reconnect always restarts from the canonical URL rather than from a spent token.
  • No call can block for ever. Every read has a deadline, because a TCP peer that dies without closing sends neither FIN nor RST — the screen names the phase it is in (connecting, headers, streaming, quiet Ns, STALLED) and gives up rather than hanging.
  • Automatic reconnection with a DNS cache flush, so dynamic-DNS names are followed.
  • Takes the Broadband Adapter or an ETH2GC, whichever is plugged in, and says on screen which one it found.

Interface

  • Two builds from one tree: the libogc text console, or a GX interface rendered by the GPU on its own thread — so drawing never paces the playback loop, and the loop never blocks on the vertical retrace.
  • Up to 128 stations in a scrolling list, in both builds.
  • A station picker that does not stop the music. In the GX build it is a state, not a modal loop: the stream keeps playing and the screen keeps updating while you choose.
  • Stations added, edited and deleted on the console, in either build, with an on-screen keyboard. The last station and volume are restored on the next start. No PC needed after the first copy.
  • A 24-band spectrum analyser: a 2048-point fixed-point FFT written for this project rather than linked, sampled where the PCM is already contiguous on its way to the DSP. No floating point anywhere.
  • A segmented VU meter — in the console build drawn straight into the framebuffer in YUY2, below everything the console prints.
  • A start-up log that names each step, so a failed start says where it stopped instead of showing a black screen.
  • Reads its configuration from SD Gecko, SD2SP2, a GC Loader or a Memory Expansion Pak, formatted FAT32 or exFAT — so a card above 32 GB, which is exFAT as sold, works as it is.

Limitations

Three of these are worth understanding before filing a bug, because they are properties of the hardware rather than defects.

1. Ping decides which bitrates can play at all

This is the one that surprises people. The GameCube's TCP window is 2920 bytes, and a sender cannot have more than one window in flight per round trip. So the absolute ceiling is:

maximum bytes per second  =  2920 / RTT

Nothing else about the console enters into it — not the CPU, not the buffer size. A distant station fails at a bitrate a nearby one sustains easily, and buying a faster internet connection changes nothing.

Ping to the station Hard ceiling Comfortable bitrate (2× margin)
10 ms 285 KB/s up to 320 kbps
25 ms 114 KB/s up to 320 kbps
50 ms 57 KB/s ~230 kbps
100 ms 29 KB/s ~115 kbps
150 ms 19 KB/s ~78 kbps
200 ms 14 KB/s ~58 kbps
300 ms 9.5 KB/s ~39 kbps

The "comfortable" column stops at 320 kbps because that is the highest rate ever sustained here in practice: 40 KB/s for an hour over a DOL-015. Below about a 2× margin, a single retransmission timeout is enough to empty the buffer.

This was not derived on paper. Two SomaFM mounts — same server, same city, same round trip, only the bitrate different — were played on the console: 128 kbps ran for an hour and a half without a single dropout, and 256 kbps broke up immediately and kept getting worse. See DOC.md §3.

What to do about it: prefer a nearby station, or a lower bitrate, or a more efficient codec. Opus at 128 kbps sounds better than MP3 at 128 kbps and costs the same bytes — on this machine the byte is scarce and the CPU cycle is not.

2. Stream formats that will not work

Transfer-Encoding: chunked not decoded. Some reverse proxies answer this way to HTTP/1.1; gcradio asks in HTTP/1.0 to avoid it.
TLS 1.3-only servers Mbed TLS 2.28 LTS speaks TLS 1.2 at the newest. Servers that have dropped 1.2 will not connect.
Playlist URLs (.pls, .m3u) not parsed. Point gcradio at the stream itself, not at the file that lists it.
Vorbis in Ogg not decoded — Opus is.
Streams behind a login or a token you must fetch first no session handling.

3. The rest

  • Booting from Datel's SD Media Launcher does not work. Chain-load Swiss instead, which is a single extra step. If you need that loader specifically, release v1.0.1 predates the change that broke it.
  • No hardware random number generator exists on a GameCube. TLS session keys are seeded from a mixture of timebase, RTC, allocator addresses and scheduling jitter. That is honest enough for authenticating a public radio station, where nothing secret is transmitted; it is not good enough for anything carrying credentials, and it must not be described as a hardware RNG.
  • A dead clock battery means no date, so certificate expiry cannot be checked. gcradio detects an implausible RTC, skips exactly the two date-related checks, keeps chain, signature and hostname verification, and says on screen which of the two situations you are in.
  • The spectrum analyser resolves the bass coarsely. Twenty-four log-spaced bands from 50 Hz need finer resolution at the bottom than 2048 points give, so the lowest two bars share a bin and move together. Raising the transform size would push that boundary down and nothing else would.
  • Editing the URL of the station currently playing takes effect on the next reconnection, not immediately. Deleting it is refused — switch away first, or the screen would name one station while the speakers played another.
  • An ETH2GC and an SD2SP2 both want Serial Port 2, so with an ETH2GC the configuration has to live on an SD Gecko. And if_configex() probes the Broadband Adapter first: on a console with both fitted, the ETH2GC will never be chosen.

Requirements

  • A GameCube with a network adapter, either of:
    • a Broadband Adapter DOL-015 on Serial Port 1
    • an ETH2GC on Serial Port 2
  • An SD Gecko (memory card slot) or SD2SP2 (Serial Port 2) to hold the .dol and its configuration. Note that an ETH2GC and an SD2SP2 both occupy Serial Port 2, so with an ETH2GC the configuration goes on an SD Gecko. A GC Loader or a Memory Expansion Pak will hold the configuration too; any of them may be FAT32 or exFAT, of any size.
  • A way to launch homebrew, typically Swiss
  • An Icecast/SHOUTcast stream over HTTP or HTTPS. TLS 1.2 is spoken through a cross-built Mbed TLS; a server that has gone TLS 1.3-only will not work.

Quick start

  1. Copy gcradio-gui.dol (the GX interface) or gcradio.dol (the text console) onto your SD card. They read the same configuration and play the same streams; pick whichever you prefer, or carry both.
  2. Copy gcradio.conf.example next to it, rename it to gcradio.conf, and put your stream URL in it. You can skip this and type the URL on the console instead — it will then be saved for you.
  3. For https:// stations, copy gcradio-ca.pem next to them as well. It is a release asset; it is also just curl's CA bundle, so curl -o gcradio-ca.pem https://curl.se/ca/cacert.pem gets you a current one. Without it, plain HTTP still plays and https:// is refused with the reason on screen.
  4. Launch gcradio.dol through Swiss.
  5. Pick a station and press A.

The trust store sits on the card rather than in the binary so that an expired root can be replaced without rebuilding anything. It does not go stale quickly: of the 119 roots in the July 2026 bundle, one expires before 2029 and eight before 2030, and the median runs to 2040. What actually matters is the root a given station chains to — NightRide reaches ISRG Root X2, good until 2040; SomaFM reaches Sectigo's R46, good until 2046.

See the user guide for the controls and the configuration file format.

Building

Requires devkitPro with the gamecube-dev package.

source /etc/profile.d/devkit-env.sh
make                      # -> gcradio.dol,  the text console, 1.1 MB
make GUI=1                # -> gcradio.dol,  the GX interface, 2.2 MB

Both write gcradio.dol, so rename the one you are keeping. GUI=1 needs GRRLIB as well: pacman -S libogc2-grrlib.

To make everything a release carries — both builds, with the clean between them that a shared build/ makes necessary, plus the example configuration and the trust store — in dist/:

make dist

That is the same target GitHub Actions runs (.github/workflows/release.yml): every push is built in devkitPro's container, and a v* tag is built and published from the tree at that tag. The tag has to match VERSION in the Makefile — the number the console build prints on its first line — or the release is refused.

There is a third build, for when the GX path misbehaves on a console that has no debugger attached:

make GUI=1 GXTRACE=1      # narrates every step of the bring-up on a text
                          # console, then hands the screen over on START

It exists because GRRLIB takes the display on its first call and never gives it back, so any error printed afterwards lands in a framebuffer nobody can see. That, and not the underlying bug, is what made two earlier attempts look like a console that had simply died.

The shipped gcradio.conf opens on both mirrors of Nectarine Demoscene Radio[US] and [EUR], the same public programme at 192 kbps, starting on the American one. With no card to read at all, which is every run under Dolphin, gcradio falls back on the [US] one alone, compiled in so that a machine that cannot save a station still plays something. To bake in your own instead:

make DEFAULT_URL=http://192.168.1.20:8000/stream DEFAULT_NAME="Living room"

Host-side tests, which need no GameCube toolchain — they build the real logic with your system gcc against substitute headers:

cd tests && make          # 217 checks

On MSYS2 use the MSYS gcc rather than the MinGW one, which ships no <sys/select.h>: PATH=/usr/bin:$PATH make.

Why libogc2

gcradio builds against libogc2, not the libogc that comes with devkitPro — the only supported stack since 2026-08-02. Two reasons, both about hardware:

  • its TCP window is 2*TCP_MSS (2920), wide enough to be useful and still inside the DOL-015's 4 KiB receive buffer — going over that overflows the adapter (see DOC.md, §10a)
  • it carries drivers for the ENC28J60, W5500 and W6100, which is what makes ETH2GC support free: if_configex() probes the DOL-015 first, then those three, on Serial Port 1, Serial Port 2 and both card slots. devkitPro's libogc has the BBA driver and nothing else.

Install it alongside libogc, which it does not replace:

# add the [libogc2-devkitpro] repository to /etc/pacman.conf, before [dkp-libs]
pacman -S libogc2-git libogc2-libdvm-git

libdvm, and not libogc2-libfat-git. The two packages carry the same name, install the same libfat.a and conflict with each other, so this line is the only place the choice is made — nothing in the Makefile or the source can tell which of the two you built against. libdvm adds a partition prober and an exFAT driver, and gcradio reaches both through fatInitDefault(), which libfat provides too. Build against libfat and everything still links: what you lose is silent, and it is every card above 32 GB (DOC.md, §9b).

Testing under Dolphin

Dolphin is fine for the application logic and useless for judging the network stack. Two independent reasons: it does not model the DOL-015's 4 KiB receive buffer at all, so a window that would overflow real hardware looks like a free win; and its emulated adapter has a receive-buffer wrap-around bug that libogc2 runs into and devkitPro's libogc does not, which makes libogc2 look several times slower than it is. Throughput comparisons belong on a console.

Project status

Honest summary, because it matters:

Builds under devkitPPC, no warnings yes
217 host-bench checks green
Network, DNS, HTTP, ICY, decoding, UI validated under Dolphin
MP3 and AAC playback confirmed on the console
Opus playback (Ogg) confirmed on the console
HTTPS, TLS 1.2 via mbedTLS confirmed on the console
Plays on a real GameCube with a DOL-015 yes, chain-loaded through Swiss
Plays on a real GameCube with an ETH2GC yes, Serial Port 2, confirmed 2026-08-02
Booting from Datel's SD Media Launcher broken — chain Swiss; see DOC.md §11a
Controller responsiveness on hardware fixed and confirmed on the console
Sustained throughput on hardware 40 KB/s over an hour on a DOL-015
HTTP redirects, VU meter confirmed on the console
Reading gcradio.conf from SD confirmed on the console
Certificate bundle from SD confirmed on the console
GX interface, rendered on its own thread confirmed on the console
Station picker in the GX build confirmed on the console
Adding, editing and deleting stations in the GX build confirmed on the console
Spectrum analyser confirmed on the console

So: it plays on a console, with either adapter, in either build, but only when Swiss is chain-loaded first — booting straight from the SD Media Launcher is unresolved and parked. Since 2026-08-02 there is no libogc branch to fall back on; if you need that loader, take release v1.0.1, which predates the switch.

The throughput figures are real measurements rather than emulator readings, and the rule they produced is the first entry under Limitations above: it is the single most useful thing to know before blaming the console for a station that will not play.

Getting the GX interface onto a thread was the expensive part of v3, and the lesson generalises: the render thread was given 16 KB of stack, FreeType's rasteriser builds its state on the caller's stack, and the overrun brought down malloc much later and somewhere else entirely. An overrun stack never fails where it happened. It is 64 KB now. The whole episode is in HISTORY.md.

The analyser produced a smaller version of the same lesson. Its first scale was anchored on a full-scale sine, which is arithmetically correct and useless: a sine puts all its energy in one bin where music spreads it over hundreds, so every real stream crawled along the bottom of the display. The bars were right and unreadable at the same time — a failure that no amount of staring at the code would have shown, and that took decoding a real stream on a PC and running the same fft_bands() over it to see.

Reports from real consoles are very welcome — especially throughput figures with the round trip quoted alongside, since that pair is what the ceiling above is made of.

Documentation

  • USER-GUIDE.md — controls, configuration, troubleshooting
  • DOC.md — technical documentation: architecture, concurrency model, ICY synchronisation, libogc API notes
  • HISTORY.md — how the project got here, and the three diagnoses that cost the most time

Licence

The source is MIT. A built gcradio.dol is GPL-2.0-or-later. Both are true at once, and the difference matters if you redistribute a binary:

Component Licence
gcradio itself MIT
minimp3, vendored as source/minimp3.h CC0
faad2 — AAC GPL-2.0
libopus — Opus BSD-3-Clause
Mbed TLS — HTTPS Apache-2.0 or GPL-2.0-or-later
GRRLIB — the GX interface MIT
FreeType — text, via GRRLIB FTL or GPL-2.0-or-later
libpng, zlib, brotli, bzip2 — pulled in by FreeType permissive
DejaVu Sans Condensed, embedded as source/font_data.c Bitstream Vera + Arev, permissive
curl's CA bundle, a release asset MPL-2.0
devkitPro / libogc2 their own

Linking faad2 is what does it: any binary containing it is GPL-2.0. Mbed TLS and FreeType are both dual-licensed and are taken here under GPL-2.0-or-later — which is what makes the combination coherent rather than merely convenient, since Apache-2.0 and GPL-2.0 do not mix.

Two of these are actually redistributed, and they are the two that needed choosing carefully:

  • the font, because it lives inside the .dol and therefore inside a GPL-2.0-or-later binary. DejaVu descends from Bitstream Vera and Arev, both permissive with no copyleft of their own, so it raises nothing. It is embedded rather than read off the card because without it there is no interface at all, and a missing file would mean a black screen with no way to say why. Regenerate it with sh tools/mkfont.sh.
  • the trust store, gcradio-ca.pem, published as a release asset beside gcradio.dol. It is MPL-2.0, being a conversion of Mozilla's certdata.txt — but it is data read from the SD card at run time, not linked code, so it touches neither the MIT source nor the GPL binary. See "HTTPS" below for how long a given bundle stays good.

Everything else is fetched by the build instructions above and is not redistributed here.

MP3 decoding by minimp3 (lieff, CC0), vendored as source/minimp3.h — public domain, no attribution required. AAC decoding by faad2 (GPLv2) and Opus by libopus (BSD), both linked from the devkitPro PowerPC portlibs and not redistributed here. Note that linking faad2 makes a distributed binary subject to the GPL.

Built on devkitPro / libogc, which carry their own licences and are not redistributed here.

About

Internet radio player for the Nintendo GameCube - Icecast/SHOUTcast MP3 streaming over the DOL-015 Broadband Adapter

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages