| import gradio as gr |
| import random |
|
|
| |
| class StrudelGenerator: |
| def __init__(self): |
| self.patterns = { |
| 'drums': ['"bd*4"', '"bd ~ sd ~"', '"bd*2 sd*2"'], |
| 'bass': ['"c2 [~ c2] f2 [~ f2]"', '"c2*4"', '"c1 ~ f1 ~"'], |
| 'chords': ['"<C^7 F^7 G^7 C^7>".voicing()', '"<c\'maj7 f\'maj7 g\'7 c\'maj7>".voicing()'], |
| 'melody': ['"c5 d5 e5 f5"', '"c5 e5 g5 c6"'] |
| } |
|
|
| def generate_code(self, description: str, tempo: int = 120) -> str: |
| """Generate simple Strudel code""" |
| desc = description.lower() |
| patterns = [] |
| |
| |
| if any(word in desc for word in ['drum', 'beat', 'kick']): |
| patterns.append(random.choice(self.patterns['drums']) + '.gain(0.8)') |
| |
| if any(word in desc for word in ['bass', 'low']): |
| patterns.append(random.choice(self.patterns['bass']) + '.sound("sawtooth").gain(0.6)') |
| |
| if any(word in desc for word in ['chord', 'harmony', 'piano']): |
| patterns.append(random.choice(self.patterns['chords']) + '.sound("piano").gain(0.4)') |
| |
| if any(word in desc for word in ['melody', 'lead', 'tune']): |
| patterns.append(random.choice(self.patterns['melody']) + '.sound("sine").gain(0.5)') |
| |
| |
| if not patterns: |
| patterns = [ |
| '"bd ~ sd ~".gain(0.7)', |
| '"<C^7 F^7 G^7 C^7>".voicing().sound("piano").gain(0.4)' |
| ] |
| |
| |
| if len(patterns) == 1: |
| code = patterns[0] |
| else: |
| code = "stack(\n" |
| for i, pattern in enumerate(patterns): |
| comma = "," if i < len(patterns) - 1 else "" |
| code += f" {pattern}{comma}\n" |
| code += ")" |
| |
| code += f".cpm({tempo})" |
| return code |
|
|
| generator = StrudelGenerator() |
|
|
| def generate_music(description, tempo): |
| """Generate music code""" |
| if not description.strip(): |
| return "// Please describe your music first!" |
| |
| code = generator.generate_code(description, tempo) |
| return code |
|
|
| |
| with gr.Blocks(title="๐ต Strudel Generator") as app: |
| |
| gr.HTML(""" |
| <div style='text-align: center; margin-bottom: 30px;'> |
| <h1 style='color: #FF6B9D; font-size: 2.5rem; margin: 0;'>๐ต Strudel Music Generator</h1> |
| <p style='color: #666; font-size: 1.1rem;'>Describe your music and generate Strudel code!</p> |
| </div> |
| """) |
| |
| |
| description = gr.Textbox( |
| label="Describe your music", |
| placeholder="e.g., 'upbeat drums with piano chords'", |
| lines=2 |
| ) |
| |
| tempo = gr.Slider(60, 180, 120, step=5, label="Tempo (BPM)") |
| |
| generate_btn = gr.Button("๐ Generate Code", variant="primary") |
| |
| |
| code_output = gr.Code( |
| label="Generated Strudel Code", |
| language="javascript", |
| lines=8 |
| ) |
| |
| |
| gr.HTML(""" |
| <div id="player" style=' |
| background: linear-gradient(135deg, #FF6B9D, #A855F7); |
| padding: 20px; |
| border-radius: 15px; |
| text-align: center; |
| margin: 20px 0; |
| '> |
| <h3 style='color: white; margin: 0 0 15px 0;'>๐ต Audio Player</h3> |
| <button id="playBtn" onclick="togglePlay()" style=' |
| background: white; |
| color: #FF6B9D; |
| border: none; |
| padding: 12px 30px; |
| border-radius: 25px; |
| font-size: 16px; |
| font-weight: bold; |
| cursor: pointer; |
| margin: 5px; |
| transition: all 0.3s; |
| '>โถ๏ธ Play</button> |
| <button onclick="stopPlay()" style=' |
| background: rgba(255,255,255,0.8); |
| color: #FF6B9D; |
| border: none; |
| padding: 12px 30px; |
| border-radius: 25px; |
| font-size: 16px; |
| font-weight: bold; |
| cursor: pointer; |
| margin: 5px; |
| transition: all 0.3s; |
| '>โน๏ธ Stop</button> |
| <div id="status" style='color: white; margin-top: 10px; font-size: 14px;'> |
| Click to enable audio, then generate code and play! |
| </div> |
| </div> |
| |
| <!-- Load Strudel.js libraries --> |
| <script type="module"> |
| import { repl, controls, mini, initAudioOnFirstClick } from 'https://strudel.cc/strudel.mjs'; |
| |
| // Make Strudel available globally |
| window.strudel = { repl, controls, mini, initAudioOnFirstClick }; |
| window.strudelReady = true; |
| |
| console.log('Strudel.js loaded successfully!'); |
| updateStatus('๐ต Strudel.js ready! Generate code and play!', 'white'); |
| </script> |
| |
| <script> |
| let isPlaying = false; |
| let currentPattern = null; |
| let strudelInitialized = false; |
| |
| function getCurrentCode() { |
| // Get code from the textarea - try multiple selectors |
| const selectors = [ |
| 'textarea[data-testid*="code"]', |
| '.code-container textarea', |
| 'textarea', |
| 'code', |
| '[data-testid="code"] textarea' |
| ]; |
| |
| for (let selector of selectors) { |
| const element = document.querySelector(selector); |
| if (element && (element.value || element.textContent)) { |
| const code = element.value || element.textContent; |
| if (code && !code.includes('Please describe') && code.includes('cpm')) { |
| return code; |
| } |
| } |
| } |
| return ''; |
| } |
| |
| function updateStatus(message, color = 'white') { |
| const status = document.getElementById('status'); |
| if (status) { |
| status.textContent = message; |
| status.style.color = color; |
| } |
| } |
| |
| function updatePlayButton(playing) { |
| const btn = document.getElementById('playBtn'); |
| if (btn) { |
| btn.textContent = playing ? 'โธ๏ธ Pause' : 'โถ๏ธ Play'; |
| } |
| } |
| |
| async function initializeStrudel() { |
| if (strudelInitialized) return true; |
| |
| try { |
| // Wait for Strudel to load |
| let attempts = 0; |
| while (!window.strudelReady && attempts < 50) { |
| await new Promise(resolve => setTimeout(resolve, 100)); |
| attempts++; |
| } |
| |
| if (!window.strudel) { |
| throw new Error('Strudel failed to load'); |
| } |
| |
| // Initialize audio on first click |
| await window.strudel.initAudioOnFirstClick(); |
| strudelInitialized = true; |
| updateStatus('๐ต Audio ready! Generate code and play!', '#ccffcc'); |
| return true; |
| } catch (error) { |
| console.error('Strudel initialization failed:', error); |
| updateStatus('โ ๏ธ Audio init failed - using demo mode', '#ffcccc'); |
| return false; |
| } |
| } |
| |
| async function togglePlay() { |
| if (isPlaying) { |
| pausePlay(); |
| } else { |
| await startPlay(); |
| } |
| } |
| |
| async function startPlay() { |
| const code = getCurrentCode(); |
| |
| if (!code || code.trim() === '' || code.includes('Please describe')) { |
| updateStatus('No code to play! Generate some first.', '#ffcccc'); |
| return; |
| } |
| |
| try { |
| // Initialize Strudel if needed |
| const initialized = await initializeStrudel(); |
| |
| if (!initialized || !window.strudel) { |
| // Fallback to demo mode |
| updateStatus('๐ฎ Demo: Playing ' + code.split('\\n')[0] + '...', '#ffffcc'); |
| isPlaying = true; |
| updatePlayButton(true); |
| |
| setTimeout(() => { |
| stopPlay(); |
| updateStatus('Demo playback finished!', 'white'); |
| }, 8000); |
| return; |
| } |
| |
| // Stop any current pattern |
| if (currentPattern) { |
| currentPattern.stop(); |
| } |
| |
| updateStatus('๐ต Evaluating code...', '#ffffcc'); |
| |
| // Clean the code (remove comments) |
| const cleanCode = code |
| .split('\\n') |
| .filter(line => !line.trim().startsWith('//')) |
| .join('\\n') |
| .trim(); |
| |
| console.log('Playing code:', cleanCode); |
| |
| // Evaluate the Strudel pattern |
| const pattern = window.strudel.repl.evaluate(cleanCode); |
| |
| if (pattern && typeof pattern.play === 'function') { |
| currentPattern = pattern; |
| await pattern.play(); |
| |
| isPlaying = true; |
| updatePlayButton(true); |
| updateStatus('๐ต Playing your music! ๐ถ', '#ccffcc'); |
| } else { |
| throw new Error('Invalid pattern - check your Strudel syntax'); |
| } |
| |
| } catch (error) { |
| console.error('Playback error:', error); |
| updateStatus('โ Error: ' + error.message, '#ffcccc'); |
| isPlaying = false; |
| updatePlayButton(false); |
| |
| // Try demo mode as fallback |
| setTimeout(() => { |
| updateStatus('๐ฎ Falling back to demo mode...', '#ffffcc'); |
| isPlaying = true; |
| updatePlayButton(true); |
| |
| setTimeout(() => { |
| stopPlay(); |
| updateStatus('Demo playback finished!', 'white'); |
| }, 5000); |
| }, 1000); |
| } |
| } |
| |
| function pausePlay() { |
| if (currentPattern && typeof currentPattern.stop === 'function') { |
| currentPattern.stop(); |
| } |
| |
| isPlaying = false; |
| updatePlayButton(false); |
| updateStatus('โธ๏ธ Paused', '#ffffcc'); |
| } |
| |
| function stopPlay() { |
| if (currentPattern && typeof currentPattern.stop === 'function') { |
| currentPattern.stop(); |
| currentPattern = null; |
| } |
| |
| isPlaying = false; |
| updatePlayButton(false); |
| updateStatus('โน๏ธ Stopped', 'white'); |
| } |
| |
| // Add hover effects to buttons |
| document.addEventListener('DOMContentLoaded', function() { |
| const buttons = document.querySelectorAll('#player button'); |
| buttons.forEach(btn => { |
| btn.addEventListener('mouseenter', function() { |
| this.style.transform = 'scale(1.05)'; |
| this.style.boxShadow = '0 5px 15px rgba(0,0,0,0.2)'; |
| }); |
| btn.addEventListener('mouseleave', function() { |
| this.style.transform = 'scale(1)'; |
| this.style.boxShadow = 'none'; |
| }); |
| }); |
| |
| // Initialize on first user interaction |
| document.addEventListener('click', initializeStrudel, { once: true }); |
| }); |
| |
| // Handle page visibility changes |
| document.addEventListener('visibilitychange', function() { |
| if (document.hidden && isPlaying) { |
| pausePlay(); |
| } |
| }); |
| </script> |
| """) |
| |
| |
| gr.HTML("<h3 style='margin-top: 30px;'>๐จ Quick Examples:</h3>") |
| |
| with gr.Row(): |
| ex1 = gr.Button("๐ฎ Chiptune", size="sm") |
| ex2 = gr.Button("๐ Lofi", size="sm") |
| ex3 = gr.Button("โก Techno", size="sm") |
| ex4 = gr.Button("๐น Jazz", size="sm") |
| |
| |
| generate_btn.click( |
| fn=generate_music, |
| inputs=[description, tempo], |
| outputs=[code_output] |
| ) |
| |
| |
| ex1.click(lambda: "8-bit chiptune with bouncy melody and simple drums", outputs=[description]) |
| ex2.click(lambda: "mellow lofi with soft piano chords and gentle drums", outputs=[description]) |
| ex3.click(lambda: "driving techno with heavy bass and pounding kick", outputs=[description]) |
| ex4.click(lambda: "smooth jazz with walking bass and chord progressions", outputs=[description]) |
|
|
| gr.HTML(""" |
| <div style='text-align: center; margin-top: 30px; opacity: 0.7;'> |
| <p>Made for National Make Music Day ๐ต</p> |
| </div> |
| """) |
|
|
| if __name__ == "__main__": |
| app.launch( |
| share=False, |
| server_name="0.0.0.0", |
| server_port=7860 |
| ) |