A simple audio sequencer using Web Audio Api & Angular

Stefanos Kouroupis - Jul 7 '19 - - Dev Community

I decided to move my posts from medium to here, for various personal reasons. As I move them I also going to delete them from there, no reason keeping both. I am starting with the least popular one. :D

As most developers, I spend my free time developing… or gaming (ahem), or since I became a parent …more seeking opportunities for sleep and less developing.

Nevertheless this time I decided to created a simple minimal audio sequencer.

So let’s pretend that I actually gave a bit of though to this project and wrote down my requirements before I started developing.

  • Visualise a sequence of n elements that represent different sounds
  • The elements can be turned on and off (muted/change color)
  • A start button that will execute the sequence
  • Change color as each element is executed
  • When done reset the state and colors of each element

Why Angular? Why not. It’s just a personal preference.

My app has one component and one service.

> sequencer.component.ts
> sound.service.ts

My models are the following

interface Block {
    color: string; // hex color
    state: true; // true = sound, false  = no sound
    note: Note;
}
interface Note {
    name: string; // name of the note
    frequency: number; // frequency (hertz) of the note
    position: number; // I don't use it but its useful to know
                     // the octave this frequency corresponds to
}

…and now that the hard work is done, we can move the easy part implementing the idea.

First our service.

@Injectable()
export class SoundService {
  // initially i had like 6 octaves but it was pretty pointless
  // so I trimmed it down to 2 (octave 4 and 5)
  public notes = [
  {
    name: 'C',
    position: 4,
    frequency: 261.63
  }, {
    name: 'C#',
    position: 4,
    frequency: 277.18
  }, {
    name: 'D',
    position: 4,
    frequency: 293.66
  } ... ] // I am not going to list all the notes
  private audioCtx = new (window['AudioContext'] || window['webkitAudioContext'])();
  private gainNode = this.audioCtx.createGain();
  public play(freq, time, delay) {
    const oscillator = this.audioCtx.createOscillator();
    oscillator.connect(this.gainNode);
    this.gainNode.connect(this.audioCtx.destination);
    oscillator.type = 'sine'; 
    oscillator.frequency.value = freq;
    oscillator.start(this.audioCtx.currentTime + delay);
    oscillator.stop(this.audioCtx.currentTime + delay + time);
  }
}

Nothing fancy we define the following variables.

  • a note 🎶 object with the corresponding frequencies
  • the audio context 🎹 which processes the signals
  • the gainNode which controls the volume 🔊

And finally our Play function. I am using the play function in a bit of a odd way. Instead of sending a note when I want the note to be executed, I am sending a note with a delay, so I can send the entire sequence. The downside of doing it this way is that the sequence cannot been stopped (except of course if you had the references from the oscillator objects).

Our template (sequencer.component.html) is really simple…to the point of being silly 🔥

<div class="pad">
  <div *ngFor="let block of blocks;let i = index" class="block">
    <div class="single-block"
         [ngStyle]="{'background-color': block.color}"
         (click)="changeState(i);">
      {{ block.note.name }}
    </div>
  </div>
</div>
<button class="btn" (click)="play()">start</button>

And now to our main bit the sequencer.component.ts!

We define some variables

@Component({
  selector: 'sequencer-pad',
  templateUrl: './sequencer.component.html',
  styleUrls: ['./sequencer.component.css']
})
export class PadComponent implements OnInit {
  public blocks: Block[] = [];
  private blockSize = 13; // sequencer will use 13 notes
  private noteLength = 1; // duration of the note (1 second)
  constructor(private soundService: SoundService) { }
  ...
}
  • blocks are our building blocks 😃
  • blockSize is how many of the notes we defined in the service we wish to use in the sequence. Bare in mind I am doing it this way to sound less boring. If I wanted to create a more realistic sequencer, I would probably have a collection of Block arrays, each array having a unique sound.
  • noteLength, this is basically the duration of the sound 🎵 produced. 1 second should be fine in this case.
  • On ngOnInit() I create my block array
ngOnInit() {
  // add default values to the blocks array
  for (let index = 0; index < this.blockSize; index++) {
    this.blocks.push({
      color: 'limegreen',
      state: true,
      note: this.soundService.notes[index]
    });
  }
}
  • when you click a note you need to change its color and state
/**
 * change the color of the div, and switch its state (on/off)
 * @param index
 */
public changeState(index: number) {
  this.blocks[index] = (this.blocks[index].color === 'limegreen') ?
  {
    color: 'tomato',
    state: false,
    note: this.blocks[index].note
  } : {
    color: 'limegreen',
    state: true,
    note: this.blocks[index].note
  };
}
  • when the sequence finishes we need to reset the colors and state
/**
 * when sequence ends this returns the colors but to limegreen
 */
private resetColor() {
  this.blocks.forEach(element => {
    element.color = 'limegreen';
    element.state = true;
    });
  }
}
  • Play the sequence and color it appropriately
/**
 * play the notes that have a true state
 */
public play() {
  this.blocks.forEach((element, index) => {
    if (element.state) {
      const note = this.soundService.notes[index];
      this.soundService.play(note.frequency,
      this.noteLength, index * this.noteLength);
    }
    // this is to emulate the progress
    setTimeout(() => {
      element.color = 'lightpink';
      if (index + 1 === this.blocks.length) {
        setTimeout(() => {
          this.resetColor();
        }, this.noteLength * 1000);
      }
    }, this.noteLength * 1000 * index);
  });
}

So like I said before I am iterating the block array and sending each note to the soundService with the appropriate delay. Because the way I designed it, I have no feedback from the soundService when a tone starts and ends, I had to use setTimeout (twice) to emulate the progress of the sequence in the UI.

Bonus: the styles I used

.pad {
margin-top: 20px;
display: flex;
flex-direction: row;
}
.single-block {
flex: auto;
min-height: 40px;
min-width: 40px;
display: inline-block;
border: none;
margin-right: 5px;
cursor: pointer;
border-radius: 5px;
text-align: center;
border-color: green;
border-style: solid;
}
.btn {
margin-top: 20px;
color: black;
background: #ffffff;
text-transform: uppercase;
padding: 20px;
border: 5px solid black;
border-radius: 6px;
display: inline-block;
}
.btn:hover {
color: #ffffff;
background: green;
transition: all 0.4s ease 0s;
}
. . . . . . . . . . . . . . . . . . . . . . . . .