|
| 1 | +/* |
| 2 | +https://github.com/eightbitjim/commodore-tape-maker/blob/master/maketape.py |
| 3 | +
|
| 4 | +# MIT License |
| 5 | +# |
| 6 | +# Copyright (c) 2018 eightbitjim |
| 7 | +# |
| 8 | +# Permission is hereby granted, free of charge, to any person obtaining a copy |
| 9 | +# of this software and associated documentation files (the "Software"), to deal |
| 10 | +# in the Software without restriction, including without limitation the rights |
| 11 | +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 12 | +# copies of the Software, and to permit persons to whom the Software is |
| 13 | +# furnished to do so, subject to the following conditions: |
| 14 | +# |
| 15 | +# The above copyright notice and this permission notice shall be included in all |
| 16 | +# copies or substantial portions of the Software. |
| 17 | +# |
| 18 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 19 | +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 20 | +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 21 | +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 22 | +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 23 | +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 24 | +# SOFTWARE. |
| 25 | +*/ |
| 26 | + |
| 27 | +export class OutputSoundFile { |
| 28 | + options: any; |
| 29 | + sampleRate: number; |
| 30 | + soundData: number[]; |
| 31 | + tapData: number[]; |
| 32 | + |
| 33 | + constructor(options: any) { |
| 34 | + this.options = options; |
| 35 | + this.sampleRate = 44100.0; |
| 36 | + this.soundData = []; |
| 37 | + //00000000 43 36 34 2d 54 41 50 45 2d 52 41 57 01 00 00 00 |C64-TAPE-RAW....| |
| 38 | + //00000010 1e 62 03 00 |
| 39 | + this.tapData = [0x43,0x36,0x34,0x2d,0x54,0x41,0x50,0x45,0x2d,0x52,0x41,0x57,0x01,0x00,0x00,0x00,0,0,0,0]; |
| 40 | + } |
| 41 | + |
| 42 | + getWAVHeader() { |
| 43 | + const header = new Uint8Array(44); |
| 44 | + const view = new DataView(header.buffer); |
| 45 | + view.setUint32(0, 0x52494646, false); // "RIFF" |
| 46 | + view.setUint32(4, 44 + this.soundData.length, true); // ChunkSize |
| 47 | + view.setUint32(8, 0x57415645, false); // "WAVE" |
| 48 | + view.setUint32(12, 0x666d7420, false); // "fmt " |
| 49 | + view.setUint32(16, 16, true); // Subchunk1Size |
| 50 | + view.setUint16(20, 1, true); // AudioFormat (PCM) |
| 51 | + view.setUint16(22, 1, true); // NumChannels |
| 52 | + view.setUint32(24, this.sampleRate, true); // SampleRate |
| 53 | + view.setUint32(28, this.sampleRate * 2, true); // ByteRate |
| 54 | + view.setUint16(32, 1, true); // BlockAlign |
| 55 | + view.setUint16(34, 8, true); // BitsPerSample |
| 56 | + view.setUint32(36, 0x64617461, false); // "data" |
| 57 | + view.setUint32(40, this.soundData.length, true); // Subchunk2Size |
| 58 | + return header; |
| 59 | + } |
| 60 | + |
| 61 | + addSilence(lengthInSeconds: number): void { |
| 62 | + const numberOfSamples = Math.floor(this.sampleRate * lengthInSeconds); |
| 63 | + for (let i = 0; i < numberOfSamples; i++) { |
| 64 | + this.soundData.push(0); |
| 65 | + } |
| 66 | + //For v1 and v2 TAP files, a $00 value is followed by 3 bytes containing the actual duration measured in clock cycles (not divided by 8). These 3 bytes are in low-high format. |
| 67 | + const numCycles = TAPFile.CLOCK_RATE * lengthInSeconds; |
| 68 | + this.tapData.push(0); |
| 69 | + this.tapData.push(numCycles & 0xff); |
| 70 | + this.tapData.push((numCycles >> 8) & 0xff); |
| 71 | + this.tapData.push((numCycles >> 16) & 0xff); |
| 72 | + } |
| 73 | + |
| 74 | + addCycle(cycles: number): void { |
| 75 | + this.tapData.push(cycles); |
| 76 | + const numberOfSamples = Math.floor(this.sampleRate * TAPFile.TAP_LENGTH_IN_SECONDS * cycles); |
| 77 | + for (let i = 0; i < numberOfSamples; i++) { |
| 78 | + let value; |
| 79 | + if (this.options.sine_wave) { |
| 80 | + value = - Math.sin((i / numberOfSamples) * 2.0 * Math.PI); |
| 81 | + } else { |
| 82 | + if (i < numberOfSamples / 2) { |
| 83 | + value = -1; |
| 84 | + } else { |
| 85 | + value = 1; |
| 86 | + } |
| 87 | + } |
| 88 | + if (this.options.invert_waveform) { |
| 89 | + value = -value; |
| 90 | + } |
| 91 | + this.soundData.push(Math.round(128 + value * 127)); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + updateTAPHeader() { |
| 96 | + let datalen = this.tapData.length - 0x14; |
| 97 | + // set bytes 0x10-0x13 to length |
| 98 | + this.tapData[0x10] = datalen & 0xff; |
| 99 | + this.tapData[0x11] = (datalen >> 8) & 0xff; |
| 100 | + this.tapData[0x12] = (datalen >> 16) & 0xff; |
| 101 | + this.tapData[0x13] = (datalen >> 24) & 0xff; |
| 102 | + } |
| 103 | + |
| 104 | + getTAPData(): Uint8Array { |
| 105 | + this.updateTAPHeader(); |
| 106 | + return new Uint8Array(this.tapData); |
| 107 | + } |
| 108 | + |
| 109 | + getSoundData(): Uint8Array { |
| 110 | + let header = this.getWAVHeader(); |
| 111 | + let data = new Uint8Array(header.length + this.soundData.length); |
| 112 | + data.set(header, 0); |
| 113 | + data.set(new Uint8Array(this.soundData), header.length); |
| 114 | + return data; |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +export class TAPFile { |
| 119 | + |
| 120 | + static CLOCK_RATE = 985248.0; |
| 121 | + static TAP_LENGTH_IN_SECONDS = 8.0 / this.CLOCK_RATE; |
| 122 | + static FILENAME_BUFFER_SIZE = 0x10; |
| 123 | + static FILE_TYPE_NONE = 0; |
| 124 | + static FILE_TYPE_RELOCATABLE = 1; |
| 125 | + static FILE_TYPE_SEQUENTIAL = 2; |
| 126 | + static FILE_TYPE_NON_RELOCATABLE = 3; |
| 127 | + static LEADER_TYPE_HEADER = 0; |
| 128 | + static LEADER_TYPE_CONTENT = 1; |
| 129 | + static LEADER_TYPE_REPEATED = 2; |
| 130 | + static NUMBER_OF_PADDING_BYTES = 171; |
| 131 | + static PADDING_CHARACTER = 0x20; |
| 132 | + static SHORT_PULSE = 0x30; |
| 133 | + static MEDIUM_PULSE = 0x42; |
| 134 | + static LONG_PULSE = 0x56; |
| 135 | + |
| 136 | + options: any; |
| 137 | + checksum: number; |
| 138 | + data: Uint8Array; |
| 139 | + filenameData: number[]; |
| 140 | + startAddress: number; |
| 141 | + endAddress: number; |
| 142 | + fileType: number; |
| 143 | + waveFile: OutputSoundFile; |
| 144 | + |
| 145 | + constructor(filename: string, options?: any) { |
| 146 | + this.options = options; |
| 147 | + this.checksum = 0; |
| 148 | + this.data = new Uint8Array(0); |
| 149 | + this.filenameData = this.makeFilename(filename); |
| 150 | + this.startAddress = 0; |
| 151 | + this.endAddress = 0; |
| 152 | + this.fileType = TAPFile.FILE_TYPE_NONE; |
| 153 | + this.waveFile = null; |
| 154 | + } |
| 155 | + |
| 156 | + makeFilename(filename: string): number[] { |
| 157 | + const filenameBuffer = []; |
| 158 | + const space = 0x20; |
| 159 | + filename = filename.toUpperCase(); // for PETSCII |
| 160 | + for (let i = 0; i < TAPFile.FILENAME_BUFFER_SIZE; i++) { |
| 161 | + if (filename.length <= i) { |
| 162 | + filenameBuffer.push(space); |
| 163 | + } else { |
| 164 | + let ch = filename.charCodeAt(i); |
| 165 | + filenameBuffer.push(ch); |
| 166 | + } |
| 167 | + } |
| 168 | + return filenameBuffer; |
| 169 | + } |
| 170 | + |
| 171 | + setContent(inputFile: { data: Uint8Array, startAddress: number, type: number }): void { |
| 172 | + this.data = inputFile.data; |
| 173 | + this.startAddress = inputFile.startAddress; |
| 174 | + this.endAddress = inputFile.startAddress + inputFile.data.length; |
| 175 | + this.fileType = inputFile.type; |
| 176 | + } |
| 177 | + |
| 178 | + generateSound(outputWaveFile: OutputSoundFile): void { |
| 179 | + this.waveFile = outputWaveFile; |
| 180 | + this.addHeader(false); |
| 181 | + this.addHeader(true); |
| 182 | + outputWaveFile.addSilence(0.1); |
| 183 | + this.addFile(); |
| 184 | + } |
| 185 | + |
| 186 | + addTapCycle(tapValue: number): void { |
| 187 | + this.waveFile.addCycle(tapValue); |
| 188 | + } |
| 189 | + |
| 190 | + addBit(value: number): void { |
| 191 | + if (value === 0) { |
| 192 | + this.addTapCycle(TAPFile.SHORT_PULSE); |
| 193 | + this.addTapCycle(TAPFile.MEDIUM_PULSE); |
| 194 | + } else { |
| 195 | + this.addTapCycle(TAPFile.MEDIUM_PULSE); |
| 196 | + this.addTapCycle(TAPFile.SHORT_PULSE); |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + addDataMarker(moreToFollow: boolean): void { |
| 201 | + if (moreToFollow) { |
| 202 | + this.addTapCycle(TAPFile.LONG_PULSE); |
| 203 | + this.addTapCycle(TAPFile.MEDIUM_PULSE); |
| 204 | + } else { |
| 205 | + this.addTapCycle(TAPFile.LONG_PULSE); |
| 206 | + this.addTapCycle(TAPFile.SHORT_PULSE); |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + resetChecksum(): void { |
| 211 | + this.checksum = 0; |
| 212 | + } |
| 213 | + |
| 214 | + addByteFrame(value: number, moreToFollow: boolean): void { |
| 215 | + let checkBit = 1; |
| 216 | + for (let i = 0; i < 8; i++) { |
| 217 | + const bit = (value & (1 << i)) !== 0 ? 1 : 0; |
| 218 | + this.addBit(bit); |
| 219 | + checkBit ^= bit; |
| 220 | + } |
| 221 | + this.addBit(checkBit); |
| 222 | + this.addDataMarker(moreToFollow); |
| 223 | + this.checksum ^= value; |
| 224 | + } |
| 225 | + |
| 226 | + addLeader(fileType: number): void { |
| 227 | + let numberofPulses; |
| 228 | + if (fileType === TAPFile.LEADER_TYPE_HEADER) { |
| 229 | + numberofPulses = 0x6a00; |
| 230 | + } else if (fileType === TAPFile.LEADER_TYPE_CONTENT) { |
| 231 | + numberofPulses = 0x1a00; |
| 232 | + } else { |
| 233 | + numberofPulses = 0x4f; |
| 234 | + } |
| 235 | + for (let i = 0; i < numberofPulses; i++) { |
| 236 | + this.addTapCycle(TAPFile.SHORT_PULSE); |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + addSyncChain(repeated: boolean): void { |
| 241 | + let value; |
| 242 | + if (repeated) { |
| 243 | + value = 0x09; |
| 244 | + } else { |
| 245 | + value = 0x89; |
| 246 | + } |
| 247 | + let count = 9; |
| 248 | + while (count > 0) { |
| 249 | + this.addByteFrame(value, true); |
| 250 | + value -= 1; |
| 251 | + count -= 1; |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + addData(): void { |
| 256 | + for (let i = 0; i < this.data.length; i++) { |
| 257 | + this.addByteFrame(this.data[i], true); |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + addFilename(): void { |
| 262 | + for (let i = 0; i < this.filenameData.length; i++) { |
| 263 | + this.addByteFrame(this.filenameData[i], true); |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + addHeader(repeated: boolean): void { |
| 268 | + if (repeated) { |
| 269 | + this.addLeader(TAPFile.LEADER_TYPE_REPEATED); |
| 270 | + } else { |
| 271 | + this.addLeader(TAPFile.LEADER_TYPE_HEADER); |
| 272 | + } |
| 273 | + this.addDataMarker(true); |
| 274 | + this.addSyncChain(repeated); |
| 275 | + this.resetChecksum(); |
| 276 | + this.addByteFrame(this.fileType, true); |
| 277 | + this.addByteFrame(this.startAddress & 0x00ff, true); |
| 278 | + this.addByteFrame((this.startAddress & 0xff00) >> 8, true); |
| 279 | + this.addByteFrame(this.endAddress & 0x00ff, true); |
| 280 | + this.addByteFrame((this.endAddress & 0xff00) >> 8, true); |
| 281 | + this.addFilename(); |
| 282 | + for (let i = 0; i < TAPFile.NUMBER_OF_PADDING_BYTES; i++) { |
| 283 | + this.addByteFrame(TAPFile.PADDING_CHARACTER, true); |
| 284 | + } |
| 285 | + this.addByteFrame(this.checksum, false); |
| 286 | + } |
| 287 | + |
| 288 | + addFile(): void { |
| 289 | + let repeated = false; |
| 290 | + for (let i = 0; i < 2; i++) { |
| 291 | + if (!repeated) { |
| 292 | + this.addLeader(TAPFile.LEADER_TYPE_CONTENT); |
| 293 | + } else { |
| 294 | + this.addLeader(TAPFile.LEADER_TYPE_REPEATED); |
| 295 | + } |
| 296 | + this.addDataMarker(true); |
| 297 | + this.addSyncChain(repeated); |
| 298 | + this.resetChecksum(); |
| 299 | + this.addData(); |
| 300 | + this.addByteFrame(this.checksum, false); |
| 301 | + repeated = true; |
| 302 | + } |
| 303 | + this.addLeader(1); |
| 304 | + } |
| 305 | +} |
0 commit comments