- Edited
Hi there! First post ) Me and my friend are very new to c++ but are trying to code a psychological experiment, using a MIDI-keyboard and the bela mini. We are currently too tired to function, but heres the state of things:
Our experiment is meant to work in such a way that participants are presented with 4 metronome tones (here "cuetones"), and then afterwards they have to keep tapping at that tempo for 24 tones. In the condition that we show here, it is set up so two of the participants have to press together, in order for the bela to generate a sound, while the third participant can play a tone alone. The âlonerâ and the âsubgroupâ take turns playing a tone. The timing of each keypress is recorded together with information about the participant, the group, the condition and the trial number.
Our code is currently a wonky combination of code from other scripts, the bela yt channel and AI and we've desperately moved things around â so please donât pay too much attention to the comments :â)
We have been told that we can solve an issue regarding an underrun warning, and to be certain about our timing, by moving the midi event processing into the âaudioFrames loopâ that runs every frame, which currently only includes actually generating the sounds. Weâve tried to move the lines 114 to 249 into the loop starting at line 250, but this completely ruined the sound generation and introduced ungodly amounts of distortion.
On top of this, we have the problem that the bela seems to stay silent for too long after the fourth metronome beat, so that pressing in time with the metronome will not generate a tone for the first tap.
We hope a kind soul will show mercy and give us a helping hand - have a nice weekend !
Here's our poor code
#include <Bela.h>
#include <libraries/Midi/Midi.h>
#include <stdlib.h>
#include <cmath>
#include <fstream>
// #include <libraries/WriteFile/WriteFile.h>
#include <chrono>
#include <string>
#include <iomanip> // for std::fixed and std::setprecision
#include <set> // Include for std::set
#include <algorithm> // For std::includes
std::set<int> activeParticipants; // Set to track currently active participants
const std::set<int> requiredParticipants = {1, 2, 3}; // Define required participants
const std::set<int> lonerParticipants = {3}; // Define required participants
const std::set<int> subgroupParticipants = {1, 2}; // Define required participants
// Global variables
// Replace WriteFile global variables with std::ofstream
std::ofstream gDataFile;
int gCurrentGroup = 0;
const char* gCurrentCondition = "Subgroup";
int gCurrentTrial = 1;
int participant = 0;
// MIDI setup
Midi gMidi;
const char* gMidiPort0 = "hw:1,0,0";
std::string gCurrentPhase;
unsigned long gStartTime;
int gTapCount = 0;
std::vector<float> logs(6); // Create a vector to store the log data
// Oscillator state - generate the tone
float gPhase = 0;
float gFrequency = 0;
float gAmplitude = 0;
double freq[6] = {440.0, 493.88, 554.37, 659.26, 739.99, 880.0}; //initializing array of 7 tones from pentatonic a-major scale
int tone_nr = 0;
// List of active notes
const int kMaxNotes = 16;
int gActiveNotes[kMaxNotes];
int gActiveNoteCount = 0;
// Cue tone variable
int CueTone = 0;
int cueTonePlayCount = 0;
int cueToneCounter = 0;
bool playingCueTone = true;
int gTotalTaps = 0;
bool gTrialComplete = false;
float audioSampleRate = 0;
bool setup(BelaContext *context, void *userData)
{
// print statement to check samplerate
rt_printf("Audio sample rate: %f\n", context->audioSampleRate);
audioSampleRate = context->audioSampleRate;
// Initialise the MIDI device
if(gMidi.readFrom(gMidiPort0) < 0) {
rt_printf("Unable to read from MIDI port %s\n", gMidiPort0);
return false;
}
gMidi.writeTo(gMidiPort0);
gMidi.enableParser(true);
// Initialize start time
gStartTime = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
// Read parameters
std::ifstream params("/root/Bela/projects/Subgroup/current_params");
if(!params) {
rt_printf("Warning: Could not read parameters file\n");
gCurrentGroup = 0;
gCurrentCondition = "Subgroup";
gCurrentTrial = 1;
} else {
params >> gCurrentGroup >> gCurrentTrial;
rt_printf("Parameters loaded: group=%d, condition=%s, trial=%d\n",
gCurrentGroup, gCurrentCondition, gCurrentTrial);
}
// Setup data logging with CSV file
char filename[100];
sprintf(filename, "/root/Bela/projects/Subgroup/data/trial_%d_%s_%d.csv",
gCurrentGroup, gCurrentCondition, gCurrentTrial);
gDataFile.open(filename);
if(!gDataFile.is_open()) {
rt_printf("Error: Could not open data file %s\n", filename);
return false;
}
// Write CSV header
gDataFile << "timestamp,participant,group,condition,trial" << std::endl;
rt_printf("Data logging setup for file: %s\n", filename);
gTotalTaps = 0;
gTrialComplete = false;
return true;
}
void render(BelaContext *context, void *userData)
{
//loop through audioframes
// At the beginning of each callback, look for available MIDI
// messages that have come in since the last block
while(gMidi.getParser()->numAvailableMessages() > 0) {
MidiChannelMessage message;
message = gMidi.getParser()->getNextChannelMessage();
message.prettyPrint(); // Print the message data
// A MIDI "note on" message type might actually hold a real
// note onset (e.g. key press), or it might hold a note off (key release).
// The latter is signified by a velocity of 0.
if(message.getType() == kmmNoteOn) {
int noteNumber = message.getDataByte(0);
int velocity = message.getDataByte(1);
// Velocity of 0 is really a note off
if(velocity > 0) {
if (!gTrialComplete && gActiveNoteCount < kMaxNotes) {
// Determine participant based on note number
if (noteNumber == 48) participant = 1;
else if (noteNumber == 59) participant = 2;
else if (noteNumber == 71) participant = 3;
else participant = 0; // Invalid participant key
if (participant > 0) {
// Log the keypress immediately
unsigned long currentTime = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
float timestamp = (currentTime - gStartTime) / 1000.0;
if (gDataFile.is_open()) {
gDataFile << std::fixed << std::setprecision(3)
<< timestamp << "," << participant << ","
<< gCurrentGroup << "," << gCurrentCondition << ","
<< gCurrentTrial << std::endl;
rt_printf("Logged: time=%.3f, participant=%d, group=%d, condition=%s, trial=%d\n",
timestamp, participant, gCurrentGroup, gCurrentCondition, gCurrentTrial);
}
// Add participant to activeParticipants for synchronization tracking
activeParticipants.insert(participant);
// Check if all required participants have pressed their keys
if (std::includes(activeParticipants.begin(), activeParticipants.end(),
subgroupParticipants.begin(), subgroupParticipants.end()) ||
std::includes(activeParticipants.begin(), activeParticipants.end(),
lonerParticipants.begin(), lonerParticipants.end())) {
// All required participants pressed keys simultaneously
gActiveNotes[gActiveNoteCount] = noteNumber;
gActiveNoteCount++;
gFrequency = freq[tone_nr];
gAmplitude = 0.5;
// Reset activeParticipants for the next synchronization event
activeParticipants.clear();
// Increment total taps only for synchronized events
gTotalTaps++;
if (tone_nr < 7) {
tone_nr++;
}
else {
tone_nr = 0;
}
rt_printf("Tap %d/24: participant=%d\n", gTotalTaps, participant);
if (gTotalTaps >= 24) {
gTrialComplete = true;
rt_printf("\nTrial complete! 24 taps recorded.\n");
}
}
}
}
}
}
else if(message.getType() == kmmNoteOff) {
// We can also encounter the "note off" message type which is the same
// as "note on" with a velocity of 0.
int noteNumber = message.getDataByte(0);
// When we receive a note off, it might be the most recent note
// that we played, or it might be an earlier note. We need to figure
// out which indexes correspond to this note number.
bool activeNoteChanged = false;
// Go through all the active notes and remove any with this number
for(int i = gActiveNoteCount - 1; i >= 0; i--) {
if(gActiveNotes[i] == noteNumber) {
// Found a match: is it the most recent note?
// TODO 1: if the note is the most recent, set the flag
// that says we will change the active note (activeNoteChanged)
// But how do we know if this note in the array is the most
// recent one? (hint: it depends on the value of i)
if (i == gActiveNoteCount-1) {
activeNoteChanged = true;
}
// TODO 2: move all the later notes to be one slot earlier in the
// array. Hint: you will need another for() loop with a new
// index, st arting from "i" and counting upward
for (int j = i; j < gActiveNoteCount-1; j++){
gActiveNotes[j] = gActiveNotes[j + 1];
}
// TODO 3: decrease the number of active notes
gActiveNoteCount--;
}
}
rt_printf("Note off: %d notes remaining\n", gActiveNoteCount);
if(gActiveNoteCount == 0) {
// No notes left
gAmplitude = 0;
}
//else if(activeNoteChanged) {
// Update the frequency but don't retrigger
// int mostRecentNote = gActiveNotes[gActiveNoteCount - 1];
// gFrequency = powf(2.0, (mostRecentNote - 69)/12.0) * 440.0; //convert to frequency
// rt_printf("Note changed: new frequency %f\n", gFrequency);
//}
}
}
//Cue Tones
for(unsigned int n = 0; n < context->audioFrames; n++) {
float value = 0;
// Cue Tone Handling
if (playingCueTone) {
if (cueTonePlayCount < 4) {
// Play cue tone for 0.5 seconds, then silence for 0.5 seconds
if (cueToneCounter < audioSampleRate / 2 ){ // Play tone
gFrequency = 440.0;
gAmplitude = 0.5;
} else if (cueToneCounter < audioSampleRate) { // Silence
gAmplitude = 0.0;
} else { // Reset cycle
cueToneCounter = 0;
cueTonePlayCount++;
}
cueToneCounter++; // Increment the sample counter
value = sin(gPhase) * gAmplitude; // Generate cue tone wave
gPhase += 2.0 * M_PI * gFrequency / audioSampleRate;
if (gPhase > 2.0 * M_PI) { // Ensure phase wraps correctly
gPhase -= 2.0 * M_PI;
}
} else {
// Cue tone finished
playingCueTone = false; // Disable cue tone mode
cueToneCounter = 0; // Reset counter
gAmplitude = 0.0; // Ensure silence is cleared
gPhase = 0.0f; // Reset phase
}
}
// Normal Note Generation (Outside Cue Tone Block)
else if (gActiveNoteCount > 0) {
gPhase += 2.0 * M_PI * gFrequency / audioSampleRate;
if (gPhase > 2.0 * M_PI) {
gPhase -= 2.0 * M_PI;
}
value = sin(gPhase) * gAmplitude;
}
// Write audio output for each channel
for (unsigned int ch = 0; ch < context->audioOutChannels; ++ch) {
audioWrite(context, n, ch, value);
}
// Log the keypress immediately
unsigned long currentTime = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
float timestamp = (currentTime - gStartTime) / 1000.0;
if (gDataFile.is_open()) {
gDataFile << std::fixed << std::setprecision(3)
<< timestamp << "," << participant << ","
<< gCurrentGroup << "," << gCurrentCondition << ","
<< gCurrentTrial << std::endl;
rt_printf("Logged: time=%.3f, participant=%d, group=%d, condition=%s, trial=%d\n",
timestamp, participant, gCurrentGroup, gCurrentCondition, gCurrentTrial);
}
}
}
void cleanup(BelaContext *context, void *userData)
{
if(gDataFile.is_open()) {
gDataFile.close();
rt_printf("Data file closed\n");
}
}