spectrusty_utils/
tap.rs

1/*
2    Copyright (C) 2020-2022  Rafal Michalski
3
4    This file is part of SPECTRUSTY, a Rust library for building emulators.
5
6    For the full copyright notice, see the lib.rs file.
7*/
8//! **TAP** format related utilities for sweetening the handling of **TAP** files.
9use core::fmt;
10use core::convert::TryFrom;
11use std::io::{Read, Write, Result, Seek, SeekFrom};
12
13use spectrusty::formats::tap::*;
14
15pub mod romload;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum TapState {
19    Idle,
20    Playing,
21    Recording
22}
23
24/// An enum with two variants, one for reading and the other for writing to the same [TAP] file.
25///
26/// The `F` can be anything that implements: [Read] + [Write] + [Seek].
27///
28/// [TAP]: spectrusty::formats::tap
29pub enum Tap<F> {
30    Reader(TapChunkPulseIter<F>),
31    Writer(TapChunkWriter<F>)
32}
33
34/// The struct that emulates a simple tape recorder.
35#[derive(Debug)]
36pub struct Tape<F> {
37    /// `true` if the tape is playing, depending on the [Tap] variant it may indicate tape playback or recording.
38    /// `false` then the tape has stopped.
39    pub running: bool,
40    /// `Some(tap)` indicates the tape cassette is inserted, `None` - there is no tape.
41    pub tap: Option<Tap<F>>
42}
43
44impl<F> fmt::Debug for Tap<F> {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Tap::Reader(..) => "Tap::Reader(..)".fmt(f),
48            Tap::Writer(..) => "Tap::Writer(..)".fmt(f)
49        }
50    }
51}
52
53impl<F> Default for Tape<F> {
54    fn default() -> Self {
55        Tape { running: false, tap: None }
56    }
57}
58
59impl<F: Write + Read + Seek> Tap<F> {
60    /// Returns a [Tap::Reader] variant.
61    pub fn new_reader(file: F) -> Self {
62        let reader = TapChunkReader::from(file);
63        let pulse_iter = TapChunkPulseIter::from(reader);
64        Tap::Reader(pulse_iter)
65    }
66
67    /// Returns a [Tap::Writer] variant on success.
68    pub fn try_new_writer(file: F) -> Result<Self> {
69        let writer = TapChunkWriter::try_new(file)?;
70        Ok(Tap::Writer(writer))
71    }
72
73    /// Returns a mutable reference to the pulse iterator if the current variant of `self` is [Tap::Reader].
74    pub fn reader_mut(&mut self) -> Option<&mut TapChunkPulseIter<F>> {
75        match self {
76            Tap::Reader(reader) => Some(reader),
77            _ => None
78        }
79    }
80
81    /// Returns a mutable reference to the tap chunk writer if the current variant of `self` is [Tap::Writer].
82    pub fn writer_mut(&mut self) -> Option<&mut TapChunkWriter<F>> {
83        match self {
84            Tap::Writer(writer) => Some(writer),
85            _ => None
86        }
87    }
88
89    /// Returns a reference to the pulse iterator if the current variant of `self` is [Tap::Reader].
90    pub fn reader_ref(&self) -> Option<&TapChunkPulseIter<F>> {
91        match self {
92            Tap::Reader(reader) => Some(reader),
93            _ => None
94        }
95    }
96
97    /// Returns a reference to the tap chunk writer if the current variant of `self` is [Tap::Writer].
98    pub fn writer_ref(&self) -> Option<&TapChunkWriter<F>> {
99        match self {
100            Tap::Writer(writer) => Some(writer),
101            _ => None
102        }
103    }
104
105    /// Returns `true` if `self` is a [Tap::Reader].
106    pub fn is_reader(&self) -> bool {
107        matches!(self, Tap::Reader(..))
108    }
109
110    /// Returns `true` if `self` is a [Tap::Writer].
111    pub fn is_writer(&self) -> bool {
112        matches!(self, Tap::Writer(..))
113    }
114
115    /// Transforms the provided [Tap] into the [Tap::Reader] on success.
116    ///
117    /// The cursor position of the reader is set to the beginning of a file.
118    ///
119    /// If `self` is a [Tap::Writer] this method ensures that the chunk being currently written is
120    /// comitted thus ensuring the integrity of the TAP file and also calls the [Write::flush] on
121    /// the file before transforming it.
122    pub fn try_into_reader(self) -> Result<Self> {
123        let mut file = self.try_into_file()?;
124        file.seek(SeekFrom::Start(0))?;
125        Ok(Self::new_reader(file))
126    }
127
128    /// Transforms the provided [Tap] into the [Tap::Writer] on success.
129    ///
130    /// The cursor position of the writer is set to the end of a file.
131    ///
132    /// If `self` is already a [Tap::Writer] this method ensures that the chunk being currently written
133    /// is comitted thus ensuring the integrity of the TAP file and also calls the [Write::flush] on
134    /// the file before transforming it.
135    pub fn try_into_writer(self) -> Result<Self> {
136        let mut file = self.try_into_file()?;
137        file.seek(SeekFrom::End(0))?;
138        Self::try_new_writer(file)
139    }
140
141    /// Returns the unwrapped file on success.
142    ///
143    /// If `self` is a [Tap::Writer] this method ensures that the chunk being currently written is
144    /// comitted thus ensuring the integrity of the TAP file and also calls the [Write::flush] on
145    /// the file before returning it.
146    pub fn try_into_file(self) -> Result<F> {
147        Ok(match self {
148            Tap::Reader(reader) => {
149                let file: F = reader.into_inner().into_inner().into_inner();
150                file
151            },
152            Tap::Writer(mut writer) => {
153                writer.end_pulse_chunk()?;
154                writer.flush()?;
155                let file: F = writer.into_inner().into_inner();
156                file
157            }
158        })
159    }
160
161    /// Returns a clone of [TapChunkReader] with a mutable reference to the file
162    /// under the guard that ensures the position of the underlying file is set back
163    /// to where it was before this method was called when the guard goes out of scope.
164    pub fn try_reader_mut(&mut self) -> Result<TapChunkReaderMut<'_, F>> {
165        match self {
166            Tap::Reader(reader) => reader.as_mut().try_clone_mut(),
167            Tap::Writer(writer) => {
168                let file_mut = writer.get_mut().get_mut();
169                let reader = TapChunkReader::from(file_mut);
170                TapChunkReaderMut::try_from(reader)
171            }
172        }
173    }
174
175    /// Conditionally rewinds a tape if its variant is [Tap::Reader]. In this instance returns `true`.
176    /// Otherwise returns `false`.
177    pub fn rewind(&mut self) -> bool {
178        if let Some(reader) = self.reader_mut() {
179            reader.rewind();
180            true
181        }
182        else {
183            false
184        }
185    }
186
187    /// Conditionally forwards a tape to the next chunk if its variant is [Tap::Reader]. In this
188    /// instance returns `Ok(Some(was_next_chunk))`. Otherwise returns `Ok(None)`.
189    pub fn forward_chunk(&mut self) -> Result<Option<bool>> {
190        self.reader_mut().map(|rd| rd.forward_chunk()).transpose()
191    }
192
193    /// Conditionally rewinds a tape to the previous chunk if its variant is [Tap::Reader]. In this
194    /// instance returns `Ok(Some(chunk_no))`. Otherwise returns `Ok(None)`.
195    pub fn rewind_prev_chunk(&mut self) -> Result<Option<u32>> {
196        self.reader_mut().map(|rd| rd.rewind_prev_chunk()).transpose()
197    }
198
199    /// Conditionally rewinds a tape to the beginning of the current chunk if its variant is
200    /// [Tap::Reader]. In this instance returns `Ok(Some(chunk_no))`. Otherwise returns `Ok(None)`.
201    pub fn rewind_chunk(&mut self) -> Result<Option<u32>> {
202        self.reader_mut().map(|rd| rd.rewind_chunk()).transpose()
203    }
204
205    /// Conditionally rewinds or forwards a tape to the nth chunk if its variant is [Tap::Reader].
206    /// In this instance returns `Ok(Some(was_a_chunk))`. Otherwise returns `Ok(None)`.
207    pub fn rewind_nth_chunk(&mut self, chunk_no: u32) -> Result<Option<bool>> {
208        self.reader_mut().map(|rd| rd.rewind_nth_chunk(chunk_no)).transpose()
209    }
210}
211
212impl<F: Write + Read + Seek> Tape<F> {
213    /// Returns a new instance of [Tape] with the tape file inserted.
214    pub fn new_with_tape(file: F) -> Self {
215        let tap = Tap::new_reader(file);
216        Tape { running: false, tap: Some(tap) }
217    }
218
219    /// Inserts the tape file as a [Tap::Reader].
220    /// Returns the previously inserted [Tap] instance.
221    pub fn insert_as_reader(&mut self, file: F) -> Option<Tap<F>> {
222        let tap = Tap::new_reader(file);
223        self.tap.replace(tap)
224    }
225
226    /// Tries to insert the tape file as a [Tap::Writer].
227    /// Returns the previously inserted [Tap] instance.
228    pub fn try_insert_as_writer(&mut self, file: F) -> Result<Option<Tap<F>>> {
229        let tap = Tap::try_new_writer(file)?;
230        Ok(self.tap.replace(tap))
231    }
232
233    /// Ejects and returns the previously inserted [Tap] instance.
234    pub fn eject(&mut self) -> Option<Tap<F>> {
235        self.running = false;
236        self.tap.take()
237    }
238
239    /// Transforms the inserted [Tap] into the [Tap::Reader] on success.
240    ///
241    /// Returns `Ok(true)` if the inserted [Tap] was a [Tap::Writer]. In this instance, the cursor
242    /// position of the reader is set to the beginning of a file and this method ensures that
243    /// the chunk being currently written is comitted thus ensuring the integrity of the TAP
244    /// file and also calls the [Write::flush] on the file before transforming it.
245    ///
246    /// If the inserted [Tap] is already a [Tap::Reader] or if there is no [Tap] inserted returns
247    /// `Ok(false)`.
248    pub fn make_reader(&mut self) -> Result<bool> {
249        if let Some(tap) = self.tap.as_ref() {
250            if tap.is_writer() {
251                let tap = self.tap.take().unwrap().try_into_reader()?;
252                self.tap = Some(tap);
253                return Ok(true)
254            }
255        }
256        Ok(false)
257    }
258
259    /// Transforms the inserted [Tap] into the [Tap::Writer] on success.
260    ///
261    /// Returns `Ok(true)` if the inserted [Tap] was a [Tap::Reader]. In this instance the cursor
262    /// position of the reader is set to the end of a file.
263    ///
264    /// If the inserted [Tap] is already a [Tap::Writer] or if there is no [Tap] inserted returns
265    /// `Ok(false)`.
266    pub fn make_writer(&mut self) -> Result<bool> {
267        if let Some(tap) = self.tap.as_ref() {
268            if tap.is_reader() {
269                let tap = self.tap.take().unwrap().try_into_writer()?;
270                self.tap = Some(tap);
271                return Ok(true)
272            }
273        }
274        Ok(false)
275    }
276
277    /// Returns `true` if there is no [Tap] inserted, otherwise returns `false`.
278    pub fn is_ejected(&self) -> bool {
279        self.tap.is_none()
280    }
281
282    /// Returns `true` if there is a [Tap] inserted, otherwise returns `false`.
283    pub fn is_inserted(&self) -> bool {
284        self.tap.is_some()
285    }
286
287    /// Returns `true` if there is some [Tap] inserted and [Tape::running] is `true`, otherwise returns `false`.
288    pub fn is_running(&self) -> bool {
289        self.running && self.tap.is_some()
290    }
291
292    /// Returns `true` if no [Tap] is inserted or [Tape::running] is `false`, otherwise returns `true`.
293    pub fn is_idle(&self) -> bool {
294        !self.is_running()
295    }
296
297    /// Returns the current status of the inserted tape as an enum.
298    pub fn tap_state(&self) -> TapState {
299        if self.running {
300            return match self.tap.as_ref() {
301                Some(Tap::Reader(..)) => TapState::Playing,
302                Some(Tap::Writer(..)) => TapState::Recording,
303                _ => TapState::Idle
304            }
305        }
306        TapState::Idle
307    }
308
309    /// Returns `true` if there is a [Tap::Reader] variant inserted and [Tape::running] is `true`,
310    /// otherwise returns `false`.
311    pub fn is_playing(&self) -> bool {
312        if self.running {
313            if let Some(tap) = self.tap.as_ref() {
314                return tap.is_reader()
315            }
316        }
317        false
318    }
319
320    /// Returns `true` if there is a [Tap::Writer] variant inserted and [Tape::running] is `true`,
321    /// otherwise returns `false`.
322    pub fn is_recording(&self) -> bool {
323        if self.running {
324            if let Some(tap) = self.tap.as_ref() {
325                return tap.is_writer()
326            }
327        }
328        false
329    }
330
331    /// Returns a mutable reference to the pulse iterator if the current variant of [Tape::tap] is [Tap::Reader].
332    pub fn reader_mut(&mut self) -> Option<&mut TapChunkPulseIter<F>> {
333        self.tap.as_mut().and_then(|tap| tap.reader_mut())
334    }
335
336    /// Returns a mutable reference to the tap chunk writer if the current variant of [Tape::tap] is [Tap::Writer].
337    pub fn writer_mut(&mut self) -> Option<&mut TapChunkWriter<F>> {
338        self.tap.as_mut().and_then(|tap| tap.writer_mut())
339    }
340
341    /// Returns a reference to the pulse iterator if the current variant of [Tape::tap] is [Tap::Reader].
342    pub fn reader_ref(&self) -> Option<&TapChunkPulseIter<F>> {
343        self.tap.as_ref().and_then(|tap| tap.reader_ref())
344    }
345
346    /// Returns a reference to the tap chunk writer if the current variant of [Tape::tap] is [Tap::Writer].
347    pub fn writer_ref(&self) -> Option<&TapChunkWriter<F>> {
348        self.tap.as_ref().and_then(|tap| tap.writer_ref())
349    }
350
351    /// Returns a mutable reference to the pulse iterator if there is a [Tap::Reader] variant inserted
352    /// and [Tape::running] is `true`, otherwise returns `None`.
353    pub fn playing_reader_mut(&mut self) -> Option<&mut TapChunkPulseIter<F>> {
354        if self.running {
355            return self.reader_mut();
356        }
357        None
358    }
359
360    /// Returns a mutable reference to the tap chunk writer if there is a [Tap::Writer] variant inserted
361    /// and [Tape::running] is `true`, otherwise returns `None`.
362    pub fn recording_writer_mut(&mut self) -> Option<&mut TapChunkWriter<F>> {
363        if self.running {
364            return self.writer_mut();
365        }
366        None
367    }
368
369    /// Returns a clone of [TapChunkReader] with a mutable reference to the file
370    /// under the guard that ensures the position of the underlying file is set back
371    /// to where it was before this method was called when the guard goes out of scope.
372    pub fn try_reader_mut(&mut self) -> Result<Option<TapChunkReaderMut<'_, F>>> {
373        self.tap.as_mut().map(|tap| tap.try_reader_mut()).transpose()
374    }
375
376    /// Sets [Tape::running] to `true` and ensures the inserted variant is a [Tap::Reader].
377    ///
378    /// Returns `Ok(true)` if the state of `self` changes.
379    pub fn play(&mut self) -> Result<bool> {
380        let running = self.running;
381        self.running = true;
382        Ok(self.make_reader()? || !running)
383    }
384
385    /// Sets [Tape::running] to `true` and ensures the inserted variant is a [Tap::Writer].
386    ///
387    /// Returns `Ok(true)` if the state of `self` changes.
388    pub fn record(&mut self) -> Result<bool> {
389        let running = self.running;
390        self.running = true;
391        Ok(self.make_writer()? || !running)
392    }
393
394    /// Sets [Tape::running] to `false`.
395    pub fn stop(&mut self) {
396        self.running = false;
397    }
398
399    /// Conditionally rewinds a tape if it's inserted and its variant is [Tap::Reader].
400    /// In this instance returns `true`. Otherwise returns `false`.
401    pub fn rewind(&mut self) -> bool {
402        self.tap.as_mut().map(|rd| rd.rewind()).filter(|b| *b).is_some()
403    }
404
405    /// Conditionally forwards a tape to the next chunk if it's inserted and its variant
406    /// is [Tap::Reader]. In this instance returns `Ok(Some(was_next_chunk))`. Otherwise returns
407    /// `Ok(None)`.
408    pub fn forward_chunk(&mut self) -> Result<Option<bool>> {
409        self.reader_mut().map(|rd| rd.forward_chunk()).transpose()
410    }
411
412    /// Conditionally rewinds a tape to the previous chunk if it's inserted and its variant
413    /// is [Tap::Reader]. In this instance returns `Ok(Some(chunk_no))`. Otherwise returns `Ok(None)`.
414    pub fn rewind_prev_chunk(&mut self) -> Result<Option<u32>> {
415        self.reader_mut().map(|rd| rd.rewind_prev_chunk()).transpose()
416    }
417
418    /// Conditionally rewinds a tape to the beginning of the current chunk if it's inserted and its
419    /// variant is [Tap::Reader]. In this instance returns `Ok(Some(chunk_no))`. Otherwise returns
420    /// `Ok(None)`.
421    pub fn rewind_chunk(&mut self) -> Result<Option<u32>> {
422        self.reader_mut().map(|rd| rd.rewind_chunk()).transpose()
423    }
424
425    /// Conditionally rewinds or forwards a tape to the nth chunk if it's inserted and its
426    /// variant is [Tap::Reader]. In this instance returns `Ok(Some(was_a_chunk))`. Otherwise
427    /// returns `Ok(None)`.
428    pub fn rewind_nth_chunk(&mut self, chunk_no: u32) -> Result<Option<bool>> {
429        self.reader_mut().map(|rd| rd.rewind_nth_chunk(chunk_no)).transpose()
430    }
431}