mirror of
https://github.com/holub/mame
synced 2025-05-20 12:48:53 +03:00

Use standard uint64_t, uint32_t, uint16_t or uint8_t instead of UINT64, UINT32, UINT16 or UINT8 also use standard int64_t, int32_t, int16_t or int8_t instead of INT64, INT32, INT16 or INT8
374 lines
11 KiB
C++
374 lines
11 KiB
C++
// license:BSD-3-Clause
|
|
// copyright-holders:Fabio Priuli
|
|
/*
|
|
|
|
Tape support for C16 / C64 / VIC20 TAP format
|
|
|
|
|
|
Credits to:
|
|
|
|
- Peter Schepers for the informations on C64 formats
|
|
- Vice Team for the source of their very complete emulator
|
|
- Attila G. for tap2wav (both the source and the actual tool)
|
|
|
|
|
|
TODO:
|
|
* to verify and fix (if needed) support for .TAP v2
|
|
* to implement reading (and logging) the remaining part of the header
|
|
* to verify if it is more accurate to use a different HIGH_WAVE value
|
|
when the pulse corresponds to a 0x00 in the .TAP file
|
|
(...)
|
|
* (far away in the future) can this code be merged with TZX code?
|
|
|
|
*/
|
|
|
|
/* Info based on http://ist.uwaterloo.ca/~schepers/formats/TAP.TXT */
|
|
/* Please refer to the webpage for the latest version and for a very
|
|
complete listing of various cart types and their bankswitch tricks */
|
|
/*
|
|
|
|
Designed by Per Hakan Sundell (author of the CCS64 C64 emulator) in 1997,
|
|
this format attempts to duplicate the data stored on a C64 cassette tape,
|
|
bit for bit. Since it is simply a representation of the raw serial data
|
|
from a tape, it should handle *any* custom tape loaders that exist.
|
|
|
|
The TAP images are generally very large, being a minimum of eight times,
|
|
and up to sixteen times as large as what a raw PRG file would be. This is
|
|
due to the way the data is stored, with each bit of the original file now
|
|
being one byte large in the TAP file. The layout is fairly simple, with a
|
|
small 14-byte header followed by file data.
|
|
|
|
Bytes: $0000-000B: File signature "C64-TAPE-RAW"
|
|
000C: TAP version (see below for description)
|
|
$00 - Original layout
|
|
01 - Updated
|
|
000D-000F: Future expansion
|
|
0010-0013: File data size (not including this header, in
|
|
LOW/HIGH format).
|
|
0014-xxxx: File data
|
|
|
|
In TAP version $00 files, each data byte in the file data area represents
|
|
the length of a pulse, when the C64's hardware will trigger again. This
|
|
pulse length is determined by the following formula:
|
|
|
|
pulse length (in seconds) = (8 * data byte) / (clock cycles)
|
|
|
|
Therefore, a data value of $2F (47 in decimal) would be:
|
|
|
|
(47 * 8) / 985248 = .00038975 seconds.
|
|
|
|
A data value of $00 represents an "overflow" condition, any pulse length
|
|
which is more that 255 * 8 in length.
|
|
|
|
The value of "clock cylces" from above (985248) is based on the PAL
|
|
value. Since this file format was developed in Europe, which is
|
|
predominantly PAL video, this is only logical. The NTSC value would be
|
|
1022730, which is very close to the PAL, and therefore won't cause a
|
|
compatability problem converting European and NTSC tapes. I would stick to
|
|
using the PAL value just in case.
|
|
|
|
|
|
In TAP version $01 files, the data value of $00 has been re-coded to
|
|
represent values greater than 255 * 8. When a $00 is encountered, three
|
|
bytes will follow which are the actual time (in cycles) of a pulse, and the
|
|
above formula does not apply. The three bytes are stored in LOW/HIGH
|
|
format.
|
|
|
|
|
|
The actual interpretation of the serial data takes a little more work to
|
|
explain. The typical ROM tape loader (and the turbo loaders) will
|
|
initialize a timer with a specified value and start it counting down. If
|
|
either the tape data changes or the timer runs out, an IRQ will occur. The
|
|
loader will determine which condition caused the IRQ. If the tape data
|
|
changed before the timer ran out, we have a short pulse, or a "0" bit. If
|
|
the timer ran out first, we have a long pulse, or a "1" bit. Doing this
|
|
continuously and we decode the entire file.
|
|
|
|
|
|
[ Additional notes on v.2:
|
|
|
|
I found no documents about this around, but it seems an expansion of the
|
|
format specifically thought for C16 tapes. In a .TAP version 2, each byte
|
|
only stores informations on half of the wave...
|
|
Unfortunately, I have no such a .tap file to test, so my implementation
|
|
below could be not working. FP ]
|
|
*/
|
|
|
|
#include <assert.h>
|
|
|
|
#include "cbm_tap.h"
|
|
|
|
|
|
#define CBM_WAV_FREQUENCY 44100
|
|
|
|
/* Systems */
|
|
#define C64 0
|
|
#define VIC20 1
|
|
#define C16 2
|
|
|
|
/* Video standards */
|
|
#define PAL 0
|
|
#define NTSC 1
|
|
|
|
/* Frequencies in [Hz] to determine the length of each pulse */
|
|
#define C64_PAL 123156 /* 985248 / 8 */
|
|
#define C64_NTSC 127841 /* 1022727 / 8 */
|
|
#define VIC20_PAL 138551 /* 1108405 / 8 */
|
|
#define VIC20_NTSC 127841 /* 1022727 / 8 */
|
|
#define C16_PAL 110840 /* 886724 / 8 */
|
|
#define C16_NTSC 111860 /* 894886 / 8 */
|
|
|
|
#define PAUSE (CBM_WAV_FREQUENCY / 50) /* tap2wav uses this value for 0x00 in .TAP v0, instead of 0x100 */
|
|
|
|
/* These values do not really matter, as long as the produced pulses
|
|
go above & below 0. However, for documentation purpose it would be
|
|
nice to find out which values were used by Commodore tapes. I was
|
|
not able to find any reference on the subject. */
|
|
#define WAVE_HIGH (0x5a9e >> 1)
|
|
#define WAVE_LOW -(0x5a9e >> 1)
|
|
#define WAVE_PAUSE 0x80
|
|
|
|
#define CBM_HEADER_SIZE 20
|
|
|
|
static int16_t wave_data = 0;
|
|
static int len;
|
|
|
|
|
|
/* This in fact gives the number of samples for half of the pulse */
|
|
static inline int tap_data_to_samplecount(int data, int frequency)
|
|
{
|
|
// return (int) (0.5 * (0.5 + (((double)CBM_WAV_FREQUENCY / frequency) * (double)data))); // MESS TZX formula
|
|
return (int) (0.5 * (((double)CBM_WAV_FREQUENCY / frequency) * (double)((data) + 0.5))); // tap2wav formula
|
|
}
|
|
|
|
/* The version with parameters could be handy if we decide to implement a
|
|
different values for byte == 0x00 below (the WAVE_PAUSE) as tap2wav does. */
|
|
#if 0
|
|
static void toggle_wave_data(int low, int high)
|
|
{
|
|
wave_data = (wave_data == low) ? high : low;
|
|
}
|
|
#endif
|
|
|
|
static void toggle_wave_data(void )
|
|
{
|
|
wave_data = (wave_data == WAVE_HIGH) ? WAVE_LOW : WAVE_HIGH;
|
|
}
|
|
|
|
static void cbm_output_wave( int16_t **buffer, int length )
|
|
{
|
|
if (buffer == nullptr)
|
|
return;
|
|
|
|
for( ; length > 0; length-- )
|
|
{
|
|
**buffer = wave_data;
|
|
*buffer = *buffer + 1;
|
|
}
|
|
}
|
|
|
|
|
|
static int cbm_tap_do_work( int16_t **buffer, int length, const uint8_t *data )
|
|
{
|
|
int i, j = 0;
|
|
int size = 0;
|
|
|
|
int version = data[0x0c];
|
|
int system = data[0x0d];
|
|
int video_standard = data[0x0e];
|
|
int tap_frequency = 0;
|
|
|
|
int byte_samples = 0;
|
|
uint8_t over_pulse_bytes[3] = {0 , 0, 0 };
|
|
int over_pulse_length = 0;
|
|
/* These waveamp_* values are currently stored but not used.
|
|
Further investigations are needed to find real pulse amplitude
|
|
in Commodore tapes. Implementation here would follow */
|
|
/* int waveamp_high, waveamp_low; */
|
|
|
|
/* Log .TAP info but only once */
|
|
if (!(buffer == nullptr))
|
|
{
|
|
LOG_FORMATS("TAP version : %d\n", version);
|
|
LOG_FORMATS("Machine type : %d\n", system);
|
|
LOG_FORMATS("Video standard : %d\n", video_standard);
|
|
LOG_FORMATS("Tape frequency : %d\n", (tap_frequency) << 3);
|
|
}
|
|
|
|
|
|
/* is this a supported version? */
|
|
if ((version < 0) || (version > 2))
|
|
{
|
|
LOG_FORMATS("Unsupported .tap version: %d \n", version);
|
|
return -1;
|
|
}
|
|
|
|
/* is the .tap file corrupted? */
|
|
if ((data == nullptr) || (length <= CBM_HEADER_SIZE))
|
|
return -1;
|
|
|
|
|
|
/* read the frequency from the .tap header */
|
|
switch (system)
|
|
{
|
|
case VIC20:
|
|
tap_frequency = (video_standard == NTSC) ? VIC20_NTSC : VIC20_PAL;
|
|
break;
|
|
|
|
case C16:
|
|
tap_frequency = (video_standard == NTSC) ? C16_NTSC : C16_PAL;
|
|
break;
|
|
|
|
case C64:
|
|
default:
|
|
tap_frequency = (video_standard == NTSC) ? C64_NTSC : C64_PAL;
|
|
break;
|
|
}
|
|
|
|
|
|
for (i = CBM_HEADER_SIZE; i < length; i++)
|
|
{
|
|
uint8_t byte = data[i];
|
|
|
|
/* .TAP v0 */
|
|
/* Here is simple:
|
|
if byte is != 0 -> length = byte
|
|
otherwise -> length = 0x100 (i.e. 0xff + 1) */
|
|
if (!version)
|
|
{
|
|
if (byte != 0x00)
|
|
{
|
|
byte_samples = tap_data_to_samplecount(byte, tap_frequency);
|
|
/* waveamp_high = WAVE_HIGH; */
|
|
}
|
|
else
|
|
{
|
|
byte_samples = tap_data_to_samplecount(PAUSE, tap_frequency); // tap2wav value
|
|
// byte_samples = tap_data_to_samplecount(0x100, tap_frequency); // vice value
|
|
/* waveamp_high = WAVE_PAUSE; */
|
|
}
|
|
/* waveamp_low = WAVE_LOW; */
|
|
}
|
|
|
|
/* .TAP v1 & v2 */
|
|
/* Here is a bit more complicate:
|
|
if byte is != 0 -> length = byte
|
|
otherwise -> the length of the pulse is stored as a 24bit value in the 3 bytes after the 0.
|
|
See below for comments on the implementation of this mechanism */
|
|
if (version)
|
|
{
|
|
if ((byte != 0x00) && !j)
|
|
{
|
|
byte_samples = tap_data_to_samplecount(byte, tap_frequency);
|
|
/* waveamp_high = WAVE_HIGH; */
|
|
}
|
|
else
|
|
{
|
|
/* If we have a long pulse close to the end of the .TAP, check that bytes still
|
|
to be read are enough to complete it. */
|
|
if (length - i + j >= 4)
|
|
{
|
|
/* Here we read the 3 following bytes, using an index j
|
|
j = 0 -> The 0x00 byte: we simply skip everything and go on
|
|
j = 1,2 -> The 1st and 2nd bytes after 0x00: we store them and go on
|
|
j = 3 -> The final byte of the pulse length: we store it,
|
|
and then we pass to finally output the wave */
|
|
if (j > 0)
|
|
{
|
|
over_pulse_bytes[j-1] = byte;
|
|
j += 1;
|
|
|
|
if (j >= 4)
|
|
{
|
|
over_pulse_length = ((over_pulse_bytes[2] << 16) | (over_pulse_bytes[1] << 8) | over_pulse_bytes[0]) >> 3;
|
|
byte_samples = tap_data_to_samplecount(over_pulse_length, tap_frequency);
|
|
/* waveamp_high = WAVE_PAUSE; */
|
|
j = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
j += 1;
|
|
LOG_FORMATS("Found a 00 byte close to the end of the .tap file.\n");
|
|
LOG_FORMATS("This is not allowed by the format specs. \n");
|
|
LOG_FORMATS("Check if your .tap file got corrupted when you created it!\n");
|
|
}
|
|
}
|
|
else j = 1;
|
|
}
|
|
/* waveamp_low = WAVE_LOW; */
|
|
}
|
|
|
|
if (j == 0)
|
|
{
|
|
cbm_output_wave( buffer, byte_samples );
|
|
size += byte_samples;
|
|
// toggle_wave_data(waveamp_low, waveamp_high);
|
|
toggle_wave_data();
|
|
if (version < 2)
|
|
{
|
|
cbm_output_wave( buffer, byte_samples );
|
|
size += byte_samples;
|
|
// toggle_wave_data(waveamp_low, waveamp_high);
|
|
toggle_wave_data();
|
|
}
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
|
|
static int cbm_tap_to_wav_size( const uint8_t *tapdata, int taplen )
|
|
{
|
|
int size = cbm_tap_do_work(nullptr, taplen, tapdata);
|
|
len = taplen;
|
|
|
|
return size;
|
|
}
|
|
|
|
static int cbm_tap_fill_wave( int16_t *buffer, int length, uint8_t *bytes )
|
|
{
|
|
int16_t *p = buffer;
|
|
|
|
return cbm_tap_do_work(&p, len, (const uint8_t *)bytes);
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct CassetteLegacyWaveFiller cbm_legacy_fill_wave = {
|
|
cbm_tap_fill_wave, /* fill_wave */
|
|
-1, /* chunk_size */
|
|
0, /* chunk_samples */
|
|
cbm_tap_to_wav_size, /* chunk_sample_calc */
|
|
CBM_WAV_FREQUENCY, /* sample_frequency */
|
|
0, /* header_samples */
|
|
0 /* trailer_samples */
|
|
};
|
|
|
|
|
|
static cassette_image::error cbm_cassette_identify( cassette_image *cassette, struct CassetteOptions *opts )
|
|
{
|
|
return cassette_legacy_identify( cassette, opts, &cbm_legacy_fill_wave );
|
|
}
|
|
|
|
|
|
static cassette_image::error cbm_cassette_load( cassette_image *cassette )
|
|
{
|
|
return cassette_legacy_construct( cassette, &cbm_legacy_fill_wave );
|
|
}
|
|
|
|
|
|
static const struct CassetteFormat cbm_tap_cassette_format = {
|
|
"tap",
|
|
cbm_cassette_identify,
|
|
cbm_cassette_load,
|
|
nullptr
|
|
};
|
|
|
|
|
|
CASSETTE_FORMATLIST_START(cbm_cassette_formats)
|
|
CASSETTE_FORMAT(cbm_tap_cassette_format)
|
|
CASSETTE_FORMATLIST_END
|