use std::path::Path;
use hound::{WavWriter, WavSpec};
use dirtydata_core::ir::{Graph, Node, Edge};
use dirtydata_core::types::{StableId, ConfigValue, PortRef};
use dirtydata_core::patch::{Operation, Patch};
use dirtydata_core::graph_utils;
use crate::offline::OfflineRenderer;
#[derive(Debug, thiserror::Error)]
pub enum FreezeError {
#[error("Node not found: {0}")]
NodeNotFound(StableId),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Hound error: {0}")]
Hound(#[from] hound::Error),
#[error("Patch error: {0}")]
Patch(#[from] dirtydata_core::patch::PatchError),
}
pub fn freeze_node(
graph: &Graph,
target_node_id: StableId,
duration_secs: f32,
sample_rate: f32,
asset_path: &Path,
) -> Result<Patch, FreezeError> {
let ancestors = graph_utils::get_upstream_nodes(graph, target_node_id);
if ancestors.is_empty() {
return Err(FreezeError::NodeNotFound(target_node_id));
}
let mut render_graph = graph_utils::clone_subgraph(graph, &ancestors);
let sink_id = StableId::new();
let sink_node = Node::new_sink("FreezeCaptureSink");
render_graph.nodes.insert(sink_id, sink_node);
let edge = Edge::new(
PortRef { node_id: target_node_id, port_name: "out".into() },
PortRef { node_id: sink_id, port_name: "in".into() }
);
render_graph.edges.insert(edge.id, edge);
let mut renderer = OfflineRenderer::new(render_graph, sample_rate);
let audio_data = renderer.render(duration_secs);
if let Some(parent) = asset_path.parent() {
std::fs::create_dir_all(parent)?;
}
let spec = WavSpec {
channels: 2,
sample_rate: sample_rate as u32,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let mut writer = WavWriter::create(asset_path, spec)?;
for &sample in &audio_data {
writer.write_sample(sample)?;
}
writer.finalize()?;
let mut operations = Vec::new();
for &id in &ancestors {
operations.push(Operation::RemoveNode(id));
}
let asset_node_id = StableId::new();
let mut asset_node = Node::new_source("FrozenAsset");
asset_node.config.insert("path".into(), ConfigValue::String(asset_path.to_string_lossy().into()));
asset_node.config.insert("name".into(), ConfigValue::String(format!("Frozen_{}", target_node_id.to_string()[..4].to_string())));
operations.push(Operation::AddNode(asset_node));
for edge in graph.edges.values() {
if ancestors.contains(&edge.source.node_id) && !ancestors.contains(&edge.target.node_id) {
let mut redirected_edge = edge.clone();
redirected_edge.id = StableId::new(); redirected_edge.source = PortRef {
node_id: asset_node_id,
port_name: "out".into(),
};
operations.push(Operation::AddEdge(redirected_edge));
}
}
Ok(Patch::from_operations(operations))
}
pub struct DifferentialCache {
entries: std::collections::HashMap<[u8; 32], std::path::PathBuf>,
}
impl DifferentialCache {
pub fn new() -> Self {
Self { entries: std::collections::HashMap::new() }
}
pub fn get_cached_asset(&self, graph: &Graph) -> Option<std::path::PathBuf> {
let hash = self.compute_graph_hash(graph);
self.entries.get(&hash).cloned()
}
pub fn insert(&mut self, graph: &Graph, path: std::path::PathBuf) {
let hash = self.compute_graph_hash(graph);
self.entries.insert(hash, path);
}
fn compute_graph_hash(&self, graph: &Graph) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(&graph.revision.0.to_le_bytes());
*hasher.finalize().as_bytes()
}
}