Developing an Audio Player for Me Rex’s Megabear

A 52 track album which can be shuffled in 8.06e+67 seamless ways

Lee Martin
9 min readJun 4, 2021
Beautiful cards designed by Jono Ganz

It was February of this year when I first heard about the concept for Me Rex’s debut album, Megabear. Apparently, this small band from South London had recorded a 52 track album, in which the majority of tracks are 32 seconds long, performed at 120 bpm and 4/4 time. The album had no beginning or end but instead was intended to be played on shuffle so the listener would hear one of 8.06e+67 possible seamless permutations. That’s uh… 8 with 67 0’s. Oh, and the band had brought on the incredible Jono Ganz to design a tarot card for each track. Needless to say, I was floored and terrified.

It reminded me a lot of the project I took on for Dave Grohl’s Play performance. These radical music concepts deserve bespoke listening experiences so that the content is perfectly presented rather than living within the confines of a streaming service. (Even though it will and should do that also.) Almost immediately I began to stress about the responsibility of potentially building the audio player for this concept. This nervousness melted away after one phone call with the band when they were receptive of the simple and accessible solutions I pitched.

Similar to Dave’s project, this concept lead with a sort of dare. We dare you to shuffle the tracks of this album and play it back to hear a new seamless composition. As such, the solution should be focused on just what is required to shuffle 52 tracks and play them back seamlessly. Once a solution for that core problem was in place, we could look into delighters related to the art and UX of cards. I did the best I could to stay focused but occasionally sent fun UX to the client to keep things exciting. Did someone say solitaire? 😅

You can now listen to Megabear in its entirety for free. Be sure to check out the pre-orders for both the vinyl and deck of cards. Read on to learn about how we solved some of the core development problems.

Content Management

Megabear desktop UX

Before getting into loading assets, I should mention that we used the excellent Nuxt Content module to manage the overall JSON content of all of our cards. This included the titles, slugs, durations, and other properties we might need to produce the experience. We can load the content into our page by using the asyncData method. I quite liked the ability to easily adjust the amount of current cards using the limit() function when debugging.

async asyncData({ $content }) {
let cards = await $content('cards').limit().fetch().catch(err => {
console.error(err)
})
return {
cards
}
}

Loading Images and Audio

I needed to decide how we were going to manage loading in the assets of Megabear. The album consisted of 52 cards which were each represented by front and back images and an audio file. 104 jpegs and 52 mp3s. I did not want the user to have to wait for everything to load before being able to click play. Side note: I also noticed our experience would crash on mobile when trying to load all assets at once and start playback.

Knowing that I wanted a certain level of control over loading, I established a pair of promise loading functions. One for images:

let loadImage = url => {
return new Promise((resolve, revoke) => {
let img = new Image()
img.onload = () => {
resolve(img)
}
img.onerror = () => {
revoke()
}
img.src = url
})
}

And another for audio, which utilized our chosen audio library, Howler.js.

let loadAudio = url => {
return new Promise((resolve, revoke) => {
let sound = new Howl({
autoplay: false,
preload: false,
src: [url],
onload: () => {
resolve(sound)
},
onloaderror: () => {
revoke()
}
})
sound.load()
})
}

We could then load all or some of the assets that made up a card when we needed them. For example, I knew I wanted to completely load the first (and perhaps the second) card so users can press play as soon as possible. So we could load both images and the audio file associated with it.

let images = await Promise.all([
loadImage('front.jpg'),
loadImage('back.jpg'),
])
let audio = await loadAudio('audio.mp3')

For the remaining cards, we could skip loading the audio and just start loading the images.

I then implemented the method of lazy loading the audio. For every current card, I made sure to load the audio of the next card. Preloading the audio greatly reduced any gaps between songs which was crucial to the concept. So, as soon as the player switched to a new card, I loaded the audio of the next card up.

Not only did this benefit the overall accessibility of the app, it also greatly reduced the data requirements of the user (if they were on a paid mobile data plan) and of the client’s AWS hosting costs. Users only downloaded the assets that were required of their consumption.

Stacking the Deck Animation

As an added delighter, we used Greensock to slide cards in and under the deck as they were loaded. To do this, we simply needed to first set cards out of the current window view and then animate them to the center of the screen.

let moveOutWidth = document.body.clientWidth
let moveOutHeight = document.body.clientHeight
let $card = document.getElementById('cards').children[i]gsap.set($card, {
x: _.sample(moveOutWidth, -moveOutWidth),
y: _.random(-moveOutHeight, moveOutHeight),
rotation: _.random(-30, 30)
})
gsap.to($card, {
duration: 3,
x: 0,
y: 0,
rotation: 0,
ease: "power3.in"
})

Seamless Audio Transition

I had some wild ideas about what might be required to get the seamless audio transitions we were aiming for. One idea was to create a single large audio file of all compositions and use track durations to seek around the file as shuffled playback occurred. Another thought, which was even crazier, was to use the Web Audio API to remaster a new single audio file of all segments each time the user shuffled the album. Luckily for me, I started with the simplest possible solution and it turned out fine: making sure the next track was preloaded and simply playing it as soon as the current track ended. As long as the audio is loaded, browsers are pretty good about handling the immediate playback.

In the end, we used Howler.js to manage all audio and it worked great. Howler has a bunch of great events you can listen for and handle accordingly.

sound.on('end', () => {
// play next sound
})

52 Cards and Infinite UX Possibilities

Yes, there is a real deck you can pre-order

Once the playback, shuffling, and transition work was done and tested, I was able to spend a bit of time on the UX. The main source of inspiration for this work was the Jono Ganz designed deck of cards that was part of the release.

Cards are present all over app design which makes them very familiar to most users. As such, I wasn’t worried about educating my user on their purpose or functionality. I already mentioned that we animated cards in to stack the deck while loading. In addition, I thought a lot about which core gestures would be part of an actual handling session of the real deck of cards. Two gestures came to mind immediately: the turn and the swipe (or deal.)

Card Turn

3D animation is not my speciality but there is a lot of fun involved with trying to reverse engineer small real-world gestures like the turn of a card. Once again, we’ll be using Greensock for this purpose. Think about turning a card over from the top of a deck. In addition to flipping it over, we must also lift the card slightly to make room for the turn. The entire gesture involves raising, then flipping, and finally placing back down. We’ll setup a Greensock timeline to handle all of this.

// initialize greensock timeline
let tl = gsap.timeline({
onComplete: () => {
this.$el.setAttribute('data-swipable', true)
}
})
// raise card
tl.to(this.$el, {
z: 52
})
// turn card
tl.to(this.$el, {
duration: 1,
rotationY: 180,
ease: "circ.out"
})
// lower card
tl.to(this.$el, {
z: 0
})

Once the animation is complete, I set a data attribute called swipable to let the app know that this card can now be swiped because it was flipped.

Card Swipe

Mobile UX

The card swipe gesture has been very popular on social apps in which the user wishes to whittle down a list of choices, such as Bumble. Truthfully, we weren’t all that sure that we wanted to include this gesture as it is sort of contradictory to the concept, but in the end we included it as an easter egg. I learned the power of custom Vue.js directives on a recent Evanescence campaign and knew that is where I would establish the logic for my swipe functionality.

directives: {
swipable: {
bind(el, binding, vNode) {
...
}
}
}

I kinda knew Hammerjs would be a part of the touch solution so I went ahead and initialized that library in my directive. This just allows the user to pan the card in any direction. In addition, when the pan starts, I add the class swiping to the element. All this does is add a slight shadow to card, as if the user has lifted it off the scene.

let hammer = new Hammer(el)hammer.get('pan').set({
direction: Hammer.DIRECTION_ALL
})
hammer.on('panstart', e => {
el.classList.add('swiping')
})

I read a lot of articles and dug through a lot of sample code about handling the swiping logic but it was the simplicity of this pen by Rob Vermeer’s which really resonated with me. I love simple code. Thanks Rob! When panning, in addition to moving the card around the x and y axis of the scene, we will slightly rotate the card. This is the same delightful choice most card swiping interfaces take.

hammer.on('pan', e => {
let xMulti = e.deltaX * 0.03
let yMulti = e.deltaY / 80
let rotate = xMulti * yMulti
gsap.set(el, {
x: e.deltaX,
y: e.deltaY,
rotation: rotate
})
})

Once the user stops panning the card, we need to decide if it was swiped or if it should simply be placed back onto the deck. To do this, we check to see if the card is greater than 80 pixels from the center of screen and the velocity of horizontal movement was greater than 0.5. If both of these are true, the card was swiped. We must now estimate where it was swiped to. To do this, we can multiply either axis velocity with the browser width to calculate how far it should go on that axis. We should also use the axis delta property to calculate which direction (left or right, up or down) it should be headed. Finally, we can use a similar rotation to the pan event to give the card some visual interest.

hammer.on('panend', e => {
el.classList.remove('swiping')
// browser width
let mow = document.body.clientWidth
// should we keep or throw it
let keep = Math.abs(e.deltaX) < 80 || Math.abs(e.velocityX) < 0.5
if (keep) {
gsap.to(el, {
x: 0,
y: 0,
rotation: 0
})
} else {
// where the card is headed horizontally
let endX = Math.max(Math.abs(e.velocityX) * mow, mow)
let toX = e.deltaX > 0 ? endX : -endX

// where the card is headed vertically
let endY = Math.abs(e.velocityY) * mow
let toY = e.deltaY > 0 ? endY : -endY
// card rotation
let xMulti = e.deltaX * 0.03
let yMulti = e.deltaY / 80
let rotate = xMulti * yMulti
// card thrown
el.setAttribute('data-thrown', true)
// animate throw
gsap.to(el, {
x: toX,
y: toY,
rotation: rotate,
onComplete: () => {
gsap.set(el, {
visibility: 'hidden'
})
}
})
// next track
}
})

Once the card is thrown, we set another data attribute to keep track of this and also set its visibility to hidden to reduce CPU usage.

Thanks

Me Rex

What a fucking opportunity. Thanks to Like Management, Automation, and Big Scary Monsters for bringing me in on this project and helping pull it off. Thanks to Jono Ganz for providing such an incredible visual world to build from. And, of course, thanks to Me Rex for taking a HUGE swing as an independent artist which will serve as a massive inspiration for years to come.

Stream your own unique permutation of Megabear today and if you like what you hear and see, check out the pre-order. The party’s never over.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Written by Lee Martin

Netmaker. Playing the Internet in your favorite band for two decades. Previously Silva Artist Management, SoundCloud, and Songkick.

No responses yet

What are your thoughts?