| import gradio as gr |
| import requests |
| import json |
| import tempfile |
| import os |
| from typing import Optional |
| import random |
|
|
| |
| MODEL_NAME = "microsoft/DialoGPT-medium" |
| HF_API_URL = f"https://api-inference.huggingface.co/models/{MODEL_NAME}" |
|
|
| def generate_strudel_code(music_style: str, api_token: Optional[str] = None) -> str: |
| """Generate Strudel code using expanded pattern library and optional LLM""" |
| |
| |
| strudel_examples = { |
| "jazz": [ |
| 'note("c3 eb3 g3 bb3").slow(2).gain(0.8)', |
| 'note("f3 a3 c4 e4").slow(3).lpf(800)', |
| 'note("bb2 d3 f3 a3").slow(2.5).room(0.3)' |
| ], |
| "techno": [ |
| 's("bd hh").fast(2).gain(0.9)', |
| 's("bd*2 ~ bd ~").stack(s("~ hh*4")).lpf(2000)', |
| 's("bd ~ ~ bd").stack(s("hh*8")).distort(0.1)' |
| ], |
| "ambient": [ |
| 'note("c4 d4 e4 f4").slow(4).lpf(400).room(0.8)', |
| 'note("g3 bb3 d4 f4").slow(6).gain(0.6).delay(0.3)', |
| 'sine(200).slow(8).lpf(300).room(0.9)' |
| ], |
| "drum and bass": [ |
| 's("bd*2 [~ sn] bd sn").fast(4)', |
| 's("bd ~ bd sn").fast(3).stack(s("hh*16").gain(0.3))', |
| 's("bd*2 ~ sn ~").fast(4).lpf(1200)' |
| ], |
| "house": [ |
| 's("bd ~ ~ ~ bd ~ ~ ~").stack(s("~ hh ~ hh"))', |
| 's("bd*2 ~ ~ bd ~ ~ ~").stack(s("hh*4")).lpf(800)', |
| 's("bd ~ bd ~").stack(s("~ hh*2")).gain(0.8)' |
| ], |
| "classical": [ |
| 'note("c4 d4 e4 f4 g4 a4 b4 c5").slow(8)', |
| 'note("g3 b3 d4 g4").slow(4).stack(note("d2 g2").slow(2))', |
| 'note("c4 e4 g4 c5").slow(6).gain(0.7)' |
| ], |
| "hip hop": [ |
| 's("bd sn bd sn").stack(s("hh*8"))', |
| 's("bd*2 ~ sn ~").stack(s("hh ~ hh ~")).slow(0.8)', |
| 's("bd ~ sn bd").stack(s("hh*4")).lpf(1000)' |
| ], |
| "rock": [ |
| 's("bd ~ sn ~").stack(note("e2 ~ g2 ~"))', |
| 's("bd bd sn ~").stack(note("a2 c3 e3").slow(2))', |
| 's("bd ~ sn bd").stack(note("e2 g2").slow(1.5))' |
| ], |
| "blues": [ |
| 'note("c3 eb3 f3 g3 bb3").slow(3)', |
| 'note("e2 a2 b2 e3").slow(4).gain(0.8)', |
| 'note("g2 bb2 d3 f3").slow(3.5).room(0.2)' |
| ], |
| "reggae": [ |
| 's("~ bd ~ sn").slow(2)', |
| 's("~ bd*2 ~ sn").stack(s("hh ~ hh ~")).slow(1.5)', |
| 's("~ bd ~ sn ~").slow(2.5).gain(0.7)' |
| ], |
| "electronic": [ |
| 'square(220).slow(4).lpf(800)', |
| 's("bd*4").stack(sawtooth(110).slow(2))', |
| 'sine(440).fast(2).lpf(1000).delay(0.2)' |
| ], |
| "minimal": [ |
| 's("bd ~ ~ ~").slow(2)', |
| 'note("c4").slow(8).gain(0.5)', |
| 's("~ ~ bd ~").stack(sine(200).slow(16))' |
| ], |
| "funk": [ |
| 's("bd ~ sn bd").stack(note("e2 ~ a2 ~"))', |
| 's("bd*2 sn ~ bd").fast(1.2).stack(s("hh*8"))', |
| 'note("a2 c3 e3").slow(2).stack(s("bd sn"))' |
| ] |
| } |
| |
| |
| best_match = None |
| for style, patterns in strudel_examples.items(): |
| if style.lower() in music_style.lower(): |
| best_match = random.choice(patterns) |
| break |
| |
| |
| if not best_match: |
| for style, patterns in strudel_examples.items(): |
| if any(word in music_style.lower() for word in style.split()): |
| best_match = random.choice(patterns) |
| break |
| |
| |
| if not best_match and api_token: |
| try: |
| headers = { |
| "Authorization": f"Bearer {api_token}", |
| "Content-Type": "application/json" |
| } |
| |
| prompt = f"Create Strudel live coding music pattern for {music_style}. Use functions: note(), s(), sound(), sine(), square(), sawtooth(), slow(), fast(), stack(), lpf(), gain(), room(), delay(). Example: s('bd hh').fast(2)" |
| |
| payload = { |
| "inputs": prompt, |
| "parameters": { |
| "max_length": 80, |
| "temperature": 0.8, |
| "return_full_text": False, |
| "num_return_sequences": 1 |
| } |
| } |
| |
| response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=15) |
| if response.status_code == 200: |
| result = response.json() |
| if isinstance(result, list) and len(result) > 0: |
| generated_text = result[0].get('generated_text', '').strip() |
| |
| if generated_text and any(func in generated_text for func in |
| ['note(', 's(', 'sound(', 'sine(', 'square(', 'sawtooth(']): |
| return generated_text |
| except Exception as e: |
| print(f"API Error: {e}") |
| |
| |
| return best_match or 'note("c4 d4 e4 f4").slow(2).gain(0.7)' |
|
|
| def create_strudel_html(strudel_code: str) -> str: |
| """Create simplified HTML with Strudel player that works better in HF Spaces""" |
| return f""" |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Strudel Music Player</title> |
| <script src="https://unpkg.com/tone@latest/build/Tone.js"></script> |
| <style> |
| * {{ |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| }} |
| |
| body {{ |
| font-family: 'Courier New', Arial, sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| padding: 20px; |
| }} |
| |
| .container {{ |
| background: rgba(0, 0, 0, 0.3); |
| backdrop-filter: blur(10px); |
| border-radius: 15px; |
| padding: 30px; |
| max-width: 600px; |
| width: 100%; |
| text-align: center; |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); |
| }} |
| |
| h1 {{ |
| margin-bottom: 20px; |
| font-size: 2.5em; |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
| }} |
| |
| .code-display {{ |
| background: rgba(0, 0, 0, 0.6); |
| padding: 20px; |
| border-radius: 10px; |
| margin: 20px 0; |
| font-family: 'Courier New', monospace; |
| font-size: 16px; |
| border-left: 4px solid #4CAF50; |
| word-break: break-all; |
| text-align: left; |
| }} |
| |
| .controls {{ |
| margin: 30px 0; |
| display: flex; |
| gap: 15px; |
| justify-content: center; |
| flex-wrap: wrap; |
| }} |
| |
| button {{ |
| padding: 12px 24px; |
| font-size: 16px; |
| font-weight: bold; |
| background: linear-gradient(45deg, #4CAF50, #45a049); |
| color: white; |
| border: none; |
| border-radius: 25px; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); |
| min-width: 100px; |
| }} |
| |
| button:hover:not(:disabled) {{ |
| transform: translateY(-2px); |
| box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4); |
| }} |
| |
| button:disabled {{ |
| background: #666; |
| cursor: not-allowed; |
| transform: none; |
| box-shadow: none; |
| }} |
| |
| .status {{ |
| font-size: 18px; |
| font-weight: bold; |
| margin: 20px 0; |
| padding: 10px; |
| border-radius: 5px; |
| transition: all 0.3s ease; |
| }} |
| |
| .playing {{ |
| background: rgba(76, 175, 80, 0.2); |
| color: #4CAF50; |
| }} |
| |
| .stopped {{ |
| background: rgba(244, 67, 54, 0.2); |
| color: #f44336; |
| }} |
| |
| .info {{ |
| margin-top: 20px; |
| font-size: 14px; |
| opacity: 0.8; |
| line-height: 1.5; |
| }} |
| |
| .error {{ |
| background: rgba(244, 67, 54, 0.2); |
| color: #f44336; |
| padding: 10px; |
| border-radius: 5px; |
| margin: 10px 0; |
| }} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>π΅ Strudel Player</h1> |
| |
| <div class="code-display"> |
| <strong>Generated Pattern:</strong><br> |
| <code id="codeDisplay">{strudel_code}</code> |
| </div> |
| |
| <div class="controls"> |
| <button id="playBtn" onclick="playMusic()">βΆοΈ Play</button> |
| <button id="pauseBtn" onclick="pauseMusic()" disabled>βΈοΈ Pause</button> |
| <button id="stopBtn" onclick="stopMusic()" disabled>βΉοΈ Stop</button> |
| </div> |
| |
| <div id="status" class="status stopped">Click Play to start audio</div> |
| |
| <div class="info"> |
| <p>π§ Use headphones for best experience</p> |
| <p>β‘ Powered by Web Audio API & Tone.js</p> |
| </div> |
| </div> |
| |
| <script> |
| // Simple audio player using Tone.js as fallback |
| let isPlaying = false; |
| let audioContext = null; |
| let currentPattern = null; |
| |
| // Initialize audio context on first user interaction |
| async function initAudio() {{ |
| try {{ |
| if (!audioContext) {{ |
| await Tone.start(); |
| audioContext = Tone.getContext(); |
| updateStatus('Audio initialized - Ready to play', 'stopped'); |
| }} |
| return true; |
| }} catch (error) {{ |
| console.error('Audio initialization failed:', error); |
| updateStatus('Audio initialization failed: ' + error.message, 'error'); |
| return false; |
| }} |
| }} |
| |
| // Simple pattern interpreter (basic Strudel-inspired) |
| function interpretPattern(code) {{ |
| try {{ |
| // Very basic pattern parsing - in a real implementation you'd want full Strudel |
| const synth = new Tone.Synth().toDestination(); |
| const player = new Tone.Player().toDestination(); |
| |
| // Simple note pattern recognition |
| const noteMatch = code.match(/note\\(["']([^"']+)["']\\)/); |
| if (noteMatch) {{ |
| const notes = noteMatch[1].split(' '); |
| let time = 0; |
| const interval = 0.5; // Default timing |
| |
| // Schedule notes |
| notes.forEach((note, index) => {{ |
| if (note.trim()) {{ |
| Tone.Transport.schedule((time) => {{ |
| synth.triggerAttackRelease(note + '4', '8n', time); |
| }}, time); |
| time += interval; |
| }} |
| }}); |
| |
| return {{ duration: time, type: 'notes' }}; |
| }} |
| |
| // Simple drum pattern recognition |
| const drumMatch = code.match(/s\\(["']([^"']+)["']\\)/); |
| if (drumMatch) {{ |
| // Simulate basic drum sounds with synth |
| const kicks = drumMatch[1].split(' '); |
| let time = 0; |
| const interval = 0.25; |
| |
| kicks.forEach((drum, index) => {{ |
| if (drum.includes('bd')) {{ |
| Tone.Transport.schedule((time) => {{ |
| synth.triggerAttackRelease('C2', '8n', time); |
| }}, time); |
| }} else if (drum.includes('sn')) {{ |
| Tone.Transport.schedule((time) => {{ |
| synth.triggerAttackRelease('E4', '16n', time); |
| }}, time); |
| }} else if (drum.includes('hh')) {{ |
| Tone.Transport.schedule((time) => {{ |
| synth.triggerAttackRelease('A5', '32n', time); |
| }}, time); |
| }} |
| time += interval; |
| }}); |
| |
| return {{ duration: time, type: 'drums' }}; |
| }} |
| |
| // Fallback - play a simple melody |
| const melody = ['C4', 'D4', 'E4', 'F4']; |
| melody.forEach((note, index) => {{ |
| Tone.Transport.schedule((time) => {{ |
| synth.triggerAttackRelease(note, '4n', time); |
| }}, index * 0.5); |
| }}); |
| |
| return {{ duration: 2, type: 'fallback' }}; |
| |
| }} catch (error) {{ |
| console.error('Pattern interpretation error:', error); |
| updateStatus('Pattern error: ' + error.message, 'error'); |
| return null; |
| }} |
| }} |
| |
| async function playMusic() {{ |
| try {{ |
| const audioReady = await initAudio(); |
| if (!audioReady) return; |
| |
| if (isPlaying) return; |
| |
| updateStatus('π΅ Playing pattern...', 'playing'); |
| |
| const code = document.getElementById('codeDisplay').textContent; |
| currentPattern = interpretPattern(code); |
| |
| if (currentPattern) {{ |
| Tone.Transport.start(); |
| isPlaying = true; |
| updateControls(); |
| |
| // Auto-stop after pattern duration |
| setTimeout(() => {{ |
| if (isPlaying) {{ |
| stopMusic(); |
| }} |
| }}, (currentPattern.duration + 1) * 1000); |
| }} else {{ |
| updateStatus('Failed to parse pattern', 'error'); |
| }} |
| |
| }} catch (error) {{ |
| console.error('Playback error:', error); |
| updateStatus('Playback error: ' + error.message, 'error'); |
| }} |
| }} |
| |
| function pauseMusic() {{ |
| if (isPlaying) {{ |
| Tone.Transport.pause(); |
| isPlaying = false; |
| updateStatus('βΈοΈ Paused', 'stopped'); |
| updateControls(); |
| }} |
| }} |
| |
| function stopMusic() {{ |
| Tone.Transport.stop(); |
| Tone.Transport.cancel(); |
| isPlaying = false; |
| currentPattern = null; |
| updateStatus('βΉοΈ Stopped', 'stopped'); |
| updateControls(); |
| }} |
| |
| function updateControls() {{ |
| document.getElementById('playBtn').disabled = isPlaying; |
| document.getElementById('pauseBtn').disabled = !isPlaying; |
| document.getElementById('stopBtn').disabled = !isPlaying; |
| }} |
| |
| function updateStatus(message, type) {{ |
| const status = document.getElementById('status'); |
| status.textContent = message; |
| status.className = `status ${{type}}`; |
| }} |
| |
| // Cleanup on page unload |
| window.addEventListener('beforeunload', () => {{ |
| if (isPlaying) {{ |
| stopMusic(); |
| }} |
| }}); |
| |
| // Handle visibility changes |
| document.addEventListener('visibilitychange', () => {{ |
| if (document.hidden && isPlaying) {{ |
| pauseMusic(); |
| }} |
| }}); |
| |
| console.log('Strudel player initialized'); |
| </script> |
| </body> |
| </html>""" |
|
|
| def generate_and_play(music_style: str, api_token: Optional[str] = None): |
| """Generate Strudel code and return playable HTML""" |
| if not music_style.strip(): |
| return "β Please enter a music style description.", "<p>Enter a music style to generate audio</p>" |
| |
| try: |
| |
| strudel_code = generate_strudel_code(music_style, api_token) |
| |
| |
| html_content = create_strudel_html(strudel_code) |
| |
| |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f: |
| f.write(html_content) |
| html_file_path = f.name |
| |
| success_msg = f"β
Generated Strudel pattern: `{strudel_code}`\n\nπ΅ Click Play in the player below to hear your music!" |
| |
| return success_msg, html_content |
| |
| except Exception as e: |
| error_msg = f"β Error generating music: {str(e)}" |
| return error_msg, "<p>Error generating audio player</p>" |
|
|
| def create_interface(): |
| """Create the Gradio interface""" |
| |
| |
| custom_css = """ |
| .gradio-container { |
| max-width: 900px !important; |
| margin: auto !important; |
| } |
| |
| .main-header { |
| text-align: center; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| font-size: 2.5em; |
| font-weight: bold; |
| margin-bottom: 20px; |
| } |
| |
| .examples-section { |
| background: #f8f9fa; |
| border-radius: 10px; |
| padding: 15px; |
| margin: 10px 0; |
| } |
| """ |
| |
| with gr.Blocks( |
| title="π΅ Strudel Music Generator", |
| theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"), |
| css=custom_css |
| ) as demo: |
| |
| gr.HTML(""" |
| <div class="main-header"> |
| π΅ Strudel Music Generator |
| </div> |
| """) |
| |
| gr.Markdown(""" |
| Generate algorithmic music patterns using [Strudel.cc](https://strudel.cc/) syntax! |
| |
| **How to use:** |
| 1. πΌ Describe your desired music style |
| 2. π (Optional) Add your Hugging Face API token for AI-generated patterns |
| 3. π΅ Click "Generate Music" to create your pattern |
| 4. βΆοΈ Use the audio player controls to play your music |
| """) |
| |
| with gr.Row(): |
| with gr.Column(scale=2): |
| music_input = gr.Textbox( |
| label="πΌ Music Style Description", |
| placeholder="Try: 'jazz piano', 'techno beats', 'ambient soundscape', 'funky bass'...", |
| lines=2, |
| max_lines=3 |
| ) |
| |
| api_token_input = gr.Textbox( |
| label="π Hugging Face API Token (Optional)", |
| placeholder="hf_... (for AI-generated patterns)", |
| type="password", |
| info="Get your free token at https://huggingface.co/settings/tokens" |
| ) |
| |
| generate_btn = gr.Button( |
| "π΅ Generate Music", |
| variant="primary", |
| size="lg" |
| ) |
| |
| with gr.Column(scale=1): |
| gr.Markdown(""" |
| <div class="examples-section"> |
| |
| ### π¨ Style Examples: |
| - **Jazz**: swing, bebop, smooth |
| - **Electronic**: techno, house, ambient |
| - **Classical**: piano, strings, orchestral |
| - **Urban**: hip hop, reggae, funk |
| - **Rock**: blues, metal, indie |
| - **Experimental**: minimal, drone, glitch |
| |
| </div> |
| """) |
| |
| with gr.Row(): |
| code_output = gr.Textbox( |
| label="π Generated Code & Status", |
| interactive=False, |
| lines=4, |
| show_copy_button=True |
| ) |
| |
| with gr.Row(): |
| audio_player = gr.HTML( |
| label="π΅ Interactive Audio Player", |
| value="<div style='text-align: center; padding: 40px; background: #f0f0f0; border-radius: 10px;'><h3>πΌ Generate music to see the player</h3><p>Your interactive audio player will appear here</p></div>", |
| elem_id="audio-player" |
| ) |
| |
| |
| generate_btn.click( |
| fn=generate_and_play, |
| inputs=[music_input, api_token_input], |
| outputs=[code_output, audio_player], |
| show_progress=True |
| ) |
| |
| |
| music_input.submit( |
| fn=generate_and_play, |
| inputs=[music_input, api_token_input], |
| outputs=[code_output, audio_player] |
| ) |
| |
| |
| gr.Examples( |
| examples=[ |
| ["jazz piano with swing rhythm"], |
| ["minimal techno beat"], |
| ["ambient electronic soundscape"], |
| ["funky bass line groove"], |
| ["classical piano arpeggios"], |
| ["drum and bass breakbeat"], |
| ["reggae dub rhythm"], |
| ["experimental glitch sounds"] |
| ], |
| inputs=[music_input], |
| label="π― Quick Start Examples" |
| ) |
| |
| gr.Markdown(""" |
| --- |
| |
| ### π About Strudel |
| |
| **Strudel** is a live coding language for algorithmic music composition. This app generates Strudel patterns and plays them using Web Audio API. |
| |
| **Key Functions:** |
| - `note("c d e f")` - Play musical notes |
| - `s("bd hh sn")` - Play drum samples (bd=bass drum, hh=hi-hat, sn=snare) |
| - `sound("piano")` - Use instrument sounds |
| - `.slow(2)` / `.fast(2)` - Change tempo |
| - `.stack()` - Layer multiple patterns |
| - `.lpf(800)` - Low-pass filter |
| - `.gain(0.5)` - Volume control |
| |
| **π§ Technical Notes:** |
| - Audio runs entirely in your browser |
| - No audio files needed - everything is synthesized |
| - Works best with modern browsers and headphones |
| - Patterns are simplified for web compatibility |
| |
| **π Want full Strudel?** Visit [strudel.cc](https://strudel.cc/) for the complete live coding environment! |
| """) |
| |
| return demo |
|
|
| |
| if __name__ == "__main__": |
| demo = create_interface() |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| share=False, |
| show_error=True |
| ) |