This week I was trying to figure out how to play sounds with Java built-in sound API. After 2 days of work I got "something" working - I can easily play/loop/pause/stop MIDIs, simple sounds and streamed audio.
But now I need to change pitch of a sound for my game project. Basically, I need to use 1x-2x pitch. I made the pitch working by changing the sampling rate of a Line. But because of the Java's sampling rate 48kHz limit... I can't set the pitch for my 44.1kHz sounds to a higher value than about 1.08x.
And I thought about something. What if I would just skip some bytes, and don't change the sampling rate any higher than 48kHz (so, if I want to set it higher, I would just set it to 48kHz and skip some bytes) ? The problem is - I can't think of a way to do that properly.
My current code:
package pl.shockah.audio; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Mixer; public abstract class Audio { protected static final Mixer mixer; static { mixer = AudioSystem.getMixer(null); } public abstract void play(); public abstract void loop(); public abstract void pause(); public abstract void stop(); public abstract boolean isPlaying(); public abstract boolean isPaused(); public abstract boolean isStopped(); public abstract boolean isLooping(); }package pl.shockah.audio; public abstract class AudioWave extends Audio { public abstract float getPitch(); public abstract void setPitch(float pitch); }
package pl.shockah.audio; import java.io.File; import java.io.InputStream; import java.net.URL; import javax.sound.midi.ControllerEventListener; import javax.sound.midi.MidiSystem; import javax.sound.midi.Sequencer; import javax.sound.midi.ShortMessage; public class MIDI extends Audio implements ControllerEventListener { protected static int[] controllers; static { controllers = new int[128]; for (int i = 0; i < controllers.length; i++) controllers[i] = i; } protected Sequencer sequencer; protected boolean paused = false, looping = false; protected float factor = 1; public MIDI(File file) { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); sequencer.setSequence(MidiSystem.getSequence(file)); sequencer.addControllerEventListener(this,controllers); } catch (Exception e) {e.printStackTrace();} } public MIDI(URL url) { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); sequencer.setSequence(MidiSystem.getSequence(url)); } catch (Exception e) {e.printStackTrace();} } public MIDI(InputStream is) { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); sequencer.setSequence(MidiSystem.getSequence(is)); } catch (Exception e) {e.printStackTrace();} } public void play() {play(false);} public void loop() {play(true);} public void play(boolean loop) { if (!isPaused()) stop(); looping = loop; paused = false; sequencer.setLoopCount(looping ? Sequencer.LOOP_CONTINUOUSLY : 0); sequencer.start(); } public void pause() { paused = true; sequencer.stop(); } public void stop() { paused = false; looping = false; sequencer.stop(); sequencer.setTickPosition(0); } public boolean isPlaying() { return sequencer.isRunning(); } public boolean isPaused() { return paused; } public boolean isStopped() { return !isPaused() && !isPlaying(); } public boolean isLooping() { return looping; } public float getTempoFactor() { return factor; } public void setTempoFactor(float factor) { this.factor = factor; sequencer.setTempoFactor(factor); } public void controlChange(ShortMessage shortMsg) { if (shortMsg.getCommand() == ShortMessage.START) paused = false; } }
package pl.shockah.audio; import java.io.File; import java.io.InputStream; import java.net.URL; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; public class Sample extends AudioWave implements LineListener { protected Clip clip; protected boolean paused = false, looping = false; protected float frequency, pitch = 1; public Sample(File file) { try { clip = AudioSystem.getClip(mixer.getMixerInfo()); clip.open(AudioSystem.getAudioInputStream(file)); frequency = clip.getFormat().getSampleRate(); clip.addLineListener(this); } catch (Exception e) {e.printStackTrace();} } public Sample(URL url) { try { clip = AudioSystem.getClip(mixer.getMixerInfo()); clip.open(AudioSystem.getAudioInputStream(url)); frequency = clip.getFormat().getSampleRate(); clip.addLineListener(this); } catch (Exception e) {e.printStackTrace();} } public Sample(InputStream is) { try { clip = AudioSystem.getClip(mixer.getMixerInfo()); clip.open(AudioSystem.getAudioInputStream(is)); frequency = clip.getFormat().getSampleRate(); clip.addLineListener(this); } catch (Exception e) {e.printStackTrace();} } public Sample(AudioInputStream ais) { try { clip = AudioSystem.getClip(mixer.getMixerInfo()); clip.open(ais); frequency = clip.getFormat().getSampleRate(); clip.addLineListener(this); } catch (Exception e) {e.printStackTrace();} } public void play() {play(false);} public void loop() {play(true);} public void play(boolean loop) { looping = loop; if (!isPaused()) stop(); paused = false; if (loop) clip.loop(Clip.LOOP_CONTINUOUSLY); else clip.start(); } public void pause() { paused = true; clip.stop(); } public void stop() { paused = false; clip.stop(); clip.setFramePosition(0); } public boolean isPlaying() { return clip.isRunning(); } public boolean isPaused() { return paused; } public boolean isStopped() { return !isPaused() && !isPlaying(); } public boolean isLooping() { return looping; } public float getPitch() { return pitch; } public void setPitch(float pitch) { this.pitch = pitch; ((FloatControl)clip.getControl(FloatControl.Type.SAMPLE_RATE)).setValue(frequency*pitch); } public void update(LineEvent event) { if (event.getType() == LineEvent.Type.START) paused = false; } }package pl.shockah.audio; import java.io.File; import java.net.URL; import java.util.ArrayList; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; public class SampleMulti extends AudioWave implements LineListener { protected ArrayList<Sample> samples = new ArrayList<Sample>(); protected AudioInputStream ais; public SampleMulti(File file) { try { ais = AudioSystem.getAudioInputStream(file); } catch (Exception e) {e.printStackTrace();} } public SampleMulti(URL url) { try { ais = AudioSystem.getAudioInputStream(url); } catch (Exception e) {e.printStackTrace();} } public ArrayList<Sample> getSamples() { return new ArrayList<Sample>(samples); } public void play() { Sample sample = new Sample(ais); sample.clip.addLineListener(this); samples.add(sample); sample.play(); } public void loop() { Sample sample = new Sample(ais); sample.clip.addLineListener(this); samples.add(sample); sample.loop(); } public void pause() { for (Sample sample : samples) sample.pause(); } public void stop() { for (Sample sample : samples) { sample.stop(); sample.clip.removeLineListener(this); } samples.clear(); } public boolean isPlaying() { for (Sample sample : samples) if (sample.isPlaying()) return true; return false; } public boolean isPaused() { for (Sample sample : samples) if (!sample.isPaused()) return false; return true; } public boolean isStopped() { for (Sample sample : samples) if (!sample.isStopped()) return false; return true; } public boolean isLooping() { for (Sample sample : samples) if (sample.isLooping()) return true; return false; } public float getPitch() { float p = 0; int n = 0; for (Sample sample : samples) {p += sample.getPitch(); n++;} if (n == 0) return 1; return p/((float)n); } public void setPitch(float pitch) { for (Sample sample : samples) sample.setPitch(pitch); } public void update(LineEvent event) { if (event.getType() == LineEvent.Type.STOP) { for (int i = 0; i < samples.size(); i++) if (event.getSource() == samples.get(i)) { if (!samples.get(i).isPaused()) { samples.get(i).clip.removeLineListener(this); samples.remove(i); return; } } } } }
package pl.shockah.audio; import java.io.File; import java.io.InputStream; import java.net.URL; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; import javax.sound.sampled.SourceDataLine; public class Stream extends AudioWave implements LineListener { protected SourceDataLine sdl; protected AudioInputStream ais; protected boolean paused = false; protected float frequency, pitch = 1; protected ThreadStream thread = null; protected File ctrFile = null; protected URL ctrURL = null; public Stream(File file) { try { ctrFile = file; } catch (Exception e) {e.printStackTrace();} } public Stream(URL url) { try { ctrURL = url; } catch (Exception e) {e.printStackTrace();} } public Stream(InputStream is) { try { ais = AudioSystem.getAudioInputStream(is); sdl = AudioSystem.getSourceDataLine(ais.getFormat(),mixer.getMixerInfo()); sdl.open(ais.getFormat()); sdl.addLineListener(this); } catch (Exception e) {e.printStackTrace();} } public Stream(AudioInputStream ais) { try { this.ais = ais; sdl = AudioSystem.getSourceDataLine(ais.getFormat(),mixer.getMixerInfo()); sdl.open(ais.getFormat()); sdl.addLineListener(this); } catch (Exception e) {e.printStackTrace();} } public void play() {play(false);} public void loop() {play(true);} public void play(boolean loop) { if (!isPaused()) stop(); (thread = new ThreadStream(loop){ public void run() { try { do { if (!isPaused()) restartStream(); paused = false; sdl.start(); int numRead = 0; byte[] buf = new byte[sdl.getBufferSize()]; while ((numRead = ais.read(buf,0,buf.length)) >= 0 && !stopThread) { int offset = 0; while (offset < numRead) { offset += sdl.write(buf,offset,numRead-offset); if (stopThread) return; } if (stopThread) return; } if (stopThread) return; sdl.drain(); if (stopThread) return; sdl.stop(); sdl.flush(); } while (isLooping() && !stopThread); } catch (Exception e) {e.printStackTrace();} } }).start(); } public void pause() { if (thread == null) return; thread.stopThread = true; sdl.stop(); paused = true; } public void stop() { if (thread == null) return; thread.stopThread = true; thread.looping = false; paused = false; sdl.stop(); sdl.flush(); thread = null; } public boolean isPlaying() { if (sdl == null) return false; return sdl.isRunning(); } public boolean isPaused() { return paused; } public boolean isStopped() { return !isPaused() && !isPlaying(); } public boolean isLooping() { if (thread == null) return false; return thread.looping; } public void update(LineEvent event) { if (event.getType() == LineEvent.Type.START) paused = false; if (event.getType() == LineEvent.Type.STOP && !isLooping() && !isPaused()) stop(); } public float getPitch() { return pitch; } public void setPitch(float pitch) { this.pitch = pitch; ((FloatControl)sdl.getControl(FloatControl.Type.SAMPLE_RATE)).setValue(frequency*pitch); } protected void restartStream() { try { if (ctrFile == null && ctrURL == null) return; if (sdl != null && sdl.isOpen()) { sdl.stop(); sdl.flush(); sdl.removeLineListener(this); sdl.close(); } if (ctrFile != null) ais = AudioSystem.getAudioInputStream(ctrFile); else if (ctrURL != null) ais = AudioSystem.getAudioInputStream(ctrURL); sdl = AudioSystem.getSourceDataLine(ais.getFormat(),mixer.getMixerInfo()); sdl.open(ais.getFormat()); sdl.addLineListener(this); frequency = sdl.getFormat().getSampleRate(); setPitch(getPitch()); } catch (Exception e) {e.printStackTrace();} } }package pl.shockah.audio; public class ThreadStream extends Thread { protected boolean looping; protected boolean stopThread = false; public ThreadStream(boolean looping) { this.looping = looping; } }