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}