diff --git a/changes.txt b/changes.txt new file mode 100644 index 0000000..be4c860 --- /dev/null +++ b/changes.txt @@ -0,0 +1,17 @@ +1.1 (21.4.2011) + ++ Added -l option to pzx2txt and annotate_pulses.pl script, allowing + convenient annotation of pulses with their initial pulse level. + +1.0 (21.8.2007) + +* Fixed handling of incomplete command line arguments. + +1.0 rc 2 (2.8.2007) + +* Fixed overflow bug in WAV generation, which caused pzx2wav render + all pauses as very short ones. + +1.0 rc 1 (28.6.2007) + ++ First release. diff --git a/docs/pzx_format.txt b/docs/pzx_format.txt new file mode 100644 index 0000000..a2a7935 --- /dev/null +++ b/docs/pzx_format.txt @@ -0,0 +1,499 @@ +PZX (Perfect ZX Tape) file format version 1.0 +============================================= + +The PZX file format consists of a sequence of blocks. Each block has the +following uniform structure: + +offset type name meaning +0 u32 tag unique identifier for the block type. +4 u32 size size of the block in bytes, excluding the tag and size fields themselves. +8 u8[size] data arbitrary amount of block data. + +The block tags are four ASCII letter identifiers indicating how to interpret the +block data. The first letter is stored first in the file (i.e., at +offset 0 of the block), the last letter is stored last (i.e., at offset 3). +This means the tag may be internally conveniently represented either as 32bit +multicharacter constants stored in big endian order or 32bit reversed +multicharacter constant stored in little endian order, whichever way an +implementation prefers. + +All integral values specified by this format are stored in little endian +format, that is, the least significant byte at lower offsets. +All values specified are in decimal, unless prefixed with 0x, +in which case they are hexadecimal. Any bits specified as unused should be +set to zero by encoding implementations and ignored by decoding implementations. + +All strings specified by this format are stored in UTF-8 encoding. However +implementations are not required to render Unicode data at all. Any +implementation may choose to render only the characters it understands and +replace the rest with either a question mark character, or distinctive +hexadecimal representation of the character, whatever the implementor prefers. +For example an implementation which understands ASCII characters only may +render only those ASCII characters in the 32-126 range it understands. Also +note that the minimal compliant implementation is not required to render any +strings at all. + +The sole purpose of the format is to encode sequence of pulses of certain duration. +Pulse level is defined to be either low or high. When loading, low level +corresponds to the bit 6 of port 0xFE reset (EAR off) and high level set (EAR on). +Similarly, when saving, low level corresponds to the bit 3 of port 0xFE reset +(MIC off) and high level to set (MIC on). (Note that O'Hara's ROM Disassembly +book got these EAR/MIC on/off terms incorrectly swapped). The block descriptions below +define the initial level at start of each block (and would do so even for any future +block which might be ever introduced, so an implementation may take this for +granted), and assume the level changes after each pulse generated (even if +its duration is zero). However note that implementations may as well decide +to invert that initial level and invert the level before each pulse, if it +makes them easier to implement. All that matters is that the level of the +pulses themselves remain the same. + +Any durations are expressed in T cycles of standard 48k Spectrum CPU. This means +one T cycle equals 1/3500000 second. It's up to an implementation to convert +the durations appropriately if it may not or does not want to use the T cycles directly. +However note that this is not necessary in case of 128k emulation. In that +case the CPU speed is not substantially different and the ROM I/O routines +use the same timing constants anyway, so an implementation may safely use +the T cycles directly as well. + +Overall regarding duration precision, note that the pulses generated by the +original Spectrum ROM saving routine itself vary by +1, -3, and -1 T cycles +in case of first pulse of first bit of leader, regular, and checksum bytes, +respectively. And memory contention can increase this by another 6 T cycles. +This means that an encoding implementation may choose to consider pulses +which do not vary by more than about 2% as reasonably equal for comparison +purposes when encoding pulse durations. + +Blocks +------ + +There are four mandatory block types which any implementation must implement in order +to claim PZX compatibility, namely PZXT, PULS, DATA and PAUS blocks. All +other block types may be safely ignored, although implementors are welcome +to implement them as they see fit. However whenever an implementation +encounters a block it doesn't understand, it must skip it and continue as if +the block was not there at all. + +In case the specified block size is smaller than the minimum size possible +for given block type, it is an error and an application may either ignore the +block and try to process the next one or to stop processing the file +entirely, whatever an implementor finds more appropriate. Similarly, if some +data inside the block indicate that the block should have at least some +minimum size but the specified block size is smaller than this, this is an +error as well and the above applies as well. + +To allow custom extensions, anyone is free to define his own custom block, +however to prevent clashes with the standard, such blocks may use only +lowercase tag names, while any standardized blocks use uppercase tag names. + + +The core blocks which must be supported are: + +PZXT - PZX header block +----------------------- + +offset type name meaning +0 u8 major major version number (currently 1). +1 u8 minor minor version number (currently 0). +2 u8[?] info tape info, see below. + +This block distinguishes the PZX files from other files and may provide +additional info about the file as well. This block must be always present as +the first block of any PZX file. + +Any implementation should check the version number before processing the +rest of the PZX file or even the rest of this block itself. Any +implementation should accept only files whose major version it implements +and reject anything else. However an implementation may report a warning in +case it encounters minor greater than it implements for given major, if the +implementor finds it convenient to do so. + +Note that this block also allows for simple concatenation of PZX files. Any +implementation should thus check the version number not only in case of the +first block, but anytime it encounters this block anywhere in the file. This +in fact suggests that an implementation might decide not to treat the first +block specially in any way, except checking the file starts with this block +type, which is usually done because of file type recognition anyway. + +The rest of the block data may provide additional info about the tape. Note +that an implementation is not required to process any of this information in +any way and may as well safely ignore it. + +The additional information consists of sequence of strings, each terminated +either by character 0x00 or end of the block, whichever comes first. +This means the last string in the sequence may or may not be terminated. + +The first string (if there is any) in the sequence is always the title of +the tape. The following strings (if there are any) form key and value pairs, +each value providing particular type of information according to the key. +In case the last value is missing, it should be treated as empty string. +The following keys are defined (for reference, the value in brackets is the +corresponding type byte as specified by the TZX standard): + +Publisher [0x01] - Software house/publisher +Author [0x02] - Author(s) +Year [0x03] - Year of publication +Language [0x04] - Language +Type [0x05] - Game/utility type +Price [0x06] - Original price +Protection [0x07] - Protection scheme/loader +Origin [0x08] - Origin +Comment [0xFF] - Comment(s) + +Note that some keys (like Author or Comment) may be used more than once. + +Any encoding implementation must use any of the key names as they are listed +above, including the case. For any type of information not covered above, it +should use either the generic Comment field or invent new sensible key name +following the style used above. This allows any decoding implementation to +classify and/or localize any of the key names it understands, and use any +others verbatim. + +Overall, same rules as for use in TZX files apply, for example it is not +necessary to specify the Language field in case all texts are in English. + +PULS - Pulse sequence +--------------------- + +offset type name meaning +0 u16 count bits 0-14 optional repeat count (see bit 15), always greater than zero + bit 15 repeat count present: 0 not present 1 present +2 u16 duration1 bits 0-14 low/high (see bit 15) pulse duration bits + bit 15 duration encoding: 0 duration1 1 ((duration1<<16)+duration2) +4 u16 duration2 optional low bits of pulse duration (see bit 15 of duration1) +6 ... ... ditto repeated until the end of the block + +This block is used to represent arbitrary sequence of pulses. The sequence +consists of pulses of given duration, with each pulse optionally repeated +given number of times. The duration may be up to 0x7FFFFFFF T cycles, +however longer durations may be achieved by concatenating the pulses by use +of zero pulses. The repeat count may be up to 0x7FFF times, however more +repetitions may be achieved by simply storing the same pulse again together +with another repeat count. + +The optional repeat count is stored first. When present, it is stored as +16 bit value with bit 15 set. When not present, the repeat count is considered to be 1. +Note that the stored repeat count must be always greater than zero, so when +decoding, a value 0x8000 is not a zero repeat count, but prefix indicating the +presence of extended duration, see below. + +The pulse duration itself is stored next. When it fits within 15 bits, it is +stored as 16 bit value as it is, with bit 15 not set. Otherwise the 15 high +bits are stored as 16 bit value with bit 15 set, followed by 16 bit value +containing the low 16 bits. Note that in the latter case the repeat count +must be present unless the duration fits within 16 bits, otherwise the +decoding implementation would treat the high bits as a repeat count. + +The above can be summarized with the following pseudocode for decoding: + + count = 1 ; + duration = fetch_u16() ; + if ( duration > 0x8000 ) { + count = duration & 0x7FFF ; + duration = fetch_u16() ; + } + if ( duration >= 0x8000 ) { + duration &= 0x7FFF ; + duration <<= 16 ; + duration |= fetch_u16() ; + } + +The pulse level is low at start of the block by default. However initial +pulse of zero duration may be easily used to make it high. Similarly, pulse +of zero duration may be used to achieve pulses lasting longer than +0x7FFFFFFF T cycles. Note that if the repeat count is present in case of +zero pulse for some reason, any decoding implementation must consistently +behave as if there was one zero pulse if the repeat count is odd and as if +there was no such pulse at all if it is even. + +For example, the standard pilot tone of Spectrum header block (leader < 128) +may be represented by following sequence: + +0x8000+8063,2168,667,735 + +The standard pilot tone of Spectrum data block (leader >= 128) would be: + +0x8000+3223,2168,667,735 + +For the record, the standard ROM save routines create the pilot tone in such +a way that the level of the first sync pulse is high and the level of the +second sync pulse is low. The bit pulses then follow, each bit starting with +high pulse. The creators of the PZX files should use this information to +determine if they got the polarity of their files right. Note that although +most loaders are not polarity sensitive and would work even if the polarity +is inverted, there are some loaders which won't, so it is better to always +stick to this scheme. + +DATA - Data block +----------------- + +offset type name meaning +0 u32 count bits 0-30 number of bits in the data stream + bit 31 initial pulse level: 0 low 1 high +4 u16 tail duration of extra pulse after last bit of the block +6 u8 p0 number of pulses encoding bit equal to 0. +7 u8 p1 number of pulses encoding bit equal to 1. +8 u16[p0] s0 sequence of pulse durations encoding bit equal to 0. +8+2*p0 u16[p1] s1 sequence of pulse durations encoding bit equal to 1. +8+2*(p0+p1) u8[ceil(bits/8)] data data stream, see below. + +This block is used to represent binary data using specified sequences of +pulses. The data bytes are processed bit by bit, most significant bits first. +Each bit of the data is represented by one of the sequences, s0 if its value +is 0 and s1 if its value is 1, respectively. Each sequence consists of +pulses of specified durations, p0 pulses for sequence s0 and p1 pulses for +sequence s1, respectively. + +The initial pulse level is specified by bit 31 of the count field. For data +saved with standard ROM routines, it should be always set to high, as +mentioned in PULS description above. Also note that pulse of zero duration +may be used to invert the pulse level at start and/or the end of the +sequence. It would be also possible to use it for pulses longer than 65535 T +cycles in the middle of the sequence, if it was ever necessary. + +For example, the sequences for standard ZX Spectrum bit encoding are: +(initial pulse level is high): + +bit 0: 855,855 +bit 1: 1710,1710 + +The sequences for ZX81 encoding would be (initial pulse level is high): + +bit 0: 530, 520, 530, 520, 530, 520, 530, 4689 +bit 1: 530, 520, 530, 520, 530, 520, 530, 520, 530, 520, 530, 520, 530, 520, 530, 520, 530, 4689 + +The sequence for direct recording at 44100kHz would be (assuming initial pulse level is low): + +bit 0: 79,0 +bit 1: 0,79 + +The sequence for direct recording resampled to match the common denominator +of standard pulse width would be (assuming initial pulse level is low): + +bit 0: 855,0 +bit 1: 0,855 + +After the very last pulse of the last bit of the data stream is output, one +last tail pulse of specified duration is output. Non zero duration is +usually necessary to terminate the last bit of the block properly, for +example for data block saved with standard ROM routine the duration of the +tail pulse is 945 T cycles and only than goes the level low again. Of course +the specified duration may be zero as well, in which case this pulse has no +effect on the output. This is often the case when multiple data blocks are +used to represent continuous stream of pulses. + +PAUS - Pause +------------ + +offset type name meaning +0 u32 duration bits 0-30 duration of the pause + bit 31 initial pulse level: 0 low 1 high + +This block may be used to produce pauses during which the pulse level is not +particularly important. The pause consists of pulse of given duration and +given level. However note that some emulators may choose to simulate random +noise during this period, occasionally toggling the pulse level. In such case +the level must not be toggled during the first 70000 T cycles and then more +than once during each 70000 T cycles. + +For example, in case of ZX Spectrum program saved by standard SAVE command +there is a low pulse of about one second (49-50 frames) between the header +and data blocks. On the other hand, there is usually no pause between the +blocks necessary at all, as long as the tail pulse of the preceding block was +used properly. + + +Additional blocks which should be supported: + +BRWS - Browse point +------------------- + +offset type name meaning +0 u8[?] text text describing this browse point + +This block may be used when it is desirable to mark some point on the tape +as target for tape browsing. The text is the single line describing this +target. + +If an implementation supports tape browsing, it should implement a mode in +which it allows jumping to PZXT or BRWS blocks only. These blocks are the +reasonable points to which jumping makes sense, and both provide descriptive +name of that point. In case of the PZXT block it is the tape title (or "Tape +start" if no name is specified) and in case of BRWS block it is the the +description of that browse point. An implementation may choose to also +implement a mode in which it allows jumping to arbitrary blocks as well. In +such case the blocks may be referred to by their tag names as well, and an +implementation may choose to attempt to extract any additional information +from the block itself as it sees fit. + +Creators of the PZX files are urged to put the BRWS blocks to wherever it makes +sense. They are often not needed, but they should be inserted prior any +level data block to which a user might eventually need to fast forward or +rewind to, together with appropriately descriptive name. + +STOP - Stop tape command +------------------------ + +offset type name meaning +0 u16 flags when exactly to stop the tape (1 48k only, other always). + +This block is used to instruct an emulator to stop the virtual tape deck. +The flags field is used to specify under which conditions this should +happen. In case the value is 0, the tape should be always stopped, in case +the value is 1, it should be stopped only if 48k Spectrum is being emulated. +Other values are not defined yet, however for future compatibility, any +implementation should treat any value it doesn't understand as 0. + +Creators of the PZX files are urged to put these blocks to whichever position the program +asks the user to stop the tape at, and use the flags field appropriately to +the situation as well. + + + + + +Issues +====== + +Q: Some pulse sequences are more common than the others. Shall we include +some of them in the format definition and provide a way of simply specifying +them in the blocks? For example, PULS block of zero size might refer to the +standard pilot sequence. Similarly, in DATA block, setting p0 to 0 might +indicate that p1 specifies one of the default sets of sequences, say 0 for +standard ZX Spectrum and 1 for ZX81. Do we want to do either or both of +this or is it just a feature creep? + +A: It is better not to hard code any specific knowledge of this type into an +implementation. This way an implementation doesn't have to deal with special +cases and it has a better chance of understanding any future revisions of the +format. The size savings alone are not worth it. + + + +F.A.Q. +====== + +Q: How do I recognize that a DATA block is suitable for flashloading? + +A: By testing the pulse sequences. What exactly you need to test may depend +on what loaders your implementation can flashload. In the common case of the +standard loaders you would simply test that each sequence consists of two +non-zero pulses, and that the total duration of the sequence s0 is less than +the total duration of sequence s1. + + + +Mapping of TZX blocks to PZX blocks: +==================================== + +Here is a brief roadmap how each of the TZX blocks may be mapped to +corresponding PZX block(s): + +ID 10 - Standard speed data block + +Trivially encoded with PULS and DATA blocks. + +ID 11 - Turbo speed data block + +Trivially encoded with PULS and DATA blocks. + +ID 12 - Pure tone + +Trivially encoded with PULS block. + +ID 13 - Sequence of pulses of various lengths + +Trivially encoded with PULS block. + +ID 14 - Pure data block + +Trivially encoded with DATA block. + +ID 15 - Direct recording block + +Encoded with DATA block, following the example sequences in DATA block +description. + +ID 18 - CSW recording block + +Encoded either as PULS block or DATA block, using the DRB like encoding with +appropriate denominator in the latter case. In either case, it would help if +the pulse durations are cleaned from the noise caused by interfering sampling frequency. + +ID 19 - Generalized data block + +Encoded as PULS block and DATA block for alphabets with no more than 2 +symbols. In case of 3+ symbols for the data the pilot would be encoded as PULS +block, and for data an encoding same as for CSW above would be used. + +ID 20 - Pause (silence) or 'Stop the tape' command + +Encoded either as PAUS block or STOP block. + +ID 21 - Group start + +Encoded as BRWS block. + +ID 22 - Group end + +Not needed. + +ID 23 - Jump to block +ID 24 - Loop start +ID 25 - Loop end +ID 26 - Call sequence +ID 27 - Return from sequence +ID 28 - Select block + +All these were dropped as not really needed. + +ID 2A - Stop the tape if in 48K mode + +Encoded as STOP block. + +ID 2B - Set signal level + +Not needed, every block defines pulse levels regardless of other blocks. + +ID 30 - Text description + +Encoded as BRWS block. + +ID 32 - Archive info + +Included as part of PZXT block. + +ID 31 - Message block +ID 33 - Hardware type +ID 35 - Custom info block + +All these were dropped as not needed, although they might be easily included +via custom blocks if anyone really has some use for them. + +ID 5A - "Glue" block (90 dec, ASCII Letter 'Z') + +Not exactly needed, although PZXT block may be used in the same way as well. + + + +History +======= + +1.0 (28.6.2007) + +* Changed ZXTP tag to PZXT to eliminate any possible problems with draft implementations. + +0.3 draft (20.5.2007) + +* Some values are now intentionally limited to 31 bits to prevent overflow issues while decoding. +* Number of pulses of DATA block bit sequences is now stored as 8 bits only. + +0.2 draft (12.5.2007) + +* The ZXTP block now stores the info types verbosely (suggested by Kio and AndyC). +* The encoding of PULS block now allows 32 bit durations and optional repeat count. +* The DATA block and PAUS blocks now contain explicit initial pulse level. +* The PAUS block now consists of single pulse, and specifies how random noise should be used. + +0.1 draft (28.4.2007) + ++ Patrik Rak presents the initial draft release. diff --git a/docs/pzx_scripts.txt b/docs/pzx_scripts.txt new file mode 100644 index 0000000..84ddff5 --- /dev/null +++ b/docs/pzx_scripts.txt @@ -0,0 +1,182 @@ +PZX scripts documentation +========================= + +The PZX tools suite contains few scripts which demonstrate the possibilities +of processing the PZX files dumped as text files. Such text files are +generated by pzx2txt and can be converted back to PZX by txt2pzx. +See PZX text format specification for more details about the format itself. + +The scripts themselves are simple perl scripts which read the input from +the file(s) specified on the command line (or standard input if no such file +is specified) and write the output to standard output. + +The following scripts are currently available: + +expand_pulses.pl +---------------- + +This script expands the pulses so each pulse is output on a separate line, +as if pzx2txt -e was used in the first place. + +Some scripts may require their input expanded in this way. + +annotate_pulses.pl +---------------- + +This script annotates the pulses with their initial pulse level, +as if pzx2txt -l was used in the first place. + +This may be useful if you want to verify the polarity is as expected. + +adjust_pulses.pl +---------------- + +This script may be used to adjust pulse durations by specified scale factor +and bias. It even supports use of different factor and bias depending on +whether the pulse level is low or high. This may be useful if the pulses +were created using either too low or too high level threshold. Note that +in this case the script needs the pulses expanded, see expand_pulses.pl. + +The script first multiplies each pulse duration by scale factor, then adds +the bias value. Both factor and bias are chosen according to current pulse +level. The resulting value is clamped to <0,2^32-1> range and output. + +The scaling factor and bias is set by use of SCALE and BIAS keywords in the +input stream. The syntax is as follows: + +SCALE [a] [b] + +This sets the scaling factors for low and high pulses to values a and b, +respectively. If b is not present, both factors are set to a. If neither a +nor b is present, both factors are set to 1. + +BIAS [a] [b] + +This sets the bias for low and high pulses to values a and b, +respectively. If b is not present, both biases are set to a. If neither a +nor b is present, both biases are set to 0. + +Example: + + SCALE 0.9 1.1 - Fix effect of sampling threshold biased towards high pulses. + SCALE 2 - Make every pulse twice as long. + SCALE - No more scaling. + + BIAS 0 855 - Make every high pulse 855 T cycles longer. + BIAS -10 - Make every pulse 10 T cycles shorter. + BIAS - No more biasing. + +filter_pulses.pl +---------------- + +This script may be used to map pulse durations in certain ranges to +specified values. This is extremely useful for fixing rounding errors caused +by sampling. + +The script maintains the list of ranges and the resulting value to use for +each range. Each pulse duration is compared with the list and the resulting +value of the first match is used instead. In case no match is found, the +duration itself is used as it is. + +The ranges are set by use of FILTER keyword in the input stream. The syntax +is as follows: + +FILTER + +Add mapping of durations in range to +given duration to the list of range mappings. + +FILTER + +Add mapping of durations in range to given duration to the list of +range mappings. + +FILTER + +Reset the range list. + +Example: + + FILTER 855 600 1300 - Map anything within <600,1300> range to 855 T cycles. + FILTER 0 100 - Map everything below 100 T cycles to zero. + FILTER - No more filtering. + +average_pulses.pl +----------------- + +This script may be used to compute the average duration and total count of +all pulses encountered. This may be useful for data analysis, as well as +actual data filtering when used in conjunction with script_filter.pl. + +The script simply sums durations and counts of all nonzero pulses and +discards anything else. The result is then printed as + +PULSE + +so it can be directly used for further script processing. + +sum_pulses.pl +------------- + +This script may be used to compute the total duration of all pulses +encountered. This may be useful for data analysis, as well as actual data +filtering when used in conjunction with script_filter.pl. + +The script simply sums durations of all pulses and discards anything else. +The result is then printed as + +PULSE + +so it can be directly used for further script processing. + +count_pulses.pl +--------------- + +This script may be used to create histogram of durations of all pulses +encountered. This may be useful for data analysis, in particular for +discovering what pulse ranges are most often used. + +The script simply sums counts for each duration encountered and discards +anything else. The result is then printed as + +PULSE + +for each duration encountered, sorted from shortest to longest duration. + +script_filter.pl +---------------- + +This script may be used to filter parts of the input through other scripts, +or, in fact, any other programs. It is extremely powerful tool with +countless possibilities, but as usual with great power comes great +responsibility - it will happily execute any command it is told to, so make +sure you know what input you are running it at. + +The script will normally just copy any input to the output, line by line. +However whenever it encounters a SCRIPT keyword specifying some command, it +will filter any further input via that command, effectively replacing the +input with the command's output. It will continue doing so until end of file +or until it encounters another SCRIPT keyword (with or without command). + +Examples: + + SCRIPT average_pulses.pl - Change duration of following pulses + to their average duration. + SCRIPT sum_pulses.pl | sed -e 's/PULSE/PAUSE/' - Turn the following pulses into pause + of their total duration. + SCRIPT count_pulses.pl - Create histogram of only + the following pulses. + SCRIPT cat info.txt - Replace following input + with content of file info.txt. + SCRIPT - End input of previous command. + +History +======= + +1.1 (21.4.2011) + ++ added annotate_pulses.pl script, and added support for annotated pulses in general. + +1.0 (16.6.2007) + ++ initial release. diff --git a/docs/pzx_text.txt b/docs/pzx_text.txt new file mode 100644 index 0000000..b133d00 --- /dev/null +++ b/docs/pzx_text.txt @@ -0,0 +1,386 @@ +PZX text format syntax version 1.1 +================================== + +This is the description of the PZX text format as used by the pzx2txt and +txt2pzx tools. + +The text format consists of sequence of lines, each line starting with a +keyword, followed by values appropriate for that keyword. Empty lines and +lines starting with # are ignored. + +Overall the format closely matches the PZX format structure. Some keywords +introduce corresponding PZX blocks while other keywords are used to provide +data for the current block. All keywords are explained in detail below. + +Current pulse level is maintained during the processing of the text input. +It starts low at the beginning and each keyword may depend on and/or affect +the current pulse level. Although most people don't need to worry about this +at all, it is explained in detail below as well. + +Keywords are simple ASCII character sequences listed later. Each keyword +specifies what values it uses and whether they are required (enclosed in <>) +or optional (enclosed in []). Keyword values themselves are either numbers, +strings, or data: + +- Numbers are normal unsigned numbers, written in decimal by default, in +hexadecimal if prefixed with 0x, or in binary when prefixed with 0b. + +- Strings are enclosed in double quotes, and backslash is used to escape +double quotes and backslash. The sequences \n, \r, \t are recognized as +well, as is the \xNN sequence for encoding any character in hexadecimal. + +- Data is a sequence of octets, each encoded either as two character +hexadecimal number, or as a dot followed by the corresponding ASCII +character. + + +Block introducing keywords +========================== + +The block-introducing keywords are defined below. + +Please note that the description of how pulse level is affected refer to the +internal pulse level maintained while processing the text form, not how the +resulting PZX blocks affect the pulse level in the PZX file. + +PZX [major.minor] +----------------- + +This keyword introduces the PZX header block. + +The major and minor version numbers are printed only for reference by +pzx2txt and are entirely ignored when encoding. + +Keywords this block may contain: INFO + +Pulse level: not affected. + +Example: + + PZX 1.0 + INFO "Test tape file" + +PULSES +------ + +This keyword introduces the PZX PULS block. + +The block itself consists of a sequence of arbitrary pulses. + +Keywords this block may contain: PULSE + +Pulse level: always set to low. + +Example: + + PULSES + PULSE 2168 8063 + PULSE 667 + PULSE 735 + +DATA [pulse level] +------------------ + +This keyword introduces the PZX DATA block. + +The block itself consists of sequence of octets, and information about how to +convert the octets to pulses. + +The optional pulse level may be used to specify the initial pulse level of +the DATA block. When not present, the current pulse level is used. + +Keywords this block may contain: SIZE, BITS, BIT0, BIT1, TAIL, BODY, BYTE, WORD, XOR, ADD, SUB + +Pulse level: always set to low. + +Example: + + DATA + SIZE 19 + TAIL 945 + BIT0 855 855 + BIT1 1710 1710 + BYTE 0 0 + BODY .T.E.S.T. .T.A.P.E. + WORD 1234 10 1234 + XOR + +PACK [pulse level] [sequence limit] [sequence order] +---------------------------------------------------- + +This keyword introduces either the PZX DATA or the PZX PULS block, +depending on whether the following pulses can be packed or not, respectively. + +The block itself consists of a sequence of pulses. Once the block is complete, an +attempt is made to find two pulse sequences which may be used to encode all +of the pulses as a sequence of bits. If such sequences are found, the pulses +are converted to the corresponding DATA block. If no such sequences are +found, warning is issued and the pulses are stored as usual as part of the +PULS block. + +The optional pulse level may be used to set the current pulse level. When +not present, the current pulse level is left intact. + +The sequence limit specifies how long pulse sequences to consider at maximum +when searching for sequences used to encode the bits. When not present, the +default value 2 is used. The maximum value is 255. + +The sequence order may be used to control which sequence is going to +become sequence 0 and which sequence 1. When set to 0 or 1, the sequence +starting at the first pulse becomes sequence 0 or 1, respectively. When set +to 2, the sequence whose total duration is shorter becomes sequence 0. When +not present, the default value 2 is used. + +Keywords this block may contain: PULSE + +Pulse level: may be forced, see above. + +Example: + + PACK + PULSE 855 4 + PULSE 1710 2 + PULSE 855 2 + PULSE 1710 8 + PULSE 945 + +PAUSE [pulse level] +------------------------------ + +This keyword introduces the PZX PAUSE block. + +The block itself consists of a pause pulse of given duration. + +The optional pulse level may be used to specify the level of the pause pulse +as well as the current pulse level. When not present, it is as if it was set to low. + +Keywords this block may contain: none + +Pulse level: always forced, see above. + +Example: + + PAUSE 3500000 + PAUSE 3500 1 + +STOP [flags] +------------ + +This keyword introduces the PZX STOP block. + +The block itself consists of information when to stop the virtual tape deck. + +The optional flags value may specify when exactly to do that. Value 0 means +stop always, value 1 means stop only in 48k mode. See PZX format +specification for more details and possibly other values. When not present, +it is as if it was set to 0. + +Keywords this block may contain: none + +Pulse level: not affected. + +Example: + + STOP + STOP 1 + +BROWSE <"string"> +----------------- + +This keyword introduces the PZX BRWS block. + +The block itself consists of string information used as aid while browsing +the tape content. See PZX format specification for more info about the string content. + +Keywords this block may contain: none + +Pulse level: not affected. + +Example: + + BROWSE "Level 1" + +TAG +-------------- + +This keyword introduces any unknown PZX block. + +The block itself consists of an opaque sequence of octets. + +The tag name is the tag used for the PZX block. It must consists of exactly +four ASCII characters. + +Keywords this block may contain: SIZE, BODY, BYTE, WORD, XOR, ADD, SUB + +Pulse level: not affected. + +Example: + + TAG wtfk + BODY FF001234 + + +Block content keywords +====================== + +The keywords which provide the block content are defined below. + +Note that some of them may appear only in one specific block, while others +may be used in multiple blocks. However, even in such case their meaning +remains the same. + +For examples, see the description of the corresponding block keyword. + +INFO <"string"> +--------------- + +This keyword provides UTF-8 encoded string used in the PZX header block. +Each such keyword adds one such string. The first string is the name of the +tape, the remaining pairs provide additional information. See the PZX format +specification for more specific details and requirements. + +Blocks this keyword may appear in: PZX + +Pulse level: not affected. + +PULSE[0|1] [count] +----------------------------- + +This keyword adds given amount of pulses of given duration to the current +block. + +Both duration and count are normally arbitrary 32 bit numbers, but may be +limited to less bits in case some special features are used when encoding. +For example, using using -p option of txt2pzx limits duration to 31 bits and +count to 15 bits, and use of the PACK block limits the duration to 16 bits. + +Note that the PULSE keyword may be annotated with the optional pulse level, +such as when generated with -l option of pzx2txt. However this annotation +has no effect on the actual level of the pulse, which is controlled solely +by the enclosing block and the amount of preceding pulses as usual. + +Blocks this keyword may appear in: PULSES, PACK + +Pulse level: toggled for each single pulse added. + +SIZE +----------- + +This keyword specifies the expected size of the data in the current block in +bytes. In case of DATA block, the byte used extra bits (see BITS keyword) +are not included. + +In either case, this keyword is printed by pzx2txt only for reference only. +When encoding, it is ignored and the size is deduced from the block data +themselves. In case there is mismatch between that size and the size +specified by this keyword, warning is printed, but the data are processed +nevertheless. + +Blocks this keyword may appear in: DATA, TAG + +Pulse level: not affected. + +BITS +----------- + +This keyword specifies how many bits of the last data byte are used. Note +that both 0 and 8 are treated as if the entire last byte is used. When not +specified, all bits of the last byte are used. + +Blocks this keyword may appear in: DATA + +Pulse level: not affected. + +BIT0 [pulse] ... +---------------- + +This keyword specifies what pulse sequence to use for bit 0 in the data. +Each use such keyword extends the sequence, which is cleared each time the DATA +block is finished. + +Blocks this keyword may appear in: DATA + +Pulse level: not affected. + +BIT1 [pulse] ... +---------------- + +Like BIT0, but for bit 1. + +TAIL +--------------- + +This keyword specifies the duration of the tail pulse for current block. +When not specified, 0 is used, meaning no tail pulse is generated. + +Blocks this keyword may appear in: DATA + +Pulse level: not affected. + +BODY [data] +----------- + +This keyword provides the bulk content of the current block. Each use of +such keyword appends more content, which is cleared each time the block is +complete. + +Blocks this keyword may appear in: DATA, TAG + +Pulse level: not affected. + +BYTE [byte] ... +--------------- + +This keyword is like BODY, except that the values are specified and +processed as 8 bit numbers, not data. This may be used to make the content +more readable. + +Blocks this keyword may appear in: DATA, TAG + +Pulse level: not affected. + +WORD [word] ... +--------------- + +This keyword is like BODY, except that the values are specified and +processed as 16 bit little endian numbers, not data. This may be used to +make the content more readable. + +Blocks this keyword may appear in: DATA, TAG + +Pulse level: not affected. + +XOR [initial value] +------------------- + +This keyword may be used to append simple checksum to the block content. + +The 8 bit accumulator is initialized with provided initial value, or zero if +not present, and then updated with exclusive-or of each byte of current block. The +result is then appended to the block content. + +Blocks this keyword may appear in: DATA, TAG + +Pulse level: not affected. + +ADD [initial value] +------------------- + +Like XOR, except that addition is used instead of exclusive-or. + +SUB [initial value] +------------------- + +Like XOR, except that subtraction is used instead of exclusive-or. + + +History +======= + +1.1 (21.4.2011) + ++ added note about annotated pulses. + +1.0 (16.6.2007) + ++ initial release. diff --git a/docs/pzx_tools.txt b/docs/pzx_tools.txt new file mode 100644 index 0000000..314a2e9 --- /dev/null +++ b/docs/pzx_tools.txt @@ -0,0 +1,189 @@ +PZX tools documentation +======================= + +The PZX tools suite consists of several tools for converting data to and +from PZX format. Each of these tools reads the data from file given on +command line (or standard input if no file name is specified on command +line) and writes the output to file specified with the -o option (or standard +output if no such file name is specified). Each tool may also have its own +set of additional options, which may be obtained simply by using the -h option. + +The purpose of each tool should easy to guess from its name, however here is +the list to make it clear: + +tzx2pzx - convert TZX files to PZX files. +tap2pzx - convert TAP files to PZX files. +csw2pzx - convert CSW files to PZX files. + +pzx2wav - convert PZX files to WAV files. + +pzx2txt - dump PZX files to text output. +txt2pzx - create PZX files from text input. + +The more detailed of each of these tools follows. + + +Converting to PZX +================= + +tzx2pzx +------- + +This tool can be used to convert TZX files to PZX. The tool currently +supports all features of TZX format specification 1.20. In case you are +interested, see PZX format specification about how each TZX block is mapped +to PZX. But most likely you don't need to worry about that at all. + +Note that in case you would like to adjust something in the resulting PZX, +like tape file information texts or their encoding, simply use pzx2txt, edit +whatever you need, then use txt2pzx to convert your edited text back to PZX. + +Options: none + +tap2pzx +------- + +This tool can be used to convert TAP (aka BLK) files to PZX. Normally you +would not need to do that, as TAP files are supported by any emulator which +supports tape files at all, but it may be eventually useful if you would +like to add things which TAP files can't store. This may include things like +textual information about the tape file itself, specific pause blocks +between the data blocks, or browse or stop blocks which may help in case of +multilevel games. + +Options: + +-p n Add pause of given duration (specified in ms) after each data block. + + By default, no pause is added, as most tape files don't need one anyway. + +csw2pzx +------- + +This tool can be used to convert CSW files to PZX. The tool currently +supports both 1.01 and 2.00 CSW files. Note that the resulting file will be +usually bigger than the original CSW file, but once compressed with an +archiving program, it may even become smaller than the original. + +Options: none + + +Converting from PZX +=================== + +pzx2wav +------- + +This tool can be used to create WAV files from PZX. WAV files are the most +common format for storing recorded sound, so this tool comes in handy whenever +you would need to "play" the tape file for whatever reason. Most often you +would need it if you want to load it on a real hardware connected to your +sound card instead of the tape deck. + +Options: + +-s n Create the WAV file using given sample rate, specified in Hz. + + By default, the 44100Hz sample rate is used, which corresponds to CD quality. + + +Messing up with PZX +=================== + +pzx2txt +------- + +This tool can be used to dump content of the PZX files in textual form. This +is invaluable tool for whoever needs to examine the content of the PZX file +in human readable form. Also, thanks to the fact that the textual output can +be converted back to PZX file using txt2pzx, the text form can be easily +used for reasonably convenient editing of the PZX files, by the means of any +text editor or any text processing tool. + +The output of the tool lists blocks of the original PZX file. The output +should be pretty easy to understand for whoever read the PZX format +specification, and is in more detail described in PZX text format specification. + +Options: + +-p Dump the DATA blocks in the same way as PULSE blocks, + as a sequence of pulses. This may be useful in case you need to + examine/process everything as pulses only. It may be also handy + if you want to pack several blocks into one DATA block with the PACK + keyword, see PZX text format specification. + + By default, the DATA blocks are dumped as DATA blocks. + +-a Dump the ASCII characters stored in DATA blocks as ASCII + characters, not hexadecimal numbers, which makes them easily + readable in the generated output. + + By default, everything is dumped as hexadecimal number, which + makes it somewhat easier for any data processing. + +-x Dump any 19 byte DATA block in a way which makes it more readable if it + was a standard ZX Spectrum header. + + By default, all DATA blocks are dumped in the same way. + +-d Don't dump the content of the DATA blocks, just their headers. + This is useful in case you want just to examine the overall PZX block + structure, not the data themselves. + + By default, the content of the DATA blocks is dumped as well. + +-e Expand the pulses, dumping each pulse of a PULSE block on its own line. + This makes it more friendly to scripts which need to process the + pulses in some way. + + By default, the pulse sequence is dumped in the same way as it was + stored in the PULSE block, the duration followed by an optional + repeat count. + + See also the -p option of txt2pzx. + +-l Annotate each pulse with its initial pulse level, 0 for low level + and 1 for high level. This makes it very easy to check the polarity + of each pulse dumped, and helps to keep the polarity right during editing. + + However note that this annotation is only informational, in particular + it has no influence on the actual pulse level when processed by txt2pzx + or any of the included processing scripts. + + By default, the dumped pulses are not distinguished in any way. + +txt2pzx +------- + +This tool can be used to create the PZX files from properly prepared text +input. Most often the input will be the output of the pzx2txt tool, but it +may be as well created by hand or by a script. The syntax of the text format +is described in more detail in PZX text format specification. + +The tool will read the input and generate the corresponding PZX output, +eventually generating some warnings in case it finds some discrepancies. +In either case the user is advised to run pzx2txt on the resulting output +to make sure the output really contains the intended input. + +Options: + +-p When creating PULSE blocks, store the pulses exactly as the are + specified in the input file. This is useful mostly for developers + who may need to prepare specific pulse sequences for testing purposes. + + By default, PULSE block boundaries are ignored and pulses themselves + are merged together whenever possible. + + See also the -e option of pzx2txt. + + +History +======= + +1.1 (21.4.2011) + ++ added -l option to pzx2txt to annotate pulses with their initial pulse level. + +1.0 (16.6.2007) + ++ initial release. diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..e6164ca --- /dev/null +++ b/license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2007 Patrik Rak (patrik@raxoft.cz) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..7a15829 --- /dev/null +++ b/readme.txt @@ -0,0 +1,29 @@ +Welcome to the PZX tools suite. + +This suite contains the tools which will allow you to create, examine, and +edit PZX files. PZX files are used to archive the content of the tape files +used by computers like Sinclair ZX Spectrum, much in the same way as TAP or +TZX files are. The difference is that the PZX files allow storing of anything +TZX files can, while being almost as simple to process as the TAP files are. + +The suite includes converters from other common formats, namely tzx2pzx for +TZX, tap2pzx for TAP and csw2pzx for CSW. These can be used to convert your +favorite tape files to PZX format. The suite also contains converter to WAV +format, pzx2wav. This is useful for example in case you want to send the +tape file to a real machine via sound output. Finally, there are two +converters which convert the PZX files to and from textual form. These can +be used to examine the content of a PZX file in a human readable form, +conveniently edit or create the content of PZX files in any text editor, or +process or analyze the content by any text processing tool. The suite even +includes few perl scripts which may be used for the latter. + +You can find the programs precompiled for Windows platform in the bin/ +directory, unless you have downloaded the source only distribution. +The source files themselves are stored in the src/ directory, +simply cd there and run make to build the programs on Linux/Unix platforms. +The scripts for processing the textual form can be found in scripts/ directory. +And last but not least, the documentation can be found in the docs/ directory. + +Enjoy! + +Patrik Rak diff --git a/scripts/adjust_pulses.pl b/scripts/adjust_pulses.pl new file mode 100755 index 0000000..2e66c18 --- /dev/null +++ b/scripts/adjust_pulses.pl @@ -0,0 +1,63 @@ +#! /usr/bin/perl -w +# +# Adjust pulse pulse durations by given scale factor and bias. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +my @scale = ( 1, 1 ) ; +my @bias = ( 0, 0 ) ; +my $level = 0 ; + +while(<>) { + + if ( my( $orig_level, $duration, $count, $rest ) = /^PULSE(\d?)\s+(\d+)\s*(\d*)(.*)$/i ) { + + $count = 1 if $count eq "" ; + + die "please expand pulses with expand_pulses.pl or pzx2txt -e\n" + if $count > 1 && ( $scale[0] != $scale[1] || $bias[0] != $bias[1] ) ; + + $duration = $duration * $scale[ $level ] + $bias[ $level ] ; + + if ( $duration < 0 ) { + $duration = 0 ; + } + elsif ( $duration > 0xFFFFFFFF ) { + $duration = 0xFFFFFFFF ; + } + else { + $duration = int( $duration ) ; + } + + print "PULSE$orig_level $duration", ( $count > 1 ? " $count" : "" ), "$rest\n" ; + + $level ^= ( $count & 1 ) ; + + next ; + } + + if ( my( $a, $b ) = /^SCALE\s*(\d*\.?\d*)\s*(\d*\.?\d*)/i ) { + $a = 1 if $a eq "" ; + $b = $a if $b eq "" ; + @scale = ( $a, $b ) ; + next ; + } + + if ( my( $a, $b ) = /^BIAS\s*(-?\d*)\s*(-?\d*)/i ) { + $a = 0 if $a eq "" ; + $b = $a if $b eq "" ; + @bias = ( $a, $b ) ; + next ; + } + + if ( /^(DATA|PACK)\s+(\d+)/i ) { + $level = ( $2 != 0 ) ; + } + elsif ( /^(PULSES|DATA)/ ) { + $level = 0 ; + } + + print ; +} diff --git a/scripts/annotate_pulses.pl b/scripts/annotate_pulses.pl new file mode 100755 index 0000000..3684999 --- /dev/null +++ b/scripts/annotate_pulses.pl @@ -0,0 +1,34 @@ +#! /usr/bin/perl -w +# +# Annotate pulses with their initial pulse level. +# +# This is the same as if pzx2txt -l was used in the first place. +# +# Copyright (C) 2011 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +my $level = 0 ; + +while(<>) { + + if ( my( $duration, $count ) = /^PULSE\d?\s+(\d+)\s*(\d*)/i ) { + + $count = 1 if $count eq "" ; + + print "PULSE$level $duration", ( $count > 1 ? " $count" : "" ), "\n" ; + + $level ^= ( $count & 1 ) ; + + next ; + } + + if ( /^(DATA|PACK)\s+(\d+)/i ) { + $level = ( $2 != 0 ) ; + } + elsif ( /^(PULSES|DATA)/ ) { + $level = 0 ; + } + + print ; +} diff --git a/scripts/average_pulses.pl b/scripts/average_pulses.pl new file mode 100755 index 0000000..e32b66a --- /dev/null +++ b/scripts/average_pulses.pl @@ -0,0 +1,26 @@ +#! /usr/bin/perl -w +# +# Compute average duration of pulses encountered. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +my $sum = 0 ; +my $total = 0 ; + +while(<>) { + + next unless ( my( $duration, $count ) = /^PULSE\d?\s+(\d+)\s*(\d*)/i ) ; + + next if $duration == 0 ; + + $count = 1 if $count eq "" ; + + $sum += $duration * $count ; + $total += $count ; +} + +my $average = $total && int( $sum / $total ) ; + +print "PULSE $average $total\n" ; diff --git a/scripts/count_pulses.pl b/scripts/count_pulses.pl new file mode 100755 index 0000000..ad84489 --- /dev/null +++ b/scripts/count_pulses.pl @@ -0,0 +1,23 @@ +#! /usr/bin/perl -w +# +# Create histogram of pulse durations encountered. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +my %stats ; + +while(<>) { + + next unless ( my( $duration, $count ) = /^PULSE\d?\s+(\d+)\s*(\d*)/i ) ; + + $count = 1 if $count eq "" ; + + $stats{$duration} += $count ; +} + +foreach my $duration ( sort { $a <=> $b } keys %stats ) { + my $count = $stats{$duration} ; + print "PULSE $duration $count\n" ; +} diff --git a/scripts/expand_pulses.pl b/scripts/expand_pulses.pl new file mode 100755 index 0000000..d946116 --- /dev/null +++ b/scripts/expand_pulses.pl @@ -0,0 +1,21 @@ +#! /usr/bin/perl -w +# +# Expand repeated pulses, so each pulse is on a separate line. +# +# This is the same as if pzx2txt -e was used in the first place. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +while(<>) { + + if ( my( $duration, $count ) = /^PULSE\d?\s+(\d+)\s+(\d+)/i ) { + while( $count-- > 0 ) { + print "PULSE $duration\n" ; + } + next ; + } + + print ; +} diff --git a/scripts/filter_pulses.pl b/scripts/filter_pulses.pl new file mode 100755 index 0000000..936482b --- /dev/null +++ b/scripts/filter_pulses.pl @@ -0,0 +1,42 @@ +#! /usr/bin/perl -w +# +# Map pulse durations in given ranges to specific values. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +my @filters ; + +loop: while(<>) { + + if ( my( $orig_level, $duration, $rest ) = /^PULSE(\d?)\s+(\d+)(.*)$/i ) { + my @f = @filters ; + while ( my( $result, $min, $max ) = splice( @f, 0, 3 ) ) { + if ( $min <= $duration && $duration <= $max ) { + print "PULSE$orig_level $result$rest\n" ; + next loop ; + } + } + print ; + next ; + + } + + if ( my( $duration, $a, $b ) = /^FILTER\s+(\d+)\s+(\d+)\s*(\d*)/i ) { + if ( $b eq "" ) { + push( @filters, $duration, $duration - $a, $duration + $a ) ; + } + else { + push( @filters, $duration, $a, $b ) ; + } + next ; + } + + if ( /^FILTER\s*$/i ) { + @filters = () ; + next ; + } + + print ; +} diff --git a/scripts/script_filter.pl b/scripts/script_filter.pl new file mode 100755 index 0000000..5ce5d21 --- /dev/null +++ b/scripts/script_filter.pl @@ -0,0 +1,41 @@ +#! /usr/bin/perl -w +# +# Filter the blocks of input through other scripts. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +use IPC::Open3 ; + +my $pid ; + +while(<>) { + + if ( $pid && /^SCRIPT/i ) { + close PIPE ; + waitpid( $pid, 0 ) ; + $pid = 0 ; + } + + if ( $pid ) { + print PIPE ; + next ; + } + + if ( my( $script ) = /^SCRIPT\s*(.*?)\s*$/i ) { + if ( $script ne "" ) { + local $SIG{__WARN__} = sub {} ; + $pid = eval { return open3( \*PIPE, ">&1", ">&2", $script ) } + or die "error: unable to run \"$script\"\n" ; + } + next ; + } + + print ; +} + +if ( $pid ) { + close PIPE ; + waitpid( $pid, 0 ) ; +} diff --git a/scripts/sum_pulses.pl b/scripts/sum_pulses.pl new file mode 100755 index 0000000..aba359a --- /dev/null +++ b/scripts/sum_pulses.pl @@ -0,0 +1,20 @@ +#! /usr/bin/perl -w +# +# Compute total duration of pulses encountered. +# +# Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) +# +# This source code is released under the MIT license, see included license.txt. + +my $sum = 0 ; + +while(<>) { + + next unless ( my( $duration, $count ) = /^PULSE\d?\s+(\d+)\s*(\d*)/i ) ; + + $count = 1 if $count eq "" ; + + $sum += $duration * $count ; +} + +print "PULSE $sum\n" ; diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..0cfbcff --- /dev/null +++ b/src/Makefile @@ -0,0 +1,73 @@ +# $Id: Makefile 320 2007-06-26 21:39:37Z patrik $ +# +# Simple makefile for the PZX tools suite. + +CXXFLAGS = -g -DDEBUG -Wall +#CXXFLAGS = -O2 -Wall +LDLIBS = -lz + +PROGS=tzx2pzx tap2pzx csw2pzx pzx2wav pzx2txt txt2pzx + +all: $(PROGS) + +tzx2pzx: tzx2pzx.o tzx.o csw.o pzx.o + $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +tap2pzx: tap2pzx.o pzx.o + $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +csw2pzx: csw2pzx.o csw.o pzx.o + $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +pzx2wav: pzx2wav.o pzx.o wav.o + $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +pzx2txt: pzx2txt.o + $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +txt2pzx: txt2pzx.o pzx.o + $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +clean: + rm -rf *.o *~ + +tidy: clean + rm -rf $(PROGS) + +archive: + tar czvf ../pzxtools.tar.gz *.cpp *.h Makefile + +dep: + sed -n -e '1,/^TOUCH=/ p' Makefile.new + ./mkdep >>Makefile.new + mv Makefile.new Makefile + +# generated by mkdep + +TOUCH=touch +csw.o : csw.cpp csw.h pzx.h +csw2pzx.o : csw2pzx.cpp csw.h pzx.h +pzx.o : pzx.cpp pzx.h +pzx2txt.o : pzx2txt.cpp pzx.h +pzx2wav.o : pzx2wav.cpp pzx.h wav.h +tap2pzx.o : tap2pzx.cpp pzx.h tap.h +txt2pzx.o : txt2pzx.cpp pzx.h +tzx.o : tzx.cpp csw.h endian.h pzx.h tap.h tzx.h +tzx2pzx.o : tzx2pzx.cpp pzx.h tzx.h +wav.o : wav.cpp buffer.h wav.h +buffer.h : debug.h endian.h + $(TOUCH) $@ +csw.h : buffer.h + $(TOUCH) $@ +debug.h : sysdefs.h + $(TOUCH) $@ +endian.h : types.h + $(TOUCH) $@ +pzx.h : buffer.h + $(TOUCH) $@ +tap.h : types.h + $(TOUCH) $@ +tzx.h : types.h + $(TOUCH) $@ +wav.h : endian.h + $(TOUCH) $@ diff --git a/src/buffer.h b/src/buffer.h new file mode 100644 index 0000000..b76d11b --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,172 @@ +// $Id: buffer.h 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file Self-inflating buffer. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef BUFFER_H +#define BUFFER_H 1 + +#include +#include +#include + +#ifndef DEBUG_H +#include "debug.h" +#endif + +#ifndef ENDIAN_H +#include "endian.h" +#endif + +/** + * Trivial class for convenient storing of arbitrary data. + */ +class Buffer { + + byte * buffer ; + uint buffer_size ; + uint bytes_used ; + +public: + + Buffer( const uint size = 65536 ) + : buffer( NULL ) + , buffer_size( 0 ) + , bytes_used( 0 ) + { + reallocate( size ) ; + } + + ~Buffer() + { + std::free( buffer ) ; + } + +private: + + void reallocate( const uint new_size ) + { + buffer = static_cast< byte * >( std::realloc( buffer, new_size ) ) ; + + if ( buffer == NULL || new_size <= buffer_size ) { + fail( "out of memory" ) ; + } + + buffer_size = new_size ; + } + +public: + + inline void clear( void ) + { + bytes_used = 0 ; + } + +public: + + bool read( FILE * const file ) + { + hope( file ) ; + + for ( ; ; ) { + + const uint bytes_free = buffer_size - bytes_used ; + const uint bytes_read = std::fread( buffer + bytes_used, 1, bytes_free, file ) ; + + bytes_used += bytes_read ; + + if ( bytes_read != bytes_free ) { + return ( std::ferror( file ) == 0 ) ; + } + + reallocate( 2 * buffer_size ) ; + } + } + + uint read( FILE * const file, const uint size ) + { + hope( file ) ; + + if ( size > buffer_size ) { + reallocate( size ) ; + } + + bytes_used = std::fread( buffer, 1, size, file ) ; + + return ( std::ferror( file ) ? ~0 : bytes_used ) ; + } + +public: + + inline void write( const void * const data, const uint size ) + { + hope( data || size == 0 ) ; + + while ( size > buffer_size - bytes_used ) { + reallocate( 2 * buffer_size ) ; + } + + std::memcpy( buffer + bytes_used, data, size ) ; + + bytes_used += size ; + } + + template< typename Type > + inline void write( const Type value ) + { + write( &value, sizeof( value ) ) ; + } + + template< typename Type > + inline void write_little( const Type value ) + { + write( little_endian( value ) ) ; + } + + template< typename Type > + inline void write_big( const Type value ) + { + write( big_endian( value ) ) ; + } + +public: + + inline byte * get_data( void ) const + { + return buffer ; + } + + template< typename Type > + inline Type * get_typed_data( void ) const + { + return reinterpret_cast< Type * >( buffer ) ; + } + + inline byte * get_data_end( void ) const + { + return buffer + bytes_used ; + } + + inline uint get_data_size( void ) const + { + return bytes_used ; + } + + inline bool is_empty( void ) const + { + return ( bytes_used == 0 ) ; + } + + inline bool is_not_empty( void ) const + { + return ( bytes_used > 0 ) ; + } + +} ; + +#endif // BUFFER_H diff --git a/src/csw.cpp b/src/csw.cpp new file mode 100644 index 0000000..010f155 --- /dev/null +++ b/src/csw.cpp @@ -0,0 +1,266 @@ +// $Id: csw.cpp 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file Rendering of CSW files. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef NO_ZLIB +#include +#endif + +#include "csw.h" +#include "pzx.h" + +/** + * Macros for fetching little endian data from data block. + */ +//@{ +#define GET1(o) (data[o]) +#define GET2(o) (data[o]+(data[(o)+1]<<8)) +#define GET3(o) (data[o]+(data[(o)+1]<<8)+(data[(o)+2]<<16)) +#define GET4(o) (data[o]+(data[(o)+1]<<8)+(data[(o)+2]<<16)+(data[(o)+3]<<24)) +//@} + +/** + * Render CSW encoded pulses to the output stream. + */ +uint csw_render_block( bool & level, const uint sample_rate, const byte * const data, const uint size ) +{ + hope( sample_rate > 0 ) ; + hope( data || size == 0 ) ; + + const byte * p = data ; + const byte * const end = data + size ; + + // Iterate over all pulses. + + uint pulse_count = 0 ; + + while ( p < end ) { + + // Fetch the pulse duration encoded as number of samples. + + uint sample_count = *p++ ; + + // In case it is 0, fetch the 32 bit sample count. + + if ( sample_count == 0 ) { + + if ( end - p < 4 ) { + warn( "premature end of CSW data detected" ) ; + break ; + } + + sample_count = *p++ ; + sample_count += *p++ << 8 ; + sample_count += *p++ << 16 ; + sample_count += *p++ << 24 ; + } + + // Now output the pulse, converting the sample count to duration in 3.5MHz T cycles first. + // + // Note that by rounding down we lose up to almost 1 T for every + // pulse, but it's precise enough for our purposes. + + uquad duration = ( ( 3500000ull * sample_count ) / sample_rate ) ; + const uint limit = 0xFFFFFFFF ; + + while ( duration > limit ) { + pzx_out( limit, level ) ; + duration -= limit ; + } + + pzx_out( uint( duration ), level ) ; + + level = ! level ; + + pulse_count++ ; + } + + return pulse_count ; +} + +/** + * Unpack given CSW block to given buffer. + */ +void csw_unpack_block( Buffer & buffer, const byte * const data, const uint size ) +{ + hope( data || size == 0 ) ; + + if ( size == 0 ) { + return ; + } + +#ifdef NO_ZLIB + + warn( "zlib support si not compiled in, so CSW Z-RLE compression is not supported" ) ; + return ; + +#else // NO_ZLIB + + // Initialize the zlib stream stucture. + + z_stream stream ; + stream.zalloc = Z_NULL ; + stream.zfree = Z_NULL ; + stream.opaque = NULL ; + stream.next_in = const_cast< byte * >( data ) ; + stream.avail_in = size ; + + if ( inflateInit( &stream ) != Z_OK ) { + warn( "error initializing zlib decompressor for CSW block: %s", stream.msg ? stream.msg : "unknown error" ) ; + return ; + } + + // Keep decompressing chunk by chunk, collecting the output in the + // output buffer. + + int result ; + + do { + + const uint chunk_size = 16384 ; + byte chunk[ chunk_size ] ; + + stream.next_out = chunk ; + stream.avail_out = chunk_size ; + + result = inflate( &stream, Z_NO_FLUSH ) ; + + if ( result != Z_OK && result != Z_STREAM_END ) { + warn( "error while decompressing CSW block: %s", stream.msg ? stream.msg : "unknown error" ) ; + break ; + } + + buffer.write( chunk, chunk_size - stream.avail_out ) ; + + } while ( result != Z_STREAM_END ) ; + + // Cleanup. + + inflateEnd( &stream ) ; + +#endif // NO_ZLIB + +} + +/** + * Render CSW encoded pulses to the output stream. + */ +uint csw_render_block( bool & level, const uint compression, const uint sample_rate, const byte * const data, const uint size ) +{ + // Process the data depending on the compression. + + switch ( compression ) { + case 1: { + return csw_render_block( level, sample_rate, data, size ) ; + } + case 2: { + Buffer buffer ; + csw_unpack_block( buffer, data, size ) ; + return csw_render_block( level, sample_rate, buffer.get_data(), buffer.get_data_size() ) ; + } + default: { + warn( "unsupported CSW compression 0x%02x scheme", compression ) ; + return 0 ; + } + } +} + +/** + * Render given CSW file to the PZX output stream. + */ +void csw_render( const byte * const data, const uint size ) +{ + hope( data ) ; + hope( size >= 0x20 ) ; + + // Check the version. + + const uint major = GET1(0x17) ; + const uint minor = GET1(0x18) ; + + uint supported_minor = 0 ; + uint header_size = 0x20 ; + + switch ( major ) { + case 1: { + supported_minor = 1 ; + break ; + } + case 2: { + header_size = 0x34 ; + break ; + } + default: { + fail( "unsupported CSW major version %u.%02u - stopping", major, minor ) ; + } + } + + if ( header_size > size ) { + fail( "CSW header is incomplete" ) ; + } + + if ( minor > supported_minor ) { + warn( "unsupported CSW minor version %u.%02u - proceeding", major, minor ) ; + } + + // Extract the necessary info. + + uint sample_rate = 0 ; + uint compression = 0 ; + uint flags = 0 ; + uint data_offset = header_size ; + + switch ( major ) { + case 1: { + sample_rate = GET2(0x19) ; + compression = GET1(0x1B) ; + flags = GET1(0x1C) ; + break ; + } + case 2: { + sample_rate = GET4(0x19) ; + compression = GET1(0x21) ; + flags = GET1(0x22) ; + data_offset += GET1(0x23) ; + break ; + } + } + + // Verify sample rate. + + if ( sample_rate == 0 ) { + fail( "invalid CSW sample rate %u", sample_rate ) ; + } + + // Prepare data block. + + if ( data_offset > size ) { + fail( "CSW file is incomplete" ) ; + } + + const byte * const block = data + data_offset ; + const uint block_size = size - data_offset ; + + // Prepare initial level. + + bool level = ( ( flags & 1 ) != 0 ) ; + + // Process the data depending on the compression. + + const uint pulse_count = csw_render_block( level, compression, sample_rate, block, block_size ) ; + + // Verify the pulse count matched. + + if ( major == 2 ) { + const uint expected_pulse_count = GET4(0x1D) ; + if ( pulse_count != expected_pulse_count ) { + warn( "real CSW pulse count %u doesn't match the advertised pulse count %u", pulse_count, expected_pulse_count ) ; + } + } +} diff --git a/src/csw.h b/src/csw.h new file mode 100644 index 0000000..924ee1c --- /dev/null +++ b/src/csw.h @@ -0,0 +1,27 @@ +// $Id: csw.h 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file CSW stuff. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef CSW_H +#define CSW_H 1 + +#ifndef BUFFER_H +#include "buffer.h" +#endif + +// Interface. + +uint csw_render_block( bool & level, const uint sample_rate, const byte * const data, const uint size ) ; +void csw_unpack_block( Buffer & buffer, const byte * const data, const uint size ) ; + +uint csw_render_block( bool & level, const uint compression, const uint sample_rate, const byte * const data, const uint size ) ; + +void csw_render( const byte * const data, const uint size ) ; + +#endif // CSW_H diff --git a/src/csw2pzx.cpp b/src/csw2pzx.cpp new file mode 100644 index 0000000..9f46c64 --- /dev/null +++ b/src/csw2pzx.cpp @@ -0,0 +1,104 @@ +// $Id: csw2pzx.cpp 359 2007-08-21 06:45:06Z patrik $ + +/** + * @file CSW->PZX convertor. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" +#include "csw.h" + +/** + * Convert given CSW file to PZX file. + */ +extern "C" +int main( int argc, char * * argv ) +{ + // Make sure the standard I/O is in binary mode. + + set_binary_mode( stdin ) ; + set_binary_mode( stdout ) ; + + // Parse the command line. + + const char * input_name = NULL ; + const char * output_name = NULL ; + + for ( int i = 1 ; i < argc ; i++ ) { + if ( argv[ i ][ 0 ] != '-' ) { + if ( input_name ) { + fail( "multiple input file names specified" ) ; + } + input_name = argv[ i ] ; + continue ; + } + switch ( argv[ i ][ 1 ] ) { + case 'o': { + if ( output_name ) { + fail( "multiple output file names specified" ) ; + } + output_name = argv[ ++i ] ; + break ; + } + default: { + fprintf( stderr, "error: invalid option %s\n", argv[ i ] ) ; + + // Fall through. + } + case 'h': { + fprintf( stderr, "usage: csw2pzx [-o output_file] [input_file]\n" ) ; + fprintf( stderr, "-o f write output to given file instead of standard output\n" ) ; + return EXIT_FAILURE ; + } + } + } + + // Now read in the input file. + + FILE * const input_file = ( input_name ? fopen( input_name, "rb" ) : stdin ) ; + if ( input_file == NULL ) { + fail( "unable to open input file" ) ; + } + + Buffer buffer( 256 * 1024 ) ; + + if ( ! buffer.read( input_file ) ) { + fail( "error reading input file" ) ; + } + + fclose( input_file ) ; + + // Make sure it is the CSW file. + + if ( buffer.get_data_size() < 32 || std::memcmp( buffer.get_data(), "Compressed Square Wave\x1a", 23 ) != 0 ) { + fail( "input is not a CSW file" ) ; + } + + // Open the output file. + + FILE * const output_file = ( output_name ? fopen( output_name, "wb" ) : stdout ) ; + if ( output_file == NULL ) { + fail( "unable to open output file" ) ; + } + + // Bind the PZX stream to output file. + + pzx_open( output_file ) ; + + // Now let the CSW renderer render the output to PZX stream. + + csw_render( buffer.get_data(), buffer.get_data_size() ) ; + + // Finally, close the PZX stream and make sure there were no errors. + + pzx_close() ; + + if ( ferror( output_file ) != 0 || fclose( output_file ) != 0 ) { + fail( "error while closing the output file" ) ; + } + + return EXIT_SUCCESS ; +} diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 0000000..cb3f39f --- /dev/null +++ b/src/debug.h @@ -0,0 +1,33 @@ +// $Id: debug.h 315 2007-06-25 20:49:34Z patrik $ + +/** + * @file Debug support. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef DEBUG_H +#define DEBUG_H 1 + +#ifndef SYSDEFS_H +#include "sysdefs.h" +#endif + +#include +#include + +// Just a lightweight version of some of the stuff I am used to. + +#ifdef DEBUG +#define hope(c) while(!(c)){std::fprintf(stderr,"hope: %s (%s:%u)\n",#c,__FILE__,__LINE__);std::abort();} +#else +#define hope(c) void(0) +#endif + +#define fail(f,...) (std::fprintf(stderr,"error: " f "\n",##__VA_ARGS__),std::exit(EXIT_FAILURE)) +#define warn(f,...) (std::fprintf(stderr,"warning: " f "\n",##__VA_ARGS__)) +#define inform(f,...) (std::fprintf(stderr,"info: " f "\n",##__VA_ARGS__)) + +#endif // DEBUG_H diff --git a/src/endian.h b/src/endian.h new file mode 100644 index 0000000..63663da --- /dev/null +++ b/src/endian.h @@ -0,0 +1,138 @@ +// $Id: endian.h 378 2007-09-13 12:16:10Z patrik $ + +/** + * @file Byte order conversions. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef ENDIAN_H +#define ENDIAN_H + +#ifndef TYPES_H +#include "types.h" +#endif + +/** + * Swap the endian order of given value. + */ +//@{ + +template< typename Type > +inline Type swapped_endian( const Type value ) ; + +template<> +inline u8 swapped_endian( const u8 value ) +{ + return value ; +} + +template<> +inline s8 swapped_endian( const s8 value ) +{ + return value ; +} + +template<> +inline u16 swapped_endian( const u16 value ) +{ + return static_cast< u16 >( + ( ( value & 0x00FF ) << 8 ) | + ( value >> 8 ) + ) ; +} + +template<> +inline s16 swapped_endian( const s16 value ) +{ + return static_cast< s16 >( swapped_endian( static_cast< u16 >( value ) ) ) ; +} + +template<> +inline u32 swapped_endian( const u32 value ) +{ + return static_cast< u32 >( + ( ( value & 0x000000FF ) << 24 ) | + ( ( value & 0x0000FF00 ) << 8 ) | + ( ( value & 0x00FF0000 ) >> 8 ) | + ( value >> 24 ) + ) ; +} + +template<> +inline s32 swapped_endian( const s32 value ) +{ + return static_cast< s32 >( swapped_endian( static_cast< u32 >( value ) ) ) ; +} + +//@} + +/** + * Keep given value in native endian order. + * + * Used for documentation purposes, that the conversion was not ommited by mistake + * and that the value should really remain in the native endian order. + */ +template< typename Type > +inline Type native_endian( const Type value ) +{ + return value ; +} + +#if ( __BYTE_ORDER == __LITTLE_ENDIAN ) + +/** + * Convert given value to/from little endian order from/to native endian order. + */ +template< typename Type > +inline Type little_endian( const Type value ) +{ + return native_endian( value ) ; +} + +/** + * Convert given value to/from big endian order from/to native endian order. + */ +template< typename Type > +inline Type big_endian( const Type value ) +{ + return swapped_endian( value ) ; +} + +/** + * Macro for preparing tag name in native order. + */ +#define TAG_NAME(a,b,c,d) ((d)<<24|(c)<<16|(b)<<8|(a)) + +#elif ( __BYTE_ORDER == __BIG_ENDIAN ) + +/** + * Convert given value to/from little endian order from/to native endian order. + */ +template< typename Type > +inline Type little_endian( const Type value ) +{ + return swapped_endian( value ) ; +} + +/** + * Convert given value to/from big endian order from/to native endian order. + */ +template< typename Type > +inline Type big_endian( const Type value ) +{ + return native_endian( value ) ; +} + +/** + * Macro for preparing tag name in native order. + */ +#define TAG_NAME(a,b,c,d) ((a)<<24|(b)<<16|(c)<<8|(d)) + +#else +#error "Unknown __BYTE_ORDER." +#endif + +#endif // ENDIAN_H diff --git a/src/pzx.cpp b/src/pzx.cpp new file mode 100644 index 0000000..25c0a5d --- /dev/null +++ b/src/pzx.cpp @@ -0,0 +1,680 @@ +// $Id: pzx.cpp 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file Saving to PZX files. + * + * Reference implementation written in C-like style, so it should be simple + * enough to rewrite to either object oriented or simple procedural languages. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" + +namespace { + +/** + * File currently used for output, if any. + */ +FILE * output_file ; + +/** + * Buffer used for PZX header. + */ +Buffer header_buffer ; + +/** + * Buffer used for accumulating content of current PULS block. + */ +Buffer pulse_buffer ; + +/** + * Buffer used for temporary block data. + */ +Buffer data_buffer ; + +/** + * Buffer used for pulse packing. + */ +Buffer pack_buffer ; + +/** + * Count and duration of most recently stored pulses, not yet commited to the pulse buffer. + */ +//@{ +uint pulse_count ; +uint pulse_duration ; +//@} + +/** + * Level and duration accumulated so far of pulse being currently output. + */ +//@{ +uint last_duration ; +bool last_level ; +//@} + +} + +/** + * Use given file for subsequent PZX output. + */ +void pzx_open( FILE * file ) +{ + hope( file ) ; + + // Remember the file. + + hope( output_file == NULL ) ; + output_file = file ; + + // Make sure the file starts with a PZX header. + + pzx_header( NULL, 0 ) ; +} + +/** + * Commit any buffered PZX output to PZX output file and stop using that file. + */ +void pzx_close( void ) +{ + hope( output_file ) ; + + // Flush pending output. + + pzx_flush() ; + + // Forget about the file. + + output_file = NULL ; +} + +/** + * Write given memory block of given size to output file. + */ +void pzx_write( const void * const data, const uint size ) +{ + hope( data || size == 0 ) ; + hope( output_file ) ; + + // Just write everything, freaking out in case of problems. + + if ( std::fwrite( data, 1, size, output_file ) != size ) { + fail( "error writing to file" ) ; + } +} + +/** + * Write given memory block of given size to output file as PZX block with given tag. + */ +void pzx_write_block( const uint tag, const void * const data, const uint size ) +{ + // Prepare block header. + + u32 header[ 2 ] ; + header[ 0 ] = native_endian< u32 >( tag ) ; + header[ 1 ] = little_endian< u32 >( size ) ; + + // Write the header followed by the data to the file. + + pzx_write( header, sizeof( header ) ) ; + pzx_write( data, size ) ; +} + +/** + * Write content of given buffer to output file as PZX block with given tag. + * + * @note The buffer content is cleared afterwards, making it ready for reuse. + */ +void pzx_write_buffer( const uint tag, Buffer & buffer ) +{ + // Write entire buffer to the file. + + pzx_write_block( tag, buffer.get_data(), buffer.get_data_size() ) ; + + // Clear the buffer so it can be reused right away. + + buffer.clear() ; +} + +/** + * Append given memory block of given size to PZX header block. + */ +void pzx_header( const void * const data, const uint size ) +{ + hope( data || size == 0 ) ; + + // Start with the version number if the buffer is still empty. + + if ( header_buffer.is_empty() ) { + header_buffer.write_little< u8 >( PZX_MAJOR ) ; + header_buffer.write_little< u8 >( PZX_MINOR ) ; + } + + // Now append the provided data. + + header_buffer.write( data, size ) ; +} + + +/** + * Append given amount of characters from given string to PZX header block. + */ +void pzx_info( const void * const string, const uint length ) +{ + // Separate multiple strings with zero byte. + + if ( header_buffer.get_data_size() > 2 ) { + header_buffer.write< u8 >( 0 ) ; + } + + // Write the string itself. + + pzx_header( string, length ) ; +} + +/** + * Append given null terminated string to PZX header block. + */ +void pzx_info( const char * const string ) +{ + hope( string ) ; + pzx_info( string, std::strlen( string ) ) ; +} + +/** + * Append given amount of pulses of given duration to PZX pulse block. + * + * @note The @a count and @a duration must fit in 15 and 31 bits, respectively. + */ +void pzx_store( const uint count, const uint duration ) +{ + hope( count > 0 ) ; + hope( count < 0x8000 ) ; + hope( duration < 0x80000000 ) ; + + // Store the count if there were multiple pulses or the duration encoding requires that. + + if ( count > 1 || duration > 0xFFFF ) { + pulse_buffer.write_little< u16 >( 0x8000 | count ) ; + } + + // Now store the duration itself using either the short or long encoding. + + if ( duration < 0x8000 ) { + pulse_buffer.write_little< u16 >( duration ) ; + } + else { + pulse_buffer.write_little< u16 >( 0x8000 | ( duration >> 16 ) ) ; + pulse_buffer.write_little< u16 >( duration & 0xFFFF ) ; + } +} + +/** + * Append pulse of given duration to PZX pulse block. + * + * @note The @a duration must fit in 31 bits. + */ +void pzx_pulse( const uint duration ) +{ + hope( duration < 0x80000000 ) ; + + // If there were already some pulses before, see if the new one + // has the same duration as well. + + if ( pulse_count > 0 ) { + + // If the duration matches and the count limit permits, just + // increase the repeat count. + + if ( pulse_duration == duration && pulse_count < 0x7FFF ) { + pulse_count++ ; + return ; + } + + // Otherwise store the previous pulse(s) before remembering the new one. + + pzx_store( pulse_count, pulse_duration ) ; + } + + // Remember the new pulse and its duration. + + pulse_duration = duration ; + pulse_count = 1 ; +} + +/** + * Append pulse of given duration and given pulse level to PZX pulse block. + */ +void pzx_out( const uint duration, const bool level ) +{ + // Zero duration doesn't extend anything. + + if ( duration == 0 ) { + return ; + } + + // In case the duration is too long that it would cause overflow + // problems, process it as multiple pulses of same level. + + const uint limit = 0x7FFFFFFF ; + + if ( duration > limit ) { + pzx_out( limit, level ) ; + pzx_out( duration - limit, level ) ; + return ; + } + + // In case the level has changed, output the previously accumulated + // duration and prepare for the new pulse. + + if ( last_level != level ) { + pzx_pulse( last_duration ) ; + last_duration = 0 ; + last_level = level ; + } + + // Extend the current pulse. + + last_duration += duration ; + + // In case the pulse duration exceeds the limit the PZX pulse encoding + // can handle at maximum, use zero pulse to concatenate multiple pulses + // to create pulse of required duration. + + if ( last_duration > limit ) { + pzx_pulse( limit ) ; + pzx_pulse( 0 ) ; + last_duration -= limit ; + } +} + +/** + * Commit any buffered header and/or pulse output to the PZX output file. + */ +void pzx_flush( void ) +{ + // First goes the header, if there is any. + + if ( header_buffer.is_not_empty() ) { + pzx_write_buffer( PZX_HEADER, header_buffer ) ; + } + + // Then finish the last pulse, if there is any. + + if ( last_duration > 0 ) { + pzx_pulse( last_duration ) ; + last_duration = 0 ; + last_level = false ; + } + + // Now if there are some pending pulses, store them to the buffer. + + if ( pulse_count > 0 ) { + pzx_store( pulse_count, pulse_duration ) ; + pulse_count = 0 ; + } + + // Finally write the entire pulse buffer to file, unless it's empty. + + if ( pulse_buffer.is_not_empty() ) { + pzx_write_buffer( PZX_PULSES, pulse_buffer ) ; + } +} + +/** + * Append given data block to PZX output file as PZX data block. + */ +void pzx_data( + const byte * const data, + const uint bit_count, + const bool initial_level, + const uint pulse_count_0, + const uint pulse_count_1, + const word * const pulse_sequence_0, + const word * const pulse_sequence_1, + const uint tail_cycles +) +{ + hope( data || bit_count == 0 ) ; + hope( bit_count < 0x80000000 ) ; + hope( pulse_count_0 <= 0xFF ) ; + hope( pulse_count_1 <= 0xFF ) ; + hope( pulse_sequence_0 || pulse_count_0 == 0 ) ; + hope( pulse_sequence_1 || pulse_count_1 == 0 ) ; + hope( tail_cycles <= 0xFFFF ) ; + + // Flush previously buffered output. + + pzx_flush() ; + + // Prepare the header. + + data_buffer.write_little< u32 >( ( initial_level << 31 ) | bit_count ) ; + data_buffer.write_little< u16 >( tail_cycles ) ; + data_buffer.write_little< u8 >( pulse_count_0 ) ; + data_buffer.write_little< u8 >( pulse_count_1 ) ; + + // Store the sequences. + + for ( uint i = 0 ; i < pulse_count_0 ; i++ ) { + data_buffer.write_little< u16 >( pulse_sequence_0[ i ] ) ; + } + for ( uint i = 0 ; i < pulse_count_1 ; i++ ) { + data_buffer.write_little< u16 >( pulse_sequence_1[ i ] ) ; + } + + // Copy the bit stream data themselves. + + data_buffer.write( data, ( bit_count + 7 ) / 8 ) ; + + // Now write the entire block to the file. + + pzx_write_buffer( PZX_DATA, data_buffer ) ; +} + +/** + * Test if given pulse sequence matches pulses in given pulse stream. + */ +bool pzx_matches( const word * const pulses, const word * const end, const word * const sequence, const uint count ) +{ + hope( pulses ) ; + hope( end ) ; + hope( pulses <= end ) ; + hope( sequence || count == 0 ) ; + + if ( ( count == 0 ) || ( count > uint( end - pulses ) ) ) { + return false ; + } + + for ( uint i = 0 ; i < count ; i++ ) { + if ( pulses[ i ] != sequence[ i ] ) { + return false ; + } + } + + return true ; +} + +/** + * Try to pack given pulses to PZX data block using given pulse sequences. + */ +bool pzx_pack( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const uint pulse_count_0, + const uint pulse_count_1, + const word * const sequence_0, + const word * const sequence_1, + const uint tail_cycles +) +{ + hope( pulses ) ; + hope( pulse_count > 0 ) ; + hope( pulse_count_0 <= 0xFF ) ; + hope( pulse_count_1 <= 0xFF ) ; + hope( sequence_0 || pulse_count_0 == 0 ) ; + hope( sequence_1 || pulse_count_1 == 0 ) ; + + // Prepare for packing. + + pack_buffer.clear() ; + + const word * const end = pulses + pulse_count ; + const word * data = pulses ; + + byte value = 0 ; + uint bit_count = 0 ; + + // Try packing until we reach the end of the pulse stream. + + while ( data < end ) { + + // See which of the sequences matches at given position. + + if ( pzx_matches( data, end, sequence_0, pulse_count_0 ) ) { + value <<= 1 ; + data += pulse_count_0 ; + } + else if ( pzx_matches( data, end, sequence_1, pulse_count_1 ) ) { + value <<= 1 ; + value |= 1 ; + data += pulse_count_1 ; + } + + // Otherwise report failure. + + else { + return false ; + } + + // Include the new bit, checking the maximum limit as well. + + bit_count++ ; + + if ( bit_count >= 0x80000000 ) { + return false ; + } + + // Store the completed byte to the buffer when necessary. + + if ( ( bit_count & 7 ) == 0 ) { + pack_buffer.write< byte >( value ) ; + } + } + + hope( data == end ) ; + + // Output the last byte, if any. + + const uint extra_bits = ( bit_count & 7 ) ; + + if ( extra_bits > 0 ) { + value <<= ( 8 - extra_bits ) ; + pack_buffer.write< byte >( value ) ; + } + + // Now write the data to the DATA block. + + pzx_data( + pack_buffer.get_data(), + bit_count, + initial_level, + pulse_count_0, + pulse_count_1, + sequence_0, + sequence_1, + tail_cycles + ) ; + + // Report success. + + return true ; +} + +/** + * Try to pack given pulses to PZX data block using given pulse sequences. + */ +bool pzx_pack( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const uint pulse_count_0, + const uint pulse_count_1, + const word * const sequence_0, + const word * const sequence_1, + const uint sequence_order, + const uint tail_cycles +) +{ + hope( sequence_0 || pulse_count_0 == 0 ) ; + hope( sequence_1 || pulse_count_1 == 0 ) ; + + // Use the specified sequence order. If it is not specified explicitly, + // try to guess it automatically, using the sequence with shorter + // duration for bit 0. + + uint order = sequence_order ; + + if ( order > 1 ) { + uint duration_0 = 0 ; + for ( uint i = 0 ; i < pulse_count_0 ; i++ ) { + duration_0 += sequence_0[ i ] ; + } + uint duration_1 = 0 ; + for ( uint i = 0 ; i < pulse_count_1 ; i++ ) { + duration_1 += sequence_1[ i ] ; + } + + order = ( ( duration_0 == 0 ) || ( duration_1 == 0 ) || ( duration_0 <= duration_1 ) ? 0 : 1 ) ; + } + + if ( order == 0 ) { + return pzx_pack( pulses, pulse_count, initial_level, pulse_count_0, pulse_count_1, sequence_0, sequence_1, tail_cycles ) ; + } + else { + return pzx_pack( pulses, pulse_count, initial_level, pulse_count_1, pulse_count_0, sequence_1, sequence_0, tail_cycles ) ; + } +} + +/** + * Try to pack given pulses to PZX data block, guessing the pulse sequences automatically. + */ +bool pzx_pack( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const uint sequence_limit, + const uint sequence_order, + const uint tail_cycles +) +{ + hope( pulses || pulse_count == 0 ) ; + + // Make sure the limit is sane. + + uint limit = sequence_limit ; + + if ( limit > pulse_count ) { + limit = pulse_count ; + } + + if ( limit > 255 ) { + limit = 255 ; + } + + // Try all sequence combinations shorter than given limit. + // + // One of the sequences always starts at the beginning. + + const word * const end = pulses + pulse_count ; + + const word * const sequence_0 = pulses ; + + for ( uint pulse_count_0 = limit ; pulse_count_0 > 0 ; pulse_count_0-- ) { + + // Find where the other sequence starts. + // + // Note that matching fails before we try to reach behind the buffer end, + // so we don't have to deal with that explicitly. + + const word * sequence_1 = pulses ; + + while ( pzx_matches( sequence_1, end, sequence_0, pulse_count_0 ) ) { + sequence_1 += pulse_count_0 ; + } + + // In the rare case the entire stream can be encoded with just one sequence, do that. + + if ( sequence_1 == end ) { + pzx_pack( pulses, pulse_count, initial_level, pulse_count_0, 0, sequence_0, NULL, sequence_order, tail_cycles ) ; + return true ; + } + + // Otherwise try shortening the second sequence until the packing succeeds. + // + // Again, note that matching fails before we try to reach behind the buffer end, + // so we don't have to deal with that explicitly. + + for ( uint pulse_count_1 = limit ; pulse_count_1 > 0 ; pulse_count_1-- ) { + if ( pzx_pack( pulses, pulse_count, initial_level, pulse_count_0, pulse_count_1, sequence_0, sequence_1, sequence_order, tail_cycles ) ) { + return true ; + } + } + } + + // Report failure. + + return false ; +} + +/** + * Output given pulses to the output. + */ +void pzx_pulses( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const uint tail_cycles +) +{ + hope( pulses || pulse_count == 0 ) ; + + // Output each pulse in turn. + + bool level = initial_level ; + + for ( uint i = 0 ; i < pulse_count ; i++ ) { + pzx_out( pulses[ i ], level ) ; + level = ! level ; + } + + pzx_out( tail_cycles, level ) ; +} + +/** + * Append PZX pause block of pause of given duration and level to PZX output file. + * + * @note The duration must fit in 31 bits. + */ +void pzx_pause( const uint duration, const bool level ) +{ + hope( duration < 0x80000000 ) ; + + pzx_flush() ; + data_buffer.write_little< u32 >( ( level << 31 ) | duration ) ; + pzx_write_buffer( PZX_PAUSE, data_buffer ) ; +} + +/** + * Append PZX stop block with given flags to PZX output file. + */ +void pzx_stop( const uint flags ) +{ + hope( flags <= 0xFFFF ) ; + + pzx_flush() ; + data_buffer.write_little< u16 >( flags ) ; + pzx_write_buffer( PZX_STOP, data_buffer ) ; +} + +/** + * Append PZX browse block using given amount of characters from given string to PZX output file. + */ +void pzx_browse( const void * const string, const uint length ) +{ + pzx_flush() ; + pzx_write_block( PZX_BROWSE, string, length ) ; +} + +/** + * Append PZX browse block using given string to PZX output file. + */ +void pzx_browse( const char * const string ) +{ + hope( string ) ; + pzx_browse( string, std::strlen( string ) ) ; +} diff --git a/src/pzx.h b/src/pzx.h new file mode 100644 index 0000000..9cbea2c --- /dev/null +++ b/src/pzx.h @@ -0,0 +1,98 @@ +// $Id: pzx.h 317 2007-06-25 20:55:26Z patrik $ + +/** + * @file PZX stuff. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef PZX_H +#define PZX_H 1 + +#include + +#ifndef BUFFER_H +#include "buffer.h" +#endif + +// PZX version we support. + +const byte PZX_MAJOR = 1 ; +const byte PZX_MINOR = 0 ; + +// PZX block tags. + +const uint PZX_HEADER = TAG_NAME('P','Z','X','T') ; +const uint PZX_PULSES = TAG_NAME('P','U','L','S') ; +const uint PZX_DATA = TAG_NAME('D','A','T','A') ; +const uint PZX_PAUSE = TAG_NAME('P','A','U','S') ; +const uint PZX_STOP = TAG_NAME('S','T','O','P') ; +const uint PZX_BROWSE = TAG_NAME('B','R','W','S') ; + +// Interface. + +void pzx_open( FILE * file ) ; +void pzx_close( void ) ; + +void pzx_write( const void * const data, const uint size ) ; +void pzx_write_block( const uint tag, const void * const data, const uint size ) ; +void pzx_write_buffer( const uint tag, Buffer & buffer ) ; + +void pzx_header( const void * const data, const uint size ) ; +void pzx_info( const void * const string, const uint size ) ; +void pzx_info( const char * const string ) ; + +void pzx_store( const uint count, const uint duration ) ; +void pzx_pulse( const uint duration ) ; +void pzx_out( const uint duration, const bool level ) ; + +void pzx_flush( void ) ; + +void pzx_data( + const byte * const data, + const uint bit_count, + const bool initial_level, + const uint pulse_count_0, + const uint pulse_count_1, + const word * const pulse_sequence_0, + const word * const pulse_sequence_1, + const uint tail_cycles +) ; + +bool pzx_pack( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const word * const sequence_0, + const word * const sequence_1, + const uint pulse_count_0, + const uint pulse_count_1, + const uint tail_cycles +) ; + +bool pzx_pack( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const uint sequence_limit, + const uint sequence_order, + const uint tail_cycles +) ; + +void pzx_pulses( + const word * const pulses, + const uint pulse_count, + const bool initial_level, + const uint tail_cycles +) ; + +void pzx_pause( const uint duration, const bool level ) ; + +void pzx_stop( const uint flags ) ; + +void pzx_browse( const void * const data, const uint size ) ; +void pzx_browse( const char * const string ) ; + +#endif // PZX_H diff --git a/src/pzx2txt.cpp b/src/pzx2txt.cpp new file mode 100644 index 0000000..a5975ce --- /dev/null +++ b/src/pzx2txt.cpp @@ -0,0 +1,671 @@ +// $Id: pzx2txt.cpp 1489 2011-04-21 19:07:40Z patrik $ + +/** + * @file PZX->TXT convertor. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" + +/** + * Global options. + */ +namespace { + +/** + * Flag set when data should be dumped as pulses. + */ +bool option_dump_pulses ; + +/** + * Flag set when data should be dumped as ASCII characters when possible. + */ +bool option_dump_ascii ; + +/** + * Flag set when data should be dumped as headers when possible. + */ +bool option_dump_headers ; + +/** + * Flag set when dumping of content of DATA blocks should be suppresed. + */ +bool option_skip_data ; + +/** + * Flag set when each pulse should be printed on separate line. + */ +bool option_expand_pulses ; + +/** + * Flag set when initial level of each pulse should be printed as well. + */ +bool option_annotate_pulses ; + +} ; + +/** + * Fetch value of specified type from given data block. + */ +template< typename Type > +Type fetch( const byte * & data, uint & data_size ) +{ + hope( data ) ; + + if ( sizeof( Type ) > data_size ) { + fail( "incomplete block detected" ) ; + } + + const Type value = little_endian( * reinterpret_cast< const Type * >( data ) ) ; + + data += sizeof( Type ) ; + data_size -= sizeof( Type ) ; + + return value ; +} + +/** + * Skip given amount of bytes in given data block. + */ +void skip( const uint amount, const byte * & data, uint & data_size ) +{ + hope( data ) ; + + if ( amount > data_size ) { + fail( "incomplete block detected" ) ; + } + + data += amount ; + data_size -= amount ; +} + +/** + * Macros for convenient fetching of values from current block. + */ +//@{ +#define GET1() fetch< u8 >( data, data_size ) +#define GET2() fetch< u16 >( data, data_size ) +#define GET4() fetch< u32 >( data, data_size ) +#define SKIP(n) skip( n, data, data_size ) +//@} + +/** + * Dump single string to a file. + */ +void dump_string( FILE * const output_file, const char * const prefix, const byte * const data, const uint data_size ) +{ + hope( prefix ) ; + hope( data ) ; + + fprintf( output_file, "%s \"", prefix ) ; + + for ( uint i = 0 ; i < data_size ; i++ ) { + const byte b = data[ i ] ; + + // Escape special characters. + + switch ( b ) { + case '\\': + case '"': + { + fprintf( output_file, "\\%c", b ) ; + continue ; + } + case '\n': + { + fprintf( output_file, "\\n" ) ; + continue ; + } + case '\r': + { + fprintf( output_file, "\\r" ) ; + continue ; + } + case '\t': + { + fprintf( output_file, "\\t" ) ; + continue ; + } + } + + // Any other control characters are printed in hex. + + if ( b < 32 ) { + fprintf( output_file, "\\x%02X", b ) ; + continue ; + } + + // Anything else is printed verbatim. Note that this includes any characters + // greater than 127, as the strings are supposed to be in UTF-8 encoding. + + fprintf( output_file, "%c", b ) ; + } + + fprintf( output_file, "\"\n" ) ; +} + +/** + * Dump strings separated with null characters to a file. + */ +void dump_strings( FILE * const output_file, const char * const prefix, const byte * data, uint data_size ) +{ + hope( data ) ; + + const byte * const end = data + data_size ; + while ( data < end ) { + const byte * const string = data ; + while ( data < end && *data != 0 ) { + data++ ; + } + dump_string( output_file, prefix, string, data - string ) ; + data++ ; + } +} + +/** + * Dump single data line to a file. + */ +void dump_data_line( FILE * const output_file, const byte * const data, const uint data_size, const bool dump_ascii = false ) +{ + hope( data ) ; + + if ( data_size == 0 ) { + return ; + } + + fprintf( output_file, "BODY " ) ; + + for ( uint i = 0 ; i < data_size ; i++ ) { + const byte b = data[ i ] ; + if ( dump_ascii && b > 32 && b < 127 ) { + fprintf( output_file, ".%c", b ) ; + } + else { + fprintf( output_file, "%02X", b ) ; + } + } + + fprintf( output_file, "\n" ) ; +} + +/** + * Dump data block to a file. + */ +void dump_data( FILE * const output_file, const byte * data, uint data_size, const bool dump_ascii = false ) +{ + hope( data ) ; + + if ( option_skip_data ) { + return ; + } + + const uint limit = 32 ; + while ( data_size > limit ) { + dump_data_line( output_file, data, limit, dump_ascii ) ; + data += limit ; + data_size -= limit ; + } + dump_data_line( output_file, data, data_size, dump_ascii ) ; +} + +/** + * Dump given amount of pulses of given duration to a file, toggling level as appropriate. + */ +void dump_pulses( + FILE * const output_file, + bool & level, + const uint duration, + uint count = 1 +) +{ + // Output the appropriate number of pulses, according to the command line options. + + if ( option_expand_pulses ) { + while ( count-- > 0 ) { + if ( option_annotate_pulses ) { + fprintf( output_file, "PULSE%u %u\n", uint( level ), duration ) ; + } + else { + fprintf( output_file, "PULSE %u\n", duration ) ; + } + level = ! level ; + } + return ; + } + + if ( option_annotate_pulses ) { + fprintf( output_file, "PULSE%u %u", uint( level ), duration ) ; + } + else { + fprintf( output_file, "PULSE %u", duration ) ; + } + if ( count > 1 ) { + fprintf( output_file, " %u", count ) ; + } + fprintf( output_file, "\n" ) ; + level ^= ( count & 1 ) ; +} + +/** + * Dump bits from given byte using given (little endian) pulse sequences. + */ +void dump_bits( + FILE * const output_file, + bool & level, + uint bit_count, + uint bits, + const uint pulse_count_0, + const uint pulse_count_1, + const byte * const sequence_0, + const byte * const sequence_1 +) +{ + hope( sequence_0 || pulse_count_0 == 0 ) ; + hope( sequence_1 || pulse_count_1 == 0 ) ; + + // Output all bits. + + while ( bit_count-- > 0 ) { + + // Choose the appropriate sequence for given bit. + + const byte * sequence ; + uint count ; + + if ( ( bits & 0x80 ) == 0 ) { + sequence = sequence_0 ; + count = pulse_count_0 ; + } + else { + sequence = sequence_1 ; + count = pulse_count_1 ; + } + + // Use next bit next time. + + bits <<= 1 ; + + // Now output the appropriate amount of pulses. + + while ( count-- > 0 ) { + uint duration = *sequence++ ; + duration += *sequence++ << 8 ; + dump_pulses( output_file, level, duration ) ; + } + } +} + +/** + * Dump given bit sequence to given output file. + */ +void dump_bit_sequence( FILE * const output_file, const uint index, const byte * sequence, uint count ) +{ + hope( sequence ) ; + + fprintf( output_file, "BIT%u", index ) ; + + while ( count-- > 0 ) { + uint duration = *sequence++ ; + duration += *sequence++ << 8 ; + fprintf( output_file, " %u", duration ) ; + } + + fprintf( output_file, "\n" ) ; +} + +/** + * Dump the DATA block to given file. + */ +void dump_data_block( FILE * const output_file, const byte * data, uint data_size ) +{ + hope( data ) ; + + // Fetch the numbers. + + uint bit_count = GET4() ; + const uint tail_cycles = GET2() ; + const uint pulse_count_0 = GET1() ; + const uint pulse_count_1 = GET1() ; + + // Extract initial pulse level. + + bool level = ( ( bit_count >> 31 ) != 0 ) ; + + bit_count &= 0x7FFFFFFF ; + + // Fetch the sequences. Note that we keep them little endian here. + + const byte * const sequence_0 = data ; + SKIP( 2 * pulse_count_0 ) ; + + const byte * const sequence_1 = data ; + SKIP( 2 * pulse_count_1 ) ; + + // Make sure the bit count matches the block size. + + if ( data_size != ( ( bit_count + 7 ) / 8 ) ) { + fail( "bit count %u does not match the actual data size %u", bit_count, data_size ) ; + } + + // Dump everything as pulses if requested. + + if ( option_dump_pulses ) { + + fprintf( output_file, "PULSES\n" ) ; + + // Make sure the level is high by using zero pulse if necessary. + + if ( level ) { + level = false ; + dump_pulses( output_file, level, 0 ) ; + } + + // Dump all the bits. + + while ( bit_count > 8 ) { + dump_bits( output_file, level, 8, *data++, pulse_count_0, pulse_count_1, sequence_0, sequence_1 ) ; + bit_count -= 8 ; + } + dump_bits( output_file, level, bit_count, *data, pulse_count_0, pulse_count_1, sequence_0, sequence_1 ) ; + + // Include the tail pulse if necessary. + + if ( tail_cycles > 0 ) { + dump_pulses( output_file, level, tail_cycles ) ; + } + + return ; + } + + // Otherwise print the data normally, staring with all the header info. + + fprintf( output_file, "DATA %u\n", level ) ; + + fprintf( output_file, "SIZE %u\n", ( bit_count / 8 ) ) ; + if ( ( bit_count & 7 ) != 0 ) { + fprintf( output_file, "BITS %u\n", ( bit_count & 7 ) ) ; + } + + fprintf( output_file, "TAIL %u\n", tail_cycles ) ; + + // Now dump the bit sequences used. + + dump_bit_sequence( output_file, 0, sequence_0, pulse_count_0 ) ; + dump_bit_sequence( output_file, 1, sequence_1, pulse_count_1 ) ; + + // If header dumping is enabled, dump whatever looks like a header in a more readable form. + + if ( option_dump_headers && data_size == 19 ) { + + const uint leader = GET1() ; + const uint type = GET1() ; + fprintf( output_file, "BYTE %u %u\n", leader, type ) ; + + dump_data_line( output_file, data, 10, true ) ; + data += 10 ; + + const uint size = GET2() ; + const uint start = GET2() ; + const uint extra = GET2() ; + fprintf( output_file, "WORD %u %u %u\n", size, start, extra ) ; + + const uint checksum = GET1() ; + fprintf( output_file, "BYTE %u\n", checksum ) ; + return ; + } + + // Otherwise dump the data as they are. + + dump_data( output_file, data, data_size, option_dump_ascii ) ; +} + +/** + * Dump the PULSE block to given file. + */ +void dump_pulse_block( FILE * const output_file, const byte * data, uint data_size ) +{ + hope( data ) ; + + fprintf( output_file, "PULSES\n" ) ; + + bool level = 0 ; + + // Dump all pulses in the block. + + while ( data_size > 0 ) { + + // Fetch the pulse repeat count and duration. + + uint count = 1 ; + uint duration = GET2() ; + if ( duration > 0x8000 ) { + count = duration & 0x7FFF ; + duration = GET2() ; + } + if ( duration >= 0x8000 ) { + duration &= 0x7FFF ; + duration <<= 16 ; + duration |= GET2() ; + } + + // Output the appropriate number of pulses, according to the command line options. + + dump_pulses( output_file, level, duration, count ) ; + } +} + +/** + * Dump given PZX block to given file. + */ +void dump_block( FILE * const output_file, const uint tag, const byte * data, uint data_size ) +{ + hope( data ) ; + + switch ( tag ) { + case PZX_HEADER: { + const uint major = GET1() ; + const uint minor = GET1() ; + if ( major != PZX_MAJOR ) { + fail( "unsupported PZX major version %u.%u - stopping", major, minor ) ; + } + if ( minor > PZX_MINOR ) { + warn( "unsupported PZX minor version %u.%u - proceeding", major, minor ) ; + } + fprintf( output_file, "PZX %u.%u\n", major, minor ) ; + dump_strings( output_file, "INFO", data, data_size ) ; + return ; + } + case PZX_PULSES: { + dump_pulse_block( output_file, data, data_size ) ; + return ; + } + case PZX_DATA: { + dump_data_block( output_file, data, data_size ) ; + return ; + } + case PZX_PAUSE: { + const uint duration = GET4() ; + fprintf( output_file, "PAUSE %u %u\n", ( duration & 0x7FFFFFFF ), ( duration >> 31 ) ) ; + break ; + } + case PZX_STOP: { + const uint flags = GET2() ; + fprintf( output_file, "STOP %u\n", flags ) ; + break ; + } + case PZX_BROWSE: { + dump_string( output_file, "BROWSE", data, data_size ) ; + return ; + } + default: { + fprintf( output_file, "TAG " ) ; + fwrite( &tag, 4, 1, output_file ) ; + fprintf( output_file, "\n" ) ; + fprintf( output_file, "SIZE %u\n", data_size ) ; + dump_data( output_file, data, data_size, option_dump_ascii ) ; + return ; + } + } + + if ( data_size > 0 ) { + warn( "excessive data (%u byte%s) detected at end of block", data_size, ( data_size > 1 ? "s" : "" ) ) ; + } +} + +/** + * Convert given PZX file to PZX text dump. + */ +extern "C" +int main( int argc, char * * argv ) +{ + // Make sure the standard input is in binary mode. + + set_binary_mode( stdin ) ; + + // Parse the command line. + + const char * input_name = NULL ; + const char * output_name = NULL ; + + for ( int i = 1 ; i < argc ; i++ ) { + if ( argv[ i ][ 0 ] != '-' ) { + if ( input_name ) { + fail( "multiple input file names specified" ) ; + } + input_name = argv[ i ] ; + continue ; + } + switch ( argv[ i ][ 1 ] ) { + case 'o': { + if ( output_name ) { + fail( "multiple output file names specified" ) ; + } + output_name = argv[ ++i ] ; + break ; + } + case 'p': { + option_dump_pulses = true ; + break ; + } + case 'a': { + option_dump_ascii = true ; + break ; + } + case 'x': { + option_dump_headers = true ; + break ; + } + case 'd': { + option_skip_data = true ; + break ; + } + case 'e': { + option_expand_pulses = true ; + break ; + } + case 'l': { + option_annotate_pulses = true ; + break ; + } + default: { + fprintf( stderr, "error: invalid option %s\n", argv[ i ] ) ; + + // Fall through. + } + case 'h': { + fprintf( stderr, "usage: pzx2txt [-p|-a|-x|-d|-e] [-o output_file] [input_file]\n" ) ; + fprintf( stderr, "-o f write output to given file instead of standard output\n" ) ; + fprintf( stderr, "-p dump bytes in data blocks as pulses\n" ) ; + fprintf( stderr, "-a dump bytes in data blocks as ASCII characters when possible\n" ) ; + fprintf( stderr, "-x dump bytes in data blocks as headers when possible\n" ) ; + fprintf( stderr, "-d don't dump content of data blocks\n" ) ; + fprintf( stderr, "-e expand pulses, dumping each one on separate line\n" ) ; + fprintf( stderr, "-l print initial level of each pulse dumped\n" ) ; + return EXIT_FAILURE ; + } + } + } + + // Open the input file. + + FILE * const input_file = ( input_name ? fopen( input_name, "rb" ) : stdin ) ; + if ( input_file == NULL ) { + fail( "unable to open input file" ) ; + } + + // Read in the header. + + Buffer buffer ; + if ( buffer.read( input_file, 8 ) != 8 ) { + fail( "error reading input file" ) ; + } + + // Make sure it is really the PZX file. + + const u32 * header = buffer.get_typed_data< u32 >() ; + + if ( header[ 0 ] != PZX_HEADER ) { + fail( "input is not a PZX file" ) ; + } + + // Only then open the output file. + + FILE * const output_file = ( output_name ? fopen( output_name, "w" ) : stdout ) ; + if ( output_file == NULL ) { + fail( "unable to open output file" ) ; + } + + // Now keep reading the blocks and process each one in turn. + + for ( ; ; ) { + + // Extract the tag and size from the header. + + const uint tag = native_endian( header[ 0 ] ) ; + const uint size = little_endian( header[ 1 ] ) ; + + // Read in the block data. + + if ( buffer.read( input_file, size ) != size ) { + fail( "error reading block data" ) ; + } + + // Dump the block. + + dump_block( output_file, tag, buffer.get_data(), size ) ; + + // Read in header of the next block, if there is any. + + const uint bytes_read = buffer.read( input_file, 8 ) ; + header = buffer.get_typed_data< u32 >() ; + + // Stop if there is nothing more. + + if ( bytes_read == 0 ) { + break ; + } + + // Check for errors. + + if ( bytes_read != 8 ) { + fail( "error reading block header" ) ; + } + + // Separate blocks with empty line. + + fprintf( output_file, "\n" ) ; + } + + // Close both input and output files and make sure there were no errors. + + fclose( input_file ) ; + + if ( ferror( output_file ) != 0 || fclose( output_file ) != 0 ) { + fail( "error while closing the output file" ) ; + } + + return EXIT_SUCCESS ; +} diff --git a/src/pzx2wav.cpp b/src/pzx2wav.cpp new file mode 100644 index 0000000..d5e7459 --- /dev/null +++ b/src/pzx2wav.cpp @@ -0,0 +1,384 @@ +// $Id: pzx2wav.cpp 359 2007-08-21 06:45:06Z patrik $ + +/** + * @file PZX->WAV convertor. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" +#include "wav.h" + +/** + * Global options. + */ +namespace { + +/** + * Default sample rate for WAV generation. + */ +const uint default_sample_rate = 44100 ; + +/** + * Sample rate used for WAV generation. + */ +uint option_sample_rate = 0 ; + +} ; + +/** + * Fetch value of specified type from given data block. + */ +template< typename Type > +Type fetch( const byte * & data, uint & data_size ) +{ + hope( data ) ; + + if ( sizeof( Type ) > data_size ) { + fail( "incomplete block detected" ) ; + } + + const Type value = little_endian( * reinterpret_cast< const Type * >( data ) ) ; + + data += sizeof( Type ) ; + data_size -= sizeof( Type ) ; + + return value ; +} + +/** + * Skip given amount of bytes in given data block. + */ +void skip( const uint amount, const byte * & data, uint & data_size ) +{ + hope( data ) ; + + if ( amount > data_size ) { + fail( "incomplete block detected" ) ; + } + + data += amount ; + data_size -= amount ; +} + +/** + * Macros for convenient fetching of values from current block. + */ +//@{ +#define GET1() fetch< u8 >( data, data_size ) +#define GET2() fetch< u16 >( data, data_size ) +#define GET4() fetch< u32 >( data, data_size ) +#define SKIP(n) skip( n, data, data_size ) +//@} + +/** + * Render bits from given byte using given (little endian) pulse sequences. + */ +void render_bits( + bool & level, + uint bit_count, + uint bits, + const uint pulse_count_0, + const uint pulse_count_1, + const byte * const sequence_0, + const byte * const sequence_1 +) +{ + hope( sequence_0 || pulse_count_0 == 0 ) ; + hope( sequence_1 || pulse_count_1 == 0 ) ; + + // Output all bits. + + while ( bit_count-- > 0 ) { + + // Choose the appropriate sequence for given bit. + + const byte * sequence ; + uint count ; + + if ( ( bits & 0x80 ) == 0 ) { + sequence = sequence_0 ; + count = pulse_count_0 ; + } + else { + sequence = sequence_1 ; + count = pulse_count_1 ; + } + + // Use next bit next time. + + bits <<= 1 ; + + // Now output the appropriate amount of pulses. + + while ( count-- > 0 ) { + uint duration = *sequence++ ; + duration += *sequence++ << 8 ; + wav_out( duration, level ) ; + level = ! level ; + } + } +} + +/** + * Render given DATA block to WAV output file. + */ +void render_data_block( const byte * data, uint data_size ) +{ + hope( data ) ; + + // Fetch the numbers. + + uint bit_count = GET4() ; + const uint tail_cycles = GET2() ; + const uint pulse_count_0 = GET1() ; + const uint pulse_count_1 = GET1() ; + + // Extract initial pulse level. + + bool level = ( ( bit_count >> 31 ) != 0 ) ; + + bit_count &= 0x7FFFFFFF ; + + // Fetch the sequences. Note that we keep them little endian here. + + const byte * const sequence_0 = data ; + SKIP( 2 * pulse_count_0 ) ; + + const byte * const sequence_1 = data ; + SKIP( 2 * pulse_count_1 ) ; + + // Make sure the bit count matches the block size. + + if ( data_size != ( ( bit_count + 7 ) / 8 ) ) { + fail( "bit count %u does not match the actual data size %u", bit_count, data_size ) ; + } + + // Now output all the bits. + + while ( bit_count > 8 ) { + render_bits( level, 8, *data++, pulse_count_0, pulse_count_1, sequence_0, sequence_1 ) ; + bit_count -= 8 ; + } + render_bits( level, bit_count, *data, pulse_count_0, pulse_count_1, sequence_0, sequence_1 ) ; + + // And finally output the optional tail pulse. + + wav_out( tail_cycles, level ) ; +} + +/** + * Render given PULSE block to WAV output file. + */ +void render_pulse_block( const byte * data, uint data_size ) +{ + hope( data ) ; + + // Prepare initial level. + + bool level = false ; + + // Render all pulses in the block. + + while ( data_size > 0 ) { + + // Fetch the pulse repeat count and duration. + + uint count = 1 ; + uint duration = GET2() ; + if ( duration > 0x8000 ) { + count = duration & 0x7FFF ; + duration = GET2() ; + } + if ( duration >= 0x8000 ) { + duration &= 0x7FFF ; + duration <<= 16 ; + duration |= GET2() ; + } + + // Output the appropriate number of pulses. + + while ( count-- > 0 ) { + wav_out( duration, level ) ; + level = ! level ; + } + } +} + +/** + * Render given PZX block to WAV output file. + */ +void render_block( const uint tag, const byte * data, uint data_size ) +{ + hope( data ) ; + + switch ( tag ) { + case PZX_HEADER: { + const uint major = GET1() ; + const uint minor = GET1() ; + if ( major != PZX_MAJOR ) { + fail( "unsupported PZX major version %u.%u - stopping", major, minor ) ; + } + if ( minor > PZX_MINOR ) { + warn( "unsupported PZX minor version %u.%u - proceeding", major, minor ) ; + } + break ; + } + case PZX_PULSES: { + render_pulse_block( data, data_size ) ; + break ; + } + case PZX_DATA: { + render_data_block( data, data_size ) ; + break ; + } + case PZX_PAUSE: { + const uint duration = GET4() ; + wav_out( ( duration & 0x7FFFFFFF ), ( duration >> 31 ) ) ; + break ; + } + } +} + +/** + * Convert given PZX file to PZX text render. + */ +extern "C" +int main( int argc, char * * argv ) +{ + // Make sure the standard I/O is in binary mode. + + set_binary_mode( stdin ) ; + set_binary_mode( stdout ) ; + + // Parse the command line. + + const char * input_name = NULL ; + const char * output_name = NULL ; + + for ( int i = 1 ; i < argc ; i++ ) { + if ( argv[ i ][ 0 ] != '-' ) { + if ( input_name ) { + fail( "multiple input file names specified" ) ; + } + input_name = argv[ i ] ; + continue ; + } + switch ( argv[ i ][ 1 ] ) { + case 'o': { + if ( output_name ) { + fail( "multiple output file names specified" ) ; + } + output_name = argv[ ++i ] ; + break ; + } + case 's': { + const char * const arg = argv[ ++i ] ; + if ( arg == NULL ) { + fail( "missing sample rate" ) ; + } + option_sample_rate = uint( atoi( arg ) ) ; + break ; + } + default: { + fprintf( stderr, "error: invalid option %s\n", argv[ i ] ) ; + + // Fall through. + } + case 'h': { + fprintf( stderr, "usage: pzx2wav [-s n] [-o output_file] [input_file]\n" ) ; + fprintf( stderr, "-o f write output to given file instead of standard output\n" ) ; + fprintf( stderr, "-s n use given sample rate instead of default %uHz\n", default_sample_rate ) ; + return EXIT_FAILURE ; + } + } + } + + // Open the input file. + + FILE * const input_file = ( input_name ? fopen( input_name, "rb" ) : stdin ) ; + if ( input_file == NULL ) { + fail( "unable to open input file" ) ; + } + + // Read in the header. + + Buffer buffer ; + if ( buffer.read( input_file, 8 ) != 8 ) { + fail( "error reading input file" ) ; + } + + // Make sure it is really the PZX file. + + const u32 * header = buffer.get_typed_data< u32 >() ; + + if ( header[ 0 ] != PZX_HEADER ) { + fail( "input is not a PZX file" ) ; + } + + // Only then open the output file. + + FILE * const output_file = ( output_name ? fopen( output_name, "wb" ) : stdout ) ; + if ( output_file == NULL ) { + fail( "unable to open output file" ) ; + } + + // Bind the WAV stream to the output file. + + const uint sample_rate = ( option_sample_rate > 0 ? option_sample_rate : default_sample_rate ) ; + + wav_open( output_file, sample_rate, 3500000 ) ; + + // Now keep reading the blocks and process each one in turn. + + for ( ; ; ) { + + // Extract the tag and size from the header. + + const uint tag = native_endian( header[ 0 ] ) ; + const uint size = little_endian( header[ 1 ] ) ; + + // Read in the block data. + + if ( buffer.read( input_file, size ) != size ) { + fail( "error reading block data" ) ; + } + + // Render the block. + + render_block( tag, buffer.get_data(), size ) ; + + // Read in header of the next block, if there is any. + + const uint bytes_read = buffer.read( input_file, 8 ) ; + header = buffer.get_typed_data< u32 >() ; + + // Stop if there is nothing more. + + if ( bytes_read == 0 ) { + break ; + } + + // Check for errors. + + if ( bytes_read != 8 ) { + fail( "error reading block header" ) ; + } + } + + // Close the input file. + + fclose( input_file ) ; + + // Finally, close the WAV stream and make sure there were no errors. + + wav_close() ; + + if ( ferror( output_file ) != 0 || fclose( output_file ) != 0 ) { + fail( "error while closing the output file" ) ; + } + + return EXIT_SUCCESS ; +} diff --git a/src/sysdefs.h b/src/sysdefs.h new file mode 100644 index 0000000..cf33687 --- /dev/null +++ b/src/sysdefs.h @@ -0,0 +1,22 @@ +// $Id: sysdefs.h 318 2007-06-26 08:12:39Z patrik $ + +/** + * @file Platform dependent stuff. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef SYSDEFS_H +#define SYSDEFS_H 1 + +#ifdef _MSC_VER +#include +#include +#define set_binary_mode(file) _setmode( _fileno( file ), _O_BINARY ) +#else +#define set_binary_mode(file) +#endif + +#endif // SYSDEFS_H diff --git a/src/tap.h b/src/tap.h new file mode 100644 index 0000000..00f6512 --- /dev/null +++ b/src/tap.h @@ -0,0 +1,30 @@ +// $Id: tap.h 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file TAP constants. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef TAP_H +#define TAP_H 1 + +#ifndef TYPES_H +#include "types.h" +#endif + +// Default loader constants. + +const uint LEADER_CYCLES = 2168 ; +const uint SHORT_LEADER_COUNT = 3223 ; +const uint LONG_LEADER_COUNT = 8063 ; +const uint SYNC_1_CYCLES = 667 ; +const uint SYNC_2_CYCLES = 735 ; +const uint BIT_0_CYCLES = 855 ; +const uint BIT_1_CYCLES = 1710 ; +const uint TAIL_CYCLES = 945 ; +const uint MILLISECOND_CYCLES = 3500 ; + +#endif // TAP_H diff --git a/src/tap2pzx.cpp b/src/tap2pzx.cpp new file mode 100644 index 0000000..3f3b078 --- /dev/null +++ b/src/tap2pzx.cpp @@ -0,0 +1,173 @@ +// $Id: tap2pzx.cpp 359 2007-08-21 06:45:06Z patrik $ + +/** + * @file TAP->PZX convertor. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" +#include "tap.h" + +/** + * Global options. + */ +namespace { + +/** + * Duration of pauses inserted between tap blocks. + */ +uint option_pause_duration ; + +} ; + +/** + * Convert given TAP file to PZX file. + */ +extern "C" +int main( int argc, char * * argv ) +{ + // Make sure the standard I/O is in binary mode. + + set_binary_mode( stdin ) ; + set_binary_mode( stdout ) ; + + // Parse the command line. + + const char * input_name = NULL ; + const char * output_name = NULL ; + + for ( int i = 1 ; i < argc ; i++ ) { + if ( argv[ i ][ 0 ] != '-' ) { + if ( input_name ) { + fail( "multiple input file names specified" ) ; + } + input_name = argv[ i ] ; + continue ; + } + switch ( argv[ i ][ 1 ] ) { + case 'o': { + if ( output_name ) { + fail( "multiple output file names specified" ) ; + } + output_name = argv[ ++i ] ; + break ; + } + case 'p': { + const char * const arg = argv[ ++i ] ; + if ( arg == NULL ) { + fail( "missing pause duration" ) ; + } + const uint value = uint( atoi( arg ) ) ; + if ( value > 10 * 60 * 1000 ) { + fail( "pause duration %ums is out of range", value ) ; + } + option_pause_duration = value * MILLISECOND_CYCLES ; + break ; + } + default: { + fprintf( stderr, "error: invalid option %s\n", argv[ i ] ) ; + + // Fall through. + } + case 'h': { + fprintf( stderr, "usage: tap2pzx [-p n] [-o output_file] [input_file]\n" ) ; + fprintf( stderr, "-o f write output to given file instead of standard output\n" ) ; + fprintf( stderr, "-p n separate TAP blocks with pause of given duration (in ms)\n" ) ; + return EXIT_FAILURE ; + } + } + } + + // Open the input file. + + FILE * const input_file = ( input_name ? fopen( input_name, "rb" ) : stdin ) ; + if ( input_file == NULL ) { + fail( "unable to open input file" ) ; + } + + // Open the output file. + + FILE * const output_file = ( output_name ? fopen( output_name, "wb" ) : stdout ) ; + if ( output_file == NULL ) { + fail( "unable to open output file" ) ; + } + + // Bind the PZX stream to output file. + + pzx_open( output_file ) ; + + // Now read each TAP block and output it to PZX stream. + + Buffer buffer ; + + for ( ; ; ) { + + // Read in the block header, stop if there is nothing more. + + const uint bytes_read = buffer.read( input_file, 2 ) ; + + if ( bytes_read == 0 ) { + break ; + } + + if ( bytes_read != 2 ) { + fail( "error reading block header" ) ; + } + + // Fetch the block size. + + const word * const header = buffer.get_typed_data< word >() ; + hope( header ) ; + + const uint size = little_endian( *header ) ; + + if ( size == 0 ) { + continue ; + } + + // Read in the block data. + + if ( buffer.read( input_file, size ) != size ) { + fail( "error reading block data" ) ; + } + + // Store the block to the PZX stream. + + const byte * const data = buffer.get_data() ; + hope( data ) ; + + const uint leader_count = ( ( *data < 128 ) ? LONG_LEADER_COUNT : SHORT_LEADER_COUNT ) ; + + pzx_store( leader_count, LEADER_CYCLES ) ; + pzx_store( 1, SYNC_1_CYCLES ) ; + pzx_store( 1, SYNC_2_CYCLES ) ; + + static word sequence_0[] = { BIT_0_CYCLES, BIT_0_CYCLES } ; + static word sequence_1[] = { BIT_1_CYCLES, BIT_1_CYCLES } ; + + pzx_data( data, 8 * size, true, 2, 2, sequence_0, sequence_1, TAIL_CYCLES ) ; + + // Separate the blocks with specified pause if necessary. + + if ( option_pause_duration > 0 ) { + pzx_pause( option_pause_duration, false ) ; + } + } + + // Close the input file. + + fclose( input_file ) ; + + // Finally, close the PZX stream and make sure there were no errors. + + pzx_close() ; + + if ( ferror( output_file ) != 0 || fclose( output_file ) != 0 ) { + fail( "error while closing the output file" ) ; + } + + return EXIT_SUCCESS ; +} diff --git a/src/txt2pzx.cpp b/src/txt2pzx.cpp new file mode 100644 index 0000000..a9622ed --- /dev/null +++ b/src/txt2pzx.cpp @@ -0,0 +1,887 @@ +// $Id: txt2pzx.cpp 359 2007-08-21 06:45:06Z patrik $ + +/** + * @file TXT->PZX convertor. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" + +#include +#include + +/** + * Global stuff. + */ +namespace { + +/** + * Line tags. + */ +//@{ + +#define TAG(a,b,c,d) (((a)<<24|(b)<<16|(c)<<8|(d))&~0x20202020) + +const uint TAG_HEADER = TAG('P','Z','X',' ') ; +const uint TAG_INFO = TAG('I','N','F','O') ; +const uint TAG_PULSE = TAG('P','U','L','S') ; +const uint TAG_PACK = TAG('P','A','C','K') ; +const uint TAG_DATA = TAG('D','A','T','A') ; +const uint TAG_SIZE = TAG('S','I','Z','E') ; +const uint TAG_BITS = TAG('B','I','T','S') ; +const uint TAG_BIT0 = TAG('B','I','T','0') ; +const uint TAG_BIT1 = TAG('B','I','T','1') ; +const uint TAG_TAIL = TAG('T','A','I','L') ; +const uint TAG_BODY = TAG('B','O','D','Y') ; +const uint TAG_BYTE = TAG('B','Y','T','E') ; +const uint TAG_WORD = TAG('W','O','R','D') ; +const uint TAG_XOR = TAG('X','O','R',' ') ; +const uint TAG_ADD = TAG('A','D','D',' ') ; +const uint TAG_SUB = TAG('S','U','B',' ') ; +const uint TAG_PAUSE = TAG('P','A','U','S') ; +const uint TAG_STOP = TAG('S','T','O','P') ; +const uint TAG_BROWSE = TAG('B','R','O','W') ; +const uint TAG_TAG = TAG('T','A','G',' ') ; + +//@} + +/** + * Buffers and variables used for collecting various information about current block. + */ +//@{ + +Buffer data_buffer ; +Buffer bit_0_buffer ; +Buffer bit_1_buffer ; +Buffer pulse_buffer ; +uint sequence_limit ; +uint sequence_order ; +uint tail_cycles ; +uint expected_data_size ; +uint extra_bit_count ; +bool output_level ; +uint unknown_tag ; + +//@} + +/** + * Flag set when pulse sequences should be stored exactly as they were specified. + */ +bool option_preserve_pulses ; + +} ; + +/** + * Parse integral number. + */ +bool parse_number( uint & value, const char * & string, const char * const what, const uint maximum = 0, const bool required = true ) +{ + hope( string ) ; + hope( what ) ; + + // Choose base ourselves, as we don't want the octal behavior. + + uint base = 10 ; + + if ( string[ 0 ] == '0' ) { + switch ( string[ 1 ] ) { + case 'x': + case 'X': + { + base = 16 ; + string += 2 ; + break ; + } + case 'b': + case 'B': + { + base = 2 ; + string += 2 ; + break ; + } + } + } + + // Convert the number. + + errno = 0 ; + char * end = NULL ; + + const unsigned long result = strtoul( string, &end, base ) ; + + // Deal with errors. + + if ( ( string == end ) || ( errno == ERANGE ) ) { + if ( required ) { + warn( "invalid %s encountered in string %s", what, string ) ; + } + return false ; + } + + if ( maximum > 0 && result > maximum ) { + warn( "%s %lu out of range in string %s", what, result, string ) ; + return false ; + } + + // Return the result otherwise. + + value = result ; + + string = end + strspn( end, " \t" ) ; + + return true ; +} + +/** + * Parse boolean value. + */ +bool parse_bool( bool & value, const char * & string, const char * const what, const bool required = true ) +{ + uint number ; + if ( ! parse_number( number, string, what, 1, required ) ) { + return false ; + } + value = ( number != 0 ) ; + return true ; +} + +/** + * Parse hexadecimal digit. + */ +uint parse_hex_digit( const char * & string ) +{ + const char c = *string++ ; + + if ( c >= '0' && c <= '9' ) { + return c - '0' ; + } + if ( c >= 'a' && c <= 'f' ) { + return 10 + c - 'a' ; + } + if ( c >= 'A' && c <= 'F' ) { + return 10 + c - 'A' ; + } + + string-- ; + + warn( "invalid hexadecimal digit 0x%02x (%c) in string %s", c, c, string ) ; + return 0 ; +} + +/** + * Parse hexadecimal number. + */ +uint parse_hex_number( const char * & string ) +{ + uint result = parse_hex_digit( string ) ; + result <<= 4 ; + result |= parse_hex_digit( string ) ; + return result ; +} + +/** + * Parse string argument. + */ +const char * parse_string( const char * const string ) +{ + hope( string ) ; + + // The underlying buffer is ours anyway, so we may as well to use it for output. + + const char * in = string ; + char * out = const_cast< char * >( in ) ; + + // Check leading quote. + + if ( *in == '"' ) { + in++ ; + } + else { + warn( "missing opening quote in a string %s", string ) ; + } + + // Now process each character until we hit end of the string. + + for ( ; ; ) { + + char c = *in++ ; + + // Stop on quote. + + if ( c == '"' ) { + break ; + } + + // Process escaped characters. + + if ( c == '\\' ) { + c = *in++ ; + switch ( c ) { + case 'n': { + c = '\n' ; + break ; + } + case 'r': { + c = '\r' ; + break ; + } + case 't': { + c = '\t' ; + break ; + } + case 'x': { + c = parse_hex_number( in ) ; + break ; + } + } + } + + // Make sure the string it is properly terminated. + // + // Note that it misreports \x00 as well, but that is not valid in the string anyway. + + if ( c == 0 ) { + warn( "missing closing quote in a string %s", string ) ; + break ; + } + + // Store it to the output. + + *out++ = c ; + } + + // Terminate the string and return it. + + *out++ = 0 ; + + return string ; +} + +/** + * Parse string argument. + */ +void parse_data_line( const char * const string ) +{ + hope( string ) ; + + // Process each character until we hit end of line. + + const char * s = string ; + + for ( ; ; ) { + + char c = *s ; + + // Stop when we hit end of line. + + if ( c == 0 ) { + break ; + } + + // If it is a dot-escaped ASCII char, fetch it as it is. + + if ( c == '.' ) { + s++ ; + c = *s++ ; + + // In case the line ends here, assume there was a space which + // was stripped by an editor. + + if ( c == 0 ) { + s-- ; + c = ' ' ; + } + } + + // Otherwise fetch the hex encoded value. + + else { + c = parse_hex_number( s ) ; + } + + // Now store it to the buffer. + + data_buffer.write< u8 >( c ) ; + } +} + +/** + * Make sure that given tag appears only in valid blocks. + */ +void check_tag( const uint tag, const uint block_tag, const uint block_tag_1, const uint block_tag_2 = 0 ) +{ + if ( block_tag == block_tag_1 ) { + return ; + } + + if ( block_tag_2 != 0 && block_tag == block_tag_2 ) { + return ; + } + + const uint tags[] = { big_endian( tag ), big_endian( block_tag ), big_endian( block_tag_1 ) } ; + + if ( block_tag == 0 ) { + fail( "tag %.4s is not valid outside of %.4s block", (char *) tags, (char *) ( tags + 2 ) ) ; + } + else { + fail( "tag %.4s is not valid in %.4s block", (char *) tags, (char *) ( tags + 1 ) ) ; + } +} + +/** + * Finish current block. + */ +void finish_block( uint & tag, const uint new_tag ) +{ + // Flush the previously buffered data. + + if ( tag != 0 ) { + pzx_flush() ; + } + + // Process the gathered data. + + switch ( tag ) { + case TAG_PACK: + { + const word * const pulses = pulse_buffer.get_typed_data< word >() ; + const uint pulse_count = pulse_buffer.get_data_size() / 2 ; + + if ( pzx_pack( pulses, pulse_count, output_level, sequence_limit, sequence_order, 0 ) ) { + // Well done. + } + else if ( + pulse_count > 0 && + pzx_pack( pulses, pulse_count - 1, output_level, sequence_limit, sequence_order, pulses[ pulse_count - 1 ] ) + ) { + // Well done as well. + } + else { + warn( "packing the pulses is not possible" ) ; + pzx_pulses( pulses, pulse_count, output_level, 0 ) ; + } + + pulse_buffer.clear() ; + + output_level ^= ( pulse_count & 1 ) ; + + break ; + + } + case TAG_DATA: + { + uint data_size = data_buffer.get_data_size() ; + if ( data_size >= 0x80000000 / 8 ) { + fail( "the data block is too big" ) ; + } + + uint bit_count = 8 * data_size ; + if ( extra_bit_count > 0 && bit_count > 0 ) { + bit_count -= 8 ; + bit_count += extra_bit_count ; + } + if ( bit_count == 0 ) { + warn( "the data block is empty" ) ; + } + if ( expected_data_size > 0 && ( bit_count / 8 ) != expected_data_size ) { + warn( "the specified byte size %u doesn't match the actual size %u", expected_data_size, bit_count / 8 ) ; + } + + const uint pulse_count_0 = bit_0_buffer.get_data_size() / 2 ; + const uint pulse_count_1 = bit_1_buffer.get_data_size() / 2 ; + + if ( pulse_count_0 > 0xFF || pulse_count_1 > 0xFF ) { + fail( "too many pulses in bit sequence" ) ; + } + if ( pulse_count_0 == 0 || pulse_count_1 == 0 ) { + warn( "the bit sequence is empty" ) ; + } + + pzx_data( + data_buffer.get_data(), + bit_count, + output_level, + pulse_count_0, + pulse_count_1, + bit_0_buffer.get_typed_data< word >(), + bit_1_buffer.get_typed_data< word >(), + tail_cycles + ) ; + + data_buffer.clear() ; + bit_0_buffer.clear() ; + bit_1_buffer.clear() ; + + expected_data_size = 0 ; + extra_bit_count = 0 ; + tail_cycles = 0 ; + output_level = false ; + + break ; + } + case TAG_TAG: + { + const uint data_size = data_buffer.get_data_size() ; + if ( expected_data_size > 0 && data_size != expected_data_size ) { + warn( "the specified byte size %u doesn't match the actual size %u", expected_data_size, data_size ) ; + } + pzx_write_buffer( unknown_tag, data_buffer ) ; + expected_data_size = 0 ; + break ; + } + } + + // Remember new block. + + tag = new_tag ; +} + + +/** + * Process single PZX text dump line. + */ +void process_line( uint & last_block_tag, const char * const line ) +{ + hope( line ) ; + hope( *line ) ; + + const char * s = line ; + + // Ignore comments. + + if ( *s == '#' ) { + return ; + } + + // Fetch the tag at line start. + + uint tag = *s++ ; + tag <<= 8 ; + tag |= *s++ ; + tag <<= 8 ; + tag |= *s++ ; + tag <<= 8 ; + tag |= *s ; + + // Make it uppercase, and convert space to zero as well. + + tag &= ~0x20202020 ; + + // Find the first argument. + + s += strcspn( s, " \t" ) ; + s += strspn( s, " \t" ) ; + + // Process the line according to the line tag. + // + // Note that we don't really care what the rest of the tag is. + + switch ( tag ) { + case TAG_HEADER: + { + finish_block( last_block_tag, tag ) ; + return ; + } + case TAG_INFO: + { + check_tag( tag, last_block_tag, TAG_HEADER ) ; + + pzx_info( parse_string( s ) ) ; + return ; + } + case TAG_PACK: + { + finish_block( last_block_tag, tag ) ; + + parse_bool( output_level, s, "initial pulse level", false ) ; + + sequence_limit = 2 ; + parse_number( sequence_limit, s, "sequence limit", 255, false ) ; + + sequence_order = 2 ; + parse_number( sequence_order, s, "sequence order", 2, false ) ; + + break ; + } + case TAG_PULSE: + { + // Fetch the duration, or start new sequence if necessary. + + uint duration ; + if ( ! parse_number( duration, s, "pulse duration", 0, false ) ) { + if ( option_preserve_pulses || last_block_tag != tag ) { + finish_block( last_block_tag, tag ) ; + } + output_level = false ; + break ; + } + + check_tag( tag, last_block_tag, TAG_PULSE, TAG_PACK ) ; + + // Fetch the optional count, too. + + uint count = 1 ; + parse_number( count, s, "pulse count", 0, false ) ; + + // In case we are packing the pulses, store them to the pulse buffer. + // + // Note that in this case the output level is adjusted after the pulses are packed. + + if ( last_block_tag == TAG_PACK ) { + if ( duration > 0xFFFF ) { + fail( "pulse duration %u is out of range to be packed", duration ) ; + } + while ( count-- > 0 ) { + pulse_buffer.write< word >( duration ) ; + } + break ; + } + + // Store the pulse sequence as specified if requested and possible. + + if ( option_preserve_pulses ) { + if ( count >= 0x8000 ) { + fail( "pulse repeat count %u is out of range to be stored as it is", count ) ; + } + if ( duration >= 0x80000000 ) { + fail( "pulse duration %u is out of range to be stored as it is", duration ) ; + } + pzx_store( count, duration ) ; + output_level ^= ( count & 1 ) ; + break ; + } + + // Otherwise let the output stream process each pulse as needed. + + for ( uint i = 0 ; i < count ; i++ ) { + pzx_out( duration, output_level ) ; + output_level = ! output_level ; + } + break ; + } + case TAG_DATA: + { + finish_block( last_block_tag, tag ) ; + + parse_bool( output_level, s, "initial pulse level", false ) ; + break ; + } + case TAG_SIZE: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + parse_number( expected_data_size, s, "data size" ) ; + break ; + } + case TAG_BITS: + { + check_tag( tag, last_block_tag, TAG_DATA ) ; + + parse_number( extra_bit_count, s, "extra bit count", 8 ) ; + break ; + } + case TAG_BIT0: + { + check_tag( tag, last_block_tag, TAG_DATA ) ; + + uint duration ; + while ( parse_number( duration, s, "pulse duration", 0xFFFF, false ) ) { + bit_0_buffer.write< word >( duration ) ; + } + break ; + } + case TAG_BIT1: + { + check_tag( tag, last_block_tag, TAG_DATA ) ; + + uint duration ; + while ( parse_number( duration, s, "pulse duration", 0xFFFF, false ) ) { + bit_1_buffer.write< word >( duration ) ; + } + break ; + } + case TAG_TAIL: + { + check_tag( tag, last_block_tag, TAG_DATA ) ; + + parse_number( tail_cycles, s, "tail pulse duration", 0xFFFF ) ; + break ; + } + case TAG_BODY: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + parse_data_line( s ) ; + return ; + } + case TAG_BYTE: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + uint value ; + while ( parse_number( value, s, "byte value", 0xFF, false ) ) { + data_buffer.write< u8 >( value ) ; + } + break ; + } + case TAG_WORD: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + uint value ; + while ( parse_number( value, s, "word value", 0xFFFF, false ) ) { + data_buffer.write< u16 >( value ) ; + } + break ; + } + case TAG_XOR: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + uint value = 0 ; + parse_number( value, s, "initial accumulator value", 0xFF, false ) ; + + const byte * p = data_buffer.get_data() ; + uint count = data_buffer.get_data_size() ; + + while ( count-- > 0 ) { + value ^= *p++ ; + } + + data_buffer.write< u8 >( value ) ; + break ; + } + case TAG_ADD: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + uint value = 0 ; + parse_number( value, s, "initial accumulator value", 0xFF, false ) ; + + const byte * p = data_buffer.get_data() ; + uint count = data_buffer.get_data_size() ; + + while ( count-- > 0 ) { + value += *p++ ; + } + + data_buffer.write< u8 >( value ) ; + break ; + } + case TAG_SUB: + { + check_tag( tag, last_block_tag, TAG_DATA, TAG_TAG ) ; + + uint value = 0 ; + parse_number( value, s, "initial accumulator value", 0xFF, false ) ; + + const byte * p = data_buffer.get_data() ; + uint count = data_buffer.get_data_size() ; + + while ( count-- > 0 ) { + value -= *p++ ; + } + + data_buffer.write< u8 >( value ) ; + break ; + } + case TAG_PAUSE: + { + finish_block( last_block_tag, tag ) ; + + uint duration = 1 ; + parse_number( duration, s, "pause duration", 0x7FFFFFFF ) ; + + output_level = false ; + parse_bool( output_level, s, "pause level", false ) ; + + pzx_pause( duration, output_level ) ; + break ; + } + case TAG_STOP: + { + finish_block( last_block_tag, tag ) ; + + uint flags = 0 ; + parse_number( flags, s, "stop flags", 0xFFFF, false ) ; + + pzx_stop( flags ) ; + break ; + } + case TAG_BROWSE: + { + finish_block( last_block_tag, tag ) ; + + pzx_browse( parse_string( s ) ) ; + return ; + } + case TAG_TAG: + { + finish_block( last_block_tag, tag ) ; + + if ( strcspn( s, " \t" ) != 4 ) { + fail( "invalid PZX tag name %s", s ) ; + } + unknown_tag = TAG_NAME( s[0], s[1], s[2], s[3] ) ; + return ; + } + default: { + warn( "invalid line %s", line ) ; + return ; + } + } + + if ( *s != 0 && *s != '#' ) { + warn( "extra characters detected at end of line %s", line ) ; + } +} + +/** + * Process PZX text dump lines. + */ +void process_lines( Buffer & buffer ) +{ + // Terminate the input buffer first so we can check line tags easily and + // make it easy to detect the end at the same time. + + buffer.write< u8 >( '\n' ) ; + buffer.write< u32 >( 0 ) ; + + // Skip byte order marker, if present. Some brain-dead editors save + // those even to UTF-8 encoded files, sigh. + + char * data = buffer.get_typed_data< char >() ; + + if ( ( data[ 0 ] == '\xEF' ) && ( data[ 1 ] == '\xBB' ) && ( data[ 2 ] == '\xBF' ) ) { + data += 3 ; + } + + // Now process line by line. + + uint last_block_tag = 0 ; + + for ( ; ; ) { + + // Skip empty lines and leading whitespace. + + data += std::strspn( data, " \t\r\n" ) ; + + // Stop if we have hit the end of file. + + if ( *data == 0 ) { + break ; + } + + // Remember current line. + + const char * const line = data ; + + // Find end of current line and terminate it. + + data += std::strcspn( data, "\r\n" ) ; + *data++ = 0 ; + + // Now process it. + + process_line( last_block_tag, line ) ; + } + + // Finish the last block. + + finish_block( last_block_tag, 0 ) ; +} + +/** + * Convert given PZX text dump to PZX file. + */ +extern "C" +int main( int argc, char * * argv ) +{ + // Make sure the standard output is in binary mode. + + set_binary_mode( stdout ) ; + + // Parse the command line. + + const char * input_name = NULL ; + const char * output_name = NULL ; + + for ( int i = 1 ; i < argc ; i++ ) { + if ( argv[ i ][ 0 ] != '-' ) { + if ( input_name ) { + fail( "multiple input file names specified" ) ; + } + input_name = argv[ i ] ; + continue ; + } + switch ( argv[ i ][ 1 ] ) { + case 'o': { + if ( output_name ) { + fail( "multiple output file names specified" ) ; + } + output_name = argv[ ++i ] ; + break ; + } + case 'p': { + option_preserve_pulses = true ; + break ; + } + default: { + fprintf( stderr, "error: invalid option %s\n", argv[ i ] ) ; + + // Fall through. + } + case 'h': { + fprintf( stderr, "usage: txt2pzx [-p] [-o output_file] [input_file]\n" ) ; + fprintf( stderr, "-o f write output to given file instead of standard output\n" ) ; + fprintf( stderr, "-p store pulse sequences exactly as specified\n" ) ; + return EXIT_FAILURE ; + } + } + } + + // Now read in the input file. + // + // Note that we could do that line by line, but why bother. + + FILE * const input_file = ( input_name ? fopen( input_name, "r" ) : stdin ) ; + if ( input_file == NULL ) { + fail( "unable to open input file" ) ; + } + + Buffer buffer( 256 * 1024 ) ; + + if ( ! buffer.read( input_file ) ) { + fail( "error reading input file" ) ; + } + + fclose( input_file ) ; + + // Open the output file. + + FILE * const output_file = ( output_name ? fopen( output_name, "wb" ) : stdout ) ; + if ( output_file == NULL ) { + fail( "unable to open output file" ) ; + } + + // Bind the PZX stream to output file. + + pzx_open( output_file ) ; + + // Now process line by line and pass the output to the PZX stream. + + process_lines( buffer ) ; + + // Finally, close the PZX stream and make sure there were no errors. + + pzx_close() ; + + if ( ferror( output_file ) != 0 || fclose( output_file ) != 0 ) { + fail( "error while closing the output file" ) ; + } + + return EXIT_SUCCESS ; +} diff --git a/src/types.h b/src/types.h new file mode 100644 index 0000000..cd76527 --- /dev/null +++ b/src/types.h @@ -0,0 +1,35 @@ +// $Id: types.h 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file Custom types. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef TYPES_H +#define TYPES_H 1 + +// Storage types. + +typedef unsigned char u8 ; +typedef signed char s8 ; +typedef unsigned short u16 ; +typedef signed short s16 ; +typedef unsigned int u32 ; +typedef signed int s32 ; +typedef unsigned long long u64 ; +typedef signed long long s64 ; +typedef float f32 ; + +// Runtime types. + +typedef unsigned char byte ; +typedef unsigned short word ; +typedef unsigned int uint ; +typedef signed int sint ; +typedef unsigned long long uquad ; +typedef signed long long squad ; + +#endif // TYPES_H diff --git a/src/tzx.cpp b/src/tzx.cpp new file mode 100644 index 0000000..72355b0 --- /dev/null +++ b/src/tzx.cpp @@ -0,0 +1,1028 @@ +// $Id: tzx.cpp 1199 2009-05-12 08:32:36Z patrik $ + +/** + * @file Rendering of TZX files. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "tzx.h" +#include "tap.h" +#include "csw.h" +#include "pzx.h" +#include "endian.h" + +/** + * Macros for fetching little endian data from current block. + */ +//@{ +#define GET1(o) (block[o]) +#define GET2(o) (block[o]+(block[(o)+1]<<8)) +#define GET3(o) (block[o]+(block[(o)+1]<<8)+(block[(o)+2]<<16)) +#define GET4(o) (block[o]+(block[(o)+1]<<8)+(block[(o)+2]<<16)+(block[(o)+3]<<24)) +//@} + +/** + * Get size of the mandatory part of given block. + */ +uint tzx_get_header_size( const byte * const block ) +{ + switch ( *block ) { + case TZX_NORMAL_BLOCK: return 1+0x04 ; + case TZX_TURBO_BLOCK: return 1+0x12 ; + case TZX_PURE_TONE: return 1+0x04 ; + case TZX_PULSE_SEQUENCE: return 1+0x01 ; + case TZX_DATA_BLOCK: return 1+0x0A ; + case TZX_SAMPLES: return 1+0x08 ; + case TZX_PAUSE: return 1+0x02 ; + case TZX_GROUP_BEGIN: return 1+0x01 ; + case TZX_GROUP_END: return 1+0x00 ; + case TZX_JUMP: return 1+0x02 ; + case TZX_LOOP_BEGIN: return 1+0x02 ; + case TZX_LOOP_END: return 1+0x00 ; + case TZX_CALL_SEQUENCE: return 1+0x02 ; + case TZX_RETURN: return 1+0x00 ; + case TZX_SELECT_BLOCK: return 1+0x02 ; + case TZX_TEXT_INFO: return 1+0x01 ; + case TZX_MESSAGE: return 1+0x02 ; + case TZX_ARCHIVE_INFO: return 1+0x02 ; + case TZX_HARDWARE_INFO: return 1+0x01 ; + case TZX_CUSTOM_INFO: return 1+0x14 ; + case TZX_GLUE: return 1+0x09 ; + default: return 1+0x04 ; + } +} + +/** + * Get size of the variable data part of given block. + */ +uint tzx_get_data_size( const byte * block ) +{ + switch ( *block++ ) { + case TZX_NORMAL_BLOCK: return GET2(0x02) ; + case TZX_TURBO_BLOCK: return GET3(0x0F) ; + case TZX_PURE_TONE: return 0 ; + case TZX_PULSE_SEQUENCE: return GET1(0x00)*2 ; + case TZX_DATA_BLOCK: return GET3(0x07) ; + case TZX_SAMPLES: return GET3(0x05) ; + case TZX_PAUSE: return 0 ; + case TZX_GROUP_BEGIN: return GET1(0x00) ; + case TZX_GROUP_END: return 0 ; + case TZX_JUMP: return 0 ; + case TZX_LOOP_BEGIN: return 0 ; + case TZX_LOOP_END: return 0 ; + case TZX_CALL_SEQUENCE: return GET2(0x00)*2 ; + case TZX_RETURN: return 0 ; + case TZX_SELECT_BLOCK: return GET2(0x00) ; + case TZX_TEXT_INFO: return GET1(0x00) ; + case TZX_MESSAGE: return GET1(0x01) ; + case TZX_ARCHIVE_INFO: return GET2(0x00) ; + case TZX_HARDWARE_INFO: return GET1(0x00)*3 ; + case TZX_CUSTOM_INFO: return GET4(0x10) ; + case TZX_GLUE: return 0 ; + default: return GET4(0) ; + } +} + +/** + * Get pointer to block following after given block, or NULL in case of failure. + */ +const byte * tzx_get_next_block( const byte * const block, const byte * const tape_end ) +{ + hope( tape_end ) ; + hope( block ) ; + + // No next block in case we get beyond the end. + + if ( block >= tape_end ) { + return NULL ; + } + + // Otherwise get the header size and make sure it doesn't exceed the data available. + + const uint header_size = tzx_get_header_size( block ) ; + hope( header_size > 0 ) ; + + if ( header_size > uint( tape_end - block ) ) { + warn( "TZX block header size exceeds file size" ) ; + return NULL ; + } + + // Only then get the data size and make sure it doesn't exceed the data available either. + + const uint data_size = tzx_get_data_size( block ) ; + + if ( data_size > uint( tape_end - block - header_size ) ) { + warn( "TZX block data size exceeds file size" ) ; + return NULL ; + } + + // Finally compute the position of the next block. + + return ( block + ( header_size + data_size ) ) ; +} + +/** + * Output pulse of given duration and level to the output stream and flip the level afterwards. + */ +void tzx_render_pulse( bool & level, const uint duration ) +{ + // Send the pulse down the PZX stream. + + pzx_out( duration, level ) ; + + // Flip the level for the next pulse to come. + + level = ! level ; +} + +/** + * Output given amount of pulses of given duration to the output stream. + */ +void tzx_render_pulses( bool & level, const uint count, const uint duration ) +{ + // Just output the pulses one by one. + + for ( uint i = 0 ; i < count ; i++ ) { + tzx_render_pulse( level, duration ) ; + } +} + +/** + * Output pilot tone using given arguments to the output stream. + */ +void tzx_render_pilot( + bool & level, + const uint leader_count, + const uint leader_cycles, + const uint sync_1_cycles, + const uint sync_2_cycles +) +{ + tzx_render_pulses( level, leader_count, leader_cycles ) ; + tzx_render_pulse( level, sync_1_cycles ) ; + tzx_render_pulse( level, sync_2_cycles ) ; +} + +/** + * Output data block using given arguments to the output stream. + */ +void tzx_render_data( + bool & level, + const bool initial_level, + const bool final_level_0, + const bool final_level_1, + const byte * const data, + const uint bit_count, + const uint pulse_count_0, + const uint pulse_count_1, + const word * const pulse_sequence_0, + const word * const pulse_sequence_1, + const uint tail_cycles, + const uint pause_length +) +{ + // If there are some bits at all, output the data block. + + if ( bit_count > 0 ) { + + // Output the block. + // + // Note that we terminate the block with tail pulse in case the + // pause is to follow. We do this always as we want the block to be + // properly terminated regardless of the following blocks. + + pzx_data( data, bit_count, initial_level, pulse_count_0, pulse_count_1, pulse_sequence_0, pulse_sequence_1, pause_length > 0 ? tail_cycles : 0 ) ; + + // Adjust the current output level according to the last bit output. + + const uint bit_index = ( bit_count - 1 ) ; + const uint bit_mask = ( 0x80 >> ( bit_index & 7 ) ) ; + const uint last_byte = data[ bit_index / 8 ] ; + level = ( ( ( last_byte & bit_mask ) != 0 ) ? final_level_1 : final_level_0 ) ; + } + + // Now if there was some pause specified, output it as well. + // + // However don't output the pause if we have already used the tail pulse for that + // and the pause was short enough. The output level is low after the pulse in either case, though. + + if ( pause_length > 0 ) { + level = false ; + if ( pause_length > 1 || tail_cycles == 0 || bit_count == 0 ) { + pzx_pause( pause_length * MILLISECOND_CYCLES, level ) ; + } + } +} + +/** + * Output data block using given arguments to the output stream. + */ +void tzx_render_data( + bool & level, + const bool initial_level, + const bool final_level_0, + const bool final_level_1, + const byte * const data, + const uint data_size, + const uint bits_in_last_byte, + const uint bit_0_cycles_1, + const uint bit_0_cycles_2, + const uint bit_1_cycles_1, + const uint bit_1_cycles_2, + const uint tail_cycles, + const uint pause_length +) +{ + // Compute the bit count, taking the amount of bits used in the last byte into account. + + uint bit_count = 8 * data_size ; + + if ( bits_in_last_byte <= 8 && bit_count >= 8 ) { + bit_count -= 8 ; + bit_count += bits_in_last_byte ; + } + + // Prepare the pulse sequences for both bit 0 and 1. + + word s0[ 2 ] ; + word s1[ 2 ] ; + + s0[ 0 ] = word( bit_0_cycles_1 ) ; + s0[ 1 ] = word( bit_0_cycles_2 ) ; + + s1[ 0 ] = word( bit_1_cycles_1 ) ; + s1[ 1 ] = word( bit_1_cycles_2 ) ; + + // Now output the block. + + tzx_render_data( level, initial_level, final_level_0, final_level_1, data, bit_count, 2, 2, s0, s1, tail_cycles, pause_length ) ; +} + +/** + * Output data block using given arguments to the output stream. + */ +void tzx_render_data( + bool & level, + const byte * const data, + const uint data_size, + const uint bits_in_last_byte, + const uint bit_0_cycles, + const uint bit_1_cycles, + const uint tail_cycles, + const uint pause_length +) +{ + tzx_render_data( + level, + level, + level, + level, + data, + data_size, + bits_in_last_byte, + bit_0_cycles, + bit_0_cycles, + bit_1_cycles, + bit_1_cycles, + tail_cycles, + pause_length + ) ; +} + + +/** + * Output pause of given duration (in ms) to the output stream. + */ +void tzx_render_pause( bool & level, const uint duration ) +{ + // Zero pause means no level change at all. + + if ( duration == 0 ) { + return ; + } + + // In case the level is high, leave it so for 1ms before bringing it low. + + if ( level ) { + tzx_render_pulse( level, MILLISECOND_CYCLES ) ; + } + + // Now output the low pulse pause of given duration. + + pzx_pause( duration * MILLISECOND_CYCLES, level ) ; +} + +/** + * Output pulses from given buffer, packing them to DATA block if possible. + */ +void tzx_render_gdb_pulses( const bool initial_level, Buffer & buffer, const uint sequence_limit, const uint sequence_order, const uint tail_cycles ) +{ + const word * const pulses = buffer.get_typed_data< word >() ; + const uint pulse_count = buffer.get_data_size() / 2 ; + + if ( ! pzx_pack( pulses, pulse_count, initial_level, sequence_limit, sequence_order, tail_cycles ) ) { + pzx_pulses( pulses, pulse_count, initial_level, tail_cycles ) ; + } + + buffer.clear() ; +} + +/** + * Output GDB symbol pulse sequence to given buffer. + */ +void tzx_render_gdb_symbol( bool & level, Buffer & buffer, const byte * sequence, const uint pulse_limit ) +{ + // Adjust the output level. + + switch ( *sequence++ ) { + case 0: { + break ; + } + case 1: { + buffer.write< word >( 0 ) ; + level = ! level ; + break ; + } + case 2: { + if ( level ) { + buffer.write< word >( 0 ) ; + } + level = false ; + break ; + } + case 3: { + if ( ! level ) { + buffer.write< word >( 0 ) ; + } + level = true ; + break ; + } + + default: { + warn( "invalid GDB pulse sequence level bits 0x%02x", sequence[ -1 ] ) ; + } + } + + // Now output the pulses. + + for ( uint i = 0 ; i < pulse_limit ; i++ ) { + word duration = *sequence++ ; + duration += *sequence++ << 8 ; + if ( duration == 0 ) { + break ; + } + buffer.write< word >( duration ) ; + level = ! level ; + } +} + +/** + * Decode GDB pilot pulses and send them to the output stream. + */ +void tzx_render_gdb_pilot( + bool & level, + Buffer & buffer, + const byte * data, + uint count, + const byte * const table, + const uint symbol_count, + const uint symbol_pulses +) +{ + const bool initial_level = level ; + + // Output all pilot symbols. + + while ( count-- > 0 ) { + + // Fetch pilot symbol and verify it. + + const uint symbol = *data++ ; + + if ( symbol >= symbol_count ) { + warn ( "pilot symbol %u is out of range <0,%u>", symbol, symbol_count - 1 ) ; + continue ; + } + + // Get the corresponding pulse sequence. + + const byte * const sequence = ( table + ( symbol * ( 2 * symbol_pulses + 1 ) ) ) ; + + // Get the repeat count. + + uint repeat_count = *data++ ; + repeat_count += *data++ << 8 ; + + // Output the symbol as many times as needed. + + while ( repeat_count-- > 0 ) { + tzx_render_gdb_symbol( level, buffer, sequence, symbol_pulses ) ; + } + } + + // Now simply send all the pulses to the output stream, without any + // extra processing. + + tzx_render_gdb_pulses( initial_level, buffer, 0, 0, 0 ) ; +} + +/** + * Decode GDB data pulses and send them to the output stream. + */ +void tzx_render_gdb_data( + bool & level, + Buffer & buffer, + const byte * data, + uint count, + const uint bit_count, + const byte * const table, + const uint symbol_count, + const uint symbol_pulses, + const uint pause_length +) +{ + const bool initial_level = level ; + + // Remember how to order the sequences depending on the first bit. Note that we use + // this even in case of weird symbol counts, as the first bit will usually match + // that of the intended sequence for given bit. + + const uint first_byte = ( count > 0 ? data[ 0 ] : 0 ) ; + const uint first_bit = ( first_byte >> 7 ) ; + const uint sequence_order = ( first_bit & 1 ) ; + + // Output all data symbols. + + uint mask = 0x80 ; + + while ( count-- > 0 ) { + + // Fetch data symbol and verify it. + + uint symbol = 0 ; + for ( uint i = 0 ; i < bit_count ; i++ ) { + symbol <<= 1 ; + if ( ( *data & mask ) != 0 ) { + symbol |= 1 ; + } + mask >>= 1 ; + if ( mask == 0 ) { + mask = 0x80 ; + data++ ; + } + } + + if ( symbol >= symbol_count ) { + warn ( "data symbol %u is out of range <0,%u>", symbol, symbol_count - 1 ) ; + continue ; + } + + // Get the corresponding pulse sequence. + + const byte * const sequence = ( table + ( symbol * ( 2 * symbol_pulses + 1 ) ) ) ; + + // Output the symbol. + + tzx_render_gdb_symbol( level, buffer, sequence, symbol_pulses ) ; + } + + // Now try to pack the pulses to DATA block, and only if it fails, + // output them as they are. Hint the packer about the maximum pulse + // sequence allowed, including the possible extra zero pulse which was + // perhaps added due to the forced level adjustments. + // + // Also try to use the tail pulse when possible, as it is preferred + // form of finishing the final pulse. + + const uint tail_cycles = ( ( pause_length > 0 ) ? MILLISECOND_CYCLES : 0 ) ; + + tzx_render_gdb_pulses( initial_level, buffer, symbol_pulses + 1, sequence_order, tail_cycles ) ; + + // Now if there was some pause specified, output it as well. + + if ( pause_length > 0 ) { + level = false ; + pzx_pause( pause_length * MILLISECOND_CYCLES, level ) ; + } +} + +/** + * Send the GDB block to the output stream. + */ +void tzx_render_gdb( bool & level, const byte * const block, const uint block_size ) +{ + if ( block_size < 0x12 ) { + warn( "TZX GDB block is too small" ) ; + return ; + } + + const byte * const block_end = ( block + 4 + block_size ) ; + + // Precompute the needed values. + + const uint pause_length = GET2(0x04) ; + + const uint pilot_symbols = GET4(0x06) ; + const uint pilot_symbol_pulses = GET1(0x0A) ; + const uint pilot_symbol_count = GET1(0x0B) ? GET1(0x0B) : 256 ; + + const uint data_symbols = GET4(0x0C) ; + const uint data_symbol_pulses = GET1(0x10) ; + const uint data_symbol_count = GET1(0x11) ? GET1(0x11) : 256 ; + + uint data_symbol_bits = 1 ; + while( data_symbol_count > ( 1u << data_symbol_bits ) ) { + data_symbol_bits++ ; + } + + // Compute sizes and positions of the tables and streams. + + const uint pilot_table_size = ( pilot_symbols ? ( pilot_symbol_count * ( pilot_symbol_pulses * 2 + 1 ) ) : 0 ) ; + const uint pilot_stream_size = ( pilot_symbols * 3 ) ; + + const uint data_table_size = ( data_symbols ? ( data_symbol_count * ( data_symbol_pulses * 2 + 1 ) ) : 0 ) ; + const uint data_stream_size = ( ( ( data_symbols * data_symbol_bits ) + 7 ) / 8 ) ; + + const byte * const pilot_table = block + 0x12 ; + const byte * const pilot_stream = pilot_table + pilot_table_size ; + + const byte * const data_table = pilot_stream + pilot_stream_size ; + const byte * const data_stream = data_table + data_table_size ; + + const byte * const end = data_stream + data_stream_size ; + + // Verify the block size. + + if ( + block < pilot_table && + pilot_table <= pilot_stream && + pilot_stream <= data_table && + data_table <= data_stream && + data_stream <= end && + end <= block_end + + ) { + if ( end != block_end ) { + warn( "TZX GDB block contains unused data" ) ; + } + } + else { + warn( "TZX GDB block has invalid size" ) ; + return ; + } + + // Now render the pilot and the data, if either is present. + + Buffer buffer ; + + tzx_render_gdb_pilot( level, buffer, pilot_stream, pilot_symbols, pilot_table, pilot_symbol_count, pilot_symbol_pulses ) ; + tzx_render_gdb_data( level, buffer, data_stream, data_symbols, data_symbol_bits, data_table, data_symbol_count, data_symbol_pulses, pause_length ) ; +} + +/** + * Send the CSW block to the output stream. + */ +void tzx_render_csw( bool & level, const byte * const block, const uint block_size ) +{ + if ( block_size < 0x0E ) { + warn( "TZX CSW block is too small" ) ; + return ; + } + + // Fetch the values. + + const uint pause_length = GET2(0x04) ; + const uint sample_rate = GET3(0x06) ; + const uint compression = GET1(0x09) ; + const uint expected_pulse_count = GET4(0x0A) ; + + const byte * const data = block + 0xE ; + const byte * const block_end = ( block + 4 + block_size ) ; + + if ( sample_rate == 0 ) { + warn( "TZX CSW sample rate %u is invalid", sample_rate ) ; + return ; + } + + // Render the pulses. + + const uint pulse_count = csw_render_block( level, compression, sample_rate, data, block_end - data ) ; + + // Check the pulse count. + + if ( pulse_count != expected_pulse_count ) { + warn( "TZX CSW block actual pulse count %u differs from expected pulse count %u", pulse_count, expected_pulse_count ) ; + } + + // Adjust the level to remain the same as of the last pulse, not the opposite. + + if ( pulse_count > 0 ) { + level = ! level ; + } + + // Output the optional pause. + + tzx_render_pause( level, pause_length ) ; +} + +/** + * Get name of given info type. + */ +const char * tzx_get_info_name( const uint type ) +{ + switch ( type ) { + case 0x00: return "Title" ; + case 0x01: return "Publisher" ; + case 0x02: return "Author" ; + case 0x03: return "Year" ; + case 0x04: return "Language" ; + case 0x05: return "Type" ; + case 0x06: return "Price" ; + case 0x07: return "Protection" ; + case 0x08: return "Origin" ; + case 0xFF: return "Comment" ; + default: return "Info" ; + } +} + +/** + * Convert info from archive info block and pass it to the output stream. + */ +void tzx_convert_info( const byte * const info, const uint info_size, const bool title_only ) +{ + hope( info ) ; + + // Fetch number of info strings. + + const byte * p = info ; + const uint count = *p++ ; + + // Iterate over all strings. + + for ( uint i = 0 ; i < count ; i++ ) { + + // Make sure we don't run away from the info block. + + if ( p + 2 > info + info_size ) { + break ; + } + + // Fetch the info type and string length. + + const uint type = *p++ ; + const uint length = *p++ ; + const byte * const string = p ; + + // Move to next string. + + p += length ; + + if ( p > info + info_size ) { + break ; + } + + // Title is converted only when we are told so, otherwise it is ignored. + + if ( title_only ) { + if ( type == 0x00 ) { + pzx_info( string, length ) ; + return ; + } + continue ; + } + else if ( type == 0x00 ) { + continue ; + } + + // Anything else is converted verbatim. + // + // Note that the output should be in UTF-8, but we don't know what + // code page was used in TZX anyway, so we just let the user to fix + // it himself if he cares enough. + + pzx_info( tzx_get_info_name( type ) ) ; + pzx_info( string, length ) ; + } + + // If we haven't found any title, just make up some if required. + + if ( title_only ) { + pzx_info( "Some tape" ) ; + } +} + +/** + * Set block index according to given relative offset. + */ +bool tzx_set_block_index( uint & block_index, const uint next_index, const sint offset, const uint block_count ) +{ + hope( next_index > 0 ) ; + + // Make sure the offset doesn't jump further than allowed. + + block_index = next_index - 1 ; + + const uint limit = ( offset < 0 ? block_index : block_count - next_index ) ; + const uint distance = uint( offset < 0 ? -offset : offset ) ; + + // In case it does, report error and proceed at next block. + + if ( distance > limit ) { + block_index = next_index ; + return false ; + } + + // Otherwise jump to given block and report success. + + block_index += offset ; + return true ; +} + +// Forward declaration. + +void tzx_process_blocks( + bool & level, + uint & block_index, + const byte * const * const blocks, + const uint block_count, + const uint end_type, + const uint nesting_level +) ; + +/** + * Process given TZX block. + */ +bool tzx_process_block( + bool & level, + uint & block_index, + const byte * const * const blocks, + const uint block_count, + const uint end_type, + const uint nesting_level, + uint & jump_count +) +{ + hope( blocks ) ; + hope( block_index < block_count ) ; + + // Fetch the block start. + + const byte * block = blocks[ block_index++ ] ; + hope( block ) ; + + // Fetch the size of the block data. + + const uint data_size = tzx_get_data_size( block ) ; + + // Now process the block according to its type ID. + + switch ( *block++ ) { + case TZX_NORMAL_BLOCK: + { + const uint leader_count = ( block[4] < 128 ? LONG_LEADER_COUNT : SHORT_LEADER_COUNT ) ; + tzx_render_pilot( level, leader_count, LEADER_CYCLES, SYNC_1_CYCLES, SYNC_2_CYCLES ) ; + tzx_render_data( level, block + 0x04, data_size, 8, BIT_0_CYCLES, BIT_1_CYCLES, TAIL_CYCLES, GET2(0x00) ) ; + break ; + } + case TZX_TURBO_BLOCK: + { + tzx_render_pilot( level, GET2(0x0A), GET2(0x00), GET2(0x02), GET2(0x04) ) ; + tzx_render_data( level, block + 0x12, data_size, GET1(0x0C), GET2(0x06), GET2(0x08), TAIL_CYCLES, GET2(0x0D) ) ; + break ; + } + case TZX_PURE_TONE: + { + tzx_render_pulses( level, GET2(0x02), GET2(0x00) ) ; + break ; + } + case TZX_PULSE_SEQUENCE: + { + uint count = *block++ ; + while ( count-- > 0 ) { + uint duration = *block++ ; + duration += *block++ << 8 ; + tzx_render_pulse( level, duration ) ; + } + break ; + } + case TZX_DATA_BLOCK: + { + tzx_render_data( level, block + 0x0A, data_size, GET1(0x04), GET2(0x00), GET2(0x02), TAIL_CYCLES, GET2(0x05) ) ; + break ; + } + case TZX_SAMPLES: + { + const uint duration = GET2(0x00) ; + tzx_render_data( level, false, false, true, block + 0x08, data_size, GET1(0x04), duration, 0, 0, duration, MILLISECOND_CYCLES, GET2(0x2) ) ; + break ; + } + case TZX_CSW: + { + tzx_render_csw( level, block, data_size ) ; + break ; + } + case TZX_GDB: + { + tzx_render_gdb( level, block, data_size ) ; + break ; + } + case TZX_SET_LEVEL: + { + level = ( GET1(0x04) != 0 ) ; + break ; + } + case TZX_PAUSE: + { + uint duration = GET2(0x00) ; + if ( duration > 0 ) { + tzx_render_pause( level, duration ) ; + } + else { + pzx_stop( 0 ) ; + } + break ; + } + case TZX_STOP_IF_48K: + { + pzx_stop( 1 ) ; + break ; + } + case TZX_GROUP_BEGIN: + { + pzx_browse( block + 1, data_size ) ; + break ; + } + case TZX_GROUP_END: + { + break ; + } + case TZX_JUMP: + { + jump_count++ ; + tzx_set_block_index( block_index, block_index, (s16) GET2(0x00), block_count ) ; + break ; + } + case TZX_LOOP_BEGIN: + { + const uint count = GET2(0x00) ; + const uint next_index = block_index ; + for ( uint i = 0 ; i < count ; i++ ) { + block_index = next_index ; + tzx_process_blocks( level, block_index, blocks, block_count, TZX_LOOP_END, nesting_level ) ; + } + break ; + } + case TZX_LOOP_END: + { + if ( end_type == TZX_LOOP_END ) { + return false ; + } + warn( "unexpected loop end block encountered" ) ; + break ; + } + case TZX_CALL_SEQUENCE: + { + const uint count = GET2(0x00) ; + const uint next_index = block_index ; + for ( uint i = 0 ; i < count ; i++ ) { + if ( ! tzx_set_block_index( block_index, next_index, (s16) GET2(0x02+2*i), block_count ) ) { + break ; + } + tzx_process_blocks( level, block_index, blocks, block_count, TZX_RETURN, nesting_level ) ; + } + block_index = next_index ; + break ; + } + case TZX_RETURN: + { + if ( end_type == TZX_RETURN ) { + return false ; + } + warn( "unexpected return block encountered" ) ; + break ; + } + case TZX_SELECT_BLOCK: + { + warn( "select block was ignored" ) ; + break ; + } + case TZX_TEXT_INFO: + { + pzx_browse( block + 1, data_size ) ; + break ; + } + case TZX_MESSAGE: + { + warn( "message block was ignored" ) ; + break ; + } + case TZX_ARCHIVE_INFO: + { + tzx_convert_info( block + 2, data_size, true ) ; + tzx_convert_info( block + 2, data_size, false ) ; + break ; + } + case TZX_HARDWARE_INFO: + { + warn( "hardware info block was ignored" ) ; + break ; + } + case TZX_CUSTOM_INFO: + { + warn( "custom info block was ignored" ) ; + break ; + } + case TZX_GLUE: + { + const uint major = GET1(0x07) ; + const uint minor = GET1(0x08) ; + if ( major != TZX_MAJOR ) { + warn( "unsupported TZX major version %u.%u encountered - stopping", major, minor ) ; + return false ; + } + if ( minor > TZX_MINOR ) { + warn( "unsupported TZX minor revision %u.%u encountered - proceeding", major, minor ) ; + } + break ; + } + default: { + warn( "unrecognized TZX block 0x%02x was ignored", block[-1] ) ; + break ; + } + } + + return true ; +} + +/** + * Process given sequence of TZX blocks, stopping at block of given type. + */ +void tzx_process_blocks( + bool & level, + uint & block_index, + const byte * const * const blocks, + const uint block_count, + const uint end_type, + const uint nesting_level +) +{ + if ( nesting_level > 10 ) { + warn( "too deep nesting detected - returning" ) ; + return ; + } + + // Simply process block by block, stopping when end block is encountered. + + uint jump_count = 0 ; + + while ( block_index < block_count ) { + if ( ! tzx_process_block( level, block_index, blocks, block_count, end_type, nesting_level + 1, jump_count ) ) { + break ; + } + if ( jump_count > block_count ) { + warn( "too many jumps detected - stopping" ) ; + break ; + } + } +} + +/** + * Render given TZX tape file to the PZX output stream. + */ +void tzx_render( const byte * const tape_start, const byte * const tape_end ) +{ + hope( tape_start ) ; + hope( tape_end ) ; + hope( tape_start <= tape_end ) ; + + // Create table of block starts. + + Buffer block_buffer ; + uint block_count = 0 ; + + const byte * block = tape_start ; + + while ( block < tape_end ) { + + block_buffer.write( block ) ; + + block = tzx_get_next_block( block, tape_end ) ; + + if ( block == NULL ) { + break ; + } + + block_count++ ; + } + + const byte * const * const blocks = block_buffer.get_typed_data< const byte * >() ; + + // Now process process each block in turn. + + bool level = false ; + uint block_index = 0 ; + tzx_process_blocks( level, block_index, blocks, block_count, 0, 0 ) ; +} diff --git a/src/tzx.h b/src/tzx.h new file mode 100644 index 0000000..f71d8f0 --- /dev/null +++ b/src/tzx.h @@ -0,0 +1,55 @@ +// $Id: tzx.h 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file TZX stuff. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef TZX_H +#define TZX_H 1 + +#ifndef TYPES_H +#include "types.h" +#endif + +// TZX version we support. + +const byte TZX_MAJOR = 1 ; +const byte TZX_MINOR = 20 ; + +// TZX block IDs. + +const byte TZX_NORMAL_BLOCK = 0x10 ; +const byte TZX_TURBO_BLOCK = 0x11 ; +const byte TZX_PURE_TONE = 0x12 ; +const byte TZX_PULSE_SEQUENCE = 0x13 ; +const byte TZX_DATA_BLOCK = 0x14 ; +const byte TZX_SAMPLES = 0x15 ; +const byte TZX_CSW = 0x18 ; +const byte TZX_GDB = 0x19 ; +const byte TZX_PAUSE = 0x20 ; +const byte TZX_GROUP_BEGIN = 0x21 ; +const byte TZX_GROUP_END = 0x22 ; +const byte TZX_JUMP = 0x23 ; +const byte TZX_LOOP_BEGIN = 0x24 ; +const byte TZX_LOOP_END = 0x25 ; +const byte TZX_CALL_SEQUENCE = 0x26 ; +const byte TZX_RETURN = 0x27 ; +const byte TZX_SELECT_BLOCK = 0x28 ; +const byte TZX_STOP_IF_48K = 0x2A ; +const byte TZX_SET_LEVEL = 0x2B ; +const byte TZX_TEXT_INFO = 0x30 ; +const byte TZX_MESSAGE = 0x31 ; +const byte TZX_ARCHIVE_INFO = 0x32 ; +const byte TZX_HARDWARE_INFO = 0x33 ; +const byte TZX_CUSTOM_INFO = 0x35 ; +const byte TZX_GLUE = 0x5A ; + +// Interface. + +void tzx_render( const byte * const tape_start, const byte * const tape_end ) ; + +#endif // TZX_H diff --git a/src/tzx2pzx.cpp b/src/tzx2pzx.cpp new file mode 100644 index 0000000..3e94d17 --- /dev/null +++ b/src/tzx2pzx.cpp @@ -0,0 +1,104 @@ +// $Id: tzx2pzx.cpp 359 2007-08-21 06:45:06Z patrik $ + +/** + * @file TZX->PZX convertor. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "pzx.h" +#include "tzx.h" + +/** + * Convert given TZX file to PZX file. + */ +extern "C" +int main( int argc, char * * argv ) +{ + // Make sure the standard I/O is in binary mode. + + set_binary_mode( stdin ) ; + set_binary_mode( stdout ) ; + + // Parse the command line. + + const char * input_name = NULL ; + const char * output_name = NULL ; + + for ( int i = 1 ; i < argc ; i++ ) { + if ( argv[ i ][ 0 ] != '-' ) { + if ( input_name ) { + fail( "multiple input file names specified" ) ; + } + input_name = argv[ i ] ; + continue ; + } + switch ( argv[ i ][ 1 ] ) { + case 'o': { + if ( output_name ) { + fail( "multiple output file names specified" ) ; + } + output_name = argv[ ++i ] ; + break ; + } + default: { + fprintf( stderr, "error: invalid option %s\n", argv[ i ] ) ; + + // Fall through. + } + case 'h': { + fprintf( stderr, "usage: tzx2pzx [-o output_file] [input_file]\n" ) ; + fprintf( stderr, "-o f write output to given file instead of standard output\n" ) ; + return EXIT_FAILURE ; + } + } + } + + // Now read in the input file. + + FILE * const input_file = ( input_name ? fopen( input_name, "rb" ) : stdin ) ; + if ( input_file == NULL ) { + fail( "unable to open input file" ) ; + } + + Buffer buffer( 256 * 1024 ) ; + + if ( ! buffer.read( input_file ) ) { + fail( "error reading input file" ) ; + } + + fclose( input_file ) ; + + // Make sure it is the TZX file. + + if ( buffer.get_data_size() < 10 || std::memcmp( buffer.get_data(), "ZXTape!\x1a", 8 ) != 0 ) { + fail( "input is not a TZX file" ) ; + } + + // Open the output file. + + FILE * const output_file = ( output_name ? fopen( output_name, "wb" ) : stdout ) ; + if ( output_file == NULL ) { + fail( "unable to open output file" ) ; + } + + // Bind the PZX stream to output file. + + pzx_open( output_file ) ; + + // Now let the TZX renderer render the output to PZX stream. + + tzx_render( buffer.get_data(), buffer.get_data_end() ) ; + + // Finally, close the PZX stream and make sure there were no errors. + + pzx_close() ; + + if ( ferror( output_file ) != 0 || fclose( output_file ) != 0 ) { + fail( "error while closing the output file" ) ; + } + + return EXIT_SUCCESS ; +} diff --git a/src/wav.cpp b/src/wav.cpp new file mode 100644 index 0000000..2211acb --- /dev/null +++ b/src/wav.cpp @@ -0,0 +1,208 @@ +// $Id: wav.cpp 336 2007-07-30 19:50:57Z patrik $ + +/** + * @file Rendering to WAV files. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#include "wav.h" +#include "buffer.h" + +namespace { + +/** + * File currently used for output, if any. + */ +FILE * output_file ; + +/** + * Buffer used for holding the complete samples. + */ +Buffer sample_buffer ; + +/** + * Numerator and denominator for converting specified durations to number of samples. + */ +//@{ +uint sample_numerator ; +uint sample_denominator ; +//@} + +/** + * Duration and value of the last sample accumulated so far, both scaled by sample_numerator. + */ +//@{ +uint sample_value ; +uint sample_duration ; +//@} + +} + +/** + * Append pulse of given duration and given pulse level to WAV output. + */ +void wav_out( const uint duration, const bool level ) +{ + // Compute how much time has passed and how much is there left + // until the next sample starts. + + uquad time_passed = ( uquad( duration ) * sample_numerator ) ; + const uint time_left = ( sample_denominator - sample_duration ) ; + + // If we have finished current sample, output it now. + + if ( time_passed >= time_left ) { + + // Adjust the values. + + time_passed -= time_left ; + if ( level ) { + sample_value += time_left ; + } + + // Output the sample. + + sample_buffer.write< u8 >( 255ull * sample_value / sample_denominator ) ; + + // Prepare for next sample. + + sample_value = 0 ; + sample_duration = 0 ; + } + + // In case the time passed covered several more samples as well, + // generate them now. + + for ( ; time_passed >= sample_denominator ; time_passed -= sample_denominator ) { + sample_buffer.write< u8 >( level ? 255 : 0 ) ; + } + + // Finally, accumulate the remainer for the next sample. + + sample_duration += uint( time_passed ) ; + if ( level ) { + sample_value += uint( time_passed ) ; + } +} + +/** + * Flush the remaining sample to the sample buffer. + */ +void wav_flush( void ) +{ + // Store the remaining sample. + + if ( sample_duration > 0 ) { + sample_buffer.write< u8 >( 255ull * sample_value / sample_denominator ) ; + + sample_value = 0 ; + sample_duration = 0 ; + } +} + +/** + * Write given memory block of given size to output file. + */ +void wav_write( const void * const data, const uint size ) +{ + hope( data || size == 0 ) ; + hope( output_file ) ; + + // Just write everything, freaking out in case of problems. + + if ( std::fwrite( data, 1, size, output_file ) != size ) { + fail( "error writing to file" ) ; + } +} + +/** + * Write content of given buffer to output file. + * + * @note The buffer content is cleared afterwards, making it ready for reuse. + */ +void wav_write( Buffer & buffer ) +{ + // Write entire buffer to the file. + + wav_write( buffer.get_data(), buffer.get_data_size() ) ; + + // Clear the buffer so it can be reused right away. + + buffer.clear() ; +} + +/** + * Use given file for subsequent WAV output. + */ +void wav_open( FILE * file, const uint numerator, const uint denominator ) +{ + hope( file ) ; + hope( numerator > 0 ) ; + hope( denominator > 0 ) ; + + // Remember the file. + + hope( output_file == NULL ) ; + output_file = file ; + + // Remember the timing factors. + + sample_numerator = numerator ; + sample_denominator = denominator ; +} + +/** + * Write everything to WAV output file and stop using that file. + */ +void wav_close( void ) +{ + hope( output_file ) ; + + // Flush everything to the sample buffer. + + wav_flush() ; + + // Make sure the buffer size is even. + + uint size = sample_buffer.get_data_size() ; + if ( ( size & 1 ) != 0 ) { + sample_buffer.write< u8 >( 0 ) ; + size++ ; + } + + // Prepare the header. + + Buffer header ; + + header.write< u32 >( WAV_HEADER ) ; + header.write_little< u32 >( 4 + ( 8 + 16 ) + ( 8 + size ) ) ; + header.write< u32 >( WAV_WAVE ) ; + + // Continue with format chunk. + + header.write< u32 >( WAV_FORMAT ) ; + header.write_little< u32 >( 16 ) ; + header.write_little< u16 >( 1 ) ; // PCM format. + header.write_little< u16 >( 1 ) ; // 1 channel. + header.write_little< u32 >( sample_numerator ) ; // sample rate. + header.write_little< u32 >( sample_numerator ) ; // byte rate. + header.write_little< u16 >( 1 ) ; // block alignment. + header.write_little< u16 >( 8 ) ; // bits per sample. + + // Append the header of the data chunk. + + header.write< u32 >( WAV_DATA ) ; + header.write_little< u32 >( size ) ; + + // Now write both the header and the data to the output file. + + wav_write( header ) ; + wav_write( sample_buffer ) ; + + // Forget about the file. + + output_file = NULL ; +} diff --git a/src/wav.h b/src/wav.h new file mode 100644 index 0000000..0f7f5ba --- /dev/null +++ b/src/wav.h @@ -0,0 +1,34 @@ +// $Id: wav.h 302 2007-06-15 07:37:58Z patrik $ + +/** + * @file WAV stuff. + * + * Copyright (C) 2007 Patrik Rak (patrik@raxoft.cz) + * + * This source code is released under the MIT license, see included license.txt. + */ + +#ifndef WAV_H +#define WAV_H 1 + +#include + +#ifndef ENDIAN_H +#include "endian.h" +#endif + +// WAV chunk tags. + +const uint WAV_HEADER = TAG_NAME('R','I','F','F') ; +const uint WAV_WAVE = TAG_NAME('W','A','V','E') ; +const uint WAV_FORMAT = TAG_NAME('f','m','t',' ') ; +const uint WAV_DATA = TAG_NAME('d','a','t','a') ; + +// Interface. + +void wav_open( FILE * file, const uint numerator, const uint denominator ) ; +void wav_close( void ) ; + +void wav_out( const uint duration, const bool level ) ; + +#endif // WAV_H