jace/src/main/java/jace/hardware/PassportMidiInterface.java

552 lines
22 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.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
/**
* Partial implementation of Passport midi card, supporting midi output routed
* to the java midi synth for playback. Compatible with Ultima V. Card
* operational notes taken from the Passport MIDI interface manual
* ftp://ftp.apple.asimov.net/pub/apple_II/documentation/hardware/misc/passport_midi.pdf
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name(value = "Passport Midi Interface", description = "MIDI sound card")
public class PassportMidiInterface extends Card {
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no rom on this card, so nothing to do here
}
// MIDI timing: 31250 BPS, 8-N-1 (roughly 3472k per second)
public static enum TIMER_MODE {
continuous, singleShot, freqComparison, pulseComparison
};
public static class PTMTimer {
// Configuration values
public boolean prescaledTimer = false; // Only available on Timer 3
public boolean enableClock = false; // False == use CX clock input
public boolean dual8BitMode = false;
public TIMER_MODE mode = TIMER_MODE.continuous;
public boolean irqEnabled = false;
public boolean counterOutputEnable = false;
// Set by data latches
public Long duration = 0L;
// Run values
public boolean irqRequested = true;
public Long value = 0L;
}
// I/O registers
// --- 6840 PTM
public static final int TIMER_CONTROL_1 = 0;
public static final int TIMER_CONTROL_2 = 1;
public static final int TIMER1_MSB = 2;
public static final int TIMER1_LSB = 3;
public static final int TIMER2_MSB = 4;
public static final int TIMER2_LSB = 5;
// (Most likely not used)
public static final int TIMER3_MSB = 6;
public static final int TIMER3_LSB = 7;
// --- 6850 ACIA registers (write)
public static final int ACIA_CONTROL = 8;
public static final int ACIA_SEND = 9;
// --- 6850 ACIA registers (read)
public static final int ACIA_STATUS = 8;
public static final int ACIA_RECV = 9;
// --- Drums
public static final int DRUM_SYNC_SET = 0x0e;
public static final int DRUM_SYNC_CLEAR = 0x0f;
//---------------------------------------------------------
// PTM control values (register 1,2 and 3)
public static final int PTM_START_TIMERS = 0;
public static final int PTM_STOP_TIMERS = 1;
public static final int PTM_RESET = 67;
// PTM select values (register 2 only) -- modifies what Reg 1 points to
public static final int PTM_SELECT_REG_1 = 1;
public static final int PTM_SELECT_REG_3 = 0;
// PTM select values (register 3 only)
public static final int TIMER_3_PRESCALED = 1;
public static final int TIMER_3_NOT_PRESCALED = 0;
// PTM bit values
public static final int PTM_CLOCK_SOURCE = 2; // Bit 1
// 0 = external, 2 = internal clock
public static final int PTM_LATCH_IS_16_BIT = 4; // Bit 2
// 0 = 16-bit, 4 = dual 8-bit
// Bits 3-5
// 5 4 3
public static final int PTM_CONTINUOUS = 0; // 0 x 0
public static final int PTM_SINGLE_SHOT = 32; // 1 x 0
public static final int PTM_FREQ_COMP = 8; // x 0 1
public static final int PTM_PULSE_COMP = 24; // x 1 1
public static final int PTM_IRQ_ENABLED = 64; // Bit 6
// 64 = IRQ Enabled, 0 = IRQ Masked
public static final int PTM_OUTPUT_ENABLED = 128; // Bit 7
// 128 = Timer output enabled, 0 = disabled
// ACIA control values
// Reset == Master reset + even parity + 2 stop bits + 8 bit + No interrupts (??)
public static final int ACIA_RESET = 19;
public static final int ACIA_MASK_INTERRUPTS = 17;
public static final int ACIA_OFF = 21;
// Counter * 1 + RTS = low, transmit interrupt enabled
public static final int ACIA_INT_ON_SEND = 49;
// Counter * 1 + RTS = high, transmit interrupt disabled + Interrupt on receive
public static final int ACIA_INT_ON_RECV = 145;
// Counter * 1 + RTS = low, transmit interrupt enabled + Interrupt on receive
public static final int ACIA_INT_ON_SEND_AND_RECV = 177;
// ACIA control register values
// --- Bits 1 and 0 control counter divide select
public static final int ACIA_COUNTER_1 = 0;
public static final int ACIA_COUNTER_16 = 1;
public static final int ACIA_COUNTER_64 = 2;
public static final int ACIA_MASTER_RESET = 3;
// Midi is always transmitted 8-N-1
public static final int ACIA_ODD_PARITY = 4; // 4 = odd, 0 = even
public static final int ACIA_STOP_BITS_1 = 8; // 8 = 1 stop bit, 0 = 2 stop bits
public static final int ACIA_WORD_LENGTH_8 = 16; // 16 = 8-bit, 0 = 7-bit
// --- Bits 5 and 6 control interrupts
// 6 5
// 0 0 RTS = low, transmit interrupt disabled
// 0 1 RTS = low, transmit interrupt enabled
// 1 0 RTS = high, transmit interrupt disabled
// 1 1 RTS = low, Transmit break, trasmit interrupt disabled
public static final int ACIA_RECV_INTERRUPT = 128; // 128 = interrupt on receive, 0 = no interrupt
// PTM configuration
private boolean ptmTimer3Selected = false; // When true, reg 1 points at timer 3
private boolean ptmTimersActive = false; // When true, timers run constantly
private PTMTimer[] ptmTimer = {
new PTMTimer(),
new PTMTimer(),
new PTMTimer()
};
private boolean ptmStatusReadSinceIRQ = false;
// ---------------------- ACIA CONFIGURATION
private boolean aciaInterruptOnSend = false;
private boolean aciaInterruptOnReceive = false;
// ---------------------- ACIA STATUS BITS
// True when MIDI IN receives a byte
private boolean receivedACIAByte = false;
// True when data is not transmitting (always true because we aren't really doing wire transmission);
private boolean transmitACIAEmpty = true;
// True if another byte is received before the previous byte was processed
private boolean receiverACIAOverrun = false;
// True if ACIA generated interrupt request
private boolean irqRequestedACIA = false;
//--- the synth
private Synthesizer synth;
@Override
public void reset() {
// TODO: Deactivate card
suspend();
}
@Override
public boolean suspend() {
// TODO: Deactivate card
suspendACIA();
return super.suspend();
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// No firmware, so do nothing
return;
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
switch (type) {
case READ_DATA:
int returnValue = 0;
switch (register) {
case ACIA_STATUS:
returnValue = getACIAStatus();
break;
case ACIA_RECV:
returnValue = getACIARecieve();
break;
//TODO: Implement PTM registers
case TIMER_CONTROL_1:
// Technically it's not supposed to return anything...
returnValue = getPTMStatus();
break;
case TIMER_CONTROL_2:
returnValue = getPTMStatus();
break;
case TIMER1_LSB:
returnValue = (int) (ptmTimer[0].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false;
}
break;
case TIMER1_MSB:
returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false;
}
break;
case TIMER2_LSB:
returnValue = (int) (ptmTimer[1].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false;
}
break;
case TIMER2_MSB:
returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false;
}
break;
case TIMER3_LSB:
returnValue = (int) (ptmTimer[2].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false;
}
break;
case TIMER3_MSB:
returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false;
}
break;
default:
System.out.println("Passport midi read unrecognized, port " + register);
}
e.setNewValue(returnValue);
// System.out.println("Passport I/O read register " + register + " == " + returnValue);
break;
case WRITE:
int v = e.getNewValue() & 0x0ff;
// System.out.println("Passport I/O write register " + register + " == " + v);
switch (register) {
case ACIA_CONTROL:
processACIAControl(v);
break;
case ACIA_SEND:
processACIASend(v);
break;
case TIMER_CONTROL_1:
if (ptmTimer3Selected) {
// System.out.println("Configuring timer 3");
ptmTimer[2].prescaledTimer = ((v & TIMER_3_PRESCALED) != 0);
processPTMConfiguration(ptmTimer[2], v);
} else {
// System.out.println("Configuring timer 1");
if ((v & PTM_STOP_TIMERS) == 0) {
startPTM();
} else {
stopPTM();
}
processPTMConfiguration(ptmTimer[0], v);
}
break;
case TIMER_CONTROL_2:
// System.out.println("Configuring timer 2");
ptmTimer3Selected = ((v & PTM_SELECT_REG_1) == 0);
processPTMConfiguration(ptmTimer[1], v);
break;
case TIMER1_LSB:
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v;
break;
case TIMER1_MSB:
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8);
break;
case TIMER2_LSB:
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v;
break;
case TIMER2_MSB:
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8);
break;
case TIMER3_LSB:
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v;
break;
case TIMER3_MSB:
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8);
break;
default:
System.out.println("Passport midi write unrecognized, port " + register);
}
break;
}
}
@Override
public void tick() {
if (ptmTimersActive) {
for (PTMTimer t : ptmTimer) {
// if (t.duration == 0) {
// continue;
// }
t.value--;
if (t.value < 0) {
// TODO: interrupt dual 8-bit mode, whatver that is!
if (t.irqEnabled) {
// System.out.println("Timer generating interrupt!");
t.irqRequested = true;
Computer.getComputer().getCpu().generateInterrupt();
ptmStatusReadSinceIRQ = false;
}
if (t.mode == TIMER_MODE.continuous || t.mode == TIMER_MODE.freqComparison) {
t.value = t.duration;
}
}
}
}
}
@Override
public String getDeviceName() {
return "Passport MIDI Controller";
}
//------------------------------------------------------ PTM
private void processPTMConfiguration(PTMTimer timer, int val) {
timer.enableClock = (val & PTM_CLOCK_SOURCE) != 0;
timer.dual8BitMode = (val & PTM_LATCH_IS_16_BIT) != 0;
switch (val & 56) {
// Evaluate bits 3, 4 and 5 to determine mode
case PTM_CONTINUOUS:
timer.mode = TIMER_MODE.continuous;
break;
case PTM_PULSE_COMP:
timer.mode = TIMER_MODE.pulseComparison;
break;
case PTM_FREQ_COMP:
timer.mode = TIMER_MODE.freqComparison;
break;
case PTM_SINGLE_SHOT:
timer.mode = TIMER_MODE.singleShot;
break;
default:
timer.mode = TIMER_MODE.continuous;
break;
}
timer.irqEnabled = (val & PTM_IRQ_ENABLED) != 0;
timer.counterOutputEnable = (val & PTM_OUTPUT_ENABLED) != 0;
}
private void stopPTM() {
// System.out.println("Passport timers halted");
ptmTimersActive = false;
}
private void startPTM() {
// System.out.println("Passport timers started");
ptmTimersActive = true;
ptmTimer[0].irqRequested = false;
ptmTimer[1].irqRequested = false;
ptmTimer[2].irqRequested = false;
ptmTimer[0].value = ptmTimer[0].duration;
ptmTimer[1].value = ptmTimer[1].duration;
ptmTimer[2].value = ptmTimer[2].duration;
}
// Bits 0, 1 and 2 == IRQ requested from timer 1, 2 or 3
// Bit 7 = Any IRQ
private int getPTMStatus() {
int status = 0;
for (int i = 0; i < 3; i++) {
PTMTimer t = ptmTimer[i];
if (t.irqRequested && t.irqEnabled) {
ptmStatusReadSinceIRQ = true;
status |= (1 << i);
status |= 128;
}
}
return status;
}
//------------------------------------------------------ ACIA
/*
ACIA status register
Bit 0 = Receive data register full
Bit 1 = Transmit data register empty
Bits 2 and 3 pertain to modem (DCD and CTS, so ignore)
Bit 4 = Framing error
Bit 5 = Receiver overrun
Bit 6 = Partity error (not used by MIDI)
Bit 7 = Interrupt request
*/
private int getACIAStatus() {
int status = 0;
if (receivedACIAByte) {
status |= 1;
}
if (transmitACIAEmpty) {
status |= 2;
}
if (receiverACIAOverrun) {
status |= 32;
}
if (irqRequestedACIA) {
status |= 128;
}
return status;
}
// TODO: Implement MIDI IN... some day
private int getACIARecieve() {
return 0;
}
private void processACIAControl(int value) {
if ((value & 0x03) == ACIA_MASTER_RESET) {
resume();
}
}
ShortMessage currentMessage;
int currentMessageStatus;
int currentMessageData1;
int currentMessageData2;
int messageSize = 255;
int currentMessageReceived = 0;
private void processACIASend(int value) {
if (!isRunning()) {
// System.err.println("ACIA not active!");
return;
} else {
// System.out.println("ACIA send "+value);
}
// First off try to finish off previous command already in play
boolean sendMessage = false;
if (currentMessage != null) {
if ((value & 0x080) > 0) {
// Any command byte received means we finished receiving another command
// and valid or not, process it as-is
if (currentMessage != null) {
sendMessage = true;
}
// If there is no current message, then we'll pick this up afterwards...
} else {
// If we receive a data byte ( < 128 ) then check if we have the right size
// if so, then the command was completely received, and it's time to send it.
currentMessageReceived++;
if (currentMessageReceived >= messageSize) {
sendMessage = true;
}
if (currentMessageReceived == 1) {
currentMessageData1 = value;
} else {
// Possibly redundant, but there's no reason a message should be longer than this...
currentMessageData2 = value;
sendMessage = true;
}
}
}
// If we have a command to send, then do it
if (sendMessage == true) {
if (synth != null && synth.isOpen()) {
// Send message
try {
// System.out.println("Sending MIDI message "+currentMessageStatus+","+currentMessageData1+","+currentMessageData2);
currentMessage.setMessage(currentMessageStatus, currentMessageData1, currentMessageData2);
synth.getReceiver().send(currentMessage, -1L);
} catch (InvalidMidiDataException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
} catch (MidiUnavailableException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
currentMessage = null;
}
// Do we have a new command byte?
if ((value & 0x080) > 0) {
// Start a new message
currentMessage = new ShortMessage();
currentMessageStatus = value;
currentMessageData1 = 0;
currentMessageData2 = 0;
try {
currentMessage.setMessage(currentMessageStatus, 0, 0);
messageSize = currentMessage.getLength();
} catch (InvalidMidiDataException ex) {
messageSize = 0;
}
currentMessageReceived = 0;
}
}
@Override
public void resume() {
if (isRunning() && synth != null && synth.isOpen()) {
return;
}
try {
MidiDevice.Info[] devices = MidiSystem.getMidiDeviceInfo();
if (devices.length == 0) {
System.out.println("No MIDI devices found");
} else {
for (MidiDevice.Info dev : devices) {
System.out.println("MIDI Device found: " + dev);
if (dev.getName().contains("Java Sound")) {
if (dev instanceof Synthesizer) {
synth = (Synthesizer) dev;
break;
}
}
}
}
if (synth == null) {
synth = MidiSystem.getSynthesizer();
}
if (synth != null) {
System.out.println("Selected MIDI device: " + synth.getDeviceInfo().getName());
synth.open();
super.resume();
}
} catch (MidiUnavailableException ex) {
System.out.println("Could not open MIDI synthesizer");
ex.printStackTrace();
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void suspendACIA() {
// TODO: Stop ACIA thread...
if (synth != null && synth.isOpen()) {
synth.close();
synth = null;
}
}
}