/* * img_tape.c - support for CAS and raw tape images * * 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 #include #include #include "atari.h" #include "cassette.h" #include "img_tape.h" #include "memory.h" #include "sio.h" #include "util.h" enum { MAX_BLOCKS = 2048 }; /* Standard record length, needed by ReadRecord() when reading raw files */ enum { DEFAULT_BUFFER_SIZE = 132 }; /* Baudrate for all written blocks and for reading from raw files. */ enum { DEFAULT_BAUDRATE = 600 }; struct IMG_TAPE_t { FILE *file; /* Stream for reading/writing of the tape image */ int isCAS; /* Indicates if the file is in CAS format, or a raw binary file */ UBYTE *buffer; /* Holds bytes of the last read or currently written data block */ size_t buffer_size; /* Size of the space allocated for BUFFER */ ULONG savetime; /* Time elapsed since last byte writing, in CPU ticks */ ULONG save_gap; /* Length of the IRG before the currently written block */ int next_blockbyte; /* Index of the byte in this block that will be read next (counted from 0) */ unsigned int current_block; /* Number of the currently-read/written block (counted from 0) */ int block_is_fsk; /* FALSE - current chunk's type is "data", otherwise "fsk " */ int block_length; /* Length of the block currently held in BUFFER */ int num_blocks; /* Number of data blocks in the whole file */ ULONG block_offsets[MAX_BLOCKS]; /* File offsets for each data block*/ int block_baudrates[MAX_BLOCKS]; /* Baudrates for each data block in the file */ char description[CASSETTE_DESCRIPTION_MAX]; /* Tape description, only for CAS files */ int was_writing; /* Indicated if the last operation on the file was writing */ }; typedef struct { char identifier[4]; UBYTE length_lo; UBYTE length_hi; UBYTE aux_lo; UBYTE aux_hi; } CAS_Header; /* Just for remembering - CAS format in short: It consists of chunks. Each chunk has a header, possibly followed by data. If a header is unknown or unexpected it may be skipped by the length of the header (8 bytes), and additionally the length given in the length-field(s). There are (until now) 3 types of chunks: -CAS file marker, has to be at begin of file - identifier "FUJI", length is number of characters of an (optional) ascii-name (without trailing 0), aux is always 0. -baud rate selector - identifier "baud", length always 0, aux is rate in baud (usually 600; one byte is 8 bits + startbit + stopbit, makes 60 bytes per second). -data record - identifier "data", length is length of the data block (usually $84 as used by the OS), aux is length of mark tone (including leader and gaps) just before the record data in milliseconds. -raw signal stream - identifier "fsk ", length and aux the same as in "data" chunk. Each 2 bytes in this chunk are a 16-bit number that represents length of a MARK or SPACE signal in 1/10s of milliseconds. (So, the chunk contains length/2 words.) The chunk starts with the SPACE signal (first 2 bytes), then the MARK signal (next 2 bytes) and alternates between SPACE and MARK till the end of the chunk. */ int IMG_TAPE_FileSupported(UBYTE const start_bytes[4]) { /* Note: doesn't detect raw binary files. */ return start_bytes[0] == 'F' && start_bytes[1] == 'U' && start_bytes[2] == 'J' && start_bytes[3] == 'I'; } /* Write contents of the file's block buffer to file, as a separate record; then empty the buffer. Returns TRUE on success or FALSE on write error. */ static int WriteRecord(IMG_TAPE_t *file) { CAS_Header header; int result; /* on a raw file, saving is denied because it can hold only 1 file and could cause confusion */ if (!file->isCAS) return FALSE; /* always append */ if (fseek(file->file, file->block_offsets[file->num_blocks], SEEK_SET) != 0) return FALSE; /* write record header */ Util_strncpy(header.identifier, "data", 4); header.length_lo = file->block_length & 0xFF; header.length_hi = (file->block_length >> 8) & 0xFF; header.aux_lo = file->save_gap & 0xff; header.aux_hi = (file->save_gap >> 8) & 0xff; if (fwrite(&header, 1, 8, file->file) != 8) return FALSE; /* Saving is supported only with standard baudrate. */ file->block_baudrates[file->num_blocks] = DEFAULT_BAUDRATE; file->num_blocks++; file->block_offsets[file->num_blocks] = file->block_offsets[file->num_blocks - 1] + file->block_length + 8; file->current_block = file->num_blocks; /* write record */ result = fwrite(file->buffer, 1, file->block_length, file->file) == file->block_length; if (result) { file->save_gap = 0; file->block_length = 0; } return result; } /* Flush any unwritten data to tape. */ static int CassetteFlush(IMG_TAPE_t *file) { if (file->block_length > 0) return WriteRecord(file) && fflush(file->file) == 0; return TRUE; } IMG_TAPE_t *IMG_TAPE_Open(char const *filename, int *writable, char const **description) { IMG_TAPE_t *img; CAS_Header header; img = (IMG_TAPE_t *)Util_malloc(sizeof(IMG_TAPE_t)); /* Check if the file is writable. If not, recording will be disabled. */ img->file = fopen(filename, "rb+"); *writable = img->file != NULL; /* If opening for reading+writing failed, reopen it as read-only. */ if (img->file == NULL) img->file = fopen(filename, "rb"); if (img->file == NULL) { free(img); return NULL; } img->description[0] = '\0'; if (fread(&header, 1, 6, img->file) == 6 && header.identifier[0] == 'F' && header.identifier[1] == 'U' && header.identifier[2] == 'J' && header.identifier[3] == 'I') { /* CAS file */ UWORD length; UWORD skip; int blocks; int baudrate = DEFAULT_BAUDRATE; img->isCAS = TRUE; fseek(img->file, 2L, SEEK_CUR); /* ignore the aux bytes */ /* read or skip file description */ skip = length = header.length_lo | (header.length_hi << 8); if (length < CASSETTE_DESCRIPTION_MAX) skip = 0; else skip -= CASSETTE_DESCRIPTION_MAX - 1; if (fread(img->description, 1, length - skip, img->file) < (length - skip)) { fclose(img->file); free(img); return NULL; } img->description[length - skip] = '\0'; fseek(img->file, skip, SEEK_CUR); /* count number of blocks */ blocks = 0; img->block_baudrates[0] = DEFAULT_BAUDRATE; img->block_offsets[0] = ftell(img->file); for (;;) { /* chunk header is always 8 bytes */ if (fread(&header, 1, 8, img->file) != 8) break; length = header.length_lo + (header.length_hi << 8); if (header.identifier[0] == 'b' && header.identifier[1] == 'a' && header.identifier[2] == 'u' && header.identifier[3] == 'd') { baudrate=header.aux_lo + (header.aux_hi << 8); img->block_offsets[blocks] += length + 8; } else if ((header.identifier[0] == 'd' && header.identifier[1] == 'a' && header.identifier[2] == 't' && header.identifier[3] == 'a') || (header.identifier[0] == 'f' && header.identifier[1] == 's' && header.identifier[2] == 'k' && header.identifier[3] == ' ')) { img->block_baudrates[blocks] = baudrate; if (++blocks >= MAX_BLOCKS) { --blocks; break; } img->block_offsets[blocks] = img->block_offsets[blocks - 1] + length + 8; } /* skip possibly present data block */ fseek(img->file, length, SEEK_CUR); } img->num_blocks = blocks; *description = img->description; } else { /* raw file */ int file_length = Util_flen(img->file); img->num_blocks = ((file_length + 127) >> 7) + 1; img->isCAS = FALSE; *writable = FALSE; /* Writing raw files is not supported */ *description = NULL; } img->savetime = 0; img->save_gap = 0; img->next_blockbyte = 0; img->block_length = 0; img->current_block = 0; img->buffer = (UBYTE *)Util_malloc((img->buffer_size = DEFAULT_BUFFER_SIZE) * sizeof(UBYTE)); img->was_writing = FALSE; return img; } void IMG_TAPE_Close(IMG_TAPE_t *file) { if (file->was_writing) CassetteFlush(file); fclose(file->file); free(file->buffer); free(file); } IMG_TAPE_t *IMG_TAPE_Create(char const *filename, char const *description) { IMG_TAPE_t *img; CAS_Header header; size_t desc_len; FILE *file = NULL; /* create new file */ file = fopen(filename, "wb+"); if (file == NULL) return NULL; /* Write the initial FUJI and baud blocks of the CAS file. */ desc_len = strlen(description); memset(&header, 0, sizeof(header)); /* write CAS-header */ header.length_lo = (UBYTE) desc_len; header.length_hi = (UBYTE) (desc_len >> 8); if (fwrite("FUJI", 1, 4, file) != 4 || fwrite(&header.length_lo, 1, 4, file) != 4 || fwrite(description, 1, desc_len, file) != desc_len) { fclose(file); return NULL; } memset(&header, 0, sizeof(header)); /* All records are written with 600 baud speed. */ header.aux_lo = DEFAULT_BAUDRATE & 0xff; header.aux_hi = DEFAULT_BAUDRATE >> 8; if (fwrite("baud", 1, 4, file) != 4 || fwrite(&header.length_lo, 1, 4, file) != 4) { fclose(file); return NULL; } img = (IMG_TAPE_t *)Util_malloc(sizeof(IMG_TAPE_t)); img->file = file; if (description != NULL) Util_strlcpy(img->description, description, CASSETTE_DESCRIPTION_MAX); img->isCAS = TRUE; img->savetime = 0; img->save_gap = 0; img->next_blockbyte = 0; img->block_length = 0; img->current_block = 0; img->num_blocks = 0; img->block_offsets[0] = strlen(description) + 16; img->buffer = (UBYTE *)Util_malloc((img->buffer_size = DEFAULT_BUFFER_SIZE) * sizeof(UBYTE)); img->was_writing = TRUE; return img; } /* Enlarge file->buffer to (at least) SIZE if needed. */ static void EnlargeBuffer(IMG_TAPE_t *file, size_t size) { if (file->buffer_size < size) { /* Enlarge the buffer at least 2 times. */ file->buffer_size *= 2; if (file->buffer_size < size) file->buffer_size = size; file->buffer = (UBYTE *)Util_realloc(file->buffer, file->buffer_size * sizeof(UBYTE)); } } /* Read a record from the file. FALSE on error/EOF. Writes length of pre-record gap (in ms) into *gap. */ static int ReadNextRecord(IMG_TAPE_t *file, int *gap) { int length; /* 0 indicates that there was no previous block being read and current_block already contains the current block number. */ if (file->block_length != 0) { /* Non-zero - a block was being read and it's finished, increase the block number. */ file->block_length = 0; if (++file->current_block >= file->num_blocks) /* Last block was already read. */ return FALSE; } if (file->isCAS) { CAS_Header header; if (fseek(file->file, file->block_offsets[file->current_block], SEEK_SET) != 0 || fread(&header, 1, 8, file->file) < 8) return FALSE; /* Determine chunk type - can be either "fsk " or "data". */ file->block_is_fsk = header.identifier[0] == 'f' && header.identifier[1] == 's' && header.identifier[2] == 'k' && header.identifier[3] == ' '; length = header.length_lo + (header.length_hi << 8); *gap = header.aux_lo + (header.aux_hi << 8); /* read block into buffer */ EnlargeBuffer(file, length); if (fread(file->buffer, 1, length, file->file) < length) return FALSE; } else { file->block_is_fsk = FALSE; length = 132; /* Don't enlarge buffer - its default size is at least 132. */ *gap = (file->current_block == 0 ? 19200 : 260); file->buffer[0] = 0x55; file->buffer[1] = 0x55; if (file->current_block + 1 >= file->num_blocks) { /* EOF record */ file->buffer[2] = 0xfe; memset(file->buffer + 3, 0, 128); } else { int bytes; if (fseek(file->file, file->current_block * 128, SEEK_SET) != 0 || (bytes = fread(file->buffer + 3, 1, 128, file->file)) == 0) return FALSE; if (bytes < 128) { file->buffer[2] = 0xfa; /* non-full record */ memset(file->buffer + 3 + bytes, 0, 127 - bytes); file->buffer[0x82] = bytes; } else file->buffer[2] = 0xfc; /* full record */ } file->buffer[0x83] = SIO_ChkSum(file->buffer, 0x83); } file->block_length = length; return TRUE; } int IMG_TAPE_Read(IMG_TAPE_t *file, unsigned int *duration, int *is_gap, UBYTE *byte) { if (file->was_writing) { CassetteFlush(file); file->was_writing = FALSE; } if (file->next_blockbyte >= file->block_length) { /* Buffer is exhausted, load next record. */ int gap; if (!ReadNextRecord(file, &gap)) return FALSE; file->next_blockbyte = 0; if (gap > 0) { /* Convert gap from ms to CPU ticks. */ *duration = gap * 1789 + gap * 790 / 1000; /* (gap * 1789790 / 1000), avoiding overflow */ *is_gap = TRUE; return TRUE; } } if (file->block_is_fsk) { /* Compose a 16-bit word with length of a signal in 1/10 of ms. */ unsigned int len = file->buffer[file->next_blockbyte++]; len |= ((unsigned int)file->buffer[file->next_blockbyte++]) << 8; /* Convert len from 1/10ms to CPU ticks. */ *duration = len * 178 + len * 9790 / 10000; /* (len * 1789790 / 10000), avoiding overflow */ *is_gap = TRUE; } else { *byte = file->buffer[file->next_blockbyte++]; *is_gap = FALSE; /* Next event will be after 10 bits of data gets loaded. */ *duration = 10 * 1789790 / (file->isCAS ? file->block_baudrates[file->current_block] : 600); } return TRUE; } void IMG_TAPE_WriteAdvance(IMG_TAPE_t *file, unsigned int num_ticks) { if (!file->was_writing) { file->savetime = 0; file->save_gap = 0; file->next_blockbyte = 0; file->block_length = 0; file->was_writing = TRUE; /* Always append to end of file. */ file->current_block = file->num_blocks; } file->savetime += num_ticks; } int IMG_TAPE_WriteByte(IMG_TAPE_t *file, UBYTE byte, unsigned int pokey_counter) { /* put_delay is time between end of last byte write / motor start, and start of writing of current BYTE (in ms). */ /* Note: byte duration in seconds: pokey_counter / (1789790/2) * 10 * in milliseconds: pokey_counter * 10 * 1000 / 1789790/2 */ int put_delay = file->savetime /1790 - 10 * pokey_counter / 895; /* better accuracy not needed */ if (put_delay > 05) { /* write previous block */ if (file->block_length > 0) { if (!WriteRecord(file)) return FALSE; /* Write error */ } /* set new gap-time */ file->save_gap += put_delay; } /* put byte into buffer */ EnlargeBuffer(file, file->block_length + 1); file->buffer[file->block_length++] = byte; /* set new last byte-put time */ file->savetime = 0; return TRUE; } int IMG_TAPE_Flush(IMG_TAPE_t *file) { if (file->was_writing) return CassetteFlush(file); return TRUE; } /* Returns position in blocks/samples, counted from 0. */ unsigned int IMG_TAPE_GetPosition(IMG_TAPE_t *file) { return file->current_block; } /* Returns size in blocks/samples. */ unsigned int IMG_TAPE_GetSize(IMG_TAPE_t *file) { return file->num_blocks; } void IMG_TAPE_Seek(IMG_TAPE_t *file, unsigned int position) { if (file->was_writing) { CassetteFlush(file); file->was_writing = FALSE; } file->current_block = (int)position; if (file->current_block > file->num_blocks) file->current_block = file->num_blocks; file->savetime = 0; file->save_gap = 0; file->next_blockbyte = 0; file->block_length = 0; } int IMG_TAPE_SerinStatus(IMG_TAPE_t *file, int event_time_left) { if (file->was_writing || file->next_blockbyte == 0) return 1; if (file->block_is_fsk) { /* Signal can be computed from current position in the block - first 2 bytes area SPACE, each next 2 bytes alternate between MARK and SPACE. */ return (~(file->next_blockbyte / 2) & 1); } else { int bit = 0; /* 0: stop bit, 1: 7th bit, ..., 8: 0th bit, 9: start bit */ /* exam rate; if time_to_irq < duration of one byte */ if (event_time_left < 10 * 1789790 / (file->isCAS ? file->block_baudrates[file->current_block] : 600) - 1) { bit = event_time_left / (1789790 / (file->isCAS ? file->block_baudrates[file->current_block] : 600)); } else { bit = 0; } /* if stopbit or out of range, return mark tone */ if ((bit <= 0) || (bit > 9)) return 1; /* if start bit, return space tone */ if (bit == 9) return 0; /* eval tone to return */ return (file->buffer[file->next_blockbyte - 1] >> (8 - bit)) & 1; } } int IMG_TAPE_SkipToData(IMG_TAPE_t *file, int ms) { if (file->was_writing) { CassetteFlush(file); file->was_writing = FALSE; } while (ms > 0) { if (file->next_blockbyte < file->block_length) { if (file->block_is_fsk) { /* FSK blocks are not supported during reads with patched SIO, and skipped as a whole. */ file->next_blockbyte = file->block_length; } else { int bytes = ms * (file->isCAS ? file->block_baudrates[file->current_block] : 600) / 1000 / 10; if (bytes > file->block_length - file->next_blockbyte) bytes = file->block_length - file->next_blockbyte; file->next_blockbyte += bytes; ms -= bytes * 10 * 1000 / (file->isCAS ? file->block_baudrates[file->current_block] : 600); } continue; } else { int gap; if (!ReadNextRecord(file, &gap)) return FALSE; file->next_blockbyte = 0; ms -= gap; } } return TRUE; } int IMG_TAPE_ReadToMemory(IMG_TAPE_t *file, UWORD dest_addr, int length) { int read_length; if (file->was_writing) { CassetteFlush(file); file->was_writing = FALSE; } read_length = file->block_length - file->next_blockbyte; if (read_length == 0) { /* No bytes left in current block, need to read next block. */ int gap; if (!ReadNextRecord(file, &gap)) /* EOF or read error */ return -1; file->next_blockbyte = 0; } if (file->block_is_fsk) /* FSK blocks are not supported during reads with patched SIO, and always cause read failure. */ return FALSE; /* Copy record to memory, excluding the checksum byte if it exists. */ MEMORY_CopyToMem(file->buffer + file->next_blockbyte, dest_addr, read_length >= length ? length : read_length); file->next_blockbyte += (read_length >= length + 1 ? length + 1 : read_length); return read_length >= length + 1 && file->buffer[length] == SIO_ChkSum(file->buffer, length); } int IMG_TAPE_WriteFromMemory(IMG_TAPE_t *file, UWORD src_addr, int length, int gap) { if (!file->was_writing) { file->savetime = 0; file->save_gap = 0; file->next_blockbyte = 0; file->block_length = 0; file->was_writing = TRUE; } EnlargeBuffer(file, length + 1); /* Put record into buffer. */ MEMORY_CopyFromMem(src_addr, file->buffer, length); /* Eval checksum over buffer data. */ file->buffer[length] = SIO_ChkSum(file->buffer, length); file->save_gap = gap; file->block_length = length + 1; return WriteRecord(file); }