Skip to content

Commit

Permalink
Improved javax sound system
Browse files Browse the repository at this point in the history
  • Loading branch information
RaphiMC committed May 13, 2024
1 parent 5886d9b commit 4e7d1a4
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@

public abstract class SoundSystem implements AutoCloseable {

protected final int maxSounds;

public SoundSystem(final int maxSounds) {
this.maxSounds = maxSounds;
}

public abstract void playSound(final String sound, final float pitch, final float volume, final float panning);

public void writeSamples() {
Expand All @@ -29,6 +35,10 @@ public void writeSamples() {
@Override
public abstract void close();

public int getMaxSounds() {
return this.maxSounds;
}

public abstract String getStatusLine();

public abstract void setMasterVolume(final float volume);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
*/
package net.raphimc.noteblocktool.audio.soundsystem.impl;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.raphimc.noteblocktool.audio.SoundMap;
import net.raphimc.noteblocktool.audio.soundsystem.SoundSystem;
import net.raphimc.noteblocktool.util.SoundSampleUtil;
Expand All @@ -27,24 +25,29 @@
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.SourceDataLine;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

public class JavaxSoundSystem extends SoundSystem {

private static final AudioFormat FORMAT = new AudioFormat(44100, 16, 2, true, false);

private final Map<String, int[]> sounds;
private final Cache<String, int[]> mutationCache;
private final List<SoundInstance> playingSounds = new CopyOnWriteArrayList<>();
private final int samplesPerTick;
private final long availableNanosPerTick;
private final SourceDataLine dataLine;
private float masterVolume = 1F;
private long[] buffer = new long[0];
private long neededNanosPerTick = 0L;

public JavaxSoundSystem(final int maxSounds, final float playbackSpeed) {
super(maxSounds);

public JavaxSoundSystem(final float playbackSpeed) {
try {
this.sounds = SoundMap.loadInstrumentSamples(FORMAT);
this.mutationCache = CacheBuilder.newBuilder().maximumSize(1000).build();
this.samplesPerTick = (int) (FORMAT.getSampleRate() / playbackSpeed) * FORMAT.getChannels();
this.availableNanosPerTick = (long) (1_000_000_000L / playbackSpeed);
this.dataLine = AudioSystem.getSourceDataLine(FORMAT);
this.dataLine.open(FORMAT, (int) FORMAT.getSampleRate());
this.dataLine.start();
Expand All @@ -57,23 +60,30 @@ public JavaxSoundSystem(final float playbackSpeed) {
public void playSound(final String sound, final float pitch, final float volume, final float panning) {
if (!this.sounds.containsKey(sound)) return;

final String key = sound + "\0" + pitch + "\0" + volume + "\0" + panning;
final int[] samples = this.mutationCache.asMap().computeIfAbsent(key, k -> SoundSampleUtil.mutate(FORMAT, this.sounds.get(sound), pitch, volume * this.masterVolume, panning));
if (this.buffer.length < samples.length) this.buffer = Arrays.copyOf(this.buffer, samples.length);
for (int i = 0; i < samples.length; i++) this.buffer[i] += samples[i];
if (this.playingSounds.size() >= this.maxSounds) {
this.playingSounds.remove(0);
}

this.playingSounds.add(new SoundInstance(this.sounds.get(sound), pitch, volume * this.masterVolume, panning));
}

@Override
public void writeSamples() {
final long[] samples = Arrays.copyOfRange(this.buffer, 0, this.samplesPerTick);
final long start = System.nanoTime();
final long[] samples = new long[this.samplesPerTick];
for (SoundInstance playingSound : this.playingSounds) {
playingSound.write(samples);
}
this.dataLine.write(this.write(samples), 0, samples.length * 2);
if (this.buffer.length > this.samplesPerTick) this.buffer = Arrays.copyOfRange(this.buffer, this.samplesPerTick, this.buffer.length);
else if (this.buffer.length != 0) this.buffer = new long[0];

this.playingSounds.removeIf(SoundInstance::isFinished);
this.neededNanosPerTick = System.nanoTime() - start;
}

@Override
public void stopSounds() {
this.dataLine.flush();
this.playingSounds.clear();
}

@Override
Expand All @@ -83,13 +93,13 @@ public void close() {

@Override
public String getStatusLine() {
return " ";
final float load = (float) this.neededNanosPerTick / this.availableNanosPerTick;
return "Sounds: " + this.playingSounds.size() + " / " + this.maxSounds + ", CPU Load: " + (int) (load * 100) + "%";
}

@Override
public void setMasterVolume(final float volume) {
this.masterVolume = volume;
this.mutationCache.invalidateAll();
}

private byte[] write(final long[] samples) {
Expand All @@ -104,4 +114,43 @@ private byte[] write(final long[] samples) {
return out;
}

private class SoundInstance {

private final int[] samples;
private final float pitch;
private final float volume;
private final float panning;
private final int step;
private final int[] sliceBuffer;
private int cursor = 0;

public SoundInstance(final int[] samples, final float pitch, final float volume, final float panning) {
this.samples = samples;
this.pitch = pitch;
this.volume = volume;
this.panning = panning;
this.step = (int) (JavaxSoundSystem.this.samplesPerTick / FORMAT.getChannels() * pitch) * FORMAT.getChannels();
this.sliceBuffer = new int[this.step + FORMAT.getChannels()];
}

public void write(final long[] buffer) {
if (this.isFinished()) return;

final int copyLength = Math.min(this.samples.length - this.cursor, this.sliceBuffer.length);
System.arraycopy(this.samples, this.cursor, this.sliceBuffer, 0, copyLength);
Arrays.fill(this.sliceBuffer, copyLength, this.sliceBuffer.length, 0);

final int[] mutatedSlice = SoundSampleUtil.mutate(FORMAT, this.sliceBuffer, this.pitch, this.volume, this.panning);
for (int i = 0; i < buffer.length; i++) {
buffer[i] += mutatedSlice[i];
}
this.cursor += this.step;
}

public boolean isFinished() {
return this.cursor >= this.samples.length;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ public static OpenALSoundSystem createCapture(final int maxSounds, final AudioFo
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("OpenAL Sound System").setDaemon(true).build());
private final Map<String, Integer> soundBuffers = new HashMap<>();
private final List<Integer> playingSources = new CopyOnWriteArrayList<>();
private final int maxSounds;
private final AudioFormat captureAudioFormat;
private long device;
private long context;
Expand All @@ -75,7 +74,8 @@ private OpenALSoundSystem(final int maxSounds) {
}

private OpenALSoundSystem(final int maxSounds, final AudioFormat captureAudioFormat) {
this.maxSounds = maxSounds;
super(maxSounds);

this.captureAudioFormat = captureAudioFormat;
int[] attributes;
if (captureAudioFormat == null) {
Expand Down Expand Up @@ -236,10 +236,6 @@ public String getStatusLine() {
return "Sounds: " + this.playingSources.size() + " / " + this.maxSounds;
}

public int getMaxSounds() {
return this.maxSounds;
}

@Override
public void setMasterVolume(final float volume) {
AL10.alListenerf(AL10.AL_GAIN, volume);
Expand Down
42 changes: 14 additions & 28 deletions src/main/java/net/raphimc/noteblocktool/frames/SongPlayerFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class SongPlayerFrame extends JFrame implements SongPlayerCallback, FullN
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
private static SongPlayerFrame instance;
private static Point lastPosition;
private static int lastOpenAlMaxSounds = 256;
private static int lastMaxSounds = 256;
private static int lastVolume = 50;

public static void open(final ListFrame.LoadedSong song) {
Expand All @@ -60,13 +60,13 @@ public static void open(final ListFrame.LoadedSong song) {
public static void open(final ListFrame.LoadedSong song, final SongView<?> view) {
if (instance != null && instance.isVisible()) {
lastPosition = instance.getLocation();
lastOpenAlMaxSounds = (int) instance.openAlMaxSoundsSpinner.getValue();
lastMaxSounds = (int) instance.maxSoundsSpinner.getValue();
lastVolume = instance.volumeSlider.getValue();
instance.dispose();
}
instance = new SongPlayerFrame(song, view);
if (lastPosition != null) instance.setLocation(lastPosition);
instance.openAlMaxSoundsSpinner.setValue(lastOpenAlMaxSounds);
instance.maxSoundsSpinner.setValue(lastMaxSounds);
instance.volumeSlider.setValue(lastVolume);
instance.playStopButton.doClick();
instance.setVisible(true);
Expand All @@ -81,12 +81,12 @@ public static void close() {
private final SongPlayer songPlayer;
private final Timer updateTimer;
private final JComboBox<String> soundSystemComboBox = new JComboBox<>(new String[]{"OpenAL (better sound quality)", "Javax (better system compatibility, laggier)"});
private final JSpinner openAlMaxSoundsSpinner = new JSpinner(new SpinnerNumberModel(256, 64, 8192, 64));
private final JSpinner maxSoundsSpinner = new JSpinner(new SpinnerNumberModel(256, 64, 8192, 64));
private final JSlider volumeSlider = new JSlider(0, 100, 50);
private final JButton playStopButton = new JButton("Play");
private final JButton pauseResumeButton = new JButton("Pause");
private final JSlider progressSlider = new JSlider(0, 100, 0);
private final JLabel statusLine = new JLabel("");
private final JLabel statusLine = new JLabel(" ");
private final JLabel progressLabel = new JLabel("Current Position: 00:00:00");
private SoundSystem soundSystem;

Expand Down Expand Up @@ -126,23 +126,12 @@ private void initComponents() {
root.add(northPanel, BorderLayout.NORTH);

int gridy = 0;
final JLabel maxSoundsLabel = new JLabel("Max Sounds:");
GBC.create(northPanel).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(new JLabel("Sound System:"));
GBC.create(northPanel).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.soundSystemComboBox, () -> {
this.soundSystemComboBox.addActionListener(e -> {
if (this.soundSystemComboBox.getSelectedIndex() == 0) {
this.openAlMaxSoundsSpinner.setVisible(true);
maxSoundsLabel.setVisible(true);
} else {
this.openAlMaxSoundsSpinner.setVisible(false);
maxSoundsLabel.setVisible(false);
}
});
});
GBC.create(northPanel).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.soundSystemComboBox);

GBC.create(northPanel).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(maxSoundsLabel);
GBC.create(northPanel).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.openAlMaxSoundsSpinner, () -> {
this.openAlMaxSoundsSpinner.addChangeListener(e -> lastOpenAlMaxSounds = (int) this.openAlMaxSoundsSpinner.getValue());
GBC.create(northPanel).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(new JLabel("Max Sounds:"));
GBC.create(northPanel).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.maxSoundsSpinner, () -> {
this.maxSoundsSpinner.addChangeListener(e -> lastMaxSounds = (int) this.maxSoundsSpinner.getValue());
});

GBC.create(northPanel).grid(0, gridy).insets(5, 5, 5, 5).anchor(GBC.LINE_START).add(new JLabel("Volume:"));
Expand Down Expand Up @@ -246,23 +235,20 @@ private void initComponents() {

private boolean initSoundSystem() {
int currentIndex = -1;
boolean soundSystemPropertiesChanged = false;
if (this.soundSystem instanceof OpenALSoundSystem) {
final OpenALSoundSystem openALSoundSystem = (OpenALSoundSystem) this.soundSystem;
soundSystemPropertiesChanged = openALSoundSystem.getMaxSounds() != (int) this.openAlMaxSoundsSpinner.getValue();
currentIndex = 0;
} else if (this.soundSystem instanceof JavaxSoundSystem) {
currentIndex = 1;
}

try {
if (this.soundSystem == null || this.soundSystemComboBox.getSelectedIndex() != currentIndex || soundSystemPropertiesChanged) {
if (this.soundSystem == null || this.soundSystemComboBox.getSelectedIndex() != currentIndex || this.soundSystem.getMaxSounds() != (int) this.maxSoundsSpinner.getValue()) {
if (this.soundSystem != null) this.soundSystem.close();

if (this.soundSystemComboBox.getSelectedIndex() == 0) {
this.soundSystem = OpenALSoundSystem.createPlayback(((Number) this.openAlMaxSoundsSpinner.getValue()).intValue());
this.soundSystem = OpenALSoundSystem.createPlayback(((Number) this.maxSoundsSpinner.getValue()).intValue());
} else if (this.soundSystemComboBox.getSelectedIndex() == 1) {
this.soundSystem = new JavaxSoundSystem(this.songPlayer.getSongView().getSpeed());
this.soundSystem = new JavaxSoundSystem(((Number) this.maxSoundsSpinner.getValue()).intValue(), this.songPlayer.getSongView().getSpeed());
} else {
throw new UnsupportedOperationException(UNAVAILABLE_MESSAGE);
}
Expand Down Expand Up @@ -296,7 +282,7 @@ public void windowClosed(WindowEvent e) {
private void tick() {
if (this.songPlayer.isRunning()) {
this.soundSystemComboBox.setEnabled(false);
this.openAlMaxSoundsSpinner.setEnabled(false);
this.maxSoundsSpinner.setEnabled(false);
this.playStopButton.setText("Stop");
this.pauseResumeButton.setEnabled(true);
if (this.songPlayer.isPaused()) this.pauseResumeButton.setText("Resume");
Expand All @@ -307,7 +293,7 @@ private void tick() {
this.progressSlider.setValue(this.songPlayer.getTick());
} else {
this.soundSystemComboBox.setEnabled(true);
this.openAlMaxSoundsSpinner.setEnabled(true);
this.maxSoundsSpinner.setEnabled(true);
this.playStopButton.setText("Play");
this.pauseResumeButton.setText("Pause");
this.pauseResumeButton.setEnabled(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static int[] readSamples(final InputStream inputStream, final AudioFormat
}

public static int[] mutate(final AudioFormat format, final int[] samples, final float pitch, final float volume, final float panning) {
final int newLength = (int) ((float) samples.length / format.getChannels() / pitch * format.getChannels());
final int newLength = (int) (samples.length / format.getChannels() / pitch) * format.getChannels();
final int[] newSamples = new int[newLength];

for (int channel = 0; channel < format.getChannels(); channel++) {
Expand Down

0 comments on commit 4e7d1a4

Please sign in to comment.