Hey Giulio, Thanks very much for the quick and informative reply.
We were in a bit of a crisis when we sent in the code yesterday, and it turns out we accidentally added an extra mistake by logging every frame instead of just when midi events happened. This is the code we should have sent:
#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);
}
}
}
void cleanup(BelaContext *context, void *userData)
{
if(gDataFile.is_open()) {
gDataFile.close();
rt_printf("Data file closed\n");
}
}
And here is the output
Running project ...
Audio sample rate: 44100.000000
Parameters loaded: group=2, condition=Subgroup, trial=1
Data logging setup for file: /root/Bela/projects/Subgroup/data/trial_2_Subgroup_1.csv
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 107,
Logged: time=5.730, participant=3, group=2, condition=Subgroup, trial=1
Tap 1/24: participant=3
Underrun detected: 4 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
1 mode switch detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 64,
Logged: time=6.628, participant=3, group=2, condition=Subgroup, trial=1
Tap 2/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 39,
Logged: time=7.302, participant=3, group=2, condition=Subgroup, trial=1
Tap 3/24: participant=3
Underrun detected: 1 blocks dropped
3 mode switches detected on the audio thread.
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 35,
Logged: time=7.871, participant=3, group=2, condition=Subgroup, trial=1
Tap 4/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 60,
Logged: time=8.419, participant=3, group=2, condition=Subgroup, trial=1
Tap 5/24: participant=3
Underrun detected: 1 blocks dropped
5 mode switches detected on the audio thread.
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 53,
Logged: time=8.868, participant=3, group=2, condition=Subgroup, trial=1
Tap 6/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 55,
Logged: time=9.211, participant=3, group=2, condition=Subgroup, trial=1
Tap 7/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 64,
Logged: time=9.507, participant=3, group=2, condition=Subgroup, trial=1
Tap 8/24: participant=3
Underrun detected: 1 blocks dropped
8 mode switches detected on the audio thread.
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 67,
Logged: time=10.100, participant=3, group=2, condition=Subgroup, trial=1
Tap 9/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 72,
Logged: time=10.551, participant=3, group=2, condition=Subgroup, trial=1
Tap 10/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
10 mode switches detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 58,
Logged: time=11.022, participant=3, group=2, condition=Subgroup, trial=1
Tap 11/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 59,
Logged: time=11.434, participant=3, group=2, condition=Subgroup, trial=1
Tap 12/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 60,
Logged: time=11.841, participant=3, group=2, condition=Subgroup, trial=1
Tap 13/24: participant=3
Underrun detected: 1 blocks dropped
13 mode switches detected on the audio thread.
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 71,
Logged: time=12.243, participant=3, group=2, condition=Subgroup, trial=1
Tap 14/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
14 mode switches detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 69,
Logged: time=12.991, participant=3, group=2, condition=Subgroup, trial=1
Tap 15/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 64,
Logged: time=13.547, participant=3, group=2, condition=Subgroup, trial=1
Tap 16/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
16 mode switches detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 117,
Logged: time=14.268, participant=3, group=2, condition=Subgroup, trial=1
Tap 17/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 83,
Logged: time=14.628, participant=3, group=2, condition=Subgroup, trial=1
Tap 18/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 114,
Logged: time=14.983, participant=3, group=2, condition=Subgroup, trial=1
Tap 19/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
19 mode switches detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 100,
Logged: time=15.317, participant=3, group=2, condition=Subgroup, trial=1
Tap 20/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 62,
Logged: time=15.650, participant=3, group=2, condition=Subgroup, trial=1
Tap 21/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 110,
Logged: time=15.980, participant=3, group=2, condition=Subgroup, trial=1
Tap 22/24: participant=3
Underrun detected: 1 blocks dropped
22 mode switches detected on the audio thread.
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 65,
Logged: time=17.085, participant=3, group=2, condition=Subgroup, trial=1
Tap 23/24: participant=3
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
23 mode switches detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 69,
Logged: time=17.522, participant=3, group=2, condition=Subgroup, trial=1
Tap 24/24: participant=3
Trial complete! 24 taps recorded.
Underrun detected: 1 blocks dropped
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 77,
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 94,
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
24 mode switches detected on the audio thread.
type: note on, channel: 0, data1: 71, data2: 76,
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
type: note on, channel: 0, data1: 71, data2: 76,
type: note off, channel: 0, data1: 71, data2: 0,
Note off: 0 notes remaining
Data file closed
Bela stopped