/*
Convert ZX81 and ZX80 tape files into wav files.
Copyright 2010 OL
This program 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 3 of the License, or
(at your option) any later version.
This program 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 this program. If not, see .
*/
#include
#include
#include
#include
/*
Info as available in www: The tapes for the ZX81: A "0" bit consists of four high pulses, a "1" bit
of nine high pulses. Both are followed by a silence period. Each pulse is split into a 150us
high period, and a 150us low period. The duration of the silence between each bit is 1300us.
The baud rate is thus 400 bps (for a "0" filled area) down to 250 bps (for a "1" filled area).
Average medium transfer rate is approximately 307 bps (38 bytes/sec) for files that contain
50% of "0" and "1" bits each.
*/
/* Handy shortcut */
typedef unsigned char u8;
/* Constants */
#define MAX_ZXNAME_LENGTH 8
#define MAX_PATH_LENGTH 200
#define MAX_P_FILE_SIZE 65544
/* Variables with file scope */
/* Some of these could als be defined as constants as they are never actually modified.
However, future version may make these parameters configurable */
static u8 verbose=1;
static u8 *pFile=0;
static u8 *wavFile=0;
static long wavCounter=0;
static float sampleRate=22050;
static u8 pauseLevel=5;
static u8 pulseLevel=250;
static u8 middleLevel=128;
static struct
{
u8 PulsesOne; /* Number of pulses for a "one" bit */
u8 PulsesZero; /* Number of pulses for a "zero" bit */
double HalfPulsDuration;/* Duration of a pulse (half cycle) */
double InterbitDelay; /* Duration of silence between two bit's pulses */
double IntroDuration;
double ExtroDuration;
}
Signal = {9, 4, 0.00015, 0.0013, 1.6, 0.4};
static enum
{
TARGET_UNKNOWN,
TARGET_ZX80,
TARGET_ZX81
}
targetFormat=TARGET_UNKNOWN;
static u8 outFileName[MAX_PATH_LENGTH+2]; /* Path and name with extension for the output */
static u8 zxFileName[MAX_ZXNAME_LENGTH+1]; /* ZX File name in ASCII codes */
/* Functions with file scope */
/* Helper: Estimate the duration of the wave signal that needs to be created.
No problem if estimation is a bit longer than then result, but it shoult never
be too short, as memory allocation is based on this estimation */
static double EstimateWavDuration(u8* file, int filesize)
{
double dur=0;
int count_0=0,count_1=0;
int i,b;
u8 mask;
/* count the '1' bits, the remaining must be '0' bits */
for(i=0;i filesize) return 0; /* File too short */
if(file[actual_e_line-OFFSET-1]!=0x80) return 0; /* Must end with 128 */
return 1;
}
/* Check whether the given file looks a ZX81 file, return 0 if not */
static u8 DoesFileLookLikeZx81(u8* file, int filesize)
{
const int OFFSET=0x4009; /* Address offset for the ZX80 data */
const int SYSVAR_E_LINE=0x4014; /* Address offset for the ZX80 data */
int actual_e_line=file[SYSVAR_E_LINE-OFFSET];
actual_e_line+= file[SYSVAR_E_LINE-OFFSET+1] << 8;
if(actual_e_line-OFFSET > filesize) return 0; /* File too short */
if(file[actual_e_line-OFFSET-1]!=0x80) return 0; /* Must end with 128 */
if(file[0]!=0) return 0; /* Must start with version code 0 */
return 1;
}
/* Deduce the output- and ZX file names from the input file name.
Output file will just a different extension. ZX file name does
neither include the path nor the extension, and is only generated
if the user did not explicitly give a name through the command line.
*/
static void TransferFileNames(u8* infilename)
{
int wrpos=0,i=0;
int namestart=0;
int extdotpos=0;
/* parse input file name, first look for path separators and extension */
while(i=extdotpos)
{
break;
}
outFileName[i]=infilename[i];
i++;
}
if(i) /* if there is space left, add the extension */
{
if(targetFormat==TARGET_ZX80) i+=sprintf(&outFileName[i],"_ZX80");
i+=sprintf(&outFileName[i],".wav");
}
outFileName[i++]=0;
/* copy to ZX file name if we do not already have a name */
if(0==zxFileName[0])
{
i=namestart;
wrpos=0;
while(i=MAX_ZXNAME_LENGTH)
{
/* filename too long, invalidate */
wrpos=0;
break;
}
zxFileName[wrpos++]=toupper(infilename[i++]);
}
zxFileName[wrpos++]=0;
}
}
/* Free all allocated memory */
static void FreeMem(void)
{
if(pFile)
{
free(pFile);
pFile=0;
}
if(wavFile)
{
free(wavFile);
pFile=0;
}
}
/* Helper for file name conversion, will set the provided error flag if the input is invalid */
static int ConvertAsciiToZxCode(int ascii_char, u8* error_flag)
{
const char* CODETABLE="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /* ZX81 code table, starting at 28 */
u8 zx_code=0;
u8 upper_ascii_c=toupper(ascii_char);
while(CODETABLE[zx_code])
{
if(CODETABLE[zx_code]==upper_ascii_c) return 28+zx_code; /* Exit with result on match */
zx_code++;
}
/* if we end up here, we have an invalid char as input. Notify caller about the problem */
if(error_flag) *error_flag|=1;
/* return a default */
return 0;
}
/* Write part of the wave with constant signal level and given duration */
static void OutputLevel(u8 level, double duration)
{
static double wavWriteTime=0.0;
/* reset and sync timing if we are at start of file */
if(0==wavCounter) wavWriteTime=0;
float sample_t = 1.0/sampleRate;
wavWriteTime+=duration; /* calculate exact desired end time */
/* write till time expires */
while(wavWriteTime > wavCounter*sample_t)
{
wavFile[wavCounter++]=level;
}
}
/* Output a logic bit */
static void OutputBit(u8 is_one)
{
u8 i;
/* silence */
OutputLevel(middleLevel, Signal.InterbitDelay);
/* pulses */
for(i=0; i < (is_one ? Signal.PulsesOne:Signal.PulsesZero); i++ )
{
OutputLevel(pulseLevel, Signal.HalfPulsDuration);
OutputLevel(pauseLevel, Signal.HalfPulsDuration);
}
}
/* Output a full byte */
static void OutputByte(u8 the_byte)
{
u8 mask=0x80;
while(1)
{
OutputBit(the_byte&mask);
if(mask==1) break;
mask>>=1;
}
}
/* Helper: Write numbers to a file in correct order, independent of whether
this software runs on a little endian or big endian machine
- Second parameter: Value to write
- Third parameter is the size of the value to write, 2 for a short 16-bit integer, 4 for 32 bit, etc.
*/
static void WriteInt(FILE* wrfile, long val, int bytes)
{
int i,byte;
/* send byte chunks */
for(i=0;i>=8;
}
}
/* Write the WAV file header as required by the WAV file format */
static void WriteWavHeader(FILE* wrfile, long wavsize)
{
fputs("RIFF",wrfile);
WriteInt(wrfile, wavsize+44-8 ,4); /* file length - 8 */
fputs("WAVE",wrfile);
fputs("fmt ",wrfile);
WriteInt(wrfile, 16 ,4); /* header size */
WriteInt(wrfile, 1 ,2); /* format tag 1=PCM */
WriteInt(wrfile, 1 ,2); /* channels, 1=mono */
WriteInt(wrfile, (long)sampleRate ,4); /* samples/sec */
WriteInt(wrfile, (long)sampleRate ,4); /* bytes/sec */
WriteInt(wrfile, 1 ,2); /* block align */
WriteInt(wrfile, 8 ,2); /* bits/sample */
fputs("data",wrfile); /* data block */
WriteInt(wrfile, wavsize ,4); /* data block size */
}
static void PrintUsage(char *programName)
{
fprintf(stderr,"\nConvert a ZX81 or ZX80 software file (*.p or *.80) to a wav file (*.wav). The\n");
fprintf(stderr,"wav output can be used to load software into your ZX by connecting it to the\n");
fprintf(stderr,"soundcard of the PC (or any other wav player) [ver0.03ol]\n");
fprintf(stderr,"\nUsage (command line):\n\n");
fprintf(stderr,"%s [options] filename\n", programName);
fprintf(stderr,"\nOptions: \n");
fprintf(stderr," -zNAME Use NAME for ZX file name (the name as used by the ZX81)\n");
fprintf(stderr," -80 Assume file is for ZX80 target (no autodetect)\n");
fprintf(stderr," -81 Assume file is for ZX81 target (no autodetect)\n");
fprintf(stderr, "\nUsage (Windows Explorer):\n\n");
fprintf(stderr,"Just drag-and-drop the *.p file onto this program's symbol.\n");
fprintf(stderr,"\nHint: Connect ZX EAR using a mono jack plug, a stereo plug may not work.\n");
}
/* Returns the index of the first argument that is not an option; i.e.
does not start with a dash or a slash
*/
int HandleOptions(int argc,char *argv[])
{
int i,j,firstnonoption=0;
for (i=1; i< argc;i++) {
if (argv[i][0] == '/' || argv[i][0] == '-') {
switch (argv[i][1]) {
/* An argument -? means help is requested */
case 'h':
case 'H':
case '?':
PrintUsage(argv[0]);
break;
case 'z': /* ZX filename given */
case 'f': /* alias */
j=0;
while(jnamearg+1)
{
fprintf(stderr,"ERROR: Invalid number of arguments\n");
PrintUsage(argv[0]);
return 1;
}
/* allocate memory for input file */
if (!(pFile=malloc(MAX_P_FILE_SIZE)))
{
fprintf (stderr,"ERROR: Out of memory\n");
return 1;
}
if(verbose)
{
printf("Input file name : %s\n",argv[namearg]);
}
if (!(rdfile=fopen (argv[namearg], "rb")))
{
fprintf (stderr,"ERROR: Can not read file %s\n", argv[namearg]);
return 1;
}
int pfile_size=fread(pFile,sizeof(u8),MAX_P_FILE_SIZE,rdfile);
fclose(rdfile);
if(verbose) printf("Input file has size of %d bytes\n",pfile_size);
/* check target if not yet determined */
if(targetFormat==TARGET_UNKNOWN)
{
if(DoesFileLookLikeZx81(pFile, pfile_size))
{
if(verbose) printf("Input file is ZX81 format\n");
targetFormat=TARGET_ZX81;
}
else if(DoesFileLookLikeZx80(pFile, pfile_size))
{
if(verbose) printf("Input file is ZX80 format\n");
targetFormat=TARGET_ZX80;
}
}
/* get output file names */
TransferFileNames(argv[namearg]);
if(0==zxFileName[0])
{
fprintf(stderr,"ERROR: Invalid zx file name\n");
return 1;
}
if(verbose)
{
printf("Output file name : %s\n",outFileName);
printf("ZX file name : %s\n",zxFileName);
}
/* reserve mem for the output file */
double maxdur=EstimateWavDuration(pFile, pfile_size);
if(verbose) printf("Estimated duration of wav output: %f seconds\n",maxdur);
wavfile_size=maxdur*sampleRate;
/* allocate memory for wave file */
if (!(wavFile=malloc(wavfile_size+20))) /* few bytes safety overhead */
{
fprintf (stderr,"ERROR: Out of memory\n");
return 1;
}
/* now generate the wave */
wavCounter=0;
/* the silent intro */
OutputLevel(middleLevel, Signal.IntroDuration);
/* the name, but not for zx80 */
if(targetFormat!=TARGET_ZX80)
{
for(i=0;;i++)
{
u8 err=0;
u8 zxchar=ConvertAsciiToZxCode(zxFileName[i],&err);
if(err)
{
fprintf (stderr,"ERROR: ZX Filename contains invalid character '%c'\n",zxFileName[i]);
return 1;
}
if(zxFileName[i+1]==0 || i+1==MAX_ZXNAME_LENGTH)
{
/* Final character */
zxchar|=0x80;
OutputByte(zxchar);
break;
}
OutputByte(zxchar);
}
}
/* the data */
for(i=0;i