matrix_music / app.py
baouws's picture
Update app.py
9054c84 verified
import gradio as gr
import random
import re
import json
# --- Strudel Code Generation and Playback Functions ---
def generate_simple_beat():
"""Generates a very simple Strudel beat as a starting point."""
code = '''// ๐ŸŽต Simple Voice-Generated Beat
// Try saying: "Make a beat" or "Play music"
// Then click PLAY or press Ctrl+Enter!
stack(
sound("bd").struct("x ~ x ~").gain(0.8),
sound("sd").struct("~ x ~ x").gain(0.7),
sound("hh").struct("x x x x").gain(0.4)
).cpm(120)'''
return "โœ… Simple beat generated!", code
def clear_code():
"""Clears the Strudel code editor to a default state."""
initial_code = '''// ๐ŸŽต Voice Strudel Synth
// Speak a command to generate music!
// Try: "Make a beat"
// Then: "Play music" (or Ctrl+Enter)
stack(
// Your generated code will appear here...
).cpm(120) // Master tempo
'''
return "๐Ÿ—‘๏ธ Code cleared - Ready for new ideas!", initial_code
# --- Voice Command Processing ---
def process_voice_command(command_text, current_code):
"""
Processes a voice command to generate or control Strudel code.
This version is highly simplified for bare-minimum functionality.
"""
if not command_text:
return "โŒ No voice command received.", current_code
command_lower = command_text.lower()
if any(word in command_lower for word in ["make a beat", "generate beat", "create music", "new beat"]):
return generate_simple_beat()
elif "play music" in command_lower or "start music" in command_lower:
# This message will instruct the user to use the play button/shortcut
return "โ–ถ๏ธ Click PLAY MUSIC or press Ctrl+Enter to hear!", current_code
elif "stop music" in command_lower:
# This message will instruct the user to use the stop button/shortcut
return "โน๏ธ Click STOP MUSIC or press Ctrl+Space!", current_code
elif "clear code" in command_lower or "reset" in command_lower or "start over" in command_lower:
return clear_code()
else:
return "๐Ÿค” Command not recognized. Try: 'Make a beat', 'Play music', 'Stop music', 'Clear code'.", current_code
# --- Gradio Interface Setup ---
def create_interface():
# --- Simplified CSS for a clean, futuristic look ---
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
body {
font-family: 'Inter', sans-serif;
overflow: hidden; /* Hide scrollbars due to matrix rain */
background: #000;
}
.gradio-container {
background: linear-gradient(135deg, #0a0a0a, #1a1a1a);
color: #00ff00;
font-family: 'Share Tech Mono', monospace !important;
min-height: 100vh;
border-radius: 15px;
box-shadow: 0 0 50px rgba(0, 255, 0, 0.3);
padding: 20px;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
font-size: 3.5em;
color: #00ff00;
text-shadow: 0 0 15px rgba(0, 255, 0, 0.7);
animation: neon-flicker 1.5s infinite alternate;
text-align: center;
margin-bottom: 5px;
}
p {
color: #00ee00;
font-size: 1.5em;
text-shadow: 0 0 10px #00ff00;
text-align: center;
margin-top: 0;
margin-bottom: 30px;
}
@keyframes neon-flicker {
0% { opacity: 1; text-shadow: 0 0 15px rgba(0, 255, 0, 0.7); }
100% { opacity: 0.9; text-shadow: 0 0 20px rgba(0, 255, 0, 0.9), 0 0 30px rgba(0, 255, 0, 0.5); }
}
.gr-textbox, .gr-code {
background: rgba(0, 10, 0, 0.7) !important;
border: 1px solid #00ff00 !important;
color: #00ff41 !important;
border-radius: 10px !important;
box-shadow: inset 0 0 8px rgba(0, 255, 0, 0.3);
padding: 15px;
margin-bottom: 20px;
width: 100%;
}
.gr-code textarea {
background: rgba(0, 0, 0, 0.9) !important;
color: #00ff41 !important;
font-family: 'Fira Code', 'Share Tech Mono', monospace !important;
font-size: 15px !important;
line-height: 1.4;
}
.gr-button {
background: linear-gradient(45deg, #008800, #00bb00) !important;
border: 2px solid #33ff33 !important;
color: #00ff00 !important;
font-weight: bold !important;
text-shadow: 0 0 10px #00ff00 !important;
border-radius: 8px !important;
transition: all 0.2s ease-in-out !important;
box-shadow: 0 0 10px rgba(51, 255, 51, 0.7);
padding: 12px 25px;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 1.1em;
margin: 5px;
}
.gr-button:hover {
background: linear-gradient(45deg, #00bb00, #00ee00) !important;
box-shadow: 0 0 30px rgba(51, 255, 51, 1);
transform: translateY(-2px) scale(1.02);
}
#voice-status {
background: rgba(0, 30, 0, 0.5) !important;
border: 2px solid #00ff00 !important;
border-radius: 12px !important;
padding: 15px !important;
text-align: center;
font-size: 1.2em;
font-weight: bold;
box-shadow: 0 0 15px rgba(0, 255, 0, 0.5);
animation: pulse 2s infinite alternate;
}
.listening {
animation: listening-pulse 1s infinite alternate !important;
background: rgba(100, 0, 0, 0.3) !important;
border-color: #ff0000 !important;
box-shadow: 0 0 25px rgba(255, 0, 0, 0.7) !important;
}
@keyframes pulse {
0% { opacity: 1; box-shadow: 0 0 15px rgba(0, 255, 0, 0.5); }
100% { opacity: 0.8; box-shadow: 0 0 25px rgba(0, 255, 0, 0.8); }
}
@keyframes listening-pulse {
0% { box-shadow: 0 0 15px #ff0000; }
100% { box-shadow: 0 0 40px #ff0000; }
}
.instructions {
background: rgba(0, 15, 0, 0.6) !important;
border: 1px dashed #00ff00 !important;
border-radius: 15px !important;
padding: 25px !important;
margin: 30px auto; /* Centered */
max-width: 800px;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.2);
}
.instructions h3 {
color: #33ff33;
text-shadow: none;
text-align: center;
margin-bottom: 15px;
}
.instructions ul {
list-style: none;
padding-left: 0;
margin-bottom: 15px;
}
.instructions ul li::before {
content: 'ยป ';
color: #00ff00;
font-weight: bold;
margin-right: 5px;
}
.instructions p {
font-size: 1em;
color: #ccffcc;
text-shadow: none;
}
#matrix-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: radial-gradient(ellipse at center, rgba(0,20,0,0.8) 0%, rgba(0,0,0,0.9) 100%);
z-index: -2;
pointer-events: none;
}
.gr-row, .gr-column {
width: 100%; /* Make rows/columns take full width */
max-width: 900px; /* Constrain main content width */
margin: 0 auto; /* Center content */
}
.main-controls {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.control-buttons {
display: flex;
justify-content: center;
width: 100%;
}
"""
# --- JavaScript for Voice Recognition and Strudel.js Integration ---
voice_strudel_js = """
function() {
console.log('๐ŸŽต Initializing Voice Recognition and Strudel.js...');
let recognition = null;
let isListening = false;
let strudelPlayer = null;
let strudelReady = false;
// Initialize Strudel.js Player
async function initStrudel() {
if (strudelReady) {
console.log("๐ŸŽถ Strudel.js already initialized.");
return true;
}
try {
// Using a direct import from unpkg for the 'strudel' package itself
const { default: Strudel } = await import('https://unpkg.com/strudel/strudel.js');
// Create or resume AudioContext
if (!window.audioContext || window.audioContext.state === 'closed') {
window.audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log("New AudioContext created.");
}
// Attempt to resume audio context, crucial for autoplay policies
if (window.audioContext.state === 'suspended') {
await window.audioContext.resume();
console.log("AudioContext resumed during initialization.");
}
strudelPlayer = Strudel.Player({ audioContext: window.audioContext }); // Pass the existing context
strudelReady = true;
console.log("๐ŸŽถ Strudel.js player created and initialized successfully!");
return true;
} catch (error) {
console.error("Failed to load or initialize Strudel.js:", error);
const statusElement = document.querySelector('#audio-status textarea');
if (statusElement) {
statusElement.value = `โŒ Audio Engine Error: ${error.message}. Check browser console for details.`;
}
return false;
}
}
// Check if browser supports speech recognition
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.interimResults = false;
recognition.lang = 'en-US';
recognition.maxAlternatives = 1;
console.log('โœ… Speech Recognition Available');
} else {
console.log('โŒ Speech Recognition Not Available. Please use a Chromium-based browser (Chrome, Edge).');
}
// Matrix rain effect (simplified and integrated)
function createMatrixRain() {
const existingCanvas = document.querySelector('#matrix-canvas');
if (existingCanvas) existingCanvas.remove();
const canvas = document.createElement('canvas');
canvas.id = 'matrix-canvas';
canvas.style.cssText = `
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(ellipse at center, rgba(0,20,0,0.8) 0%, rgba(0,0,0,0.9) 100%);
z-index: -2; pointer-events: none;
`;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789โ™ชโ™ซโ™ฌโ™ฉ๐ŸŽต๐ŸŽถโšก๐Ÿ”Š๐ŸŽค๐ŸŽง';
const fontSize = 16;
let columns, drops;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.floor(canvas.width / fontSize);
drops = Array(columns).fill(1);
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
function draw() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#00ff41');
gradient.addColorStop(0.5, '#00aa00');
gradient.addColorStop(1, 'rgba(0, 255, 0, 0.1)');
ctx.fillStyle = gradient;
ctx.font = `${fontSize}px 'Share Tech Mono', monospace`;
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
const x = i * fontSize;
const y = drops[i] * fontSize;
if (text.match(/[โ™ชโ™ซโ™ฌโ™ฉ๐ŸŽต๐ŸŽถโšก๐Ÿ”Š๐ŸŽค๐ŸŽง]/)) {
ctx.shadowColor = '#00ff41'; ctx.shadowBlur = 20;
} else { ctx.shadowBlur = 0; }
ctx.fillText(text, x, y);
if (y * fontSize > canvas.height && Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
}
}
setInterval(draw, 60);
}
createMatrixRain(); // Initialize matrix effect
// Start voice recognition
window.startVoiceRecognition = function() {
if (!recognition) {
return "โŒ Speech recognition not supported in this browser. Try Chrome/Edge.";
}
if (isListening) {
return "๐ŸŽค Already listening...";
}
isListening = true;
recognition.onstart = function() {
console.log('๐ŸŽค Voice recognition started');
const statusElement = document.querySelector('#voice-status textarea');
if (statusElement) {
statusElement.value = "๐ŸŽค LISTENING... Speak your command now!";
statusElement.parentElement.classList.add('listening');
}
};
recognition.onresult = function(event) {
const transcript = event.results[0][0].transcript;
console.log('๐Ÿ—ฃ๏ธ Voice command:', transcript);
const voiceInputs = document.querySelectorAll('textarea');
for (let input of voiceInputs) {
if (input.placeholder && input.placeholder.includes('Voice commands')) {
input.value = transcript;
input.dispatchEvent(new Event('input', { bubbles: true }));
break;
}
}
setTimeout(() => {
const processBtn = Array.from(document.querySelectorAll('button')).find(
button => button.textContent.includes('PROCESS') || button.querySelector('span')?.textContent.includes('PROCESS')
);
if (processBtn) processBtn.click();
}, 100);
};
recognition.onerror = function(event) {
console.error('๐Ÿšซ Voice recognition error:', event.error);
const statusElement = document.querySelector('#voice-status textarea');
if (statusElement) {
statusElement.value = `โŒ Error: ${event.error}. Please try again.`;
statusElement.parentElement.classList.remove('listening');
}
isListening = false;
};
recognition.onend = function() {
console.log('๐ŸŽค Voice recognition ended');
const statusElement = document.querySelector('#voice-status textarea');
if (statusElement) {
if (statusElement.value.includes('LISTENING')) {
statusElement.value = "โœ… Voice command captured! Processing...";
}
statusElement.parentElement.classList.remove('listening');
}
isListening = false;
};
try {
recognition.start();
return "๐ŸŽค Voice recognition started - speak now!";
} catch (error) {
console.error('Failed to start recognition:', error);
isListening = false;
return "โŒ Failed to start voice recognition";
}
};
// Stop voice recognition
window.stopVoiceRecognition = function() {
if (recognition && isListening) {
recognition.stop();
isListening = false;
return "โน๏ธ Voice recognition stopped";
}
return "โน๏ธ Voice recognition not active";
};
// --- Strudel Playback Functions ---
window.playStrudelCode = async function(code) {
const isInitialized = await initStrudel();
if (!isInitialized) {
return "โŒ Strudel audio engine could not be initialized.";
}
try {
if (strudelPlayer && strudelPlayer.stop) {
strudelPlayer.stop();
console.log("Previous Strudel pattern stopped.");
}
if (window.audioContext && window.audioContext.state === 'suspended') {
await window.audioContext.resume();
console.log("AudioContext resumed on user interaction for playback.");
}
if (strudelPlayer && strudelPlayer.setSynth && strudelPlayer.play) {
await strudelPlayer.setSynth(code);
strudelPlayer.play();
console.log("โ–ถ๏ธ Playing Strudel code.");
return "โ–ถ๏ธ Playing music...";
} else {
console.error("Strudel player methods (setSynth, play) not available.");
return "โŒ Strudel player not fully ready for playback.";
}
} catch (error) {
console.error("Error playing Strudel code:", error);
let errorMessage = `โŒ Audio Error: ${error.message}.`;
if (error.message.includes("Unexpected token") || error.message.includes("SyntaxError")) {
errorMessage = "โŒ Strudel code syntax error. Check code!";
} else if (error.message.includes("Failed to load module")) {
errorMessage = "โŒ Strudel.js library failed to load. Check browser console for network/CORS issues.";
} else if (error.message.includes("AudioContext") || error.message.includes("Web Audio API")) {
errorMessage = "โŒ Browser audio engine issue. Try refreshing.";
}
return errorMessage;
}
};
window.stopStrudelCode = function() {
if (strudelPlayer && strudelReady && strudelPlayer.stop) {
strudelPlayer.stop();
console.log("โน๏ธ Strudel music stopped.");
return "โน๏ธ Music stopped";
}
return "โน๏ธ Music not active";
};
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.ctrlKey || e.metaKey) { // Ctrl for Windows/Linux, Cmd for Mac
switch(e.key.toLowerCase()) {
case 'enter':
e.preventDefault();
const playBtn = Array.from(document.querySelectorAll('button')).find(
button => button.textContent.includes('PLAY') || button.querySelector('span')?.textContent.includes('PLAY')
);
if (playBtn) playBtn.click();
break;
case ' ':
e.preventDefault();
const stopBtn = Array.from(document.querySelectorAll('button')).find(
button => button.textContent.includes('STOP') || button.querySelector('span')?.textContent.includes('STOP')
);
if (stopBtn) stopBtn.click();
break;
case 'm':
e.preventDefault();
window.startVoiceRecognition();
break;
}
}
});
console.log('๐ŸŽต Voice-Controlled Strudel Generator Ready!');
console.log('๐ŸŽผ Shortcuts: Ctrl+Enter (Play), Ctrl+Space (Stop), Ctrl+M (Voice)');
return "๐ŸŽต Real Voice Recognition and Strudel.js Initialized!";
}
"""
# --- Gradio Interface Layout ---
with gr.Blocks(css=custom_css, js=voice_strudel_js, title="๐ŸŽต Voice Strudel Synth") as interface:
# Header Section
gr.HTML("""
<div style="text-align: center; padding: 20px;">
<h1>๐ŸŽต VOICE STRUDEL SYNTH</h1>
<p>Speak Your Beats Into Existence โ€” Code, See, Hear!</p>
</div>
""")
with gr.Column(elem_classes="main-controls"):
# Voice Input and Status
voice_status = gr.Textbox(
value="๐ŸŽค Ready - Click 'Start Voice' or Ctrl+M!",
label="VOICE STATUS",
elem_id="voice-status",
interactive=False,
max_lines=2
)
with gr.Row(elem_classes="control-buttons"):
start_voice_btn = gr.Button("๐ŸŽค START VOICE", variant="primary")
stop_voice_btn = gr.Button("โน๏ธ STOP VOICE", variant="secondary")
voice_input = gr.Textbox(
label="๐Ÿ—ฃ๏ธ CAPTURED VOICE COMMAND",
placeholder="Voice commands appear here automatically...",
lines=1, # Simplified to 1 line for minimal display
interactive=False
)
process_voice_btn = gr.Button("๐ŸŽฏ PROCESS VOICE COMMAND", variant="primary")
gr.Markdown("---") # Separator
# Audio Status and Controls
audio_status = gr.Textbox(
value="โน๏ธ Ready to play",
label="๐Ÿ”Š AUDIO STATUS",
interactive=False,
max_lines=2
)
with gr.Row(elem_classes="control-buttons"):
play_btn = gr.Button("โ–ถ๏ธ PLAY MUSIC", variant="primary")
stop_btn = gr.Button("โน๏ธ STOP MUSIC", variant="secondary")
# Strudel Code Editor
music_code = gr.Code(
value='''// ๐ŸŽต Voice Strudel Synth
// Speak a command to generate music!
// Try: "Make a beat"
// Then: "Play music" (or Ctrl+Enter)
stack(
// Your generated code will appear here...
).cpm(120) // Master tempo
''',
label="๐ŸŽผ STRUDEL CODE EDITOR",
language="javascript", # Strudel is JavaScript-like
lines=15, # Reduced lines for more compact view
interactive=True
)
# Instructions Section (Simplified)
gr.HTML("""
<div class="instructions">
<h3>HOW TO USE</h3>
<ul>
<li>Click <strong>"START VOICE"</strong> or press <strong>Ctrl+M</strong>.</li>
<li>Say: <strong>"Make a beat"</strong></li>
<li>Then: <strong>"Play music"</strong> (or click <strong>"PLAY MUSIC"</strong> / <strong>Ctrl+Enter</strong>)</li>
<li>To stop: <strong>"Stop music"</strong> (or click <strong>"STOP MUSIC"</strong> / <strong>Ctrl+Space</strong>)</li>
<li>To clear: <strong>"Clear code"</strong></li>
</ul>
<p style="text-align: center;">๐ŸŽค IMPORTANT: Allow microphone access when prompted!</p>
</div>
""")
# --- Event Handlers (Python functions triggered by Gradio events) ---
start_voice_btn.click(
fn=lambda: "๐ŸŽค Voice recognition started - speak now!",
outputs=voice_status,
js="() => window.startVoiceRecognition()"
)
stop_voice_btn.click(
fn=lambda: "โน๏ธ Voice recognition stopped",
outputs=voice_status,
js="() => window.stopVoiceRecognition()"
)
process_voice_btn.click(
fn=process_voice_command,
inputs=[voice_input, music_code],
outputs=[voice_status, music_code]
)
# This handles the play button click, which also triggers JS play function
def handle_play_button_click_status(js_status_message): # Only receive the status message from JS
return js_status_message
play_btn.click(
fn=handle_play_button_click_status,
inputs=[music_code], # music_code is passed to the JS function, not the Python fn directly
outputs=audio_status,
js="(code) => window.playStrudelCode(code)" # Pass code to JS, JS handles audio and returns status
)
stop_btn.click(
fn=lambda: "โน๏ธ Music stopped",
outputs=audio_status,
js="() => window.stopStrudelCode()"
)
clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear Code", variant="secondary")
clear_btn.click(fn=clear_code, outputs=[voice_status, music_code])
return interface
# --- Main execution block for Gradio ---
if __name__ == "__main__":
create_interface().launch()