├── .settings ├── org.eclipse.m2e.core.prefs ├── org.eclipse.core.resources.prefs └── org.eclipse.jdt.core.prefs ├── src ├── main │ └── java │ │ └── log │ │ └── charter │ │ └── dsp │ │ ├── Complex.java │ │ ├── HammingWindow.java │ │ ├── App.java │ │ ├── FourierTransform.java │ │ └── NoteExtractor.java └── test │ └── java │ └── log │ └── charter │ └── dsp │ └── AppTest.java ├── .gitignore └── pom.xml /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /src/main/java/log/charter/dsp/Complex.java: -------------------------------------------------------------------------------- 1 | package log.charter.dsp; 2 | 3 | public class Complex { 4 | public float real; 5 | public float imag; 6 | } 7 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/main/java=UTF-8 3 | encoding//src/test/java=UTF-8 4 | encoding/=UTF-8 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | 13 | # Eclipse m2e generated files 14 | # Eclipse Core 15 | .project 16 | # JDT-specific (Eclipse Java Development Tools) 17 | .classpath -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 3 | org.eclipse.jdt.core.compiler.compliance=1.8 4 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 5 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 6 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore 7 | org.eclipse.jdt.core.compiler.release=disabled 8 | org.eclipse.jdt.core.compiler.source=1.8 9 | -------------------------------------------------------------------------------- /src/main/java/log/charter/dsp/HammingWindow.java: -------------------------------------------------------------------------------- 1 | package log.charter.dsp; 2 | 3 | public class HammingWindow { 4 | 5 | // Alpha always equals 25/46 for a Hamming window 6 | // Why? I don't know. Ask the guy who invented it. 7 | private static final float alpha = 25 / 46f; 8 | 9 | public static float[] generate(final int length) { 10 | int N = length; 11 | 12 | float[] W = new float[N]; 13 | for (int n = 0; n < N; ++n) { 14 | W[n] = alpha + (1 - alpha) * (float)Math.cos(2 * Math.PI * n / (float)N); 15 | } 16 | 17 | return W; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/log/charter/dsp/AppTest.java: -------------------------------------------------------------------------------- 1 | package log.charter.dsp; 2 | 3 | import junit.framework.Test; 4 | import junit.framework.TestCase; 5 | import junit.framework.TestSuite; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest 11 | extends TestCase 12 | { 13 | /** 14 | * Create the test case 15 | * 16 | * @param testName name of the test case 17 | */ 18 | public AppTest( String testName ) 19 | { 20 | super( testName ); 21 | } 22 | 23 | /** 24 | * @return the suite of tests being tested 25 | */ 26 | public static Test suite() 27 | { 28 | return new TestSuite( AppTest.class ); 29 | } 30 | 31 | /** 32 | * Rigourous Test :-) 33 | */ 34 | public void testApp() 35 | { 36 | assertTrue( true ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | log.charter 6 | dsp 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | dsp 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.apache.commons 20 | commons-math3 21 | 3.6.1 22 | 23 | 24 | org.bytedeco 25 | fftw-platform 26 | 3.3.10-1.5.10 27 | 28 | 29 | junit 30 | junit 31 | 3.8.1 32 | test 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/log/charter/dsp/App.java: -------------------------------------------------------------------------------- 1 | package log.charter.dsp; 2 | 3 | public class App 4 | { 5 | public static void main( String[] args ) 6 | { 7 | //Sampling rate 8 | int sampleRate = 44100; 9 | 10 | // Frequency of the cosine wave in Hz 11 | float frequency = 440; 12 | 13 | // Length of the cosine wave in samples 14 | int bufferSize = 4096; 15 | 16 | float[] input = new float[bufferSize]; 17 | 18 | // Generate the cosine wave 19 | for (int i = 0; i < input.length; ++i) { 20 | float t = i / (float)sampleRate; 21 | input[i] = (float)Math.cos(2 * Math.PI * frequency * t); 22 | } 23 | 24 | NoteExtractor noteExtractor = new NoteExtractor(bufferSize, sampleRate); 25 | 26 | float[] frequencies = noteExtractor.frequencies(); 27 | Complex[] output = noteExtractor.allocOutput(); 28 | 29 | noteExtractor.execute(input, output); 30 | 31 | for (int bin = 0; bin < output.length; ++bin) { 32 | float mag = (float)Math.sqrt(output[bin].real * output[bin].real + output[bin].imag * output[bin].imag); 33 | System.out.println("BIN: " + bin + " Frequency: " + frequencies[bin] + ": " + mag); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/log/charter/dsp/FourierTransform.java: -------------------------------------------------------------------------------- 1 | package log.charter.dsp; 2 | 3 | import static org.bytedeco.fftw.global.fftw3.*; 4 | 5 | import java.util.Arrays; 6 | 7 | import org.bytedeco.javacpp.*; 8 | 9 | public class FourierTransform { 10 | private static final int REAL = 0; 11 | private static final int IMAG = 1; 12 | 13 | private final int bufferSize; 14 | private FloatPointer fftInput; 15 | private FloatPointer fftOutput; 16 | private fftwf_plan fftPlan; 17 | 18 | // Temporary buffer used to translate the output into Complex values 19 | private float[] outputBuffer; 20 | 21 | // Performs FFT of real input data 22 | public FourierTransform(final int bufferSize) { 23 | this.bufferSize = bufferSize; 24 | fftInput = fftwf_alloc_real(bufferSize); 25 | fftOutput = fftwf_alloc_complex(bufferSize / 2 + 1); 26 | fftPlan = fftwf_plan_dft_r2c_1d(bufferSize, fftInput, fftOutput, FFTW_ESTIMATE); 27 | 28 | outputBuffer = new float[(bufferSize / 2 + 1) * 2]; 29 | } 30 | 31 | public float[] allocInput() { 32 | return new float[bufferSize]; 33 | } 34 | 35 | public Complex[] allocOutput() { 36 | Complex[] output = new Complex[bufferSize / 2 + 1]; 37 | for (int bin = 0; bin < output.length; ++bin) { 38 | output[bin] = new Complex(); 39 | } 40 | 41 | return output; 42 | } 43 | 44 | public void execute(final float[] input, Complex[] output) { 45 | this.fftInput.put(input); 46 | fftwf_execute(this.fftPlan); 47 | this.fftOutput.get(outputBuffer); 48 | 49 | // Normalize the output and store it in the output array 50 | for (int bin = 0; bin < output.length; ++bin) { 51 | output[bin].real = outputBuffer[2 * bin + REAL] / bufferSize; 52 | output[bin].imag = outputBuffer[2 * bin + IMAG] / bufferSize; 53 | } 54 | } 55 | 56 | // Cleanup 57 | @Override 58 | protected void finalize() { 59 | fftwf_destroy_plan(this.fftPlan); 60 | fftwf_free(this.fftInput); 61 | fftwf_free(this.fftOutput); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/log/charter/dsp/NoteExtractor.java: -------------------------------------------------------------------------------- 1 | package log.charter.dsp; 2 | 3 | import java.util.Arrays; 4 | 5 | public class NoteExtractor { 6 | 7 | // Declare reference frequency of A4 8 | private static final float A4 = 440f; 9 | 10 | // Go down as low as C0 (57 semitones below A4) 11 | private static final float SEMITONES_BELOW_A4 = 57; 12 | 13 | // Span 8 octaves 14 | private static final int OCTAVES = 8; 15 | private static final int NOTES_PER_OCTAVE = 12; 16 | private static final int NOTE_COUNT = OCTAVES * NOTES_PER_OCTAVE; 17 | 18 | // 1/4 tone resolution 19 | private static final float RESOLUTION = 0.25f; 20 | private static final int BIN_COUNT = (int)(NOTE_COUNT * (0.5f / RESOLUTION)); 21 | private static final int BINS_PER_OCTAVE = (int)(NOTES_PER_OCTAVE * (0.5f / RESOLUTION)); 22 | 23 | private final float[] frequencyBins = new float[BIN_COUNT]; 24 | 25 | private final FourierTransform fft; 26 | private final float[] fftInput; 27 | private final Complex[] fftOutput; 28 | 29 | private final float[] window; 30 | 31 | NoteExtractor(final int bufferSize, final int sampleRate) { 32 | this(bufferSize, sampleRate, 0); 33 | } 34 | 35 | NoteExtractor(final int bufferSize, final int sampleRate, final int centOffset) { 36 | if (bufferSize > sampleRate) { 37 | throw new IllegalArgumentException("NoteExtractor input buffer must be less than 1 second in length."); 38 | } 39 | 40 | // Calculate the frequency bins 41 | final float ref_frequency = A4 * (float)Math.pow(2, centOffset / (NOTES_PER_OCTAVE * 1000f)); 42 | for (int bin = 0; bin < this.frequencyBins.length; ++bin) { 43 | frequencyBins[bin] = ref_frequency * (float)Math.pow(2, (bin - SEMITONES_BELOW_A4 * (0.5f / RESOLUTION)) / (float)BINS_PER_OCTAVE); 44 | } 45 | 46 | // Initialize the FFT with a buffer size equal to the sample rate 47 | // This makes the outputs precisely 1Hz apart and simplifies calculations 48 | fft = new FourierTransform(sampleRate); 49 | fftInput = fft.allocInput(); 50 | fftOutput = fft.allocOutput(); 51 | 52 | // Initialize the FFT input (zero pad up to the sample rate) 53 | Arrays.fill(fftInput, 0f); 54 | 55 | // Create a window 56 | window = HammingWindow.generate(bufferSize); 57 | } 58 | 59 | public float[] frequencies() { 60 | return this.frequencyBins; 61 | } 62 | 63 | public Complex[] allocOutput() { 64 | Complex[] output = new Complex[BIN_COUNT]; 65 | for (int bin = 0; bin < output.length; ++bin) { 66 | output[bin] = new Complex(); 67 | } 68 | 69 | return output; 70 | } 71 | 72 | public void execute(float[] input, Complex[] output) { 73 | if (input.length != window.length) { 74 | throw new IllegalArgumentException("NoteExtractor input size does not match the specified buffer size."); 75 | } 76 | 77 | // Copy the input signal into the FFT input buffer 78 | // The buffer is already appropriately zero padded from when it was initialized 79 | for (int i = 0; i < input.length; ++i) { 80 | fftInput[i] = input[i] * window[i]; 81 | } 82 | 83 | fft.execute(fftInput, fftOutput); 84 | 85 | // Interpolate the results to line up with musical notes 86 | for (int bin = 0; bin < frequencyBins.length; ++bin) { 87 | final float frequency = frequencyBins[bin]; 88 | 89 | // Because FFT outputs line up with their corresponding frequencies 90 | // fLow and fHigh can be used directly to index fftOutput 91 | final int fLow = (int)Math.floor(frequency); 92 | final int fHigh = fLow + 1; 93 | 94 | // Multiply all indexes by 2 because the output is complex and takes 2 indexes per number 95 | output[bin].real = fftOutput[fLow].real + (frequency - fLow) * (fftOutput[fHigh].real - fftOutput[fLow].real); 96 | output[bin].imag = fftOutput[fLow].imag + (frequency - fLow) * (fftOutput[fHigh].imag - fftOutput[fLow].imag); 97 | } 98 | } 99 | } 100 | --------------------------------------------------------------------------------