Alarm Sounds App

by Audrey M. Roy Greenfeld | Sat, Jan 18, 2025

Demo of adding sounds to a FastHTML app with Tone.js. Sounds make web apps come alive and feel interactive.


Here I notebookify and improve alarmsounds.

Setup

from fasthtml.common import *
from fasthtml.jupyter import *
from monsterui.all import *
app, rt = fast_app(hdrs=(Theme.slate.headers(), Script(src="https://unpkg.com/tone")))

The Sounds

beeping_sounds = (
    ("Classic", "Repeating beeps", """const synth = new Tone.Synth().toDestination();
        const loopA = new Tone.Loop((time) => {
            synth.triggerAttackRelease("C5", "16n");
        }, "4n").start(0);
     Tone.getTransport().start();"""),
        
    ("Digital Beep", "Modern digital alarm", """
        const synth = new Tone.Synth({
            oscillator: {type: "square"},
            envelope: {attack: 0.001, decay: 0.1, sustain: 1, release: 0.1}
        }).toDestination();
        
        const loop = new Tone.Loop(time => {
            synth.triggerAttackRelease("C6", "16n");
        }, "0.25").start(0);
        
        Tone.Transport.start();"""),
    
    ("Pulsing Alarm", "Pulsing alarm with rising intensity", """
        const pulseA = new Tone.Synth({
            oscillator: {type: "triangle"},
            envelope: {attack: 0.01, decay: 0.3, sustain: 0.1, release: 0.1}
        }).toDestination();
        pulseA.volume.value = -12;

        const pulseB = new Tone.Synth({
            oscillator: {type: "square"},
            envelope: {attack: 0.01, decay: 0.2, sustain: 0.1, release: 0.1}
        }).toDestination();
        pulseB.volume.value = -20;

        // Start quiet and get louder
        const vol = new Tone.Volume(-20).toDestination();
        pulseA.connect(vol);
        pulseB.connect(vol);
        vol.volume.rampTo(-5, 5);

        const loop = new Tone.Loop(time => {
            pulseA.triggerAttackRelease("E5", "16n", time);
            pulseB.triggerAttackRelease("B5", "16n", time + 0.1);
        }, "0.3").start(0);

        Tone.Transport.start();""")
)
musical_sounds = (
    ("Good Morning", "Bright polyphonic tones", """const synth = new Tone.PolySynth(Tone.Synth).toDestination();
        const now = Tone.now();
        synth.triggerAttack("D4", now);
        synth.triggerAttack("F4", now + 0.5);
        synth.triggerAttack("A4", now + 1);
        synth.triggerAttack("C5", now + 1.5);
        synth.triggerAttack("E5", now + 2);
        synth.triggerRelease(["D4", "F4", "A4", "C5", "E5"], now + 4);"""),
    ("Metallophone", "Striking metal", """
        const bell = new Tone.MetalSynth({
            frequency: 150,
            envelope: {
                attack: 0.001,
                decay: 2.0,
                release: 0.8
            },
            harmonicity: 12,
            modulationIndex: 20,
            resonance: 1000,
            octaves: 1.2
        }).toDestination();
        
        const loop = new Tone.Loop(time => {
            bell.triggerAttackRelease("32n", time);
        }, "0.4").start(0);
        
        Tone.Transport.start();"""),

)
nature_sounds = (
        
    ("Forest Morning", "Birds and rustling leaves", """
        // Bird chirps
        const bird1 = new Tone.FMSynth({
            modulationIndex: 10,
            envelope: {attack: 0.001, decay: 0.1}
        }).toDestination();
        const bird2 = new Tone.FMSynth({
            modulationIndex: 8,
            envelope: {attack: 0.001, decay: 0.08}
        }).toDestination();
        
        // Leaves rustling (filtered noise)
        const noise = new Tone.Noise("pink").toDestination();
        noise.volume.value = -30;
        const filter = new Tone.Filter({
            frequency: 2000,
            type: "highpass"
        }).toDestination();
        noise.connect(filter);
        
        // Random bird calls
        const birdLoop = new Tone.Loop(time => {
            const notes = ["E6", "G6", "A6"];
            bird1.triggerAttackRelease(notes[Math.floor(Math.random() * notes.length)], "16n");
            if(Math.random() > 0.7) {
                bird2.triggerAttackRelease("C7", "32n", "+0.1");
            }
        }, "4n").start();
        
        noise.start();
        Tone.Transport.start();"""),
        
    ("Mountain Stream", "Bubbling water and morning birds", """
        // Water sounds
        const water = new Tone.Noise("white").toDestination();
        water.volume.value = -25;
        const waterFilter = new Tone.Filter({
            frequency: 1000,
            type: "bandpass",
            Q: 2
        }).toDestination();
        water.connect(waterFilter);
        
        // Occasional bird chirps
        const bird = new Tone.Synth({
            oscillator: {type: "triangle"},
            envelope: {attack: 0.001, decay: 0.1, sustain: 0, release: 0.1}
        }).toDestination();
        
        const birdLoop = new Tone.Loop(time => {
            if(Math.random() > 0.8) {
                const note = Math.random() > 0.5 ? "E6" : "G6";
                bird.triggerAttackRelease(note, "16n");
                bird.triggerAttackRelease(note, "16n", "+0.1");
            }
        }, "2n").start();
        
        water.start();
        Tone.Transport.start();""")
)
emergency_sounds = (
    ("Fire Alarm", "Standard emergency siren", """
        const synth = new Tone.Synth({
            oscillator: {type: "sawtooth"},
            envelope: {attack: 0.01, decay: 0.1, sustain: 1, release: 0.1}
        }).toDestination();
        
        let pitch = true;
        const loop = new Tone.Loop(time => {
            synth.triggerAttackRelease(pitch ? "C5" : "G5", "0.25");
            pitch = !pitch;
        }, "0.25").start(0);
        
        Tone.Transport.start();"""),

)
def MusicLi(t,hk=''): return Li(A(DivFullySpaced(t,P(hk,cls=TextFont.muted_sm))))
def Album(title,artist,scr):
    return Div(
        Div(
            UkIcon('alarm-clock', height=150, width=150,
                cls="transition-transform duration-200 hover:scale-105", 
                onmousedown=scr),
            cls="overflow-hidden rounded-md"),
        Div(cls='space-y-1')(P(title,cls=TextT.bold),P(artist,cls=TextT.muted)))

Tabs

Currently the app only has an Alarms tab:

I'm not using these at the moment but may re-add them in later, or I may just remove them:

def Tab(title, desc, sounds):
    return (
        Div(H2(title), cls="mt-6 space-y-1"),
                    P(desc, cls=TextFont.muted_sm),
                    DividerLine(),
                    Grid(*[Album(t,a,s) for t,a,s in sounds], cls='gap-8'))
def BeepingTab():
    return Tab("Beeping", "Listen to your favorite beeping alarm clock sounds.", beeping_sounds)
def MusicalTab():
    return Tab("Musical", "Alarm sounds that add music to your waking experience.", musical_sounds)
def NatureTab():
    return Tab("Nature", "Wake up to nature-inspired sounds.", nature_sounds)
def EmergencyTab():
    return Tab("Emergency", "Startle yourself out of bed with emergency-inspired sounds.", emergency_sounds)

Main Page

@rt('/')
def page():
    return Container(
            H1("Alarm Sounds", cls="mb-6"),
            Div(cls="col-span-4")(
                Div(
                    DivFullySpaced(
                        Div(
                            TabContainer(
                                Li(A('Beeping', href='#'), cls='uk-active'),
                                Li(A('Musical',    href='#'),
                                Li(A('Nature'), href='#'),
                                Li(A('Emergency'), href='#')),
                                uk_switcher='connect: #component-nav; animation: uk-animation-fade',
                                alt=True), 
                            cls="max-w-80"),
                    ),
                    Ul(
                        Li(BeepingTab()),
                        Li(MusicalTab()),
                        Li(NatureTab()),
                        Li(EmergencyTab()),
                        id="component-nav", 
                        cls="uk-switcher")
                )
            ),
            cols=5)

Serve

server = JupyUvi(app)

Show App in This Notebook

# HTMX(page, height=500)

Stop

server.stop()

Next Steps