Skip to content

Commit cebe8a9

Browse files
authored
Implement XO-Chip Audio (#56)
* Implement audio pattern buffer and resampling functions * Implement audio waveform generation and playback
1 parent aa51567 commit cebe8a9

File tree

3 files changed

+142
-24
lines changed

3 files changed

+142
-24
lines changed

src/main/java/ca/craigthomas/chip8java/emulator/components/CentralProcessingUnit.java

Lines changed: 133 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
*/
55
package ca.craigthomas.chip8java.emulator.components;
66

7+
import java.io.ByteArrayOutputStream;
78
import java.util.Random;
89
import java.util.Timer;
910
import java.util.TimerTask;
1011
import java.util.logging.Logger;
1112
import javax.sound.midi.*;
13+
import javax.sound.sampled.*;
1214

1315
/**
1416
* 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
4446
// The start location of the stack pointer
4547
private static final int STACK_POINTER_START = 0x52;
4648

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+
4766
// The internal 8-bit registers
4867
protected short[] v;
4968

@@ -121,6 +140,15 @@ public class CentralProcessingUnit extends Thread
121140
// Whether clip quirks are enabled
122141
private boolean clipQuirks = false;
123142

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+
124152
CentralProcessingUnit(Memory memory, Keyboard keyboard, Screen screen) {
125153
this.random = new Random();
126154
this.memory = memory;
@@ -395,6 +423,10 @@ protected void executeInstruction(int opcode) {
395423
setBitplane();
396424
break;
397425

426+
case 0x02:
427+
loadAudioPatternBuffer();
428+
break;
429+
398430
case 0x07:
399431
moveDelayTimerIntoRegister();
400432
break;
@@ -448,16 +480,6 @@ protected void executeInstruction(int opcode) {
448480
break;
449481

450482
default:
451-
if ((operand & 0xF) == 0x2) {
452-
storeSubsetOfRegistersInMemory();
453-
return;
454-
}
455-
456-
if ((operand & 0xF) == 0x3) {
457-
loadSubsetOfRegistersFromMemory();
458-
return;
459-
}
460-
461483
lastOpDesc = "Operation " + toHex(operand, 4) + " not supported";
462484
break;
463485
}
@@ -1006,6 +1028,23 @@ protected void setBitplane() {
10061028
lastOpDesc = "BITPLANE " + toHex(bitplane, 1);
10071029
}
10081030

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+
10091048
/**
10101049
* Fx07 - LOAD Vx, DELAY
10111050
* Move the value of the delay timer into the target register.
@@ -1211,20 +1250,29 @@ public void reset() {
12111250
screen.clearScreen(bitplane);
12121251
}
12131252
awaitingKeypress = false;
1253+
audioPatternBuffer = new int[16];
1254+
soundPlaying = false;
12141255
}
12151256

12161257
/**
12171258
* Decrement the delay timer and the sound timer if they are not zero.
12181259
*/
12191260
private void decrementTimers() {
12201261
delay -= (delay != 0) ? (short) 1 : (short) 0;
1262+
sound -= (sound != 0) ? (short) 1 : (short) 0;
12211263

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+
}
12251269
}
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+
}
12281276
}
12291277
}
12301278

@@ -1268,6 +1316,76 @@ private void scrollUp(int operand) {
12681316
lastOpDesc = "Scroll Up " + numPixels;
12691317
}
12701318

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+
12711389
/**
12721390
* Return the string of the last operation that occurred.
12731391
*

src/main/java/ca/craigthomas/chip8java/emulator/components/Keyboard.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2013-2018 Craig Thomas
2+
* Copyright (C) 2013-2024 Craig Thomas
33
* This project uses an MIT style license - see LICENSE for details.
44
*/
55
package ca.craigthomas.chip8java.emulator.components;

src/test/java/ca/craigthomas/chip8java/emulator/components/CentralProcessingUnitTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,7 @@ public void testStoreSubsetOneTwo() {
11741174
cpu.v[1] = 5;
11751175
cpu.v[2] = 6;
11761176
cpu.index = 0x5000;
1177-
cpu.operand = 0xF122;
1177+
cpu.operand = 0x5122;
11781178
cpu.storeSubsetOfRegistersInMemory();
11791179
assertEquals(5, memory.read(0x5000));
11801180
assertEquals(6, memory.read(0x5001));
@@ -1185,7 +1185,7 @@ public void testStoreSubsetOneOne() {
11851185
cpu.v[1] = 5;
11861186
cpu.v[2] = 6;
11871187
cpu.index = 0x5000;
1188-
cpu.operand = 0xF112;
1188+
cpu.operand = 0x5112;
11891189
cpu.storeSubsetOfRegistersInMemory();
11901190
assertEquals(5, memory.read(0x5000));
11911191
assertEquals(0, memory.read(0x5001));
@@ -1197,7 +1197,7 @@ public void testStoreSubsetThreeOne() {
11971197
cpu.v[2] = 6;
11981198
cpu.v[3] = 7;
11991199
cpu.index = 0x5000;
1200-
cpu.operand = 0xF312;
1200+
cpu.operand = 0x5312;
12011201
cpu.storeSubsetOfRegistersInMemory();
12021202
assertEquals(7, memory.read(0x5000));
12031203
assertEquals(6, memory.read(0x5001));
@@ -1210,7 +1210,7 @@ public void testStoreSubsetIntegration() {
12101210
cpu.v[2] = 6;
12111211
cpu.v[3] = 7;
12121212
cpu.index = 0x5000;
1213-
memory.write(0xF3, 0x0200);
1213+
memory.write(0x53, 0x0200);
12141214
memory.write(0x12, 0x0201);
12151215
cpu.fetchIncrementExecute();
12161216
assertEquals(7, memory.read(0x5000));
@@ -1223,7 +1223,7 @@ public void testLoadSubsetOneTwo() {
12231223
cpu.v[1] = 5;
12241224
cpu.v[2] = 6;
12251225
cpu.index = 0x5000;
1226-
cpu.operand = 0xF123;
1226+
cpu.operand = 0x5123;
12271227
memory.write(7, 0x5000);
12281228
memory.write(8, 0x5001);
12291229
cpu.loadSubsetOfRegistersFromMemory();
@@ -1236,7 +1236,7 @@ public void testLoadSubsetOneOne() {
12361236
cpu.v[1] = 5;
12371237
cpu.v[2] = 6;
12381238
cpu.index = 0x5000;
1239-
cpu.operand = 0xF113;
1239+
cpu.operand = 0x5113;
12401240
memory.write(7, 0x5000);
12411241
memory.write(8, 0x5001);
12421242
cpu.loadSubsetOfRegistersFromMemory();
@@ -1250,7 +1250,7 @@ public void testLoadSubsetThreeOne() {
12501250
cpu.v[2] = 6;
12511251
cpu.v[3] = 7;
12521252
cpu.index = 0x5000;
1253-
cpu.operand = 0xF313;
1253+
cpu.operand = 0x5313;
12541254
memory.write(8, 0x5000);
12551255
memory.write(9, 0x5001);
12561256
memory.write(10, 0x5002);
@@ -1266,7 +1266,7 @@ public void testLoadSubsetIntegration() {
12661266
cpu.v[2] = 6;
12671267
cpu.v[3] = 7;
12681268
cpu.index = 0x5000;
1269-
memory.write(0xF3, 0x0200);
1269+
memory.write(0x53, 0x0200);
12701270
memory.write(0x13, 0x0201);
12711271
memory.write(8, 0x5000);
12721272
memory.write(9, 0x5001);

0 commit comments

Comments
 (0)