6
6
"""
7
7
# I M P O R T S ###############################################################
8
8
9
+ import numpy as np
10
+
9
11
from pygame import key
12
+ from pygame import mixer
13
+ from pygame .mixer import Sound
10
14
from random import randint
11
15
12
- from chip8 .config import (
13
- STACK_POINTER_START , KEY_MAPPINGS , PROGRAM_COUNTER_START
14
- )
16
+ from chip8 .config import STACK_POINTER_START , KEY_MAPPINGS , PROGRAM_COUNTER_START
15
17
16
18
# C O N S T A N T S ###########################################################
17
19
28
30
"64K" : 65536 ,
29
31
}
30
32
33
+ # The minimum number of audio samples we want to generate. The minimum amount
34
+ # of time an audio clip can be played is 1/60th of a second (the frequency
35
+ # that the sound timer is decremented). Since we initialize the pygame
36
+ # audio mixer to require 48000 samples per second, this means each 1/60th
37
+ # of a second requires 800 samples. The audio pattern buffer is only
38
+ # 128 bits long, so we will need to repeat it to fill at least 1/60th of a
39
+ # second with audio (resampled at the correct frequency). To be safe,
40
+ # we'll construct a buffer of at least 4/60ths of a second of
41
+ # audio. We can be bigger than the minimum number of samples below, but
42
+ # we don't want less than that.
43
+ MIN_AUDIO_SAMPLES = 3200
44
+
45
+ # The audio playback rate to use for Pygame mixer initialization
46
+ PYGAME_AUDIO_PLAYBACK_RATE = 48000
47
+
31
48
# C L A S S E S ###############################################################
32
49
33
50
@@ -91,8 +108,12 @@ def __init__(
91
108
self .sp = STACK_POINTER_START
92
109
self .index = 0
93
110
self .rpl = [0 ] * NUM_REGISTERS
111
+
94
112
self .pitch = 64
95
113
self .playback_rate = 4000
114
+ self .audio_pattern_buffer = [0 ] * 16
115
+ self .sound_playing = False
116
+ self .sound_waveform = None
96
117
97
118
self .bitplane = 1
98
119
@@ -164,6 +185,7 @@ def __init__(
164
185
self .misc_routine_lookup = {
165
186
0x00 : self .index_load_long , # F000 - LOADLONG
166
187
0x01 : self .set_bitplane , # Fn01 - BITPLANE n
188
+ 0x02 : self .load_audio_pattern_buffer , # F002 - AUDIO
167
189
0x07 : self .move_delay_timer_into_reg , # Ft07 - LOAD Vt, DELAY
168
190
0x0A : self .wait_for_keypress , # Ft0A - KEYD Vt
169
191
0x15 : self .move_reg_into_delay_timer , # Fs15 - LOAD DELAY, Vs
@@ -184,6 +206,7 @@ def __init__(
184
206
self .memory = bytearray (MEM_SIZE [mem_size ])
185
207
self .reset ()
186
208
self .running = True
209
+ mixer .init (frequency = PYGAME_AUDIO_PLAYBACK_RATE , size = 8 , channels = 1 )
187
210
188
211
def __str__ (self ):
189
212
val = f"PC:{ self .last_pc :04X} OP:{ self .operand :04X} "
@@ -945,6 +968,18 @@ def set_bitplane(self):
945
968
self .bitplane = (self .operand & 0x0F00 ) >> 8
946
969
self .last_op = f"BITPLANE { self .bitplane :01X} "
947
970
971
+ def load_audio_pattern_buffer (self ):
972
+ """
973
+ F002 - AUDIO
974
+
975
+ Loads the 16-byte audio pattern buffer with 16 bytes from memory
976
+ pointed to by the index register.
977
+ """
978
+ for x in range (16 ):
979
+ self .audio_pattern_buffer [x ] = self .memory [self .index + x ]
980
+ self .calculate_audio_waveform ()
981
+ self .last_op = f"AUDIO { self .index :04X} "
982
+
948
983
def move_delay_timer_into_reg (self ):
949
984
"""
950
985
Fx07 - LOAD Vx, DELAY
@@ -1198,6 +1233,9 @@ def reset(self):
1198
1233
self .rpl = [0 ] * NUM_REGISTERS
1199
1234
self .pitch = 64
1200
1235
self .playback_rate = 4000
1236
+ self .audio_pattern_buffer = [0 ] * 16
1237
+ self .sound_playing = False
1238
+ self .sound_waveform = None
1201
1239
self .bitplane = 1
1202
1240
1203
1241
def load_rom (self , filename , offset = PROGRAM_COUNTER_START ):
@@ -1221,6 +1259,51 @@ def decrement_timers(self):
1221
1259
"""
1222
1260
self .delay -= 1 if self .delay > 0 else 0
1223
1261
self .sound -= 1 if self .delay > 0 else 0
1224
-
1262
+ if self .sound > 0 and not self .sound_playing :
1263
+ if self .sound_waveform :
1264
+ self .sound_waveform .play (loops = - 1 )
1265
+ self .sound_playing = True
1266
+
1267
+ if self .sound == 0 and self .sound_playing :
1268
+ if self .sound_waveform :
1269
+ self .sound_waveform .stop ()
1270
+ self .sound_playing = False
1271
+
1272
+ def calculate_audio_waveform (self ):
1273
+ """
1274
+ Based on a playback rate specified by the XO Chip pitch, generate
1275
+ an audio waveform from the 16-byte audio_pattern_buffer. It converts
1276
+ the 16-bytes pattern into 128 separate bits. The bits are then used to fill
1277
+ a sample buffer. The sample buffer is filled by resampling the 128-bit
1278
+ pattern at the specified frequency. The sample buffer is then repeated
1279
+ until it is at least MIN_AUDIO_SAMPLES long. Playback (if currently
1280
+ happening) is stopped, the new waveform is loaded, and then playback
1281
+ is starts again (if the emulator had previously been playing a sound).
1282
+ """
1283
+ # Convert the 16-byte value into an array of 128-bit samples
1284
+ data = [int (bit ) * 255 for bit in '' .join (f"{ audio_byte :08b} " for audio_byte in self .audio_pattern_buffer )]
1285
+ step = self .playback_rate / PYGAME_AUDIO_PLAYBACK_RATE
1286
+ buffer = []
1287
+
1288
+ # Generate the initial re-sampled buffer
1289
+ position = 0.0
1290
+ while position < 128 :
1291
+ buffer .append (data [int (position )])
1292
+ position += step
1293
+
1294
+ # Lengthen the buffer until it is at least MIN_AUDIO_SAMPLES long
1295
+ while len (buffer ) < MIN_AUDIO_SAMPLES :
1296
+ buffer += buffer
1297
+
1298
+ # Stop playing any waveform if it is currently playing
1299
+ if self .sound_playing and self .sound_waveform :
1300
+ self .sound_waveform .stop ()
1301
+
1302
+ # Generate a new waveform from the sample buffer
1303
+ self .sound_waveform = Sound (np .array (buffer ).astype (np .uint8 ))
1304
+
1305
+ # Start playing the sound again if we should be playing one
1306
+ if self .sound_playing :
1307
+ self .sound_waveform .play (loops = - 1 )
1225
1308
1226
1309
# E N D O F F I L E ########################################################
0 commit comments