Javascript Sleep Sounds PWA (That Works Offline)

There sure are a lot of these “sleep sounds”, “white noise”, or “sleep relaxation” apps these days. But know what? They are actually pretty easy to create with modern HTML/CSS/JS. But let’s take it one step up, and create an offline PWA 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 Sleep Sounds PWA.

 

PART 1) INITIALIZE

assets/js-sleep-sounds.js
// (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("CB-worker.js")
  .then(reg => ss.draw())
  .catch(err => {
    alert(err.message);
    console.error(err);
  });
},
// ...
window.onload = ss.init;

On window load, ss.init() is the first thing that runs. Pretty self-explanatory – Checks if the required features are available, and proceeds to register a service worker that will support the offline app/file caching.

 

 

PART 2) HTML INTERFACE

assets/js-sleep-sounds.js
// (B) AVAILABLE SOUNDS - FEEL FREE TO ADD YOUR OWN
sounds : [
  { name: "Rain", ico: "grain", src: "rain.mp3" },
  { name: "Chimes", ico: "notifications_active", src: "chimes.mp3" },
  { name: "Crickets", ico: "emoji_nature", src: "crickets.mp3" },
  { name: "Campfire", ico: "local_fire_department", src: "campfire.mp3" }
],
 
// (C) DRAW SOUND BUTTONS
draw : () => {
  let wrap = document.getElementById("cb-main");
  for (let sound of ss.lib) {
    sound.button = document.createElement("div");
    sound.button.className = "sound";
    sound.button.innerHTML = `<div class="mi">${sound.ico}</div>
    <div class="name">${sound.name}</div>`;
    sound.button.onclick = () => ss.preload(sound);
    wrap.appendChild(sound.button);
  }
},

After the initialization, ss.draw() will generate the HTML interface.

  • (B) Add/remove your available sounds here. should be pretty self-explanatory.
    • name The “display name” of the sound.
    • ico “Icon to use”, this app is using Google Material Icons, check out Google Fonts for the full list.
    • src The file name, place your sound file in the assets folder.
    • Also, remember to add your sound file to the service worker for caching js-sleep-sound-sw.js.
  • (C) Creates the HTML for the “sound buttons”. Pretty much just a <div class="mi"> for the icon, and <div class="text"> for the text.

 

 

PART 3) PRELOAD SOUND & TOGGLE PLAY/PAUSE

assets/js-sleep-sounds.js
// (D) PRELOAD SOUND
preload : sound => { if (sound.player == undefined) {
  // (D1) FADE ICON TO INDICATE "LOADING"
  let ico = sound.button.querySelector(".mi");
  ico.style.opacity = 0.5;
 
  // (D2) CHANGE ONCLICK TO DIRECTLY PLAY/PAUSE
  sound.button.onclick = () => ss.toggle(sound);
 
  // (D3) CREATE AUDIO OBJECT
  sound.player = new Audio("assets/" + sound.src);
 
  // (D4) LOOP SOUND
  sound.player.loop = true;
  sound.player.autoplay = true;
 
  // (D5) TOGGLE ICON HIGHLIGHT ON PLAY/PAUSE
  sound.player.onplay = () => sound.button.classList.add("playing");
  sound.player.onpause = () => sound.button.classList.remove("playing");

  // (D6) ON SOUND LOADED
  sound.player.oncanplaythrough = () => {
    // (D6-1) "UNFADE" ICON
    ico.style.opacity = 1;
 
    // (D6-2) START PLAYING NOW
    sound.player.play();
  };
}},
 
// (E) PLAY/PAUSE
toggle : sound => {
  if (sound.player.paused) { sound.player.play(); }
  else { sound.player.pause(); }
}
  • (D) Although the service worker will save all the audio files into a local cache, I figure that slow servers and devices are going to suffer if we pre-load all the audio files at once. Thus, ss.preload() will only trigger on the first click to create the new Audio() object.
  • (E) All subsequent clicks on the sound icons will simply toggle play/pause.

 

 

PART 4) SERVICE WORKER

CB-worker.js
// (A) FILES TO CACHE
const cName = "JSSleep",
cFiles = [
  // (A1) ICONS + FONTS
  "assets/favicon.png",
  "assets/icon-512.png",
  "assets/maticon.woff2",
  // (A2) APP
  "assets/js-sleep-sounds.css",
  "assets/js-sleep-sounds.js",
  "CB-manifest.json",
  "js-sleep-sounds.html",
  // (A3) SOUNDS
  "assets/chimes.mp3",
  "assets/crickets.mp3",
  "assets/rain.mp3"
];
 
// (B) CREATE/INSTALL CACHE
self.addEventListener("install", evt => evt.waitUntil(
  caches.open(cName)
  .then(cache => cache.addAll(cFiles))
  .catch(err => console.error(err))
));
 
// (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))
));

Not going to explain this line-by-line, but basically:

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

 

PART 5) INSTALLABLE PWA

js-sleep-sounds.html
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="CB-manifest.json">

All of the above mechanics already make an offline-capable web app. But just one last final step, we add a manifest file to the project, and magic – It becomes an installable web app.

 

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 from… 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.

 

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!

Leave a Comment

Your email address will not be published. Required fields are marked *