diff --git a/Networking-Test-Kit/OSC/max-fft-parsing.js b/Networking-Test-Kit/OSC/max-fft-parsing.js new file mode 100644 index 000000000..9e921faea --- /dev/null +++ b/Networking-Test-Kit/OSC/max-fft-parsing.js @@ -0,0 +1,208 @@ +// OpenBCI FFT Data Parser for Max/MSP +// PROVIDED AS-IS UNDER MIT LICENSE. NO WARRANTY OF ANY KIND. USE AT YOUR OWN RISK. +// Usage: +// [udpreceive ] -> (route or oscbank) -> [js max-fft-parsing.js] +// Send list messages of the form: parseOSC +// Example: parseOSC /openbci/fft/ch0/bin4 1.815383 +// +// Provided JS functions (send as messages to the js object): +// parseOSC
// Update internal buffer for a single bin +// getChannel // Output all bins for channel on outlet 1 +// getBandPower // Average power over bin range on outlet 2 +// getEEGBands // Outputs delta, theta, alpha, beta, gamma on outlet 3 +// getPeakFrequency // Outputs peak frequency (Hz) + amplitude on outlet 4 +// outputMatrix // Outputs entire channel arrays on outlet 5 +// setChannels // Resize data structure (#channels) +// setBins // Resize number of bins +// clear // Reset all stored values to 0 +// testOutlets // Emits a test message from every outlet +// +// Outlets: +// 0: bin_data +// 1: channel_data +// 2: band_power +// 3: eeg_bands +// 4: peak_freq +// 5: matrix +// +// NOTE: Declare outlets BEFORE doing work so Max creates them. +var inlets = 1; // single inlet for all commands (var required for Max JS global) +var outlets = 6; // six data/analysis outlets (var required for Max JS global) + +// Assist strings for UI clarity +setoutletassist(0, 'bin_data: ch bin value'); +setoutletassist(1, 'channel_data: ch bin value'); +setoutletassist(2, 'band_power: ch startBin endBin avgPower'); +setoutletassist(3, 'eeg_bands: ch delta theta alpha beta gamma'); +setoutletassist(4, 'peak_freq: ch peakFreqHz amplitude'); +setoutletassist(5, 'matrix: ch list_of_bins'); + +// Global variables to store FFT data +var fftData = {}; +var numChannels = 8; // Adjust based on your OpenBCI setup +var numBins = 125; +var binSize = 2; // Hz per bin (assuming 250Hz sample rate) + +// Initialize data structure +function init() { + for (var ch = 0; ch < numChannels; ch++) { + fftData[ch] = new Array(numBins); + for (var bin = 0; bin < numBins; bin++) { + fftData[ch][bin] = 0; + } + } + post( + 'OpenBCI FFT Parser initialized for ' + + numChannels + + ' channels, ' + + numBins + + ' bins\n' + ); +} + +// Main function to parse incoming OSC messages +function parseOSC() { + var oscAddress = arguments[0]; + var value = arguments[1]; + + // Parse the OSC address pattern + // Expected format: /openbci/fft/ch{channel}/bin{bin} + // Match either /openbci/fft/chX/binY or /chX/binY (if prefix routed out earlier) + var addressMatch = oscAddress.match(/(?:\/openbci\/fft)?\/ch(\d+)\/bin(\d+)/); + + if (addressMatch) { + var channel = parseInt(addressMatch[1], 10); + var bin = parseInt(addressMatch[2], 10); + + // Store the FFT value + if (channel < numChannels && bin < numBins) { + fftData[channel][bin] = parseFloat(value); + + // Output individual bin data + outlet(0, 'bin_data', channel, bin, value); + } + } +} + +// Get FFT data for a specific channel +function getChannel(channel) { + if (channel >= 0 && channel < numChannels && fftData[channel]) { + // Output all bins for this channel + for (var bin = 0; bin < numBins; bin++) { + outlet(1, channel, bin, fftData[channel][bin]); + } + } +} + +// Get specific frequency band power +function getBandPower(channel, startBin, endBin) { + if (channel >= 0 && channel < numChannels && fftData[channel]) { + var bandPower = 0; + var binCount = 0; + + for (var bin = startBin; bin <= endBin && bin < numBins; bin++) { + bandPower += fftData[channel][bin]; + binCount++; + } + + if (binCount > 0) { + var avgPower = bandPower / binCount; + outlet(2, 'band_power', channel, startBin, endBin, avgPower); + } + } +} + +// Get common EEG frequency bands +function getEEGBands(channel) { + if (channel >= 0 && channel < numChannels && fftData[channel]) { + // Frequency bands (assuming 250Hz sample rate, 125 bins) + // Each bin = 2Hz, so bin N = N * 2 Hz + + var delta = getBandAverage(channel, 0, 2); // 0-4 Hz + var theta = getBandAverage(channel, 2, 4); // 4-8 Hz + var alpha = getBandAverage(channel, 4, 6); // 8-12 Hz + var beta = getBandAverage(channel, 6, 15); // 12-30 Hz + var gamma = getBandAverage(channel, 15, 25); // 30-50 Hz + + outlet(3, 'eeg_bands', channel, delta, theta, alpha, beta, gamma); + } +} + +// Helper function to calculate average power in a bin range +function getBandAverage(channel, startBin, endBin) { + var sum = 0; + var count = 0; + + for (var bin = startBin; bin <= endBin && bin < numBins; bin++) { + sum += fftData[channel][bin]; + count++; + } + + return count > 0 ? sum / count : 0; +} + +// Find peak frequency in a channel +function getPeakFrequency(channel) { + if (channel >= 0 && channel < numChannels && fftData[channel]) { + var maxValue = 0; + var peakBin = 0; + + for (var bin = 1; bin < numBins; bin++) { + // Skip DC component (bin 0) + if (fftData[channel][bin] > maxValue) { + maxValue = fftData[channel][bin]; + peakBin = bin; + } + } + + var peakFreq = peakBin * binSize; + outlet(4, 'peak_freq', channel, peakFreq, maxValue); + } +} + +// Output all channel data as a matrix +function outputMatrix() { + for (var ch = 0; ch < numChannels; ch++) { + if (fftData[ch]) { + var channelArray = []; + for (var bin = 0; bin < numBins; bin++) { + channelArray.push(fftData[ch][bin]); + } + outlet(5, 'matrix', ch, channelArray); + } + } +} + +// Emit a test message from every outlet so user can confirm they exist +function testOutlets() { + outlet(0, 'test', 'outlet0'); + outlet(1, 'test', 'outlet1'); + outlet(2, 'test', 'outlet2'); + outlet(3, 'test', 'outlet3'); + outlet(4, 'test', 'outlet4'); + outlet(5, 'test', 'outlet5'); + post('Emitted test messages on all 6 outlets\n'); +} + +// Set number of channels dynamically +function setChannels(numCh) { + numChannels = parseInt(numCh); + init(); + post('Set to ' + numChannels + ' channels\n'); +} + +// Set number of bins dynamically +function setBins(numBin) { + numBins = parseInt(numBin); + init(); + post('Set to ' + numBins + ' bins\n'); +} + +// Clear all data +function clear() { + init(); + post('FFT data cleared\n'); +} + +// Initialize on load +init(); diff --git a/Networking-Test-Kit/OSC/openbci_osc_fft.maxpat b/Networking-Test-Kit/OSC/openbci_osc_fft.maxpat new file mode 100644 index 000000000..372090dd2 --- /dev/null +++ b/Networking-Test-Kit/OSC/openbci_osc_fft.maxpat @@ -0,0 +1,241 @@ +{ + "patcher" : { + "fileversion" : 1, + "appversion" : { + "major" : 8, + "minor" : 6, + "revision" : 5, + "architecture" : "x64", + "modernui" : 1 + } +, + "classnamespace" : "box", + "rect" : [ -1830.0, 144.0, 1269.0, 833.0 ], + "bglocked" : 0, + "openinpresentation" : 0, + "default_fontsize" : 12.0, + "default_fontface" : 0, + "default_fontname" : "Arial", + "gridonopen" : 1, + "gridsize" : [ 15.0, 15.0 ], + "gridsnaponopen" : 1, + "objectsnaponopen" : 1, + "statusbarvisible" : 2, + "toolbarvisible" : 1, + "lefttoolbarpinned" : 0, + "toptoolbarpinned" : 0, + "righttoolbarpinned" : 0, + "bottomtoolbarpinned" : 0, + "toolbars_unpinned_last_save" : 0, + "tallnewobj" : 0, + "boxanimatetime" : 200, + "enablehscroll" : 1, + "enablevscroll" : 1, + "devicewidth" : 0.0, + "description" : "", + "digest" : "", + "tags" : "", + "style" : "", + "subpatcher_template" : "", + "assistshowspatchername" : 0, + "boxes" : [ { + "box" : { + "id" : "obj-30", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 749.0, 220.0, 119.0, 22.0 ], + "text" : "getPeakFrequency 0" + } + + } +, { + "box" : { + "id" : "obj-26", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 591.600000000000023, 654.0, 184.0, 22.0 ], + "text" : "peak_freq 1 20 14.689284" + } + + } +, { + "box" : { + "id" : "obj-24", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 537.200000000000045, 614.0, 165.0, 22.0 ] + } + + } +, { + "box" : { + "id" : "obj-22", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 462.799999999999955, 566.0, 166.0, 22.0 ] + } + + } +, { + "box" : { + "id" : "obj-20", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 353.399999999999977, 519.0, 202.0, 22.0 ] + } + + } +, { + "box" : { + "id" : "obj-18", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 89.0, 268.0, 359.0, 22.0 ], + "text" : "parseOSC /openbci/fft/ch7/bin124 0.010692" + } + + } +, { + "box" : { + "id" : "obj-16", + "maxclass" : "newobj", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 463.0, 227.0, 112.0, 22.0 ], + "text" : "prepend parseOSC" + } + + } +, { + "box" : { + "id" : "obj-15", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 146.0, 472.0, 336.0, 22.0 ], + "text" : "bin_data 7 124 0.010692" + } + + } +, { + "box" : { + "id" : "obj-6", + "maxclass" : "newobj", + "numinlets" : 1, + "numoutlets" : 6, + "outlettype" : [ "", "", "", "", "", "" ], + "patching_rect" : [ 463.0, 299.0, 386.0, 22.0 ], + "saved_object_attributes" : { + "filename" : "max-fft-parsing.js", + "parameter_enable" : 0 + } +, + "text" : "js max-fft-parsing.js" + } + + } +, { + "box" : { + "id" : "obj-2", + "maxclass" : "newobj", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 463.0, 179.0, 104.0, 22.0 ], + "text" : "udpreceive 12348" + } + + } + ], + "lines" : [ { + "patchline" : { + "destination" : [ "obj-18", 1 ], + "order" : 1, + "source" : [ "obj-16", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-6", 0 ], + "order" : 0, + "source" : [ "obj-16", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-16", 0 ], + "source" : [ "obj-2", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-6", 0 ], + "source" : [ "obj-30", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-15", 1 ], + "source" : [ "obj-6", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-20", 1 ], + "source" : [ "obj-6", 1 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-22", 1 ], + "source" : [ "obj-6", 2 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-24", 1 ], + "source" : [ "obj-6", 3 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-26", 1 ], + "source" : [ "obj-6", 4 ] + } + + } + ], + "dependency_cache" : [ { + "name" : "max-fft-parsing.js", + "bootpath" : "~/Documents/GitHub/OpenBCI_GUI_PUBLIC_MAIN/Networking-Test-Kit/OSC", + "patcherrelativepath" : ".", + "type" : "TEXT", + "implicit" : 1 + } + ], + "autosave" : 0 + } + +} diff --git a/OpenBCI_GUI/NetworkStreamOutOSC.pde b/OpenBCI_GUI/NetworkStreamOutOSC.pde index 63b5769e3..62668acd0 100644 --- a/OpenBCI_GUI/NetworkStreamOutOSC.pde +++ b/OpenBCI_GUI/NetworkStreamOutOSC.pde @@ -70,8 +70,8 @@ class NetworkStreamOutOSC extends NetworkStreamOut { msg.clearArguments(); msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/ch" + i + "/bin" + j); msg.add(fftBuff[i].getBand(j)); + outputUsingProtocol(); } - outputUsingProtocol(); } }