SPC player in java help?

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
SPC player in java help?
by on (#66456)
I'm trying to play spc files using java, and I stumbled across this thread:

http://nesdev.com/bbs/viewtopic.php?t=6222

In it, the user blargg posted his java music player that had the capability of playing spc files.

However I am not able to just rip out the spc playing part, can anyone help me please?

by on (#66477)
a detailed question would be much better :P

by on (#66537)
ok, can someone explain the protocol for playing spc files in java, and post the example code for playing a single spc file?

please and thanks

by on (#66556)
ok nice :)

are you developer or you do it just for fun?

I also checked the code and (sorry blargg) the code is not as easy readable for me as java developer.
You should try to analyze the project and "extract" the SPC code. If you got questions on some java related parts, i could help you but if you got questions related to the emulation-core code, you should directly ask blargg.

If i had time, i would do a little redesing of the project and would release it on sourgeforge or something else - if blargg would agree. But at the moment i am working on a browser game in java.

by on (#66595)
I'm making a mario game for fun and the original spc music files are very small so I'd like to use them.

And yeah, his code is hard to understand since it's not documented, and I can't find the spc protocol.

by on (#66614)
Look at VGMPlayer.java, and gme.java which uses it.

by on (#66628)
I still haven't understood what this thread is about. What is the drawback of using the player as it currently is?

by on (#66638)
I guess the question is: I want to use the code as my music driver for my java game but I don't know how (i.e. like using famitracker driver in a nes game).

by on (#66645)
Here's the main class:
Code:
class VGMPlayer implements Runnable
{
    // Establishes sample rate
    public VGMPlayer( int 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( String url, String path ) throws Exception
   
    // Stops and closes current file and unloads things from memory
    void closeFile() throws Exception
   
    // Number of tracks
    public int getTrackCount()
   
    // 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
   
    // Currently playing track
    public int getCurrentTrack()
   
    // Number of seconds played since last startTrack() call
    public int getCurrentTime()
   
    // 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 )
   
    // Current playback volume
    public double getVolume()
   
    // Pauses if track was playing.
    public void pause() throws Exception
   
    // True if track is currently playing
    public boolean isPlaying()
   
    // Resumes playback where it was paused
    public void play() throws Exception
   
    // Stops playback and closes audio
    public void stop() throws Exception
   
    // Called periodically when a track is playing
    protected void idle()
}

// example of use

VGMPlayer player = new VGMPlayer( 44100 );

// Open a file, or a file in a zip archive
player.loadFile( "file.spc", "" );
player.loadFile( "archive.zip", "file.spc" );

// Play for 4 minutes, then fade
player.startTrack( 0, 240 );

by on (#66971)
Can't seem to get it working.

Code:
java.lang.ArrayIndexOutOfBoundsException
        at java.lang.System.arraycopy(Native Method)
        at src.sounds.emu.SpcEmu.startTrack(SpcEmu.java:147)
        at src.sounds.EmuPlayer.startTrack(VGMPlayer.java:44)
        at src.sounds.SoundManager.play(SoundManager.java:42)
        at src.Client.<init>(Client.java:30)
        at src.Client.main(Client.java:15)



sounds.zip is zip archive holding 62 spc files. I load all of them to a byte array..

However I get an error when I call the play method.

The only file I have modified is VGMPlayer

Code:
package src.sounds;

import java.io.*;
import java.util.zip.*;
import javax.sound.sampled.*;

import src.sounds.emu.*;

public class SoundManager {


   private static final SoundManager instance = new SoundManager();

   public static final SoundManager getInstance() {
      return instance;
   }

   private SoundManager() {
      try {
         player = new VGMPlayer(44100);
         ZipInputStream in = new ZipInputStream(new FileInputStream("data/sounds.zip"));
         ZipEntry clip = null;
         int i = 0;
         byte[] buf;
         while ((clip = in.getNextEntry()) != null) {
            buf = new byte[(int) clip.getCompressedSize()];
            in.read(buf);
            sound[i++] = buf;
         }
         in.close();
      } catch (Exception e) {
         System.exit(1);
      }
   }

   private byte[][] sound = new byte[62][];
   private VGMPlayer player;

   public void play(int id, int time) {
      try {
         player.loadFile(sound[id]);
         player.startTrack(0, time);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }

}


Code:
package src.sounds;

import javax.sound.sampled.*;
import java.io.*;


import src.sounds.emu.*;

/* 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 {

   public int getTrackCount() {
      return emu.trackCount();
   }

   public void startTrack(int track, int time) throws Exception {
      pause();
      if (line != null)
         line.flush();
      emu.startTrack(track);
      emu.setFade(time, 6);
      play();
   }

   public int getCurrentTrack() {
      return emu.currentTrack();
   }

   public int getCurrentTime() {
      return (emu == null ? 0 : emu.currentTime());
   }

   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));
      }
   }

   public double getVolume() {
      return volume_;
   }

   public void pause() throws Exception {
      if (thread != null) {
         playing_ = false;
         thread.join();
         thread = null;
      }
   }

   public boolean isPlaying() {
      return playing_;
   }

   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();
   }

   public void stop() throws Exception {
      pause();
      
      if (line != null) {
         line.close();
         line = null;
      }
   }

   protected void idle() { }

   private int sampleRate = 0;
   AudioFormat audioFormat;
   DataLine.Info lineInfo;
   MusicEmu emu = new SpcEmu();
   Thread thread;
   volatile boolean playing_;
   SourceDataLine line;
   double volume_ = 1.0;
   
   public void run() {
      line.start();

      byte [] buf = new byte [66092];
      while (playing_ && !emu.trackEnded()) {
         int count = emu.play(buf, buf.length / 2);
         line.write(buf, 0, count * 2);
         idle();
      }
      playing_ = false;
      line.stop();
   }
}

class VGMPlayer extends EmuPlayer {

   int sampleRate;
   
   public VGMPlayer(int sampleRate) {
      this.sampleRate = sampleRate;
   }

   public void loadFile(byte[] data) throws Exception {
      stop();

      int actualSampleRate = emu.setSampleRate( sampleRate );
      emu.loadFile(data);
   }

   void closeFile() throws Exception {
      stop();
   }

}

by on (#66974)
Your changes removed essential things. I don't have any Java compiler handy, but try the following class in your own code (revert back to the unmodified GME sources; I don't think you need to modify them):
Code:
class SPCPlayer extends EmuPlayer
{
   public void useSpcFile( byte [] data, int sampleRate ) throws Exception {
      stop();
     
      MusicEmu emu = new SpcEmu();
      int actualSampleRate = emu.setSampleRate(sampleRate);
      emu.loadFile(data);
      setEmu(emu, actualSampleRate);
   }
   
   void closeFile() throws Exception  {
      stop();
      setEmu(null, 0);
   }
}

by on (#66998)
Hmm I changed back to original class then added your SPCPlayer class but I still get the same error.

Code:
java.lang.ArrayIndexOutOfBoundsException
        at java.lang.System.arraycopy(Native Method)
        at src.sounds.emu.SpcEmu.startTrack(SpcEmu.java:147)
        at src.sounds.EmuPlayer.startTrack(SPCPlayer.java:45)
        at src.sounds.SoundManager.play(SoundManager.java:43)
        at src.Client.<init>(Client.java:30)
        at src.Client.main(Client.java:15)

by on (#67010)
Have you verified that the data array you're passing to useSpcFile is a valid SPC file? If you're still having trouble, you can send me your full Java source and I'll figure out what's going on.

by on (#67022)
Yeah I'm pretty sure it is.

by on (#67026)
Could you check, before I spend a lot of time examining your source? Be sure the header looks right, AND please report back the file length. I'm also wondering whether the original unmodified source worked for playing a SPC. That is, where you pass it the path to the file.

by on (#67032)
HOLD ON A DAMN SECOND.

It appears I was reading them the wrong way. I set the buffer size to each spc's compressed file size instead of actual size.... :?

But now i just get 'emulation error' when I try to play a spc file.

Code:
   protected int play_( byte out [], int count )
   {
      dsp.setOutput( out );
      
      // Run for count/2*32 clocks + extra to get DSP time half-way between samples,
      // since CPU might run for slightly less than requested
      int clockCount = count * (32 / 2) + 16 - ((time - dspTime) & 31);
      time            -= clockCount;
      dspTime         -= clockCount;
      timers [0].time -= clockCount;
      timers [1].time -= clockCount;
      timers [2].time -= clockCount;
      runCpu();
      
      if ( time < 0 ) // emulation error
      {
         logError();
         return 0;
      }
      
      // Catch up to CPU
      runTimer( timers [0], time );
      runTimer( timers [1], time );
      runTimer( timers [2], time );
      
      // Run DSP to present
      int delta;
      if ( (delta = time - dspTime) >= 0 )
      {
         delta = (delta >> 5) + 1;
         dspTime += delta << 5;
         dsp.run( delta );
      }
      
      assert dsp.sampleCount() == count;
      return dsp.sampleCount();
   }


Code:
   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 );
         line.write( buf, 0, count * 2 );
         idle();
      }
      
      playing_ = false;
      line.stop();
   }

by on (#67036)
Does the original code play the same SPC file correctly? I think you should be able to give it a URL to the file, of the form file:/// or something like that.

by on (#67055)
Hmm yeah it does...

EDIT: Used your method of loading from DataReader class.

Now it works, thanks :D

Only problem is most of the time the sound messes up in the beginning. (Occasionally it doesn't)

EDIT: Sorted that out... Thanks a bunch!

by on (#67089)
Sound messes up at the beginning, I'm curious. Was it some Java sound driver issue? Java was always fairly glitchy on my machine. I ran tests where I fed it a pure sine wave, where my code was almost surely bug-free, yet I still got glitches when starting, sometimes repeated buffers etc.

by on (#67093)
I just run a sound first and cut it off before it actually makes a sound to load all the variables and instantiate classes etc...

EDIT: Using Xcomp argument to fix. So yeah, I was right, it probably just took a bit to load everything. Let me see how this works out later, might switch to Xint or some other method to fix the slow compiling.

by on (#67097)
Nifty solution! I'll have to remember that. You could set the volume to zero and load an SPC and play it for a moment. Maybe that's what you meant.

by on (#67100)
I just play it for 1 ms but now I'm gonna set the volume to 0 anyways thanks. (Assuming using arguments causes problems)

EDIT: Hmm this small fix doesn't seem to work when doing any heavy operations, not just starting up the jvm...

EDIT2: cool, found using a bigger buffer fixed problem (you set to 8192 bytes in the run method of SPCPlayer class or whatever it is called).