Building a Virtual Beat Box in Redwood
Milecia
Posted on October 28, 2021
Sometimes you don't need to make a serious app to practice your JavaScript skills. We're going to play with a full-stack music app! It'll be a virtual beat box that you can make music with and store it in a database.
Setting up the app
We'll just jump in and start building the Redwood app because it has integrations to make it easier to set up the front-end and back-end. So in a terminal, run this command:
yarn create redwood-app virtual-music-box
This generates a new Redwood project with a lot of new files and directories for us and we'll be focused on the web
and api
directories. The web
directory will hold all of the front-end code, which we'll get to a little later. The api
directory contains all of the back-end code.
To get started, let's write the back-end code.
Building the back-end
Redwood uses GraphQL to handle the back-end and Prisma to work with the database. We'll start by setting up a local Postgres instance. If you don't have Postgres installed, you can download it here.
Now you're going to add a new file to the root of the project called .env
. Inside that file, you'll need to add the connection string for your Postgres instance. It should look similar to this:
DATABASE_URL=postgres://postgres:admin@localhost:5432/mixes
With this connection string in place, let's move to the schema.prisma
file in the api > db
directory. This is where you can add the models for your database. In this file, you'll see a provider
with sqlite
as the value. We're going to update that to postgresql
like this:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
This is where we connect to the database using the connection string in that .env
file we made. Next, we'll add a model to hold the music we make.
Making the model
You can delete the example model in prisma.schema
and replace it with this:
model Mix {
id String @id @default(cuid())
name String
sample String
}
We're creating a new table called Mix
that has a cuid
for the id
, a name
for the song, and the sample
of notes that make up the song. Since we have the model we need in place, we can run a database migration now with this command:
yarn rw prisma migrate dev
This will create a new database on your local Postgres server and it will create a new migrations
directory inside api > db
with the SQL to update the database.
Creating the GraphQL types and resolvers
With the database ready to go, we can start working on the GraphQL server. A cool feature that Redwood has is autogenerating the types and resolvers for the basic CRUD functionality we need to get going. We'll take advantage of this with the following command:
yarn rw g sdl mix --crud
This creates the GraphQL types and resolvers we need to create, update, delete, and read mixes we want to work with. If you take a look in api > src > graphql
, you'll see a new file called mixes.sdl.ts
. This has all of the types we need based on the model we created earlier.
Next, take a look in api > src > services > mixes
. This holds the file for our resolvers and testing. If you open mixes.ts
, you'll see all of the resolvers for create, read, update, and delete functionality already written for us.
So now we have a fully functional back-end! That means we can switch our focus to the front-end where we actually get to make music.
Moving to the front-end
We have to set up an interface for our users to select notes to play. We'll use a grid to handle this. There are a few libraries we need to install before we start working on the component.
In a terminal, go to the web
directory and run these commands:
yarn add tone
yarn add styled-components
The tone
library is how we'll add sound to the browser. We'll use styled-components
to help make the grid.
Let's start by creating a new page in Redwood. In a terminal, go back to the root directory of the project and run this:
yarn rw g page mixer /
This will create a new page for the main view of our app. It automatically updates Routes.tsx
for us and if you take a look in web > src > pages > MixerPage
, you'll see the component, a Storybook story, and a unit test. Redwood generates all of this for us from that command above.
Adding the mixer
Go ahead and open MixerPage.tsx
and delete everything out of it. We'll be making a completely new component. To start, we'll add all of the imports we need.
import { useState } from 'react'
import { useMutation } from '@redwoodjs/web'
import * as Tone from 'tone'
import styled from 'styled-components'
Now we can define the MixerPage
component and a few styled components to get started. We'll write the code and then walk through it.
const Flex = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
`
const Square = styled.div`
background-color: #ABABAB;
border: 2px solid #313131;
height: 250px;
width: 250px;
`
const MixerPage = () => {
const notes = ['G3', 'A6', 'C9', 'B5', 'D7', 'F1', 'E8', 'A7', 'G6', 'B1', 'F4', 'C5']
return (
<>
<h1>Mixer Page</h1>
<Flex>
{notes.map(note => (
<Square key={note} onClick={() => console.log(note)} />
))}
</Flex>
<button onClick={() => console.log(mix)}>Save Sounds</button>
</>
)
}
export default MixerPage
First, we make a couple of styled components. The Flex
component is a flexbox we're able to make the grid shape we need for the beat box. The Square
component is a colored box that represents a square in our grid.
Then we define the MixerPage
component and add the export statement at the bottom of the file. Inside the component, we add a notes
array that holds the notes we want users to be able to play.
Next, we add the return statement where we create our grid based on the number of notes in the array. We map over the notes
array and add an onClick
callback to work with notes. Then there's a save button that will eventually connect to the back-end and store all of the beats we make.
If you run the app with yarn rw dev
, you should see something like this in your browser.
Connecting the back-end to save beats
There's one more thing we need to add and that's the connection to the back-end. We'll add our GraphQL mutation for saving new beats right below the Square
styled component.
const CREATE_MIX_MUTATION = gql`
mutation CreateMixMutation($input: CreateMixInput!) {
createMix(input: $input) {
id
}
}
`
Now we can start adding the real functionality to our grid. Inside the MixerPage
component, add this code above the notes
array:
const [createMix] = useMutation(CREATE_MIX_MUTATION)
const [mix, setMix] = useState([])
This gives us access to the createMix
mutation defined in the GraphQL resolvers we made earlier. It also creates the mix
state we'll use to store the notes on in the database.
Now we get to do the fun thing and add the sound to our app. Below the mix
state, add this line:
const mixer = new Tone.MembraneSynth().toDestination()
This is how we use the tone
library to play some kind of sound through our speakers. You can check out some of the others in their docs.
Playing the notes
With the mixer
object ready, we need to add the function that will play the notes when a user clicks on a Square
.
const playNote = (note) => {
mixer.triggerAttackRelease(note, "6n")
const isSet = mix.includes(note)
if (!isSet) {
setMix([...mix, note])
} else {
const updateMix = mix.filter((mixNote) => mixNote !== note)
setMix(updateMix)
}
}
This playNote
function takes in a string for the note
value, which will be the note for the clicked Square
. Then we use the mixer
to actually play the sound with the triggerAttackRelease
method and we pass it the note
and a string for how we want the note to sound. You can play with this value and see how it changes the sound.
Next, we do a quick check to see if the note is already in the mix
state. If it is not in the mix
state, we'll update the state. Otherwise, we will filter out the note from the existing state and update the mix
state.
The other function we need to make will handle saving the mixes we make.
const saveMix = (mix) => {
const input = { name: `mix-${mix[0]}`, sample: mix.join() }
createMix({ variables: { input } })
}
This function takes the mix
state and creates the input
value we need to pass to the GraphQL mutation. Then we call the createMix
mutation with the input
value and save the mix to the database.
Now we're ready to wrap things up by calling these functions in our elements.
Updating the elements
We need to update some props on the Square
element.
<Square key={note} selected={mix.includes(note)} onClick={() => playNote(note)} />
We're using the selected
prop to update the color of a square. That means we'll have to make a minor update to the Square
styled component to take advantage of this prop.
const Square = styled.div`
background-color: ${props => props.selected ? '#ABABAB' : '#EFEFEF'};
border: 2px solid #313131;
height: 250px;
width: 250px;
`
Now when a note is selected or unselected, the color of the square will update.
Next, we need to call the saveMix
function when the button
is clicked.
<button onClick={() => saveMix(mix)}>Save Sounds</button>
This takes the current mix
state and passes it to the GraphQL mutation. If you run the app and click a few squares, you should see something like this.
There's one more thing we can add to take this app to the next level. We can play specific videos after the mix has been saved.
Adding media
We'll start by adding an array with links to different videos hosted in Cloudinary. Using Cloudinary just makes it easier to work with media files instead of worrying about hosting them ourselves in AWS or storing things in the database.
So right under the notes
array, add the following videos
array:
const videos = ['https://res.cloudinary.com/milecia/video/upload/v1606580790/elephant_herd.mp4', 'https://res.cloudinary.com/milecia/video/upload/v1606580788/sea-turtle.mp4', 'https://res.cloudinary.com/milecia/video/upload/v1625835105/test0/tq0ejpc2uz5jakz54dsj.mp4', 'https://res.cloudinary.com/milecia/video/upload/v1625799334/test0/ebxcgjdw8fvgnj4zdson.mp4']
Feel free to make your own Cloudinary account and use some videos you like!
This has a few video links that we'll use to display something when a mix has been saved. Now we need to create a new state to store the video source URL for when we get ready to render. You can add this below the mix
state:
const [video, setVideo] = useState('')
We also need to add a video
element below the button
and its source is the video
state. The video
element will only display when the video
state is not an empty string.
{video !== '' &&
<video src={video} width='480' height='360' controls>
</video>
}
The last bit of code we need is to update the video
state when we've successfully saved a beat. We'll add this to the saveMix
method after we call the mutation.
const randomInt = Math.floor(Math.random() * (videos.length - 1))
setVideo(videos[randomInt])
This gets a random video from the array and makes it the video that plays after a successful submission. After you save a mix, you should see something like this in the browser.
Finished code
You can take a look at the front-end code in this Code Sandbox or you can check out the whole project in the virtual-music-box
folder of this repo.
Conclusion
There are a lot of different ways you can play with Tone.js to improve your apps. You could use it to make things more accessible for users. You can add a different level of entertainment for users that work with your app frequently. Or you can start teaching music theory online.
Web apps with sound give users a different experience and it's also fun to work with. Don't be afraid to try new things! You never know what you might find useful or interesting.
Posted on October 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.