Building a Virtual Beat Box in Redwood
Tags: react, javascript, graphql, tone
Published: 2021-10-28 13:45:11

---
title: Building a Virtual Beat Box in Redwood
published: true
description: Most of the time the only media types that get attention in web development are videos and images. We're going to make a small app that will let users work with audio in the browser and make their own beats!
tags: #react, #javascript, #graphql, #tone
cover_image: https://cdn.sanity.io/images/5ad74sb4/production/01608009dbf71522062e7b37e55f62d281f57628-1000x420.png?w=2000&h=2000&fit=max
---

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
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](https://www.postgresql.org/download/).

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:

```bash
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:

```javascript
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:

```javascript
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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.

```javascript
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.

```javascript
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.

![initial beat box grid](https://res.cloudinary.com/jesse-thisdot/image/upload/v1629992847/e-603fc55d218a650069f5228b/ysntk7jovn6omb6jdds4.png)

### 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.

```javascript
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:

```javascript
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:

```javascript
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](https://tonejs.github.io/).

### 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`.

```javascript
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.

```javascript
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.

```javascript
<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.

```javascript
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.

```javascript
<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.

![selected notes in the grid](https://res.cloudinary.com/jesse-thisdot/image/upload/v1629992879/e-603fc55d218a650069f5228b/ewihgxrswbgy7n9veku7.png)

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:

```javascript
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](https://cloudinary.com/users/register/free) 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:

```javascript
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.

```javascript
{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.

```javascript
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.

![video playing after saving the mix](https://res.cloudinary.com/jesse-thisdot/image/upload/v1629992905/e-603fc55d218a650069f5228b/eevq3pih1orelocsgeod.png)

## Finished code

You can take a look at the front-end code in this [Code Sandbox](https://codesandbox.io/s/jolly-grass-lj5ds) or you can check out the whole project in [the `virtual-music-box` folder of this repo](https://github.com/flippedcoder/blog-examples/tree/main/virtual-music-box).

## 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.