FFT Guitar Synthesizer

A hardware/software project combining an LPCXpresso microcontroller, Fast Fourier Transforms, Bluetooth, and an Android synthesizer to detect guitar notes in real time and reproduce them digitally.

FFT on the LPCXpresso

The FFT was implemented directly on the LPCXpresso so that only minimal data needed to travel over Bluetooth, reducing latency. After evaluating several libraries, we settled on integer_fft.c — a lightweight C library that accepts time-domain integer samples and returns real and imaginary frequency-domain components.

We verified our sampling rate empirically by capturing waveform-generator output in MATLAB and counting samples per wavelength, confirming an actual ADC rate of 15.463 kHz. The peak frequency is then found within the expected guitar range (75 – 500 Hz) and its array index (0 – 511) is sent over Bluetooth UART in a single byte.


Android Synthesizer — playNote

Rather than running sound generation in an infinite loop, each button press triggers playNote(), which fills a buffer of frame_size × note_length samples with a sine wave, then writes it to Android's AudioTrack frame by frame.

Synthesizer.java
public void playNote(float freq, int amp, int note_length) {
    int i, j;
    short samples_arr[] = new short[frame_size * note_length];

    for (i = 0; i < frame_size * note_length; i++) {
        samples_arr[i] = (short) (amp * ((float) Math.sin(ph)));
        ph += twopi * freq / sr;
    }

    for (i = 0; i < note_length; i++) {
        // second arg is the offset into the array
        audioTrack.write(samples_arr, i * frame_size, frame_size);
    }
}

Volume Envelope (Attack & Decay)

A simple two-phase envelope shapes the amplitude of each note: it ramps from zero to peak during the attack phase, then decays back to zero over the remainder of the note. The resulting scalar is multiplied into each sample before it is written to the buffer.

Envelope.java
if (i < attack) {
    envelope = (float) i / (float) attack;
} else {
    envelope = (1 - ((float) i) / ((float) buffer_size * note_length));
}

// multiply envelope into the waveform sample
samples_arr[i] = (short) (amp * envelope * ((float) Math.sin(ph)));

MainBuffer — Polyphonic Playback

To play multiple notes simultaneously, incoming note data is accumulated into a circular MainBuffer (capacity: 10 notes) before being sent to the audio system. Each frame position carries a validity flag so that already-played frames are not replayed.

The main loop simply checks whether a valid frame is ready and, if so, plays it:

MainActivity.java — main loop
if (mainBuff.playReady()) {   // a valid frame is waiting
    mainBuff.playNextFrame();
}

playNextFrame() advances a playhead through the circular buffer, wrapping back to zero once the end is reached:

MainBuffer.java
public void playNextFrame() {
    playFrame(playhead);
    playhead++;
    if (playhead == note_length * 10) {  // reached end of circular buffer
        playhead = 0;
    }
}

Writing a new note adds its samples to whatever is already in the buffer, producing the sensation of two notes sounding simultaneously:

MainBuffer.java — writeSample
// additive mix: new note blends with any already-playing audio
data[offset] += datapt;

Bluetooth Transmission

The Android side is based on the BluetoothChat sample project, with the UUID updated to accept serial input from the LPCXpresso. The single received byte — the FFT peak index — is converted back to a frequency and queued into the synthesizer:

BluetoothReceiver.java
// Reconstruct the frequency from the FFT peak index
fr = (float) passval0 / (float) NUM_SAMPLES * (float) ADC_SAMPLE_RATE;

// Queue the note into the polyphonic synthesizer
mainBuff.addNote(fr, amp, att, nl, false, wf);