PlayPiano: pianoforte virtuale a 88 tasti con app desktop

App web a file singolo con pianoforte campionato, spartiti MIDI e MusicXML,
pentagramma, cascata di note, modalità apprendimento, compositore di idee,
generatore di accordi e idee con IA locale tramite Ollama.
Include il contenitore Electron e il flusso GitHub Actions per i pacchetti
macOS, Windows e Linux.
This commit is contained in:
luzadev committed 2026-09-18 12:51:55 +02:00
commit 7a4ee5de28
44 files changed
+5401

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
# Costruisce i pacchetti desktop su macchine native di GitHub.
# Si avvia a mano (Actions > Pacchetti desktop > Run workflow) oppure pubblicando un tag "v*",
# nel qual caso i pacchetti vengono anche allegati a una Release.
name: Pacchetti desktop
on:
workflow_dispatch:
push:
tags: ["v*"]
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- { os: windows-latest, name: windows, args: "--win nsis portable --x64" }
- { os: ubuntu-latest, name: linux, args: "--linux AppImage --x64" }
- { os: macos-latest, name: macos, args: "--mac" }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx electron-builder ${{ matrix.args }} --publish never
- uses: actions/upload-artifact@v4
with:
name: PlayPiano-${{ matrix.name }}
if-no-files-found: error
path: |
dist/*.exe
dist/*.dmg
dist/*.AppImage
- name: Allega alla Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.exe
dist/*.dmg
dist/*.AppImage
+3
View File
@@ -0,0 +1,3 @@
node_modules/
dist/
.DS_Store
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Avvia PlayPiano su http://localhost:8765 (necessario perché Ollama accetti le richieste della pagina)
cd "$(dirname "$0")" || exit 1
PORT=8765
if lsof -nP -iTCP:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
echo "PlayPiano è già in esecuzione: apro il browser."
open "http://localhost:$PORT/"
exit 0
fi
echo "PlayPiano in esecuzione su http://localhost:$PORT/"
echo "Lascia aperta questa finestra mentre usi l'app. Chiudila (o premi Ctrl+C) per terminare."
( sleep 1; open "http://localhost:$PORT/" ) &
exec python3 -m http.server "$PORT" --bind 127.0.0.1
+34
View File
@@ -0,0 +1,34 @@
# PlayPiano
Pianoforte virtuale a 88 tasti con spartiti (MIDI e MusicXML), pentagramma, modalità apprendimento,
compositore di idee e generazione con IA locale tramite Ollama.
## Usarlo nel browser
Doppio clic su `Avvia PlayPiano.command` (macOS). Apre l'app su http://localhost:8765.
## App desktop (macOS, Windows, Linux)
Serve Node.js 20 o superiore.
npm install # solo la prima volta
npm start # avvia l'app desktop in prova
### Creare i pacchetti
npm run dist:mac # dist/PlayPiano-<versione>-arm64.dmg (Apple Silicon) e PlayPiano-<versione>.dmg (Intel)
npx electron-builder --win zip --x64 # versione portatile per Windows
npx electron-builder --linux tar.gz --x64 # versione portatile per Linux
L'AppImage per Linux richiede strumenti per processori Intel. Su un Mac con chip Apple si costruisce con Docker:
docker run --rm --platform linux/amd64 -v "$PWD":/project -w /project \
electronuserland/builder:wine /bin/bash -lc "npx electron-builder --linux AppImage --x64"
L'installer Windows (.exe) non si può produrre da un Mac con chip Apple. Si costruisce su un PC Windows con
`npm install` e `npm run dist:win`, oppure su GitHub con il flusso `.github/workflows/build.yml`.
I pacchetti finiscono nella cartella `dist/`.
## Note
- I pacchetti non sono firmati con un certificato a pagamento. Su macOS, al primo avvio: clic destro sull'app e poi "Apri".
Su Windows: "Ulteriori informazioni" e poi "Esegui comunque".
- Le idee con IA richiedono Ollama in esecuzione sullo stesso computer (http://localhost:11434).
- Campioni di pianoforte: Salamander Grand Piano di Alexander Holm, licenza CC-BY 3.0 (vedi `samples/LEGGIMI.txt`).
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+54
View File
@@ -0,0 +1,54 @@
// PlayPiano desktop: finestra Electron che carica l'app da un piccolo server locale.
// Il server serve a due cose: i campioni del pianoforte si leggono via http, e Ollama
// accetta le richieste solo da pagine con origine http://localhost (non da file://).
const { app, BrowserWindow, session, shell, Menu } = require('electron');
const http = require('http'), fs = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..');
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.mp3': 'audio/mpeg', '.mid': 'audio/midi', '.musicxml': 'application/xml', '.mxl': 'application/zip', '.png': 'image/png', '.txt': 'text/plain; charset=utf-8', '.json': 'application/json' };
const ALLOWED = ['index.html', 'samples', 'esempi'];
function startServer() {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
let rel; try { rel = decodeURIComponent(new URL(req.url, 'http://localhost').pathname).replace(/^\/+/, '') || 'index.html'; } catch (_) { res.writeHead(400); return res.end(); }
const file = path.normalize(path.join(ROOT, rel));
if (!file.startsWith(ROOT + path.sep) || !ALLOWED.includes(rel.split('/')[0])) { res.writeHead(404); return res.end('Not found'); }
fs.readFile(file, (err, data) => {
if (err) { res.writeHead(404); return res.end('Not found'); }
res.writeHead(200, { 'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream', 'Cache-Control': 'no-cache' });
res.end(data);
});
});
server.on('error', reject);
server.listen(0, '127.0.0.1', () => resolve(server.address().port));
});
}
let win = null;
async function createWindow() {
const port = await startServer();
win = new BrowserWindow({
width: 1440, height: 960, minWidth: 720, minHeight: 560, backgroundColor: '#14110f', title: 'PlayPiano', autoHideMenuBar: true,
icon: path.join(__dirname, 'icon.png'),
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false }
});
win.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:/.test(url)) shell.openExternal(url); return { action: 'deny' }; });
win.on('closed', () => { win = null; });
await win.loadURL('http://localhost:' + port + '/');
}
if (!app.requestSingleInstanceLock()) app.quit();
else {
app.on('second-instance', () => { if (win) { if (win.isMinimized()) win.restore(); win.focus(); } });
app.whenReady().then(() => {
// tastiere MIDI: concedi il permesso senza chiedere
const ok = p => p === 'midi' || p === 'midiSysex' || p === 'keyboardLock';
session.defaultSession.setPermissionRequestHandler((wc, permission, cb) => cb(ok(permission)));
session.defaultSession.setPermissionCheckHandler((wc, permission) => ok(permission));
if (process.platform !== 'darwin') Menu.setApplicationMenu(null);
createWindow();
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
});
app.on('window-all-closed', () => app.quit());
}
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0"?><score-partwise version="3.1"><part-list><score-part id="P1"><part-name>Piano</part-name></score-part></part-list><part id="P1"><measure number="1"><attributes><divisions>4</divisions><key><fifths>2</fifths></key><time><beats>4</beats><beat-type>4</beat-type></time><staves>2</staves></attributes><direction><sound tempo="90"/></direction><note><pitch><step>D</step><octave>4</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>F</step><alter>1</alter><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>A</step><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>D</step><octave>5</octave></pitch><duration>6</duration><staff>1</staff></note><note><pitch><step>C</step><alter>1</alter><octave>5</octave></pitch><duration>2</duration><staff>1</staff></note><backup><duration>16</duration></backup><note><pitch><step>D</step><octave>3</octave></pitch><duration>8</duration><staff>2</staff></note><note><chord/><pitch><step>A</step><octave>3</octave></pitch><duration>8</duration><staff>2</staff></note><note><pitch><step>A</step><octave>2</octave></pitch><duration>8</duration><staff>2</staff></note></measure><measure number="2"><note><pitch><step>B</step><octave>4</octave></pitch><duration>1</duration><staff>1</staff></note><note><pitch><step>C</step><alter>1</alter><octave>5</octave></pitch><duration>1</duration><staff>1</staff></note><note><pitch><step>D</step><octave>5</octave></pitch><duration>1</duration><staff>1</staff></note><note><pitch><step>E</step><octave>5</octave></pitch><duration>1</duration><staff>1</staff></note><note><pitch><step>F</step><alter>0</alter><octave>5</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>F</step><alter>1</alter><octave>5</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>A</step><octave>5</octave></pitch><duration>4</duration><staff>1</staff></note><backup><duration>16</duration></backup><note><pitch><step>G</step><octave>2</octave></pitch><duration>16</duration><staff>2</staff></note><note><chord/><pitch><step>D</step><octave>3</octave></pitch><duration>16</duration><staff>2</staff></note><note><chord/><pitch><step>E</step><octave>3</octave></pitch><duration>16</duration><staff>2</staff></note></measure><measure number="3"><note><pitch><step>D</step><octave>5</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>C</step><octave>6</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>B</step><alter>-1</alter><octave>3</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>A</step><octave>4</octave></pitch><duration>4</duration><staff>1</staff></note><backup><duration>16</duration></backup><note><pitch><step>D</step><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>F</step><alter>1</alter><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>A</step><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>D</step><octave>4</octave></pitch><duration>4</duration><staff>2</staff></note></measure><measure number="4"><note><pitch><step>D</step><octave>5</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>C</step><octave>6</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>B</step><alter>-1</alter><octave>3</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>A</step><octave>4</octave></pitch><duration>4</duration><staff>1</staff></note><backup><duration>16</duration></backup><note><pitch><step>D</step><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>F</step><alter>1</alter><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>A</step><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>D</step><octave>4</octave></pitch><duration>4</duration><staff>2</staff></note></measure><measure number="5"><note><pitch><step>D</step><octave>5</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>C</step><octave>6</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>B</step><alter>-1</alter><octave>3</octave></pitch><duration>4</duration><staff>1</staff></note><note><pitch><step>A</step><octave>4</octave></pitch><duration>4</duration><staff>1</staff></note><backup><duration>16</duration></backup><note><pitch><step>D</step><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>F</step><alter>1</alter><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>A</step><octave>3</octave></pitch><duration>4</duration><staff>2</staff></note><note><pitch><step>D</step><octave>4</octave></pitch><duration>4</duration><staff>2</staff></noLine truncated
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0"?><score-partwise version="3.1"><part-list><score-part id="P1"><part-name>Piano</part-name></score-part></part-list><part id="P1"><measure number="1"><attributes><divisions>2</divisions><staves>2</staves></attributes><direction><sound tempo="100"/></direction><note><pitch><step>C</step><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>D</step><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>E</step><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>C</step><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><backup><duration>8</duration></backup><note><pitch><step>C</step><octave>3</octave></pitch><duration>8</duration><tie type="start"/><staff>2</staff></note><note><chord/><pitch><step>G</step><octave>3</octave></pitch><duration>8</duration><staff>2</staff></note></measure><measure number="2"><note><pitch><step>E</step><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>F</step><alter>1</alter><octave>4</octave></pitch><duration>2</duration><staff>1</staff></note><note><pitch><step>G</step><octave>4</octave></pitch><duration>4</duration><staff>1</staff></note><backup><duration>8</duration></backup><note><pitch><step>C</step><octave>3</octave></pitch><duration>8</duration><tie type="stop"/><staff>2</staff></note></measure></part></score-partwise>
BIN
View File
Binary file not shown.
+1559
View File
File diff suppressed because it is too large. Load diff
+3598
View File
File diff suppressed because it is too large. Load diff
+91
View File
@@ -0,0 +1,91 @@
{
"name": "playpiano",
"productName": "PlayPiano",
"version": "1.0.0",
"description": "Pianoforte virtuale a 88 tasti con spartiti, modalità apprendimento e compositore",
"author": "PlayPiano",
"license": "UNLICENSED",
"private": true,
"main": "desktop/main.js",
"scripts": {
"start": "electron .",
"dist:mac": "electron-builder --mac",
"dist:win": "electron-builder --win",
"dist:linux": "electron-builder --linux",
"dist": "electron-builder --mac --win --linux"
},
"build": {
"appId": "it.playpiano.app",
"productName": "PlayPiano",
"directories": {
"output": "dist"
},
"files": [
"index.html",
"samples/**",
"esempi/**",
"desktop/main.js",
"desktop/icon.png",
"package.json"
],
"icon": "desktop/icon.png",
"mac": {
"category": "public.app-category.music",
"target": [
{
"target": "dmg",
"arch": [
"arm64",
"x64"
]
}
],
"identity": "-",
"hardenedRuntime": false,
"gatekeeperAssess": false
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
"x64"
]
}
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"artifactName": "PlayPiano-Setup-${version}.${ext}"
},
"portable": {
"artifactName": "PlayPiano-Portable-${version}.${ext}"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
}
],
"category": "AudioVideo",
"artifactName": "PlayPiano-${version}-${arch}.${ext}"
},
"dmg": {
"sign": false
}
},
"devDependencies": {
"electron": "^44.4.2",
"electron-builder": "^26.15.3"
}
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
Campioni di pianoforte: Salamander Grand Piano (Yamaha C5) di Alexander Holm.
Licenza Creative Commons Attribution 3.0 (CC-BY 3.0).
File in formato mp3 scaricati da https://tonejs.github.io/audio/salamander/