Strudel Integration Plan - Option A
```bash cd apps/desktop/cc-echelon/apps/echelon-tauri npm install @strudel.cycles/core @strudel.cycles/webaudio @strudel.cycles/tonal tone ```
Full Public Reader
Strudel Integration Plan - Option A
## Overview
Wire existing Rust components (Conductor, Rehearsal, LIM-RPS) to Strudel.js for real-time pattern-based music generation.
## Current State
- ✅ LIM-RPS latent physics (Python + Rust)
- ✅ Computational rehearsal (`cc-brain/rehearsal.rs`)
- ✅ Conductor with section state machine (`cc-brain/conductor.rs`)
- ✅ Strudel-IR types complete (`cc-protocol/strudel_ir`)
- ✅ Audio engine with synths/effects
- ✅ Tauri frontend with real-time visualization
- ❌ No Strudel runtime executing patterns
- ❌ No bridge from Rust PatternEdit → JavaScript execution
Implementation Phases
Phase 1: Add Strudel Runtime (Days 1-2)
1.1 Install Strudel Dependencies
cd apps/desktop/cc-echelon/apps/echelon-tauri
npm install @strudel.cycles/core @strudel.cycles/webaudio @strudel.cycles/tonal tone1.2 Create StrudelEngine.js
File: `apps/echelon-tauri/src/audio/StrudelEngine.js`
import { Pattern, repl } from '@strudel.cycles/core'
import { webaudio, initAudio } from '@strudel.cycles/webaudio'
import * as Tone from 'tone'
export class StrudelEngine {
constructor() {
this.initialized = false
this.currentPattern = null
this.params = {
kick_gain: 0.8,
hihat_density: 8,
bass_cutoff: 400,
pad_reverb: 0.3,
}
}
async init() {
if (this.initialized) return
// Initialize Tone.js and Strudel
await Tone.start()
await initAudio()
// Set up default pattern (house template)
this.setDefaultPattern()
this.initialized = true
console.log('✅ StrudelEngine initialized')
}
setDefaultPattern() {
// Simple house template
const pattern = `
stack(
s("bd").every(4, x => x.fast(2)).gain($kick_gain),
s("hh*$hihat_density").gain(0.5),
note("c2 _ _ eb2").s("sawtooth").lpf($bass_cutoff).gain(0.6),
note("c4 eb4 g4").s("triangle").room($pad_reverb).gain(0.3)
)
`.trim()
this.currentPattern = pattern
this.evaluate(pattern)
}
evaluate(patternCode) {
try {
// Substitute parameter values
let code = patternCode
for (const [key, value] of Object.entries(this.params)) {
code = code.replaceAll(`$${key}`, value)
}
repl({ defaultOutput: 'webaudio' })(code)
console.log('🎵 Pattern evaluated:', code.substring(0, 50) + '...')
} catch (error) {
console.error('❌ Strudel error:', error)
}
}
// Apply PatternEdit command from Rust
applyEdit(edit) {
switch (edit.type) {
case 'set_param':
this.setParam(edit.name, edit.value)
break
case 'schedule_event':
this.scheduleEvent(edit.beat_offset, edit.action)
break
case 'transform':
this.applyTransform(edit.track, edit.transform, edit.duration_beats)
break
default:
console.warn('Unknown edit type:', edit.type)
}
// Re-evaluate with new params
this.evaluate(this.currentPattern)
}
setParam(name, value) {
this.params[name] = value
console.log(`🎛️ ${name} = ${value}`)
}
scheduleEvent(beatOffset, action) {
// Schedule action at future beat
console.log(`⏰ Scheduled ${action} in ${beatOffset} beats`)
// TODO: Implement Tone.Transport scheduling
}
applyTransform(track, transform, durationBeats) {
console.log(`🔄 Transform ${transform} on ${track} for ${durationBeats} beats`)
// TODO: Implement track-specific transforms
}
stop() {
Tone.Transport.stop()
}
}1.3 Integrate into React App
File: `apps/echelon-tauri/src/App.jsx` (add hook)
import { useEffect, useRef } from 'react'
import { StrudelEngine } from './audio/StrudelEngine'
function App() {
const strudelRef = useRef(null)
// Initialize Strudel on mount
useEffect(() => {
const initStrudel = async () => {
if (!strudelRef.current) {
strudelRef.current = new StrudelEngine()
await strudelRef.current.init()
}
}
initStrudel()
return () => {
strudelRef.current?.stop()
}
}, [])
// ... rest of existing code
}---
Phase 2: Rust → JS Bridge (Days 3-4)
2.1 Add Tauri Command for PatternEdits
File: `apps/echelon-tauri/src-tauri/src/commands.rs`
use serde::{Deserialize, Serialize};
use tauri::State;
use cc_brain::conductor::PatternEdit; // Import from cc-brain
/// Send pattern edit to frontend
#[tauri::command]
pub async fn apply_pattern_edit(
edit: PatternEdit,
window: tauri::Window,
) -> Result<(), String> {
// Emit event to frontend
window.emit("pattern_edit", &edit)
.map_err(|e| format!("Failed to emit edit: {}", e))?
Ok(())
}
/// Get conductor status
#[tauri::command]
pub async fn get_conductor_status(
state: State<'_, AppState>,
) -> Result<ConductorStatusJson, String> {
// TODO: Add conductor to AppState
Ok(ConductorStatusJson {
section: "Groove".to_string(),
bar: 0,
beat: 0,
tempo_bpm: 120.0,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConductorStatusJson {
pub section: String,
pub bar: u32,
pub beat: u32,
pub tempo_bpm: f32,
}Register commands in `main.rs`:
// In main() function, update invoke_handler:
.invoke_handler(tauri::generate_handler![
commands::get_latent_state,
commands::get_choreo_moment,
commands::set_style,
commands::set_mode,
commands::get_connection_status,
commands::apply_pattern_edit, // NEW
commands::get_conductor_status, // NEW
])2.2 Listen to PatternEdits in Frontend
File: `apps/echelon-tauri/src/App.jsx`
import { useEffect, useRef } from 'react'
import { listen } from '@tauri-apps/api/event'
import { StrudelEngine } from './audio/StrudelEngine'
function App() {
const strudelRef = useRef(null)
useEffect(() => {
// Initialize Strudel
const initStrudel = async () => {
if (!strudelRef.current) {
strudelRef.current = new StrudelEngine()
await strudelRef.current.init()
}
}
initStrudel()
// Listen for PatternEdit events from Rust
const unlisten = listen('pattern_edit', (event) => {
if (strudelRef.current) {
console.log('📨 Received PatternEdit:', event.payload)
strudelRef.current.applyEdit(event.payload)
}
})
return () => {
unlisten.then(fn => fn())
strudelRef.current?.stop()
}
}, [])
// ... rest of existing code
}---
Phase 3: Wire Conductor (Days 5-6)
3.1 Add Conductor to AppState
File: `apps/echelon-tauri/src-tauri/src/main.rs`
use cc_brain::{Conductor, ConductorConfig};
use std::sync::Arc;
use tokio::sync::Mutex as TokioMutex;
pub struct AppState {
pub latent_state: Arc<Mutex<LatentState>>,
pub cloud_client: Arc<CloudClient>,
pub sc_client: Option<SharedSuperColliderClient>,
pub style_index: Arc<AtomicU8>,
pub mode_index: Arc<AtomicU8>,
pub sc_enabled: bool,
pub conductor: Arc<TokioMutex<Conductor>>, // NEW
}
fn main() {
// ... existing setup ...
// Create conductor
let conductor_config = ConductorConfig {
tempo_bpm: 120.0,
param_cooldown_ms: 200,
event_cooldown_ms: 500,
section_cooldown_ms: 4000,
max_pending_edits: 20,
pattern_coder_url: None, // No ML model yet, use rules
};
let conductor = Arc::new(TokioMutex::new(
Conductor::new(conductor_config)
));
let app_state = AppState {
latent_state,
cloud_client: client,
sc_client,
style_index,
mode_index,
sc_enabled,
conductor, // NEW
};
// ... rest of main
}3.2 Add Conductor Update Loop
File: `apps/echelon-tauri/src-tauri/src/conductor_thread.rs` (new file)
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tauri::Window;
use cc_brain::{Conductor, LatentTrajectory};
use motion_bridge::LatentState;
pub fn spawn_conductor_thread(
conductor: Arc<Mutex<Conductor>>,
latent_rx: std::sync::mpsc::Receiver<LatentState>,
window: Window,
) {
tokio::spawn(async move {
println!("🎼 Conductor thread started");
loop {
// Receive latent state
match latent_rx.recv_timeout(Duration::from_millis(100)) {
Ok(latent) => {
// TODO: Convert LatentState → LatentTrajectory with rehearsal
// For now, just tick the conductor
let mut cond = conductor.lock().await;
cond.tick(std::time::Instant::now());
// Get any pending edits
if let Some(edits) = cond.get_pending_edits() {
for edit in edits {
// Send to frontend
if let Err(e) = window.emit("pattern_edit", &edit) {
eprintln!("Failed to emit edit: {}", e);
}
}
}
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
});
}---
Phase 4: Rule-Based Pattern Mapping (Day 7)
4.1 Implement Section → Pattern Rules
File: `cc-brain/src/conductor.rs` (extend existing)
impl Conductor {
/// Generate pattern edits based on current section and trajectory
pub fn generate_edits_from_section(&mut self) -> Vec<PatternEdit> {
let mut edits = Vec::new();
match self.section {
MusicSection::Intro => {
// Intro: sparse kick, minimal hats
edits.push(PatternEdit::SetParam {
name: "kick_gain".to_string(),
value: 0.6,
curve: None,
transition_beats: Some(4.0),
});
edits.push(PatternEdit::SetParam {
name: "hihat_density".to_string(),
value: 4.0,
curve: None,
transition_beats: Some(4.0),
});
}
MusicSection::Groove => {
// Groove: full kick, medium hats
edits.push(PatternEdit::SetParam {
name: "kick_gain".to_string(),
value: 0.8,
curve: None,
transition_beats: Some(2.0),
});
edits.push(PatternEdit::SetParam {
name: "hihat_density".to_string(),
value: 8.0,
curve: None,
transition_beats: Some(2.0),
});
}
MusicSection::Build => {
// Build: increase tension
edits.push(PatternEdit::SetParam {
name: "hihat_density".to_string(),
value: 16.0,
curve: Some("exponential".to_string()),
transition_beats: Some(8.0),
});
edits.push(PatternEdit::SetParam {
name: "bass_cutoff".to_string(),
value: 1200.0,
curve: Some("linear".to_string()),
transition_beats: Some(8.0),
});
}
MusicSection::Climax => {
// Climax: everything maxed
edits.push(PatternEdit::SetParam {
name: "kick_gain".to_string(),
value: 1.0,
curve: None,
transition_beats: Some(0.5),
});
edits.push(PatternEdit::ScheduleEvent {
beat_offset: 0.0,
action: "crash".to_string(),
intensity: Some(1.0),
});
}
MusicSection::Breakdown => {
// Breakdown: drop drums, emphasize pads
edits.push(PatternEdit::SetParam {
name: "kick_gain".to_string(),
value: 0.0,
curve: None,
transition_beats: Some(1.0),
});
edits.push(PatternEdit::SetParam {
name: "pad_reverb".to_string(),
value: 0.8,
curve: None,
transition_beats: Some(4.0),
});
}
MusicSection::Outro => {
// Outro: fade everything
edits.push(PatternEdit::SetParam {
name: "kick_gain".to_string(),
value: 0.3,
curve: Some("exponential".to_string()),
transition_beats: Some(16.0),
});
}
}
edits
}
}---
Phase 5: End-to-End Test (Days 8-10)
5.1 Test Flow
1. iOS Phone → WebSocket → cc-mcs (cloud/local)
2. cc-mcs → LatentState → echelon-tauri (Tauri backend)
3. echelon-tauri → Conductor.tick()
4. Conductor → PatternEdit (based on section rules)
5. Tauri → emit("pattern_edit")
6. React → StrudelEngine.applyEdit()
7. Strudel → Tone.js → Audio Output ✨5.2 Verification Checklist
- [ ] Run `echelon-tauri` with `--local-mcs`
- [ ] Start EchelonCapture iOS app, connect to local backend
- [ ] Dance with phone in hand
- [ ] Observe latent state updating in Tauri console
- [ ] Hear Strudel patterns change based on movement
- [ ] Check console for "📨 Received PatternEdit" messages
- [ ] Verify section transitions (Intro → Groove → Build)
---
Next Steps After Phase 5
Once basic integration works:
1. Add Rehearsal Integration (Week 2)
- Wire `RehearsalEngine` to predict future latent states
- Use trajectory shape to pre-schedule edits
2. Expand Pattern Vocabulary (Week 3)
- More complex Strudel patterns (polyrhythms, euclidean, etc.)
- Track-specific transforms (stutter, reverse, degrade)
- FX automation (filter sweeps, delay throws)
3. Optional: ML Pattern-Coder (Week 4+)
- Collect {trajectory, edit, body response} data
- Train small transformer for learned pattern generation
---
File Structure Summary
apps/desktop/cc-echelon/
├── apps/echelon-tauri/
│ ├── src/
│ │ ├── audio/
│ │ │ └── StrudelEngine.js # NEW - Strudel runtime wrapper
│ │ └── App.jsx # MODIFIED - listen for pattern_edit events
│ ├── src-tauri/src/
│ │ ├── main.rs # MODIFIED - add Conductor to AppState
│ │ ├── commands.rs # MODIFIED - add pattern_edit commands
│ │ └── conductor_thread.rs # NEW - conductor update loop
│ └── package.json # MODIFIED - add Strudel deps
└── crates/
└── cc-brain/src/
└── conductor.rs # MODIFIED - add section→pattern rules---
Timeline Estimate
- Days 1-2: Strudel setup + basic pattern playback
- Days 3-4: Rust→JS bridge via Tauri events
- Days 5-6: Conductor integration + state management
- Day 7: Section-based pattern rules
- Days 8-10: End-to-end testing + refinement
Total: 10 days (2 weeks)
---
Success Criteria
✅ Strudel playing audio in Tauri app
✅ PatternEdit commands flowing from Rust → JS
✅ Conductor advancing through sections
✅ Movement from iOS phone affects Strudel patterns
✅ Rule-based pattern changes feel musical
✅ No crashes, low latency (<50ms)
---
Notes
- Start with simple patterns (kick/hat/bass/pad)
- Focus on stability over complexity
- Test incrementally at each phase
- Keep existing audio engine as fallback
- SuperCollider integration remains independent
Promotion Decision
Attach run IDs, datasets, metrics, and reproduction commands.
Source Anchor
Comp-Core/core/audio-media/cc-echelon/STRUDEL_INTEGRATION_PLAN.md
Detected Structure
Method · Evaluation · Math · Figures · Code Anchors · Architecture