Skip to main content

dirtydata_runtime/
freeze.rs

1use std::path::Path;
2use hound::{WavWriter, WavSpec};
3
4use dirtydata_core::ir::{Graph, Node, Edge};
5use dirtydata_core::types::{StableId, ConfigValue, PortRef};
6use dirtydata_core::patch::{Operation, Patch};
7use dirtydata_core::graph_utils;
8use crate::offline::OfflineRenderer;
9
10#[derive(Debug, thiserror::Error)]
11pub enum FreezeError {
12    #[error("Node not found: {0}")]
13    NodeNotFound(StableId),
14    #[error("IO error: {0}")]
15    Io(#[from] std::io::Error),
16    #[error("Hound error: {0}")]
17    Hound(#[from] hound::Error),
18    #[error("Patch error: {0}")]
19    Patch(#[from] dirtydata_core::patch::PatchError),
20}
21
22/// Freezes a node and its upstream dependencies into a WAV asset and returns a Patch 
23/// to replace the subgraph with an AssetReaderNode.
24pub fn freeze_node(
25    graph: &Graph,
26    target_node_id: StableId,
27    duration_secs: f32,
28    sample_rate: f32,
29    asset_path: &Path,
30) -> Result<Patch, FreezeError> {
31    // 1. Identify all nodes that need to be frozen (target + its ancestors)
32    let ancestors = graph_utils::get_upstream_nodes(graph, target_node_id);
33    if ancestors.is_empty() {
34        return Err(FreezeError::NodeNotFound(target_node_id));
35    }
36
37    // 2. Clone the minimal subgraph for rendering
38    let mut render_graph = graph_utils::clone_subgraph(graph, &ancestors);
39
40    // 3. Add a temporary Sink node to capture target_node's output
41    let sink_id = StableId::new();
42    let sink_node = Node::new_sink("FreezeCaptureSink");
43    render_graph.nodes.insert(sink_id, sink_node);
44
45    // 4. Connect target_node to the capture Sink
46    // Assume port "out" exists on target (standard for processors/sources)
47    let edge = Edge::new(
48        PortRef { node_id: target_node_id, port_name: "out".into() },
49        PortRef { node_id: sink_id, port_name: "in".into() }
50    );
51    render_graph.edges.insert(edge.id, edge);
52
53    // 5. Perform offline rendering
54    let mut renderer = OfflineRenderer::new(render_graph, sample_rate);
55    let audio_data = renderer.render(duration_secs);
56
57    // 6. Save results to WAV file
58    if let Some(parent) = asset_path.parent() {
59        std::fs::create_dir_all(parent)?;
60    }
61    
62    let spec = WavSpec {
63        channels: 2,
64        sample_rate: sample_rate as u32,
65        bits_per_sample: 32,
66        sample_format: hound::SampleFormat::Float,
67    };
68    let mut writer = WavWriter::create(asset_path, spec)?;
69    for &sample in &audio_data {
70        writer.write_sample(sample)?;
71    }
72    writer.finalize()?;
73
74    // 7. Construct Patch to transform the original graph
75    let mut operations = Vec::new();
76
77    // A. Remove all frozen nodes (upstream cascade will handle most edges)
78    for &id in &ancestors {
79        operations.push(Operation::RemoveNode(id));
80    }
81
82    // B. Add the replacement AssetReaderNode
83    let asset_node_id = StableId::new();
84    let mut asset_node = Node::new_source("FrozenAsset");
85    asset_node.config.insert("path".into(), ConfigValue::String(asset_path.to_string_lossy().into()));
86    asset_node.config.insert("name".into(), ConfigValue::String(format!("Frozen_{}", target_node_id.to_string()[..4].to_string())));
87    operations.push(Operation::AddNode(asset_node));
88
89    // C. Reconnect downstream consumers
90    // We need to find edges in the original graph that were consuming target_node's output
91    for edge in graph.edges.values() {
92        // If the source was the target_node or one of its ancestors being removed...
93        // AND the target is NOT one of the ancestors being removed...
94        if ancestors.contains(&edge.source.node_id) && !ancestors.contains(&edge.target.node_id) {
95            // Re-route this connection to come from our new FrozenAsset node
96            let mut redirected_edge = edge.clone();
97            redirected_edge.id = StableId::new(); // New ID for the new edge
98            redirected_edge.source = PortRef {
99                node_id: asset_node_id,
100                port_name: "out".into(),
101            };
102            operations.push(Operation::AddEdge(redirected_edge));
103        }
104    }
105
106    Ok(Patch::from_operations(operations))
107}
108
109pub struct DifferentialCache {
110    /// Maps subgraph hash (blake3) to WAV asset path
111    entries: std::collections::HashMap<[u8; 32], std::path::PathBuf>,
112}
113
114impl DifferentialCache {
115    pub fn new() -> Self {
116        Self { entries: std::collections::HashMap::new() }
117    }
118
119    pub fn get_cached_asset(&self, graph: &Graph) -> Option<std::path::PathBuf> {
120        let hash = self.compute_graph_hash(graph);
121        self.entries.get(&hash).cloned()
122    }
123
124    pub fn insert(&mut self, graph: &Graph, path: std::path::PathBuf) {
125        let hash = self.compute_graph_hash(graph);
126        self.entries.insert(hash, path);
127    }
128
129    fn compute_graph_hash(&self, graph: &Graph) -> [u8; 32] {
130        // In a real implementation, this would walk the graph and hash nodes/edges
131        let mut hasher = blake3::Hasher::new();
132        hasher.update(&graph.revision.0.to_le_bytes());
133        *hasher.finalize().as_bytes()
134    }
135}