copied all files and added two heart rate modules

Esse commit está contido em:
Kyle Mathewson
2020-01-04 16:25:56 -07:00
commit 3a73303b7b
6 arquivos alterados com 730 adições e 10 exclusões
+36 -2
Ver Arquivo
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from "react";
import React, { useState, useCallback } from "react";
import { MuseClient } from "muse-js";
import { Select, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
@@ -8,6 +8,8 @@ import * as generalTranslations from "./components/translations/en";
import { emptyChannelData } from "./components/chartOptions";
import * as funIntro from "./components/EEGEduIntro/EEGEduIntro"
import * as funHeartRaw from "./components/EEGEduHeartRaw/EEGEduHeartRaw"
import * as funHeartSpectra from "./components/EEGEduHeartSpectra/EEGEduHeartSpectra"
import * as funRaw from "./components/EEGEduRaw/EEGEduRaw";
import * as funSpectra from "./components/EEGEduSpectra/EEGEduSpectra";
import * as funBands from "./components/EEGEduBands/EEGEduBands";
@@ -17,6 +19,8 @@ import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha"
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep"
const intro = translations.types.intro;
const heartRaw = translations.types.heartRaw;
const heartSpectra = translations.types.heartSpectra;
const raw = translations.types.raw;
const spectra = translations.types.spectra;
const bands = translations.types.bands;
@@ -29,6 +33,8 @@ export function PageSwitcher() {
// data pulled out of multicast$
const [introData, setIntroData] = useState(emptyChannelData)
const [heartRawData, setHeartRawData] = useState(emptyChannelData);
const [heartSpectraData, setHeartSpectraData] = useState(emptyChannelData);
const [rawData, setRawData] = useState(emptyChannelData);
const [spectraData, setSpectraData] = useState(emptyChannelData);
const [bandsData, setBandsData] = useState(emptyChannelData);
@@ -39,6 +45,8 @@ export function PageSwitcher() {
// pipe settings
const [introSettings] = useState(funIntro.getSettings);
const [heartRawSettings] = useState(funHeartRaw.getSettings);
const [heartSpectraSettings] = useState(funHeartSpectra.getSettings);
const [rawSettings, setRawSettings] = useState(funRaw.getSettings);
const [spectraSettings, setSpectraSettings] = useState(funSpectra.getSettings);
const [bandsSettings, setBandsSettings] = useState(funBands.getSettings);
@@ -58,6 +66,8 @@ export function PageSwitcher() {
console.log("Switching to: " + value);
if (window.subscriptionIntro) window.subscriptionIntro.unsubscribe();
if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe();
if (window.subscriptionHeartSpectra) window.subscriptionHeartSpectra.unsubscribe();
if (window.subscriptionRaw) window.subscriptionRaw.unsubscribe();
if (window.subscriptionSpectra) window.subscriptionSpectra.unsubscribe();
if (window.subscriptionBands) window.subscriptionBands.unsubscribe();
@@ -80,6 +90,8 @@ export function PageSwitcher() {
const chartTypes = [
{ label: intro, value: intro },
{ label: heartRaw, value: heartRaw },
{ label: heartSpectra, value: heartSpectra },
{ label: raw, value: raw },
{ label: spectra, value: spectra },
{ label: bands, value: bands },
@@ -91,6 +103,8 @@ export function PageSwitcher() {
function buildPipes(value) {
funIntro.buildPipe(introSettings);
funHeartRaw.buildPipe(heartRawSettings);
funHeartSpectra.buildPipe(heartSpectraSettings);
funRaw.buildPipe(rawSettings);
funSpectra.buildPipe(spectraSettings);
funBands.buildPipe(bandsSettings);
@@ -105,6 +119,12 @@ export function PageSwitcher() {
case intro:
funIntro.setup(setIntroData, introSettings);
break;
case heartRaw:
funHeartRaw.setup(setHeartRawData, heartRawSettings);
break;
case heartSpectra:
funHeartSpectra.setup(setHeartSpectraData, heartSpectraSettings);
break;
case raw:
funRaw.setup(setRawData, rawSettings);
break;
@@ -173,6 +193,10 @@ export function PageSwitcher() {
switch(selected) {
case intro:
return null
case heartRaw:
return null
case heartSpectra:
return null
case raw:
return (
funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings)
@@ -209,6 +233,10 @@ export function PageSwitcher() {
switch (selected) {
case intro:
return <funIntro.renderModule data={introData} />;
case heartRaw:
return <funHeartRaw.renderModule data={heartRawData} />;
case heartSpectra:
return <funHeartSpectra.renderModule data={heartSpectraData} />;
case raw:
return <funRaw.renderModule data={rawData} />;
case spectra:
@@ -232,8 +260,14 @@ export function PageSwitcher() {
switch (selected) {
case intro:
return null
case heartRaw:
return null
case heartSpectra:
return (
funHeartSpectra.renderRecord(recordPopChange, recordPop, status, heartSpectraSettings)
)
case raw:
return(
return (
funRaw.renderRecord(recordPopChange, recordPop, status, rawSettings)
)
case spectra:
@@ -0,0 +1,321 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
export function getSettings () {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
interval: 50,
srate: 256,
duration: 1024,
name: 'Raw'
}
};
export function buildPipe(Settings) {
if (window.subscriptionRaw) window.subscriptionRaw.unsubscribe();
window.pipeRaw$ = null;
window.multicastRaw$ = null;
window.subscriptionRaw = null;
// Build Pipe
window.pipeRaw$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
catchError(err => {
console.log(err);
})
);
window.multicastRaw$ = window.pipeRaw$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastRaw$) {
window.subscriptionRaw = window.multicastRaw$.subscribe(data => {
setData(rawData => {
Object.values(rawData).forEach((channel, index) => {
if (index < 4) {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
channel.datasets[0].qual = standardDeviation(data.data[index])
}
});
return {
ch0: rawData.ch0,
ch1: rawData.ch1,
ch2: rawData.ch2,
ch3: rawData.ch3
};
});
});
window.multicastRaw$.connect();
console.log("Subscribed to Raw");
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
const options = {
...generalOptions,
scales: {
xAxes: [
{
scaleLabel: {
...generalOptions.scales.xAxes[0].scaleLabel,
labelString: specificTranslations.xlabel
}
}
],
yAxes: [
{
scaleLabel: {
...generalOptions.scales.yAxes[0].scaleLabel,
labelString: specificTranslations.ylabel
},
ticks: {
max: 300,
min: -300
}
}
]
},
elements: {
line: {
borderColor: 'rgba(' + channel.datasets[0].qual*10 + ', 128, 128)',
fill: false
},
point: {
radius: 0
}
},
animation: {
duration: 0
},
title: {
...generalOptions.title,
text: generalTranslations.channel + channelNames[index] + ' --- SD: ' + channel.datasets[0].qual
}
};
return (
<Card.Section key={"Card_" + index}>
<Line key={"Line_" + index} data={channel} options={options} />
</Card.Section>
);
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
export function renderSliders(setData, setSettings, status, Settings) {
function resetPipeSetup(value) {
buildPipe(Settings);
setup(setData, Settings)
}
function handleIntervalRangeSliderChange(value) {
setSettings(prevState => ({...prevState, interval: value}));
resetPipeSetup();
}
function handleCutoffLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffLow: value}));
resetPipeSetup();
}
function handleCutoffHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffHigh: value}));
resetPipeSetup();
}
function handleDurationRangeSliderChange(value) {
setSettings(prevState => ({...prevState, duration: value}));
resetPipeSetup();
}
return (
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
label={'Epoch duration (Sampling Points): ' + Settings.duration}
value={Settings.duration}
onChange={handleDurationRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={10} step={1} max={Settings.duration}
label={'Sampling points between epochs onsets: ' + Settings.interval}
value={Settings.interval}
onChange={handleIntervalRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={.01} step={.5} max={Settings.cutOffHigh - .5}
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
value={Settings.cutOffLow}
onChange={handleCutoffLowRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return (
<Card title={'Record ' + Settings.name + ' Data'} sectioned>
<Card.Section>
<p>
{"When you are recording raw data it is recommended you set the "}
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
{"It will make the live plots appear more choppy."}
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// for each module subscribe to multicast and make header
// take one sample from selected observable object for headers
localObservable$ = window.multicastRaw$.pipe(
take(1)
);
//take one sample to get header info
localObservable$.subscribe({
next(x) {
dataToSave.push(
"Timestamp (ms),",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastRaw$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -0,0 +1,14 @@
{
"title": "Heart Rate (Electrocardiogram)",
"description": [
"As a first introduction to measurement of electrical potentials from the body we will look at something accessible. ",
"As the heart beats and pumps blood throuhgouts our body, a series of electrical potentials are created, which can be measured using electrodes placed around the heart. ",
"This is referred to as the Electrocardiogram (ECG), and is best measured comparing the potential accross the left vs. right side of the body. ",
"Therefore, we can take off the muse and place a finger on one hand on the muse's reference electrode (in the center of the forehead). ",
"We can then place a finger of our opposite hand on one of the eeg electrodes. For this example pick the left forehead electrode. ",
"So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ",
"Rest the muse on the table as you do this, and relax your body. Soon you should see spikes in voltage measured between thsoe two electrodes each time your heart beats. "
],
"xlabel": "Time (msec)",
"ylabel": "Voltage (\u03BCV)"
}
@@ -0,0 +1,336 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Subject } from "rxjs";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
sliceFFT
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
export function getSettings() {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
interval: 100,
bins: 256,
sliceFFTLow: 1,
sliceFFTHigh: 30,
duration: 1024,
srate: 256,
name: 'Spectra'
}
};
export function buildPipe(Settings) {
if (window.subscriptionSpectra) window.subscriptionSpectra.unsubscribe();
window.pipeSpectra$ = null;
window.multicastSpectra$ = null;
window.subscriptionSpectra = null;
// Build Pipe
window.pipeSpectra$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
catchError(err => {
console.log(err);
})
);
window.multicastSpectra$ = window.pipeSpectra$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastSpectra$) {
window.subscriptionSpectra = window.multicastSpectra$.subscribe(data => {
setData(spectraData => {
Object.values(spectraData).forEach((channel, index) => {
if (index < 4) {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs;
}
});
return {
ch0: spectraData.ch0,
ch1: spectraData.ch1,
ch2: spectraData.ch2,
ch3: spectraData.ch3
};
});
});
window.multicastSpectra$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
const options = {
...generalOptions,
scales: {
xAxes: [
{
scaleLabel: {
...generalOptions.scales.xAxes[0].scaleLabel,
labelString: specificTranslations.xlabel
}
}
],
yAxes: [
{
scaleLabel: {
...generalOptions.scales.yAxes[0].scaleLabel,
labelString: specificTranslations.ylabel
},
ticks: {
max: 25,
min: 0
}
}
]
},
elements: {
point: {
radius: 3
}
},
title: {
...generalOptions.title,
text: generalTranslations.channel + channelNames[index]
}
};
return (
<Card.Section key={"Card_" + index}>
<Line key={"Line_" + index} data={channel} options={options} />
</Card.Section>
);
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
export function renderSliders(setData, setSettings, status, Settings) {
function resetPipeSetup(value) {
buildPipe(Settings);
setup(setData, Settings)
}
function handleIntervalRangeSliderChange(value) {
setSettings(prevState => ({...prevState, interval: value}));
resetPipeSetup();
}
function handleCutoffLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffLow: value}));
resetPipeSetup();
}
function handleCutoffHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffHigh: value}));
resetPipeSetup();
}
function handleSliceFFTLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, sliceFFTLow: value}));
resetPipeSetup();
}
function handleSliceFFTHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
resetPipeSetup();
}
function handleDurationRangeSliderChange(value) {
setSettings(prevState => ({...prevState, duration: value}));
resetPipeSetup();
}
return (
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
label={'Epoch duration (Sampling Points): ' + Settings.duration}
value={Settings.duration}
onChange={handleDurationRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={10} step={5} max={Settings.duration}
label={'Sampling points between epochs onsets: ' + Settings.interval}
value={Settings.interval}
onChange={handleIntervalRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={.01} step={.5} max={Settings.cutOffHigh - .5}
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
value={Settings.cutOffLow}
onChange={handleCutoffLowRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={1} max={Settings.sliceFFTHigh - 1}
label={'Slice FFT Lower limit: ' + Settings.sliceFFTLow + ' Hz'}
value={Settings.sliceFFTLow}
onChange={handleSliceFFTLowRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={Settings.sliceFFTLow + 1}
label={'Slice FFT Upper limit: ' + Settings.sliceFFTHigh + ' Hz'}
value={Settings.sliceFFTHigh}
onChange={handleSliceFFTHighRangeSliderChange}
/>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// take one sample from selected observable object for headers
localObservable$ = window.multicastSpectra$.pipe(
take(1)
);
localObservable$.subscribe({
next(x) {
let freqs = Object.values(x.freqs);
dataToSave.push(
"Timestamp (ms),",
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastSpectra$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -0,0 +1,13 @@
{
"title": "Heart Rate (Electrocardiogram)",
"description": [
"Now we look at the same data, but in a different way. Instead of looking at the voltage over time, ",
"We now transform the data to show us what frequencies are present in the continuous signal. ",
"In this frequency domain, we now ignore time and consider how much power of each frequency there is in a segment of data. ",
"If you place your fingers the same way you did when looking at your ECG, you should start to see a peak ",
"Along the horizontal axis is the beats per minute, or the frequency of the peaks in your ECG. ",
"The vertical y-axis shows the power of the rhythms in the data at each frequency, or how large the changes are between peak and through of the oscillations. "
],
"xlabel": "Time (msec)",
"ylabel": "Voltage (\u03BCV)"
}
+10 -8
Ver Arquivo
@@ -1,13 +1,15 @@
{
"title": "Choose your Module",
"types": {
"intro": "Introduction",
"raw": "Raw and Filtered Data",
"spectra": "Frequency Spectra",
"bands": "Frequency Bands",
"animate": "Brain Controlled Animation",
"spectro": "Spectrogram (spectra over time)",
"alpha": "Eyes open vs. Eyes closed Experiment",
"ssvep": "Steady-State Visual Evoked Potential (SSVEP) Experiment"
"intro": "1. Introduction",
"heartRaw": "2. Electrocardiogram (Heart beats)",
"heartSpectra": "3. Heart Rate (Beats per minute)",
"raw": "4. Raw and Filtered Data",
"spectra": "5. Frequency Spectra",
"bands": "6. Frequency Bands",
"animate": "7. Brain Controlled Animation",
"spectro": "8. Spectrogram (spectra over time)",
"alpha": "9. Eyes open vs. Eyes closed Experiment",
"ssvep": "10. Steady-State Visual Evoked Potential (SSVEP) Experiment"
}
}