Skip to main content

max / audiofiles

7.4 KB · 210 lines History Blame Raw
1 //! `DirectBackend` Sample Forge: chop and conform, in both the synchronous form
2 //! and the worker-backed form that streams progress back to the UI.
3
4 use super::{
5 BackendError, BackendResult, ChopMethod, ConformResult, ConformTarget, DirectBackend,
6 ForgeBackend, ForgeCommand, ForgePending, NodeId, VfsId, instrument,
7 };
8
9 impl ForgeBackend for DirectBackend {
10 // --- Sample Forge ---
11
12 #[instrument(skip_all)]
13 fn start_chop_preview(&self, hash: &str, ext: &str, method: &ChopMethod) -> BackendResult<()> {
14 // Resolve the source path under a brief lock; the decode + slice runs on
15 // the forge worker and the marks arrive via ForgeEvent::PreviewComplete.
16 let path = {
17 let db = self.db.lock();
18 audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?
19 };
20 // Reuse the forge worker if present (preview queues behind any running
21 // chop/conform); spawn one lazily otherwise. Preview is read-only and
22 // sets no forge_pending, so it never interferes with an import.
23 let mut guard = self.forge_worker.lock();
24 if guard.is_none() {
25 let handle = audiofiles_core::forge::spawn_forge_worker()
26 .map_err(|e| BackendError::Other(format!("failed to spawn forge worker: {e}")))?;
27 *guard = Some(handle);
28 }
29 let delivered = guard.as_ref().is_some_and(|w| {
30 w.send(ForgeCommand::Preview {
31 source_path: path,
32 method: method.clone(),
33 })
34 });
35 // Dead cached worker → drop it (next call respawns) and report Err so the
36 // caller doesn't set forge.busy and wedge the repaint guard / busy-guard.
37 if !delivered {
38 *guard = None;
39 return Err(BackendError::Other(
40 "forge worker is not running".to_string(),
41 ));
42 }
43 Ok(())
44 }
45
46 #[instrument(skip_all)]
47 fn chop_sample(
48 &self,
49 vfs_id: VfsId,
50 hash: &str,
51 ext: &str,
52 name: &str,
53 parent_id: Option<NodeId>,
54 method: &ChopMethod,
55 ) -> BackendResult<usize> {
56 let db = self.db.lock();
57 let path = audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?;
58 let result = audiofiles_core::forge::chop_to_vfs(
59 &self.store,
60 &db,
61 vfs_id,
62 &path,
63 name,
64 parent_id,
65 method,
66 )?;
67 Ok(result.slice_count)
68 }
69
70 #[instrument(skip_all)]
71 fn conform_sample(
72 &self,
73 vfs_id: VfsId,
74 hash: &str,
75 ext: &str,
76 name: &str,
77 parent_id: Option<NodeId>,
78 target: &ConformTarget,
79 ) -> BackendResult<ConformResult> {
80 let db = self.db.lock();
81 // Overshoot policy: trim to the ceiling only when the user has opted in;
82 // the default leaves the signal untouched and merely reports.
83 let auto_trim = db
84 .conn()
85 .query_row(
86 "SELECT value FROM user_config WHERE key = ?1",
87 [crate::backend::FORGE_AUTO_TRIM_OVERSHOOT_KEY],
88 |row| row.get::<_, String>(0),
89 )
90 .ok()
91 .is_some_and(|v| v == "true");
92 let path = audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?;
93 Ok(audiofiles_core::forge::conform_to_vfs(
94 &self.store,
95 &db,
96 vfs_id,
97 &path,
98 name,
99 parent_id,
100 target,
101 auto_trim,
102 )?)
103 }
104
105 #[instrument(skip_all)]
106 fn start_chop(
107 &self,
108 vfs_id: VfsId,
109 hash: &str,
110 ext: &str,
111 name: &str,
112 parent_id: Option<NodeId>,
113 method: &ChopMethod,
114 ) -> BackendResult<()> {
115 // Resolve the source path under a brief lock; the heavy work runs on the
116 // worker, and the import lands when ForgeEvent::ChopComplete is drained.
117 let path = {
118 let db = self.db.lock();
119 audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?
120 };
121
122 // Reuse the persistent worker. Sending Cancel sets the cancel flag so any
123 // in-flight job bails cooperatively; the queued Chop then runs. Dropping
124 // the old handle here would join its thread ON the GUI thread, a visible
125 // frame stall on a long in-flight job. The worker resets the flag when it
126 // dequeues the new command.
127 {
128 let mut guard = self.forge_worker.lock();
129 if guard.is_none() {
130 *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| {
131 BackendError::Other(format!("failed to spawn forge worker: {e}"))
132 })?);
133 }
134 let handle = guard.as_ref().expect("forge worker present");
135 let _ = handle.send(ForgeCommand::Cancel);
136 let delivered = handle.send(ForgeCommand::Chop {
137 source_path: path,
138 base_name: name.to_string(),
139 method: method.clone(),
140 });
141 if !delivered {
142 *guard = None;
143 return Err(BackendError::Other(
144 "forge worker is not running".to_string(),
145 ));
146 }
147 }
148 *self.forge_pending.lock() = Some(ForgePending::Chop { vfs_id, parent_id });
149 Ok(())
150 }
151
152 #[instrument(skip_all)]
153 fn start_conform(
154 &self,
155 vfs_id: VfsId,
156 hash: &str,
157 ext: &str,
158 name: &str,
159 parent_id: Option<NodeId>,
160 target: &ConformTarget,
161 ) -> BackendResult<()> {
162 let (path, auto_trim) = {
163 let db = self.db.lock();
164 let auto_trim = db
165 .conn()
166 .query_row(
167 "SELECT value FROM user_config WHERE key = ?1",
168 [crate::backend::FORGE_AUTO_TRIM_OVERSHOOT_KEY],
169 |row| row.get::<_, String>(0),
170 )
171 .ok()
172 .is_some_and(|v| v == "true");
173 let path = audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?;
174 (path, auto_trim)
175 };
176
177 // Reuse the persistent worker (see start_chop): Cancel + queue, never
178 // drop+join on the GUI thread.
179 {
180 let mut guard = self.forge_worker.lock();
181 if guard.is_none() {
182 *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| {
183 BackendError::Other(format!("failed to spawn forge worker: {e}"))
184 })?);
185 }
186 let handle = guard.as_ref().expect("forge worker present");
187 let _ = handle.send(ForgeCommand::Cancel);
188 let delivered = handle.send(ForgeCommand::Conform {
189 source_path: path,
190 base_name: name.to_string(),
191 target: target.clone(),
192 auto_trim,
193 });
194 if !delivered {
195 *guard = None;
196 return Err(BackendError::Other(
197 "forge worker is not running".to_string(),
198 ));
199 }
200 }
201 *self.forge_pending.lock() = Some(ForgePending::Conform {
202 vfs_id,
203 parent_id,
204 sample_rate: target.sample_rate,
205 bit_depth: target.bit_depth,
206 });
207 Ok(())
208 }
209 }
210