dirtydata_runtime/
freeze.rs1use 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
22pub 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 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 let mut render_graph = graph_utils::clone_subgraph(graph, &ancestors);
39
40 let sink_id = StableId::new();
42 let sink_node = Node::new_sink("FreezeCaptureSink");
43 render_graph.nodes.insert(sink_id, sink_node);
44
45 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 let mut renderer = OfflineRenderer::new(render_graph, sample_rate);
55 let audio_data = renderer.render(duration_secs);
56
57 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 let mut operations = Vec::new();
76
77 for &id in &ancestors {
79 operations.push(Operation::RemoveNode(id));
80 }
81
82 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 for edge in graph.edges.values() {
92 if ancestors.contains(&edge.source.node_id) && !ancestors.contains(&edge.target.node_id) {
95 let mut redirected_edge = edge.clone();
97 redirected_edge.id = StableId::new(); 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 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 let mut hasher = blake3::Hasher::new();
132 hasher.update(&graph.revision.0.to_le_bytes());
133 *hasher.finalize().as_bytes()
134 }
135}