Control Audio Playback With Your AI-Powered Assistant

Control Audio Playback With Your AI-Powered Assistant

Integrate music control with your personal assistant on the Houndify API

Welcome back to the third part of this series! First, we set up our initial application. Next, in the last article, we got the client to play music.

And after today, we'll be able to ask the assistant to "pause the music" or "play the last song".

Below is a demo of what we'll build.

Setting up

If you didn't read the last article in the series, I'd recommend following that first.

But if you prefer to skip ahead, you can find the starter code here.

To make this feature, we'll need another domain.

illustration1.jpeg

Enable and click Save Changes in the top right. This domain will enable the commands for playback control.

Handling the Music Player Command

First, create a new file in the src/handlers folder. Name it MusicPlayerCommand.ts. This file will contain a function which receives the data for the command.

Add the following code to the top of the file.

import { Howler } from "howler";
import playSound, { music } from "../lib/playSound";

const SUCCESS_RESULT = "SuccessfulPlayerCommand";
const FAILED_RESULT = "AutoPlayFailedResult";

const VOLUME_DELTA = 5 / 100;

SUCCESS_RESULT and FAILED_RESULT define locations for new results that contain suitable follow-up responses.

VOLUME_DELTA is the percentage of volume we'll raise or lower by when needed.

Now in the handlePlayerCommand function, we use an object from Howler.js to control the music. But first, we need to check the CommandType from the result's data.

Feel free to copy-paste this one.

const handlePlayerCommand = async (result: any) => {
    try {
        let commandType = result.NativeData.CommandType;
        commandType = commandType.slice(22);

        switch (commandType) {
            case "MUTE":
                music.mute(true);
                break;
            case "UNMUTE":
                music.mute(false);
                break;
            case "RAISE_VOLUME":
                music.volume(Math.min(music.volume() + VOLUME_DELTA, 1));
                break;
            case "LOWER_VOLUME":
                music.volume(Math.max(music.volume() - VOLUME_DELTA, 0));
                break;
            case "STOP":
                music.stop();
                break;
            case "REPLAY":
                music.seek(0);
                break;
            case "FAST_FORWARD":
                const skipAmount =
                    result.NativeData.RewindFastForwardAmountInSeconds;
                music.seek(
                    Math.min(music.seek() + skipAmount, music.duration())
                );
                break;
            case "REWIND":
                const rewindAmount =
                    result.NativeData.RewindFastForwardAmountInSeconds;
                music.seek(Math.max(music.seek() - rewindAmount, 0));
                break;
            case "PAUSE":
                music.pause();
                break;
            case "PLAY_CURRENT_SONG":
                if (result.NativeData.UserRequestedResume) {
                    music.play();
                }
                break;
            case "SEEK":
                if ("SeekOffsetInSeconds" in result.NativeData) {
                    music.seek(result.NativeData.SeekOffsetInSeconds);
                } else if ("SeekOffsetPercentage" in result.NativeData) {
                    music.seek(
                        result.NativeData.SeekOffsetPercentage *
                            music.duration()
                    );
                }
                break;
            case "PLAY_LAST_SONG":
                const lastSong = localStorage.getItem("lastSong");
                if (lastSong) {
                    Howler.stop();
                    playSound(lastSong);
                }
                break;
            default:
                throw new Error();
        }

        return result[SUCCESS_RESULT];
    } catch {
        return result[FAILED_RESULT];
    }
};

And don't forget to add our new file to the handlers array in index.ts.

const COMMANDS = ["MusicCommand", "MusicPlayerCommand"];

Keeping Track of Songs Played

If you read the last article, you should know that there's no export named music in playSound.ts. It should be a reference to the Howl object created when playing audio.

Also, we should only update it when the audio played is a song, not any feedback sounds.

Open up the file and replace its contents with the following:

import { Howl, HowlOptions } from "howler";

export let music: Howl;

export default function playSound(
    src: string,
    options?: Omit<HowlOptions, "src">,
    type: "music" | "sound" = "sound"
) {
    const sound = new Howl({
        src,
        ...options,
    });

    if (type === "music") {
        music = sound;
    }

    return sound.play();
}

But also, when we play a song with play sound, we'll need to set the type to "music".

We also need to store the URL for the current song in case the user wants to play it again. For that, we can use localStorage.

Open up MusicCommand.ts and replace the line where we play the song with this:

playSound(audioURL, { format: "webm" }, "music");
localStorage.setItem("lastSong", audioURL);

Conclusion

And that's everything! We used Howler.js to control the music and localStorage to store the last played song. Like before, you can find the source code for this article here if you wish.

Next time in this series, we'll perform music recognition so that we can ask, "What's that song?". Enjoy!

Did you find this article valuable?

Support CS310 by becoming a sponsor. Any amount is appreciated!