|
4 | 4 | */ |
5 | 5 | package ca.craigthomas.chip8java.emulator.components; |
6 | 6 |
|
| 7 | +import java.io.ByteArrayOutputStream; |
7 | 8 | import java.util.Random; |
8 | 9 | import java.util.Timer; |
9 | 10 | import java.util.TimerTask; |
10 | 11 | import java.util.logging.Logger; |
11 | 12 | import javax.sound.midi.*; |
| 13 | +import javax.sound.sampled.*; |
12 | 14 |
|
13 | 15 | /** |
14 | 16 | * A class to emulate a Super Chip 8 CPU. There are several good resources out on the |
@@ -44,6 +46,23 @@ public class CentralProcessingUnit extends Thread |
44 | 46 | // The start location of the stack pointer |
45 | 47 | private static final int STACK_POINTER_START = 0x52; |
46 | 48 |
|
| 49 | + // The audio playback rate |
| 50 | + private static final int AUDIO_PLAYBACK_RATE = 48000; |
| 51 | + |
| 52 | + /** |
| 53 | + * The minimum number of audio samples we want to generate. The minimum amount |
| 54 | + * of time an audio clip can be played is 1/60th of a second (the frequency |
| 55 | + * that the sound timer is decremented). Since we initialize the |
| 56 | + * audio mixer to require 48000 samples per second, this means each 1/60th |
| 57 | + * of a second requires 800 samples. The audio pattern buffer is only |
| 58 | + * 128 bits long, so we will need to repeat it to fill at least 1/60th of a |
| 59 | + * second with audio (resampled at the correct frequency). To be safe, |
| 60 | + * we'll construct a buffer of at least 4/60ths of a second of |
| 61 | + * audio. We can be bigger than the minimum number of samples below, but |
| 62 | + * we don't want less than that. |
| 63 | + */ |
| 64 | + private static final int MIN_AUDIO_SAMPLES = 3200; |
| 65 | + |
47 | 66 | // The internal 8-bit registers |
48 | 67 | protected short[] v; |
49 | 68 |
|
@@ -121,6 +140,15 @@ public class CentralProcessingUnit extends Thread |
121 | 140 | // Whether clip quirks are enabled |
122 | 141 | private boolean clipQuirks = false; |
123 | 142 |
|
| 143 | + // The 16-byte audio pattern buffer |
| 144 | + protected int [] audioPatternBuffer; |
| 145 | + |
| 146 | + // Whether an audio pattern is being played |
| 147 | + private boolean soundPlaying = false; |
| 148 | + |
| 149 | + // Stores the generated sound clip |
| 150 | + Clip generatedClip = null; |
| 151 | + |
124 | 152 | CentralProcessingUnit(Memory memory, Keyboard keyboard, Screen screen) { |
125 | 153 | this.random = new Random(); |
126 | 154 | this.memory = memory; |
@@ -395,6 +423,10 @@ protected void executeInstruction(int opcode) { |
395 | 423 | setBitplane(); |
396 | 424 | break; |
397 | 425 |
|
| 426 | + case 0x02: |
| 427 | + loadAudioPatternBuffer(); |
| 428 | + break; |
| 429 | + |
398 | 430 | case 0x07: |
399 | 431 | moveDelayTimerIntoRegister(); |
400 | 432 | break; |
@@ -448,16 +480,6 @@ protected void executeInstruction(int opcode) { |
448 | 480 | break; |
449 | 481 |
|
450 | 482 | default: |
451 | | - if ((operand & 0xF) == 0x2) { |
452 | | - storeSubsetOfRegistersInMemory(); |
453 | | - return; |
454 | | - } |
455 | | - |
456 | | - if ((operand & 0xF) == 0x3) { |
457 | | - loadSubsetOfRegistersFromMemory(); |
458 | | - return; |
459 | | - } |
460 | | - |
461 | 483 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; |
462 | 484 | break; |
463 | 485 | } |
@@ -1006,6 +1028,23 @@ protected void setBitplane() { |
1006 | 1028 | lastOpDesc = "BITPLANE " + toHex(bitplane, 1); |
1007 | 1029 | } |
1008 | 1030 |
|
| 1031 | + /** |
| 1032 | + * F002 - AUDIO |
| 1033 | + * Loads he 16-byte audio pattern buffer with 16 bytes from memory |
| 1034 | + * pointed to by the index register. |
| 1035 | + */ |
| 1036 | + protected void loadAudioPatternBuffer() { |
| 1037 | + for (int x = 0; x < 16; x++) { |
| 1038 | + audioPatternBuffer[x] = memory.read(index + x); |
| 1039 | + } |
| 1040 | + try { |
| 1041 | + calculateAudioWaveform(); |
| 1042 | + } catch (Exception e) { |
| 1043 | + throw new RuntimeException(e); |
| 1044 | + } |
| 1045 | + lastOpDesc = "AUDIO " + toHex(index, 4); |
| 1046 | + } |
| 1047 | + |
1009 | 1048 | /** |
1010 | 1049 | * Fx07 - LOAD Vx, DELAY |
1011 | 1050 | * Move the value of the delay timer into the target register. |
@@ -1211,20 +1250,29 @@ public void reset() { |
1211 | 1250 | screen.clearScreen(bitplane); |
1212 | 1251 | } |
1213 | 1252 | awaitingKeypress = false; |
| 1253 | + audioPatternBuffer = new int[16]; |
| 1254 | + soundPlaying = false; |
1214 | 1255 | } |
1215 | 1256 |
|
1216 | 1257 | /** |
1217 | 1258 | * Decrement the delay timer and the sound timer if they are not zero. |
1218 | 1259 | */ |
1219 | 1260 | private void decrementTimers() { |
1220 | 1261 | delay -= (delay != 0) ? (short) 1 : (short) 0; |
| 1262 | + sound -= (sound != 0) ? (short) 1 : (short) 0; |
1221 | 1263 |
|
1222 | | - if (sound != 0) { |
1223 | | - sound--; |
1224 | | - midiChannel.noteOn(60, 50); |
| 1264 | + if ((sound > 0) && (!soundPlaying)) { |
| 1265 | + if (generatedClip != null) { |
| 1266 | + generatedClip.loop(Clip.LOOP_CONTINUOUSLY); |
| 1267 | + soundPlaying = true; |
| 1268 | + } |
1225 | 1269 | } |
1226 | | - if (sound == 0 && midiChannel != null) { |
1227 | | - midiChannel.noteOff(60); |
| 1270 | + |
| 1271 | + if ((sound == 0) && soundPlaying) { |
| 1272 | + if (generatedClip != null) { |
| 1273 | + generatedClip.stop(); |
| 1274 | + soundPlaying = false; |
| 1275 | + } |
1228 | 1276 | } |
1229 | 1277 | } |
1230 | 1278 |
|
@@ -1268,6 +1316,76 @@ private void scrollUp(int operand) { |
1268 | 1316 | lastOpDesc = "Scroll Up " + numPixels; |
1269 | 1317 | } |
1270 | 1318 |
|
| 1319 | + /** |
| 1320 | + * Based on a playback rate specified by the XO Chip pitch, generate |
| 1321 | + * an audio waveform from the 16-byte audio_pattern_buffer. It converts |
| 1322 | + * the 16-bytes pattern into 128 separate bits. The bits are then used to fill |
| 1323 | + * a sample buffer. The sample buffer is filled by resampling the 128-bit |
| 1324 | + * pattern at the specified frequency. The sample buffer is then repeated |
| 1325 | + * until it is at least MIN_AUDIO_SAMPLES long. Playback (if currently |
| 1326 | + * happening) is stopped, the new waveform is loaded, and then playback |
| 1327 | + * is starts again (if the emulator had previously been playing a sound). |
| 1328 | + */ |
| 1329 | + private void calculateAudioWaveform() throws Exception { |
| 1330 | + // Convert the 16-byte value into an array of 128-bit samples |
| 1331 | + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| 1332 | + for (int x = 0; x < 16; x++) { |
| 1333 | + int audioByte = audioPatternBuffer[x]; |
| 1334 | + int bufferMask = 0x80; |
| 1335 | + for (int y = 0; y < 8; y++) { |
| 1336 | + outputStream.write((audioByte & bufferMask) > 0 ? 127 : 0); |
| 1337 | + bufferMask = bufferMask >> 1; |
| 1338 | + } |
| 1339 | + } |
| 1340 | + outputStream.flush(); |
| 1341 | + byte [] workingBuffer = outputStream.toByteArray(); |
| 1342 | + outputStream.close(); |
| 1343 | + |
| 1344 | + // Generate the initial re-sampled buffer |
| 1345 | + float position = 0.0f; |
| 1346 | + float step = (float) (playbackRate / AUDIO_PLAYBACK_RATE); |
| 1347 | + outputStream = new ByteArrayOutputStream(); |
| 1348 | + while (position < 128.0f) { |
| 1349 | + outputStream.write(workingBuffer[(int) position]); |
| 1350 | + position += step; |
| 1351 | + } |
| 1352 | + outputStream.flush(); |
| 1353 | + workingBuffer = outputStream.toByteArray(); |
| 1354 | + outputStream.close(); |
| 1355 | + |
| 1356 | + // Generate a final audio buffer that is at least MIN_AUDIO_SAMPLES long |
| 1357 | + int minCopies = MIN_AUDIO_SAMPLES / workingBuffer.length; |
| 1358 | + outputStream = new ByteArrayOutputStream(); |
| 1359 | + for (int currentCopy = 0; currentCopy < minCopies; currentCopy++) { |
| 1360 | + outputStream.write(workingBuffer, 0, workingBuffer.length); |
| 1361 | + } |
| 1362 | + outputStream.flush(); |
| 1363 | + workingBuffer = outputStream.toByteArray(); |
| 1364 | + outputStream.close(); |
| 1365 | + |
| 1366 | + // If there is an existing sound clip, stop it and close it |
| 1367 | + if (generatedClip != null) { |
| 1368 | + generatedClip.flush(); |
| 1369 | + generatedClip.stop(); |
| 1370 | + generatedClip.close(); |
| 1371 | + } |
| 1372 | + |
| 1373 | + // Generate a new clip from the working audio buffer |
| 1374 | + AudioFormat audioFormat = new AudioFormat(AUDIO_PLAYBACK_RATE, 8, 1, true, false); |
| 1375 | + generatedClip = AudioSystem.getClip(); |
| 1376 | + generatedClip.addLineListener(event -> { |
| 1377 | + if (LineEvent.Type.STOP.equals(event.getType())) { |
| 1378 | + event.getLine().close(); |
| 1379 | + } |
| 1380 | + }); |
| 1381 | + generatedClip.open(audioFormat, workingBuffer, 0, workingBuffer.length); |
| 1382 | + |
| 1383 | + // If the sound should be playing, restart the sound |
| 1384 | + if (soundPlaying) { |
| 1385 | + generatedClip.loop(Clip.LOOP_CONTINUOUSLY); |
| 1386 | + } |
| 1387 | + } |
| 1388 | + |
1271 | 1389 | /** |
1272 | 1390 | * Return the string of the last operation that occurred. |
1273 | 1391 | * |
|
0 commit comments