diff --git a/examples/music-composer/app.yaml b/examples/music-composer/app.yaml
new file mode 100644
index 0000000..e4d9675
--- /dev/null
+++ b/examples/music-composer/app.yaml
@@ -0,0 +1,6 @@
+name: Music Composer
+icon: 🎵
+description: A music composer app that lets you create melodies by composing notes with different durations and play them using sound generation.
+bricks:
+ - arduino:web_ui
+ - arduino:sound_generator
diff --git a/examples/music-composer/assets/app.js b/examples/music-composer/assets/app.js
new file mode 100644
index 0000000..b066ce0
--- /dev/null
+++ b/examples/music-composer/assets/app.js
@@ -0,0 +1,321 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+(function(){
+ const socket = io({ transports: ['websocket'] });
+
+ // Logger utility
+ const log = {
+ info: (msg, ...args) => console.log(`[MusicComposer] ${msg}`, ...args),
+ debug: (msg, ...args) => console.debug(`[MusicComposer] ${msg}`, ...args),
+ warn: (msg, ...args) => console.warn(`[MusicComposer] ${msg}`, ...args),
+ error: (msg, ...args) => console.error(`[MusicComposer] ${msg}`, ...args)
+ };
+
+ // Configuration
+ const GRID_STEPS = 16;
+ const NOTES = ['B4', 'A#4', 'A4', 'G#4', 'G4', 'F#4', 'F4', 'E4', 'D#4', 'D4', 'C#4', 'C4'];
+
+ // State
+ let grid = null; // {noteIndex: {stepIndex: true/false}} - null until server sends state
+ let isPlaying = false;
+ let currentStep = 0;
+ let bpm = 120;
+ let playInterval = null;
+ let effects = {
+ reverb: 0,
+ chorus: 0,
+ tremolo: 0,
+ vibrato: 0,
+ overdrive: 0
+ };
+
+ // DOM elements
+ const playBtn = document.getElementById('play-btn');
+ const stopBtn = document.getElementById('stop-btn');
+ const bpmInput = document.getElementById('bpm-input');
+ const resetBpmBtn = document.getElementById('reset-bpm');
+ const undoBtn = document.getElementById('undo-btn');
+ const redoBtn = document.getElementById('redo-btn');
+ const clearBtn = document.getElementById('clear-btn');
+ const exportBtn = document.getElementById('export-btn');
+ const sequencerGrid = document.getElementById('sequencer-grid');
+ const volumeSlider = document.getElementById('volume-slider');
+ const waveButtons = document.querySelectorAll('.wave-btn');
+ const knobs = document.querySelectorAll('.knob');
+
+ // Initialize
+ socket.on('connect', () => {
+ log.info('Connected to server');
+ socket.emit('composer:get_state', {});
+ });
+
+ // Socket events
+ socket.on('composer:state', (data) => {
+ log.info('Received state from server:', JSON.stringify(data));
+ if (data.grid) {
+ const oldGrid = JSON.stringify(grid);
+ grid = data.grid;
+ const newGrid = JSON.stringify(grid);
+ if (oldGrid !== newGrid) {
+ log.info('Grid changed from', oldGrid, 'to', newGrid);
+ }
+ } else {
+ // Initialize empty grid if server sends nothing
+ grid = {};
+ log.info('Grid initialized as empty');
+ }
+ if (data.bpm) {
+ bpm = data.bpm;
+ bpmInput.value = bpm;
+ log.info('BPM updated:', bpm);
+ }
+ if (data.effects) {
+ effects = data.effects;
+ log.info('Effects updated:', effects);
+ }
+ renderGrid();
+ updateEffectsKnobs();
+ });
+
+ socket.on('composer:step_playing', (data) => {
+ log.debug('Step playing:', data.step);
+ highlightStep(data.step);
+ });
+
+ // Build grid
+ function buildGrid() {
+ sequencerGrid.innerHTML = '';
+
+ // Top-left corner (empty)
+ const corner = document.createElement('div');
+ sequencerGrid.appendChild(corner);
+
+ // Column labels (step numbers)
+ for (let step = 0; step < GRID_STEPS; step++) {
+ const label = document.createElement('div');
+ label.className = 'grid-col-label';
+ label.textContent = step + 1;
+ sequencerGrid.appendChild(label);
+ }
+
+ // Grid rows
+ NOTES.forEach((note, noteIndex) => {
+ // Row label (note name)
+ const rowLabel = document.createElement('div');
+ rowLabel.className = 'grid-row-label';
+ rowLabel.textContent = note;
+ sequencerGrid.appendChild(rowLabel);
+
+ // Grid cells
+ for (let step = 0; step < GRID_STEPS; step++) {
+ const cell = document.createElement('div');
+ cell.className = 'grid-cell';
+ cell.dataset.note = noteIndex;
+ cell.dataset.step = step;
+
+ // Add beat separator every 4 steps
+ if ((step + 1) % 4 === 0 && step < GRID_STEPS - 1) {
+ cell.classList.add('beat-separator');
+ }
+
+ cell.addEventListener('click', () => toggleCell(noteIndex, step));
+ sequencerGrid.appendChild(cell);
+ }
+ });
+
+ }
+
+ function toggleCell(noteIndex, step) {
+ if (grid === null) grid = {}; // Initialize if still null
+ const noteKey = String(noteIndex);
+ const stepKey = String(step);
+ if (!grid[noteKey]) grid[noteKey] = {};
+
+ // Explicit toggle: if undefined or false, set to true; if true, set to false
+ const currentValue = grid[noteKey][stepKey] === true;
+ const newValue = !currentValue;
+ grid[noteKey][stepKey] = newValue;
+
+ log.info(`Toggle cell [${NOTES[noteIndex]}][step ${step}]: ${currentValue} -> ${newValue}`);
+ log.info('Grid before emit:', JSON.stringify(grid));
+ renderGrid();
+ socket.emit('composer:update_grid', { grid });
+ }
+
+ function renderGrid() {
+ if (grid === null) {
+ log.info('Grid is null, skipping render');
+ return; // Don't render until we have state from server
+ }
+ log.info('Rendering grid:', JSON.stringify(grid));
+ const cells = document.querySelectorAll('.grid-cell');
+ let activeCount = 0;
+ let activeCells = [];
+ cells.forEach(cell => {
+ const noteKey = String(cell.dataset.note);
+ const stepKey = String(cell.dataset.step);
+ const isActive = grid[noteKey] && grid[noteKey][stepKey] === true;
+
+ // Force remove class first, then add if needed
+ cell.classList.remove('active');
+ if (isActive) {
+ cell.classList.add('active');
+ activeCount++;
+ activeCells.push(`[${NOTES[noteKey]}][step ${stepKey}]`);
+ }
+ });
+ log.info(`Rendered ${activeCount} active cells: ${activeCells.join(', ')}`);
+ }
+
+ function highlightStep(step) {
+ const cells = document.querySelectorAll('.grid-cell');
+ cells.forEach(cell => {
+ const cellStep = parseInt(cell.dataset.step);
+ cell.classList.toggle('playing', cellStep === step);
+ });
+ }
+
+ // Play button
+ playBtn.addEventListener('click', () => {
+ if (!isPlaying) {
+ isPlaying = true;
+ playBtn.style.display = 'none';
+ stopBtn.style.display = 'flex';
+ log.info('Starting playback at', bpm, 'BPM');
+ socket.emit('composer:play', { grid, bpm });
+ }
+ });
+
+ // Stop button
+ stopBtn.addEventListener('click', () => {
+ if (isPlaying) {
+ isPlaying = false;
+ stopBtn.style.display = 'none';
+ playBtn.style.display = 'flex';
+ log.info('Stopping playback');
+ socket.emit('composer:stop', {});
+ highlightStep(-1);
+ }
+ });
+
+ // BPM controls
+ bpmInput.addEventListener('change', () => {
+ bpm = parseInt(bpmInput.value);
+ log.info('BPM changed to:', bpm);
+ socket.emit('composer:set_bpm', { bpm });
+ });
+
+ resetBpmBtn.addEventListener('click', () => {
+ bpm = 120;
+ bpmInput.value = bpm;
+ log.info('BPM reset to 120');
+ socket.emit('composer:set_bpm', { bpm });
+ });
+
+ // Clear button
+ clearBtn.addEventListener('click', () => {
+ if (confirm('Clear all notes?')) {
+ grid = {};
+ NOTES.forEach((note, noteIndex) => {
+ const noteKey = String(noteIndex);
+ grid[noteKey] = {};
+ });
+ renderGrid();
+ socket.emit('composer:update_grid', { grid });
+ }
+ });
+
+ // Export button
+ exportBtn.addEventListener('click', () => {
+ socket.emit('composer:export', { grid });
+ });
+
+ socket.on('composer:export_data', (data) => {
+ const blob = new Blob([data.content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = data.filename || 'composition.h';
+ a.click();
+ URL.revokeObjectURL(url);
+ });
+
+ // Wave buttons
+ waveButtons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ waveButtons.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ const wave = btn.dataset.wave;
+ socket.emit('composer:set_waveform', { waveform: wave });
+ });
+ });
+
+ // Volume slider
+ volumeSlider.addEventListener('input', () => {
+ const volume = parseInt(volumeSlider.value);
+ socket.emit('composer:set_volume', { volume });
+ });
+
+ // Knobs
+ knobs.forEach(knob => {
+ let isDragging = false;
+ let startY = 0;
+ let startValue = 0;
+
+ knob.addEventListener('mousedown', (e) => {
+ isDragging = true;
+ startY = e.clientY;
+ startValue = parseFloat(knob.dataset.value) || 0;
+ e.preventDefault();
+ });
+
+ document.addEventListener('mousemove', (e) => {
+ if (!isDragging) return;
+
+ const delta = (startY - e.clientY) * 0.5;
+ let newValue = startValue + delta;
+ newValue = Math.max(0, Math.min(100, newValue));
+
+ knob.dataset.value = newValue;
+ const rotation = (newValue / 100) * 270 - 135;
+ knob.querySelector('.knob-indicator').style.transform =
+ `translateX(-50%) rotate(${rotation}deg)`;
+
+ const effectName = knob.id.replace('-knob', '');
+ effects[effectName] = newValue;
+ });
+
+ document.addEventListener('mouseup', () => {
+ if (isDragging) {
+ isDragging = false;
+ socket.emit('composer:set_effects', { effects });
+ }
+ });
+ });
+
+ function updateEffectsKnobs() {
+ Object.keys(effects).forEach(key => {
+ const knob = document.getElementById(`${key}-knob`);
+ if (knob) {
+ const value = effects[key] || 0;
+ knob.dataset.value = value;
+ const rotation = (value / 100) * 270 - 135;
+ knob.querySelector('.knob-indicator').style.transform =
+ `translateX(-50%) rotate(${rotation}deg)`;
+ }
+ });
+ }
+
+ // Initialize grid
+ buildGrid();
+
+ // Ensure play button is visible and stop button is hidden on load
+ playBtn.style.display = 'flex';
+ stopBtn.style.display = 'none';
+ log.info('Grid UI built, waiting for server state...');
+
+})();
diff --git a/examples/music-composer/assets/fonts/Open Sans/OFL.txt b/examples/music-composer/assets/fonts/Open Sans/OFL.txt
new file mode 100644
index 0000000..d2a4922
--- /dev/null
+++ b/examples/music-composer/assets/fonts/Open Sans/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/examples/music-composer/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf b/examples/music-composer/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..548c15f
Binary files /dev/null and b/examples/music-composer/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf differ
diff --git a/examples/music-composer/assets/fonts/Roboto/OFL.txt b/examples/music-composer/assets/fonts/Roboto/OFL.txt
new file mode 100644
index 0000000..5d6f71c
--- /dev/null
+++ b/examples/music-composer/assets/fonts/Roboto/OFL.txt
@@ -0,0 +1,91 @@
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/examples/music-composer/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf b/examples/music-composer/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf
new file mode 100644
index 0000000..3a2d704
Binary files /dev/null and b/examples/music-composer/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf differ
diff --git a/examples/music-composer/assets/fonts/fonts.css b/examples/music-composer/assets/fonts/fonts.css
new file mode 100644
index 0000000..86cf716
--- /dev/null
+++ b/examples/music-composer/assets/fonts/fonts.css
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+@font-face {
+ font-family: 'Roboto Mono';
+ font-style: normal;
+ font-display: swap;
+ src: url('Roboto/RobotoMono-VariableFont_wght.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-display: swap;
+ src: url('Open Sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype');
+}
\ No newline at end of file
diff --git a/examples/music-composer/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg b/examples/music-composer/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg
new file mode 100644
index 0000000..c942003
--- /dev/null
+++ b/examples/music-composer/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/examples/music-composer/assets/index.html b/examples/music-composer/assets/index.html
new file mode 100644
index 0000000..157802f
--- /dev/null
+++ b/examples/music-composer/assets/index.html
@@ -0,0 +1,118 @@
+
+
+
+
+