Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
SN76496 |
|
| 3.5714285714285716;3.571 |
1 | /* |
|
2 | * SN76496.java |
|
3 | * |
|
4 | * This file is part of JavaGear. |
|
5 | * |
|
6 | * JavaGear is free software; you can redistribute it and/or modify |
|
7 | * it under the terms of the GNU General Public License as published by |
|
8 | * the Free Software Foundation; either version 2 of the License, or |
|
9 | * (at your option) any later version. |
|
10 | * |
|
11 | * JavaGear is distributed in the hope that it will be useful, |
|
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | * GNU General Public License for more details. |
|
15 | * |
|
16 | * You should have received a copy of the GNU General Public License |
|
17 | * along with JavaGear; if not, write to the Free Software |
|
18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|
19 | */ |
|
20 | ||
21 | package uk.co.javagear; |
|
22 | ||
23 | import java.io.File; |
|
24 | import java.io.FileInputStream; |
|
25 | import java.io.FileWriter; |
|
26 | import java.io.IOException; |
|
27 | import javax.sound.sampled.AudioFileFormat; |
|
28 | import javax.sound.sampled.AudioFormat; |
|
29 | import javax.sound.sampled.AudioInputStream; |
|
30 | import javax.sound.sampled.AudioSystem; |
|
31 | import javax.sound.sampled.DataLine; |
|
32 | import javax.sound.sampled.LineUnavailableException; |
|
33 | import javax.sound.sampled.SourceDataLine; |
|
34 | import org.apache.log4j.Logger; |
|
35 | ||
36 | /** |
|
37 | * Texas SN76496 Emulation. |
|
38 | * |
|
39 | * @author Copyright (C) 2002-2003 Chris White |
|
40 | * @version 18th January 2003 |
|
41 | * @see "JavaGear Final Project Report" |
|
42 | */ |
|
43 | public final class SN76496 { |
|
44 | /** |
|
45 | * Tone Generator 1. |
|
46 | */ |
|
47 | private ToneGenerator chan0; |
|
48 | ||
49 | /** |
|
50 | * Tone Generator 2. |
|
51 | */ |
|
52 | private ToneGenerator chan1; |
|
53 | ||
54 | /** |
|
55 | * Tone Generator 3. |
|
56 | */ |
|
57 | private ToneGenerator chan2; |
|
58 | ||
59 | /** |
|
60 | * Noise Generator. |
|
61 | */ |
|
62 | private NoiseGenerator chan3; |
|
63 | ||
64 | /** |
|
65 | * Pointer to current Tone Generator. |
|
66 | */ |
|
67 | private ToneGenerator currentGenerator; |
|
68 | ||
69 | /** |
|
70 | * Buffer for sound data. |
|
71 | */ |
|
72 | private byte [] buffer; |
|
73 | ||
74 | /** |
|
75 | * For Recording Sound to Disk. |
|
76 | */ |
|
77 | private FileWriter fileWriter; |
|
78 | ||
79 | /** |
|
80 | * Specifies the arrangement of sound data. |
|
81 | */ |
|
82 | private AudioFormat audioFormat; |
|
83 | ||
84 | /** |
|
85 | * The line that transmits audio to speakers. |
|
86 | */ |
|
87 | private SourceDataLine line; |
|
88 | ||
89 | /** |
|
90 | * PSG Clock Speed. |
|
91 | */ |
|
92 | private double clockSpeed; |
|
93 | ||
94 | /** |
|
95 | * Output Sample Rate. |
|
96 | */ |
|
97 | private int sampleRate; |
|
98 | ||
99 | /** |
|
100 | * Samples to Generate per video frame. |
|
101 | */ |
|
102 | private int samplesPerFrame; |
|
103 | ||
104 | /** |
|
105 | * Sound Enabled. |
|
106 | */ |
|
107 | private boolean enabled; |
|
108 | ||
109 | /** |
|
110 | * Record Sound to Disk. |
|
111 | */ |
|
112 | private boolean recording; |
|
113 | ||
114 | ||
115 | ||
116 | /** |
|
117 | * SN76496 Constructor. |
|
118 | * |
|
119 | * @param c Clock Speed (Hz) |
|
120 | * @param s Sample Rate (Hz) |
|
121 | */ |
|
122 | 0 | public SN76496(double c, int s) { |
123 | 0 | clockSpeed = c; |
124 | 0 | sampleRate = s; |
125 | ||
126 | 0 | chan0 = new ToneGenerator(clockSpeed, sampleRate); |
127 | 0 | chan1 = new ToneGenerator(clockSpeed, sampleRate); |
128 | 0 | chan2 = new ToneGenerator(clockSpeed, sampleRate); |
129 | 0 | chan3 = new NoiseGenerator(clockSpeed, sampleRate, chan2); |
130 | ||
131 | // AudioFormat(sample_rate(hz), sampleSizeInBits, channels, signed, bigEndian) |
|
132 | 0 | audioFormat = new AudioFormat(sampleRate, 8, 1, true, false); |
133 | 0 | DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); |
134 | ||
135 | 0 | if (!AudioSystem.isLineSupported(info)) { |
136 | 0 | System.out.print("Audio not supported..."); |
137 | } else { |
|
138 | try { |
|
139 | 0 | line = (SourceDataLine) AudioSystem.getLine(info); |
140 | 0 | line.open(audioFormat); |
141 | 0 | line.start(); |
142 | 0 | } catch (LineUnavailableException lue) { |
143 | 0 | Logger.getLogger(this.getClass()).error("Unable to open audio line.", lue); |
144 | 0 | System.out.print("Audio Line Unavailable..."); |
145 | 0 | } |
146 | } |
|
147 | ||
148 | 0 | recording = false; |
149 | ||
150 | // Default to 60 FPS |
|
151 | 0 | setFPS(60); |
152 | ||
153 | // Reset to Defaults |
|
154 | 0 | reset(); |
155 | ||
156 | // Enabled by Default |
|
157 | 0 | setEnabled(); |
158 | 0 | } |
159 | ||
160 | ||
161 | /** |
|
162 | * Reset SN76496 to Default Values. |
|
163 | */ |
|
164 | public void reset() { |
|
165 | 0 | currentGenerator = chan0; |
166 | 0 | chan0.reset(); |
167 | 0 | chan1.reset(); |
168 | 0 | chan2.reset(); |
169 | 0 | chan3.reset(); |
170 | 0 | } |
171 | ||
172 | ||
173 | /** |
|
174 | * Returns <code>true</code> if this instance of <code>SN76496</code> is enabled and |
|
175 | * <code>false</code> otherwise. |
|
176 | * |
|
177 | * @return <code>true</code> if enabled. |
|
178 | */ |
|
179 | public boolean isEnabled() { |
|
180 | 0 | return enabled; |
181 | } |
|
182 | ||
183 | ||
184 | /** |
|
185 | * Toggle SN76496 On/Off. |
|
186 | */ |
|
187 | public void setEnabled() { |
|
188 | 0 | if (enabled) { |
189 | 0 | stopSound(); |
190 | } else { |
|
191 | 0 | startSound(); |
192 | } |
|
193 | 0 | } |
194 | ||
195 | ||
196 | /** |
|
197 | * Toggle a particular channel On/Off. |
|
198 | * |
|
199 | * @param channel channel to toggle (0-3) |
|
200 | * @throws IllegalArgumentException if the value of channels is not 1, 2 or 3. |
|
201 | */ |
|
202 | public void setChannelEnabled(int channel) throws IllegalArgumentException { |
|
203 | 0 | switch (channel) { |
204 | case 0: |
|
205 | 0 | chan0.setEnabled(); |
206 | 0 | break; |
207 | case 1: |
|
208 | 0 | chan1.setEnabled(); |
209 | 0 | break; |
210 | case 2: |
|
211 | 0 | chan2.setEnabled(); |
212 | 0 | break; |
213 | case 3: |
|
214 | 0 | chan3.setEnabled(); |
215 | 0 | break; |
216 | default: |
|
217 | 0 | throw new IllegalArgumentException("Invalid Channel: " + channel); |
218 | } |
|
219 | 0 | } |
220 | ||
221 | /** |
|
222 | * Set current FPS Rate. |
|
223 | * Required to generate the correct number of samples per frame. |
|
224 | * |
|
225 | * @param v FPS Rate |
|
226 | */ |
|
227 | public void setFPS(int v) { |
|
228 | 0 | samplesPerFrame = (sampleRate / v); |
229 | 0 | buffer = new byte[samplesPerFrame]; |
230 | ||
231 | 0 | } |
232 | ||
233 | ||
234 | /** |
|
235 | * Program the PSG. Connected this procedure to a Z80 Port. |
|
236 | * |
|
237 | * @param value Value to write (0-0xFF) |
|
238 | */ |
|
239 | public void write(int value) { |
|
240 | 0 | if ((!enabled) && (!recording)) { |
241 | 0 | return; |
242 | } |
|
243 | ||
244 | 0 | if ((value & 0x80) == 0x80) { |
245 | 0 | switch((value >> 4) & 0x07) { |
246 | case 0x00: // 000 rr = 00 (Channel 0 Frequency) |
|
247 | 0 | chan0.setFirstByte(value & 0x0F); |
248 | 0 | currentGenerator = chan0; |
249 | 0 | break; |
250 | case 0x01: // 001 rr = 00 (Channel 0 Volume) |
|
251 | 0 | chan0.setVolume(value & 0x0F); |
252 | 0 | currentGenerator = chan0; |
253 | 0 | break; |
254 | case 0x02: // 010 rr = 01 (Channel 1 Frequency) |
|
255 | 0 | chan1.setFirstByte(value & 0x0F); |
256 | 0 | currentGenerator = chan1; |
257 | 0 | break; |
258 | case 0x03: // 011 rr = 01 (Channel 1 Volume) |
|
259 | 0 | chan1.setVolume(value & 0x0F); |
260 | 0 | currentGenerator = chan1; |
261 | 0 | break; |
262 | case 0x04: // 100 rr = 10 (Channel 2 Frequency) |
|
263 | 0 | chan2.setFirstByte(value & 0x0F); |
264 | 0 | currentGenerator = chan2; |
265 | 0 | break; |
266 | case 0x05: // 101 rr = 10 (Channel 2 Volume) |
|
267 | 0 | chan2.setVolume(value & 0x0F); |
268 | 0 | currentGenerator = chan2; |
269 | 0 | break; |
270 | case 0x06: // 110 rr = 11 (Noise Channel Frequency) |
|
271 | 0 | chan3.setFrequency(value & 0x07); |
272 | 0 | currentGenerator = null; |
273 | 0 | break; |
274 | case 0x07: // 111 rr = 11 (Noise Channel Volume) |
|
275 | 0 | chan3.setVolume(value & 0x0F); |
276 | 0 | currentGenerator = null; |
277 | 0 | break; |
278 | default: |
|
279 | 0 | break; |
280 | } |
|
281 | 0 | } else if (currentGenerator != null) { // Set significant bits of frequency |
282 | 0 | currentGenerator.setFreqSigf(value); |
283 | } else { |
|
284 | 0 | chan3.setFrequency(value & 0x07); |
285 | } |
|
286 | 0 | } |
287 | ||
288 | /** |
|
289 | * Start emulation. |
|
290 | */ |
|
291 | public synchronized void startSound() { |
|
292 | 0 | reset(); |
293 | 0 | enabled = true; |
294 | 0 | line.start(); |
295 | 0 | } |
296 | ||
297 | /** |
|
298 | * Stop emulation. |
|
299 | */ |
|
300 | public synchronized void stopSound() { |
|
301 | 0 | enabled = false; |
302 | 0 | line.stop(); |
303 | 0 | } |
304 | ||
305 | ||
306 | /** |
|
307 | * Convert PSG settings to Java Sound. |
|
308 | */ |
|
309 | public void output() { |
|
310 | 0 | if (!enabled && !recording) { |
311 | 0 | return; |
312 | } |
|
313 | ||
314 | // Loop for length of this video frame |
|
315 | 0 | for (int i = 0; i < samplesPerFrame; i++) { |
316 | // Sum the channel outputs |
|
317 | 0 | int join = 0; |
318 | 0 | join += chan0.getSample(); |
319 | 0 | join += chan1.getSample(); |
320 | 0 | join += chan2.getSample(); |
321 | 0 | join += chan3.getSample(); |
322 | ||
323 | // Scale volume up |
|
324 | 0 | join <<= 1; |
325 | ||
326 | // Check boundaries |
|
327 | 0 | if (join > 0x7F) { |
328 | 0 | join = 0x7F; |
329 | 0 | } else if (join < -0x80) { |
330 | 0 | join = -0x80; |
331 | } |
|
332 | ||
333 | 0 | buffer[i] = (byte) join; |
334 | ||
335 | 0 | if (recording) { |
336 | try { |
|
337 | 0 | fileWriter.write(join & 0xff); // output 8 bit signed mono |
338 | 0 | } catch (IOException ioe) { |
339 | 0 | Logger.getLogger(this.getClass()).error("An error occurred while writing the" |
340 | + " sound file.", ioe); |
|
341 | 0 | } |
342 | } |
|
343 | } |
|
344 | ||
345 | // Write to Java Line |
|
346 | 0 | if (enabled) { |
347 | try { |
|
348 | // Output Stream write(byte[] b, int off, int len) |
|
349 | // Small buffer to avoid latency, but more intensive CPU usage |
|
350 | 0 | line.write(buffer, 0, samplesPerFrame); |
351 | 0 | } catch (IllegalArgumentException iae) { |
352 | 0 | Logger.getLogger(this.getClass()).error("Error writing to the audio line. " |
353 | + "The bytes do not represent complete frames.", iae); |
|
354 | 0 | } catch (ArrayIndexOutOfBoundsException aiobe) { |
355 | 0 | Logger.getLogger(this.getClass()).error("Error writing to the audio line. " |
356 | + "The buffer does not contain the number of bytes specified.", aiobe); |
|
357 | 0 | } |
358 | } |
|
359 | 0 | } |
360 | ||
361 | ||
362 | /** |
|
363 | * Toggle sound recording to WAV file. |
|
364 | */ |
|
365 | public void setRecord() { |
|
366 | 0 | if (recording) { |
367 | 0 | stopRecording(); |
368 | } else { |
|
369 | 0 | startRecording(); |
370 | } |
|
371 | 0 | } |
372 | ||
373 | ||
374 | /** |
|
375 | * Start sound recording to WAV file. |
|
376 | */ |
|
377 | private void startRecording() { |
|
378 | 0 | if (!recording) { |
379 | try { |
|
380 | 0 | fileWriter = new FileWriter("output.raw"); |
381 | 0 | } catch (IOException ioe) { |
382 | 0 | Logger.getLogger(this.getClass()).error("Could not open file for recording.", ioe); |
383 | 0 | System.out.println("Could not open file for recording"); |
384 | 0 | } |
385 | 0 | recording = true; |
386 | } |
|
387 | 0 | } |
388 | ||
389 | ||
390 | /** |
|
391 | * Stop sound recording to WAV file. |
|
392 | */ |
|
393 | public void stopRecording() { |
|
394 | 0 | if (recording) { |
395 | try { |
|
396 | 0 | fileWriter.close(); |
397 | 0 | convertToWav(); |
398 | 0 | } catch (IOException ioe) { |
399 | 0 | Logger.getLogger(this.getClass()).error("Failed whilst closing output.raw", ioe); |
400 | 0 | System.out.println("Failed whilst closing output.raw"); |
401 | 0 | } |
402 | 0 | recording = false; |
403 | } |
|
404 | 0 | } |
405 | ||
406 | /** |
|
407 | * Convert RAW output to WAV file. |
|
408 | */ |
|
409 | private void convertToWav() { |
|
410 | 0 | File input = new File("output.raw"); |
411 | 0 | File output = new File("output.wav"); |
412 | ||
413 | try { |
|
414 | 0 | FileInputStream fileInputStream = new FileInputStream(input); |
415 | ||
416 | 0 | AudioInputStream audioInputStream = new AudioInputStream(fileInputStream, audioFormat |
417 | , input.length()); |
|
418 | 0 | AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, output); |
419 | 0 | audioInputStream.close(); |
420 | 0 | input.delete(); // Delete old file |
421 | 0 | System.out.println("OUTPUT.WAV recorded"); |
422 | 0 | } catch (IOException ioe) { |
423 | 0 | Logger.getLogger(this.getClass()).error("Error writing WAV file.", ioe); |
424 | 0 | System.out.println("Error writing WAV file"); |
425 | 0 | } |
426 | 0 | } |
427 | ||
428 | } |