2024-02-21 16:52:06 +00:00
/ * *
* 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 .
* * /
2017-12-28 16:40:15 +00:00
package jace.apple2e ;
2023-07-03 20:44:23 +00:00
import java.io.File ;
import java.io.FileNotFoundException ;
import java.io.FileOutputStream ;
import java.io.IOException ;
import java.io.OutputStream ;
2023-10-23 05:28:55 +00:00
import java.util.concurrent.ExecutionException ;
2017-12-28 16:40:15 +00:00
import java.util.logging.Level ;
import java.util.logging.Logger ;
2023-07-03 20:44:23 +00:00
import jace.Emulator ;
import jace.LawlessLegends ;
import jace.config.ConfigurableField ;
import jace.config.InvokableAction ;
2024-02-21 16:52:06 +00:00
import jace.core.Device ;
2023-07-03 20:44:23 +00:00
import jace.core.RAMEvent ;
import jace.core.RAMListener ;
import jace.core.SoundMixer ;
2023-10-23 05:28:55 +00:00
import jace.core.SoundMixer.SoundBuffer ;
2024-02-10 05:50:53 +00:00
import jace.core.SoundMixer.SoundError ;
2024-02-21 16:52:06 +00:00
import jace.core.TimedDevice ;
2024-02-10 05:50:53 +00:00
import jace.core.Utility ;
2023-07-03 20:44:23 +00:00
import javafx.stage.FileChooser ;
2017-12-28 16:40:15 +00:00
/ * *
* Apple // Speaker Emulation Created on May 9, 2007, 9:55 PM
*
* @author Brendan Robert ( BLuRry ) brendan . robert @gmail.com
* /
2024-02-21 16:52:06 +00:00
public class Speaker extends Device {
2017-12-28 16:40:15 +00:00
static boolean fileOutputActive = false ;
static OutputStream out ;
2023-07-03 20:44:23 +00:00
@ConfigurableField ( category = " sound " , name = " 1mhz timing " , description = " Force speaker output to 1mhz? " )
public static boolean force1mhz = true ;
2017-12-28 16:40:15 +00:00
2024-04-23 15:08:38 +00:00
@ConfigurableField ( category = " sound " , name = " Show sound " , description = " Use black color value to show sound output " )
public static boolean showSound = false ;
2023-07-03 20:44:23 +00:00
@InvokableAction ( category = " sound " , name = " Record sound " , description = " Toggles recording (saving) sound output to a file " , defaultKeyMapping = " ctrl+shift+w " )
2017-12-28 16:40:15 +00:00
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 ( ) ;
2017-12-29 07:50:53 +00:00
File f = fileChooser . showSaveDialog ( LawlessLegends . getApplication ( ) . primaryStage ) ;
2017-12-28 16:40:15 +00:00
if ( f = = null ) {
return ;
}
try {
out = new FileOutputStream ( f ) ;
fileOutputActive = true ;
} catch ( FileNotFoundException ex ) {
Logger . getLogger ( Speaker . class . getName ( ) ) . log ( Level . SEVERE , null , ex ) ;
}
}
}
2023-07-03 20:44:23 +00:00
2017-12-28 16:40:15 +00:00
/ * *
* 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 " )
2024-03-05 06:06:47 +00:00
public static int VOLUME = 400 ;
2024-04-23 15:08:38 +00:00
private int currentVolume = 0 ;
private int fadeOffAmount = 1 ;
2017-12-28 16:40:15 +00:00
/ * *
* Manifestation of the apple speaker softswitch
* /
private boolean speakerBit = false ;
2024-04-23 15:08:38 +00:00
private static double TICKS_PER_SAMPLE = ( ( double ) TimedDevice . NTSC_1MHZ ) / SoundMixer . RATE ;
2017-12-28 16:40:15 +00:00
private RAMListener listener = null ;
2023-10-23 05:28:55 +00:00
private SoundBuffer buffer = null ;
2017-12-28 16:40:15 +00:00
2024-04-23 15:08:38 +00:00
/ * *
* 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 ;
2017-12-28 16:40:15 +00:00
/ * *
* Suspend playback of sound
*
* @return
* /
@Override
public boolean suspend ( ) {
boolean result = super . suspend ( ) ;
speakerBit = false ;
2023-10-23 05:28:55 +00:00
if ( buffer ! = null ) {
try {
buffer . shutdown ( ) ;
2024-02-10 05:50:53 +00:00
} catch ( InterruptedException | ExecutionException | SoundError e ) {
2023-10-23 05:28:55 +00:00
// Ignore
2024-02-21 16:52:06 +00:00
} finally {
buffer = null ;
2023-10-23 05:28:55 +00:00
}
2020-01-02 00:56:30 +00:00
}
2024-04-23 15:08:38 +00:00
Emulator . withComputer ( c - > c . getMotherboard ( ) . cancelSpeedRequest ( this ) ) ;
2017-12-28 16:40:15 +00:00
return result ;
}
/ * *
* Start or resume playback of sound
* /
@Override
public void resume ( ) {
2024-02-10 05:50:53 +00:00
if ( Utility . isHeadlessMode ( ) ) {
return ;
}
2024-02-21 16:52:06 +00:00
if ( buffer = = null | | ! buffer . isAlive ( ) ) {
try {
buffer = SoundMixer . createBuffer ( false ) ;
} catch ( InterruptedException | ExecutionException | SoundError e ) {
e . printStackTrace ( ) ;
detach ( ) ;
return ;
2024-02-10 05:50:53 +00:00
}
2023-10-23 05:28:55 +00:00
}
if ( buffer ! = null ) {
counter = 0 ;
idleCycles = 0 ;
level = 0 ;
} else {
Logger . getLogger ( getClass ( ) . getName ( ) ) . severe ( " Unable to get audio buffer for speaker! " ) ;
detach ( ) ;
return ;
2017-12-28 16:40:15 +00:00
}
2023-07-03 20:44:23 +00:00
if ( force1mhz ) {
2024-02-21 16:52:06 +00:00
TICKS_PER_SAMPLE = ( ( double ) TimedDevice . NTSC_1MHZ ) / SoundMixer . RATE ;
2023-07-03 20:44:23 +00:00
} else {
TICKS_PER_SAMPLE = Emulator . withComputer ( c - > ( ( double ) c . getMotherboard ( ) . getSpeedInHz ( ) ) / SoundMixer . RATE , 0 . 0 ) ;
}
2024-03-05 06:06:47 +00:00
super . resume ( ) ;
2017-12-28 16:40:15 +00:00
}
/ * *
* Reset idle counter whenever sound playback occurs
* /
public void resetIdle ( ) {
2024-04-23 15:08:38 +00:00
currentVolume = VOLUME ;
2017-12-28 16:40:15 +00:00
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 + + ;
2024-04-23 15:08:38 +00:00
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 ) ;
}
2017-12-28 16:40:15 +00:00
}
counter + = 1 . 0d ;
if ( counter > = TICKS_PER_SAMPLE ) {
2024-04-23 15:08:38 +00:00
if ( idleCycles > = MAX_IDLE_CYCLES ) {
currentVolume - = fadeOffAmount ;
}
playSample ( level * currentVolume ) ;
// Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
2017-12-28 16:40:15 +00:00
// Set level back to 0
level = 0 ;
// Set counter to 0
2024-04-23 15:08:38 +00:00
counter - = TICKS_PER_SAMPLE ;
2017-12-28 16:40:15 +00:00
}
}
private void toggleSpeaker ( RAMEvent e ) {
2023-10-23 05:28:55 +00:00
// if (e.getType() == RAMEvent.TYPE.WRITE) {
// level += 2;
// }
2023-07-03 20:44:23 +00:00
speakerBit = ! speakerBit ;
2017-12-28 16:40:15 +00:00
resetIdle ( ) ;
}
2023-07-03 20:44:23 +00:00
private void playSample ( int sample ) {
2023-10-23 05:28:55 +00:00
try {
2024-02-10 05:50:53 +00:00
if ( buffer = = null | | ! buffer . isAlive ( ) ) {
2024-02-21 16:52:06 +00:00
// Logger.getLogger(getClass().getName()).severe("Audio buffer not initalized properly!");
2024-02-10 05:50:53 +00:00
buffer = SoundMixer . createBuffer ( false ) ;
if ( buffer = = null ) {
System . err . println ( " Unable to create emergency audio buffer, detaching speaker " ) ;
detach ( ) ;
return ;
}
}
2023-10-23 05:28:55 +00:00
buffer . playSample ( ( short ) sample ) ;
} catch ( InterruptedException | ExecutionException e ) {
e . printStackTrace ( ) ;
2024-02-10 05:50:53 +00:00
} catch ( SoundError e ) {
System . err . println ( " Sound error, detaching speaker: " + e . getMessage ( ) ) ;
e . printStackTrace ( ) ;
detach ( ) ;
buffer = null ;
2023-07-03 20:44:23 +00:00
}
if ( fileOutputActive ) {
2023-10-23 05:28:55 +00:00
byte [ ] bytes = new byte [ 2 ] ;
bytes [ 0 ] = ( byte ) ( sample & 0x0ff ) ;
bytes [ 1 ] = ( byte ) ( ( sample > > 8 ) & 0x0ff ) ;
2023-07-03 20:44:23 +00:00
try {
2023-10-23 05:28:55 +00:00
out . write ( bytes , 0 , 2 ) ;
2023-07-03 20:44:23 +00:00
} catch ( IOException ex ) {
Logger . getLogger ( getClass ( ) . getName ( ) ) . log ( Level . SEVERE , " Error recording sound " , ex ) ;
toggleFileOutput ( ) ;
}
}
}
2017-12-28 16:40:15 +00:00
/ * *
* Add a memory event listener for C03x for capturing speaker events
* /
private void configureListener ( ) {
2024-02-10 05:50:53 +00:00
listener = Emulator . withMemory ( m - > m . observe ( " Speaker " , RAMEvent . TYPE . ANY , 0x0c030 , 0x0c03f , this : : toggleSpeaker ) , null ) ;
2017-12-28 16:40:15 +00:00
}
private void removeListener ( ) {
2024-02-10 05:50:53 +00:00
Emulator . withMemory ( m - > m . removeListener ( listener ) ) ;
2017-12-28 16:40:15 +00:00
}
/ * *
* 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 ( ) ;
}
}