1use std::path::PathBuf;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::mpsc::{self, Receiver, RecvError, RecvTimeoutError, Sender, TryRecvError};
24use std::thread::{self, JoinHandle};
25use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
26
27use midir::{Ignore, MidiInput, MidiInputConnection, MidiOutput, MidiOutputConnection};
28
29#[cfg(feature = "ble")]
30use crate::ble::{self, Ble};
31use crate::capture::{self, Entry};
32use crate::catalog::Module;
33use crate::midi::{Msg, hex, host, req};
34use crate::queue::{RequestQueue, follow_up, handshake};
35use crate::records::{Preset, Settings};
36use crate::upload::{self, UploadError, UserAmpInfo};
37use crate::{Assembler, Origin, is_ble_port, is_stomp_port};
38
39#[derive(Clone, Debug)]
41pub struct Config {
42 pub client_name: String,
44 pub auto_connect: bool,
47 pub sync: bool,
49 pub reply_timeout: Duration,
51 pub scan_interval: Duration,
53 pub bluetooth: bool,
55}
56
57impl Default for Config {
58 fn default() -> Self {
59 Config {
60 client_name: "Open Stomp".into(),
61 auto_connect: true,
62 sync: true,
63 reply_timeout: Duration::from_secs(1),
64 scan_interval: Duration::from_millis(1500),
65 bluetooth: true,
66 }
67 }
68}
69
70pub enum Command {
71 Connect(String),
73 Disconnect,
74 Resync,
76 SetSync(bool),
78 Send(Vec<u8>),
80 Write(Vec<Vec<u8>>),
82 Replay { path: PathBuf, speed: f32 },
84 StopReplay,
85}
86
87#[derive(Clone, Debug, PartialEq)]
88pub enum Status {
89 Disconnected,
90 Connected {
91 port: String,
92 output: bool,
94 },
95 Replaying {
96 file: String,
97 progress: f32,
98 },
99}
100
101#[derive(Clone, Debug, PartialEq)]
102pub enum Update {
103 Ports(Vec<String>),
105 Status(Status),
106 Sync { done: usize, total: usize },
108 Message { at: SystemTime, origin: Origin, bytes: Vec<u8> },
110 Mark { at: SystemTime, text: String },
112 Info { at: SystemTime, text: String, error: bool },
114 Reset,
116}
117
118enum Internal {
119 Cmd(Command),
120 Input(Vec<u8>),
121 #[cfg(feature = "ble")]
122 Ble(ble::Event),
123 Shutdown,
124}
125
126type Waker = Arc<dyn Fn() + Send + Sync>;
127
128pub struct Device {
130 tx: Sender<Internal>,
131 rx: Receiver<Update>,
132 thread: Option<JoinHandle<()>>,
133}
134
135impl Device {
136 pub fn spawn(config: Config) -> Device {
138 Device::spawn_with_waker(config, || {})
139 }
140
141 pub fn spawn_with_waker(config: Config, wake: impl Fn() + Send + Sync + 'static) -> Device {
143 let (tx, rx_internal) = mpsc::channel();
144 let (tx_updates, rx) = mpsc::channel();
145 let engine_tx = tx.clone();
146 let wake: Waker = Arc::new(wake);
147 let thread = thread::Builder::new()
148 .name("open-stomp-engine".into())
149 .spawn(move || Engine::new(config, tx_updates, engine_tx, wake).run(rx_internal))
150 .expect("spawn engine thread");
151 Device { tx, rx, thread: Some(thread) }
152 }
153
154 pub fn command(&self, c: Command) {
155 let _ = self.tx.send(Internal::Cmd(c));
156 }
157
158 pub fn try_recv(&self) -> Result<Update, TryRecvError> {
159 self.rx.try_recv()
160 }
161
162 pub fn recv(&self) -> Result<Update, RecvError> {
163 self.rx.recv()
164 }
165
166 pub fn recv_timeout(&self, timeout: Duration) -> Result<Update, RecvTimeoutError> {
167 self.rx.recv_timeout(timeout)
168 }
169
170 pub fn drain(&self) -> impl Iterator<Item = Update> + '_ {
172 self.rx.try_iter()
173 }
174
175 pub fn connect(&self, port: impl Into<String>) {
176 self.command(Command::Connect(port.into()));
177 }
178
179 pub fn disconnect(&self) {
180 self.command(Command::Disconnect);
181 }
182
183 pub fn resync(&self) {
184 self.command(Command::Resync);
185 }
186
187 pub fn set_sync(&self, on: bool) {
188 self.command(Command::SetSync(on));
189 }
190
191 pub fn replay(&self, path: impl Into<PathBuf>, speed: f32) {
192 self.command(Command::Replay { path: path.into(), speed });
193 }
194
195 pub fn stop_replay(&self) {
196 self.command(Command::StopReplay);
197 }
198
199 pub fn send(&self, bytes: Vec<u8>) {
201 self.command(Command::Send(bytes));
202 }
203
204 pub fn write(&self, msgs: Vec<Vec<u8>>) {
206 self.command(Command::Write(msgs));
207 }
208
209 pub fn select_preset(&self, index: u8) {
211 self.send(host::program(index));
212 }
213
214 pub fn set_head(&self, m: Module, model: u8, on: bool) {
216 if let Some(b) = host::head(m, model, on) {
217 self.send(b);
218 }
219 }
220
221 pub fn set_param(&self, m: Module, slot: usize, value: u8) {
223 if let Some(b) = host::param(m, slot, value) {
224 self.send(b);
225 }
226 }
227
228 pub fn set_volume(&self, value: u8) {
229 self.send(host::volume(value));
230 }
231
232 pub fn show_page(&self, m: Module) {
234 self.send(host::page(m));
235 }
236
237 pub fn looper_button(&self, button: u8) {
239 self.send(host::looper_button(button));
240 }
241
242 pub fn save_preset(&self, source_slot: u8, preset: &Preset) {
245 self.write(host::save_preset(source_slot, preset));
246 }
247
248 pub fn import_user_amp(&self, slot: u8, name: &str, data: &[f32], factor: f32) -> Result<(), UploadError> {
252 upload::check(slot, data)?;
253 if let Some(b) = host::head(Module::Amp, upload::USER_MODEL_BASE + slot, true) {
254 self.send(b);
255 }
256 self.write(upload::user_amp_upload(&UserAmpInfo::new(slot, name, factor), data));
257 Ok(())
258 }
259
260 #[cfg(feature = "nam")]
268 pub fn import_nam_file(
269 &self,
270 slot: u8,
271 path: impl AsRef<std::path::Path>,
272 ) -> Result<crate::nam::Converted, Box<dyn std::error::Error + Send + Sync>> {
273 self.import_nam_file_as(slot, path, crate::nam::convert::Measure::Optimised, true)
274 }
275
276 #[cfg(feature = "nam")]
281 pub fn import_nam_file_with(
282 &self,
283 slot: u8,
284 path: impl AsRef<std::path::Path>,
285 measure: crate::nam::convert::Measure,
286 ) -> Result<crate::nam::Converted, Box<dyn std::error::Error + Send + Sync>> {
287 self.import_nam_file_as(slot, path, measure, false)
288 }
289
290 #[cfg(feature = "nam")]
294 pub fn import_nam_file_as(
295 &self,
296 slot: u8,
297 path: impl AsRef<std::path::Path>,
298 measure: crate::nam::convert::Measure,
299 normalise: bool,
300 ) -> Result<crate::nam::Converted, Box<dyn std::error::Error + Send + Sync>> {
301 let path = path.as_ref();
302 if slot >= upload::USER_SLOTS {
303 return Err(Box::new(UploadError::Slot(slot)));
304 }
305 let mut model = crate::nam::Model::load(path)?;
306 let mut converted =
307 crate::nam::convert::convert_measured(&mut model, &crate::nam::convert::excitation(), measure);
308 if normalise {
309 crate::nam::convert::normalise_level(&mut converted, &mut model);
310 }
311 let name = path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
312 self.import_user_amp(slot, &name, &converted.data, converted.factor)?;
313 Ok(converted)
314 }
315
316 pub fn delete_user_amp(&self, slot: u8) -> Result<(), UploadError> {
318 if slot >= upload::USER_SLOTS {
319 return Err(UploadError::Slot(slot));
320 }
321 self.write(vec![upload::user_amp_delete(slot), req::user_amp(slot)]);
322 Ok(())
323 }
324
325 pub fn rename_user_amp(&self, slot: u8, name: &str) -> Result<(), UploadError> {
330 if slot >= upload::USER_SLOTS {
331 return Err(UploadError::Slot(slot));
332 }
333 self.write(vec![UserAmpInfo::new(slot, name, 1.0).write_frame(), req::user_amp(slot)]);
334 Ok(())
335 }
336
337 pub fn import_user_ir(&self, slot: u8, name: &str, data: &[f32]) -> Result<(), UploadError> {
341 upload::check(slot, data)?;
342 self.send(host::page(Module::Cab));
343 if let Some(b) = host::head(Module::Cab, upload::USER_MODEL_BASE + slot, true) {
344 self.send(b);
345 }
346 self.write(upload::user_ir_upload(slot, name, data));
347 Ok(())
348 }
349
350 pub fn rename_user_ir(&self, slot: u8, name: &str) -> Result<(), UploadError> {
355 if slot >= upload::USER_SLOTS {
356 return Err(UploadError::Slot(slot));
357 }
358 self.write(vec![upload::user_ir_name(slot, name), req::ir_slot(slot)]);
359 Ok(())
360 }
361
362 pub fn import_ir_file(
369 &self,
370 slot: u8,
371 path: impl AsRef<std::path::Path>,
372 ) -> Result<crate::ir::ConvertedIr, Box<dyn std::error::Error + Send + Sync>> {
373 #[cfg(feature = "nam")]
374 let method = crate::ir::Method::Optimised;
375 #[cfg(not(feature = "nam"))]
376 let method = crate::ir::Method::App;
377 self.import_ir_file_with(slot, path, method)
378 }
379
380 pub fn import_ir_file_with(
382 &self,
383 slot: u8,
384 path: impl AsRef<std::path::Path>,
385 method: crate::ir::Method,
386 ) -> Result<crate::ir::ConvertedIr, Box<dyn std::error::Error + Send + Sync>> {
387 let path = path.as_ref();
388 if slot >= upload::USER_SLOTS {
389 return Err(Box::new(UploadError::Slot(slot)));
390 }
391 let converted = crate::ir::convert_with(&crate::ir::Wav::load(path)?, method);
392 self.import_user_ir(slot, &crate::ir::slot_name(path), &converted.data)?;
393 Ok(converted)
394 }
395
396 pub fn write_settings(&self, settings: &Settings) {
398 self.write(vec![host::settings_write(settings)]);
399 }
400}
401
402impl Drop for Device {
403 fn drop(&mut self) {
404 let _ = self.tx.send(Internal::Shutdown);
405 if let Some(t) = self.thread.take() {
406 let _ = t.join();
407 }
408 }
409}
410
411struct Connection {
412 port: String,
413 link: Link,
414}
415
416enum Link {
417 Midi {
418 _input: MidiInputConnection<()>,
419 output: Option<MidiOutputConnection>,
420 },
421 #[cfg(feature = "ble")]
423 Ble,
424}
425
426struct Engine {
427 config: Config,
428 out: Sender<Update>,
429 self_tx: Sender<Internal>,
430 wake: Waker,
431 scanner: Option<MidiInput>,
432 ports: Vec<String>,
433 last_scan: Option<Instant>,
434 conn: Option<Connection>,
435 replay: Option<Arc<AtomicBool>>,
436 asm: Assembler,
437 queue: RequestQueue,
438 #[cfg(feature = "ble")]
439 ble: Option<Ble>,
440 #[cfg(feature = "ble")]
442 ble_ports: Vec<String>,
443}
444
445impl Engine {
446 fn new(config: Config, out: Sender<Update>, self_tx: Sender<Internal>, wake: Waker) -> Self {
447 let scanner = MidiInput::new(&format!("{} scan", config.client_name)).ok();
448 let queue = RequestQueue::new(config.reply_timeout);
449 #[cfg(feature = "ble")]
450 let ble = config.bluetooth.then(|| Ble::spawn(ble::forward(self_tx.clone(), Internal::Ble)));
451 Engine {
452 config,
453 out,
454 self_tx,
455 wake,
456 scanner,
457 ports: vec![],
458 last_scan: None,
459 conn: None,
460 replay: None,
461 asm: Assembler::default(),
462 queue,
463 #[cfg(feature = "ble")]
464 ble,
465 #[cfg(feature = "ble")]
466 ble_ports: vec![],
467 }
468 }
469
470 fn emit(&self, u: Update) {
471 let _ = self.out.send(u);
472 (self.wake)();
473 }
474
475 fn info(&self, text: impl Into<String>, error: bool) {
476 self.emit(Update::Info { at: SystemTime::now(), text: text.into(), error });
477 }
478
479 fn emit_progress(&self) {
480 let (done, total) = self.queue.progress();
481 self.emit(Update::Sync { done, total });
482 }
483
484 fn run(mut self, rx: Receiver<Internal>) {
485 if self.scanner.is_none() {
486 self.info("MIDI is unavailable on this system (could not create a MIDI client)", true);
487 }
488 loop {
489 match rx.recv_timeout(Duration::from_millis(50)) {
490 Ok(Internal::Cmd(c)) => self.command(c),
491 Ok(Internal::Input(data)) => self.input(&data),
492 #[cfg(feature = "ble")]
493 Ok(Internal::Ble(e)) => self.ble_event(e),
494 Ok(Internal::Shutdown) | Err(RecvTimeoutError::Disconnected) => break,
495 Err(RecvTimeoutError::Timeout) => {}
496 }
497 self.pump();
498 #[cfg(target_os = "macos")]
499 run_loop_tick();
500 if self.last_scan.is_none_or(|t| t.elapsed() > self.config.scan_interval) {
501 self.scan();
502 }
503 }
504 self.stop_replay();
505 self.conn = None;
506 }
507
508 fn command(&mut self, c: Command) {
509 match c {
510 Command::Connect(port) => {
511 self.stop_replay();
512 self.config.auto_connect = true;
513 self.connect(&port);
514 }
515 Command::Disconnect => {
516 self.config.auto_connect = false;
517 self.disconnect("Disconnected");
518 }
519 Command::Resync => {
520 if self.conn.is_some() {
521 self.emit(Update::Reset);
522 self.start_sync();
523 }
524 }
525 Command::SetSync(on) => {
526 self.config.sync = on;
527 if !on {
528 self.queue.clear();
529 self.emit_progress();
530 }
531 }
532 Command::Send(bytes) => {
533 if self.writable() {
534 self.transmit(&bytes);
535 }
536 }
537 Command::Write(msgs) => {
538 if self.writable() {
539 for bytes in msgs {
540 self.queue.write(bytes);
541 }
542 }
543 }
544 Command::Replay { path, speed } => {
545 self.config.auto_connect = false;
546 self.disconnect("Disconnected for replay");
547 self.start_replay(path, speed);
548 }
549 Command::StopReplay => {
550 if self.stop_replay() {
551 self.emit(Update::Status(Status::Disconnected));
552 }
553 }
554 }
555 }
556
557 fn scan(&mut self) {
558 self.last_scan = Some(Instant::now());
559 let mut ports: Vec<String> = match &self.scanner {
560 Some(s) => s.ports().iter().filter_map(|p| s.port_name(p).ok()).collect(),
561 None => vec![],
562 };
563 #[cfg(feature = "ble")]
564 ports.extend(self.ble_ports.iter().cloned());
565 if ports != self.ports {
566 self.ports = ports.clone();
567 self.emit(Update::Ports(ports.clone()));
568 }
569 if let Some(c) = &self.conn
570 && !ports.contains(&c.port)
571 {
572 self.disconnect("Pedal unplugged");
573 }
574 if self.conn.is_none()
575 && self.replay.is_none()
576 && self.config.auto_connect
577 && let Some(p) = ports
578 .iter()
579 .find(|p| is_stomp_port(p) && !is_ble_port(p))
580 .or_else(|| ports.iter().find(|p| is_stomp_port(p)))
581 {
582 self.connect(&p.clone());
583 }
584 if self.config.auto_connect
586 && self.conn.as_ref().is_some_and(|c| is_ble_port(&c.port))
587 && let Some(p) = ports.iter().find(|p| is_stomp_port(p) && !is_ble_port(p))
588 {
589 self.connect(&p.clone());
590 }
591 }
592
593 #[cfg(feature = "ble")]
594 fn ble_event(&mut self, e: ble::Event) {
595 match e {
596 ble::Event::Ports(p) => {
597 self.ble_ports = p;
598 self.scan();
599 }
600 ble::Event::Input(bytes) => {
601 if self.conn.as_ref().is_some_and(|c| matches!(c.link, Link::Ble)) {
602 self.input(&bytes);
603 }
604 }
605 ble::Event::Lost => {
606 if self.conn.as_ref().is_some_and(|c| matches!(c.link, Link::Ble)) {
607 self.disconnect("Bluetooth connection lost");
608 }
609 }
610 ble::Event::Unavailable(why) => {
611 self.ble = None;
612 self.info(format!("Bluetooth is unavailable: {why}"), false);
613 }
614 }
615 }
616
617 fn connect(&mut self, port_name: &str) {
618 self.disconnect_quiet();
619 #[cfg(feature = "ble")]
620 if is_ble_port(port_name)
621 && let Some(ble) = &self.ble
622 {
623 self.info(format!("Connecting to {port_name} …"), false);
624 match ble.connect(port_name) {
625 Ok(()) => self.connected(Connection { port: port_name.to_owned(), link: Link::Ble }),
626 Err(e) => self.info(format!("Could not connect: {e}"), true),
627 }
628 return;
629 }
630 let result = (|| -> Result<Connection, String> {
631 let mut input = MidiInput::new(&self.config.client_name).map_err(|e| e.to_string())?;
632 input.ignore(Ignore::None);
633 let port = input
634 .ports()
635 .into_iter()
636 .find(|p| input.port_name(p).as_deref() == Ok(port_name))
637 .ok_or_else(|| format!("port “{port_name}” not found"))?;
638 let tx = self.self_tx.clone();
639 let input = input
640 .connect(
641 &port,
642 "stomp-in",
643 move |_ts, data, _| {
644 let _ = tx.send(Internal::Input(data.to_vec()));
645 },
646 (),
647 )
648 .map_err(|e| e.to_string())?;
649 let output = open_output(&self.config.client_name, port_name);
650 Ok(Connection { port: port_name.to_owned(), link: Link::Midi { _input: input, output } })
651 })();
652 match result {
653 Ok(c) => self.connected(c),
654 Err(e) => self.info(format!("Could not connect: {e}"), true),
655 }
656 }
657
658 fn connected(&mut self, c: Connection) {
659 let port = c.port.clone();
660 self.conn = Some(c);
661 let output = self.has_output();
662 self.emit(Update::Reset);
663 self.emit(Update::Status(Status::Connected { port: port.clone(), output }));
664 self.info(format!("Connected to {port}"), false);
665 if !output {
666 self.info("No MIDI output to the pedal: listening only", true);
667 }
668 self.start_sync();
669 }
670
671 fn disconnect_quiet(&mut self) {
672 #[cfg(feature = "ble")]
673 if let Some(Connection { link: Link::Ble, .. }) = self.conn
674 && let Some(ble) = &self.ble
675 {
676 ble.disconnect();
677 }
678 self.conn = None;
679 self.queue.clear();
680 self.asm = Assembler::default();
681 }
682
683 fn disconnect(&mut self, why: &str) {
684 if self.conn.is_some() {
685 self.disconnect_quiet();
686 self.emit(Update::Status(Status::Disconnected));
687 self.emit_progress();
688 self.info(why, false);
689 }
690 }
691
692 fn has_output(&self) -> bool {
693 self.conn.as_ref().is_some_and(|c| match &c.link {
694 Link::Midi { output, .. } => output.is_some(),
695 #[cfg(feature = "ble")]
696 Link::Ble => true,
697 })
698 }
699
700 fn start_sync(&mut self) {
701 self.queue.clear();
702 if !self.config.sync || !self.has_output() {
703 return;
704 }
705 self.queue.start_sync(handshake());
706 self.emit_progress();
707 }
708
709 fn writable(&self) -> bool {
711 let ok = self.config.sync && self.has_output();
712 if !ok {
713 self.info("Not sent: listen-only mode, or no MIDI output to the pedal", true);
714 }
715 ok
716 }
717
718 fn transmit(&mut self, bytes: &[u8]) -> bool {
720 let sent = match self.conn.as_mut().map(|c| &mut c.link) {
721 Some(Link::Midi { output: Some(out), .. }) => out.send(bytes).map_err(|e| e.to_string()),
722 #[cfg(feature = "ble")]
723 Some(Link::Ble) => match &self.ble {
724 Some(ble) if ble.send(vec![bytes.to_vec()]) => Ok(()),
725 _ => Err("Bluetooth stopped".into()),
726 },
727 _ => return false,
728 };
729 if let Err(e) = sent {
730 self.info(format!("Send failed: {e}"), true);
731 return false;
732 }
733 self.emit(Update::Message { at: SystemTime::now(), origin: Origin::Host, bytes: bytes.to_vec() });
734 true
735 }
736
737 fn pump(&mut self) {
739 if let Some(done) = self.queue.expire(Instant::now()) {
740 let b = done.request.bytes();
741 self.info(format!("No reply from pedal to {} …", hex(&b[..b.len().min(8)])), true);
742 if done.request.is_sync() {
743 self.emit_progress();
744 }
745 }
746 while let Some(r) = self.queue.pop() {
747 if !self.has_output() {
748 self.queue.clear();
749 return;
750 }
751 if !self.transmit(r.bytes()) {
752 return;
753 }
754 self.queue.sent(r, Instant::now());
755 }
756 }
757
758 fn input(&mut self, data: &[u8]) {
759 if self.conn.is_none() {
760 return;
761 }
762 let at = SystemTime::now();
763 for bytes in self.asm.push(data) {
764 let msg = Msg::parse(&bytes);
765 if let Some(done) = self.queue.on_message(&msg)
766 && done.request.is_sync()
767 {
768 self.emit_progress();
769 }
770 if self.config.sync {
771 for r in follow_up(&msg) {
772 self.queue.read(r);
773 }
774 }
775 self.emit(Update::Message { at, origin: Origin::Pedal, bytes });
776 }
777 }
778
779 fn start_replay(&mut self, path: PathBuf, speed: f32) {
780 self.stop_replay();
781 let text = match std::fs::read_to_string(&path) {
782 Ok(t) => t,
783 Err(e) => {
784 self.info(format!("Could not read {}: {e}", path.display()), true);
785 return;
786 }
787 };
788 let entries = capture::parse(&text);
789 let file = path.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
790 let stop = Arc::new(AtomicBool::new(false));
791 self.replay = Some(stop.clone());
792 self.emit(Update::Reset);
793 self.emit(Update::Status(Status::Replaying { file: file.clone(), progress: 0.0 }));
794 self.info(format!("Replaying {file} ({} messages) at {speed}×", entries.len()), false);
795 let (out, wake) = (self.out.clone(), self.wake.clone());
796 thread::spawn(move || replay(entries, file, speed, stop, out, wake));
797 }
798
799 fn stop_replay(&mut self) -> bool {
801 match self.replay.take() {
802 Some(stop) => {
803 stop.store(true, Ordering::Relaxed);
804 true
805 }
806 None => false,
807 }
808 }
809}
810
811#[cfg(target_os = "macos")]
815fn run_loop_tick() {
816 use core_foundation_sys::runloop::{CFRunLoopRunInMode, kCFRunLoopDefaultMode};
817 unsafe {
819 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, 1);
820 }
821}
822
823fn open_output(client: &str, port_name: &str) -> Option<MidiOutputConnection> {
824 let out = MidiOutput::new(client).ok()?;
825 let ports = out.ports();
826 let names: Vec<String> = ports.iter().map(|p| out.port_name(p).unwrap_or_default()).collect();
827 let i = names.iter().position(|n| n == port_name).or_else(|| names.iter().position(|n| is_stomp_port(n)))?;
829 out.connect(&ports[i], "stomp-out").ok()
830}
831
832fn replay(entries: Vec<Entry>, file: String, speed: f32, stop: Arc<AtomicBool>, out: Sender<Update>, wake: Waker) {
834 let total = entries.len().max(1);
835 let mut prev_ms: Option<u64> = None;
836 for (i, e) in entries.into_iter().enumerate() {
837 if stop.load(Ordering::Relaxed) {
838 return;
839 }
840 let ms = match &e {
841 Entry::Mark { ms, .. } | Entry::Message { ms, .. } => *ms,
842 };
843 if let Some(p) = prev_ms {
844 let gap = ms.saturating_sub(p).min(1500) as f32 / speed.max(0.01);
845 if gap >= 1.0 {
846 thread::sleep(Duration::from_millis(gap as u64));
847 }
848 }
849 prev_ms = Some(ms);
850 let at = UNIX_EPOCH + Duration::from_millis(ms);
851 let u = match e {
852 Entry::Mark { text, .. } => Update::Mark { at, text },
853 Entry::Message { origin, bytes, .. } => Update::Message { at, origin, bytes },
854 };
855 if out.send(u).is_err() {
856 return;
857 }
858 if i % 25 == 0 {
859 let progress = i as f32 / total as f32;
860 let _ = out.send(Update::Status(Status::Replaying { file: file.clone(), progress }));
861 }
862 wake();
863 }
864 let _ = out.send(Update::Status(Status::Replaying { file, progress: 1.0 }));
865 let _ = out.send(Update::Info { at: SystemTime::now(), text: "Replay finished".into(), error: false });
866 wake();
867}