Rename Renderer to AudioPlayer

pull/35/head
James Ball 2021-06-17 17:56:48 +01:00
rodzic 45289200a4
commit 77ede06cf4
7 zmienionych plików z 281 dodań i 281 usunięć

Wyświetl plik

@ -2,244 +2,23 @@ package sh.ball.audio;
import sh.ball.audio.effect.Effect;
import java.io.*;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public interface AudioPlayer<S, T> extends Runnable {
import sh.ball.audio.engine.AudioEngine;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
void stop();
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;
void setQuality(double quality);
public class AudioPlayer implements Renderer<List<Shape>, AudioInputStream> {
void addFrame(S frame);
// Arbitrary max count for effects
private static final int MAX_COUNT = 10000;
private static final int BUFFER_SIZE = 5;
// Is this always true? Might need to check from AudioEngine
private static final int BITS_PER_SAMPLE = 16;
private static final boolean SIGNED = true;
private static final boolean BIG_ENDIAN = false;
// Stereo audio
private static final int NUM_OUTPUTS = 2;
void addEffect(Object identifier, Effect effect);
private final AudioEngine audioEngine;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new HashMap<>();
private final ReentrantLock renderLock = new ReentrantLock();
private final List<Listener> listeners = new ArrayList<>();
void removeEffect(Object identifier);
private ByteArrayOutputStream outputStream;
private boolean recording = false;
private int framesRecorded = 0;
private List<Shape> frame;
private int currentShape = 0;
private int audioFramesDrawn = 0;
private int count = 0;
void read(byte[] buffer) throws InterruptedException;
private double weight = Shape.DEFAULT_WEIGHT;
void startRecord();
public AudioPlayer(AudioEngine audioEngine) {
this.audioEngine = audioEngine;
}
private Vector2 generateChannels() throws InterruptedException {
Shape shape = getCurrentShape().setWeight(weight);
double totalAudioFrames = shape.getWeight() * shape.getLength();
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
Vector2 nextVector = applyEffects(count, shape.nextVector(drawingProgress));
Vector2 channels = cutoff(nextVector);
writeChannels((float) channels.getX(), (float) channels.getY());
audioFramesDrawn++;
if (++count > MAX_COUNT) {
count = 0;
}
if (audioFramesDrawn > totalAudioFrames) {
audioFramesDrawn = 0;
currentShape++;
}
if (currentShape >= frame.size()) {
currentShape = 0;
frame = frameQueue.take();
}
return channels;
}
private void writeChannels(float leftChannel, float rightChannel) {
int left = (int)(leftChannel * Short.MAX_VALUE);
int right = (int)(rightChannel * Short.MAX_VALUE);
byte b0 = (byte) left;
byte b1 = (byte)(left >> 8);
byte b2 = (byte) right;
byte b3 = (byte)(right >> 8);
if (recording) {
outputStream.write(b0);
outputStream.write(b1);
outputStream.write(b2);
outputStream.write(b3);
}
for (Listener listener : listeners) {
listener.write(b0);
listener.write(b1);
listener.write(b2);
listener.write(b3);
listener.notifyIfFull();
}
framesRecorded++;
}
private Vector2 cutoff(Vector2 vector) {
if (vector.getX() < -1) {
vector = vector.setX(-1);
} else if (vector.getX() > 1) {
vector = vector.setX(1);
}
if (vector.getY() < -1) {
vector = vector.setY(-1);
} else if (vector.getY() > 1) {
vector = vector.setY(1);
}
return vector;
}
private Vector2 applyEffects(int frame, Vector2 vector) {
for (Effect effect : effects.values()) {
vector = effect.apply(frame, vector);
}
return vector;
}
@Override
public void setQuality(double quality) {
this.weight = quality;
}
private Shape getCurrentShape() {
if (frame.size() == 0) {
return new Vector2();
}
return frame.get(currentShape);
}
@Override
public void run() {
try {
frame = frameQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException("Initial frame not found. Cannot continue.");
}
audioEngine.play(this::generateChannels, renderLock);
}
@Override
public void stop() {
audioEngine.stop();
}
@Override
public void addFrame(List<Shape> frame) {
try {
frameQueue.put(frame);
} catch (InterruptedException e) {
e.printStackTrace();
System.err.println("Frame missed.");
}
}
@Override
public void addEffect(Object identifier, Effect effect) {
effects.put(identifier, effect);
}
@Override
public void removeEffect(Object identifier) {
effects.remove(identifier);
}
@Override
public void read(byte[] buffer) throws InterruptedException {
Listener listener = new Listener(buffer);
try {
renderLock.lock();
listeners.add(listener);
} finally {
renderLock.unlock();
}
listener.waitUntilFull();
try {
renderLock.lock();
listeners.remove(listener);
} finally {
renderLock.unlock();
}
}
@Override
public void startRecord() {
outputStream = new ByteArrayOutputStream();
framesRecorded = 0;
recording = true;
}
@Override
public int samplesPerSecond() {
return audioEngine.sampleRate();
}
@Override
public AudioInputStream stopRecord() {
recording = false;
byte[] input = outputStream.toByteArray();
outputStream = null;
AudioFormat audioFormat = new AudioFormat(audioEngine.sampleRate(), BITS_PER_SAMPLE, NUM_OUTPUTS, SIGNED, BIG_ENDIAN);
return new AudioInputStream(new ByteArrayInputStream(input), audioFormat, framesRecorded);
}
private static class Listener {
private final byte[] buffer;
private final Semaphore sema;
private int offset;
private Listener(byte[] buffer) {
this.buffer = buffer;
this.sema = new Semaphore(0);
}
private void waitUntilFull() throws InterruptedException {
sema.acquire();
}
private void notifyIfFull() {
if (offset >= buffer.length) {
sema.release();
}
}
private void write(byte b) {
if (offset < buffer.length) {
buffer[offset++] = b;
}
}
}
int samplesPerSecond();
T stopRecord();
}

Wyświetl plik

@ -2,13 +2,13 @@ package sh.ball.audio;
public class FrameProducer<S, T> implements Runnable {
private final Renderer<S, T> renderer;
private final AudioPlayer<S, T> audioPlayer;
private final FrameSet<S> frames;
private boolean running;
public FrameProducer(Renderer<S, T> renderer, FrameSet<S> frames) {
this.renderer = renderer;
public FrameProducer(AudioPlayer<S, T> audioPlayer, FrameSet<S> frames) {
this.audioPlayer = audioPlayer;
this.frames = frames;
}
@ -16,7 +16,7 @@ public class FrameProducer<S, T> implements Runnable {
public void run() {
running = true;
while (running) {
renderer.addFrame(frames.next());
audioPlayer.addFrame(frames.next());
}
}

Wyświetl plik

@ -12,14 +12,14 @@ public class FrequencyAnalyser<S, T> implements Runnable {
// increase this for higher frequency resolution, but less frequent frequency calculation
private static final int DEFAULT_POWER_OF_TWO = 18;
private final Renderer<S, T> renderer;
private final AudioPlayer<S, T> audioPlayer;
private final List<FrequencyListener> listeners = new ArrayList<>();
private final int frameSize;
private final int sampleRate;
private final int powerOfTwo;
public FrequencyAnalyser(Renderer<S, T> renderer, int frameSize, int sampleRate) {
this.renderer = renderer;
public FrequencyAnalyser(AudioPlayer<S, T> audioPlayer, int frameSize, int sampleRate) {
this.audioPlayer = audioPlayer;
this.frameSize = frameSize;
this.sampleRate = sampleRate;
this.powerOfTwo = (int) (DEFAULT_POWER_OF_TWO - Math.log(DEFAULT_SAMPLE_RATE / sampleRate) / Math.log(2));
@ -42,7 +42,7 @@ public class FrequencyAnalyser<S, T> implements Runnable {
while (true) {
try {
renderer.read(buf);
audioPlayer.read(buf);
} catch (InterruptedException e) {
e.printStackTrace();
}

Wyświetl plik

@ -1,24 +0,0 @@
package sh.ball.audio;
import sh.ball.audio.effect.Effect;
public interface Renderer<S, T> extends Runnable {
void stop();
void setQuality(double quality);
void addFrame(S frame);
void addEffect(Object identifier, Effect effect);
void removeEffect(Object identifier);
void read(byte[] buffer) throws InterruptedException;
void startRecord();
int samplesPerSecond();
T stopRecord();
}

Wyświetl plik

@ -0,0 +1,245 @@
package sh.ball.audio;
import sh.ball.audio.effect.Effect;
import java.io.*;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import sh.ball.audio.engine.AudioEngine;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;
public class ShapeAudioPlayer implements AudioPlayer<List<Shape>, AudioInputStream> {
// Arbitrary max count for effects
private static final int MAX_COUNT = 10000;
private static final int BUFFER_SIZE = 5;
// Is this always true? Might need to check from AudioEngine
private static final int BITS_PER_SAMPLE = 16;
private static final boolean SIGNED = true;
private static final boolean BIG_ENDIAN = false;
// Stereo audio
private static final int NUM_OUTPUTS = 2;
private final AudioEngine audioEngine;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new HashMap<>();
private final ReentrantLock renderLock = new ReentrantLock();
private final List<Listener> listeners = new ArrayList<>();
private ByteArrayOutputStream outputStream;
private boolean recording = false;
private int framesRecorded = 0;
private List<Shape> frame;
private int currentShape = 0;
private int audioFramesDrawn = 0;
private int count = 0;
private double weight = Shape.DEFAULT_WEIGHT;
public ShapeAudioPlayer(AudioEngine audioEngine) {
this.audioEngine = audioEngine;
}
private Vector2 generateChannels() throws InterruptedException {
Shape shape = getCurrentShape().setWeight(weight);
double totalAudioFrames = shape.getWeight() * shape.getLength();
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
Vector2 nextVector = applyEffects(count, shape.nextVector(drawingProgress));
Vector2 channels = cutoff(nextVector);
writeChannels((float) channels.getX(), (float) channels.getY());
audioFramesDrawn++;
if (++count > MAX_COUNT) {
count = 0;
}
if (audioFramesDrawn > totalAudioFrames) {
audioFramesDrawn = 0;
currentShape++;
}
if (currentShape >= frame.size()) {
currentShape = 0;
frame = frameQueue.take();
}
return channels;
}
private void writeChannels(float leftChannel, float rightChannel) {
int left = (int)(leftChannel * Short.MAX_VALUE);
int right = (int)(rightChannel * Short.MAX_VALUE);
byte b0 = (byte) left;
byte b1 = (byte)(left >> 8);
byte b2 = (byte) right;
byte b3 = (byte)(right >> 8);
if (recording) {
outputStream.write(b0);
outputStream.write(b1);
outputStream.write(b2);
outputStream.write(b3);
}
for (Listener listener : listeners) {
listener.write(b0);
listener.write(b1);
listener.write(b2);
listener.write(b3);
listener.notifyIfFull();
}
framesRecorded++;
}
private Vector2 cutoff(Vector2 vector) {
if (vector.getX() < -1) {
vector = vector.setX(-1);
} else if (vector.getX() > 1) {
vector = vector.setX(1);
}
if (vector.getY() < -1) {
vector = vector.setY(-1);
} else if (vector.getY() > 1) {
vector = vector.setY(1);
}
return vector;
}
private Vector2 applyEffects(int frame, Vector2 vector) {
for (Effect effect : effects.values()) {
vector = effect.apply(frame, vector);
}
return vector;
}
@Override
public void setQuality(double quality) {
this.weight = quality;
}
private Shape getCurrentShape() {
if (frame.size() == 0) {
return new Vector2();
}
return frame.get(currentShape);
}
@Override
public void run() {
try {
frame = frameQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException("Initial frame not found. Cannot continue.");
}
audioEngine.play(this::generateChannels, renderLock);
}
@Override
public void stop() {
audioEngine.stop();
}
@Override
public void addFrame(List<Shape> frame) {
try {
frameQueue.put(frame);
} catch (InterruptedException e) {
e.printStackTrace();
System.err.println("Frame missed.");
}
}
@Override
public void addEffect(Object identifier, Effect effect) {
effects.put(identifier, effect);
}
@Override
public void removeEffect(Object identifier) {
effects.remove(identifier);
}
@Override
public void read(byte[] buffer) throws InterruptedException {
Listener listener = new Listener(buffer);
try {
renderLock.lock();
listeners.add(listener);
} finally {
renderLock.unlock();
}
listener.waitUntilFull();
try {
renderLock.lock();
listeners.remove(listener);
} finally {
renderLock.unlock();
}
}
@Override
public void startRecord() {
outputStream = new ByteArrayOutputStream();
framesRecorded = 0;
recording = true;
}
@Override
public int samplesPerSecond() {
return audioEngine.sampleRate();
}
@Override
public AudioInputStream stopRecord() {
recording = false;
byte[] input = outputStream.toByteArray();
outputStream = null;
AudioFormat audioFormat = new AudioFormat(audioEngine.sampleRate(), BITS_PER_SAMPLE, NUM_OUTPUTS, SIGNED, BIG_ENDIAN);
return new AudioInputStream(new ByteArrayInputStream(input), audioFormat, framesRecorded);
}
private static class Listener {
private final byte[] buffer;
private final Semaphore sema;
private int offset;
private Listener(byte[] buffer) {
this.buffer = buffer;
this.sema = new Semaphore(0);
}
private void waitUntilFull() throws InterruptedException {
sema.acquire();
}
private void notifyIfFull() {
if (offset >= buffer.length) {
sema.release();
}
}
private void write(byte b) {
if (offset < buffer.length) {
buffer[offset++] = b;
}
}
}
}

Wyświetl plik

@ -49,7 +49,7 @@ public class Controller implements Initializable, FrequencyListener, Listener {
private static final InputStream DEFAULT_OBJ = Controller.class.getResourceAsStream("/models/cube.obj");
private final FileChooser fileChooser = new FileChooser();
private final Renderer<List<Shape>, AudioInputStream> renderer;
private final AudioPlayer<List<Shape>, AudioInputStream> audioPlayer;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final int sampleRate;
@ -121,12 +121,12 @@ public class Controller implements Initializable, FrequencyListener, Listener {
@FXML
private Slider wobbleSlider;
public Controller(Renderer<List<Shape>, AudioInputStream> renderer) throws IOException {
this.renderer = renderer;
public Controller(AudioPlayer<List<Shape>, AudioInputStream> audioPlayer) throws IOException {
this.audioPlayer = audioPlayer;
FrameSet<List<Shape>> frames = new ObjParser(DEFAULT_OBJ).parse();
frames.addListener(this);
this.producer = new FrameProducer<>(renderer, frames);
this.sampleRate = renderer.samplesPerSecond();
this.producer = new FrameProducer<>(audioPlayer, frames);
this.sampleRate = audioPlayer.samplesPerSecond();
this.rotateEffect = new RotateEffect(sampleRate);
this.translateEffect = new TranslateEffect(sampleRate);
this.wobbleEffect = new WobbleEffect(sampleRate);
@ -136,7 +136,7 @@ public class Controller implements Initializable, FrequencyListener, Listener {
private Map<Slider, Consumer<Double>> initializeSliderMap() {
return Map.of(
weightSlider,
renderer::setQuality,
audioPlayer::setQuality,
rotateSpeedSlider,
rotateEffect::setSpeed,
translationSpeedSlider,
@ -238,15 +238,15 @@ public class Controller implements Initializable, FrequencyListener, Listener {
updateObjectRotateSpeed();
renderer.addEffect(EffectType.SCALE, scaleEffect);
renderer.addEffect(EffectType.ROTATE, rotateEffect);
renderer.addEffect(EffectType.TRANSLATE, translateEffect);
audioPlayer.addEffect(EffectType.SCALE, scaleEffect);
audioPlayer.addEffect(EffectType.ROTATE, rotateEffect);
audioPlayer.addEffect(EffectType.TRANSLATE, translateEffect);
executor.submit(producer);
Thread renderThread = new Thread(renderer);
Thread renderThread = new Thread(audioPlayer);
renderThread.setUncaughtExceptionHandler((thread, throwable) -> throwable.printStackTrace());
renderThread.start();
FrequencyAnalyser<List<Shape>, AudioInputStream> analyser = new FrequencyAnalyser<>(renderer, 2, sampleRate);
FrequencyAnalyser<List<Shape>, AudioInputStream> analyser = new FrequencyAnalyser<>(audioPlayer, 2, sampleRate);
analyser.addListener(this);
analyser.addListener(wobbleEffect);
new Thread(analyser).start();
@ -257,10 +257,10 @@ public class Controller implements Initializable, FrequencyListener, Listener {
if (recording) {
recordLabel.setText("Recording...");
recordButton.setText("Stop Recording");
renderer.startRecord();
audioPlayer.startRecord();
} else {
recordButton.setText("Record");
AudioInputStream input = renderer.stopRecord();
AudioInputStream input = audioPlayer.stopRecord();
try {
File file = fileChooser.showSaveDialog(stage);
SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
@ -315,10 +315,10 @@ public class Controller implements Initializable, FrequencyListener, Listener {
private void updateEffect(EffectType type, boolean checked, Effect effect) {
if (checked) {
renderer.addEffect(type, effect);
audioPlayer.addEffect(type, effect);
effectTypes.get(type).setDisable(false);
} else {
renderer.removeEffect(type);
audioPlayer.removeEffect(type);
effectTypes.get(type).setDisable(true);
}
}
@ -329,7 +329,7 @@ public class Controller implements Initializable, FrequencyListener, Listener {
String path = file.getAbsolutePath();
FrameSet<List<Shape>> frames = ParserFactory.getParser(path).parse();
frames.addListener(this);
producer = new FrameProducer<>(renderer, frames);
producer = new FrameProducer<>(audioPlayer, frames);
updateObjectRotateSpeed();
updateFocalLength();

Wyświetl plik

@ -10,7 +10,7 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import sh.ball.audio.AudioPlayer;
import sh.ball.audio.ShapeAudioPlayer;
import sh.ball.audio.engine.XtAudioEngine;
import sh.ball.engine.Vector3;
@ -24,7 +24,7 @@ public class Gui extends Application {
System.setProperty("prism.lcdtext", "false");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/osci-render.fxml"));
Controller controller = new Controller(new AudioPlayer(new XtAudioEngine()));
Controller controller = new Controller(new ShapeAudioPlayer(new XtAudioEngine()));
loader.setController(controller);
Parent root = loader.load();