Javascript Relaxation Sounds PWA (Free Download)

There sure are a lot of these “white noise” or “relaxation sound” apps these days. But know what? They are actually pretty easy to create with modern HTML/CSS/JS – Just create multiple new Audio() and set them to loop infinitely. But let’s take it one step up, and create an offline web app in this guide – Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES & REQUIREMENTS

  • This is not quite a “newbie-friendly” open-source project and example – It involves the use of service workers, cache storage, and media element API.
  • Too much of a hassle to create an “online example”, just download the source code or grab it from GitHub.
  • “Grade A” browser required, alongside an https:// server. http://localhost is fine for testing too.
  • To set your own sounds, check out part 2 of “how it works” below.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

LICENSE & DOWNLOAD

This project is released under the MIT License. You are free to use it for your own personal and commercial projects, and modify it as you see fit. On the condition that there the software is provided “as-is”. There are no warranties provided and “no strings attached”. Code Boxx and the authors are not liable for any claims, damages, or liabilities.

Download | GitHub

 

 

HOW IT WORKS

Not going to explain everything line-by-line (will take forever), but here’s a quick walkthrough of the Javascript Relax Sounds PWA.

 

PART 1) SOUNDS LIST

js-relax-sounds.js
var sounds = [
  { name: "Rain", ico: "grain", src: "rain.webm" },
  { name: "Chimes", ico: "notifications_active", src: "chimes.webm" },
  { name: "Crickets", ico: "bug_report", src: "crickets.webm" },
  { name: "Campfire", ico: "local_fire_department", src: "campfire.webm" },
  { name: "Fountain", ico: "water_drop", src: "fountain.webm" },
  { name: "Bird", ico: "flutter_dash", src: "bird.webm" }
];

First, we start with “Captain Obvious”. Don’t think it needs an explanation, the list of available sounds, the name, and icon. Feel free to add more by yourself.

 

PART 2) USER INTERFACE

2A) THE HTML

js-relax.html
<!-- (A) SOUNDS -->
<div id="sWrap"></div>

The HTML is pretty much an empty <div>. We will run through the above sounds and generate the buttons in Javascript.

 

 

2B) JAVASCRIPT INIT & DRAW HTML

assets/js-relax.js
var ss = {
  // (A) INITIALIZE
  init : () => {
    // (A1) CHECK - AUDIO
    if (!"audio" in window) {
      alert("Your browser does not support HTML audio.");
      return;
    }
 
    // (A2) CHECK - SERVICE WORKER
    if (!"serviceWorker" in navigator) {
      alert("Your browser does not support service workers.");
      return;
    }
 
    // (A3) CHECK - CACHE STORAGE
    if (!"caches" in window) {
      alert("Your browser does not support cache storage.");
      return;
    }
 
    // (A4) REGISTER SERVICE WORKER
    navigator.serviceWorker.register("js-relax-worker.js");
 
    // (A5) DRAW HTML SOUND BUTTONS
    let wrap = document.getElementById("sWrap");
    for (let sound of sounds) {
      let button = document.createElement("div");
      button.className = "sound";
      button.innerHTML = `<div class="mi">${sound.ico}</div>
      <div class="control">
        <div class="name">${sound.name}</div>
        <input class="vol" type="range" min="0.1" max="1" step="0.1" value="1" disabled>
      </div>`;
      sound.hIco = button.querySelector(".mi");
      sound.hVol = button.querySelector(".vol");
      sound.hIco.onclick = () => ss.preload(sound);
      wrap.appendChild(button);
    }
  },
  // ...
};
window.onload = ss.init;

On window load, ss.init() will run.

  • (A1 to A3) Checks if the browser supports the required features.
  • (A4) Register a service worker. This is required to make an “installable web app”.
  • (A5) Loop through sounds above and generate their respective HTML buttons.

 

 

PART 3) PRELOAD SOUND & TOGGLE PLAY/PAUSE

assets/js-relax.js
// (B) PRELOAD SOUND
preload : sound => {
  // (B1) FADE ICON TO INDICATE "NOW LOADING"
  sound.hIco.style.opacity = 0.5;
  sound.hIco.onclick = "";
 
  // (B2) CREATE AUDIO OBJECT
  sound.player = new Audio("assets/" + sound.src);
  sound.player.loop = true;
  sound.player.autoplay = true;
 
  // (B3) ON SOUND LOADED
  sound.player.oncanplaythrough = () => {
    // (B3-1) SET CSS ON PLAY/PAUSE
    sound.player.onplay = () => sound.hIco.classList.add("playing");
    sound.player.onpause = () => sound.hIco.classList.remove("playing");
 
    // (B3-2) CLICK BUTTON TO PLAY/PAUSE
    sound.hIco.onclick = () => ss.toggle(sound);
 
    // (B3-3) ENABLE VOLUME CONTROL
    sound.hVol.onchange = () => sound.player.volume = sound.hVol.value;
    sound.hVol.disabled = false;
 
    // (B3-4) "UNFADE" ICON + START PLAYING NOW
    sound.hIco.style.opacity = 1;
    sound.hIco.classList.add("playing");
    sound.player.play();
  };
 
  // (B4) ATTACH SOUND SOURCE - START LOAD
  sound.player.src = "assets/" + sound.src;
},
 
// (C) PLAY/PAUSE
toggle : sound => {
  if (sound.player.paused) { sound.player.play(); }
  else { sound.player.pause(); }
}
  • (B) Take note, we do not preload all the sounds automatically… Slow servers are going to suffer. Thus, we run ss.preload() once when the user first clicks on the sound – This will load the selected sound, enable the controls, and start playing when ready.
  • (C) All subsequent clicks on the loaded sound will simply toggle play/pause.

 

 

PART 4) INSTALLABLE WEB APP

4A) HTML META HEADERS

js-sleep-sounds.html
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="js-relax-manifest.json">
 
<!-- ANDROID + CHROME + APPLE + WINDOWS APP -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="white">
<link rel="apple-touch-icon" href="assets/icon-512.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="JS Relax">
<meta name="msapplication-TileImage" content="assets/icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">

We already have a complete web app. But to take this one step further and turn it into an “installable web app”, there are 3 things to address.

  • HTML meta headers – Specify the app name and icons. Quite a pain, but the above is what I gathered from the Internet.
  • Define a web manifest file.
  • Register a service worker.

 

4B) WEB MANIFEST

js-relax-manifest.json
{
  "short_name": "JS Relaxation",
  "name": "JS Relaxation",
  "icons": [{
    "src": "assets/favicon.png",
    "sizes": "64x64",
    "type": "image/png"
  }, {
    "src": "assets/icon-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "js-relax.html",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

Well, a manifest file is what it is. A file containing the app name, icon, theme, settings, information, etc…

 

 

4C) SERVICE WORKER

js-relax-worker.js
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open("JSSleep")
    .then(cache => cache.addAll([
      "assets/favicon.png",
      "assets/icon-512.png",
      "assets/head-pwa-relax.webp",
      "assets/maticon.woff2",
      "assets/js-relax.css",
      "assets/js-relax-sounds.js",
      "assets/js-relax.js",
      "js-relax-manifest.json",
      "js-relax.html",
      "assets/bird.webm",
      "assets/campfire.webm",
      "assets/chimes.webm",
      "assets/crickets.webm",
      "assets/fountain.webm",
      "assets/rain.webm"
    ]))
    .catch(err => console.error(err))
  );
});
 
// (B) CLAIM CONTROL INSTANTLY
self.addEventListener("activate", evt => self.clients.claim());
 
// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", evt => evt.respondWith(
  caches.match(evt.request).then(res => res || fetch(evt.request))
));

Finally, for the uninitiated – A “service worker” is simply Javascript that runs in the background.

  • (A) Create a local cache and save all the specified files.
  • (C) Offline capabilities – Try to load the required files from the cache first, and fallback to loading from the network if not available.

In simple terms, this worker does the “offline app magic”.

 

EXTRA BITS & LINKS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

COMPATIBILITY CHECKS

Most of the required features are already well-supported on modern “Grade A” browsers.

 

FREE SOUNDS?

There are plenty of websites to get free sounds. Just do a “free sound download” on the Internet. Here are a few:

 

PERFORMANCE & LIMITATIONS

As you can see, this web app simply creates multiple new Audio("SOUND.MP3") and plays them. Most modern smartphones should have sufficient processing power to handle multiple sounds playing at once. But still, the older or lower-end phones are going to suffer. After all, Javascript does not have the same level of performance as a native app.

Also, the page needs to stay on the screen to keep playing on mobile devices. Although there are ways around this, by turning it into a Cordova app and working with background music.

 

IMPROVEMENT IDEAS

Yes, this simple “sleep sounds” PWA works. But there is still room for improvement – Volume sliders for individual sounds, a timer to “switch off all sounds”, or even the crazy stuff of connecting to a health tracker with the Web Bluetooth API. But for now, I shall put these on the “wish list”.

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!