FJFehr commited on
Commit
43a0d3f
·
1 Parent(s): 11b77dd

First prototype of the engine with parrot

Browse files
Files changed (5) hide show
  1. app.py +122 -11
  2. engines.py +180 -0
  3. keyboard.html +11 -0
  4. static/engine.js +242 -0
  5. static/keyboard.js +111 -4
app.py CHANGED
@@ -4,25 +4,30 @@ Virtual MIDI Keyboard - Gradio Application
4
  This application provides a browser-based MIDI keyboard that can:
5
  - Play notes with various synthesizer sounds (Tone.js)
6
  - Record MIDI events with timestamps
 
7
  - Export recordings as .mid files
8
  - Support computer keyboard input
9
  - Monitor MIDI events in real-time
10
 
11
  File Structure:
12
  - app.py: Gradio server and MIDI conversion (this file)
 
13
  - keyboard.html: Main UI structure
14
  - static/styles.css: All application styles
15
  - static/keyboard.js: Client-side logic and interactivity
 
16
  """
17
 
18
  import base64
19
  import html
20
  import io
 
21
 
22
  import mido
23
  from mido import MidiFile, MidiTrack, Message, MetaMessage
24
 
25
  import gradio as gr
 
26
 
27
 
28
  # =============================================================================
@@ -109,6 +114,94 @@ def save_midi_api(events):
109
  return {"midi_base64": midi_b64}
110
 
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  # =============================================================================
113
  # GRADIO APPLICATION
114
  # =============================================================================
@@ -138,19 +231,37 @@ iframe_html = (
138
  )
139
 
140
  # Create Gradio interface
141
- with gr.Blocks() as demo:
142
  gr.HTML(iframe_html)
143
 
144
- # Hidden API endpoint components (required for Gradio 6.x)
145
- with gr.Row(visible=False):
146
- api_input = gr.JSON()
147
- api_output = gr.JSON()
148
- api_btn = gr.Button("save")
149
-
150
- # Connect API endpoint
151
- api_btn.click(
152
- fn=save_midi_api, inputs=api_input, outputs=api_output, api_name="save_midi"
153
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
 
156
  # =============================================================================
 
4
  This application provides a browser-based MIDI keyboard that can:
5
  - Play notes with various synthesizer sounds (Tone.js)
6
  - Record MIDI events with timestamps
7
+ - Process recordings through various engines
8
  - Export recordings as .mid files
9
  - Support computer keyboard input
10
  - Monitor MIDI events in real-time
11
 
12
  File Structure:
13
  - app.py: Gradio server and MIDI conversion (this file)
14
+ - engines.py: MIDI processing engines
15
  - keyboard.html: Main UI structure
16
  - static/styles.css: All application styles
17
  - static/keyboard.js: Client-side logic and interactivity
18
+ - static/engine.js: Client-side engine abstraction
19
  """
20
 
21
  import base64
22
  import html
23
  import io
24
+ import json
25
 
26
  import mido
27
  from mido import MidiFile, MidiTrack, Message, MetaMessage
28
 
29
  import gradio as gr
30
+ from engines import EngineRegistry, ParrotEngine
31
 
32
 
33
  # =============================================================================
 
114
  return {"midi_base64": midi_b64}
115
 
116
 
117
+ # =============================================================================
118
+ # ENGINE API ENDPOINTS
119
+ # =============================================================================
120
+
121
+
122
+ def list_engines():
123
+ """
124
+ API endpoint to list available MIDI engines
125
+
126
+ Returns:
127
+ List of engine info dictionaries
128
+ """
129
+ engines = EngineRegistry.list_engines()
130
+ return {"engines": [EngineRegistry.get_engine_info(e) for e in engines]}
131
+
132
+
133
+ def process_with_engine(engine_id: str, events: list):
134
+ """
135
+ API endpoint to process MIDI events through an engine
136
+
137
+ Args:
138
+ engine_id: The engine to use (e.g. 'parrot')
139
+ events: List of MIDI event dictionaries
140
+
141
+ Returns:
142
+ Processed events or error message
143
+ """
144
+ if not engine_id or not events:
145
+ return {"error": "Missing engine_id or events"}
146
+
147
+ try:
148
+ engine = EngineRegistry.get_engine(engine_id)
149
+ processed = engine.process(events)
150
+ return {"success": True, "events": processed}
151
+ except ValueError as e:
152
+ return {"error": str(e)}
153
+ except Exception as e:
154
+ return {"error": f"Processing error: {str(e)}"}
155
+
156
+
157
+ def process_engine_api(payload: dict):
158
+ """
159
+ Wrapper API endpoint that accepts JSON payload
160
+
161
+ Args:
162
+ payload: Dict with 'engine_id' and 'events' keys
163
+
164
+ Returns:
165
+ Processed events or error message
166
+ """
167
+ try:
168
+ print(f"[DEBUG] process_engine_api called with payload type: {type(payload)}")
169
+ print(
170
+ f"[DEBUG] payload keys: {payload.keys() if isinstance(payload, dict) else 'N/A'}"
171
+ )
172
+
173
+ # Handle both direct dict and wrapped dict formats
174
+ data = payload
175
+ if isinstance(payload, dict) and "data" in payload:
176
+ # If wrapped in a data field, unwrap it
177
+ data = payload["data"]
178
+ if isinstance(data, list) and len(data) > 0:
179
+ data = data[0]
180
+
181
+ print(
182
+ f"[DEBUG] extracted data type: {type(data)}, has engine_id: {'engine_id' in data if isinstance(data, dict) else False}"
183
+ )
184
+
185
+ engine_id = data.get("engine_id") if isinstance(data, dict) else None
186
+ events = data.get("events", []) if isinstance(data, dict) else []
187
+
188
+ print(
189
+ f"[DEBUG] engine_id: {engine_id}, events count: {len(events) if isinstance(events, list) else 'N/A'}"
190
+ )
191
+
192
+ result = process_with_engine(engine_id, events)
193
+ print(
194
+ f"[DEBUG] process_engine_api returning: {result.keys() if isinstance(result, dict) else type(result)}"
195
+ )
196
+ return result
197
+ except Exception as e:
198
+ print(f"[DEBUG] Exception in process_engine_api: {str(e)}")
199
+ import traceback
200
+
201
+ traceback.print_exc()
202
+ return {"error": f"API error: {str(e)}"}
203
+
204
+
205
  # =============================================================================
206
  # GRADIO APPLICATION
207
  # =============================================================================
 
231
  )
232
 
233
  # Create Gradio interface
234
+ with gr.Blocks(title="Virtual MIDI Keyboard") as demo:
235
  gr.HTML(iframe_html)
236
 
237
+ # Hidden API endpoints using Gradio's function API
238
+ with gr.Group(visible=False) as api_group:
239
+ # Process engine endpoint
240
+ engine_input = gr.Textbox(label="engine_payload")
241
+ engine_output = gr.Textbox(label="engine_result")
242
+
243
+ def call_engine_api(payload_json):
244
+ """Wrapper to call engine API with JSON input"""
245
+ import json
246
+
247
+ try:
248
+ payload = (
249
+ json.loads(payload_json)
250
+ if isinstance(payload_json, str)
251
+ else payload_json
252
+ )
253
+ result = process_engine_api(payload)
254
+ return json.dumps(result)
255
+ except Exception as e:
256
+ return json.dumps({"error": str(e)})
257
+
258
+ engine_btn = gr.Button("process_engine", visible=False)
259
+ engine_btn.click(
260
+ fn=call_engine_api,
261
+ inputs=engine_input,
262
+ outputs=engine_output,
263
+ api_name="process_engine",
264
+ )
265
 
266
 
267
  # =============================================================================
engines.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Virtual MIDI Keyboard - Engines
3
+
4
+ This module contains MIDI processing engines that can transform,
5
+ analyze, or manipulate MIDI events in various ways.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import List, Dict, Any, Callable
10
+ import time
11
+
12
+
13
+ # =============================================================================
14
+ # BASE ENGINE CLASS
15
+ # =============================================================================
16
+
17
+
18
+ class MIDIEngine(ABC):
19
+ """Abstract base class for MIDI engines"""
20
+
21
+ def __init__(self, name: str):
22
+ self.name = name
23
+ self.recordings = {}
24
+ self.current_recording_id = None
25
+
26
+ @abstractmethod
27
+ def process(self, events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
28
+ """
29
+ Process MIDI events and return transformed events.
30
+
31
+ Args:
32
+ events: List of MIDI event dictionaries
33
+
34
+ Returns:
35
+ List of processed MIDI event dictionaries
36
+ """
37
+ pass
38
+
39
+ def start_recording(self, recording_name: str = None) -> str:
40
+ """Start recording a new performance"""
41
+ if recording_name is None:
42
+ recording_name = f"recording_{int(time.time() * 1000)}"
43
+
44
+ self.current_recording_id = recording_name
45
+ self.recordings[recording_name] = {
46
+ "name": recording_name,
47
+ "events": [],
48
+ "created_at": time.time(),
49
+ }
50
+ return recording_name
51
+
52
+ def stop_recording(self) -> Dict[str, Any]:
53
+ """Stop recording and return the recording"""
54
+ if not self.current_recording_id:
55
+ return None
56
+
57
+ recording_id = self.current_recording_id
58
+ self.current_recording_id = None
59
+ return self.recordings[recording_id]
60
+
61
+ def add_event(self, event: Dict[str, Any]) -> bool:
62
+ """Add event to current recording"""
63
+ if not self.current_recording_id:
64
+ return False
65
+
66
+ recording = self.recordings[self.current_recording_id]
67
+ recording["events"].append(event)
68
+ return True
69
+
70
+ def get_recording(self, recording_id: str) -> Dict[str, Any]:
71
+ """Get a recording by ID"""
72
+ return self.recordings.get(recording_id)
73
+
74
+ def get_all_recordings(self) -> List[Dict[str, Any]]:
75
+ """Get all recordings"""
76
+ return list(self.recordings.values())
77
+
78
+ def delete_recording(self, recording_id: str) -> bool:
79
+ """Delete a recording"""
80
+ if recording_id in self.recordings:
81
+ if self.current_recording_id == recording_id:
82
+ self.stop_recording()
83
+ del self.recordings[recording_id]
84
+ return True
85
+ return False
86
+
87
+
88
+ # =============================================================================
89
+ # PARROT ENGINE - Returns exactly what was played
90
+ # =============================================================================
91
+
92
+
93
+ class ParrotEngine(MIDIEngine):
94
+ """
95
+ Parrot Engine - Captures and plays back MIDI exactly as recorded.
96
+
97
+ This is the simplest engine - it just repeats what the user played.
98
+ Perfect for learning and basic playback.
99
+ """
100
+
101
+ def __init__(self):
102
+ super().__init__("Parrot")
103
+ self.description = "Captures and plays back MIDI exactly as recorded"
104
+
105
+ def process(self, events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
106
+ """
107
+ Process MIDI events - in Parrot mode, just return them unchanged.
108
+
109
+ Args:
110
+ events: List of MIDI event dictionaries
111
+
112
+ Returns:
113
+ The same events unmodified
114
+ """
115
+ if not events:
116
+ return []
117
+
118
+ # Simply return the events as-is
119
+ processed_events = []
120
+ for event in events:
121
+ processed_events.append(
122
+ {
123
+ "type": event.get("type"),
124
+ "note": event.get("note"),
125
+ "velocity": event.get("velocity"),
126
+ "time": event.get("time"),
127
+ "channel": event.get("channel", 0),
128
+ }
129
+ )
130
+
131
+ return processed_events
132
+
133
+
134
+ # =============================================================================
135
+ # ENGINE REGISTRY
136
+ # =============================================================================
137
+
138
+
139
+ class EngineRegistry:
140
+ """Registry for managing available MIDI engines"""
141
+
142
+ _engines = {"parrot": ParrotEngine}
143
+
144
+ @classmethod
145
+ def register(cls, engine_id: str, engine_class: type):
146
+ """Register a new engine"""
147
+ cls._engines[engine_id] = engine_class
148
+
149
+ @classmethod
150
+ def get_engine(cls, engine_id: str) -> MIDIEngine:
151
+ """Get an engine instance by ID"""
152
+ if engine_id not in cls._engines:
153
+ raise ValueError(f"Unknown engine: {engine_id}")
154
+ return cls._engines[engine_id]()
155
+
156
+ @classmethod
157
+ def list_engines(cls) -> List[str]:
158
+ """List all available engines"""
159
+ return list(cls._engines.keys())
160
+
161
+ @classmethod
162
+ def get_engine_info(cls, engine_id: str) -> Dict[str, str]:
163
+ """Get info about an engine"""
164
+ if engine_id not in cls._engines:
165
+ raise ValueError(f"Unknown engine: {engine_id}")
166
+
167
+ engine = cls._engines[engine_id]()
168
+ return {
169
+ "id": engine_id,
170
+ "name": engine.name,
171
+ "description": getattr(engine, "description", "No description"),
172
+ }
173
+
174
+
175
+ # =============================================================================
176
+ # GLOBAL ENGINE INSTANCES
177
+ # =============================================================================
178
+
179
+ # Create default engine instance
180
+ default_engine = ParrotEngine()
keyboard.html CHANGED
@@ -31,8 +31,16 @@
31
  </select>
32
  </label>
33
 
 
 
 
 
 
 
 
34
  <button id="recordBtn">Record</button>
35
  <button id="stopBtn" disabled>Stop</button>
 
36
  <button id="saveBtn" disabled>Save MIDI</button>
37
 
38
  <label>
@@ -57,6 +65,9 @@
57
  <!-- External Dependencies -->
58
  <script src="https://unpkg.com/tone@next/build/Tone.js"></script>
59
 
 
 
 
60
  <!-- Application Logic -->
61
  <script src="/file=static/keyboard.js"></script>
62
  </body>
 
31
  </select>
32
  </label>
33
 
34
+ <label>
35
+ Engine:
36
+ <select id="engineSelect">
37
+ <option value="parrot">Parrot</option>
38
+ </select>
39
+ </label>
40
+
41
  <button id="recordBtn">Record</button>
42
  <button id="stopBtn" disabled>Stop</button>
43
+ <button id="playbackBtn" disabled>Playback</button>
44
  <button id="saveBtn" disabled>Save MIDI</button>
45
 
46
  <label>
 
65
  <!-- External Dependencies -->
66
  <script src="https://unpkg.com/tone@next/build/Tone.js"></script>
67
 
68
+ <!-- Engine -->
69
+ <script src="/file=static/engine.js"></script>
70
+
71
  <!-- Application Logic -->
72
  <script src="/file=static/keyboard.js"></script>
73
  </body>
static/engine.js ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Virtual MIDI Keyboard - Engine
3
+ *
4
+ * The engine processes MIDI signals and can:
5
+ * - Capture and store MIDI events
6
+ * - Play back captured sequences
7
+ * - Process and transform MIDI data
8
+ *
9
+ * This is the core processing unit for all signal manipulation.
10
+ */
11
+
12
+ // =============================================================================
13
+ // ENGINE STATE
14
+ // =============================================================================
15
+
16
+ class MIDIEngine {
17
+ constructor() {
18
+ this.recordings = {}; // Store multiple recordings
19
+ this.currentRecordingId = null;
20
+ this.isPlayingBack = false;
21
+ this.playbackSpeed = 1.0; // 1.0 = normal speed
22
+ this.callbacks = {
23
+ onNoteOn: null,
24
+ onNoteOff: null,
25
+ onPlaybackStart: null,
26
+ onPlaybackEnd: null,
27
+ onProgress: null
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Start recording a new performance
33
+ * Returns the recording ID
34
+ */
35
+ startRecording(recordingName = 'Recording_' + Date.now()) {
36
+ this.currentRecordingId = recordingName;
37
+ this.recordings[recordingName] = {
38
+ name: recordingName,
39
+ events: [],
40
+ startTime: performance.now(),
41
+ duration: 0,
42
+ metadata: {
43
+ createdAt: new Date().toISOString(),
44
+ instrument: 'default'
45
+ }
46
+ };
47
+ return recordingName;
48
+ }
49
+
50
+ /**
51
+ * Stop recording and return the recording
52
+ */
53
+ stopRecording() {
54
+ if (!this.currentRecordingId) return null;
55
+
56
+ const recording = this.recordings[this.currentRecordingId];
57
+ if (recording && recording.events.length > 0) {
58
+ const lastEvent = recording.events[recording.events.length - 1];
59
+ recording.duration = lastEvent.time;
60
+ }
61
+
62
+ const recordingId = this.currentRecordingId;
63
+ this.currentRecordingId = null;
64
+ return recording;
65
+ }
66
+
67
+ /**
68
+ * Add a MIDI event to the current recording
69
+ */
70
+ addEvent(event) {
71
+ if (!this.currentRecordingId) return false;
72
+
73
+ const recording = this.recordings[this.currentRecordingId];
74
+ if (!recording) return false;
75
+
76
+ recording.events.push({
77
+ ...event,
78
+ id: recording.events.length
79
+ });
80
+
81
+ return true;
82
+ }
83
+
84
+ /**
85
+ * Get a recording by ID
86
+ */
87
+ getRecording(recordingId) {
88
+ return this.recordings[recordingId];
89
+ }
90
+
91
+ /**
92
+ * Get all recordings
93
+ */
94
+ getAllRecordings() {
95
+ return Object.values(this.recordings);
96
+ }
97
+
98
+ /**
99
+ * Delete a recording
100
+ */
101
+ deleteRecording(recordingId) {
102
+ if (this.currentRecordingId === recordingId) {
103
+ this.stopRecording();
104
+ }
105
+ delete this.recordings[recordingId];
106
+ }
107
+
108
+ /**
109
+ * Play back a recording
110
+ * Calls the provided callbacks for each note
111
+ */
112
+ async playback(recordingId, callbacks = {}) {
113
+ const recording = this.getRecording(recordingId);
114
+ if (!recording || recording.events.length === 0) {
115
+ console.warn('No recording found or recording is empty');
116
+ return;
117
+ }
118
+
119
+ // Merge provided callbacks with instance callbacks
120
+ const finalCallbacks = { ...this.callbacks, ...callbacks };
121
+
122
+ this.isPlayingBack = true;
123
+ if (finalCallbacks.onPlaybackStart) {
124
+ finalCallbacks.onPlaybackStart(recording);
125
+ }
126
+
127
+ const events = recording.events;
128
+ let playbackStartTime = performance.now();
129
+
130
+ for (let i = 0; i < events.length; i++) {
131
+ if (!this.isPlayingBack) break; // Stop if playback was cancelled
132
+
133
+ const event = events[i];
134
+ const scheduledTime = (event.time / this.playbackSpeed) * 1000; // ms
135
+ const now = performance.now() - playbackStartTime;
136
+ const waitTime = scheduledTime - now;
137
+
138
+ if (waitTime > 0) {
139
+ await this.sleep(waitTime);
140
+ }
141
+
142
+ // Execute the event callback
143
+ if (event.type === 'note_on' && finalCallbacks.onNoteOn) {
144
+ finalCallbacks.onNoteOn(event);
145
+ } else if (event.type === 'note_off' && finalCallbacks.onNoteOff) {
146
+ finalCallbacks.onNoteOff(event);
147
+ }
148
+
149
+ // Progress callback
150
+ if (finalCallbacks.onProgress) {
151
+ finalCallbacks.onProgress({
152
+ currentIndex: i,
153
+ totalEvents: events.length,
154
+ currentTime: event.time,
155
+ totalDuration: recording.duration,
156
+ progress: (i / events.length) * 100
157
+ });
158
+ }
159
+ }
160
+
161
+ this.isPlayingBack = false;
162
+ if (finalCallbacks.onPlaybackEnd) {
163
+ finalCallbacks.onPlaybackEnd(recording);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Stop playback
169
+ */
170
+ stopPlayback() {
171
+ this.isPlayingBack = false;
172
+ }
173
+
174
+ /**
175
+ * Set playback speed (1.0 = normal, 2.0 = double speed, etc.)
176
+ */
177
+ setPlaybackSpeed(speed) {
178
+ this.playbackSpeed = Math.max(0.1, Math.min(2.0, speed));
179
+ }
180
+
181
+ /**
182
+ * Export recording as JSON
183
+ */
184
+ exportAsJSON(recordingId) {
185
+ const recording = this.getRecording(recordingId);
186
+ if (!recording) return null;
187
+ return JSON.stringify(recording, null, 2);
188
+ }
189
+
190
+ /**
191
+ * Import recording from JSON
192
+ */
193
+ importFromJSON(jsonString, recordingName = null) {
194
+ try {
195
+ const recording = JSON.parse(jsonString);
196
+ const id = recordingName || recording.name || 'imported_' + Date.now();
197
+ this.recordings[id] = recording;
198
+ return id;
199
+ } catch (error) {
200
+ console.error('Failed to import recording:', error);
201
+ return null;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Get recording statistics
207
+ */
208
+ getStats(recordingId) {
209
+ const recording = this.getRecording(recordingId);
210
+ if (!recording) return null;
211
+
212
+ const events = recording.events;
213
+ const noteOnEvents = events.filter(e => e.type === 'note_on');
214
+ const noteOffEvents = events.filter(e => e.type === 'note_off');
215
+
216
+ return {
217
+ recordingName: recording.name,
218
+ totalEvents: events.length,
219
+ noteOnCount: noteOnEvents.length,
220
+ noteOffCount: noteOffEvents.length,
221
+ duration: recording.duration,
222
+ avgNotesPerSecond: (noteOnEvents.length / recording.duration).toFixed(2),
223
+ minNote: Math.min(...noteOnEvents.map(e => e.note)),
224
+ maxNote: Math.max(...noteOnEvents.map(e => e.note)),
225
+ instrument: recording.metadata?.instrument || 'unknown'
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Helper: Sleep for ms milliseconds
231
+ */
232
+ sleep(ms) {
233
+ return new Promise(resolve => setTimeout(resolve, ms));
234
+ }
235
+ }
236
+
237
+ // =============================================================================
238
+ // EXPORT
239
+ // =============================================================================
240
+
241
+ // Create a global engine instance
242
+ const midiEngine = new MIDIEngine();
static/keyboard.js CHANGED
@@ -63,9 +63,11 @@ const keyboardEl = document.getElementById('keyboard');
63
  const statusEl = document.getElementById('status');
64
  const recordBtn = document.getElementById('recordBtn');
65
  const stopBtn = document.getElementById('stopBtn');
 
66
  const saveBtn = document.getElementById('saveBtn');
67
  const keyboardToggle = document.getElementById('keyboardToggle');
68
  const instrumentSelect = document.getElementById('instrumentSelect');
 
69
  const terminal = document.getElementById('terminal');
70
  const clearTerminal = document.getElementById('clearTerminal');
71
 
@@ -78,6 +80,7 @@ let recording = false;
78
  let startTime = 0;
79
  let events = [];
80
  const pressedKeys = new Set();
 
81
 
82
  // =============================================================================
83
  // INSTRUMENT CONFIGURATIONS
@@ -206,7 +209,12 @@ function beginRecord() {
206
  statusEl.textContent = 'Recording...';
207
  recordBtn.disabled = true;
208
  stopBtn.disabled = false;
 
209
  saveBtn.disabled = true;
 
 
 
 
210
  logToTerminal('', '');
211
  logToTerminal('▶▶▶ RECORDING STARTED ◀◀◀', 'timestamp');
212
  logToTerminal('', '');
@@ -218,6 +226,11 @@ function stopRecord() {
218
  recordBtn.disabled = false;
219
  stopBtn.disabled = true;
220
  saveBtn.disabled = events.length === 0;
 
 
 
 
 
221
  logToTerminal('', '');
222
  logToTerminal(`■■■ RECORDING STOPPED (${events.length} events captured) ■■■`, 'timestamp');
223
  logToTerminal('', '');
@@ -239,13 +252,16 @@ function noteOn(midiNote, velocity = 100) {
239
  );
240
 
241
  if (recording) {
242
- events.push({
243
  type: 'note_on',
244
  note: midiNote,
245
  velocity: Math.max(1, velocity | 0),
246
  time: nowSec() - startTime,
247
  channel: 0
248
- });
 
 
 
249
  }
250
  }
251
 
@@ -261,13 +277,16 @@ function noteOff(midiNote) {
261
  );
262
 
263
  if (recording) {
264
- events.push({
265
  type: 'note_off',
266
  note: midiNote,
267
  velocity: 0,
268
  time: nowSec() - startTime,
269
  channel: 0
270
- });
 
 
 
271
  }
272
  }
273
 
@@ -460,6 +479,93 @@ recordBtn.addEventListener('click', async () => {
460
 
461
  stopBtn.addEventListener('click', () => stopRecord());
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  saveBtn.addEventListener('click', () => saveMIDI());
464
 
465
  // =============================================================================
@@ -476,6 +582,7 @@ function init() {
476
  recordBtn.disabled = false;
477
  stopBtn.disabled = true;
478
  saveBtn.disabled = true;
 
479
  }
480
 
481
  // Start the application
 
63
  const statusEl = document.getElementById('status');
64
  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');
71
  const terminal = document.getElementById('terminal');
72
  const clearTerminal = document.getElementById('clearTerminal');
73
 
 
80
  let startTime = 0;
81
  let events = [];
82
  const pressedKeys = new Set();
83
+ let selectedEngine = 'parrot'; // Default engine
84
 
85
  // =============================================================================
86
  // INSTRUMENT CONFIGURATIONS
 
209
  statusEl.textContent = 'Recording...';
210
  recordBtn.disabled = true;
211
  stopBtn.disabled = false;
212
+ playbackBtn.disabled = true;
213
  saveBtn.disabled = true;
214
+
215
+ // Start engine recording
216
+ const recordingId = midiEngine.startRecording('keyboard_' + Date.now());
217
+
218
  logToTerminal('', '');
219
  logToTerminal('▶▶▶ RECORDING STARTED ◀◀◀', 'timestamp');
220
  logToTerminal('', '');
 
226
  recordBtn.disabled = false;
227
  stopBtn.disabled = true;
228
  saveBtn.disabled = events.length === 0;
229
+ playbackBtn.disabled = events.length === 0;
230
+
231
+ // Stop engine recording
232
+ midiEngine.stopRecording();
233
+
234
  logToTerminal('', '');
235
  logToTerminal(`■■■ RECORDING STOPPED (${events.length} events captured) ■■■`, 'timestamp');
236
  logToTerminal('', '');
 
252
  );
253
 
254
  if (recording) {
255
+ const event = {
256
  type: 'note_on',
257
  note: midiNote,
258
  velocity: Math.max(1, velocity | 0),
259
  time: nowSec() - startTime,
260
  channel: 0
261
+ };
262
+ events.push(event);
263
+ // Also add to engine
264
+ midiEngine.addEvent(event);
265
  }
266
  }
267
 
 
277
  );
278
 
279
  if (recording) {
280
+ const event = {
281
  type: 'note_off',
282
  note: midiNote,
283
  velocity: 0,
284
  time: nowSec() - startTime,
285
  channel: 0
286
+ };
287
+ events.push(event);
288
+ // Also add to engine
289
+ midiEngine.addEvent(event);
290
  }
291
  }
292
 
 
479
 
480
  stopBtn.addEventListener('click', () => stopRecord());
481
 
482
+ engineSelect.addEventListener('change', (e) => {
483
+ selectedEngine = e.target.value;
484
+ logToTerminal(`Engine switched to: ${selectedEngine}`, 'timestamp');
485
+ });
486
+
487
+ playbackBtn.addEventListener('click', async () => {
488
+ if (events.length === 0) return alert('No recording to play back');
489
+
490
+ statusEl.textContent = 'Playing back...';
491
+ playbackBtn.disabled = true;
492
+ recordBtn.disabled = true;
493
+
494
+ logToTerminal('', '');
495
+ logToTerminal('♫♫♫ PLAYBACK STARTED ♫♫♫', 'timestamp');
496
+ logToTerminal('', '');
497
+
498
+ try {
499
+ // For now, skip engine processing and directly play recorded events
500
+ // TODO: Call engine API when Gradio routing is figured out
501
+ const processedEvents = events;
502
+
503
+ console.log(`Playing back ${processedEvents.length} events`);
504
+
505
+ // Play back the recorded events
506
+ statusEl.textContent = 'Playing back...';
507
+ let eventIndex = 0;
508
+
509
+ const playEvent = () => {
510
+ if (eventIndex >= processedEvents.length) {
511
+ // Playback complete
512
+ statusEl.textContent = 'Playback complete';
513
+ playbackBtn.disabled = false;
514
+ recordBtn.disabled = false;
515
+
516
+ logToTerminal('', '');
517
+ logToTerminal('♫♫♫ PLAYBACK FINISHED ♫♫♫', 'timestamp');
518
+ logToTerminal('', '');
519
+ return;
520
+ }
521
+
522
+ const event = processedEvents[eventIndex];
523
+ const nextTime = eventIndex + 1 < processedEvents.length
524
+ ? processedEvents[eventIndex + 1].time
525
+ : event.time;
526
+
527
+ if (event.type === 'note_on') {
528
+ const freq = Tone.Frequency(event.note, "midi").toFrequency();
529
+ synth.triggerAttack(freq, undefined, event.velocity / 127);
530
+
531
+ const noteName = midiToNoteName(event.note);
532
+ logToTerminal(
533
+ `[${event.time.toFixed(3)}s] ► ${noteName} (${event.note})`,
534
+ 'note-on'
535
+ );
536
+
537
+ // Highlight the key being played
538
+ const keyEl = getKeyElement(event.note);
539
+ if (keyEl) keyEl.style.filter = 'brightness(0.7)';
540
+ } else if (event.type === 'note_off') {
541
+ const freq = Tone.Frequency(event.note, "midi").toFrequency();
542
+ synth.triggerRelease(freq);
543
+
544
+ const noteName = midiToNoteName(event.note);
545
+ logToTerminal(
546
+ `[${event.time.toFixed(3)}s] ◄ ${noteName}`,
547
+ 'note-off'
548
+ );
549
+
550
+ // Remove key highlight
551
+ const keyEl = getKeyElement(event.note);
552
+ if (keyEl) keyEl.style.filter = '';
553
+ }
554
+
555
+ eventIndex++;
556
+ const deltaTime = Math.max(0, nextTime - event.time);
557
+ setTimeout(playEvent, deltaTime * 1000);
558
+ };
559
+
560
+ playEvent();
561
+ } catch (err) {
562
+ console.error('Playback error:', err);
563
+ statusEl.textContent = 'Playback error: ' + err.message;
564
+ playbackBtn.disabled = false;
565
+ recordBtn.disabled = false;
566
+ }
567
+ });
568
+
569
  saveBtn.addEventListener('click', () => saveMIDI());
570
 
571
  // =============================================================================
 
582
  recordBtn.disabled = false;
583
  stopBtn.disabled = true;
584
  saveBtn.disabled = true;
585
+ playbackBtn.disabled = true;
586
  }
587
 
588
  // Start the application