308 lines
9.3 KiB
Java
308 lines
9.3 KiB
Java
/**
|
|
* Copyright 2024 Brendan Robert
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
**/
|
|
|
|
package jace.apple2e;
|
|
|
|
import java.io.File;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import jace.Emulator;
|
|
import jace.LawlessLegends;
|
|
import jace.config.ConfigurableField;
|
|
import jace.config.InvokableAction;
|
|
import jace.core.Device;
|
|
import jace.core.RAMEvent;
|
|
import jace.core.RAMListener;
|
|
import jace.core.SoundMixer;
|
|
import jace.core.SoundMixer.SoundBuffer;
|
|
import jace.core.SoundMixer.SoundError;
|
|
import jace.core.TimedDevice;
|
|
import jace.core.Utility;
|
|
import javafx.stage.FileChooser;
|
|
|
|
/**
|
|
* Apple // Speaker Emulation Created on May 9, 2007, 9:55 PM
|
|
*
|
|
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
|
|
*/
|
|
public class Speaker extends Device {
|
|
|
|
static boolean fileOutputActive = false;
|
|
static OutputStream out;
|
|
|
|
@ConfigurableField(category = "sound", name = "1mhz timing", description = "Force speaker output to 1mhz?")
|
|
public static boolean force1mhz = true;
|
|
|
|
@ConfigurableField(category = "sound", name = "Show sound", description = "Use black color value to show sound output")
|
|
public static boolean showSound = false;
|
|
|
|
@InvokableAction(category = "sound", name = "Record sound", description="Toggles recording (saving) sound output to a file", defaultKeyMapping = "ctrl+shift+w")
|
|
public static void toggleFileOutput() {
|
|
if (fileOutputActive) {
|
|
try {
|
|
out.close();
|
|
} catch (IOException ex) {
|
|
Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
|
|
}
|
|
out = null;
|
|
fileOutputActive = false;
|
|
} else {
|
|
FileChooser fileChooser = new FileChooser();
|
|
File f = fileChooser.showSaveDialog(LawlessLegends.getApplication().primaryStage);
|
|
if (f == null) {
|
|
return;
|
|
}
|
|
try {
|
|
out = new FileOutputStream(f);
|
|
fileOutputActive = true;
|
|
} catch (FileNotFoundException ex) {
|
|
Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Counter tracks the number of cycles between sampling
|
|
*/
|
|
private double counter = 0;
|
|
/**
|
|
* Level is the number of cycles the speaker has been on
|
|
*/
|
|
private int level = 0;
|
|
/**
|
|
* Idle cycles counts the number of cycles the speaker has not been changed
|
|
* (used to deactivate sound when not in use)
|
|
*/
|
|
private int idleCycles = 0;
|
|
/**
|
|
* Playback volume (should be < 1423)
|
|
*/
|
|
@ConfigurableField(name = "Speaker Volume", shortName = "vol", description = "Should be under 1400")
|
|
public static int VOLUME = 400;
|
|
private int currentVolume = 0;
|
|
private int fadeOffAmount = 1;
|
|
/**
|
|
* Manifestation of the apple speaker softswitch
|
|
*/
|
|
private boolean speakerBit = false;
|
|
private static double TICKS_PER_SAMPLE = ((double) TimedDevice.NTSC_1MHZ) / SoundMixer.RATE;
|
|
private RAMListener listener = null;
|
|
private SoundBuffer buffer = null;
|
|
|
|
/**
|
|
* Number of idle cycles until speaker playback is deactivated
|
|
*/
|
|
@ConfigurableField(name = "Idle cycles before sleep", shortName = "idle")
|
|
// public static int MAX_IDLE_CYCLES = (int) (SoundMixer.BUFFER_SIZE * TICKS_PER_SAMPLE * 2);
|
|
public static int MAX_IDLE_CYCLES = (int) TimedDevice.NTSC_1MHZ / 4;
|
|
|
|
/**
|
|
* Suspend playback of sound
|
|
*
|
|
* @return
|
|
*/
|
|
@Override
|
|
public boolean suspend() {
|
|
boolean result = super.suspend();
|
|
speakerBit = false;
|
|
if (buffer != null) {
|
|
try {
|
|
buffer.shutdown();
|
|
} catch (InterruptedException | ExecutionException | SoundError e) {
|
|
// Ignore
|
|
} finally {
|
|
buffer = null;
|
|
}
|
|
}
|
|
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Start or resume playback of sound
|
|
*/
|
|
@Override
|
|
public void resume() {
|
|
if (Utility.isHeadlessMode()) {
|
|
return;
|
|
}
|
|
if (buffer == null || !buffer.isAlive()) {
|
|
try {
|
|
buffer = SoundMixer.createBuffer(false);
|
|
} catch (InterruptedException | ExecutionException | SoundError e) {
|
|
e.printStackTrace();
|
|
detach();
|
|
return;
|
|
}
|
|
}
|
|
if (buffer != null) {
|
|
counter = 0;
|
|
idleCycles = 0;
|
|
level = 0;
|
|
} else {
|
|
Logger.getLogger(getClass().getName()).severe("Unable to get audio buffer for speaker!");
|
|
detach();
|
|
return;
|
|
}
|
|
|
|
if (force1mhz) {
|
|
TICKS_PER_SAMPLE = ((double) TimedDevice.NTSC_1MHZ) / SoundMixer.RATE;
|
|
} else {
|
|
TICKS_PER_SAMPLE = Emulator.withComputer(c-> ((double) c.getMotherboard().getSpeedInHz()) / SoundMixer.RATE, 0.0);
|
|
}
|
|
super.resume();
|
|
}
|
|
|
|
/**
|
|
* Reset idle counter whenever sound playback occurs
|
|
*/
|
|
public void resetIdle() {
|
|
currentVolume = VOLUME;
|
|
idleCycles = 0;
|
|
if (!isRunning()) {
|
|
resume();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Motherboard cycle tick Every 23 ticks a sample will be added to the
|
|
* buffer If the buffer is full, this will block until there is room in the
|
|
* buffer, thus keeping the emulation in sync with the sound
|
|
*/
|
|
@Override
|
|
public void tick() {
|
|
if (speakerBit) {
|
|
level++;
|
|
if (showSound) {
|
|
VideoNTSC.CHANGE_BLACK_COLOR(40, 20, 20);
|
|
}
|
|
} else if (showSound) {
|
|
VideoNTSC.CHANGE_BLACK_COLOR(20,20,40);
|
|
}
|
|
if (idleCycles++ >= MAX_IDLE_CYCLES && (currentVolume <= 0 || !speakerBit)) {
|
|
suspend();
|
|
if (showSound) {
|
|
VideoNTSC.CHANGE_BLACK_COLOR(0,0,0);
|
|
}
|
|
}
|
|
counter += 1.0d;
|
|
if (counter >= TICKS_PER_SAMPLE) {
|
|
if (idleCycles >= MAX_IDLE_CYCLES) {
|
|
currentVolume -= fadeOffAmount;
|
|
}
|
|
playSample(level * currentVolume);
|
|
// Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
|
|
|
|
// Set level back to 0
|
|
level = 0;
|
|
// Set counter to 0
|
|
counter -= TICKS_PER_SAMPLE;
|
|
}
|
|
}
|
|
|
|
private void toggleSpeaker(RAMEvent e) {
|
|
// if (e.getType() == RAMEvent.TYPE.WRITE) {
|
|
// level += 2;
|
|
// }
|
|
speakerBit = !speakerBit;
|
|
resetIdle();
|
|
}
|
|
|
|
private void playSample(int sample) {
|
|
try {
|
|
if (buffer == null || !buffer.isAlive()) {
|
|
// Logger.getLogger(getClass().getName()).severe("Audio buffer not initalized properly!");
|
|
buffer = SoundMixer.createBuffer(false);
|
|
if (buffer == null) {
|
|
System.err.println("Unable to create emergency audio buffer, detaching speaker");
|
|
detach();
|
|
return;
|
|
}
|
|
}
|
|
buffer.playSample((short) sample);
|
|
} catch (InterruptedException | ExecutionException e) {
|
|
e.printStackTrace();
|
|
} catch (SoundError e) {
|
|
System.err.println("Sound error, detaching speaker: " + e.getMessage());
|
|
e.printStackTrace();
|
|
detach();
|
|
buffer = null;
|
|
}
|
|
|
|
if (fileOutputActive) {
|
|
byte[] bytes = new byte[2];
|
|
bytes[0] = (byte) (sample & 0x0ff);
|
|
bytes[1] = (byte) ((sample >> 8) & 0x0ff);
|
|
|
|
try {
|
|
out.write(bytes, 0, 2);
|
|
} catch (IOException ex) {
|
|
Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Error recording sound", ex);
|
|
toggleFileOutput();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Add a memory event listener for C03x for capturing speaker events
|
|
*/
|
|
private void configureListener() {
|
|
listener = Emulator.withMemory(m->m.observe("Speaker", RAMEvent.TYPE.ANY, 0x0c030, 0x0c03f, this::toggleSpeaker), null);
|
|
}
|
|
|
|
private void removeListener() {
|
|
Emulator.withMemory(m->m.removeListener(listener));
|
|
}
|
|
|
|
/**
|
|
* Returns "Speaker"
|
|
*
|
|
* @return "Speaker"
|
|
*/
|
|
@Override
|
|
protected String getDeviceName() {
|
|
return "Speaker";
|
|
}
|
|
|
|
@Override
|
|
public String getShortName() {
|
|
return "spk";
|
|
}
|
|
|
|
@Override
|
|
public final void reconfigure() {
|
|
}
|
|
|
|
@Override
|
|
public void attach() {
|
|
configureListener();
|
|
}
|
|
|
|
@Override
|
|
public void detach() {
|
|
removeListener();
|
|
super.detach();
|
|
}
|
|
}
|