picoamp
Building games and apps in Pico-8 and Picotron has put me back into the fun zone of programming… back when I was learning in classic ASP, Java J2EE, and ColdFusion. In those days, I’d drive my 14 year old self to the radio station after school, put on some headphones and play with code for hours, always listening to 90s Shoutcast streams with Winamp. Picoamp has made me feel like that again, so I threw together this Winamp sort of clone that runs in Picotron to really complete my coding time machine bubble.
Before diving into Picoamp, a quick note on what Picotron is.
What is Picotron
Picotron is this incredible fantasy workstation from Lexaloffle, built around imaginary hardware where you can create and share Lua apps and games that you load and run on the desktop. Its limitations are where the retro computing piece really gets fun. When you build a Picotron app, it gets saved to a virtual cart as a graphic. You can share that graphic to share the app. Being a “virtual cart” based storage mechanism, you have a limited amount of size, limited display and color palette, and limited sound options.
The self-imposed constraints make it fun and interesting - you can’t just install libraries to do stuff, you have to figure it all out at a base level and get creative with it.
How it works
Picotron doesn’t have any of what it needs to play music, but my host OS sure does. So, if we can bridge across the Picotron gap and interact with the OS in some way, we can both drive the (obviously lame in comparison to Winamp) Apple Music app and get data needed for our Picoamp visualization. The quick and dirtiest way I found to cross this bridge was to basically write data files from Picotron that a Ruby service could read and pick up. Similarly, the Ruby service uses the Mac’s osascript interface to get information from Apple Music and write to files that Picotron can pick up to update the interface. If I were doing it again from scratch, I’d probably open a socket from the Ruby service and use direct socket comms from Picotron instead and treat it a bit more like a pub/sub sort of interface.
Ruby service
On the host machine, a small Ruby service runs and talks to Apple Music using osascript commands.
def write_current_track
track_data = {
name: `osascript -e 'tell application "Music" to get name of current track'`.strip.downcase,
artist: `osascript -e 'tell application "Music" to get artist of current track'`.strip.downcase,
album: `osascript -e 'tell application "Music" to get album of current track'`.strip.downcase,
duration: `osascript -e 'tell application "Music" to get duration of current track'`.strip.downcase
}
File.write('../../appdata/current_track.pod',
"--[[pod,created=\"#{Time.now}\",modified=\"#{Time.now}\",revision=0]]\n#{ruby_hash_to_lua_table(track_data)}")
end
def write_current_position
position_data = {
position: `osascript -e 'tell application "Music" to get player position'`.strip,
volume: `osascript -e 'tell application "Music" to get sound volume'`.strip
}
File.write('../../appdata/playhead.pod',
"--[[pod,created=\"#{Time.now}\",modified=\"#{Time.now}\",revision=0]]\n#{ruby_hash_to_lua_table(position_data)}")
end
def listen_for_commands
current_track_timer = 0
Kernel.loop do
if current_track_timer == 0
current_track_timer = 10
write_current_track
end
current_track_timer -= 1
command_content = File.read('../../appdata/command.txt').split("\n")
# Get the second line of the file (after the newline)
command = command_content[1]
File.write('../../appdata/command.txt', command_content[0])
puts "Received command: #{command}" if command && !command.empty?
# Process commands like play, pause, etc.
end
In these examples, we’re writing current track and position information to .pod files and listening for commands to be written from Picotron. Pretty simple, not exactly the most responsive, but it works.
Picoamp UI Updates
With playhead information written from the service, Picoamp can pick it up and update the UI:
function player_status.update_playhead_status()
local playhead_data = fetch("/appdata/playhead.pod")
player_status.current_position = playhead_data.position
player_status.current_volume = playhead_data.volume
local play_pct = player_status.current_position / player_status.track_data.duration
playhead_min_x = 17
playhead_max_x = 240
playhead_x = playhead_min_x + (playhead_max_x - playhead_min_x) * play_pct
if playhead_x < controls.playhead.x then
player_status.update_track_data()
end
controls.playhead.x = playhead_x
end
function player_status.update_volume_position()
local volume_pct = player_status.current_volume / 100
volume_x = controls.volume.min_x + (controls.volume.max_x - controls.volume.min_x) * volume_pct
controls.volume.x = volume_x
end
Wrapping Up
Like many of my quick silly little builds, this was a nostalgic experiment to recapture the joy of coding from my teenage years, and it’s evolved into a quirky tool I still use daily for music listening. The file-based bridge to Apple Music isn’t perfect—it’s a bit slow and definitely not production-ready—but it proves that with a little creativity, you can bend platforms like Picotron to do unexpected things.
That said, this is very much a personal hack: the Ruby service ties it to macOS and Apple Music, making it hard to package for others. If you’re into retro programming or Lua, give Picotron a try—it’s a playground for ideas like this.