diff --git a/lib/wellen.jar b/lib/wellen.jar index 26ad763..dc15d09 100644 Binary files a/lib/wellen.jar and b/lib/wellen.jar differ diff --git a/src/wellen/Tone.java b/src/wellen/Tone.java index 5579faa..55c4d08 100644 --- a/src/wellen/Tone.java +++ b/src/wellen/Tone.java @@ -49,7 +49,7 @@ public static T create_instrument(Class instrument_cla // c = instrument_class.getDeclaredConstructor(Minim.class, int.class); // mInstrument = c.newInstance((Minim) ((ToneEngineMinim) instance()).minim(), ID); // } else { - c = instrument_class.getDeclaredConstructor(int.class); + c = instrument_class.getDeclaredConstructor(int.class); mInstrument = c.newInstance(ID); // } } catch (Exception ex) { @@ -85,6 +85,20 @@ public static float[] get_buffer() { return instance().get_buffer(); } + /** + * @return reference to left output buffer + */ + public static float[] get_output_buffer_left() { + return get_buffer_left(); + } + + /** + * @return reference to right output buffer + */ + public static float[] get_output_buffer_right() { + return get_buffer_right(); + } + public static float[] get_buffer_left() { return instance().get_buffer_left(); } @@ -251,7 +265,7 @@ public static void stop() { private static void printAlreadyStartedWarning() { System.err.println("+++ WARNING @" + Tone.class.getSimpleName() + ".start" + " / tone engine already " + - "initialized. make sure that `start` is the first call to `Ton`. " + "use " + - "`set_engine(ToneEngine)` to switch tone engines."); + "initialized. make sure that `start` is the first call to `Ton`. " + "use " + + "`set_engine(ToneEngine)` to switch tone engines."); } } diff --git a/src/wellen/WAVConverter.java b/src/wellen/WAVConverter.java index ed2a988..13512e2 100644 --- a/src/wellen/WAVConverter.java +++ b/src/wellen/WAVConverter.java @@ -12,29 +12,29 @@ public class WAVConverter { // @TODO(write header could also support `WAVE_FORMAT_PCM_32BIT_FLOAT`) // @TODO(currently fixed to little endianness) - public static boolean VERBOSE = false; - private static final String WAV_CHUNK_DATA = "data"; - private static final String WAV_CHUNK_FMT_ = "fmt "; - private static final String WAV_CHUNK_RIFF = "RIFF"; - private static final String WAV_CHUNK_WAVE = "WAVE"; - private final int mBitsPerSample; - private final int mChannels; - private final int mCompressionFormat; - private final ArrayList mData; - private final ArrayList mHeader; - private final int mSampleRate; + public static boolean VERBOSE = false; + private static final String WAV_CHUNK_DATA = "data"; + private static final String WAV_CHUNK_FMT_ = "fmt "; + private static final String WAV_CHUNK_RIFF = "RIFF"; + private static final String WAV_CHUNK_WAVE = "WAVE"; + private final int mBitsPerSample; + private final int mChannels; + private final int mCompressionFormat; + private final ArrayList mData; + private final ArrayList mHeader; + private final int mSampleRate; public WAVConverter(Info pInfo) { this(pInfo.channels, pInfo.bits_per_sample, pInfo.sample_rate, pInfo.format); } public WAVConverter(int pChannels, int pBitsPerSample, int pSampleRate, int pCompressionFormat) { - mChannels = pChannels; - mBitsPerSample = pBitsPerSample; - mSampleRate = pSampleRate; + mChannels = pChannels; + mBitsPerSample = pBitsPerSample; + mSampleRate = pSampleRate; mCompressionFormat = pCompressionFormat; - mData = new ArrayList<>(); - mHeader = new ArrayList<>(); + mData = new ArrayList<>(); + mHeader = new ArrayList<>(); } public static Info convert_bytes_to_samples(byte[] pHeader) { @@ -42,7 +42,7 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { // see http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html // see https://sites.google.com/site/musicgapi/technical-documents/wav-file-format /* RIFF Chunk */ - int mOffset = 0x00; + int mOffset = 0x00; final String mRIFFChunkName = WAVConverter.read_string(pHeader, mOffset + 0x00); if (!mRIFFChunkName.equalsIgnoreCase(WAV_CHUNK_RIFF)) { System.err.println("+++ WARNING @" + WAVConverter.class.getSimpleName() + " / expected `" + WAV_CHUNK_RIFF + "`" + " in header."); @@ -65,14 +65,14 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { System.err.println("+++ WARNING @" + WAVConverter.class.getSimpleName() + " / expected `" + WAV_CHUNK_FMT_ + "` " + "in header."); } final int mFormatChunkSize = WAVConverter.read__int32(pHeader, mOffset + 0x04); - mWAVStruct.format = WAVConverter.read__int16(pHeader, mOffset + 0x08); - mWAVStruct.channels = WAVConverter.read__int16(pHeader, mOffset + 0x0A); - mWAVStruct.sample_rate = WAVConverter.read__int32(pHeader, mOffset + 0x0C); + mWAVStruct.format = WAVConverter.read__int16(pHeader, mOffset + 0x08); + mWAVStruct.channels = WAVConverter.read__int16(pHeader, mOffset + 0x0A); + mWAVStruct.sample_rate = WAVConverter.read__int32(pHeader, mOffset + 0x0C); mWAVStruct.bits_per_sample = WAVConverter.read__int16(pHeader, mOffset + 0x16); if (VERBOSE) { System.out.println("+++ CHUNK: " + mFormatChunkName); System.out.println(" chunk size : " + mFormatChunkSize); - System.out.println(" format code: " + mWAVStruct.format); + System.out.println(" format code: " + mWAVStruct.format + " (" + getFormatString(mWAVStruct.format) + ")"); System.out.println(" channels : " + mWAVStruct.channels); System.out.println(" sample rate: " + mWAVStruct.sample_rate); System.out.println(" byte/sec : " + WAVConverter.read__int32(pHeader, mOffset + 0x10)); @@ -81,7 +81,7 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { } if (mWAVStruct.format != Wellen.WAV_FORMAT_PCM && mWAVStruct.format != Wellen.WAV_FORMAT_IEEE_FLOAT_32BIT) { System.err.println("+++ WARNING @" + WAVConverter.class.getSimpleName() + " / format not " + "supported. " - + "currently only `WAV_FORMAT_PCM` + `WAV_FORMAT_IEEE_FLOAT_32BIT` " + "works" + "." + " " + "(" + mWAVStruct.format + ")"); + + "currently only `WAV_FORMAT_PCM` + `WAV_FORMAT_IEEE_FLOAT_32BIT` " + "works" + "." + " " + "(" + mWAVStruct.format + ")"); } /* data chunk */ @@ -89,7 +89,7 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { if (WAVConverter.read_string(pHeader, mOffset + 0x00).equalsIgnoreCase("fact")) { // @TODO(hack! skipping `fact` chunk … handle this a bit more elegantly) final int mFactChunkSize = WAVConverter.read__int32(pHeader, mOffset + 0x04); - final int mPeakOffset = WAVConverter.read__int32(pHeader, mOffset + 0x10); + final int mPeakOffset = WAVConverter.read__int32(pHeader, mOffset + 0x10); if (VERBOSE) { System.out.println("+++ skipping `fact` chunk"); System.out.println("+++ CHUNK: " + WAVConverter.read_string(pHeader, mOffset + 0x00)); @@ -105,9 +105,9 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { if (!mDataChunkName.equalsIgnoreCase(WAV_CHUNK_DATA)) { System.err.println("+++ WARNING @" + WAVConverter.class.getSimpleName() + " / expected `" + WAV_CHUNK_DATA + "`" + " in header."); } - final int mDataChunkSize = WAVConverter.read__int32(pHeader, mOffset + 0x04); - byte[] mInterlacedByteBuffer = WAVConverter.read__bytes(pHeader, mOffset + 0x08, mDataChunkSize); - int mDataSize = mInterlacedByteBuffer.length / mWAVStruct.channels / (mWAVStruct.bits_per_sample / 8); + final int mDataChunkSize = WAVConverter.read__int32(pHeader, mOffset + 0x04); + byte[] mInterlacedByteBuffer = WAVConverter.read__bytes(pHeader, mOffset + 0x08, mDataChunkSize); + int mDataSize = mInterlacedByteBuffer.length / mWAVStruct.channels / (mWAVStruct.bits_per_sample / 8); if (VERBOSE) { System.out.println("+++ CHUNK: " + mDataChunkName); System.out.println(" chunk size : " + mDataChunkSize); @@ -116,10 +116,10 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { mWAVStruct.samples = new float[mWAVStruct.channels][mDataSize]; final int mBytesPerSample = mWAVStruct.bits_per_sample / 8; - final int mStride = mWAVStruct.channels * mBytesPerSample; + final int mStride = mWAVStruct.channels * mBytesPerSample; for (int j = 0; j < mWAVStruct.channels; j++) { byte[] mByteSamples = new byte[mBytesPerSample * mDataSize]; - int c = 0; + int c = 0; for (int i = 0; i < mInterlacedByteBuffer.length; i += mStride) { for (int l = 0; l < mBytesPerSample; l++) { byte b = mInterlacedByteBuffer[i + j * mBytesPerSample + l]; @@ -137,6 +137,21 @@ public static Info convert_bytes_to_samples(byte[] pHeader) { return mWAVStruct; } + private static String getFormatString(int pFormat) { + String mFormatString; + switch (pFormat) { + case Wellen.WAV_FORMAT_PCM: + mFormatString = "PCM_16BIT"; + break; + case Wellen.WAV_FORMAT_IEEE_FLOAT_32BIT: + mFormatString = "IEEE_FLOAT_32BIT"; + break; + default: + mFormatString = "UNKNOWN/UNSUPPORTED"; + } + return mFormatString; + } + public static byte[] convert_samples_to_bytes(Info pInfo) { WAVConverter mWAVConverter = new WAVConverter(pInfo); mWAVConverter.appendData(pInfo.samples); @@ -150,11 +165,11 @@ public static byte[] convert_samples_to_bytes(float[][] pBuffer, int pSampleRate, int pCompressionCode) { Info mInfo = new Info(); - mInfo.samples = pBuffer; - mInfo.channels = pChannels; + mInfo.samples = pBuffer; + mInfo.channels = pChannels; mInfo.bits_per_sample = pBitsPerSample; - mInfo.sample_rate = pSampleRate; - mInfo.format = pCompressionCode; + mInfo.sample_rate = pSampleRate; + mInfo.format = pCompressionCode; return convert_samples_to_bytes(mInfo); } @@ -203,8 +218,8 @@ private static int read__int32(byte[] pBuffer, int pStart) { } private static String read_string(byte[] pBuffer, int pStart) { - final int mStringLength = 4; - StringBuilder sb = new StringBuilder(); + final int mStringLength = 4; + StringBuilder sb = new StringBuilder(); for (int i = 0; i < mStringLength; i++) { sb.append((char) pBuffer[pStart + i]); } @@ -255,7 +270,7 @@ private static void write_string(ArrayList pBuffer, String s) { } public void appendData(float[][] pFloatBuffer) { - int mNumberOfFrames = findSingleBufferLength(pFloatBuffer); + int mNumberOfFrames = findSingleBufferLength(pFloatBuffer); float[] mInterleavedFloatBuffer = new float[mNumberOfFrames * mChannels]; for (int i = 0; i < mNumberOfFrames; i++) { for (int mChannel = 0; mChannel < mChannels; mChannel++) { @@ -272,7 +287,9 @@ public void appendData(float[][] pFloatBuffer) { System.err.println("+++ ERROR @" + WAVConverter.class.getSimpleName() + " / data format not supported."); mByteBuffer = null; } - write__bytes(mData, mByteBuffer); + if (mByteBuffer != null) { + write__bytes(mData, mByteBuffer); + } } public void writeHeader() { @@ -307,11 +324,11 @@ public byte[] getByteData() { } public static class Info { - public int bits_per_sample; - public int channels; - public byte[] data; - public int format; - public int sample_rate; + public int bits_per_sample; + public int channels; + public byte[] data; + public int format; + public int sample_rate; public float[][] samples; } } diff --git a/src/wellen/Wellen.java b/src/wellen/Wellen.java index 286a19b..457d0d6 100644 --- a/src/wellen/Wellen.java +++ b/src/wellen/Wellen.java @@ -30,6 +30,7 @@ import javax.sound.sampled.Mixer; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.TargetDataLine; +import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; @@ -257,16 +258,28 @@ public static float[] bytes_to_floatIEEEs(byte[] pBytes) { return mSignal; } + /** + * convert byte array to float array. + * + * @param pBytes source unsigned byte array + * @param pFloats destination float array + * @param pBitsPerFloat number of bits per float ( usually 8, 16, 24, or 32-bits ) + */ public static void bytes_to_floats(byte[] pBytes, float[] pFloats, int pBitsPerFloat) { - final int mBytesPerFloat = pBitsPerFloat / 8; + final int mBytesPerFloat = pBitsPerFloat / 8; + final double mScale = 1.0 / ((1 << (pBitsPerFloat - 1)) - 1); for (int i = 0; i < pFloats.length; i++) { - final double mScale = 1.0 / ((1 << (pBitsPerFloat - 1)) - 1); - long f = 0; + long f = 0; + for (int j = 0; j < mBytesPerFloat; j++) { - final long mBitShift = j * 8; - long b = pBytes[i * mBytesPerFloat + j]; - f += b << mBitShift; + int b = pBytes[i * mBytesPerFloat + j] & 0xFF; + f |= (long) b << (j * 8); + } + + if (f >= (1L << (pBitsPerFloat - 1))) { + f -= 1L << pBitsPerFloat; } + pFloats[i] = (float) (f * mScale); } } @@ -606,7 +619,8 @@ public static float[] get_extremum(float[] pSignal) { } public static String get_resource_path() { - return Wellen.class.getResource("").getPath(); + URL mURLPath = Wellen.class.getResource(""); + return mURLPath == null ? "" : mURLPath.getPath(); } public static float[][] importWAV(PApplet p, String pFilepath) { diff --git a/src/wellen/dsp/Sampler.java b/src/wellen/dsp/Sampler.java index fc38b19..273c107 100644 --- a/src/wellen/dsp/Sampler.java +++ b/src/wellen/dsp/Sampler.java @@ -39,28 +39,28 @@ */ public class Sampler implements DSPNodeOutput { - public static final int NO_LOOP_POINT = -1; - private final ArrayList fSamplerListeners; - private final ArrayList fRecording; - private final float fSamplingRate; - private float fAmplitude; - private float[] fBuffer; - private float fBufferIndex; - private boolean fDirectionForward; - private int fEdgeFadePadding; - private boolean fEvaluateLoop; - private float fFrequency; - private float fFrequencyScale; - private int fInPoint; - private boolean fInterpolateSamples; - private boolean fIsPlaying; - private int fLoopIn; - private int fLoopOut; - private int fOutPoint; - private float fSpeed; - private float fStepSize; - private boolean fIsFlaggedDone; - private boolean fIsRecording; + public static final int NO_LOOP_POINT = -1; + private final ArrayList fSamplerListeners; + private final ArrayList fRecording; + private final float fSamplingRate; + private float fAmplitude; + private float[] fBuffer; + private double fBufferIndex; + private boolean fDirectionForward; + private int fEdgeFadePadding; + private boolean fEvaluateLoop; + private float fFrequency; + private float fFrequencyScale; + private int fInPoint; + private boolean fInterpolateSamples; + private boolean fIsPlaying; + private int fLoopIn; + private int fLoopOut; + private int fOutPoint; + private float fSpeed; + private float fStepSize; + private boolean fIsFlaggedDone; + private boolean fIsRecording; public Sampler() { this(0); @@ -76,20 +76,21 @@ public Sampler(float[] buffer) { public Sampler(float[] buffer, float sampling_rate) { fSamplerListeners = new ArrayList<>(); - fSamplingRate = sampling_rate; + fSamplingRate = sampling_rate; set_buffer(buffer); - fBufferIndex = 0; + fBufferIndex = 0; fInterpolateSamples = false; - fEdgeFadePadding = 0; - fIsPlaying = false; - fInPoint = 0; - fOutPoint = 0; + fEdgeFadePadding = 0; + fIsPlaying = false; + fEvaluateLoop = false; + fInPoint = 0; + fOutPoint = 0; set_in(0); set_out(fBuffer.length - 1); fFrequencyScale = 1.0f; set_speed(1.0f); set_amplitude(1.0f); - fRecording = new ArrayList<>(); + fRecording = new ArrayList<>(); fIsRecording = false; } @@ -155,7 +156,7 @@ public float get_speed() { } public void set_speed(float speed) { - fSpeed = speed; + fSpeed = speed; fDirectionForward = speed > 0; set_frequency(PApplet.abs(speed) * fSamplingRate / fBuffer.length); /* aka `step_size = speed` */ } @@ -163,7 +164,7 @@ public void set_speed(float speed) { public void set_frequency(float frequency) { if (fFrequency != frequency) { fFrequency = frequency; - fStepSize = fFrequency / fFrequencyScale * ((float) fBuffer.length / fSamplingRate); + fStepSize = fFrequency / fFrequencyScale * ((float) fBuffer.length / fSamplingRate); } } @@ -181,7 +182,7 @@ public void set_buffer(float[] buffer) { set_speed(fSpeed); set_in(0); set_out(fBuffer.length - 1); - fLoopIn = NO_LOOP_POINT; + fLoopIn = NO_LOOP_POINT; fLoopOut = NO_LOOP_POINT; } @@ -198,11 +199,11 @@ public int get_position() { } public float get_position_normalized() { - return fBuffer.length > 0 ? fBufferIndex / fBuffer.length : 0.0f; + return fBuffer.length > 0 ? (float) fBufferIndex / fBuffer.length : 0.0f; } public float get_position_fractional_part() { - return fBufferIndex - get_position(); + return (float) fBufferIndex - get_position(); } public boolean is_playing() { @@ -214,7 +215,7 @@ public void set_duration(float seconds) { return; } final float mNormDurationSec = (fBuffer.length / fSamplingRate); - final float mSpeed = mNormDurationSec / seconds; + final float mSpeed = mNormDurationSec / seconds; set_speed(mSpeed); } @@ -242,8 +243,8 @@ public float output() { fBufferIndex += fDirectionForward ? fStepSize : -fStepSize; final int mRoundedIndex = (int) fBufferIndex; - final float mFrac = fBufferIndex - mRoundedIndex; - final int mCurrentIndex = wrapIndex(mRoundedIndex); + final double mFrac = fBufferIndex - mRoundedIndex; + final int mCurrentIndex = wrapIndex(mRoundedIndex); fBufferIndex = mCurrentIndex + mFrac; if (fDirectionForward ? (mCurrentIndex >= fOutPoint) : (mCurrentIndex <= fInPoint)) { @@ -253,14 +254,18 @@ public float output() { fIsFlaggedDone = false; } - float mSample = fBuffer[mCurrentIndex]; + return getSample(mCurrentIndex, mFrac); + } + + private float getSample(int mCurrentIndex, double mFrac) { + double mSample = fBuffer[mCurrentIndex]; /* interpolate */ if (fInterpolateSamples) { // TODO evaluate direction? - final int mNextIndex = wrapIndex(mCurrentIndex + 1); - final float mNextSample = fBuffer[mNextIndex]; - mSample = mSample * (1.0f - mFrac) + mNextSample * mFrac; + final int mNextIndex = wrapIndex(mCurrentIndex + 1); + final double mNextSample = fBuffer[mNextIndex]; + mSample = mSample * (1.0 - mFrac) + mNextSample * mFrac; } mSample *= fAmplitude; @@ -276,8 +281,7 @@ public float output() { mSample *= mFadeOutAmount; } } - - return mSample; + return (float) mSample; } public int get_edge_fading() { @@ -306,8 +310,8 @@ public void enable_loop(boolean loop) { public void set_loop_all() { fEvaluateLoop = true; - fLoopIn = 0; - fLoopOut = fBuffer.length > 0 ? (fBuffer.length - 1) : 0; + fLoopIn = 0; + fLoopOut = fBuffer.length > 0 ? (fBuffer.length - 1) : 0; } public void play() { @@ -500,10 +504,10 @@ public static void draw_sampler_buffer_circular(Sampler sampler, int step) { g.beginShape(); for (int i = 0; i < sampler.get_buffer().length; i += step) { - final float r = TWO_PI * i / sampler.get_buffer().length; + final float r = TWO_PI * i / sampler.get_buffer().length; final float mSample = map(sampler.get_buffer()[i], -1.0f, 1.0f, radius_min, radius_max); - final float x = cos(r) * mSample; - final float y = sin(r) * mSample; + final float x = cos(r) * mSample; + final float y = sin(r) * mSample; g.vertex(x, y); } g.endShape(CLOSE); diff --git a/src/wellen/tests/TestMultiChannelDSP.java b/src/wellen/tests/TestMultiChannelDSP.java new file mode 100644 index 0000000..5933a76 --- /dev/null +++ b/src/wellen/tests/TestMultiChannelDSP.java @@ -0,0 +1,50 @@ +package wellen.tests; + +import processing.core.PApplet; +import wellen.Wellen; +import wellen.dsp.DSP; + +public class TestMultiChannelDSP extends PApplet { + +// private int mCounter = 0; +// private float mDetune = 1.1f; +// private float mFreq = 344.53125f; + + public void settings() { + size(640, 480); + } + + public void setup() { + Wellen.dumpAudioInputAndOutputDevices(); + DSP.start(this, + Wellen.DEFAULT_AUDIO_DEVICE, + 2, + Wellen.DEFAULT_AUDIO_DEVICE, + 0); + } + + public void draw() { + background(255); + stroke(0); + DSP.draw_buffers(g, width, height); + } + + public void mouseMoved() { +// mFreq = map(mouseX, 0, width, 86.1328125f, 344.53125f); +// mDetune = map(mouseY, 0, height, 1.0f, 1.5f); + } + + public void audioblock(float[][] output_signalLeft, float[][] output_signalRight) { +// for (int i = 0; i < output_signalLeft.length; i++) { +// mCounter++; +// float mLeft = 0.5f * sin(2 * PI * mFreq * mCounter / DSP.get_sample_rate()); +// float mRight = 0.5f * sin(2 * PI * mFreq * mDetune * mCounter / DSP.get_sample_rate()); +// output_signalLeft[i] = mLeft * 0.7f + mRight * 0.3f; +// output_signalRight[i] = mLeft * 0.3f + mRight * 0.7f; +// } + } + + public static void main(String[] args) { + PApplet.main(TestMultiChannelDSP.class.getName()); + } +}