Continuous Acquisition With Annotations#

This is the full MATLAB example file for annotated streaming, saving to a .mat file, and plotting the collected data.

Download the source: example_eeg_acquisition_to_matlab.m

example_eeg_acquisition_to_matlab.m#
function result = example_eeg_acquisition_to_matlab(device_name, channel_ids, sample_rate_hz, annotation_count, output_mat_path)
%EXAMPLE_EEG_ACQUISITION_TO_MATLAB Continuous acquisition with annotations.
%
% Usage:
%   example_eeg_acquisition_to_matlab()
%   example_eeg_acquisition_to_matlab('BA MINI 001')
%   result = example_eeg_acquisition_to_matlab('BA MINI 001', [], 250, 10)

project_root = fileparts(fileparts(mfilename('fullpath')));
addpath(fullfile(project_root, 'matlab'));

if nargin < 1
    device_name = '';
end
if nargin < 2
    channel_ids = [];
end
if nargin < 3 || isempty(sample_rate_hz)
    sample_rate_hz = 250;
end
if nargin < 4 || isempty(annotation_count)
    annotation_count = 10;
end
if nargin < 5 || isempty(output_mat_path)
    timestamp = char(datetime("now", Format="yyyyMMdd-HHmmss"));
    output_mat_path = fullfile(tempdir, ...
        sprintf('brainaccess-%s.mat', timestamp));
end

ba_setup();
ba_init();
cleanup_obj = onCleanup(@() safe_shutdown());
devices = ba_scan();
device_name = resolve_device_name(device_name, devices);

fprintf('Connecting to %s\n', device_name);
connect_result = ba_connect(device_name);
if ~connect_result.connected
    error('brainaccess:api', 'Connection failed: %s (%s)', ...
        connect_result.message, connect_result.status_name);
end

device_features = ba_get_device_features();
channel_ids = resolve_channel_ids(channel_ids, device_features);
fprintf('Using %d EEG channels.\n', numel(channel_ids));

ba_configure_eeg(channel_ids, sample_rate_hz, 8, []);
ba_start_stream();

warmup_seconds = 5.0;
fprintf('Stream started. Waiting %.1f seconds before annotations.\n', warmup_seconds);
pause(warmup_seconds);

all_data = [];
annotation_messages = strings(1, annotation_count);
chunk_meta = cell(1, annotation_count);

for idx = 1:annotation_count
    pause(1.0);
    annotation_messages(idx) = string(idx);
    fprintf('Sending annotation %s\n', annotation_messages(idx));
    ba_annotate(char(annotation_messages(idx)));

    [chunk, meta] = ba_read_chunk(1000, true);
    chunk_meta{idx} = meta;
    if isstruct(meta) && isfield(meta, 'available') && meta.available && ~isempty(chunk)
        all_data = append_chunk(all_data, chunk);
    end
end

settle_seconds = 2.0;
fprintf('Waiting %.1f seconds for final chunks.\n', settle_seconds);
pause(settle_seconds);

[final_chunk, final_meta] = ba_read_chunk(0, true);
if isstruct(final_meta) && isfield(final_meta, 'available') ...
        && final_meta.available && ~isempty(final_chunk)
    all_data = append_chunk(all_data, final_chunk);
end

annotations = ba_get_annotations();
ba_stop_stream();
ba_disconnect();
ba_close();
clear cleanup_obj

save(output_mat_path, 'all_data', 'annotations', 'annotation_messages', ...
    'chunk_meta', 'channel_ids', 'device_name', 'final_meta', 'sample_rate_hz');
fprintf('Saved acquisition: %s\n', output_mat_path);

plot_eeg(all_data, sample_rate_hz, device_name, annotations);

result = struct( ...
    'annotation_messages', {cellstr(annotation_messages)}, ...
    'annotations', annotations, ...
    'channel_ids', channel_ids, ...
    'data', all_data, ...
    'device_name', device_name, ...
    'device_features', device_features, ...
    'devices', devices, ...
    'final_meta', final_meta, ...
    'output_mat_path', output_mat_path, ...
    'sample_rate_hz', sample_rate_hz);
end

function channel_ids = resolve_channel_ids(channel_ids, device_features)
if ~isempty(channel_ids)
    return;
end

electrode_count = double(device_features.electrode_count);
channel_ids = 0:(electrode_count - 1);
end

function device_name = resolve_device_name(device_name, devices)
if ~isempty(device_name)
    return;
end

device_name = getenv('BA_DEVICE_NAME');
if ~isempty(device_name)
    return;
end

if isempty(devices)
    error('brainaccess:api', ...
        'No devices found. Set BA_DEVICE_NAME or pass a device name explicitly.');
end

device_name = devices(1).name;
end

function data = append_chunk(data, chunk)
if isempty(data)
    data = chunk;
    return;
end

if size(data, 1) ~= size(chunk, 1)
    error('brainaccess:api', ...
        'Chunk channel count changed during acquisition.');
end

data = [data, chunk];
end

function plot_eeg(data, sample_rate_hz, device_name, annotations)
if isempty(data)
    error('brainaccess:api', 'No samples were collected.');
end

channel_count = size(data, 1);
sample_count = size(data, 2);
time_axis = (0:(sample_count - 1)) / sample_rate_hz;
centered = data - mean(data, 2);
spacing = max(std(centered, 0, 2));
if isempty(spacing) || spacing <= 0
    spacing = 1;
end
offsets = ((channel_count - 1):-1:0)' * spacing * 4;

figure('Name', 'BrainAccess Acquisition With Annotations');
plot(time_axis, (centered + offsets)');
xlabel('Time (s)');
ylabel('Amplitude + offset');
title(sprintf('BrainAccess annotated acquisition: %s', device_name));
grid on;

annotation_count = min(numel(annotations.annotations), numel(annotations.timestamps));
if annotation_count > 0
    hold on;
    for idx = 1:annotation_count
        annotation_time = double(annotations.timestamps(idx)) / sample_rate_hz;
        if annotation_time < time_axis(1) || annotation_time > time_axis(end)
            continue;
        end
        xline(annotation_time, '--', annotations.annotations{idx}, ...
            'Color', [0.85, 0.2, 0.2], ...
            'LineWidth', 1.0, ...
            'LabelVerticalAlignment', 'middle', ...
            'LabelHorizontalAlignment', 'left');
    end
    hold off;
end
end

function safe_shutdown()
status = ba_status();
if status.streaming
    ba_stop_stream();
end
if status.connected
    ba_disconnect();
end
if status.core_initialized || status.manager_present
    ba_close();
end
end