Skip to content

Commit

Permalink
experimental USB audio support
Browse files Browse the repository at this point in the history
requires hardware modification to rev <= 2 boards to connect PWM output pin
  • Loading branch information
aWZHY0yQH81uOYvH committed May 4, 2024
1 parent d0e15b4 commit f008058
Show file tree
Hide file tree
Showing 30 changed files with 1,048 additions and 138 deletions.
65 changes: 57 additions & 8 deletions Emulator/AudioEngine.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "AudioEngine.h"

#include <algorithm>
#include <cassert>

using namespace std;
Expand All @@ -9,11 +10,44 @@ AudioEngine::AudioEngine(list<Coil> &coils): coils(coils), sample(0), lastUpdate
conv.emplace_back(ir, IR_LENGTH, FRAMES_PER_BUFFER);
}

int AudioEngine::genAudio(const void *input, void *_output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData) {
portaudio::StreamParameters AudioEngine::desiredInputParameters(portaudio::Device &d) {
return {portaudio::DirectionSpecificStreamParameters(d, CHANNELS, portaudio::FLOAT32, true, 0, NULL),
portaudio::DirectionSpecificStreamParameters::null(),
F_SAMP,
FRAMES_PER_BUFFER,
0};
}

portaudio::StreamParameters AudioEngine::desiredOutputParameters(portaudio::Device &d) {
return {portaudio::DirectionSpecificStreamParameters::null(),
portaudio::DirectionSpecificStreamParameters(d, CHANNELS, portaudio::FLOAT32, true, 0, NULL),
F_SAMP,
FRAMES_PER_BUFFER,
0};
}

int AudioEngine::inputCallback(const void *_input, void *output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData) {

AudioEngine &engine = *static_cast<AudioEngine*>(userData);
const float *input = static_cast<const float*>(_input);

frameCount *= CHANNELS;

for(; frameCount && engine.inputBuffer.size() < MAX_FIFO_SIZE; frameCount--)
engine.inputBuffer.push(*input++);

return paContinue;
}

int AudioEngine::outputCallback(const void *input, void *_output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData) {

AudioEngine &engine = *static_cast<AudioEngine*>(userData);
float *output = static_cast<float*>(_output);
Expand All @@ -35,15 +69,30 @@ void AudioEngine::generate(float *output) {
for(size_t ind = 0; ind < FRAMES_PER_BUFFER; ind++, sample++) {
float lout = 0, rout = 0;

if(inputBuffer.size() >= CHANNELS)
for(unsigned int chan = 0; chan < CHANNELS; chan++)
inputSamples[chan] = inputBuffer.pop();

for(auto &coil:coils) {
float lweight = 1, rweight = 1;
float inputSample;

if(coil.aoMode == Coil::LEFT)
if(coil.aoMode == Coil::LEFT) {
rweight = STEREO_SEPARATION;
else if(coil.aoMode == Coil::RIGHT)
inputSample = inputSamples[0];
} else if(coil.aoMode == Coil::RIGHT) {
lweight = STEREO_SEPARATION;
inputSample = inputSamples[1];
} else // Mono
inputSample = (inputSamples[0] + inputSamples[1]) / 2;

// MIDI voices
float sample = coil.getNextSample();

// Processed audio
sample += min(max(coil.audio.processSample(inputSample * 0x7FFF) / (float)(F_CPU/NOM_SAMPLE_RATE), -1.0f), 1.0f);

const float sample = coil.getNextSample() * VOLUME;
sample *= VOLUME;

lout += lweight * sample;
rout += rweight * sample;
Expand Down
17 changes: 14 additions & 3 deletions Emulator/AudioEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#define FRAMES_PER_BUFFER 128
#define CHANNELS 2

#define MAX_FIFO_SIZE (FRAMES_PER_BUFFER*CHANNELS*4)
#define MAX_FIFO_SIZE (FRAMES_PER_BUFFER*CHANNELS*16)

#define VOLUME 0.8f
#define STEREO_SEPARATION 0.4f
Expand All @@ -27,16 +27,27 @@ class AudioEngine {
public:
AudioEngine(std::list<Coil> &coils);

// portaudio callback
static int genAudio(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData);
// Desired portaudio stream parameters
static portaudio::StreamParameters desiredInputParameters(portaudio::Device &d);
static portaudio::StreamParameters desiredOutputParameters(portaudio::Device &d);

// portaudio callbacks
static int inputCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData);
static int outputCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData);

protected:
std::list<Coil> &coils;

// Buffers before and after processing
RingBuffer<float, MAX_FIFO_SIZE+1> inputBuffer;

// Unprocessed audio generation state
unsigned long sample; // Index of last sample generated
unsigned long lastUpdateSample; // Index of last sample when synth was updated

// Last input samples
float inputSamples[CHANNELS];

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

Expand Down
3 changes: 2 additions & 1 deletion Emulator/Coil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
#include "Coil.h"
#include "AudioEngine.h"

Coil::Coil(uint8_t MIDIbaseChannel, AudioOutputMode aoMode): aoMode(aoMode), _millis(0), midi(this), synth(this), voicesUpdating(0) {
Coil::Coil(uint8_t MIDIbaseChannel, AudioOutputMode aoMode): aoMode(aoMode), _millis(0), midi(this), synth(this), audio(this), voicesUpdating(0) {
midi.MIDIbaseChannel = MIDIbaseChannel;
audio.audioMode = Audio::AM_PWM;
memset(oscillators, 0, sizeof(oscillators));
memset(voices, 0, sizeof(voices));
}
Expand Down
7 changes: 3 additions & 4 deletions Emulator/Coil.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "MIDI.h"
#include "Synth.h"
#include "Audio.h"
#include "Voice.h"

// Class representing a single MIDI controller/Tesla coil pair
Expand Down Expand Up @@ -44,10 +45,11 @@ class Coil {
// millis() time
unsigned long _millis;

protected:
public:
// Instances of synth components that would normally be global scope in the real controller
MIDI midi;
Synth synth;
Audio audio;
Voice::Voice voices[NVOICES];
volatile uint8_t voicesUpdating;

Expand All @@ -57,7 +59,4 @@ class Coil {

// Replacement for Arduino millis() function
unsigned long millis(void);

friend class MIDI;
friend class Synth;
};
4 changes: 2 additions & 2 deletions Emulator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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 Convolution.cpp ir.s
HEADERS=seded/MIDI.h seded/Synth.h patched/Voice.h Coil.h AudioEngine.h Convolution.h RingBuffer.h
SOURCES=seded/MIDI.cpp seded/Synth.cpp seded/Audio.cpp Drum.cpp Coil.cpp AudioEngine.cpp Convolution.cpp ir.s
HEADERS=seded/MIDI.h seded/Synth.h patched/Voice.h seded/Audio.h Coil.h AudioEngine.h Convolution.h RingBuffer.h

# Get portaudio flags from pkg-config
PORTAUDIO_CFLAGS:=$(shell pkg-config --cflags portaudiocpp)
Expand Down
4 changes: 2 additions & 2 deletions Emulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ Prerequisites
* `sed`
* C++17 compiler

You may need to modify the makefile slightly to get it to find your PortAudio install (it uses `pkg-config` by default) or to select a different backend for `libremidi` depending on your operating system.
You may need to modify the makefile slightly to get it to find the libraries on your system (it uses `pkg-config` by default) or to select a different backend for `libremidi` depending on your operating system.

Running `make` will take the code from the main firmware in this repo, apply some patches to remove MCU-specific code, and use `sed` to convert all global variables into C++ class members so that multiple instances of the synthesizer can be run at a time.

## 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 is approximated using an FIR filter.
When the program is run, it will ask you for audio input and output devices and a MIDI input device. Set the audio input device to a guitar or loopback source, 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
99 changes: 54 additions & 45 deletions Emulator/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <vector>
#include <list>
#include <utility>
#include <memory>

#include <libremidi/libremidi.hpp>
#include <portaudiocpp/PortAudioCpp.hxx>
Expand All @@ -25,15 +26,6 @@ int choose(const char *message, vector<pair<string, int>> choices) {
return choices[choice-1].second;
}

// Generate fixed stream parameters for a given PortAudio device
portaudio::StreamParameters desiredParameters(portaudio::Device &d) {
return {portaudio::DirectionSpecificStreamParameters::null(),
portaudio::DirectionSpecificStreamParameters(d, 2, portaudio::FLOAT32, true, 0, NULL),
F_SAMP,
FRAMES_PER_BUFFER,
0};
}

int main(int argc, char **argv) {

int ncoils = 1;
Expand Down Expand Up @@ -63,52 +55,69 @@ int main(int argc, char **argv) {
coils.emplace_back(4, Coil::RIGHT);
}

// Create audio engine
AudioEngine engine(coils);

// Init portaudio
portaudio::AutoSystem autoSys;
portaudio::System &sys = portaudio::System::instance();

// Select audio output device
vector<pair<string, int>> audioDeviceNames;
for(portaudio::System::DeviceIterator i = sys.devicesBegin(); i != sys.devicesEnd(); i++)
// Make sure this device supports the desired output parameters
if(desiredParameters(*i).isSupported())
audioDeviceNames.emplace_back(i->name(), i->index());

portaudio::Device &device = sys.deviceByIndex(choose("Choose audio output device", audioDeviceNames));

// Create audio engine
AudioEngine engine(coils);

// portaudio stream
portaudio::CFunCallbackStream stream(desiredParameters(device), &AudioEngine::genAudio, &engine);
vector<pair<string, int>> deviceNames;
auto filterDevices = [&](auto desiredParameters) {
for(portaudio::System::DeviceIterator i = sys.devicesBegin(); i != sys.devicesEnd(); i++)
// Make sure this device supports the desired parameters
if(desiredParameters(*i).isSupported())
deviceNames.emplace_back(i->name(), i->index());
};

filterDevices(AudioEngine::desiredOutputParameters);
portaudio::Device &outputDevice = sys.deviceByIndex(choose("Choose audio output device", deviceNames));
portaudio::CFunCallbackStream outputStream(AudioEngine::desiredOutputParameters(outputDevice), &AudioEngine::outputCallback, &engine);

deviceNames.clear();
deviceNames.emplace_back("None", -1);
filterDevices(AudioEngine::desiredInputParameters);
const int inputDeviceInd = choose("Choose audio input device", deviceNames);
unique_ptr<portaudio::CFunCallbackStream> inputStream;
if(inputDeviceInd >= 0) {
portaudio::Device &inputDevice = sys.deviceByIndex(inputDeviceInd);
inputStream.reset(new portaudio::CFunCallbackStream(AudioEngine::desiredInputParameters(inputDevice), &AudioEngine::inputCallback, &engine));
}

// Select MIDI input device
libremidi::midi_in midi;
vector<pair<string, int>> midiDeviceNames;
deviceNames.clear();
deviceNames.emplace_back("None", -1);
for(unsigned int x = 0; x < midi.get_port_count(); x++)
midiDeviceNames.emplace_back(midi.get_port_name(x), x);

midi.open_port(choose("Choose MIDI input device", midiDeviceNames));

midi.set_callback([&](const libremidi::message& message) {
unsigned char pass[3] = {0};
switch(message.size()) {
case 0:
return;
default:
case 3:
pass[2] = message[2];
case 2:
pass[1] = message[1];
case 1:
pass[0] = message[0];
break;
}
for(auto &coil:coils)
coil.handleMIDI(pass);
});
deviceNames.emplace_back(midi.get_port_name(x), x);

const int chosenMidiDevice = choose("Choose MIDI input device", deviceNames);
if(chosenMidiDevice >= 0) {
midi.open_port(chosenMidiDevice);

midi.set_callback([&](const libremidi::message& message) {
unsigned char pass[3] = {0};
switch(message.size()) {
case 0:
return;
default:
case 3:
pass[2] = message[2];
case 2:
pass[1] = message[1];
case 1:
pass[0] = message[0];
break;
}
for(auto &coil:coils)
coil.handleMIDI(pass);
});
}

stream.start();
outputStream.start();
if(inputStream)
inputStream->start();

// Run forever...
while(true) sys.sleep(1e6);
Expand Down
Loading

0 comments on commit f008058

Please sign in to comment.