1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
//! VTX sound format parsing library
//!
//! See sources of `vtx-bin` crate for usage example
pub mod player;

use byteorder::{LittleEndian, ReadBytesExt};
use delharc::decode::{Decoder, Lh5Decoder};
use num_traits::FromPrimitive;
use thiserror::Error;

/// AY/YM Sound chip register count
pub const AY_REGISTER_COUNT: usize = 14;
/// Special R13 (`vtx.frame_registers[R13_NO_CHANGE_VALUE]`) register value which signals
/// that R13 register should not be changed for the current emulation frame. This should
/// be taken into account during emulation for correct envelope emulation
pub const R13_NO_CHANGE_VALUE: u8 = 0xFF;

/// Stereo configuration
/// See [aym::AyMode] documentation for more info
#[derive(Debug, num_derive::FromPrimitive)]
#[allow(clippy::upper_case_acronyms)]
pub enum Stereo {
    Mono,
    ABC,
    ACB,
    BAC,
    BCA,
    CAB,
    CBA,
}

/// Sound chip type
/// See [aym::SoundChip] documentation for more info
#[derive(Debug)]
pub enum SoundChip {
    AY,
    YM,
}

#[derive(Debug)]
pub struct Vtx {
    /// Sound chip which should be used for this track
    pub chip: SoundChip,
    /// Stereo configuration of the track
    pub stereo: Stereo,
    /// Sound chip frequency (e.g. For ZX Spectrum it is usually 1773400 Hz)
    pub frequency: u32,
    /// Sound frames per second (e.g. For ZX Spectrum usually equals to 50)
    pub player_frequency: u8,
    /// Starting frame index for looped playback (e.g. 0 - beginning of the song)
    pub loop_start_frame: u16,
    /// Year of the track
    pub year: u16,
    /// Title of the track
    pub title: String,
    /// Author of the track
    pub author: String,
    /// Source of the song (e.g. In which game was used)
    pub from: String,
    /// Tracker program used to make this song
    pub tracker: String,
    /// Author comment
    pub comment: String,
    /// Stores sequential blocks of register values for each frame. Each block has size if
    /// `AY_REGISTER_COUNT`. It is advised to use `frame_registers` to access frame data instead of
    /// direct access to `frame_data` field
    pub frame_data: Vec<u8>,
}

impl Vtx {
    /// Returns frame count of the track
    fn frames_count(&self) -> usize {
        self.frame_data.len() / AY_REGISTER_COUNT
    }

    /// Retrurns slice with register values for the given frame or `None` if index is
    /// out of bounds
    fn frame_registers(&self, index: usize) -> Option<&[u8]> {
        let offset = index * AY_REGISTER_COUNT;
        if offset + AY_REGISTER_COUNT > self.frame_data.len() {
            return None;
        }

        Some(&self.frame_data[offset..offset + AY_REGISTER_COUNT])
    }
}

#[derive(Error, Debug)]
pub enum VtxError {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error("Invalid VTX header: {}", message)]
    InvalidHeader { message: &'static str },
    #[error("Faield to decode lh5 compressed data")]
    DecompressFailure,
}

impl Vtx {
    /// Loads VTX file using provided reader
    pub fn load<R>(mut reader: R) -> Result<Self, VtxError>
    where
        R: std::io::Read + std::io::Seek,
    {
        let mut magic = [0u8; 2];
        reader
            .read_exact(&mut magic)
            .map_err(|_| VtxError::InvalidHeader {
                message: "Failed to read file identifier",
            })?;

        let chip = match magic {
            [b'a', b'y'] => SoundChip::AY,
            [b'y', b'm'] => SoundChip::YM,
            _ => {
                return Err(VtxError::InvalidHeader {
                    message: "Not supported file identifier",
                });
            }
        };

        let stereo = Stereo::from_u8(reader.read_u8()?).ok_or(VtxError::InvalidHeader {
            message: "Invalid stereo configuration",
        })?;

        let loop_start_rame = reader.read_u16::<LittleEndian>()?;
        let frequency = reader.read_u32::<LittleEndian>()?;
        let player_frequency = reader.read_u8()?;
        let year = reader.read_u16::<LittleEndian>()?;
        let decompressed_frames_size = reader.read_u32::<LittleEndian>()?;

        if decompressed_frames_size % AY_REGISTER_COUNT as u32 != 0 {
            return Err(VtxError::InvalidHeader {
                message: "Invalid decompressed frames data size",
            });
        }

        let strings_start = reader.stream_position()?;

        const READ_STRING_BUFFER_SIZE: usize = 256;
        const EXPECTED_STRINGS_COUNT: usize = 5;

        let mut strings_block_size = 0;
        let mut null_terminators_read = 0;
        while null_terminators_read != 5 {
            let mut strings_partial_buffer = [0u8; READ_STRING_BUFFER_SIZE];
            let bytes_read = reader.read(&mut strings_partial_buffer)?;
            let mut current_buffer_bytes_count = 0;
            while current_buffer_bytes_count < bytes_read {
                if let Some(pos) = strings_partial_buffer[current_buffer_bytes_count..]
                    .iter()
                    .position(|x| *x == b'\0')
                {
                    null_terminators_read += 1;
                    current_buffer_bytes_count += pos + 1;
                } else {
                    current_buffer_bytes_count = bytes_read;
                }
                if null_terminators_read == EXPECTED_STRINGS_COUNT {
                    break;
                }
            }
            strings_block_size += current_buffer_bytes_count;
        }

        if null_terminators_read != EXPECTED_STRINGS_COUNT {
            return Err(VtxError::InvalidHeader {
                message: "Invalid strings block",
            });
        }

        reader.seek(std::io::SeekFrom::Start(strings_start))?;
        let mut strings_buffer = vec![0u8; strings_block_size - 1];
        reader.read_exact(&mut strings_buffer)?;
        if reader.read_u8()? != b'\0' {
            return Err(VtxError::InvalidHeader {
                message: "Missing strings block terminator",
            });
        }
        let mut strings = strings_buffer
            .split(|b| *b == b'\0')
            .map(|buf| String::from_utf8_lossy(buf).into_owned())
            .collect::<Vec<_>>();

        assert_eq!(
            strings.len(),
            EXPECTED_STRINGS_COUNT,
            "Iterator size should be assured above"
        );

        let comment = strings.pop().unwrap();
        let tracker = strings.pop().unwrap();
        let from = strings.pop().unwrap();
        let author = strings.pop().unwrap();
        let title = strings.pop().unwrap();

        let mut transposed_frame_data = vec![0u8; decompressed_frames_size as usize];
        let mut decoder = Lh5Decoder::new(reader);
        decoder
            .fill_buffer(&mut transposed_frame_data)
            .map_err(|_| VtxError::DecompressFailure)?;

        // VTX originally stores pre-transposed data, therefore we need to tarnspose it
        let frames_count = transposed_frame_data.len() / AY_REGISTER_COUNT;
        let mut frame_data = Vec::with_capacity(transposed_frame_data.len());
        for idx in 0..transposed_frame_data.len() {
            let frame_idx = idx / AY_REGISTER_COUNT;
            let reg_idx = idx % AY_REGISTER_COUNT;
            frame_data.push(transposed_frame_data[reg_idx * frames_count + frame_idx]);
        }

        let vtx = Self {
            chip,
            stereo,
            frequency,
            player_frequency,
            loop_start_frame: loop_start_rame,
            year,
            title,
            author,
            from,
            tracker,
            comment,
            frame_data,
        };

        Ok(vtx)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use expect_test::expect;
    use sha2::{Digest, Sha256};

    fn check_load_vtx(data: &[u8]) -> (Vtx, String) {
        let mut vtx = Vtx::load(std::io::Cursor::new(data)).unwrap();

        let mut frame_data = vec![];
        std::mem::swap(&mut frame_data, &mut vtx.frame_data);

        let mut hasher = Sha256::default();
        hasher.update(&frame_data);
        let frame_data_fingerprint = hasher.finalize();
        (vtx, format!("{:x}", frame_data_fingerprint))
    }

    #[test]
    fn decode_succeeds_1() {
        let (vtx, frame_data_fingerprint) = check_load_vtx(include_bytes!("test/csoon.vtx"));
        expect![[r#"
            Vtx {
                chip: AY,
                stereo: ABC,
                frequency: 1773400,
                player_frequency: 50,
                loop_start_frame: 0,
                year: 0,
                title: "Coming Soon",
                author: "",
                from: "Cube Megademo",
                tracker: "Sound Tracker",
                comment: "",
                frame_data: [],
            }
        "#]]
        .assert_debug_eq(&vtx);
        expect![[r#"c5a486a006be9dd29cc24961b491596c4036b5534f7a6b465a4678974c713874"#]]
            .assert_eq(&frame_data_fingerprint);
    }

    #[test]
    fn decode_succeeds_2() {
        let (vtx, frame_data_fingerprint) = check_load_vtx(include_bytes!("test/secret.vtx"));
        expect![[r#"
            Vtx {
                chip: YM,
                stereo: ABC,
                frequency: 1773400,
                player_frequency: 50,
                loop_start_frame: 0,
                year: 2005,
                title: "\"SECRET LAND\" ( Sandra ' 95 )",
                author: "AY_VER BY IGNEOUS'2000",
                from: "",
                tracker: "",
                comment: "Created by Sergey Bulba's AY-3-8910/12 Emulator v2.6",
                frame_data: [],
            }
        "#]]
        .assert_debug_eq(&vtx);
        expect![[r#"9226e785a21e943e588dde6489284f87673138493ddf87ba483a4470311176de"#]]
            .assert_eq(&frame_data_fingerprint);
    }

    #[test]
    fn decode_succeeds_3() {
        let (vtx, frame_data_fingerprint) = check_load_vtx(include_bytes!("test/sil00.vtx"));
        expect![[r#"
            Vtx {
                chip: AY,
                stereo: ACB,
                frequency: 1773400,
                player_frequency: 50,
                loop_start_frame: 0,
                year: 1989,
                title: "Tune 1",
                author: "Fuxoft",
                from: "Song In Lines 3-5",
                tracker: "",
                comment: "Created by Sergey Bulba's AY-3-8910/12 Emulator v1.5",
                frame_data: [],
            }
        "#]]
        .assert_debug_eq(&vtx);
        expect![[r#"e81a26d67779064af9f8c9826174881d2532212add961b15399b0b8db7eff8c5"#]]
            .assert_eq(&frame_data_fingerprint);
    }

    #[test]
    fn decode_succeeds_4() {
        let (vtx, frame_data_fingerprint) = check_load_vtx(include_bytes!("test/spf21_00.vtx"));
        expect![[r#"
            Vtx {
                chip: AY,
                stereo: ABC,
                frequency: 1773400,
                player_frequency: 50,
                loop_start_frame: 0,
                year: 0,
                title: "Spectrofon 21 main menu tune",
                author: "ARNO",
                from: "Spectrofon 21 magazine",
                tracker: "Pro Tracker v2.1",
                comment: "      Converted to VTX by           Ivan Yuskin  (Krogoth)",
                frame_data: [],
            }
        "#]]
        .assert_debug_eq(&vtx);
        expect![[r#"bfab6a3e657854cb4263fc0492cd2db51451a57e6ba98e5d3050e2bed1f904fb"#]]
            .assert_eq(&frame_data_fingerprint);
    }

    #[test]
    fn frame_indexing_success() {
        let vtx = Vtx::load(std::io::Cursor::new(include_bytes!("test/csoon.vtx"))).unwrap();

        expect![[r#"
            Some(
                [
                    94,
                    0,
                    224,
                    7,
                    94,
                    0,
                    0,
                    16,
                    0,
                    30,
                    0,
                    32,
                    0,
                    255,
                ],
            )
        "#]]
        .assert_debug_eq(&vtx.frame_registers(42));
    }

    #[test]
    fn frame_indexing_failure() {
        let vtx = Vtx::load(std::io::Cursor::new(include_bytes!("test/csoon.vtx"))).unwrap();

        expect![[r#"
            None
        "#]]
        .assert_debug_eq(&vtx.frame_registers(999999));
    }
}