Skip to content

Commit

Permalink
improve emulator audio realism
Browse files Browse the repository at this point in the history
convolve output with sampled IR of Tesla Coil + reverb
  • Loading branch information
aWZHY0yQH81uOYvH committed Apr 28, 2024
1 parent 3af6c20 commit ee485c3
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 23 deletions.
23 changes: 14 additions & 9 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y portaudio19-dev libasound2-dev
sudo apt install -y portaudio19-dev libasound2-dev libfftw3-dev
- name: Build
run: make -C Emulator -j`nproc` RELEASE_BUILD=1
- name: Upload
Expand All @@ -70,19 +70,23 @@ jobs:
- name: Install dependencies
run: |
brew update
brew install portaudio coreutils
brew install portaudio fftw coreutils
mkdir -p Emulator/static/x64 Emulator/static/arm64
curl -sL https://formulae.brew.sh/api/formula/portaudio.json -o portaudio.json
ARM_VERSION=$(jq -r '.bottle.stable.files | to_entries[] | .key' portaudio.json | grep -i '^arm64_' | fgrep -ivm1 linux)
X64_VERSION=$(jq -r '.bottle.stable.files | to_entries[] | .key' portaudio.json | grep -Eivm1 '^arm64_|linux')
for lib in portaudio fftw; do
curl -sL https://formulae.brew.sh/api/formula/$lib.json -o $lib.json
ARM_VERSION=$(jq -r '.bottle.stable.files | to_entries[] | .key' $lib.json | grep -i '^arm64_' | fgrep -ivm1 linux)
X64_VERSION=$(jq -r '.bottle.stable.files | to_entries[] | .key' $lib.json | grep -Eivm1 '^arm64_|linux')
echo Using $ARM_VERSION for arm64
echo Using $X64_VERSION for x64
echo Using $ARM_VERSION for arm64 $lib
echo Using $X64_VERSION for x64 $lib
jq -r ".bottle.stable.files.$ARM_VERSION.url" portaudio.json | xargs curl -sLH "Authorization: Bearer QQ==" | tar xf - -C Emulator/static/arm64 --strip-components=3 '*/libportaudio*.a'
jq -r ".bottle.stable.files.$X64_VERSION.url" portaudio.json | xargs curl -sLH "Authorization: Bearer QQ==" | tar xf - -C Emulator/static/x64 --strip-components=3 '*/libportaudio*.a'
jq -r ".bottle.stable.files.$ARM_VERSION.url" $lib.json | xargs curl -sLH "Authorization: Bearer QQ==" | tar xf - -C Emulator/static/arm64 --strip-components=3 '*/lib'$lib'*.a'
jq -r ".bottle.stable.files.$X64_VERSION.url" $lib.json | xargs curl -sLH "Authorization: Bearer QQ==" | tar xf - -C Emulator/static/x64 --strip-components=3 '*/lib'$lib'*.a'
done
ls -lR Emulator/static
- name: Build
run: make -C Emulator -j`nproc` RELEASE_BUILD=1 tcsynth_universal
- name: Upload
Expand Down Expand Up @@ -111,6 +115,7 @@ jobs:
patch
coreutils
mingw-w64-x86_64-portaudio
mingw-w64-x86_64-fftw
mingw-w64-x86_64-toolchain
- uses: actions/checkout@v4
with:
Expand Down
19 changes: 15 additions & 4 deletions Emulator/AudioEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

using namespace std;

AudioEngine::AudioEngine(list<Coil> &coils): coils(coils) {}
AudioEngine::AudioEngine(list<Coil> &coils): coils(coils) {
for(size_t chan = 0; chan < CHANNELS; chan++)
conv.emplace_back(ir, IR_LENGTH, FRAMES_PER_BUFFER);
}

AudioEngine::~AudioEngine() {
stopStream();
Expand Down Expand Up @@ -126,7 +129,15 @@ void AudioEngine::genOutput() {
if(inputBuffer.size() < FRAMES_PER_BUFFER*CHANNELS)
return;

// TODO: process
for(size_t count = FRAMES_PER_BUFFER*CHANNELS; count; count--)
outputBuffer.push(inputBuffer.pop());
for(size_t count = 0; count < FRAMES_PER_BUFFER; count++)
for(size_t chan = 0; chan < CHANNELS; chan++)
conv[chan].feedSample(inputBuffer.pop());

const float *output[CHANNELS];
for(size_t chan = 0; chan < CHANNELS; chan++)
output[chan] = conv[chan].getOutput();

for(size_t count = 0; count < FRAMES_PER_BUFFER; count++)
for(size_t chan = 0; chan < CHANNELS; chan++)
outputBuffer.push(output[chan][count]);
}
18 changes: 14 additions & 4 deletions Emulator/AudioEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
#include <thread>
#include <mutex>
#include <chrono>
#include <queue>
#include <deque>
#include <memory>

#include <portaudiocpp/PortAudioCpp.hxx>

#include "Coil.h"
#include "Convolution.h"
#include "RingBuffer.h"

#define F_SAMP 48000
Expand All @@ -20,8 +21,13 @@
#define MIN_FIFO_SIZE (FRAMES_PER_BUFFER*CHANNELS*2)
#define MAX_FIFO_SIZE (FRAMES_PER_BUFFER*CHANNELS*4)

#define VOLUME 0.5f
#define STEREO_SEPARATION 0.25f
#define VOLUME 0.8f
#define STEREO_SEPARATION 0.4f

// Impulse response data loaded from ir.bin
// Data has been pre-FFTed, so length is actually 2*(IR_LENGTH/2+1) = 4098 floats in RE-IM-RE-IM format
#define IR_LENGTH 4096
extern const float ir[IR_LENGTH/2+1];

class AudioEngine {
public:
Expand All @@ -46,10 +52,14 @@ class AudioEngine {
unsigned long sample; // Index of last sample generated
unsigned long lastUpdateSample; // Index of last sample when synth was updated
std::chrono::steady_clock::time_point wakeTime; // Time when generator thread should wake up
constexpr static std::chrono::duration wakePeriod = std::chrono::milliseconds(1);
constexpr static std::chrono::duration wakePeriod = std::chrono::microseconds(500);
std::atomic<bool> runGenerator;
std::unique_ptr<std::thread> generator;

// Convolution objects to apply IR filter to each channel
std::deque<Convolution> conv;

// Thread function
void generatorThread();

// Generate a single unprocessed sample
Expand Down
82 changes: 82 additions & 0 deletions Emulator/Convolution.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include "Convolution.h"

#include <cstring>
#include <cmath>

Convolution::Convolution(const float *ir, size_t irLen, size_t bufLen): ir(ir), irLen(irLen), bufLen(bufLen), scale(1.0f/sqrtf(irLen)) {
inputBuffer = (float*)fftwf_malloc(irLen * sizeof(float));
memset(inputBuffer, 0, irLen * sizeof(float));
inputBufferInd = 0;

intermediate = (fftwf_complex*)fftwf_malloc((irLen/2+1) * sizeof(fftwf_complex));

outputBuffers.resize(irLen/bufLen);
for(float *&buffer:outputBuffers) {
buffer = (float*)fftwf_malloc(irLen * sizeof(float));
memset(buffer, 0, irLen * sizeof(float));
}
outputBufferInd = 0;

finalBuffer = (float*)fftwf_malloc(bufLen * sizeof(float));

forwardPlan = fftwf_plan_dft_r2c_1d(irLen, inputBuffer, intermediate, 0);
reversePlan = fftwf_plan_dft_c2r_1d(irLen, intermediate, outputBuffers.at(0), 0);
}

Convolution::~Convolution() {
fftwf_free(inputBuffer);
fftwf_free(intermediate);
for(float *buffer:outputBuffers)
fftwf_free(buffer);
fftwf_free(finalBuffer);
fftwf_destroy_plan(forwardPlan);
fftwf_destroy_plan(reversePlan);
}

void Convolution::feedSample(float sample) {
if(inputBufferInd >= bufLen)
return;

// Pre-scale input to keep energy normalized
inputBuffer[inputBufferInd++] = sample * scale;
}

const float *Convolution::getOutput() {
inputBufferInd = 0;

// Compute FFT of incoming data
fftwf_execute(forwardPlan);

// Apply impulse response
for(size_t i = 0; i < irLen/2+1; i++) {
const float * const a = ir + 2*i;
const fftwf_complex &b = intermediate[i];
fftwf_complex result;
result[0] = a[0]*b[0] - a[1]*b[1];
result[1] = a[0]*b[1] + a[1]*b[0];
intermediate[i][0] = result[0];
intermediate[i][1] = result[1];
}

// Compute inverse FFT
fftwf_execute_dft_c2r(reversePlan, intermediate, outputBuffers[outputBufferInd]);

// Compute output
memset(finalBuffer, 0, bufLen * sizeof(float));
ssize_t ind = outputBufferInd;
for(size_t i = 0; i < outputBuffers.size(); i++) {
for(size_t j = 0; j < bufLen; j++)
finalBuffer[j] += outputBuffers[ind][j + i*bufLen];

ind--;
if(ind < 0)
ind = outputBuffers.size()-1;
}

// Rotate output buffers
outputBufferInd++;
if(outputBufferInd >= outputBuffers.size())
outputBufferInd = 0;

return finalBuffer;
}
40 changes: 40 additions & 0 deletions Emulator/Convolution.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <vector>
#include <stddef.h>

#include <fftw3.h>

class Convolution {
public:
Convolution(const float *ir, size_t irLen, size_t bufLen);
~Convolution();

// Feed an input sample; must call bufferLen times between each call to getOutput
void feedSample(float sample);

// Get output buffer
const float *getOutput();

protected:
const float * const ir;
const size_t irLen, bufLen;

const float scale;

// Contiguous input data
float *inputBuffer;
size_t inputBufferInd;

// Intermediate FFT results for convolving
fftwf_complex *intermediate;

// Output buffers to be summed to create continuous impulse response
std::vector<float*> outputBuffers;
size_t outputBufferInd;

// Final summed output buffer
float *finalBuffer;

fftwf_plan forwardPlan, reversePlan;
};
10 changes: 6 additions & 4 deletions Emulator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ BIN=tcsynth
MCU=../Tesla_Coil_MIDI_Synth

# Files to compile and patch if necessary
SOURCES=seded/MIDI.cpp seded/Synth.cpp Drum.cpp Coil.cpp AudioEngine.cpp
HEADERS=seded/MIDI.h seded/Synth.h patched/Voice.h Coil.h AudioEngine.h RingBuffer.h
SOURCES=seded/MIDI.cpp seded/Synth.cpp Drum.cpp Coil.cpp AudioEngine.cpp Convolution.cpp ir.s
HEADERS=seded/MIDI.h seded/Synth.h patched/Voice.h Coil.h AudioEngine.h Convolution.h RingBuffer.h

# Get portaudio flags from pkg-config
PORTAUDIO_CFLAGS:=$(shell pkg-config --cflags portaudiocpp)
PORTAUDIO_LIBS:=$(shell pkg-config --libs portaudiocpp)
FFTW3_CFLAGS:=$(shell pkg-config --cflags fftw3)
FFTW3_LIBS:=$(shell pkg-config --libs fftw3) -lfftw3f

# Specify libremidi based on your platform (some auto-detection below)
# see https://github.com/jcelerier/libremidi/blob/master/docs/header-only.md
LIBREMIDI_CFLAGS:=-DLIBREMIDI_HEADER_ONLY=1
LIBREMIDI_LIBS:=

INCLUDE=-I. -Ibuild/seded -Ibuild/patched -I$(MCU) -Ilibremidi/include
LIBS=$(OTHER_LIBS) $(PORTAUDIO_LIBS) $(LIBREMIDI_LIBS)
CXXFLAGS=$(TARGET) -O2 -Wall -Wextra -Wno-unused-parameter -std=c++17 -DF_CPU=84000000 $(INCLUDE) $(PORTAUDIO_CFLAGS) $(LIBREMIDI_CFLAGS)
LIBS=$(OTHER_LIBS) $(PORTAUDIO_LIBS) $(FFTW3_LIBS) $(LIBREMIDI_LIBS)
CXXFLAGS=$(TARGET) -O2 -Wall -Wextra -Wno-unused-parameter -std=c++17 -DF_CPU=84000000 $(INCLUDE) $(PORTAUDIO_CFLAGS) $(FFTW3_CFLAGS) $(LIBREMIDI_CFLAGS)

OBJS:=$(SOURCES:%.cpp=build/%.o)
HEADERS:=$(HEADERS:patched/%=build/patched/%)
Expand Down
3 changes: 2 additions & 1 deletion Emulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This folder contains code to patch/modify the software that runs on the microcon

Prerequisites
* [`portaudiocpp`](https://github.com/PortAudio/portaudio)
* [`fftw`](https://www.fftw.org)
* [`libremidi`](https://github.com/jcelerier/libremidi)
* GNU `make`
* `patch`
Expand All @@ -18,7 +19,7 @@ Running `make` will take the code from the main firmware in this repo, apply som

## Using

When the program is run, it will ask you for an audio output device and a MIDI input device. Set the audio output device to your headphones and the MIDI input to a loopback device coming out of your DAW. It should respond exactly like the real MIDI controller since it's the exact same code. The sound output is pure square waves, lacking "coloring" from the Tesla coil, but that could probably be approximated by a simple FIR filter...
When the program is run, it will ask you for an audio output device and a MIDI input device. Set the audio output device to your headphones and the MIDI input to a loopback device coming out of your DAW. It should respond exactly like the real MIDI controller since it's the exact same code. The sound is approximated using an FIR filter.

If run with the `--stereo` flag, the program will emulate two Tesla coil controllers, one panned to the left and the other to the right. The right one responds to MIDI channels starting at 5 instead of 1.

Expand Down
Binary file added Emulator/ir.bin
Binary file not shown.
5 changes: 5 additions & 0 deletions Emulator/ir.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.global ir
.global _ir
ir:
_ir:
.incbin "ir.bin"
2 changes: 1 addition & 1 deletion Tesla_Coil_MIDI_Synth/Version.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#pragma once

#define VERSION "v1.1"
#define VERSION "v1.2"

0 comments on commit ee485c3

Please sign in to comment.