Created
August 22, 2024 14:00
-
-
Save mazbox/b4f2064f1f27288f1dfad6daec16a6cc to your computer and use it in GitHub Desktop.
Get audio / midi prototyping testbed up and running in C++ in one file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
/* | |
Get audio / midi prototyping testbed up and running in C++ in one file | |
Included in this comment are a basic example of usage and a CMakeLists.txt file to build the example (including | |
CPM installation of RtAudio and RtMidi). | |
############################################################################################## | |
## BASIC EXAMPLE | |
#include <iostream> | |
#include "AudioMidiIO.h" | |
#include <unistd.h> | |
int note = 0; | |
float mtof(int midi) { | |
return 440.0f * powf(2.0f, (midi - 69) / 12.0f); | |
} | |
void processStereoAudio(float *, float *outs, int numFrames) { | |
float freq = mtof(note); | |
static double phase = 0.0; | |
for (int i = 0; i < numFrames; i++) { | |
outs[i * 2] = outs[i * 2 + 1] = sin(phase) * (note ? 0.5f : 0.0f); | |
phase += M_PI * 2.0 * freq / 44100.0; | |
} | |
} | |
void midiMessageReceived(const MidiMessage &m) { | |
if (m.isNoteOn()) { | |
std::cout << "Note on: " << m.pitch << std::endl; | |
note = m.pitch; | |
} else if (m.isNoteOff()) { | |
std::cout << "Note off: " << m.pitch << std::endl; | |
note = 0; | |
} | |
} | |
int main() { | |
auto midiIns = std::make_unique<AllMidiIns>([](double, const MidiMessage &m) { midiMessageReceived(m); }); | |
auto audioIO = std::make_unique<AudioIO>( | |
[](float *ins, float *outs, int numFrames) { processStereoAudio(ins, outs, numFrames); }); | |
while (1) | |
sleep(1); | |
} | |
########################################################################################### | |
# CMakeLists.txt | |
cmake_minimum_required(VERSION 3.12) | |
set(PROJECT_NAME piezobongo) | |
project(${PROJECT_NAME}) | |
set(CMAKE_CXX_STANDARD 20) | |
set(CMAKE_CXX_STANDARD_REQUIRED ON) | |
# Download CPM.cmake module | |
if (NOT EXISTS "${CMAKE_BINARY_DIR}/cmake/CPM.cmake") | |
message(STATUS "Downloading CPM.cmake module...") | |
file(DOWNLOAD | |
"https://github.com/TheLartians/CPM.cmake/releases/latest/download/cpm.cmake" | |
"${CMAKE_BINARY_DIR}/cmake/CPM.cmake" | |
SHOW_PROGRESS | |
) | |
endif () | |
# Include CPM.cmake module | |
include(${CMAKE_BINARY_DIR}/cmake/CPM.cmake) | |
# Define CPM options | |
option(CPM_USE_LOCAL_PACKAGES "Use local packages" OFF) | |
option(CPM_USE_PACKAGE_ONLY_MODE "Use package only mode" ON) | |
# need this to get around build target naming conflicts between RtAudio and RtMidi | |
set(RTMIDI_TARGETNAME_UNINSTALL "unin---stall" CACHE STRING "Name of 'uninstall' build target" FORCE) | |
set(RTMIDI_BUILD_TESTING OFF CACHE BOOL "Build test programs for RTMIDI" FORCE) | |
# Import RtAudio using CPM | |
CPMAddPackage( | |
NAME RtAudio | |
GIT_REPOSITORY https://github.com/thestk/rtaudio.git | |
GIT_TAG 6.0.1 | |
) | |
# Import RtMidi using CPM | |
CPMAddPackage( | |
NAME RtMidi | |
GIT_REPOSITORY https://github.com/thestk/rtmidi.git | |
GIT_TAG 6.0.0 | |
) | |
# Set the source files for the executable | |
set(SOURCES main.cpp) | |
# Include RtAudio and RtMidi | |
include_directories(${RtAudio_SOURCE_DIR}/include) | |
include_directories(${RtMidi_SOURCE_DIR}/include) | |
# Create the executable | |
add_executable(${PROJECT_NAME} ${SOURCES}) | |
# Link RtAudio and RtMidi libraries to the executable | |
target_link_libraries(${PROJECT_NAME} PRIVATE rtaudio rtmidi) | |
*/ | |
#include <RtAudio.h> | |
#include <RtMidi.h> | |
#include <functional> | |
#include <vector> | |
#include <memory> | |
#include <iostream> | |
#include <stdint.h> | |
#include <string.h> // for memcpy() | |
#include <algorithm> | |
////////////////////////////////////////////////////////////////////////// | |
// CONFIG | |
////////////////////////////////////////////////////////////////////////// | |
#define SAMPLE_RATE 44100 | |
#define FRAMES_PER_BUFFER 256 | |
////////////////////////////////////////////////////////////////////////// | |
// AUDIO | |
////////////////////////////////////////////////////////////////////////// | |
class AudioIO { | |
public: | |
RtAudio audio; | |
using AudioCallback = std::function<void(float *, float *, int)>; | |
explicit AudioIO(AudioCallback callback) : callback(std::move(callback)) { | |
if (audio.getDeviceCount() < 1) { | |
std::cerr << "No audio devices found!" << std::endl; | |
return; | |
} | |
RtAudio::StreamParameters inputParams, outputParams; | |
inputParams.deviceId = audio.getDefaultInputDevice(); | |
inputParams.nChannels = 2; | |
inputParams.firstChannel = 0; | |
outputParams.deviceId = audio.getDefaultOutputDevice(); | |
outputParams.nChannels = 2; | |
outputParams.firstChannel = 0; | |
unsigned int bufferFrames = FRAMES_PER_BUFFER; | |
audio.openStream(&outputParams, &inputParams, RTAUDIO_FLOAT32, SAMPLE_RATE, &bufferFrames, | |
&AudioIO::audioCallback, this); | |
audio.startStream(); | |
} | |
AudioCallback callback; | |
// This routine will be called by the RtAudio engine when audio is needed. | |
static int audioCallback(void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames, | |
double streamTime, RtAudioStreamStatus status, void *userData) { | |
auto *out = static_cast<float *>(outputBuffer); | |
auto *in = static_cast<float *>(inputBuffer); | |
static_cast<AudioIO *>(userData)-> | |
callback(in, out, nBufferFrames); | |
return 0; | |
} | |
~AudioIO() { | |
audio.stopStream(); | |
if (audio.isStreamOpen()) audio.closeStream(); | |
} | |
}; | |
////////////////////////////////////////////////////////////////////////// | |
// MIDI | |
////////////////////////////////////////////////////////////////////////// | |
#define MIDI_UNKNOWN 0x00 | |
// channel voice messages | |
#define MIDI_NOTE_OFF 0x80 | |
#define MIDI_NOTE_ON 0x90 | |
#define MIDI_CONTROL_CHANGE 0xB0 | |
#define MIDI_PROGRAM_CHANGE 0xC0 | |
#define MIDI_PITCH_BEND 0xE0 | |
#define MIDI_AFTERTOUCH 0xD0 | |
#define MIDI_POLY_AFTERTOUCH 0xA0 | |
// system messages | |
#define MIDI_SYSEX 0xF0 // 240 | |
#define MIDI_TIME_CODE 0xF1 | |
#define MIDI_SONG_POS_POINTER 0xF2 | |
#define MIDI_SONG_SELECT 0xF3 | |
#define MIDI_TUNE_REQUEST 0xF6 | |
#define MIDI_SYSEX_END 0xF7 | |
#define MIDI_TIME_CLOCK 0xF8 // AKA midi *BEAT* clock | |
#define MIDI_START 0xFA // 250 in decimal | |
#define MIDI_CONTINUE 0xFB | |
#define MIDI_STOP 0xFC | |
#define MIDI_ACTIVE_SENSING 0xFE | |
#define MIDI_SYSTEM_RESET 0xFF | |
// just a note for me, a CC at 123 is all notes off | |
#define MIDI_CC_ALL_NOTES_OFF 123 | |
#define MIDI_CC_SUSTAIN_PEDAL 64 | |
struct MidiMessage { | |
int status = 0; | |
int channel = 0; | |
union { | |
int pitch; | |
int control; | |
}; | |
union { | |
int velocity; | |
int value; | |
}; | |
MidiMessage() {} | |
MidiMessage(int status) | |
: status(status) {} | |
MidiMessage(const uint8_t *bytes, int length) { setFromBytes(bytes, length); } | |
MidiMessage(const std::vector<uint8_t> &bytes) { setFromBytes(bytes.data(), (int) bytes.size()); } | |
~MidiMessage() = default; | |
MidiMessage(const MidiMessage &other) { *this = other; } | |
MidiMessage &operator=(const MidiMessage &other) { | |
if (this != &other) { | |
auto data = other.getBytes(); | |
setFromBytes(data.data(), data.size()); | |
} | |
return *this; | |
} | |
[[nodiscard]] bool isNoteOn() const { return status == MIDI_NOTE_ON && velocity > 0; } | |
[[nodiscard]] bool isNoteOff() const { | |
return status == MIDI_NOTE_OFF || (status == MIDI_NOTE_ON && velocity == 0); | |
} | |
[[nodiscard]] bool isPitchBend() const { return status == MIDI_PITCH_BEND; } | |
[[nodiscard]] bool isModWheel() const { return status == MIDI_CONTROL_CHANGE && control == 1; } | |
[[nodiscard]] bool isCC() const { return status == MIDI_CONTROL_CHANGE; } | |
[[nodiscard]] bool isPC() const { return status == MIDI_PROGRAM_CHANGE; } | |
[[nodiscard]] bool isSysex() const { return status == MIDI_SYSEX; } | |
[[nodiscard]] bool isAllNotesOff() const { | |
return status == MIDI_CONTROL_CHANGE && control == MIDI_CC_ALL_NOTES_OFF; | |
} | |
[[nodiscard]] bool isPolyPressure() const { return status == MIDI_POLY_AFTERTOUCH; } | |
[[nodiscard]] bool isChannelPressure() const { return status == MIDI_AFTERTOUCH; } | |
[[nodiscard]] bool isSongPositionPointer() const { return status == MIDI_SONG_POS_POINTER; } | |
[[nodiscard]] static MidiMessage noteOn(int channel, int pitch, int velocity) { | |
MidiMessage m; | |
m.status = MIDI_NOTE_ON; | |
m.channel = channel; | |
m.velocity = velocity; | |
m.pitch = pitch; | |
return m; | |
} | |
[[nodiscard]] static MidiMessage noteOff(int channel, int pitch) { | |
MidiMessage m; | |
m.status = MIDI_NOTE_OFF; | |
m.channel = channel; | |
m.velocity = 0; | |
m.pitch = pitch; | |
return m; | |
} | |
[[nodiscard]] static MidiMessage cc(int channel, int control, int value) { | |
MidiMessage m; | |
m.status = MIDI_CONTROL_CHANGE; | |
m.channel = channel; | |
m.control = control; | |
m.value = value; | |
return m; | |
} | |
[[nodiscard]] static MidiMessage songPositionPointer(int v) { | |
MidiMessage m(MIDI_SONG_POS_POINTER); | |
m.value = v; | |
return m; | |
} | |
[[nodiscard]] static MidiMessage allNotesOff() { return cc(0, MIDI_CC_ALL_NOTES_OFF, 0); } | |
[[nodiscard]] static MidiMessage clock() { return MidiMessage(MIDI_TIME_CLOCK); } | |
[[nodiscard]] static MidiMessage songStart() { return MidiMessage(MIDI_START); } | |
[[nodiscard]] static MidiMessage songStop() { return MidiMessage(MIDI_STOP); } | |
std::vector<uint8_t> getBytes() const { | |
if (status == MIDI_SYSEX) { | |
return sysexData; | |
} | |
static const std::vector<uint8_t> singleByteMessages{ | |
MIDI_TIME_CLOCK, MIDI_START, MIDI_CONTINUE, MIDI_STOP, MIDI_ACTIVE_SENSING, MIDI_SYSTEM_RESET}; | |
if (std::find(std::begin(singleByteMessages), std::end(singleByteMessages), status) | |
!= std::end(singleByteMessages)) { | |
return {static_cast<uint8_t>(status)}; | |
} | |
std::vector<uint8_t> bytes; | |
bytes.push_back(status + (channel - 1)); | |
switch (status) { | |
case MIDI_NOTE_ON: | |
case MIDI_NOTE_OFF: | |
bytes.push_back(pitch); | |
bytes.push_back(velocity); | |
break; | |
case MIDI_CONTROL_CHANGE: | |
bytes.push_back(control); | |
bytes.push_back(value); | |
break; | |
case MIDI_PROGRAM_CHANGE: | |
case MIDI_AFTERTOUCH: | |
bytes.push_back(value); | |
break; | |
case MIDI_PITCH_BEND: | |
bytes.push_back(value & 0x7F); // lsb 7bit | |
bytes.push_back((value >> 7) & 0x7F); // msb 7bit | |
break; | |
case MIDI_POLY_AFTERTOUCH: | |
bytes.push_back(pitch); | |
bytes.push_back(value); | |
break; | |
case MIDI_SONG_POS_POINTER: | |
bytes.push_back(value & 0x7F); // lsb 7bit | |
bytes.push_back((value >> 7) & 0x7F); // msb 7bit | |
break; | |
default: | |
//printf("Unknown message type : %d\n", status); | |
break; | |
} | |
return bytes; | |
} | |
float getPitchBend() const { | |
if (value > 8192) { | |
return (value - 8191) / 8192.f; | |
} else { | |
return (value - 8192) / 8192.f; | |
} | |
} | |
double getSongPosition() const { | |
if (!isSongPositionPointer()) { | |
return -1.0; | |
} | |
return static_cast<double>((((value >> 7) & 0x7F) << 7) | (value & 0x7F)) / 24.0; | |
} | |
private: | |
std::vector<uint8_t> sysexData; | |
void setFromBytes(const uint8_t *bytes, int length) { | |
if (bytes[0] > MIDI_SYSEX) { | |
status = bytes[0]; | |
channel = 0; | |
} else if (bytes[0] == MIDI_SYSEX) { | |
status = MIDI_SYSEX; | |
channel = 0; | |
sysexData.resize(length); | |
memcpy(sysexData.data(), bytes, length); | |
} else { | |
status = (bytes[0] & 0xF0); | |
channel = (int) (bytes[0] & 0x0F) + 1; | |
} | |
switch (status) { | |
case MIDI_NOTE_ON: | |
case MIDI_NOTE_OFF: | |
pitch = (int) bytes[1]; | |
velocity = (int) bytes[2]; | |
break; | |
case MIDI_CONTROL_CHANGE: | |
control = (int) bytes[1]; | |
value = (int) bytes[2]; | |
break; | |
case MIDI_PROGRAM_CHANGE: | |
case MIDI_AFTERTOUCH: | |
value = (int) bytes[1]; | |
break; | |
case MIDI_PITCH_BEND: | |
value = (int) (bytes[2] << 7) + (int) bytes[1]; // msb + lsb | |
break; | |
case MIDI_POLY_AFTERTOUCH: | |
pitch = (int) bytes[1]; | |
value = (int) bytes[2]; | |
break; | |
case MIDI_SONG_POS_POINTER: | |
value = (int) (bytes[2] << 7) + (int) bytes[1]; // msb + lsb | |
break; | |
default: | |
//printf("Unknown message type : %d\n", status); | |
break; | |
} | |
} | |
}; | |
class AllMidiIns { | |
public: | |
using MidiCallback = std::function<void(double, MidiMessage)>; | |
AllMidiIns(MidiCallback callback) : callback(callback) { | |
scanAndConnect(); | |
} | |
~AllMidiIns() { | |
for (auto &midiIn: midiIns) { | |
midiIn->closePort(); | |
} | |
} | |
void setCallback(MidiCallback callback) { | |
this->callback = callback; | |
} | |
private: | |
std::vector<std::unique_ptr<RtMidiIn>> midiIns; | |
MidiCallback callback; | |
void scanAndConnect() { | |
RtMidiIn mIn; | |
unsigned int nPorts = mIn.getPortCount(); | |
for (unsigned int i = 0; i < nPorts; ++i) { | |
auto midiIn = std::make_unique<RtMidiIn>(); | |
midiIn->openPort(i); | |
std::string portName = mIn.getPortName(i); | |
std::cout << "Connecting to MIDI port: " << portName << std::endl; | |
midiIn->setCallback(&AllMidiIns::midiCallback, this); | |
midiIn->ignoreTypes(false, false, false); | |
midiIns.push_back(std::move(midiIn)); | |
} | |
} | |
static void midiCallback(double deltatime, std::vector<unsigned char> *message, void *userData) { | |
AllMidiIns *self = static_cast<AllMidiIns *>(userData); | |
if (self->callback) { | |
self->callback(deltatime, *message); | |
} | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment