Summary
Highlights
Introduction to the ambient audio mixer web app project. The app uses vanilla JavaScript, focusing on object-oriented programming with classes, encapsulation, single responsibility, and reusability. A demo showcases features like individual sound controls (rain, ocean, thunder), volume adjustments, default presets, custom preset saving, a reset function, play/pause all, master volume, sleep timer, and a theme toggler. The HTML and CSS (using Tailwind CSS) are provided as a template, with the focus on building the JavaScript functionality from scratch.
An overview of the project's architecture, including `app.js` (entry point), `soundManager.js` (audio elements), `ui.js` (DOM manipulations), `presetManager.js` (custom presets in local storage), `timer.js` (self-contained timer), and `soundData.js` (sound and preset data). Key design patterns like the module pattern (ES6 classes), direct instantiation, centralized state management, callback pattern, and event delegation are discussed. The video then guides on setting up the template, creating JavaScript files (`app.js`, `soundManager.js`, `presetManager.js`, `ui.js`, `timer.js`, `soundData.js`), and linking `app.js` as a module in `index.html`.
This section covers populating `soundData.js` with an array of sound objects (ID, name, icon, color, file, description) and default preset objects (focus, relax, sleep) including their associated sounds and volumes. It then moves to `app.js` to create the main `AmbientMixer` class. The constructor initializes properties for `SoundManager`, `UI`, `PresetManager`, `Timer`, `currentSoundState` (to track playing sounds and their volumes), and `isInitialized`. The `init` method is introduced, which will be called when the DOM is ready, setting `isInitialized` to true and preparing the initial application state.
The `SoundManager` class is created with a constructor that initializes an `audioElements` Map (for unique key-value pairs of sound ID to audio element) and an `isPlaying` boolean. The `loadSound` method is added, taking a `soundID` and `filePath`. This method creates a new HTML `Audio` element, sets its `src` and `loop` properties, and sets `preload` to 'metadata' for mobile compatibility. The loaded audio element is then added to the `audioElements` Map. The `app.js` is updated to instantiate `SoundManager` and call `loadSound` to test loading a single sound file.
Instead of manually loading each sound, a `loadAllSounds` method is created in `app.js`. This method iterates through the `sounds` array from `soundData.js`, constructs the audio URL, and calls `soundManager.loadSound` for each. Basic error handling is included if a sound cannot be loaded. The `init` method in `app.js` is updated to call `loadAllSounds`. The `SoundManager` is extended with `playSound`, `pauseSound`, and `setVolume` methods. `playSound` is asynchronous and uses `audio.play()`, `pauseSound` uses `audio.pause()`, and `setVolume` converts a 0-100 scale to a 0-1 decimal for HTML audio. A global `window.app` is temporarily exposed for console testing of these audio functions.
The focus shifts to `ui.js` to render the interactive sound cards. The `UI` class constructor initializes DOM element properties (like `soundCardsContainer`, `masterVolumeSlider`, `playPauseButton`, `resetButton`, etc.) to null. The `init` method then populates these properties by selecting elements from the DOM using `querySelector` and `getElementById`. A `createSoundCard` method is introduced to dynamically generate the HTML structure for a single sound card, including its data attributes for identification and Tailwind CSS classes. A `renderSoundCards` method iterates through the `sounds` array, calls `createSoundCard` for each, and appends the generated card to the `soundCardsContainer`. Finally, `app.js` is updated to import and instantiate the `UI` class and call `ui.init` and `ui.renderSoundCards` to display the cards on page load.
This section implements the play/pause functionality for individual sound cards. All click events are handled using event delegation on the document in `app.js` within a `setupEventListeners` method. This efficiently manages events for dynamically created elements. When a `play-btn` is clicked, its `data-sound` attribute provides the `soundID`. A new `toggleSound` method in `app.js` is called, which checks if the sound is currently paused or playing. If paused, it sets the volume (defaulting to 50 if zero) and calls `soundManager.playSound`. If playing, it calls `soundManager.pauseSound`. The `ui.js` is updated with an `updateSoundPlayButton` method to change the icon and add/remove a 'playing' class based on the sound's state. `setupEventListeners` is called from `app.init` to activate these listeners.
This part focuses on making the individual volume sliders interactive. An event listener for 'input' events is added to the document (using event delegation) to detect changes on elements with the `volume-slider` class. The `soundID` and the slider's `value` are extracted. A new `setSoundVolume` method in `app.js` is created, which calls `soundManager.setVolume` to adjust the actual audio element's volume and `ui.updateVolumeDisplay` to visually update the volume number and bar fill on the sound card. The `ui.updateVolumeDisplay` method updates the text content of the volume value and sets the width of the volume bar fill. The `toggleSound` method is also updated to automatically set the volume to 50% and update the UI if a sound is played from a 0% volume.
The master volume slider is implemented. A single 'input' event listener is added to the master volume slider in `app.js`. The `AmbientMixer` class now has a `masterVolume` property initialized to 100. A `setMasterVolume` method updates this property and calls an `applyMasterVolumeToAll` method. `applyMasterVolumeToAll` iterates through all currently playing audio elements (from `soundManager.audioElements`), calculates an 'effective volume' (individual volume * master volume / 100), and applies it to each audio element. The `setSoundVolume` method is also updated to account for the master volume when adjusting individual sound volumes. This ensures that opening the master volume applies a percentage of the master volume to all playing sounds.
This section adds the 'play all' and 'pause all' features. In `soundManager.js`, `playAll` and `pauseAll` methods are created. `playAll` iterates through all `audioElements` and calls `audio.play()` if a sound is paused, setting the manager's `isPlaying` state to true. `pauseAll` does the opposite. In `ui.js`, an `updateMainPlayButton` method is created to toggle the icon on the master play/pause button (from play to pause and vice-versa) based on the `isPlaying` state. An `updateMainPlayButtonState` method in `app.js` checks if any sounds are actively playing and updates the `soundManager.isPlaying` and `ui.updateMainPlayButton` accordingly. A `toggleAllSounds` method in `app.js` orchestrates playing or pausing all sounds and updating all related UI elements (individual sound card buttons and the main play button). Finally, an event listener is added to the master play/pause button to call `toggleAllSounds`.
The reset functionality is implemented. In `soundManager.js`, a `stopAll` method is added, similar to `pauseAll` but it also resets the `currentTime` of each audio element to 0. In `ui.js`, a `resetUI` method is created to reset all individual volume sliders to zero, set all play/pause buttons back to the play icon, remove the 'playing' class from all sound cards, hide the master play/pause button, and reset the master volume slider and its display to 100%. In `app.js`, a `resetAll` method calls `soundManager.stopAll`, resets the internal `masterVolume` to 100, clears all `currentSoundState` entries, and then calls `ui.resetUI`. An event listener for the reset button is added in `app.js` to trigger the `resetAll` method.
This part focuses on making the default presets (Focus, Relax, Sleep) functional. The `currentSoundState` in `app.js` is initialized by looping through all sounds and setting their initial volume to 0. The `setSoundVolume` method is updated to record the current volume of a sound in `currentSoundState`. A `loadPreset` method (in `app.js`) is created that takes a `presetKey`. It retrieves the preset data from `defaultPresets` in `soundData.js`. The method first stops all current sounds, resets all individual volumes and UI states to zero, then iterates through the sounds defined in the selected preset. For each sound in the preset, it updates `currentSoundState`, `ui.updateVolumeDisplay`, calculates an effective volume incorporating the master volume, sets the audio element's volume, plays the sound, and updates the individual sound card's play button. An event listener (using delegation) in `app.js` is added to trigger `loadPreset` when a default preset button is clicked. The `resetAll` method is also updated to reset the `currentSoundState`.
The functionality for saving custom presets is added. In `presetManager.js`, a `PresetManager` class is created. Its constructor loads `customPresets` from local storage using a `loadCustomPresets` method (which parses a JSON string or returns an empty object). A `saveCustomPresets` method stringifies `customPresets` and saves it to local storage. A `savePreset` method is implemented to take a `name` and `soundStates` (from `app.js`). It generates a unique ID, creates a preset object containing only active (non-zero volume) sounds from the `soundStates`, adds it to `customPresets`, and then calls `saveCustomPresets`. A `presetNameExists` method checks for duplicate preset names. In `ui.js`, `showModal` and `hideModal` methods are added to toggle the visibility and focus of the save preset modal. In `app.js`, `toggleSound` is updated to correctly track playing/paused sounds in `currentSoundState`. Event listeners are set up for the 'Save Mix' button (to show the modal, but only if sounds are active), and for the 'Cancel' button and modal backdrop (to hide the modal). Finally, a `saveCurrentPreset` method is created in `app.js` to handle the logic when the 'Confirm Save' button is clicked, including validation (name presence, uniqueness), calling `presetManager.savePreset` with `currentSoundState`, and then hiding the modal and logging success.
This section enables loading and deleting custom presets. In `presetManager.js`, a `loadPreset` method is added to retrieve a custom preset by its ID from `customPresets`. The `loadPreset` method in `app.js` is refactored to accept a `custom` flag. If `custom` is true, it uses `presetManager.loadPreset`; otherwise, it loads from `defaultPresets`. In `ui.js`, `createCustomPresetButton` is implemented to generate the HTML for a custom preset button, including a delete icon and `data-preset` attributes. An `addCustomPreset` method (in `ui.js`) takes a name and preset ID, creates the button, and appends it to the `customPresetsContainer`. In `app.js`, after saving a preset, `ui.addCustomPreset` is called to immediately display the new button. A `loadCustomPresetsUI` method is added to `app.js` (and called from `app.init`) to load all saved custom presets from local storage and render their buttons on page load. A `setActivePreset` method in `ui.js` is created to highlight the currently active preset and deactivate others. This method is called from `app.loadPreset`. The `resetAll` method is updated to clear any active preset highlighting. For deletion, a `deletePreset` method is added to `presetManager.js` to remove an entry from `customPresets` and resave to local storage. In `ui.js`, `removeCustomPreset` targets and removes the button from the DOM. A `deleteCustomPreset` method in `app.js` calls these, and an event listener (with `stopPropogation`) is set up for delete icons on custom preset buttons.
The sleep timer functionality is implemented. A `Timer` class is created in `timer.js` with properties for `duration`, `remaining`, `intervalId`, `onComplete`, `onTick`, and `isRunning`. The constructor takes `onComplete` and `onTick` callback functions. The `start` method takes minutes, converts them to seconds, sets `duration` and `remaining`, and uses `setInterval` to decrement `remaining` every second. It calls `onTick` (which updates the UI) and `onComplete` when `remaining` reaches zero. `stop` clears the interval and resets timer properties. `complete` stops the timer and invokes `onComplete`. `updateDisplay` calculates minutes and seconds from `remaining` and calls `onTick` with these values, padding single-digit numbers with leading zeros. In `app.js`, the `Timer` class is imported and instantiated, passing `onTimerComplete` as the `onComplete` callback and `ui.updateTimerDisplay` as the `onTick` callback. The `onTimerComplete` method in `app.js` stops all sounds, resets all individual sound card buttons,, resets the timer dropdown value, and hides the timer display. The `ui.updateTimerDisplay` method (in `ui.js`) formats and displays the countdown, making the timer visible. An event listener for the timer select dropdown is added in `app.js` to start or stop the timer based on the selected duration. The `resetAll` method is also updated to stop the timer.
The final feature implemented is the theme toggler. In `ui.js`, the `themeToggle` element is initialized. A `toggleTheme` method is added to `ui.js` that checks if the `body` element has the 'light-theme' class. If it does, it removes 'light-theme' and changes the toggle icon from a moon to a sun. If not, it adds 'light-theme' and changes the icon from a sun to a moon. In `app.js`, an event listener is added to the `ui.themeToggle` element to call `ui.toggleTheme` on click. This completes all application functionality. Finally, the tutorial explains how to deploy the application to Vercel. It covers initializing a Git repository, adding and committing code, creating a new GitHub repository, pushing local changes to GitHub, and then importing and deploying the project from GitHub to Vercel. The entire process, from pushing to GitHub to live deployment on Vercel, is demonstrated.