/* 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