Back
VilleOlof
11/6/2024, 4:28:00 AM
10 min read
Website
Website
Vylet Ponlde is a website game i recently made that is in the genre of “wordle” games.
Everyday you get 5 random songs from 90%+ of Vylet Pony’s discography and you get 3 clues to guess which song it is.
0.5
random seconds from the song1
random second from the song2.5
seconds from the start of the songAnd at the end you can share your calculated score with some funny watermelon squares (🟥🟩⬛).
Before i go into more nerdy lovely details, i must credit Ottomated.
He made a very similar site for Porter Robinson (here).
Which i played everyday for the first month. And its features is almost exactly the same.
A lot of small stuff and or straight up hack “taken” from his site (will go more into that later).
So thank you Ottooo, youre awesome sauce.
This project took just 4~ days to complete.
And it begun with a fast prototype for the server API that was gonna serve the audio.
I often code in Rust
for plenty of reasons. But i wanted this to be done pretty quick.
So i made the choice to make the server in typescript
using Bun in a Ubuntu WSL.
Which was a very good choice i believe here.
Bun has this nice API called "$ Shell"
which makes it very easy to interop a bash-like shell with JS/TS.
And of course im gonna use ffmpeg
and ffprobe
to cut out the sound clips from the songs and gather the song duration.
So i could just do
async function get_song_duration(song) {
const duration = await $`
ffprobe
-i "songs/${song}"
-show_entries
format=duration
-v quiet
-of
csv="p=0"
`.text();
return parseFloat(duration);
}
Which simply gets the duration of a specific song, so i later can find a randomized point in the song.
And oh, the songs? well well,
let me just say i ran a small little command
and yoinked all songs from Vylet Pony’s Soundcloud
At this point i got the ffmpeg stuff and actual audio part down and ready to get used.
Now for the randomization of songs, clue start -> end timestamps and the api routes.
My idea is like this, every api call provides a date. Which is then normalized to midnight
date.setHours(0, 0, 0, 0);
So i then can use the date’s unix time as a seed for randomness.
and i straight up just searched for some basic javascript seed random and stackoverflow got me.
So ended up using cyrb128
to generate 4 numbers from a string
and then sfc32
to use those 4 to get one number.
This would be a 0-1
number value, and then i can just do:
Math.floor(seed * (duration - 1));
Do get a random starting point within the song for clue 2 for example. Then the end timestamp is just that +1
.
And since i could do something similar for the song itself (Math.floor(seed * songs.length);
), i got that down now.
And so i implemented the API routes with Bun.serve({ ... });
and tried it out at first.
Aaand it worked quite well, had to fiddle around with mime types for the audio and what not but it worked!
Everything here on just 1 night and practially speedrunning this.
(and i did skip a ton of boring stuff like validation, error handling, etc.)
This was a fun thing to do!
Since this site was made for Vylet Pony, i just took the style they got on their own site.
I copied the font, colors, and some images and gifs to make this all fit together.
(i can actually do good looking websites when im not in charge of the design)
And since this is heavily inspired by Ottomated’s site, i already had like the layout + design made.
Just needed to mash them together into one and implement all the game logic.
Well, that was most of the work on this in terms of coding.
The server only sends audio clips and song data to the client. Nothing else.
The client handles all game logic and such, and suuure you could easily cheat.
But thats only for youre self, doesnt affect anyone else.
So easy, right?
Take the current date, yoink the songs for this day.
Then load in all the audio clips to get them ready.
Now i did some caching here and added a simple cache for the audio clips on the server.
Storing all the audio clips for 24h so i wouldnt need to go to disk and ffmpeg everytime.
And also on the client, by loading all 15 audio clips (5 songs * 3 clues) on page load.
So after starting a new SvelteKit
project, installing TailwindCSS
and some other dependencies.
I made the basic ground layout, added in the logo, description and shit.
So when it came to audio playback, shit went sooouth.
Turns out the <audio/>
html tag is REALLY slow on updating its .currentTime
variable.
Which i wanted to read a lot to get a custom seamless audio player.
Instead of using the default audio player, i wanted for the background on each clue to be the progress bar for the audio.
Totally not “inspired” by Otto, again
But this shit wouldnt work, .currentTime
was too damn slow and clunky.
And soo i dig a little snooping in Ottomated’s code for his website, and saw AudioContext
and AudioBufferSourceNode
and shit.
This is an audio API in the browser i didnt know existed (you learn something new everyday).
After a little bit of research and testing, i saw its basically a more complex <audio/>
tag in every way.
So most of my audio code is just taken from Otto, and i then adapted it for my scenario and layout.
// basic overview
const context = new AudioContext();
// needed to control volume later
const gain_node = context.createGain();
gain_node.connect(context.destination);
const audio = context.createBufferSource();
audio.buffer = // audio buffer ...
audio.connect(gain_node);
audio.start();
Then i dont really remember exactly what i did or what.
But i did a ton of design work for the clue cards, help menu, archive page, etc.
But after doing most of the gameplay UI, i could actually try it out. And it seemed to work.
Except i still had a ton of issues that just ran by me.
So first off, how the server handles randomization via normalized dates is a bad idea.
But not for the reason you might think.
const song = songs[Math.floor(seed * songs.length)];
See any issues in this code? This will 100% cause problems in the future, maybe not now, but later.
If i add ANY new songs, all previous days will have new random songs because songs.length
is now different.
Lets say seed
is 0.752
, and i have 67
songs. (0.752 * 67
) is equals to 50.384
, floored to a clean 50
.
But then lets say i got 68 songs, (0.752 * 68
) is 51.136
, floored to 51
.
So what would be song index 50 is now 51. Shifting all songs for this day by 1.
I cant have this shit.
So my solution?
Keep a local sqlite
database of every previous day, its 5 random songs and each clues starting timestamp.
And everytime someone tries to send an API request for a date
which hasnt gotten its random data generated.
It goes and generates it, once. Then saves it down for requests after it.
So nice, that issue got fixed and i cleaned up my random module and fixed the API routes to handle this.
Second issue: FUCKING SONG METADATA
Okay let me explain, if you go onto the site right now. And skip and or guess the 3 clues.
You will see which song was correct, its art, a link to the song and featuring artists.
And i got 247 songs from Vylet Pony in here.
So in the every living fuck am i gonna get all this data.
I also need this data for checking if peoples guesses are correct.
Every song can have multiple names (slight variants and or names).
And i also allow users to guess songs using their abbriviation.
and after thinking it through i came to this data template
{
"song_file_name": {
"cover": "cover_image_name",
"link": "<url>",
"artists": [
"Vylet Pony"
"..."
],
"acronyms": ["ABC"],
"names": ["Alpha Beta Calculator"]
}
}
This covers all the data i need for a song.
Now do the data gathering, 247 songs? and all of this data?
Any sane person would make a quick little web scraper and or script to call some API to gather this data.
but im no sane person.
i fucking hand picked all this data, for all fucking 247 songs. took me one entire fucking day of manual data processing
I EVEN WROTE A SMALL CLI TO ASK ME FOR THE DATA IN A SPECIFIC ORDER TO THEN THROW IT INTO THE JSON FILE.
anyway the json file is over 3000
lines and includes acronyms, names, featuring artists, links, cover image references.
I could now send this data instead of just the song name to the client. So it could display the right data.
And use the names and acronyms for checking if the users input is correct or not.
I also made it so the first name in the names
list is the displayed name.
// example taken from the yak song
"the yak song": {
"cover": "yak_song",
// imagine a tidal url
// it was too long for this
"link": "<url>",
"artists": [
"Vylet Pony",
"NekoSnicker",
"GalaxySquid",
"Namii"
],
// only names longer than
// 3 words got acronyms to them
"acronyms": [],
"names": [
"the yak song",
"yak song"
]
},
Another issue, i got no cover images.
SOooo i spent 2 hours going through Vylet’s Soundcloud, Bandcamp and personal website
to save all images of albums & singles that i could find.
Most album tracks just use the “generic” album cover for its track.
But a ton of tracks in albums also has their own cover art, and some dont.
I tried my best to use a tracks own specific cover art if i could.
I also saved them all down, rescaled every single one to 256x256
and in .webp
.
Which was 98 covers, thats a lot of images but only 6.29 MB
. I love webp.
Im not out of the swamp of bugs yet. As any good developer, i tried out the website on
multiple browsers and on my mobile device (IOS Safari).
And turns out the audio didnt work AT ALL on my mobile browser, huh weird?
But then i remember one very weird file that Ottomated had on his site, called:
safariHacks.ts
So i peaked inside and only assumed this somehow fixed my exact problem.
So i yoinked the file, added it into my audio player. AND IT FUCKING WORKED??
Now im no genius, i didnt go to school really.
But this fucking little file does some stupid shit by playing quiet audio?
I still, to this day dont understand what the fuck its doing, but hey it works!
So after fixing most critical issues, it was time to make a cleanup on everything i could.
I added a favicon, proper page titles, fixed some client bugs here and there.
Added hotkeys for clues and going to the next song and stuff.
I fixed a /song
page to view all active songs on the site, and stuff like that.
And then it was just to add some DNS CNames for the site and its api.
Fix cors, as always.
SSH onto my server, git clone the shit. Build it all and deploy it.
And i actually didnt have any issues at all on launch day.
It all worked out smoothly and after testing the production site on a few devices and browsers.
I announced it for the public in Vylet Pony’s discord and people seemed to love it!
(including Vylet!!! which was so fucking awesome ❤️)
I glossed over a ton of small details but this is my first technical blog about something.
So hopefully they can get a bit more clean in the future, but overall this was a really fun project to make.
And i actually learned quite a few new things that im happy i did.
Thanks to Vylet Pony for all the music, really, its the best.