2 min read

Play Sounds with Stimulus JS

Play Sounds with Stimulus JS

How to load sounds in a Stimulus Controller in Rails 7.1

I'm working on a game using Rails 7.1 and Stimulus JS. In the game, when the user scores a point I want to play a sound. I created an sfx_controller.js in my javascript/controllers and wrote:

import { Controller } from "@hotwired/stimulus"
import correctSound from "./assets/correct.mp3";

// Connects to data-controller="sfx"
export default class extends Controller {
  static values = {
    correct: Boolean 
  }

  connect() {
    console.log("connected")
    console.log(this.correctValue)
    if (this.correctValue) {
      this.playCorrectSound();
    }
  }


  playCorrectSound() {
    console.log("playing correct sound");
    const audio = new Audio(correctSound);
    audio.play();
  }
}

And in my view I have:

<div data-controller="sfx" data-sfx-correct-value="<%= !!@correct %>">
  ...
</div>

Unfortunately when I loaded my page I got the following error in my console.

GET http://localhost:3000/assets/controllers/assets/correct.mp3 net::ERR_ABORTED 404 (Not Found)

I inspected my <div data-controller="sfx"> tag to ensure that the !!@correct value was being set to the expected boolean value. This confirmed my suspicion that import correctSound from "./assets/correct.mp3" was at issue.

Solution

First, I added an audio tag to my view. You can render a little player by setting controls: true . When I did so, I clicked the play button to verify that the correct.mp3 was indeed where I thought it was in my assets directory.

<div data-controller="sfx" data-sfx-correct-value="<%= !!@correct %>">
  ...
  <%= audio_tag "correct.mp3", controls: true %>
</div>

When I loaded the page, I got the following extremely helpful error:

Asset `correct.mp3` was not declared to be precompiled in production.
Declare links to your assets in `app/assets/config/manifest.js`.

  //= link correct.mp3

I followed these instructions and added //= link_tree ../sounds right under the existing //= link_tree ../images following to my app/assets/config/manifest.js:

//= link_tree ../images
//= link_tree ../sounds
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
//= link_tree ../builds

I believe this makes everything in my assets/sounds directory "compilable" for production.

When I reloaded my page I could inspect the audio tag and see the following:

<audio data-sfx-target="sound" src="/assets/correct-37...1b1df46.mp3"></audio>

This led me to the understanding that the correct.mp3 file in my assets/sounds directory was being renamed by Rails. My hypothesis was that if I could grab the src value of this audio tag in my Stimulus sfx_controller.js I'd be able to then play it.

I updated my view to add a data target attribute to my audio tag to make it accessible in my sfx_controller.js:

<div data-controller="sfx" data-sfx-correct-value="<%= !!@correct %>">
  ...
  <%= audio_tag "correct.mp3", controls: false, data: { sfx_target: "sound" } %>
</div>

I updated my sfx_controller.js to be:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="sfx"
export default class extends Controller {
  // The sound target points to the audio tag in the view
  static targets = ["sound"]
  static values = {
    correct: Boolean 
  }

  connect() {
    if (this.correctValue) {
      this.playCorrectSound();
    }
  }


  playCorrectSound() {
    try {
      // I pull the src value from the audio tag
      const url = this.soundTarget.src;
      const audio = new Audio(url);
      audio.play();
    } catch (error) {
      console.error("Failed to play sound:", error);
    }
  }
}

I am not asserting that this is the right way to do this, but it does work. And no amount of Googling or ChatGPTing could quickly answer this problem for me. I hope this helps the next person trying to play an audio file in their Stimulus controller.

For reference, you can take a look at my commit on github that includes the changes described in this post.