Grand Diomande Research · Full HTML Reader

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 ```

Embodied Trajectory Systems research note experiment writeup candidate score 42 .md

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

bash
cd apps/desktop/cc-echelon/apps/echelon-tauri
npm install @strudel.cycles/core @strudel.cycles/webaudio @strudel.cycles/tonal tone

1.2 Create StrudelEngine.js

File: `apps/echelon-tauri/src/audio/StrudelEngine.js`

javascript
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)

jsx
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`

rust
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`:

rust
// 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`

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`

rust
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)

rust
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)

rust
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

text
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