Files
libretro-atari800/atari800/src/cassette.c
T
2015-12-14 14:00:35 +01:00

480 lines
13 KiB
C

/*
* cassette.c - cassette emulation
*
* Copyright (C) 2001 Piotr Fusik
* Copyright (C) 2001-2011 Atari800 development team (see DOC/CREDITS)
*
* This file is part of the Atari800 emulator project which emulates
* the Atari 400, 800, 800XL, 130XE, and 5200 8-bit computers.
*
* Atari800 is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Atari800 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Atari800; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "atari.h"
#include "cpu.h"
#include "cassette.h"
#include "esc.h"
#include "img_tape.h"
#include "log.h"
#include "util.h"
#include "pokey.h"
static IMG_TAPE_t *cassette_file = NULL;
/* Time till the end of the current tape event (byte or gap), in CPU ticks. */
static SLONG event_time_left = 0;
/* Indicates that there is a SERIN transmission in progress and when it ends,
the current byte should be copied to POKEY_SERIN. This can be reset by
rewinding/removing the tape or by resetting POKEY.
Note that this variable has any meaning when PASSING_GAP is FALSE,
so it doesn't have to be reset during PASSING_IRG. */
static int pending_serin = FALSE;
/* Indicates that an Inter-Record-Gap is currently being passed. It's set to TRUE
at the beginning of each block. */
static int passing_gap = FALSE;
/* if penting_serin == TRUE, this holds the byte that is currently loaded from
tape. It might be later copied to serin_byte. */
static UBYTE pending_serin_byte = 0xff;
/* Byte most recently loaded from tape; will be accessed by SIO_GetByte(). */
static UBYTE serin_byte = 0xff;
char CASSETTE_filename[FILENAME_MAX];
CASSETTE_status_t CASSETTE_status = CASSETTE_STATUS_NONE;
int CASSETTE_write_protect = FALSE;
int CASSETTE_record = FALSE;
int CASSETTE_writable = FALSE;
int CASSETTE_readable = FALSE;
char CASSETTE_description[CASSETTE_DESCRIPTION_MAX];
static int cassette_gapdelay = 0; /* in ms, includes leader and all gaps */
static int cassette_motor = 0;
int CASSETTE_hold_start_on_reboot = 0;
int CASSETTE_hold_start = 0;
int CASSETTE_press_space = 0;
/* Indicates whether the tape has ended. During saving the value is always 0;
during loading it is equal to (CASSETTE_GetPosition() >= CASSETTE_GetSize()). */
static int eof_of_tape = 0;
/* Call this function after each change of
cassette_motor, CASSETTE_status or eof_of_tape. */
static void UpdateFlags(void)
{
CASSETTE_readable = cassette_motor &&
(CASSETTE_status == CASSETTE_STATUS_READ_WRITE ||
CASSETTE_status == CASSETTE_STATUS_READ_ONLY) &&
!eof_of_tape;
CASSETTE_writable = cassette_motor &&
CASSETTE_status == CASSETTE_STATUS_READ_WRITE &&
!CASSETTE_write_protect;
}
int CASSETTE_ReadConfig(char *string, char *ptr)
{
if (strcmp(string, "CASSETTE_FILENAME") == 0)
Util_strlcpy(CASSETTE_filename, ptr, sizeof(CASSETTE_filename));
else if (strcmp(string, "CASSETTE_LOADED") == 0) {
int value = Util_sscanbool(ptr);
if (value == -1)
return FALSE;
CASSETTE_status = (value ? CASSETTE_STATUS_READ_WRITE : CASSETTE_STATUS_NONE);
}
else if (strcmp(string, "CASSETTE_WRITE_PROTECT") == 0) {
int value = Util_sscanbool(ptr);
if (value == -1)
return FALSE;
CASSETTE_write_protect = value;
}
else return FALSE;
return TRUE;
}
void CASSETTE_WriteConfig(FILE *fp)
{
fprintf(fp, "CASSETTE_FILENAME=%s\n", CASSETTE_filename);
fprintf(fp, "CASSETTE_LOADED=%d\n", CASSETTE_status != CASSETTE_STATUS_NONE);
fprintf(fp, "CASSETTE_WRITE_PROTECT=%d\n", CASSETTE_write_protect);
}
int CASSETTE_Initialise(int *argc, char *argv[])
{
int i;
int j;
int protect = FALSE; /* Is write-protect requested in command line? */
for (i = j = 1; i < *argc; i++) {
int i_a = (i + 1 < *argc); /* is argument available? */
int a_m = FALSE; /* error, argument missing! */
if (strcmp(argv[i], "-tape") == 0) {
if (i_a) {
Util_strlcpy(CASSETTE_filename, argv[++i], sizeof(CASSETTE_filename));
CASSETTE_status = CASSETTE_STATUS_READ_WRITE;
/* Reset any write-protection read from config file. */
CASSETTE_write_protect = FALSE;
}
else a_m = TRUE;
}
else if (strcmp(argv[i], "-boottape") == 0) {
if (i_a) {
Util_strlcpy(CASSETTE_filename, argv[++i], sizeof(CASSETTE_filename));
CASSETTE_status = CASSETTE_STATUS_READ_WRITE;
/* Reset any write-protection read from config file. */
CASSETTE_write_protect = FALSE;
CASSETTE_hold_start = 1;
}
else a_m = TRUE;
}
else if (strcmp(argv[i], "-tape-readonly") == 0)
protect = TRUE;
else {
if (strcmp(argv[i], "-help") == 0) {
Log_print("\t-tape <file> Insert cassette image");
Log_print("\t-boottape <file> Insert cassette image and boot it");
Log_print("\t-tape-readonly Mark the attached cassette image as read-only");
}
argv[j++] = argv[i];
}
if (a_m) {
Log_print("Missing argument for '%s'", argv[i]);
return FALSE;
}
}
*argc = j;
/* If CASSETTE_status was set in this function or in CASSETTE_ReadConfig(),
then tape is to be mounted. */
if (CASSETTE_status != CASSETTE_STATUS_NONE && CASSETTE_filename[0] != '\0') {
/* Tape is mounted unprotected by default - overrun it if needed. */
protect = protect || CASSETTE_write_protect;
if (!CASSETTE_Insert(CASSETTE_filename)) {
CASSETTE_status = CASSETTE_STATUS_NONE;
Log_print("Cannot open cassette image %s", CASSETTE_filename);
}
else if (protect)
CASSETTE_ToggleWriteProtect();
}
return TRUE;
}
void CASSETTE_Exit(void)
{
CASSETTE_Remove();
}
int CASSETTE_Insert(const char *filename)
{
int writable;
char const *description;
IMG_TAPE_t *file = IMG_TAPE_Open(filename, &writable, &description);
if (file == NULL)
return FALSE;
CASSETTE_Remove();
cassette_file = file;
/* Guard against providing CASSETTE_filename as parameter. */
if (CASSETTE_filename != filename)
strcpy(CASSETTE_filename, filename);
eof_of_tape = 0;
CASSETTE_status = (writable ? CASSETTE_STATUS_READ_WRITE : CASSETTE_STATUS_READ_ONLY);
event_time_left = 0;
pending_serin = FALSE;
passing_gap = FALSE;
if (description != NULL)
Util_strlcpy(CASSETTE_description, description, sizeof(CASSETTE_description));
CASSETTE_write_protect = FALSE;
CASSETTE_record = FALSE;
UpdateFlags();
cassette_gapdelay = 0;
return TRUE;
}
void CASSETTE_Remove(void)
{
if (cassette_file != NULL) {
IMG_TAPE_Close(cassette_file);
cassette_file = NULL;
}
CASSETTE_status = CASSETTE_STATUS_NONE;
CASSETTE_description[0] = '\0';
UpdateFlags();
}
int CASSETTE_CreateCAS(const char *filename, const char *description) {
IMG_TAPE_t *file = IMG_TAPE_Create(filename, description);
if (file == NULL)
return FALSE;
CASSETTE_Remove(); /* Unmount any previous tape image. */
cassette_file = file;
Util_strlcpy(CASSETTE_filename, filename, sizeof(CASSETTE_filename));
if (description != NULL)
Util_strlcpy(CASSETTE_description, description, sizeof(CASSETTE_description));
CASSETTE_status = CASSETTE_STATUS_READ_WRITE;
event_time_left = 0;
pending_serin = FALSE;
passing_gap = FALSE;
cassette_gapdelay = 0;
eof_of_tape = 0;
CASSETTE_record = TRUE;
CASSETTE_write_protect = FALSE;
UpdateFlags();
return TRUE;
}
unsigned int CASSETTE_GetPosition(void)
{
if (cassette_file == NULL)
return 0;
return IMG_TAPE_GetPosition(cassette_file) + 1;
}
unsigned int CASSETTE_GetSize(void)
{
if (cassette_file == NULL)
return 0;
return IMG_TAPE_GetSize(cassette_file);
}
void CASSETTE_Seek(unsigned int position)
{
if (cassette_file != NULL) {
if (position > 0)
position --;
IMG_TAPE_Seek(cassette_file, position);
event_time_left = 0;
pending_serin = FALSE;
passing_gap = FALSE;
eof_of_tape = 0;
CASSETTE_record = FALSE;
UpdateFlags();
}
}
int CASSETTE_GetByte(void)
{
return serin_byte;
}
int CASSETTE_IOLineStatus(void)
{
/* if motor off and EOF return always 1 (equivalent the mark tone) */
if (!CASSETTE_readable || CASSETTE_record) {
return 1;
}
return IMG_TAPE_SerinStatus(cassette_file, event_time_left);
}
void CASSETTE_PutByte(int byte)
{
if (!ESC_enable_sio_patch && CASSETTE_writable && CASSETTE_record)
IMG_TAPE_WriteByte(cassette_file, byte, POKEY_AUDF[POKEY_CHAN3] + POKEY_AUDF[POKEY_CHAN4]*0x100);
}
void CASSETTE_TapeMotor(int onoff)
{
if (cassette_motor != onoff) {
if (CASSETTE_record && CASSETTE_writable)
/* Recording disabled, flush the tape */
IMG_TAPE_Flush(cassette_file);
cassette_motor = onoff;
UpdateFlags();
}
}
int CASSETTE_ToggleWriteProtect(void)
{
if (CASSETTE_status != CASSETTE_STATUS_READ_WRITE)
return FALSE;
CASSETTE_write_protect = !CASSETTE_write_protect;
UpdateFlags();
return TRUE;
}
int CASSETTE_ToggleRecord(void)
{
if (CASSETTE_status == CASSETTE_STATUS_NONE)
return FALSE;
CASSETTE_record = !CASSETTE_record;
if (CASSETTE_record)
eof_of_tape = FALSE;
else if (CASSETTE_writable)
/* Recording disabled, flush the tape */
IMG_TAPE_Flush(cassette_file);
event_time_left = 0;
pending_serin = FALSE;
passing_gap = FALSE;
UpdateFlags();
/* Return FALSE to indicate that recording will not work. */
return !CASSETTE_record || (CASSETTE_status == CASSETTE_STATUS_READ_WRITE && !CASSETTE_write_protect);
}
static void CassetteWrite(int num_ticks)
{
if (CASSETTE_writable)
IMG_TAPE_WriteAdvance(cassette_file, num_ticks);
}
/* Sets the stamp of next SERIN IRQ event and loads new record if necessary.
Returns TRUE if a new byte was loaded and POKEY_SERIN should be updated.
The function assumes that current_block <= max_block. */
static int CassetteRead(int num_ticks)
{
if (CASSETTE_readable) {
int loaded = FALSE; /* Function's return value */
event_time_left -= num_ticks;
while (event_time_left < 0) {
unsigned int length;
if (!passing_gap && pending_serin) {
serin_byte = pending_serin_byte;
/* A byte is loaded, return TRUE so it gets stored in POKEY_SERIN. */
loaded = TRUE;
}
/* If POKEY is in reset state, no serial I/O occurs. */
pending_serin = (POKEY_SKCTL & 0x03) != 0;
if (!IMG_TAPE_Read(cassette_file, &length, &passing_gap, &pending_serin_byte)) {
eof_of_tape = 1;
UpdateFlags();
return loaded;
}
event_time_left += length;
}
return loaded;
}
return FALSE;
}
int CASSETTE_AddScanLine(void)
{
/* increment elapsed cassette time */
if (CASSETTE_record) {
CassetteWrite(114);
return FALSE;
} else
return CassetteRead(114);
}
void CASSETTE_ResetPOKEY(void)
{
/* Resetting POKEY stops any serial transmission. */
pending_serin = FALSE;
pending_serin_byte = 0xff;
}
/* --- Functions for loading/saving with SIO patch --- */
int CASSETTE_AddGap(int gaptime)
{
cassette_gapdelay += gaptime;
if (cassette_gapdelay < 0)
cassette_gapdelay = 0;
return cassette_gapdelay;
}
/* Indicates that a loading leader is expected by the OS */
void CASSETTE_LeaderLoad(void)
{
if (CASSETTE_record)
CASSETTE_ToggleRecord();
CASSETTE_TapeMotor(TRUE);
cassette_gapdelay = 9600;
/* registers for SETVBV: third system timer, ~0.1 sec */
CPU_regA = 3;
CPU_regX = 0;
CPU_regY = 5;
}
/* indicates that a save leader is written by the OS */
void CASSETTE_LeaderSave(void)
{
if (!CASSETTE_record)
CASSETTE_ToggleRecord();
CASSETTE_TapeMotor(TRUE);
cassette_gapdelay = 19200;
/* registers for SETVBV: third system timer, ~0.1 sec */
CPU_regA = 3;
CPU_regX = 0;
CPU_regY = 5;
}
int CASSETTE_ReadToMemory(UWORD dest_addr, int length)
{
CASSETTE_TapeMotor(1);
if (!CASSETTE_readable)
return 0;
/* Convert wait_time to ms ( wait_time * 1000 / 1789790 ) and subtract. */
cassette_gapdelay -= event_time_left / 1789; /* better accuracy not needed */
if (!IMG_TAPE_SkipToData(cassette_file, cassette_gapdelay)) {
/* Ignore the eventual error, assume it is the end of file */
cassette_gapdelay = 0;
eof_of_tape = 1;
UpdateFlags();
return 0;
}
cassette_gapdelay = 0;
/* Load bytes */
switch (IMG_TAPE_ReadToMemory(cassette_file, dest_addr, length)) {
case TRUE:
return TRUE;
case -1: /* Read error/EOF */
eof_of_tape = 1;
UpdateFlags();
/* FALLTHROUGH */
default: /* case FALSE */
return FALSE;
}
}
int CASSETTE_WriteFromMemory(UWORD src_addr, int length)
{
int result;
CASSETTE_TapeMotor(1);
if (!CASSETTE_writable)
return 0;
result = IMG_TAPE_WriteFromMemory(cassette_file, src_addr, length, cassette_gapdelay);
cassette_gapdelay = 0;
return result;
}
/*
vim:ts=4:sw=4:
*/