package audio.gme;
// Video game music player that runs emulator and plays through speaker
// http://www.slack.net/~ant/
/* Load a music file into player, then start a track. Volume can be
adjusted, track can be paused and resumed, a new track can be started,
or a new file can be loaded at any time.
The file is specified as an HTTP address and optional filename to use
if it's a ZIP archive. To avoid loading file more than necessary over
HTTP, the most recently loaded file is kept in memory and a load request
for the same URL is eliminated. This allows a web page to switch between
several tracks in a ZIP archive or of a multi-track music file, without
having to keep track of whether the file was already loaded. */
import javax.sound.sampled.*;
import java.io.*;
import java.net.URL;
/* Copyright (C) 2007-2008 Shay Green. This module 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
module 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 module; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */
class EmuPlayer implements Runnable
{
// Number of tracks
public int getTrackCount() { return emu.trackCount(); }
// Starts new track playing, where 0 is the first track.
// After time seconds, the track starts fading.
public void startTrack( int track, int time ) throws Exception
{
pause();
if ( line != null )
line.flush();
emu.startTrack( track );
emu.setFade( time, 6 );
play();
}
// Currently playing track
public int getCurrentTrack() { return emu.currentTrack(); }
// Number of seconds played since last startTrack() call
public int getCurrentTime() { return (emu == null ? 0 : emu.currentTime()); }
// Sets playback volume, where 1.0 is normal, 2.0 is twice as loud.
// Can be changed while track is playing.
public void setVolume( double v )
{
volume_ = v;
if ( line != null )
{
FloatControl mg = (FloatControl) line.getControl( FloatControl.Type.MASTER_GAIN );
if ( mg != null )
mg.setValue( (float) (Math.log( v ) / Math.log( 10.0 ) * 20.0) );
}
}
// Current playback volume
public double getVolume() { return volume_; }
// Pauses if track was playing.
public void pause() throws Exception
{
if ( thread != null )
{
playing_ = false;
thread.join();
thread = null;
}
}
// True if track is currently playing
public boolean isPlaying() { return playing_; }
// Resumes playback where it was paused
public void play() throws Exception
{
if ( line == null )
{
line = (SourceDataLine) AudioSystem.getLine( lineInfo );
line.open( audioFormat );
setVolume( volume_ );
}
thread = new Thread( this );
playing_ = true;
thread.start();
}
// Stops playback and closes audio
public void stop() throws Exception
{
//pause(); Commented by Rafael: was causing the code to freeze
if ( line != null )
{
line.close();
line = null;
}
}
// Called periodically when a track is playing
protected void idle() { }
// private
// Sets music emulator to get samples from
void setEmu( MusicEmu emu, int sampleRate ) throws Exception
{
stop();
this.emu = emu;
if ( emu != null && line == null && this.sampleRate != sampleRate )
{
audioFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
sampleRate, 16, 2, 4, sampleRate, true );
lineInfo = new DataLine.Info( SourceDataLine.class, audioFormat );
this.sampleRate = sampleRate;
}
}
private int sampleRate = 0;
AudioFormat audioFormat;
DataLine.Info lineInfo;
MusicEmu emu;
Thread thread;
volatile boolean playing_;
SourceDataLine line;
double volume_ = 1.0;
public void run()
{
line.start();
// play track until stop signal
byte [] buf = new byte [8192];
while ( playing_ && !emu.trackEnded() )
{
int count = emu.play( buf, buf.length / 2 );
if(line!= null) // Rafael
line.write( buf, 0, count * 2 );
else
break;
idle();
}
playing_ = false;
if(line!=null)
line.stop();
}
}
class VGMPlayer extends EmuPlayer
{
int sampleRate;
public VGMPlayer( int sampleRate ) { this.sampleRate = sampleRate; }
// Stops playback and loads file from given URL (HTTP only).
// If it's an archive (.zip) then path specifies the file within
// the archive.
public void loadFile( URL url, String path ) throws Exception
{
stop();
if (loadedUrl==null || !loadedUrl.equals( url ) || !loadedPath.equals( path ) )
{
byte [] data = readFile( url, path );
String name = url.getFile().toUpperCase();
if ( name.endsWith( ".ZIP" ) )
name = path.toUpperCase();
if ( name.endsWith( ".GZ" ) )
name = name.substring( 0, name.length() - 3 );
MusicEmu emu = createEmu( name );
if ( emu == null )
return; // TODO: throw exception?
int actualSampleRate = emu.setSampleRate( sampleRate );
emu.loadFile( data );
// now that new emulator is ready, replace old one
setEmu( emu, actualSampleRate );
loadedUrl = url;
loadedPath = path;
}
}
// Stops and closes current file and unloads things from memory
void closeFile() throws Exception
{
stop();
setEmu( null, 0 );
archiveUrl = null;
archiveData = null;
loadedUrl = null;
loadedPath = "";
}
// private
URL loadedUrl = null; // URL and path of file loaded into emulator
String loadedPath = "";
URL archiveUrl = null; // URL of (ZIP) file cached in archiveData
byte [] archiveData;
// Creates appropriate emulator for given filename
MusicEmu createEmu( String name )
{
if ( name.endsWith( ".VGM" ) || name.endsWith( ".VGZ" ) )
return new VgmEmu();
if ( name.endsWith( ".GBS" ) )
return new GbsEmu();
if ( name.endsWith( ".NSF" ) )
return new NsfEmu();
if ( name.endsWith( ".SPC" ) )
return new SpcEmu();
return null;
}
// Loads given URL and file within archive, and caches archive for future access
byte [] readFile( URL url, String path ) throws Exception
{
InputStream in = null;
String name = url.getFile().toUpperCase();
if ( !name.endsWith( ".ZIP" ) )
{
archiveData = null; // dump previously cached ZIP file
archiveUrl = null;
in = DataReader.openHttp( url );
//System.out.println( "Load " + url );
}
else
{
if ( !archiveUrl.equals( url ) )
{
archiveData = DataReader.loadData( DataReader.openHttp( url ) );
archiveUrl = url;
//System.out.println( "Load " + url );
}
in = DataReader.openZip( new ByteArrayInputStream( archiveData ), path );
name = path.toUpperCase();
//System.out.println( "Unzip " + url );
}
if ( name.endsWith( ".GZ" ) || name.endsWith( ".VGZ" ) )
in = DataReader.openGZIP( in );
return DataReader.loadData( in );
}
}