@@ -7,46 +7,41 @@
import java.util.* ;
import java.util.prefs.Preferences ;
/**
* Handles the acoustic functionality for the game, including music playback, sound effects,
* volume management, and transitions between game states.
*/
public class AcousticHandler {
private MdgaApp app ; // Reference to the main application
private MdgaState state = MdgaState . NONE ; // Current state of the game
private boolean playGame = false ; // Whether the game tracks are currently playing
private ArrayList < MusicAsset > gameTracks = new ArrayList < > ( ) ; // List of available game music tracks
private NanoTimer trackTimer = new NanoTimer ( ) ; // Timer for managing track transitions
private MdgaApp app ;
private boolean fading = false ; // Whether a fade transition is in progress
private NanoTimer fadeTimer = new NanoTimer ( ) ; // Timer for fade transitions
private static final float FADE_DURATION = 3 . 0f ; // Duration of fade-out
private static final float CROSSFADE_DURATION = 1 . 5f ; // Duration of fade-in
private GameMusic playing = null ; // Currently playing music track
private GameMusic scheduled = null ; // Track scheduled to play next
private GameMusic old = null ; // Track being faded out
private MdgaState state = MdgaState . NONE ;
private float mainVolume = 0 . 0f ; // Main volume level
private float m usicVolume = 1 . 0f ; // Music volume level
private float soundVolu me = 1 . 0f ; // Sound effects volume level
private boolean playGame = false ;
private ArrayList < M usicAsset > gameTracks = new ArrayList < > ( ) ;
private NanoTimer trackTi mer = new NanoTimer ( ) ;
private ArrayList < GameSound > sounds = new ArrayList < > ( ) ; // List of active sound effect s
private Preferences prefs = Preferences . userNodeForPackage ( AcousticHandler . class ) ; // User preferences for volume setting s
private boolean fading = false ; // Indicates if a fade is in progres s
private NanoTimer fadeTimer = new NanoTimer ( ) ; // Timer to track fade progres s
private static final float FADE_DURATION = 3 . 0f ; // Duration for outfade
private static final float CROSSFADE_DURATION = 1 . 5f ; // Duration for infade
private GameMusic playing = null ; // Currently playing track
private GameMusic scheduled = null ; // Scheduled track to play next
private GameMusic old = null ; // Old track being faded out
private float mainVolume = 0 . 0f ;
private float musicVolume = 1 . 0f ;
private float soundVolume = 1 . 0f ;
private ArrayList < GameSound > sounds = new ArrayList < > ( ) ;
private Preferences prefs = Preferences . userNodeForPackage ( AcousticHandler . class ) ;
/**
* Initializes the AcousticHandler with the main application and loads user volume settings.
*
* @param app The main application instance.
*/
public AcousticHandler ( MdgaApp app ) {
this . app = app ;
mainVolume = prefs . getFloat ( " mainVolume " , 1 . 0f ) ;
musicVolume = prefs . getFloat ( " musicVolume " , 1 . 0f ) ;
soundVolume = prefs . getFloat ( " soundVolume " , 1 . 0f ) ;
}
/**
* Updates the state of the A cousticHandler. S hould be called every frame.
* This method updates the a cousticHandler and s hould be called every frame
*/
public void update ( ) {
updateVolumeAndTrack ( ) ;
@@ -58,7 +53,9 @@ public void update() {
Iterator < GameSound > iterator = sounds . iterator ( ) ;
while ( iterator . hasNext ( ) ) {
GameSound s = iterator . next ( ) ;
s . update ( getSoundVolumeTotal ( ) ) ;
if ( ! s . isPlaying ( ) ) {
iterator . remove ( ) ;
}
@@ -66,76 +63,314 @@ public void update() {
}
/**
* Plays a sound effect immediately.
* This method instantly plays a sound
*
* @param sound T he sound effect to play.
* @param sound t he sound to be played
*/
public void playSound ( MdgaSound sound ) {
// Implementation for playing predefined sound effects based on game events
ArrayList < SoundAssetDelayVolume > assets = new ArrayList < SoundAssetDelayVolume > ( ) ;
switch ( sound ) {
case LOST :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . LOST , 1 . 0f , 0 . 0f ) ) ;
break ;
case VICTORY :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . VICTORY , 1 . 0f , 0 . 0f ) ) ;
break ;
case BUTTON_PRESSED :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . BUTTON_PRESS , 0 . 7f , 0 . 0f ) ) ;
break ;
case WRONG_INPUT :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . ERROR , 1 . 0f , 0 . 0f ) ) ;
break ;
case UI_CLICK :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . UI_CLICK , 0 . 8f , 0 . 0f ) ) ;
break ;
case START :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . START , 0 . 8f , 0 . 5f ) ) ;
break ;
case THROW :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . LAUGHT , 1 . 0f , 0 . 2f ) ) ;
break ;
case POWERUP :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . POWERUP , 1 . 0f , 0 . 2f ) ) ;
break ;
case SELF_READY :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . ROBOT_READY , 1 . 0f , 0 . 0f ) ) ;
break ;
case OTHER_READY :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . UNIT_READY , 1 . 0f , 0 . 0f ) ) ;
break ;
case OTHER_CONNECTED :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . CONNECTED , 1 . 0f , 0 . 0f ) ) ;
break ;
case NOT_READY :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . UI_SOUND , 1 . 0f , 0 . 0f ) ) ;
break ;
case LEAVE :
assets . add ( new SoundAssetDelayVolume ( SoundAsset . UI_SOUND2 , 0 . 6f , 0 . 0f ) ) ;
break ;
default :
break ;
}
for ( SoundAssetDelayVolume sawd : assets ) {
GameSound gameSound = new GameSound ( app , sawd . asset ( ) , getSoundVolumeTotal ( ) , sawd . subVolume ( ) , sawd . delay ( ) ) ;
sounds . add ( gameSound ) ;
}
}
/**
* Transitions music playback to match the specified gam e state.
* This method fades the played music to fit th e state.
*
* @param state T he new game state.
* @param state t he state of which the corresponding music should be played to be played
*/
public void playState ( MdgaState state ) {
// Implementation for managing state-specific music playback
if ( this . state = = state ) {
return ;
}
MusicAsset asset = null ;
switch ( state ) {
case MAIN :
playGame = false ;
asset = MusicAsset . MAIN_MENU ;
break ;
case LOBBY :
playGame = false ;
asset = MusicAsset . LOBBY ;
break ;
case GAME :
addGameTracks ( ) ;
playGame = true ;
assert ( ! gameTracks . isEmpty ( ) ) : " no more game music available " ;
asset = gameTracks . remove ( 0 ) ;
break ;
case CEREMONY :
playGame = false ;
asset = MusicAsset . CEREMONY ;
break ;
case NONE :
throw new RuntimeException ( " no music for state NONE " ) ;
}
assert ( null ! = asset ) : " music sceduling went wrong " ;
scheduled = new GameMusic ( app , asset , getMusicVolumeTotal ( ) , asset . getSubVolume ( ) , asset . getLoop ( ) , 0 . 0f ) ;
}
/**
* Performs linear interpolation between two values.
* Performs linear interpolation between two float values.
*
* @param start The starting value.
* @param end The ending value.
* @param t The interpolation factor (0 to 1) .
* @return The interpolated value.
* @param t The interpolation factor, typically between 0 and 1.
* @return The interpolated value between start and end .
*/
private float lerp ( float start , float end , float t ) {
return start + t * ( end - start ) ;
}
/**
* Manages audio playb ack transitions and volume adjustments.
* Updates the state of audio playback, handling tr ack transitions and volume adjustments.
*
* This method ensures smooth transitions between tracks using fade-in and fade-out effects.
* It also handles cases where no track is playing, starting a scheduled track immediately at full volume.
* The method prioritizes the latest scheduled track if multiple scheduling occurs quickly.
*
* Behavior:
* 1. If nothing is scheduled and no track is playing, it exits early.
* 2. If a scheduled track exists and no track is playing, the scheduled track starts immediately at full volume.
* 3. If a scheduled track exists while a track is playing, it initiates a fade-out for the currently playing track
* and prepares for the new track to fade in.
* 4. If a track transition is in progress (fading), it processes the fade-out and fade-in states.
* If a new track is scheduled during this process, it interrupts the current transition and prioritizes the new track.
* 5. If no fading is needed and a track is playing, it ensures the track's volume is updated.
*
* Special cases:
* - If no track is playing and a new track is scheduled, it starts the track immediately without fading.
* - If a new track is scheduled during fading, it resets the transition to prioritize the new track.
*/
private void updateVolumeAndTrack ( ) {
// Implementation for handling fade-ins, fade-outs, and volume updates
if ( scheduled = = null & & ! fading & & playing = = null ) {
// Nothing to do, early exit
return ;
}
if ( scheduled ! = null & & playing = = null & & ! fading ) {
// No current track, start scheduled track immediately at full volume
playing = scheduled ;
scheduled = null ;
playing . play ( ) ;
playing . update ( getMusicVolumeTotal ( ) ) ; // Set volume to full
return ;
}
if ( scheduled ! = null & & ! fading ) {
// Initiate a fade process if a new track is scheduled
fading = true ;
fadeTimer . reset ( ) ;
old = playing ; // The currently playing track becomes the old track
playing = null ; // Clear the playing track during the fade process
}
if ( fading ) {
handleFadeProcess ( ) ;
// Handle any interruptions due to newly scheduled tracks
if ( scheduled ! = null & & playing ! = null & & playing ! = scheduled ) {
// Interrupt the current infade and switch to the new scheduled track
old = playing ; // Treat the currently infading track as the old track
playing = null ; // Reset playing to allow switching
fadeTimer . reset ( ) ; // Restart fade timer for the new track
}
} else if ( playing ! = null ) {
// Update volume for the currently playing track
playing . update ( getMusicVolumeTotal ( ) ) ;
} else if ( scheduled ! = null ) {
// If no track is playing and one is scheduled, start it immediately at full volume
playing = scheduled ;
scheduled = null ;
playing . play ( ) ;
playing . update ( getMusicVolumeTotal ( ) ) ; // Set volume to full
}
}
/**
* H andl es the fade-out and fade- in processes for audio transitions.
* M anag es the fading process during audio track transitions.
*
* This method handles the fade-out of the currently playing (old) track, manages any pause between the fade-out
* and fade-in, and initiates the fade-in for the new track if applicable. It ensures smooth transitions between
* tracks while maintaining the correct volume adjustments.
*
* Behavior:
* 1. **Outfade:** Gradually decreases the volume of the `old` track over the duration of `FADE_DURATION`.
* Once the outfade completes, the `old` track is paused and cleared.
* 2. **Pause Handling:** Waits for a defined pause (if applicable) before initiating the infade for the next track.
* 3. **Infade:** If a `scheduled` track exists and the outfade and pause are complete, it begins playing
* the new track (`playing`) and initiates the infade process.
*
* Key Details:
* - The outfade volume adjustment is interpolated linearly from full volume to zero using the `lerp` function.
* - The pause duration is retrieved from the scheduled track if it is specified.
* - If a new track is scheduled during the fade process, it is handled by external logic to prioritize transitions.
*
* Preconditions:
* - `fading` is expected to be `true` when this method is called.
* - The method is invoked as part of the `updateVolumeAndTrack` process.
*/
private void handleFadeProcess ( ) {
// Implementation for managing fade transitions
float time = fadeTimer . getTimeInSeconds ( ) ;
// Handle outfade for the old track
if ( old ! = null & & time < = FADE_DURATION ) {
float t = Math . min ( time / FADE_DURATION , 1 . 0f ) ;
float oldVolume = lerp ( 1 . 0f , 0 . 0f , t ) ;
old . update ( getMusicVolumeTotal ( ) * oldVolume ) ;
}
if ( old ! = null & & time > FADE_DURATION ) {
// Complete outfade
old . pause ( ) ;
old = null ;
}
// Handle pause duration before infade
float pause = ( scheduled ! = null ) ? scheduled . getPause ( ) : 0 . 0f ;
if ( time > FADE_DURATION + pause ) {
if ( playing = = null & & scheduled ! = null ) {
// Begin infade for the new track
playing = scheduled ;
scheduled = null ;
playing . play ( ) ; // Start playing the new track
}
handleInfade ( time - FADE_DURATION - pause ) ;
}
}
/**
* Manages the fade-in process for the currently playing track.
*
* @param infadeTime Time elapsed since the fade-in process started.
* This method gradually increases the volume of the `playing` track from zero to full volume
* over the duration of `CROSSFADE_DURATION`. It ensures a smooth transition into the new track.
*
* Behavior:
* 1. If no track is set as `playing`, the method exits early, as there is nothing to fade in.
* 2. Linearly interpolates the volume of the `playing` track from 0.0 to 1.0 based on the elapsed
* `infadeTime` and the specified `CROSSFADE_DURATION`.
* 3. Once the fade-in is complete (when `infadeTime` exceeds `CROSSFADE_DURATION`), the method:
* - Marks the fade process (`fading`) as complete.
* - Ensures the `playing` track is updated to its full volume.
*
* Key Details:
* - Uses the `lerp` function to calculate the volume level for the `playing` track during the fade-in.
* - Ensures the volume is always a value between 0.0 and 1.0.
* - The `infadeTime` parameter should be relative to the start of the fade-in process.
*
* Preconditions:
* - The `playing` track must be initialized and actively fading in for this method to have an effect.
* - The method is invoked as part of the `updateVolumeAndTrack` process.
*
* @param infadeTime The elapsed time (in seconds) since the fade-in process started.
*/
private void handleInfade ( float infadeTime ) {
// Implementation for handling the fade-in process
if ( playing = = null ) {
// Nothing to infade
return ;
}
// Proceed with the infade for the current playing track
float t = Math . min ( infadeTime / CROSSFADE_DURATION , 1 . 0f ) ;
float newVolume = lerp ( 0 . 0f , 1 . 0f , t ) ;
playing . update ( getMusicVolumeTotal ( ) * newVolume ) ;
if ( infadeTime > CROSSFADE_DURATION ) {
// Infade is complete, finalize state
fading = false ;
playing . update ( getMusicVolumeTotal ( ) ) ; // Ensure full volume
}
}
/**
* Adds and shuffles game music tracks .
* Adds a list of game tracks to the gameTracks collection and shuffles them .
* This method adds predefined game tracks to the track list and shuffles the order.
*/
private void addGameTracks ( ) {
// Adds predefined game music tracks to the list and shuffles them
Random random = new Random ( ) ;
for ( int i = 1 ; i < = 6 ; i + + ) {
gameTracks . add ( MusicAsset . valueOf ( " GAME_ " + i ) ) ;
}
Collections . shuffle ( gameTracks , random ) ;
}
/**
* Updates the game music tracks, scheduling new ones as needed.
* Updates the current game tracks. If the currently playing track is nearing its end,
* a new track will be scheduled to play. If the list of game tracks is empty, it will be refreshed.
*/
private void updateGameTracks ( ) {
// Handles scheduling and transitioning between game tracks
if ( null = = playing ) {
return ;
}
if ( playing . nearEnd ( 10 ) ) {
if ( gameTracks . isEmpty ( ) ) {
addGameTracks ( ) ;
}
}
if ( playing ! = null & & playing . nearEnd ( 3 ) & & trackTimer . getTimeInSeconds ( ) > 20 ) {
trackTimer . reset ( ) ;
MusicAsset nextTrack = gameTracks . remove ( 0 ) ;
scheduled = new GameMusic ( app , nextTrack , getMusicVolumeTotal ( ) , nextTrack . getSubVolume ( ) , nextTrack . getLoop ( ) , 0 . 0f ) ;
}
}
/**
* G ets the main volume level.
* R etrieve s the main volume level.
*
* @return The main volume level.
* @return The current main volume level.
*/
public float getMainVolume ( ) {
return mainVolume ;
@@ -160,7 +395,7 @@ public float getSoundVolume() {
}
/**
* Sets the main volume level and saves the setting .
* Sets the main volume level.
*
* @param mainVolume The desired main volume level.
*/
@@ -169,27 +404,6 @@ public void setMainVolume(float mainVolume) {
prefs . putFloat ( " mainVolume " , mainVolume ) ;
}
// Similar getters and setters for musicVolume and soundVolume...
/**
* Calculates the total music volume (main volume × music volume).
*
* @return The total music volume.
*/
float getMusicVolumeTotal ( ) {
return musicVolume * mainVolume ;
}
/**
* Calculates the total sound volume (main volume × sound volume).
*
* @return The total sound volume.
*/
float getSoundVolumeTotal ( ) {
return soundVolume * mainVolume ;
}
/**
* Sets the music volume level.
*
@@ -209,4 +423,23 @@ public void setSoundVolume(float soundVolume) {
this . soundVolume = soundVolume ;
prefs . putFloat ( " soundVolume " , soundVolume ) ;
}
/**
* Calculates the total music volume by multiplying the music volume by the main volume.
*
* @return The total music volume.
*/
float getMusicVolumeTotal ( ) {
return getMusicVolume ( ) * getMainVolume ( ) ;
}
/**
* Calculates the total sound volume by multiplying the sound volume by the main volume.
*
* @return The total sound volume.
*/
float getSoundVolumeTotal ( ) {
return getSoundVolume ( ) * getMainVolume ( ) ;
}
}