Spaces:
Running
Running
Included the reverse parrot, more keyboard keys and panic button
Browse files- README.md +31 -67
- config.py +11 -4
- engines.py +62 -1
- keyboard.html +2 -0
- static/keyboard.js +98 -19
README.md
CHANGED
|
@@ -1,98 +1,62 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: πΉ
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.5.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
short_description:
|
| 11 |
---
|
| 12 |
|
| 13 |
-
#
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
## Features
|
| 18 |
|
| 19 |
-
|
| 20 |
-
- π΅ Multiple instrument sounds (Synth, Piano, Organ, Bass, Pluck, FM)
|
| 21 |
-
- β¨οΈ Computer keyboard input support
|
| 22 |
-
- πΉ MIDI event recording with timestamps
|
| 23 |
-
- πΎ Export recordings as .mid files
|
| 24 |
-
- π Real-time MIDI event monitor
|
| 25 |
-
- π¨ Clean, responsive interface
|
| 26 |
-
|
| 27 |
-
## Project Structure
|
| 28 |
|
| 29 |
```
|
| 30 |
-
|
| 31 |
-
βββ app.py
|
| 32 |
-
βββ
|
|
|
|
|
|
|
|
|
|
| 33 |
βββ static/
|
| 34 |
-
β βββ
|
| 35 |
-
β
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
βββ pyproject.toml # Project metadata
|
| 39 |
-
βββ README.md # This file
|
| 40 |
```
|
| 41 |
|
| 42 |
-
##
|
| 43 |
-
|
| 44 |
-
- app.py: Gradio app + MIDI export API
|
| 45 |
-
- keyboard.html: client-side keyboard (Tone.js)
|
| 46 |
-
|
| 47 |
-
## Run locally
|
| 48 |
|
| 49 |
```bash
|
| 50 |
-
|
| 51 |
uv pip install -r requirements.txt
|
|
|
|
|
|
|
| 52 |
uv run python app.py
|
| 53 |
```
|
| 54 |
|
| 55 |
-
Open http://127.0.0.1:
|
| 56 |
-
|
| 57 |
-
## Deploy to Hugging Face Spaces
|
| 58 |
-
|
| 59 |
-
### Quick Setup
|
| 60 |
-
|
| 61 |
-
1. **Create a Space**
|
| 62 |
-
- Go to https://huggingface.co/spaces
|
| 63 |
-
- Click "Create new Space"
|
| 64 |
-
- Choose **Gradio SDK**
|
| 65 |
-
- Name it (e.g., `virtual_keyboard`)
|
| 66 |
|
| 67 |
-
|
| 68 |
-
```bash
|
| 69 |
-
git remote add hf git@hf.co:spaces/YOUR_USERNAME/virtual_keyboard
|
| 70 |
-
git push hf main
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
That's it! Your Space will automatically deploy.
|
| 74 |
-
|
| 75 |
-
### Push to Both GitHub and HF
|
| 76 |
|
| 77 |
```bash
|
| 78 |
-
git
|
|
|
|
| 79 |
```
|
| 80 |
|
| 81 |
-
##
|
| 82 |
|
| 83 |
-
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
POST /gradio_api/call/save_midi
|
| 87 |
-
{
|
| 88 |
-
"data": [events]
|
| 89 |
-
}
|
| 90 |
-
```
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
```
|
| 95 |
-
GET /gradio_api/call/save_midi/{event_id}
|
| 96 |
-
```
|
| 97 |
|
| 98 |
-
The response includes base64 MIDI data at data[0].midi_base64.
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SYNTHIA
|
| 3 |
emoji: πΉ
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: cyan
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.5.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
short_description: Browser-based MIDI keyboard with recording and synthesis
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# SYNTHIA
|
| 14 |
|
| 15 |
+
A minimal, responsive browser-based MIDI keyboard. Play live, record performances, and export as MIDI files. πΉ
|
| 16 |
|
|
|
|
| 17 |
|
| 18 |
+
## ποΈ Project Structure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
```
|
| 21 |
+
.
|
| 22 |
+
βββ app.py # Gradio server & API endpoints
|
| 23 |
+
βββ config.py # Centralized configuration
|
| 24 |
+
βββ engines.py # MIDI processing engines
|
| 25 |
+
βββ midi.py # MIDI file utilities
|
| 26 |
+
βββ keyboard.html # HTML structure
|
| 27 |
βββ static/
|
| 28 |
+
β βββ keyboard.js # Client-side audio (Tone.js)
|
| 29 |
+
β βββ styles.css # Styling & animations
|
| 30 |
+
βββ requirements.txt # Python dependencies
|
| 31 |
+
βββ README.md # This file
|
|
|
|
|
|
|
| 32 |
```
|
| 33 |
|
| 34 |
+
## π Quick Start
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
```bash
|
| 37 |
+
# Install dependencies
|
| 38 |
uv pip install -r requirements.txt
|
| 39 |
+
|
| 40 |
+
# Run the app
|
| 41 |
uv run python app.py
|
| 42 |
```
|
| 43 |
|
| 44 |
+
Open **http://127.0.0.1:7861**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
## π Deploy to Hugging Face Spaces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
```bash
|
| 49 |
+
git remote add hf git@hf.co:spaces/YOUR_USERNAME/synthia
|
| 50 |
+
git push hf main
|
| 51 |
```
|
| 52 |
|
| 53 |
+
## π§ Technology
|
| 54 |
|
| 55 |
+
- **Frontend**: Tone.js v6+ (Web Audio API)
|
| 56 |
+
- **Backend**: Gradio 6.x + Python 3.10+
|
| 57 |
+
- **MIDI**: mido library
|
| 58 |
|
| 59 |
+
## π License
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
Open source - free to use and modify.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
|
|
config.py
CHANGED
|
@@ -43,6 +43,7 @@ KEYBOARD_KEYS = [
|
|
| 43 |
|
| 44 |
# Computer keyboard shortcuts to MIDI notes
|
| 45 |
KEYBOARD_SHORTCUTS = {
|
|
|
|
| 46 |
60: "A", # C4
|
| 47 |
61: "W", # C#4
|
| 48 |
62: "S", # D4
|
|
@@ -55,6 +56,12 @@ KEYBOARD_SHORTCUTS = {
|
|
| 55 |
69: "H", # A4
|
| 56 |
70: "U", # A#4
|
| 57 |
71: "J", # B4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
# =============================================================================
|
|
@@ -80,7 +87,7 @@ INSTRUMENTS = {
|
|
| 80 |
"attack": 0.005,
|
| 81 |
"decay": 0.1,
|
| 82 |
"sustain": 0.3,
|
| 83 |
-
"release":
|
| 84 |
},
|
| 85 |
},
|
| 86 |
"piano": {
|
|
@@ -91,7 +98,7 @@ INSTRUMENTS = {
|
|
| 91 |
"attack": 0.001,
|
| 92 |
"decay": 0.2,
|
| 93 |
"sustain": 0.1,
|
| 94 |
-
"release":
|
| 95 |
},
|
| 96 |
},
|
| 97 |
"organ": {
|
|
@@ -113,7 +120,7 @@ INSTRUMENTS = {
|
|
| 113 |
"attack": 0.01,
|
| 114 |
"decay": 0.1,
|
| 115 |
"sustain": 0.4,
|
| 116 |
-
"release":
|
| 117 |
},
|
| 118 |
},
|
| 119 |
"pluck": {
|
|
@@ -136,7 +143,7 @@ INSTRUMENTS = {
|
|
| 136 |
"attack": 0.01,
|
| 137 |
"decay": 0.2,
|
| 138 |
"sustain": 0.2,
|
| 139 |
-
"release":
|
| 140 |
},
|
| 141 |
},
|
| 142 |
}
|
|
|
|
| 43 |
|
| 44 |
# Computer keyboard shortcuts to MIDI notes
|
| 45 |
KEYBOARD_SHORTCUTS = {
|
| 46 |
+
# First octave (C4-B4)
|
| 47 |
60: "A", # C4
|
| 48 |
61: "W", # C#4
|
| 49 |
62: "S", # D4
|
|
|
|
| 56 |
69: "H", # A4
|
| 57 |
70: "U", # A#4
|
| 58 |
71: "J", # B4
|
| 59 |
+
# Second octave (C5-E5)
|
| 60 |
+
72: "K", # C5
|
| 61 |
+
73: "O", # C#5
|
| 62 |
+
74: "L", # D5
|
| 63 |
+
75: "P", # D#5
|
| 64 |
+
76: ";", # E5
|
| 65 |
}
|
| 66 |
|
| 67 |
# =============================================================================
|
|
|
|
| 87 |
"attack": 0.005,
|
| 88 |
"decay": 0.1,
|
| 89 |
"sustain": 0.3,
|
| 90 |
+
"release": 0.2,
|
| 91 |
},
|
| 92 |
},
|
| 93 |
"piano": {
|
|
|
|
| 98 |
"attack": 0.001,
|
| 99 |
"decay": 0.2,
|
| 100 |
"sustain": 0.1,
|
| 101 |
+
"release": 0.3,
|
| 102 |
},
|
| 103 |
},
|
| 104 |
"organ": {
|
|
|
|
| 120 |
"attack": 0.01,
|
| 121 |
"decay": 0.1,
|
| 122 |
"sustain": 0.4,
|
| 123 |
+
"release": 0.3,
|
| 124 |
},
|
| 125 |
},
|
| 126 |
"pluck": {
|
|
|
|
| 143 |
"attack": 0.01,
|
| 144 |
"decay": 0.2,
|
| 145 |
"sustain": 0.2,
|
| 146 |
+
"release": 0.2,
|
| 147 |
},
|
| 148 |
},
|
| 149 |
}
|
engines.py
CHANGED
|
@@ -64,6 +64,67 @@ class ParrotEngine(MIDIEngine):
|
|
| 64 |
]
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
# =============================================================================
|
| 68 |
# ENGINE REGISTRY
|
| 69 |
# =============================================================================
|
|
@@ -72,7 +133,7 @@ class ParrotEngine(MIDIEngine):
|
|
| 72 |
class EngineRegistry:
|
| 73 |
"""Registry for managing available MIDI engines"""
|
| 74 |
|
| 75 |
-
_engines = {"parrot": ParrotEngine}
|
| 76 |
|
| 77 |
@classmethod
|
| 78 |
def register(cls, engine_id: str, engine_class: type):
|
|
|
|
| 64 |
]
|
| 65 |
|
| 66 |
|
| 67 |
+
# =============================================================================
|
| 68 |
+
# REVERSE PARROT ENGINE
|
| 69 |
+
# =============================================================================
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class ReverseParrotEngine(MIDIEngine):
|
| 73 |
+
"""
|
| 74 |
+
Reverse Parrot Engine - plays back MIDI in reverse order.
|
| 75 |
+
|
| 76 |
+
Takes the recorded performance and reverses the sequence of notes,
|
| 77 |
+
playing them backwards while maintaining their timing relationships.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
def __init__(self):
|
| 81 |
+
super().__init__("Reverse Parrot")
|
| 82 |
+
|
| 83 |
+
def process(self, events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 84 |
+
"""Reverse the sequence of note numbers while keeping timing and event types"""
|
| 85 |
+
if not events:
|
| 86 |
+
return []
|
| 87 |
+
|
| 88 |
+
# Separate note_on and note_off events
|
| 89 |
+
note_on_events = [e for e in events if e.get("type") == "note_on"]
|
| 90 |
+
note_off_events = [e for e in events if e.get("type") == "note_off"]
|
| 91 |
+
|
| 92 |
+
# Extract note numbers from note_on events and reverse them
|
| 93 |
+
on_notes = [e.get("note") for e in note_on_events]
|
| 94 |
+
reversed_on_notes = list(reversed(on_notes))
|
| 95 |
+
|
| 96 |
+
# Extract note numbers from note_off events and reverse them
|
| 97 |
+
off_notes = [e.get("note") for e in note_off_events]
|
| 98 |
+
reversed_off_notes = list(reversed(off_notes))
|
| 99 |
+
|
| 100 |
+
# Reconstruct events with reversed notes but original structure
|
| 101 |
+
result = []
|
| 102 |
+
on_index = 0
|
| 103 |
+
off_index = 0
|
| 104 |
+
|
| 105 |
+
for event in events:
|
| 106 |
+
if event.get("type") == "note_on":
|
| 107 |
+
result.append({
|
| 108 |
+
"type": "note_on",
|
| 109 |
+
"note": reversed_on_notes[on_index],
|
| 110 |
+
"velocity": event.get("velocity"),
|
| 111 |
+
"time": event.get("time"),
|
| 112 |
+
"channel": event.get("channel", 0),
|
| 113 |
+
})
|
| 114 |
+
on_index += 1
|
| 115 |
+
elif event.get("type") == "note_off":
|
| 116 |
+
result.append({
|
| 117 |
+
"type": "note_off",
|
| 118 |
+
"note": reversed_off_notes[off_index],
|
| 119 |
+
"velocity": event.get("velocity"),
|
| 120 |
+
"time": event.get("time"),
|
| 121 |
+
"channel": event.get("channel", 0),
|
| 122 |
+
})
|
| 123 |
+
off_index += 1
|
| 124 |
+
|
| 125 |
+
return result
|
| 126 |
+
|
| 127 |
+
|
| 128 |
# =============================================================================
|
| 129 |
# ENGINE REGISTRY
|
| 130 |
# =============================================================================
|
|
|
|
| 133 |
class EngineRegistry:
|
| 134 |
"""Registry for managing available MIDI engines"""
|
| 135 |
|
| 136 |
+
_engines = {"parrot": ParrotEngine, "reverse_parrot": ReverseParrotEngine}
|
| 137 |
|
| 138 |
@classmethod
|
| 139 |
def register(cls, engine_id: str, engine_class: type):
|
keyboard.html
CHANGED
|
@@ -34,6 +34,7 @@
|
|
| 34 |
Engine:
|
| 35 |
<select id="engineSelect">
|
| 36 |
<option value="parrot">Parrot</option>
|
|
|
|
| 37 |
</select>
|
| 38 |
</label>
|
| 39 |
|
|
@@ -41,6 +42,7 @@
|
|
| 41 |
<button id="stopBtn" disabled>Stop</button>
|
| 42 |
<button id="playbackBtn" disabled>Playback</button>
|
| 43 |
<button id="saveBtn" disabled>Save MIDI</button>
|
|
|
|
| 44 |
|
| 45 |
<label>
|
| 46 |
<input type="checkbox" id="keyboardToggle">
|
|
|
|
| 34 |
Engine:
|
| 35 |
<select id="engineSelect">
|
| 36 |
<option value="parrot">Parrot</option>
|
| 37 |
+
<option value="reverse_parrot">Reverse Parrot</option>
|
| 38 |
</select>
|
| 39 |
</label>
|
| 40 |
|
|
|
|
| 42 |
<button id="stopBtn" disabled>Stop</button>
|
| 43 |
<button id="playbackBtn" disabled>Playback</button>
|
| 44 |
<button id="saveBtn" disabled>Save MIDI</button>
|
| 45 |
+
<button id="panicBtn" style="background: #ff4444; color: white;">Panic π¨</button>
|
| 46 |
|
| 47 |
<label>
|
| 48 |
<input type="checkbox" id="keyboardToggle">
|
static/keyboard.js
CHANGED
|
@@ -33,7 +33,7 @@ const keys = [
|
|
| 33 |
{name:'B', offset:11, black:false}
|
| 34 |
];
|
| 35 |
|
| 36 |
-
// Computer keyboard mapping (
|
| 37 |
const keyMap = {
|
| 38 |
'a': 60, // C4
|
| 39 |
'w': 61, // C#4
|
|
@@ -46,13 +46,19 @@ const keyMap = {
|
|
| 46 |
'y': 68, // G#4
|
| 47 |
'h': 69, // A4
|
| 48 |
'u': 70, // A#4
|
| 49 |
-
'j': 71
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
};
|
| 51 |
|
| 52 |
-
// Keyboard shortcuts displayed on keys
|
| 53 |
const keyShortcuts = {
|
| 54 |
60: 'A', 61: 'W', 62: 'S', 63: 'E', 64: 'D', 65: 'F',
|
| 55 |
-
66: 'T', 67: 'G', 68: 'Y', 69: 'H', 70: 'U', 71: 'J'
|
|
|
|
| 56 |
};
|
| 57 |
|
| 58 |
// =============================================================================
|
|
@@ -65,6 +71,7 @@ const recordBtn = document.getElementById('recordBtn');
|
|
| 65 |
const stopBtn = document.getElementById('stopBtn');
|
| 66 |
const playbackBtn = document.getElementById('playbackBtn');
|
| 67 |
const saveBtn = document.getElementById('saveBtn');
|
|
|
|
| 68 |
const keyboardToggle = document.getElementById('keyboardToggle');
|
| 69 |
const instrumentSelect = document.getElementById('instrumentSelect');
|
| 70 |
const engineSelect = document.getElementById('engineSelect');
|
|
@@ -142,10 +149,6 @@ async function initializeFromConfig() {
|
|
| 142 |
window.keyMapFromServer[key.toLowerCase()] = parseInt(midiStr);
|
| 143 |
}
|
| 144 |
|
| 145 |
-
console.log('β Configuration loaded from server');
|
| 146 |
-
console.log(`β ${Object.keys(instruments).length} instruments ready`);
|
| 147 |
-
console.log(`β ${Object.keys(window.keyboardShortcutsFromServer).length} keyboard shortcuts configured`);
|
| 148 |
-
|
| 149 |
// Render keyboard after config is loaded
|
| 150 |
buildKeyboard();
|
| 151 |
|
|
@@ -154,12 +157,12 @@ async function initializeFromConfig() {
|
|
| 154 |
// Fallback: Use hardcoded values for development/debugging
|
| 155 |
console.warn('Using fallback hardcoded configuration');
|
| 156 |
instruments = buildInstruments({
|
| 157 |
-
'synth': {name: 'Synth', type: 'Synth', oscillator: 'sine', envelope: {attack: 0.005, decay: 0.1, sustain: 0.3, release:
|
| 158 |
-
'piano': {name: 'Piano', type: 'Synth', oscillator: 'triangle', envelope: {attack: 0.001, decay: 0.2, sustain: 0.1, release:
|
| 159 |
'organ': {name: 'Organ', type: 'Synth', oscillator: 'sine4', envelope: {attack: 0.001, decay: 0, sustain: 1, release: 0.1}},
|
| 160 |
-
'bass': {name: 'Bass', type: 'Synth', oscillator: 'sawtooth', envelope: {attack: 0.01, decay: 0.1, sustain: 0.4, release:
|
| 161 |
'pluck': {name: 'Pluck', type: 'Synth', oscillator: 'triangle', envelope: {attack: 0.001, decay: 0.3, sustain: 0, release: 0.3}},
|
| 162 |
-
'fm': {name: 'FM', type: 'FMSynth', harmonicity: 3, modulationIndex: 10, envelope: {attack: 0.01, decay: 0.2, sustain: 0.2, release:
|
| 163 |
});
|
| 164 |
window.keyboardShortcutsFromServer = keyShortcuts; // Use hardcoded as fallback
|
| 165 |
window.keyMapFromServer = keyMap; // Use hardcoded as fallback
|
|
@@ -529,6 +532,14 @@ engineSelect.addEventListener('change', (e) => {
|
|
| 529 |
playbackBtn.addEventListener('click', async () => {
|
| 530 |
if (events.length === 0) return alert('No recording to play back');
|
| 531 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
statusEl.textContent = 'Playing back...';
|
| 533 |
playbackBtn.disabled = true;
|
| 534 |
recordBtn.disabled = true;
|
|
@@ -538,11 +549,47 @@ playbackBtn.addEventListener('click', async () => {
|
|
| 538 |
logToTerminal('', '');
|
| 539 |
|
| 540 |
try {
|
| 541 |
-
//
|
| 542 |
-
|
| 543 |
-
const
|
| 544 |
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
// Play back the recorded events
|
| 548 |
statusEl.textContent = 'Playing back...';
|
|
@@ -550,7 +597,16 @@ playbackBtn.addEventListener('click', async () => {
|
|
| 550 |
|
| 551 |
const playEvent = () => {
|
| 552 |
if (eventIndex >= processedEvents.length) {
|
| 553 |
-
// Playback complete
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
statusEl.textContent = 'Playback complete';
|
| 555 |
playbackBtn.disabled = false;
|
| 556 |
recordBtn.disabled = false;
|
|
@@ -605,11 +661,36 @@ playbackBtn.addEventListener('click', async () => {
|
|
| 605 |
statusEl.textContent = 'Playback error: ' + err.message;
|
| 606 |
playbackBtn.disabled = false;
|
| 607 |
recordBtn.disabled = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
}
|
| 609 |
});
|
| 610 |
|
| 611 |
saveBtn.addEventListener('click', () => saveMIDI());
|
| 612 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
// =============================================================================
|
| 614 |
// =============================================================================
|
| 615 |
// INITIALIZATION
|
|
@@ -631,8 +712,6 @@ async function init() {
|
|
| 631 |
stopBtn.disabled = true;
|
| 632 |
saveBtn.disabled = true;
|
| 633 |
playbackBtn.disabled = true;
|
| 634 |
-
|
| 635 |
-
console.log('β Virtual MIDI Keyboard initialized');
|
| 636 |
}
|
| 637 |
|
| 638 |
// Start the application when DOM is ready
|
|
|
|
| 33 |
{name:'B', offset:11, black:false}
|
| 34 |
];
|
| 35 |
|
| 36 |
+
// Computer keyboard mapping (fallback)
|
| 37 |
const keyMap = {
|
| 38 |
'a': 60, // C4
|
| 39 |
'w': 61, // C#4
|
|
|
|
| 46 |
'y': 68, // G#4
|
| 47 |
'h': 69, // A4
|
| 48 |
'u': 70, // A#4
|
| 49 |
+
'j': 71, // B4
|
| 50 |
+
'k': 72, // C5
|
| 51 |
+
'o': 73, // C#5
|
| 52 |
+
'l': 74, // D5
|
| 53 |
+
'p': 75, // D#5
|
| 54 |
+
';': 76 // E5
|
| 55 |
};
|
| 56 |
|
| 57 |
+
// Keyboard shortcuts displayed on keys (fallback)
|
| 58 |
const keyShortcuts = {
|
| 59 |
60: 'A', 61: 'W', 62: 'S', 63: 'E', 64: 'D', 65: 'F',
|
| 60 |
+
66: 'T', 67: 'G', 68: 'Y', 69: 'H', 70: 'U', 71: 'J',
|
| 61 |
+
72: 'K', 73: 'O', 74: 'L', 75: 'P', 76: ';'
|
| 62 |
};
|
| 63 |
|
| 64 |
// =============================================================================
|
|
|
|
| 71 |
const stopBtn = document.getElementById('stopBtn');
|
| 72 |
const playbackBtn = document.getElementById('playbackBtn');
|
| 73 |
const saveBtn = document.getElementById('saveBtn');
|
| 74 |
+
const panicBtn = document.getElementById('panicBtn');
|
| 75 |
const keyboardToggle = document.getElementById('keyboardToggle');
|
| 76 |
const instrumentSelect = document.getElementById('instrumentSelect');
|
| 77 |
const engineSelect = document.getElementById('engineSelect');
|
|
|
|
| 149 |
window.keyMapFromServer[key.toLowerCase()] = parseInt(midiStr);
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
// Render keyboard after config is loaded
|
| 153 |
buildKeyboard();
|
| 154 |
|
|
|
|
| 157 |
// Fallback: Use hardcoded values for development/debugging
|
| 158 |
console.warn('Using fallback hardcoded configuration');
|
| 159 |
instruments = buildInstruments({
|
| 160 |
+
'synth': {name: 'Synth', type: 'Synth', oscillator: 'sine', envelope: {attack: 0.005, decay: 0.1, sustain: 0.3, release: 0.2}},
|
| 161 |
+
'piano': {name: 'Piano', type: 'Synth', oscillator: 'triangle', envelope: {attack: 0.001, decay: 0.2, sustain: 0.1, release: 0.3}},
|
| 162 |
'organ': {name: 'Organ', type: 'Synth', oscillator: 'sine4', envelope: {attack: 0.001, decay: 0, sustain: 1, release: 0.1}},
|
| 163 |
+
'bass': {name: 'Bass', type: 'Synth', oscillator: 'sawtooth', envelope: {attack: 0.01, decay: 0.1, sustain: 0.4, release: 0.3}},
|
| 164 |
'pluck': {name: 'Pluck', type: 'Synth', oscillator: 'triangle', envelope: {attack: 0.001, decay: 0.3, sustain: 0, release: 0.3}},
|
| 165 |
+
'fm': {name: 'FM', type: 'FMSynth', harmonicity: 3, modulationIndex: 10, envelope: {attack: 0.01, decay: 0.2, sustain: 0.2, release: 0.2}}
|
| 166 |
});
|
| 167 |
window.keyboardShortcutsFromServer = keyShortcuts; // Use hardcoded as fallback
|
| 168 |
window.keyMapFromServer = keyMap; // Use hardcoded as fallback
|
|
|
|
| 532 |
playbackBtn.addEventListener('click', async () => {
|
| 533 |
if (events.length === 0) return alert('No recording to play back');
|
| 534 |
|
| 535 |
+
// Ensure all notes are off before starting playback
|
| 536 |
+
if (synth) {
|
| 537 |
+
synth.releaseAll();
|
| 538 |
+
}
|
| 539 |
+
keyboardEl.querySelectorAll('.key').forEach(k => {
|
| 540 |
+
k.style.filter = '';
|
| 541 |
+
});
|
| 542 |
+
|
| 543 |
statusEl.textContent = 'Playing back...';
|
| 544 |
playbackBtn.disabled = true;
|
| 545 |
recordBtn.disabled = true;
|
|
|
|
| 549 |
logToTerminal('', '');
|
| 550 |
|
| 551 |
try {
|
| 552 |
+
// Process events through the selected engine
|
| 553 |
+
let processedEvents = events;
|
| 554 |
+
const selectedEngine = engineSelect.value;
|
| 555 |
|
| 556 |
+
if (selectedEngine && selectedEngine !== 'parrot') {
|
| 557 |
+
// Step 1: Start the engine processing call
|
| 558 |
+
const startResp = await fetch('/gradio_api/call/process_engine', {
|
| 559 |
+
method: 'POST',
|
| 560 |
+
headers: { 'Content-Type': 'application/json' },
|
| 561 |
+
body: JSON.stringify({
|
| 562 |
+
data: [{
|
| 563 |
+
engine_id: selectedEngine,
|
| 564 |
+
events: events
|
| 565 |
+
}]
|
| 566 |
+
})
|
| 567 |
+
});
|
| 568 |
+
|
| 569 |
+
if (!startResp.ok) {
|
| 570 |
+
console.error('Engine API start failed:', startResp.status);
|
| 571 |
+
} else {
|
| 572 |
+
const startJson = await startResp.json();
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
// Step 2: Poll for the result
|
| 576 |
+
if (startJson && startJson.event_id) {
|
| 577 |
+
const resultResp = await fetch(`/gradio_api/call/process_engine/${startJson.event_id}`);
|
| 578 |
+
if (resultResp.ok) {
|
| 579 |
+
const resultText = await resultResp.text();
|
| 580 |
+
const dataLine = resultText.split('\n').find(line => line.startsWith('data:'));
|
| 581 |
+
if (dataLine) {
|
| 582 |
+
const payloadList = JSON.parse(dataLine.replace('data:', '').trim());
|
| 583 |
+
const result = Array.isArray(payloadList) ? payloadList[0] : null;
|
| 584 |
+
|
| 585 |
+
if (result && result.events) {
|
| 586 |
+
processedEvents = result.events;
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
}
|
| 593 |
|
| 594 |
// Play back the recorded events
|
| 595 |
statusEl.textContent = 'Playing back...';
|
|
|
|
| 597 |
|
| 598 |
const playEvent = () => {
|
| 599 |
if (eventIndex >= processedEvents.length) {
|
| 600 |
+
// Playback complete - ensure all notes are off
|
| 601 |
+
if (synth) {
|
| 602 |
+
synth.releaseAll();
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
// Clear all key highlights
|
| 606 |
+
keyboardEl.querySelectorAll('.key').forEach(k => {
|
| 607 |
+
k.style.filter = '';
|
| 608 |
+
});
|
| 609 |
+
|
| 610 |
statusEl.textContent = 'Playback complete';
|
| 611 |
playbackBtn.disabled = false;
|
| 612 |
recordBtn.disabled = false;
|
|
|
|
| 661 |
statusEl.textContent = 'Playback error: ' + err.message;
|
| 662 |
playbackBtn.disabled = false;
|
| 663 |
recordBtn.disabled = false;
|
| 664 |
+
|
| 665 |
+
// Ensure all notes are off on error
|
| 666 |
+
if (synth) {
|
| 667 |
+
synth.releaseAll();
|
| 668 |
+
}
|
| 669 |
+
keyboardEl.querySelectorAll('.key').forEach(k => {
|
| 670 |
+
k.style.filter = '';
|
| 671 |
+
});
|
| 672 |
}
|
| 673 |
});
|
| 674 |
|
| 675 |
saveBtn.addEventListener('click', () => saveMIDI());
|
| 676 |
|
| 677 |
+
panicBtn.addEventListener('click', () => {
|
| 678 |
+
// Stop all notes immediately
|
| 679 |
+
if (synth) {
|
| 680 |
+
synth.releaseAll();
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// Clear all pressed keys
|
| 684 |
+
pressedKeys.clear();
|
| 685 |
+
|
| 686 |
+
// Reset all visual key highlights
|
| 687 |
+
keyboardEl.querySelectorAll('.key').forEach(k => {
|
| 688 |
+
k.style.filter = '';
|
| 689 |
+
});
|
| 690 |
+
|
| 691 |
+
logToTerminal('π¨ PANIC - All notes stopped', 'timestamp');
|
| 692 |
+
});
|
| 693 |
+
|
| 694 |
// =============================================================================
|
| 695 |
// =============================================================================
|
| 696 |
// INITIALIZATION
|
|
|
|
| 712 |
stopBtn.disabled = true;
|
| 713 |
saveBtn.disabled = true;
|
| 714 |
playbackBtn.disabled = true;
|
|
|
|
|
|
|
| 715 |
}
|
| 716 |
|
| 717 |
// Start the application when DOM is ready
|