jace/src/main/java/jace/hardware/CardMockingboard.java

407 lines
15 KiB
Java

/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.RAMListener;
import jace.core.SoundMixer;
import static jace.core.Utility.*;
import jace.hardware.mockingboard.PSG;
import jace.hardware.mockingboard.R6522;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
/**
* Mockingboard-C implementation (with partial Phasor support). This uses two
* 6522 chips to communicate to two respective AY PSG sound chips. This class
* manages the I/O access as well as the sound playback thread.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Mockingboard")
public class CardMockingboard extends Card implements Runnable {
// If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips
static final int[] AY_ADDRESSES = new int[]{0, 0x080, 0x010, 0x090};
@ConfigurableField(name = "Volume", shortName = "vol",
category = "Sound",
description = "Mockingboard volume, 100=max, 0=silent")
public int volume = 100;
static public int MAX_AMPLITUDE = 0x007fff;
@ConfigurableField(name = "Phasor mode",
category = "Sound",
description = "If enabled, card will have 4 sound chips instead of 2")
public boolean phasorMode = false;
@ConfigurableField(name = "Clock Rate (hz)",
category = "Sound",
defaultValue = "1020484",
description = "Clock rate of AY oscillators")
public int CLOCK_SPEED = 1020484;
public int SAMPLE_RATE = 48000;
@ConfigurableField(name = "Buffer size",
category = "Sound",
description = "Number of samples to generate on each pass")
public int BUFFER_LENGTH = 64;
// The array of configured AY chips
public PSG[] chips;
// The 6522 controllr chips (always 2)
public R6522[] controllers;
private int ticksBeteenPlayback = 5000;
Lock timerSync = new ReentrantLock();
Condition cpuCountReached = timerSync.newCondition();
Condition playbackFinished = timerSync.newCondition();
@ConfigurableField(name = "Idle sample threshold", description = "Number of samples to wait before suspending sound")
private int MAX_IDLE_SAMPLES = SAMPLE_RATE;
@Override
public String getDeviceName() {
return "Mockingboard";
}
public CardMockingboard() {
controllers = new R6522[2];
for (int i = 0; i < 2; i++) {
//don't ask...
final int j = i;
controllers[i] = new R6522() {
@Override
public void sendOutputA(int value) {
if (activeChip != null) {
activeChip.setBus(value);
} else {
System.out.println("No active AY chip!");
}
}
@Override
public void sendOutputB(int value) {
if (activeChip != null) {
activeChip.setControl(value & 0x07);
} else {
System.out.println("No active AY chip!");
}
}
@Override
public int receiveOutputA() {
return activeChip == null ? 0 : activeChip.bus;
}
@Override
public int receiveOutputB() {
return 0;
}
@Override
public String getShortName() {
return "timer" + j;
}
};
}
}
@Override
public void reset() {
// Reset PSG registers
suspend();
if (chips != null) {
for (PSG psg : chips) {
psg.reset();
}
}
}
RAMListener mainListener = null;
PSG activeChip = null;
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// System.out.println(e.getType().toString() + " event to mockingboard register "+Integer.toHexString(register)+", value "+e.getNewValue());
activeChip = null;
resume();
int chip = 0;
for (PSG psg : chips) {
if (psg.getBaseReg() == (register & 0x0f0)) {
activeChip = psg;
break;
}
chip++;
}
if (activeChip == null) {
System.err.println("Could not determine which PSG to communicate to");
e.setNewValue(Computer.getComputer().getVideo().getFloatingBus());
return;
}
R6522 controller = controllers[chip & 1];
if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f);
// System.out.println("Register returns "+Integer.toHexString(val));
e.setNewValue(val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
}
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
// Oddly, all IO is done at the firmware address bank. It's a strange card.
e.setNewValue(Computer.getComputer().getVideo().getFloatingBus());
}
long ticksSinceLastPlayback = 0;
@Override
public void tick() {
for (R6522 c : controllers) {
if (c == null || !c.isRunning()) {
continue;
}
c.tick();
}
if (isRunning() && !pause) {
timerSync.lock();
try {
ticksSinceLastPlayback++;
if (ticksSinceLastPlayback >= ticksBeteenPlayback) {
cpuCountReached.signalAll();
while (isRunning() && ticksSinceLastPlayback >= ticksBeteenPlayback) {
if (!playbackFinished.await(1, TimeUnit.SECONDS)) {
gripe("The mockingboard playback thread has stalled. Disabling mockingboard.");
suspend();
}
}
}
} catch (InterruptedException ex) {
suspend();
// Do nothing, probably suspending CPU
} finally {
timerSync.unlock();
}
}
}
@Override
public void reconfigure() {
boolean restart = suspend();
initPSG();
for (PSG chip : chips) {
chip.setRate(CLOCK_SPEED, SAMPLE_RATE);
chip.reset();
}
super.reconfigure();
if (restart) {
resume();
}
}
///////////////////////////////////////////////////////////
public static int[] VolTable;
int[][] buffers;
int bufferLength = -1;
public void playSound(int[] left, int[] right) {
chips[0].update(left, true, left, false, left, false, BUFFER_LENGTH);
chips[1].update(right, true, right, false, right, false, BUFFER_LENGTH);
if (phasorMode) {
chips[2].update(left, false, left, false, left, false, BUFFER_LENGTH);
chips[3].update(right, false, right, false, right, false, BUFFER_LENGTH);
}
}
public void buildMixerTable() {
VolTable = new int[16];
int numChips = phasorMode ? 4 : 2;
/* calculate the volume->voltage conversion table */
/* The AY-3-8910 has 16 levels, in a logarithmic scale (3dB per step) */
/* The YM2149 still has 16 levels for the tone generators, but 32 for */
/* the envelope generator (1.5dB per step). */
double out = ((double) MAX_AMPLITUDE * (double) volume) / 100.0;
// Reduce max amplitude to reflect post-mixer values so we don't have to scale volume when mixing channels
out = out * 2.0 / 3.0 / numChips;
double delta = 1.15;
for (int i = 15; i > 0; i--) {
VolTable[i] = (int) Math.round(out); /* round to nearest */ // [TC: unsigned int cast]
// out /= 1.188502227; /* = 10 ^ (1.5/20) = 1.5dB */
// out /= 1.15; /* = 10 ^ (3/20) = 3dB */
delta += 0.0225;
out /= delta; // As per applewin's source, the levels don't scale as documented.
}
VolTable[0] = 0;
}
Thread playbackThread = null;
boolean pause = false;
@Override
public void resume() {
pause = false;
if (!isRunning()) {
if (chips == null) {
initPSG();
}
for (R6522 controller : controllers) {
controller.attach();
controller.resume();
}
}
super.resume();
if (playbackThread == null || !playbackThread.isAlive()) {
playbackThread = new Thread(this, "Mockingboard sound playback");
playbackThread.start();
}
}
@Override
public boolean suspend() {
super.suspend();
for (R6522 controller : controllers) {
controller.suspend();
controller.detach();
}
if (playbackThread == null || !playbackThread.isAlive()) {
return false;
}
if (playbackThread != null) {
playbackThread.interrupt();
try {
// Wait for thread to die
playbackThread.join();
} catch (InterruptedException ex) {
}
}
playbackThread = null;
return true;
}
@Override
/**
* This is the audio playback thread
*/
public void run() {
try {
SourceDataLine out = Motherboard.mixer.getLine(this);
int[] leftBuffer = new int[BUFFER_LENGTH];
int[] rightBuffer = new int[BUFFER_LENGTH];
int frameSize = out.getFormat().getFrameSize();
byte[] buffer = new byte[BUFFER_LENGTH * frameSize];
System.out.println("Mockingboard playback started");
int bytesPerSample = frameSize / 2;
buildMixerTable();
ticksBeteenPlayback = (int) ((Motherboard.SPEED * BUFFER_LENGTH) / SAMPLE_RATE);
ticksSinceLastPlayback = 0;
int zeroSamples = 0;
while (isRunning()) {
Motherboard.requestSpeed(this);
playSound(leftBuffer, rightBuffer);
int p = 0;
for (int idx = 0; idx < BUFFER_LENGTH; idx++) {
int sampleL = leftBuffer[idx];
int sampleR = rightBuffer[idx];
// Convert left + right samples into buffer format
if (sampleL == 0 && sampleR == 0) {
zeroSamples++;
} else {
zeroSamples = 0;
}
for (int shift = SoundMixer.BITS - 8, index = 0; shift >= 0; shift -= 8, index++) {
buffer[p + index] = (byte) (sampleR >> shift);
buffer[p + index + bytesPerSample] = (byte) (sampleL >> shift);
}
p += frameSize;
}
try {
timerSync.lock();
ticksSinceLastPlayback -= ticksBeteenPlayback;
} finally {
timerSync.unlock();
}
out.write(buffer, 0, buffer.length);
if (zeroSamples >= MAX_IDLE_SAMPLES) {
zeroSamples = 0;
pause = true;
Motherboard.cancelSpeedRequest(this);
while (pause && isRunning()) {
try {
Thread.sleep(50);
timerSync.lock();
playbackFinished.signalAll();
} catch (InterruptedException ex) {
return;
} catch (IllegalMonitorStateException ex) {
// Do nothing
} finally {
try {
timerSync.unlock();
} catch (IllegalMonitorStateException ex) {
// Do nothing -- this is probably caused by a suspension event
}
}
}
}
try {
timerSync.lock();
playbackFinished.signalAll();
while (isRunning() && ticksSinceLastPlayback < ticksBeteenPlayback) {
cpuCountReached.await();
}
} catch (InterruptedException ex) {
// Do nothing, probably killing playback thread on purpose
} finally {
timerSync.unlock();
}
}
} catch (LineUnavailableException ex) {
Logger.getLogger(CardMockingboard.class
.getName()).log(Level.SEVERE, null, ex);
} finally {
Motherboard.cancelSpeedRequest(this);
System.out.println("Mockingboard playback stopped");
Motherboard.mixer.returnLine(this);
}
}
private void initPSG() {
int max = phasorMode ? 4 : 2;
chips = new PSG[max];
for (int i = 0; i < max; i++) {
chips[i] = new PSG(AY_ADDRESSES[i], CLOCK_SPEED, SAMPLE_RATE, "AY" + i);
}
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no c8 rom access to emulate
}
// This fixes freezes when resizing the window, etc.
@Override
public boolean suspendWithCPU() {
return true;
}
}