Skip to content

Commit 6dd55c5

Browse files
committed
Add support for asymmetric pulse handling like the Software Projects loader.
1 parent 9ec39ac commit 6dd55c5

11 files changed

Lines changed: 317 additions & 120 deletions

File tree

src/main/java/xyz/meunier/wav2pzx/blocks/PZXDataBlock.java

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626
package xyz.meunier.wav2pzx.blocks;
2727

28+
import com.google.common.collect.ImmutableList;
2829
import com.google.common.primitives.Bytes;
2930
import xyz.meunier.wav2pzx.pulselist.PulseList;
3031

@@ -36,6 +37,9 @@
3637

3738
import static com.google.common.base.Preconditions.checkArgument;
3839
import static com.google.common.base.Preconditions.checkNotNull;
40+
import static com.google.common.collect.ImmutableList.copyOf;
41+
import static com.google.common.primitives.Longs.asList;
42+
import static java.lang.Long.valueOf;
3943
import static xyz.meunier.wav2pzx.blocks.PZXEncodeUtils.*;
4044
import static xyz.meunier.wav2pzx.romdecoder.LoaderContext.*;
4145

@@ -76,30 +80,30 @@ public final class PZXDataBlock implements PZXBlock {
7680
private final boolean isHeader;
7781

7882
// The length of the zero pulse identified in the source file
79-
private final short zeroPulseLength;
83+
private final ImmutableList<Long> zeroPulseLengths;
8084

8185
// The length of the one pulse identified in the source file
82-
private final short onePulseLength;
86+
private final ImmutableList<Long> onePulseLengths;
8387

8488
/**
8589
* Constructs a new PZXDataBlock.
8690
* @param newPulses the original tape pulses that have been decoded into this block
87-
* @param zeroPulseLength the length of the zero pulses in the block
88-
* @param onePulseLength the length of the one pulses in the block
91+
* @param zeroPulseLengths the lengths of the zero pulses in the block
92+
* @param onePulseLengths the lengths of the one pulses in the block
8993
* @param tailLength the length of the tail pulse in the block
9094
* @param numBitsInLastByte the number of bits used in the last byte of the data collection
9195
* @param data the decoded data from the tape image
9296
* @throws NullPointerException if newPulses or data is null
9397
* @throws IllegalArgumentException if data is empty
9498
*/
95-
public PZXDataBlock(PulseList newPulses, long zeroPulseLength, long onePulseLength, long tailLength,
99+
public PZXDataBlock(PulseList newPulses, List<Long> zeroPulseLengths, List<Long> onePulseLengths, long tailLength,
96100
int numBitsInLastByte, Collection<Byte> data) {
97101
checkNotNull(newPulses, "newPulses must not be null");
98102
checkNotNull(data, "data must not be null");
99103
checkArgument(!data.isEmpty(), "data array must not be empty");
100104
this.pulses = newPulses;
101-
this.zeroPulseLength = (short)zeroPulseLength;
102-
this.onePulseLength = (short)onePulseLength;
105+
this.zeroPulseLengths = copyOf(zeroPulseLengths);
106+
this.onePulseLengths = copyOf(onePulseLengths);
103107
this.tailLength = (short)tailLength;
104108
this.numBitsInLastByte = numBitsInLastByte;
105109
this.data = Bytes.toArray(data);
@@ -118,7 +122,8 @@ public PZXDataBlock(PulseList newPulses, long zeroPulseLength, long onePulseLeng
118122
* @throws IllegalArgumentException if data is empty
119123
*/
120124
public PZXDataBlock(PulseList newPulses, int numBitsInLastByte, Collection<Byte> data) {
121-
this(newPulses, ZERO, ONE, TAIL, numBitsInLastByte, data);
125+
this(newPulses, asList(valueOf(ZERO), valueOf(ZERO)), asList(valueOf(ONE), valueOf(ONE)), TAIL,
126+
numBitsInLastByte, data);
122127
}
123128

124129
// Calculates the checksum for the data according to the algorithm in the
@@ -179,23 +184,27 @@ public byte[] getPZXBlockDiskRepresentation() {
179184
// use standard duration tail pulse after last bit of the block if we found one
180185
putUnsignedLittleEndianShort(tailLength == 0 ? 0 : tailLength, output);
181186

182-
putUnsignedByte((byte)2, output); // number of pulses encoding bit equal to 0.
183-
putUnsignedByte((byte)2, output); // number of pulses encoding bit equal to 1.
187+
putUnsignedByte((byte)zeroPulseLengths.size(), output); // number of pulses encoding bit equal to 0.
188+
putUnsignedByte((byte)onePulseLengths.size(), output); // number of pulses encoding bit equal to 1.
184189

185190
// sequence of pulse durations encoding bit equal to 0.
186-
putUnsignedLittleEndianShort(zeroPulseLength, output);
187-
putUnsignedLittleEndianShort(zeroPulseLength, output);
188-
191+
putPulseList(zeroPulseLengths, output);
192+
189193
// sequence of pulse durations encoding bit equal to 1.
190-
putUnsignedLittleEndianShort(onePulseLength, output);
191-
putUnsignedLittleEndianShort(onePulseLength, output);
192-
194+
putPulseList(onePulseLengths, output);
195+
193196
// data stream
194197
output.addAll(Bytes.asList(data));
195198

196199
return addPZXBlockHeader("DATA", output);
197200
}
198-
201+
202+
private void putPulseList(List<Long> pulseLengths, Collection<Byte> output) {
203+
for (Long pulseLength : pulseLengths) {
204+
putUnsignedLittleEndianShort(pulseLength.shortValue(), output);
205+
}
206+
}
207+
199208
@Override
200209
public String getSummary() {
201210
StringBuilder retval = new StringBuilder();
@@ -276,8 +285,8 @@ public boolean checkChecksum() {
276285

277286
@Override
278287
public String toString() {
279-
return "PZXDataBlock{" + pulses.toString() + ", zeroPulseLength=" + zeroPulseLength + ", onePulseLength="
280-
+ onePulseLength + ", tailLength=" + tailLength + ", numBitsInLastByte=" + numBitsInLastByte
288+
return "PZXDataBlock{" + pulses.toString() + ", zeroPulseLengths=" + zeroPulseLengths + ", onePulseLengths="
289+
+ onePulseLengths + ", tailLength=" + tailLength + ", numBitsInLastByte=" + numBitsInLastByte
281290
+ ", calculatedChecksum=" + String.format("0x%x", calculatedChecksum) + ", suppliedChecksum="
282291
+ String.format("0x%x", suppliedChecksum) + ", isHeader=" + isHeader + ", data.length="
283292
+ data.length + '}';
@@ -289,9 +298,11 @@ public int hashCode() {
289298
result = 31 * result + (int) tailLength;
290299
result = 31 * result + Arrays.hashCode(data);
291300
result = 31 * result + numBitsInLastByte;
301+
result = 31 * result + (int) calculatedChecksum;
292302
result = 31 * result + (int) suppliedChecksum;
293-
result = 31 * result + (int) zeroPulseLength;
294-
result = 31 * result + (int) onePulseLength;
303+
result = 31 * result + (isHeader ? 1 : 0);
304+
result = 31 * result + zeroPulseLengths.hashCode();
305+
result = 31 * result + onePulseLengths.hashCode();
295306
return result;
296307
}
297308

@@ -304,12 +315,13 @@ public boolean equals(Object o) {
304315

305316
if (tailLength != that.tailLength) return false;
306317
if (numBitsInLastByte != that.numBitsInLastByte) return false;
318+
if (calculatedChecksum != that.calculatedChecksum) return false;
307319
if (suppliedChecksum != that.suppliedChecksum) return false;
308-
if (zeroPulseLength != that.zeroPulseLength) return false;
309-
if (onePulseLength != that.onePulseLength) return false;
320+
if (isHeader != that.isHeader) return false;
310321
if (!pulses.equals(that.pulses)) return false;
311-
return Arrays.equals(data, that.data);
312-
322+
if (!Arrays.equals(data, that.data)) return false;
323+
if (!zeroPulseLengths.equals(that.zeroPulseLengths)) return false;
324+
return onePulseLengths.equals(that.onePulseLengths);
313325
}
314326

315327
/**

src/main/java/xyz/meunier/wav2pzx/generaldecoder/DualPulseDataBlockProcessor.java

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import static xyz.meunier.wav2pzx.generaldecoder.RangeFinder.*;
4444

4545
final class DualPulseDataBlockProcessor {
46-
private static final int SYNC_OR_TAIL_BUFFER_SIZE = 4;
46+
private static final int SYNC_OR_TAIL_BUFFER_SIZE = 10;
4747
private static final int SYNC_AND_TAIL_TOTAL_LIMIT = SYNC_OR_TAIL_BUFFER_SIZE * 2;
4848
private static final int MINIMUM_DATA_BLOCK_PULSE_COUNT = SYNC_AND_TAIL_TOTAL_LIMIT + 1;
4949
private static final int THREE_BYTES_OF_PULSES = 24;
@@ -76,27 +76,27 @@ List<TapeBlock> processDataBlock() {
7676
int limit = pulseLengths.size() > MINIMUM_DATA_BLOCK_PULSE_COUNT + THREE_BYTES_OF_PULSES ?
7777
getPulseLengthsSizeWithoutSyncAndTailArea() : 0;
7878

79-
ArrayList<Long> fullBits = new ArrayList<>(limit/2);
80-
for(int i = SYNC_OR_TAIL_BUFFER_SIZE; i < limit; i+=2) {
81-
Long bit = pulseLengths.get(i) + pulseLengths.get(i+1);
79+
ArrayList<Long> fullBits = new ArrayList<>(limit / 2);
80+
for (int i = SYNC_OR_TAIL_BUFFER_SIZE; i < limit; i += 2) {
81+
Long bit = pulseLengths.get(i) + pulseLengths.get(i + 1);
8282
fullBits.add(bit);
8383
}
8484

8585
List<Range<Long>> ranges = getRanges(fullBits.stream().distinct().collect(toList()));
8686

8787
// If we don't have enough bits to make a good population of dual pulses just dump out the source pulses
88-
if(ranges.isEmpty()) {
88+
if (ranges.isEmpty()) {
8989
ranges = RangeFinder.getRangesForSinglePulses(pulseLengths);
9090
TapeBlock newBlock = new TapeBlock(UNKNOWN, getSingletonPulseLengthsOfRanges(ranges), pulseList);
9191
getLogger(LoaderContextImpl.class.getName()).log(Level.INFO, newBlock.toString());
9292
return singletonList(newBlock);
9393
}
9494

9595
// Find average of pulse pairs matching each range in this block
96-
pulseSubstitutions = getReplacementBitDataOfRanges(ranges, fullBits);
96+
pulseSubstitutions = getZeroAndOnePulsePairs(ranges, fullBits);
9797

9898
// If we have more than two ranges we can't encode this block as a data block - just dump out the source pulses
99-
if(pulseSubstitutions.size() != 2) {
99+
if (pulseSubstitutions.size() != 2) {
100100
TapeBlock newBlock = new TapeBlock(UNKNOWN, pulseSubstitutions, pulseList);
101101
getLogger(LoaderContextImpl.class.getName()).log(Level.INFO, newBlock.toString());
102102
return singletonList(newBlock);
@@ -112,14 +112,14 @@ List<TapeBlock> processDataBlock() {
112112
List<Long> bitPulse = new ArrayList<>(2);
113113
bitPulse.addAll(pulseLengths.subList(i, i + 2));
114114

115-
if(replaceInRangeBitsWithProcessedValues(bitPulse)) {
115+
if (replaceInRangeBitsWithProcessedValues(bitPulse)) {
116116
newDataBlockPulses.addAll(bitPulse);
117117
} else {
118118
// Finish any open PulseList
119-
finishDataBlock();
119+
finishBlock(i);
120120

121121
// Add pulses to new PulseList
122-
if(isInSyncCandidateArea(i)) {
122+
if (isInSyncCandidateArea(i)) {
123123
// SYNC bit candidates at the beginning of the block
124124
addTapeBlockForSinglePulses(SYNC_CANDIDATE, bitPulse, firstPulseLevel);
125125
} else {
@@ -138,20 +138,34 @@ List<TapeBlock> processDataBlock() {
138138
return newTapeBlockList;
139139
}
140140

141+
private void finishBlock(int index) {
142+
// Data blocks should have at least 2 bytes of data
143+
if (!newDataBlockPulses.isEmpty() && newDataBlockPulses.size() < (2*8*2)) {
144+
if (isInSyncCandidateArea(index)) {
145+
addTapeBlockForSinglePulses(SYNC_CANDIDATE, newDataBlockPulses, firstPulseLevel);
146+
} else {
147+
addTapeBlock(UNKNOWN, pulseSubstitutions, newPulseList(newDataBlockPulses, firstPulseLevel));
148+
}
149+
newDataBlockPulses.clear();
150+
return;
151+
}
152+
finishDataBlock();
153+
}
154+
141155
private int getPulseLengthsSizeWithoutSyncAndTailArea() {
142156
return (pulseLengths.size() - SYNC_AND_TAIL_TOTAL_LIMIT) - hasCandidateTailPulse;
143157
}
144158

145159
private void finishDataBlock() {
146160
// Finish any open PulseList
147-
if(!newDataBlockPulses.isEmpty()) {
161+
if (!newDataBlockPulses.isEmpty()) {
148162
addTapeBlock(DATA, pulseSubstitutions, newPulseList(newDataBlockPulses, firstPulseLevel));
149163
newDataBlockPulses.clear();
150164
}
151165
}
152166

153167
private void processCandidateTailPulse() {
154-
if(hasCandidateTailPulse == 1) {
168+
if (hasCandidateTailPulse == 1) {
155169
int size = pulseLengths.size();
156170
addTapeBlockForSinglePulses(TAIL_CANDIDATE, pulseLengths.subList(size - 1, size), flipPulseLevel(firstPulseLevel));
157171
}
@@ -163,7 +177,7 @@ private boolean replaceInRangeBitsWithProcessedValues(List<Long> bitPulse) {
163177

164178
Long bit = bitPulse.get(0) + bitPulse.get(1);
165179
for (BitData bitData : pulseSubstitutions) {
166-
if(bitData.getQualificationRange().contains(bit)) {
180+
if (bitData.getQualificationRange().contains(bit)) {
167181
// Transform bit pulses
168182
bitPulse.clear();
169183
bitPulse.addAll(bitData.getPulses());
@@ -194,7 +208,7 @@ private static int flipPulseLevel(int pulseLevel) {
194208
}
195209

196210
private static List<BitData> getSyncRange(List<Range<Long>> ranges) {
197-
return ranges.stream().distinct().map(r -> new BitData(r, r.lowerEndpoint())).collect(toList());
211+
return ranges.stream().distinct().map(r -> new BitData(r, singletonList(r.lowerEndpoint()))).collect(toList());
198212
}
199213

200214
}

src/main/java/xyz/meunier/wav2pzx/generaldecoder/LoaderContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ public interface LoaderContext {
4242
/**
4343
* The minimum length of a pilot pulse
4444
*/
45-
long MIN_PILOT_LENGTH = 1600;
45+
long MIN_PILOT_LENGTH = 1100;
4646

4747
/**
4848
* The maximum length of a pilot pulse
4949
*/
50-
long MAX_PILOT_LENGTH = 2560;
50+
long MAX_PILOT_LENGTH = 3560;
5151

5252
Range<Long> PILOT_CANDIDATE_RANGE = Range.closed(MIN_PILOT_LENGTH, MAX_PILOT_LENGTH);
5353

src/main/java/xyz/meunier/wav2pzx/generaldecoder/PZXBuilder.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,23 @@ private static PZXBlock getPzxBlock(PeekingIterator<TapeBlock> iterator) {
106106
private static PZXDataBlock getPzxDataBlock(PeekingIterator<TapeBlock> iterator, TapeBlock block) {
107107
long tailLength = getTailLength(iterator);
108108

109-
long zeroPulseLength = block.getZeroBit().getFullPulse() / 2;
110-
long onePulseLength = block.getOneBit().getFullPulse() / 2;
109+
List<Long> zeroPulseLengths = block.getZeroBit().getPulses();
110+
List<Long> onePulseLengths = block.getOneBit().getPulses();
111111
DataBuilder dataBuilder = new DataBuilder();
112112

113113
ImmutableList<Long> pulseLengths = block.getPulseList().getPulseLengths();
114114
for (int i = 0; i < pulseLengths.size(); i += 2) {
115-
Long pulse1 = pulseLengths.get(i);
116-
Long pulse2 = pulseLengths.get(i + 1);
117-
if (isSpecifiedPulsePair(zeroPulseLength, pulse1, pulse2)) {
115+
ImmutableList<Long> pulses = pulseLengths.subList(i, i + 2);
116+
if (isSpecifiedPulseSequence(zeroPulseLengths, pulses)) {
118117
dataBuilder.addBit(0);
119-
} else if (isSpecifiedPulsePair(onePulseLength, pulse1, pulse2)) {
118+
} else if (isSpecifiedPulseSequence(onePulseLengths, pulses)) {
120119
dataBuilder.addBit(1);
121120
}
122121
// FIXME: Some kind of error, fall back to PulseBlock?
123122
}
124123

125124
int numBitsInLastByte = dataBuilder.getNumBitsInCurrentByte();
126-
return new PZXDataBlock(block.getPulseList(), zeroPulseLength, onePulseLength, tailLength,
125+
return new PZXDataBlock(block.getPulseList(), zeroPulseLengths, onePulseLengths, tailLength,
127126
numBitsInLastByte, dataBuilder.getData());
128127
}
129128

@@ -135,12 +134,12 @@ private static long getTailLength(PeekingIterator<TapeBlock> iterator) {
135134
return tailLength;
136135
}
137136

138-
private static boolean isSpecifiedPulsePair(long pulseLength, Long pulse1, Long pulse2) {
139-
return pulse1 == pulseLength && pulse2 == pulseLength;
137+
private static boolean isSpecifiedPulseSequence(List<Long> pulseLength, List<Long> pulses) {
138+
return pulseLength.equals(pulses);
140139
}
141140

142141
private static PZXPulseBlock getPzxPulseBlock(PeekingIterator<TapeBlock> iterator, PulseList blockPulseList) {
143-
if (iterator.hasNext() && iterator.peek().getBlockType() == SYNC_CANDIDATE) {
142+
while (iterator.hasNext() && iterator.peek().getBlockType() == SYNC_CANDIDATE) {
144143
// Make a new PulseList with this block and the next one
145144
blockPulseList = new PulseList(blockPulseList, iterator.next().getPulseList());
146145
}

0 commit comments

Comments
 (0)