Building a Chiming Clock with Electron and JavaScript
For whom the bell tolls... every 15 minutes
My dad has his computer set to announce the time every 15 minutes using a jarring robotic voice. I told him it was awful. We both agreed the sound of church bells would be far better.
So I built an app to do just that: a simple Electron app that rings church bells every 15 minutes and on the hour. One short peel for :15 past, two short peels for :30, three short peels for :45, and long, deeper peels on the hour — one for each hour, just like real church bells.
The problem was, I’d never built an Electron app before. Time to start researching.
Electron is deceptively simple once you get your head around it. It’s just JavaScript. Like any web app, you need a backend to run logic (Node.js), and a frontend (HTML/JS/CSS) to display content. Then you wrap it all in Electron and let it run as a native desktop app.
My main goals:
The app needed to run in the background — no open window required
It had to be self-contained
It had to be written entirely in JavaScript
Setting up Electron
Quick start:
npm init -y
npm install electron --save-dev
Update package.json:
"scripts": {
"start": "electron ."
}
With our dependencies set up, it’s time to write some code. I started with a basic index.html, styles.css, and renderer.js, like building a web app “the old-school way.”
But this app was all about audio — playing sounds on a timed schedule — and at first, running npm run start did nothing. The app just crashed. I needed two things:
Proper Electron wiring (the Electron entry point)
Time and audio logic in the right place
Wiring Electron
The entry point for Electron needs to be a file like main.js, which controls the application lifecycle and window creation.
Here's a simplified main.js:
const { app, BrowserWindow } = require('electron');
function createWindow() {
const mainWindow = new BrowserWindow({
width: 500,
height: 500,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(createWindow);
The webPreferences options are important here: we’re enabling Node.js in the frontend and disabling context isolation (for simplicity).
Once I had a working window that could load my HTML file, I confirmed everything was wired up by scaffolding a quick "Hello World" and running npm run start.
Making It Chime
Now came the heart of the app: the bells. I created a function called playBellSound(type, count) which builds the filename from two arguments:
typeis either "hourly" or "quarter"counttells the function how many chimes to play (1, 2, 3, or up to 12)
I used the play-sound Node package to handle the audio playback, and stored all audio files in an audio/ folder.
const path = require('path');
const player = require('play-sound')();
function playBellSound(type, count) {
const fileName = type === "hourly"
? `bells-hour-${count}.mp3`
: `bells-quarter-${count}.mp3`;
const baseDir = app.isPackaged
? path.join(process.resourcesPath, 'app.asar.unpacked', 'audio')
: path.join(__dirname, 'audio');
const fullPath = path.join(baseDir, fileName);
player.play(fullPath, (err) => {
if (err) {
console.error("Error playing sound:", err);
} else {
console.log("Chimed:", fileName);
}
});
}
Scheduling the Chimes
To trigger the chimes, I set up a simple scheduler:
function startBellScheduler() {
setInterval(() => {
const now = new Date();
const minutes = now.getMinutes();
const hours = now.getHours() % 12 || 12;
if (minutes === 0) {
playBellSound("hourly", hours);
} else if (minutes === 15) {
playBellSound("quarter", 1);
} else if (minutes === 30) {
playBellSound("quarter", 2);
} else if (minutes === 45) {
playBellSound("quarter", 3);
}
}, 60 * 1000);
}
I called this function after the window was created, and the chimes began.
Packaging the App
Once the app worked, I needed to bundle it with electron-builder. But audio didn’t play in the packaged version — because Electron packages everything into a .asar archive, which doesn’t always work well with binary/audio files.
To fix that, I updated package.json to unpack the audio directory:
"build": {
"appId": "com.timhilton.churchbells",
"productName": "Church Bells",
"directories": {
"output": "dist"
},
"files": [
"index.html",
"styles.css",
"main.js",
"audio/**/*",
"assets/icon.icns"
],
"asarUnpack": ["audio"],
"mac": {
"icon": "assets/icon.icns",
"target": ["dmg"]
}
}
Now it worked.
Run the build:
npx electron-builder --mac --publish never(If you’re deploying to the Mac App Store, you’ll also need entitlements.mas.plist and provisioning profiles. I’ll let you dig into that.)
Final Thoughts
This project was a blast. I got to replace an annoying robotic voice with the pleasant ring of bells — and learned how to build and package an Electron app in the process.
You can read the source code and try it yourself.
For whom the bell tolls… every 15 minutes.