Skip to content

Commit

Permalink
Fix AudioSample position calculation (refs #28) and other bugs
Browse files Browse the repository at this point in the history
Change AudioSample to manually manage isPlaying() status to stop jump() from unintentionally spawning new sampleplayers
Add informative warning messages when calling read()/write() on stereo AudioSamples
  • Loading branch information
kevinstadler committed May 11, 2019
1 parent aaef8fa commit 9c14289
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 35 deletions.
142 changes: 108 additions & 34 deletions src/processing/sound/AudioSample.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public class AudioSample extends SoundObject {
protected FloatSample sample;
protected VariableRateDataReader player;

// cued frame index of this sample
protected int startFrame = 0;
// DataReader's queue status, required for accurate computation of playback
// position within the sample
protected long startFrameCountOffset = 0;

public AudioSample(PApplet parent, int frames) {
this(parent, frames, false);
Expand Down Expand Up @@ -115,6 +119,10 @@ protected void initiatePlayer() {
// always stay connected to the JSyn synths, since they make no noise
// as long as their dataQueue is empty
super.play(); // doesn't actually start playback, just adds the (silent) units

// this is kinda hack-ish, should maybe replace with its own boolean (and
// overwrite public boolean isPlaying() to return that boolean instead)
this.isPlaying = false;
}

/**
Expand Down Expand Up @@ -161,7 +169,9 @@ public void cue(float time) {
* frame number to start playback from.
**/
public void cueFrame(int frameNumber) {
this.setStartFrame(frameNumber);
if (this.checkStartFrame(frameNumber)) {
this.setStartFrame(frameNumber);
}
}

/**
Expand Down Expand Up @@ -202,7 +212,8 @@ private boolean setStartTime(float time) {
}
int startFrame = Math.round(this.sampleRate() * time);
if (startFrame >= this.frames()) {
Engine.printError("can't cue past of end of sample (total duration is " + this.duration() + "s)");
// TODO implement wrapping over to beginning of file and printing warning instead of error
Engine.printError("can't cue past end of sample (total duration is " + this.duration() + "s)");
return false;
}
this.startFrame = startFrame;
Expand All @@ -219,6 +230,8 @@ private boolean setStartTime(float time) {
* @webref sound
**/
public void jump(float time) {
// FIXME this currently only works for simply *playing* files, if the
// current playback was a loop, all of the looping information is lost
if (this.setStartTime(time)) {
this.stop();
this.play(); // if the file wasn't playing when jump() was called, just start playing it
Expand All @@ -242,12 +255,33 @@ protected AudioSample getUnusedPlayer() {
}
}

private void setStartFrameCountOffset() {
this.startFrameCountOffset = this.player.dataQueue.getFrameCount();
}

// FIXME cueing subsections of a file for looping is not supported at the
// moment because looping information would need to be stored to correctly
// compute the current position
private void loopInternal(int startFrame, int numFrames, int numLoops) {
// always use current sample player
this.stop();
this.setStartFrameCountOffset();
this.startFrame = startFrame;
if (numLoops > 1) {
this.player.dataQueue.queueLoop(this.sample, startFrame, numFrames, numLoops);
} else {
this.player.dataQueue.queueLoop(this.sample, startFrame, numFrames);
}
this.isPlaying = true;
}

private void loopInternal(int startFrame, int numFrames) {
this.loopInternal(startFrame, numFrames, 0);
}


public void loop() {
AudioSample source = this.getUnusedPlayer();
source.player.dataQueue.queueLoop(source.sample, 0, source.frames() - source.startFrame);
// for improved handling by the user, could return a reference to whichever
// sound file is the source of the newly triggered playback
// return source;
this.loopInternal(0, this.frames());
}

public void loop(float rate) {
Expand All @@ -267,7 +301,7 @@ public void loop(float rate, float pos, float amp) {
}

/**
* Starts playback which will loop at the end of the sample.
* Starts playback which loops from the beginning to the end of the sample.
*
* @param rate
* relative playback rate to use. 1 is the original speed. 0.5 is
Expand All @@ -288,6 +322,23 @@ public void loop(float rate, float pos, float amp, float add) {
this.loop(rate, pos, amp);
}

// public void loopFrames(int startFrame, int numFrames) {
// TODO check startFrame, numFrames
// this.loopInternal(startFrame, numFrames, 0);
// }

// public void loopFrames(int startFrame, int numFrames, int loops) {
// TODO check startFrame, numFrames, loop > 1
// this.loopInternal(startFrame, numFrames, loops);
// }

// TODO add same functions but specifying loop section in seconds
// public void loopSection(float start, float duration) {
// }
// public void loopSection(float start, float duration, int loops) {
// }


/*
* FIXME cueing a position for loops has to be handled differently than for
* simple playback, because passing a startFrame to dataQueue.queueLoop() causes
Expand All @@ -299,9 +350,19 @@ public void loop(float rate, float pos, float amp, float add) {
* cue) { this.cue(cue); this.loop(rate, pos, amp, add); }
*/

private void playInternal() {
this.playInternal(this.startFrame, this.frames() - this.startFrame);
}

private void playInternal(int startFrame, int numFrames) {
this.setStartFrameCountOffset();
this.player.dataQueue.queue(this.sample, startFrame, numFrames);
this.isPlaying = true;
}

public void play() {
AudioSample source = this.getUnusedPlayer();
source.player.dataQueue.queue(source.sample, source.startFrame, source.frames() - source.startFrame);
source.playInternal();
// for improved handling by the user, could return a reference to
// whichever audiosample object is the actual source (i.e. JSyn
// container) of the newly triggered playback
Expand Down Expand Up @@ -442,6 +503,7 @@ public void set(float rate, float pos, float amp, float add) {
**/
public void stop() {
this.player.dataQueue.clear();
this.isPlaying = false;
}

// new methods go here
Expand All @@ -453,32 +515,27 @@ public void stop() {
* @webref sound
*/
public float position() {
// TODO progress in sample seconds or current-rate-playback seconds??
// TODO might have to offset getFrameCount by this.startFrame?
return (this.player.dataQueue.getFrameCount() % this.frames()) / (float) this.sampleRate();
return this.positionFrame() / (float) this.sampleRate();
}

/**
* Get current sound file playback position in percent.
* Get frame index of current sound file playback position.
*
* @return The current position of the sound file playback in percent (a value
* between 0 and 100).
* @return The current frame index position of the sound file playback
* @webref sound
*/
public float percent() {
// TODO might have to offset getFrameCount by this.startFrame?
return 100f * (this.player.dataQueue.getFrameCount() % this.frames()) / (float) this.frames();
public int positionFrame() {
return (int) (this.startFrame + this.player.dataQueue.getFrameCount() - this.startFrameCountOffset) % this.frames();
}

/**
* Check whether this audiosample is currently playing.
* Get current sound file playback position in percent.
*
* @return `true` if the audiosample is currently playing, `false` if it is not.
* @return The current position of the sound file playback in percent (a value
* between 0 and 100).
* @webref sound
*/
public boolean isPlaying() {
// overrides the SoundObject's default implementation
return this.player.dataQueue.hasMore();
public float percent() {
return 100f * this.positionFrame() / (float) this.frames();
}

/**
Expand All @@ -489,10 +546,11 @@ public boolean isPlaying() {
*/
public void pause() {
if (this.isPlaying()) {
this.startFrame = (int) this.player.dataQueue.getFrameCount() % this.frames();
this.stop();
this.startFrame = this.positionFrame();
this.setStartFrameCountOffset();
} else {
Engine.printWarning("audio sample is not currently playing");
Engine.printWarning("audio sample is already paused");
}
}

Expand Down Expand Up @@ -522,16 +580,19 @@ protected boolean checkStartFrame(int startFrame, boolean verbose) {
* the target array that the read data is written to
*/
public void read(float[] data) {
if (data.length != this.frames()) {
if (this.channels() == 2 && data.length != 2 * this.frames()) {
Engine.printWarning(
"the length of the array passed to read(float[]) does not match the size of the data of this stereo audio sample (note that stereo samples contain two values per frame!)");
} else if (this.channels() == 1 && data.length != this.frames()) {
Engine.printWarning(
"the length of the given array does not match the number of frames of this audio sample");
"the length of the array passed to read(float[]) does not match the number of frames of this audio sample");
}
// TODO catch exception and print understandable error message
this.sample.read(data);
}

/**
* Read some frames of this audio sample into an array.
* Read some frames of this audio sample into an array. Stereo samples contain two data points per frame.
*
* @param startFrame
* the index of the first frame of the audiosample that should be
Expand All @@ -541,11 +602,12 @@ public void read(float[] data) {
* written to (typically 0)
* @param numFrames
* the number of frames that should be read (can't be greater than
* data.length - startIndex)
* audiosample.channels() * data.length - startIndex)
* @webref sound
*/
public void read(int startFrame, float[] data, int startIndex, int numFrames) {
if (this.checkStartFrame(startFrame)) {
// TODO check and print informative warning about stereo case
if (startFrame + numFrames < this.frames()) {
this.sample.read(startFrame, data, startIndex, numFrames);
} else {
Expand All @@ -564,6 +626,10 @@ public void read(int startFrame, float[] data, int startIndex, int numFrames) {
* @return the value of the audio sample at the given frame
*/
public float read(int index) {
if (this.channels() == 2) {
Engine.printWarning(
"read(int) only returns data for the left channel of a stereo file, please use one of the other read() methods to access data for all channels");
}
// TODO catch exception and print understandable error message
return (float) this.sample.readDouble(index);
}
Expand All @@ -576,15 +642,18 @@ public float read(int index) {
* the array from which the sample data should be taken
*/
public void write(float[] data) {
if (data.length != this.frames()) {
if (this.channels() == 2 && data.length != 2 * this.frames()) {
Engine.printWarning(
"the length of the array passed to write(float[]) does not match the size of the data of this stereo audio sample (note that stereo samples contain two values per frame!)");
} else if (this.channels() == 1 && data.length != this.frames()) {
Engine.printWarning(
"the length of the given array does not match the number of frames of this audio sample");
"the length of the array passed to write(float[]) does not match the number of frames of this audio sample");
}
this.sample.write(data);
}

/**
* Write some frames of this audio sample.
* Write some frames of this audio sample. Stereo samples require two data points per frame.
*
* @param startFrame
* the index of the first frame of the audiosample that should be
Expand All @@ -594,10 +663,11 @@ public void write(float[] data) {
* taken from (typically 0)
* @param numFrames
* the number of frames that should be written (can't be greater than
* data.length - startIndex)
* audiosample.channels() * data.length - startIndex)
* @webref sound
*/
public void write(int startFrame, float[] data, int startIndex, int numFrames) {
// FIXME check stereo case
if (this.checkStartFrame(startFrame)) {
if (startFrame + numFrames < this.frames()) {
this.sample.write(startFrame, data, startIndex, numFrames);
Expand All @@ -619,6 +689,10 @@ public void write(int startFrame, float[] data, int startIndex, int numFrames) {
* the float value that the given audio frame should be set to
*/
public void write(int index, float value) {
if (this.channels() == 2) {
Engine.printWarning(
"write(int, float) only writes data to the left channel of a stereo file, please use one of the other write() methods to write data to all channels");
}
if (this.checkStartFrame(startFrame)) {
this.sample.writeDouble(index, value);
}
Expand Down
2 changes: 1 addition & 1 deletion src/processing/sound/SoundObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public abstract class SoundObject {
protected UnitInputPort amplitude;

protected float amp = 1.0f;
private boolean isPlaying = false;
protected boolean isPlaying = false;

protected SoundObject(PApplet parent) {
Engine.getEngine(parent);
Expand Down

0 comments on commit 9c14289

Please sign in to comment.