Skip to content

Instantly share code, notes, and snippets.

@mazbox
Created August 22, 2024 14:00
Show Gist options
  • Save mazbox/b4f2064f1f27288f1dfad6daec16a6cc to your computer and use it in GitHub Desktop.
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
#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